diff --git a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationProgress.tsx b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationProgress.tsx index 5d14a10f238..c169cfa0a3a 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationProgress.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationProgress.tsx @@ -18,18 +18,30 @@ import { durationText } from "../model/format"; import { nextCheckStatus } from "../model/status"; import { type Lens, type Job } from "../model/types"; import { cn } from "@/lib/cva.config"; +import { QueueReasonText } from "./QueueReasonText"; +import { useQueueReason, type QueueContext } from "./useQueueReason"; const steps = ["review runs", "find patterns", "check evidence"]; const markers = { done: "✓", active: "▸", todo: "·" }; const blocks = 32; +const QUEUED_TITLE = { busy: "Waiting in line", no_worker: "Waiting for a worker", starting: "Picking up" } as const; function stageState(index: number, current: number): keyof typeof markers { if (index < current) return "done"; return index === current ? "active" : "todo"; } -export function InvestigationProgress({ job, onCancel }: { job: Job; onCancel?: () => void }) { +export function InvestigationProgress({ + job, + queue, + onCancel, +}: { + job: Job; + queue?: QueueContext; + onCancel?: () => void; +}) { const now = useNow(1000); + const reason = useQueueReason(job, queue); const progress = analysisProgress(job); const fraction = analysisFraction(progress); const [samples, setSamples] = useState([]); @@ -53,7 +65,7 @@ export function InvestigationProgress({ job, onCancel }: { job: Job; onCancel?:
- {progress.title} + {reason ? QUEUED_TITLE[reason.kind] : progress.title} {!queued && · {progress.detail}} {onCancel && ( @@ -117,7 +129,9 @@ export function InvestigationProgress({ job, onCancel }: { job: Job; onCancel?:
{queued ? ( - {progress.detail} + + {reason ? : progress.detail} + ) : ( stats.map(([key, value]) => ( diff --git a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationsView.tsx b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationsView.tsx index 3a5894962ec..187e96df238 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationsView.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationsView.tsx @@ -281,6 +281,12 @@ export function InvestigationsView({ connected={connected} results={results} agents={Array.isArray(agents.data) ? agents.data : []} + queue={{ + lenses, + workers: query.data?.workers ?? [], + onConnect: () => setWorkerSetup(true), + onOpenLens: selectLens, + }} /> )} {editing && ( diff --git a/ui/litellm-dashboard/src/components/lens/investigations/QueueReasonText.tsx b/ui/litellm-dashboard/src/components/lens/investigations/QueueReasonText.tsx new file mode 100644 index 00000000000..cec98b30fbc --- /dev/null +++ b/ui/litellm-dashboard/src/components/lens/investigations/QueueReasonText.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { queueReasonText, workerTaskText, type QueueReason } from "../model/status"; +import type { QueueContext } from "./useQueueReason"; + +export function QueueReasonText({ reason, onConnect }: { reason: QueueReason; onConnect?: () => void }) { + if (reason.kind !== "no_worker" || !onConnect) return <>{queueReasonText(reason)}; + return ( + <> + No worker connected. Start one from{" "} + + . + + ); +} + +export function WorkerTasks({ reason, queue }: { reason: QueueReason; queue?: QueueContext }) { + if (reason.kind === "no_worker" || !reason.tasks.length) return null; + return ( +
    + {reason.tasks.map((task) => ( +
  1. + +
  2. + ))} +
+ ); +} diff --git a/ui/litellm-dashboard/src/components/lens/investigations/detail/InvestigationDetail.tsx b/ui/litellm-dashboard/src/components/lens/investigations/detail/InvestigationDetail.tsx index 1a9d2297593..431bc9eefd3 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/detail/InvestigationDetail.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/detail/InvestigationDetail.tsx @@ -7,6 +7,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { RunsTab } from "./RunsTab"; import { InvestigationProgress } from "../InvestigationProgress"; import { LiveRun } from "../live/LiveRun"; +import { type QueueContext } from "../useQueueReason"; import { liveJob } from "../../model/live"; import { RunNowDialog } from "./RunNowDialog"; import { useState } from "react"; @@ -32,6 +33,7 @@ export function InvestigationDetail({ connected, results, agents = [], + queue, }: { lens: Lens; readOnly: boolean; @@ -43,6 +45,7 @@ export function InvestigationDetail({ connected: boolean; results: ReturnType; agents?: readonly string[]; + queue?: QueueContext; }) { const [runNow, setRunNow] = useState(false); const { @@ -102,10 +105,11 @@ export function InvestigationDetail({ void update((api) => api.cancelRun(lens.id))} /> )} - {live && } + {live && } {runNow && ( (null); @@ -42,7 +56,15 @@ export function LiveRun({ job, reviews, name }: { job: Job; reviews: readonly Re {stripOpen ? ( + + + + ) + } playback={playback} reviewed={reviewed} selected={job.coverage.selected} diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveStrip.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveStrip.tsx index bdd04b36621..3a7db401f5a 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveStrip.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveStrip.tsx @@ -1,6 +1,7 @@ "use client"; import { PanelRight, X } from "lucide-react"; +import type { ReactNode } from "react"; import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; import { agoLabel } from "@/components/view_logs/TraceView/lensField"; @@ -63,11 +64,13 @@ export function LiveStrip({ selected, issues, cost, + waiting, onOpen, onClose, }: { model: string; state: StripState; + waiting?: ReactNode; playback: Pick; reviewed: number; selected: number; @@ -96,9 +99,9 @@ export function LiveStrip({

)} {state.kind === "waiting" && ( -

- {state.message} -

+
+ {waiting ?? state.message} +
)} {recent.length > 0 && state.kind !== "waiting" && (
    diff --git a/ui/litellm-dashboard/src/components/lens/investigations/useQueueReason.ts b/ui/litellm-dashboard/src/components/lens/investigations/useQueueReason.ts new file mode 100644 index 00000000000..7337892d671 --- /dev/null +++ b/ui/litellm-dashboard/src/components/lens/investigations/useQueueReason.ts @@ -0,0 +1,19 @@ +"use client"; + +import { useNow } from "@/hooks/useNow"; + +import { queueReason, type QueueReason } from "../model/status"; +import type { Job, Lens, LensList } from "../model/types"; + +export interface QueueContext { + lenses: readonly Lens[]; + workers: readonly LensList["workers"][number][]; + onConnect: () => void; + onOpenLens: (id: string) => void; +} + +export function useQueueReason(job: Job, queue: QueueContext | undefined): QueueReason | null { + const now = useNow(1000); + if (job.status !== "queued" || !queue) return null; + return queueReason(job, queue.lenses, queue.workers, now); +}