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