agent-runtime/static/js/dashboard.js
Nico 217d1a57d9 v0.16.0: Workspace component system — cards, lists, structured display
New workspace components:
- emit_card: structured detail card with title, subtitle, fields, actions
  Fields can be clickable links (action property)
  Used for: entity details (Kunde, Objekt, Auftrag)
- emit_list: vertical list of cards for multiple entities
  Used for: search results, navigation lists
- "WHEN TO USE WHAT" guide in expert prompt

Frontend rendering:
- renderCard() with key-value fields, clickable links, action buttons
- List container with title + stacked cards
- Full CSS: dark theme cards, hover states, link styling

Pipeline:
- ExpertNode handles emit_card/emit_list in tool execution
- UINode passes card/list through as-is (not wrapped in display)
- Test runner: check_actions supports "has card", "has list", "has X or Y"

Workspace components test: 22/22

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:54:47 +02:00

168 lines
6.2 KiB
JavaScript

/** Dashboard: workspace controls rendering (buttons, tables, labels, displays, machines). */
import { esc } from './util.js';
import { addTrace } from './trace.js';
import { setDashboard } from './chat.js';
let _ws = null;
export function setWs(ws) { _ws = ws; }
export function dockControls(controls) {
setDashboard(controls); // S3*: remember what's rendered
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 = () => {
if (_ws && _ws.readyState === 1) {
_ws.send(JSON.stringify({ type: 'action', action: ctrl.action, data: ctrl.payload || ctrl.data || {} }));
addTrace('runtime', 'action', ctrl.action);
}
};
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';
const style = ctrl.style ? ' display-' + ctrl.style : '';
disp.className = 'control-display display-' + dt + style;
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 if (dt === 'status') {
disp.innerHTML = '<span class="cd-icon">' + (ctrl.style === 'success' ? '\u2713' : ctrl.style === 'error' ? '\u2717' : '\u2139') + '</span>'
+ '<span class="cd-label">' + esc(ctrl.label) + '</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') {
container.appendChild(renderCard(ctrl));
} else if (ctrl.type === 'list') {
const listEl = document.createElement('div');
listEl.className = 'ws-list';
if (ctrl.title) {
const h = document.createElement('div');
h.className = 'ws-list-title';
h.textContent = ctrl.title;
listEl.appendChild(h);
}
for (const item of (ctrl.items || [])) {
item.type = item.type || 'card';
listEl.appendChild(renderCard(item));
}
container.appendChild(listEl);
}
}
body.appendChild(container);
}
function renderCard(card) {
const el = document.createElement('div');
el.className = 'ws-card';
if (card.action) {
el.classList.add('ws-card-clickable');
el.onclick = () => {
if (_ws && _ws.readyState === 1) {
_ws.send(JSON.stringify({ type: 'action', action: card.action, data: card.payload || {} }));
addTrace('runtime', 'action', card.action);
}
};
}
let html = '';
if (card.title) html += '<div class="ws-card-title">' + esc(card.title) + '</div>';
if (card.subtitle) html += '<div class="ws-card-subtitle">' + esc(card.subtitle) + '</div>';
if (card.fields && card.fields.length) {
html += '<div class="ws-card-fields">';
for (const f of card.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>';
}
if (card.actions && card.actions.length) {
html += '<div class="ws-card-actions">';
for (const a of card.actions) {
html += '<button class="control-btn ws-card-btn" data-action="' + esc(a.action || '') + '">' + esc(a.label || '') + '</button>';
}
html += '</div>';
}
el.innerHTML = html;
// Wire up field links and action buttons
el.querySelectorAll('.ws-card-link').forEach(link => {
link.onclick = (e) => {
e.stopPropagation();
const action = link.dataset.action;
if (_ws && _ws.readyState === 1) {
_ws.send(JSON.stringify({ type: 'action', action, data: {} }));
addTrace('runtime', 'action', action);
}
};
});
el.querySelectorAll('.ws-card-btn').forEach(btn => {
btn.onclick = (e) => {
e.stopPropagation();
const action = btn.dataset.action;
if (_ws && _ws.readyState === 1) {
_ws.send(JSON.stringify({ type: 'action', action, data: {} }));
addTrace('runtime', 'action', action);
}
};
});
return el;
}
export function clearDashboard() {
const body = document.getElementById('workspace-body');
if (body) body.innerHTML = '';
}