feat(lens): show a now reading stage that types each trace's reasoning

This commit is contained in:
Ishaan Jaff 2026-10-03 18:09:47 -07:00
parent 69b5265a17
commit 3577f3bebf
No known key found for this signature in database
3 changed files with 132 additions and 0 deletions

View file

@ -69,6 +69,16 @@
--animate-slot-slide-in: slot-slide-in 150ms cubic-bezier(0, 0, 0.2, 1) both;
--animate-trace-drawer-in: trace-drawer-in 200ms cubic-bezier(0.25, 1, 0.5, 1) both;
--animate-trace-drawer-out: trace-drawer-out 200ms cubic-bezier(0.4, 0, 1, 1) both;
--animate-lens-shimmer: lens-shimmer 1.1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
@keyframes lens-shimmer {
from {
transform: translateX(-100%);
}
to {
transform: translateX(250%);
}
}
@keyframes trace-drawer-in {
from {

View file

@ -0,0 +1,88 @@
"use client";
import { cn } from "@/lib/cva.config";
import { durationLabel, outcome, shortVerdict } from "../../model/live";
import { laneText, typedChars, type Lane } from "../../model/stage";
import { ModelName } from "./LiveStrip";
const RED = "text-[#e5484d]";
function LaneRow({ lane, now, charMs }: { lane: Lane; now: number; charMs: number }) {
const { review } = lane;
const text = review ? laneText(review.reasoning) : "";
const typed = typedChars(lane, now, charMs);
const typing = !!review && typed < text.length;
const issue = review ? outcome(review) === "issue" : false;
return (
<li className="flex flex-col gap-1 rounded-lg bg-background/70 px-2.5 py-2 text-[12px] ring-1 ring-border/60 motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-top-1 motion-safe:duration-200">
<div className="flex items-center gap-2">
<span
aria-hidden="true"
className={cn(
"size-1.5 shrink-0 rounded-full",
!review ? "bg-foreground motion-safe:animate-pulse" : issue && !typing ? "bg-[#e5484d]" : "bg-foreground/50",
)}
/>
<span className="truncate font-medium text-foreground">{lane.agent || "trace"}</span>
<span className="truncate font-mono text-[11px] text-muted-foreground">{lane.traceId.slice(0, 8)}</span>
<span className="ml-auto shrink-0 text-[11px] tabular-nums text-muted-foreground">
{review ? durationLabel(review.duration_ms) : `reading · ${durationLabel(Math.max(0, now - lane.startedAt))}`}
</span>
</div>
{!review ? (
<div aria-hidden="true" className="h-0.5 overflow-hidden rounded-full bg-muted">
<div className="h-full w-2/5 rounded-full bg-foreground/30 motion-safe:animate-lens-shimmer" />
</div>
) : (
<>
<p className="min-h-[1.25rem] leading-relaxed text-muted-foreground">
{text.slice(0, typed)}
{typing && <span aria-hidden="true" className="ml-px inline-block h-3 w-px translate-y-0.5 bg-foreground" />}
</p>
{!typing && (
<p className={cn("truncate font-medium motion-safe:animate-in motion-safe:fade-in", issue ? RED : "text-foreground")}>
{shortVerdict(review)}
</p>
)}
</>
)}
</li>
);
}
export function NowReading({
model,
counter,
lanes,
now,
charMs,
}: {
model: string;
counter: string;
lanes: readonly Lane[];
now: number;
charMs: number;
}) {
return (
<section aria-label="Now reading" className="mb-3 flex flex-col gap-2 rounded-xl bg-muted p-2.5 ring-1 ring-border">
<header className="flex items-center justify-between gap-3 px-0.5">
<ModelName model={model} size="md" />
<span role="status" className="text-[11px] tabular-nums text-muted-foreground">
{counter}
</span>
</header>
{lanes.length ? (
<ol className="flex flex-col gap-1.5">
{lanes.map((lane) => (
<LaneRow key={lane.key} lane={lane} now={now} charMs={charMs} />
))}
</ol>
) : (
<div aria-hidden="true" className="h-0.5 overflow-hidden rounded-full bg-background/60">
<div className="h-full w-2/5 rounded-full bg-foreground/30 motion-safe:animate-lens-shimmer" />
</div>
)}
</section>
);
}

View file

@ -0,0 +1,34 @@
"use client";
import { useEffect, useState } from "react";
import type { InFlight } from "../../model/live";
import { startStage, stepStage, type Stage } from "../../model/stage";
import type { Review } from "../../model/types";
const TICK_MS = 30;
const CHAR_MS = 3;
function prefersReducedMotion(): boolean {
return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
}
export function useStage(
reviews: readonly Review[],
reading: readonly InFlight[],
running: boolean,
slots: number,
): { stage: Stage; now: number; charMs: number } {
const [charMs] = useState(() => (prefersReducedMotion() ? 0 : CHAR_MS));
const [stage, setStage] = useState(() => startStage(reviews));
const [now, setNow] = useState(Date.now);
const next = stepStage(stage, { reading, reviews, now, slots, running, charMs });
if (next !== stage) setStage(next);
const busy = running || next.lanes.length > 0;
useEffect(() => {
if (!busy) return;
const timer = window.setInterval(() => setNow(Date.now()), TICK_MS);
return () => window.clearInterval(timer);
}, [busy]);
return { stage: next, now, charMs };
}