| 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 | */ |
| 7 | import { |
| 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"; |
| 22 | import type { ReactNode } from "react"; |
| 23 | |
| 24 | import { cn } from "../lib/cn"; |
| 25 | |
| 26 | /** A round letter avatar: lavender for agents, gray for people. */ |
| 27 | function 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. */ |
| 45 | function 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 | |
| 51 | function 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 | |
| 82 | function Mention({ children }: { children: ReactNode }) { |
| 83 | return <span className="rounded bg-accent/15 px-0.5 text-accent">{children}</span>; |
| 84 | } |
| 85 | |
| 86 | function 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. */ |
| 91 | function 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 | |
| 115 | const 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 | |
| 124 | const 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 | |
| 131 | const 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 | */ |
| 142 | export 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'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'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 | |
| 251 | function 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. */ |
| 264 | export 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'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 | } |