diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx index ba4867aa0ec..efcec3987c9 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx @@ -1,170 +1,78 @@ "use client"; -import { ChevronDown, Loader2 } from "lucide-react"; -import { useMemo, useRef } from "react"; +import { useState } from "react"; -import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; -import { Button } from "@/components/ui/button"; -import { useNow } from "@/hooks/useNow"; -import { cn } from "@/lib/cva.config"; - -import { durationText, money } from "../../model/format"; import { modelsUsed } from "../../model/inbox"; import { analysisModel, conclusions, - isCompact, - liveStats, - outcome, - providerOf, + focusedReview, + issueCount, reviewKey, shownCount, - tickerLine, - verdictLine, + stripState, } from "../../model/live"; import type { Job, Review } from "../../model/types"; -import { ConclusionsPanel, type Arrival } from "./ConclusionsPanel"; -import { ReadingPanel } from "./ReadingPanel"; -import { ReviewQueue } from "./ReviewQueue"; +import { LiveDrawer } from "./LiveDrawer"; +import { LiveStrip } from "./LiveStrip"; +import { useDrawerOpen, useStripOpen } from "./useLivePanels"; import { useReviewPlayback } from "./useReviewPlayback"; -import { useShowWork } from "./useShowWork"; -const COLUMN_HEAD = - "sticky top-0 z-sticky flex h-8 shrink-0 items-center justify-between gap-2 border-b border-border bg-muted/40 px-3 text-[10px] tracking-[0.08em] text-muted-foreground uppercase backdrop-blur"; -const COLUMN = "flex max-h-[320px] min-h-0 flex-col overflow-y-auto"; - -function useArrival(review: Review | null, verdict: boolean, enabled: boolean): Arrival | null { - const issue = review && verdict && enabled ? review.verdicts.find((v) => v.kind === "issue") : undefined; - const key = review && issue ? reviewKey(review) : ""; - const checkId = issue?.check_id ?? ""; - const text = review ? verdictLine(review) : ""; - return useMemo(() => (key ? { key, checkId, text } : null), [key, checkId, text]); -} - -function Model({ model }: { model: string }) { - if (!model) return null; - const provider = providerOf(model); - return ( - - {provider && } - {model} - - ); -} - -function Ticker({ review }: { review: Review | null }) { - return ( - - {review && ( - - {tickerLine(review)} - - )} - - ); -} - -export function LiveRun({ job, onCancel }: { job: Job; onCancel?: () => void }) { +export function LiveRun({ job, name }: { job: Job; name: string }) { const live = job.status === "queued" || job.status === "running"; - const [open, toggle] = useShowWork(); + const [stripOpen, setStripOpen] = useStripOpen(); + const [drawerOpen, setDrawerOpen] = useDrawerOpen(job.id, live); + const [pinned, setPinned] = useState(null); const playback = useReviewPlayback(job.reviews, live); - const { current, played, phase, duration, replay } = playback; - const now = useNow(1000); - const verdictRef = useRef(null); const model = analysisModel([...job.reviews.map((r) => r.model), ...modelsUsed(job.steps), job.settings.model]); - const decided = phase.verdict && current ? [...played, current] : played; - const groups = conclusions(decided, job.settings.checks); - const arrival = useArrival(current, phase.verdict, open && !isCompact(duration)); - const stats = liveStats(job, now); + const { review: focused, following } = focusedReview(job.reviews, pinned, playback.current); + const issues = issueCount(job); const reviewed = live ? shownCount(job.reviewed, playback) : job.reviewed; - const selected = job.coverage.selected; - const percent = live ? Math.min(100, Math.round((reviewed / Math.max(1, selected)) * 100)) : 100; - const issues = (live ? decided : job.reviews).filter((r) => outcome(r) === "issue").length; - const showWork = () => { - if (!open && !live) replay(); - toggle(); + const open = (review?: Review) => { + setPinned(review && review !== playback.current ? reviewKey(review) : null); + setDrawerOpen(true); }; - const issueText = `${issues} ${issues === 1 ? "issue" : "issues"}`; return ( -
-
- {live ? ( - <> -
-
-
+ {stripOpen && ( + setStripOpen(false)} /> -
- {open && ( -
-
-
- Traces - {reviewed} -
- -
-
-
- {live ? "Reading trace" : "Last trace"} -
- -
-
-
- Conclusions - {issueText} -
- -
-
)} -
+ {!stripOpen && ( + + )} + setDrawerOpen(false)} + name={name} + model={model} + job={job} + playback={playback} + live={live} + focused={focused} + following={following} + phase={playback.phase} + groups={conclusions(job.reviews, job.settings.checks)} + scope={issues.scope === "findings" ? `from the last ${job.reviews.length} reviews` : issues.scope} + onPick={(review) => setPinned(review === playback.current ? null : reviewKey(review))} + onFollow={() => setPinned(null)} + /> + ); } diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveStrip.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveStrip.tsx new file mode 100644 index 00000000000..d471bfa96be --- /dev/null +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveStrip.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { PanelRight, X } from "lucide-react"; + +import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; +import { agoLabel } from "@/components/view_logs/TraceView/lensField"; +import { Button } from "@/components/ui/button"; +import { useNow } from "@/hooks/useNow"; +import { cn } from "@/lib/cva.config"; + +import { money } from "../../model/format"; +import { outcome, providerOf, queueRows, reviewKey, shortVerdict, type IssueCount, type StripState } from "../../model/live"; +import type { Playback } from "../../model/live"; +import type { Review } from "../../model/types"; + +const RECENT = 5; +const MARK = { issue: "●", clear: "✓", unknown: "–" } as const; + +export function ModelName({ model }: { model: string }) { + if (!model) return null; + const provider = providerOf(model); + return ( + + {provider && } + {model} + + ); +} + +function RecentLine({ review, now, onOpen }: { review: Review; now: number; onOpen: (review: Review) => void }) { + const result = outcome(review); + const issue = result === "issue"; + return ( +
  • + +
  • + ); +} + +function issueLabel({ count, scope }: IssueCount): string { + const noun = count === 1 ? "issue" : "issues"; + if (scope === "findings") return `${count} ${noun} found`; + return scope ? `${count} ${noun} ${scope}` : `${count} ${noun}`; +} + +export function LiveStrip({ + model, + state, + playback, + reviewed, + selected, + issues, + cost, + onOpen, + onClose, +}: { + model: string; + state: StripState; + playback: Pick; + reviewed: number; + selected: number; + issues: IssueCount; + cost: number; + onOpen: (review?: Review) => void; + onClose: () => void; +}) { + const now = useNow(5000); + const recent = queueRows(playback, RECENT); + return ( +
    +
    + + + {reviewed} of {selected} traces · {issueLabel(issues)} · {money(cost)} + +
    +
    + {state.kind === "failed" && ( +

    + {state.message} +

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

    + {state.message} +

    + )} + {recent.length > 0 && state.kind !== "waiting" && ( +
      + {recent.map((review) => ( + + ))} +
    + )} +
    +
    + + +
    +
    + ); +} diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/useLivePanels.ts b/ui/litellm-dashboard/src/components/lens/investigations/live/useLivePanels.ts new file mode 100644 index 00000000000..610e588a0f7 --- /dev/null +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/useLivePanels.ts @@ -0,0 +1,43 @@ +"use client"; + +import { useCallback, useState } from "react"; + +const STRIP_KEY = "litellm.lens.liveStrip"; +const DRAWER_KEY = "litellm.lens.liveDrawerClosed"; + +function read(key: string): string | null { + try { + return window.localStorage.getItem(key); + } catch { + return null; + } +} + +function write(key: string, value: string): void { + try { + window.localStorage.setItem(key, value); + } catch { + return; + } +} + +export function useStripOpen(): [boolean, (open: boolean) => void] { + const [open, setOpen] = useState(() => read(STRIP_KEY) !== "closed"); + const update = useCallback((next: boolean) => { + setOpen(next); + write(STRIP_KEY, next ? "open" : "closed"); + }, []); + return [open, update]; +} + +export function useDrawerOpen(jobId: string, live: boolean): [boolean, (open: boolean) => void] { + const [open, setOpen] = useState(() => live && read(DRAWER_KEY) !== jobId); + const update = useCallback( + (next: boolean) => { + setOpen(next); + if (!next) write(DRAWER_KEY, jobId); + }, + [jobId], + ); + return [open, update]; +} diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/useShowWork.ts b/ui/litellm-dashboard/src/components/lens/investigations/live/useShowWork.ts deleted file mode 100644 index 65e23b46674..00000000000 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/useShowWork.ts +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import { useCallback, useState } from "react"; - -const KEY = "litellm.lens.showWork"; - -function readStored(): boolean { - try { - return window.localStorage.getItem(KEY) === "1"; - } catch { - return false; - } -} - -function store(open: boolean): void { - try { - window.localStorage.setItem(KEY, open ? "1" : "0"); - } catch { - return; - } -} - -export function useShowWork(): [boolean, () => void] { - const [open, setOpen] = useState(readStored); - const toggle = useCallback(() => { - setOpen((current) => { - store(!current); - return !current; - }); - }, []); - return [open, toggle]; -}