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