import { useEffect, useMemo } from "react"; import { LoaderCircle } from "lucide-react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { apiErrorMessage } from "../../api/client"; import { Button } from "../../components/ui/button"; import { getSensitivityAnalysisRun, listSensitivityAnalysisEvents, listSensitivityAnalysisRuns, type SensitivityAnalysisRun, } from "../../api/catalog-databases"; import { useTailFollowing } from "../activityScroll"; import { FleetLedgerDrawer } from "./FleetLedgerShell"; interface Props { open: boolean; databaseId?: string | null; run: SensitivityAnalysisRun | null; awaitingRun?: boolean; onClose: () => void; onRunUpdate: (run: SensitivityAnalysisRun) => void; } const HISTORY_QUERY_KEY = ["sensitive-data-suggestion-runs", 50] as const; const SAFE_FINAL_ERROR = "The run ended before all columns were classified. Review the event log for safe details."; function statusLabel(run: SensitivityAnalysisRun): string { const label = run.status.replaceAll("_", " "); return `${label[0].toUpperCase()}${label.slice(1)}`; } function isTerminal(run?: SensitivityAnalysisRun): boolean { return Boolean(run && ["completed", "failed", "interrupted"].includes(run.status)); } function outcomeClass(run: SensitivityAnalysisRun): string { if (run.status === "completed") return "thot-sensitive-run--success"; if (["failed", "interrupted"].includes(run.status)) return "thot-sensitive-run--failure"; return ""; } function safeFinalError(summary: string | null): string | null { const value = summary?.trim(); if (!value) return null; if ( value.length > 500 || /[\r\n]/.test(value) || /traceback|stack trace|(?:^|\s)at\s+\S+[:(]|prompt|provider\s+payload|source\s+sample|bearer\s+\S+|api[_ -]?key|sk-[a-z0-9_-]+/i.test(value) ) return SAFE_FINAL_ERROR; return value; } function timestamp(value: string | null): string { return value ? new Date(value).toLocaleString() : "Not available"; } export function SensitivityAnalysisHistoryDrawer({ open, databaseId = null, run: initialRun, awaitingRun = false, onClose, onRunUpdate, }: Props) { const queryClient = useQueryClient(); const runId = initialRun?.id ?? null; const runQuery = useQuery({ queryKey: ["sensitive-data-suggestion-run", runId], queryFn: () => getSensitivityAnalysisRun(runId!), enabled: open && Boolean(runId), initialData: initialRun ?? undefined, retry: false, refetchInterval: (query) => isTerminal(query.state.data) ? false : 750, }); const run = runQuery.data; const eventQuery = useQuery({ queryKey: ["sensitive-data-suggestion-events", runId], queryFn: () => listSensitivityAnalysisEvents(runId!), enabled: open && Boolean(runId), retry: false, refetchInterval: run && isTerminal(run) ? false : 750, }); const historyQuery = useQuery({ queryKey: HISTORY_QUERY_KEY, queryFn: () => listSensitivityAnalysisRuns(50), enabled: open, retry: false, refetchInterval: (query) => awaitingRun ? 500 : query.state.data?.some((item) => !isTerminal(item)) ? 3_000 : false, }); const visibleHistory = useMemo( () => (historyQuery.data ?? []).filter((item) => !databaseId || item.databaseId === databaseId), [databaseId, historyQuery.data], ); const events = eventQuery.data ?? []; const latestEventSequence = events.at(-1)?.sequence ?? 0; const { followingTail: followingEventTail, handleScroll: handleEventLogScroll, jumpToTail: jumpToLatestEvent, viewportRef: eventLogRef, } = useTailFollowing({ active: open && Boolean(run), resetKey: runId, tail: latestEventSequence, }); useEffect(() => { if (!open) return; const candidate = awaitingRun ? visibleHistory.find((item) => !isTerminal(item)) : visibleHistory[0]; if (!candidate || candidate.id === initialRun?.id) return; onRunUpdate(candidate); }, [awaitingRun, initialRun?.id, onRunUpdate, open, visibleHistory]); useEffect(() => { if (!run) return; onRunUpdate(run); queryClient.setQueryData(HISTORY_QUERY_KEY, (current) => { if (!current) return [run]; return current.some((item) => item.id === run.id) ? current.map((item) => item.id === run.id ? run : item) : [run, ...current]; }); }, [onRunUpdate, queryClient, run]); if (!open) return null; if (!run && awaitingRun && !historyQuery.isError) { return ( ); } if (!run) { return ( {historyQuery.isError ? (
{apiErrorMessage(historyQuery.error)}
) : historyQuery.isLoading || awaitingRun || (historyQuery.data?.length ?? 0) > 0 ? (

{awaitingRun ? "Starting sensitivity analysis…" : "Loading analysis history…"}

) : (

No sensitivity analysis runs yet.

Analyze sensitive fields to create the first history entry.

)}
); } const finalError = safeFinalError(run.errorSummary); const engineDisplay = run.engine === "local" ? `Local rules (${run.policyVersion ?? "unknown policy"})` : `Legacy LLM (${run.modelId ?? "unknown model"})`; const progressCounters = [ ["total", "Total columns"], ["suggestedSensitive", "Sensitive"], ["suggestedNonSensitive", "Not sensitive"], ["unknown", "Unprocessed"], ] as const; return ( {!isTerminal(run) ? ); }