Rewrite virtual office with pixel-art characters and compact layout

Replaced generic bot icons with CSS pixel-art people — colored head,
shirt body, and legs, each agent with a stable color derived from their
name. Layout is now compact flex-wrap (42px per agent) instead of one
agent per row. Sidebar narrowed to 172px. Tooltip anchored to right
edge to prevent clipping. Department floors use the app's panel-2
background for a clean integrated look.
This commit is contained in:
ZacharyZhang-NY committed 2026-06-30 22:20:24 -04:00
1 parent 7ff8e4becd
commit e37faf1a2e
3 files changed
+102 -173

No files matched your search

@@ -174,7 +174,7 @@ export function Shell({
{officeOpen && (
<aside className="office-sidebar">
<div className="office-sidebar-header">
<span style={{ fontSize: 12, fontWeight: 600, color: "var(--text-2)" }}>Office</span>
<span style={{ fontSize: 11, fontWeight: 600, color: "var(--text-3)" }}>Office</span>
<IconButton icon="x" label="Close office" onClick={() => setOfficeOpen(false)} />
</div>
<VirtualOffice
@@ -1,7 +1,6 @@
import { useState, type CSSProperties } from "react";
import { useState } from "react";
import type { AgentRecord, TaskRecord, TaskRunRecord } from "../../../shared/domain";
import type { ChatPresenceRecord } from "../../../shared/chat-domain";
import { Avatar } from "./ui";
interface VirtualOfficeProps {
agents: AgentRecord[];
@@ -11,237 +10,167 @@ interface VirtualOfficeProps {
orgId: string;
}
type PresenceStatus = "active" | "thinking" | "busy" | "idle";
type Status = "active" | "thinking" | "busy" | "idle";
interface AgentDesk {
interface Desk {
agent: AgentRecord;
status: PresenceStatus;
currentTask: string | null;
status: Status;
task: string | null;
}
function resolveDesks(props: VirtualOfficeProps): AgentDesk[] {
const SKIN = ["#f4c28b", "#e8b472", "#c68e5b", "#a56c3e", "#8b5232"];
const SHIRT = ["#5b8dd9", "#d95b7a", "#5bbf6f", "#d9a65b", "#9b6fd9", "#5bc4c4"];
function stableColor(palette: string[], name: string): string {
let h = 0;
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) | 0;
return palette[Math.abs(h) % palette.length];
}
function resolveDesks(props: VirtualOfficeProps): Desk[] {
const { agents, tasks, taskRuns, presence, orgId } = props;
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 running = taskRuns.some((r) => r.agentId === agent.id && r.state === "running");
return { agent, status: deriveStatus(p?.status, running), currentTask: activeTask?.title ?? null };
let status: Status = "idle";
if (running) status = "active";
else if (p?.status === "thinking") status = "thinking";
else if (p?.status === "waiting_approval" || p?.status === "busy") status = "busy";
else if (p?.status === "active" || p?.status === "online") status = "active";
return { agent, status, task: activeTask?.title ?? null };
});
}
function deriveStatus(raw: string | undefined, running: boolean): PresenceStatus {
if (running) return "active";
if (raw === "thinking") return "thinking";
if (raw === "waiting_approval" || raw === "busy") return "busy";
if (raw === "active" || raw === "online") return "active";
return "idle";
}
const COLOR: Record<PresenceStatus, string> = {
active: "#34d399",
thinking: "#818cf8",
busy: "#fbbf24",
idle: "rgba(120,120,120,0.3)",
};
export function VirtualOffice(props: VirtualOfficeProps) {
const desks = resolveDesks(props);
const [hoveredId, setHoveredId] = useState<string | null>(null);
const [hovered, setHovered] = useState<string | null>(null);
const floors = new Map<string, AgentDesk[]>();
for (const desk of desks) {
const dept = desk.agent.department;
const floors = new Map<string, Desk[]>();
for (const d of desks) {
const dept = d.agent.department;
if (!floors.has(dept)) floors.set(dept, []);
floors.get(dept)!.push(desk);
floors.get(dept)!.push(d);
}
if (desks.length === 0) return null;
const floorEntries = [...floors.entries()];
const activeCount = desks.filter((d) => d.status === "active" || d.status === "thinking").length;
return (
<div style={{ padding: "8px 6px", overflowY: "auto", height: "100%" }}>
<BuildingHeader total={desks.length} active={activeCount} />
<div style={buildingFrame}>
{floorEntries.map(([dept, deptDesks], index) => (
<Floor
key={dept}
label={dept}
level={floorEntries.length - index}
desks={deptDesks}
hoveredId={hoveredId}
onHover={setHoveredId}
/>
))}
</div>
<div style={{ padding: "6px 4px", overflowY: "auto", overflowX: "hidden", height: "100%" }}>
{[...floors.entries()].map(([dept, agents]) => (
<div key={dept} style={{ marginBottom: 8 }}>
<div style={{ fontSize: 9, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.08em", color: "var(--text-4)", padding: "0 6px 3px" }}>
{dept}
</div>
<div style={{
background: "var(--panel-2)",
borderRadius: 8,
padding: "6px 4px",
display: "flex",
flexWrap: "wrap",
gap: 2,
}}>
{agents.map((d) => (
<PixelDesk key={d.agent.id} desk={d} isHovered={hovered === d.agent.id} onHover={setHovered} />
))}
</div>
</div>
))}
</div>
);
}
function BuildingHeader({ total, active }: { total: number; active: number }) {
return (
<div style={{ display: "flex", alignItems: "center", gap: 6, padding: "0 4px 8px", justifyContent: "space-between" }}>
<div style={{ display: "flex", alignItems: "center", gap: 4 }}>
<div style={{ width: 6, height: 6, borderRadius: "50%", background: active > 0 ? "#34d399" : "rgba(120,120,120,0.3)" }} />
<span style={{ fontSize: 10, color: "var(--text-3)" }}>{active}/{total}</span>
</div>
</div>
);
}
function PixelDesk({ desk, isHovered, onHover }: { desk: Desk; isHovered: boolean; onHover: (id: string | null) => void }) {
const skin = stableColor(SKIN, desk.agent.name);
const shirt = stableColor(SHIRT, desk.agent.name + desk.agent.role);
const glow = desk.status === "active" || desk.status === "thinking";
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 (
<div style={{ borderBottom: "1px solid rgba(255,255,255,0.04)" }}>
<div style={{
display: "flex",
alignItems: "center",
gap: 4,
padding: "4px 8px 2px",
}}>
<span style={{ fontSize: 8, fontWeight: 700, color: "var(--text-4)", fontVariantNumeric: "tabular-nums", width: 12 }}>{level}F</span>
<span style={{ fontSize: 9, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.06em", color: "var(--text-4)" }}>{label}</span>
</div>
<div style={{
padding: "4px 6px 8px",
display: "flex",
flexWrap: "wrap",
gap: 3,
}}>
{desks.map((desk) => (
<Workstation key={desk.agent.id} desk={desk} hovered={hoveredId === desk.agent.id} onHover={onHover} />
))}
</div>
</div>
);
}
function Workstation({ desk, hovered, onHover }: { desk: AgentDesk; hovered: boolean; onHover: (id: string | null) => void }) {
const color = COLOR[desk.status];
const alive = desk.status === "active" || desk.status === "thinking";
return (
<div
style={{
width: 56,
padding: "5px 3px 4px",
borderRadius: 8,
width: 42,
display: "flex",
flexDirection: "column",
alignItems: "center",
position: "relative",
padding: "4px 0 2px",
borderRadius: 6,
cursor: "default",
transition: "background 0.15s",
background: hovered ? "rgba(255,255,255,0.05)" : "transparent",
position: "relative",
background: isHovered ? "var(--hover)" : "transparent",
}}
onMouseEnter={() => onHover(desk.agent.id)}
onMouseLeave={() => onHover(null)}
>
{/* Monitor */}
<div style={{
width: 24,
height: 15,
borderRadius: 2,
border: `1.5px solid ${alive ? color : "rgba(255,255,255,0.08)"}`,
marginBottom: 1,
position: "relative",
overflow: "hidden",
transition: "border-color 0.2s",
}}>
{alive && <ScreenGlow color={color} />}
</div>
{/* Monitor stand */}
<div style={{ width: 4, height: 2, background: "rgba(255,255,255,0.06)" }} />
<div style={{ width: 10, height: 1, background: "rgba(255,255,255,0.06)", borderRadius: 1, marginBottom: 3 }} />
{/* Agent */}
<div style={{ position: "relative" }}>
<Avatar name={desk.agent.name} size="sm" bot />
{/* Pixel person */}
<div style={{ position: "relative", width: 16, height: 24 }}>
{/* Head */}
<div style={{ width: 8, height: 8, borderRadius: 2, background: skin, margin: "0 auto" }} />
{/* Body */}
<div style={{ width: 12, height: 8, borderRadius: "2px 2px 0 0", background: shirt, margin: "1px auto 0" }} />
{/* Legs */}
<div style={{ display: "flex", justifyContent: "center", gap: 2 }}>
<div style={{ width: 4, height: 6, background: "#555", borderRadius: "0 0 1px 1px" }} />
<div style={{ width: 4, height: 6, background: "#555", borderRadius: "0 0 1px 1px" }} />
</div>
{/* Status dot */}
<div style={{
position: "absolute",
bottom: -1,
right: -2,
width: 6,
height: 6,
top: -1,
right: -1,
width: 5,
height: 5,
borderRadius: "50%",
background: color,
border: "1.5px solid var(--bg)",
transition: "background 0.2s",
background: statusColor(desk.status),
boxShadow: glow ? `0 0 4px ${statusColor(desk.status)}` : undefined,
}} />
</div>
{/* Desk surface */}
<div style={{ width: 20, height: 3, background: "var(--border)", borderRadius: 1, marginTop: 1 }} />
{/* Name */}
<div style={{
fontSize: 8,
fontSize: 7,
color: "var(--text-3)",
marginTop: 2,
textAlign: "center",
maxWidth: 52,
maxWidth: 40,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
lineHeight: 1,
}}>
{desk.agent.name.split(" ")[0]}
</div>
{hovered && <HoverCard desk={desk} />}
{isHovered && <Tip desk={desk} />}
</div>
);
}
function ScreenGlow({ color }: { color: string }) {
function Tip({ desk }: { desk: Desk }) {
return (
<div style={{
position: "absolute",
inset: 0,
background: `${color}33`,
animation: "office-pulse 2s ease-in-out infinite",
}}>
<div style={{ width: "60%", height: 2, background: `${color}55`, margin: "3px 2px", borderRadius: 1 }} />
<div style={{ width: "40%", height: 2, background: `${color}44`, margin: "1px 2px", borderRadius: 1 }} />
</div>
);
}
function HoverCard({ desk }: { desk: AgentDesk }) {
const color = COLOR[desk.status];
return (
<div style={{
position: "absolute",
bottom: "calc(100% + 4px)",
left: "50%",
transform: "translateX(-50%)",
bottom: "calc(100% + 2px)",
right: 0,
background: "var(--panel)",
border: "1px solid var(--border)",
borderRadius: 8,
padding: "6px 10px",
zIndex: 30,
boxShadow: "0 4px 20px rgba(0,0,0,0.2)",
borderRadius: 6,
padding: "4px 8px",
zIndex: 50,
boxShadow: "0 2px 10px rgba(0,0,0,0.15)",
pointerEvents: "none",
minWidth: 110,
maxWidth: 190,
whiteSpace: "nowrap",
maxWidth: 180,
}}>
<div style={{ fontSize: 11, fontWeight: 560, color: "var(--text-1)" }}>{desk.agent.name}</div>
<div style={{ fontSize: 9, color: "var(--text-3)", marginBottom: desk.currentTask ? 3 : 0 }}>{desk.agent.role}</div>
{desk.currentTask && (
<div style={{
fontSize: 9,
color,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}>
{desk.currentTask}
</div>
)}
<div style={{ fontSize: 9, color: "var(--text-3)" }}>{desk.agent.role}</div>
{desk.task && <div style={{ fontSize: 9, color: statusColor(desk.status), overflow: "hidden", textOverflow: "ellipsis", marginTop: 2 }}>{desk.task}</div>}
</div>
);
}
function statusColor(s: Status): string {
if (s === "active") return "#34d399";
if (s === "thinking") return "#818cf8";
if (s === "busy") return "#fbbf24";
return "#ccc";
}
+3 -3
View File
@@ -370,7 +370,7 @@ a { color: inherit; }
}
.office-sidebar {
width: 220px;
width: 172px;
flex-shrink: 0;
display: flex;
flex-direction: column;
@@ -379,12 +379,12 @@ a { color: inherit; }
overflow: hidden;
}
.office-sidebar-header {
height: 48px;
height: 40px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--sp-3);
padding: 0 var(--sp-2) 0 var(--sp-3);
border-bottom: 1px solid var(--border-soft);
}