const ZONES = { altar: { x: 50, y: 100, label: 'Altar' }, library: { x: 180, y: 120, label: 'Library' }, court: { x: 340, y: 120, label: 'Court' }, forge: { x: 500, y: 180, label: 'Forge' }, ledge: { x: 650, y: 260, label: 'Ledge' }, sentinel_gate: { x: 420, y: 360, label: 'Sentinel Gate' } }; const AGENTS = [ { name: 'SCRIBE', emoji: 'πŸ”πŸ“–', role: 'theological_retrieval', zone: 'library', color: '#4a9' }, { name: 'JUDGE', emoji: 'βš–οΈπŸ§ ', role: 'logic_validator', zone: 'court', color: '#a84' }, { name: 'PROPHET', emoji: 'πŸ”₯', role: 'warning_system', zone: 'forge', color: '#a44' }, { name: 'SENTINEL', emoji: 'πŸ›‘οΈ', role: 'security_gate', zone: 'sentinel_gate', color: '#44a' }, { name: 'LEDGE', emoji: 'πŸ§ΎπŸ”', role: 'worm_seal', zone: 'ledge', color: '#888' } ]; const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const logEl = document.getElementById('log'); const agentListEl = document.getElementById('agent-list'); const queryInput = document.getElementById('query'); const chainStatusEl = document.getElementById('chain-status'); const lastVerdictEl = document.getElementById('last-verdict'); let agentStates = {}; AGENTS.forEach(a => { agentStates[a.name] = { ...a, status: 'idle', x: ZONES[a.zone].x, y: ZONES[a.zone].y }; }); function resize() { canvas.width = canvas.parentElement.clientWidth; canvas.height = canvas.parentElement.clientHeight; } resize(); window.addEventListener('resize', resize); function renderAgentList() { agentListEl.innerHTML = AGENTS.map(a => `
${a.emoji}${a.name}
${a.role}
${ZONES[a.zone].label} (${ZONES[a.zone].x}, ${ZONES[a.zone].y})
`).join(''); } function addLog(agent, message) { const time = new Date().toLocaleTimeString(); const cls = agent.toLowerCase(); logEl.innerHTML += `
[${time}] ${agent} ${message}
`; logEl.scrollTop = logEl.scrollHeight; } function drawZone(z, zoneData) { const x = zoneData.x; const y = zoneData.y; ctx.strokeStyle = '#1a1a1a'; ctx.strokeRect(x - 40, y - 30, 80, 60); ctx.fillStyle = '#1a1a1a'; ctx.font = '9px Courier New'; ctx.textAlign = 'center'; ctx.fillText(zoneData.label, x, y + 45); } function drawAgent(a) { const state = agentStates[a.name]; ctx.font = '24px serif'; ctx.textAlign = 'center'; ctx.fillText(state.emoji, state.x, state.y + 8); ctx.font = '10px Courier New'; ctx.fillStyle = state.status === 'running' ? a.color : '#333'; ctx.fillText(a.name, state.x, state.y + 25); } let particles = []; function drawParticles() { particles = particles.filter(p => p.life > 0); particles.forEach(p => { p.x += p.vx; p.y += p.vy; p.life--; ctx.globalAlpha = p.life / p.maxLife; ctx.fillStyle = p.color; ctx.fillRect(p.x, p.y, 2, 2); }); ctx.globalAlpha = 1; } function spawnParticle(from, to, color) { const fx = ZONES[from].x, fy = ZONES[from].y; const tx = ZONES[to].x, ty = ZONES[to].y; for (let i = 0; i < 5; i++) { particles.push({ x: fx, y: fy, vx: (tx - fx) / 60 + (Math.random() - 0.5) * 2, vy: (ty - fy) / 60 + (Math.random() - 0.5) * 2, life: 60, maxLife: 60, color }); } } function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); Object.entries(ZONES).forEach(([k, v]) => drawZone(k, v)); AGENTS.forEach(a => drawAgent(a)); drawParticles(); requestAnimationFrame(render); } renderAgentList(); render(); async function runPipeline(query) { addLog('SCRIBE', `searching: "${query}"`); agentStates.SCRIBE.status = 'running'; spawnParticle('altar', 'library', '#4a9'); try { const res = await fetch('/query', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, source_type: query.includes('define') || query.includes('πŸ“š') ? 'dictionary' : query.includes('🌐') ? 'wikipedia' : 'scripture' }) }); const data = await res.json(); agentStates.SCRIBE.status = 'idle'; addLog('SCRIBE', `found ${data.scribe.citations.length} citation(s), confidence ${data.scribe.confidence}`); agentStates.JUDGE.status = 'running'; addLog('JUDGE', `verdict: ${data.judge.verdict}`); spawnParticle('library', 'court', '#a84'); agentStates.JUDGE.status = 'idle'; agentStates.PROPHET.status = 'running'; addLog('PROPHET', `risk: ${data.prophet.riskLevel}, flags: ${data.prophet.flags.join(', ') || 'none'}`); spawnParticle('court', 'forge', '#a44'); agentStates.PROPHET.status = 'idle'; agentStates.SENTINEL.status = 'running'; addLog('SENTINEL', data.sentinel.allowed ? 'cleared' : `blocked: ${data.sentinel.reason}`); spawnParticle('forge', 'sentinel_gate', '#44a'); agentStates.SENTINEL.status = 'idle'; if (data.ledge) { agentStates.LEDGE.status = 'running'; addLog('LEDGE', `sealed seq ${data.ledge.sequence}`); spawnParticle('sentinel_gate', 'ledge', '#888'); agentStates.LEDGE.status = 'idle'; } lastVerdictEl.textContent = `verdict: ${data.finalVerdict}`; } catch (err) { addLog('SYSTEM', `error: ${err.message}`); AGENTS.forEach(a => { agentStates[a.name].status = 'idle'; }); } } queryInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && queryInput.value.trim()) { runPipeline(queryInput.value.trim()); queryInput.value = ''; } }); async function refreshChain() { try { const res = await fetch('/worm'); const data = await res.json(); chainStatusEl.textContent = `chain: ${data.entries.length} entries`; } catch { chainStatusEl.textContent = 'chain: offline'; } } refreshChain(); setInterval(refreshChain, 5000);