Skip to content
110 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/**
68 * AGENT, beside an agent's name wherever it speaks; DELETED AGENT, in the
69 * quiet colours, beside the name an agent deleted for good spoke under
70 * (docs.g1t.sh/guides/chat/, "Archived and deleted agents").
71 */
72export function AgentPill({ className, deleted = false }: { className?: string; deleted?: boolean }) {
73 // The avatar beside it carries the agent marker; this is the word, for where the name stands alone.
74 return <AgentWord className={cn(deleted && "bg-raised text-muted", className)}>{deleted ? "Deleted agent" : "Agent"}</AgentWord>;
75}
76
77function AgentWord({ className, children }: { className?: string; children: string }) {
78 return (
79 <span
80 className={cn(
81 "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",
82 className,
83 )}
84 >
85 {children}
86 </span>
87 );
88}
89
90const STATUS: Record<AgentStatus, { label: string; dot: string }> = {
91 idle: { label: "Idle", dot: "bg-faint/70" },
92 working: { label: "Working", dot: "bg-success shadow-[0_0_0_3px_color-mix(in_srgb,var(--color-success)_18%,transparent)]" },
93 waiting: { label: "Waiting on you", dot: "bg-warn" },
94 out_of_budget: { label: "Out of budget", dot: "bg-danger" },
95 paused: { label: "Paused", dot: "bg-muted" },
96};
97
98export function statusLabel(status: AgentStatus): string {
99 return STATUS[status]?.label ?? status;
100}
101
102/** An agent's status as a small dot; the label is for screen readers unless shown. */
103export function StatusDot({ status, className }: { status: AgentStatus; className?: string }) {
104 const known = STATUS[status] ?? STATUS.idle;
105 return (
106 <span className={cn("inline-block size-2 shrink-0 rounded-full", known.dot, className)}>
107 <span className="sr-only">{known.label}</span>
108 </span>
109 );
110}