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 550e2a0236c..d657ef800ec 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/ConclusionsPanel.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/ConclusionsPanel.tsx @@ -1,96 +1,27 @@ -import { useEffect, useRef, type RefObject } from "react"; - import { cn } from "@/lib/cva.config"; import { type Conclusion } from "../../model/live"; -export interface Arrival { - key: string; - checkId: string; - text: string; -} - -const FLY_MS = 400; - -function fly(from: DOMRect, to: DOMRect, text: string) { - const node = document.createElement("div"); - node.textContent = text; - node.setAttribute("aria-hidden", "true"); - node.className = - "pointer-events-none fixed z-floating truncate rounded-md border bg-background px-3 py-1.5 text-[12px] font-medium text-[#e5484d] shadow-sm"; - Object.assign(node.style, { left: `${from.left}px`, top: `${from.top}px`, width: `${from.width}px` }); - document.body.appendChild(node); - const animation = node.animate( - [ - { transform: "translate(0, 0)", opacity: 1 }, - { transform: `translate(${to.left - from.left}px, ${to.top - from.top}px)`, opacity: 0 }, - ], - { duration: FLY_MS, easing: "ease-out" }, - ); - animation.onfinish = () => node.remove(); - return () => node.remove(); -} - -function useFlyIn(arrival: Arrival | null, from: RefObject, list: RefObject) { - useEffect(() => { - const source = from.current; - const box = list.current; - if (!arrival || !source || !box || typeof HTMLElement.prototype.animate !== "function") return; - if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return; - const target = box.querySelector(`[data-check="${CSS.escape(arrival.checkId)}"]`) ?? box; - return fly(source.getBoundingClientRect(), target.getBoundingClientRect(), arrival.text); - }, [arrival, from, list]); -} - -export function ConclusionsPanel({ - groups, - reviewed, - arrival, - verdictRef, -}: { - groups: readonly Conclusion[]; - reviewed: number; - arrival: Arrival | null; - verdictRef: RefObject; -}) { - const list = useRef(null); - useFlyIn(arrival, verdictRef, list); +export function ConclusionsPanel({ groups, scope }: { groups: readonly Conclusion[]; scope: string }) { return ( - - - {groups.map((group) => ( - - - - - - ))} - {!groups.length && ( - - - - )} - -
- - {group.label} - - {Math.round((group.count / Math.max(1, reviewed)) * 100)}% of reviewed - - - {group.count} -
Nothing flagged yet.
+
    + {groups.map((group) => ( +
  1. +
  2. + ))} + {!groups.length &&
  3. Nothing flagged yet.
  4. } + {groups.length > 0 && scope &&
  5. {scope}
  6. } +
); } diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx new file mode 100644 index 00000000000..fdb093dfd41 --- /dev/null +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveDrawer.tsx @@ -0,0 +1,91 @@ +"use client"; + +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"; + +import type { Conclusion, Phase, Playback } from "../../model/live"; +import type { Job, Review } from "../../model/types"; +import { ConclusionsPanel } from "./ConclusionsPanel"; +import { ReadingPanel } from "./ReadingPanel"; +import { ReviewQueue } from "./ReviewQueue"; +import { ModelName } from "./LiveStrip"; + +const STATUS: Record = { + queued: { label: "Queued", tone: "neutral" }, + running: { label: "Running", tone: "neutral" }, + completed: { label: "Done", tone: "neutral" }, + 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"; + +export function LiveDrawer({ + open, + onClose, + name, + model, + job, + playback, + live, + focused, + following, + phase, + groups, + scope, + onPick, + onFollow, +}: { + open: boolean; + onClose: () => void; + name: string; + model: string; + job: Job; + playback: Pick; + live: boolean; + focused: Review | null; + following: boolean; + phase: Phase; + groups: readonly Conclusion[]; + 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 }; + return ( + !value && onClose()}> + + + {name} + } className="flex flex-wrap items-center gap-2"> + + + {!following && live && ( + + )} + + +
+ {focused ? ( + + ) : ( +

Waiting for the first trace review.

+ )} +
+
+ Conclusions +
+ +
+ Traces + {job.reviewed} +
+ +
+
+ ); +} diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/ReadingPanel.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/ReadingPanel.tsx index 5ab9bd35503..0d6541a43d9 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/ReadingPanel.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/ReadingPanel.tsx @@ -1,8 +1,7 @@ -import type { Ref } from "react"; - import { cn } from "@/lib/cva.config"; import { outcome, verdictLine, type Phase } from "../../model/live"; +import { spanPreviewLines } from "../../model/spanPreview"; import type { Review } from "../../model/types"; const VERDICT_TONE = { @@ -18,83 +17,77 @@ const VERDICT_DOT = { function Spans({ review, phase }: { review: Review; phase: Phase }) { return ( - - - {review.spans.map((span, index) => { - const reading = phase.span === index; - const cited = phase.verdict && span.cited; - return ( - - - - - ); - })} - {!review.spans.length && ( - - - - )} - -
- {span.kind || span.name} - - {span.preview || span.name} -
No trace parts were shown to the model
+
    + {review.spans.map((span, index) => { + const reading = phase.span === index; + const cited = phase.verdict && span.cited; + return ( +
  1. +

    + {span.kind} + {span.name} +

    + {spanPreviewLines(span.preview).map((line, n) => ( +

    + {line.label && ( + {line.label} + )} + {line.text} +

    + ))} +
  2. + ); + })} + {!review.spans.length && ( +
  3. No trace parts were shown to the model
  4. + )} +
); } -export function ReadingPanel({ - review, - phase, - verdictRef, -}: { - review: Review | null; - phase: Phase; - verdictRef: Ref; -}) { - if (!review) { - return

Waiting for the first trace review.

; - } +export function ReadingPanel({ review, phase }: { review: Review; phase: Phase }) { const result = outcome(review); const typing = phase.typed < review.reasoning.length; return ( -
-
+
+
+ {review.agent || review.name} trace {review.trace_id.slice(0, 8)} - {review.agent || review.name}
-
+
Reasoning {review.model}
-

+

{review.reasoning ? review.reasoning.slice(0, phase.typed) : !typing && "No reasoning was returned."} {typing &&

); diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/ReviewQueue.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/ReviewQueue.tsx index 4fcf3b1c225..80fda884e0b 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/ReviewQueue.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/ReviewQueue.tsx @@ -3,48 +3,52 @@ import { Loader2 } from "lucide-react"; import { cn } from "@/lib/cva.config"; import { outcome, queueRows, reviewKey, type Playback } from "../../model/live"; +import type { Review } from "../../model/types"; -const LIMIT = 80; +const LIMIT = 60; -export function ReviewQueue({ playback, live }: { playback: Pick; live: boolean }) { +export function ReviewQueue({ + playback, + live, + focused, + onPick, +}: { + playback: Pick; + live: boolean; + focused: Review | null; + onPick: (review: Review) => void; +}) { return ( - - - {queueRows(playback, LIMIT).map((review) => { - const active = live && review === playback.current; - const result = outcome(review); - return ( - + {queueRows(playback, LIMIT).map((review) => { + const reading = live && review === playback.current; + const result = outcome(review); + const selected = review === focused; + return ( +
  • +
  • - - - - ); - })} - -
    - {active ? ( - - ) : ( - - )} - {review.agent || review.name} - {review.trace_id.slice(0, 8)} -
    + {reading ? ( + + ) : ( + + )} + {review.agent || review.name} + {review.trace_id.slice(0, 8)} + + + ); + })} + ); }