Skip to content
106 linesCodeBlameRaw
1import type { AgentStatus, MemberProfile } from "@g1t/contracts";
2
3import { AgentAvatar } from "../agent-avatar";
4import { WithPresence } from "../presence";
5
6import { cn } from "../../lib/cn";
7import { Avatar } from "../ui/avatar";
8
9/**
10 * A member's face: a person's avatar, or an agent's picture or bot face
11 * with the agent marker (components/ui/avatar.tsx). With `presence`, a
12 * person's carries their dot (active, away, notifications paused;
13 * components/presence.tsx), cut out of `ring`, the colour behind it, which
14 * also rings the agent marker. Agents keep their own status, shown apart.
15 */
16export function MemberAvatar({
17 member,
18 size = 20,
19 presence = false,
20 ring,
21}: {
22 member: { kind: "user" | "agent"; id?: string; name: string; avatar: string | null; avatar_seed?: string | null; look?: MemberProfile["look"] };
23 size?: number;
24 presence?: boolean;
25 ring?: string;
26}) {
27 if (member.kind === "agent") return <AgentAvatar agent={member} size={size} ring={ring} />;
28 const avatar = <Avatar name={member.name} image={member.avatar} size={size} />;
29 if (!presence) return avatar;
30 return (
31 <WithPresence person={{ id: member.id, username: member.name }} size={size} ring={ring}>
32 {avatar}
33 </WithPresence>
34 );
35}
36
37/**
38 * A person and an agent together, for a mixed direct message's row: the
39 * person's face behind, the agent's in front with its marker, so the pair
40 * says at a glance that an agent is in the conversation.
41 */
42export function PairAvatar({
43 person,
44 agent,
45 size = 20,
46 ring = "var(--color-bg)",
47}: {
48 person: { kind: "user" | "agent"; id?: string; name: string; avatar: string | null };
49 agent: { kind: "user" | "agent"; id?: string; name: string; avatar: string | null; avatar_seed?: string | null; look?: MemberProfile["look"] };
50 size?: number;
51 ring?: string;
52}) {
53 const each = Math.max(16, Math.round(size * 0.85));
54 const width = Math.round(each * 1.55);
55 return (
56 <span className="relative inline-flex shrink-0" style={{ width, height: size + 2 }} aria-hidden="true">
57 <span className="absolute top-0 left-0 inline-flex rounded-full" style={{ boxShadow: `0 0 0 1.5px ${ring}` }}>
58 <Avatar name={person.name} image={person.avatar} size={each} />
59 </span>
60 <span className="absolute inline-flex rounded-md" style={{ left: width - each, top: size + 2 - each, boxShadow: `0 0 0 1.5px ${ring}` }}>
61 <AgentAvatar agent={agent} size={each} ring={ring} />
62 </span>
63 </span>
64 );
65}
66
67/** AGENT, beside an agent's name wherever it speaks. */
68export function AgentPill({ className }: { className?: string }) {
69 // The avatar beside it carries the agent marker; this is the word, for where the name stands alone.
70 return <AgentWord className={className} />;
71}
72
73function AgentWord({ className }: { className?: string }) {
74 return (
75 <span
76 className={cn(
77 "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",
78 className,
79 )}
80 >
81 Agent
82 </span>
83 );
84}
85
86const STATUS: Record<AgentStatus, { label: string; dot: string }> = {
87 idle: { label: "Idle", dot: "bg-faint/70" },
88 working: { label: "Working", dot: "bg-success shadow-[0_0_0_3px_color-mix(in_srgb,var(--color-success)_18%,transparent)]" },
89 waiting: { label: "Waiting on you", dot: "bg-warn" },
90 out_of_budget: { label: "Out of budget", dot: "bg-danger" },
91 paused: { label: "Paused", dot: "bg-muted" },
92};
93
94export function statusLabel(status: AgentStatus): string {
95 return STATUS[status]?.label ?? status;
96}
97
98/** An agent's status as a small dot; the label is for screen readers unless shown. */
99export function StatusDot({ status, className }: { status: AgentStatus; className?: string }) {
100 const known = STATUS[status] ?? STATUS.idle;
101 return (
102 <span className={cn("inline-block size-2 shrink-0 rounded-full", known.dot, className)}>
103 <span className="sr-only">{known.label}</span>
104 </span>
105 );
106}