/** Awareness panel: memorizer state, sensor readings. * Node detail panel: per-node model, tokens, progress, last event. */ import { esc, truncate } from './util.js'; let _sensorReadings = {}; // --- Node state tracker --- const _nodeState = {}; // { nodeName: { model, tokens, maxTokens, fillPct, lastEvent, lastDetail, status, toolCalls, startedAt } } // Normalize node names to avoid duplicates (pa_v1→pa, expert_eras→eras, etc.) function _normName(name) { return name.replace('_v1', '').replace('_v2', '').replace('expert_', ''); } function _getNode(name) { const key = _normName(name); if (!_nodeState[key]) { _nodeState[key] = { model: '', tokens: 0, maxTokens: 0, fillPct: 0, lastEvent: '', lastDetail: '', status: 'idle', toolCalls: 0, lastTool: '', }; } return _nodeState[key]; } export function updateNodeFromHud(node, event, data) { const n = _getNode(node); if (event === 'context') { if (data.model) n.model = data.model.replace('google/', '').replace('anthropic/', ''); if (data.tokens !== undefined) n.tokens = data.tokens; if (data.max_tokens !== undefined) n.maxTokens = data.max_tokens; if (data.fill_pct !== undefined) n.fillPct = data.fill_pct; } if (event === 'thinking') { n.status = 'thinking'; n.lastEvent = 'thinking'; n.lastDetail = data.detail || ''; } else if (event === 'perceived') { n.status = 'done'; n.lastEvent = 'perceived'; const a = data.analysis || {}; n.lastDetail = `${a.intent || '?'}/${a.language || '?'}/${a.tone || '?'}`; } else if (event === 'decided' || event === 'routed') { n.status = 'done'; n.lastEvent = event; n.lastDetail = data.goal || data.instruction || data.job || ''; } else if (event === 'tool_call') { n.status = 'tool'; n.lastEvent = 'tool_call'; n.lastTool = data.tool || ''; n.lastDetail = data.tool || ''; n.toolCalls++; } else if (event === 'tool_result') { n.lastEvent = 'tool_result'; n.lastDetail = truncate(data.output || '', 50); } else if (event === 'streaming') { n.status = 'streaming'; n.lastEvent = 'streaming'; } else if (event === 'done') { n.status = 'done'; n.lastEvent = 'done'; } else if (event === 'updated') { n.status = 'done'; n.lastEvent = 'updated'; } else if (event === 'planned') { n.status = 'planned'; n.lastEvent = 'planned'; n.lastDetail = `${data.tools || 0} tools`; } else if (event === 'interpreted') { n.status = 'done'; n.lastEvent = 'interpreted'; n.lastDetail = truncate(data.summary || '', 50); } renderNodes(); } // Fixed pipeline order — no re-sorting // Fixed pipeline order using normalized names const PIPELINE_ORDER = ['input', 'pa', 'director', 'eras', 'plankiste', 'thinker', 'interpreter', 'output', 'memorizer', 'ui', 'sensor']; function renderNodes() { const el = document.getElementById('node-metrics'); if (!el) return; const entries = Object.entries(_nodeState) .filter(([name]) => name !== 'runtime' && name !== 'frame_engine'); const sorted = entries.sort((a, b) => { const ia = PIPELINE_ORDER.indexOf(a[0]); const ib = PIPELINE_ORDER.indexOf(b[0]); return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib); }); let html = ''; for (const [name, n] of sorted) { const statusClass = n.status === 'thinking' || n.status === 'tool' ? 'nm-active' : n.status === 'streaming' ? 'nm-streaming' : ''; const shortName = name.replace('_v1', '').replace('_v2', '').replace('expert_', ''); const modelShort = n.model ? n.model.split('/').pop().replace('-001', '').replace('-4.5', '4.5') : ''; const tokenStr = n.maxTokens ? `${n.tokens}/${n.maxTokens}t` : ''; const fillW = n.fillPct || 0; const detail = n.lastDetail ? truncate(n.lastDetail, 45) : ''; const toolStr = n.toolCalls > 0 ? ` [${n.toolCalls} calls]` : ''; html += `
${esc(shortName)} ${esc(modelShort)} ${esc(tokenStr)}
${esc(n.lastEvent)} ${esc(detail)}${esc(toolStr)}
`; } el.innerHTML = html; } export function initNodesFromGraph(graphData) { // Populate node cards from graph definition (before any messages) const nodes = graphData.nodes || {}; const details = graphData.node_details || {}; for (const [role, impl] of Object.entries(nodes)) { const n = _getNode(role); const d = details[role]; if (d) { n.model = (d.model || '').replace('google/', '').replace('anthropic/', ''); n.maxTokens = d.max_tokens || 0; } n.lastEvent = 'idle'; n.status = 'idle'; } renderNodes(); } export function clearNodes() { for (const key of Object.keys(_nodeState)) delete _nodeState[key]; const el = document.getElementById('node-metrics'); if (el) el.innerHTML = ''; } // Keep old meter function for backward compat (called from ws.js) export function updateMeter(node, tokens, maxTokens, fillPct) { const n = _getNode(node); n.tokens = tokens; n.maxTokens = maxTokens; n.fillPct = fillPct; renderNodes(); } // --- Awareness: memorizer state --- export function updateAwarenessState(state) { const body = document.getElementById('aw-state-body'); if (!body) return; const display = [ ['user', state.user_name], ['mood', state.user_mood], ['topic', state.topic], ['lang', state.language], ['style', state.style_hint], ['situation', state.situation], ]; const facts = state.facts || []; const history = state.topic_history || []; let html = display.map(([k, v]) => `
${esc(k)}${esc(v || 'null')}
` ).join(''); if (facts.length) { html += '
facts' + facts.map(f => esc(truncate(f, 40))).join('
') + '
'; } if (history.length) { html += '
topics' + history.map(t => esc(truncate(t, 25))).join(', ') + '
'; } body.innerHTML = html; } // --- Awareness: sensor readings --- export function updateAwarenessSensors(tick, deltas) { const body = document.getElementById('aw-sensor-body'); if (!body) return; for (const [k, v] of Object.entries(deltas)) { _sensorReadings[k] = v; } let html = `
tick#${tick}
`; for (const [k, v] of Object.entries(_sensorReadings)) { html += `
${esc(k)}${esc(String(v))}
`; } body.innerHTML = html; }