Skip to content

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

148 lines5,157 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 { cacheKinds, duration, formatLabel, servedBy, 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", "Served by", "Input", "Output", "Cache", "Cost", "Status", "Token"] as const;
22
23/** One request's row. */
24function Row({ request }: { request: GatewayRequest }) {
25 const status = statusTone(request.status);
26 const served = servedBy(request);
27 return (
28 <tr className="text-sm">
29 <td className="px-3 py-2 whitespace-nowrap text-muted">
30 <Hint label={`${new Date(request.createdAt).toISOString().replace("T", " ").slice(0, 19)} UTC, ${duration(request.durationMs)}${request.streamed ? ", streamed" : ""}`}>
31 <span tabIndex={0}>
32 <TimeAgo at={request.createdAt} />
33 </span>
34 </Hint>
35 </td>
36 <td className="max-w-[15rem] px-3 py-2 whitespace-nowrap">
37 <Hint label={request.model}>
38 <span tabIndex={0} className="block truncate font-mono text-xs">
39 {request.model}
40 </span>
41 </Hint>
42 <span className="block text-[0.6875rem] text-faint">{formatLabel(request.format)} format</span>
43 </td>
44 <td className="max-w-[12rem] truncate px-3 py-2 whitespace-nowrap">
45 <Hint label={served.hint}>
46 <span tabIndex={0} className={request.ownKey ? "" : "text-muted"}>
47 {served.label}
48 </span>
49 </Hint>
50 </td>
51 <td className={NUM}>
52 <Hint label={tokenKinds(request)}>
53 <span tabIndex={0}>{shortCount(request.input)}</span>
54 </Hint>
55 </td>
56 <td className={NUM}>{shortCount(request.output)}</td>
57 <td className={NUM}>
58 <Hint label={cacheKinds(request)}>
59 <span tabIndex={0}>
60 {shortCount(request.cacheRead)}
61 <span className="text-faint"> / </span>
62 {shortCount(request.cacheWrite)}
63 </span>
64 </Hint>
65 </td>
66 <td className={NUM}>
67 {request.ownKey ? (
68 <Hint label="Sent to the workspace's own provider: counted, not charged.">
69 <span tabIndex={0}>
70 <Badge>Not charged</Badge>
71 </span>
72 </Hint>
73 ) : (
74 money(request.chargedMicros)
75 )}
76 </td>
77 <td className="px-3 py-2 whitespace-nowrap">
78 <Hint label={request.error}>
79 <span tabIndex={request.error ? 0 : undefined}>
80 <Badge tone={status.tone}>{status.label}</Badge>
81 </span>
82 </Hint>
83 </td>
84 <td className="max-w-[12rem] truncate px-3 py-2 text-muted">
85 <Hint label={request.tokenId}>
86 <span tabIndex={0}>{request.tokenName ?? request.tokenId}</span>
87 </Hint>
88 </td>
89 </tr>
90 );
91}
92
93/** Whether a column holds a number, read right-aligned. */
94const numeric = (index: number) => index >= 3 && index <= 6;
95
96/** The table's frame: its heading row, around its body. */
97function Frame({ children }: { children: ReactNode }) {
98 return (
99 <div className="overflow-x-auto rounded-xl border border-line bg-surface">
100 <table className="w-full">
101 <thead className="border-b border-line">
102 <tr>
103 {COLUMNS.map((column, index) => (
104 <th key={column} className={`${HEAD} ${numeric(index) ? "text-right" : "text-left"}`} scope="col">
105 {column}
106 </th>
107 ))}
108 </tr>
109 </thead>
110 {children}
111 </table>
112 </div>
113 );
114}
115
116/** The log, one row a request. */
117export function GatewayTable({ requests }: { requests: GatewayRequest[] }) {
118 return (
119 <Frame>
120 <tbody className="divide-y divide-line">
121 {requests.map((request) => (
122 <Row key={request.id} request={request} />
123 ))}
124 </tbody>
125 </Frame>
126 );
127}
128
129/** The log while it loads: the table's own frame, with rows to come. */
130export function GatewaySkeleton() {
131 return (
132 <Loading label="Loading AI Gateway requests…">
133 <Frame>
134 <tbody className="divide-y divide-line" aria-hidden="true">
135 {Array.from({ length: 8 }, (_, row) => (
136 <tr key={row} className="text-sm">
137 {COLUMNS.map((column, index) => (
138 <td key={column} className={numeric(index) ? NUM : "px-3 py-2"}>
139 <SkeletonLine className={numeric(index) ? "ml-auto w-10" : index === 1 ? "w-36" : index === 2 ? "w-24" : "w-16"} />
140 </td>
141 ))}
142 </tr>
143 ))}
144 </tbody>
145 </Frame>
146 </Loading>
147 );
148}