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);