Skip to content
324 linesCodeBlameRaw
1/**
2 * Static pictures of the product for the landing page: a channel where a
3 * person and an agent talk over a pull request, and an agent's profile.
4 * Drawn in the app's own tokens so they read as the real thing; nothing
5 * here loads data.
6 */
7import {
8 AtSign,
9 Bot,
10 BookOpen,
11 Check,
12 Code2,
13 GitMerge,
14 GitPullRequest,
15 Hash,
16 Sun,
17 Bell,
18 MessagesSquare,
19 Paperclip,
20 SendHorizontal,
21} from "lucide-react";
22import type { ReactNode } from "react";
23
24import { cn } from "../lib/cn";
25import { AgentAvatar } from "./agent-avatar";
26
27/** A round letter avatar: lavender for agents, gray for people. */
28function Face({ letter, agent, size = "md" }: { letter: string; agent?: boolean; size?: "sm" | "md" | "lg" }) {
29 return (
30 <span
31 aria-hidden="true"
32 className={cn(
33 "flex shrink-0 items-center justify-center font-semibold",
34 size === "sm" && "size-5 text-[0.625rem]",
35 size === "md" && "size-8 text-xs",
36 size === "lg" && "size-12 text-lg",
37 agent ? "rounded-lg bg-accent/15 text-accent ring-1 ring-accent/30" : "rounded-full bg-raised text-fg-soft ring-1 ring-line-strong",
38 )}
39 >
40 {letter}
41 </span>
42 );
43}
44
45/** The small tag beside an agent's name. */
46function AgentTag() {
47 return (
48 <span className="rounded bg-accent/15 px-1 py-px font-mono text-[0.5625rem] tracking-wide text-accent uppercase">Agent</span>
49 );
50}
51
52function Message({
53 face,
54 name,
55 agent,
56 role,
57 time,
58 children,
59}: {
60 face: ReactNode;
61 name: string;
62 agent?: boolean;
63 role?: string;
64 time: string;
65 children: ReactNode;
66}) {
67 return (
68 <div className="flex gap-3 px-4 py-2.5">
69 {face}
70 <div className="min-w-0 flex-1">
71 <p className="flex flex-wrap items-baseline gap-x-1.5 text-[0.8125rem]">
72 <span className="font-semibold text-fg">{name}</span>
73 {agent && <AgentTag />}
74 {role && <span className="text-xs text-faint">{role}</span>}
75 <span className="text-[0.6875rem] text-faint">{time}</span>
76 </p>
77 <div className="mt-0.5 space-y-2 text-[0.8125rem] leading-6 text-fg-soft">{children}</div>
78 </div>
79 </div>
80 );
81}
82
83function Mention({ children }: { children: ReactNode }) {
84 return <span className="rounded bg-accent/15 px-0.5 text-accent">{children}</span>;
85}
86
87function Code({ children }: { children: ReactNode }) {
88 return <code className="rounded bg-raised px-1 py-px font-mono text-[0.75rem] text-fg">{children}</code>;
89}
90
91/** The pull request card an agent posts into the thread. */
92function PullCard() {
93 return (
94 <div className="max-w-md rounded-xl bg-bg ring-1 ring-line">
95 <div className="flex items-start gap-3 px-3.5 py-3">
96 <GitPullRequest size={16} className="mt-0.5 shrink-0 text-success" />
97 <div className="min-w-0 flex-1">
98 <p className="text-[0.8125rem] font-medium text-fg">Invite emails use the team name</p>
99 <p className="mt-0.5 font-mono text-[0.6875rem] text-faint">acme/web #418 · fixes #412</p>
100 </div>
101 <span className="shrink-0 rounded-full bg-success/15 px-2 py-0.5 text-[0.6875rem] font-medium text-success">Ready</span>
102 </div>
103 <div className="flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-line px-3.5 py-2 text-[0.6875rem] text-muted">
104 <span className="flex items-center gap-1.5">
105 <Check size={12} className="text-success" />3 of 3 checks passed
106 </span>
107 <span className="font-mono">
108 <span className="text-success">+24</span> <span className="text-danger">−6</span>
109 </span>
110 <span>Reviewed by @lint</span>
111 </div>
112 </div>
113 );
114}
115
116const RAIL: { icon: ReactNode; label: string; on?: boolean }[] = [
117 { icon: <Sun size={16} />, label: "Today" },
118 { icon: <Code2 size={16} />, label: "Code" },
119 { icon: <MessagesSquare size={16} />, label: "Chat", on: true },
120 { icon: <BookOpen size={16} />, label: "Docs" },
121 { icon: <Bot size={16} />, label: "Agents" },
122 { icon: <Bell size={16} />, label: "Notifications" },
123];
124
125const CHANNELS: { name: string; on?: boolean; unread?: number }[] = [
126 { name: "web", on: true },
127 { name: "releases", unread: 2 },
128 { name: "support" },
129 { name: "design" },
130];
131
132const DMS: { name: string; agent?: boolean; letter: string }[] = [
133 { name: "Ship", agent: true, letter: "S" },
134 { name: "Lint", agent: true, letter: "L" },
135 { name: "Priya Shah", letter: "P" },
136];
137
138/**
139 * A channel in the Chat mode: the rail, the channel list, and a short
140 * exchange where an agent answers, links the pull request it made, and
141 * waits for a person to approve the merge.
142 */
143export function ChatShot({ className }: { className?: string }) {
144 return (
145 <figure
146 aria-label="A channel in g1t: a person asks an agent about a release, and the agent answers with the pull request it opened"
147 className={cn("overflow-hidden rounded-2xl bg-surface text-left shadow-2xl shadow-black/40 ring-1 ring-line", className)}
148 >
149 <div className="flex min-h-[27rem]">
150 {/* The rail. */}
151 <nav aria-hidden="true" className="hidden w-14 shrink-0 flex-col items-center gap-1.5 border-r border-line bg-bg py-3 md:flex">
152 {RAIL.map((item) => (
153 <span
154 key={item.label}
155 className={cn(
156 "flex size-9 items-center justify-center rounded-lg",
157 item.on ? "bg-accent/15 text-accent" : "text-faint",
158 )}
159 >
160 {item.icon}
161 </span>
162 ))}
163 </nav>
164
165 {/* The channel list. */}
166 <div aria-hidden="true" className="hidden w-52 shrink-0 border-r border-line px-2 py-3 lg:block">
167 <p className="px-2 text-[0.8125rem] font-semibold text-fg">Acme</p>
168 <p className="mt-4 px-2 text-[0.6875rem] font-medium text-faint">Channels</p>
169 <ul className="mt-1 space-y-px">
170 {CHANNELS.map((channel) => (
171 <li
172 key={channel.name}
173 className={cn(
174 "flex items-center gap-1.5 rounded-md px-2 py-1 text-[0.8125rem]",
175 channel.on ? "bg-raised text-fg" : "text-muted",
176 )}
177 >
178 <Hash size={13} className="text-faint" />
179 <span className="flex-1">{channel.name}</span>
180 {channel.unread && (
181 <span className="rounded-full bg-accent px-1.5 text-[0.625rem] font-semibold text-bg">{channel.unread}</span>
182 )}
183 </li>
184 ))}
185 </ul>
186 <p className="mt-4 px-2 text-[0.6875rem] font-medium text-faint">Direct messages</p>
187 <ul className="mt-1 space-y-px">
188 {DMS.map((dm) => (
189 <li key={dm.name} className="flex items-center gap-2 rounded-md px-2 py-1 text-[0.8125rem] text-muted">
190 <Face letter={dm.letter} agent={dm.agent} size="sm" />
191 <span className="flex-1">{dm.name}</span>
192 {dm.agent && <span className="size-1.5 rounded-full bg-success" />}
193 </li>
194 ))}
195 </ul>
196 </div>
197
198 {/* The channel. */}
199 <div className="flex min-w-0 flex-1 flex-col">
200 <div className="flex items-center gap-2 border-b border-line px-4 py-3">
201 <Hash size={15} className="text-faint" />
202 <span className="text-sm font-semibold text-fg">web</span>
203 <span className="hidden truncate text-xs text-faint sm:inline">The web app, its releases and its bugs</span>
204 <span className="ml-auto flex -space-x-1.5" aria-hidden="true">
205 <Face letter="P" size="sm" />
206 <Face letter="S" agent size="sm" />
207 <Face letter="L" agent size="sm" />
208 <Face letter="D" size="sm" />
209 </span>
210 </div>
211
212 <div className="flex-1 py-2">
213 <Message face={<Face letter="P" />} name="Priya Shah" time="10:42">
214 <p>
215 <Mention>@ship</Mention> what is left before Thursday&apos;s release?
216 </p>
217 </Message>
218 <Message face={<Face letter="S" agent />} name="Ship" agent role="Release manager" time="10:42">
219 <p>
220 One thing: <span className="text-accent">#412</span>, invite emails show the workspace name instead of the
221 team&apos;s. The fix is ready. The subject now comes from <Code>invite_subject()</Code>, with a test for both
222 names.
223 </p>
224 <PullCard />
225 <p>It needs one approval from a person, then it lands through the merge queue.</p>
226 </Message>
227 <Message face={<Face letter="P" />} name="Priya Shah" time="10:44">
228 <p>Approved. Thanks!</p>
229 </Message>
230 <div className="flex items-center gap-2 px-4 pt-1 text-xs text-faint">
231 <GitMerge size={13} className="text-merged" />
232 <span>
233 <span className="text-fg-soft">#418</span> merged to main · deploying to production
234 </span>
235 </div>
236 </div>
237
238 <div className="px-4 pb-4">
239 <div className="flex items-center gap-2 rounded-xl bg-bg px-3 py-2.5 text-[0.8125rem] text-faint ring-1 ring-line">
240 <span className="flex-1">Message #web</span>
241 <AtSign size={15} />
242 <Paperclip size={15} />
243 <SendHorizontal size={15} className="text-accent" />
244 </div>
245 </div>
246 </div>
247 </div>
248 </figure>
249 );
250}
251
252function Field({ label, children, soon }: { label: string; children: ReactNode; soon?: boolean }) {
253 return (
254 <div className="grid grid-cols-[6.5rem_1fr] gap-3 border-t border-line px-5 py-3 text-[0.8125rem]">
255 <dt className="text-faint">
256 {label}
257 {soon && <span className="sr-only"> (coming soon)</span>}
258 </dt>
259 <dd className="min-w-0 text-fg-soft">{children}</dd>
260 </div>
261 );
262}
263
264/** An agent's profile: who it is, what it answers for, how it talks, what it may use and spend. */
265export function AgentCardShot({ className }: { className?: string }) {
266 const spent = 12.4;
267 const budget = 40;
268 return (
269 <figure
270 aria-label="An agent's profile in g1t: Margo, hired from the QA Engineer template, on the QA team, with her responsibilities, voice, model routing, budget and what she may do alone"
271 className={cn("overflow-hidden rounded-2xl bg-surface text-left ring-1 ring-line", className)}
272 >
273 <div className="flex items-center gap-4 px-5 py-5">
274 <AgentAvatar agent={{ handle: "margo", name: "Margo" }} size={48} />
275 <div className="min-w-0">
276 <p className="flex items-center gap-2 font-semibold text-fg">
277 Margo <AgentTag />
278 </p>
279 <p className="text-sm text-muted">@margo · QA Engineer on the QA team</p>
280 </div>
281 <span className="ml-auto hidden items-center gap-1.5 rounded-full bg-success/10 px-2.5 py-1 text-xs text-success sm:flex">
282 <span className="size-1.5 rounded-full bg-success" />
283 Idle
284 </span>
285 </div>
286 <dl>
287 <Field label="Answers for">
288 <ul className="space-y-0.5">
289 <li>Reviewing pull requests for risk and test coverage</li>
290 <li>Test plans for new features</li>
291 <li>Chasing flaky checks</li>
292 </ul>
293 </Field>
294 <Field label="Personality">
295 <span className="text-fg">Crisp.</span> Short answers, points at the exact line, proposes the fix.
296 </Field>
297 <Field label="Models">
298 <span className="text-fg">Auto</span>, never below Standard
299 <span className="mt-1.5 flex flex-wrap gap-1.5">
300 <span className="rounded-md bg-raised px-1.5 py-0.5 text-xs text-muted ring-1 ring-line">g1t&apos;s models</span>
301 <span className="rounded-md bg-raised px-1.5 py-0.5 text-xs text-muted ring-1 ring-line">Your own provider</span>
302 </span>
303 </Field>
304 <Field label="Subagents">
305 <span className="flex flex-wrap items-center gap-1.5">
306 <span className="rounded-md bg-raised px-1.5 py-0.5 font-mono text-xs text-muted ring-1 ring-line">flake-hunter</span>
307 <span className="rounded-md bg-raised px-1.5 py-0.5 font-mono text-xs text-muted ring-1 ring-line">migration-checker</span>
308 <span className="text-xs text-faint">run soon</span>
309 </span>
310 </Field>
311 <Field label="Budget">
312 <span className="font-mono text-xs text-fg tabular-nums">
313 ${spent.toFixed(2)} of ${budget} this month
314 </span>
315 <span className="mt-2 block h-1.5 overflow-hidden rounded-full bg-raised">
316 <span className="block h-full rounded-full bg-accent" style={{ width: `${(spent / budget) * 100}%` }} />
317 </span>
318 <span className="mt-1.5 block text-xs text-faint">$5 a task</span>
319 </Field>
320 <Field label="Asks first">Merge · Deploy to production</Field>
321 </dl>
322 </figure>
323 );
324}