Skip to content
139 linesCodeBlameRaw
1import { data, useOutletContext } from "react-router";
2
3import type { AgentEffortCosts, AgentRecommendations, AgentSpendBreakdown, WorkspaceAgent } from "@g1t/contracts";
4
5import type { Route } from "./+types/spend";
6import { agentsAction, answer, readOrNull } from "../../../components/agents/actions.server";
7import { EffortCard, Savings } from "../../../components/effort";
8import { readEffort } from "../../../lib/agent-form";
9import { effortSetting } from "../../../lib/effort";
10import { meterTone, monthName, shareOf } from "../../../components/agents/format";
11import { DailyBars, Meter, Panel, Quiet, SessionRow, SliceList } from "../../../components/agents/parts";
12import { requireUser, roleIn } from "../../../lib/session.server";
13import { workspaceAgents } from "../../../lib/services.server";
14import { money } from "../../../lib/usage";
15
16/** Where this agent's month went, what each effort level has cost it, and ways to spend less. */
17export async function loader({ params, context, request }: Route.LoaderArgs): Promise<{
18 spend: AgentSpendBreakdown | null;
19 costs: AgentEffortCosts | null;
20 recs: AgentRecommendations | null;
21 owner: boolean;
22}> {
23 const viewer = requireUser(context, request);
24 const role = roleIn(viewer, params.owner);
25 if (!role) throw data(null, { status: 404 });
26 const slug = params.owner.toLowerCase();
27 const handle = params.handle.toLowerCase();
28 const [spend, costs, recs] = await Promise.all([
29 readOrNull(workspaceAgents.spend(slug, viewer, handle)),
30 readOrNull(workspaceAgents.effortCosts(slug, handle, viewer)),
31 readOrNull(workspaceAgents.recommendations(slug, viewer, handle)),
32 ]);
33 return { spend, costs, recs, owner: role === "owner" };
34}
35
36/** Owners set the agent's effort, and apply or dismiss a suggestion. */
37export async function action({ params, context, request }: Route.ActionArgs) {
38 const { viewer, slug, isOwner, form } = await agentsAction(request, context, params.owner);
39 const intent = String(form.get("intent") ?? "");
40 if (!isOwner) return { ok: false as const, intent, error: "Only the workspace's owners change this." };
41 if (intent === "effort") {
42 const effort = readEffort(form.get("effort"));
43 if (!effort) return { ok: false as const, intent, error: "Pick Auto, Low, Medium, High or Max." };
44 return answer(intent, workspaceAgents.update(slug, params.handle.toLowerCase(), viewer, { routing: { effort } }));
45 }
46 if (intent === "recommendation") {
47 const what = form.get("do") === "apply" ? "apply" : "dismiss";
48 return answer(intent, workspaceAgents.resolveRecommendation(slug, viewer, String(form.get("id") ?? ""), what));
49 }
50 return { ok: false as const, intent, error: "Unknown request." };
51}
52
53/**
54 * Spend this month against the agent's budget: by day, by kind of work, by
55 * model, by who asked, and its costliest sessions. A session's helpers and
56 * subagents are paid here when this agent started it.
57 */
58export default function Spend({ loaderData, params }: Route.ComponentProps) {
59 const agent = useOutletContext<WorkspaceAgent>();
60 const { spend, costs, recs, owner } = loaderData;
61 const here = `/${params.owner}/-/agents/${params.handle}/spend`;
62 const spent = spend?.total_micros ?? agent.spent_month_micros ?? 0;
63 const cap = agent.budget.monthly_micros;
64 const share = shareOf(spent, cap);
65 const tone = meterTone(share);
66 return (
67 <div className="space-y-6">
68 <section className="rounded-2xl border border-line bg-surface p-6">
69 <p className="text-sm text-muted">Spent in {monthName(spend?.period ?? new Date().toISOString().slice(0, 7))}</p>
70 <p className="mt-1 flex flex-wrap items-baseline gap-x-2">
71 <span className="text-3xl font-semibold tracking-tight tabular-nums">{money(spent)}</span>
72 <span className="text-sm text-muted">{cap != null ? `of ${money(cap)}` : "No monthly budget of its own: the workspace's agent budget applies"}</span>
73 </p>
74 {cap != null && (
75 <>
76 <Meter spent={spent} cap={cap} label="Spent of its monthly budget" className="mt-4" />
77 <p className={`mt-2 text-xs ${tone === "danger" ? "text-danger" : tone === "warn" ? "text-warn" : "text-faint"}`}>
78 {Math.round((share ?? 0) * 100)}% used. At 100% it takes no new work until the 1st, or until an owner raises its budget on its profile.
79 </p>
80 </>
81 )}
82 <dl className="mt-6 grid grid-cols-2 gap-4 border-t border-line pt-5 sm:grid-cols-4">
83 <Stat label="Daily cap" value={agent.budget.daily_micros != null ? money(agent.budget.daily_micros) : "None"} />
84 <Stat label="Per session" value={agent.budget.task_micros != null ? money(agent.budget.task_micros) : "Workspace default"} />
85 <Stat label="While idle" value="Costs nothing" />
86 <Stat label="Rolls up to" value="The workspace's bill" />
87 </dl>
88 </section>
89
90 <EffortCard value={effortSetting(agent.routing)} costs={costs} owner={owner} action={here} name={agent.display_name} />
91
92 <section aria-labelledby="savings" className="space-y-3">
93 <h2 id="savings" className="text-base font-semibold tracking-tight">
94 Spend less, keep quality
95 </h2>
96 <Savings recs={recs} owner={owner} action={here} showAgent={false} />
97 </section>
98
99 {!spend ? (
100 <Quiet title="The breakdown can't be shown right now">The agents service didn&apos;t answer. Reload in a moment.</Quiet>
101 ) : (
102 <>
103 <Panel title="By day" aside="UTC">
104 <DailyBars period={spend.period} days={spend.days} className="px-4 pt-4 pb-3" />
105 </Panel>
106 <div className="grid gap-4 lg:grid-cols-2">
107 <Panel title="By kind of work">
108 <SliceList slices={spend.by_kind} unit="runs" />
109 </Panel>
110 <Panel title="By model">
111 <SliceList slices={spend.by_model} unit="runs" />
112 </Panel>
113 <Panel title="By who asked" className="lg:col-span-2">
114 <SliceList slices={spend.by_person} unit="runs" />
115 </Panel>
116 </div>
117 {spend.top_sessions.length > 0 && (
118 <Panel title="Costliest sessions" aside="With everything they brought in">
119 <ul className="divide-y divide-line/60">
120 {spend.top_sessions.map((session) => (
121 <SessionRow key={session.id} slug={params.owner} session={session} showAgent={session.agent_id !== agent.id} />
122 ))}
123 </ul>
124 </Panel>
125 )}
126 </>
127 )}
128 </div>
129 );
130}
131
132function Stat({ label, value }: { label: string; value: string }) {
133 return (
134 <div>
135 <dt className="text-xs text-faint">{label}</dt>
136 <dd className="mt-0.5 text-sm tabular-nums">{value}</dd>
137 </div>
138 );
139}