From 3577f3bebf825529f296425f8ceadeb060a2d9ab Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Sat, 3 Oct 2026 18:09:47 -0700 Subject: [PATCH] feat(lens): show a now reading stage that types each trace's reasoning --- ui/litellm-dashboard/src/app/globals.css | 10 +++ .../lens/investigations/live/NowReading.tsx | 88 +++++++++++++++++++ .../lens/investigations/live/useStage.ts | 34 +++++++ 3 files changed, 132 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/live/NowReading.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/live/useStage.ts 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 ( +
  • +
    +
    + {!review ? ( +
  • + ); +} + +export function NowReading({ + model, + counter, + lanes, + now, + charMs, +}: { + model: string; + counter: string; + lanes: readonly Lane[]; + now: number; + charMs: number; +}) { + return ( +
    +
    + + + {counter} + +
    + {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 }; +}