- /api/graph/active now includes node_details (model, max_tokens per node) - graph.js calls initNodesFromGraph() after fetching active graph - Nodes panel shows all nodes with models immediately on load (before first message) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
192 lines
6.5 KiB
JavaScript
192 lines
6.5 KiB
JavaScript
/** 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 } }
|
|
|
|
function _getNode(name) {
|
|
if (!_nodeState[name]) {
|
|
_nodeState[name] = {
|
|
model: '', tokens: 0, maxTokens: 0, fillPct: 0,
|
|
lastEvent: '', lastDetail: '', status: 'idle',
|
|
toolCalls: 0, lastTool: '',
|
|
};
|
|
}
|
|
return _nodeState[name];
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
function renderNodes() {
|
|
const el = document.getElementById('node-metrics');
|
|
if (!el) { console.warn('[nodes] #node-metrics not found'); return; }
|
|
|
|
// Sort: active nodes first, then by name
|
|
const statusOrder = { thinking: 0, tool: 0, streaming: 0, planned: 1, done: 2, idle: 3 };
|
|
const sorted = Object.entries(_nodeState)
|
|
.filter(([name]) => name !== 'runtime' && name !== 'frame_engine')
|
|
.sort((a, b) => (statusOrder[a[1].status] || 3) - (statusOrder[b[1].status] || 3));
|
|
|
|
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 += `<div class="node-card ${statusClass}">
|
|
<div class="nc-header">
|
|
<span class="nc-name">${esc(shortName)}</span>
|
|
<span class="nc-model">${esc(modelShort)}</span>
|
|
<span class="nc-tokens">${esc(tokenStr)}</span>
|
|
</div>
|
|
<div class="nc-bar"><div class="nc-fill" style="width:${fillW}%"></div></div>
|
|
<div class="nc-status">
|
|
<span class="nc-event">${esc(n.lastEvent)}</span>
|
|
<span class="nc-detail">${esc(detail)}${esc(toolStr)}</span>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
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]) =>
|
|
`<div class="aw-row"><span class="aw-key">${esc(k)}</span><span class="aw-val">${esc(v || 'null')}</span></div>`
|
|
).join('');
|
|
|
|
if (facts.length) {
|
|
html += '<div class="aw-row"><span class="aw-key">facts</span><span class="aw-val">'
|
|
+ facts.map(f => esc(truncate(f, 40))).join('<br>') + '</span></div>';
|
|
}
|
|
if (history.length) {
|
|
html += '<div class="aw-row"><span class="aw-key">topics</span><span class="aw-val">'
|
|
+ history.map(t => esc(truncate(t, 25))).join(', ') + '</span></div>';
|
|
}
|
|
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 = `<div class="aw-row"><span class="aw-key">tick</span><span class="aw-val">#${tick}</span></div>`;
|
|
for (const [k, v] of Object.entries(_sensorReadings)) {
|
|
html += `<div class="aw-row"><span class="aw-key">${esc(k)}</span><span class="aw-val">${esc(String(v))}</span></div>`;
|
|
}
|
|
body.innerHTML = html;
|
|
}
|