TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import * as React from "react";2import Link from "next/link";3import { ArrowRight, Check, Columns2, Download, EyeOff, FileText, FolderOpen, Keyboard, Paperclip, Route, ServerCog, Smartphone, Sparkles, Star, WandSparkles } from "lucide-react";4import { ProviderIcon } from "@/components/brand/provider-icon";5import { Badge } from "@/components/ui/badge";6import { cn } from "@/lib/utils";7import { Check as CheckMark, Section, SectionHeading } from "./section";8import { Reveal } from "./reveal";9import { DesktopFrame } from "./frames";10import { DesktopModelsMock, DesktopUsageMock, ScoreboardMock } from "./product-mock";11import { MOCK_MODELS } from "./mock-data";1213/* ------------------------------------------------------------------------------------------------14 * Shared two-column feature block — spatial grouping, no boxed cards15 * ---------------------------------------------------------------------------------------------- */16function FeatureBlock({17 id,18 eyebrow,19 title,20 description,21 bullets,22 visual,23 flip = false,24 tone,25 cta,26}: {27 id?: string;28 eyebrow: string;29 title: React.ReactNode;30 description: React.ReactNode;31 bullets?: string[];32 visual: React.ReactNode;33 flip?: boolean;34 tone?: "default" | "subtle";35 cta?: { label: string; href: string };36}) {37 return (38 <Section id={id} tone={tone} className="py-14 sm:py-20">39 <div className="grid items-center gap-10 lg:grid-cols-12 lg:gap-14">40 <div className={cn("lg:col-span-5", flip && "lg:order-2")}>41 <SectionHeading eyebrow={eyebrow} title={title} description={description} />42 {bullets ? (43 <Reveal delay={0.1}>44 <ul className="mt-6 space-y-2.5">45 {bullets.map((b) => (46 <li key={b} className="flex items-start gap-2.5 text-[15px] leading-6 text-fg">47 <CheckMark className="mt-1" />48 <span>{b}</span>49 </li>50 ))}51 </ul>52 </Reveal>53 ) : null}54 {cta ? (55 <Reveal delay={0.15}>56 <Link href={cta.href} className="mt-6 inline-flex items-center gap-1.5 text-[14px] font-medium text-accent underline-offset-4 hover:underline">57 {cta.label} <ArrowRight className="size-4" aria-hidden />58 </Link>59 </Reveal>60 ) : null}61 </div>62 <Reveal delay={0.1} y={20} className={cn("min-w-0 lg:col-span-7", flip && "lg:order-1")}>63 {visual}64 </Reveal>65 </div>66 </Section>67 );68}6970/* ------------------------------------------------------------------------------------------------71 * 1. Smart Router (AUTO)72 * ---------------------------------------------------------------------------------------------- */73function RouterVisual() {74 const rec = MOCK_MODELS[2]; // Gemini 3.8 Flash75 const alt = [MOCK_MODELS[1], MOCK_MODELS[5]];76 return (77 <div className="panel mx-auto w-full max-w-xl p-4 sm:p-5">78 <div className="flex items-center gap-2 text-[12.5px] text-fg-muted">79 <span className="inline-flex h-7 items-center gap-1.5 rounded-md border border-border bg-bg-elevated px-2 text-[12.5px] font-medium text-fg">80 <Route className="size-3.5 text-accent" /> AUTO81 <span className="font-normal text-fg-subtle">· balanced</span>82 </span>83 <span className="hidden sm:inline">Smart Router picks from the models you connected.</span>84 </div>85 <p className="mt-4 rounded-2xl rounded-br-md bg-bg-muted px-3.5 py-2 text-[13.5px] leading-6">Summarize this 40-page PDF into a one-page brief for the board. Keep the numbers exact.</p>86 <div className="mt-2 flex flex-wrap gap-1.5" aria-label="Prompt analysis">87 {["document · 38K tokens", "summarization", "precision matters", "no code", "English"].map((t) => (88 <span key={t} className="rounded-full bg-bg-elevated px-2 py-0.5 font-mono text-[10.5px] text-fg-muted">89 {t}90 </span>91 ))}92 </div>93 <div className="mt-4 rounded-xl border border-accent/30 bg-bg-elevated p-3.5 shadow-xs">94 <div className="flex items-center gap-2">95 <ProviderIcon provider={rec.provider} size={16} />96 <span className="text-[14px] font-semibold">{rec.name}</span>97 <Badge variant="accent">recommended</Badge>98 <span className="ml-auto font-mono text-[11px] tabular-nums text-fg-subtle">≈ $0.014</span>99 </div>100 <p className="mt-2 text-[13px] leading-6 text-fg-muted">Long document, no reasoning needed: a 1M-context model at $0.30 / M input is 17× cheaper than Opus here and finishes in about 6 s. Vision is on for the charts.</p>101 <div className="mt-3 flex flex-wrap gap-2">102 <span className="inline-flex h-8 items-center rounded-md bg-fg px-3 text-[12.5px] font-medium text-bg">Use {rec.name}</span>103 <span className="inline-flex h-8 items-center rounded-md border border-border px-3 text-[12.5px] font-medium">Choose another</span>104 <span className="inline-flex h-8 items-center rounded-md px-2 text-[12.5px] text-fg-muted">Always auto-route</span>105 </div>106 </div>107 <ul className="mt-3 space-y-1.5 px-1 text-[12px] text-fg-muted">108 {alt.map((m, i) => (109 <li key={m.key} className="flex items-center gap-2">110 <ProviderIcon provider={m.provider} size={12} />111 <span className="text-fg">{m.name}</span>112 <span className="font-mono text-[10.5px] text-fg-subtle">{i === 0 ? "≈ $0.062 · reasoning on" : "≈ $0.024 · no vision"}</span>113 </li>114 ))}115 </ul>116 </div>117 );118}119120export function FeatureRouter() {121 return (122 <FeatureBlock123 id="features"124 eyebrow="Smart Router"125 title="Pick AUTO and let the prompt choose the model"126 description="The router reads your prompt — length, code, attachments, reasoning needs, language — and recommends one of your connected models with a one-line explanation and a cost estimate before anything is sent. Accept, choose another, or let it route every time."127 bullets={["Modes: balanced, cheapest, fastest, best quality", "Runs in your browser — the prompt never leaves until you press Send", "Transparent: every recommendation says why"]}128 visual={<RouterVisual />}129 />130 );131}132133/* ------------------------------------------------------------------------------------------------134 * 2. Arena + Blind Arena + scoreboard135 * ---------------------------------------------------------------------------------------------- */136function BlindArenaVisual() {137 return (138 <div className="panel p-4">139 <div className="flex items-center gap-2 text-[12.5px] font-semibold">140 <EyeOff className="size-4 text-fg-muted" /> Blind Arena141 <span className="ml-auto font-mono text-[10.5px] font-normal text-fg-subtle">models hidden until you vote</span>142 </div>143 <div className="mt-3 grid grid-cols-2 gap-2">144 {(["Model A", "Model B"] as const).map((label, i) => (145 <div key={label} className="rounded-lg bg-bg-elevated p-3 text-[12.5px]">146 <div className="flex items-center justify-between">147 <span className="font-medium">{label}</span>148 <span className="font-mono text-[10.5px] text-fg-subtle">{i === 0 ? "1.9 s" : "1.1 s"}</span>149 </div>150 <p className="mt-1.5 line-clamp-3 leading-5 text-fg-muted">{i === 0 ? "A retry budget caps the total number of retries across a call tree so cascading retries can't amplify load…" : "Think of a retry budget as a shared allowance: every retry spends from it, and when the pool is empty callers fail fast…"}</p>151 <span className={cn("mt-2.5 inline-flex h-7 items-center gap-1 rounded-full border px-2.5 text-[11.5px] font-medium", i === 1 ? "border-fg bg-fg text-bg" : "border-border text-fg-muted")}>152 {i === 1 ? <Check className="size-3" /> : null} Best153 </span>154 </div>155 ))}156 </div>157 <p className="mt-3 flex items-center gap-2 text-[12px] text-fg-muted">158 <Sparkles className="size-3.5 text-accent" /> Revealed: B was <span className="inline-flex items-center gap-1 font-medium text-fg"><ProviderIcon provider="gemini" size={12} /> Gemini 3.8 Flash</span> at $0.0011.159 </p>160 </div>161 );162}163164export function FeatureArena() {165 return (166 <FeatureBlock167 id="arena"168 eyebrow="Arena"169 title="Compare models side by side, then keep score"170 description="Send one prompt to up to four models at once and watch the answers stream in parallel with time to first token, throughput and cost per column. Vote — Best, Most accurate, Best writing, Best coding, Best value, Fastest — and PolyLLM builds a personal scoreboard of which model earns its price for your actual work."171 bullets={["Blind Arena hides the models until you vote", "Scoreboard filters: coding, research, writing, reasoning, cost efficiency", "Export a comparison as Markdown or JSON, or share it with a link"]}172 visual={173 <div className="grid gap-3 sm:grid-cols-2">174 <ScoreboardMock />175 <BlindArenaVisual />176 </div>177 }178 flip179 tone="subtle"180 cta={{ label: "Try the Arena", href: "/app/arena" }}181 />182 );183}184185/* ------------------------------------------------------------------------------------------------186 * 3. Catalog & compare187 * ---------------------------------------------------------------------------------------------- */188export function FeatureCatalog() {189 return (190 <FeatureBlock191 eyebrow="Model catalog"192 title="Every model your keys unlock, with its real capability sheet"193 description="PolyLLM syncs each provider's model list and merges it with a documented catalog: context window, prices, lifecycle (new, active, deprecated, retiring), and exactly which parameters the model accepts. Search by intent — “cheap vision model”, “1M context”, “under $1/M” — and open any two models in a public compare page."194 bullets={["Public /models and /compare pages, no account needed", "Badges: New, Fast, Cheap, Reasoning, Vision, Coding, Long context", "Favorites, recents and custom labels in the picker (⌘/)"]}195 visual={196 <DesktopFrame url="polyllm.io/models" className="mx-auto w-full max-w-2xl">197 <DesktopModelsMock className="min-h-0 [&_aside]:hidden" />198 </DesktopFrame>199 }200 cta={{ label: "Browse the catalog", href: "/models" }}201 />202 );203}204205/* ------------------------------------------------------------------------------------------------206 * 4. Projects, prompt library, context library207 * ---------------------------------------------------------------------------------------------- */208function WorkspaceVisual() {209 return (210 <div className="panel grid gap-px overflow-hidden p-1 sm:grid-cols-[1.1fr_1fr]">211 <div className="rounded-[14px] bg-bg-elevated p-4">212 <div className="flex items-center gap-2 text-[13px] font-semibold">213 <FolderOpen className="size-4 text-accent" /> Research214 <Badge className="ml-auto">3 models</Badge>215 </div>216 <p className="mt-3 text-[11px] font-medium uppercase tracking-[0.08em] text-fg-subtle">Instructions</p>217 <p className="mt-1 text-[12.5px] leading-5 text-fg-muted">Answer like a careful analyst. Cite sources inline. Use CAD unless told otherwise. Prefer tables for comparisons.</p>218 <p className="mt-3 text-[11px] font-medium uppercase tracking-[0.08em] text-fg-subtle">Files</p>219 <ul className="mt-1 space-y-1 text-[12.5px]">220 {[221 ["Q3-board-deck.pdf", "38K tok"],222 ["pricing-2026.csv", "4K tok"],223 ].map(([n, t]) => (224 <li key={n} className="flex items-center gap-2">225 <FileText className="size-3.5 text-fg-subtle" /> {n}226 <span className="ml-auto font-mono text-[10.5px] text-fg-subtle">{t}</span>227 </li>228 ))}229 </ul>230 <p className="mt-3 text-[11px] font-medium uppercase tracking-[0.08em] text-fg-subtle">Conversations</p>231 <ul className="mt-1 space-y-1 text-[12.5px] text-fg-muted">232 <li>Board brief, draft 2</li>233 <li>Competitor pricing scan</li>234 </ul>235 </div>236 <div className="flex flex-col gap-px">237 <div className="rounded-[14px] bg-bg-elevated p-4">238 <div className="flex items-center gap-2 text-[13px] font-semibold">239 <WandSparkles className="size-4 text-accent" /> Prompt library240 </div>241 <div className="mt-3 rounded-lg bg-bg-subtle p-3 font-mono text-[11.5px] leading-5 text-fg-muted">242 Review <span className="rounded bg-accent-soft px-1 text-accent">{"{{language}}"}</span> code for <span className="rounded bg-accent-soft px-1 text-accent">{"{{focus}}"}</span>. Return findings as a table with severity.243 </div>244 <p className="mt-2 text-[11.5px] text-fg-subtle">Variables are filled in a sheet before insertion.</p>245 </div>246 <div className="rounded-[14px] bg-bg-elevated p-4">247 <div className="flex items-center gap-2 text-[13px] font-semibold">248 <Paperclip className="size-4 text-accent" /> Context library249 </div>250 <p className="mt-2 text-[12.5px] leading-5 text-fg-muted">Upload once, attach anywhere. Every file shows its token estimate so you know what it costs before sending.</p>251 </div>252 </div>253 </div>254 );255}256257export function FeatureWorkspace() {258 return (259 <FeatureBlock260 eyebrow="Projects & libraries"261 title="Give every piece of work its own instructions, files and prompts"262 description="Projects carry system instructions, preferred models, notes and files that apply to every conversation inside them. The prompt library keeps reusable prompts with {{variables}}; the context library keeps documents you attach again and again."263 bullets={["Conversations filtered by project, folders, pins and tags", "Prompt variables with defaults and a fill-in sheet", "Token estimates on every file before you attach it"]}264 visual={<WorkspaceVisual />}265 flip266 tone="subtle"267 />268 );269}270271/* ------------------------------------------------------------------------------------------------272 * 5. Usage analytics with savings273 * ---------------------------------------------------------------------------------------------- */274export function FeatureAnalytics() {275 return (276 <FeatureBlock277 eyebrow="Usage analytics"278 title="Know what every conversation costs — and where to save"279 description="Every request records input and output tokens, the cost from the provider's current price sheet, time to first token and throughput. Filter by period, provider, model or project, see the projected monthly cost, and get savings opportunities computed from your own usage."280 bullets={["KPIs: requests, tokens, cost, avg TTFT, tok/s, error rate", "Charts by day, by provider and by model", "Cost estimate before you send; confirmation above a threshold"]}281 visual={282 <DesktopFrame url="polyllm.io/app/usage" className="mx-auto w-full max-w-2xl">283 <DesktopUsageMock className="min-h-0 [&_aside]:hidden" />284 </DesktopFrame>285 }286 />287 );288}289290/* ------------------------------------------------------------------------------------------------291 * 6. Custom endpoints for local models292 * ---------------------------------------------------------------------------------------------- */293const ENDPOINTS = [294 { name: "Ollama (Mac Studio)", url: "http://m3u96a.local:11434/v1", models: ["qwen3.6:32b", "gemma-4-27b", "llama-4-scout"], status: "Connected" },295 { name: "LM Studio", url: "http://127.0.0.1:1234/v1", models: ["mistral-small-4"], status: "Connected" },296 { name: "vLLM · lab GPU", url: "https://vllm.lab.example/v1", models: ["deepseek-v4-flash"], status: "Discovering…" },297];298299function EndpointsVisual() {300 return (301 <div className="panel p-2">302 <ul className="divide-y divide-hairline rounded-[14px] bg-bg-elevated">303 {ENDPOINTS.map((e, i) => (304 <li key={e.name} className="flex items-start gap-3 p-3.5 text-[12.5px]">305 <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-bg-subtle">306 <ServerCog className="size-4 text-fg-muted" />307 </span>308 <div className="min-w-0 flex-1">309 <div className="flex items-center gap-2">310 <span className="font-medium">{e.name}</span>311 <Badge variant={i === 2 ? "warning" : "success"}>{e.status}</Badge>312 </div>313 <p className="mt-0.5 truncate font-mono text-[11px] text-fg-subtle">{e.url}</p>314 <div className="mt-2 flex flex-wrap gap-1">315 {e.models.map((m) => (316 <span key={m} className="rounded-md bg-bg-subtle px-1.5 py-0.5 font-mono text-[10.5px] text-fg-muted">317 custom/{m}318 </span>319 ))}320 </div>321 </div>322 </li>323 ))}324 </ul>325 <p className="px-2 pb-1 pt-2.5 text-[11.5px] text-fg-subtle">Models are discovered from GET {"{baseUrl}"}/models or entered by hand, then appear in the picker and the Arena like any other model.</p>326 </div>327 );328}329330export function FeatureEndpoints() {331 return (332 <FeatureBlock333 eyebrow="Custom endpoints"334 title="Bring your local models too"335 description="Point PolyLLM at any OpenAI-compatible endpoint — Ollama, LM Studio, vLLM, llama.cpp, MLX — with its own base URL and headers. Discovered models sit next to the cloud ones in the picker, the Arena and the usage dashboard."336 bullets={["Per-endpoint base URL, headers and manual model list", "Compare a local model with a frontier model in the same Arena", "Zero-cost pricing tracked like everything else"]}337 visual={<EndpointsVisual />}338 flip339 tone="subtle"340 />341 );342}343344/* ------------------------------------------------------------------------------------------------345 * 7. Also: mobile PWA, keyboard-first, exports346 * ---------------------------------------------------------------------------------------------- */347const ALSO = [348 { icon: <Smartphone />, title: "Designed for phones first", body: "Bottom navigation, swipeable Arena, bottom sheets, a composer that stays above the keyboard. Install it to your home screen and it behaves like a native app." },349 { icon: <Keyboard />, title: "Keyboard-first on desktop", body: "⌘K command palette and search, ⌘N new chat, ⌘/ model picker, ⌘Enter send, Esc stop, ⌘B sidebar. Everything reachable without a mouse." },350 { icon: <Download />, title: "Your data, portable", body: "Export any conversation as Markdown, TXT, JSON or PDF, export your whole account in one file, share a conversation or an Arena comparison with a revocable link." },351 { icon: <Columns2 />, title: "Compare inside a chat", body: "Pick “Compare with…” on any message to run it through two to four models and continue with the one you prefer." },352 { icon: <Star />, title: "Presets that travel", body: "Save temperature, reasoning effort, output format and tools as a preset and apply it to any compatible model." },353 { icon: <EyeOff />, title: "Temporary chats", body: "A chat you never want stored: streamed, shown, gone. Marked “Not stored in history”." },354];355356export function FeatureAlso() {357 return (358 <Section className="py-14 sm:py-20">359 <SectionHeading eyebrow="And more" title="The details that make it feel finished" />360 <ul className="mt-10 grid gap-x-8 gap-y-8 sm:grid-cols-2 lg:grid-cols-3">361 {ALSO.map((item, i) => (362 <Reveal key={item.title} as="li" delay={0.04 * i} className="flex gap-3.5">363 <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-accent-soft text-accent [&_svg]:size-4.5">{item.icon}</span>364 <div className="min-w-0">365 <h3 className="text-[15px] font-semibold tracking-tight">{item.title}</h3>366 <p className="mt-1 text-[13.5px] leading-6 text-fg-muted">{item.body}</p>367 </div>368 </Reveal>369 ))}370 </ul>371 </Section>372 );373}374