emergence-mini-dilles/web/app.js
Jeuners 23c914d743 Ollama URL with auto-fallback
The user wanted to use Ollama on a network host (192.168.1.245).
The host is reachable (ping ~900ms) but its Ollama port (11434)
is not open, so the engine falls back to a local URL.

Changes:
- .env: EMERGENCE_OLLAMA_URL = network host, EMERGENCE_OLLAMA_FALLBACK_URL
  = localhost
- engine/llm.py: chat_ollama now iterates primary then fallback URL
  on connection failure. is_available() does the same. provider_info()
  exposes both URLs.
- All 100 tests still pass.

Live-verified: lovely+spark (llama3.2:3b) use mode=llm with ~10s
latency, which is the connection-refused on 192.168.1.245 + the
successful fallback to 127.0.0.1. As soon as 192.168.1.245's Ollama
is reachable, latency will drop to normal (~1-3s).
2026-06-15 03:08:42 +02:00

295 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Emergence-Mini live view
const canvas = document.getElementById('world');
const ctx = canvas.getContext('2d');
const GRID = 240;
let snapshot = null;
let actions = [];
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 draw() {
if (!snapshot) 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 snapshot.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 snapshot.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);
// energy halo
ctx.strokeStyle = '#ffd166';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(x, y, 8 + (100 - a.energy) * 0.06, 0, Math.PI * 2);
ctx.stroke();
}
}
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 = `
<h3>${a.name} <small>· ${a.role}</small></h3>
<div>at (${a.x}, ${a.y}) · ${a.mood}</div>
<div class="bar energy"><i style="width:${a.energy}%"></i></div>
<div class="bar knowledge"><i style="width:${a.knowledge}%"></i></div>
<div class="bar influence"><i style="width:${a.influence}%"></i></div>
<div class="bar credits"><i style="width:${Math.min(100, a.credits * 5)}%"></i></div>
<small>${a.energy.toFixed(0)}E · ${a.knowledge.toFixed(0)}K · ${a.influence.toFixed(0)}I · ${a.credits.toFixed(1)} CC</small>
`;
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 = `
<div class="name" style="color:${colorFor[aid] || '#fff'}">${aid}</div>
<div class="tau-bar"><i style="width:${pct}%; background:${colorFor[aid] || '#fff'}"></i></div>
<div class="meta">τ = ${c.tau.toFixed(2)} · pace = ${c.pace.toFixed(2)} op/s · ${c.n_ops} ops</div>
`;
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 = '<small>No multi-agent drift yet.</small>';
return;
}
const top = d.pairs[0];
if (top.divergent) {
wrap.className = 'drift-warn';
wrap.innerHTML = `<b>⚠ DILATION DRIFT</b> · ${top.a}${top.b}: |Δτ| = ${top.drift.toFixed(1)} (γ=${top.gamma_ab})<br>
<small>${top.a} has experienced ${top.tau_a} units, ${top.b} ${top.tau_b}. Frame transformation exceeds threshold ${d.threshold}.</small>`;
} else {
wrap.className = 'drift-ok';
wrap.innerHTML = `<b>✓ COHERENT</b> · 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 = '<small>No active proposals.</small>';
return;
}
for (const p of d.active) {
const el = document.createElement('div');
el.className = 'proposal';
el.innerHTML = `<h4>#${p.id} ${p.title}</h4>
<small>by ${p.author} · ${p.category} · ${p.votes} votes</small>
<p>${p.body}</p>`;
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 = `<b>Article ${a.id}${a.title}</b><p>${a.body}</p>`;
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 = `<span class="who">${entry.name}</span>
<span class="tool">${entry.tool}</span>
<span class="why">— ${entry.rationale || ''}</span>
<small> · ${t}</small>`;
} else {
li.innerHTML = `<small>${entry.type} · ${t}</small>`;
}
ul.prepend(li);
while (ul.children.length > 80) ul.removeChild(ul.lastChild);
}
function refreshHeader() {
document.getElementById('tick').textContent = snapshot?.tick || 0;
document.getElementById('agentCount').textContent = snapshot?.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);
});
refreshAll();
populateManual();
refreshTexts();
connectWS();
setInterval(refreshProposals, 5000);
setInterval(refreshTexts, 8000); // periodic refresh of historical texts