"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) => ( ))} {body.map((r, ri) => ( {r.map((c, ci) => ( ))} ))}
{renderInline(c, `h${i}`)}
{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

{prompt}

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 ( <> } />

{prompt}

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 ⌘/
{MOCK_MODELS.slice(0, 7).map((m, i) => ( ))}
Model Context $ / M in · out Badges

{m.name}

{m.key}

{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]}

Cost by provider

); } export function MobileUsageMock() { return ( <> 30d} />
{USAGE_KPIS.map((k) => (

{k.label}

{k.value}

))}

Requests per day

peak {Math.max(...USAGE_SERIES)}

Cost by provider

); } /* ------------------------------------------------------------------------------------------------ * 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}%
  • ); })}
); }