"use client"; import { useCallback, useEffect, useState, type ReactNode } from "react"; import Image from "next/image"; import { useTranslations } from "next-intl"; import { LlmChatCard, type LlmChatControls, } from "@/app/(dashboard)/dashboard/media-providers/components/LlmChatCard"; import ProviderIcon from "@/shared/components/ProviderIcon"; import { useApiKey } from "@/app/(dashboard)/dashboard/providers/hooks/useApiKey"; import { useProviderModels } from "@/app/(dashboard)/dashboard/providers/hooks/useProviderModels"; interface SlideOverProvider { id?: string; name: string; color?: string; apiType?: string; deprecated?: boolean; deprecationReason?: string; subscriptionRisk?: boolean; serviceKinds?: string[]; } interface ProviderTestSlideOverProps { isOpen: boolean; onClose: () => void; providerId: string; provider: SlideOverProvider; staticIconPath?: string | null; initialTab?: TabKey; } type TabKey = "test" | "logs"; const TABS: { key: TabKey; label: string; icon: string }[] = [ { key: "test", label: "test", icon: "play_arrow" }, { key: "logs", label: "logs", icon: "receipt_long" }, ]; export default function ProviderTestSlideOver(props: ProviderTestSlideOverProps) { if (!props.isOpen) return null; return ; } function ProviderTestSlideOverPanel({ onClose, providerId, provider, staticIconPath, initialTab = "test", }: ProviderTestSlideOverProps) { const t = useTranslations("providerTest"); const [tab, setTab] = useState(initialTab); const [model, setModel] = useState(""); const [selectedKey, setSelectedKey] = useState(""); const [controls, setControls] = useState(null); const onControlsChange = useCallback((c: LlmChatControls) => setControls(c), []); const { keys } = useApiKey(); const { models } = useProviderModels(providerId); const firstModel = models[0]?.id ?? ""; useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [onClose]); useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, []); const color = provider.color || "#64748b"; const modelOptions = models.length > 0 ? models : []; return (
{tab === "test" && ( )}
{tab === "test" && (
)} {tab === "logs" && }
); } function SlideOverHeader({ provider, providerId, staticIconPath, color, onClose, }: { provider: SlideOverProvider; providerId: string; staticIconPath?: string | null; color: string; onClose: () => void; }) { const t = useTranslations("providerTest"); return (
{staticIconPath ? ( {provider.name} ) : ( )}

{provider.name}

{provider.apiType && {provider.apiType}} {provider.deprecated && ( <> · block {t("deprecated")} )} {provider.subscriptionRisk && ( <> · info {t("risk")} )}
); } function TestToolbar({ model, onModelChange, modelOptions, selectedKey, onSelectedKeyChange, keys, controls, }: { model: string; onModelChange: (m: string) => void; modelOptions: { id: string }[]; selectedKey: string; onSelectedKeyChange: (k: string) => void; keys: { id: string; key: string; name?: string }[]; controls: LlmChatControls | null; }) { const t = useTranslations("providerTest"); const hasMessages = controls?.hasMessages ?? false; return (
{keys.length > 0 && (
)} {hasMessages && ( )}
); } function SlideOverTabs({ tab, onChange }: { tab: TabKey; onChange: (next: TabKey) => void }) { const translate = useTranslations("providerTest"); return (
{TABS.map((tabItem) => { const active = tabItem.key === tab; return ( ); })}
); } function TabPlaceholder({ icon, title, body }: { icon: string; title: string; body: ReactNode }) { return (
{icon}

{title}

{body}
); } interface LogEntry { id: string | number; timestamp: string; model?: string; requestedModel?: string; provider?: string; providerDisplay?: string | null; status?: number; duration?: number; tokens?: { in?: number; out?: number }; account?: string; apiKey?: string; apiKeyName?: string; apiKeyId?: string; } function formatRequester( log: LogEntry, unknownRequester: string ): { label: string; title: string } { const name = log.apiKeyName || log.account; const keyHint = log.apiKey || log.apiKeyId; const masked = typeof keyHint === "string" && keyHint.length > 8 ? `${keyHint.slice(0, 4)}…${keyHint.slice(-4)}` : keyHint || ""; if (name && masked) return { label: name, title: `${name} (${masked})` }; if (name) return { label: name, title: name }; if (masked) return { label: masked, title: keyHint || masked }; return { label: "—", title: unknownRequester }; } type LogsState = | { status: "loading" } | { status: "ready"; logs: LogEntry[] } | { status: "error"; message: string }; function LogsTab({ providerId }: { providerId: string }) { const t = useTranslations("providerTest"); const [state, setState] = useState({ status: "loading" }); const [expanded, setExpanded] = useState(null); const [refreshTick, setRefreshTick] = useState(0); useEffect(() => { let cancelled = false; const ctrl = new AbortController(); async function load() { try { const url = `/api/usage/call-logs?provider=${encodeURIComponent(providerId)}&limit=20`; const res = await fetch(url, { signal: ctrl.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); if (cancelled) return; const logs: LogEntry[] = Array.isArray(data) ? data : (data?.logs ?? []); setState({ status: "ready", logs }); } catch (err) { if (cancelled || (err as { name?: string })?.name === "AbortError") return; setState({ status: "error", message: (err as Error).message || t("failedToLoadLogs") }); } } void load(); const interval = setInterval(load, 2000); return () => { cancelled = true; ctrl.abort(); clearInterval(interval); }; }, [providerId, refreshTick, t]); const handleRefresh = useCallback(() => setRefreshTick((n) => n + 1), []); if (state.status === "loading") { return (
progress_activity {t("loadingLogs")}
); } if (state.status === "error") { return ( {state.message}

} /> ); } if (state.logs.length === 0) { return (

{t("noLogsDescription")}

{t("openFullLogs")} open_in_new } /> ); } return (
{t("live")} · {t("tailingLast", { count: state.logs.length })}
    {state.logs.map((log) => { const key = String(log.id); const isExpanded = expanded === key; const statusOk = typeof log.status === "number" && log.status >= 200 && log.status < 400; const statusColor = statusOk ? "text-emerald-600 dark:text-emerald-400" : typeof log.status === "number" ? "text-red-500" : "text-text-muted"; const requester = formatRequester(log, t("unknownRequester")); return (
  • {isExpanded && }
  • ); })}
); } function LogDetail({ log }: { log: LogEntry }) { const t = useTranslations("providerTest"); const requester = formatRequester(log, t("unknownRequester")); const tokensIn = log.tokens?.in; const tokensOut = log.tokens?.out; const rows: { label: string; value: string; mono?: boolean }[] = [ { label: t("fields.timestamp"), value: new Date(log.timestamp).toLocaleString(), mono: true }, { label: t("fields.status"), value: String(log.status ?? "—") }, { label: t("fields.duration"), value: formatDurationMs(log.duration) }, { label: t("fields.model"), value: log.model || "—", mono: true }, { label: t("fields.requestedModel"), value: log.requestedModel || "—", mono: true }, { label: t("fields.provider"), value: log.providerDisplay || log.provider || "—", mono: true }, { label: t("fields.requester"), value: requester.title, mono: true }, { label: t("fields.tokens"), value: tokensIn != null || tokensOut != null ? `in ${tokensIn ?? 0} · out ${tokensOut ?? 0}` : "—", }, ]; return (
{rows.map((row) => (
{row.label}
{row.value}
))}
); } function formatRelativeTs(ts: string | undefined): string { if (!ts) return "—"; const date = new Date(ts); if (isNaN(date.getTime())) return "—"; const diff = (Date.now() - date.getTime()) / 1000; if (diff < 60) return `${Math.max(0, Math.floor(diff))}s ago`; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; return date.toLocaleDateString(); } function formatDurationMs(ms: number | undefined): string { if (ms == null) return "—"; if (ms < 1000) return `${Math.round(ms)}ms`; return `${(ms / 1000).toFixed(2)}s`; }