From 7d9995f62c0bd171328b7fa38caf2dabe2006cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 30 Jun 2026 21:54:05 -0400 Subject: [PATCH] Polish virtual office with building floors and animated workstations Inspired by claude-office's multi-floor building metaphor but rendered as a polished UI instead of pixel art. Departments appear as numbered floors in a building frame. Each workstation has a monitor with animated screen glow for active agents, a stand, and a status dot. The building header shows the active/total agent ratio at a glance. --- .../src/renderer/src/shared/VirtualOffice.tsx | 204 ++++++++++-------- AgentCompany-New/src/renderer/src/styles.css | 4 + 2 files changed, 121 insertions(+), 87 deletions(-) diff --git a/AgentCompany-New/src/renderer/src/shared/VirtualOffice.tsx b/AgentCompany-New/src/renderer/src/shared/VirtualOffice.tsx index 789dedbe..16a766af 100644 --- a/AgentCompany-New/src/renderer/src/shared/VirtualOffice.tsx +++ b/AgentCompany-New/src/renderer/src/shared/VirtualOffice.tsx @@ -17,7 +17,6 @@ interface AgentDesk { agent: AgentRecord; status: PresenceStatus; currentTask: string | null; - runningNow: boolean; } function resolveDesks(props: VirtualOfficeProps): AgentDesk[] { @@ -25,103 +24,102 @@ function resolveDesks(props: VirtualOfficeProps): AgentDesk[] { return agents.filter((a) => a.orgId === orgId).map((agent) => { const p = presence.find((r) => r.principalType === "agent" && r.principalId === agent.id); const activeTask = tasks.find((t) => t.orgId === orgId && t.assigneeAgentId === agent.id && t.status === "in_progress"); - const runningNow = taskRuns.some((r) => r.agentId === agent.id && r.state === "running"); - return { agent, status: mapStatus(p?.status, runningNow), currentTask: activeTask?.title ?? null, runningNow }; + const running = taskRuns.some((r) => r.agentId === agent.id && r.state === "running"); + return { agent, status: deriveStatus(p?.status, running), currentTask: activeTask?.title ?? null }; }); } -function mapStatus(raw: string | undefined, running: boolean): PresenceStatus { +function deriveStatus(raw: string | undefined, running: boolean): PresenceStatus { if (running) return "active"; - if (!raw) return "idle"; if (raw === "thinking") return "thinking"; if (raw === "waiting_approval" || raw === "busy") return "busy"; if (raw === "active" || raw === "online") return "active"; return "idle"; } -const STATUS_COLOR: Record = { +const COLOR: Record = { active: "#34d399", thinking: "#818cf8", busy: "#fbbf24", - idle: "rgba(120,120,120,0.35)", -}; - -const DESK_BG: CSSProperties = { - background: "rgba(255,255,255,0.03)", - border: "1px solid rgba(255,255,255,0.06)", - borderRadius: 10, - padding: "10px 6px 8px", - display: "flex", - flexDirection: "column", - alignItems: "center", - position: "relative", - cursor: "default", - transition: "border-color 0.15s, box-shadow 0.15s", -}; - -const MONITOR: CSSProperties = { - width: 28, - height: 18, - borderRadius: 3, - border: "1.5px solid rgba(255,255,255,0.12)", - marginBottom: 2, - display: "flex", - alignItems: "center", - justifyContent: "center", - fontSize: 8, - position: "relative", - overflow: "hidden", + idle: "rgba(120,120,120,0.3)", }; export function VirtualOffice(props: VirtualOfficeProps) { const desks = resolveDesks(props); const [hoveredId, setHoveredId] = useState(null); - const departments = new Map(); + const floors = new Map(); for (const desk of desks) { const dept = desk.agent.department; - if (!departments.has(dept)) departments.set(dept, []); - departments.get(dept)!.push(desk); + if (!floors.has(dept)) floors.set(dept, []); + floors.get(dept)!.push(desk); } if (desks.length === 0) return null; + const floorEntries = [...floors.entries()]; + const activeCount = desks.filter((d) => d.status === "active" || d.status === "thinking").length; + return ( -
- {[...departments.entries()].map(([dept, deptDesks]) => ( - - ))} +
+ +
+ {floorEntries.map(([dept, deptDesks], index) => ( + + ))} +
); } -function Room({ label, desks, hoveredId, onHover }: { +function BuildingHeader({ total, active }: { total: number; active: number }) { + return ( +
+
+
0 ? "#34d399" : "rgba(120,120,120,0.3)" }} /> + {active}/{total} +
+
+ ); +} + +const buildingFrame: CSSProperties = { + border: "1px solid rgba(255,255,255,0.06)", + borderRadius: 12, + overflow: "hidden", + background: "rgba(0,0,0,0.08)", +}; + +function Floor({ label, level, desks, hoveredId, onHover }: { label: string; + level: number; desks: AgentDesk[]; hoveredId: string | null; onHover: (id: string | null) => void; }) { return ( -
+
- {label} + {level}F + {label}
{desks.map((desk) => ( @@ -132,61 +130,93 @@ function Room({ label, desks, hoveredId, onHover }: { } function Workstation({ desk, hovered, onHover }: { desk: AgentDesk; hovered: boolean; onHover: (id: string | null) => void }) { - const color = STATUS_COLOR[desk.status]; - const glowing = desk.status === "active" || desk.status === "thinking"; + const color = COLOR[desk.status]; + const alive = desk.status === "active" || desk.status === "thinking"; return (
onHover(desk.agent.id)} onMouseLeave={() => onHover(null)} > -
- {glowing &&
} + {/* Monitor */} +
+ {alive && }
-
+ {/* Monitor stand */} +
+
+ {/* Agent */}
{desk.agent.name.split(" ")[0]}
- {hovered && ( - - )} + {hovered && }
); } -function Tooltip({ name, role, task, status }: { name: string; role: string; task: string | null; status: PresenceStatus }) { +function ScreenGlow({ color }: { color: string }) { return (
+
+
+
+ ); +} + +function HoverCard({ desk }: { desk: AgentDesk }) { + const color = COLOR[desk.status]; + return ( +
-
{name}
-
{role}
- {task && ( +
{desk.agent.name}
+
{desk.agent.role}
+ {desk.currentTask && (
- {task} + {desk.currentTask}
)}
diff --git a/AgentCompany-New/src/renderer/src/styles.css b/AgentCompany-New/src/renderer/src/styles.css index a4a0922d..e331ed15 100644 --- a/AgentCompany-New/src/renderer/src/styles.css +++ b/AgentCompany-New/src/renderer/src/styles.css @@ -434,6 +434,10 @@ a { color: inherit; } from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } } +@keyframes office-pulse { + 0%, 100% { opacity: 0.6; } + 50% { opacity: 1; } +} /* Split-layout routes fill the content area and let their panes scroll independently instead of scrolling the whole page. */ .content.flush {