"use client"; import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import Card from "./Card"; import RequestLoggerDetail from "./RequestLoggerDetail"; import { PROTOCOL_COLORS, PROVIDER_COLORS, getHttpStatusStyle as getStatusStyle, } from "@/shared/constants/colors"; import { formatTime, formatDuration, maskSegment, maskAccount, formatApiKeyLabel, } from "@/shared/utils/formatting"; // Quick filter categories - status-based only (providers are dynamic from data) const STATUS_FILTERS = [ { key: "all", label: "All" }, { key: "error", label: "Errors", icon: "error" }, { key: "ok", label: "Success", icon: "check_circle" }, { key: "combo", label: "Combo", icon: "hub" }, ]; // Column definitions for visibility toggles const COLUMNS = [ { key: "status", label: "Status" }, { key: "model", label: "Model" }, { key: "provider", label: "Provider" }, { key: "protocol", label: "Protocol" }, { key: "account", label: "Account" }, { key: "apiKey", label: "API Key" }, { key: "combo", label: "Combo" }, { key: "tokens", label: "Tokens" }, { key: "duration", label: "Duration" }, { key: "time", label: "Time" }, ]; const DEFAULT_VISIBLE = Object.fromEntries(COLUMNS.map((c) => [c.key, true])); function getLogTotalTokens(log) { return (log?.tokens?.in || 0) + (log?.tokens?.out || 0); } export default function RequestLoggerV2() { const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); const [recording, setRecording] = useState(true); const [search, setSearch] = useState(""); const [activeFilter, setActiveFilter] = useState("all"); const [selectedModel, setSelectedModel] = useState(""); const [selectedAccount, setSelectedAccount] = useState(""); const [selectedProvider, setSelectedProvider] = useState(""); const [selectedApiKey, setSelectedApiKey] = useState(""); const [sortBy, setSortBy] = useState("newest"); const [selectedLog, setSelectedLog] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [detailData, setDetailData] = useState(null); const intervalRef = useRef(null); const hasLoadedRef = useRef(false); // Column visibility with localStorage persistence const [visibleColumns, setVisibleColumns] = useState(() => { if (typeof window === "undefined") return DEFAULT_VISIBLE; try { const saved = localStorage.getItem("loggerVisibleColumns"); return saved ? { ...DEFAULT_VISIBLE, ...JSON.parse(saved) } : DEFAULT_VISIBLE; } catch { return DEFAULT_VISIBLE; } }); const toggleColumn = useCallback((key) => { setVisibleColumns((prev) => { const next = { ...prev, [key]: !prev[key] }; try { localStorage.setItem("loggerVisibleColumns", JSON.stringify(next)); } catch {} return next; }); }, []); const fetchLogs = useCallback( async (showLoading = false) => { if (showLoading) setLoading(true); try { const params = new URLSearchParams(); if (search) params.set("search", search); if (activeFilter === "error") params.set("status", "error"); if (activeFilter === "ok") params.set("status", "ok"); if (activeFilter === "combo") params.set("combo", "1"); if (selectedModel) params.set("model", selectedModel); if (selectedProvider) params.set("provider", selectedProvider); if (selectedAccount) params.set("account", selectedAccount); if (selectedApiKey) params.set("apiKey", selectedApiKey); params.set("limit", "300"); const res = await fetch(`/api/usage/call-logs?${params}`); if (res.ok) { const data = await res.json(); setLogs(data); } } catch (error) { console.error("Failed to fetch call logs:", error); } finally { if (showLoading) setLoading(false); } }, [search, activeFilter, selectedModel, selectedAccount, selectedProvider, selectedApiKey] ); useEffect(() => { const showLoading = !hasLoadedRef.current; hasLoadedRef.current = true; fetchLogs(showLoading); }, [fetchLogs]); // Auto-refresh useEffect(() => { if (intervalRef.current) clearInterval(intervalRef.current); if (recording) { intervalRef.current = setInterval(() => fetchLogs(false), 3000); } return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [recording, fetchLogs]); const filteredLogs = useMemo(() => { if (activeFilter === "combo") return logs.filter((l) => l.comboName); return logs; }, [activeFilter, logs]); const sortedLogs = useMemo(() => { const arr = [...filteredLogs]; arr.sort((a, b) => { switch (sortBy) { case "oldest": return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(); case "tokens_desc": return getLogTotalTokens(b) - getLogTotalTokens(a); case "tokens_asc": return getLogTotalTokens(a) - getLogTotalTokens(b); case "duration_desc": return (b.duration || 0) - (a.duration || 0); case "duration_asc": return (a.duration || 0) - (b.duration || 0); case "status_desc": return (b.status || 0) - (a.status || 0); case "status_asc": return (a.status || 0) - (b.status || 0); case "model_asc": return (a.model || "").localeCompare(b.model || ""); case "model_desc": return (b.model || "").localeCompare(a.model || ""); case "newest": default: return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(); } }); return arr; }, [filteredLogs, sortBy]); // Fetch log detail const openDetail = async (logEntry) => { setSelectedLog(logEntry); setDetailLoading(true); setDetailData(null); try { const res = await fetch(`/api/usage/call-logs/${logEntry.id}`); if (res.ok) { const data = await res.json(); setDetailData(data); } } catch (error) { console.error("Failed to fetch log detail:", error); } finally { setDetailLoading(false); } }; const closeDetail = () => { setSelectedLog(null); setDetailData(null); }; // Copy to clipboard const copyToClipboard = async (text) => { try { await navigator.clipboard.writeText(text); return true; } catch { // Fallback for non-HTTPS or older browsers try { const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "fixed"; textarea.style.left = "-9999px"; document.body.appendChild(textarea); textarea.select(); document.execCommand("copy"); document.body.removeChild(textarea); return true; } catch { return false; } } }; // Unique accounts and providers for dropdowns const uniqueAccounts = [...new Set(logs.map((l) => l.account).filter((a) => a && a !== "-"))]; const uniqueModels = [...new Set(logs.map((l) => l.model).filter(Boolean))].sort(); const uniqueProviders = [ ...new Set(logs.map((l) => l.provider).filter((p) => p && p !== "-")), ].sort(); const uniqueApiKeys = [ ...new Set(logs.map((l) => l.apiKeyId || l.apiKeyName).filter(Boolean)), ].sort(); // Stats const totalCount = filteredLogs.length; const okCount = filteredLogs.filter((l) => l.status >= 200 && l.status < 300).length; const errorCount = filteredLogs.filter((l) => l.status >= 400).length; const comboCount = logs.filter((l) => l.comboName).length; const apiKeyCount = uniqueApiKeys.length; return (
{/* Header Bar */}
{/* Recording Toggle */} {/* Search */}
search setSearch(e.target.value)} className="w-full pl-10 pr-4 py-2 rounded-lg bg-bg-subtle border border-border text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:border-primary" />
{/* Provider Dropdown */} {/* Model Dropdown */} {/* Account Dropdown */} {/* API Key Dropdown */} {/* Stats */}
{totalCount} total {okCount} OK {errorCount > 0 && ( {errorCount} ERR )} {comboCount > 0 && ( {comboCount} combo )} {apiKeyCount > 0 && ( {apiKeyCount} keys )} {sortedLogs.length} shown
{/* Sort Dropdown */} {/* Refresh */}
{/* Quick Filters */}
{/* Status Filters */} {STATUS_FILTERS.map((f) => ( ))} {/* Divider */} {uniqueProviders.length > 0 && } {/* Dynamic Provider Quick Filters (from data) */} {uniqueProviders.map((p) => { const pc = PROVIDER_COLORS[p] || { bg: "#374151", text: "#fff", label: p.toUpperCase() }; const isActive = selectedProvider === p; return ( ); })}
{/* Column Visibility Toggles */}
Columns {COLUMNS.map((col) => ( ))}
{/* Table */}
{loading && logs.length === 0 ? (
Loading logs...
) : logs.length === 0 ? (
receipt_long No logs recorded yet. Make some API calls to see them here.
) : sortedLogs.length === 0 ? (
No logs match the current filters.
) : ( {visibleColumns.status && ( )} {visibleColumns.model && ( )} {visibleColumns.provider && ( )} {visibleColumns.protocol && ( )} {visibleColumns.account && ( )} {visibleColumns.apiKey && ( )} {visibleColumns.combo && ( )} {visibleColumns.tokens && ( )} {visibleColumns.duration && ( )} {visibleColumns.time && ( )} {sortedLogs.map((log) => { const statusStyle = getStatusStyle(log.status); const protocolKey = log.sourceFormat || log.provider; const protocol = PROTOCOL_COLORS[protocolKey] || PROTOCOL_COLORS[log.provider] || { bg: "#6B7280", text: "#fff", label: (protocolKey || log.provider || "-").toUpperCase(), }; const providerColor = PROVIDER_COLORS[log.provider] || { bg: "#374151", text: "#fff", label: (log.provider || "-").toUpperCase(), }; const isError = log.status >= 400; return ( openDetail(log)} className={`cursor-pointer hover:bg-primary/5 transition-colors ${isError ? "bg-red-500/5" : ""}`} > {visibleColumns.status && ( )} {visibleColumns.model && ( )} {visibleColumns.provider && ( )} {visibleColumns.protocol && ( )} {visibleColumns.account && ( )} {visibleColumns.apiKey && ( )} {visibleColumns.combo && ( )} {visibleColumns.tokens && ( )} {visibleColumns.duration && ( )} {visibleColumns.time && ( )} ); })}
Status Model Provider Protocol Account API Key Combo Tokens Duration Time
{log.status || "..."} {log.model} {providerColor.label} {protocol.label} {maskAccount(log.account)} {formatApiKeyLabel(log.apiKeyName, log.apiKeyId)} {log.comboName ? ( {log.comboName} ) : ( )} I:{" "} {log.tokens?.in?.toLocaleString() || 0} | O:{" "} {log.tokens?.out?.toLocaleString() || 0} {formatDuration(log.duration)} {formatTime(log.timestamp)}
)}
Call logs are also saved as JSON files to {`{DATA_DIR}/call_logs/`} with 7-day rotation.
{/* Detail Modal */} {selectedLog && ( )}
); }