Pipeline/frontend/src/components/organisms/ProviderNavbarStatus.tsx

389 lines
12 KiB
TypeScript

"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { ChevronDown } from "lucide-react";
import { useAuth } from "@/auth/clerk";
import type {
ProviderCredentialRead,
ProviderUsageLiveRead,
SubscriptionWindowRead,
} from "@/api/generated/model";
import {
getProviderUsageLiveApiV1ProviderCredentialsCredentialIdUsageGet,
listProviderCredentialsApiV1ProviderCredentialsGet,
} from "@/api/generated/provider-credentials/provider-credentials";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
type NavbarProviderId = "anthropic" | "openai";
type ProviderNavbarItem = {
providerId: NavbarProviderId;
label: string;
status: "Active" | "Inactive" | "Not configured" | "Syncing";
sessionRemaining: number | null;
modelsRemaining: number | null;
reset: string;
};
const NAVBAR_PROVIDER_IDS: NavbarProviderId[] = ["anthropic", "openai"];
function providerShortLabel(provider: string): string {
if (provider === "anthropic") return "Claude";
if (provider === "openai") return "GPT";
return provider;
}
function clampPct(pct: number): number {
return Math.max(0, Math.min(100, pct));
}
function fmtResetMs(ms: number | null | undefined): string {
if (ms == null || ms <= 0) return "< 1m";
const s = Math.floor(ms / 1000);
if (s < 60) return `${s}s`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m ${s % 60}s`;
const h = Math.floor(m / 60);
return `${h}h ${m % 60}m`;
}
function remainingPct(window: SubscriptionWindowRead | null): number | null {
if (!window) return null;
return clampPct(100 - window.pct_used);
}
function remainingColor(pct: number | null): string {
if (pct == null) return "bg-[color:var(--text-quiet)]";
if (pct <= 10) return "bg-[color:var(--danger)]";
if (pct <= 25) return "bg-[color:var(--warning)]";
return "bg-[color:var(--success)]";
}
function findSessionWindow(usage: ProviderUsageLiveRead | null | undefined) {
const windows = usage?.subscription_windows ?? [];
return (
windows.find((window) => {
const key = `${window.key} ${window.label}`.toLowerCase();
return (
key.includes("current") ||
key.includes("session") ||
key.includes("five") ||
key.includes("hour")
);
}) ??
windows[0] ??
null
);
}
function findModelsWindow(
usage: ProviderUsageLiveRead | null | undefined,
sessionWindow: SubscriptionWindowRead | null,
) {
const windows = usage?.subscription_windows ?? [];
return (
windows.find((window) => {
if (window.key === sessionWindow?.key) return false;
const key = `${window.key} ${window.label}`.toLowerCase();
return key.includes("all") || key.includes("model") || key.includes("week");
}) ??
windows.find((window) => window.key !== sessionWindow?.key) ??
null
);
}
function resetLabel(
sessionWindow: SubscriptionWindowRead | null,
modelsWindow: SubscriptionWindowRead | null,
): string {
const resetInMs = sessionWindow?.reset_in_ms ?? modelsWindow?.reset_in_ms;
return resetInMs == null ? "-" : fmtResetMs(resetInMs);
}
function formatRemaining(pct: number | null): string {
return pct == null ? "-" : `${Math.round(pct)}% left`;
}
function providerStatusColor(status: ProviderNavbarItem["status"]): string {
if (status === "Active") return "bg-[color:var(--success)]";
if (status === "Syncing") return "bg-[color:var(--accent)]";
if (status === "Inactive") return "bg-[color:var(--warning)]";
return "bg-[color:var(--text-quiet)]";
}
function MiniRemainingBar({
pct,
className = "w-10",
}: {
pct: number | null;
className?: string;
}) {
if (pct == null) return null;
const width = clampPct(pct);
return (
<span
className={`h-1 overflow-hidden rounded-full bg-[color:var(--surface-strong)] ${className}`}
aria-hidden="true"
>
<span
className={`block h-full rounded-full ${remainingColor(pct)}`}
style={{ width: `${width}%` }}
/>
</span>
);
}
function buildProviderNavbarItems({
credentials,
usageByCredentialId,
isLoading,
}: {
credentials: ProviderCredentialRead[];
usageByCredentialId: Record<string, ProviderUsageLiveRead | null>;
isLoading: boolean;
}): ProviderNavbarItem[] {
return NAVBAR_PROVIDER_IDS.map((providerId) => {
const providerCredentials = credentials
.filter((cred) => cred.provider === providerId)
.sort((a, b) => a.account_key.localeCompare(b.account_key));
const activeCredential =
providerCredentials.find(
(cred) =>
cred.active &&
(cred.has_api_key || cred.has_session_key || Boolean(cred.base_url)),
) ?? null;
const credential = activeCredential ?? providerCredentials[0] ?? null;
const usage = activeCredential
? usageByCredentialId[activeCredential.id]
: null;
const status: ProviderNavbarItem["status"] = activeCredential
? isLoading && usage == null
? "Syncing"
: "Active"
: credential
? "Inactive"
: "Not configured";
const sessionWindow = findSessionWindow(usage);
const modelsWindow = findModelsWindow(usage, sessionWindow);
const sessionRemaining = remainingPct(sessionWindow);
const modelsRemaining = remainingPct(modelsWindow);
return {
providerId,
label: providerShortLabel(providerId),
status,
sessionRemaining,
modelsRemaining,
reset: resetLabel(sessionWindow, modelsWindow),
};
});
}
function ProviderInlineStatus({
item,
compact = false,
}: {
item: ProviderNavbarItem;
compact?: boolean;
}) {
return (
<div
className={`flex min-w-0 items-center ${
compact ? "flex-wrap gap-x-2 gap-y-1" : "gap-1.5 whitespace-nowrap"
}`}
>
<span
className={`h-1.5 w-1.5 shrink-0 rounded-full ${providerStatusColor(item.status)}`}
/>
<span className="font-semibold text-[color:var(--text)]">{item.label}</span>
<span className="text-[color:var(--text-muted)]">{item.status}</span>
<span className="text-[color:var(--text-quiet)]">|</span>
<span className="text-[color:var(--text-muted)]">
Session{" "}
<span className="tabular-nums text-[color:var(--text)]">
{formatRemaining(item.sessionRemaining)}
</span>
</span>
<MiniRemainingBar pct={item.sessionRemaining} className="w-8 opacity-80" />
<span className="text-[color:var(--text-quiet)]">|</span>
<span className="text-[color:var(--text-muted)]">
Models{" "}
<span className="tabular-nums text-[color:var(--text)]">
{formatRemaining(item.modelsRemaining)}
</span>
</span>
<MiniRemainingBar pct={item.modelsRemaining} className="w-8 opacity-80" />
<span className="text-[color:var(--text-quiet)]">|</span>
<span className="text-[color:var(--text-muted)]">
Reset <span className="tabular-nums text-[color:var(--text)]">{item.reset}</span>
</span>
</div>
);
}
export function ProviderNavbarStatus() {
const { isSignedIn } = useAuth();
const [credentials, setCredentials] = useState<ProviderCredentialRead[]>([]);
const [usageByCredentialId, setUsageByCredentialId] = useState<
Record<string, ProviderUsageLiveRead | null>
>({});
const [isUsageLoading, setIsUsageLoading] = useState(false);
const usageCredentials = useMemo(() => {
return credentials
.filter(
(cred) =>
NAVBAR_PROVIDER_IDS.includes(cred.provider as NavbarProviderId) &&
cred.active &&
(cred.has_api_key || cred.has_session_key || Boolean(cred.base_url)),
)
.sort((a, b) => {
const providerDelta =
NAVBAR_PROVIDER_IDS.indexOf(a.provider as NavbarProviderId) -
NAVBAR_PROVIDER_IDS.indexOf(b.provider as NavbarProviderId);
if (providerDelta !== 0) return providerDelta;
return a.account_key.localeCompare(b.account_key);
});
}, [credentials]);
const loadCredentials = useCallback(async () => {
try {
const res = await listProviderCredentialsApiV1ProviderCredentialsGet();
if (res.status === 200) setCredentials(res.data);
} catch {
setCredentials([]);
}
}, []);
useEffect(() => {
if (!isSignedIn) return;
const initialLoad = window.setTimeout(() => {
void loadCredentials();
}, 0);
window.addEventListener("provider-credentials-updated", loadCredentials);
const interval = window.setInterval(() => {
void loadCredentials();
}, 60_000);
return () => {
window.clearTimeout(initialLoad);
window.removeEventListener("provider-credentials-updated", loadCredentials);
window.clearInterval(interval);
};
}, [isSignedIn, loadCredentials]);
useEffect(() => {
if (!isSignedIn || usageCredentials.length === 0) {
const resetUsage = window.setTimeout(() => {
setUsageByCredentialId({});
setIsUsageLoading(false);
}, 0);
return () => window.clearTimeout(resetUsage);
}
let cancelled = false;
const fetchUsage = async (refresh = false) => {
setIsUsageLoading(true);
const pairs = await Promise.all(
usageCredentials.map(async (cred) => {
try {
const res = await getProviderUsageLiveApiV1ProviderCredentialsCredentialIdUsageGet(
cred.id,
refresh ? { refresh: true } : undefined,
);
return [cred.id, res.status === 200 ? res.data : null] as const;
} catch {
return [cred.id, null] as const;
}
}),
);
if (!cancelled) {
setUsageByCredentialId(Object.fromEntries(pairs));
setIsUsageLoading(false);
}
};
const initialFetch = window.setTimeout(() => {
void fetchUsage();
}, 0);
const interval = window.setInterval(() => {
void fetchUsage(true);
}, 60_000);
return () => {
cancelled = true;
window.clearTimeout(initialFetch);
window.clearInterval(interval);
};
}, [isSignedIn, usageCredentials]);
const items = buildProviderNavbarItems({
credentials,
usageByCredentialId,
isLoading: isUsageLoading,
});
const activeCount = items.filter(
(item) => item.status === "Active" || item.status === "Syncing",
).length;
const primaryRemaining =
items
.flatMap((item) => [item.sessionRemaining, item.modelsRemaining])
.filter((pct): pct is number => pct != null)
.sort((a, b) => a - b)[0] ?? null;
return (
<div className="flex h-9 min-w-0 items-center text-[11px]">
<div className="hidden h-9 min-w-0 items-center gap-2 overflow-hidden xl:flex">
{items.map((item, index) => (
<div key={item.providerId} className="flex min-w-0 items-center gap-2">
{index > 0 ? (
<span className="h-3.5 w-px shrink-0 bg-[color:var(--border)]" />
) : null}
<ProviderInlineStatus item={item} />
</div>
))}
</div>
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className="flex h-8 min-w-0 max-w-[42vw] items-center gap-2 text-[11px] font-medium text-[color:var(--text-muted)] transition hover:text-[color:var(--text)] xl:hidden"
aria-label="Open provider status summary"
>
<span
className={`h-1.5 w-1.5 shrink-0 rounded-full ${
activeCount > 0
? "bg-[color:var(--success)]"
: "bg-[color:var(--text-quiet)]"
}`}
/>
<span className="truncate">Providers {activeCount} active</span>
<MiniRemainingBar pct={primaryRemaining} className="w-10 shrink-0 opacity-80" />
<span className="shrink-0 tabular-nums">
{formatRemaining(primaryRemaining)}
</span>
<ChevronDown className="h-3 w-3 shrink-0" />
</button>
</PopoverTrigger>
<PopoverContent
align="center"
sideOffset={10}
className="w-[min(420px,calc(100vw-2rem))] rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-3 shadow-xl"
>
<div className="space-y-2 text-[11px]">
{items.map((item) => (
<ProviderInlineStatus key={item.providerId} item={item} compact />
))}
</div>
</PopoverContent>
</Popover>
</div>
);
}