Redesign virtual office with desk workstations and spatial layout
Each agent sits at a workstation with a monitor, status-colored glow for active agents, and a rich tooltip showing name, role, and current task. Departments render as labeled rooms with subtle borders. The monitor lights up when the agent has a running task run, giving a real sense of who is working and who is idle.
This commit is contained in:
1 parent
453e47274a
commit
3a3c5b7ca3
2 files changed
+133
-119
No files matched your search
@@ -180,6 +180,7 @@ export function Shell({
|
||||
<VirtualOffice
|
||||
agents={snapshot.agents}
|
||||
tasks={snapshot.tasks}
|
||||
taskRuns={snapshot.taskRuns}
|
||||
presence={snapshot.chatPresence}
|
||||
orgId={organization.id}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import type { AgentRecord, TaskRecord } from "../../../shared/domain";
|
||||
import { useState, type CSSProperties } 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[];
|
||||
tasks: TaskRecord[];
|
||||
taskRuns: TaskRunRecord[];
|
||||
presence: ChatPresenceRecord[];
|
||||
orgId: string;
|
||||
}
|
||||
@@ -15,29 +16,22 @@ type PresenceStatus = "active" | "thinking" | "busy" | "idle";
|
||||
interface AgentDesk {
|
||||
agent: AgentRecord;
|
||||
status: PresenceStatus;
|
||||
statusText: string;
|
||||
currentTask: string | null;
|
||||
runningNow: boolean;
|
||||
}
|
||||
|
||||
function resolveDesks(props: VirtualOfficeProps): AgentDesk[] {
|
||||
const { agents, tasks, presence, orgId } = props;
|
||||
const orgAgents = agents.filter((a) => a.orgId === orgId);
|
||||
return orgAgents.map((agent) => {
|
||||
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 status = mapStatus(p?.status);
|
||||
return {
|
||||
agent,
|
||||
status,
|
||||
statusText: p?.statusText ?? "",
|
||||
currentTask: activeTask?.title ?? null,
|
||||
};
|
||||
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 };
|
||||
});
|
||||
}
|
||||
|
||||
function mapStatus(raw: string | undefined): PresenceStatus {
|
||||
function mapStatus(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";
|
||||
@@ -45,25 +39,39 @@ function mapStatus(raw: string | undefined): PresenceStatus {
|
||||
return "idle";
|
||||
}
|
||||
|
||||
const STATUS_RING: Record<PresenceStatus, string> = {
|
||||
active: "var(--green)",
|
||||
thinking: "var(--accent)",
|
||||
busy: "var(--amber)",
|
||||
idle: "var(--text-4)",
|
||||
const STATUS_COLOR: Record<PresenceStatus, string> = {
|
||||
active: "#34d399",
|
||||
thinking: "#818cf8",
|
||||
busy: "#fbbf24",
|
||||
idle: "rgba(120,120,120,0.35)",
|
||||
};
|
||||
|
||||
const DEPT_FLOOR: Record<string, { label: string; color: string }> = {
|
||||
engineering: { label: "Engineering", color: "var(--accent-soft)" },
|
||||
product: { label: "Product", color: "rgba(168, 130, 255, 0.08)" },
|
||||
design: { label: "Design", color: "rgba(255, 170, 130, 0.08)" },
|
||||
marketing: { label: "Marketing", color: "rgba(130, 200, 255, 0.08)" },
|
||||
operations: { label: "Operations", color: "rgba(180, 180, 180, 0.06)" },
|
||||
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",
|
||||
};
|
||||
|
||||
function floorForDept(dept: string): { label: string; color: string } {
|
||||
const key = dept.toLowerCase();
|
||||
return DEPT_FLOOR[key] ?? { label: dept, color: "rgba(180, 180, 180, 0.06)" };
|
||||
}
|
||||
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",
|
||||
};
|
||||
|
||||
export function VirtualOffice(props: VirtualOfficeProps) {
|
||||
const desks = resolveDesks(props);
|
||||
@@ -76,127 +84,132 @@ export function VirtualOffice(props: VirtualOfficeProps) {
|
||||
departments.get(dept)!.push(desk);
|
||||
}
|
||||
|
||||
if (desks.length === 0) {
|
||||
return <div style={{ padding: 24, color: "var(--text-3)", fontSize: 13 }}>No agents yet</div>;
|
||||
}
|
||||
if (desks.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div style={{ padding: "12px 8px", overflowY: "auto", height: "100%" }}>
|
||||
{[...departments.entries()].map(([dept, deptDesks]) => {
|
||||
const floor = floorForDept(dept);
|
||||
return (
|
||||
<div key={dept} style={{ marginBottom: 16 }}>
|
||||
<div style={{
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
color: "var(--text-3)",
|
||||
padding: "0 8px 6px",
|
||||
}}>
|
||||
{floor.label}
|
||||
</div>
|
||||
<div style={{
|
||||
background: floor.color,
|
||||
borderRadius: 10,
|
||||
padding: 8,
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fill, 64px)",
|
||||
gap: 4,
|
||||
justifyContent: "start",
|
||||
}}>
|
||||
{deptDesks.map((desk) => (
|
||||
<DeskCell
|
||||
key={desk.agent.id}
|
||||
desk={desk}
|
||||
hovered={hoveredId === desk.agent.id}
|
||||
onHover={setHoveredId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div style={{ padding: "12px 10px", overflowY: "auto", height: "100%" }}>
|
||||
{[...departments.entries()].map(([dept, deptDesks]) => (
|
||||
<Room key={dept} label={dept} desks={deptDesks} hoveredId={hoveredId} onHover={setHoveredId} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DeskCell({
|
||||
desk,
|
||||
hovered,
|
||||
onHover,
|
||||
}: {
|
||||
desk: AgentDesk;
|
||||
hovered: boolean;
|
||||
function Room({ label, desks, hoveredId, onHover }: {
|
||||
label: string;
|
||||
desks: AgentDesk[];
|
||||
hoveredId: string | null;
|
||||
onHover: (id: string | null) => void;
|
||||
}) {
|
||||
const ring = STATUS_RING[desk.status];
|
||||
const isWorking = desk.status === "active" || desk.status === "thinking";
|
||||
return (
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<div style={{
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.06em",
|
||||
color: "var(--text-4)",
|
||||
padding: "0 4px 5px",
|
||||
}}>
|
||||
{label}
|
||||
</div>
|
||||
<div style={{
|
||||
border: "1px solid rgba(255,255,255,0.05)",
|
||||
borderRadius: 12,
|
||||
padding: 6,
|
||||
background: "rgba(255,255,255,0.015)",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fill, 72px)",
|
||||
gap: 4,
|
||||
justifyContent: "start",
|
||||
}}>
|
||||
{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 = STATUS_COLOR[desk.status];
|
||||
const glowing = desk.status === "active" || desk.status === "thinking";
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
padding: "6px 2px",
|
||||
borderRadius: 8,
|
||||
cursor: "default",
|
||||
position: "relative",
|
||||
transition: "background 0.15s",
|
||||
background: hovered ? "rgba(255,255,255,0.06)" : "transparent",
|
||||
...DESK_BG,
|
||||
borderColor: hovered ? "rgba(255,255,255,0.14)" : undefined,
|
||||
boxShadow: glowing ? `0 0 12px -4px ${color}` : undefined,
|
||||
}}
|
||||
onMouseEnter={() => onHover(desk.agent.id)}
|
||||
onMouseLeave={() => onHover(null)}
|
||||
>
|
||||
<div style={{ ...MONITOR, borderColor: glowing ? color : undefined }}>
|
||||
{glowing && <div style={{ width: "100%", height: "100%", background: `${color}22` }} />}
|
||||
</div>
|
||||
<div style={{ width: 6, height: 3, background: "rgba(255,255,255,0.08)", borderRadius: "0 0 2px 2px", marginBottom: 4 }} />
|
||||
<div style={{ position: "relative" }}>
|
||||
<Avatar name={desk.agent.name} size="lg" bot />
|
||||
<Avatar name={desk.agent.name} size="sm" bot />
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
bottom: -1,
|
||||
right: -1,
|
||||
width: 10,
|
||||
height: 10,
|
||||
right: -2,
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: "50%",
|
||||
background: ring,
|
||||
border: "2px solid var(--panel)",
|
||||
boxShadow: isWorking ? `0 0 4px ${ring}` : undefined,
|
||||
background: color,
|
||||
border: "1.5px solid var(--bg)",
|
||||
boxShadow: glowing ? `0 0 5px ${color}` : undefined,
|
||||
}} />
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 10,
|
||||
color: "var(--text-2)",
|
||||
marginTop: 4,
|
||||
fontSize: 9,
|
||||
color: "var(--text-3)",
|
||||
marginTop: 3,
|
||||
textAlign: "center",
|
||||
lineHeight: 1.2,
|
||||
maxWidth: 60,
|
||||
lineHeight: 1.15,
|
||||
maxWidth: 64,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}>
|
||||
{desk.agent.name.split(" ")[0]}
|
||||
</div>
|
||||
{hovered && desk.currentTask && (
|
||||
{hovered && (
|
||||
<Tooltip name={desk.agent.name} role={desk.agent.role} task={desk.currentTask} status={desk.status} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Tooltip({ name, role, task, status }: { name: string; role: string; task: string | null; status: PresenceStatus }) {
|
||||
return (
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
bottom: "calc(100% + 6px)",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
background: "var(--panel)",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 8,
|
||||
padding: "6px 10px",
|
||||
zIndex: 30,
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.18)",
|
||||
pointerEvents: "none",
|
||||
minWidth: 120,
|
||||
maxWidth: 200,
|
||||
}}>
|
||||
<div style={{ fontSize: 12, fontWeight: 560, color: "var(--text-1)", marginBottom: 1 }}>{name}</div>
|
||||
<div style={{ fontSize: 10, color: "var(--text-3)", marginBottom: task ? 4 : 0 }}>{role}</div>
|
||||
{task && (
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
bottom: "100%",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
background: "var(--panel)",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 6,
|
||||
padding: "4px 8px",
|
||||
fontSize: 11,
|
||||
color: "var(--text-1)",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: 200,
|
||||
fontSize: 10,
|
||||
color: STATUS_COLOR[status],
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
zIndex: 20,
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.12)",
|
||||
marginBottom: 4,
|
||||
pointerEvents: "none",
|
||||
whiteSpace: "nowrap",
|
||||
}}>
|
||||
{desk.currentTask}
|
||||
{task}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user