"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.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 (
);
}
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`;
}