Skip to content
87 linesCodeBlameRaw
1import { Sparkles } from "lucide-react";
2
3import type { AgentStatus } from "@g1t/contracts";
4
5import { AgentAvatar } from "../agent-avatar";
6import { WithPresence } from "../presence";
7import { Avatar } from "../ui";
8import { cn } from "../../lib/cn";
9
10/**
11 * A workspace agent's face when it has no picture: a sparkle on a lavender
12 * square, so agents read apart from people (round letters) at a glance.
13 */
14export function AgentMark({ size = 20, className }: { size?: number; className?: string }) {
15 return (
16 <span
17 aria-hidden="true"
18 className={cn("inline-flex shrink-0 items-center justify-center bg-accent/18 text-accent ring-1 ring-accent/30 ring-inset", className)}
19 style={{ width: size, height: size, borderRadius: Math.round(size * 0.28) }}
20 >
21 <Sparkles size={Math.max(10, Math.round(size * 0.56))} strokeWidth={2.1} />
22 </span>
23 );
24}
25
26/**
27 * A member's face: a person's avatar, or an agent's picture or mark. With
28 * `presence`, a person's carries their dot (active, away, notifications
29 * paused; components/presence.tsx), cut out of `ring`, the colour behind
30 * it. Agents keep their own status, shown apart.
31 */
32export function MemberAvatar({
33 member,
34 size = 20,
35 presence = false,
36 ring,
37}: {
38 member: { kind: "user" | "agent"; id?: string; name: string; avatar: string | null; avatar_seed?: string | null };
39 size?: number;
40 presence?: boolean;
41 ring?: string;
42}) {
43 if (member.kind === "agent") return <AgentAvatar agent={member} size={size} />;
44 const avatar = <Avatar name={member.name} image={member.avatar} size={size} />;
45 if (!presence) return avatar;
46 return (
47 <WithPresence person={{ id: member.id, username: member.name }} size={size} ring={ring}>
48 {avatar}
49 </WithPresence>
50 );
51}
52
53/** AGENT, beside an agent's name wherever it speaks. */
54export function AgentPill({ className }: { className?: string }) {
55 return (
56 <span
57 className={cn(
58 "inline-flex h-4 shrink-0 items-center rounded-[4px] bg-accent/15 px-1 text-[0.5625rem] font-semibold tracking-[0.06em] text-accent uppercase",
59 className,
60 )}
61 >
62 Agent
63 </span>
64 );
65}
66
67const STATUS: Record<AgentStatus, { label: string; dot: string }> = {
68 idle: { label: "Idle", dot: "bg-faint/70" },
69 working: { label: "Working", dot: "bg-success shadow-[0_0_0_3px_color-mix(in_srgb,var(--color-success)_18%,transparent)]" },
70 waiting: { label: "Waiting on you", dot: "bg-warn" },
71 out_of_budget: { label: "Out of budget", dot: "bg-danger" },
72 paused: { label: "Paused", dot: "bg-muted" },
73};
74
75export function statusLabel(status: AgentStatus): string {
76 return STATUS[status]?.label ?? status;
77}
78
79/** An agent's status as a small dot; the label is for screen readers unless shown. */
80export function StatusDot({ status, className }: { status: AgentStatus; className?: string }) {
81 const known = STATUS[status] ?? STATUS.idle;
82 return (
83 <span className={cn("inline-block size-2 shrink-0 rounded-full", known.dot, className)}>
84 <span className="sr-only">{known.label}</span>
85 </span>
86 );
87}