From 09313bcf1bddfc490b91bef328f34c3ce58b6bf2 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:29:16 +0000 Subject: [PATCH] refactor(ui): reorganize Lens dashboard components (#44354) * refactor(ui): reorganize Lens dashboard components * refactor(ui): align Lens forms with react-hook-form Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ui): keep Lens submit errors out of form validity Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(ui): reduce Lens lint budget usage Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(ui): use a query key factory for Lens queries Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Yujong Lee Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../lens/_components/ActivityScope.tsx | 513 ------------ .../lens/_components/AnalysisKey.tsx | 185 ----- .../lens/_components/LensOverview.tsx | 200 ----- .../lens/_components/LensSetup.tsx | 413 --------- .../(dashboard)/lens/_components/LensView.tsx | 785 ------------------ .../lens/_components/WorkerSetup.tsx | 322 ------- .../(dashboard)/lens/_components/lensData.ts | 361 -------- .../src/app/(dashboard)/lens/page.test.tsx | 81 +- .../src/app/(dashboard)/lens/page.tsx | 2 +- .../lens/LensNavigation.integration.test.tsx | 69 ++ .../lens}/LensWorkspace.integration.test.tsx | 10 +- .../lens}/LensWorkspace.tsx | 10 +- .../src/components/lens/api/mutations.ts | 118 +++ .../src/components/lens/api/queries.ts | 239 ++++++ .../src/components/lens/api/useLensApi.ts | 8 + .../lens/demo/createLensDemo.test.ts} | 4 +- .../lens/demo/createLensDemo.ts} | 103 +-- .../lens/demo}/lensDemoLongTrace.ts | 0 .../src/components/lens/demo/scenarios.ts | 98 +++ .../FindingSheet.integration.test.tsx | 51 ++ .../lens/investigations/FindingSheet.tsx} | 32 +- .../lens/investigations/InvestigationList.tsx | 64 ++ .../InvestigationNavigation.tsx | 49 ++ .../investigations/InvestigationProgress.tsx} | 41 +- .../investigations/InvestigationStatus.tsx | 45 + .../InvestigationsView.integration.test.tsx} | 55 +- .../investigations/InvestigationsView.tsx | 270 ++++++ .../investigations/InvestigationsWelcome.tsx} | 28 +- .../lens/investigations/IssueBrief.tsx} | 9 +- .../lens/investigations/ReadinessBanner.tsx | 27 + .../investigations/RequestEvidenceSheet.tsx | 69 ++ .../lens/investigations/TraceSheet.tsx} | 2 +- .../investigations/detail/CriteriaTab.tsx | 61 ++ .../investigations/detail/FindingsTab.tsx | 109 +++ .../lens/investigations/detail/HistoryTab.tsx | 91 ++ .../detail/InvestigationActions.tsx | 67 ++ .../detail/InvestigationDetail.tsx | 181 ++++ .../detail/InvestigationFailure.tsx | 48 ++ .../detail/InvestigationSummary.tsx | 49 ++ .../lens/investigations/detail/RunPicker.tsx | 92 ++ .../lens/investigations/detail/RunsTab.tsx} | 10 +- .../investigations/useInvestigationResults.ts | 103 +++ .../components/lens/model/findings.test.ts | 72 ++ .../src/components/lens/model/findings.ts | 37 + .../src/components/lens/model/format.test.ts | 32 + .../src/components/lens/model/format.ts | 34 + .../lens/model/progress.test.ts} | 93 +-- .../src/components/lens/model/progress.ts | 143 ++++ .../src/components/lens/model/status.test.ts | 102 +++ .../src/components/lens/model/status.ts | 45 + .../src/components/lens/model/types.ts | 34 + ...stigationSetupDialog.integration.test.tsx} | 160 +++- .../lens/setup/InvestigationSetupDialog.tsx | 205 +++++ .../lens/setup/MatchingActivity.tsx | 168 ++++ .../lens/setup/MatchingActivityPreview.tsx | 149 ++++ .../lens/setup/MonitoringDialog.tsx | 103 +++ .../lens/setup}/WatchPicker.tsx | 17 +- .../lens/setup/fields/AnalysisModelField.tsx | 60 ++ .../lens/setup/fields/MetadataFilters.tsx | 82 ++ .../lens/setup/fields/SampleFields.tsx | 52 ++ .../lens/setup/fields/ScopeFields.tsx | 133 +++ .../lens/setup/fields/analysisModels.ts | 22 + .../src/components/lens/setup/filters.test.ts | 13 + .../src/components/lens/setup/filters.ts | 8 + .../lens/setup/investigationSchema.test.ts | 139 ++++ .../lens/setup/investigationSchema.ts | 220 +++++ .../lens/setup/steps/ExpectationsStep.tsx | 71 ++ .../components/lens/setup/steps/RunStep.tsx | 125 +++ .../components/lens/setup/steps/ScopeStep.tsx | 38 + .../src/components/lens/setup/watches.ts | 92 ++ .../setup/worker/AnalysisAccessFields.tsx | 59 ++ .../lens/setup/worker}/AnalysisKeyDetails.tsx | 29 +- .../AnalysisKeyPicker.integration.test.tsx} | 25 +- .../lens/setup/worker/AnalysisKeyPicker.tsx | 93 +++ .../worker/WorkerDialog.integration.test.tsx} | 12 +- .../lens/setup/worker/WorkerDialog.tsx | 196 +++++ .../lens/setup/worker/WorkerForm.tsx | 50 ++ .../lens/setup/worker/WorkerInstall.tsx | 81 ++ .../lens/setup/worker/WorkerList.tsx | 62 ++ .../lens/setup/worker/workerCommand.ts | 23 + .../lens/setup/worker/workerSchema.test.ts | 83 ++ .../lens/setup/worker/workerSchema.ts | 64 ++ .../DurationInput.integration.test.tsx | 2 +- .../shared}/DurationInput.tsx | 16 +- .../src/components/shared/ListRow.tsx | 17 + .../src/components/shared/StatusDot.tsx | 20 + ui/litellm-dashboard/src/hooks/useNow.ts | 12 + 87 files changed, 5166 insertions(+), 3201 deletions(-) delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/lens/_components/ActivityScope.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/lens/_components/AnalysisKey.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensOverview.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensSetup.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/lens/_components/WorkerSetup.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensData.ts create mode 100644 ui/litellm-dashboard/src/components/lens/LensNavigation.integration.test.tsx rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components => components/lens}/LensWorkspace.integration.test.tsx (97%) rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components => components/lens}/LensWorkspace.tsx (93%) create mode 100644 ui/litellm-dashboard/src/components/lens/api/mutations.ts create mode 100644 ui/litellm-dashboard/src/components/lens/api/queries.ts create mode 100644 ui/litellm-dashboard/src/components/lens/api/useLensApi.ts rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components/lensDemoData.test.ts => components/lens/demo/createLensDemo.test.ts} (96%) rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components/lensDemoData.ts => components/lens/demo/createLensDemo.ts} (79%) rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components => components/lens/demo}/lensDemoLongTrace.ts (100%) create mode 100644 ui/litellm-dashboard/src/components/lens/demo/scenarios.ts create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/FindingSheet.integration.test.tsx rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components/LensFinding.tsx => components/lens/investigations/FindingSheet.tsx} (85%) create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/InvestigationList.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/InvestigationNavigation.tsx rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components/LensProgress.tsx => components/lens/investigations/InvestigationProgress.tsx} (81%) create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/InvestigationStatus.tsx rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components/LensView.integration.test.tsx => components/lens/investigations/InvestigationsView.integration.test.tsx} (91%) create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/InvestigationsView.tsx rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components/LensWelcome.tsx => components/lens/investigations/InvestigationsWelcome.tsx} (77%) rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components/LensIssueBrief.tsx => components/lens/investigations/IssueBrief.tsx} (88%) create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/ReadinessBanner.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/RequestEvidenceSheet.tsx rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components/TracePanel.tsx => components/lens/investigations/TraceSheet.tsx} (97%) create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/detail/CriteriaTab.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/detail/FindingsTab.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/detail/HistoryTab.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/detail/InvestigationActions.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/detail/InvestigationDetail.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/detail/InvestigationFailure.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/detail/InvestigationSummary.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/detail/RunPicker.tsx rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components/LensRuns.tsx => components/lens/investigations/detail/RunsTab.tsx} (94%) create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/useInvestigationResults.ts create mode 100644 ui/litellm-dashboard/src/components/lens/model/findings.test.ts create mode 100644 ui/litellm-dashboard/src/components/lens/model/findings.ts create mode 100644 ui/litellm-dashboard/src/components/lens/model/format.test.ts create mode 100644 ui/litellm-dashboard/src/components/lens/model/format.ts rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components/lensData.test.ts => components/lens/model/progress.test.ts} (69%) create mode 100644 ui/litellm-dashboard/src/components/lens/model/progress.ts create mode 100644 ui/litellm-dashboard/src/components/lens/model/status.test.ts create mode 100644 ui/litellm-dashboard/src/components/lens/model/status.ts create mode 100644 ui/litellm-dashboard/src/components/lens/model/types.ts rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components/LensSetup.integration.test.tsx => components/lens/setup/InvestigationSetupDialog.integration.test.tsx} (81%) create mode 100644 ui/litellm-dashboard/src/components/lens/setup/InvestigationSetupDialog.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/setup/MatchingActivity.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/setup/MatchingActivityPreview.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/setup/MonitoringDialog.tsx rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components => components/lens/setup}/WatchPicker.tsx (85%) create mode 100644 ui/litellm-dashboard/src/components/lens/setup/fields/AnalysisModelField.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/setup/fields/MetadataFilters.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/setup/fields/SampleFields.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/setup/fields/ScopeFields.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/setup/fields/analysisModels.ts create mode 100644 ui/litellm-dashboard/src/components/lens/setup/filters.test.ts create mode 100644 ui/litellm-dashboard/src/components/lens/setup/filters.ts create mode 100644 ui/litellm-dashboard/src/components/lens/setup/investigationSchema.test.ts create mode 100644 ui/litellm-dashboard/src/components/lens/setup/investigationSchema.ts create mode 100644 ui/litellm-dashboard/src/components/lens/setup/steps/ExpectationsStep.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/setup/steps/RunStep.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/setup/steps/ScopeStep.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/setup/watches.ts create mode 100644 ui/litellm-dashboard/src/components/lens/setup/worker/AnalysisAccessFields.tsx rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components => components/lens/setup/worker}/AnalysisKeyDetails.tsx (77%) rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components/AnalysisKey.integration.test.tsx => components/lens/setup/worker/AnalysisKeyPicker.integration.test.tsx} (68%) create mode 100644 ui/litellm-dashboard/src/components/lens/setup/worker/AnalysisKeyPicker.tsx rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components/WorkerSetup.integration.test.tsx => components/lens/setup/worker/WorkerDialog.integration.test.tsx} (93%) create mode 100644 ui/litellm-dashboard/src/components/lens/setup/worker/WorkerDialog.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/setup/worker/WorkerForm.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/setup/worker/WorkerInstall.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/setup/worker/WorkerList.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/setup/worker/workerCommand.ts create mode 100644 ui/litellm-dashboard/src/components/lens/setup/worker/workerSchema.test.ts create mode 100644 ui/litellm-dashboard/src/components/lens/setup/worker/workerSchema.ts rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components => components/shared}/DurationInput.integration.test.tsx (95%) rename ui/litellm-dashboard/src/{app/(dashboard)/lens/_components => components/shared}/DurationInput.tsx (84%) create mode 100644 ui/litellm-dashboard/src/components/shared/ListRow.tsx create mode 100644 ui/litellm-dashboard/src/components/shared/StatusDot.tsx create mode 100644 ui/litellm-dashboard/src/hooks/useNow.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/ActivityScope.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/ActivityScope.tsx deleted file mode 100644 index 91a74abe8e1..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/ActivityScope.tsx +++ /dev/null @@ -1,513 +0,0 @@ -"use client"; - -import { useEffect, useId, useState, type ReactNode } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { Plus, X, ChevronRight, RotateCw } from "lucide-react"; -import { apiClient } from "@/components/networking"; -import { Button } from "@/components/ui/button"; -import { - Combobox, - ComboboxInput, - ComboboxContent, - ComboboxList, - ComboboxItem, - ComboboxEmpty, -} from "@/components/ui/combobox"; -import { Input } from "@/components/ui/input"; -import { TracePanel } from "./TracePanel"; -import { type Sample, type Settings, runTime, durationLabel } from "./lensData"; - -import { DurationInput } from "./DurationInput"; - -export type ActivitySelection = Pick & - Partial< - Pick< - Settings, - | "service" - | "agent_name" - | "filters" - | "lookback_hours" - | "sample_percent" - | "sample_size" - | "team_id" - | "execution_ids" - > - >; - -const selectClass = "h-9 w-full rounded-md border border-input bg-background px-3 text-sm"; - -export function RunList({ executions }: { executions: Sample["executions"] }) { - return ( -
- {executions.map((run) => ( -
-

{run.name}

-

- {runTime(run.start_time)} ·{" "} - {run.source === "traces" ? `${run.span_count} ${run.span_count === 1 ? "step" : "steps"}` : "LLM request"} -

-
- ))} -
- ); -} - -export function ActivityScope({ - value, - onChange, - accessToken, - mode = "scope", - onPreviewReady, - manualSelection = false, - nameField, -}: { - value: ActivitySelection; - onChange: (selection: ActivitySelection) => void; - accessToken: string; - mode?: "scope" | "activity"; - onPreviewReady?: (ready: boolean) => void; - manualSelection?: boolean; - nameField?: ReactNode; -}) { - const id = useId(); - const hasFilters = !!value.filters?.length || !!value.team_id; - const [advanced, setAdvanced] = useState(hasFilters || !!value.service || value.source !== "traces"); - const [offset, setOffset] = useState(0); - const [scope, setScope] = useState(value); - const [trace, setTrace] = useState<{ id: string; ref?: string } | null>(null); - const [asOf, setAsOf] = useState(() => new Date().toISOString()); - const serialized = JSON.stringify({ ...value, execution_ids: [] }); - useEffect(() => { - const timer = setTimeout(() => { - setScope(JSON.parse(serialized) as ActivitySelection); - setOffset(0); - setAsOf(new Date().toISOString()); - }, 350); - return () => clearTimeout(timer); - }, [serialized]); - const historyHours = value.lookback_hours ?? 24; - const validWindow = Number.isInteger(historyHours) && historyHours >= 1 && historyHours <= 8760; - const percent = scope.sample_percent ?? 100; - const cap = scope.sample_size; - const validCap = cap == null || (Number.isInteger(cap) && cap > 0); - const validSampling = percent > 0 && percent <= 100 && validCap; - const validFilters = (scope.filters ?? []).every((f) => f.key.trim() && f.value.trim()); - const valid = validWindow && validSampling && validFilters; - const load = (selection: ActivitySelection, pageOffset = 0) => { - const { lookback_hours, ...selectionSettings } = selection; - return apiClient.post("/lens/preview/sample", { - accessToken, - body: { - offset: pageOffset, - as_of: asOf, - settings: { - ...selectionSettings, - execution_ids: [], - name: "Preview", - model: "preview", - - checks: [{ id: "preview", instruction: "Preview recorded activity" }], - }, - lookback_hours: lookback_hours ?? 24, - }, - }); - }; - const discoveryScope: ActivitySelection = { - source: value.source, - service: "", - filters: [], - lookback_hours: value.lookback_hours, - }; - const discoveryOptions = { - queryKey: ["lens-activity-options", value.source, value.lookback_hours, asOf, accessToken], - queryFn: () => load(discoveryScope), - staleTime: 60000, - enabled: validWindow, - }; - const discovery = useQuery(discoveryOptions); - const agentOptions = { - queryKey: ["lens-agents", accessToken, asOf], - queryFn: () => apiClient.get("/lens/agents", { accessToken }), - enabled: value.source !== "requests", - staleTime: 60000, - }; - const agents = useQuery(agentOptions); - const previewOptions = { - queryKey: ["lens-activity-preview", scope, offset, asOf, accessToken], - queryFn: () => load(scope, offset), - enabled: valid, - staleTime: 30000, - }; - const preview = useQuery(previewOptions); - const empty = preview.data?.eligible === 0; - useEffect(() => { - if (!empty || !valid) return; - const timer = window.setTimeout(() => setAsOf(new Date().toISOString()), 15000); - return () => window.clearTimeout(timer); - }, [empty, valid, asOf]); - const refreshPreview = () => { - setOffset(0); - setAsOf(new Date().toISOString()); - }; - const runs = discovery.data?.executions ?? []; - const services = [...new Set(runs.map((r) => r.service).filter(Boolean))].sort(); - const selectedName = value.source === "requests" ? value.service : value.agent_name; - const names = value.source === "requests" ? services : agents.data ?? []; - const selectName = (name: string) => - onChange({ ...value, [value.source === "requests" ? "service" : "agent_name"]: name, execution_ids: [] }); - const attributes = runs.flatMap((r) => r.metadata ?? []); - const keys = [...new Set(attributes.map((a) => a.key).filter((key) => !key.startsWith("litellm.")))].sort(); - const pending = serialized !== JSON.stringify(scope) || preview.isFetching; - const ready = !pending && valid; - const hasSelection = !manualSelection || !!value.execution_ids?.length; - const hasMatches = !preview.error && (preview.data?.selected ?? 0) > 0; - const canReview = ready && hasMatches && hasSelection; - useEffect(() => { - onPreviewReady?.(canReview); - }, [canReview, onPreviewReady]); - const filters = value.filters ?? []; - const edit = (index: number, field: "key" | "value", text: string) => - onChange({ ...value, filters: filters.map((f, i) => (i === index ? { ...f, [field]: text } : f)) }); - - const changeSource = (source: Settings["source"]) => { - const selection = { ...value, source, service: "", agent_name: "", filters: [], execution_ids: [] }; - onChange(selection); - }; - const windowLabel = validWindow - ? `Last ${durationLabel(value.lookback_hours ?? 24, "hours")}` - : "Choose a valid history window"; - const previewTitle = () => { - if (pending) return "Finding matching activity…"; - if (!validWindow) return "Choose a history window between 1 hour and 365 days"; - if (!valid) return "Complete your condition to preview matches"; - if (!preview.data) return "Preview unavailable"; - const noun = value.source === "requests" ? "request" : "run"; - return `${preview.data.eligible} matching ${noun}${preview.data.eligible === 1 ? "" : "s"}`; - }; - return ( -
-
- {mode === "scope" ? ( - <> - {nameField} - - {value.source !== "requests" && agents.isError && ( -

- Could not load agents.{" "} - -

- )} -
setAdvanced(event.currentTarget.open)} className="group"> - - Advanced filters{filters.length ? ` (${filters.length})` : ""} - -
- {value.source !== "requests" && ( - - )} - -

- Match any recorded metadata, such as a user ID, environment, or tag. All conditions must match. -

- {filters.map((f, index) => ( -
-
- edit(index, "key", e.target.value)} - /> - -
- edit(index, "value", e.target.value)} - /> - - {[...new Set(attributes.filter((a) => a.key === f.key).map((a) => a.value))].sort().map((v) => ( - -
- ))} - - {keys.map((key) => ( - - - -
-
- - ) : ( - <> - onChange({ ...value, lookback_hours })} - /> - - - )} - {manualSelection && !!value.execution_ids?.length && ( - - )} -
- {mode === "activity" && ( - - onChange({ - ...value, - execution_ids: checked - ? [...(value.execution_ids ?? []), runId] - : (value.execution_ids ?? []).filter((id) => id !== runId), - }) - } - manualSelection={manualSelection} - selectedIds={value.execution_ids ?? []} - selectedCount={ - manualSelection - ? Math.min( - Math.ceil(((value.execution_ids?.length ?? 0) * (value.sample_percent ?? 100)) / 100), - value.sample_size ?? Infinity, - ) - : preview.data?.selected ?? 0 - } - title={previewTitle()} - windowLabel={windowLabel} - ready={ready} - error={preview.error} - data={preview.data} - onRetry={refreshPreview} - onOpen={(run) => setTrace({ id: run.trace_id, ref: run.trace_ref })} - /> - )} - {trace && ( - setTrace(null)} - /> - )} -
- ); -} - -function MatchingActivity({ - offset, - onPage, - onSelect, - selectedIds, - manualSelection, - selectedCount, - title, - windowLabel, - ready, - error, - data, - onOpen, - onRetry, -}: { - offset: number; - onPage: (offset: number) => void; - onSelect: (id: string, checked: boolean) => void; - selectedIds: string[]; - manualSelection: boolean; - selectedCount: number; - title: string; - windowLabel: string; - ready: boolean; - error: Error | null; - data: Sample | undefined; - onRetry: () => void; - onOpen: (run: Sample["executions"][number]) => void; -}) { - const paginated = data?.next_offset != null || offset > 0; - const showSelection = selectedCount !== data?.eligible || paginated; - const selectionData = ready && showSelection ? data : undefined; - return ( -
-
-
-

- {title} -

- -
-

{windowLabel} · No analysis cost

-
-
- {ready && error && ( -

- {error.message}{" "} - -

- )} - {ready && data?.eligible === 0 && ( -

- No matches. Try removing a condition or check that your agent records this metadata. Recent trace updates - need two minutes to settle. -

- )} - {ready && - data?.executions.map((run) => ( -
- {manualSelection && ( - onSelect(run.id, e.target.checked)} - /> - )} -
- -
- {run.source === "traces" && ( - - )} -
- ))} -
- {selectionData && ( -
-

- {selectedCount} selected for analysis - {paginated && ( - <> - {" "} - · Showing {offset + (selectionData.executions.length ? 1 : 0)}– - {offset + selectionData.executions.length} of {selectionData.eligible} - - )} -

- {paginated && ( -
- - -
- )} -
- )} -
- ); -} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/AnalysisKey.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/AnalysisKey.tsx deleted file mode 100644 index 4033b9634b2..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/AnalysisKey.tsx +++ /dev/null @@ -1,185 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; -import { z } from "zod"; -import { apiClient } from "@/components/networking"; -import { SearchSelect } from "@/components/shared/SearchSelect"; -import { Input } from "@/components/ui/input"; -import { AnalysisKeyDetails } from "./AnalysisKeyDetails"; -import { - Combobox, - ComboboxContent, - ComboboxEmpty, - ComboboxInput, - ComboboxItem, - ComboboxList, -} from "@/components/ui/combobox"; - -const keySchema = z.object({ token: z.string(), key_alias: z.string().nullable().optional() }); -const pageSchema = z.object({ keys: z.array(keySchema), total_pages: z.number() }); -type Key = z.infer; - -export function AnalysisKey({ - accessToken, - value, - onChange, -}: { - accessToken: string; - value: string | null; - onChange: (key: string | null) => void; -}) { - const [query, setQuery] = useState(""); - const [selected, setSelected] = useState(value ? { token: value } : null); - - const queryOptions = { - queryKey: ["lens-analysis-keys", accessToken, query], - initialPageParam: 1, - queryFn: async ({ pageParam, signal }: { pageParam: number; signal: AbortSignal }) => - pageSchema.parse( - await apiClient.get("/key/list", { - accessToken, - signal, - query: { - page: String(pageParam), - size: "25", - return_full_object: "true", - key_alias: query || undefined, - substring_matching: "true", - include_team_keys: "true", - include_created_by_keys: "true", - status: "active", - }, - }), - ), - getNextPageParam: (lastPage: z.infer, pages: z.infer[]) => - pages.length < lastPage.total_pages ? pages.length + 1 : undefined, - }; - const keyPages = useInfiniteQuery(queryOptions); - const keys = keyPages.data?.pages.flatMap((page) => page.keys) ?? []; - const choice = keys.find((key) => key.token === value) ?? selected; - const loading = keyPages.isFetching; - - const changeKey = (key: Key | null, details: { cancel: () => void }) => { - if (key?.token === "load-more") { - details.cancel(); - if (!loading) void keyPages.fetchNextPage(); - return; - } - setSelected(key); - onChange(key?.token ?? null); - }; - const choices = choice && !keys.some((key) => key.token === choice.token) ? [choice, ...keys] : keys; - const items = keyPages.hasNextPage - ? [...choices, { token: "load-more", key_alias: loading ? "Loading…" : "Load more keys" }] - : choices; - return ( -
-

Charge analysis to

-
-
- key.key_alias || `${key.token.slice(0, 8)}…`} - isItemEqualToValue={(a: Key, b: Key) => a.token === b.token} - onInputValueChange={(text, details) => { - if (details.reason === "input-change" || details.reason === "input-clear") { - setQuery(text); - } - }} - onValueChange={changeKey} - > - - - {loading ? "Loading keys…" : "No matching keys"} - - {(key: Key) => ( - - {key.key_alias || `${key.token.slice(0, 8)}…`} - - )} - - - -
-
- {choice && } - {keyPages.error && ( -

- {keyPages.error.message} -

- )} -
- ); -} - -export type AnalysisAccess = { model: string | null; budget: string }; - -export function AnalysisAccessFields({ - accessToken, - value, - onChange, -}: { - accessToken: string; - value: AnalysisAccess; - onChange: (value: AnalysisAccess) => void; -}) { - const models = useQuery({ - queryKey: ["lens-models", accessToken], - queryFn: () => apiClient.get<{ data: { id: string }[] }>("/models", { accessToken }), - }); - return ( -
-
- - ({ label: id, value: id }))} - value={value.model} - onValueChange={(model) => onChange({ ...value, model })} - placeholder={models.isLoading ? "Loading models…" : "Select a model"} - /> -
-
- - onChange({ ...value, budget: e.target.value })} - /> -

Shared across all investigations.

-
- {models.error && ( -

- {models.error.message} -

- )} -
- ); -} - -export async function createAnalysisKey(accessToken: string, access: AnalysisAccess): Promise { - if (!access.model || !Number.isFinite(Number(access.budget)) || Number(access.budget) <= 0) - throw new Error("Choose a model and a monthly limit greater than zero"); - const result = await apiClient.post("/key/generate", { - accessToken, - body: { - key_alias: "Lens analysis", - models: [access.model], - max_budget: Number(access.budget), - budget_duration: "1mo", - metadata: { purpose: "lens" }, - }, - }); - if (!result.token_id) throw new Error("The proxy did not return the new key's ID"); - return result.token_id; -} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensOverview.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensOverview.tsx deleted file mode 100644 index c6cd8485cbe..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensOverview.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { useState } from "react"; -import { ChevronRight, Search } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { NextCheck } from "./LensProgress"; -import { DurationInput } from "./DurationInput"; -import { lensStatus, runTime, scopeLabel, type Lens, type Settings, type Job } from "./lensData"; - -export function InvestigationList({ - lenses, - connected, - onSelect, -}: { - lenses: Lens[]; - connected: boolean; - onSelect: (id: string) => void; -}) { - const [search, setSearch] = useState(""); - const shown = lenses.filter((lens) => - `${lens.settings.name} ${scopeLabel(lens.settings)}`.toLowerCase().includes(search.toLowerCase()), - ); - return ( -
-
- - setSearch(e.target.value)} - /> -
-
- {shown.map((lens) => ( - - ))} - {!shown.length &&

No investigations match your search.

} -
-
- ); -} - -export function MonitoringSetup({ - settings, - ready, - onSave, - onClose, -}: { - settings: Settings; - ready: boolean; - onSave: (settings: Settings) => Promise; - onClose: () => void; -}) { - const [interval, setInterval] = useState(settings.interval_minutes ?? 30); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(""); - const save = async () => { - setBusy(true); - try { - await onSave({ ...settings, enabled: true, interval_minutes: interval }); - onClose(); - } catch (cause) { - setError(cause instanceof Error ? cause.message : "Could not enable monitoring"); - } finally { - setBusy(false); - } - }; - const validInterval = Number.isInteger(interval) && interval >= 1 && interval <= 10080; - return ( - { - if (!open && !busy) onClose(); - }} - > - - - Keep monitoring - - Repeat this investigation with the saved scope, sample, model, and budget. - - - -

- Each investigation looks back over the saved time range. The interval starts after the previous run finishes. -

- {!ready && ( -

- Reconnect the worker before enabling monitoring. -

- )} - {error && ( -

- {error} -

- )} - - - - -
-
- ); -} - -export function InvestigationSummary({ lens, connected }: { lens: Lens; connected: boolean }) { - const lastCompleted = lens.jobs.find((job) => job.status === "completed"); - const lastSuccess = lastCompleted?.finished_at ?? lens.last_scan_at; - const spent = lens.budget_month === new Date().toISOString().slice(0, 7) ? lens.spent ?? 0 : 0; - return ( -
- - Latest run:{" "} - - {lensStatus(lens, connected)} - - - - Last success: {lastSuccess ? runTime(lastSuccess) : "Not yet"} - - - This month:{" "} - - ${spent.toFixed(3)} / ${lens.settings.monthly_budget ?? 100} - - - {lens.settings.enabled && ( - - Monitoring every {lens.settings.interval_minutes} minutes - - - )} -
- ); -} - -export function InvestigationFailure({ job, connected }: { job: Job; connected: boolean }) { - return ( -
-

This investigation did not finish

-

{job.error}

-
- Troubleshooting details -
-
-
Run:
-
{job.id}
-
-
-
Model:
-
{job.settings.model}
-
-
-
Worker:
-
{connected ? "Connected now" : "Not connected"}
-
-
-
Started:
-
{runTime(job.created_at)}
-
-
-

- Use the run ID to find the error in proxy and worker logs. Check the worker key's model permissions and - budget before retrying. -

-
-
- ); -} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensSetup.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensSetup.tsx deleted file mode 100644 index 88000eb5192..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensSetup.tsx +++ /dev/null @@ -1,413 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { X } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, -} from "@/components/ui/dialog"; -import { SearchSelect } from "@/components/shared/SearchSelect"; -import { DurationInput } from "./DurationInput"; -import { ActivityScope, type ActivitySelection } from "./ActivityScope"; -import { WatchPicker } from "./WatchPicker"; -import { - analysisModelOptions, - initialWatches, - isWatch, - normalizeFilters, - watchChecks, - type AnalysisModelInfo, - type Settings, -} from "./lensData"; - -function validateSample(selection: ActivitySelection) { - const hours = selection.lookback_hours ?? 24; - if (!Number.isInteger(hours) || hours < 1 || hours > 8760) - throw new Error("Choose a time range between 1 hour and 365 days"); - const percent = selection.sample_percent ?? 100; - if (!Number.isFinite(percent) || percent <= 0 || percent > 100) - throw new Error("Choose a sampling percentage greater than 0 and up to 100"); - if (selection.sample_size != null && (!Number.isInteger(selection.sample_size) || selection.sample_size < 1)) - throw new Error("Choose a positive maximum or leave it blank for no limit"); -} - -function newCheck(instruction = ""): Settings["checks"][number] { - return { id: crypto.randomUUID(), instruction, enabled: true }; -} - -export function LensSetup({ - initial, - mode = initial ? "edit" : "new", - models, - modelDetails = [], - modelsLoading = false, - modelsError, - defaultModel, - defaultSource = "traces", - accessToken, - ready = true, - onClose, - onSave, -}: { - initial?: Settings; - mode?: "new" | "edit" | "duplicate"; - models: string[]; - modelDetails?: AnalysisModelInfo[]; - modelsLoading?: boolean; - modelsError?: string; - defaultModel?: string; - defaultSource?: Settings["source"]; - accessToken: string; - ready?: boolean; - onClose: () => void; - onSave: (settings: Settings) => Promise; -}) { - const [step, setStep] = useState(0); - const [previewReady, setPreviewReady] = useState(false); - const [manualSelection, setManualSelection] = useState(!!initial?.execution_ids?.length); - const [name, setName] = useState(initial?.name ?? ""); - const initialSelection: Required = { - source: initial?.source ?? defaultSource, - service: initial?.service ?? "", - agent_name: initial?.agent_name ?? "", - filters: initial?.filters ?? [], - lookback_hours: initial?.lookback_hours ?? 24, - sample_size: initial?.sample_size ?? null, - sample_percent: initial?.sample_percent ?? 100, - team_id: initial?.team_id ?? "", - execution_ids: initial?.execution_ids ?? [], - }; - const [selection, setSelection] = useState(initialSelection); - const [context, setContext] = useState(initial?.context ?? ""); - const [watching, setWatching] = useState>(() => initialWatches(initial?.checks)); - const [questions, setQuestions] = useState(() => (initial?.checks ?? []).filter((check) => !isWatch(check))); - const [selectedModel, setModel] = useState(initial?.model ?? null); - const model = selectedModel ?? defaultModel ?? ""; - const [budget, setBudget] = useState(initial?.monthly_budget ?? 100); - const [repeat, setRepeat] = useState(mode === "edit" && !!initial?.enabled); - const [interval, setInterval] = useState(initial?.interval_minutes ?? 30); - const [error, setError] = useState(""); - const [busy, setBusy] = useState(false); - const filledChecks = questions.filter((check) => check.instruction.trim()); - const suggestedName = filledChecks[0]?.instruction.trim() || context.trim().split("\n")[0] || "Investigation"; - const title = name.trim() || suggestedName.slice(0, 100); - const changeSelection = (next: ActivitySelection) => { - const pool = (s: ActivitySelection) => - JSON.stringify([s.source, s.service, s.agent_name, s.filters, s.lookback_hours, s.team_id]); - setSelection({ - ...selection, - ...next, - execution_ids: pool(next) === pool(selection) ? next.execution_ids ?? [] : [], - }); - }; - const validate = () => { - normalizeFilters(selection.filters ?? []); - if (step >= 2 && manualSelection && !selection.execution_ids?.length) - throw new Error("Choose at least one run or turn off individual selection"); - validateSample(selection); - const nothingToCheck = !context.trim() && !filledChecks.length && !watching.size; - if (step >= 1 && nothingToCheck) throw new Error("Describe the expected behavior or pick something to watch for"); - if (filledChecks.some((check) => check.instruction.trim().length < 3)) - throw new Error("Use at least three characters for each check"); - }; - const next = () => { - try { - validate(); - setError(""); - setStep(step + 1); - } catch (cause) { - setError(cause instanceof Error ? cause.message : "Check your settings"); - } - }; - const save = async () => { - setBusy(true); - setError(""); - try { - validate(); - const settings: Settings = { - ...initial, - ...selection, - name: title, - context: context.trim(), - model, - monthly_budget: budget, - enabled: repeat, - interval_minutes: interval, - concurrency: initial?.concurrency ?? 8, - filters: normalizeFilters(selection.filters ?? []), - checks: [ - ...watchChecks(watching), - ...filledChecks.map((check) => ({ ...check, instruction: check.instruction.trim() })), - ], - }; - await onSave(settings); - } catch (cause) { - setError(cause instanceof Error ? cause.message : "Could not save investigation"); - } finally { - setBusy(false); - } - }; - const unsupported = modelDetails.some((m) => m.model_group === model && m.mode && m.mode !== "chat"); - const budgetValid = Number.isFinite(budget) && budget > 0 && budget <= 100000; - const canRun = ready || mode === "edit"; - const modelsReady = !modelsLoading && !modelsError; - const unavailable = !!model && modelsReady && !models.includes(model); - const supported = !unsupported && !unavailable; - const preservingSavedModel = mode === "edit" && model === initial?.model; - const modelReady = modelsReady || preservingSavedModel; - const modelValid = !!model && supported && modelReady; - const intervalRangeValid = interval >= 1 && interval <= 10080; - const intervalValid = !repeat || (Number.isInteger(interval) && intervalRangeValid); - const configurationValid = modelValid && budgetValid && intervalValid; - const runReady = canRun && (mode === "edit" || previewReady); - const selectionValid = !manualSelection || !!selection.execution_ids?.length; - const validSettings = configurationValid && selectionValid; - const canSave = !busy && runReady && validSettings; - const createLabel = repeat ? "Run and monitor" : "Run investigation"; - const saveLabel = mode === "edit" ? "Save changes" : createLabel; - const headings = [ - "Which activity should we investigate?", - "What should Lens look for?", - mode === "edit" ? "Review changes" : "Ready to investigate", - ]; - return ( - { - if (!open && !busy) onClose(); - }} - > - - - {headings[step]} - - { - [ - "Start with an agent, or use filters to investigate any recorded activity.", - "Describe the expected behavior, the questions you have, or both.", - "Review the selected activity, then start your investigation.", - ][step] - } - - - -
- {(step === 0 || step === 2) && ( - - Investigation name - setName(e.target.value)} - placeholder="e.g. Support quality" - maxLength={100} - /> - - ) : undefined - } - mode={step === 0 ? "scope" : "activity"} - onPreviewReady={setPreviewReady} - manualSelection={manualSelection} - /> - )} - {step === 1 && ( - <> -