An agent adds a signup funnel to a product analytics dashboard. The card arrives with its file, then each bar fills as the tracking call for that step is written.
registry/demos/agent-session-demo.tsx
"use client";
import { motion } from "motion/react";
import {
AgentSession,
AgentSessionTarget,
useAgentSessionEffect,
type AgentSessionStep,
} from "@/components/ui/agent-session";
const script: AgentSessionStep[] = [
{ prompt: "Add a signup funnel to the dashboard and track each step" },
{
run: "npx tern events list --match signup",
ms: 900,
output: [
"signup_viewed page /signup",
"signup_submitted form #signup-form",
"email_verified server POST /api/verify",
],
more: 4,
},
{
run: "npx tern add funnel signup --to app/dashboard",
ms: 1100,
output: [
{ text: "+ app/dashboard/signup-funnel.tsx SignupFunnel", tone: "added", effect: "funnel" },
{ text: "~ app/dashboard/layout.tsx Funnels (1)", effect: "nav" },
],
},
{
run: "npx tern track signup_viewed signup_submitted email_verified",
ms: 1200,
pace: 420,
output: [
{ text: '+ app/signup/page.tsx track("signup_viewed")', tone: "added", effect: "step-1" },
{ text: '+ app/signup/form.tsx track("signup_submitted")', tone: "added", effect: "step-2" },
{ text: '+ app/api/verify/route.ts track("email_verified")', tone: "added", effect: "step-3" },
],
},
{
say: "Added a three-step signup funnel to the dashboard. Last week 1,204 of 12,480 visitors finished, and most of the drop happens before the form is sent.",
},
];
const steps = [
{ effect: "step-1", label: "Viewed signup", count: "12,480", share: 1 },
{ effect: "step-2", label: "Sent the form", count: "2,911", share: 0.233 },
{ effect: "step-3", label: "Verified email", count: "1,204", share: 0.096 },
];
const nav = ["Overview", "Funnels", "Events", "Retention", "Settings"];
export function AgentSessionDemo() {
return (
<AgentSession
script={script}
title="web — agent"
url="localhost:3000/dashboard"
className="max-w-5xl [--agent-session-height:26rem]"
>
<div className="@container/app flex h-full text-zinc-900 dark:text-zinc-100">
<aside className="hidden w-32 shrink-0 flex-col gap-0.5 border-r border-zinc-950/[0.06] bg-zinc-50/70 p-2.5 @[24rem]/app:flex dark:border-white/[0.06] dark:bg-white/[0.02]">
<div className="mb-3 flex items-center gap-1.5 px-1.5 pt-1 text-[13px] font-semibold tracking-tight">
<svg viewBox="0 0 16 16" className="size-3.5" aria-hidden>
<path d="M2 13 8 3l6 10H2Z" fill="currentColor" />
</svg>
Tern
</div>
{nav.map((item, i) => (
<div
key={item}
className={
i === 0
? "flex items-center justify-between rounded-md bg-white px-2 py-1.5 text-xs font-medium shadow-sm ring-1 ring-zinc-950/5 dark:bg-white/10 dark:ring-white/5"
: "flex items-center justify-between rounded-md px-2 py-1.5 text-xs text-zinc-500 dark:text-zinc-400"
}
>
{item}
{item === "Funnels" && (
<AgentSessionTarget effect="nav" className="[--agent-session-ring-radius:999px]">
<span className="rounded-full bg-zinc-900 px-1.5 text-[10px] leading-4 font-medium text-white tabular-nums dark:bg-white dark:text-zinc-900">
1
</span>
</AgentSessionTarget>
)}
</div>
))}
</aside>
<main className="flex min-w-0 flex-1 flex-col gap-3 p-4">
<div className="flex items-baseline justify-between">
<h3 className="text-sm font-semibold tracking-tight">Overview</h3>
<span className="text-[11px] text-zinc-400">Last 7 days</span>
</div>
<div className="grid grid-cols-2 gap-2">
<Stat label="Visitors" value="12,480" delta="+8.2%" />
<Stat label="Signups" value="1,204" delta="+3.1%" />
</div>
<AgentSessionTarget
effect="funnel"
className="[--agent-session-ring-radius:14px]"
fallback={
<div className="flex h-full min-h-36 flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-zinc-950/15 text-center dark:border-white/15">
<p className="text-xs font-medium">No funnels yet</p>
<p className="text-[11px] text-zinc-400">Ask for one, or add it from Funnels.</p>
</div>
}
>
<div className="rounded-xl bg-white p-3 ring-1 ring-zinc-950/[0.08] shadow-sm dark:bg-white/[0.04] dark:ring-white/10">
<div className="mb-2.5 flex items-baseline justify-between">
<p className="text-xs font-semibold">Signup funnel</p>
<p className="text-[11px] text-zinc-400 tabular-nums">9.6% convert</p>
</div>
<div className="flex flex-col gap-2">
{steps.map((step) => (
<FunnelRow key={step.effect} {...step} />
))}
</div>
</div>
</AgentSessionTarget>
</main>
</div>
</AgentSession>
);
}
function Stat({ label, value, delta }: { label: string; value: string; delta: string }) {
return (
<div className="rounded-xl bg-white p-2.5 ring-1 ring-zinc-950/[0.08] dark:bg-white/[0.04] dark:ring-white/10">
<p className="text-[11px] text-zinc-500 dark:text-zinc-400">{label}</p>
<p className="mt-0.5 flex items-baseline gap-1.5">
<span className="text-base font-semibold tracking-tight tabular-nums">{value}</span>
<span className="text-[10px] font-medium text-emerald-600 dark:text-emerald-400">{delta}</span>
</p>
</div>
);
}
// Each row fills once its tracking call lands, so the bars arrive in the order the agent wrote them.
function FunnelRow({ effect, label, count, share }: (typeof steps)[number]) {
const landed = useAgentSessionEffect(effect);
return (
<AgentSessionTarget effect={effect} enter="none" className="[--agent-session-ring-radius:8px]">
<div className="flex flex-col gap-1">
<div className="flex items-baseline justify-between text-[11px]">
<span className="text-zinc-600 dark:text-zinc-300">{label}</span>
<motion.span
initial={false}
animate={{ opacity: landed ? 1 : 0.35 }}
className="font-medium tabular-nums"
>
{landed ? count : "—"}
</motion.span>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-zinc-100 dark:bg-white/[0.07]">
<motion.div
initial={false}
animate={{ transform: `scaleX(${landed ? share : 0})` }}
transition={{ type: "spring", visualDuration: 0.6, bounce: 0 }}
className="h-full origin-left rounded-full bg-zinc-900 dark:bg-zinc-100"
/>
</div>
</div>
</AgentSessionTarget>
);
}
Live preview, examples and props: https://keyframeui.com/components/agent-session#agent-session-demo