mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
feat(lens): refine onboarding motion and linked examples
This commit is contained in:
parent
28dd5b512e
commit
290fc203ac
3 changed files with 295 additions and 114 deletions
108
ui/litellm-dashboard/src/components/lens/setup/GatewayFlow.tsx
Normal file
108
ui/litellm-dashboard/src/components/lens/setup/GatewayFlow.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { useId } from "react";
|
||||
import styles from "./LensIntroduction.module.css";
|
||||
|
||||
const streams = [
|
||||
{
|
||||
id: "runs",
|
||||
color: "text-sky-500 dark:text-sky-400",
|
||||
path: "M -32 66 C 24 66 32 22 96 22 S 192 86 256 86 S 368 28 422 28 C 464 28 472 54 500 54 C 558 54 584 86 646 86 S 756 22 818 22 S 924 86 986 86 S 1040 54 1056 54",
|
||||
},
|
||||
{
|
||||
id: "steps",
|
||||
color: "text-violet-500 dark:text-violet-400",
|
||||
path: "M -32 30 C 40 30 58 78 120 78 S 218 24 284 24 S 390 76 444 76 C 470 76 480 54 503 54",
|
||||
},
|
||||
{
|
||||
id: "tools",
|
||||
color: "text-amber-500 dark:text-amber-400",
|
||||
path: "M -32 54 C 26 54 68 68 128 68 S 230 38 288 38 S 396 62 448 62 C 474 62 484 54 503 54",
|
||||
},
|
||||
];
|
||||
|
||||
export function GatewayFlow() {
|
||||
const id = useId();
|
||||
return (
|
||||
<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-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-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-0.5 leading-4 text-muted-foreground">Findings to improve your agents</p>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 1000 108"
|
||||
className={`${styles.flowGraphic} mt-2 h-auto w-full`}
|
||||
focusable="false"
|
||||
>
|
||||
<defs>
|
||||
<pattern id={`${id}-field`} width="12" height="12" patternUnits="userSpaceOnUse">
|
||||
<circle cx="6" cy="6" r="1.15" className="fill-muted-foreground/15" />
|
||||
</pattern>
|
||||
<linearGradient id={`${id}-fade`}>
|
||||
<stop offset="0" stopColor="white" stopOpacity="0" />
|
||||
<stop offset="0.06" stopColor="white" />
|
||||
<stop offset="0.94" stopColor="white" />
|
||||
<stop offset="1" stopColor="white" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<mask id={`${id}-edges`}>
|
||||
<rect width="1000" height="108" fill={`url(#${id}-fade)`} />
|
||||
</mask>
|
||||
<radialGradient id={`${id}-glow`} className="text-indigo-500 dark:text-indigo-400">
|
||||
<stop stopColor="currentColor" stopOpacity="0.2" />
|
||||
<stop offset="1" stopColor="currentColor" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
<linearGradient id={`${id}-gate`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop stopColor="currentColor" className="text-sky-400" />
|
||||
<stop offset="0.5" stopColor="currentColor" className="text-indigo-500 dark:text-indigo-300" />
|
||||
<stop offset="1" stopColor="currentColor" className="text-violet-400" />
|
||||
</linearGradient>
|
||||
{streams.map((stream) => (
|
||||
<linearGradient
|
||||
key={stream.id}
|
||||
id={`${id}-${stream.id}`}
|
||||
x1="470"
|
||||
x2="530"
|
||||
y1="0"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
className={stream.color}
|
||||
>
|
||||
<stop stopColor="currentColor" />
|
||||
<stop offset="1" stopColor="currentColor" className="text-indigo-500 dark:text-indigo-400" />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<g mask={`url(#${id}-edges)`}>
|
||||
<rect width="1000" height="108" fill={`url(#${id}-field)`} />
|
||||
<ellipse cx="500" cy="54" rx="58" ry="54" fill={`url(#${id}-glow)`} className={styles.gatewayGlow} />
|
||||
{streams.map((stream) => (
|
||||
<g key={stream.id} fill="none" stroke={`url(#${id}-${stream.id})`} strokeLinecap="round">
|
||||
<path d={stream.path} strokeWidth="1" opacity="0.13" />
|
||||
<path d={stream.path} strokeWidth="2.4" className={styles.trails} />
|
||||
<path d={stream.path} strokeWidth="3.6" className={styles.packets} />
|
||||
</g>
|
||||
))}
|
||||
</g>
|
||||
<rect x="497" y="10" width="6" height="88" rx="3" className="fill-background/90" />
|
||||
<path
|
||||
d="M 490 10 H 497 V 98 H 490 M 510 10 H 503 V 98 H 510"
|
||||
fill="none"
|
||||
stroke={`url(#${id}-gate)`}
|
||||
strokeWidth="1.4"
|
||||
opacity="0.75"
|
||||
/>
|
||||
<circle cx="500" cy="54" r="4" className="fill-background stroke-indigo-400" strokeWidth="1.4" />
|
||||
<circle cx="500" cy="54" r="1.6" className="fill-indigo-500 dark:fill-indigo-300" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,18 +1,117 @@
|
|||
.flow {
|
||||
animation: gateway-flow 10s linear infinite;
|
||||
.packets {
|
||||
stroke-dasharray: 0 16;
|
||||
animation: signal-flow 14s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes gateway-flow {
|
||||
.trails {
|
||||
stroke-dasharray: 6 42;
|
||||
stroke-dashoffset: -10;
|
||||
opacity: 0.3;
|
||||
animation: signal-trails 14s linear infinite;
|
||||
}
|
||||
|
||||
.gatewayGlow {
|
||||
opacity: 0.7;
|
||||
transition: opacity 300ms ease;
|
||||
}
|
||||
|
||||
.flowGraphic:hover .gatewayGlow {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sampleCard {
|
||||
transition:
|
||||
border-color 200ms ease,
|
||||
box-shadow 200ms ease;
|
||||
}
|
||||
|
||||
.sampleCard:hover,
|
||||
.sampleCard:focus-within {
|
||||
border-color: color-mix(in oklab, var(--color-indigo-500) 22%, var(--border));
|
||||
box-shadow: 0 4px 20px -16px color-mix(in oklab, var(--color-indigo-500) 40%, transparent);
|
||||
}
|
||||
|
||||
.examples {
|
||||
--evidence-tint: color-mix(in oklab, var(--color-rose-400) 6%, transparent);
|
||||
--evidence-outline: color-mix(in oklab, var(--color-rose-400) 32%, transparent);
|
||||
}
|
||||
|
||||
.evidenceLink {
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
outline-offset: 4px;
|
||||
transition: background-color 200ms ease;
|
||||
}
|
||||
|
||||
.evidenceLink:focus-visible {
|
||||
outline: 2px solid var(--ring);
|
||||
}
|
||||
|
||||
.evidenceLink:hover {
|
||||
background: var(--evidence-tint);
|
||||
}
|
||||
|
||||
.evidenceOutput,
|
||||
.evidenceFinding,
|
||||
.evidenceBar {
|
||||
transition:
|
||||
background-color 200ms ease,
|
||||
box-shadow 200ms ease;
|
||||
}
|
||||
|
||||
.evidenceFinding {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.examples:is(
|
||||
[data-evidence-active="true"],
|
||||
:has([data-evidence-link]:hover),
|
||||
:has([data-evidence-link]:focus-visible)
|
||||
) {
|
||||
.evidenceOutput {
|
||||
box-shadow: inset 0 0 0 1px var(--evidence-outline);
|
||||
}
|
||||
|
||||
.evidenceFinding {
|
||||
background: var(--evidence-tint);
|
||||
box-shadow: 0 0 0 7px var(--evidence-tint);
|
||||
}
|
||||
|
||||
.evidenceBar {
|
||||
box-shadow: 0 0 10px 2px var(--evidence-outline);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes signal-flow {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(240px);
|
||||
stroke-dashoffset: -144;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes signal-trails {
|
||||
from {
|
||||
stroke-dashoffset: -10;
|
||||
}
|
||||
to {
|
||||
stroke-dashoffset: -154;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.flow {
|
||||
.packets,
|
||||
.trails {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.gatewayGlow,
|
||||
.sampleCard,
|
||||
.evidenceLink,
|
||||
.evidenceOutput,
|
||||
.evidenceFinding,
|
||||
.evidenceBar {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,77 +1,8 @@
|
|||
import { useId } from "react";
|
||||
import { useState } 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 % 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: swarmColors[(phase + row) % swarmColors.length],
|
||||
};
|
||||
}
|
||||
|
||||
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-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-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-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-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-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})`} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
import { GatewayFlow } from "./GatewayFlow";
|
||||
|
||||
const traceSteps = [
|
||||
{ name: "research_agent", start: 0, duration: 14.2, failed: false },
|
||||
|
|
@ -84,9 +15,14 @@ const traceSteps = [
|
|||
|
||||
const sampleAnswer = "The column store is 40% faster than the row store for your workload.";
|
||||
|
||||
function TraceExample() {
|
||||
type EvidenceProps = { highlighted: boolean; onHighlight: () => void };
|
||||
|
||||
function TraceExample({ highlighted, onHighlight }: EvidenceProps) {
|
||||
return (
|
||||
<section aria-labelledby="lens-sample-trace" className="min-w-0 rounded-xl border bg-card p-4">
|
||||
<section
|
||||
aria-labelledby="lens-sample-trace"
|
||||
className={`${styles.sampleCard} 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
|
||||
|
|
@ -94,38 +30,59 @@ function TraceExample() {
|
|||
<p className="text-xs text-muted-foreground">Sample trace · 14.2s</p>
|
||||
</div>
|
||||
<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}`}
|
||||
className="grid grid-cols-[minmax(0,1fr)_minmax(48px,1fr)_2.5rem] items-center gap-2 sm:gap-3"
|
||||
>
|
||||
<span className={`flex min-w-0 items-center gap-1 ${index > 0 ? "pl-2" : ""}`}>
|
||||
{index === 0 ? (
|
||||
<ChevronDown aria-hidden="true" className="size-3 shrink-0" />
|
||||
) : (
|
||||
<ChevronRight aria-hidden="true" className="size-3 shrink-0" />
|
||||
)}
|
||||
<span className="truncate" title={item.name}>
|
||||
{item.name}
|
||||
{traceSteps.map((item, index) => {
|
||||
const linked = item.failed || item.name === "answer";
|
||||
const rowClass =
|
||||
"grid w-full grid-cols-[minmax(0,1fr)_minmax(48px,1fr)_2.5rem] items-center gap-2 text-left sm:gap-3";
|
||||
const content = (
|
||||
<>
|
||||
<span className={`flex min-w-0 items-center gap-1 ${index > 0 ? "pl-2" : ""}`}>
|
||||
{index === 0 ? (
|
||||
<ChevronDown aria-hidden="true" className="size-3 shrink-0" />
|
||||
) : (
|
||||
<ChevronRight aria-hidden="true" className="size-3 shrink-0" />
|
||||
)}
|
||||
<span className="truncate" title={item.name}>
|
||||
{item.name}
|
||||
</span>
|
||||
{item.failed && <span className="sr-only">Failed</span>}
|
||||
</span>
|
||||
{item.failed && <span className="sr-only">Failed</span>}
|
||||
</span>
|
||||
<span aria-hidden="true" className="relative h-2.5 overflow-hidden rounded-sm bg-muted/70">
|
||||
<span
|
||||
className={`absolute inset-y-0 rounded-sm ${item.failed ? "bg-rose-400" : "bg-indigo-600 dark:bg-indigo-400"}`}
|
||||
style={{ left: `${(item.start / 14.2) * 100}%`, width: `${(item.duration / 14.2) * 100}%` }}
|
||||
/>
|
||||
</span>
|
||||
<span className="text-right tabular-nums text-muted-foreground">{item.duration}s</span>
|
||||
</li>
|
||||
))}
|
||||
<span aria-hidden="true" className="relative h-2.5 overflow-hidden rounded-sm bg-muted/70">
|
||||
<span
|
||||
className={`absolute inset-y-0 rounded-sm ${linked ? styles.evidenceBar : ""} ${item.failed ? "bg-rose-400" : "bg-indigo-600 dark:bg-indigo-400"}`}
|
||||
style={{ left: `${(item.start / 14.2) * 100}%`, width: `${(item.duration / 14.2) * 100}%` }}
|
||||
/>
|
||||
</span>
|
||||
<span className="text-right tabular-nums text-muted-foreground">{item.duration}s</span>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<li key={`${item.name}-${item.start}`}>
|
||||
{linked ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`${rowClass} ${styles.evidenceLink}`}
|
||||
data-evidence-link=""
|
||||
aria-label={`${item.name}, ${item.duration} seconds${item.failed ? ", failed" : ""}. Highlight related finding`}
|
||||
aria-controls="lens-linked-finding"
|
||||
aria-pressed={highlighted}
|
||||
onClick={onHighlight}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
) : (
|
||||
<div className={rowClass}>{content}</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
<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">
|
||||
<div id="lens-benchmark-output" className={`${styles.evidenceOutput} rounded-lg bg-muted/60 p-2.5`}>
|
||||
<p className="text-xs text-muted-foreground">run_benchmark · output</p>
|
||||
<p className="mt-1.5 text-[13px] leading-[18px]">Error: benchmark runner unavailable (503)</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-muted/60 p-2.5">
|
||||
<div id="lens-answer-output" className={`${styles.evidenceOutput} rounded-lg bg-muted/60 p-2.5`}>
|
||||
<p className="text-xs text-muted-foreground">answer · output</p>
|
||||
<p className="mt-1.5 text-[13px] leading-[18px]">“{sampleAnswer}”</p>
|
||||
</div>
|
||||
|
|
@ -134,9 +91,12 @@ function TraceExample() {
|
|||
);
|
||||
}
|
||||
|
||||
function FindingExamples() {
|
||||
function FindingExamples({ highlighted, onHighlight }: EvidenceProps) {
|
||||
return (
|
||||
<section aria-labelledby="lens-sample-findings" className="min-w-0 rounded-xl border bg-card p-4">
|
||||
<section
|
||||
aria-labelledby="lens-sample-findings"
|
||||
className={`${styles.sampleCard} 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
|
||||
|
|
@ -144,10 +104,19 @@ function FindingExamples() {
|
|||
<p className="text-xs text-muted-foreground">500 sample runs reviewed</p>
|
||||
</div>
|
||||
<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
|
||||
<article id="lens-linked-finding" className={`${styles.evidenceFinding} py-3`}>
|
||||
<h4 className="text-sm font-semibold">
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.evidenceLink} flex w-full items-start gap-2 text-left`}
|
||||
data-evidence-link=""
|
||||
aria-controls="lens-benchmark-output lens-answer-output"
|
||||
aria-pressed={highlighted}
|
||||
onClick={onHighlight}
|
||||
>
|
||||
<span aria-hidden="true" className="mt-1.5 size-2 shrink-0 rounded-full bg-rose-500" />
|
||||
Claims numbers it never measured
|
||||
</button>
|
||||
</h4>
|
||||
<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>
|
||||
|
|
@ -177,6 +146,8 @@ function FindingExamples() {
|
|||
}
|
||||
|
||||
export function LensIntroduction({ onStart, onDemo }: { onStart: () => void; onDemo?: () => void }) {
|
||||
const [highlighted, setHighlighted] = useState(false);
|
||||
const toggleEvidence = () => setHighlighted((current) => !current);
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="lens-introduction"
|
||||
|
|
@ -210,9 +181,12 @@ export function LensIntroduction({ onStart, onDemo }: { onStart: () => void; onD
|
|||
</a>
|
||||
</div>
|
||||
<GatewayFlow />
|
||||
<div className="mt-4 grid items-stretch gap-3 md:grid-cols-2">
|
||||
<TraceExample />
|
||||
<FindingExamples />
|
||||
<div
|
||||
className={`${styles.examples} mt-4 grid items-stretch gap-3 md:grid-cols-2`}
|
||||
data-evidence-active={highlighted}
|
||||
>
|
||||
<TraceExample highlighted={highlighted} onHighlight={toggleEvidence} />
|
||||
<FindingExamples highlighted={highlighted} onHighlight={toggleEvidence} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue