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