Skip to content
170 linesCodeBlameRaw
1/**
2 * One agent template: what an agent started from it is responsible for,
3 * the helpers it works with, how it talks, which models it runs on, the
4 * instructions it starts from, what you configure when you start one, and
5 * the agents in the workspace that started from it.
6 */
7import { ArrowLeft } from "lucide-react";
8import { Link, data } from "react-router";
9
10import type { Route } from "./+types/template";
11import { AgentAvatar } from "../../../components/agent-avatar";
12import { StartAction } from "../../../components/agent-templates";
13import { PRESETS } from "../../../lib/agent-form";
14import { routingWords, templateListings, templatesPath } from "../../../lib/agent-templates";
15import { loadTemplates } from "../../../lib/agent-templates.server";
16import { page } from "../../../lib/meta";
17import { requireUser, roleIn } from "../../../lib/session.server";
18import { Card } from "../../../components/ui/card";
19
20export function meta({ params, ...args }: Route.MetaArgs) {
21 const template = args.loaderData?.templates?.find((t) => t.id === params.template);
22 return page(args, { title: `${template ? template.title : "Template"} · Agent templates · ${params.owner} · g1t` });
23}
24
25export async function loader({ params, context, request }: Route.LoaderArgs) {
26 const viewer = requireUser(context, request);
27 const slug = params.owner.toLowerCase();
28 const role = roleIn(viewer, slug);
29 if (!role) throw data(null, { status: 404 });
30 const loaded = await loadTemplates(slug, viewer);
31 if (loaded.templates && !loaded.templates.some((t) => t.id === params.template)) throw data(null, { status: 404 });
32 // Owners start agents; members start personal ones, unless owners turned that off.
33 return { slug, owner: role === "owner" || loaded.members_create_agents, ...loaded };
34}
35
36export default function AgentTemplate({ loaderData, params }: Route.ComponentProps) {
37 const { slug, owner } = loaderData;
38 const listing = templateListings(loaderData.templates ?? [], loaderData.agents).find((l) => l.template.id === params.template);
39 const back = (
40 <Link to={templatesPath(slug)} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
41 <ArrowLeft size={14} />
42 All templates
43 </Link>
44 );
45 if (!listing) {
46 return (
47 <div>
48 {back}
49 <p className="mt-6 text-sm text-muted">Templates can't be shown right now: the agents service didn't answer. Reload in a minute.</p>
50 </div>
51 );
52 }
53 const { template, agents } = listing;
54 const voice = PRESETS.find((preset) => preset.value === template.personality_preset);
55 const names = [template.display_name, ...template.name_ideas.filter((name) => name !== template.display_name)];
56 return (
57 <div className="pb-4">
58 {back}
59 <header className="mt-5 flex flex-wrap items-center gap-4">
60 <AgentAvatar agent={{ handle: template.handle }} size={64} />
61 <div className="min-w-0 grow basis-60">
62 <h1 className="text-2xl font-semibold tracking-tight">{template.title}</h1>
63 <p className="mt-0.5 text-sm text-muted">A role template from g1t · Suggests {template.display_name}</p>
64 </div>
65 <StartAction listing={listing} slug={slug} owner={owner} className="h-9 px-4 text-sm" />
66 </header>
67
68 <div className="mt-8 grid gap-8 lg:grid-cols-[minmax(0,1fr)_18rem]">
69 <div className="min-w-0 space-y-8">
70 <section aria-labelledby="duties">
71 <h2 id="duties" className="text-sm font-semibold">
72 What it's responsible for
73 </h2>
74 <ul className="mt-3 space-y-2">
75 {template.responsibilities.map((duty) => (
76 <li key={duty} className="flex gap-2.5 text-sm text-fg-soft">
77 <span className="mt-2 size-1.5 shrink-0 rounded-full bg-accent" aria-hidden="true" />
78 {duty}
79 </li>
80 ))}
81 </ul>
82 </section>
83
84 {template.subagents.length > 0 && (
85 <section aria-labelledby="helpers">
86 <h2 id="helpers" className="text-sm font-semibold">
87 Helpers it works with
88 </h2>
89 <p className="mt-1 text-sm text-muted">Smaller agents it starts for one piece of its work, paid from its own budget.</p>
90 <Card asChild divided className="mt-3 overflow-hidden">
91 <ul>
92 {template.subagents.map((helper) => (
93 <li key={helper.name} className="px-4 py-3">
94 <p className="font-mono text-[0.8125rem]">{helper.name}</p>
95 <p className="mt-0.5 text-sm text-muted">{helper.description}</p>
96 </li>
97 ))}
98 </ul>
99 </Card>
100 </section>
101 )}
102
103 <section aria-labelledby="instructions">
104 <h2 id="instructions" className="text-sm font-semibold">
105 The instructions it starts from
106 </h2>
107 <p className="mt-1 text-sm text-muted">Yours to edit when you start the agent, and on its profile after.</p>
108 <Card asChild className="mt-3 max-h-96 overflow-auto p-4 font-sans text-[0.8125rem] leading-relaxed whitespace-pre-wrap text-fg-soft">
109 <pre>{template.instructions}</pre>
110 </Card>
111 </section>
112 </div>
113
114 <aside className="space-y-6 text-sm">
115 <Card asChild className="p-4">
116 <section aria-labelledby="configure">
117 <h2 id="configure" className="text-xs text-faint">
118 What you configure
119 </h2>
120 <dl className="mt-3 space-y-4">
121 <div>
122 <dt className="text-xs text-faint">Name</dt>
123 <dd className="mt-0.5">{names.slice(0, 4).join(", ")}, or your own</dd>
124 </div>
125 <div>
126 <dt className="text-xs text-faint">Voice</dt>
127 <dd className="mt-0.5">{voice ? voice.label : template.personality_preset}</dd>
128 {voice && <dd className="text-xs text-muted">{voice.about}</dd>}
129 </div>
130 <div>
131 <dt className="text-xs text-faint">Models</dt>
132 <dd className="mt-0.5">{routingWords(template.routing)}</dd>
133 </div>
134 <div>
135 <dt className="text-xs text-faint">Budget</dt>
136 <dd className="mt-0.5">The workspace's default for a new agent, billed at what it costs</dd>
137 </div>
138 <div>
139 <dt className="text-xs text-faint">Team and access</dt>
140 <dd className="mt-0.5">A team you pick. It works with the access of the person asking, never more.</dd>
141 </div>
142 </dl>
143 </section>
144 </Card>
145 <Card asChild className="p-4">
146 <section aria-labelledby="here">
147 <h2 id="here" className="text-xs text-faint">
148 In this workspace
149 </h2>
150 {agents.length === 0 ? (
151 <p className="mt-1 text-muted">No agent has started from it yet.</p>
152 ) : (
153 <ul className="mt-2 space-y-1.5">
154 {agents.map((agent) => (
155 <li key={agent.id} className="flex items-center gap-2">
156 <AgentAvatar agent={agent} size={20} />
157 <Link to={`/${slug}/-/agents/${agent.handle}`} className="truncate hover:underline">
158 {agent.display_name} <span className="text-faint">@{agent.handle}</span>
159 </Link>
160 </li>
161 ))}
162 </ul>
163 )}
164 </section>
165 </Card>
166 </aside>
167 </div>
168 </div>
169 );
170}