"use client";
import * as React from "react";
import {
ArrowUp,
BarChart3,
Boxes,
Check,
ChevronDown,
ChevronRight,
Copy,
FolderOpen,
GitBranch,
Library,
Menu,
MessageSquare,
Mic,
PanelLeft,
Plus,
RotateCcw,
Search,
Sparkles,
SquarePen,
Star,
Swords,
Trophy,
WandSparkles,
Zap,
} from "lucide-react";
import { Logo } from "@/components/brand/logo";
import { ProviderIcon } from "@/components/brand/provider-icon";
import { Badge } from "@/components/ui/badge";
import { MOBILE_TABS } from "@/components/app/bottom-nav";
import { cn } from "@/lib/utils";
import { approxTokens } from "./fake-stream";
import { MOCK_MODELS, MOCK_PROJECTS, RECENT_CONVERSATIONS, SCOREBOARD, USAGE_BY_PROVIDER, USAGE_COST_SERIES, USAGE_DAY_LABELS, USAGE_KPIS, USAGE_SAVINGS, USAGE_SERIES, PROVIDER_LABEL, type MockModel } from "./mock-data";
/* ------------------------------------------------------------------------------------------------
* Ultra-light inline markdown for the mocks (bold, inline code, paragraphs, pipe tables).
* Not the real renderer — just enough to look like it.
* ---------------------------------------------------------------------------------------------- */
function renderInline(line: string, keyPrefix: string) {
const parts = line.split(/(\*\*[^*]+\*\*|`[^`]+`)/g).filter(Boolean);
return parts.map((p, i) => {
if (p.startsWith("**") && p.endsWith("**")) return {p.slice(2, -2)};
if (p.startsWith("`") && p.endsWith("`")) return {p.slice(1, -1)};
return {p};
});
}
function MockTable({ block, streaming }: { block: string; streaming: boolean }) {
const lines = block.split("\n").filter((l) => l.trim().startsWith("|"));
const rows = lines.filter((l) => !/^\|\s*-{2,}/.test(l.trim())).map((l) => l.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim()));
if (!rows.length) return null;
const [head, ...body] = rows;
return (
{head.map((c, i) => (
| {renderInline(c, `h${i}`)} |
))}
{body.map((r, ri) => (
{r.map((c, ci) => (
| {renderInline(c, `c${ri}-${ci}`)} |
))}
))}
);
}
export function MockMarkdown({ text, streaming, className }: { text: string; streaming: boolean; className?: string }) {
const blocks = text.split(/\n\n/);
return (
{blocks.map((b, i) => {
const last = i === blocks.length - 1;
if (b.trim().startsWith("|")) {
return (
{streaming && last ? : null}
);
}
return (
{renderInline(b, String(i))}
{streaming && last ? : null}
);
})}
);
}
/* ------------------------------------------------------------------------------------------------
* Atoms that mirror the real app
* ---------------------------------------------------------------------------------------------- */
export function ModelPill({ model, className, size = "md" }: { model: MockModel; className?: string; size?: "sm" | "md" }) {
return (
{model.name}
);
}
export function ContextIndicator({ used, total, className }: { used: string; total: string; className?: string }) {
return (
{used} / {total}
);
}
/** The metadata line shown under an assistant message in the real app. */
export function MessageMeta({ chars, elapsedMs, ttftMs, done, model, className }: { chars: number; elapsedMs: number; ttftMs: number; done: boolean; model: MockModel; className?: string }) {
const out = approxTokens(chars);
const inTok = 60 + Math.round(chars / 40);
const secs = Math.max(0.1, elapsedMs / 1000);
const tps = chars > 0 ? Math.round(out / secs) : 0;
const cost = (out / 1_000_000) * parseFloat(model.output.slice(1)) + (inTok / 1_000_000) * parseFloat(model.input.slice(1));
return (
{model.name}
{inTok} in · {out} out
{(secs + ttftMs / 1000).toFixed(1)} s
TTFT {(ttftMs / 1000).toFixed(1)} s
{tps} tok/s
≈ ${cost.toFixed(4)}
{done ? (
) : null}
);
}
/** Desktop composer as in the real app: auto-grow area, tools row, active model pill, send. */
export function ComposerMock({ text, model, streaming, compact }: { text?: string; model: MockModel; streaming?: boolean; compact?: boolean }) {
return (
{text ?? "Ask anything…"}
{text && !streaming ? : null}
Web search
{streaming ? : }
);
}
/** Phone composer: `[+] Ask anything… [mic] [send]` — one row, 16 px-equivalent text. */
export function MobileComposerMock({ text, streaming }: { text?: string; streaming?: boolean }) {
return (
{text ?? "Ask anything…"}
{streaming ? : }
);
}
/** Mirrors `components/app/bottom-nav.tsx` (same tabs, same geometry) inside the phone frame. */
export function BottomNavMock({ active }: { active: "Chat" | "Arena" | "Models" | "Usage" | "Account" }) {
return (
);
}
export function MobileHeaderMock({ center, right, title }: { center?: React.ReactNode; right?: React.ReactNode; title?: string }) {
return (
{center ?? {title}}
{right ?? (
)}
);
}
/* ------------------------------------------------------------------------------------------------
* Desktop sidebar (mirrors components/app/sidebar.tsx)
* ---------------------------------------------------------------------------------------------- */
const SIDEBAR_NAV = [
{ label: "Chat", icon: MessageSquare },
{ label: "Arena", icon: Swords },
{ label: "Models", icon: Boxes },
{ label: "Prompts", icon: WandSparkles },
{ label: "Presets", icon: Sparkles },
{ label: "Library", icon: Library },
{ label: "Usage", icon: BarChart3 },
] as const;
export function SidebarMock({ active = "Chat", activeConversation = RECENT_CONVERSATIONS[0], className }: { active?: (typeof SIDEBAR_NAV)[number]["label"]; activeConversation?: string | null; className?: string }) {
return (
);
}
/* ------------------------------------------------------------------------------------------------
* Chat — desktop and phone
* ---------------------------------------------------------------------------------------------- */
export interface ChatMockProps {
prompt: string;
reply: string;
progress: number;
elapsedMs: number;
ttftMs: number;
model: MockModel;
className?: string;
}
function AssistantTurn({ reply, progress, elapsedMs, ttftMs, model, dense }: Omit & { dense?: boolean }) {
const shown = reply.slice(0, progress);
const done = progress >= reply.length;
return (
{progress === 0 ? (
Thinking…
) : (
)}
{progress > 0 ?
: null}
);
}
export function DesktopChatMock({ prompt, reply, progress, elapsedMs, ttftMs, model, className }: ChatMockProps) {
const done = progress >= reply.length;
return (
thinking · 16K
0} compact />
);
}
/** Full-screen phone chat with header, messages, composer and bottom nav — the real mobile layout. */
export function MobileChatMock({ prompt, reply, progress, elapsedMs, ttftMs, model }: ChatMockProps) {
const done = progress >= reply.length;
return (
<>
} />
0} />
>
);
}
/* ------------------------------------------------------------------------------------------------
* Arena — desktop grid, phone swipeable single response, hero mini card
* ---------------------------------------------------------------------------------------------- */
export interface ArenaMockProps {
prompt: string;
models: MockModel[];
replies: string[];
progress: number[];
elapsedFor: (i: number) => number;
ttftFor: (i: number) => number;
fastest: number;
allDone: boolean;
/** Index the "user" votes for once everything is done (scripted). */
winner?: number;
className?: string;
}
const VOTES = ["Best", "Most accurate", "Best writing", "Best value"] as const;
function ArenaColumn({ m, text, progress, elapsedMs, ttftMs, isFastest, allDone, winner, dense }: { m: MockModel; text: string; progress: number; elapsedMs: number; ttftMs: number; isFastest: boolean; allDone: boolean; winner: boolean; dense?: boolean }) {
const shown = text.slice(0, progress);
const done = progress >= text.length;
return (
{m.name}
{done && isFastest ? (
fastest
) : !done && progress > 0 ? (
) : !done ? (
queued
) : null}
{progress === 0 ?
Waiting for first token…
:
}
{progress > 0 ?
:
—}
{VOTES.map((v, i) => (
{winner && i === 0 ? : null}
{v}
))}
);
}
export function DesktopArenaMock({ prompt, models, replies, progress, elapsedFor, ttftFor, fastest, allDone, winner = 0, className }: ArenaMockProps) {
return (
Arena
{models.length} models
Blind: off
{allDone ? (
Winner: {models[winner].name}
) : (
streaming…
)}
Prompt ·
{prompt}
{models.map((m, i) => (
))}
);
}
/** Phone Arena: sticky model tabs + a `.snap-row` carousel, one response at a time (as in the real app). */
export function MobileArenaMock({ prompt, models, replies, progress, elapsedFor, ttftFor, fastest, allDone, winner = 0 }: ArenaMockProps) {
const ref = React.useRef(null);
const [index, setIndex] = React.useState(0);
React.useEffect(() => {
const el = ref.current;
if (!el) return;
let raf = 0;
const onScroll = () => {
cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => setIndex(Math.max(0, Math.min(models.length - 1, Math.round(el.scrollLeft / (el.clientWidth || 1))))));
};
el.addEventListener("scroll", onScroll, { passive: true });
return () => {
el.removeEventListener("scroll", onScroll);
cancelAnimationFrame(raf);
};
}, [models.length]);
const go = (i: number) => ref.current?.scrollTo({ left: i * (ref.current.clientWidth || 0), behavior: "smooth" });
return (
<>
New} />
Prompt ·
{prompt}
{models.map((m, i) => (
))}
{models.map((m, i) => (
))}
{models.map((m, i) => (
))}
>
);
}
/** Small "Arena · 3 models" card layered over the hero window. */
export function ArenaMiniCard({ models, replies, progress, elapsedFor, fastest, allDone, className }: Omit) {
return (
Arena
{allDone ? "done" : "streaming"}
{models.map((m, i) => {
const pct = Math.min(100, Math.round((progress[i] / replies[i].length) * 100));
const tps = progress[i] > 0 ? Math.round(approxTokens(progress[i]) / Math.max(0.1, elapsedFor(i) / 1000)) : 0;
return (
-
{m.name}
{pct >= 100 && i === fastest ?
: null}
{tps ? `${tps} tok/s` : "—"}
);
})}
);
}
/* ------------------------------------------------------------------------------------------------
* Models catalog — desktop table, phone stacked rows
* ---------------------------------------------------------------------------------------------- */
const MODEL_CHIPS = ["All", "Favorites", "Reasoning", "Vision", "Cheap", "Long context", "New"] as const;
const FAVORITES = new Set(["anthropic/claude-opus-5", "openai/gpt-5.5", "gemini/gemini-3.8-flash"]);
const TAG_LABEL: Record[number], { label: string; variant: "accent" | "success" | "info" | "warning" | "default" }> = {
new: { label: "New", variant: "accent" },
fast: { label: "Fast", variant: "success" },
cheap: { label: "Cheap", variant: "success" },
reasoning: { label: "Reasoning", variant: "info" },
vision: { label: "Vision", variant: "default" },
coding: { label: "Coding", variant: "default" },
long: { label: "Long context", variant: "warning" },
};
function ChipsMock({ active = "All", className }: { active?: (typeof MODEL_CHIPS)[number]; className?: string }) {
return (
{MODEL_CHIPS.map((c) => (
{c}
))}
);
}
export function DesktopModelsMock({ className }: { className?: string }) {
return (
Models
{MOCK_MODELS.length * 6 - 2} models · 9 providers
cheap vision model
⌘/
| Model |
Context |
$ / M in · out |
Badges |
|
{MOCK_MODELS.slice(0, 7).map((m, i) => (
|
|
{m.context} |
{m.input} · {m.output}
|
{(m.tags ?? []).slice(0, 2).map((t) => (
{TAG_LABEL[t].label}
))}
|
|
))}
Synced 4 min ago
2 selected → Compare
);
}
export function MobileModelsMock() {
return (
<>
} />
{MOCK_MODELS.slice(0, 7).map((m) => (
-
{m.name}
{m.context} ctx · {m.input} / {m.output}
))}
>
);
}
/* ------------------------------------------------------------------------------------------------
* Usage analytics — desktop dashboard, phone stacked tiles
* ---------------------------------------------------------------------------------------------- */
function Bars({ values, className, label }: { values: number[]; className?: string; label: string }) {
const max = Math.max(...values);
return (
{values.map((v, i) => (
))}
);
}
function ProviderCostRows({ dense }: { dense?: boolean }) {
const total = USAGE_BY_PROVIDER.reduce((a, b) => a + b.cost, 0);
return (
{USAGE_BY_PROVIDER.map((row) => (
-
{PROVIDER_LABEL[row.provider]}
${row.cost.toFixed(2)}
))}
);
}
function SavingsCard({ dense }: { dense?: boolean }) {
return (
Savings opportunity
{USAGE_SAVINGS.title}
{USAGE_SAVINGS.body}
);
}
export function DesktopUsageMock({ className }: { className?: string }) {
return (
Usage
{["Today", "7d", "30d", "90d"].map((r) => (
{r}
))}
{USAGE_KPIS.map((k) => (
{k.label}
{k.value}
{k.hint}
))}
Cost per day
peak ${Math.max(...USAGE_COST_SERIES).toFixed(2)}
{USAGE_DAY_LABELS[0]}
{USAGE_DAY_LABELS[USAGE_DAY_LABELS.length - 1]}
);
}
export function MobileUsageMock() {
return (
<>
30d} />
{USAGE_KPIS.map((k) => (
))}
Requests per day
peak {Math.max(...USAGE_SERIES)}
>
);
}
/* ------------------------------------------------------------------------------------------------
* Scoreboard (feature visual)
* ---------------------------------------------------------------------------------------------- */
export function ScoreboardMock({ className }: { className?: string }) {
return (
Your scoreboard
Coding · last 30 d
{SCOREBOARD.map((r, i) => {
const pct = Math.round((r.wins / r.total) * 100);
return (
-
{i + 1}
{r.model.name}
{r.wins}/{r.total}
{pct}%
);
})}
);
}