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