mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
feat(lens): slide one model rectangle over the traces being read
A single rounded rectangle carrying the provider logo and model wraps the real in-flight rows from job.reading. It translates and resizes over 250ms as traces finish in place. Before job.reading arrives it sits on a top slot showing the honest progress line, and when the run completes it fades out over 400ms. Rows have a fixed height and stable execution_id keys, so polls don't cause jumps or flicker.
This commit is contained in:
parent
5724190869
commit
28f70ea41d
4 changed files with 134 additions and 52 deletions
|
|
@ -10,6 +10,7 @@ import type { Job, Review } from "../../model/types";
|
|||
import { ConclusionsPanel } from "./ConclusionsPanel";
|
||||
import { ModelName } from "./LiveStrip";
|
||||
import { TraceList } from "./TraceList";
|
||||
import { useLensMode } from "./useLensMode";
|
||||
|
||||
const STATUS: Record<Job["status"], { label: string; tone: StatusTone }> = {
|
||||
queued: { label: "Queued", tone: "neutral" },
|
||||
|
|
@ -29,6 +30,7 @@ export function LiveDrawer({
|
|||
reviewed,
|
||||
reviews,
|
||||
now,
|
||||
done,
|
||||
reading,
|
||||
groups,
|
||||
scope,
|
||||
|
|
@ -42,6 +44,7 @@ export function LiveDrawer({
|
|||
reviewed: number;
|
||||
reviews: readonly Review[];
|
||||
now: string | null;
|
||||
done: string | null;
|
||||
reading: readonly InFlight[];
|
||||
groups: readonly Conclusion[];
|
||||
scope: string;
|
||||
|
|
@ -49,6 +52,8 @@ export function LiveDrawer({
|
|||
}) {
|
||||
const [group, setGroup] = useState<string | null>(null);
|
||||
const badge = STATUS[status];
|
||||
const live = status === "queued" || status === "running";
|
||||
const mode = useLensMode(live);
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={(value) => !value && onClose()}>
|
||||
<SheetContent className="flex h-full w-full flex-col gap-0 data-[side=right]:sm:max-w-[min(1200px,94vw)]">
|
||||
|
|
@ -63,13 +68,14 @@ export function LiveDrawer({
|
|||
<section aria-label="Traces" className="min-h-0 overflow-y-auto border-b px-4 py-4 md:border-r md:border-b-0">
|
||||
<div className="mb-2 flex items-baseline justify-between gap-3 px-2">
|
||||
<h2 className={PANE_TITLE}>Traces</h2>
|
||||
<span className="text-[11px] tabular-nums text-muted-foreground">
|
||||
{now ?? `${reviewed} reviewed`}
|
||||
<span className="inline-flex items-center gap-1.5 text-[11px] tabular-nums text-muted-foreground">
|
||||
{now ?? done ?? `${reviewed} reviewed`}
|
||||
{done && !now && <ModelName model={model} />}
|
||||
{reviewed > reviews.length && reviews.length ? ` · showing latest ${reviews.length}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
{reviews.length || reading.length ? (
|
||||
<TraceList reading={reading} reviews={reviews} model={model} group={group} />
|
||||
{reviews.length || reading.length || status === "running" ? (
|
||||
<TraceList reading={reading} reviews={reviews} model={model} group={group} mode={mode} nowLine={now} />
|
||||
) : (
|
||||
<div className="flex flex-col gap-1 px-2 py-6 text-[12px] text-muted-foreground">
|
||||
<p className="text-[13px] text-foreground">Waiting for the first trace…</p>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useState } from "react";
|
|||
import { modelsUsed } from "../../model/inbox";
|
||||
import {
|
||||
analysisModel,
|
||||
doneLine,
|
||||
conclusions,
|
||||
inFlight,
|
||||
issueCount,
|
||||
|
|
@ -80,6 +81,7 @@ export function LiveRun({
|
|||
reviews={reviews}
|
||||
now={job.status === "running" ? nowLine(job, reading.length) : null}
|
||||
reading={reading}
|
||||
done={job.status === "completed" ? doneLine(job) : null}
|
||||
groups={conclusions(reviews, job.settings.checks)}
|
||||
scope={job.reviewed > reviews.length ? `From the latest ${reviews.length} of ${job.reviewed} reviewed traces` : ""}
|
||||
waiting={waiting}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
|
||||
import { agoLabel } from "@/components/view_logs/TraceView/lensField";
|
||||
import { useNow } from "@/hooks/useNow";
|
||||
|
|
@ -12,6 +12,9 @@ import { ModelName } from "./LiveStrip";
|
|||
|
||||
const LIMIT = 200;
|
||||
const RED = "text-[#e5484d]";
|
||||
const ROW = "grid h-9 w-full grid-cols-[0.75rem_minmax(0,8rem)_minmax(0,1fr)_auto] items-center gap-2 px-2 text-left text-[12px]";
|
||||
|
||||
export type LensMode = "live" | "settling" | "off";
|
||||
|
||||
function Expanded({ review }: { review: Review }) {
|
||||
const verdicts = review.verdicts.length
|
||||
|
|
@ -38,17 +41,12 @@ function Expanded({ review }: { review: Review }) {
|
|||
);
|
||||
}
|
||||
|
||||
function ReadingRow({ item, model, now }: { item: InFlight; model: string; now: number }) {
|
||||
function ReadingRow({ item, now }: { item: InFlight; now: number }) {
|
||||
return (
|
||||
<div className="grid w-full grid-cols-[0.75rem_minmax(0,8rem)_minmax(0,1fr)_auto] items-center gap-2 rounded-lg bg-muted px-2 py-2 text-[12px] ring-1 ring-border transition-colors duration-300">
|
||||
<div className={ROW}>
|
||||
<span aria-label="reading" className="size-1.5 rounded-full bg-foreground motion-safe:animate-pulse" />
|
||||
<span className="truncate text-foreground">{item.agent || "trace"}</span>
|
||||
<span className="flex min-w-0 items-center gap-2 text-muted-foreground">
|
||||
<span className="inline-flex shrink-0 items-center rounded-md bg-background px-1.5 py-0.5 ring-1 ring-border">
|
||||
<ModelName model={model} />
|
||||
</span>
|
||||
<span className="truncate font-mono text-[11px]">{item.trace_id.slice(0, 8)}</span>
|
||||
</span>
|
||||
<span className="truncate font-mono text-[11px] text-muted-foreground">{item.trace_id.slice(0, 8)}</span>
|
||||
<span className="text-[11px] tabular-nums text-muted-foreground">
|
||||
reading · {durationLabel(Math.max(0, now - Date.parse(item.started_at)))}
|
||||
</span>
|
||||
|
|
@ -56,61 +54,114 @@ function ReadingRow({ item, model, now }: { item: InFlight; model: string; now:
|
|||
);
|
||||
}
|
||||
|
||||
function DoneRow({ review, now, open, onToggle }: { review: Review; now: number; open: boolean; onToggle: () => void }) {
|
||||
const result = outcome(review);
|
||||
return (
|
||||
<>
|
||||
<button type="button" aria-expanded={open} onClick={onToggle} className={cn(ROW, "rounded-lg hover:bg-muted/50")}>
|
||||
<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={cn(
|
||||
"truncate motion-safe:animate-in motion-safe:fade-in motion-safe:duration-300",
|
||||
result === "issue" ? RED : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{shortVerdict(review)}
|
||||
</span>
|
||||
<span className="text-[11px] tabular-nums text-muted-foreground">
|
||||
{durationLabel(review.duration_ms)} · {agoLabel(Date.parse(review.at), now)}
|
||||
</span>
|
||||
</button>
|
||||
{open && <Expanded review={review} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function useLensBox(target: string, layout: string) {
|
||||
const list = useRef<HTMLDivElement>(null);
|
||||
const [box, setBox] = useState<{ top: number; height: number } | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
const nodes = [...(list.current?.querySelectorAll<HTMLElement>(`[data-lens="${target}"]`) ?? [])];
|
||||
const first = nodes[0];
|
||||
const last = nodes.at(-1);
|
||||
setBox(first && last ? { top: first.offsetTop, height: last.offsetTop + last.offsetHeight - first.offsetTop } : null);
|
||||
}, [target, layout]);
|
||||
return { list, box };
|
||||
}
|
||||
|
||||
function Lens({ box, model, mode }: { box: { top: number; height: number }; model: string; mode: LensMode }) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-x-0 top-0 rounded-xl bg-muted ring-1 ring-border",
|
||||
"transition-[transform,height,opacity] duration-[250ms] ease-out motion-reduce:transition-none",
|
||||
mode === "settling" && "opacity-0 duration-[400ms]",
|
||||
)}
|
||||
style={{ transform: `translateY(${box.top}px)`, height: box.height }}
|
||||
>
|
||||
<span className="absolute -top-2.5 right-3 inline-flex items-center rounded-full bg-background px-2 py-0.5 shadow-sm ring-1 ring-border">
|
||||
<ModelName model={model} size="md" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TraceList({
|
||||
reading,
|
||||
reviews,
|
||||
model,
|
||||
group,
|
||||
mode,
|
||||
nowLine,
|
||||
}: {
|
||||
reading: readonly InFlight[];
|
||||
reviews: readonly Review[];
|
||||
model: string;
|
||||
group: string | null;
|
||||
mode: LensMode;
|
||||
nowLine: string | null;
|
||||
}) {
|
||||
const now = useNow(reading.length ? 200 : 5000);
|
||||
const [expanded, setExpanded] = useState<string | null>(null);
|
||||
const rows = liveRows(reading, reviews, LIMIT).filter((row) => row.kind === "reading" || inGroup(row.review, group));
|
||||
if (!rows.length) return <p className="px-2 py-2 text-[12px] text-muted-foreground">No traces in this group yet.</p>;
|
||||
const inFlight = rows.some((row) => row.kind === "reading");
|
||||
const target = inFlight ? "reading" : "slot";
|
||||
const layout = `${expanded}|${rows.map((row) => `${row.kind}:${row.key}`).join(",")}`;
|
||||
const { list, box } = useLensBox(target, layout);
|
||||
const showSlot = mode === "live" && !inFlight;
|
||||
return (
|
||||
<ol aria-label="Reviewed traces" className="flex flex-col">
|
||||
{rows.map((row) => {
|
||||
if (row.kind === "reading") {
|
||||
return (
|
||||
<li key={row.key} className="motion-safe:animate-in motion-safe:fade-in motion-safe:duration-150">
|
||||
<ReadingRow item={row.item} model={model} now={now} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
const { review, key } = row;
|
||||
const result = outcome(review);
|
||||
const open = expanded === key;
|
||||
return (
|
||||
<li
|
||||
key={key}
|
||||
className="motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-top-1 motion-safe:duration-150"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
onClick={() => setExpanded(open ? null : key)}
|
||||
className="grid w-full grid-cols-[0.75rem_minmax(0,8rem)_minmax(0,1fr)_auto] items-center gap-2 rounded-lg px-2 py-2 text-left text-[12px] transition-colors duration-300 hover:bg-muted/50"
|
||||
>
|
||||
<span
|
||||
aria-label={result}
|
||||
className={cn("size-1.5 rounded-full", result === "issue" ? "bg-[#e5484d]" : "bg-muted-foreground/40")}
|
||||
<div ref={list} className="relative pt-3">
|
||||
{box && mode !== "off" && <Lens box={box} model={model} mode={mode} />}
|
||||
{showSlot && nowLine && (
|
||||
<div data-lens="slot" role="status" className={cn(ROW, "relative text-muted-foreground")}>
|
||||
<span aria-hidden="true" className="size-1.5 rounded-full bg-foreground motion-safe:animate-pulse" />
|
||||
<span className="col-span-3 truncate tabular-nums">{nowLine}</span>
|
||||
</div>
|
||||
)}
|
||||
<ol aria-label="Reviewed traces" className="relative flex flex-col">
|
||||
{rows.map((row) => (
|
||||
<li key={row.key} data-lens={row.kind === "reading" ? "reading" : undefined}>
|
||||
{row.kind === "reading" ? (
|
||||
<ReadingRow item={row.item} now={now} />
|
||||
) : (
|
||||
<DoneRow
|
||||
review={row.review}
|
||||
now={now}
|
||||
open={expanded === row.key}
|
||||
onToggle={() => setExpanded(expanded === row.key ? null : row.key)}
|
||||
/>
|
||||
<span className="truncate text-foreground">{review.agent || review.name}</span>
|
||||
<span className={cn("truncate", result === "issue" ? RED : "text-muted-foreground")}>
|
||||
{shortVerdict(review)}
|
||||
</span>
|
||||
<span className="text-[11px] tabular-nums text-muted-foreground">
|
||||
{durationLabel(review.duration_ms)} · {agoLabel(Date.parse(review.at), now)}
|
||||
</span>
|
||||
</button>
|
||||
{open && <Expanded review={review} />}
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
))}
|
||||
</ol>
|
||||
{!rows.length && !showSlot && (
|
||||
<p className="px-2 py-2 text-[12px] text-muted-foreground">No traces in this group yet.</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { LensMode } from "./TraceList";
|
||||
|
||||
const SETTLE_MS = 400;
|
||||
|
||||
export function useLensMode(live: boolean): LensMode {
|
||||
const [wasLive, setWasLive] = useState(live);
|
||||
const [settled, setSettled] = useState(!live);
|
||||
if (live && !wasLive) {
|
||||
setWasLive(true);
|
||||
setSettled(false);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (live || settled) return;
|
||||
const timer = window.setTimeout(() => setSettled(true), SETTLE_MS);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [live, settled]);
|
||||
if (live) return "live";
|
||||
return settled ? "off" : "settling";
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue