| "use client";
|
|
|
| import { useEffect, useState } from "react";
|
| import { useTranslations } from "next-intl";
|
| import { maskAccount } from "@/shared/utils/formatting";
|
| import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel";
|
| import useEmailPrivacyStore from "@/store/emailPrivacyStore";
|
|
|
| type ActiveRequestRow = {
|
| model: string;
|
| provider: string;
|
| account: string;
|
| startedAt: number;
|
| runningTimeMs: number;
|
| count: number;
|
| clientEndpoint?: string | null;
|
| clientRequest?: unknown;
|
| providerRequest?: unknown;
|
| providerUrl?: string | null;
|
| stage?: string | null;
|
| stageUpdatedAt?: number | null;
|
| };
|
|
|
| const STAGE_LABELS: Record<string, string> = {
|
| registered: "activeStageRegistered",
|
| payload_prepared: "activeStagePayloadPrepared",
|
| waiting_account_slot: "activeStageWaitingAccountSlot",
|
| waiting_rate_limit: "activeStageWaitingRateLimit",
|
| rate_limit_slot_acquired: "activeStageRateLimitSlotAcquired",
|
| sending_to_provider: "activeStageSendingToProvider",
|
| provider_response_started: "activeStageProviderResponseStarted",
|
| };
|
|
|
| function formatDuration(ms: number): string {
|
| const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
| const minutes = Math.floor(totalSeconds / 60);
|
| const seconds = totalSeconds % 60;
|
| if (minutes <= 0) return `${seconds}s`;
|
| if (minutes < 60) return `${minutes}m ${seconds}s`;
|
| const hours = Math.floor(minutes / 60);
|
| return `${hours}h ${minutes % 60}m`;
|
| }
|
|
|
| export default function ActiveRequestsPanel() {
|
| const t = useTranslations("logs");
|
| const { emailsVisible } = useEmailPrivacyStore();
|
| const [rows, setRows] = useState<ActiveRequestRow[]>([]);
|
| const [loading, setLoading] = useState(true);
|
| const [selectedRow, setSelectedRow] = useState<ActiveRequestRow | null>(null);
|
| const [providerNodes, setProviderNodes] = useState<
|
| Array<{ id?: string; prefix?: string; name?: string }>
|
| >([]);
|
|
|
| useEffect(() => {
|
| fetch("/api/provider-nodes")
|
| .then((r) => (r.ok ? r.json() : { nodes: [] }))
|
| .then((d) => setProviderNodes(d.nodes || []))
|
| .catch(() => {});
|
| }, []);
|
|
|
| useEffect(() => {
|
| let cancelled = false;
|
|
|
| const load = async () => {
|
|
|
| if (document.visibilityState !== "visible") return;
|
| try {
|
| const res = await fetch("/api/logs/active", { cache: "no-store" });
|
| if (!res.ok) return;
|
| const data = await res.json();
|
| if (!cancelled) {
|
| setRows(Array.isArray(data.activeRequests) ? data.activeRequests : []);
|
| }
|
| } catch (error) {
|
| if (!cancelled) {
|
| console.error("Failed to load active requests:", error);
|
| }
|
| } finally {
|
| if (!cancelled) {
|
| setLoading(false);
|
| }
|
| }
|
| };
|
|
|
| load();
|
| const interval = setInterval(load, 15_000);
|
| return () => {
|
| cancelled = true;
|
| clearInterval(interval);
|
| };
|
| }, []);
|
|
|
| const handleClearAll = async () => {
|
| if (!window.confirm(t("confirmClearActiveRequests") || "Clear all active requests?")) return;
|
| try {
|
| const res = await fetch("/api/logs/active", { method: "DELETE" });
|
| if (res.ok) {
|
| setRows([]);
|
| setSelectedRow(null);
|
| }
|
| } catch (error) {
|
| console.error("Failed to clear active requests:", error);
|
| }
|
| };
|
|
|
| if (!loading && rows.length === 0) {
|
| return null;
|
| }
|
|
|
| const selectedAccountLabel = selectedRow ? maskAccount(selectedRow.account, emailsVisible) : "";
|
| const formatStage = (stage?: string | null): string => {
|
| if (!stage) return t("activeStageUnknown");
|
| const key = STAGE_LABELS[stage];
|
| if (key) return t(key);
|
| return stage.replace(/_/g, " ");
|
| };
|
|
|
| return (
|
| <div className="rounded-xl border border-border bg-surface">
|
| <div className="flex items-center justify-between gap-4 border-b border-border px-4 py-3">
|
| <div>
|
| <h3 className="text-sm font-semibold uppercase tracking-wide text-text-main">
|
| {t("runningRequests")}
|
| </h3>
|
| <p className="text-xs text-text-muted">{t("runningRequestsDesc")}</p>
|
| </div>
|
| <div className="flex items-center gap-3">
|
| <div className="inline-flex items-center gap-2 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-3 py-1 text-xs font-medium text-emerald-300">
|
| <span className="h-2 w-2 rounded-full bg-emerald-400 animate-pulse" />
|
| {loading ? t("loading") : t("activeCount", { count: rows.length })}
|
| </div>
|
| {rows.length > 0 && (
|
| <button
|
| onClick={handleClearAll}
|
| className="rounded-md border border-red-500/30 bg-red-500/10 px-3 py-1 text-xs font-medium text-red-400 transition-colors hover:bg-red-500/20 hover:text-red-300"
|
| >
|
| {t("clearAll") || "Clear All"}
|
| </button>
|
| )}
|
| </div>
|
| </div>
|
|
|
| <div className="overflow-x-auto">
|
| <table className="min-w-full text-sm">
|
| <thead className="bg-sidebar/40 text-left text-xs uppercase tracking-wide text-text-muted">
|
| <tr>
|
| <th className="px-4 py-3 font-medium">{t("model")}</th>
|
| <th className="px-4 py-3 font-medium">{t("provider")}</th>
|
| <th className="px-4 py-3 font-medium">{t("account")}</th>
|
| <th className="px-4 py-3 font-medium">{t("elapsed")}</th>
|
| <th className="px-4 py-3 font-medium">{t("activeStage")}</th>
|
| <th className="px-4 py-3 font-medium">{t("count")}</th>
|
| <th className="px-4 py-3 font-medium">{t("payloads")}</th>
|
| </tr>
|
| </thead>
|
| <tbody>
|
| {rows.map((row) => {
|
| const accountLabel = maskAccount(row.account, emailsVisible);
|
| return (
|
| <tr
|
| key={`${row.account}:${row.provider}:${row.model}:${row.startedAt}`}
|
| className="border-t border-border/60"
|
| >
|
| <td className="px-4 py-3 font-medium text-text-main">{row.model}</td>
|
| <td className="px-4 py-3 text-text-muted">
|
| {getProviderDisplayLabel(row.provider, providerNodes) || row.provider}
|
| </td>
|
| <td className="px-4 py-3 text-text-muted" title={accountLabel}>
|
| {accountLabel}
|
| </td>
|
| <td className="px-4 py-3 text-text-main">{formatDuration(row.runningTimeMs)}</td>
|
| <td className="px-4 py-3 text-text-muted">{formatStage(row.stage)}</td>
|
| <td className="px-4 py-3 text-text-main">{row.count}</td>
|
| <td className="px-4 py-3">
|
| <button
|
| type="button"
|
| onClick={() => setSelectedRow(row)}
|
| className="rounded-md border border-border px-3 py-1.5 text-xs font-medium text-text-main transition-colors hover:bg-sidebar/40"
|
| >
|
| {t("viewPayloads")}
|
| </button>
|
| </td>
|
| </tr>
|
| );
|
| })}
|
| </tbody>
|
| </table>
|
| </div>
|
|
|
| {selectedRow && (
|
| <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/55 px-4 py-6 backdrop-blur-sm">
|
| <div className="flex max-h-[85vh] w-full max-w-5xl flex-col overflow-hidden rounded-2xl border border-border bg-surface shadow-2xl">
|
| <div className="flex items-start justify-between gap-4 border-b border-border px-5 py-4">
|
| <div>
|
| <h4 className="text-lg font-semibold text-text-main">
|
| {getProviderDisplayLabel(selectedRow.provider, providerNodes) ||
|
| selectedRow.provider}{" "}
|
| / {selectedRow.model}
|
| </h4>
|
| <p className="mt-1 text-sm text-text-muted">
|
| {t("runningRequestDetailMeta", {
|
| account: selectedAccountLabel,
|
| elapsed: formatDuration(selectedRow.runningTimeMs),
|
| })}
|
| </p>
|
| <p className="mt-1 text-xs text-text-muted">
|
| {t("activeStage")}: {formatStage(selectedRow.stage)}
|
| </p>
|
| </div>
|
| <button
|
| type="button"
|
| onClick={() => setSelectedRow(null)}
|
| className="rounded-full border border-border p-2 text-text-muted transition-colors hover:bg-sidebar/40 hover:text-text-main"
|
| aria-label={t("close")}
|
| >
|
| <span className="material-symbols-outlined text-[18px]">close</span>
|
| </button>
|
| </div>
|
|
|
| <div className="grid gap-4 overflow-y-auto px-5 py-5 md:grid-cols-2">
|
| <section className="rounded-xl border border-border bg-bg-subtle p-4">
|
| <div className="mb-3">
|
| <h5 className="text-sm font-semibold text-text-main">{t("clientPayload")}</h5>
|
| <p className="mt-1 text-xs text-text-muted">
|
| {selectedRow.clientEndpoint || t("notAvailable")}
|
| </p>
|
| </div>
|
| <pre className="overflow-x-auto rounded-lg border border-border/70 bg-bg p-3 text-xs text-text-muted">
|
| {JSON.stringify(selectedRow.clientRequest || {}, null, 2)}
|
| </pre>
|
| </section>
|
|
|
| <section className="rounded-xl border border-border bg-bg-subtle p-4">
|
| <div className="mb-3">
|
| <h5 className="text-sm font-semibold text-text-main">{t("upstreamPayload")}</h5>
|
| <p className="mt-1 break-all text-xs text-text-muted">
|
| {selectedRow.providerUrl || formatStage(selectedRow.stage)}
|
| </p>
|
| </div>
|
| <pre className="overflow-x-auto rounded-lg border border-border/70 bg-bg p-3 text-xs text-text-muted">
|
| {JSON.stringify(selectedRow.providerRequest || {}, null, 2)}
|
| </pre>
|
| </section>
|
| </div>
|
| </div>
|
| </div>
|
| )}
|
| </div>
|
| );
|
| }
|
|
|