// Emergence-Mini live view const canvas = document.getElementById('world'); const ctx = canvas.getContext('2d'); const GRID = 240; let snapshot = null; let actions = []; let replay = { frames: [], idx: 0, playing: false, raf: null, lastFrame: 0 }; let mode = 'live'; // 'live' or 'replay' const AGENT_COLORS = { anchor: '#ffd166', flora: '#6cf0c2', lovely: '#ff8fb1', spark: '#82aaff', }; const LANDMARK_COLORS = { town_hall: '#ff6c6c', library: '#82aaff', plaza: '#ff8fb1', park: '#6cf08a', victory_arch: '#ffd166', police: '#cccccc', bookworm: '#a47cff', cafe: '#ffb066', billboard: '#dddddd', techhub: '#6cf0c2', home_anchor: '#3a3a3a', home_flora: '#3a3a3a', home_lovely: '#3a3a3a', home_spark: '#3a3a3a', }; function effectiveState() { if (mode === 'replay' && replay.frames.length) { const f = replay.frames[Math.min(replay.idx, replay.frames.length - 1)]; const agents = []; for (const id of Object.keys(f.agents)) { const a = f.agents[id]; agents.push({ id, name: a.name || id, x: a.x, y: a.y, energy: a.energy ?? 0, knowledge: a.knowledge ?? 0, influence: a.influence ?? 0, credits: a.credits ?? 0, mood: a.mood || 'neutral', model: a.model || '', tool: a.tool || '', }); } return { agents, landmarks: snapshot ? snapshot.landmarks : [], clocks: f.clocks || {}, drift: f.drift, tick: f.tick, _replayFrame: f, }; } return snapshot; } function draw() { const state = effectiveState(); if (!state) return; const W = canvas.width, H = canvas.height; ctx.fillStyle = '#060a0e'; ctx.fillRect(0, 0, W, H); // grid ctx.strokeStyle = '#0e1620'; ctx.lineWidth = 1; for (let i = 0; i <= GRID; i += 30) { const px = (i / GRID) * W; ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, H); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, px); ctx.lineTo(W, px); ctx.stroke(); } // landmarks for (const lm of state.landmarks) { const x = (lm.x / GRID) * W; const y = (lm.y / GRID) * H; const isHome = lm.id.startsWith('home_'); ctx.fillStyle = LANDMARK_COLORS[lm.id] || '#888'; ctx.beginPath(); ctx.arc(x, y, isHome ? 5 : 7, 0, Math.PI * 2); ctx.fill(); if (!isHome) { ctx.fillStyle = '#0a1018'; ctx.font = '10px ui-monospace'; ctx.textAlign = 'center'; ctx.fillText(lm.name, x, y - 9); } } // agents for (const a of state.agents) { const x = (a.x / GRID) * W; const y = (a.y / GRID) * H; ctx.fillStyle = AGENT_COLORS[a.id] || '#fff'; ctx.beginPath(); ctx.arc(x, y, 8, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#0a1018'; ctx.font = 'bold 10px ui-monospace'; ctx.textAlign = 'center'; ctx.fillText(a.name[0], x, y + 3); ctx.strokeStyle = '#ffd166'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(x, y, 8 + (100 - a.energy) * 0.06, 0, Math.PI * 2); ctx.stroke(); } if (mode === 'replay' && state._replayFrame) { drawReplayOverlay(state._replayFrame); } } function drawReplayOverlay(f) { const W = canvas.width, H = canvas.height; ctx.fillStyle = 'rgba(6,10,14,0.75)'; ctx.fillRect(8, H - 66, 220, 58); ctx.strokeStyle = '#1c2733'; ctx.lineWidth = 1; ctx.strokeRect(8, H - 66, 220, 58); ctx.fillStyle = '#6cf0c2'; ctx.font = '11px ui-monospace'; ctx.textAlign = 'left'; const t = new Date(f.ts * 1000).toLocaleTimeString(); ctx.fillText(`tick ${f.tick ?? '---'} | ${t}`, 14, H - 50); if (f.kind === 'action' && f.event) { const a = f.event.agent || f.actor; const tool = f.event.tool || ''; ctx.fillStyle = AGENT_COLORS[a] || '#d6e2ee'; ctx.fillText(`${a} -> ${tool}`, 14, H - 34); const model = shortModel(f.event.model || ''); ctx.fillStyle = '#8aa1b6'; ctx.fillText(model || 'fallback', 14, H - 18); } else { ctx.fillStyle = '#8aa1b6'; ctx.fillText('tick update', 14, H - 34); } } function shortModel(name) { if (!name) return ''; // Strip org/ prefix and :tag for compactness const s = name.replace(/^.*\//, '').replace(/:latest$/, ''); return s; } function modelTag(m, provider) { if (!m) return ''; const isCloud = provider === 'openrouter' || (m || '').includes('/'); const cls = isCloud ? 'cloud' : 'local'; const label = isCloud ? '☁ ' + shortModel(m) : '💻 ' + shortModel(m); return `${label}`; } function refreshAgentCards() { const wrap = document.getElementById('agentList'); wrap.innerHTML = ''; for (const a of snapshot.agents) { const div = document.createElement('div'); div.className = 'agent-card'; div.innerHTML = `

${a.name}${modelTag(a.model, a.provider)}

${a.role}
at (${a.x}, ${a.y}) · ${a.mood}
${a.energy.toFixed(0)}E · ${a.knowledge.toFixed(0)}K · ${a.influence.toFixed(0)}I · ${a.credits.toFixed(1)} CC `; wrap.appendChild(div); } } function refreshClocks() { if (!snapshot || !snapshot.clocks) return; const wrap = document.getElementById('clocks'); wrap.innerHTML = ''; const entries = Object.entries(snapshot.clocks); // sort by τ descending entries.sort((a, b) => b[1].tau - a[1].tau); const maxTau = Math.max(1, entries.length ? entries[0][1].tau : 1); const colorFor = { anchor: '#ffd166', flora: '#6cf0c2', lovely: '#ff8fb1', spark: '#82aaff' }; for (const [aid, c] of entries) { const div = document.createElement('div'); div.className = 'clock-card'; const pct = (c.tau / maxTau) * 100; div.innerHTML = `
${aid}
τ = ${c.tau.toFixed(2)} · pace = ${c.pace.toFixed(2)} op/s · ${c.n_ops} ops
`; wrap.appendChild(div); } } function refreshDrift() { if (!snapshot || !snapshot.drift) return; const wrap = document.getElementById('drift'); const d = snapshot.drift; if (!d.pairs || d.pairs.length === 0) { wrap.innerHTML = 'No multi-agent drift yet.'; return; } const top = d.pairs[0]; if (top.divergent) { wrap.className = 'drift-warn'; wrap.innerHTML = `⚠ DILATION DRIFT · ${top.a}↔${top.b}: |Δτ| = ${top.drift.toFixed(1)} (γ=${top.gamma_ab})
${top.a} has experienced ${top.tau_a} units, ${top.b} ${top.tau_b}. Frame transformation exceeds threshold ${d.threshold}.`; } else { wrap.className = 'drift-ok'; wrap.innerHTML = `✓ COHERENT · max drift = ${d.max_drift.toFixed(2)} (threshold ${d.threshold})`; } } function refreshProposals() { const wrap = document.getElementById('proposals'); wrap.innerHTML = ''; fetch('/api/proposals').then(r => r.json()).then(d => { if (!d.active.length) { wrap.innerHTML = 'No active proposals.'; return; } for (const p of d.active) { const el = document.createElement('div'); el.className = 'proposal'; el.innerHTML = `

#${p.id} ${p.title}

by ${p.author} · ${p.category} · ${p.votes} votes

${p.body}

`; wrap.appendChild(el); } }); } function refreshConstitution() { const wrap = document.getElementById('constitution'); wrap.innerHTML = ''; fetch('/api/constitution').then(r => r.json()).then(c => { for (const a of c.articles) { const el = document.createElement('div'); el.className = 'article'; el.innerHTML = `Article ${a.id} — ${a.title}

${a.body}

`; wrap.appendChild(el); } }); } function addFeed(entry) { const ul = document.getElementById('feed'); const li = document.createElement('li'); const t = new Date().toLocaleTimeString(); if (entry.type === 'action') { li.innerHTML = `${entry.name} ${entry.tool} — ${entry.rationale || ''} · ${t}`; } else { li.innerHTML = `${entry.type} · ${t}`; } ul.prepend(li); while (ul.children.length > 80) ul.removeChild(ul.lastChild); } function refreshHeader() { const state = effectiveState(); document.getElementById('tick').textContent = state?.tick || 0; document.getElementById('agentCount').textContent = state?.agents.length || 0; } async function refreshAll() { const r = await fetch('/api/state'); snapshot = await r.json(); draw(); refreshHeader(); refreshAgentCards(); refreshClocks(); refreshDrift(); refreshProposals(); refreshConstitution(); // LLM info if (snapshot.llm) { const info = document.getElementById('llmInfo'); info.textContent = `${snapshot.llm.provider} · ${snapshot.llm.model}`; } } function connectWS() { const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws'); const status = document.getElementById('wsStatus'); ws.onopen = () => { status.textContent = 'live'; status.className = 'ws-status connected'; }; ws.onclose = () => { status.textContent = 'disconnected — retrying'; status.className = 'ws-status disconnected'; setTimeout(connectWS, 2000); }; ws.onmessage = (ev) => { const msg = JSON.parse(ev.data); if (msg.type === 'snapshot') { snapshot = msg.data; draw(); refreshHeader(); refreshAgentCards(); } else if (msg.type === 'action') { // update local agent snapshot if (snapshot) { const a = snapshot.agents.find(x => x.id === msg.agent); if (a) { a.x = msg.x; a.y = msg.y; a.energy = msg.energy; a.knowledge = msg.knowledge; a.influence = msg.influence; a.credits = msg.credits; a.mood = msg.mood; } draw(); refreshAgentCards(); } addFeed(msg); // refresh proposals / constitution occasionally if (msg.tool && msg.tool.startsWith('vote_on') || msg.tool === 'submit_townhall_proposal') { refreshProposals(); } if (msg.tool === 'submit_townhall_proposal') { setTimeout(refreshConstitution, 500); } } else if (msg.type === 'tick') { document.getElementById('tick').textContent = msg.tick; if (msg.clocks) snapshot.clocks = msg.clocks; if (msg.drift) snapshot.drift = msg.drift; refreshClocks(); refreshDrift(); } }; } async function populateManual() { const a = await (await fetch('/api/agents')).json(); const selA = document.querySelector('#manual select[name=agent]'); for (const ag of a) { const o = document.createElement('option'); o.value = ag.id; o.textContent = ag.name; selA.appendChild(o); } const tools = [ 'go_to_place','go_home','say_to_agent','speak_to_all', 'show_emoticon','recharge_energy','add_to_longterm_memory', 'write_blog','add_to_billboard','submit_townhall_proposal', 'vote_on_proposal','idle', ]; const selT = document.querySelector('#manual select[name=tool]'); for (const t of tools) { const o = document.createElement('option'); o.value = t; o.textContent = t; selT.appendChild(o); } } document.getElementById('manual').addEventListener('submit', async (e) => { e.preventDefault(); const f = e.target; let args = {}; try { args = JSON.parse(f.args.value || '{}'); } catch { alert('args must be JSON'); return; } const body = { tool: f.tool.value, args }; const r = await fetch(`/api/turn/${f.agent.value}`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(body), }); const out = await r.json(); console.log('manual turn result', out); }); function refreshTexts() { fetch('/api/texts').then(r => r.json()).then(d => { document.getElementById('textsCount').textContent = `(${d.length})`; const wrap = document.getElementById('texts'); if (!d.length) { wrap.innerHTML = 'Noch keine Texte. Agenten generieren welche ueber write_blog, add_to_billboard, speak_to_all, say_to_agent, add_to_longterm_memory.'; return; } wrap.innerHTML = ''; for (const t of d) { const el = document.createElement('div'); el.className = 'text-card'; const isLocal = !((t.model || '').includes('/')); const src = t.source === 'fallback' ? 'fallback' : 'llm'; el.innerHTML = `
${t.agent} ${shortModel(t.model)} ${t.kind}
${escapeHtml(t.body)}
${new Date(t.ts * 1000).toLocaleString()}
`; wrap.appendChild(el); } }); } function escapeHtml(s) { return s.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } /* ---------- Replay ---------- */ const slider = document.getElementById('replaySlider'); const playBtn = document.getElementById('replayPlay'); const liveBtn = document.getElementById('replayLive'); const hoursSel = document.getElementById('replayHours'); const replayTime = document.getElementById('replayTime'); const replayPanel = document.getElementById('replayPanel'); function setReplayUI() { if (mode === 'live') { replayPanel.classList.remove('active'); liveBtn.classList.add('active'); playBtn.textContent = 'Play'; replayTime.textContent = 'live'; stopReplay(); return; } replayPanel.classList.add('active'); liveBtn.classList.remove('active'); slider.max = Math.max(0, replay.frames.length - 1); } function updateReplayFrame() { replay.idx = Math.max(0, Math.min(replay.idx, replay.frames.length - 1)); slider.value = replay.idx; const f = replay.frames[replay.idx]; if (f) { const t = new Date(f.ts * 1000); replayTime.textContent = `tick ${f.tick ?? '---'} | ${t.toLocaleTimeString()}`; if (mode === 'replay') { refreshHeader(); refreshClocks(); refreshDrift(); draw(); } } } function stopReplay() { replay.playing = false; if (replay.raf) cancelAnimationFrame(replay.raf); replay.raf = null; } function playReplay() { if (!replay.frames.length) { loadReplay(); return; } replay.playing = !replay.playing; playBtn.textContent = replay.playing ? 'Pause' : 'Play'; if (replay.playing) replayLoop(); } function replayLoop() { if (!replay.playing || mode !== 'replay') return; const now = performance.now(); if (!replay.lastFrame) replay.lastFrame = now; const dt = now - replay.lastFrame; if (dt > 80) { replay.idx = Math.min(replay.idx + 1, replay.frames.length - 1); updateReplayFrame(); replay.lastFrame = now; if (replay.idx >= replay.frames.length - 1) { replay.playing = false; playBtn.textContent = 'Play'; } } replay.raf = requestAnimationFrame(replayLoop); } async function loadReplay() { stopReplay(); const hours = parseFloat(hoursSel.value); const r = await fetch(`/api/history?hours=${hours}`); const data = await r.json(); replay.frames = data.frames || []; replay.idx = 0; if (replay.frames.length) { mode = 'replay'; setReplayUI(); updateReplayFrame(); playReplay(); } } function enterLive() { mode = 'live'; setReplayUI(); refreshAll(); } function enterReplayFromSlider() { mode = 'replay'; setReplayUI(); } slider.addEventListener('input', () => { if (mode !== 'replay') enterReplayFromSlider(); replay.idx = parseInt(slider.value, 10); updateReplayFrame(); }); playBtn.addEventListener('click', () => { if (mode !== 'replay') loadReplay(); else playReplay(); }); liveBtn.addEventListener('click', enterLive); hoursSel.addEventListener('change', loadReplay); refreshAll(); populateManual(); refreshTexts(); connectWS(); setInterval(refreshProposals, 5000); setInterval(refreshTexts, 8000); // Optional demo mode: http://.../?replay=1 auto-starts replay if (location.search.includes('replay=1')) { setTimeout(loadReplay, 600); }