A multi-page chat studio for OpenRouter, OpenAI, Anthropic, Google Gemini, Mistral, Cohere and Groq — compare up to three LLMs side by side, in one session.
Overview · Features · Getting started · Privacy & legal · Development · Resources
Read this in other languages:English · Français · Español · Deutsch · Italiano · Português · 汉语 · 日本語 · 한국어 · Русский
Converser avec l'intelligence. A quiet, editorial interface for interrogating several language models in parallel, comparing their voices, and composing the right answer.
PolyChat AI is a single-page web app that talks to seven AI providers — OpenRouter, OpenAI, Anthropic, Google Gemini, Mistral, Cohere and Groq — each configured independently with your own API key (BYOK). Its signature feature is multi-page composition: open up to three chat columns side-by-side inside a single session, pick a different provider and model in each, and stream their answers in parallel — perfect for benchmarking, prompt iteration, or simply getting a second opinion.
Everything lives in your browser. No accounts, no server, no telemetry: sessions, settings and consent are stored in localStorage, and your API key never leaves the device.
| Interface | French-language UI, editorial layout (Fraunces · IBM Plex · JetBrains Mono) |
| Providers | OpenRouter, OpenAI, Anthropic, Google Gemini, Mistral, Cohere, Groq — configured independently, mixed per column |
| Composition | 1, 2 or 3 parallel chat columns per session, model per column |
| Streaming | Live token streaming with per-window stop and regeneration |
| Markdown | react-markdown + remark-gfm (tables, task lists, code blocks) |
| Theming | Light / dark, persisted across sessions |
| Storage | localStorage only — no backend, no cookies, no remote DB |
| Legal | RGPD/CNIL consent gate on first load, French legal documents in-app |
- Multi-page composition — open up to three chat columns inside one session. Broadcast a single message to all columns, or send a targeted one to the focused window.
- Seven providers, one interface — OpenRouter, OpenAI, Anthropic, Google Gemini, Mistral, Cohere and Groq, each with its own key and default model. Six of them relay through a same-origin proxy (
api/proxy.ts) to work around browser CORS; OpenRouter still calls the browser directly, unchanged. - Streaming with control —
AbortController-based cancellation per window, regenerate any assistant message with one click, stop everything at once. - Session history — sidebar grouped by Aujourd'hui / Hier / date, with inline search, rename and delete. Empty sessions are pruned automatically.
- Model picker — searchable dropdown sourced live from
/api/v1/models. Set per-window. - Generation controls — system prompt, temperature and max-tokens in Settings, applied to every request.
- Markdown rendering — full GFM in the assistant messages, including tables, task lists and code fences.
- Theme toggle — light and dark variants, persisted, pre-paint script in
index.htmlavoids the flash on reload. - Keyboard-friendly — focus traps in modals,
Escto dismiss,Tabcycling, skip-link to main content, ARIA roles on the column switcher. - Data sovereignty — export everything (settings, sessions, consent) as a single JSON file, or wipe all three stores in one click.
- Node.js 20 or newer
- An API key from at least one supported provider — OpenRouter, OpenAI, Anthropic, Google Gemini, Mistral, Cohere or Groq
git clone https://github.com/Teeflo/PolyChat-AI.git
cd PolyChat-AI
npm install
npm run devThe dev server starts on http://localhost:5173. Open it, click Configurer la clé in the empty state, pick a provider tab, paste its key, and you're in.
npm run build # tsc -b && vite build — type-check + bundle
npm run preview # serve dist/ locallyThe build script is the verification gate: it runs tsc -b (which catches type errors) before bundling. There is no test runner in this project — see Development for the full command list.
All configuration happens in Settings (Ctrl/⌘ + K) and is persisted in localStorage["polychat-settings"].
| Field | Description |
|---|---|
| Fournisseur | One tab per provider (OpenRouter, OpenAI, Anthropic, Google Gemini, Mistral, Cohere, Groq) — each with its own key, help link and default model, stored XOR-obfuscated in localStorage (see Security below). |
| Fournisseur actif | Which configured provider is used to create brand-new sessions; existing windows keep whichever provider they were created with. |
| Modèle | Per-window model picker, grouped by provider, sourced live from every configured provider's model list. |
| Thème | Clair / Sombre. |
| Prompt système | Prepended to every request in the session. |
| Température | Sampling temperature, 0.0 – 2.0. |
| Longueur max | Max tokens per response (Court → Très long presets). |
Tip
The very first launch shows a two-step empty state: configure your key, then choose a model. The settings modal also lets you open the privacy panel to manage consent at any time.
This is a French-language application and respects RGPD / CNIL guidelines by design.
- First-load consent gate — the app blocks until you accept the current legal version. The version is tracked in
src/hooks/useLegal.ts(CURRENT_LEGAL_VERSION); bump it whenever the legal text changes meaningfully. - Google Fonts are consent-gated — they are not declared in
index.css. They are injected bysrc/services/fontLoader.tsonly after you grantfontsConsentin the privacy modal. The pre-paint script inindex.htmlreadspolychat-legalto setdata-fontsbefore the first paint, so there is no flash. - No tracking, no cookies, no backend — only three
localStoragekeys are used:polychat-settings,polychat_history,polychat-legal. - Data export —
exportAllUserData()insrc/services/dataExport.tsbundles settings, sessions and consent into a downloadable JSON. - Full wipe —
clearAllUserData()removes all three keys. Accessible from the privacy modal. - In-app legal documents — Privacy, Terms, Cookies, AI notices, Legal notices are rendered from
src/legal/documents.tsand reachable from the sidebar footer and modals.
Important
The polychat-settings key holds every provider's API key XOR-obfuscated via src/services/crypto.ts. This is obfuscation, not encryption — the key and the obfuscation function both ship in the JS bundle. Don't rely on it against a determined local attacker. For the six non-OpenRouter providers, requests are relayed through api/proxy.ts (a same-origin Vercel Edge Function, mirrored in dev by a Vite middleware) to work around browser CORS — the proxy validates the target provider against a hardcoded server-side allow-list, forwards your key on each request, and never logs or stores it. The root .env file is leftover and not read by the app; the settings modal is the only source of truth for the keys.
npm run dev # Vite dev server with HMR
npm run build # tsc -b + vite build (type-check gate)
npm run lint # ESLint flat config (React Hooks + react-refresh)
npm run format # Prettier — 2 spaces, single quotes, 100 cols
npm run preview # Serve the production build locallyThere is no test runner in this repository — vitest and jest are not installed. Treat npm run build as the green light: if tsc -b passes, the app is type-safe.
Note
The hand-written CSS in src/index.css (~71 KB) is the only styling layer. The tailwindcss and @tailwindcss/postcss packages listed in package.json are not used and have no PostCSS config. Do not introduce Tailwind without explicit owner approval.
src/
├── components/ # Flat — one file per component, PascalCase
│ ├── ChatColumn.tsx # One of up to 3 parallel chat columns per session
│ ├── ChatInput.tsx
│ ├── MessagesArea.tsx # react-markdown + remark-gfm rendering
│ ├── ModelSelector.tsx # Searchable, grouped by provider
│ ├── SettingsModal.tsx # One tab per provider
│ ├── Sidebar.tsx # Session list grouped by date
│ ├── TopBar.tsx
│ ├── ConsentGate.tsx # RGPD/CNIL first-load gate
│ ├── LegalModal.tsx
│ ├── PrivacyModal.tsx
│ ├── LegalFooter.tsx
│ └── FAQModal.tsx
├── hooks/ # Zustand stores
│ ├── useChatStore.ts # sessions[], activeSessionId, focusedWindowId
│ ├── useSettings.ts # providers{}, activeProvider, theme, systemPrompt…
│ └── useLegal.ts # legalAccepted, fontsConsent, version
├── services/
│ ├── openRouter.ts # OpenRouter client — unchanged, still a direct browser fetch
│ ├── providers/ # Common adapter interface + one implementation per provider
│ │ ├── types.ts # ProviderAdapter, StreamChatArgs
│ │ ├── registry.ts # getProvider(id), ALL_PROVIDERS
│ │ ├── openaiCompatible.ts # Shared factory: OpenAI, Mistral, Groq
│ │ ├── anthropic.ts, google.ts, cohere.ts, openrouter.ts
│ │ └── proxyClient.ts, sse.ts, errors.ts # Proxy fetch, SSE parsing, error normalization
│ ├── localStorage.ts # Save/load sessions with migration
│ ├── crypto.ts # XOR obfuscation helpers
│ ├── fontLoader.ts # Injects Google Fonts only after consent
│ └── dataExport.ts # exportAllUserData, clearAllUserData
├── legal/
│ └── documents.ts # All French legal text
└── types/index.ts # Message, Model, ProviderConfig, Settings, PageWindow, ChatSession
api/
├── proxy.ts # Vercel Edge Function entry point
└── _proxyHandler.ts # Shared allow-listed relay logic (also driven by the Vite dev middleware)
- OpenRouter documentation — the original API PolyChat talks to
- OpenAI, Anthropic, Google Gemini, Mistral, Cohere and Groq documentation — the six other providers PolyChat can talk to
- React 19 release notes — concurrent features used for streaming
- Vite guide — dev server and build
- Zustand persist middleware — the three localStorage stores
- CNIL — Guide RGPD développeurs — privacy-by-design reference
- In-app FAQ — open the sidebar and look for the help link; the FAQ modal routes to settings, legal documents and the privacy panel in one click.
- Bug reports — open an issue on the repository with reproduction steps and the output of
npm run buildandnpm run lint. - Security disclosures — please use a private channel rather than filing a public issue for anything marked security in Privacy & legal.