diff --git a/ui/litellm-dashboard/src/app/globals.css b/ui/litellm-dashboard/src/app/globals.css
index 72d304c0dc7..adfc1c5a57e 100644
--- a/ui/litellm-dashboard/src/app/globals.css
+++ b/ui/litellm-dashboard/src/app/globals.css
@@ -69,6 +69,16 @@
--animate-slot-slide-in: slot-slide-in 150ms cubic-bezier(0, 0, 0.2, 1) both;
--animate-trace-drawer-in: trace-drawer-in 200ms cubic-bezier(0.25, 1, 0.5, 1) both;
--animate-trace-drawer-out: trace-drawer-out 200ms cubic-bezier(0.4, 0, 1, 1) both;
+ --animate-lens-shimmer: lens-shimmer 1.1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
+
+ @keyframes lens-shimmer {
+ from {
+ transform: translateX(-100%);
+ }
+ to {
+ transform: translateX(250%);
+ }
+ }
@keyframes trace-drawer-in {
from {
diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/NowReading.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/NowReading.tsx
new file mode 100644
index 00000000000..b785616eea9
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/investigations/live/NowReading.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { cn } from "@/lib/cva.config";
+
+import { durationLabel, outcome, shortVerdict } from "../../model/live";
+import { laneText, typedChars, type Lane } from "../../model/stage";
+import { ModelName } from "./LiveStrip";
+
+const RED = "text-[#e5484d]";
+
+function LaneRow({ lane, now, charMs }: { lane: Lane; now: number; charMs: number }) {
+ const { review } = lane;
+ const text = review ? laneText(review.reasoning) : "";
+ const typed = typedChars(lane, now, charMs);
+ const typing = !!review && typed < text.length;
+ const issue = review ? outcome(review) === "issue" : false;
+ return (
+
+
+
+ {lane.agent || "trace"}
+ {lane.traceId.slice(0, 8)}
+
+ {review ? durationLabel(review.duration_ms) : `reading ยท ${durationLabel(Math.max(0, now - lane.startedAt))}`}
+
+
+ {!review ? (
+
+ ) : (
+ <>
+
+ {text.slice(0, typed)}
+ {typing && }
+
+ {!typing && (
+
+ {shortVerdict(review)}
+
+ )}
+ >
+ )}
+
+ );
+}
+
+export function NowReading({
+ model,
+ counter,
+ lanes,
+ now,
+ charMs,
+}: {
+ model: string;
+ counter: string;
+ lanes: readonly Lane[];
+ now: number;
+ charMs: number;
+}) {
+ return (
+
+
+ {lanes.length ? (
+
+ {lanes.map((lane) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/useStage.ts b/ui/litellm-dashboard/src/components/lens/investigations/live/useStage.ts
new file mode 100644
index 00000000000..3beb93d21d4
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/investigations/live/useStage.ts
@@ -0,0 +1,34 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+import type { InFlight } from "../../model/live";
+import { startStage, stepStage, type Stage } from "../../model/stage";
+import type { Review } from "../../model/types";
+
+const TICK_MS = 30;
+const CHAR_MS = 3;
+
+function prefersReducedMotion(): boolean {
+ return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
+}
+
+export function useStage(
+ reviews: readonly Review[],
+ reading: readonly InFlight[],
+ running: boolean,
+ slots: number,
+): { stage: Stage; now: number; charMs: number } {
+ const [charMs] = useState(() => (prefersReducedMotion() ? 0 : CHAR_MS));
+ const [stage, setStage] = useState(() => startStage(reviews));
+ const [now, setNow] = useState(Date.now);
+ const next = stepStage(stage, { reading, reviews, now, slots, running, charMs });
+ if (next !== stage) setStage(next);
+ const busy = running || next.lanes.length > 0;
+ useEffect(() => {
+ if (!busy) return;
+ const timer = window.setInterval(() => setNow(Date.now()), TICK_MS);
+ return () => window.clearInterval(timer);
+ }, [busy]);
+ return { stage: next, now, charMs };
+}