mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
feat(lens): add a live trace results drawer with readable spans
This commit is contained in:
parent
b4ef33b7a1
commit
09ac1663b8
4 changed files with 197 additions and 178 deletions
|
|
@ -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<HTMLElement | null>, list: RefObject<HTMLElement | null>) {
|
||||
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<HTMLElement>(`[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<HTMLElement | null>;
|
||||
}) {
|
||||
const list = useRef<HTMLTableSectionElement>(null);
|
||||
useFlyIn(arrival, verdictRef, list);
|
||||
export function ConclusionsPanel({ groups, scope }: { groups: readonly Conclusion[]; scope: string }) {
|
||||
return (
|
||||
<table aria-label="Conclusions" className="w-full table-fixed border-collapse text-left">
|
||||
<tbody ref={list}>
|
||||
{groups.map((group) => (
|
||||
<tr
|
||||
key={group.checkId}
|
||||
data-check={group.checkId}
|
||||
className="border-b border-border/60 text-[12px]"
|
||||
title={group.latest}
|
||||
>
|
||||
<td className="w-6 pl-3 align-top">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"mt-3.5 block size-1.5 rounded-full",
|
||||
group.issue ? "bg-[#e5484d]" : "bg-muted-foreground/40",
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 pr-2">
|
||||
<span className="line-clamp-2 text-foreground">{group.label}</span>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{Math.round((group.count / Math.max(1, reviewed)) * 100)}% of reviewed
|
||||
</span>
|
||||
</td>
|
||||
<td className="w-12 px-3 py-2 text-right align-top font-mono tabular-nums text-foreground">
|
||||
{group.count}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!groups.length && (
|
||||
<tr className="text-[12px] text-muted-foreground">
|
||||
<td className="px-3 py-6">Nothing flagged yet.</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<ol aria-label="Conclusions" className="divide-y divide-border/60">
|
||||
{groups.map((group) => (
|
||||
<li
|
||||
key={group.checkId}
|
||||
data-check={group.checkId}
|
||||
title={group.latest}
|
||||
className="grid grid-cols-[0.75rem_minmax(0,1fr)_auto] items-start gap-2 px-4 py-2 text-[12px] motion-safe:animate-in motion-safe:fade-in"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn("mt-1.5 size-1.5 rounded-full", group.issue ? "bg-[#e5484d]" : "bg-muted-foreground/40")}
|
||||
/>
|
||||
<span className="line-clamp-2 text-foreground">{group.label}</span>
|
||||
<span className="font-mono tabular-nums text-foreground">{group.count}</span>
|
||||
</li>
|
||||
))}
|
||||
{!groups.length && <li className="px-4 py-3 text-[12px] text-muted-foreground">Nothing flagged yet.</li>}
|
||||
{groups.length > 0 && scope && <li className="px-4 py-1.5 text-[11px] text-muted-foreground">{scope}</li>}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Job["status"], { label: string; tone: StatusTone }> = {
|
||||
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<Playback, "played" | "current">;
|
||||
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 (
|
||||
<Sheet open={open} onOpenChange={(value) => !value && onClose()}>
|
||||
<SheetContent className="w-full gap-0 overflow-y-auto data-[side=right]:sm:max-w-[600px]">
|
||||
<SheetHeader className="gap-1.5 border-b">
|
||||
<SheetTitle className="pr-8 text-base">{name}</SheetTitle>
|
||||
<SheetDescription render={<div />} className="flex flex-wrap items-center gap-2">
|
||||
<StatusBadge tone={status.tone} label={status.label} />
|
||||
<ModelName model={model} />
|
||||
{!following && live && (
|
||||
<Button variant="outline" size="xs" className="ml-auto rounded-full" onClick={onFollow}>
|
||||
Back to live
|
||||
</Button>
|
||||
)}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="py-3">
|
||||
{focused ? (
|
||||
<ReadingPanel key={following ? "live" : focused.execution_id} review={focused} phase={shownPhase} />
|
||||
) : (
|
||||
<p className="px-4 py-6 text-[12px] text-muted-foreground">Waiting for the first trace review.</p>
|
||||
)}
|
||||
</div>
|
||||
<div className={SECTION_HEAD}>
|
||||
<span>Conclusions</span>
|
||||
</div>
|
||||
<ConclusionsPanel groups={groups} scope={scope} />
|
||||
<div className={SECTION_HEAD}>
|
||||
<span>Traces</span>
|
||||
<span className="font-mono tracking-normal normal-case">{job.reviewed}</span>
|
||||
</div>
|
||||
<ReviewQueue playback={playback} live={live} focused={focused} onPick={onPick} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<table aria-label="Trace" className="w-full table-fixed border-collapse text-left">
|
||||
<tbody>
|
||||
{review.spans.map((span, index) => {
|
||||
const reading = phase.span === index;
|
||||
const cited = phase.verdict && span.cited;
|
||||
return (
|
||||
<tr
|
||||
key={span.span_id}
|
||||
data-state={reading ? "reading" : cited ? "cited" : "idle"}
|
||||
className={cn(
|
||||
"h-9 border-b border-border/60 text-[12px] transition-colors duration-150 motion-reduce:transition-none",
|
||||
reading && "bg-trace-row-hover",
|
||||
)}
|
||||
>
|
||||
<td className="w-[88px] truncate px-3 font-mono text-[11px] text-muted-foreground" title={span.name}>
|
||||
{span.kind || span.name}
|
||||
</td>
|
||||
<td className={cn("truncate px-3", cited ? "text-[#e5484d]" : "text-foreground")} title={span.preview}>
|
||||
{span.preview || span.name}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{!review.spans.length && (
|
||||
<tr className="h-9 text-[12px] text-muted-foreground">
|
||||
<td className="px-3">No trace parts were shown to the model</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<ol aria-label="Trace" className="divide-y divide-border/60 border-y border-border/60">
|
||||
{review.spans.map((span, index) => {
|
||||
const reading = phase.span === index;
|
||||
const cited = phase.verdict && span.cited;
|
||||
return (
|
||||
<li
|
||||
key={span.span_id}
|
||||
data-state={reading ? "reading" : cited ? "cited" : "idle"}
|
||||
className={cn(
|
||||
"px-4 py-2 text-[12px] transition-colors duration-150 motion-reduce:transition-none",
|
||||
reading && "bg-trace-row-hover",
|
||||
cited && "shadow-[inset_2px_0_0_#e5484d]",
|
||||
)}
|
||||
>
|
||||
<p className="mb-0.5 flex gap-2 font-mono text-[11px] text-muted-foreground">
|
||||
<span>{span.kind}</span>
|
||||
<span className="truncate">{span.name}</span>
|
||||
</p>
|
||||
{spanPreviewLines(span.preview).map((line, n) => (
|
||||
<p
|
||||
key={n}
|
||||
className={cn("line-clamp-3 break-words", line.error ? "text-[#e5484d]" : "text-foreground")}
|
||||
>
|
||||
{line.label && (
|
||||
<span className="mr-1.5 font-mono text-[11px] text-muted-foreground">{line.label}</span>
|
||||
)}
|
||||
{line.text}
|
||||
</p>
|
||||
))}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
{!review.spans.length && (
|
||||
<li className="px-4 py-2 text-[12px] text-muted-foreground">No trace parts were shown to the model</li>
|
||||
)}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
||||
export function ReadingPanel({
|
||||
review,
|
||||
phase,
|
||||
verdictRef,
|
||||
}: {
|
||||
review: Review | null;
|
||||
phase: Phase;
|
||||
verdictRef: Ref<HTMLDivElement>;
|
||||
}) {
|
||||
if (!review) {
|
||||
return <p className="px-3 py-6 text-[12px] text-muted-foreground">Waiting for the first trace review.</p>;
|
||||
}
|
||||
export function ReadingPanel({ review, phase }: { review: Review; phase: Phase }) {
|
||||
const result = outcome(review);
|
||||
const typing = phase.typed < review.reasoning.length;
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex h-8 items-center justify-between gap-3 border-b border-border px-3 font-mono text-[11px] text-muted-foreground">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between gap-3 px-4 font-mono text-[11px] text-muted-foreground">
|
||||
<span className="truncate text-foreground">{review.agent || review.name}</span>
|
||||
<span className="truncate">trace {review.trace_id.slice(0, 8)}</span>
|
||||
<span className="truncate">{review.agent || review.name}</span>
|
||||
</div>
|
||||
<Spans review={review} phase={phase} />
|
||||
<section aria-label="Reasoning" className="m-3 rounded-md border bg-muted/40">
|
||||
<section aria-label="Reasoning" className="mx-4 rounded-md border bg-muted/40">
|
||||
<header className="flex h-8 items-center justify-between gap-3 border-b px-3">
|
||||
<span className="text-[10px] tracking-[0.08em] text-muted-foreground uppercase">Reasoning</span>
|
||||
<span className="truncate font-mono text-[11px] text-muted-foreground">{review.model}</span>
|
||||
</header>
|
||||
<p className="min-h-16 px-3 py-2 text-[12px] leading-relaxed whitespace-pre-wrap text-foreground">
|
||||
<p className="min-h-12 px-3 py-2 text-[12px] leading-relaxed whitespace-pre-wrap text-foreground">
|
||||
{review.reasoning ? review.reasoning.slice(0, phase.typed) : !typing && "No reasoning was returned."}
|
||||
{typing && <span aria-hidden="true" className="ml-px inline-block h-3 w-px translate-y-0.5 bg-foreground" />}
|
||||
</p>
|
||||
</section>
|
||||
<div
|
||||
ref={verdictRef}
|
||||
role="status"
|
||||
aria-label="Verdict"
|
||||
data-outcome={result}
|
||||
className={cn(
|
||||
"mx-3 mb-3 flex items-center gap-2 text-[12px] font-medium transition-opacity duration-150 motion-reduce:transition-none",
|
||||
"mx-4 flex items-center gap-2 text-[12px] font-medium transition-opacity duration-150 motion-reduce:transition-none",
|
||||
VERDICT_TONE[result],
|
||||
phase.verdict ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
<span aria-hidden="true" className={cn("size-1.5 shrink-0 rounded-full", VERDICT_DOT[result])} />
|
||||
<span className="min-w-0 truncate">{phase.verdict ? verdictLine(review) : ""}</span>
|
||||
<span className="min-w-0">{phase.verdict ? verdictLine(review) : ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<Playback, "played" | "current">; live: boolean }) {
|
||||
export function ReviewQueue({
|
||||
playback,
|
||||
live,
|
||||
focused,
|
||||
onPick,
|
||||
}: {
|
||||
playback: Pick<Playback, "played" | "current">;
|
||||
live: boolean;
|
||||
focused: Review | null;
|
||||
onPick: (review: Review) => void;
|
||||
}) {
|
||||
return (
|
||||
<table aria-label="Reviewed traces" className="w-full table-fixed border-collapse text-left">
|
||||
<tbody>
|
||||
{queueRows(playback, LIMIT).map((review) => {
|
||||
const active = live && review === playback.current;
|
||||
const result = outcome(review);
|
||||
return (
|
||||
<tr
|
||||
key={reviewKey(review)}
|
||||
data-state={active ? "active" : result}
|
||||
aria-current={active ? "step" : undefined}
|
||||
<ol aria-label="Reviewed traces" className="divide-y divide-border/60">
|
||||
{queueRows(playback, LIMIT).map((review) => {
|
||||
const reading = live && review === playback.current;
|
||||
const result = outcome(review);
|
||||
const selected = review === focused;
|
||||
return (
|
||||
<li key={reviewKey(review)} className={review === playback.current ? "motion-safe:animate-in motion-safe:fade-in" : ""}>
|
||||
<button
|
||||
type="button"
|
||||
aria-current={selected ? "true" : undefined}
|
||||
onClick={() => onPick(review)}
|
||||
className={cn(
|
||||
"h-9 border-b border-border/60 text-[12px]",
|
||||
active && "bg-trace-row-hover",
|
||||
review === playback.current && "motion-safe:animate-in motion-safe:fade-in",
|
||||
"grid h-8 w-full grid-cols-[1.25rem_minmax(0,1fr)_auto] items-center gap-2 px-4 text-left text-[12px] hover:bg-trace-row-hover",
|
||||
selected && "bg-trace-row-hover",
|
||||
)}
|
||||
>
|
||||
<td className="w-7 pl-3">
|
||||
{active ? (
|
||||
<Loader2 aria-label="Reviewing" className="size-3 text-muted-foreground motion-safe:animate-spin" />
|
||||
) : (
|
||||
<span
|
||||
aria-label={result}
|
||||
className={cn(
|
||||
"block size-1.5 rounded-full",
|
||||
result === "issue" ? "bg-[#e5484d]" : "bg-muted-foreground/40",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="truncate px-2 text-foreground">{review.agent || review.name}</td>
|
||||
<td className="w-[84px] truncate px-3 text-right font-mono text-[11px] text-muted-foreground">
|
||||
{review.trace_id.slice(0, 8)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{reading ? (
|
||||
<Loader2 aria-label="Reviewing" className="size-3 text-muted-foreground motion-safe:animate-spin" />
|
||||
) : (
|
||||
<span
|
||||
aria-label={result}
|
||||
className={cn("size-1.5 rounded-full", result === "issue" ? "bg-[#e5484d]" : "bg-muted-foreground/40")}
|
||||
/>
|
||||
)}
|
||||
<span className="truncate text-foreground">{review.agent || review.name}</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{review.trace_id.slice(0, 8)}</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue