| 1 | /** |
| 2 | * Agent templates' pieces (routes/workspace/agents/templates.tsx): a |
| 3 | * template as a card, and what the viewer can do with it. Owners start an |
| 4 | * agent from one; everyone else can read what each brings. |
| 5 | */ |
| 6 | import { Plus } from "lucide-react"; |
| 7 | import { Link } from "react-router"; |
| 8 | |
| 9 | import { PixelCreature } from "./agent-avatar"; |
| 10 | import { Hint } from "./ui/hint"; |
| 11 | import { type TemplateListing, startPath, templatePath } from "../lib/agent-templates"; |
| 12 | import { cn } from "../lib/cn"; |
| 13 | |
| 14 | const SMALL = "inline-flex h-8 shrink-0 items-center justify-center gap-1.5 rounded-md px-3 text-[0.8125rem] font-medium transition-colors"; |
| 15 | const PRIMARY = `${SMALL} bg-fg text-bg hover:bg-fg-hover`; |
| 16 | const QUIET = `${SMALL} border border-line text-fg/85 hover:border-line-strong hover:bg-raised hover:text-fg`; |
| 17 | |
| 18 | /** Starting an agent from a template: the new-agent form with it chosen, for owners; a word on who can, for everyone else. */ |
| 19 | export function StartAction({ listing, slug, owner, className }: { listing: TemplateListing; slug: string; owner: boolean; className?: string }) { |
| 20 | if (!owner) { |
| 21 | return ( |
| 22 | <Hint label="Owners start new agents. Ask one in chat if you want an agent like this."> |
| 23 | <span tabIndex={0} className={cn(SMALL, "border border-line text-muted outline-none focus-visible:ring-2 focus-visible:ring-accent", className)}> |
| 24 | Owners start agents |
| 25 | </span> |
| 26 | </Hint> |
| 27 | ); |
| 28 | } |
| 29 | return ( |
| 30 | <Link to={startPath(slug, listing.template.id)} className={cn(listing.agents.length > 0 ? QUIET : PRIMARY, className)}> |
| 31 | <Plus size={14} /> |
| 32 | {listing.agents.length > 0 ? "Start another" : "Start from this"} |
| 33 | </Link> |
| 34 | ); |
| 35 | } |
| 36 | |
| 37 | /** A template as a card: the face it suggests, its name and title, what it does, and which agents started from it. */ |
| 38 | export function TemplateCard({ listing, slug, owner }: { listing: TemplateListing; slug: string; owner: boolean }) { |
| 39 | const { template, agents } = listing; |
| 40 | return ( |
| 41 | <article className="relative flex flex-col rounded-xl border border-line bg-surface p-4 transition-colors hover:border-line-strong"> |
| 42 | <div className="flex items-start gap-3"> |
| 43 | <PixelCreature seed={template.handle} size={44} /> |
| 44 | <div className="min-w-0 grow"> |
| 45 | <h3 className="truncate text-sm font-semibold"> |
| 46 | <Link to={templatePath(slug, template.id)} prefetch="intent" className="outline-none after:absolute after:inset-0 after:rounded-xl focus-visible:after:ring-2 focus-visible:after:ring-accent"> |
| 47 | {template.title} |
| 48 | </Link> |
| 49 | </h3> |
| 50 | <p className="truncate text-xs text-muted"> |
| 51 | {template.department ? `${template.department} · ` : ""}Suggests {template.display_name} |
| 52 | </p> |
| 53 | </div> |
| 54 | </div> |
| 55 | <ul className="mt-3 grow space-y-1"> |
| 56 | {template.responsibilities.slice(0, 3).map((duty) => ( |
| 57 | <li key={duty} className="flex gap-2 text-[0.8125rem] leading-snug text-muted"> |
| 58 | <span className="mt-[0.45rem] size-1 shrink-0 rounded-full bg-line-strong" aria-hidden="true" /> |
| 59 | <span className="line-clamp-1">{duty}</span> |
| 60 | </li> |
| 61 | ))} |
| 62 | </ul> |
| 63 | <div className="relative mt-4 flex items-center justify-between gap-3 border-t border-line pt-3"> |
| 64 | <span className="min-w-0 truncate text-xs text-faint"> |
| 65 | {agents.length === 0 ? "No agent started from it" : agents.length === 1 ? `@${agents[0]!.handle} started from it` : `${agents.length} agents started from it`} |
| 66 | </span> |
| 67 | {/* A member reads on; who may start one is said on the template's page. */} |
| 68 | {owner && <StartAction listing={listing} slug={slug} owner={owner} />} |
| 69 | </div> |
| 70 | </article> |
| 71 | ); |
| 72 | } |