122 lines
4.2 KiB
TypeScript
122 lines
4.2 KiB
TypeScript
import { STATUS_COLORS, STATUS_LABELS } from "@/lib/constants";
|
|||
|
|
import { generateAppearance } from "@/lib/appearance";
|
||
|
|
import { useOfficeStore } from "@/store/office-store";
|
||
|
|
import { getDirector } from "@/sim/runtime";
|
||
|
|
import { Pawn } from "./Pawn";
|
||
|
|
|
||
|
|
const ROLE_LABELS = { lead: "Lead Agent", agent: "Agent", subagent: "Subagent" } as const;
|
||
|
|
|
||
|
|
export function SidePanel() {
|
||
|
|
const selectedAgentId = useOfficeStore((s) => s.selectedAgentId);
|
||
|
|
const agent = useOfficeStore((s) => (s.selectedAgentId ? s.agents.get(s.selectedAgentId) : undefined));
|
||
|
|
const agents = useOfficeStore((s) => s.agents);
|
||
|
|
const events = useOfficeStore((s) => s.events);
|
||
|
|
const selectAgent = useOfficeStore((s) => s.selectAgent);
|
||
|
|
const mode = useOfficeStore((s) => s.mode);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<aside className="side-panel">
|
||
|
|
{selectedAgentId && agent ? (
|
||
|
|
<div className="agent-card">
|
||
|
|
<div className="agent-card-head">
|
||
|
|
<svg viewBox="-30 -42 60 68" width="64" height="72">
|
||
|
|
<Pawn appearance={generateAppearance(agent.id)} pose="stand" motion="none" />
|
||
|
|
</svg>
|
||
|
|
<div>
|
||
|
|
<h2>
|
||
|
|
{agent.role === "lead" && <span className="mark">▸ </span>}
|
||
|
|
{agent.name}
|
||
|
|
</h2>
|
||
|
|
<span className="role-badge">{ROLE_LABELS[agent.role]}</span>
|
||
|
|
<code className="model-tag">{agent.model}</code>
|
||
|
|
</div>
|
||
|
|
<button className="close-btn" onClick={() => selectAgent(null)}>
|
||
|
|
✕
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="agent-stat-row">
|
||
|
|
<span className="status-chip" style={{ backgroundColor: `${STATUS_COLORS[agent.status]}22`, color: STATUS_COLORS[agent.status] }}>
|
||
|
|
● {STATUS_LABELS[agent.status]}
|
||
|
|
</span>
|
||
|
|
{agent.currentTool && <code className="tool-now">{agent.currentTool.name}()</code>}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<dl className="agent-facts">
|
||
|
|
<div>
|
||
|
|
<dt>工具調用</dt>
|
||
|
|
<dd>{agent.toolCallCount} 次</dd>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<dt>所在區域</dt>
|
||
|
|
<dd>{agent.zone}</dd>
|
||
|
|
</div>
|
||
|
|
{agent.parentId && (
|
||
|
|
<div>
|
||
|
|
<dt>上級</dt>
|
||
|
|
<dd>
|
||
|
|
<button className="link-btn" onClick={() => selectAgent(agent.parentId)}>
|
||
|
|
{agents.get(agent.parentId)?.name ?? agent.parentId}
|
||
|
|
</button>
|
||
|
|
</dd>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{agent.childIds.length > 0 && (
|
||
|
|
<div>
|
||
|
|
<dt>Subagents</dt>
|
||
|
|
<dd>
|
||
|
|
{agent.childIds.map((cid) => (
|
||
|
|
<button key={cid} className="link-btn" onClick={() => selectAgent(cid)}>
|
||
|
|
{agents.get(cid)?.name ?? cid}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</dd>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</dl>
|
||
|
|
|
||
|
|
{agent.toolHistory.length > 0 && (
|
||
|
|
<div className="tool-history">
|
||
|
|
<h3>最近工具</h3>
|
||
|
|
<div className="tool-chips">
|
||
|
|
{agent.toolHistory.map((t, i) => (
|
||
|
|
<code key={i}>{t}</code>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{agent.role !== "subagent" && mode === "sim" && (
|
||
|
|
<button
|
||
|
|
className="btn assign-btn"
|
||
|
|
onClick={() => {
|
||
|
|
if (!getDirector().assignTask(agent.id)) {
|
||
|
|
useOfficeStore.getState().addEvent("💬", `${agent.name} 正忙,稍後再指派`);
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
📋 指派任務
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="panel-hint">
|
||
|
|
<p>點擊辦公室裡的 agent 查看詳情</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="event-feed">
|
||
|
|
<h3>事件紀錄</h3>
|
||
|
|
<ul>
|
||
|
|
{events.map((e) => (
|
||
|
|
<li key={e.at}>
|
||
|
|
<span className="event-icon">{e.icon}</span>
|
||
|
|
{e.text}
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
</aside>
|
||
|
|
);
|
||
|
|
}
|