/** * NeuroCaster web UI — telemetry polling, fixed-size Chart.js panels, training + chat. * Chart height is locked in CSS (.chart-surface); options use maintainAspectRatio: false. */ const CONFIG = { telemetryUrl: "/api/telemetry?n=100", trainStatusUrl: "/api/train/status", trainStreamUrl: "/api/training/stream", maxSeriesPoints: 60, maxLogLines: 500, pollFastMs: 2000, pollIdleMs: 8000, pollBackgroundMs: 20000, }; const el = { tabTraining: document.querySelector("#tab-training"), tabStageTester: document.querySelector("#tab-stage-tester"), workspaceTraining: document.querySelector("#workspace-training"), workspaceStageTester: document.querySelector("#workspace-stage-tester"), trainForm: document.querySelector("#train-form"), trainButton: document.querySelector("#start-training"), trainStatus: document.querySelector("#train-status"), modelName: document.querySelector("#model-name"), maxEpisodes: document.querySelector("#max-episodes"), metricEpisode: document.querySelector("#metric-episode"), metricReward: document.querySelector("#metric-reward"), metricCognitiveLoad: document.querySelector("#metric-cognitive-load"), brainSts: document.querySelector("#brain-sts"), brainTpj: document.querySelector("#brain-tpj"), brainBroca: document.querySelector("#brain-broca"), meterSts: document.querySelector("#meter-sts"), meterTpj: document.querySelector("#meter-tpj"), meterBroca: document.querySelector("#meter-broca"), chatForm: document.querySelector("#chat-form"), chatInput: document.querySelector("#chat-input"), chatScore: document.querySelector("#chat-score"), chatResponse: document.querySelector("#chat-response"), trainLog: document.querySelector("#train-log"), testerReset: document.querySelector("#tester-reset"), testerRunAuto: document.querySelector("#tester-run-auto"), testerDownloadTrace: document.querySelector("#tester-download-trace"), testerForm: document.querySelector("#tester-form"), testerStage: document.querySelector("#tester-stage"), testerActionJson: document.querySelector("#tester-action-json"), testerLoadTemplate: document.querySelector("#tester-load-template"), testerAutoPrompt: document.querySelector("#tester-auto-prompt"), testerSummary: document.querySelector("#tester-summary"), testerResponse: document.querySelector("#tester-response"), }; const pointLabelEls = document.querySelectorAll("[data-chart-points]"); function setChartPointLabels() { const n = String(CONFIG.maxSeriesPoints); pointLabelEls.forEach((node) => { node.textContent = n; }); } Chart.defaults.color = "rgba(248, 251, 255, 0.78)"; Chart.defaults.borderColor = "rgba(255, 255, 255, 0.09)"; Chart.defaults.font.family = "Inter, system-ui, sans-serif"; function baseChartOptions(yTitle) { return { responsive: true, maintainAspectRatio: false, animation: false, interaction: { intersect: false, mode: "index" }, resizeDelay: 0, layout: { padding: { top: 6, right: 8, bottom: 4, left: 4 }, }, elements: { point: { radius: 0, hoverRadius: 4 }, line: { borderWidth: 2, tension: 0.25 }, }, plugins: { legend: { display: true, position: "top", align: "end", labels: { boxWidth: 10, usePointStyle: true, padding: 8, font: { size: 10 } }, }, tooltip: { enabled: true, intersect: false, }, }, scales: { x: { display: true, title: { display: true, text: "Episode", font: { size: 11 } }, grid: { color: "rgba(255, 255, 255, 0.04)" }, ticks: { maxRotation: 0, autoSkip: true, maxTicksLimit: 8, font: { size: 10 } }, }, y: { display: true, title: { display: true, text: yTitle, font: { size: 11 } }, grid: { color: "rgba(255, 255, 255, 0.04)" }, ticks: { font: { size: 10 } }, }, }, }; } function datasetLine(label, color, fill) { return { label, data: [], borderColor: color, backgroundColor: fill ? `${color}22` : "transparent", fill: Boolean(fill), pointRadius: 0, }; } const rewardChart = new Chart(document.getElementById("reward-chart"), { type: "line", data: { labels: [], datasets: [datasetLine("Total reward", "#12c2e9", true)], }, options: baseChartOptions("Reward"), }); const brainChart = new Chart(document.getElementById("brain-chart"), { type: "line", data: { labels: [], datasets: [ datasetLine("STS", "#12c2e9", false), datasetLine("TPJ", "#c471ed", false), datasetLine("Broca", "#f64f59", false), ], }, options: baseChartOptions("Z-score"), }); function formatNumber(value) { const n = Number(value); return Number.isFinite(n) ? n.toFixed(3) : "0.000"; } function metric(row, ...keys) { for (const key of keys) { const v = row?.[key]; if (v !== undefined && v !== null && v !== "") { const n = Number(v); return Number.isFinite(n) ? n : 0; } } return 0; } function updateMeter(node, value) { const n = Math.max(0, Math.min(100, 50 + Number(value || 0) * 18)); node.style.width = `${n}%`; } const series = { lastEpisode: 0, labels: [], rewards: [], sts: [], tpj: [], broca: [], }; let lastHash = ""; let pollTimer = null; let pollIntervalMs = CONFIG.pollIdleMs; let fetchLock = false; let trainingRunning = false; let trainingStream = null; const logLines = []; let testerLastReset = null; let testerTrace = []; function appendTrainingLog(line) { const text = String(line || "").trim(); if (!text) { return; } logLines.push(text); while (logLines.length > CONFIG.maxLogLines) { logLines.shift(); } if (el.trainLog) { el.trainLog.textContent = logLines.join("\n"); el.trainLog.scrollTop = el.trainLog.scrollHeight; } } function disconnectTrainingStream() { if (trainingStream) { trainingStream.close(); trainingStream = null; } } function connectTrainingStream(force = false) { if (!window.EventSource) { return; } if (trainingStream && !force) { return; } disconnectTrainingStream(); try { trainingStream = new EventSource(CONFIG.trainStreamUrl); } catch (_) { trainingStream = null; return; } trainingStream.addEventListener("log", (event) => { try { const payload = JSON.parse(event.data || "{}"); appendTrainingLog(payload.line || ""); } catch (_) { appendTrainingLog(event.data || ""); } }); trainingStream.addEventListener("status", (event) => { try { const payload = JSON.parse(event.data || "{}"); const was = trainingRunning; trainingRunning = Boolean(payload.running); if (was && !trainingRunning) { if (payload.status === "failed") { el.trainStatus.textContent = `Training failed (exit ${payload.exit_code ?? "?"}). Live logs shown below.`; } else { el.trainStatus.textContent = "Training finished. You can use the playground."; } } } catch (_) { // no-op } }); trainingStream.addEventListener("end", (event) => { try { const payload = JSON.parse(event.data || "{}"); if (payload.status === "failed") { el.trainStatus.textContent = `Training failed (exit ${payload.exit_code ?? "?"}). Live logs shown below.`; } } catch (_) { // no-op } disconnectTrainingStream(); }); trainingStream.onerror = () => { // Stream may be disabled by config or temporarily unavailable. disconnectTrainingStream(); }; } function setupTabs() { if (!el.tabTraining || !el.tabStageTester) { return; } const activate = (target) => { const trainingActive = target === "training"; el.tabTraining.classList.toggle("is-active", trainingActive); el.tabStageTester.classList.toggle("is-active", !trainingActive); el.tabTraining.setAttribute("aria-selected", trainingActive ? "true" : "false"); el.tabStageTester.setAttribute("aria-selected", !trainingActive ? "true" : "false"); if (el.workspaceTraining) { el.workspaceTraining.hidden = !trainingActive; } if (el.workspaceStageTester) { el.workspaceStageTester.hidden = trainingActive; } }; el.tabTraining.addEventListener("click", () => activate("training")); el.tabStageTester.addEventListener("click", () => activate("stage_tester")); activate("training"); } function prettyJson(value) { return JSON.stringify(value, null, 2); } function stageTemplate(stage) { if (stage === "investigator") { return { metadata: { done_investigating: true }, }; } if (stage === "drafter") { const audioAnchor = testerLastReset?.observation?.task?.audio_anchor || "/app/shared_data/audio/voices/reference.wav"; return { slide_markdown: `--- title: Stage Tester audio: ${audioAnchor} --- # Architecture Snapshot \`\`\`mermaid flowchart TD user[User] --> service[Service] \`\`\` `, }; } return {}; } async function testerResetEpisode() { const response = await fetch("/reset", { method: "POST" }); const payload = await response.json(); testerTrace = []; testerTrace.push({ timestamp: new Date().toISOString(), step: "reset", payload, }); testerLastReset = payload; if (el.testerSummary) { const obs = payload?.observation || {}; el.testerSummary.textContent = `Reset ok. Next stage: ${obs.stage || "unknown"}, task: ${obs.task?.task_id || "n/a"}`; } if (el.testerResponse) { el.testerResponse.textContent = prettyJson(payload); } return payload; } async function testerRunStep(stage, actionBody) { const response = await fetch("/step", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ stage, ...actionBody }), }); const payload = await response.json(); if (el.testerSummary) { const obs = payload?.observation || {}; el.testerSummary.textContent = `Step ${stage} => next ${obs.stage || "unknown"}, reward ${Number(payload?.reward ?? 0).toFixed(3)}, done ${Boolean(payload?.done)}`; } if (el.testerResponse) { el.testerResponse.textContent = prettyJson(payload); } testerTrace.push({ timestamp: new Date().toISOString(), step: stage, action: { stage, ...actionBody }, payload, }); return payload; } function setTesterSummary(message) { if (el.testerSummary) { el.testerSummary.textContent = message; } } function setTesterResponseSections(sections) { if (!el.testerResponse) { return; } const text = sections .map((section) => `=== ${section.title} ===\n${prettyJson(section.payload)}`) .join("\n\n"); el.testerResponse.textContent = text; } async function generateAutomatedDraft(prompt) { const response = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt }), }); const payload = await response.json(); if (!response.ok) { throw new Error(payload.detail || "Automated draft generation failed"); } testerTrace.push({ timestamp: new Date().toISOString(), step: "llm_draft_generation", request: { prompt }, payload, }); return payload; } async function runAutomatedEpisode() { const sections = []; setTesterSummary("Automated episode: reset..."); const resetPayload = await testerResetEpisode(); sections.push({ title: "reset", payload: resetPayload }); const task = resetPayload?.observation?.task || {}; setTesterSummary("Automated episode: investigator..."); const investigatorPayload = await testerRunStep("investigator", { repo_path: task.repo_path || "", query: task.query || "", metadata: { done_investigating: true }, }); sections.push({ title: "investigator", payload: investigatorPayload }); const promptOverride = (el.testerAutoPrompt?.value || "").trim(); const llmPrompt = promptOverride || String(task.query || "Explain this repository architecture."); setTesterSummary("Automated episode: generating drafter markdown with model..."); const draftGeneration = await generateAutomatedDraft(llmPrompt); sections.push({ title: "llm_draft_generation", payload: draftGeneration }); setTesterSummary("Automated episode: drafter..."); const drafterPayload = await testerRunStep("drafter", { slide_markdown: String(draftGeneration.markdown || ""), }); sections.push({ title: "drafter", payload: drafterPayload }); setTesterSummary("Automated episode: verifier_bio..."); const verifierPayload = await testerRunStep("verifier_bio", {}); sections.push({ title: "verifier_bio", payload: verifierPayload }); const finalDone = Boolean(verifierPayload?.done); const finalReward = Number(verifierPayload?.reward ?? 0).toFixed(3); setTesterSummary(`Automated episode complete: done=${finalDone}, reward=${finalReward}`); setTesterResponseSections(sections); } function setupStageTester() { if (!el.testerReset || !el.testerForm || !el.testerStage || !el.testerActionJson) { return; } el.testerReset.addEventListener("click", async () => { el.testerReset.disabled = true; try { await testerResetEpisode(); } catch (error) { if (el.testerSummary) { el.testerSummary.textContent = `Reset failed: ${error.message}`; } } finally { el.testerReset.disabled = false; } }); el.testerLoadTemplate?.addEventListener("click", () => { const stage = el.testerStage.value; el.testerActionJson.value = prettyJson(stageTemplate(stage)); }); el.testerForm.addEventListener("submit", async (event) => { event.preventDefault(); const stage = el.testerStage.value; let parsed = {}; try { parsed = JSON.parse(el.testerActionJson.value || "{}"); } catch (error) { if (el.testerSummary) { el.testerSummary.textContent = `Invalid JSON: ${error.message}`; } return; } const runButton = el.testerForm.querySelector("button[type='submit']"); if (runButton) { runButton.disabled = true; } try { await testerRunStep(stage, parsed); } catch (error) { if (el.testerSummary) { el.testerSummary.textContent = `Step failed: ${error.message}`; } } finally { if (runButton) { runButton.disabled = false; } } }); el.testerRunAuto?.addEventListener("click", async () => { const autoBtn = el.testerRunAuto; autoBtn.disabled = true; try { await runAutomatedEpisode(); } catch (error) { setTesterSummary(`Automated run failed: ${error.message}`); } finally { autoBtn.disabled = false; } }); el.testerDownloadTrace?.addEventListener("click", () => { if (!testerTrace.length) { setTesterSummary("No trace available yet. Run reset/steps first."); return; } const episodeId = testerTrace[0]?.payload?.observation?.episode_id || `episode-${Date.now()}`; const filename = `${String(episodeId).replace(/[^a-zA-Z0-9-_]/g, "_")}-trace.json`; const blob = new Blob([prettyJson(testerTrace)], { type: "application/json" }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; document.body.appendChild(anchor); anchor.click(); anchor.remove(); URL.revokeObjectURL(url); setTesterSummary(`Trace exported: ${filename}`); }); } function resetSeries() { series.lastEpisode = 0; series.labels.length = 0; series.rewards.length = 0; series.sts.length = 0; series.tpj.length = 0; series.broca.length = 0; } function trimSeries() { while (series.labels.length > CONFIG.maxSeriesPoints) { series.labels.shift(); series.rewards.shift(); series.sts.shift(); series.tpj.shift(); series.broca.shift(); } } function applySeriesToCharts() { const r = rewardChart; const b = brainChart; r.data.labels = series.labels; r.data.datasets[0].data = series.rewards; b.data.labels = series.labels; b.data.datasets[0].data = series.sts; b.data.datasets[1].data = series.tpj; b.data.datasets[2].data = series.broca; r.update("none"); b.update("none"); } function ingestRows(rows) { const cap = CONFIG.maxSeriesPoints; const slice = rows.length > cap ? rows.slice(-cap) : rows; const latest = slice.at(-1); if (!latest) { return; } const latestEp = Number(latest.episode || 0); if (latestEp > 0 && latestEp < series.lastEpisode) { resetSeries(); } const reward = metric(latest, "total_reward", "cumulative_reward", "reward"); const h = `${latestEp}-${reward}`; if (h === lastHash) { return; } lastHash = h; for (const row of slice) { let ep = Number(row.episode || 0); if (!Number.isFinite(ep) || ep <= 0) { ep = series.lastEpisode + 1; } if (ep <= series.lastEpisode) { continue; } series.lastEpisode = ep; series.labels.push(ep); series.rewards.push(metric(row, "total_reward", "cumulative_reward", "reward")); series.sts.push(metric(row, "STS_Z_Score", "Z_STS")); series.tpj.push(metric(row, "TPJ_Z_Score", "Z_TPJ")); series.broca.push(metric(row, "Broca_Z_Score", "Z_Broca", "cognitive_load")); } trimSeries(); applySeriesToCharts(); const s = metric(latest, "STS_Z_Score", "Z_STS"); const t = metric(latest, "TPJ_Z_Score", "Z_TPJ"); const b = metric(latest, "Broca_Z_Score", "Z_Broca", "cognitive_load"); el.metricEpisode.textContent = String(latest.episode ?? rows.length); el.metricReward.textContent = formatNumber(reward); el.metricCognitiveLoad.textContent = formatNumber(b); el.brainSts.textContent = formatNumber(s); el.brainTpj.textContent = formatNumber(t); el.brainBroca.textContent = formatNumber(b); updateMeter(el.meterSts, s); updateMeter(el.meterTpj, t); updateMeter(el.meterBroca, b); } async function getTelemetry() { const res = await fetch(CONFIG.telemetryUrl, { cache: "no-store" }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } const data = await res.json(); if (!Array.isArray(data)) { return; } ingestRows(data); } async function getTrainStatus() { const res = await fetch(CONFIG.trainStatusUrl, { cache: "no-store" }); if (!res.ok) { return; } const json = await res.json(); const was = trainingRunning; trainingRunning = Boolean(json.running); if (was && !trainingRunning) { if (json.status === "failed") { el.trainStatus.textContent = `Training failed (exit ${json.exit_code ?? "?"}). Live logs shown below.`; } else { el.trainStatus.textContent = "Training finished. You can use the playground."; } } else if (!was && trainingRunning) { el.trainStatus.textContent = `Training (pid ${json.pid}) — streaming telemetry.`; } } function clearPoll() { if (pollTimer !== null) { clearTimeout(pollTimer); pollTimer = null; } } function schedulePoll(ms) { clearPoll(); pollTimer = window.setTimeout(tick, ms); } async function tick() { if (fetchLock) { schedulePoll(pollIntervalMs); return; } fetchLock = true; try { await getTrainStatus(); await getTelemetry(); } catch (e) { el.trainStatus.textContent = `Telemetry error: ${e.message}`; } finally { fetchLock = false; if (document.hidden) { pollIntervalMs = CONFIG.pollBackgroundMs; } else { pollIntervalMs = trainingRunning ? CONFIG.pollFastMs : CONFIG.pollIdleMs; } schedulePoll(pollIntervalMs); } } el.trainForm.addEventListener("submit", async (e) => { e.preventDefault(); el.trainButton.disabled = true; el.trainStatus.textContent = "Starting training…"; try { const res = await fetch("/api/train", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model_name: el.modelName.value.trim(), max_episodes: Number(el.maxEpisodes.value || 1), }), }); const json = await res.json(); if (!res.ok || json.ok === false) { throw new Error(json.detail || json.status || "Could not start training"); } el.trainStatus.textContent = `Started (pid ${json.pid}).`; trainingRunning = true; logLines.length = 0; appendTrainingLog(`[neurocaster] training started (pid ${json.pid})`); connectTrainingStream(true); pollIntervalMs = CONFIG.pollFastMs; await getTelemetry(); schedulePoll(0); } catch (err) { el.trainStatus.textContent = `Error: ${err.message}`; } finally { el.trainButton.disabled = false; } }); el.chatForm.addEventListener("submit", async (e) => { e.preventDefault(); const prompt = el.chatInput.value.trim(); if (!prompt) { return; } const btn = el.chatForm.querySelector("button"); btn.disabled = true; el.chatScore.textContent = "Generating…"; el.chatResponse.innerHTML = ""; try { const res = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt }), }); const json = await res.json(); if (!res.ok) { throw new Error(json.detail || "Request failed"); } const md = json.markdown || ""; if (window.marked && typeof window.marked.parse === "function") { el.chatResponse.innerHTML = window.marked.parse(md); } else { el.chatResponse.textContent = md; } const m = json.metrics || {}; el.chatScore.textContent = `TRIBE-v2: ${formatNumber(json.biological_reward)} ` + `(STS ${formatNumber(m.STS_Z_Score)}, TPJ ${formatNumber(m.TPJ_Z_Score)}, Broca ${formatNumber(m.Broca_Z_Score)})`; } catch (err) { el.chatScore.textContent = `Error: ${err.message}`; } finally { btn.disabled = false; } }); document.addEventListener("visibilitychange", () => { pollIntervalMs = document.hidden ? CONFIG.pollBackgroundMs : trainingRunning ? CONFIG.pollFastMs : CONFIG.pollIdleMs; schedulePoll(0); }); /* One layout sync after fonts — avoids a single mis-sized first frame */ window.addEventListener("load", () => { rewardChart.resize(); brainChart.resize(); }); window.addEventListener("beforeunload", () => { clearPoll(); disconnectTrainingStream(); rewardChart.destroy(); brainChart.destroy(); }); setChartPointLabels(); setupTabs(); setupStageTester(); connectTrainingStream(); schedulePoll(0);