建立 Claude Office 虛擬辦公室(模擬 + 實況雙模式)
摘要: 參考 WW-AI-Lab/openclaw-office(MIT)的 office-2d 邏輯,重新實作 Claude 風格的 multi-agent 虛擬辦公室:四分區平面圖、走廊尋路、chibi 小人、狀態表情氣泡、 subagent 熱桌、會議聚集,以及可視化真實 Claude Code 活動的實況模式。 根本原因: 需要一個能一眼看出各專案 Claude Code 工作狀態(是否派發任務、調用工具、 派生 subagent)的監控介面,現有工具沒有對應的視覺化。 影響: 新專案,不影響既有系統。實況模式的監看伺服器僅綁 localhost:5179, 會讀取本機 ~/.claude/projects/ 的 session transcripts。 修法: - Vite 6 + React 19 + Zustand,SVG 平面圖 + CSS 動畫,實測 60fps - src/sim/director.ts:劇本模擬引擎(FSM + 會議排程) - server/watch.mjs:零依賴 Node watcher,tail JSONL 轉譯為 SSE 事件 - src/gateway/live.ts:事件對映 store action,支援模式即時切換 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { STATUS_COLORS, STATUS_LABELS } from "@/lib/constants";
|
||||
import { useOfficeStore } from "@/store/office-store";
|
||||
import { getDirector, setMode } from "@/sim/runtime";
|
||||
import type { AgentStatus } from "@/lib/types";
|
||||
|
||||
const LEGEND: AgentStatus[] = ["idle", "thinking", "tool_calling", "speaking", "spawning", "error"];
|
||||
|
||||
export function HeaderBar() {
|
||||
const paused = useOfficeStore((s) => s.paused);
|
||||
const setPaused = useOfficeStore((s) => s.setPaused);
|
||||
const speed = useOfficeStore((s) => s.speed);
|
||||
const setSpeed = useOfficeStore((s) => s.setSpeed);
|
||||
const theme = useOfficeStore((s) => s.theme);
|
||||
const setTheme = useOfficeStore((s) => s.setTheme);
|
||||
const mode = useOfficeStore((s) => s.mode);
|
||||
const agentCount = useOfficeStore((s) => s.agents.size);
|
||||
|
||||
const isLive = mode === "live";
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
<div className="header-brand">
|
||||
<span className="header-mark">✳</span>
|
||||
<div>
|
||||
<h1>Claude Office</h1>
|
||||
<p>multi-agent 虛擬辦公室 · {isLive ? "實況模式" : "模擬模式"}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="header-legend">
|
||||
{LEGEND.map((s) => (
|
||||
<span key={s} className="legend-chip">
|
||||
<span className="legend-dot" style={{ backgroundColor: STATUS_COLORS[s] }} />
|
||||
{STATUS_LABELS[s]}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="header-controls">
|
||||
<span className="agent-count">{agentCount} agents</span>
|
||||
<button
|
||||
className={`btn mode-btn ${isLive ? "live-on" : ""}`}
|
||||
onClick={() => setMode(isLive ? "sim" : "live")}
|
||||
title={isLive ? "切回劇本模擬" : "連上真實 Claude Code 活動 (需 npm run watch)"}
|
||||
>
|
||||
{isLive ? "● LIVE" : "▶ 模擬"}
|
||||
</button>
|
||||
{!isLive && (
|
||||
<>
|
||||
<button className="btn" onClick={() => getDirector().hireAgent()} title="招募一位 agent">
|
||||
+ Agent
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => setSpeed(speed >= 4 ? 1 : speed * 2)}
|
||||
title="模擬速度"
|
||||
>
|
||||
{speed}×
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button className="btn" onClick={() => setPaused(!paused)} title={paused ? "繼續" : "暫停"}>
|
||||
{paused ? "▶" : "⏸"}
|
||||
</button>
|
||||
<button className="btn" onClick={() => setTheme(theme === "light" ? "dark" : "light")} title="切換主題">
|
||||
{theme === "light" ? "🌙" : "☀️"}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user