From 27981b2c7a6b9707dbb323c8ec688fdeac8809a1 Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:32:59 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20orchestration=20room=20UI=20=E2=80=94?= =?UTF-8?q?=20side=20panels,=20bob=20animation,=20demo=20sequencer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Three-column layout: left panel (conductor/secretary), canvas, right panel (5 agent presets). Canvas CSS-scales to fill available width. - Side badges: opacity 0.3 when agent not alive, full when alive. Dot color: green=alive, orange=toolcall (fast blink), blue=waiting, yellow=talking. - Bob animation: 4-frame cycle [0,-1,-2,-1]px, speed set per state: idle 450ms/frame (1.8s cycle, calm) waiting 650ms/frame (2.6s cycle, lazy — LLM response pending) toolcall 90ms/frame (360ms cycle, rapid — tool executing) talking 280ms/frame - Speech bubbles: yellow box with pixel tail, auto-expire. - Status ring: orange halo on toolcall, blue halo on waiting. - Demo sequencer plays a full scenario (spawn → toolcall → wait → converse → gc → repeat) to demonstrate all states without a live WebSocket connection. WebSocket event contract (for future backend wiring): { type: "agent_spawn", id, task } { type: "agent_state", id, state } // toolcall | waiting | idle { type: "conversation", from, to, text } { type: "agent_gc", id } Co-Authored-By: Claude Sonnet 4.6 --- pkg/miniapp/static/map-preview.html | 472 ++++++++++++++++++++++++---- 1 file changed, 416 insertions(+), 56 deletions(-) diff --git a/pkg/miniapp/static/map-preview.html b/pkg/miniapp/static/map-preview.html index 4df562f39..5cadcae5d 100644 --- a/pkg/miniapp/static/map-preview.html +++ b/pkg/miniapp/static/map-preview.html @@ -3,10 +3,10 @@ - Orchestration Room — Preview + Orchestration Room - -
- 👑 conductor - 👩‍💼 secretary - 🔍 scout - 📊 analyst - 💻 coder - 🔧 worker - 🎯 coordinator - 🚪 door +
+ + +
+
+
👑
+
CNDR
+
+
+
+
👩‍💼
+
SEC
+
+
- + // move toward target + if (c.target) { + var dx = c.target.x - c.x; + var dy = c.target.y - c.y; + var dist = Math.sqrt(dx * dx + dy * dy); + if (dist > 1.5) { + var spd = WALK_SPEED * dt / 1000; + c.x += dx / dist * spd; + c.y += dy / dist * spd; + } else { + c.x = c.target.x; + c.y = c.target.y; + c.target = null; + if (c._onArrive) { c._onArrive(); c._onArrive = null; } + } + } + + // bubble timeout + if (c.bubble) { + c.bubble.ttl -= dt; + if (c.bubble.ttl <= 0) c.bubble = null; + } + }); +} + +// ─── draw loop ──────────────────────────────────────────────────────────── +function drawBubble(c) { + if (!c.bubble) return; + var text = c.bubble.text; + var yOff = BOB[c.frame]; + var bx = c.x; + var by = c.y + yOff - 18; + + ctx.font = '7px Silkscreen, monospace'; + var tw = ctx.measureText(text).width; + var pw = tw + 8; + var ph = 12; + + // clamp to canvas + var lx = Math.max(4, Math.min(316 - pw, bx - pw / 2)); + + // box + ctx.fillStyle = '#facc15'; + ctx.fillRect(Math.floor(lx), Math.floor(by - ph), Math.ceil(pw), Math.ceil(ph)); + // tail pixel + ctx.fillRect(Math.floor(bx) - 1, Math.floor(by), 3, 3); + // text + ctx.fillStyle = '#0a0a00'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + ctx.fillText(text, Math.floor(lx + 4), Math.floor(by - ph / 2)); +} + +function drawChar(c) { + if (!c.alive && c.state !== 'entering' && c.state !== 'exiting') return; + var yOff = BOB[c.frame]; + var cx = Math.floor(c.x); + var cy = Math.floor(c.y + yOff); + + // status ring (toolcall = orange, waiting = blue) + if (c.state === 'toolcall') { + ctx.fillStyle = 'rgba(251,146,60,0.35)'; + ctx.beginPath(); + ctx.arc(cx, cy, 13, 0, Math.PI * 2); + ctx.fill(); + } else if (c.state === 'waiting') { + ctx.fillStyle = 'rgba(96,165,250,0.25)'; + ctx.beginPath(); + ctx.arc(cx, cy, 11, 0, Math.PI * 2); + ctx.fill(); + } + + // emoji + ctx.font = '18px serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(c.emoji, cx, cy); + + // name label + ctx.font = '6px Silkscreen, monospace'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + ctx.fillStyle = c.state === 'talking' ? '#facc15' : '#3a4a7a'; + ctx.fillText(c.id.toUpperCase(), cx, cy + 11); + + drawBubble(c); +} + +function render(ts) { + if (lastTs === null) lastTs = ts; + var dt = Math.min(ts - lastTs, 80); // cap at 80ms to avoid spiral + lastTs = ts; + + update(dt); + + ctx.imageSmoothingEnabled = false; + drawMap(ctx); + + allChars().forEach(drawChar); + + requestAnimationFrame(render); +} + +// ─── demo sequencer (replaces WebSocket in preview) ────────────────────── +// WebSocket events will look like: +// { type: "agent_spawn", id: "s0", task: "find APIs" } +// { type: "agent_state", id: "s0", state: "toolcall" } +// { type: "agent_state", id: "s0", state: "waiting" } +// { type: "conversation", from: "conductor", to: "s0", text: "summary?" } +// { type: "agent_gc", id: "s0" } + +var demoLabel = document.getElementById('demo-label'); + +function demoSpawn(agent) { + agent.alive = true; + agent.x = MAP_POSITIONS.door.x; + agent.y = MAP_POSITIONS.door.y; + setState(agent, 'entering'); + syncBadge(agent.id, 'entering', true); + moveTo(agent, agent.home, function() { + setState(agent, 'idle'); + }); +} + +function demoConverse(from, to, text, reply) { + // move toward each other + var mid = { + x: (from.x + to.x) / 2, + y: (from.y + to.y) / 2, + }; + setState(from, 'talking'); + setState(to, 'talking'); + moveTo(from, { x: mid.x - 18, y: mid.y }, function() { + say(from, text, 2400); + }); + moveTo(to, { x: mid.x + 18, y: mid.y }, function() { + if (reply) setTimeout(function() { say(to, reply, 2200); }, 1600); + setTimeout(function() { + moveTo(from, from.home, function() { setState(from, 'idle'); }); + moveTo(to, to.home, function() { setState(to, 'idle'); }); + }, reply ? 3800 : 2600); + }); +} + +function demoGC(agent) { + setState(agent, 'exiting'); + moveTo(agent, MAP_POSITIONS.door, function() { + agent.alive = false; + setState(agent, 'idle'); + syncBadge(agent.id, 'idle', false); + }); +} + +// demo timeline +var demo = [ + [ 400, function() { demoLabel.textContent = 'demo: spawning scout…'; demoSpawn(agents[0]); }], + [ 1600, function() { demoLabel.textContent = 'demo: spawning coder…'; demoSpawn(agents[2]); }], + [ 2600, function() { demoLabel.textContent = 'demo: toolcall (fast bob)'; setState(agents[0], 'toolcall'); setState(agents[2], 'toolcall'); }], + [ 4800, function() { demoLabel.textContent = 'demo: llm wait (slow bob)'; setState(agents[0], 'waiting'); setState(agents[2], 'waiting'); }], + [ 7200, function() { demoLabel.textContent = 'demo: conversation'; + demoConverse(conductor, agents[0], 'found anything?', 'yes — 3 hits'); }], + [11800, function() { demoLabel.textContent = 'demo: secretary plans with coder'; + demoConverse(secretary, agents[2], 'review plan?', 'looks good'); }], + [16200, function() { demoLabel.textContent = 'demo: agent exits (gc)'; demoGC(agents[0]); }], + [18400, function() { demoLabel.textContent = 'demo: agent exits (gc)'; demoGC(agents[2]); }], + [20000, function() { + // restart + demo.forEach(function(e) { e[2] = false; }); + demoStart = performance.now(); + demoLabel.textContent = 'demo: restarting…'; + }], +]; + +var demoStart = null; +function tickDemo(ts) { + if (demoStart === null) demoStart = ts; + var elapsed = ts - demoStart; + demo.forEach(function(e) { + if (!e[2] && elapsed >= e[0]) { e[2] = true; e[1](); } + }); + requestAnimationFrame(tickDemo); +} + +// ─── init ───────────────────────────────────────────────────────────────── +loadMapAsset(function() { + requestAnimationFrame(render); + requestAnimationFrame(tickDemo); +}); +