const msgs = document.getElementById('messages'); const inputEl = document.getElementById('input'); const statusEl = document.getElementById('status'); const traceEl = document.getElementById('trace'); let ws, currentEl; let authToken = localStorage.getItem('cog_token'); let authConfig = null; // --- OIDC Auth --- async function initAuth() { try { const resp = await fetch('/auth/config'); authConfig = await resp.json(); } catch { authConfig = { enabled: false }; } if (!authConfig.enabled) { connect(); return; } // Handle OIDC callback if (location.pathname === '/callback') { const params = new URLSearchParams(location.search); const code = params.get('code'); const verifier = sessionStorage.getItem('pkce_verifier'); if (code && verifier) { 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', client_id: authConfig.clientId, code, redirect_uri: location.origin + '/callback', code_verifier: verifier, }), }); const tokens = await tokenResp.json(); if (tokens.access_token) { // Store access token for userinfo, id_token for JWT validation localStorage.setItem('cog_access_token', tokens.access_token); authToken = tokens.id_token || tokens.access_token; localStorage.setItem('cog_token', authToken); sessionStorage.removeItem('pkce_verifier'); } } history.replaceState(null, '', '/'); } if (authToken) { connect(); } else { showLogin(); } } function showLogin() { statusEl.textContent = 'not authenticated'; statusEl.style.color = '#f59e0b'; const btn = document.createElement('button'); btn.textContent = 'Log in with loop42'; btn.className = 'login-btn'; btn.onclick = startLogin; document.getElementById('input-bar').replaceChildren(btn); } async function startLogin() { // PKCE: generate code_verifier + code_challenge const verifier = randomString(64); sessionStorage.setItem('pkce_verifier', verifier); const encoder = new TextEncoder(); const digest = await crypto.subtle.digest('SHA-256', encoder.encode(verifier)); const challenge = btoa(String.fromCharCode(...new Uint8Array(digest))) .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', }); location.href = authConfig.issuer + '/oauth/v2/authorize?' + params; } function randomString(len) { const arr = new Uint8Array(len); crypto.getRandomValues(arr); return btoa(String.fromCharCode(...arr)).replace(/[^a-zA-Z0-9]/g, '').slice(0, len); } // --- WebSocket --- function connect() { 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 = () => { statusEl.textContent = 'connected'; statusEl.style.color = '#22c55e'; addTrace('runtime', 'connected', 'ws open'); }; ws.onclose = () => { statusEl.textContent = 'disconnected'; statusEl.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') { if (!currentEl) { currentEl = addMsg('assistant', ''); currentEl.classList.add('streaming'); } currentEl.textContent += data.content; scroll(msgs); } else if (data.type === 'done') { if (currentEl) currentEl.classList.remove('streaming'); currentEl = null; } else if (data.type === 'controls') { renderControls(data.controls); dockControls(data.controls); } }; } function handleHud(data) { const node = data.node || 'unknown'; const event = data.event || ''; if (event === 'context') { // Update node meter if (data.tokens !== undefined) { updateMeter(node, data.tokens, data.max_tokens, data.fill_pct); } // Expandable: show message count + token info const count = (data.messages || []).length; const tokenInfo = data.tokens ? ` [${data.tokens}/${data.max_tokens}t ${data.fill_pct}%]` : ''; const summary = count + ' msgs' + tokenInfo + ': ' + (data.messages || []).map(m => m.role[0].toUpperCase() + ':' + truncate(m.content, 30) ).join(' | '); const detail = (data.messages || []).map((m, i) => i + ' [' + m.role + '] ' + m.content ).join('\n'); addTrace(node, 'context', summary, 'context', detail); } else if (event === 'perceived') { addTrace(node, 'perceived', data.instruction, 'instruction'); } else if (event === 'decided') { addTrace(node, 'decided', data.instruction, '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(' '); const detail = JSON.stringify(data.state, null, 2); addTrace(node, 'state', pairs, 'state', detail); updateAwarenessState(data.state); } else if (event === 'process_start') { addTrace(node, 'run ' + (data.tool || 'python'), truncate(data.code || '', 80), 'instruction', data.code); showProcessCard(data.pid, data.tool || 'python', data.code || ''); showAwarenessProcess(data.pid, data.tool || 'python', data.code || ''); } else if (event === 'process_done') { addTrace(node, (data.exit_code === 0 ? 'done' : 'failed'), truncate(data.output || '', 80), data.exit_code === 0 ? '' : 'error', data.output); updateProcessCard(data.pid, data.exit_code === 0 ? 'done' : 'failed', data.output || '', data.elapsed); updateAwarenessProcess(data.pid, data.exit_code === 0 ? 'done' : 'failed', data.output || '', data.elapsed); } else if (event === 'error') { addTrace(node, 'error', data.detail || '', 'error'); } else if (event === 'thinking') { addTrace(node, 'thinking', data.detail || ''); } else if (event === 'streaming') { addTrace(node, 'streaming', ''); } else if (event === 'done') { addTrace(node, 'done', ''); } else if (event === 'tick') { // Update sensor meter with tick count const meter = document.getElementById('meter-sensor'); if (meter) { const text = meter.querySelector('.nm-text'); const deltas = Object.entries(data.deltas || {}).map(([k,v]) => k + '=' + v).join(' '); text.textContent = 'tick #' + (data.tick || 0) + (deltas ? ' | ' + deltas : ''); } if (data.deltas && Object.keys(data.deltas).length) { const deltas = Object.entries(data.deltas).map(([k,v]) => k + '=' + truncate(String(v), 30)).join(' '); addTrace(node, 'tick #' + data.tick, deltas); } updateAwarenessSensors(data.tick || 0, data.deltas || {}); } else if (event === 'started' || event === 'stopped') { const meter = document.getElementById('meter-sensor'); if (meter) meter.querySelector('.nm-text').textContent = event; addTrace(node, event, ''); } else { // Generic fallback const detail = JSON.stringify(data, null, 2); addTrace(node, event, '', '', detail); } } function addTrace(node, event, text, cls, detail) { 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); } function renderControls(controls) { 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.data || {} })); addTrace('runtime', 'action', ctrl.action); } }; container.appendChild(btn); } else if (ctrl.type === 'table') { const table = document.createElement('table'); table.className = 'control-table'; // Header 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); } // Rows 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 === 'process') { const card = document.createElement('div'); card.className = 'process-card ' + (ctrl.status || 'running'); card.innerHTML = '' + esc(ctrl.tool || 'python') + '' + '' + esc(ctrl.status || 'running') + '' + (ctrl.status === 'running' ? '' : '') + '
' + esc(ctrl.output || '') + '
'; container.appendChild(card); } } msgs.appendChild(container); scroll(msgs); } function cancelProcess(pid) { if (ws && ws.readyState === 1) { ws.send(JSON.stringify({ type: 'cancel_process', pid })); } } function showProcessCard(pid, tool, code) { const card = document.createElement('div'); card.className = 'process-card running'; card.id = 'proc-' + pid; card.innerHTML = '' + esc(tool) + '' + 'running' + '' + '
' + esc(truncate(code, 200)) + '
' + '
';
  msgs.appendChild(card);
  scroll(msgs);
}

function updateProcessCard(pid, status, output, elapsed) {
  const card = document.getElementById('proc-' + pid);
  if (!card) return;
  card.className = 'process-card ' + status;
  const statusEl = card.querySelector('.pc-status');
  if (statusEl) statusEl.textContent = status + (elapsed ? ' (' + elapsed + 's)' : '');
  const stopBtn = card.querySelector('.pc-stop');
  if (stopBtn) stopBtn.remove();
  const outEl = card.querySelector('.pc-output');
  if (outEl && output) outEl.textContent = output;
}

function updateMeter(node, tokens, maxTokens, fillPct) {
  const meter = document.getElementById('meter-' + node);
  if (!meter) return;
  const fill = meter.querySelector('.nm-fill');
  const text = meter.querySelector('.nm-text');
  fill.style.width = fillPct + '%';
  fill.style.backgroundColor = fillPct > 80 ? '#ef4444' : fillPct > 50 ? '#f59e0b' : '#22c55e';
  text.textContent = tokens + ' / ' + maxTokens + 't (' + fillPct + '%)';
}

function scroll(el) { el.scrollTop = el.scrollHeight; }
function esc(s) { const d = document.createElement('span'); d.textContent = s; return d.innerHTML; }
function truncate(s, n) { return s.length > n ? s.slice(0, n) + '\u2026' : s; }

function addMsg(role, text) {
  const el = document.createElement('div');
  el.className = 'msg ' + role;
  el.textContent = text;
  msgs.appendChild(el);
  scroll(msgs);
  return el;
}

function send() {
  const text = inputEl.value.trim();
  if (!text || !ws || ws.readyState !== 1) return;
  addMsg('user', text);
  addTrace('runtime', 'user_msg', truncate(text, 60));
  ws.send(JSON.stringify({ text }));
  inputEl.value = '';
}

// --- Awareness panel updates ---

let _sensorReadings = {};

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],
    ['language', state.language],
    ['style', state.style_hint],
    ['situation', state.situation],
  ];
  let html = '';
  for (const [k, v] of display) {
    if (!v) continue;
    const moodCls = k === 'mood' ? ' mood-' + v : '';
    html += '
' + esc(k) + '' + esc(String(v)) + '
'; } const facts = state.facts || []; if (facts.length) { html += ''; } body.innerHTML = html || 'no state yet'; } function updateAwarenessSensors(tick, deltas) { // Merge deltas into persistent readings for (const [k, v] of Object.entries(deltas)) { _sensorReadings[k] = { value: v, at: Date.now() }; } const body = document.getElementById('aw-sensors-body'); if (!body) return; const entries = Object.entries(_sensorReadings); if (!entries.length) { body.innerHTML = 'waiting for tick...'; return; } let html = ''; for (const [name, r] of entries) { const age = Math.round((Date.now() - r.at) / 1000); const ageStr = age < 5 ? 'now' : age < 60 ? age + 's' : Math.floor(age / 60) + 'm'; html += '
' + esc(name) + '' + esc(String(r.value)) + '' + ageStr + '
'; } body.innerHTML = html; } function showAwarenessProcess(pid, tool, code) { const body = document.getElementById('aw-proc-body'); if (!body) return; // Remove "idle" placeholder const empty = body.querySelector('.aw-empty'); if (empty) empty.remove(); const el = document.createElement('div'); el.className = 'aw-proc running'; el.id = 'aw-proc-' + pid; el.innerHTML = '
' + esc(tool) + 'running' + '
' + '
' + esc(truncate(code, 150)) + '
' + '
'; body.appendChild(el); } function updateAwarenessProcess(pid, status, output, elapsed) { const el = document.getElementById('aw-proc-' + pid); if (!el) return; el.className = 'aw-proc ' + status; const st = el.querySelector('.aw-proc-status'); if (st) st.textContent = status + (elapsed ? ' (' + elapsed + 's)' : ''); const stop = el.querySelector('.aw-proc-stop'); if (stop) stop.remove(); const out = el.querySelector('.aw-proc-output'); if (out && output) out.textContent = output; // Auto-remove done processes after 10s if (status === 'done') { setTimeout(() => { el.remove(); const body = document.getElementById('aw-proc-body'); if (body && !body.children.length) body.innerHTML = 'idle'; }, 10000); } } function dockControls(controls) { const body = document.getElementById('aw-ctrl-body'); if (!body) return; // Replace previous controls with new ones 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.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); } } body.appendChild(container); } inputEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') send(); }); initAuth();