hahma / src /shared /components /ActiveRequestsPanel.tsx
automindy's picture
Upload 1980 files
bc4a7e8 verified
Raw
History Blame Contribute Delete
10.5 kB
"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 () => {
// Skip polling when tab is not visible to save resources
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>
);
}