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 42fda962ffa..550e2a0236c 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/ConclusionsPanel.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/ConclusionsPanel.tsx @@ -10,26 +10,22 @@ export interface Arrival { text: string; } -const FLY_MS = 550; +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-lg border border-[#e5484d]/30 bg-background px-3 py-2 text-xs font-semibold text-[#e5484d] shadow-lg"; + "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)", width: `${from.width}px`, opacity: 1 }, - { - transform: `translate(${to.left - from.left}px, ${to.top - from.top}px)`, - width: `${Math.max(200, to.width)}px`, - opacity: 0.2, - }, + { transform: "translate(0, 0)", opacity: 1 }, + { transform: `translate(${to.left - from.left}px, ${to.top - from.top}px)`, opacity: 0 }, ], - { duration: FLY_MS, easing: "cubic-bezier(.3,.7,.2,1)" }, + { duration: FLY_MS, easing: "ease-out" }, ); animation.onfinish = () => node.remove(); return () => node.remove(); @@ -57,41 +53,44 @@ export function ConclusionsPanel({ arrival: Arrival | null; verdictRef: RefObject; }) { - const list = useRef(null); + const list = useRef(null); useFlyIn(arrival, verdictRef, list); return ( -
    - {groups.map((group) => ( -
  1. -

    -

    -

    - - {group.count} {group.count === 1 ? "trace" : "traces"} - - {Math.round((group.count / Math.max(1, reviewed)) * 100)}% of reviewed -

    - {group.latest !== group.label && ( -

    - latest: {group.latest} -

    - )} -
  2. - ))} - {!groups.length &&
  3. Nothing flagged yet.
  4. } -
+ + + {groups.map((group) => ( + + + + + + ))} + {!groups.length && ( + + + + )} + +
+ + {group.label} + + {Math.round((group.count / Math.max(1, reviewed)) * 100)}% of reviewed + + + {group.count} +
Nothing flagged yet.
); } diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx index caf00f38511..2dcd4900483 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveRun.tsx @@ -1,12 +1,12 @@ "use client"; -import { useMemo, useRef, useState, type CSSProperties } from "react"; +import { useMemo, useRef } from "react"; +import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; +import { StatusBadge, type StatusTone } from "@/components/shared/table_cells"; import { Button } from "@/components/ui/button"; import { useNow } from "@/hooks/useNow"; -import { cn } from "@/lib/cva.config"; -import { sampleBatch } from "../../__fixtures__/reviews"; import { durationText, money } from "../../model/format"; import { modelsUsed } from "../../model/inbox"; import { @@ -15,27 +15,28 @@ import { isCompact, liveStats, outcome, - providerAccent, providerOf, - rateLabel, reviewKey, shownCount, tokenLabel, verdictLine, } from "../../model/live"; -import { analysisProgress } from "../../model/progress"; import type { Job, Review } from "../../model/types"; import { ConclusionsPanel, type Arrival } from "./ConclusionsPanel"; -import { ProviderBadge } from "./ProviderBadge"; import { ReadingPanel } from "./ReadingPanel"; import { ReviewQueue } from "./ReviewQueue"; import { useReviewPlayback } from "./useReviewPlayback"; -const NEUTRAL_ACCENT = "var(--foreground)"; -const SAMPLE_POLL_MS = 2000; -const SAMPLE_TOTAL = 60; +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 COLUMN_HEAD = - "flex h-9 shrink-0 items-center justify-between gap-2 border-b px-3.5 text-[10px] tracking-[0.08em] text-muted-foreground uppercase"; + "sticky top-0 z-sticky flex h-8 shrink-0 items-center justify-between gap-2 border-b border-border bg-muted/40 px-3 text-[10px] tracking-[0.08em] text-muted-foreground uppercase backdrop-blur"; +const COLUMN = "flex min-h-0 flex-col overflow-y-auto"; function useArrival(review: Review | null, verdict: boolean, compact: boolean): Arrival | null { const issue = review && verdict && !compact ? review.verdicts.find((v) => v.kind === "issue") : undefined; @@ -45,72 +46,51 @@ function useArrival(review: Review | null, verdict: boolean, compact: boolean): return useMemo(() => (key ? { key, checkId, text } : null), [key, checkId, text]); } -function Status({ job }: { job: Job }) { - if (job.status === "queued") return Queued; - if (job.status === "running") - return ( - - Running - - ); - if (job.status === "completed") - return ( - - Done - - ); - return {job.status}; -} - -function useSampleReviews(enabled: boolean): readonly Review[] { - const now = useNow(SAMPLE_POLL_MS); - const [startedAt] = useState(now); - return useMemo(() => { - if (!enabled) return []; - const polls = Math.floor((now - startedAt) / SAMPLE_POLL_MS); - return Array.from({ length: polls + 1 }, (_, poll) => sampleBatch(poll, poll < 3 ? 1 : 3)) - .flat() - .slice(0, SAMPLE_TOTAL); - }, [enabled, now, startedAt]); +function ModelLabel({ model }: { model: string }) { + if (!model) return null; + const provider = providerOf(model); + return ( + + {provider && } + {model} + + ); } export function LiveRun({ job, onCancel }: { job: Job; onCancel?: () => void }) { const live = job.status === "queued" || job.status === "running"; - const sample = live && job.reviews.length === 0; - const sampled = useSampleReviews(sample); - const reviews = sample ? sampled : job.reviews; - const playback = useReviewPlayback(reviews, live); - const { current, played, pending, phase, duration } = playback; + const playback = useReviewPlayback(job.reviews, live); + const { current, played, phase, duration } = playback; const now = useNow(1000); const verdictRef = useRef(null); - const model = analysisModel([current?.model ?? "", ...reviews.map((r) => r.model), ...modelsUsed(job.steps)]); - const accent = providerAccent(providerOf(model)) ?? NEUTRAL_ACCENT; + const model = analysisModel([...job.reviews.map((r) => r.model), ...modelsUsed(job.steps), job.settings.model]); const decided = phase.verdict && current ? [...played, current] : played; const groups = conclusions(decided, job.settings.checks); const arrival = useArrival(current, phase.verdict, isCompact(duration)); const stats = liveStats(job, now); - const reviewed = sample ? decided.length : shownCount(job.reviewed, playback); - const selected = sample ? SAMPLE_TOTAL : job.coverage.selected; + const reviewed = shownCount(job.reviewed, playback); + const selected = job.coverage.selected; const percent = live ? Math.min(100, Math.round((reviewed / Math.max(1, selected)) * 100)) : 100; const issues = decided.filter((r) => outcome(r) === "issue").length; - const progress = analysisProgress(job); + const status = STATUS[job.status]; return ( -
-
-
- - - {live ? progress.title : `Reviewed ${reviewed} ${reviewed === 1 ? "trace" : "traces"}`} - {sample && " · preview data until the worker reports reviews"} +
+
+
+ + + {live ? job.stage : "Complete"} + + {reviewed} of {selected} traces + +
-
- +
+ {durationText(stats.elapsedSeconds)} + {tokenLabel(stats.tokens)} + {money(stats.cost)} {live && onCancel && (
diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/ProviderBadge.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/ProviderBadge.tsx deleted file mode 100644 index 3bc3d1402d9..00000000000 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/ProviderBadge.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { getProviderLogoAndName } from "@/components/provider_info_helpers"; -import { cn } from "@/lib/cva.config"; - -import { modelName, providerOf } from "../../model/live"; - -const MARK_CROP: Readonly> = { - cerebras: "absolute top-[-8px] left-[-2.4px] h-[30px] w-[40px] max-w-none", -}; - -export function ProviderLogo({ model, className }: { model: string; className?: string }) { - const provider = providerOf(model); - const { logo, displayName } = getProviderLogoAndName(provider); - if (!provider || !logo) return null; - const crop = MARK_CROP[provider]; - return ( - - {/* eslint-disable-next-line @next/next/no-img-element -- provider logos are static svgs from /assets/logos */} - {displayName} - - ); -} - -export function ProviderBadge({ model }: { model: string }) { - if (!model) return null; - const provider = providerOf(model); - const { displayName } = getProviderLogoAndName(provider); - return ( - - - - {provider ? `Analyzed by ${displayName} · ${modelName(model)}` : `Analyzed by ${model}`} - - - ); -} 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 5949e8172ec..5ab9bd35503 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/ReadingPanel.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/ReadingPanel.tsx @@ -2,112 +2,98 @@ import type { Ref } from "react"; import { cn } from "@/lib/cva.config"; -import { modelName, outcome, verdictLine, type Phase } from "../../model/live"; +import { outcome, verdictLine, type Phase } from "../../model/live"; import type { Review } from "../../model/types"; -import { ProviderLogo } from "./ProviderBadge"; const VERDICT_TONE = { - issue: "border-[#e5484d]/30 bg-[#e5484d]/5 text-[#e5484d]", - clear: "border-border bg-muted/60 text-foreground", - unknown: "border-border bg-muted/60 text-muted-foreground", + issue: "text-[#e5484d]", + clear: "text-foreground", + unknown: "text-muted-foreground", +} as const; +const VERDICT_DOT = { + issue: "bg-[#e5484d]", + clear: "bg-muted-foreground/40", + unknown: "bg-muted-foreground/40", } as const; -const VERDICT_MARK = { issue: "●", clear: "✓", unknown: "?" } as const; function Spans({ review, phase }: { review: Review; phase: Phase }) { return ( -
-
- trace {review.trace_id.slice(0, 8)} - {review.agent || review.name} -
-
    + + {review.spans.map((span, index) => { - const scanning = phase.span === index; + const reading = phase.span === index; const cited = phase.verdict && span.cited; return ( -
  1. - +
  2. + + ); })} - {!review.spans.length &&
  3. No trace parts shown
  4. } - - - ); -} - -function Reasoning({ review, phase, tokens }: { review: Review; phase: Phase; tokens: string }) { - const typing = phase.typed < review.reasoning.length; - return ( -
    -
    - - - {modelName(review.model) || "analysis model"} · reasoning - - {tokens} -
    -

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

    + + )} -

    - + +
    {span.kind || span.name} - - + {span.preview || span.name} - - +
    No trace parts were shown to the model
    ); } export function ReadingPanel({ review, phase, - tokens, verdictRef, }: { review: Review | null; phase: Phase; - tokens: string; verdictRef: Ref; }) { if (!review) { - return ( -

    - Traces appear here as the worker reviews them. -

    - ); + return

    Waiting for the first trace review.

    ; } const result = outcome(review); + const typing = phase.typed < review.reasoning.length; return ( -
    +
    +
    + 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 56902d82c07..4fcf3b1c225 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/ReviewQueue.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/ReviewQueue.tsx @@ -1,69 +1,50 @@ +import { Loader2 } from "lucide-react"; + import { cn } from "@/lib/cva.config"; -import { outcome, reviewKey } from "../../model/live"; -import type { Review } from "../../model/types"; +import { outcome, queueRows, reviewKey, type Playback } from "../../model/live"; -const BEFORE = 6; -const AFTER = 12; -const MARK = { issue: "●", clear: "✓", unknown: "?" } as const; +const LIMIT = 80; -type Row = { review: Review; state: "done" | "active" | "next" }; - -function rows(played: readonly Review[], current: Review | null, pending: readonly Review[], live: boolean): Row[] { - const tail = played.slice(live ? -BEFORE : -(BEFORE + AFTER)); - return [ - ...tail.map((review): Row => ({ review, state: "done" })), - ...(current ? [{ review: current, state: live ? "active" : "done" } as Row] : []), - ...pending.slice(0, AFTER).map((review): Row => ({ review, state: "next" })), - ]; -} - -export function ReviewQueue({ - played, - current, - pending, - live, -}: { - played: readonly Review[]; - current: Review | null; - pending: readonly Review[]; - live: boolean; -}) { +export function ReviewQueue({ playback, live }: { playback: Pick; live: boolean }) { return ( -
      - {rows(played, current, pending, live).map(({ review, state }) => { - const result = outcome(review); - return ( -
    1. - {state === "active" && ( - - )} - {state === "done" && ( - - {MARK[result]} - - )} - {state === "next" && ·} - {review.agent || review.name} - {review.trace_id.slice(0, 8)} -
    2. - ); - })} -
    + + + {queueRows(playback, LIMIT).map((review) => { + const active = live && review === playback.current; + const result = outcome(review); + return ( + + + + + + ); + })} + +
    + {active ? ( + + ) : ( + + )} + {review.agent || review.name} + {review.trace_id.slice(0, 8)} +
    ); }