TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import Link from "next/link";4import { CheckCircle2, XCircle, CircleStop, ChevronRight } from "lucide-react";5import { Badge } from "@/components/ui/badge";6import { Tooltip } from "@/components/ui/tooltip";7import { ProviderIcon } from "@/components/brand/provider-icon";8import { providerName } from "@/lib/client/providers";9import { formatUsd, formatTokens, formatMs, formatRelative, cn } from "@/lib/utils";10import { formatDateTime } from "@/lib/client/humanize";11import type { RecentRecord } from "./types";1213function StatusBadge({ r }: { r: RecentRecord }) {14 if (r.status === "ok")15 return (16 <Badge variant="success">17 <CheckCircle2 /> ok18 </Badge>19 );20 if (r.status === "stopped")21 return (22 <Badge>23 <CircleStop /> stopped24 </Badge>25 );26 return (27 <Tooltip content={r.errorCode ?? undefined}>28 <Badge variant="danger">29 <XCircle /> {r.errorCode?.toLowerCase().replace(/_/g, " ") ?? "error"}30 </Badge>31 </Tooltip>32 );33}3435/** Phone: stacked rows (tap → conversation). Desktop (md+): table. */36export function RecentActivity({ rows, modelLabel, className }: { rows: RecentRecord[]; modelLabel: (key: string) => string; className?: string }) {37 return (38 <section className={cn("panel overflow-hidden", className)} aria-label="Recent requests">39 <header className="flex items-center justify-between px-4 py-3">40 <h3 className="text-[13px] font-semibold tracking-tight">Recent requests</h3>41 <span className="text-[11px] text-fg-subtle">Last {rows.length}</span>42 </header>43 {rows.length === 0 ? (44 <p className="px-4 pb-6 text-center text-xs text-fg-subtle">No requests in this period.</p>45 ) : (46 <>47 {/* Phone rows */}48 <ul className="divide-y divide-hairline md:hidden">49 {rows.map((r) => {50 const inner = (51 <>52 <ProviderIcon provider={r.provider} size={16} className="mt-0.5" />53 <div className="min-w-0 flex-1">54 <div className="flex items-center justify-between gap-2">55 <p className="truncate text-[14px] font-medium">{modelLabel(r.modelKey)}</p>56 <span className="shrink-0 font-mono text-[13px] tabular-nums">{formatUsd(r.costUsd, { precise: true })}</span>57 </div>58 <p className="mt-0.5 flex flex-wrap items-center gap-x-1.5 text-[12px] text-fg-muted">59 <span>{formatRelative(r.createdAt)}</span>60 <span aria-hidden>·</span>61 <span className="capitalize">{r.kind}</span>62 <span aria-hidden>·</span>63 <span className="font-mono tabular-nums">64 {formatTokens(r.inputTokens)}/{formatTokens(r.outputTokens)}65 </span>66 {r.latencyMs ? (67 <>68 <span aria-hidden>·</span>69 <span className="font-mono tabular-nums">{formatMs(r.latencyMs)}</span>70 </>71 ) : null}72 </p>73 <div className="mt-1.5 flex items-center justify-between gap-2">74 <StatusBadge r={r} />75 {r.conversationTitle ? <span className="truncate text-[11px] text-fg-subtle">{r.conversationTitle}</span> : null}76 </div>77 </div>78 {r.conversationId ? <ChevronRight className="mt-3 size-4 shrink-0 text-fg-subtle" /> : null}79 </>80 );81 const cls = "flex min-h-[64px] items-start gap-3 px-4 py-3 active:bg-bg-muted/60";82 return (83 <li key={r.id}>84 {r.conversationId && r.kind !== "arena" ? (85 <Link href={`/app/chat/${r.conversationId}`} className={cls}>86 {inner}87 </Link>88 ) : (89 <div className={cls}>{inner}</div>90 )}91 </li>92 );93 })}94 </ul>9596 {/* Desktop table */}97 <div className="hidden overflow-x-auto md:block">98 <table className="w-full text-[13px]">99 <thead>100 <tr className="border-y border-border bg-bg-subtle/60 text-left text-[11px] font-medium uppercase tracking-wide text-fg-subtle">101 <th className="px-4 py-2">Time</th>102 <th className="px-3 py-2">Model</th>103 <th className="px-3 py-2">Conversation</th>104 <th className="px-3 py-2">Kind</th>105 <th className="px-3 py-2 text-right">Tokens in / out</th>106 <th className="px-3 py-2 text-right">Est. cost</th>107 <th className="px-3 py-2 text-right">TTFT</th>108 <th className="px-3 py-2 text-right">Latency</th>109 <th className="px-4 py-2 text-right">Status</th>110 </tr>111 </thead>112 <tbody>113 {rows.map((r) => (114 <tr key={r.id} className="border-b border-hairline last:border-b-0 hover:bg-bg-subtle/50">115 <td className="whitespace-nowrap px-4 py-2 text-fg-muted">116 <Tooltip content={formatDateTime(r.createdAt, { year: "numeric", second: "2-digit" })}>117 <span>{formatRelative(r.createdAt)}</span>118 </Tooltip>119 </td>120 <td className="px-3 py-2">121 <span className="flex items-center gap-1.5">122 <ProviderIcon provider={r.provider} size={13} />123 <span className="truncate font-medium">{modelLabel(r.modelKey)}</span>124 <span className="hidden text-[11px] text-fg-subtle xl:inline">{providerName(r.provider)}</span>125 </span>126 </td>127 <td className="max-w-[220px] truncate px-3 py-2 text-fg-muted">128 {r.conversationId && r.kind !== "arena" ? (129 <Link href={`/app/chat/${r.conversationId}`} className="hover:text-fg hover:underline">130 {r.conversationTitle ?? "Open"}131 </Link>132 ) : (133 <span className="text-fg-subtle">{r.kind === "arena" ? "Arena" : "—"}</span>134 )}135 </td>136 <td className="px-3 py-2 capitalize text-fg-muted">{r.kind}</td>137 <td className="px-3 py-2 text-right font-mono tabular-nums text-fg-muted">138 {formatTokens(r.inputTokens)} / {formatTokens(r.outputTokens)}139 </td>140 <td className="px-3 py-2 text-right font-mono tabular-nums">{formatUsd(r.costUsd, { precise: true })}</td>141 <td className="px-3 py-2 text-right font-mono tabular-nums text-fg-muted">{formatMs(r.ttftMs)}</td>142 <td className="px-3 py-2 text-right font-mono tabular-nums text-fg-muted">{formatMs(r.latencyMs)}</td>143 <td className="px-4 py-2 text-right">144 <StatusBadge r={r} />145 </td>146 </tr>147 ))}148 </tbody>149 </table>150 </div>151 </>152 )}153 </section>154 );155}156