diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/ConclusionsPanel.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/ConclusionsPanel.tsx index d657ef800ec..4c00863141b 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/ConclusionsPanel.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/ConclusionsPanel.tsx @@ -1,27 +1,90 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + import { cn } from "@/lib/cva.config"; -import { type Conclusion } from "../../model/live"; +import { grownGroups, share, type Conclusion } from "../../model/live"; -export function ConclusionsPanel({ groups, scope }: { groups: readonly Conclusion[]; scope: string }) { +const FLASH_MS = 150; + +function useFlashing(groups: readonly Conclusion[]): ReadonlySet { + const previous = useRef(groups); + const [flashing, setFlashing] = useState>(new Set()); + useEffect(() => { + const grown = grownGroups(previous.current, groups); + previous.current = groups; + if (!grown.size) return; + setFlashing(grown); + const timer = window.setTimeout(() => setFlashing(new Set()), FLASH_MS); + return () => window.clearTimeout(timer); + }, [groups]); + return flashing; +} + +export function ConclusionsPanel({ + groups, + total, + scope, + selected, + onSelect, +}: { + groups: readonly Conclusion[]; + total: number; + scope: string; + selected: string | null; + onSelect: (key: string | null) => void; +}) { + const flashing = useFlashing(groups); + if (!groups.length) return

Nothing concluded yet.

; return ( -
    - {groups.map((group) => ( -
  1. -
  2. - ))} - {!groups.length &&
  3. Nothing flagged yet.
  4. } - {groups.length > 0 && scope &&
  5. {scope}
  6. } -
+
+
    + {groups.map((group) => { + const active = selected === group.key; + return ( +
  1. + +
  2. + ); + })} +
+ {scope &&

{scope}

} +
); } 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 fdb093dfd41..a99a4da1f6f 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx @@ -1,5 +1,7 @@ "use client"; +import { useState } from "react"; + import { StatusBadge, type StatusTone } from "@/components/shared/table_cells"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"; @@ -7,9 +9,9 @@ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from " import type { Conclusion, Phase, Playback } from "../../model/live"; import type { Job, Review } from "../../model/types"; import { ConclusionsPanel } from "./ConclusionsPanel"; +import { ModelName } from "./LiveStrip"; import { ReadingPanel } from "./ReadingPanel"; import { ReviewQueue } from "./ReviewQueue"; -import { ModelName } from "./LiveStrip"; const STATUS: Record = { queued: { label: "Queued", tone: "neutral" }, @@ -18,21 +20,23 @@ const STATUS: Record = { failed: { label: "Failed", tone: "error" }, cancelled: { label: "Cancelled", tone: "neutral" }, }; -const SECTION_HEAD = - "flex h-8 items-center justify-between border-y border-border bg-muted/40 px-4 text-[10px] tracking-[0.08em] text-muted-foreground uppercase"; +const PANE_TITLE = "text-[12px] font-semibold text-foreground"; +const SETTLED: Phase = { span: -1, typed: Number.MAX_SAFE_INTEGER, verdict: true }; export function LiveDrawer({ open, onClose, name, model, - job, + status, + reviewed, playback, live, focused, following, phase, groups, + decided, scope, onPick, onFollow, @@ -41,27 +45,30 @@ export function LiveDrawer({ onClose: () => void; name: string; model: string; - job: Job; + status: Job["status"]; + reviewed: number; playback: Pick; live: boolean; focused: Review | null; following: boolean; phase: Phase; groups: readonly Conclusion[]; + decided: number; scope: string; onPick: (review: Review) => void; onFollow: () => void; }) { - const status = STATUS[job.status]; - const shownPhase = following ? phase : { span: -1, typed: Number.MAX_SAFE_INTEGER, verdict: true }; + const [group, setGroup] = useState(null); + const badge = STATUS[status]; + const shown = playback.played.length + (playback.current ? 1 : 0); return ( !value && onClose()}> - - - {name} - } className="flex flex-wrap items-center gap-2"> - - + + + {name} + } className="flex flex-wrap items-center gap-2.5 pr-10"> + + {!following && live && ( ); 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 2d6ab928f3c..825872ba43d 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/useReviewPlayback.ts +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/useReviewPlayback.ts @@ -3,6 +3,7 @@ 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; @@ -46,6 +47,6 @@ export function useReviewPlayback( const phase = still || !current ? { span: -1, typed: current?.reasoning.length ?? 0, verdict: true } - : playbackPhase(now - state.startedAt, state.duration, current.spans.length, current.reasoning.length); + : playbackPhase(now - state.startedAt, state.duration, timeline(current.spans).length, current.reasoning.length); return { ...state, phase, replay }; } diff --git a/ui/litellm-dashboard/src/components/lens/model/spanPreview.ts b/ui/litellm-dashboard/src/components/lens/model/spanPreview.ts index ae7a7e0d480..fed5baee994 100644 --- a/ui/litellm-dashboard/src/components/lens/model/spanPreview.ts +++ b/ui/litellm-dashboard/src/components/lens/model/spanPreview.ts @@ -151,11 +151,3 @@ export function timeline(spans: readonly Span[]): TimelineItem[] { ]; return items.length ? items : fallback(spans); } - -export function spanPreviewLines(preview: string): { label: string; text: string; error: boolean }[] { - const { input, output } = parts(preview); - return [ - { label: "in", text: compact(input), error: false }, - { label: "out", text: compact(output), error: !!stepFailure(preview) }, - ].filter((line) => line.text); -}