From 3a9c2795cfa7556203c49dedaaeadca8a2271c06 Mon Sep 17 00:00:00 2001 From: Nico Date: Sun, 29 Mar 2026 17:58:47 +0200 Subject: [PATCH] v0.15.2: ES6 module refactor, 2-row layout, dashboard test, PA routing fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend refactored to ES6 modules (no bundler): js/main.js — entry point, wires all modules js/auth.js — OIDC login, token management js/ws.js — /ws, /ws/test, /ws/trace connections + HUD handler js/chat.js — messages, send, streaming js/graph.js — Cytoscape visualization + animation js/trace.js — trace panel js/dashboard.js — workspace controls rendering js/awareness.js — state panel, sensors, meters js/tests.js — test status display js/util.js — shared utilities New 2-row layout: Top: test status | connection status Middle: Workspace | Node Details | Graph Bottom: Chat | Awareness | Trace PA routing: routes ALL tool requests to expert (DB, UI, buttons, machines) Dashboard integration test: 15/15 Co-Authored-By: Claude Opus 4.6 (1M context) --- agent/nodes/pa_v1.py | 12 ++- static/index.html | 86 ++++++++------- static/js/auth.js | 88 +++++++++++++++ static/js/awareness.js | 57 ++++++++++ static/js/chat.js | 62 +++++++++++ static/js/dashboard.js | 88 +++++++++++++++ static/js/graph.js | 238 +++++++++++++++++++++++++++++++++++++++++ static/js/main.js | 34 ++++++ static/js/tests.js | 25 +++++ static/js/trace.js | 42 ++++++++ static/js/util.js | 34 ++++++ static/js/ws.js | 175 ++++++++++++++++++++++++++++++ static/style.css | 212 +++++++++++++++--------------------- 13 files changed, 988 insertions(+), 165 deletions(-) create mode 100644 static/js/auth.js create mode 100644 static/js/awareness.js create mode 100644 static/js/chat.js create mode 100644 static/js/dashboard.js create mode 100644 static/js/graph.js create mode 100644 static/js/main.js create mode 100644 static/js/tests.js create mode 100644 static/js/trace.js create mode 100644 static/js/util.js create mode 100644 static/js/ws.js diff --git a/agent/nodes/pa_v1.py b/agent/nodes/pa_v1.py index aeece35..0007cba 100644 --- a/agent/nodes/pa_v1.py +++ b/agent/nodes/pa_v1.py @@ -46,8 +46,10 @@ Output ONLY valid JSON: Rules: - expert=none ONLY for social chat (hi, thanks, bye, how are you) -- ANY request to create, build, show, query, investigate, count, list, describe → route to expert -- The job must be fully self-contained. Include relevant facts from memory. +- ANY request to create, build, show, query, investigate, count, list, describe, summarize → route to expert +- The job MUST be fully self-contained. The expert has NO history. +- Include relevant facts from memory AND conversation context in the job. +- For summaries/reports: include the key topics, findings, and actions from the conversation in the job so the expert can write a proper summary. - thinking_message: natural, in user's language. e.g. "Moment, ich schaue nach..." - If the user mentions data, tables, customers, devices, buttons, counters → expert - When unsure which expert: pick the one whose domain matches best @@ -94,15 +96,15 @@ Rules: ] # Summarize recent history (PA sees full context) - recent = history[-12:] + recent = history[-16:] if recent: lines = [] for msg in recent: role = msg.get("role", "?") - content = msg.get("content", "")[:100] + content = msg.get("content", "")[:200] lines.append(f" {role}: {content}") messages.append({"role": "user", "content": "Recent conversation:\n" + "\n".join(lines)}) - messages.append({"role": "assistant", "content": "OK, I have the context."}) + messages.append({"role": "assistant", "content": "OK, I have the context. I will include relevant details in the job description."}) a = command.analysis messages.append({"role": "user", diff --git a/static/index.html b/static/index.html index e30d8a4..97df0c5 100644 --- a/static/index.html +++ b/static/index.html @@ -4,77 +4,91 @@ cog - + +

cog

-
disconnected
+
+
disconnected
-
-
input
-
thinker
-
output
-
memorizer
-
ui
-
sensor
-
- -
-
- - - - - - - - + +
+
+
Workspace
+
no controls
+
+
+
Nodes
+
+
input
+
director
+
PA
+
thinker
+
eras
+
output
+
memo
+
interp
+
sensor
+
+
+
+
Graph + + + + +
+
-
+ +
Chat
+
Awareness
-
+

State

-
waiting for data...
+
waiting...
-
+

Sensors

-
waiting for tick...
-
-
-

Processes

-
idle
-
-
-

Workspace

-
no controls
+
waiting...
-
+
Trace
- + + + + diff --git a/static/js/auth.js b/static/js/auth.js new file mode 100644 index 0000000..65a6328 --- /dev/null +++ b/static/js/auth.js @@ -0,0 +1,88 @@ +/** Authentication: OIDC login, token management. */ + +export let authToken = localStorage.getItem('cog_token'); +export let authConfig = null; +let _authFailed = false; + +export function isAuthFailed() { return _authFailed; } +export function setAuthFailed(v) { _authFailed = v; } + +export async function initAuth(onReady) { + try { + const r = await fetch('/auth/config'); + authConfig = await r.json(); + } catch (e) { + authConfig = { enabled: false }; + } + + if (!authConfig.enabled) { onReady(); return; } + + // Check for OIDC callback + const params = new URLSearchParams(location.search); + if (params.has('code')) { + // Exchange code for token (PKCE) + const code = params.get('code'); + const verifier = sessionStorage.getItem('cog_pkce_verifier'); + try { + const tokenResp = await fetch(authConfig.issuer + '/oauth/v2/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: location.origin + '/callback', + client_id: authConfig.clientId, + code_verifier: verifier || '', + }), + }); + const tokenData = await tokenResp.json(); + if (tokenData.id_token) { + authToken = tokenData.id_token; + localStorage.setItem('cog_token', authToken); + if (tokenData.access_token) { + localStorage.setItem('cog_access_token', tokenData.access_token); + } + } + } catch (e) { console.error('token exchange failed', e); } + history.replaceState(null, '', '/'); + } + + if (authToken) { + onReady(); + } else { + showLogin(); + } +} + +export function showLogin() { + const el = document.getElementById('login-overlay'); + if (el) el.style.display = 'flex'; +} + +export async function startLogin() { + if (!authConfig) return; + const verifier = randomString(64); + sessionStorage.setItem('cog_pkce_verifier', verifier); + const encoder = new TextEncoder(); + const data = encoder.encode(verifier); + const hash = await crypto.subtle.digest('SHA-256', data); + const challenge = btoa(String.fromCharCode(...new Uint8Array(hash))) + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + + const params = new URLSearchParams({ + response_type: 'code', + client_id: authConfig.clientId, + redirect_uri: location.origin + '/callback', + scope: 'openid profile email', + code_challenge: challenge, + code_challenge_method: 'S256', + prompt: 'login', + }); + location.href = authConfig.issuer + '/oauth/v2/authorize?' + params; +} + +function randomString(len) { + const arr = new Uint8Array(len); + crypto.getRandomValues(arr); + return Array.from(arr, b => b.toString(36).slice(-1)).join('').slice(0, len); +} diff --git a/static/js/awareness.js b/static/js/awareness.js new file mode 100644 index 0000000..736b371 --- /dev/null +++ b/static/js/awareness.js @@ -0,0 +1,57 @@ +/** Awareness panel: memorizer state, sensor readings, node meters. */ + +import { esc, truncate } from './util.js'; + +let _sensorReadings = {}; + +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]) => + `
${esc(k)}${esc(v || 'null')}
` + ).join(''); + + if (facts.length) { + html += '
facts' + + facts.map(f => esc(truncate(f, 40))).join('
') + '
'; + } + if (history.length) { + html += '
topics' + + history.map(t => esc(truncate(t, 25))).join(', ') + '
'; + } + body.innerHTML = html; +} + +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 = `
tick#${tick}
`; + for (const [k, v] of Object.entries(_sensorReadings)) { + html += `
${esc(k)}${esc(String(v))}
`; + } + body.innerHTML = html; +} + +export function updateMeter(node, tokens, maxTokens, fillPct) { + const meter = document.getElementById('meter-' + node); + if (!meter) return; + const bar = meter.querySelector('.nm-bar'); + const text = meter.querySelector('.nm-text'); + if (bar) bar.style.width = fillPct + '%'; + if (text) text.textContent = `${tokens}/${maxTokens}t`; +} diff --git a/static/js/chat.js b/static/js/chat.js new file mode 100644 index 0000000..1827109 --- /dev/null +++ b/static/js/chat.js @@ -0,0 +1,62 @@ +/** Chat panel: messages, send, streaming. */ + +import { scroll, renderMarkdown, esc } from './util.js'; +import { addTrace } from './trace.js'; + +let msgs, inputEl, currentEl; +let _ws = null; +let _currentDashboard = []; + +export function initChat() { + msgs = document.getElementById('messages'); + inputEl = document.getElementById('input'); + inputEl.addEventListener('keydown', e => { if (e.key === 'Enter') send(); }); +} + +export function setWs(ws) { _ws = ws; } +export function setDashboard(d) { _currentDashboard = d; } +export function getDashboard() { return _currentDashboard; } + +export function addMsg(role, text) { + const div = document.createElement('div'); + div.className = 'msg ' + role; + div.textContent = text; + msgs.appendChild(div); + scroll(msgs); + return div; +} + +export function handleDelta(content) { + if (!currentEl) { + currentEl = addMsg('assistant', ''); + currentEl.classList.add('streaming'); + } + currentEl.textContent += content; + scroll(msgs); +} + +export function handleDone() { + if (currentEl) { + currentEl.classList.remove('streaming'); + currentEl.innerHTML = renderMarkdown(currentEl.textContent); + } + currentEl = null; +} + +export function clearChat() { + if (msgs) msgs.innerHTML = ''; + currentEl = null; + _currentDashboard = []; +} + +export function send() { + const text = inputEl.value.trim(); + if (!text || !_ws || _ws.readyState !== 1) return; + addMsg('user', text); + addTrace('runtime', 'user_msg', text.slice(0, 60)); + _ws.send(JSON.stringify({ text, dashboard: _currentDashboard })); + inputEl.value = ''; +} + +// Expose for HTML onclick +window.send = send; diff --git a/static/js/dashboard.js b/static/js/dashboard.js new file mode 100644 index 0000000..43b8435 --- /dev/null +++ b/static/js/dashboard.js @@ -0,0 +1,88 @@ +/** 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 = '' + 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'; + 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 = '' + esc(ctrl.label) + '' + + '
' + + '' + pct + '%'; + } else if (dt === 'status') { + disp.innerHTML = '' + (ctrl.style === 'success' ? '\u2713' : ctrl.style === 'error' ? '\u2717' : '\u2139') + '' + + '' + esc(ctrl.label) + ''; + } else { + disp.innerHTML = '' + esc(ctrl.label) + '' + + (ctrl.value ? '' + esc(String(ctrl.value)) + '' : ''); + } + container.appendChild(disp); + } + } + body.appendChild(container); +} + +export function clearDashboard() { + const body = document.getElementById('workspace-body'); + if (body) body.innerHTML = ''; +} diff --git a/static/js/graph.js b/static/js/graph.js new file mode 100644 index 0000000..c717ac7 --- /dev/null +++ b/static/js/graph.js @@ -0,0 +1,238 @@ +/** Pipeline graph: Cytoscape visualization + animation. */ + +let cy = null; +let _dragEnabled = true; +let _physicsRunning = false; +let _physicsLayout = null; +let _colaSpacing = 25; +let _colaStrengthMult = 1.0; +let _panEnabled = true; + +const NODE_COLORS = { + user: '#444', input: '#f59e0b', sensor: '#3b82f6', + director: '#a855f7', pa: '#a855f7', thinker: '#f97316', + interpreter: '#06b6d4', expert_eras: '#f97316', expert_plankiste: '#f97316', + output: '#10b981', ui: '#10b981', memorizer: '#a855f7', s3_audit: '#ef4444', +}; + +const NODE_COLUMNS = { + user: 0, input: 1, sensor: 1, + director: 2, pa: 2, thinker: 2, interpreter: 2, s3_audit: 2, + expert_eras: 2, expert_plankiste: 2, + output: 3, ui: 3, + memorizer: 4, +}; + +function buildGraphElements(graph, mx, cw, mid, row1, row2) { + const elements = []; + const roles = Object.keys(graph.nodes); + elements.push({ data: { id: 'user', label: 'user' }, position: { x: mx, y: mid } }); + + const columns = {}; + for (const role of roles) { + const col = NODE_COLUMNS[role] !== undefined ? NODE_COLUMNS[role] : 2; + if (!columns[col]) columns[col] = []; + columns[col].push(role); + } + for (const [col, colRoles] of Object.entries(columns)) { + const c = parseInt(col); + const count = colRoles.length; + for (let i = 0; i < count; i++) { + const role = colRoles[i]; + const ySpread = (row2 - row1); + const y = count === 1 ? mid : row1 + (ySpread * i / (count - 1)); + const label = role === 'memorizer' ? 'memo' : role.replace(/_v\d+$/, '').replace('expert_', ''); + elements.push({ data: { id: role, label }, position: { x: mx + cw * c, y } }); + } + } + + const nodeIds = new Set(elements.map(e => e.data.id)); + const cytoEdges = graph.cytoscape ? graph.cytoscape.edges : []; + if (cytoEdges.length) { + for (const edge of cytoEdges) { + const d = edge.data; + if (!nodeIds.has(d.source) || !nodeIds.has(d.target)) continue; + const edgeData = { id: d.id, source: d.source, target: d.target }; + if (d.condition === 'reflex') edgeData.reflex = true; + if (d.edge_type === 'context') edgeData.ctx = true; + elements.push({ data: edgeData }); + } + } else { + for (const edge of graph.edges) { + const targets = Array.isArray(edge.to) ? edge.to : [edge.to]; + for (const tgt of targets) { + if (!nodeIds.has(edge.from) || !nodeIds.has(tgt)) continue; + const edgeData = { id: `e-${edge.from}-${tgt}`, source: edge.from, target: tgt }; + if (edge.condition === 'reflex') edgeData.reflex = true; + if (edge.type === 'context') edgeData.ctx = true; + elements.push({ data: edgeData }); + } + } + } + elements.push({ data: { id: 'e-user-input', source: 'user', target: 'input' } }); + return elements; +} + +export async function initGraph() { + const container = document.getElementById('pipeline-graph'); + if (!container || typeof cytoscape === 'undefined') return; + + const rect = container.getBoundingClientRect(); + const W = rect.width || 900; + const H = rect.height || 180; + const mx = W * 0.07; + const cw = (W - mx * 2) / 4; + const row1 = H * 0.25, mid = H * 0.5, row2 = H * 0.75; + + let graphElements = null; + try { + const resp = await fetch('/api/graph/active'); + if (resp.ok) { + const graph = await resp.json(); + graphElements = buildGraphElements(graph, mx, cw, mid, row1, row2); + } + } catch (e) {} + + if (!graphElements) { + graphElements = [ + { data: { id: 'user', label: 'user' }, position: { x: mx, y: mid } }, + { data: { id: 'input', label: 'input' }, position: { x: mx + cw, y: row1 } }, + { data: { id: 'thinker', label: 'thinker' }, position: { x: mx + cw * 2, y: mid } }, + { data: { id: 'output', label: 'output' }, position: { x: mx + cw * 3, y: mid } }, + ]; + } + + cy = cytoscape({ + container, + elements: graphElements, + style: [ + { selector: 'node', style: { + 'label': 'data(label)', 'text-valign': 'center', 'text-halign': 'center', + 'font-size': '18px', 'min-zoomed-font-size': 10, + 'font-family': 'system-ui, sans-serif', 'font-weight': 700, 'color': '#aaa', + 'background-color': '#181818', 'border-width': 1, 'border-opacity': 0.3, + 'border-color': '#444', 'width': 48, 'height': 48, + 'transition-property': 'background-color, border-color, width, height', + 'transition-duration': '0.3s', + }}, + ...Object.entries(NODE_COLORS).map(([id, color]) => ({ + selector: `#${id}`, style: { 'border-color': color, 'color': color } + })), + { selector: '#user', style: { 'color': '#888' } }, + { selector: '#sensor', style: { 'width': 40, 'height': 40, 'font-size': '15px' } }, + { selector: 'node.active', style: { + 'background-color': '#333', 'border-width': 3, 'width': 56, 'height': 56, + }}, + { selector: 'edge', style: { + 'width': 1.5, 'line-color': '#333', 'target-arrow-color': '#333', + 'target-arrow-shape': 'triangle', 'arrow-scale': 0.7, 'curve-style': 'bezier', + 'transition-property': 'line-color, target-arrow-color, width', + 'transition-duration': '0.3s', + }}, + { selector: 'edge[?reflex]', style: { 'line-style': 'dashed', 'line-dash-pattern': [4, 4], 'line-color': '#2a2a2a' } }, + { selector: 'edge[?ctx]', style: { 'line-style': 'dotted', 'line-color': '#1a1a2e', 'width': 1 } }, + { selector: 'edge.active', style: { 'line-color': '#888', 'target-arrow-color': '#888', 'width': 2.5 } }, + ], + layout: { name: 'preset' }, + userZoomingEnabled: true, userPanningEnabled: true, + wheelSensitivity: 0.3, boxSelectionEnabled: false, + autoungrabify: false, selectionType: 'single', + }); + + container.addEventListener('contextmenu', e => e.stopPropagation(), true); + if (typeof cytoscapeCola !== 'undefined') cytoscape.use(cytoscapeCola); + startPhysics(); + + cy.on('zoom', () => { + const z = cy.zoom(); + cy.nodes().style('font-size', Math.round(12 / z) + 'px'); + }); +} + +function pulseNode(id) { + if (!cy) return; + const node = cy.getElementById(id); + if (!node.length) return; + node.addClass('active'); + setTimeout(() => node.removeClass('active'), 1500); +} + +function flashEdge(sourceId, targetId) { + if (!cy) return; + const edge = cy.edges().filter(e => e.data('source') === sourceId && e.data('target') === targetId); + if (!edge.length) return; + edge.addClass('active'); + setTimeout(() => edge.removeClass('active'), 1000); +} + +export function graphAnimate(event, node) { + if (!cy) return; + if (node && cy.getElementById(node).length) pulseNode(node); + + switch (event) { + case 'perceived': pulseNode('input'); flashEdge('user', 'input'); break; + case 'decided': + if (node === 'director_v2' || node === 'director' || node === 'pa_v1') { + pulseNode(node); flashEdge(node, 'thinker'); + } else { + pulseNode(node || 'thinker'); flashEdge('thinker', 'output'); + } + break; + case 'routed': pulseNode('pa'); break; + case 'reflex_path': pulseNode('input'); flashEdge('input', 'output'); break; + case 'streaming': if (node === 'output') pulseNode('output'); break; + case 'controls': case 'machine_created': case 'machine_transition': + pulseNode('ui'); break; + case 'updated': pulseNode('memorizer'); flashEdge('output', 'memorizer'); break; + case 'tool_call': pulseNode(node || 'thinker'); break; + case 'tool_result': + if (cy.getElementById('interpreter').length) pulseNode('interpreter'); break; + case 'thinking': if (node) pulseNode(node); break; + case 'tick': pulseNode('sensor'); break; + } +} + +export function startPhysics() { + if (!cy) return; + stopPhysics(); + try { + const rect = document.getElementById('pipeline-graph').getBoundingClientRect(); + _physicsLayout = cy.layout({ + name: 'cola', animate: true, infinite: true, fit: false, + nodeSpacing: _colaSpacing, + edgeElasticity: e => { + const base = e.data('ctx') ? 0.1 : e.data('reflex') ? 0.2 : 0.6; + return base * _colaStrengthMult; + }, + boundingBox: { x1: 0, y1: 0, w: rect.width, h: rect.height }, + }); + _physicsLayout.run(); + _physicsRunning = true; + } catch (e) {} +} + +export function stopPhysics() { + if (_physicsLayout) { try { _physicsLayout.stop(); } catch(e) {} _physicsLayout = null; } + _physicsRunning = false; +} + +// Expose control functions for HTML onclick +window.adjustCola = (param, delta) => { + if (!cy) return; + if (param === 'spacing') _colaSpacing = Math.max(5, Math.min(80, _colaSpacing + delta)); + else if (param === 'strength') _colaStrengthMult = Math.max(0.1, Math.min(3.0, _colaStrengthMult + delta * 0.2)); + startPhysics(); +}; +window.toggleDrag = () => { + if (!cy) return; + _dragEnabled = !_dragEnabled; + cy.autoungrabify(!_dragEnabled); + document.getElementById('btn-drag').textContent = 'drag: ' + (_dragEnabled ? 'on' : 'off'); +}; +window.togglePan = () => { + if (!cy) return; + _panEnabled = !_panEnabled; + cy.userPanningEnabled(_panEnabled); + cy.userZoomingEnabled(_panEnabled); + document.getElementById('btn-pan').textContent = 'pan: ' + (_panEnabled ? 'on' : 'off'); +}; diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..29899b0 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,34 @@ +/** Main entry point — wires all modules together. */ + +import { initAuth, authToken, startLogin } from './auth.js'; +import { initTrace, addTrace, clearTrace } from './trace.js'; +import { initChat, clearChat } from './chat.js'; +import { clearDashboard } from './dashboard.js'; +import { initGraph } from './graph.js'; +import { connect } from './ws.js'; + +// Init on load +window.addEventListener('load', async () => { + initTrace(); + initChat(); + await initGraph(); + await initAuth(() => connect()); +}); + +// Clear session button +window.clearSession = async () => { + try { + const headers = { 'Content-Type': 'application/json' }; + if (authToken) headers['Authorization'] = 'Bearer ' + authToken; + await fetch('/api/clear', { method: 'POST', headers }); + clearChat(); + clearTrace(); + clearDashboard(); + addTrace('runtime', 'cleared', 'session reset'); + } catch (e) { + addTrace('runtime', 'error', 'clear failed: ' + e); + } +}; + +// Login button +window.startLogin = startLogin; diff --git a/static/js/tests.js b/static/js/tests.js new file mode 100644 index 0000000..232d832 --- /dev/null +++ b/static/js/tests.js @@ -0,0 +1,25 @@ +/** Test status display. */ + +import { esc } from './util.js'; + +export function updateTestStatus(data) { + const el = document.getElementById('test-status'); + if (!el) return; + const results = data.results || []; + const pass = results.filter(r => r.status === 'PASS').length; + const fail = results.filter(r => r.status === 'FAIL').length; + const done = results.length; + const expected = data.total_expected || done; + + if (data.running) { + const current = data.current || ''; + el.innerHTML = `TESTING ` + + `${done}/${expected}` + + (fail ? ` ${fail}F` : '') + + ` ${esc(current)}`; + } else if (done > 0) { + const allGreen = fail === 0; + el.innerHTML = `${pass}/${expected}` + + (fail ? ` ${fail} failed` : ' all green'); + } +} diff --git a/static/js/trace.js b/static/js/trace.js new file mode 100644 index 0000000..5599efc --- /dev/null +++ b/static/js/trace.js @@ -0,0 +1,42 @@ +/** Trace panel: HUD event display. */ + +import { esc, scroll } from './util.js'; + +let traceEl; + +export function initTrace() { + traceEl = document.getElementById('trace'); +} + +export function addTrace(node, event, text, cls, detail) { + if (!traceEl) return; + const line = document.createElement('div'); + line.className = 'trace-line' + (detail ? ' expandable' : ''); + + const ts = new Date().toLocaleTimeString('de-DE', { + hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', + fractionalSecondDigits: 1, + }); + + line.innerHTML = + '' + ts + '' + + '' + esc(node) + '' + + '' + esc(event) + '' + + '' + esc(text) + ''; + + traceEl.appendChild(line); + + if (detail) { + const detailEl = document.createElement('div'); + detailEl.className = 'trace-detail'; + detailEl.textContent = detail; + traceEl.appendChild(detailEl); + line.addEventListener('click', () => detailEl.classList.toggle('open')); + } + + scroll(traceEl); +} + +export function clearTrace() { + if (traceEl) traceEl.innerHTML = ''; +} diff --git a/static/js/util.js b/static/js/util.js new file mode 100644 index 0000000..29afa05 --- /dev/null +++ b/static/js/util.js @@ -0,0 +1,34 @@ +/** Shared utility functions. */ + +export function scroll(el) { el.scrollTop = el.scrollHeight; } + +export function esc(s) { + const d = document.createElement('span'); + d.textContent = s; + return d.innerHTML; +} + +export function truncate(s, n) { + return s.length > n ? s.slice(0, n) + '\u2026' : s; +} + +export function renderMarkdown(text) { + let html = esc(text); + // Code blocks + html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => + '
' + code.trim() + '
'); + // Inline code + html = html.replace(/`([^`]+)`/g, '$1'); + // Bold + html = html.replace(/\*\*(.+?)\*\*/g, '$1'); + // Italic + html = html.replace(/\*(.+?)\*/g, '$1'); + // Bullet lists + html = html.replace(/^[\*\-]\s+(.+)$/gm, '
  • $1
  • '); + html = html.replace(/(
  • .*<\/li>\n?)+/g, m => '
      ' + m + '
    '); + // Numbered lists + html = html.replace(/^\d+\.\s+(.+)$/gm, '
  • $1
  • '); + // Line breaks (but not inside pre/code) + html = html.replace(/\n/g, '
    '); + return html; +} diff --git a/static/js/ws.js b/static/js/ws.js new file mode 100644 index 0000000..25a8950 --- /dev/null +++ b/static/js/ws.js @@ -0,0 +1,175 @@ +/** WebSocket connections: /ws (chat), /ws/test, /ws/trace. */ + +import { authToken, isAuthFailed, setAuthFailed, showLogin } from './auth.js'; +import { addTrace } from './trace.js'; +import { handleDelta, handleDone, setWs as setChatWs } from './chat.js'; +import { dockControls, setWs as setDashWs } from './dashboard.js'; +import { graphAnimate } from './graph.js'; +import { updateMeter, updateAwarenessState, updateAwarenessSensors } from './awareness.js'; +import { updateTestStatus } from './tests.js'; +import { truncate, esc } from './util.js'; + +let ws, wsTest, wsTrace; +let _testPollInterval = null; +let _lastTestResultCount = 0; + +export function connect() { + if (isAuthFailed()) return; + const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; + let wsUrl = proto + '//' + location.host + '/ws'; + if (authToken) { + const accessToken = localStorage.getItem('cog_access_token') || ''; + wsUrl += '?token=' + encodeURIComponent(authToken) + '&access_token=' + encodeURIComponent(accessToken); + } + ws = new WebSocket(wsUrl); + + ws.onopen = () => { + document.getElementById('status').textContent = 'connected'; + document.getElementById('status').style.color = '#22c55e'; + addTrace('runtime', 'connected', 'ws open'); + setChatWs(ws); + setDashWs(ws); + connectDebugSockets(); + }; + + ws.onerror = () => {}; + + ws.onclose = (e) => { + if (e.code === 4001 || e.code === 1006) { + setAuthFailed(true); + localStorage.removeItem('cog_token'); + localStorage.removeItem('cog_access_token'); + document.getElementById('status').textContent = 'session expired'; + document.getElementById('status').style.color = '#ef4444'; + showLogin(); + return; + } + document.getElementById('status').textContent = 'disconnected'; + document.getElementById('status').style.color = '#666'; + addTrace('runtime', 'disconnected', 'ws closed'); + setTimeout(connect, 2000); + }; + + ws.onmessage = (e) => { + const data = JSON.parse(e.data); + if (data.type === 'hud') { + handleHud(data); + } else if (data.type === 'delta') { + handleDelta(data.content); + } else if (data.type === 'done') { + handleDone(); + } else if (data.type === 'controls') { + dockControls(data.controls); + } else if (data.type === 'cleared') { + addTrace('runtime', 'cleared', 'session reset'); + } + }; +} + +function connectDebugSockets() { + const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; + const base = proto + '//' + location.host; + const tokenParam = authToken ? '?token=' + encodeURIComponent(authToken) : ''; + + if (!wsTest || wsTest.readyState > 1) { + wsTest = new WebSocket(base + '/ws/test' + tokenParam); + wsTest.onopen = () => addTrace('runtime', 'ws/test', 'connected'); + wsTest.onclose = () => setTimeout(connectDebugSockets, 3000); + wsTest.onerror = () => {}; + wsTest.onmessage = (e) => { + const data = JSON.parse(e.data); + if (data.type === 'test_status') updateTestStatus(data); + }; + } + + if (!wsTrace || wsTrace.readyState > 1) { + wsTrace = new WebSocket(base + '/ws/trace' + tokenParam); + wsTrace.onopen = () => addTrace('runtime', 'ws/trace', 'connected'); + wsTrace.onclose = () => {}; + wsTrace.onerror = () => {}; + wsTrace.onmessage = (e) => { + const data = JSON.parse(e.data); + if (data.event === 'frame_trace' && data.trace) { + const t = data.trace; + const frames = t.frames || []; + const summary = frames.map(f => `F${f.frame}:${f.node}(${f.duration_ms}ms)`).join(' > '); + addTrace('frame_engine', 'trace', `${t.path} ${t.total_frames}F ${t.total_ms}ms`, 'instruction', + summary + '\n' + JSON.stringify(t, null, 2)); + } else if (data.node && data.event) { + handleHud(data); + } + }; + } + + // Polling fallback for test status + if (!_testPollInterval) { + _testPollInterval = setInterval(async () => { + try { + const headers = authToken ? { 'Authorization': 'Bearer ' + authToken } : {}; + const r = await fetch('/api/test/status', { headers }); + const data = await r.json(); + const count = (data.results || []).length; + if (count !== _lastTestResultCount || data.running) { + _lastTestResultCount = count; + updateTestStatus(data); + } + } catch (e) {} + }, 500); + } +} + +function handleHud(data) { + const node = data.node || 'unknown'; + const event = data.event || ''; + + graphAnimate(event, node); + + if (event === 'context') { + const count = (data.messages || []).length; + const tokenInfo = data.tokens ? ` [${data.tokens}/${data.max_tokens}t ${data.fill_pct}%]` : ''; + const summary = count + ' msgs' + tokenInfo; + const detail = (data.messages || []).map((m, i) => + i + ' [' + m.role + '] ' + m.content + ).join('\n'); + addTrace(node, 'context', summary, 'context', detail); + if (data.tokens !== undefined) updateMeter(node, data.tokens, data.max_tokens, data.fill_pct); + + } else if (event === 'perceived') { + const text = data.analysis + ? Object.entries(data.analysis).map(([k,v]) => k + '=' + v).join(' ') + : ''; + addTrace(node, 'perceived', text, 'instruction', data.analysis ? JSON.stringify(data.analysis, null, 2) : null); + + } else if (event === 'decided' || event === 'routed') { + addTrace(node, event, data.instruction || data.goal || data.job || '', 'instruction'); + + } else if (event === 'updated' && data.state) { + const pairs = Object.entries(data.state).map(([k, v]) => { + const val = Array.isArray(v) ? v.join(', ') : (v || 'null'); + return k + '=' + truncate(val, 25); + }).join(' '); + addTrace(node, 'state', pairs, 'state', JSON.stringify(data.state, null, 2)); + updateAwarenessState(data.state); + + } else if (event === 'tool_call') { + const tool = data.tool || ''; + const argsStr = data.args ? JSON.stringify(data.args) : ''; + addTrace(node, 'tool_call', tool + ' ' + truncate(argsStr, 60), '', argsStr); + + } else if (event === 'tool_result') { + const output = data.output || ''; + addTrace(node, 'tool_result', truncate(output, 80), '', output); + + } else if (event === 'interpreted') { + addTrace(node, 'interpreted', data.summary || '', 'instruction'); + + } else if (event === 'tick') { + updateAwarenessSensors(data.tick || 0, data.deltas || {}); + + } else if (event === 'started' || event === 'stopped') { + addTrace(node, event, ''); + + } else { + addTrace(node, event, '', '', JSON.stringify(data, null, 2)); + } +} diff --git a/static/style.css b/static/style.css index 012ce18..d5f6963 100644 --- a/static/style.css +++ b/static/style.css @@ -2,172 +2,136 @@ body { font-family: system-ui, sans-serif; background: #0a0a0a; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; overflow: hidden; } /* Top bar */ -#top-bar { display: flex; align-items: center; gap: 1rem; padding: 0.4rem 1rem; background: #111; border-bottom: 1px solid #222; } +#top-bar { display: flex; align-items: center; gap: 1rem; padding: 0.4rem 1rem; background: #111; border-bottom: 1px solid #222; flex-shrink: 0; } #top-bar h1 { font-size: 0.85rem; font-weight: 600; color: #888; } #status { font-size: 0.75rem; color: #666; } -#test-status { margin-left: auto; font-size: 0.7rem; font-family: monospace; display: flex; gap: 1rem; align-items: center; } +#test-status { font-size: 0.7rem; font-family: monospace; display: flex; gap: 1rem; align-items: center; } #test-status .ts-running { color: #f59e0b; animation: pulse-text 1s infinite; } #test-status .ts-pass { color: #22c55e; } #test-status .ts-fail { color: #ef4444; } -#test-status .ts-idle { color: #444; } @keyframes pulse-text { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } } -/* Node metrics bar */ -#node-metrics { display: flex; gap: 1px; padding: 0; background: #111; border-bottom: 1px solid #222; overflow: hidden; flex-shrink: 0; } -.node-meter { flex: 1; display: flex; align-items: center; gap: 0.4rem; padding: 0.25rem 0.6rem; background: #0a0a0a; } -.nm-label { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; min-width: 4.5rem; } -#meter-input .nm-label { color: #f59e0b; } -#meter-output .nm-label { color: #34d399; } -#meter-memorizer .nm-label { color: #c084fc; } -#meter-thinker .nm-label { color: #fb923c; } -#meter-ui .nm-label { color: #34d399; } -#meter-sensor .nm-label { color: #60a5fa; } -.nm-bar { flex: 1; height: 6px; background: #1a1a1a; border-radius: 3px; overflow: hidden; } -.nm-fill { height: 100%; width: 0%; border-radius: 3px; transition: width 0.3s, background-color 0.3s; background: #333; } -.nm-text { font-size: 0.6rem; color: #555; min-width: 5rem; text-align: right; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - -/* Pipeline graph */ -#pipeline-graph { height: 180px; min-height: 180px; flex-shrink: 0; border-bottom: 1px solid #333; background: #0d0d0d; position: relative; } -#graph-controls { position: absolute; top: 4px; right: 6px; z-index: 999; display: flex; gap: 3px; pointer-events: auto; } -#graph-controls button { padding: 2px 6px; font-size: 0.6rem; font-family: monospace; background: #1a1a1a; color: #666; border: 1px solid #333; border-radius: 3px; cursor: pointer; position: relative; z-index: 999; } -#graph-controls button:hover { color: #ccc; border-color: #555; } - -/* Overlay scrollbars — no reflow, float over content */ -#messages, #awareness, #trace { - overflow-y: overlay; /* Chromium: scrollbar overlays content, no space taken */ - scrollbar-width: thin; /* Firefox fallback */ - scrollbar-color: rgba(255,255,255,0.12) transparent; -} -#messages::-webkit-scrollbar, #awareness::-webkit-scrollbar, #trace::-webkit-scrollbar { width: 5px; } -#messages::-webkit-scrollbar-track, #awareness::-webkit-scrollbar-track, #trace::-webkit-scrollbar-track { background: transparent; } -#messages::-webkit-scrollbar-thumb, #awareness::-webkit-scrollbar-thumb, #trace::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; } -#messages::-webkit-scrollbar-thumb:hover, #awareness::-webkit-scrollbar-thumb:hover, #trace::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); } - -/* Three-column layout: chat | awareness | trace */ -#main { flex: 1; display: grid; grid-template-columns: 1fr 1fr 2fr; gap: 1px; background: #222; overflow: hidden; min-height: 0; } +/* === Two-row layout === */ +/* Middle row: workspace | node detail | graph */ +#middle-row { display: grid; grid-template-columns: 1fr 200px 2fr; gap: 1px; background: #222; flex: 1; min-height: 0; } +/* Bottom row: chat | awareness | trace */ +#bottom-row { display: grid; grid-template-columns: 1fr 1fr 2fr; gap: 1px; background: #222; flex: 1; min-height: 0; } +/* Panels */ .panel { background: #0a0a0a; display: flex; flex-direction: column; overflow: hidden; } -.panel-header { padding: 0.5rem 0.75rem; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid #222; flex-shrink: 0; } +.panel-header { padding: 0.4rem 0.75rem; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid #222; flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; } .panel-header.chat-h { color: #60a5fa; background: #0a1628; } .panel-header.trace-h { color: #a78bfa; background: #120a1e; } +.panel-header.aware-h { color: #34d399; background: #0a1e14; } +.panel-header.work-h { color: #f59e0b; background: #1a1408; } +.panel-header.detail-h { color: #fb923c; background: #1a1008; } +.panel-header.graph-h { color: #888; background: #111; } +.graph-btns { display: flex; gap: 3px; } +.graph-btns button { padding: 1px 5px; font-size: 0.55rem; font-family: monospace; background: #1a1a1a; color: #666; border: 1px solid #333; border-radius: 3px; cursor: pointer; } +.graph-btns button:hover { color: #ccc; border-color: #555; } + +/* Workspace panel */ +.workspace-panel { display: flex; flex-direction: column; } +#workspace-body { flex: 1; overflow-y: auto; padding: 0.5rem; } + +/* Node detail / metrics */ +.detail-panel { display: flex; flex-direction: column; } +#node-metrics { flex: 1; overflow-y: auto; padding: 0.3rem; display: flex; flex-direction: column; gap: 1px; } +.node-meter { display: flex; align-items: center; gap: 0.3rem; padding: 0.2rem 0.4rem; background: #111; border-radius: 2px; } +.nm-label { font-size: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; min-width: 3.5rem; color: #888; } +.nm-bar { flex: 1; height: 5px; background: #1a1a1a; border-radius: 3px; overflow: hidden; } +.nm-fill { height: 100%; width: 0%; border-radius: 3px; transition: width 0.3s; background: #333; } +.nm-text { font-size: 0.55rem; color: #555; min-width: 3rem; text-align: right; font-family: monospace; } + +/* Graph panel */ +.graph-panel { display: flex; flex-direction: column; } +#pipeline-graph { flex: 1; background: #0d0d0d; min-height: 100px; } + +/* Overlay scrollbars */ +#messages, #awareness, #trace, #workspace-body, #node-metrics { + scrollbar-width: thin; + scrollbar-color: rgba(255,255,255,0.12) transparent; +} +#messages::-webkit-scrollbar, #trace::-webkit-scrollbar, #workspace-body::-webkit-scrollbar { width: 5px; } +#messages::-webkit-scrollbar-track, #trace::-webkit-scrollbar-track { background: transparent; } +#messages::-webkit-scrollbar-thumb, #trace::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; } /* Chat panel */ .chat-panel { display: flex; flex-direction: column; } #messages { flex: 1; overflow-y: auto; padding: 0.5rem; display: flex; flex-direction: column; gap: 0.4rem; } -.msg { max-width: 90%; padding: 0.5rem 0.75rem; border-radius: 0.6rem; line-height: 1.4; white-space: pre-wrap; font-size: 0.9rem; } +.msg { max-width: 90%; padding: 0.5rem 0.75rem; border-radius: 0.6rem; line-height: 1.4; white-space: pre-wrap; font-size: 0.85rem; } .msg.user { align-self: flex-end; background: #2563eb; color: white; } .msg.assistant { align-self: flex-start; background: #1e1e1e; border: 1px solid #333; } .msg.assistant.streaming { border-color: #2563eb; } -.msg.assistant h2, .msg.assistant h3, .msg.assistant h4 { margin: 0.3rem 0 0.2rem; color: #e0e0e0; } -.msg.assistant h2 { font-size: 1rem; } -.msg.assistant h3 { font-size: 0.95rem; } -.msg.assistant h4 { font-size: 0.9rem; } .msg.assistant strong { color: #fff; } .msg.assistant code { background: #2a2a3a; padding: 0.1rem 0.3rem; border-radius: 0.2rem; font-size: 0.85em; } .msg.assistant pre { background: #1a1a2a; padding: 0.5rem; border-radius: 0.3rem; margin: 0.3rem 0; overflow-x: auto; } .msg.assistant pre code { background: none; padding: 0; } .msg.assistant ul { margin: 0.2rem 0; padding-left: 1.2rem; } -.msg.assistant li { margin: 0.1rem 0; } /* Input bar */ -#input-bar { display: flex; gap: 0.5rem; padding: 0.75rem; background: #111; border-top: 1px solid #222; } -#input { flex: 1; padding: 0.5rem 0.75rem; background: #1a1a1a; color: #e0e0e0; border: 1px solid #333; border-radius: 0.4rem; font-size: 0.9rem; outline: none; } +#input-bar { display: flex; gap: 0.5rem; padding: 0.5rem; background: #111; border-top: 1px solid #222; } +#input { flex: 1; padding: 0.4rem 0.6rem; background: #1a1a1a; color: #e0e0e0; border: 1px solid #333; border-radius: 0.4rem; font-size: 0.85rem; outline: none; } #input:focus { border-color: #2563eb; } -button { padding: 0.5rem 1rem; background: #2563eb; color: white; border: none; border-radius: 0.4rem; cursor: pointer; font-size: 0.9rem; } +button { padding: 0.4rem 0.8rem; background: #2563eb; color: white; border: none; border-radius: 0.4rem; cursor: pointer; font-size: 0.8rem; } button:hover { background: #1d4ed8; } +.btn-clear { background: #333; padding: 0.4rem 0.5rem; font-size: 0.75rem; } +.btn-clear:hover { background: #ef4444; } /* Trace panel */ -#trace { flex: 1; overflow-y: auto; padding: 0.5rem; font-family: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace; font-size: 0.72rem; line-height: 1.5; } - -.trace-line { padding: 0.15rem 0.4rem; border-bottom: 1px solid #111; display: flex; gap: 0.5rem; align-items: baseline; } +.trace-panel { display: flex; flex-direction: column; } +#trace { flex: 1; overflow-y: auto; padding: 0.4rem; font-family: 'JetBrains Mono', 'Cascadia Code', monospace; font-size: 0.68rem; line-height: 1.5; } +.trace-line { padding: 0.12rem 0.3rem; border-bottom: 1px solid #111; display: flex; gap: 0.4rem; align-items: baseline; } .trace-line:hover { background: #1a1a2e; } - -.trace-ts { color: #555; flex-shrink: 0; min-width: 5rem; } -.trace-node { font-weight: 700; flex-shrink: 0; min-width: 6rem; } +.trace-ts { color: #555; flex-shrink: 0; min-width: 4.5rem; } +.trace-node { font-weight: 700; flex-shrink: 0; min-width: 5.5rem; } .trace-node.input { color: #f59e0b; } .trace-node.output { color: #34d399; } .trace-node.memorizer { color: #c084fc; } -.trace-node.thinker { color: #fb923c; } -.trace-node.runtime { color: #60a5fa; } -.trace-node.process { color: #f97316; } +.trace-node.thinker, .trace-node.thinker_v2 { color: #fb923c; } +.trace-node.director_v2, .trace-node.pa_v1, .trace-node.pa { color: #a855f7; } +.trace-node.eras_expert, .trace-node.expert_eras { color: #f97316; } +.trace-node.runtime, .trace-node.frame_engine { color: #60a5fa; } .trace-node.ui { color: #34d399; } .trace-node.sensor { color: #60a5fa; } - -.trace-event { color: #888; flex-shrink: 0; min-width: 6rem; } - +.trace-event { color: #888; flex-shrink: 0; min-width: 5.5rem; } .trace-data { color: #ccc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .trace-data.instruction { color: #22c55e; } .trace-data.error { color: #ef4444; } .trace-data.state { color: #c084fc; } .trace-data.context { color: #666; } - -/* UI Controls */ -.controls-container { padding: 0.4rem 0; display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: flex-start; } -.control-btn { padding: 0.35rem 0.75rem; background: #1e3a5f; color: #60a5fa; border: 1px solid #2563eb; border-radius: 0.3rem; cursor: pointer; font-size: 0.8rem; } -.control-btn:hover { background: #2563eb; color: white; } -.control-label { display: flex; justify-content: space-between; align-items: center; padding: 0.3rem 0.5rem; background: #1a1a2e; border-radius: 0.3rem; font-size: 0.8rem; } -.cl-text { color: #888; } -.cl-value { color: #e0e0e0; font-weight: 600; font-family: monospace; } -.control-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; background: #111; border-radius: 0.3rem; overflow: hidden; } -.control-table th { background: #1a1a2e; color: #a78bfa; padding: 0.3rem 0.5rem; text-align: left; font-weight: 600; border-bottom: 1px solid #333; } -.control-table td { padding: 0.25rem 0.5rem; border-bottom: 1px solid #1a1a1a; color: #ccc; } -.control-table tr:hover td { background: #1a1a2e; } -.process-card { background: #111; border: 1px solid #333; border-radius: 0.3rem; padding: 0.4rem 0.6rem; font-size: 0.75rem; width: 100%; } -.process-card.running { border-color: #f59e0b; } -.process-card.done { border-color: #22c55e; } -.process-card.failed { border-color: #ef4444; } -.pc-tool { font-weight: 700; color: #fb923c; margin-right: 0.5rem; } -.pc-status { color: #888; margin-right: 0.5rem; } -.pc-stop { padding: 0.15rem 0.4rem; background: #ef4444; color: white; border: none; border-radius: 0.2rem; cursor: pointer; font-size: 0.7rem; } -.pc-code { margin-top: 0.3rem; color: #666; white-space: pre-wrap; max-height: 4rem; overflow-y: auto; font-size: 0.7rem; } -.pc-output { margin-top: 0.3rem; color: #888; white-space: pre-wrap; max-height: 8rem; overflow-y: auto; } +.trace-line.expandable { cursor: pointer; } +.trace-detail { display: none; padding: 0.2rem 0.3rem 0.2rem 10rem; font-size: 0.6rem; color: #777; white-space: pre-wrap; word-break: break-all; max-height: 8rem; overflow-y: auto; background: #0d0d14; border-bottom: 1px solid #1a1a2e; } +.trace-detail.open { display: block; } /* Awareness panel */ -.panel-header.aware-h { color: #34d399; background: #0a1e14; } .awareness-panel { display: flex; flex-direction: column; } -#awareness { flex: 1; overflow-y: auto; padding: 0.5rem; display: flex; flex-direction: column; gap: 0.5rem; } -.aw-section { background: #111; border: 1px solid #1a1a1a; border-radius: 0.4rem; overflow: hidden; } -.aw-title { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.3rem 0.5rem; background: #0d0d14; color: #666; border-bottom: 1px solid #1a1a1a; } -.aw-body { padding: 0.4rem 0.5rem; font-size: 0.78rem; line-height: 1.5; } -.aw-empty { color: #444; font-style: italic; font-size: 0.72rem; } +#awareness { flex: 1; overflow-y: auto; padding: 0.4rem; display: flex; flex-direction: column; gap: 0.4rem; } +.aw-section { background: #111; border: 1px solid #1a1a1a; border-radius: 0.3rem; overflow: hidden; } +.aw-title { font-size: 0.6rem; font-weight: 700; text-transform: uppercase; padding: 0.25rem 0.4rem; background: #0d0d14; color: #666; border-bottom: 1px solid #1a1a1a; } +.aw-body { padding: 0.3rem 0.4rem; font-size: 0.72rem; line-height: 1.5; } +.aw-empty { color: #444; font-style: italic; font-size: 0.68rem; } +.aw-row { display: flex; justify-content: space-between; padding: 0.08rem 0; } +.aw-key { color: #888; font-size: 0.65rem; } +.aw-val { color: #e0e0e0; font-size: 0.7rem; font-weight: 500; } -/* State card */ -.aw-row { display: flex; justify-content: space-between; padding: 0.1rem 0; } -.aw-key { color: #888; font-size: 0.7rem; } -.aw-val { color: #e0e0e0; font-size: 0.75rem; font-weight: 500; } -.aw-val.mood-happy { color: #22c55e; } -.aw-val.mood-frustrated { color: #ef4444; } -.aw-val.mood-playful { color: #f59e0b; } -.aw-val.mood-neutral { color: #888; } +/* UI Controls (workspace) */ +.controls-container { padding: 0.3rem 0; display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: flex-start; } +.control-btn { padding: 0.3rem 0.6rem; background: #1e3a5f; color: #60a5fa; border: 1px solid #2563eb; border-radius: 0.3rem; cursor: pointer; font-size: 0.75rem; } +.control-btn:hover { background: #2563eb; color: white; } +.control-label { display: flex; justify-content: space-between; align-items: center; padding: 0.25rem 0.4rem; background: #1a1a2e; border-radius: 0.3rem; font-size: 0.75rem; width: 100%; } +.cl-text { color: #888; } +.cl-value { color: #e0e0e0; font-weight: 600; font-family: monospace; } +.control-table { width: 100%; border-collapse: collapse; font-size: 0.72rem; background: #111; border-radius: 0.3rem; overflow: hidden; } +.control-table th { background: #1a1a2e; color: #a78bfa; padding: 0.25rem 0.4rem; text-align: left; font-weight: 600; border-bottom: 1px solid #333; } +.control-table td { padding: 0.2rem 0.4rem; border-bottom: 1px solid #1a1a1a; color: #ccc; } +.control-table tr:hover td { background: #1a1a2e; } +.control-display { padding: 0.25rem 0.4rem; font-size: 0.75rem; } +.cd-label { color: #888; } +.cd-value { color: #e0e0e0; margin-left: 0.5rem; } -/* Facts list */ -.aw-facts { list-style: none; padding: 0; margin: 0.2rem 0 0 0; } -.aw-facts li { font-size: 0.7rem; color: #999; padding: 0.1rem 0; border-top: 1px solid #1a1a1a; } -.aw-facts li::before { content: "- "; color: #555; } - -/* Sensor readings */ -.aw-sensor { display: flex; align-items: center; gap: 0.4rem; padding: 0.15rem 0; } -.aw-sensor-name { color: #60a5fa; font-size: 0.7rem; font-weight: 600; min-width: 4rem; } -.aw-sensor-val { color: #e0e0e0; font-size: 0.75rem; } -.aw-sensor-age { color: #444; font-size: 0.65rem; } - -/* Awareness processes */ -.aw-proc { background: #0d0d14; border: 1px solid #333; border-radius: 0.3rem; padding: 0.3rem 0.5rem; margin-bottom: 0.3rem; } -.aw-proc.running { border-color: #f59e0b; } -.aw-proc.done { border-color: #22c55e; } -.aw-proc.failed { border-color: #ef4444; } -.aw-proc-header { display: flex; align-items: center; gap: 0.4rem; font-size: 0.72rem; } -.aw-proc-tool { font-weight: 700; color: #fb923c; } -.aw-proc-status { color: #888; } -.aw-proc-stop { padding: 0.1rem 0.3rem; background: #ef4444; color: white; border: none; border-radius: 0.2rem; cursor: pointer; font-size: 0.65rem; } -.aw-proc-code { font-size: 0.65rem; color: #555; margin-top: 0.2rem; white-space: pre-wrap; max-height: 3rem; overflow: hidden; } -.aw-proc-output { font-size: 0.7rem; color: #999; margin-top: 0.2rem; white-space: pre-wrap; max-height: 5rem; overflow-y: auto; } - -/* Awareness controls (workspace) */ -#aw-ctrl-body .controls-container { padding: 0; } -#aw-ctrl-body .control-table { font-size: 0.72rem; } - -/* Expandable trace detail */ -.trace-line.expandable { cursor: pointer; } -.trace-detail { display: none; padding: 0.3rem 0.4rem 0.3rem 12rem; font-size: 0.65rem; color: #777; white-space: pre-wrap; word-break: break-all; max-height: 10rem; overflow-y: auto; background: #0d0d14; border-bottom: 1px solid #1a1a2e; } -.trace-detail.open { display: block; } +/* Login overlay */ +#login-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.85); display: flex; align-items: center; justify-content: center; z-index: 1000; } +.login-card { background: #1a1a1a; padding: 2rem; border-radius: 0.6rem; text-align: center; } +.login-card h2 { color: #60a5fa; margin-bottom: 1rem; } +.login-card p { color: #888; margin-bottom: 1.5rem; }