SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
19.9 KB · 374 lines tsx
Raw Blame History
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