Skip to content
74 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 { AgentAvatar } from "./agent-avatar";
10import { ButtonLink } from "./ui";
11import { buttonVariants } from "./ui/button";
12import { Card } from "./ui/card";
13import { Hint } from "./ui/hint";
14import { type TemplateListing, startPath, templatePath } from "../lib/agent-templates";
15import { cn } from "../lib/cn";
16
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="This workspace's owners have turned off personal agents. Ask an owner in chat if you want an agent like this.">
23 <span tabIndex={0} className={cn(buttonVariants({ variant: "outline", size: "sm" }), "text-muted hover:border-line hover:bg-transparent hover:text-muted", className)}>
24 Owners start agents
25 </span>
26 </Hint>
27 );
28 }
29 return (
30 <ButtonLink to={startPath(slug, listing.template.id)} variant={listing.agents.length > 0 ? "outline" : "default"} size="sm" className={className}>
31 <Plus size={14} />
32 {listing.agents.length > 0 ? "Start another" : "Start from this"}
33 </ButtonLink>
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 <Card asChild className="relative flex flex-col p-4 transition-colors hover:border-line-strong">
42 <article>
43 <div className="flex items-start gap-3">
44 <AgentAvatar agent={{ handle: template.handle }} size={44} />
45 <div className="min-w-0 grow">
46 <h3 className="truncate text-sm font-semibold">
47 <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">
48 {template.title}
49 </Link>
50 </h3>
51 <p className="truncate text-xs text-muted">
52 Suggests {template.display_name}
53 </p>
54 </div>
55 </div>
56 <ul className="mt-3 grow space-y-1">
57 {template.responsibilities.slice(0, 3).map((duty) => (
58 <li key={duty} className="flex gap-2 text-[0.8125rem] leading-snug text-muted">
59 <span className="mt-[0.45rem] size-1 shrink-0 rounded-full bg-line-strong" aria-hidden="true" />
60 <span className="line-clamp-1">{duty}</span>
61 </li>
62 ))}
63 </ul>
64 <div className="relative mt-4 flex items-center justify-between gap-3 border-t border-line pt-3">
65 <span className="min-w-0 truncate text-xs text-faint">
66 {agents.length === 0 ? "No agent started from it" : agents.length === 1 ? `@${agents[0]!.handle} started from it` : `${agents.length} agents started from it`}
67 </span>
68 {/* A member reads on; who may start one is said on the template's page. */}
69 {owner && <StartAction listing={listing} slug={slug} owner={owner} />}
70 </div>
71 </article>
72 </Card>
73 );
74}