diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/useStage.ts b/ui/litellm-dashboard/src/components/lens/investigations/live/useStage.ts index 130b6b75e06..f862e2acaf7 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/useStage.ts +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/useStage.ts @@ -2,11 +2,9 @@ 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"; +import { stageTick, startStage, stepStage, type Stage } from "../../model/stage"; +import type { InFlight, Review } from "../../model/types"; -const TICK_MS = 30; const CHAR_MS = 3; function prefersReducedMotion(): boolean { @@ -25,11 +23,11 @@ export function useStage( const input = { reading, reviews, now, slots, running, charMs }; const next = stepStage(stage, input); if (next !== stage) setStage(next); - const busy = running || next.lanes.length > 0; + const tick = stageTick(next, running, now, charMs); useEffect(() => { - if (!busy) return; - const timer = window.setInterval(() => setNow(Date.now()), TICK_MS); + if (!tick) return; + const timer = window.setInterval(() => setNow(Date.now()), tick); return () => window.clearInterval(timer); - }, [busy]); + }, [tick]); return { stage: next, now, charMs }; } diff --git a/ui/litellm-dashboard/src/components/lens/model/stage.test.ts b/ui/litellm-dashboard/src/components/lens/model/stage.test.ts index 593584d098f..12e5f367f72 100644 --- a/ui/litellm-dashboard/src/components/lens/model/stage.test.ts +++ b/ui/litellm-dashboard/src/components/lens/model/stage.test.ts @@ -1,6 +1,15 @@ import { describe, expect, it } from "vitest"; -import { holdMs, laneText, releasedReviews, startStage, stepStage, typedChars, type StageInput } from "./stage"; +import { + holdMs, + laneText, + releasedReviews, + stageTick, + startStage, + stepStage, + typedChars, + type StageInput, +} from "./stage"; import type { Review } from "./types"; function review(id: string, reasoning = "Checked the tool output. It matched."): Review { @@ -106,3 +115,13 @@ describe("now reading stage", () => { expect([...stopped.released].sort()).toEqual(["a", "x"]); }); }); + +describe("stage tick", () => { + it("ticks fast only while reasoning is typing, slowly for live timers, and stops when idle", () => { + const landed = stepStage(startStage([]), input({ reviews: [review("a")], now: 0, charMs: 3 })); + expect(stageTick(landed, true, 1, 3)).toBe(30); + expect(stageTick(landed, true, 10_000, 3)).toBe(500); + expect(stageTick(startStage([]), true, 0, 3)).toBe(500); + expect(stageTick(startStage([]), false, 0, 3)).toBeNull(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/lens/model/stage.ts b/ui/litellm-dashboard/src/components/lens/model/stage.ts index d4ac960e958..558de0095f1 100644 --- a/ui/litellm-dashboard/src/components/lens/model/stage.ts +++ b/ui/litellm-dashboard/src/components/lens/model/stage.ts @@ -1,4 +1,4 @@ -import type { InFlight } from "./live"; +import type { InFlight } from "./types"; import type { Review } from "./types"; export interface Lane { @@ -119,6 +119,17 @@ export function stepStage(stage: Stage, input: StageInput): Stage { }; } +const TYPING_TICK_MS = 30; +const CLOCK_TICK_MS = 500; + +export function stageTick(stage: Stage, running: boolean, now: number, charMs: number): number | null { + const typing = stage.lanes.some( + (lane) => lane.review && typedChars(lane, now, charMs) < laneText(lane.review.reasoning).length, + ); + if (typing) return TYPING_TICK_MS; + return running || stage.lanes.length ? CLOCK_TICK_MS : null; +} + export function releasedReviews(reviews: readonly Review[], stage: Stage): Review[] { return reviews.filter((review) => stage.released.has(review.execution_id)); }