Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
6a5f8b4
fix(worker): make Comunica QueryEngine work in Vite browser worker
May 5, 2026
2942432
Merge pull request #9 from ThHanke/fix/sparql-worker
ThHanke May 5, 2026
e8ed0af
fix(relay): generic streaming detection + help() format enforcement
Apr 30, 2026
6002c42
feat(owui): session scripts, Playwright config, and session guide
Apr 30, 2026
7edb13e
fix(relay): harden streaming detection and fix common tool param mist…
Apr 30, 2026
67e1c31
feat(relay): switch dispatch from MutationObserver to fire-on-idle poll
Apr 30, 2026
61fc3cb
revert(playwright): restore qwen3:8b as default model in fresh-setup
Apr 30, 2026
85f3a68
docs(solutions): capture fire-on-idle relay dispatch pattern and add …
Apr 30, 2026
5acb671
fix(relay): remove isAiStreaming check from doSubmit; add HMR watch t…
Apr 30, 2026
52616a6
fix(relay): restore setContent as primary injection path to prevent […
Apr 30, 2026
40575a5
fix(relay): clear editor before paste-inject; restore Enter-only-for-…
Apr 30, 2026
125b44f
fix(relay): remove hasContent shortcut; setContent as primary injection
Apr 30, 2026
63ca8b6
refactor(relay): single injection path — setContent + transaction eve…
Apr 30, 2026
b9bc47a
fix(relay): text-stability idle detection + delta extraction for FhGenie
Apr 30, 2026
f578081
fix(relay): detect FhGenie icon-only stop button + class-based send b…
Apr 30, 2026
f81dca3
refactor(relay): button-primary streaming detection + clean idle poll
Apr 30, 2026
324903c
fix(relay): defer textarea submit 50ms for React state sync
Apr 30, 2026
121a0e2
fix(relay): wait for UI ready before TipTap setContent injection
Apr 30, 2026
7988679
fix(relay): dispatch only on streaming→idle transition, never on user…
Apr 30, 2026
863e916
fix(relay): exclude chat input from all tool-call scans
Apr 30, 2026
ad1efff
fix(relay): exclude input text from dispatch; remove prevStreaming guard
Apr 30, 2026
6ce12f5
fix(relay): fix getPageText DOM exclusion using SHOW_ELEMENT|SHOW_TEX…
Apr 30, 2026
31e4f73
fix(relay): wait for send button enabled before paste and after submit
Apr 30, 2026
9303973
fix(relay): fall through to spinner signals when button disabled + em…
Apr 30, 2026
102e882
fix(relay): restore FhGenie idle detection; add 1s stable-idle before…
Apr 30, 2026
bdf8365
fix(relay): increase waitSubmit stability to 3 ticks (300ms) for OWUI
Apr 30, 2026
f73198f
fix(relay): scope tool-call extraction to assistant messages only
Apr 30, 2026
4b23e80
fix(relay): replace tick-polling with isEmpty retry + 600ms grace period
Apr 30, 2026
3ee723c
feat(relay): add Socratic pizza demo session scripts and video spec
May 4, 2026
7e843e6
demo(pizza-socratic): add recorded demo video
May 4, 2026
75da3a5
feat(demo): add live Socratic OWUI recording spec with captions
May 4, 2026
ed2055f
feat(demo): extend Socratic pizza demo to full Manchester ontology arc
May 4, 2026
cf62b9b
feat(demo): side-by-side OWUI+Ontosphere recording via iframe stage
May 4, 2026
7c9cb45
fix(demo): concrete INSTR example + verify-layout turn fixes empty ca…
May 4, 2026
42048a6
refactor(demo): remove tool-name references from Socratic questions; …
May 5, 2026
62c4361
fix(relay): sync SEED, help() text, and relayBridge to addTriple rename
May 5, 2026
8bd2394
refactor(e2e): replace Manchester arc with validated 10-turn Socratic…
May 5, 2026
2a9745c
fix(demo): steer T6 toward owl:NamedIndividual; fix pizza spec modal …
May 5, 2026
572d03a
fix(demo): prevent qwen3 IRI anchoring and wrong layout tool name
May 5, 2026
604fb0b
fix(demo): require 2s stable idle before injecting next turn
May 5, 2026
630d8ee
fix(demo): anchor T1 on rdfs:subClassOf with explicit predicate hint
May 5, 2026
632e70d
fix(demo): replace isAiStreaming() with content-length + relay-idle d…
May 5, 2026
2901e10
fix(demo): robust socratic session — full starter prompt + T1/T4/T7 f…
May 5, 2026
46b342a
chore(demo): update videos from second confirmed-passing build run
May 5, 2026
b0eb8cb
chore(demo): remove outdated openwebui-pizza spec and videos
May 5, 2026
9efff20
fix(demo): replace isAiStreaming() in setup + cap waitQuiet with maxM…
May 5, 2026
15578e8
fix(demo): T1 explicit both subClassOf edges; T7 fresh ABox individuals
May 5, 2026
408b3df
fix(demo): T4 force rdfs:domain/range; T7 fix hasPart direction + use…
May 5, 2026
0cc7bb8
fix(demo): reliable OWL-RL inference in Socratic pizza demo
May 5, 2026
75f8717
fix(demo): show help() subtitle and wait 30s before Socratic questions
May 5, 2026
66b2161
fix(demo): reduce post-help() idle wait from 30s to 10s
May 5, 2026
1a40539
feat(mcp): expandNode navigates to node after expanding
May 5, 2026
5b462bf
feat(demo): inject autoApplyLayout=true before recording
May 5, 2026
ee0c30e
feat(demo): fix OWL-RL reasoning by isolating pizza ontology from def…
May 6, 2026
dd0e8c0
feat(worker): skolemize blank nodes to urn:vg:bnode: IRIs at store wr…
May 7, 2026
643518e
refactor(worker): replace random session map with content-hash skolem…
May 7, 2026
308193f
test(config): exclude .worktrees from vitest scan
May 7, 2026
6e8df66
chore(demo): harden socratic demo for blank-node pipeline reliability
May 7, 2026
8befe67
chore(dev): document logs/ convention and OWUI WebSocket solution
May 7, 2026
2f4bda4
fix(worker): use label-only hash for blank node skolemization
May 7, 2026
5e1a1ff
fix(mcp): guard addTriple against inline Turtle syntax; add addTriple…
May 7, 2026
677d32c
feat(scripts): shorten idle passages in demo videos
May 7, 2026
10583a4
chore(demo): re-record openwebui-socratic demo with idle trimming
May 7, 2026
7292da4
fix(demo): use video.saveAs() for reliable recording capture; re-record
May 7, 2026
f5c3301
fix(mcp): reject addTriple IRIs containing spaces; add demo tool-call…
May 7, 2026
487af29
feat(mcp): improve loadRdf validation and relay retry instruction
May 8, 2026
d4e7629
chore(demo): rebuild all demo videos and docs with addTriple fix; dro…
May 8, 2026
c2f14e2
fix(demo): replace addLink → addTriple in pizza-tutorial-chat; re-rec…
May 8, 2026
cb23182
fix(demo): fix video collection prefix collision; re-record pizza videos
May 8, 2026
25e5c01
fix(test): update loadRdf test to expect injected built-in prefixes
May 8, 2026
42f533b
fix(reasoning): clear inferred graph refreshes canvas via emitAllSubj…
May 11, 2026
7097075
Merge branch 'feat/blank-node-skolemization'
May 12, 2026
d5a6371
fix(mcp): accept subjectIri as alias for iri in addNode
May 12, 2026
1c21435
chore(demo): re-record all 6 demos; fix demo-video.sh for reliable re…
May 12, 2026
f368b29
chore(demo): record openwebui-socratic demo video
May 12, 2026
69504c8
fix(demo): abort early if model fires no tools during help() cycle
May 12, 2026
22a2277
chore(demo): re-record openwebui-socratic with working model; fix abo…
May 12, 2026
eade191
fix(relay): replace 1-s isAiStreaming() idle check with 12-s content-…
May 12, 2026
a967bcf
fix(demo): complete OWL restriction triples in T4; preserve last vide…
May 12, 2026
b3c5636
fix(mcp): permit addTriple blank-node restrictions; fix loadRdf 'only…
May 13, 2026
0dd780b
fix(mcp): permit both restriction paths; fix loadRdf no-@prefix example
May 13, 2026
d658bd7
fix(demo): wait for first MCP call before quiet timer; sanitize Turtl…
May 13, 2026
5206925
chore(demo): record working openwebui-socratic demo with classification
May 13, 2026
fc2d3cd
fix(queryGraph): shorten IRIs in results; map urn:vg:bnode: to _:bnode
May 13, 2026
7481f74
fix(demo): all 3 pizzas classified; fix relay flush stability + capti…
May 13, 2026
1359c48
build: bump rdf-reasoner-konclude to 0.2.0, add postinstall asset copy
May 19, 2026
f8b596b
refactor(reasoning): migrate KoncludeReasoner to rdf-reasoner-konclud…
May 19, 2026
56a9678
test(reasoning): document Konclude echo behaviour — 12 echoed, 15 gen…
May 19, 2026
9c524c5
fix(reasoning): filter Konclude-echoed source triples from inferred g…
May 19, 2026
20f4ac0
feat(reasoning): Konclude as default backend; add backend selector UI…
May 19, 2026
3fa76cd
test(reasoning): pin N3 tests to reasonerBackend='n3' (default is now…
May 19, 2026
b201bd8
test(mcp): update getCapabilities snapshot to include reasonerBackends
May 19, 2026
9f657e5
fix(reasoning): pass reasonerBackend from config in handleRunReasoning
May 19, 2026
7e63cc1
chore: bump version to 1.3.0
May 19, 2026
26eefcc
fix(reasoning): forward reasonerBackend through runReasoning dispatch…
May 19, 2026
bdb47bc
test(mcp): fix nodes/graph test failures; drop unskippable jsdom comp…
May 19, 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
8 changes: 8 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 @@ -40,6 +41,13 @@ AGENT.md

# Local test/dev HTML pages (bookmarklet testing, not for the repo)
public/relay-test.html

# Konclude WASM assets — auto-generated by postinstall, never commit
public/rdf-reasoner-konclude/
.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