picoclaw/pkg/miniapp/static/map-preview.html

459 lines
14 KiB
HTML
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.

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Orchestration Room</title>
<link href="https://fonts.googleapis.com/css2?family=Silkscreen&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #060810;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 16px 0;
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 {
image-rendering: pixelated;
image-rendering: crisp-edges;
display: block;
width: 100%;
aspect-ratio: 1 / 1;
}
/* ── demo state indicator (bottom) ── */
.demo-bar {
margin-top: 10px;
font-size: 8px;
color: #2a3259;
display: flex;
gap: 16px;
justify-content: center;
}
.demo-bar span { color: #3a4a80; }
.demo-bar span.on { color: #4ade80; }
</style>
<script src="dist/map.js"></script>
</head>
<body>
<div class="room-row">
<!-- left panel: permanent characters -->
<div class="side-panel" id="panel-left">
<div class="badge alive" id="badge-conductor" data-id="conductor">
<div class="badge-emoji">👑</div>
<div class="badge-label">CNDR</div>
<div class="badge-dot"></div>
</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>
<script>
'use strict';
// ─── canvas setup ─────────────────────────────────────────────────────────
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
// ─── bob frames: 4-step cycle [down, mid, up, mid] ───────────────────────
// カクカクした待機モーション — yオフセット (px, canvas logical units)
var BOB = [0, -1, -2, -1];
// 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,
};
// move speed px/s
var WALK_SPEED = 55;
// ─── character definitions ────────────────────────────────────────────────
var PRESETS = [
{ 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' },
];
function makeChar(id, emoji, home) {
return {
id: id,
emoji: emoji,
x: home.x,
y: home.y,
home: home,
target: null,
state: 'idle',
frame: 0,
frameTimer:0,
bubble: null, // { text, ttl }
alive: false,
};
}
var conductor = makeChar('conductor', '👑', MAP_POSITIONS.conductor);
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);
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>
</body>
</html>