agent-runtime/static/js/dashboard.js
Nico 925fff731f v0.17.0: User expectation tracking, PA retry loop, machine state in PA context
- Memorizer tracks user_expectation (conversational/delegated/waiting_input/observing)
- Output node adjusts phrasing per expectation
- PA retry loop: reformulates job on expert failure (all retries exhausted or tool skip)
- Machine state in PA context: get_machine_summary includes current state, buttons, stored data
- Expert writes to machine state via update_machine + transition_machine
- Expanded baked schema coverage
- Awareness panel shows color-coded expectation state
- Dashboard and workspace component updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 19:03:07 +02:00

322 lines
12 KiB
JavaScript

/** Dashboard: workspace artifact + control rendering.
* Artifact system: typed artifacts (entity_detail, data_table, document_page, action_bar, status, machine).
* Legacy: dockControls() still works as fallback for old control format.
*/
import { esc, renderMarkdown } from './util.js';
import { addTrace } from './trace.js';
import { setDashboard } from './chat.js';
let _ws = null;
export function setWs(ws) { _ws = ws; }
function _sendAction(action, data) {
if (_ws && _ws.readyState === 1) {
_ws.send(JSON.stringify({ type: 'action', action, data: data || {} }));
addTrace('runtime', 'action', action);
}
}
// --- Artifact system ---
export function dockArtifacts(artifacts) {
const body = document.getElementById('workspace-body');
if (!body) return;
body.innerHTML = '';
const container = document.createElement('div');
container.className = 'artifacts-container';
for (const art of artifacts) {
const wrapper = document.createElement('div');
wrapper.className = 'ws-artifact ws-artifact-' + (art.type || 'unknown');
wrapper.dataset.artifactId = art.id || '';
const renderer = RENDERERS[art.type];
if (renderer) {
renderer(wrapper, art);
} else {
wrapper.innerHTML = '<div class="ws-artifact-fallback">' + esc(JSON.stringify(art.data || {})) + '</div>';
}
container.appendChild(wrapper);
}
body.appendChild(container);
// Also set dashboard for S3* audit (flatten actions from artifacts)
const flatControls = artifacts.flatMap(a => (a.actions || []).map(act => ({type: 'button', ...act})));
setDashboard(flatControls);
}
// --- Artifact renderers ---
const RENDERERS = {
entity_detail: renderEntityDetail,
data_table: renderDataTable,
document_page: renderDocumentPage,
action_bar: renderActionBar,
status: renderStatus,
machine: renderMachine,
};
function renderEntityDetail(el, art) {
const d = art.data || {};
let html = '';
if (d.title) html += '<div class="ws-card-title">' + esc(d.title) + '</div>';
if (d.subtitle) html += '<div class="ws-card-subtitle">' + esc(d.subtitle) + '</div>';
// List mode (multiple items)
if (d.items && d.items.length) {
html += '<div class="ws-list">';
for (const item of d.items) {
html += '<div class="ws-card ws-card-nested">';
if (item.title) html += '<div class="ws-card-title">' + esc(item.title) + '</div>';
if (item.fields) {
html += '<div class="ws-card-fields">';
for (const f of item.fields) {
html += '<div class="ws-card-field"><span class="ws-card-key">' + esc(f.label || '') + '</span><span class="ws-card-val">' + esc(String(f.value ?? '')) + '</span></div>';
}
html += '</div>';
}
html += '</div>';
}
html += '</div>';
}
// Single entity fields
if (d.fields && d.fields.length) {
html += '<div class="ws-card-fields">';
for (const f of d.fields) {
const val = f.action
? '<span class="ws-card-link" data-action="' + esc(f.action) + '">' + esc(String(f.value ?? '')) + '</span>'
: '<span class="ws-card-val">' + esc(String(f.value ?? '')) + '</span>';
html += '<div class="ws-card-field"><span class="ws-card-key">' + esc(f.label || '') + '</span>' + val + '</div>';
}
html += '</div>';
}
// Actions
if (art.actions && art.actions.length) {
html += '<div class="ws-card-actions">';
for (const a of art.actions) {
html += '<button class="control-btn ws-card-btn" data-action="' + esc(a.action || '') + '">' + esc(a.label || '') + '</button>';
}
html += '</div>';
}
el.innerHTML = html;
_wireActions(el);
}
function renderDataTable(el, art) {
const d = art.data || {};
if (d.title) {
const title = document.createElement('div');
title.className = 'ws-artifact-header';
title.textContent = d.title;
el.appendChild(title);
}
const table = document.createElement('table');
table.className = 'control-table';
const cols = d.columns || (d.rows && d.rows.length ? Object.keys(d.rows[0]) : []);
if (cols.length) {
const thead = document.createElement('tr');
for (const col of cols) {
const th = document.createElement('th');
th.textContent = col;
thead.appendChild(th);
}
table.appendChild(thead);
}
for (const row of (d.rows || d.data || [])) {
const tr = document.createElement('tr');
if (Array.isArray(row)) {
for (const cell of row) {
const td = document.createElement('td'); td.textContent = cell; tr.appendChild(td);
}
} else if (typeof row === 'object') {
for (const col of cols) {
const td = document.createElement('td'); td.textContent = row[col] ?? ''; tr.appendChild(td);
}
}
table.appendChild(tr);
}
el.appendChild(table);
}
function renderDocumentPage(el, art) {
const d = art.data || {};
let html = '';
if (d.title) html += '<div class="ws-doc-title">' + esc(d.title) + '</div>';
for (const section of (d.sections || [])) {
html += '<div class="ws-doc-section">';
if (section.heading) html += '<div class="ws-doc-heading">' + esc(section.heading) + '</div>';
if (section.content) html += '<div class="ws-doc-content">' + renderMarkdown(section.content) + '</div>';
html += '</div>';
}
// Actions (e.g. PDF export)
if (art.actions && art.actions.length) {
html += '<div class="ws-card-actions">';
for (const a of art.actions) {
html += '<button class="control-btn ws-card-btn" data-action="' + esc(a.action || '') + '">' + esc(a.label || '') + '</button>';
}
html += '</div>';
}
el.innerHTML = html;
_wireActions(el);
}
function renderActionBar(el, art) {
for (const a of (art.actions || [])) {
const btn = document.createElement('button');
btn.className = 'control-btn';
btn.textContent = a.label || '';
btn.onclick = () => _sendAction(a.action, a.payload || {});
el.appendChild(btn);
}
}
function renderStatus(el, art) {
const d = art.data || {};
const dt = d.display_type || 'text';
el.classList.add('display-' + dt);
if (dt === 'progress') {
const pct = Math.min(100, Math.max(0, Number(d.value) || 0));
el.innerHTML = '<span class="cd-label">' + esc(d.label) + '</span>'
+ '<div class="cd-bar"><div class="cd-fill" style="width:' + pct + '%"></div></div>'
+ '<span class="cd-pct">' + pct + '%</span>';
} else if (dt === 'info') {
el.innerHTML = '<span class="cd-icon">\u2139</span><span class="cd-label">' + esc(d.label) + '</span>';
} else {
el.innerHTML = '<span class="cd-label">' + esc(d.label || '') + '</span>'
+ (d.value ? '<span class="cd-value">' + esc(String(d.value)) + '</span>' : '');
}
}
function renderMachine(el, art) {
const d = art.data || {};
const mid = d.machine_id || '';
// Header
let html = '<div class="ws-machine-header"><span class="ws-machine-name">' + esc(mid) + '</span>'
+ '<span class="ws-machine-state">' + esc(d.current || '') + '</span></div>';
// Content
for (const text of (d.content || [])) {
html += '<div class="ws-machine-content">' + esc(text) + '</div>';
}
// Stored data
const stored = d.stored_data || {};
if (Object.keys(stored).length) {
html += '<div class="ws-machine-data">';
for (const [k, v] of Object.entries(stored)) {
html += '<span class="ws-machine-datum">' + esc(k) + '=' + esc(String(v)) + '</span>';
}
html += '</div>';
}
// Buttons
if (art.actions && art.actions.length) {
html += '<div class="ws-card-actions">';
for (const a of art.actions) {
html += '<button class="control-btn ws-card-btn" data-action="' + esc(a.action || '') + '">' + esc(a.label || '') + '</button>';
}
html += '</div>';
}
el.innerHTML = html;
_wireActions(el);
}
// --- Helpers ---
function _wireActions(el) {
el.querySelectorAll('.ws-card-link').forEach(link => {
link.onclick = (e) => { e.stopPropagation(); _sendAction(link.dataset.action, {}); };
});
el.querySelectorAll('.ws-card-btn').forEach(btn => {
btn.onclick = (e) => { e.stopPropagation(); _sendAction(btn.dataset.action, {}); };
});
}
// --- Legacy control rendering (backward compat) ---
export function dockControls(controls) {
setDashboard(controls);
const body = document.getElementById('workspace-body');
if (!body) return;
body.innerHTML = '';
const container = document.createElement('div');
container.className = 'controls-container';
for (const ctrl of controls) {
if (ctrl.type === 'button') {
const btn = document.createElement('button');
btn.className = 'control-btn';
btn.textContent = ctrl.label;
btn.onclick = () => _sendAction(ctrl.action, ctrl.payload || ctrl.data || {});
container.appendChild(btn);
} else if (ctrl.type === 'table') {
const table = document.createElement('table');
table.className = 'control-table';
if (ctrl.columns) {
const thead = document.createElement('tr');
for (const col of ctrl.columns) {
const th = document.createElement('th'); th.textContent = col; thead.appendChild(th);
}
table.appendChild(thead);
}
for (const row of (ctrl.data || [])) {
const tr = document.createElement('tr');
if (Array.isArray(row)) {
for (const cell of row) { const td = document.createElement('td'); td.textContent = cell; tr.appendChild(td); }
} else if (typeof row === 'object') {
for (const col of (ctrl.columns || Object.keys(row))) { const td = document.createElement('td'); td.textContent = row[col] ?? ''; tr.appendChild(td); }
}
table.appendChild(tr);
}
container.appendChild(table);
} else if (ctrl.type === 'label') {
const lbl = document.createElement('div');
lbl.className = 'control-label';
lbl.innerHTML = '<span class="cl-text">' + esc(ctrl.text || '') + '</span><span class="cl-value">' + esc(String(ctrl.value ?? '')) + '</span>';
container.appendChild(lbl);
} else if (ctrl.type === 'display') {
const disp = document.createElement('div');
const dt = ctrl.display_type || 'text';
disp.className = 'control-display display-' + dt;
if (dt === 'progress') {
const pct = Math.min(100, Math.max(0, Number(ctrl.value) || 0));
disp.innerHTML = '<span class="cd-label">' + esc(ctrl.label) + '</span><div class="cd-bar"><div class="cd-fill" style="width:' + pct + '%"></div></div><span class="cd-pct">' + pct + '%</span>';
} else {
disp.innerHTML = '<span class="cd-label">' + esc(ctrl.label) + '</span>' + (ctrl.value ? '<span class="cd-value">' + esc(String(ctrl.value)) + '</span>' : '');
}
container.appendChild(disp);
} else if (ctrl.type === 'card') {
const card = document.createElement('div');
card.className = 'ws-card';
let html = '';
if (ctrl.title) html += '<div class="ws-card-title">' + esc(ctrl.title) + '</div>';
if (ctrl.subtitle) html += '<div class="ws-card-subtitle">' + esc(ctrl.subtitle) + '</div>';
if (ctrl.fields && ctrl.fields.length) {
html += '<div class="ws-card-fields">';
for (const f of ctrl.fields) {
html += '<div class="ws-card-field"><span class="ws-card-key">' + esc(f.label || '') + '</span><span class="ws-card-val">' + esc(String(f.value ?? '')) + '</span></div>';
}
html += '</div>';
}
if (ctrl.actions && ctrl.actions.length) {
html += '<div class="ws-card-actions">';
for (const a of ctrl.actions) {
html += '<button class="control-btn ws-card-btn" data-action="' + esc(a.action || '') + '">' + esc(a.label || '') + '</button>';
}
html += '</div>';
}
card.innerHTML = html;
_wireActions(card);
container.appendChild(card);
}
}
body.appendChild(container);
}
export function clearDashboard() {
const body = document.getElementById('workspace-body');
if (body) body.innerHTML = '';
}