v0.15.2: ES6 module refactor, 2-row layout, dashboard test, PA routing fix
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) <noreply@anthropic.com>
This commit is contained in:
parent
fda0d7cfce
commit
3a9c2795cf
@ -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",
|
||||
|
||||
@ -4,77 +4,91 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>cog</title>
|
||||
<link rel="stylesheet" href="/static/style.css?v=14.5">
|
||||
<link rel="stylesheet" href="/static/style.css?v=15">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.28.1/cytoscape.min.js"></script>
|
||||
<script src="https://unpkg.com/webcola@3.4.0/WebCola/cola.min.js"></script>
|
||||
<script src="https://unpkg.com/cytoscape-cola@2.5.1/cytoscape-cola.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Top bar -->
|
||||
<div id="top-bar">
|
||||
<h1>cog</h1>
|
||||
<div id="status">disconnected</div>
|
||||
<div id="test-status"></div>
|
||||
<div style="flex:1"></div>
|
||||
<div id="status">disconnected</div>
|
||||
</div>
|
||||
|
||||
<!-- Middle row: Workspace | Node Detail | Graph -->
|
||||
<div id="middle-row">
|
||||
<div class="panel workspace-panel">
|
||||
<div class="panel-header work-h">Workspace</div>
|
||||
<div id="workspace-body"><span class="aw-empty">no controls</span></div>
|
||||
</div>
|
||||
<div class="panel detail-panel">
|
||||
<div class="panel-header detail-h">Nodes</div>
|
||||
<div id="node-metrics">
|
||||
<div class="node-meter" id="meter-input"><span class="nm-label">input</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text">—</span></div>
|
||||
<div class="node-meter" id="meter-thinker"><span class="nm-label">thinker</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text">—</span></div>
|
||||
<div class="node-meter" id="meter-output"><span class="nm-label">output</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text">—</span></div>
|
||||
<div class="node-meter" id="meter-memorizer"><span class="nm-label">memorizer</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text">—</span></div>
|
||||
<div class="node-meter" id="meter-ui"><span class="nm-label">ui</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text">—</span></div>
|
||||
<div class="node-meter" id="meter-sensor"><span class="nm-label">sensor</span><span class="nm-text" style="flex:1">—</span></div>
|
||||
<div class="node-meter" id="meter-input"><span class="nm-label">input</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
|
||||
<div class="node-meter" id="meter-director_v2"><span class="nm-label">director</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
|
||||
<div class="node-meter" id="meter-pa_v1"><span class="nm-label">PA</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
|
||||
<div class="node-meter" id="meter-thinker"><span class="nm-label">thinker</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
|
||||
<div class="node-meter" id="meter-eras_expert"><span class="nm-label">eras</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
|
||||
<div class="node-meter" id="meter-output"><span class="nm-label">output</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
|
||||
<div class="node-meter" id="meter-memorizer"><span class="nm-label">memo</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
|
||||
<div class="node-meter" id="meter-interpreter"><span class="nm-label">interp</span><div class="nm-bar"><div class="nm-fill"></div></div><span class="nm-text"></span></div>
|
||||
<div class="node-meter" id="meter-sensor"><span class="nm-label">sensor</span><span class="nm-text" style="flex:1"></span></div>
|
||||
</div>
|
||||
|
||||
<div id="pipeline-graph">
|
||||
<div id="graph-controls">
|
||||
<button onclick="toggleDrag()" id="btn-drag" title="Toggle node dragging">drag: on</button>
|
||||
<button onclick="togglePan()" id="btn-pan" title="Toggle viewport panning">pan: on</button>
|
||||
<button onclick="adjustCola('spacing', -5)" title="Tighter">tight</button>
|
||||
<button onclick="adjustCola('spacing', 5)" title="Looser">loose</button>
|
||||
<button onclick="adjustCola('strength', -1)" title="Weaker edges">weak</button>
|
||||
<button onclick="adjustCola('strength', 1)" title="Stronger edges">strong</button>
|
||||
<button onclick="cy && cy.fit(10)" title="Fit to view">fit</button>
|
||||
<button onclick="copyGraphConfig()" id="btn-copy" title="Copy full settings JSON">copy</button>
|
||||
</div>
|
||||
<div class="panel graph-panel">
|
||||
<div class="panel-header graph-h">Graph
|
||||
<span class="graph-btns">
|
||||
<button onclick="toggleDrag()" id="btn-drag" title="Drag">drag</button>
|
||||
<button onclick="togglePan()" id="btn-pan" title="Pan">pan</button>
|
||||
</span>
|
||||
</div>
|
||||
<div id="pipeline-graph"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<!-- Bottom row: Chat | Awareness | Trace -->
|
||||
<div id="bottom-row">
|
||||
<div class="panel chat-panel">
|
||||
<div class="panel-header chat-h">Chat</div>
|
||||
<div id="messages"></div>
|
||||
<div id="input-bar">
|
||||
<input id="input" placeholder="Type a message..." autocomplete="off">
|
||||
<button onclick="send()">Send</button>
|
||||
<button onclick="clearSession()" class="btn-clear" title="Clear session">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel awareness-panel">
|
||||
<div class="panel-header aware-h">Awareness</div>
|
||||
<div id="awareness">
|
||||
<section id="awareness-state" class="aw-section">
|
||||
<section class="aw-section">
|
||||
<h3 class="aw-title">State</h3>
|
||||
<div class="aw-body" id="aw-state-body"><span class="aw-empty">waiting for data...</span></div>
|
||||
<div class="aw-body" id="aw-state-body"><span class="aw-empty">waiting...</span></div>
|
||||
</section>
|
||||
<section id="awareness-sensors" class="aw-section">
|
||||
<section class="aw-section">
|
||||
<h3 class="aw-title">Sensors</h3>
|
||||
<div class="aw-body" id="aw-sensors-body"><span class="aw-empty">waiting for tick...</span></div>
|
||||
</section>
|
||||
<section id="awareness-processes" class="aw-section">
|
||||
<h3 class="aw-title">Processes</h3>
|
||||
<div class="aw-body" id="aw-proc-body"><span class="aw-empty">idle</span></div>
|
||||
</section>
|
||||
<section id="awareness-controls" class="aw-section">
|
||||
<h3 class="aw-title">Workspace</h3>
|
||||
<div class="aw-body" id="aw-ctrl-body"><span class="aw-empty">no controls</span></div>
|
||||
<div class="aw-body" id="aw-sensor-body"><span class="aw-empty">waiting...</span></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel trace-panel">
|
||||
<div class="panel-header trace-h">Trace</div>
|
||||
<div id="trace"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/app.js?v=14.5"></script>
|
||||
<!-- Login overlay -->
|
||||
<div id="login-overlay" style="display:none">
|
||||
<div class="login-card">
|
||||
<h2>cog</h2>
|
||||
<p>Please log in to continue</p>
|
||||
<button onclick="startLogin()">Log in with Zitadel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/static/js/main.js?v=15"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
88
static/js/auth.js
Normal file
88
static/js/auth.js
Normal file
@ -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);
|
||||
}
|
||||
57
static/js/awareness.js
Normal file
57
static/js/awareness.js
Normal file
@ -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]) =>
|
||||
`<div class="aw-row"><span class="aw-key">${esc(k)}</span><span class="aw-val">${esc(v || 'null')}</span></div>`
|
||||
).join('');
|
||||
|
||||
if (facts.length) {
|
||||
html += '<div class="aw-row"><span class="aw-key">facts</span><span class="aw-val">'
|
||||
+ facts.map(f => esc(truncate(f, 40))).join('<br>') + '</span></div>';
|
||||
}
|
||||
if (history.length) {
|
||||
html += '<div class="aw-row"><span class="aw-key">topics</span><span class="aw-val">'
|
||||
+ history.map(t => esc(truncate(t, 25))).join(', ') + '</span></div>';
|
||||
}
|
||||
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 = `<div class="aw-row"><span class="aw-key">tick</span><span class="aw-val">#${tick}</span></div>`;
|
||||
for (const [k, v] of Object.entries(_sensorReadings)) {
|
||||
html += `<div class="aw-row"><span class="aw-key">${esc(k)}</span><span class="aw-val">${esc(String(v))}</span></div>`;
|
||||
}
|
||||
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`;
|
||||
}
|
||||
62
static/js/chat.js
Normal file
62
static/js/chat.js
Normal file
@ -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;
|
||||
88
static/js/dashboard.js
Normal file
88
static/js/dashboard.js
Normal file
@ -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 = '<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);
|
||||
}
|
||||
}
|
||||
body.appendChild(container);
|
||||
}
|
||||
|
||||
export function clearDashboard() {
|
||||
const body = document.getElementById('workspace-body');
|
||||
if (body) body.innerHTML = '';
|
||||
}
|
||||
238
static/js/graph.js
Normal file
238
static/js/graph.js
Normal file
@ -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');
|
||||
};
|
||||
34
static/js/main.js
Normal file
34
static/js/main.js
Normal file
@ -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;
|
||||
25
static/js/tests.js
Normal file
25
static/js/tests.js
Normal file
@ -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 = `<span class="ts-running">TESTING</span> `
|
||||
+ `<span class="ts-pass">${done}</span>/<span>${expected}</span>`
|
||||
+ (fail ? ` <span class="ts-fail">${fail}F</span>` : '')
|
||||
+ ` <span style="color:#888;max-width:20rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(current)}</span>`;
|
||||
} else if (done > 0) {
|
||||
const allGreen = fail === 0;
|
||||
el.innerHTML = `<span class="${allGreen ? 'ts-pass' : 'ts-fail'}">${pass}/${expected}</span>`
|
||||
+ (fail ? ` <span class="ts-fail">${fail} failed</span>` : ' <span class="ts-pass">all green</span>');
|
||||
}
|
||||
}
|
||||
42
static/js/trace.js
Normal file
42
static/js/trace.js
Normal file
@ -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 =
|
||||
'<span class="trace-ts">' + ts + '</span>' +
|
||||
'<span class="trace-node ' + esc(node) + '">' + esc(node) + '</span>' +
|
||||
'<span class="trace-event">' + esc(event) + '</span>' +
|
||||
'<span class="trace-data' + (cls ? ' ' + cls : '') + '">' + esc(text) + '</span>';
|
||||
|
||||
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 = '';
|
||||
}
|
||||
34
static/js/util.js
Normal file
34
static/js/util.js
Normal file
@ -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) =>
|
||||
'<pre class="md-code"><code>' + code.trim() + '</code></pre>');
|
||||
// Inline code
|
||||
html = html.replace(/`([^`]+)`/g, '<code class="md-inline">$1</code>');
|
||||
// Bold
|
||||
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||
// Italic
|
||||
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||||
// Bullet lists
|
||||
html = html.replace(/^[\*\-]\s+(.+)$/gm, '<li>$1</li>');
|
||||
html = html.replace(/(<li>.*<\/li>\n?)+/g, m => '<ul>' + m + '</ul>');
|
||||
// Numbered lists
|
||||
html = html.replace(/^\d+\.\s+(.+)$/gm, '<li>$1</li>');
|
||||
// Line breaks (but not inside pre/code)
|
||||
html = html.replace(/\n/g, '<br>');
|
||||
return html;
|
||||
}
|
||||
175
static/js/ws.js
Normal file
175
static/js/ws.js
Normal file
@ -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));
|
||||
}
|
||||
}
|
||||
212
static/style.css
212
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; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user