feat: orchestration room UI — side panels, bob animation, demo sequencer
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
10a932a024
commit
27981b2c7a
1 changed files with 416 additions and 56 deletions
|
|
@ -3,10 +3,10 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Orchestration Room — Preview</title>
|
<title>Orchestration Room</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Silkscreen&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Silkscreen&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: #060810;
|
background: #060810;
|
||||||
|
|
@ -15,83 +15,443 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
gap: 16px;
|
padding: 16px 0;
|
||||||
padding: 24px;
|
font-family: 'Silkscreen', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── three-column layout ── */
|
||||||
|
.room-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding-top: 24px;
|
||||||
|
width: 44px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
opacity: 0.3;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.badge.alive { opacity: 1; }
|
||||||
|
.badge.talking { opacity: 1; }
|
||||||
|
|
||||||
|
.badge-emoji { font-size: 18px; line-height: 1; }
|
||||||
|
|
||||||
|
.badge-label {
|
||||||
|
font-size: 7px;
|
||||||
|
color: #3a4259;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.badge.alive .badge-label { color: #4a6ac0; }
|
||||||
|
.badge.talking .badge-label { color: #facc15; }
|
||||||
|
|
||||||
|
.badge-dot {
|
||||||
|
width: 4px; height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #1a1f2e;
|
||||||
|
}
|
||||||
|
.badge.alive .badge-dot { background: #4ade80; }
|
||||||
|
.badge.toolcall .badge-dot { background: #fb923c; animation: blink 0.2s step-end infinite; }
|
||||||
|
.badge.waiting .badge-dot { background: #60a5fa; }
|
||||||
|
.badge.talking .badge-dot { background: #facc15; animation: blink 0.6s step-end infinite; }
|
||||||
|
|
||||||
|
@keyframes blink { 50% { opacity: 0; } }
|
||||||
|
|
||||||
|
/* ── canvas ── */
|
||||||
|
.canvas-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
canvas {
|
canvas {
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
/* 2× for retina preview */
|
|
||||||
width: 320px;
|
|
||||||
height: 320px;
|
|
||||||
border: 1px solid #1a1f2e;
|
|
||||||
display: block;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend {
|
/* ── demo state indicator (bottom) ── */
|
||||||
font-family: 'Silkscreen', monospace;
|
.demo-bar {
|
||||||
font-size: 9px;
|
margin-top: 10px;
|
||||||
color: #3a4259;
|
font-size: 8px;
|
||||||
|
color: #2a3259;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 16px;
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.demo-bar span { color: #3a4a80; }
|
||||||
.legend span { color: #64748b; }
|
.demo-bar span.on { color: #4ade80; }
|
||||||
</style>
|
</style>
|
||||||
<script src="map.js"></script>
|
<script src="map.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="c" width="320" height="320"></canvas>
|
|
||||||
|
|
||||||
<div class="legend">
|
<div class="room-row">
|
||||||
<span>👑 conductor</span>
|
|
||||||
<span>👩💼 secretary</span>
|
<!-- left panel: permanent characters -->
|
||||||
<span>🔍 scout</span>
|
<div class="side-panel" id="panel-left">
|
||||||
<span>📊 analyst</span>
|
<div class="badge alive" id="badge-conductor" data-id="conductor">
|
||||||
<span>💻 coder</span>
|
<div class="badge-emoji">👑</div>
|
||||||
<span>🔧 worker</span>
|
<div class="badge-label">CNDR</div>
|
||||||
<span>🎯 coordinator</span>
|
<div class="badge-dot"></div>
|
||||||
<span>🚪 door</span>
|
</div>
|
||||||
|
<div class="badge alive" id="badge-secretary" data-id="secretary">
|
||||||
|
<div class="badge-emoji">👩💼</div>
|
||||||
|
<div class="badge-label">SEC</div>
|
||||||
|
<div class="badge-dot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- canvas -->
|
||||||
|
<div class="canvas-wrap">
|
||||||
|
<canvas id="c" width="320" height="320"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- right panel: subagent presets -->
|
||||||
|
<div class="side-panel" id="panel-right">
|
||||||
|
<div class="badge" id="badge-s0" data-id="s0">
|
||||||
|
<div class="badge-emoji">🔍</div>
|
||||||
|
<div class="badge-label">SCOUT</div>
|
||||||
|
<div class="badge-dot"></div>
|
||||||
|
</div>
|
||||||
|
<div class="badge" id="badge-s1" data-id="s1">
|
||||||
|
<div class="badge-emoji">📊</div>
|
||||||
|
<div class="badge-label">ANLY</div>
|
||||||
|
<div class="badge-dot"></div>
|
||||||
|
</div>
|
||||||
|
<div class="badge" id="badge-s2" data-id="s2">
|
||||||
|
<div class="badge-emoji">💻</div>
|
||||||
|
<div class="badge-label">CODE</div>
|
||||||
|
<div class="badge-dot"></div>
|
||||||
|
</div>
|
||||||
|
<div class="badge" id="badge-s3" data-id="s3">
|
||||||
|
<div class="badge-emoji">🔧</div>
|
||||||
|
<div class="badge-label">WRKR</div>
|
||||||
|
<div class="badge-dot"></div>
|
||||||
|
</div>
|
||||||
|
<div class="badge" id="badge-s4" data-id="s4">
|
||||||
|
<div class="badge-emoji">🎯</div>
|
||||||
|
<div class="badge-label">CORD</div>
|
||||||
|
<div class="badge-dot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-bar">
|
||||||
|
<span id="demo-label">demo: idle</span>
|
||||||
|
<span>⬡ fast = toolcall</span>
|
||||||
|
<span>⬡ slow = llm wait</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ─── canvas setup ─────────────────────────────────────────────────────────
|
||||||
var canvas = document.getElementById('c');
|
var canvas = document.getElementById('c');
|
||||||
var ctx = canvas.getContext('2d');
|
var ctx = canvas.getContext('2d');
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
var EMOJIS = {
|
// ─── bob frames: 4-step cycle [down, mid, up, mid] ───────────────────────
|
||||||
conductor: '👑',
|
// カクカクした待機モーション — yオフセット (px, canvas logical units)
|
||||||
secretary: '👩💼',
|
var BOB = [0, -1, -2, -1];
|
||||||
meeting: null,
|
|
||||||
stations: ['🔍', '📊', '💻', '🔧', '🎯'],
|
// frame duration per state (ms/frame → full cycle = 4×)
|
||||||
|
var FRAME_MS = {
|
||||||
|
idle: 450, // 1.8s cycle — very calm
|
||||||
|
waiting: 650, // 2.6s cycle — lazy, LLM response pending
|
||||||
|
toolcall: 90, // 360ms cycle — rapid, tool executing
|
||||||
|
talking: 280, // 1.1s cycle — attentive
|
||||||
|
entering: 220,
|
||||||
|
exiting: 220,
|
||||||
};
|
};
|
||||||
|
|
||||||
function drawCharacters() {
|
// move speed px/s
|
||||||
ctx.font = '20px serif';
|
var WALK_SPEED = 55;
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
|
|
||||||
// permanent characters
|
// ─── character definitions ────────────────────────────────────────────────
|
||||||
ctx.fillText(EMOJIS.conductor, MAP_POSITIONS.conductor.x, MAP_POSITIONS.conductor.y);
|
var PRESETS = [
|
||||||
ctx.fillText(EMOJIS.secretary, MAP_POSITIONS.secretary.x, MAP_POSITIONS.secretary.y);
|
{ id: 's0', emoji: '🔍', label: 'scout' },
|
||||||
|
{ id: 's1', emoji: '📊', label: 'analyst' },
|
||||||
|
{ id: 's2', emoji: '💻', label: 'coder' },
|
||||||
|
{ id: 's3', emoji: '🔧', label: 'worker' },
|
||||||
|
{ id: 's4', emoji: '🎯', label: 'coordinator' },
|
||||||
|
];
|
||||||
|
|
||||||
// agents at their stations
|
function makeChar(id, emoji, home) {
|
||||||
MAP_POSITIONS.stations.forEach(function(pos, i) {
|
return {
|
||||||
ctx.fillText(EMOJIS.stations[i], pos.x, pos.y);
|
id: id,
|
||||||
});
|
emoji: emoji,
|
||||||
|
x: home.x,
|
||||||
// door marker
|
y: home.y,
|
||||||
ctx.font = '14px serif';
|
home: home,
|
||||||
ctx.fillText('🚪', MAP_POSITIONS.door.x, MAP_POSITIONS.door.y - 8);
|
target: null,
|
||||||
|
state: 'idle',
|
||||||
|
frame: 0,
|
||||||
|
frameTimer:0,
|
||||||
|
bubble: null, // { text, ttl }
|
||||||
|
alive: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// load map.png if it exists, otherwise use procedural fallback
|
var conductor = makeChar('conductor', '👑', MAP_POSITIONS.conductor);
|
||||||
loadMapAsset(function() {
|
var secretary = makeChar('secretary', '👩💼', MAP_POSITIONS.secretary);
|
||||||
|
conductor.alive = true;
|
||||||
|
secretary.alive = true;
|
||||||
|
|
||||||
|
// subagents start at door (off-screen), not alive
|
||||||
|
var agents = PRESETS.map(function(p, i) {
|
||||||
|
var c = makeChar(p.id, p.emoji, MAP_POSITIONS.stations[i]);
|
||||||
|
c.x = MAP_POSITIONS.door.x;
|
||||||
|
c.y = MAP_POSITIONS.door.y;
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
|
||||||
|
function allChars() {
|
||||||
|
return [conductor, secretary].concat(agents);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── character helpers ────────────────────────────────────────────────────
|
||||||
|
function setState(c, state) {
|
||||||
|
c.state = state;
|
||||||
|
syncBadge(c.id, state, c.alive);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveTo(c, pos, onArrive) {
|
||||||
|
c.target = pos;
|
||||||
|
c._onArrive = onArrive || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function say(c, text, ttl) {
|
||||||
|
c.bubble = { text: text, ttl: ttl || 2200 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncBadge(id, state, alive) {
|
||||||
|
var el = document.getElementById('badge-' + id);
|
||||||
|
if (!el) return;
|
||||||
|
el.className = 'badge'
|
||||||
|
+ (alive ? ' alive' : '')
|
||||||
|
+ (state === 'talking' ? ' talking' : '')
|
||||||
|
+ (state === 'toolcall' ? ' toolcall' : '')
|
||||||
|
+ (state === 'waiting' ? ' waiting' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── update loop ──────────────────────────────────────────────────────────
|
||||||
|
var lastTs = null;
|
||||||
|
|
||||||
|
function update(dt) {
|
||||||
|
allChars().forEach(function(c) {
|
||||||
|
if (!c.alive && c.state !== 'entering') return;
|
||||||
|
|
||||||
|
// advance bob frame
|
||||||
|
c.frameTimer += dt;
|
||||||
|
var dur = FRAME_MS[c.state] || 450;
|
||||||
|
if (c.frameTimer >= dur) {
|
||||||
|
c.frame = (c.frame + 1) % 4;
|
||||||
|
c.frameTimer -= dur;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
drawMap(ctx);
|
||||||
drawCharacters();
|
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue