fix(lens): compact onboarding and animate gateway flow

This commit is contained in:
moe-berri 2026-10-03 18:27:59 -07:00
parent debe7b2343
commit 48fff86a27
4 changed files with 89 additions and 52 deletions

View file

@ -26,7 +26,8 @@ describe("Lens interactive demo", () => {
onUrlUpdate,
});
expect(await screen.findByRole("heading", { name: "The gateway that helps your agents improve" })).toBeVisible();
await user.click(screen.getByRole("button", { name: "Preview sample" }));
expect(screen.queryByRole("button", { name: "Preview sample" })).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Explore with sample data" }));
expect(await screen.findByText("Where is order #1042?")).toBeVisible();
expect(screen.getByText("You’re viewing demo data")).toBeVisible();
expect(screen.queryByRole("button", { name: "Set up tracing" })).not.toBeInTheDocument();
@ -58,8 +59,7 @@ describe("Lens interactive demo", () => {
searchParams: "?tab=investigations",
onUrlUpdate,
});
await screen.findByRole("button", { name: "Preview sample" });
await user.click(screen.getByRole("button", { name: "Preview sample" }));
await user.click(await screen.findByRole("button", { name: "Explore with sample data" }));
await user.click(await screen.findByRole("tab", { name: "Findings" }));
network.mockClear();
await user.click(await screen.findByRole("row", { name: /Repeated lookups leave customers without an answer/ }));
@ -111,7 +111,7 @@ describe("Lens interactive demo", () => {
expect(screen.queryByRole("button", { name: "Preview sample" })).not.toBeInTheDocument();
});
it("shows the header preview only for the active tab that still needs setup", async () => {
it("offers sample data in the main panel only for the active tab that needs setup", async () => {
const user = userEvent.setup();
const saved = createLensDemoData().lenses[0];
network.mockImplementation(async (input) => {
@ -122,13 +122,14 @@ describe("Lens interactive demo", () => {
return Response.json({ data: [], traces: false, requests: false });
});
renderWithProviders(<LensWorkspace accessToken="live-token" userRole="Admin" readOnly={false} />);
expect(await screen.findByRole("button", { name: "Preview sample" })).toBeVisible();
expect(await screen.findByRole("button", { name: "Explore with sample data" })).toBeVisible();
const tabs = within(screen.getByRole("tablist", { name: "Lens" }));
await user.click(tabs.getByRole("tab", { name: "Investigations" }));
expect(await screen.findByRole("row", { name: new RegExp(saved.settings.name) })).toBeVisible();
expect(screen.queryByRole("button", { name: "Preview sample" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Explore with sample data" })).not.toBeInTheDocument();
await user.click(tabs.getByRole("tab", { name: "Traces" }));
await user.click(await screen.findByRole("button", { name: "Preview sample" }));
await user.click(await screen.findByRole("button", { name: "Explore with sample data" }));
expect(await screen.findByRole("table", { name: "Agent runs" })).toBeVisible();
expect(screen.queryByRole("button", { name: "Preview sample" })).not.toBeInTheDocument();
});

View file

@ -9,7 +9,6 @@ import { lensQueries } from "../api/queries";
import { useSaveLens } from "../api/mutations";
import { useLensApi } from "../services";
import { TraceSheet } from "../investigations/TraceSheet";
import { LensPreviewButton } from "../LensPreviewButton";
import { useAnalysisKeyInfo } from "./worker/AnalysisKeyDetails";
import { WorkerDialog } from "./worker/WorkerDialog";
import { InvestigationSetupDialog } from "./InvestigationSetupDialog";
@ -65,8 +64,7 @@ export function LensGettingStarted({
};
return (
<div className="mx-auto w-full max-w-7xl space-y-10 py-5 sm:space-y-12 sm:py-7">
{onDemo && <LensPreviewButton onClick={onDemo} />}
<div className="mx-auto w-full max-w-7xl space-y-8 py-3 sm:space-y-10 sm:py-4">
<LensIntroduction onStart={start} onDemo={onDemo} />
<div className="grid items-start gap-6 xl:grid-cols-[minmax(0,1fr)_280px] xl:gap-8">
<section

View file

@ -0,0 +1,18 @@
.flow {
animation: gateway-flow 10s linear infinite;
}
@keyframes gateway-flow {
from {
transform: translateX(0);
}
to {
transform: translateX(240px);
}
}
@media (prefers-reduced-motion: reduce) {
.flow {
animation: none;
}
}

View file

@ -1,51 +1,72 @@
import { useId } from "react";
import { ArrowRight, ArrowUpRight, ChevronDown, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import styles from "./LensIntroduction.module.css";
const swarmColors = ["fill-violet-400", "fill-sky-400", "fill-amber-400", "fill-rose-400", "fill-indigo-400"];
function flowDot(index: number) {
const column = index % 100;
const row = Math.floor(index / 100);
const wave = 2.5 + 2.6 * Math.sin((column + 5) / 3.8);
const column = (index % 124) - 24;
const row = Math.floor(index / 124);
const phase = (column + 24) % 24;
const wave = 2.5 + 2.6 * Math.sin(((phase + 5) / 24) * Math.PI * 2);
if (Math.abs(row - wave) > 0.9) return null;
return {
x: column * 10 + 5,
y: row * 10 + 5,
color: column < 61 ? swarmColors[(column + row) % swarmColors.length] : "fill-indigo-600 dark:fill-indigo-400",
color: swarmColors[(phase + row) % swarmColors.length],
};
}
const flowDots = Array.from({ length: 600 }, (_, index) => flowDot(index)).filter((dot) => dot !== null);
const flowDots = Array.from({ length: 744 }, (_, index) => flowDot(index)).filter((dot) => dot !== null);
function GatewayFlow() {
const patternId = useId();
const beforeGateway = `${patternId}-before`;
const afterGateway = `${patternId}-after`;
return (
<div className="mt-10 sm:mt-12">
<div className="grid grid-cols-3 gap-3 text-xs sm:text-sm">
<div className="mt-5 sm:mt-6">
<div className="grid grid-cols-3 gap-3 text-xs">
<div>
<p className="font-semibold">Agent swarms</p>
<p className="mt-1 leading-5 text-muted-foreground">Every run, every recorded step</p>
<p className="mt-0.5 leading-4 text-muted-foreground">Every run, every recorded step</p>
</div>
<div className="text-center">
<p className="font-semibold">LiteLLM gateway</p>
<p className="mt-1 leading-5 text-muted-foreground">One place, your infrastructure</p>
<p className="mt-0.5 leading-4 text-muted-foreground">One place, your infrastructure</p>
</div>
<div className="text-right">
<p className="font-semibold">Lens</p>
<p className="mt-1 leading-5 text-muted-foreground">Findings to improve your agents</p>
<p className="mt-0.5 leading-4 text-muted-foreground">Findings to improve your agents</p>
</div>
</div>
<svg aria-hidden="true" viewBox="0 0 1000 60" className="mt-3 h-auto w-full" focusable="false">
<svg aria-hidden="true" viewBox="0 0 1000 60" className="mt-2 h-auto w-full" focusable="false">
<defs>
<pattern id={patternId} width="10" height="10" patternUnits="userSpaceOnUse">
<circle cx="5" cy="5" r="2.3" className="fill-muted-foreground/10" />
</pattern>
<clipPath id={beforeGateway}>
<rect width="609" height="60" />
</clipPath>
<clipPath id={afterGateway}>
<rect x="609" width="391" height="60" />
</clipPath>
</defs>
<rect width="1000" height="60" fill={`url(#${patternId})`} />
{flowDots.map((dot) => (
<circle key={`${dot.x}-${dot.y}`} cx={dot.x} cy={dot.y} r="2.3" className={dot.color} />
))}
<g clipPath={`url(#${beforeGateway})`}>
<g className={styles.flow}>
{flowDots.map((dot) => (
<circle key={`${dot.x}-${dot.y}`} cx={dot.x} cy={dot.y} r="2.3" className={dot.color} />
))}
</g>
</g>
<g clipPath={`url(#${afterGateway})`} className="fill-indigo-600 dark:fill-indigo-400">
<g className={styles.flow}>
{flowDots.map((dot) => (
<circle key={`${dot.x}-${dot.y}`} cx={dot.x} cy={dot.y} r="2.3" />
))}
</g>
</g>
<path d="M 615 1 H 609 V 59 H 615" fill="none" stroke="currentColor" strokeWidth="1.8" />
</svg>
</div>
@ -65,14 +86,14 @@ const sampleAnswer = "The column store is 40% faster than the row store for your
function TraceExample() {
return (
<section aria-labelledby="lens-sample-trace" className="min-w-0 rounded-2xl border bg-card p-4 sm:p-5">
<section aria-labelledby="lens-sample-trace" className="min-w-0 rounded-xl border bg-card p-4">
<div className="flex flex-wrap items-center justify-between gap-2 text-sm">
<h3 id="lens-sample-trace" className="font-semibold">
Tracing
</h3>
<p className="text-xs text-muted-foreground">Sample trace · 14.2s</p>
</div>
<ol className="mt-5 space-y-3.5 text-xs sm:text-[13px]" aria-label="Sample trace timeline">
<ol className="mt-3 space-y-1.5 text-xs leading-5 sm:text-[13px]" aria-label="Sample trace timeline">
{traceSteps.map((item, index) => (
<li
key={`${item.name}-${item.start}`}
@ -99,14 +120,14 @@ function TraceExample() {
</li>
))}
</ol>
<div className="mt-5 grid gap-3 border-t pt-4 sm:grid-cols-2">
<div className="rounded-xl bg-muted/60 p-3.5">
<div className="mt-3 grid gap-3 border-t pt-3 sm:grid-cols-2">
<div className="rounded-lg bg-muted/60 p-2.5">
<p className="text-xs text-muted-foreground">run_benchmark · output</p>
<p className="mt-2 text-[13px] leading-5">Error: benchmark runner unavailable (503)</p>
<p className="mt-1.5 text-[13px] leading-[18px]">Error: benchmark runner unavailable (503)</p>
</div>
<div className="rounded-xl bg-muted/60 p-3.5">
<div className="rounded-lg bg-muted/60 p-2.5">
<p className="text-xs text-muted-foreground">answer · output</p>
<p className="mt-2 text-[13px] leading-5">“{sampleAnswer}”</p>
<p className="mt-1.5 text-[13px] leading-[18px]">“{sampleAnswer}”</p>
</div>
</div>
</section>
@ -115,38 +136,38 @@ function TraceExample() {
function FindingExamples() {
return (
<section aria-labelledby="lens-sample-findings" className="min-w-0 rounded-2xl border bg-card p-4 sm:p-5">
<section aria-labelledby="lens-sample-findings" className="min-w-0 rounded-xl border bg-card p-4">
<div className="flex flex-wrap items-center justify-between gap-2 text-sm">
<h3 id="lens-sample-findings" className="font-semibold">
Lens findings
</h3>
<p className="text-xs text-muted-foreground">500 sample runs reviewed</p>
</div>
<div className="mt-4 divide-y border-t">
<article className="py-4">
<div className="mt-3 divide-y border-t">
<article className="py-3">
<h4 className="flex items-start gap-2 text-sm font-semibold">
<span aria-hidden="true" className="mt-1.5 size-2 shrink-0 rounded-full bg-rose-500" />
Claims numbers it never measured
</h4>
<div className="ml-4 mt-2 space-y-2 text-[13px] leading-5">
<p className="text-muted-foreground">
When the benchmark tool fails, the agent still reports a speedup in its answer.
</p>
<blockquote className="border-l-2 pl-3 italic">“{sampleAnswer}”</blockquote>
<div className="ml-4 mt-1.5 space-y-1.5 text-[13px] leading-[18px]">
<p className="text-muted-foreground">Reports a speedup even though the benchmark failed.</p>
<blockquote className="border-l-2 pl-3 italic">
“…40% faster than the row store for your workload.”
</blockquote>
<p>
<span className="text-muted-foreground">Next step: </span>Tell the agent to report the failed benchmark
instead of estimating.
<span className="text-muted-foreground">Next step: </span>Report the failed benchmark instead of
estimating.
</p>
<p className="text-xs text-muted-foreground">38 linked runs · High priority</p>
</div>
</article>
<article className="pt-4">
<article className="pt-3">
<h4 className="flex items-start gap-2 text-sm font-semibold">
<span aria-hidden="true" className="mt-1.5 size-2 shrink-0 rounded-full bg-amber-500" />
Promises to follow up, then stops
</h4>
<div className="ml-4 mt-2 space-y-2 text-[13px] leading-5 text-muted-foreground">
<p>After the order lookup times out, the support agent replies “I will check” and ends the run.</p>
<div className="ml-4 mt-1.5 space-y-1.5 text-[13px] leading-[18px] text-muted-foreground">
<p>After an order lookup times out, the agent promises to check and ends the run.</p>
<p className="text-xs">12 linked runs · Medium priority</p>
</div>
</article>
@ -159,25 +180,24 @@ export function LensIntroduction({ onStart, onDemo }: { onStart: () => void; onD
return (
<section
aria-labelledby="lens-introduction"
className="rounded-2xl border bg-card p-5 sm:p-8 xl:rounded-3xl xl:p-10"
className="rounded-2xl border bg-card p-5 sm:px-6 sm:py-5 xl:px-7 xl:py-6"
>
<h2
id="lens-introduction"
className="max-w-[800px] text-3xl leading-[1.12] font-semibold tracking-[-0.035em] sm:text-4xl xl:text-[42px]"
className="text-2xl leading-tight font-semibold tracking-[-0.03em] sm:text-[30px] xl:text-[32px]"
>
The gateway that helps your agents improve
</h2>
<p className="mt-5 max-w-[780px] text-base leading-7 text-muted-foreground">
Lens turns recorded agent runs into findings tied to the exact steps that caused them, so you can see what needs
to change without reading every trace by hand.
<p className="mt-3 max-w-[780px] text-sm leading-6 text-muted-foreground">
Turn recorded agent runs into findings linked to the exact steps, so you know what happened and what to change.
</p>
<div className="mt-7 flex flex-wrap items-center gap-3">
<div className="mt-4 flex flex-wrap items-center gap-3">
{onDemo && (
<Button size="lg" className="h-11 rounded-lg px-5" onClick={onDemo}>
<Button className="rounded-lg px-4" onClick={onDemo}>
Explore with sample data <ArrowRight aria-hidden="true" className="size-4" />
</Button>
)}
<Button size="lg" variant={onDemo ? "outline" : "default"} className="h-11 rounded-lg px-5" onClick={onStart}>
<Button variant={onDemo ? "outline" : "default"} className="rounded-lg px-4" onClick={onStart}>
Set up Lens
</Button>
<a
@ -190,7 +210,7 @@ export function LensIntroduction({ onStart, onDemo }: { onStart: () => void; onD
</a>
</div>
<GatewayFlow />
<div className="mt-7 grid items-stretch gap-4 lg:grid-cols-2">
<div className="mt-4 grid items-stretch gap-3 md:grid-cols-2">
<TraceExample />
<FindingExamples />
</div>