| |
| |
| |
| |
|
|
| 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 (_) { |
| |
| } |
| }); |
| 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 (_) { |
| |
| } |
| disconnectTrainingStream(); |
| }); |
| trainingStream.onerror = () => { |
| |
| 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 |
| <!-- narration: This stage tester slide checks verifier and TRIBE paths. --> |
| |
| \`\`\`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); |
| }); |
|
|
| |
| window.addEventListener("load", () => { |
| rewardChart.resize(); |
| brainChart.resize(); |
| }); |
|
|
| window.addEventListener("beforeunload", () => { |
| clearPoll(); |
| disconnectTrainingStream(); |
| rewardChart.destroy(); |
| brainChart.destroy(); |
| }); |
|
|
| setChartPointLabels(); |
| setupTabs(); |
| setupStageTester(); |
| connectTrainingStream(); |
| schedulePoll(0); |
|
|