"use client"; import { useEffect, useMemo, useState } from "react"; import type { NodeTypes } from "@xyflow/react"; import { useTranslations } from "next-intl"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@omniroute/ui-kit/tabs"; import Badge from "@/shared/components/Badge"; import Select from "@/shared/components/Select"; import { FlowCanvas } from "@/shared/components/flow/FlowCanvas"; import { FLOW_EDGE_COLORS, flowColorAlpha } from "@/shared/components/flow/edgeStyles"; import { comboRunToFlow, reduceComboEvent, enrichRunWithBreakers, enrichRunWithConnectionCooldown, type ComboRunModel, type ComboEventInput, type ProviderBreakerSnapshot, type ConnectionCooldownSnapshot, } from "./comboFlowModel"; import { aggregateComboEventsToSets } from "./fleetAggregation"; import { StrategyNode } from "./nodes/StrategyNode"; import { ProviderCascadeNode } from "./nodes/ProviderCascadeNode"; import { ResponseNode } from "./nodes/ResponseNode"; import { RequestNode } from "./nodes/RequestNode"; // ── Node type map ───────────────────────────────────────────────────────── // // comboRunToFlow (comboFlowModel.ts) emits exactly these `type` strings: // "request" → entry node (RequestNode) // "strategy" → routing strategy pill (StrategyNode) // "target" → one per combo target (ProviderCascadeNode) // "response" → terminal outcome node (ResponseNode) const NODE_TYPES: NodeTypes = { request: RequestNode as unknown as NodeTypes["request"], strategy: StrategyNode as unknown as NodeTypes["strategy"], target: ProviderCascadeNode as unknown as NodeTypes["target"], response: ResponseNode as unknown as NodeTypes["response"], }; // ── Fleet overview ──────────────────────────────────────────────────────── const FLEET_WINDOW_MS = 60_000; // 1-minute rolling window interface FleetOverviewProps { comboEvents: ComboEventInput[]; } function FleetOverview({ comboEvents }: FleetOverviewProps) { const t = useTranslations("combos"); // `now` must advance, or the 60s rolling window freezes at mount and aging // events are never re-classified out of "active". A low-frequency tick rolls it. const [now, setNow] = useState(() => Date.now()); useEffect(() => { const id = setInterval(() => setNow(Date.now()), 5_000); return () => clearInterval(id); }, []); const sets = useMemo( () => aggregateComboEventsToSets(comboEvents, FLEET_WINDOW_MS, now), [comboEvents, now] ); const total = sets.active.size + sets.error.size + sets.last.size; if (total === 0) { return (
⌁

{t("liveNoProviders")}

{t("liveFleetDataHint")}

); } return (
{sets.active.size > 0 && (
{t("liveActiveCount", { count: sets.active.size })}
{[...sets.active].map((p) => ( {p} ))}
)} {sets.error.size > 0 && (
{t("liveErrorCount", { count: sets.error.size })}
{[...sets.error].map((p) => ( {p} ))}
)} {sets.last.size > 0 && (
{t("liveInactiveCount", { count: sets.last.size })}
{[...sets.last].map((p) => ( {p} ))}
)}
); } // ── Empty state ─────────────────────────────────────────────────────────── function EmptyState() { const t = useTranslations("combos"); return (
⌁

{t("liveNoRun")}

{t("liveDataHint")}

); } // ── Disconnected banner ─────────────────────────────────────────────────── function DisconnectedBanner() { const t = useTranslations("combos"); return (
{t("liveDisconnected")}
); } // ── Props ───────────────────────────────────────────────────────────────── export interface ComboLiveStudioProps { /** * If provided, renders this run directly (static / test mode — no WS dependency). * Mirror of how CompressionCockpit accepts `run?`. */ run?: ComboRunModel | null; /** * All raw combo events used to (a) populate the combo selector and * (b) fold into a run for the selected combo. * In live mode, callers supply events from `useLiveComboStatus().comboEvents`. */ comboEvents?: ComboEventInput[]; /** * Available combo names for the combo selector. * Defaults to the unique comboNames found in `comboEvents`. */ combos?: string[]; /** * Whether the WebSocket connection is live. Defaults to true. * When false, shows the "Live disabled" banner. */ isConnected?: boolean; /** * Per-provider circuit-breaker snapshot (`providerHealth` from * GET /api/monitoring/health). When supplied, the cascade overlays the REAL * breaker state (CB: OPEN · 41s) onto each target — U1b enrichment. Optional; * absent → no breaker badges (graceful). */ providerHealth?: Record | null; /** * Per-provider connection-cooldown snapshot (`connectionHealth` from * GET /api/monitoring/health). When supplied, the cascade overlays the REAL * cooldown state (cooldown 2/3 · 28s) onto each target — U1b Slice 2. Optional; * absent → no cooldown badges (graceful). */ connectionHealth?: Record | null; } // ── Main component ──────────────────────────────────────────────────────── /** * ComboLiveStudio — Tela B: Combo/Routing Studio. * * Renders the combo routing cascade as a ReactFlow canvas: * Request → StrategyNode → [ProviderCascadeNode × N] → ResponseNode * * Features: * - Combo selector: picks which combo's latest run to display. * - Single ⇄ Fleet toggle: Single = cascade flow; Fleet = radial provider * aggregation over all `comboEvents` (last 60 s). * - Graceful empty state when no run is available. * - "Live disabled" banner when `isConnected` is false. * * Testable with a static `run` prop — no WS required. */ export function ComboLiveStudio({ run: runProp, comboEvents = [], combos: combosProp, isConnected = true, providerHealth, connectionHealth, }: ComboLiveStudioProps) { const t = useTranslations("combos"); const [mode, setMode] = useState<"single" | "fleet">("single"); const [selectedCombo, setSelectedCombo] = useState(""); // Derive combo list from events when not supplied explicitly const comboOptions = useMemo(() => { if (combosProp) return combosProp; const seen = new Set(); for (const ev of comboEvents) seen.add(ev.comboName); return [...seen]; }, [combosProp, comboEvents]); // Build the displayed run: static prop wins; otherwise fold live events. // Finally overlay real circuit-breaker state (U1b) — a no-op when no health. const displayRun = useMemo(() => { let baseRun: ComboRunModel | null; if (runProp !== undefined) { baseRun = runProp ?? null; } else if (!selectedCombo) { baseRun = null; } else { const eventsForCombo = comboEvents .filter((e) => e.comboName === selectedCombo) .sort((a, b) => a.timestamp - b.timestamp); baseRun = eventsForCombo.length === 0 ? null : eventsForCombo.reduce( (acc, ev) => reduceComboEvent(acc, ev), null ); } // Compose overlays: breaker state first, then connection cooldown. Both are // pure no-ops when their health map is absent, and they touch disjoint fields. return enrichRunWithConnectionCooldown( enrichRunWithBreakers(baseRun, providerHealth), connectionHealth ); }, [runProp, selectedCombo, comboEvents, providerHealth, connectionHealth]); // Build ReactFlow graph from the current run const { nodes, edges } = useMemo(() => { if (!displayRun) return { nodes: [], edges: [] }; return comboRunToFlow(displayRun); }, [displayRun]); const fitKey = displayRun ? `${displayRun.comboName}-${displayRun.targets.length}-${displayRun.outcome}` : "empty"; // Whether to show the combo selector (hidden when a static run prop is given) const showSelector = runProp === undefined; return ( setMode(next as "single" | "fleet")} className="flex flex-col h-full gap-2" data-testid="combo-live-studio" > {/* ── Toolbar ──────────────────────────────────────────────────────── */}
{showSelector && (