Skip to content

g1t/apps/web/app/components/gateway.tsx

126 lines4,387 bytesCodeBlame
1/**
2 * The AI Gateway page's log: a workspace's recent model requests, one row
3 * each, newest first, and the shape it has while it loads.
4 */
5
6import type { ReactNode } from "react";
7
8import type { GatewayRequest } from "@g1t/contracts";
9
10import { duration, shortCount, statusTone, tokenKinds } from "../lib/gateway";
11import { money } from "../lib/usage";
12import { TimeAgo } from "./ui";
13import { Badge } from "./ui/badge";
14import { Hint } from "./ui/hint";
15import { Loading, SkeletonLine } from "./ui/skeleton";
16
17const HEAD = "px-3 py-2 text-xs font-medium text-muted whitespace-nowrap";
18const NUM = "px-3 py-2 text-right tabular-nums whitespace-nowrap";
19
20/** The columns, so the skeleton has as many as the table. */
21const COLUMNS = ["Time", "Model", "Input", "Output", "Cache read", "Cache write", "Cost", "Status", "Token"] as const;
22
23/** One request's row. */
24function Row({ request }: { request: GatewayRequest }) {
25 const status = statusTone(request.status);
26 return (
27 <tr className="text-sm">
28 <td className="px-3 py-2 whitespace-nowrap text-muted">
29 <Hint label={`${new Date(request.createdAt).toISOString().replace("T", " ").slice(0, 19)} UTC, ${duration(request.durationMs)}${request.streamed ? ", streamed" : ""}`}>
30 <span tabIndex={0}>
31 <TimeAgo at={request.createdAt} />
32 </span>
33 </Hint>
34 </td>
35 <td className="px-3 py-2 font-mono text-xs whitespace-nowrap">{request.model}</td>
36 <td className={NUM}>
37 <Hint label={tokenKinds(request)}>
38 <span tabIndex={0}>{shortCount(request.input)}</span>
39 </Hint>
40 </td>
41 <td className={NUM}>{shortCount(request.output)}</td>
42 <td className={NUM}>{shortCount(request.cacheRead)}</td>
43 <td className={NUM}>{shortCount(request.cacheWrite)}</td>
44 <td className={NUM}>
45 {request.ownKey ? (
46 <Hint label="Sent with the workspace's own provider key: counted, not charged.">
47 <span tabIndex={0}>
48 <Badge>Own key</Badge>
49 </span>
50 </Hint>
51 ) : (
52 money(request.chargedMicros)
53 )}
54 </td>
55 <td className="px-3 py-2 whitespace-nowrap">
56 <Hint label={request.error}>
57 <span tabIndex={request.error ? 0 : undefined}>
58 <Badge tone={status.tone}>{status.label}</Badge>
59 </span>
60 </Hint>
61 </td>
62 <td className="max-w-[12rem] truncate px-3 py-2 text-muted">
63 <Hint label={request.tokenId}>
64 <span tabIndex={0}>{request.tokenName ?? request.tokenId}</span>
65 </Hint>
66 </td>
67 </tr>
68 );
69}
70
71/** Whether a column holds a number, read right-aligned. */
72const numeric = (index: number) => index >= 2 && index <= 6;
73
74/** The table's frame: its heading row, around its body. */
75function Frame({ children }: { children: ReactNode }) {
76 return (
77 <div className="overflow-x-auto rounded-xl border border-line bg-surface">
78 <table className="w-full">
79 <thead className="border-b border-line">
80 <tr>
81 {COLUMNS.map((column, index) => (
82 <th key={column} className={`${HEAD} ${numeric(index) ? "text-right" : "text-left"}`} scope="col">
83 {column}
84 </th>
85 ))}
86 </tr>
87 </thead>
88 {children}
89 </table>
90 </div>
91 );
92}
93
94/** The log, one row a request. */
95export function GatewayTable({ requests }: { requests: GatewayRequest[] }) {
96 return (
97 <Frame>
98 <tbody className="divide-y divide-line">
99 {requests.map((request) => (
100 <Row key={request.id} request={request} />
101 ))}
102 </tbody>
103 </Frame>
104 );
105}
106
107/** The log while it loads: the table's own frame, with rows to come. */
108export function GatewaySkeleton() {
109 return (
110 <Loading label="Loading AI Gateway requests…">
111 <Frame>
112 <tbody className="divide-y divide-line" aria-hidden="true">
113 {Array.from({ length: 8 }, (_, row) => (
114 <tr key={row} className="text-sm">
115 {COLUMNS.map((column, index) => (
116 <td key={column} className={numeric(index) ? NUM : "px-3 py-2"}>
117 <SkeletonLine className={numeric(index) ? "ml-auto w-10" : index === 1 ? "w-36" : "w-16"} />
118 </td>
119 ))}
120 </tr>
121 ))}
122 </tbody>
123 </Frame>
124 </Loading>
125 );
126}