| 1 | import { useOutletContext } from "react-router"; |
| 2 | |
| 3 | import type { WorkspaceAgent } from "@g1t/contracts"; |
| 4 | |
| 5 | import { formatDollars } from "../../../lib/agent-form"; |
| 6 | |
| 7 | /** Spend this month against the agent's budget; by task and by model as tasks land. */ |
| 8 | export default function Spend() { |
| 9 | const agent = useOutletContext<WorkspaceAgent>(); |
| 10 | const spent = agent.spent_month_micros ?? 0; |
| 11 | const cap = agent.budget.monthly_micros; |
| 12 | const share = cap ? Math.min(1, spent / cap) : 0; |
| 13 | const tone = share >= 1 ? "bg-danger" : share >= 0.8 ? "bg-warn" : "bg-accent"; |
| 14 | return ( |
| 15 | <div className="space-y-8"> |
| 16 | <section className="rounded-2xl border border-line bg-surface p-6"> |
| 17 | <p className="text-sm text-muted">Spent this month</p> |
| 18 | <p className="mt-1 flex flex-wrap items-baseline gap-x-2"> |
| 19 | <span className="text-3xl font-semibold tracking-tight tabular-nums">{formatDollars(spent)}</span> |
| 20 | <span className="text-sm text-muted">{cap ? `of ${formatDollars(cap)}` : "No monthly cap: the workspace limit applies"}</span> |
| 21 | </p> |
| 22 | {cap != null && ( |
| 23 | <> |
| 24 | <div |
| 25 | className="mt-4 h-2 overflow-hidden rounded-full bg-line" |
| 26 | role="meter" |
| 27 | aria-label="Spent of the monthly cap" |
| 28 | aria-valuemin={0} |
| 29 | aria-valuemax={cap} |
| 30 | aria-valuenow={spent} |
| 31 | > |
| 32 | <div className={`h-full rounded-full ${tone} transition-[width]`} style={{ width: `${Math.max(share * 100, spent > 0 ? 1.5 : 0)}%` }} /> |
| 33 | </div> |
| 34 | <p className="mt-2 text-xs text-faint"> |
| 35 | {Math.round(share * 100)}% used. At 80% it tells the channel that pays for it; at 100% it takes no new tasks. |
| 36 | </p> |
| 37 | </> |
| 38 | )} |
| 39 | <dl className="mt-6 grid grid-cols-2 gap-4 border-t border-line pt-5 sm:grid-cols-3"> |
| 40 | <div> |
| 41 | <dt className="text-xs text-faint">Daily cap</dt> |
| 42 | <dd className="mt-0.5 text-sm tabular-nums">{agent.budget.daily_micros != null ? formatDollars(agent.budget.daily_micros) : "None"}</dd> |
| 43 | </div> |
| 44 | <div> |
| 45 | <dt className="text-xs text-faint">Per task</dt> |
| 46 | <dd className="mt-0.5 text-sm tabular-nums">{agent.budget.task_micros != null ? formatDollars(agent.budget.task_micros) : "None"}</dd> |
| 47 | </div> |
| 48 | <div> |
| 49 | <dt className="text-xs text-faint">While idle</dt> |
| 50 | <dd className="mt-0.5 text-sm">Costs nothing</dd> |
| 51 | </div> |
| 52 | </dl> |
| 53 | </section> |
| 54 | <section> |
| 55 | <h2 className="text-sm font-medium">By task and by model</h2> |
| 56 | <div className="mt-3 grid gap-3 sm:grid-cols-2"> |
| 57 | <div className="rounded-xl border border-dashed border-line px-5 py-8"> |
| 58 | <p className="text-sm font-medium">By task</p> |
| 59 | <p className="mt-1 text-[0.8125rem] text-muted">Each task's spend against its cap shows here once it has worked one.</p> |
| 60 | </div> |
| 61 | <div className="rounded-xl border border-dashed border-line px-5 py-8"> |
| 62 | <p className="text-sm font-medium">By model</p> |
| 63 | <p className="mt-1 text-[0.8125rem] text-muted">Every step records the model that ran it; the split shows here once it has worked.</p> |
| 64 | </div> |
| 65 | </div> |
| 66 | </section> |
| 67 | </div> |
| 68 | ); |
| 69 | } |