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