import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useRequestLogs } from "@/lib/query/usage"; import { KNOWN_APP_TYPES, getFreshInputTokens, type LogFilters, type UsageRangeSelection, } from "@/types/usage"; import { ChevronLeft, ChevronRight, Search, X } from "lucide-react"; import { UsageDateRangePicker } from "./UsageDateRangePicker"; import { fmtInt, fmtUsd, getLocaleFromLanguage, parseFiniteNumber, } from "./format"; interface RequestLogTableProps { range: UsageRangeSelection; rangeLabel: string; appType?: string; refreshIntervalMs: number; onRangeChange?: (range: UsageRangeSelection) => void; } export function RequestLogTable({ range, rangeLabel, appType: dashboardAppType, refreshIntervalMs, onRangeChange, }: RequestLogTableProps) { const { t, i18n } = useTranslation(); const [appliedFilters, setAppliedFilters] = useState({}); const [draftFilters, setDraftFilters] = useState({}); const [page, setPage] = useState(0); const [pageInput, setPageInput] = useState(""); const pageSize = 20; const dashboardAppTypeActive = dashboardAppType && dashboardAppType !== "all"; const effectiveFilters: LogFilters = dashboardAppTypeActive ? { ...appliedFilters, appType: dashboardAppType } : appliedFilters; const { data: result, isLoading } = useRequestLogs({ filters: effectiveFilters, range, page, pageSize, options: { refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false, }, }); const logs = result?.data ?? []; const total = result?.total ?? 0; const totalPages = Math.ceil(total / pageSize); useEffect(() => { setPage(0); }, [ dashboardAppType, range.customEndDate, range.customStartDate, range.preset, ]); const handleSearch = () => { setAppliedFilters(draftFilters); setPage(0); }; const handleReset = () => { setDraftFilters({}); setAppliedFilters({}); setPage(0); }; const applySelectFilter = ( key: K, value: LogFilters[K], ) => { setDraftFilters((prev) => ({ ...prev, [key]: value, })); setAppliedFilters((prev) => ({ ...prev, [key]: value, })); setPage(0); }; const handleGoToPage = () => { const trimmed = pageInput.trim(); if (!/^\d+$/.test(trimmed)) return; const parsed = Number(trimmed); if (parsed < 1 || parsed > totalPages) return; setPage(parsed - 1); setPageInput(""); }; const language = i18n.resolvedLanguage || i18n.language || "en"; const locale = getLocaleFromLanguage(language); return (
{/* App type */} {/* Status code */} {/* Provider search */}
setDraftFilters({ ...draftFilters, providerName: e.target.value || undefined, }) } onKeyDown={(e) => { if (e.key === "Enter") handleSearch(); }} />
{/* Model search */}
setDraftFilters({ ...draftFilters, model: e.target.value || undefined, }) } onKeyDown={(e) => { if (e.key === "Enter") handleSearch(); }} />
{onRangeChange && ( )} {/* Search & Reset (icon-only) */}
{isLoading ? (
) : ( <>
{t("usage.time")} {t("usage.provider")} {t("usage.billingModel")} {t("usage.inputTokens")} {t("usage.outputTokens")} {t("usage.totalCost")} {t("usage.timingInfo")} {t("usage.status")} {t("usage.source", { defaultValue: "Source" })} {logs.length === 0 ? ( {t("usage.noData")} ) : ( logs.map((log) => ( {new Date(log.createdAt * 1000).toLocaleString(locale, { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", })} {log.providerName || t("usage.unknownProvider")}
{log.requestModel && log.requestModel !== log.model ? ( {log.requestModel} {" → "} {log.model} ) : ( log.model )}
{(() => { const freshInput = getFreshInputTokens(log); const isCacheInclusive = log.inputTokens !== freshInput; return (
{fmtInt(freshInput, locale)}
); })()} {(log.cacheReadTokens > 0 || log.cacheCreationTokens > 0) && (
{[ log.cacheReadTokens > 0 && `R${fmtInt(log.cacheReadTokens, locale)}`, log.cacheCreationTokens > 0 && `W${fmtInt(log.cacheCreationTokens, locale)}`, ] .filter(Boolean) .join("·")}
)}
{fmtInt(log.outputTokens, locale)}
{fmtUsd(log.totalCostUsd, 4)}
{parseFiniteNumber(log.costMultiplier) != null && parseFiniteNumber(log.costMultiplier) !== 1 && (
× {parseFiniteNumber(log.costMultiplier)?.toFixed( 2, )}
)}
{(log.latencyMs / 1000).toFixed(1)}s {log.firstTokenMs != null && ( /{(log.firstTokenMs / 1000).toFixed(1)}s )} = 200 && log.statusCode < 300 ? "text-green-600" : "text-red-600" } > {log.statusCode} {log.dataSource || "proxy"}
)) )}
{t("usage.totalRecords", { total })}
{(() => { const pages: (number | string)[] = []; if (totalPages <= 9) { for (let i = 0; i < totalPages; i++) pages.push(i); } else { const pageSet = new Set(); for (let i = 0; i < 3; i++) pageSet.add(i); for (let i = totalPages - 3; i < totalPages; i++) pageSet.add(i); for ( let i = Math.max(0, page - 1); i <= Math.min(totalPages - 1, page + 1); i++ ) pageSet.add(i); const sorted = Array.from(pageSet).sort((a, b) => a - b); for (let i = 0; i < sorted.length; i++) { if (i > 0 && sorted[i] - sorted[i - 1] > 1) { pages.push(`ellipsis-${i}`); } pages.push(sorted[i]); } } return pages.map((p) => typeof p === "string" ? ( ... ) : ( ), ); })()}
setPageInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleGoToPage(); }} placeholder={t("usage.pageInputPlaceholder")} className="h-8 w-16 text-center text-xs" />
)}
); }