- 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>
322 lines
12 KiB
JavaScript
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 = '';
|
|
}
|