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