+
+
+
+
Traces
+
+ {reviewed} reviewed{reviewed > shown && shown ? ` · showing latest ${shown}` : ""}
+
+
+ {empty ? (
+
Waiting for the first trace…
{waiting}
+ ) : (
+
)}
-
-
-
Conclusions so far
-
-
-
-
-
Traces
-
- {reviewed} reviewed{reviewed > shown ? ` · showing latest ${shown}` : ""}
-
-
-
-
+
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 d046fbd4069..067bf3be2fb 100644
--- a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx
+++ b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx
@@ -7,9 +7,7 @@ import {
analysisModel,
conclusions,
decidedReviews,
- focusedReview,
issueCount,
- reviewKey,
shownCount,
stripState,
} from "../../model/live";
@@ -37,19 +35,13 @@ export function LiveRun({
const reason = useQueueReason(job, queue);
const [stripOpen, setStripOpen] = useStripOpen();
const [drawerOpen, setDrawerOpen] = useState(false);
- const [pinned, setPinned] = useState
(null);
const playback = useReviewPlayback(reviews, live);
const withReviews = { ...job, reviews: [...reviews] };
const model = analysisModel([...reviews.map((r) => r.model), ...modelsUsed(job.steps), job.settings.model]);
- const { review: focused, following } = focusedReview(reviews, pinned, playback.current);
const issues = issueCount(withReviews);
const reviewed = live ? shownCount(job.reviewed, playback) : job.reviewed;
const decided = decidedReviews(playback, playback.phase.verdict);
- const pin = (review: Review) => setPinned(review === playback.current ? null : reviewKey(review));
- const open = (review?: Review) => {
- if (review) pin(review);
- setDrawerOpen(true);
- };
+ const open = () => setDrawerOpen(true);
const waiting = reason && (
<>
@@ -89,16 +81,10 @@ export function LiveRun({
status={job.status}
reviewed={reviewed}
playback={playback}
- live={live}
- focused={focused}
- following={following}
- phase={playback.phase}
groups={conclusions(decided, job.settings.checks)}
decided={decided.length}
scope={job.reviewed > reviews.length ? `From the latest ${reviews.length} of ${job.reviewed} reviewed traces` : ""}
waiting={waiting}
- onPick={pin}
- onFollow={() => setPinned(null)}
/>
>
);
diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/TraceList.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/TraceList.tsx
new file mode 100644
index 00000000000..49ad1146699
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/investigations/live/TraceList.tsx
@@ -0,0 +1,132 @@
+"use client";
+
+import { useLayoutEffect, useRef, useState } from "react";
+
+import { agoLabel } from "@/components/view_logs/TraceView/lensField";
+import { useNow } from "@/hooks/useNow";
+import { cn } from "@/lib/cva.config";
+
+import { briefReasoning, inGroup, outcome, reviewKey, shortVerdict, traceRows, type Playback } from "../../model/live";
+import type { Review } from "../../model/types";
+import { ModelName } from "./LiveStrip";
+
+const LIMIT = 80;
+const RED = "text-[#e5484d]";
+
+function Expanded({ review }: { review: Review }) {
+ const verdicts = review.verdicts.length
+ ? review.verdicts
+ : [{ check_id: "", kind: "pattern" as const, summary: review.cannot_assess ? "Not enough evidence to judge" : "No issue observed" }];
+ return (
+
+ {review.reasoning &&
{briefReasoning(review.reasoning)}
}
+
+ {verdicts.map((verdict, index) => (
+ -
+
+ {verdict.summary}
+
+ ))}
+
+
+ );
+}
+
+function useHighlighter(active: string | null, layout: string) {
+ const list = useRef(null);
+ const [box, setBox] = useState<{ top: number; height: number } | null>(null);
+ useLayoutEffect(() => {
+ const row = active ? list.current?.querySelector(`[data-row="${CSS.escape(active)}"]`) : null;
+ setBox(row ? { top: row.offsetTop, height: row.offsetHeight } : null);
+ }, [active, layout]);
+ return { list, box };
+}
+
+export function TraceList({
+ playback,
+ model,
+ live,
+ group,
+}: {
+ playback: Pick;
+ model: string;
+ live: boolean;
+ group: string | null;
+}) {
+ const now = useNow(5000);
+ const [expanded, setExpanded] = useState(null);
+ const rows = traceRows(playback, LIMIT)
+ .map((row) => (live || row.state !== "reviewing" ? row : { ...row, state: "done" as const }))
+ .filter(({ review, state }) => state !== "done" || inGroup(review, group));
+ const active = live && playback.current ? reviewKey(playback.current) : null;
+ const { list, box } = useHighlighter(active, `${expanded}|${rows.map((row) => reviewKey(row.review)).join(",")}`);
+ if (!rows.length) return No traces yet.
;
+ return (
+
+ {box && (
+
+ )}
+ {rows.map(({ review, state }) => {
+ const key = reviewKey(review);
+ const result = outcome(review);
+ const open = expanded === key;
+ const reviewing = state === "reviewing";
+ const queued = state === "queued";
+ return (
+ -
+
+ {open && }
+
+ );
+ })}
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/useReviewPlayback.ts b/ui/litellm-dashboard/src/components/lens/investigations/live/useReviewPlayback.ts
index 825872ba43d..2d6ab928f3c 100644
--- a/ui/litellm-dashboard/src/components/lens/investigations/live/useReviewPlayback.ts
+++ b/ui/litellm-dashboard/src/components/lens/investigations/live/useReviewPlayback.ts
@@ -3,7 +3,6 @@
import { useCallback, useEffect, useReducer, useState } from "react";
import { playbackPhase, playbackReducer, startPlayback, type Phase, type Playback } from "../../model/live";
-import { timeline } from "../../model/spanPreview";
import type { Review } from "../../model/types";
const FRAME_MS = 40;
@@ -47,6 +46,6 @@ export function useReviewPlayback(
const phase =
still || !current
? { span: -1, typed: current?.reasoning.length ?? 0, verdict: true }
- : playbackPhase(now - state.startedAt, state.duration, timeline(current.spans).length, current.reasoning.length);
+ : playbackPhase(now - state.startedAt, state.duration, current.spans.length, current.reasoning.length);
return { ...state, phase, replay };
}