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%
16.8 KB

# PolyLLM 1.0 — "AI operating system" upgrade (2026-09-11)

Massive product/UX/UI/mobile-first upgrade of www.polyllm.io. Goal: a premium, app-like multi-model workspace ("One interface. Every model.") that feels native on iPhone/Android and excellent on desktop, without breaking any existing backend behaviour (auth, encrypted keys, chat history, Arena, presets, usage, exports, folders, attachments).

This document is the shared contract between the parallel workstreams. Read it fully before touching code.

# Ground rules (all workstreams)

  1. Do not break existing functionality. Reuse services in src/lib/*; extend, don't rewrite. Provider quirks stay in src/lib/ai/providers/*; components only see PolyModel capabilities (see CLAUDE.md).
  2. Ownership. Edit only the files/dirs listed for your workstream (plus new files you create there). If you need a change in someone else's file, add a small adapter in your own area and leave a // TODO(integration: <area>) comment, or put the request in docs/upgrade-notes/<your-area>.md. Exception: src/lib/client/types.ts — append-only.
  3. Schema. Tables for the upgrade live in src/db/schema-workspace.ts (already migrated: drizzle/0001_workspace_upgrade.sql). If you need more columns/tables: append to schema-workspace.ts (or create src/db/schema-<area>.ts, re-export it in src/db/index.ts), then run pnpm db:generate --name <area> and pnpm db:migrate locally. Never edit existing migration files. Never drop/rename columns.
  4. No dev server in this phase. Multiple next dev in the same tree conflict on .next. Validate with pnpm typecheck, pnpm lint, pnpm test (vitest) and, for server logic, small vitest units in tests/unit. Visual QA happens in the integration phase against a single dev server.
  5. No fake functionality. Every button works or is explicitly labelled "Coming soon" (use <ComingSoon /> badge from components/ui/misc — add it if missing). No placeholder handlers.
  6. Mobile first. Design for 375 / 390 / 393 / 430 px widths first, then tablet, then desktop. Rules:
    • Phones: bottom sheets instead of centered modals (ResponsiveDialog, BottomSheet, ActionSheet in components/ui/sheet.tsx), long-press for message/row actions (useLongPress), swipe carousels (.snap-row + useSnapCarousel), 44 px touch targets (.tap helper or explicit min-h), 16 px inputs, 100dvh/.h-app, safe areas (pb-safe, pt-safe, --sab).
    • Never window.prompt/confirm/alert — use PromptDialog / ConfirmDialog (components/common).
    • No desktop tables overflowing on phones: use stacked rows/cards below md, tables from md up.
    • No horizontal overflow, no clipped text, no fixed element blocking content. Respect prefers-reduced-motion.
    • Hover-only controls must also be reachable on touch (.hover-reveal shows them on coarse pointers).
  7. Design language (src/app/globals.css tokens): neutral warm graphite palette, iris accent, fewer borders, spatial grouping (.panel, hairlines) over boxed cards, restrained 150–300 ms motion, text-balance headlines, tabular numbers for metrics. Use Segmented/ChipRow (components/ui/segmented.tsx) for filters. Icons: lucide only.
  8. Performance. No new heavy dependencies without a note in your area doc (allowed if < ~40 KB gz and clearly justified — e.g. katex for LaTeX). Virtualize long lists (@tanstack/react-virtual is installed). Memoize streaming renders.
  9. Accessibility. Labels/aria on icon buttons, focus-visible, keyboard nav for lists/palettes, semantic elements.
  10. Commit nothing. The integrator commits. Leave the tree compiling (pnpm typecheck && pnpm lint green) when you finish.
  11. Final report. End with: files created/changed, new API routes (method, path, body → response), anything left as "Coming soon", and QA notes (what to check visually) in docs/upgrade-notes/<area>.md.

# Shared primitives (already implemented — use them)

What Where Notes
Tokens, utilities src/app/globals.css .h-app, .pb-nav, .pb-safe, .tap, .hover-reveal, .snap-row, .panel, .glass-strong, .mask-x, .marquee, coarse:/fine: variants
Hooks src/lib/client/hooks.ts useIsMobile, useIsTablet, useIsCoarsePointer, useMounted, useKeyboardInset, useLongPress, useSwipe, useLocalStorage, useDebounced, useCopy, useSnapCarousel, useStandalone
Sheets src/components/ui/sheet.tsx BottomSheet (handle, snap `content
Segmented / chips src/components/ui/segmented.tsx Segmented, ChipRow
Dialog replacements src/components/common/prompt-dialog.tsx, confirm-dialog.tsx
Shell src/components/app/shell.tsx, bottom-nav.tsx, sidebar.tsx, store.tsx Mobile bottom nav (Chat/Arena/Models/Usage/Account), drawer with edge-swipe, keyboard inset. Store adds labels, setLabel, activeProjectId, searchOpen, AUTO_MODEL_KEY.
Token/cost estimator src/lib/client/tokens.ts estimateTextTokens, estimateAttachmentTokens, estimateContext, estimateCost, formatEstimate, COST_CONFIRM_THRESHOLD_USD
Smart router src/lib/client/router.ts analyzePrompt, routeModels, explainRoute, ROUTER_MODES (pure, transparent)
Brand src/components/brand/logo.tsx, public/icon.svg, public/og.png, scripts/brand-assets.sh, src/app/manifest.ts "Prism P" mark; Wordmark
Schema src/db/schema-workspace.ts projects, project_files, prompts, user_model_labels, custom_endpoints, arena_votes, shared_arena_sessions; conversations.project_id
Cross-area stubs components/projects/sidebar-section.tsx, components/library/file-library-picker.tsx Baselines; the Projects workstream owns/finishes them. Chat imports FileLibraryPicker from that path.

# Workstreams & ownership

# A — Chat & composer (components/chat/* except model-selector.tsx, model-config.tsx, model-badges.tsx; components/markdown/*; app/app/chat/*; lib/chat/*; app/api/chat/*; app/api/attachments/*)

Mobile composer (+ sheet: upload image/document, camera, paste, from library, tools, web search, structured output, system prompt, temporary chat), auto-grow 1→6 lines, voice dictation (Web Speech API where available, hidden otherwise), active model pill above composer (opens the model picker via ModelSelector — keep its public props), attachments (drag/drop, paste, camera, chips, multiple), context indicator (43K / 200K, warnings at 80/95 %), cost estimate + confirm above COST_CONFIRM_THRESHOLD_USD with one-tap model alternatives, Smart Router AUTO mode (selectedModelKey === AUTO_MODEL_KEY → recommendation card before sending: Use / Choose another / Always auto-route), temporary chat (server: ephemeral: true in chatRequestSchema → stream without persisting; badge "Not stored in history"), message toolbar (desktop hover / mobile long-press ActionSheet: copy, retry, edit, branch, compare, change model from here, save as prompt, export, rate, more), expandable message metadata (model, provider, tokens, cost, TTFT, tok/s, reasoning level), deprecated-model warning with replacement suggestion, humanized provider errors with Retry / Switch model / Details, streaming performance (memoized blocks, stable markdown, code block Copy/Download/Wrap/Expand, LaTeX via katex + rehype-katex allowed), inline Compare with… (2–4 models; desktop side-by-side, mobile swipeable panels with sticky tabs; reuse /api/arena + /api/arena/stream to run, store as Arena session, "Continue with this model"), empty state with quick actions (Write / Code / Research / Analyze a file / Compare models), project instructions applied when activeProjectId is set (fetch /api/projects/:id — Projects workstream provides it; degrade gracefully if 404).

# B — Models (components/chat/model-selector.tsx, model-config.tsx, model-badges.tsx; app/app/models/*; new app/(marketing)/models/*, app/(marketing)/compare/*; components/models/*; lib/ai/registry/* (additive); app/api/models/* (additive); components/presets/*, app/app/presets/*; lib/models/*)

Model picker redesign (bottom sheet on phones; desktop dialog), fast search with intent parsing ("cheap vision model", "1M context", "under $1/M", "fastest gemini", "json schema"), sections (Favorites, Recent, Recommended, Fast, Best reasoning, Cheapest, Largest context, Vision, Coding, Open source, New), compact metadata rows, star favorites, custom labels (store.setLabel), AUTO entry (Smart Router) with mode selector, model badges (NEW/FAST/CHEAP/REASONING/VISION/CODING/LONG CONTEXT), lifecycle badges (New/Active/Deprecated/Retiring/Unavailable), model profile sheet (all fields; show "—" for unknown — never invent release dates or cutoffs), /app/models catalog (sortable, filters, mobile stacked rows, select → Compare), public /models and /compare/[slug] (gpt-5.5-vs-claude-sonnet-5) pages with SEO metadata reading the registry server-side (no auth), capability-aware settings grouped Generation / Reasoning / Output / Tools / Advanced with Reset and Save preset (presets apply across compatible models).

# C — Arena (components/arena/*; app/app/arena/*; lib/arena/*; app/api/arena/*; new app/share/arena/*)

Up to 4 models: desktop adaptive 2×2 / side-by-side, phone one panel at a time (.snap-row, sticky model tabs, swipe), live metrics (status, TTFT, tok/s, in/out tokens, cost), votes (Best / Most accurate / Best writing / Best coding / Best value / Fastest + custom criteria) stored in arena_votes, Arena Winner summary with comparative metrics, Blind Arena (Model A/B/C until vote; reveal), history, personal scoreboard /app/arena/scoreboard (win rate per model, filters Coding/Research/ Writing/Reasoning/Cost efficiency), exports (Markdown/JSON incl. models, parameters, metrics, ratings), public share of an Arena comparison (shared_arena_sessions, /share/arena/[id], revoke).

# D — Projects, prompt library, context library (components/projects/*, components/library/*, components/prompts/*; app/app/projects/*, app/app/library/*, app/app/prompts/*; lib/projects/*, lib/library/*, lib/prompts/*; app/api/projects/*, app/api/library/*, app/api/prompts/*)

Projects CRUD (/api/projects, /api/projects/[id] returning { project, conversations, files, prompts }), project page (instructions, preferred models, notes, files, conversations), activeProjectId selector, conversations filtered by project (add projectId to ListFilter/PATCH in lib/conversations/service.ts — small additive change allowed), file library (/api/library/files GET/POST/DELETE with estimatedTokens, /api/library/files/attach POST {fileIds} → {attachments} copying into message_attachments), prompt library with {{variables}} (parse, defaults, fill-in sheet before insert into composer via ?prompt= / router.push('/app/chat?promptId=…') and a usePromptInsert event on window), folders/tags/ favorites, keep existing prompt_presets/model_presets pages working (migrate the /app/prompts page to the new library while still listing legacy presets). Finish the two stubs listed above.

# E — Usage analytics, providers, custom endpoints (components/usage/*; app/app/usage/*; lib/usage/*; app/api/usage/*; app/app/settings/*; components/providers/*; lib/providers/*; app/api/providers/*; new lib/ai/providers/custom/*, app/api/endpoints/*)

Usage dashboard (KPIs: requests, tokens, cost, avg TTFT, tok/s, avg context, error rate; charts: cost/tokens/requests over time, cost by provider/model, requests by model, latency by model; filters Today/7d/30d/90d/Custom + provider/model/project; projections "Estimated monthly cost"; savings opportunities computed from real usage_records + pricing; mobile: stacked KPI tiles, chart cards full-width, tables → rows). Follow dataviz skill guidance (palette from tokens, accessible). Provider management redesign (status Connected/Not connected/Validation failed, last validated, models available, Test connection with latency, key rotation, remove), extensible provider list. Custom OpenAI-compatible endpoints (Ollama, LM Studio, vLLM, llama.cpp, MLX…): custom_endpoints table exists; implement adapter via createOpenAICompatAdapter with per-endpoint baseURL/headers, discovery (GET {baseUrl}/models) or manual models, models keyed custom/<endpointId>:<modelId> surfaced in /api/models, chat routing in lib/chat/service.ts resolution (coordinate via a resolveCustomEndpoint() helper you export; document the one-line hook needed in the chat service in your notes if you can't wire it without editing A's files — the integrator will wire it). Add "custom" to PROVIDER_IDS only if every switch over providers stays exhaustive-safe (grep first). Settings pages mobile-first (full-screen sections, bottom sheets).

# F — Marketing, onboarding, auth, PWA polish (components/marketing/*; app/(marketing)/* except models/compare; app/(auth)/*; components/app/onboarding.tsx; app/app/onboarding/*; app/sitemap.ts; public/* (additive))

Homepage redesign: hero "One interface. / Every model." + sub-line (Bring your own keys. Compare models. Control every parameter. Track every token.), CTAs "Start using PolyLLM" / "Try Arena", animated real product UI in the hero (no abstract art), live model strip (public GET /api/public/models summary — create it; marquee with provider marks, context, price, status), interactive demo switchable Chat/Arena/Models/Usage with simulated streaming and a phone-shaped mobile demo that mirrors the real mobile UI, features, security page /security (Browser → PolyLLM encrypted server layer → Provider; key lifecycle; AES-256-GCM, Argon2id, HttpOnly cookies, CSP, rate limits, audit logs — technically accurate only, verify in code), FAQ, footer with contact@spboucher.ai · Simon-Pierre Boucher · Hosted on MacLustr (www.maclustr.io), /contact optional. Onboarding 6 steps (account → providers → first key → validate → favorite models → first prompt; skippable; progress; swipeable on phone) as a full page /app/onboarding + first-run redirect logic in components/app/onboarding.tsx (keep the existing Onboarding export working). Auth pages polish (mobile-first). Keep all metadata/OG (already set in app/layout.tsx).

# G — Search, command palette, share/export (components/app/command-palette.tsx; new components/search/*; app/api/search/*; lib/search/*; app/share/[id]/*; lib/conversations/service.ts export/share functions (additive); app/api/conversations/[id]/actions/route.ts (additive))

Full-text search API with filter syntax model:claude project:research after:2026-08-01 before: provider: role: over titles

  • message content (Postgres ilike/to_tsvector — add a GIN index via a new migration if you use tsvector), grouped results, snippets with highlights; search UI: desktop inside ⌘K (search mode), phone full-screen sheet (store.searchOpen); universal command palette (New chat, Search, Switch model, Open Arena/Usage/Providers/Project, Upload file, Toggle theme, New temporary chat (/app/chat?temporary=1), Copy conversation URL, keyboard-first). Share: options Entire conversation / Selected messages (messageIds) with privacy warning sheet, revoke, list of active links in Settings → Data; share page redesign (mobile-first, brand). Export: Markdown, TXT, JSON, PDF (print-optimized route /share/print/[id] + window.print() is acceptable — label it "PDF (print)").
  • Desktop: sidebar (New chat, Search, Chat/Arena/Models/Prompts/Presets/Library/Usage, Projects, Pinned, Folders, Recent, account menu). Collapsible ⌘B.
  • Phone: bottom nav Chat / Arena / Models / Usage / Account; drawer (hamburger or edge-swipe) for conversations, projects, search; page headers use a compact 48 px bar with a hamburger on the left (setSidebarOpen(true)).
  • Every app page must reserve space for the bottom nav (the shell adds padding-bottom; pages use h-full min-h-0 flex-col layouts and scroll inside, never the document).

# Integration phase (integrator)

  1. Wire cross-area hooks (custom endpoint resolution in chat service, project instructions, prompt insert).
  2. pnpm typecheck && pnpm lint && pnpm test, pnpm build.
  3. Single dev server → Playwright QA at 375×812, 390×844, 393×852, 430×932 + desktop 1440 for every screen listed in the brief (login, signup, onboarding, chat, composer+keyboard, attachments, model switcher, Arena + swipe, drawer, search, projects, catalog, usage, providers, account, sheets). Fix overflow/clipping/tap targets.
  4. Migration on prod via mld hook (pnpm db:migrate), deploy mld stage . polyllm && mld deploy polyllm --node M3U96a.