"use client";
import { useCallback, useEffect, useState, type ReactNode } from "react";
import Image from "next/image";
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 [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;
}) {
return (
{staticIconPath ? (
) : (
)}
{provider.name}
{provider.apiType && {provider.apiType}}
{provider.deprecated && (
<>
·
block
deprecated
>
)}
{provider.subscriptionRisk && (
<>
·
info
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 hasMessages = controls?.hasMessages ?? false;
return (
{keys.length > 0 && (
)}
{hasMessages && (
)}
);
}
function SlideOverTabs({ tab, onChange }: { tab: TabKey; onChange: (next: TabKey) => void }) {
return (
{TABS.map((t) => {
const active = t.key === tab;
return (
);
})}
);
}
function TabPlaceholder({ icon, title, body }: { icon: string; title: string; body: ReactNode }) {
return (
);
}
interface LogEntry {
id: string | number;
timestamp: string;
model?: string;
requestedModel?: string;
provider?: string;
status?: number;
duration?: number;
tokens?: { in?: number; out?: number };
account?: string;
apiKey?: string;
apiKeyName?: string;
apiKeyId?: string;
}
function formatRequester(log: LogEntry): { 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: "unknown requester" };
}
type LogsState =
| { status: "loading" }
| { status: "ready"; logs: LogEntry[] }
| { status: "error"; message: string };
function LogsTab({ providerId }: { providerId: string }) {
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 || "Failed to load logs" });
}
}
void load();
const interval = setInterval(load, 2000);
return () => {
cancelled = true;
ctrl.abort();
clearInterval(interval);
};
}, [providerId, refreshTick]);
const handleRefresh = useCallback(() => setRefreshTick((n) => n + 1), []);
if (state.status === "loading") {
return (
progress_activity
Loading logs…
);
}
if (state.status === "error") {
return (
{state.message}
} />
);
}
if (state.logs.length === 0) {
return (
Send a test message from the Test tab — logs for this provider will appear here.
Open full logs page
open_in_new
>
}
/>
);
}
return (
Live
·
tailing last {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);
return (
-
{isExpanded && }
);
})}
);
}
function LogDetail({ log }: { log: LogEntry }) {
const requester = formatRequester(log);
const tokensIn = log.tokens?.in;
const tokensOut = log.tokens?.out;
const rows: { label: string; value: string; mono?: boolean }[] = [
{ label: "Timestamp", value: new Date(log.timestamp).toLocaleString(), mono: true },
{ label: "Status", value: String(log.status ?? "—") },
{ label: "Duration", value: formatDurationMs(log.duration) },
{ label: "Model", value: log.model || "—", mono: true },
{ label: "Requested model", value: log.requestedModel || "—", mono: true },
{ label: "Provider", value: log.provider || "—", mono: true },
{ label: "Requester", value: requester.title, mono: true },
{
label: "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`;
}