Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
e636711
fix(relay): generic streaming detection + help() format enforcement
Apr 30, 2026
37f2b82
feat(owui): session scripts, Playwright config, and session guide
Apr 30, 2026
c1178d5
fix(relay): harden streaming detection and fix common tool param mist…
Apr 30, 2026
13661f2
feat(relay): switch dispatch from MutationObserver to fire-on-idle poll
Apr 30, 2026
2472f6d
revert(playwright): restore qwen3:8b as default model in fresh-setup
Apr 30, 2026
ae52030
docs(solutions): capture fire-on-idle relay dispatch pattern and add …
Apr 30, 2026
d6a7210
fix(relay): remove isAiStreaming check from doSubmit; add HMR watch t…
Apr 30, 2026
33c15ca
fix(relay): restore setContent as primary injection path to prevent […
Apr 30, 2026
0f094a0
fix(relay): clear editor before paste-inject; restore Enter-only-for-…
Apr 30, 2026
c9578ee
fix(relay): remove hasContent shortcut; setContent as primary injection
Apr 30, 2026
132ad44
refactor(relay): single injection path — setContent + transaction eve…
Apr 30, 2026
a1c888b
fix(relay): text-stability idle detection + delta extraction for FhGenie
Apr 30, 2026
e1eac68
fix(relay): detect FhGenie icon-only stop button + class-based send b…
Apr 30, 2026
3040d74
refactor(relay): button-primary streaming detection + clean idle poll
Apr 30, 2026
c125920
fix(relay): defer textarea submit 50ms for React state sync
Apr 30, 2026
1ceb538
fix(relay): wait for UI ready before TipTap setContent injection
Apr 30, 2026
22f295c
fix(relay): dispatch only on streaming→idle transition, never on user…
Apr 30, 2026
6c16195
fix(relay): exclude chat input from all tool-call scans
Apr 30, 2026
d433d9a
fix(relay): exclude input text from dispatch; remove prevStreaming guard
Apr 30, 2026
c012777
fix(relay): fix getPageText DOM exclusion using SHOW_ELEMENT|SHOW_TEX…
Apr 30, 2026
9deeeb2
fix(relay): wait for send button enabled before paste and after submit
Apr 30, 2026
3a07c11
fix(relay): fall through to spinner signals when button disabled + em…
Apr 30, 2026
b2be781
fix(relay): restore FhGenie idle detection; add 1s stable-idle before…
Apr 30, 2026
cd2132a
fix(relay): increase waitSubmit stability to 3 ticks (300ms) for OWUI
Apr 30, 2026
f4039d5
fix(relay): scope tool-call extraction to assistant messages only
Apr 30, 2026
dd02132
fix(relay): replace tick-polling with isEmpty retry + 600ms grace period
Apr 30, 2026
1b62143
feat(relay): add Socratic pizza demo session scripts and video spec
May 4, 2026
0426daa
demo(pizza-socratic): add recorded demo video
May 4, 2026
fe0b0d8
feat(demo): add live Socratic OWUI recording spec with captions
May 4, 2026
af5d325
feat(demo): extend Socratic pizza demo to full Manchester ontology arc
May 4, 2026
391175a
feat(demo): side-by-side OWUI+Ontosphere recording via iframe stage
May 4, 2026
663955b
fix(demo): concrete INSTR example + verify-layout turn fixes empty ca…
May 4, 2026
86e72f4
refactor(demo): remove tool-name references from Socratic questions; …
May 5, 2026
2c69462
fix(relay): sync SEED, help() text, and relayBridge to addTriple rename
May 5, 2026
d808da4
refactor(e2e): replace Manchester arc with validated 10-turn Socratic…
May 5, 2026
dcf6c2e
fix(demo): steer T6 toward owl:NamedIndividual; fix pizza spec modal …
May 5, 2026
5901758
fix(demo): prevent qwen3 IRI anchoring and wrong layout tool name
May 5, 2026
fb12702
fix(demo): require 2s stable idle before injecting next turn
May 5, 2026
c3976d1
fix(demo): anchor T1 on rdfs:subClassOf with explicit predicate hint
May 5, 2026
efd0946
fix(demo): replace isAiStreaming() with content-length + relay-idle d…
May 5, 2026
c28da7c
fix(demo): robust socratic session — full starter prompt + T1/T4/T7 f…
May 5, 2026
8c7409f
chore(demo): update videos from second confirmed-passing build run
May 5, 2026
732f503
chore(demo): remove outdated openwebui-pizza spec and videos
May 5, 2026
6d07d1a
fix(demo): replace isAiStreaming() in setup + cap waitQuiet with maxM…
May 5, 2026
4098171
fix(demo): T1 explicit both subClassOf edges; T7 fresh ABox individuals
May 5, 2026
d2b50ad
fix(demo): T4 force rdfs:domain/range; T7 fix hasPart direction + use…
May 5, 2026
2487370
fix(demo): reliable OWL-RL inference in Socratic pizza demo
May 5, 2026
6a6962c
fix(demo): show help() subtitle and wait 30s before Socratic questions
May 5, 2026
2827e95
fix(demo): reduce post-help() idle wait from 30s to 10s
May 5, 2026
0c65e49
feat(mcp): expandNode navigates to node after expanding
May 5, 2026
68b5195
feat(demo): inject autoApplyLayout=true before recording
May 5, 2026
71cbaf7
feat(demo): fix OWL-RL reasoning by isolating pizza ontology from def…
May 6, 2026
3bc26dd
feat(worker): skolemize blank nodes to urn:vg:bnode: IRIs at store wr…
May 7, 2026
18308c9
refactor(worker): replace random session map with content-hash skolem…
May 7, 2026
e0fe868
test(config): exclude .worktrees from vitest scan
May 7, 2026
884e867
chore(demo): harden socratic demo for blank-node pipeline reliability
May 7, 2026
6901439
chore(dev): document logs/ convention and OWUI WebSocket solution
May 7, 2026
6094cd1
fix(worker): use label-only hash for blank node skolemization
May 7, 2026
ef29912
fix(mcp): guard addTriple against inline Turtle syntax; add addTriple…
May 7, 2026
57801b5
feat(scripts): shorten idle passages in demo videos
May 7, 2026
fa4344b
chore(demo): re-record openwebui-socratic demo with idle trimming
May 7, 2026
c35347a
fix(demo): use video.saveAs() for reliable recording capture; re-record
May 7, 2026
f22986f
fix(mcp): reject addTriple IRIs containing spaces; add demo tool-call…
May 7, 2026
f64ed02
feat(mcp): improve loadRdf validation and relay retry instruction
May 8, 2026
efa86d7
chore(demo): rebuild all demo videos and docs with addTriple fix; dro…
May 8, 2026
ea50558
fix(demo): replace addLink → addTriple in pizza-tutorial-chat; re-rec…
May 8, 2026
c8e4c2c
fix(demo): fix video collection prefix collision; re-record pizza videos
May 8, 2026
6e77f90
fix(test): update loadRdf test to expect injected built-in prefixes
May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dist-ssr
playwright-reports
test-results
.playwright-mcp
debug.png
scripts/outputs/

# Claude Code
Expand All @@ -43,3 +44,7 @@ public/relay-test.html
.worktrees/
docs/demo-videos/.last-run.json
docs/demo-videos/e2e-*/

# OpenWebUI demo auth state — contains session tokens, never commit
.playwright/owui-auth.json
logs/
24 changes: 24 additions & 0 deletions .playwright/demo-restart.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# demo-restart.sh — Clean restart for OWUI relay demo session.
#
# Run this from the terminal BEFORE asking Claude to run pizza-demo-setup.js.
# Kills stale playwright-mcp + Chrome instances and removes Chrome lock files
# so the MCP browser tools start fresh on the next call.
#
# Usage: bash .playwright/demo-restart.sh

set -e

echo "Killing playwright-mcp processes..."
pkill -f "playwright-mcp" 2>/dev/null || true
pkill -f "mcp-chrome-for-testing" 2>/dev/null || true
sleep 2

echo "Removing Chrome lock files..."
PROFILE_DIR="$HOME/.cache/ms-playwright/mcp-chrome-for-testing-5c936c5"
rm -f "$PROFILE_DIR/SingletonLock" \
"$PROFILE_DIR/SingletonSocket" \
"$PROFILE_DIR/SingletonCookie" 2>/dev/null || true

echo "Done. MCP browser will start fresh on next tool call."
echo "Now ask Claude to run: mcp__playwright__browser_run_code_unsafe filename=.playwright/pizza-demo-setup.js"
43 changes: 43 additions & 0 deletions .playwright/fresh-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
async (page) => {
// 1. Remove extra model slots until only one remains
while (true) {
const removeBtn = await page.$('button[aria-label*="Remove Model"]');
if (!removeBtn) break;
await removeBtn.click();
await page.waitForTimeout(300);
}

// 2. Open model selector and pick qwen3:8b
const modelBtn = await page.$('#model-selector-0-button');
if (!modelBtn) return { ok: false, error: 'no model-selector-0-button' };
await modelBtn.click();
await page.waitForTimeout(400);

const searchInput = await page.$('input[placeholder*="Search" i], input[placeholder*="search" i]');
const MODEL = 'qwen3:4b';
if (searchInput) {
await searchInput.fill(MODEL);
await page.waitForTimeout(400);
}

const modelBtn2 = await page.$(`button:has-text("${MODEL}"), [data-value*="${MODEL}"]`);
if (modelBtn2) {
await modelBtn2.click();
await page.waitForTimeout(400);
}

// 3. Clear the chat-input via PM dispatch (replace with empty)
const cleared = await page.evaluate(() => {
const el = document.getElementById('chat-input');
if (!el) return false;
const tiptap = el.editor;
if (tiptap && tiptap.view) {
const state = tiptap.view.state;
tiptap.view.dispatch(state.tr.delete(0, state.doc.content.size));
return true;
}
return false;
});

return { ok: true, cleared, url: page.url() };
}
28 changes: 28 additions & 0 deletions .playwright/inject-relay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
async (page) => {
const pages = page.context().pages();
const ontospherePage = pages.find(p => p.url().includes('docker-dev'));
if (!ontospherePage) return { ok: false, error: 'Ontosphere tab not found' };

const code = await ontospherePage.evaluate(async () => {
const r = await fetch('/relay-bookmarklet.js');
let src = await r.text();
src = src.replace(/__RELAY_URL__/g, 'http://docker-dev.iwm.fraunhofer.de:8080/relay.html');
src = src.replace(/__RELAY_ORIGIN__/g, 'http://docker-dev.iwm.fraunhofer.de:8080');
// Expose relay internals globally before the IIFE closes
src = src.replace(/\}\)\(\);\s*$/, [
' window.__vgInjectResult = injectResult;',
' window.__vgIsStreaming = isAiStreaming;',
' window.__vgWaitForIdle = waitForIdle;',
'})();'
].join('\n'));
return src;
});

await page.addScriptTag({ content: code });
return await page.evaluate(() => ({
ok: true,
instanceId: window.__vgRelayInstanceId,
popup: window.__vgRelayPopup ? !window.__vgRelayPopup.closed : false,
exposed: typeof window.__vgInjectResult === 'function'
}));
}
11 changes: 11 additions & 0 deletions .playwright/owui-auth-inject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
async (page) => {
const fs = await import('node:fs');
const state = JSON.parse(fs.readFileSync('/home/hanke/ontosphere/.playwright/owui-auth.json', 'utf8'));
const token = state.cookies?.find(c => c.name === 'token')?.value;
if (!token) return { ok: false, error: 'no token' };
await page.evaluate((t) => { document.cookie = `token=${t}; path=/`; }, token);
await page.reload();
await page.waitForLoadState('networkidle');
const loggedIn = await page.evaluate(() => !!document.cookie.includes('token'));
return { ok: loggedIn, url: page.url() };
}
11 changes: 11 additions & 0 deletions .playwright/owui-login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
async (page) => {
await page.context().addCookies([{
name: 'token',
value: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRmNjQwZjYzLThmY2MtNGMxMy1iNDcyLTFhMzBjZTU5ZjAwNCIsImp0aSI6Ijc0M2JiMmQ1LWVhMTQtNDc2Yi1iMTFkLTJlZWNmMDY2NTM3OCJ9.88gzl7KSJY_S2s_xJkxXi1f_nP2gQHtrBYzlVmrdPls',
domain: 'gpuserver1-sit.iwm.fraunhofer.de',
path: '/',
}]);
await page.reload();
await page.waitForLoadState('networkidle');
return { url: page.url(), title: await page.title() };
}
144 changes: 144 additions & 0 deletions .playwright/pizza-demo-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* pizza-demo-setup.js — Bootstrap OWUI relay session for pizza ontology recording
*
* Usage: mcp__playwright__browser_run_code_unsafe filename=.playwright/pizza-demo-setup.js
*
* Prerequisites:
* - Ontosphere running at http://docker-dev.iwm.fraunhofer.de:8080
* - OWUI tab already open + authenticated at https://gpuserver1-sit.iwm.fraunhofer.de
*
* What it does:
* 1. fresh-setup: select qwen3:4b, clear input
* 2. Navigate to OWUI home
* 3. Type the full README starter prompt (Shift+Enter for newlines) → creates /c/ URL
* The starter prompt embeds the relay format + help() call.
* The model reads it and calls help() itself — no separate INSTR injection.
* 4. Inject relay bookmarklet (relay pre-seeds the embedded help() call so it won't
* re-execute it; only the model's own help() call in its response is dispatched)
* 5. Wait for model's help() cycle to complete
* 6. Inject Socratic starter question (Turn 0) via relay
*
* After this script: relay connected, Turn 0 live. Drive with turn-driver.js.
*/

async (page) => {
const MODEL = 'qwen3:4b';

const pages = page.context().pages();
const owuiPage = pages.find(p => p.url().includes('gpuserver1-sit'));
const vgPage = pages.find(p => p.url().includes('docker-dev') && !p.url().includes('relay'));
if (!owuiPage) return { ok: false, error: 'no OWUI tab' };
if (!vgPage) return { ok: false, error: 'no Ontosphere tab' };

// ── 0. Reload Ontosphere to clear any graph state from previous sessions ───
await vgPage.reload();
await vgPage.waitForFunction(
() => typeof window.__mcpTools?.addNode === 'function',
{ timeout: 30_000 },
);

// ── 1. fresh-setup ─────────────────────────────────────────────────────────
await owuiPage.goto('https://gpuserver1-sit.iwm.fraunhofer.de/');
await owuiPage.waitForTimeout(1500);

while (true) {
const btn = await owuiPage.$('button[aria-label*="Remove Model"]');
if (!btn) break;
await btn.click();
await owuiPage.waitForTimeout(300);
}
const modelBtn = await owuiPage.$('#model-selector-0-button');
if (modelBtn) {
await modelBtn.click();
await owuiPage.waitForTimeout(400);
const search = await owuiPage.$('input[placeholder*="Search" i]');
if (search) { await search.fill(MODEL); await owuiPage.waitForTimeout(400); }
const pick = await owuiPage.$(`button:has-text("${MODEL}"), [data-value*="${MODEL}"]`);
if (pick) { await pick.click(); await owuiPage.waitForTimeout(400); }
}

// ── 2. Type full README starter prompt — Shift+Enter for newlines ──────────
// Source of truth: README.md "Starter prompt" section.
// The embedded help() call (`id:0`) is pre-seeded by the relay on startup so
// it will NOT be executed. Only the model's own help() call in its response fires.
const STARTER_LINES = [
'You are connected to Ontosphere via a relay. A script in this tab intercepts your tool calls, runs them in Ontosphere, and injects results back as a user message. If a tool call returns success:false, read the error, fix the argument, and retry the same call immediately — never skip a failed call. Ask the user what they would like to build.',
'',
'Output format — one JSON-RPC 2.0 call per line, backtick-wrapped:',
'`{"jsonrpc":"2.0","id":<N>,"method":"tools/call","params":{"name":"<toolName>","arguments":{...}}}`',
'',
'Call help first to get full instructions and the tool list:',
'`{"jsonrpc":"2.0","id":0,"method":"tools/call","params":{"name":"help","arguments":{}}}`',
];

const chatInput = await owuiPage.$('#chat-input');
if (!chatInput) return { ok: false, error: 'no #chat-input' };
await chatInput.click();
await owuiPage.waitForTimeout(200);

for (let i = 0; i < STARTER_LINES.length; i++) {
if (STARTER_LINES[i]) await owuiPage.keyboard.type(STARTER_LINES[i], { delay: 2 });
if (i < STARTER_LINES.length - 1) await owuiPage.keyboard.press('Shift+Enter');
}
await owuiPage.keyboard.press('Enter');
await owuiPage.waitForFunction(() => location.pathname.startsWith('/c/'), { timeout: 10000 });
const chatUrl = owuiPage.url();

// ── 3. Inject relay (fetch from VG tab, addScriptTag bypasses mixed-content) ─
const relayCode = await vgPage.evaluate(async () => {
const r = await fetch('/relay-bookmarklet.js');
let src = await r.text();
src = src.replace(/__RELAY_URL__/g, 'http://docker-dev.iwm.fraunhofer.de:8080/relay.html');
src = src.replace(/__RELAY_ORIGIN__/g, 'http://docker-dev.iwm.fraunhofer.de:8080');
src = src.replace(/\}\)\(\);\s*$/, [
' window.__vgInjectResult = injectResult;',
' window.__vgIsStreaming = isAiStreaming;',
' window.__vgWaitForIdle = waitForIdle;',
' window.__vgIsRelayIdle = function() { return callQueue.length === 0 && !isProcessing; };',
'})();',
].join('\n'));
return src;
});
await owuiPage.addScriptTag({ content: relayCode });
await owuiPage.waitForTimeout(300);

// ── 4. Wait for model's help() cycle to complete ──────────────────────────
// Model reads starter prompt → calls help() itself → relay executes → model
// reads manifest and responds. Use content-length + relay-idle (same as
// turn-driver.js) — isAiStreaming() is unreliable in newer OWUI.
let _lastLen = -1;
async function isBusy() {
const state = await owuiPage.evaluate(() => ({
relayBusy: !(window.__vgIsRelayIdle?.() ?? true),
len: document.body.innerText?.length ?? 0,
})).catch(() => ({ relayBusy: false, len: _lastLen }));
const growing = _lastLen >= 0 && state.len !== _lastLen;
_lastLen = state.len;
return state.relayBusy || growing;
}
const helpDeadline = Date.now() + 180_000;
const pollMs = 500;
let silentMs = 0;
while (Date.now() < helpDeadline) {
const busy = await isBusy();
if (busy) silentMs = 0; else silentMs += pollMs;
if (silentMs >= 3_000) break;
await owuiPage.waitForTimeout(pollMs);
}
await owuiPage.waitForTimeout(1000);

// ── 5. Inject Turn 0 — Socratic starter ───────────────────────────────────
const TURN0 = 'I want to learn OWL ontology concepts through a hands-on example. I will guide you through the pizza domain step by step — one concept at a time. Rule: for each question I ask, model exactly the concept I ask about on the canvas, then stop and wait. Do not add anything beyond what I asked. Do not arrange nodes automatically. Use the ex: prefix for all IRIs (ex: maps to http://example.org/). First question: in OWL, what is the most fundamental building block for representing a concept? Create a single Pizza class — just this one node, nothing more. Wait for my next question.';
let turn0Ok = false;
for (let i = 0; i < 8; i++) {
turn0Ok = await owuiPage.evaluate((text) => window.__vgInjectResult?.(text) ?? false, TURN0);
if (turn0Ok !== false) break;
await owuiPage.waitForTimeout(500);
}

await owuiPage.waitForTimeout(800);
const t0btn = await owuiPage.$('#send-message-button:not([disabled])');
if (t0btn) await t0btn.click();

return { ok: true, chatUrl, turn0: turn0Ok };
}
37 changes: 37 additions & 0 deletions .playwright/send-pizza.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
async (page) => {
const TASK = "Build a pizza ontology. Add these three OWL classes:\n- Pizza (IRI: http://www.pizza-ontology.com/pizza.owl#Pizza)\n- PizzaBase (IRI: http://www.pizza-ontology.com/pizza.owl#PizzaBase)\n- PizzaTopping (IRI: http://www.pizza-ontology.com/pizza.owl#PizzaTopping)\n\nAll typeIri: http://www.w3.org/2002/07/owl#Class. After all three are added, run a layered layout with spacing 200.";

// Wait for model idle using rating buttons — more reliable than __vgIsStreaming for qwen3.
await page.waitForSelector('button[aria-label="Good Response"]', { timeout: 120000 })
.catch(() => {});
// Extra guard: wait for __vgIsStreaming if available
await page.waitForFunction(
() => typeof window.__vgIsStreaming !== 'function' || !window.__vgIsStreaming(),
{ timeout: 30000, polling: 500 }
).catch(() => {});
await page.waitForTimeout(500);

const reply = await page.evaluate(() => {
const msgs = document.querySelectorAll('[data-message-author-role="assistant"]');
return msgs[msgs.length - 1]?.innerText?.slice(0, 400) ?? '';
});
console.log('[QWEN][RESPONSE]', reply.replace(/\n+/g, ' '));

const toolMsgs = await page.evaluate(() => {
const msgs = document.querySelectorAll('[data-message-author-role="user"]');
return [...msgs].filter(m => m.innerText?.includes('[Ontosphere')).map(m => m.innerText?.slice(0, 200));
});
toolMsgs.forEach(t => console.log('[TOOL]', t.replace(/\n+/g, ' ')));

console.log('[INJECT][TASK]', TASK.slice(0, 120));
const injected = await page.evaluate((text) => {
if (typeof window.__vgInjectResult !== 'function') return false;
return window.__vgInjectResult(text);
}, TASK);

await page.waitForTimeout(800);
const btn = await page.$('#send-message-button:not([disabled])');
if (btn) await btn.click();

return { ok: true, injected };
}
67 changes: 67 additions & 0 deletions .playwright/send-starter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
async (page) => {
// Canonical starter prompt — must match README.md "Starter prompt" section (plain-text line only, no backticks).
const SEED = "You are connected to Ontosphere via a relay. A script in this tab intercepts your tool calls, runs them in Ontosphere, and injects results back as a user message. Ask the user what they would like to build.";

// INSTR: compact format reminder + pizza task inline.
// No help() call — avoids multi-thousand-word explanation that truncates.
// "Respond with ONLY tool calls" keeps response short.
const INSTR = [
'RELAY FORMAT — single backtick only. ALL other formats silently ignored (no error, no response):',
'`{"jsonrpc":"2.0","id":N,"method":"tools/call","params":{"name":"TOOL_NAME","arguments":{...}}}`',
'WRONG (silently ignored): ```json{...}``` or {"tool":"x","params":{}} or {"method":"addNode",...}',
'Respond only with backtick-wrapped JSON-RPC 2.0 calls. No prose.',
].join('\n');

// Step 1: plain seed — creates /c/ URL (no backticks = no Notes routing)
const el = await page.$('#chat-input');
if (!el) return { ok: false, error: 'no #chat-input' };
await el.click();
await page.waitForTimeout(200);
await page.keyboard.type(SEED, { delay: 2 });
console.log('[INJECT][SEED]', SEED);
await page.keyboard.press('Enter');
await page.waitForFunction(() => location.pathname.startsWith('/c/'), { timeout: 8000 });
const chatUrl = page.url();

// Step 2: inject relay with exposed internals
const pages = page.context().pages();
const vgPage = pages.find(p => p.url().includes('docker-dev'));
if (!vgPage) return { ok: false, error: 'no Ontosphere tab', chatUrl };
const code = await vgPage.evaluate(async () => {
const r = await fetch('/relay-bookmarklet.js');
let src = await r.text();
src = src.replace(/__RELAY_URL__/g, 'http://docker-dev.iwm.fraunhofer.de:8080/relay.html');
src = src.replace(/__RELAY_ORIGIN__/g, 'http://docker-dev.iwm.fraunhofer.de:8080');
src = src.replace(/\}\)\(\);\s*$/, [
' window.__vgInjectResult = injectResult;',
' window.__vgIsStreaming = isAiStreaming;',
' window.__vgWaitForIdle = waitForIdle;',
'})();'
].join('\n'));
return src;
});
await page.addScriptTag({ content: code });

// Step 3: wait for seed response via relay __vgIsStreaming poll
const deadline = Date.now() + 600_000;
while (Date.now() < deadline) {
const s = await page.evaluate(() => window.__vgIsStreaming?.() ?? false);
if (!s) break;
await page.waitForTimeout(1000);
}
await page.waitForTimeout(500);
console.log('[QWEN][SEED] idle');

// Step 4: inject INSTR+task (contains backticks — OK now we're on /c/)
console.log('[INJECT][INSTR]', INSTR.slice(0, 120));
const injected = await page.evaluate((text) => {
if (typeof window.__vgInjectResult !== 'function') return false;
return window.__vgInjectResult(text);
}, INSTR);

await page.waitForTimeout(800);
const btn = await page.$('#send-message-button:not([disabled])');
if (btn) await btn.click();

return { ok: true, chatUrl, relayExposed: injected };
}
Loading
Loading