diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx index 6b55c0778e0..b3947522732 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx @@ -10,6 +10,7 @@ import type { Job, Review } from "../../model/types"; import { ConclusionsPanel } from "./ConclusionsPanel"; import { ModelName } from "./LiveStrip"; import { TraceList } from "./TraceList"; +import { useLensMode } from "./useLensMode"; const STATUS: Record = { queued: { label: "Queued", tone: "neutral" }, @@ -29,6 +30,7 @@ export function LiveDrawer({ reviewed, reviews, now, + done, reading, groups, scope, @@ -42,6 +44,7 @@ export function LiveDrawer({ reviewed: number; reviews: readonly Review[]; now: string | null; + done: string | null; reading: readonly InFlight[]; groups: readonly Conclusion[]; scope: string; @@ -49,6 +52,8 @@ export function LiveDrawer({ }) { const [group, setGroup] = useState(null); const badge = STATUS[status]; + const live = status === "queued" || status === "running"; + const mode = useLensMode(live); return ( !value && onClose()}> @@ -63,13 +68,14 @@ export function LiveDrawer({

Traces

- - {now ?? `${reviewed} reviewed`} + + {now ?? done ?? `${reviewed} reviewed`} + {done && !now && } {reviewed > reviews.length && reviews.length ? ` · showing latest ${reviews.length}` : ""}
- {reviews.length || reading.length ? ( - + {reviews.length || reading.length || status === "running" ? ( + ) : (

Waiting for the first trace…

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 0b9bffc0455..7c4a16365dd 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx @@ -5,6 +5,7 @@ import { useState } from "react"; import { modelsUsed } from "../../model/inbox"; import { analysisModel, + doneLine, conclusions, inFlight, issueCount, @@ -80,6 +81,7 @@ export function LiveRun({ reviews={reviews} now={job.status === "running" ? nowLine(job, reading.length) : null} reading={reading} + done={job.status === "completed" ? doneLine(job) : null} groups={conclusions(reviews, job.settings.checks)} scope={job.reviewed > reviews.length ? `From the latest ${reviews.length} of ${job.reviewed} reviewed traces` : ""} waiting={waiting} diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/TraceList.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/TraceList.tsx index 876158fa935..d502ab4794d 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/TraceList.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/TraceList.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useLayoutEffect, useRef, useState } from "react"; import { agoLabel } from "@/components/view_logs/TraceView/lensField"; import { useNow } from "@/hooks/useNow"; @@ -12,6 +12,9 @@ import { ModelName } from "./LiveStrip"; const LIMIT = 200; const RED = "text-[#e5484d]"; +const ROW = "grid h-9 w-full grid-cols-[0.75rem_minmax(0,8rem)_minmax(0,1fr)_auto] items-center gap-2 px-2 text-left text-[12px]"; + +export type LensMode = "live" | "settling" | "off"; function Expanded({ review }: { review: Review }) { const verdicts = review.verdicts.length @@ -38,17 +41,12 @@ function Expanded({ review }: { review: Review }) { ); } -function ReadingRow({ item, model, now }: { item: InFlight; model: string; now: number }) { +function ReadingRow({ item, now }: { item: InFlight; now: number }) { return ( -
+
{item.agent || "trace"} - - - - - {item.trace_id.slice(0, 8)} - + {item.trace_id.slice(0, 8)} reading · {durationLabel(Math.max(0, now - Date.parse(item.started_at)))} @@ -56,61 +54,114 @@ function ReadingRow({ item, model, now }: { item: InFlight; model: string; now: ); } +function DoneRow({ review, now, open, onToggle }: { review: Review; now: number; open: boolean; onToggle: () => void }) { + const result = outcome(review); + return ( + <> + + {open && } + + ); +} + +function useLensBox(target: string, layout: string) { + const list = useRef(null); + const [box, setBox] = useState<{ top: number; height: number } | null>(null); + useLayoutEffect(() => { + const nodes = [...(list.current?.querySelectorAll(`[data-lens="${target}"]`) ?? [])]; + const first = nodes[0]; + const last = nodes.at(-1); + setBox(first && last ? { top: first.offsetTop, height: last.offsetTop + last.offsetHeight - first.offsetTop } : null); + }, [target, layout]); + return { list, box }; +} + +function Lens({ box, model, mode }: { box: { top: number; height: number }; model: string; mode: LensMode }) { + return ( + + ); +} + export function TraceList({ reading, reviews, model, group, + mode, + nowLine, }: { reading: readonly InFlight[]; reviews: readonly Review[]; model: string; group: string | null; + mode: LensMode; + nowLine: string | null; }) { const now = useNow(reading.length ? 200 : 5000); const [expanded, setExpanded] = useState(null); const rows = liveRows(reading, reviews, LIMIT).filter((row) => row.kind === "reading" || inGroup(row.review, group)); - if (!rows.length) return

No traces in this group yet.

; + const inFlight = rows.some((row) => row.kind === "reading"); + const target = inFlight ? "reading" : "slot"; + const layout = `${expanded}|${rows.map((row) => `${row.kind}:${row.key}`).join(",")}`; + const { list, box } = useLensBox(target, layout); + const showSlot = mode === "live" && !inFlight; return ( -
    - {rows.map((row) => { - if (row.kind === "reading") { - return ( -
  1. - -
  2. - ); - } - const { review, key } = row; - const result = outcome(review); - const open = expanded === key; - return ( -
  3. - - {open && } + )}
  4. - ); - })} -
+ ))} + + {!rows.length && !showSlot && ( +

No traces in this group yet.

+ )} +
); } diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/useLensMode.ts b/ui/litellm-dashboard/src/components/lens/investigations/live/useLensMode.ts new file mode 100644 index 00000000000..ff350072f9b --- /dev/null +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/useLensMode.ts @@ -0,0 +1,23 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import type { LensMode } from "./TraceList"; + +const SETTLE_MS = 400; + +export function useLensMode(live: boolean): LensMode { + const [wasLive, setWasLive] = useState(live); + const [settled, setSettled] = useState(!live); + if (live && !wasLive) { + setWasLive(true); + setSettled(false); + } + useEffect(() => { + if (live || settled) return; + const timer = window.setTimeout(() => setSettled(true), SETTLE_MS); + return () => window.clearTimeout(timer); + }, [live, settled]); + if (live) return "live"; + return settled ? "off" : "settling"; +}