Skip to content
72 linesCodeBlameRaw
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 */
6import { Plus } from "lucide-react";
7import { Link } from "react-router";
8
9import { PixelCreature } from "./agent-avatar";
10import { Hint } from "./ui/hint";
11import { type TemplateListing, startPath, templatePath } from "../lib/agent-templates";
12import { cn } from "../lib/cn";
13
14const 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";
15const PRIMARY = `${SMALL} bg-fg text-bg hover:bg-fg-hover`;
16const 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. */
19export 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. */
38export 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}