TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { useInView, useReducedMotion } from "motion/react";4import { BarChart3, Boxes, MessageSquare, RotateCcw, Swords } from "lucide-react";5import { Badge } from "@/components/ui/badge";6import { Segmented } from "@/components/ui/segmented";7import { cn } from "@/lib/utils";8import { Section, SectionHeading } from "./section";9import { Reveal } from "./reveal";10import { DesktopFrame, PhoneFrame } from "./frames";11import { fastestIndex, useFakeStreams, type FakeStream } from "./fake-stream";12import { DesktopArenaMock, DesktopChatMock, DesktopModelsMock, DesktopUsageMock, MobileArenaMock, MobileChatMock, MobileModelsMock, MobileUsageMock } from "./product-mock";13import { ARENA_PROMPT, ARENA_REPLIES, CHAT_PROMPT, CHAT_REPLY, MOCK_MODELS } from "./mock-data";1415type Tab = "chat" | "arena" | "models" | "usage";1617const CHAT_MODEL = MOCK_MODELS[0];18const CHAT_STREAMS: FakeStream[] = [{ text: CHAT_REPLY, speed: 4, delay: 700 }];1920const ARENA_MODELS = [MOCK_MODELS[1], MOCK_MODELS[2], MOCK_MODELS[3]];21const ARENA_TEXTS = ARENA_MODELS.map((m) => ARENA_REPLIES[m.key]);22const ARENA_STREAMS: FakeStream[] = [23 { text: ARENA_TEXTS[0], speed: 3, delay: 900 },24 { text: ARENA_TEXTS[1], speed: 5, delay: 350 },25 { text: ARENA_TEXTS[2], speed: 2, delay: 1300 },26];27const ARENA_FASTEST = fastestIndex(ARENA_STREAMS);2829const TAB_OPTIONS = [30 { value: "chat" as Tab, label: "Chat", icon: <MessageSquare /> },31 { value: "arena" as Tab, label: "Arena", icon: <Swords /> },32 { value: "models" as Tab, label: "Models", icon: <Boxes /> },33 { value: "usage" as Tab, label: "Usage", icon: <BarChart3 /> },34];3536const TAB_URL: Record<Tab, string> = { chat: "polyllm.io/app/chat", arena: "polyllm.io/app/arena", models: "polyllm.io/app/models", usage: "polyllm.io/app/usage" };37const TAB_NOTE: Record<Tab, string> = {38 chat: "Real streaming with a live metadata line: tokens, time to first token, throughput and cost.",39 arena: "One prompt, three models in parallel. On phones you swipe between answers; on desktop they sit side by side.",40 models: "The catalog reads the registry: context, prices, lifecycle and capability badges. Star favorites, select two to compare.",41 usage: "Usage is computed from real request records and the provider price sheets, with savings suggestions.",42};4344/** Chat and Arena streams live here so the desktop and phone frames stay in sync. */45function DemoScreens({ tab, active, replay }: { tab: Tab; active: boolean; replay: number }) {46 const reduce = !!useReducedMotion();47 const chat = useFakeStreams(CHAT_STREAMS, active && tab === "chat", replay, reduce);48 const arena = useFakeStreams(ARENA_STREAMS, active && tab === "arena", replay, reduce);4950 const chatProps = { prompt: CHAT_PROMPT, reply: CHAT_REPLY, progress: chat.progress[0], elapsedMs: chat.elapsedFor(0), ttftMs: chat.ttftFor(0), model: CHAT_MODEL };51 const arenaProps = { prompt: ARENA_PROMPT, models: ARENA_MODELS, replies: ARENA_TEXTS, progress: arena.progress, elapsedFor: arena.elapsedFor, ttftFor: arena.ttftFor, fastest: ARENA_FASTEST, allDone: arena.allDone, winner: 1 };5253 return (54 <div className="grid items-start gap-6 xl:grid-cols-[minmax(0,1fr)_300px]">55 {/* Desktop frame — never squeezed into the phone */}56 <div className="hidden min-w-0 md:block">57 <DesktopFrame url={TAB_URL[tab]}>58 {tab === "chat" ? <DesktopChatMock {...chatProps} /> : tab === "arena" ? <DesktopArenaMock {...arenaProps} /> : tab === "models" ? <DesktopModelsMock /> : <DesktopUsageMock />}59 </DesktopFrame>60 </div>61 {/* Phone frame — the real mobile layout */}62 <div className="md:hidden xl:block">63 <PhoneFrame label={`PolyLLM ${tab} on a phone`}>64 {tab === "chat" ? <MobileChatMock {...chatProps} /> : tab === "arena" ? <MobileArenaMock {...arenaProps} /> : tab === "models" ? <MobileModelsMock /> : <MobileUsageMock />}65 </PhoneFrame>66 </div>67 </div>68 );69}7071export function InteractiveDemo() {72 const ref = React.useRef<HTMLDivElement>(null);73 const inView = useInView(ref, { once: true, margin: "0px 0px -15% 0px" });74 const [tab, setTab] = React.useState<Tab>("chat");75 const [replay, setReplay] = React.useState(0);7677 const select = (t: Tab) => {78 setTab(t);79 setReplay((r) => r + 1);80 };8182 return (83 <Section id="demo" tone="subtle">84 <SectionHeading align="center" eyebrow="Interactive demo" title="See it work before you sign up" description="A hand-built preview of the workspace on desktop and on a phone. The text is scripted and streams locally in your browser; nothing is sent to any provider." />85 <Reveal delay={0.1} y={20} className="mt-10">86 <div ref={ref} className="mx-auto w-full max-w-6xl">87 <div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">88 <Segmented<Tab> value={tab} onChange={select} options={TAB_OPTIONS} ariaLabel="Demo view" className="w-full sm:w-auto" fill />89 <div className="flex items-center justify-between gap-2 sm:justify-end">90 <Badge variant="outline" className="sm:inline-flex">91 Product mock · scripted output92 </Badge>93 {tab === "chat" || tab === "arena" ? (94 <button type="button" onClick={() => setReplay((r) => r + 1)} className="tap inline-flex h-9 items-center gap-1.5 rounded-md border border-border bg-bg-elevated px-3 text-[13px] text-fg-muted transition-colors hover:text-fg">95 <RotateCcw className="size-3.5" aria-hidden />96 Replay97 </button>98 ) : null}99 </div>100 </div>101 <DemoScreens tab={tab} active={inView} replay={replay} />102 <p className={cn("mx-auto mt-4 max-w-2xl text-center text-[13px] leading-6 text-fg-muted")} aria-live="polite">103 {TAB_NOTE[tab]}104 </p>105 </div>106 </Reveal>107 </Section>108 );109}110