/** 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 = '
' + esc(JSON.stringify(art.data || {})) + '
'; } 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 += '
' + esc(d.title) + '
'; if (d.subtitle) html += '
' + esc(d.subtitle) + '
'; // List mode (multiple items) if (d.items && d.items.length) { html += '
'; for (const item of d.items) { html += '
'; if (item.title) html += '
' + esc(item.title) + '
'; if (item.fields) { html += '
'; for (const f of item.fields) { html += '
' + esc(f.label || '') + '' + esc(String(f.value ?? '')) + '
'; } html += '
'; } html += '
'; } html += '
'; } // Single entity fields if (d.fields && d.fields.length) { html += '
'; for (const f of d.fields) { const val = f.action ? '' + esc(String(f.value ?? '')) + '' : '' + esc(String(f.value ?? '')) + ''; html += '
' + esc(f.label || '') + '' + val + '
'; } html += '
'; } // Actions if (art.actions && art.actions.length) { html += '
'; for (const a of art.actions) { html += ''; } html += '
'; } 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 += '
' + esc(d.title) + '
'; for (const section of (d.sections || [])) { html += '
'; if (section.heading) html += '
' + esc(section.heading) + '
'; if (section.content) html += '
' + renderMarkdown(section.content) + '
'; html += '
'; } // Actions (e.g. PDF export) if (art.actions && art.actions.length) { html += '
'; for (const a of art.actions) { html += ''; } html += '
'; } 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 = '' + esc(d.label) + '' + '
' + '' + pct + '%'; } else if (dt === 'info') { el.innerHTML = '\u2139' + esc(d.label) + ''; } else { el.innerHTML = '' + esc(d.label || '') + '' + (d.value ? '' + esc(String(d.value)) + '' : ''); } } function renderMachine(el, art) { const d = art.data || {}; const mid = d.machine_id || ''; // Header let html = '
' + esc(mid) + '' + '' + esc(d.current || '') + '
'; // Content for (const text of (d.content || [])) { html += '
' + esc(text) + '
'; } // Stored data const stored = d.stored_data || {}; if (Object.keys(stored).length) { html += '
'; for (const [k, v] of Object.entries(stored)) { html += '' + esc(k) + '=' + esc(String(v)) + ''; } html += '
'; } // Buttons if (art.actions && art.actions.length) { html += '
'; for (const a of art.actions) { html += ''; } html += '
'; } 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 = '' + esc(ctrl.text || '') + '' + esc(String(ctrl.value ?? '')) + ''; 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 = '' + esc(ctrl.label) + '
' + pct + '%'; } else { disp.innerHTML = '' + esc(ctrl.label) + '' + (ctrl.value ? '' + esc(String(ctrl.value)) + '' : ''); } container.appendChild(disp); } else if (ctrl.type === 'card') { const card = document.createElement('div'); card.className = 'ws-card'; let html = ''; if (ctrl.title) html += '
' + esc(ctrl.title) + '
'; if (ctrl.subtitle) html += '
' + esc(ctrl.subtitle) + '
'; if (ctrl.fields && ctrl.fields.length) { html += '
'; for (const f of ctrl.fields) { html += '
' + esc(f.label || '') + '' + esc(String(f.value ?? '')) + '
'; } html += '
'; } if (ctrl.actions && ctrl.actions.length) { html += '
'; for (const a of ctrl.actions) { html += ''; } html += '
'; } card.innerHTML = html; _wireActions(card); container.appendChild(card); } } body.appendChild(container); } export function clearDashboard() { const body = document.getElementById('workspace-body'); if (body) body.innerHTML = ''; }