SPB Git

spb/zyquo-cloud-web

Public MIT

Zyquo Cloud Web — every cloud model, one beautiful chat, entirely in your browser.

phase0: provider reuse study, CORS matrix (empirical + documented), native UX reference

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Jul 31, 2026) Browse files

7 changed files +932 −0

added .gitignore +18 −0
@@ -0,0 +1,18 @@
1+#
2+# .gitignore
3+# Zyquo Cloud Web
4+#
5+# Author: Simon-Pierre Boucher
6+# Mail: contact@spboucher.ai
7+#
8+
9+node_modules/
10+dist/
11+dist-ssr/
12+coverage/
13+*.local
14+.env
15+.env.*
16+.keys.local.json
17+.DS_Store
18+*.log
added CLAUDE.md +272 −0
@@ -0,0 +1,272 @@
1+# CLAUDE.md — Zyquo Cloud Web
2+
3+## Project Identity
4+
5+**Zyquo Cloud Web** is the **browser edition of Zyquo Cloud**: a legendary, fast, beautiful **React** web app that delivers the same multi-provider AI chat experience as the native macOS **Zyquo Cloud** — but running entirely in the browser, with **no authentication, no accounts, and no backend**. Everything lives on the user's device: the user enters their own provider API keys, which are stored in **`localStorage`**, and their **conversation history is also stored locally** (localStorage / IndexedDB). There is no server, no login, no user database — open the page, add your keys, start chatting.
6+
7+It fronts **the same 12 providers and the exact same models as native Zyquo Cloud**, with streaming and non-streaming chat, model switching, Markdown + code rendering, and the family's premium design language. Because there is no backend, API calls go **directly from the browser to each provider's API** using the user's keys.
8+
9+⚠️ **Reality of a keyless, backend-less browser app (design around this honestly):**
10+- API keys in `localStorage` are **not strongly secure** — any script running on the page (or someone with device access) can read them. Be transparent about this in the UI (a clear one-time notice), keep the app dependency-light and CSP-hardened to reduce XSS risk, and never transmit keys anywhere except directly to the chosen provider. This is a deliberate "bring-your-own-key, local-only" model, not a secure vault.
11+- **CORS:** some providers' APIs do not allow direct browser (cross-origin) calls. During Phase 0 you MUST determine, per provider, whether direct browser calls work, and document the reality + the optional fallback (a user-supplied proxy URL / the option to run Zyquo Router locally and point the web app at it). Do not pretend all 12 work browser-side without checking.
12+
13+**Naming conventions (use consistently everywhere):**
14+- Display name / product name: `Zyquo Cloud Web`
15+- App title / PWA name: `Zyquo Cloud`
16+- Deployment domain: **`www.zyquo.cloud`** (canonical, HTTPS; redirect apex `zyquo.cloud` → `www`)
17+- Repo module prefix in file headers: `Zyquo Cloud Web`
18+- Storage key namespace: `zyquo.cloud.web.*` (e.g., `zyquo.cloud.web.keys`, `zyquo.cloud.web.conversations`, `zyquo.cloud.web.settings`)
19+
20+---
21+
22+## 📋 MANDATORY FILE HEADER — EVERY CODE FILE
23+
24+**Every single code file you write** (all `.ts`, `.tsx`, `.js`, `.jsx`, `.css`, config files, `Makefile`, shell/build scripts — anything containing code) **MUST begin with this header comment**, adapted to the file's comment syntax:
25+
26+```ts
27+/*
28+ * <FileName>
29+ * Zyquo Cloud Web
30+ *
31+ * Author: Simon-Pierre Boucher
32+ * Mail: contact@spboucher.ai
33+ */
34+```
35+
36+For shell scripts / Makefiles / YAML (`#` comments):
37+
38+```bash
39+#
40+# <filename>
41+# Zyquo Cloud Web
42+#
43+# Author: Simon-Pierre Boucher
44+# Mail: contact@spboucher.ai
45+#
46+```
47+
48+No exceptions. If you ever create or refactor a file and the header is missing, add it. Before declaring the project done, run a sweep over the repository to verify every code file carries the header.
49+
50+---
51+
52+## 🧭 METHODOLOGY — WORK METHODICALLY, KEEP EVERYTHING COHERENT
53+
54+You must execute this project **strictly in phase order (0 → 8)**. Do not jump ahead, do not interleave phases, do not build the full UI before one provider streams a real completion in the browser, and do not write provider code before Phase 0 research + Zyquo Cloud study are complete.
55+
56+**Working rules:**
57+
58+1. **One phase at a time.** At the start of each phase, write a checklist into `docs/PLAN.md`; check items off as you go. At the end of each phase, run a **phase checkpoint**: build (`npm run build`), type-check, lint, fix everything, write a 3–5 line phase summary in `docs/PLAN.md` before moving on.
59+2. **Phase gates:** Phase 0 is complete only when `docs/PROVIDER-REUSE.md` and `docs/CORS-MATRIX.md` are complete. Phase 2 is complete only when a real streaming completion works browser-side against at least one provider. Phase 3 is complete only when local persistence (keys + conversations) round-trips reliably. Phase 4 spec is the contract for all UI in Phase 6. Phase 7 is complete only when the provider matrix is verified with real keys. Phase 8 is complete only when the production build + deployment are done.
60+3. **Single source of truth, everywhere:**
61+ - Provider/model behavior → ported from the native **Zyquo Cloud** repo (Phase 0.B), adapted to browser `fetch`/streaming; never re-invent request formats. **All Zyquo Cloud models are available.**
62+ - Colors, fonts, spacing, radii → only from `ZyquoTheme` design tokens (the same family system). Zero raw hex or magic numbers in components.
63+ - All storage access → only through a `storage/` layer (never call `localStorage` directly from components).
64+ - All provider networking → only in the `providers/` layer.
65+ - Naming → per the conventions above.
66+4. **Coherence sweeps:** after Phases 3, 6, and 8 (consistent naming — always `Provider`, `AIModel`, `Conversation`, `Message`; no dead code; headers present; one reusable message/bubble component; folders match Phase 2).
67+5. **Build early, build often.** Keep the type-checker and linter green; never accumulate more than one file of unbuilt changes.
68+6. **Commit discipline:** one logical unit per commit, phase-prefixed. **Never commit any API keys** or `.env` with secrets.
69+7. **Local-only is a hard invariant:** no analytics that capture prompt content, no backend calls except directly to the user's chosen provider (and an optional user-configured proxy). Keys and history never leave the device except as part of a provider request the user initiated.
70+
71+---
72+
73+## ⚠️ PHASE 0 — MANDATORY RESEARCH + ZYQUO CLOUD STUDY (DO THIS FIRST, BEFORE ANY CODE)
74+
75+Two mandatory tracks, two documents. No React until both are done.
76+
77+### 0.B — `docs/PROVIDER-REUSE.md` — study the native Zyquo Cloud repo and reuse its providers
78+
79+**Before writing provider code, read and study the native Zyquo Cloud repository** (the macOS app already built from its own CLAUDE.md). Locate it on disk (check the user's projects folder; if not found, ask the user for its path). Document and reuse:
80+
81+1. **Exactly how each provider's API is called** in Zyquo Cloud: base URLs, endpoints, auth headers (`Bearer` vs `x-api-key`, `anthropic-version`, Gemini key param), request/response shapes, the shared OpenAI-compatible client, the native Anthropic (Messages API) and Gemini (`generateContent`) clients, and the SSE streaming formats. The web app must call providers the **same way**, re-implemented with the browser **`fetch` API + `ReadableStream`** for SSE (instead of URLSession).
82+2. **The complete model catalog** (`ModelCatalog` / `docs/PROVIDERS.md`) with capabilities (vision, tools, reasoning, context, max output) and pricing. **Include ALL of these models** in the web app's catalog. Reproduce it as a typed TS catalog.
83+3. **Provider-specific streaming/parse quirks** (Anthropic event blocks, Gemini chunks, DeepSeek/Qwen reasoning fields, Perplexity citations) so the browser streaming layer normalizes them behind one interface.
84+4. The native app's **design tokens and UX** (light-theme flagship, layout, message rendering, model chip) so the web app matches the family look. (The web app re-implements the vault concept as plain localStorage — see the honesty note in Identity — NOT the machine-bound encryption, since a browser has no equivalent; be explicit about this difference.)
85+
86+### 0.A — `docs/CORS-MATRIX.md` — browser feasibility per provider (INTENSIVE WEB RESEARCH)
87+
88+Because calls go **directly from the browser**, you MUST research and document, per provider, the CURRENT reality:
89+
90+1. **CORS support:** does the provider's API send `Access-Control-Allow-Origin` allowing direct browser calls? Test/verify from official docs and community reports for each of the 12 providers (OpenAI, Anthropic, xAI, Mistral, Gemini, Qwen/DashScope, DeepSeek, Kimi, Perplexity, Together, DeepInfra, Cerebras). Note that some providers explicitly support browser use (and may need a special header like Anthropic's `anthropic-dangerous-direct-browser-access` — verify current requirement) and some block CORS entirely.
91+2. **Per-provider verdict:** ✅ direct browser call works / ⚠️ works with a specific header or caveat / ❌ blocked by CORS.
92+3. **Fallback design:** for ❌/⚠️ providers, document the **optional user-configurable proxy**: a base-URL override per provider so a user can point at their own CORS proxy **or at a locally running Zyquo Router** (the family's local gateway) which exposes an OpenAI-compatible endpoint. The app ships fully client-side; the proxy is opt-in and user-supplied. Make the UI surface this clearly ("This provider can't be called directly from the browser — set a proxy URL or run Zyquo Router locally").
93+4. **Streaming over fetch:** confirm SSE-over-`fetch` `ReadableStream` parsing works for each shape (OpenAI SSE, Anthropic events, Gemini stream) in the browser, incl. cancellation via `AbortController`.
94+
95+---
96+
97+## PHASE 1 — Project Setup (React)
98+
99+- **Stack:** **React + TypeScript (strict) + Vite**. Styling: Tailwind CSS (or CSS variables + tokens) implementing `ZyquoTheme`. State: lightweight (Zustand or React context + reducers) — no heavy framework. Routing minimal (single-page; optional hash routes for settings). Markdown: a well-maintained renderer (react-markdown + remark/rehype) with a syntax highlighter (Shiki or highlight.js) and copy buttons. No backend, no auth libraries.
100+- **PWA (recommended):** installable, offline shell (the app UI works offline; only provider calls need network), `manifest.webmanifest`, service worker for the app shell (NOT for caching API responses/keys).
101+- **Scripts:** `npm run dev`, `build`, `preview`, `lint`, `typecheck`, `check`.
102+- **Security posture from the start:** strict **Content-Security-Policy** (allow connections only to the known provider API origins + a user-set proxy origin; no inline scripts; no third-party script tags), Subresource Integrity where applicable, and no dependencies that aren't needed (every dependency is XSS surface for stored keys).
103+- **Deployment target:** static hosting (served as static assets). If deployed under the Zyquo platform, coordinate with the Node+ngrok topology on `m2u64` used by the main site, OR any static host — document in Phase 8.
104+
105+---
106+
107+## PHASE 2 — Architecture + Browser Provider Layer
108+
109+```
110+src/
111+├── main.tsx, App.tsx
112+├── design/ # ZyquoTheme tokens, global styles, theme (light default) + dark toggle
113+├── types/ # Provider, AIModel, Conversation, Message, Settings…
114+├── providers/ # PORTED FROM NATIVE ZYQUO CLOUD (browser fetch/stream)
115+│ ├── types.ts # ProviderClient interface (send, stream)
116+│ ├── openaiCompatible.ts # OpenAI, xAI, Mistral, DashScope, DeepSeek, Kimi, Perplexity, Together, DeepInfra, Cerebras, Gemini-compat
117+│ ├── anthropic.ts # native Messages API + browser header
118+│ ├── gemini.ts # native generateContent
119+│ ├── sse.ts # fetch + ReadableStream SSE parser, AbortController cancel
120+│ └── catalog.ts # ALL models from Zyquo Cloud (typed)
121+├── storage/ # the ONLY place that touches localStorage/IndexedDB
122+│ ├── keys.ts # get/set/remove provider keys (localStorage, namespaced)
123+│ ├── conversations.ts # CRUD conversations/messages (IndexedDB for scale, or localStorage)
124+│ └── settings.ts # theme, default model, per-provider proxy base URLs
125+├── state/ # store (Zustand): conversations, activeModel, streaming state, settings
126+├── features/ # ai orchestration: send message, stream, stop, regenerate, title-gen
127+├── components/ # UI (see Phase 4)
128+├── hooks/
129+└── lib/
130+```
131+
132+- **Streaming with `fetch` + `AbortController`:** parse SSE from a `ReadableStream`; normalize every provider's stream into a common token/tool/usage event; cancel on stop or when the user sends a new message.
133+- **PHASE GATE:** a real **streaming** completion renders token-by-token in a bare test view against at least one ✅-CORS provider, using a key from `localStorage`.
134+
135+---
136+
137+## PHASE 3 — LOCAL PERSISTENCE (KEYS + HISTORY, NO BACKEND)
138+
139+All state is local. **PHASE GATE:** keys and full conversation history survive a page reload and browser restart, and can be exported/imported.
140+
141+### 3.A — Keys (`storage/keys.ts`)
142+- Store per-provider keys in `localStorage` under `zyquo.cloud.web.keys` (a JSON map). Provide get/set/remove, masked display (last 4 chars), and a per-provider **Test** action.
143+- Optional light **obfuscation at rest** (e.g., encrypt with a key derived from a user-set passphrase via WebCrypto AES-GCM) offered as an opt-in "lock with passphrase" feature — but be honest in the UI that without a passphrase the keys are stored in plaintext, and even with one, a compromised page can read them once unlocked. Default is plain localStorage with a clear first-run notice; passphrase lock is a bonus.
144+
145+### 3.B — Conversations (`storage/conversations.ts`)
146+- Persist conversations and messages locally. **Prefer IndexedDB** (via a tiny wrapper or `idb`) for capacity and performance with long histories; fall back to localStorage for small data. Namespace `zyquo.cloud.web.conversations`.
147+- Store: conversation id, title, created/updated, model per conversation, system prompt, params, and the full message list (role, content, images as base64/blob refs, usage, reasoning, citations, timestamps).
148+- CRUD + search across all conversations; autosave on every change; safe migration/versioning of the stored schema.
149+
150+### 3.C — Settings & data portability
151+- `storage/settings.ts`: theme (light default), default model, per-provider proxy base URLs (from the CORS matrix), UI prefs.
152+- **Export / Import all data** (keys optional, conversations, settings) as a JSON file — the only "backup" mechanism since there's no cloud. Clearly warn that exported files may contain API keys if the user includes them.
153+- **Clear data** controls (per-conversation, all conversations, all keys, everything) with confirmation.
154+
155+---
156+
157+## PHASE 4 — DESIGN SYSTEM & UI (LIGHT THEME FLAGSHIP, RESPONSIVE)
158+
159+Match native Zyquo Cloud's look via the family `ZyquoTheme` tokens; **light theme is the default and flagship**, dark is a secondary opt-in toggle. The app is a chat client and must be **responsive / mobile-friendly** (usable on a phone browser).
160+
161+### 4.1 — Light theme (family standard)
162+Reuse Zyquo Cloud's light palette: airy off-white background (`#FAFBFD`), white surfaces, sky-indigo accent (`#4E6AF0`), 0.5pt-equivalent hairlines, ultra-soft shadows on floating elements only, generous body line-height (1.45). Typography/spacing/radii identical to the family. Dark theme derived. **Default = light**, never dark-auto.
163+
164+### 4.2 — Layout & screens
165+- **Shell:** a chat layout with a **sidebar** (conversations: search, new chat, pinned/date groups, rename/delete, model badge) and a **chat area** (header with editable title + centered **model chip** → the full **Model Menu** in 4.2.1; transcript; input bar). Matches native Zyquo Cloud's structure.
166+
167+#### 4.2.1 — The Model Menu (a headline surface — make it exceptional)
168+Clicking the model chip opens a rich, fast **command-palette-style model picker** (⌘/Ctrl+K also opens it):
169+- **Instant search/filter** by model name, provider, or capability; fuzzy match; keyboard-navigable (arrows + Enter), Esc to close.
170+- **Grouped by provider** with the provider glyph/logo, collapsible sections; a **Favorites** group pinned at top (star any model); a **Recents** group.
171+- **Filter chips/toggles:** Vision, Tools/Function-calling, Reasoning, JSON mode, Long-context, Cheapest, Fastest — filter the list live (data from the ported catalog capabilities).
172+- **Each row shows:** model name, provider, **capability badges** (👁 vision / 🛠 tools / 🧠 reasoning), **context window**, max output, and **price per 1M tokens** (in/out) from the catalog; a subtle "not configured" state if the provider has no key yet (with a quick "Add key" affordance), and a "needs proxy" flag for CORS-blocked providers.
173+- **Set as default**, **use for this conversation**, or **use for this one message** (regenerate with a different model right from the menu).
174+- **Compare picker:** a "Compare…" action to select 2–4 models for side-by-side compare mode.
175+- **Aliases:** user-defined friendly aliases (e.g., `fast`, `smart`) mapping to a chosen model, surfaced at the top.
176+- Beautiful empty/loading/error states; virtualized list so 100+ models stay instant.
177+- **Transcript:** user bubbles right (accent-subtle), assistant left on surface; full **Markdown** with tables, blockquotes, and **syntax-highlighted code blocks with copy**; collapsible **reasoning/"thinking"** section for reasoning models; **Perplexity citations** as numbered chips; per-message token/cost (from `usage` + catalog pricing) on hover; message actions (copy, edit & resend, regenerate — optionally with another model, delete).
178+- **Input bar:** floating card, multiline auto-grow, **image attach/paste** for vision-capable models (base64), attachment thumbnails, params quick-toggle, circular accent **send** (⌘/Ctrl+Enter), **Stop** during streaming.
179+- **Settings (modal or route):** tabs — **Providers & Keys** (per-provider masked field, Test button + status dot, proxy base URL field for ⚠️/❌ providers, the honesty notice about localStorage), **Models** (full catalog, capability badges, pricing, favorites, default model), **Appearance** (light/dark/system + accent choices + chat font size), **Data** (export/import, clear data), **Advanced** (passphrase lock toggle, streaming toggle, default params).
180+- **First-run:** a clean, honest welcome — "Your keys and history stay in this browser (localStorage). Add a provider key to begin." + quick model pick. Beautiful, not scary, but transparent.
181+- **Empty chat state:** centered Zyquo mark, greeting, a few suggested prompts, model chip.
182+
183+### 4.3 — Responsive & motion
184+- **Responsive:** sidebar collapses to a drawer on phones; transcript and input bar adapt full-width; ≥44pt touch targets; code blocks scroll internally; no horizontal overflow; works portrait/landscape. Mobile is a real target.
185+- **Motion:** smooth streaming (no jitter, blinking caret), 150ms message-in, 80ms hovers, snappy popovers, 60fps; respect `prefers-reduced-motion`.
186+
187+### 4.4 — Quality gate
188+Review every state at phone + desktop widths in light (default) and dark: consistent tokens, no overflow, working drawer nav, streaming, stop, error (bad key → clear message, CORS-blocked → explain proxy/Router), empty/loading, long conversation performance (virtualize the transcript if needed), export/import, passphrase lock/unlock. Confirm **light default**. If it looks templated or breaks on a phone, iterate.
189+
190+---
191+
192+## PHASE 5 — BRAND ASSETS
193+
194+- Favicon set + `manifest.webmanifest` (installable PWA) using the Zyquo mark (chunky charcoal-black Z with electric-blue base) and the Cloud accent; apple-touch-icon; `theme-color` light.
195+- App wordmark as crisp inline SVG (light/dark); reuse the Zyquo Cloud icon for the PWA/install icon so it matches the native app in the Dock/home screen.
196+- OG/social meta for the hosted URL (static image, on-brand).
197+
198+---
199+
200+## PHASE 6 — Features (Parity with native Zyquo Cloud, browser-native)
201+
202+### Cool chat features (make this genuinely delightful — go beyond a basic chat box)
203+- **Rich Model Menu** (Phase 4.2.1): searchable, capability-filtered, priced, favorites/recents, per-message model override, aliases.
204+- **Branching / regenerate variants:** regenerate a response (same or different model) and keep **multiple variants** you can swipe/tab between; branch a conversation from any message to explore alternatives without losing the original thread.
205+- **Edit & fork:** edit any earlier user message and re-run from that point (forking the thread); edit assistant messages for note-taking.
206+- **Streaming niceties:** token-by-token with a blinking caret, live **tokens/sec** and elapsed timer, **Stop** and **Continue** (ask the model to keep going), and **scroll-lock with a "jump to latest" pill**.
207+- **Multi-model compare mode:** send one prompt to 2–4 models side-by-side in columns, each streaming independently, with per-column copy/regenerate and a quick "promote this answer into the thread" action.
208+- **Prompt Library & slash commands:** ship ≥40 quality templates; type **`/`** in the input for a slash-command menu (templates, personas, tools like `/summarize`, `/translate`, `/rewrite`, `/explain`); templates support `{{variables}}` with a quick fill form.
209+- **Personas:** named assistants = system prompt + preferred model + params; switch persona per conversation; ship a few great defaults (Coder, Writer, Analyst, Tutor…).
210+- **Attachments:** drag/drop or paste **images** (vision models) with thumbnails; attach **text/code/CSV/JSON/MD files** injected as context; paste large text as a collapsible block.
211+- **Rich rendering:** GitHub-flavored **Markdown**, tables, task lists, footnotes; **syntax-highlighted code** with language label, copy button, and per-block "copy"/"wrap" toggles; **math via KaTeX**; **Mermaid diagram** rendering; collapsible **reasoning/"thinking"** panels; **Perplexity citations** as numbered, clickable chips.
212+- **Message actions:** copy (as text or Markdown), quote-reply, pin/star a message, add a note, delete, and **read-aloud** (Web Speech API TTS) with voice/speed controls; optional **speech-to-text input** (Web Speech API) for dictation.
213+- **Conversation tools:** auto-generated titles, **summarize this conversation**, **conversation-level search** and **in-conversation find**, tags/folders, pin, archive, duplicate, and **export a single conversation** to Markdown / JSON / (client-side) PDF; shareable **export link is NOT server-based** — export is a file since there's no backend.
214+- **Token & cost HUD:** live per-message and per-conversation token counts and **estimated cost** (from catalog pricing), plus a context-window usage bar showing how full the context is, with **auto-trim oldest turns** (keeping system prompt) when near the limit.
215+- **Parameter controls:** per-conversation temperature, top_p, max tokens, penalties, seed, JSON mode/`response_format`, and **reasoning effort** where supported — the panel only shows params the active model/provider actually supports.
216+- **Quality-of-life:** command palette (⌘/Ctrl+K) for everything (new chat, switch model, search, jump to conversation, run template); full keyboard shortcuts; **undo** for deletes (soft-delete + toast); autosave everywhere; offline-friendly UI shell (PWA); **retry with backoff** on transient provider errors; graceful, human error toasts (bad key names the provider, CORS-blocked explains the proxy/Router option, rate-limit shows retry).
217+- **Theming extras:** light default + dark toggle + a few accent choices; adjustable chat font size and message density (comfortable/compact); optional "focus mode" hiding the sidebar.
218+
219+### Core parity
220+- Multi-conversation local history (search, pin, rename, delete, groups) — all in IndexedDB/localStorage
221+- Streaming + non-streaming chat across **all 12 providers / all Cloud models** (browser fetch), model switch per conversation and per message
222+- Vision (image attach/paste) for capable models; text-file attach injected into messages
223+- System prompt per conversation + global default; per-conversation params (only those the provider supports)
224+- Keys + history in `localStorage`/IndexedDB with optional passphrase lock; full export/import; clear-data controls; per-provider proxy override for CORS-blocked providers (point at a local Zyquo Router)
225+- PWA installable; **light theme default**; fully responsive (usable on a phone browser)
226+
227+---
228+
229+## PHASE 7 — VERIFICATION (MANDATORY)
230+
231+The user will provide **real API keys** (same providers as Zyquo Cloud). You MUST:
232+
233+1. **Provider matrix (browser-side):** for **every provider/model** in the catalog, from an actual browser context, verify: non-streaming completion; **streaming** renders token-by-token; cancellation via Stop works; vision on vision-capable models; reasoning content on reasoning models; Perplexity citations. Record ✅/❌ per provider/model and the **CORS verdict** encountered (direct vs. needs-proxy). Fix everything fixable; for CORS-blocked providers, verify the **proxy/Zyquo Router fallback** path works and is clearly surfaced in the UI.
234+2. **Persistence:** keys and full history survive reload and browser restart; export→clear→import restores exactly; schema migration works; large-history performance is smooth (virtualization if needed).
235+3. **Security/privacy sanity:** confirm no key or prompt content is sent anywhere except the chosen provider (or user-set proxy) — inspect network traffic; confirm the CSP blocks unexpected origins; confirm the first-run localStorage notice is present; passphrase lock encrypts/decrypts correctly.
236+4. **Responsive/cross-browser:** verify on Chrome, Safari, Firefox (desktop) and iOS Safari / Android Chrome (phone): layout, streaming, drawer nav, no overflow; **light theme loads by default**.
237+5. Never commit or log any key; keys live only in the browser's localStorage during use.
238+
239+---
240+
241+## PHASE 8 — BUILD & DEPLOYMENT
242+
243+No notarization (it's a web app). Phase 8 is a proper **production build + static deployment**:
244+1. `npm run build` → optimized static bundle (code-split, hashed assets, tree-shaken, minimal JS); zero type/lint errors; PWA assets generated; CSP finalized to the exact set of provider origins (+ a placeholder for the user's proxy origin, handled at runtime).
245+2. **Deploy as static assets** at **`www.zyquo.cloud`** (HTTPS, apex `zyquo.cloud` → `www` redirect). If served under the Zyquo platform, use the same **Node server on `m2u64` exposed through ngrok** as the main site (bind the `www.zyquo.cloud` custom domain to the tunnel + DNS CNAME), OR any static host (Cloudflare Pages/Vercel/Netlify) as a documented alternative. Set security headers (CSP, HSTS, X-Content-Type-Options, Referrer-Policy) and long-cache for hashed assets.
246+3. Because it's 100% client-side, it also runs from `file://`-style local hosting or a simple static server — document how a user can self-host it, which pairs naturally with running **Zyquo Router** locally for CORS-blocked providers.
247+4. Post-deploy: verify HTTPS, PWA install, all provider calls from the deployed origin (CSP must allow them), light-default, and mobile.
248+
249+---
250+
251+## Engineering Standards
252+
253+- TypeScript strict; ESLint + Prettier clean; zero build warnings; minimal, audited dependencies (each is key-theft surface)
254+- All `localStorage`/IndexedDB access behind `storage/`; all networking behind `providers/`; components stay presentational
255+- Provider layer faithfully ported from native Zyquo Cloud (all models); SSE via `fetch`+`ReadableStream`+`AbortController`; robust human-readable errors (invalid key names the provider; **CORS-blocked explains the proxy/Router fallback**; rate-limit shows retry)
256+- Design tokens only — no hardcoded colors/sizes; **light theme default**; responsive/mobile-first friendly
257+- Strict CSP; never transmit keys/history anywhere but the chosen provider (or user proxy); transparent first-run notice about localStorage
258+- `README.md` (dev, self-host, "use with Zyquo Router") + `docs/` (PROVIDER-REUSE, CORS-MATRIX, PLAN)
259+- Commit in logical, phase-prefixed increments; never commit keys/secrets
260+
261+## Definition of Done
262+
263+- A React web app that reproduces Zyquo Cloud's chat experience in the browser with **no auth, no accounts, no backend**
264+- API **keys stored in `localStorage`** and **conversation history stored locally** (IndexedDB/localStorage), surviving reloads/restarts, with export/import and clear-data — plus an optional passphrase lock and an honest first-run notice about local storage
265+- Streaming + non-streaming chat across **all 12 providers / all models from native Zyquo Cloud**, called **directly from the browser**, with per-provider **CORS reality documented** and a **proxy / local Zyquo Router fallback** for blocked providers, verified with real keys (Phase 7 matrix)
266+- Full chat features: a rich searchable/priced **Model Menu**, per-message model override, **branching/regenerate variants**, edit & fork, **multi-model compare**, **slash commands** + prompt library, personas, Markdown + code + **KaTeX + Mermaid**, reasoning display, citations, vision, **read-aloud / dictation**, token/cost HUD + context bar with auto-trim, and a ⌘K command palette
267+- Deployed at **`www.zyquo.cloud`** (HTTPS, apex→www); **light theme loads by default**; dark is a secondary toggle; the app is **responsive and usable on phones**; installable PWA
268+- Strict CSP; keys/history never leave the device except as user-initiated provider requests
269+- The Zyquo mark/wordmark and Cloud accent applied consistently as crisp SVG
270+- **Every code file starts with the mandatory Author/Mail header** (verified by a repo-wide sweep)
271+- `docs/PLAN.md` shows every phase completed; PROVIDER-REUSE and CORS-MATRIX complete and traceable
272+- Zyquo Cloud Web feels like the legendary, private, local-only browser edition of Zyquo Cloud
added docs/CORS-MATRIX.md +95 −0
@@ -0,0 +1,95 @@
1+<!--
2+ CORS-MATRIX.md
3+ Zyquo Cloud Web
4+
5+ Author: Simon-Pierre Boucher
6+ Mail: contact@spboucher.ai
7+-->
8+
9+# CORS Matrix — Browser Feasibility per Provider (Phase 0.A)
10+
11+**Method.** Every verdict below is **empirical**, not guessed: on 2026-07-31 each
12+provider's chat endpoint was probed with (1) a browser-identical `OPTIONS`
13+preflight (`Origin: https://www.zyquo.cloud`, `Access-Control-Request-Method:
14+POST`, `Access-Control-Request-Headers: authorization,content-type` — or the
15+Anthropic header set), and (2) a **real authenticated POST** carrying the same
16+`Origin`, inspecting the `Access-Control-Allow-*` response headers. A streaming
17+(`stream: true`) request was additionally verified to return
18+`content-type: text/event-stream` **with** `access-control-allow-origin` intact.
19+Probe script: `scripts/cors-probe.sh` (keys read from gitignored
20+`.keys.local.json`; never committed).
21+
22+## Verdict table
23+
24+| # | Provider | Endpoint | Preflight | POST ACAO | Verdict |
25+|---|----------|----------|-----------|-----------|---------|
26+| 1 | OpenAI | `https://api.openai.com/v1/chat/completions` | 200, allows `authorization,content-type`, max-age 86400 | `*` (echoes origin on preflight) | ✅ direct |
27+| 2 | Anthropic | `https://api.anthropic.com/v1/messages` | 200, explicitly allows `x-api-key, anthropic-version, anthropic-dangerous-direct-browser-access` | `*` — **only when** `anthropic-dangerous-direct-browser-access: true` is sent; without it the API returns 401 `"CORS requests must set 'anthropic-dangerous-direct-browser-access' header"` | ⚠️ direct **with header** |
28+| 3 | xAI | `https://api.x.ai/v1/chat/completions` | 200, `allow-methods: *`, `allow-headers: *` | `*` | ✅ direct |
29+| 4 | Mistral | `https://api.mistral.ai/v1/chat/completions` | 200, allows `Authorization, Content-Type, …` | `*` | ✅ direct |
30+| 5 | Google Gemini (OpenAI-compat) | `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions` | 200, echoes origin, max-age 3600 | echoes origin | ✅ direct |
31+| 6 | Alibaba Qwen (DashScope intl) | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions` | 200, echoes origin, credentials true, max-age 86400 | echoes origin | ✅ direct |
32+| 7 | DeepSeek | `https://api.deepseek.com/chat/completions` | 200, `allow-methods: POST`, echoes origin | echoes origin | ✅ direct |
33+| 8 | Kimi (Moonshot) | `https://api.moonshot.ai/v1/chat/completions` | 204, echoes origin, credentials true | echoes origin | ✅ direct |
34+| 9 | Perplexity | `https://api.perplexity.ai/chat/completions` | 204, `*`, generous header allowlist, max-age 1728000 | `*` | ✅ direct |
35+| 10 | Together AI | `https://api.together.xyz/v1/chat/completions` | 200, `*` | `*` | ✅ direct |
36+| 11 | DeepInfra | `https://api.deepinfra.com/v1/openai/chat/completions` | 200, `*` | `*` | ✅ direct |
37+| 12 | Cerebras | `https://api.cerebras.ai/v1/chat/completions` | 200, `*`, max-age 600 | `*` | ✅ direct |
38+
39+**Bottom line: all 12 providers are callable directly from the browser today.**
40+11 are plain ✅; Anthropic is ⚠️ only in the sense that the client must add
41+`anthropic-dangerous-direct-browser-access: true` on every request (the header
42+name is Anthropic's deliberate reminder that browser-resident keys are exposed —
43+which matches this app's transparent bring-your-own-key model).
44+
45+## Probe caveats observed (not CORS failures)
46+
47+- **Perplexity** rejects `max_tokens < 16` (`400 max_tokens must be at least 16`) — keep test calls ≥ 16.
48+- **Together** now requires serverless-hosted model IDs (e.g. `meta-llama/Llama-3.3-70B-Instruct-Turbo`); older turbo IDs 400 with a "dedicated endpoint" message. CORS headers present on the error, so the verdict stands.
49+- **Kimi/Cerebras** model IDs rotate quickly (`kimi-k3`, `kimi-k2.6`… / `gpt-oss-120b`, `gemma-4-31b`, `zai-glm-4.7`); 404s were model-name-only, CORS intact.
50+- **DeepSeek** serves `deepseek-chat` as `deepseek-v4-flash` (alias resolution).
51+- **xAI** resolves legacy aliases (probe with `grok-3-mini` answered as `grok-4.3`).
52+- Several providers echo the exact origin instead of `*` (Gemini, DashScope, DeepSeek, Kimi) — irrelevant to the app, but means responses vary by `Origin` and are not cacheable cross-origin.
53+
54+## Documented stance (official docs, web-researched + verified 2026-07-31)
55+
56+- **Anthropic — the only provider with officially documented CORS support.** Added Aug 22 2024 (Claude Platform release notes, still live): CORS headers are returned when the request carries `anthropic-dangerous-direct-browser-access: true` (what the SDKs send for `dangerouslyAllowBrowser: true`). The deliberately scary name is Anthropic's warning about browser-resident keys — Anthropic frames it as acceptable for internal tools and **bring-your-own-key apps**, which is exactly this app. Sources: [release notes](https://platform.claude.com/docs/en/release-notes/api), [anthropic-sdk-typescript](https://github.com/anthropics/anthropic-sdk-typescript), [Simon Willison's explainer](https://simonwillison.net/2024/Aug/23/anthropic-dangerous-direct-browser-access/).
57+- **OpenAI** — docs are silent on CORS; permissive ACAO is undocumented behavior. Official guidance is the strongest anti-browser language of the twelve ("Never deploy your key in client-side environments"); the Node SDK gates browsers behind `dangerouslyAllowBrowser: true`. ([key safety](https://help.openai.com/en/articles/5112595-best-practices-for-api-key-safety), [openai-node](https://github.com/openai/openai-node))
58+- **Google Gemini** — API-key doc explicitly acknowledges client-side use and its extraction risk; documented client-side alternative (ephemeral tokens) applies only to the Live API, not the OpenAI-compat chat endpoint used here. ([API keys](https://ai.google.dev/gemini-api/docs/api-key))
59+- **Alibaba Qwen/DashScope** — browser use officially anticipated via short-lived **temporary API keys** minted by a backend (60 s validity); permanent keys detected as publicly exposed may be auto-disabled. The app uses the **international** endpoint (`dashscope-intl.aliyuncs.com`), same as native Zyquo Cloud. ([temporary API keys](https://www.alibabacloud.com/help/en/model-studio/generate-temporary-api-key))
60+- **xAI** — docs silent on CORS for chat; ephemeral client tokens exist only for the realtime/audio API. ([docs.x.ai](https://docs.x.ai/developers/quickstart))
61+- **Together AI / Cerebras** — docs silent on CORS, but both official TS/Node SDKs list **web browsers as supported runtimes** with no browser gate. ([together-typescript](https://github.com/togethercomputer/together-typescript), [cerebras-cloud-sdk-node](https://github.com/Cerebras/cerebras-cloud-sdk-node))
62+- **Mistral / DeepSeek / Kimi (Moonshot) / Perplexity / DeepInfra** — docs silent on CORS either way; Perplexity and Moonshot carry generic "never expose keys client-side" guidance. ([Mistral](https://docs.mistral.ai/api), [DeepSeek](https://api-docs.deepseek.com/), [Kimi](https://platform.kimi.ai/docs/api/overview), [Perplexity](https://docs.perplexity.ai/guides/api-key-management), [DeepInfra](https://docs.deepinfra.com/quickstart))
63+
64+**Implication:** outside Anthropic, permissive CORS is undocumented behavior that
65+any provider could withdraw without notice — which is why the per-provider proxy
66+override ships even though everything works today. The universal "don't ship
67+your key in client code" warnings target developers embedding *their own* key;
68+Zyquo Cloud Web's model is the opposite: each user brings **their own** key,
69+which never leaves their device except toward the provider itself.
70+
71+## Fallback design (proxy / Zyquo Router) — still shipped
72+
73+Even though everything works today, CORS policies are provider-controlled and
74+can change without notice. The app therefore ships a **per-provider base-URL
75+override** (Settings → Providers & Keys → "Proxy / base URL"):
76+
77+- Any provider's base URL can be overridden with a user-supplied CORS proxy or
78+ a locally running **Zyquo Router** (the family's local gateway, OpenAI-compatible).
79+- When a request fails with a network/CORS-shaped error (fetch `TypeError` with
80+ no HTTP status), the error toast explains: *"This provider may be blocking
81+ browser calls — set a proxy URL in Settings or run Zyquo Router locally."*
82+- The override is stored in `zyquo.cloud.web.settings`; the CSP includes a
83+ runtime-managed `connect-src` strategy (see Phase 8) so a user-set proxy origin
84+ remains reachable.
85+
86+## Streaming over fetch
87+
88+- Verified: streaming responses (`text/event-stream`) carry the same
89+ `access-control-allow-origin` headers (OpenAI probe above), so
90+ `fetch` + `ReadableStream` SSE parsing works browser-side.
91+- All 11 OpenAI-compatible providers use `data:` SSE lines with a `[DONE]`
92+ terminator; Anthropic uses named `event:` blocks. One parser
93+ (`providers/sse.ts`, ported from the native `SSEParser`) handles both.
94+- Cancellation: `AbortController.abort()` on the fetch tears down the stream —
95+ used by Stop and new-message preemption.
added docs/NATIVE-UX.md +191 −0
@@ -0,0 +1,191 @@
1+<!--
2+ NATIVE-UX.md
3+ Zyquo Cloud Web
4+
5+ Author: Simon-Pierre Boucher
6+ Mail: contact@spboucher.ai
7+-->
8+
9+# Native Zyquo Cloud UX Reference (Phase 0.B study — feeds Phases 4 & 6)
10+
11+Detailed study of the native macOS app's views (2026-07-31), used as the
12+contract for the web UI. Web-specific departures are marked ⤷web.
13+
14+## Layout
15+
16+- **Root:** sidebar (260, min 260 / max 360) + detail; window default 1240×800.
17+ ⤷web: sidebar collapses to a drawer under ~768px.
18+- **Sidebar (top→bottom):** wordmark (CloudZGlyph 22 + "Zyquo Cloud" 14/medium);
19+ search field (surfaceSecondary@0.7, radius 6, ⌘F); full-width accent **New
20+ Chat** button (radius 6, ⌘N); conversation list (LazyVStack spacing 2, groups
21+ **Pinned / Today / Yesterday / Previous 7 Days / Older**, headers caption/
22+ textTertiary); rows = title 13 + sub-line `modelID · relative time` caption,
23+ hover pin/trash, selected = accentSubtle, radius 6; context menu Pin / Add
24+ Tag / Remove Tag / Delete; tags capsule strip above footer; footer = gear
25+ (Settings) + usage caption `X.XK tok · ~$Y.YY`.
26+- **Chat header (52px, hairline below):** model chip absolutely centered;
27+ left = editable title (13/medium, double-click rename); right = Compare,
28+ Export (⌘⇧E), Info popover (340 wide: tokens in/out, est. cost, editable
29+ system prompt 300×90).
30+- **Transcript:** centered 760px column, 16px turn rhythm/padding; auto-scroll
31+ while pinned to bottom; floating **"Jump to latest"** pill (capsule, surface,
32+ hairline, soft shadow) when unpinned during streaming.
33+- **Bubbles:** user right (accentSubtle fill, no border, ≥60px left spacer);
34+ assistant left with 26×26 circular provider avatar (accent glyph on
35+ accentSubtle) + surface bubble with 0.5px border; pad 12h/10v, radius 10;
36+ metadata row fades in on hover.
37+- **Input bar:** floating card (pad 12, radius 14, surface, hairline, soft
38+ shadow, margins 12); optional 56px attachment strip; paperclip → auto-grow
39+ editor ("Message…", max 200px) → params button (popover) → **28×28 circular
40+ send** (arrow up, accent; danger + stop icon while streaming), ⌘↩. Drag
41+ target: 1.5px dashed accent border. Images png/jpg/webp/gif; text files
42+ <512 KB. Thumbnails 56×56 radius 6 with remove badge.
43+- **Model chip:** provider glyph 11 + name 12.5/medium + chevron 8, pad 8h/4v,
44+ radius 6, surfaceSecondary, press-scale; opens picker popover.
45+
46+## Model picker (native popover 320×380)
47+
48+Search field → "Favorites" section (query empty only) → per-provider sections,
49+providers with keys first then alphabetical. Row: glyph · name 12.5 · capability
50+icons 9pt (eye/brain/wrench) · context badge · star toggle (warning color).
51+Rank: favorites → recommended → normal → legacy. **No recents, no capability
52+filters, click-only.** ⤷web (Phase 4.2.1 spec goes beyond native): ⌘K palette
53+sizing, fuzzy search, filter chips, Recents group, pricing per row,
54+not-configured / needs-proxy states, aliases, compare picker, keyboard nav,
55+virtualized list.
56+
57+## Message rendering
58+
59+- Markdown blocks (spacing 12): headings ×1.55/1.35/1.2/1.05 semibold;
60+ blockquote = 3px accent left bar + textSecondary; task lists; ordered lists
61+ monospaced-digit in 20px slot; tables = zebra rows, header surfaceSecondary,
62+ hairline grid, radius 6; inline code mono size−1 on surfaceSecondary; links
63+ accent.
64+- Code blocks: surfaceSecondary card radius 10; header = UPPERCASED language
65+ caption + hover-reveal copy (checkmark success 1.2s); body mono size−1,
66+ h-scroll. Syntax theme: keyword=accent, string=success, comment=textTertiary,
67+ number #B26A0B/#E0A458, type #2380C2/#62B7F0, function #6E4FD4/#A48CF2,
68+ property #2F6FBF/#7FB4E8, attribute=warning.
69+- Reasoning: collapsed disclosure "Thinking…" (streaming) / "Thought process";
70+ expanded = mono size−2, textSecondary, surfaceSecondary pad 8 radius 6.
71+- Citations: capsule chips — 13px accent circle with white index + title/host
72+ caption; opens URL.
73+- Streaming caret: 7×15 accent rounded rect blinking 1↔0.15 @0.55s.
74+- Actions (hover, 10pt): copy, edit-&-resend (user), regenerate (assistant),
75+ quote-reply (`> ` prefix into draft), delete. Edit mode = inline textarea +
76+ Cancel/Resend.
77+- Error state: warning triangle + danger text on danger@8% radius 6.
78+- Metadata: time `HH:mm`, `{in}→{out} tok`, `~$0.0000` when >0.
79+
80+## Settings (native 720×520, 5 tabs)
81+
82+1. **Providers & Keys** — per-provider row: glyph, name + status dot
83+ (green verified / grey saved / spinner testing / red failed), sub-line
84+ `Verified · 123 ms` or error or masked key; 220px secure field (mono 11),
85+ **Test** (round-trips provider's cheapest model), trash.
86+ ⤷web adds: proxy base-URL field, localStorage honesty notice.
87+2. **Models** — provider picker, **Refresh from API** (`N live · M not in
88+ catalog`), Add Custom (sheet: id, name, base URL, context, vision toggle);
89+ table: star, name + Featured/Legacy badges, mono id, capability badges,
90+ context, `$in / $out`.
91+3. **Appearance** — theme System/Light/Dark; accents: Sky Indigo #4E6AF0/#6D84F5
92+ (subtle #EBEFFD/#28304C), Graphite #5B6270/#8A93A6 (#EEF0F4/#2A2E3A), Teal
93+ #1D9A8F/#3BB5AA (#E6F6F4/#1F3A38), Amber #C77D1D/#DD9B45 (#FBF1E3/#3D3222),
94+ Rose #C94F7C/#DE7099 (#FAEAF1/#3D2733); chat font slider 12–18 step 0.5 with
95+ live preview card. ⤷web default = **light** (never dark-auto).
96+4. **Shortcuts** — ⌘N new, ⌘K palette, ⌘F search, ⌘↩ send, ⌘⇧E export,
97+ ⌘, settings, ⌘. stop.
98+5. **Advanced** — default system prompt (90px editor); data location note.
99+ ⤷web: Data tab (export/import/clear), passphrase lock, streaming toggle.
100+
101+## Command palette (⌘K, sheet 560, results 380)
102+
103+Sections: **Models** (max 8; empty query → recommended; switches current
104+conversation's model) → **Personas** (max 6; applies system prompt + params +
105+optional model) → **Templates** (max 10; inserts with `{{input}}` ← current
106+draft). Row: accent symbol 13 + title 13 / subtitle caption. Native is
107+click-only — ⤷web adds arrow/Enter keyboard nav.
108+
109+## Compare (sheet ≥900×560)
110+
111+2–4 equal columns, hairline separators; per-column: model chip, output-token
112+caption, copy, regenerate, remove (>2 only); body = reasoning preview (mono
113+size−2.5, max 6 lines) + markdown at size−0.5 + caret; footer prompt input
114+broadcasts to all columns, each streaming independently; no system prompt,
115+default params, not saved. ⤷web adds "promote answer into thread".
116+
117+## Empty state
118+
119+Centered slightly above middle: CloudZGlyph 88 → "Welcome to Zyquo Cloud"
120+20/semibold → "Your keys, every cloud model, one beautiful chat."
121+(textSecondary) → model chip → 2×2 grid (max 460) of suggestion cards
122+(surface, radius 10, hairline, pad 12): Explain something / Review my code /
123+Summarize a document / Translate. Native has **no first-run flow** —
124+⤷web adds the honest localStorage welcome + quick key setup.
125+
126+## Personas (8 built-ins)
127+
128+Senior Code Reviewer, Technical Writer, Product Strategist, Research Assistant,
129+Socratic Tutor, UX Critic, Data Analyst, Ruthless Editor — dense ~120-word
130+system prompts, no fixed model (use default), default params. Ported verbatim
131+in Phase 6 (`features/personas.ts` data).
132+
133+## Prompt library (56 templates, 8 categories × 7)
134+
135+Writing / Coding / Analysis / Translation & Language / Business / Learning /
136+Creativity / Productivity; each body contains `{{input}}` exactly once
137+(missing → input appended). Ported verbatim in Phase 6. Built-ins immutable;
138+user templates appended after.
139+
140+## ConversationStore behaviors to port
141+
142+- Conversations newest-first; search matches title or any message text;
143+ 5 sidebar groups.
144+- Send: append user msg → placeholder assistant msg → build request from
145+ system prompt + messages (excluding placeholder and errored messages) →
146+ stream deltas; usage + estimatedCost applied at end. Per-message
147+ `overrideModel` also switches the conversation model.
148+- Regenerate drops the trailing assistant turn (⤷web: keep as variant instead —
149+ native has **no branching/variants**; the web app adds `variants` per
150+ assistant message and branch-from-message).
151+- Edit & resend rewrites the user message and truncates after it.
152+- **Auto-title:** after first completed exchange (exactly 1 assistant msg,
153+ `hasAutoTitle` true): cheapest non-legacy non-reasoning model for the
154+ conversation's provider, non-streaming, maxTokens 24, prompt
155+ "Write a title of at most 5 words for this conversation. Reply with the
156+ title only, no quotes." + first user text (500 chars) + last assistant text
157+ (500 chars); trim quotes/whitespace, cap 60 chars; silent failure; manual
158+ rename disables auto-titling.
159+- Params per conversation: temperature, topP, maxTokens, frequencyPenalty,
160+ presencePenalty, reasoningEffort, thinkingEnabled — **nil = provider default,
161+ omitted from request**; editor only shows rows the model supports, each
162+ slider gated by an enable checkbox.
163+- Persistence: autosave on every mutation (streaming persisted at stream end,
164+ not per-token). ⤷web: IndexedDB, same document shape.
165+
166+## Brand mark (CloudZGlyph → SVG)
167+
168+Single-tint glyph, 1024 grid, tight box x 267–770 / y 257–766:
169+
170+```svg
171+<svg viewBox="0 0 503 509" xmlns="http://www.w3.org/2000/svg">
172+ <g transform="translate(-267,-257)" fill="none" stroke="currentColor">
173+ <path d="M320 310H704L320 700H556" stroke-width="106"
174+ stroke-linecap="round" stroke-linejoin="round"/>
175+ <rect x="653" y="585" width="104" height="168" rx="24"
176+ fill="currentColor" stroke="none"/>
177+ </g>
178+</svg>
179+```
180+
181+Full app icon (`~/Desktop/zyquo-cloud/assets/icon/zyquo-cloud.svg`): Big Sur
182+squircle (inset 100, 824 wide) with vertical gradient #6FA8FF → #4E6AF0@52% →
183+#3A3F9E, top gloss, caret glow radial #BFD5FF @ (705,655,r200); Z drawn twice —
184+plum #2A3490 contour at width 132 under near-white face gradient (#FFFFFF →
185+#E9EFFC) at width 106; caret likewise (640,572,130×194,r34 plum under
186+653,585,104×168,r24 face). Reused for the PWA icon in Phase 5.
187+
188+## Extras not ported
189+
190+Quick Chat (⌥Space Spotlight panel) and the menu-bar extra are macOS-only;
191+their role is covered by the web command palette.
added docs/PLAN.md +55 −0
@@ -0,0 +1,55 @@
1+<!--
2+ PLAN.md
3+ Zyquo Cloud Web
4+
5+ Author: Simon-Pierre Boucher
6+ Mail: contact@spboucher.ai
7+-->
8+
9+# Zyquo Cloud Web — Build Plan & Phase Log
10+
11+Strict phase order 0 → 8. Each phase gets a checklist here, checked off as work
12+lands, plus a 3–5 line summary at its checkpoint.
13+
14+---
15+
16+## Phase 0 — Research + Zyquo Cloud study
17+
18+- [x] Locate native Zyquo Cloud repo (`~/Desktop/zyquo-cloud`)
19+- [x] Study provider layer (ProviderID, ProviderRegistry, OpenAICompatibleClient, AnthropicClient, StreamingService/SSEParser, ProviderProtocol)
20+- [x] Study AIModel / capabilities / pricing / ParameterSupport model
21+- [x] Study ZyquoTheme design tokens
22+- [x] Extract complete model catalog (170 models, 12 providers — port from ModelCatalogData.swift, NOT the drifted catalog-summary.md)
23+- [x] Study native UX (views, model picker, personas, prompt library, brand glyph) → `docs/NATIVE-UX.md`
24+- [x] Empirical CORS probes, all 12 providers (preflight + real POST with Origin)
25+- [x] Anthropic browser header confirmed (`anthropic-dangerous-direct-browser-access: true`)
26+- [x] Web research: documented CORS stance per provider (cited in CORS-MATRIX)
27+- [x] Write `docs/CORS-MATRIX.md`
28+- [x] Write `docs/PROVIDER-REUSE.md`
29+
30+**Phase 0 summary.** Native repo studied end-to-end: only two wire formats
31+(OpenAI chat/completions ×11 incl. Gemini-compat; Anthropic Messages ×1), one
32+SSE parser, 170-model catalog, ZyquoTheme tokens extracted. Empirical CORS
33+probes with real keys: **all 12 providers callable directly from the browser**
34+(Anthropic needs `anthropic-dangerous-direct-browser-access: true`); documented
35+stances researched and cited. Proxy/Zyquo Router override still ships since
36+only Anthropic documents CORS officially. Gate satisfied: PROVIDER-REUSE.md +
37+CORS-MATRIX.md complete.
38+
39+---
40+
41+## Phase 1 — Project setup (React + TS + Vite) — pending
42+
43+## Phase 2 — Architecture + browser provider layer — pending
44+
45+## Phase 3 — Local persistence — pending
46+
47+## Phase 4 — Design system & UI — pending
48+
49+## Phase 5 — Brand assets — pending
50+
51+## Phase 6 — Features — pending
52+
53+## Phase 7 — Verification — pending
54+
55+## Phase 8 — Build & deployment — pending
added docs/PROVIDER-REUSE.md +235 −0
@@ -0,0 +1,235 @@
1+<!--
2+ PROVIDER-REUSE.md
3+ Zyquo Cloud Web
4+
5+ Author: Simon-Pierre Boucher
6+ Mail: contact@spboucher.ai
7+-->
8+
9+# Provider Reuse — Porting the Native Zyquo Cloud Provider Layer (Phase 0.B)
10+
11+Source of truth: the native macOS repo at `~/Desktop/zyquo-cloud` (studied
12+2026-07-31: `ProviderID.swift`, `ProviderRegistry.swift`,
13+`OpenAICompatibleClient.swift`, `AnthropicClient.swift`, `StreamingService.swift`,
14+`ProviderProtocol.swift`, `AIModel.swift`, `ModelCatalog.swift`,
15+`ModelCatalogData.swift`, `ZyquoTheme.swift`, Views/). The web app re-implements
16+this layer 1:1 on browser `fetch` + `ReadableStream` + `AbortController`; it
17+never re-invents request formats.
18+
19+## 1. Architecture to reproduce
20+
21+Native shape → web shape:
22+
23+| Native (Swift) | Web (TS) |
24+|---|---|
25+| `ProviderID` enum (12 + custom) | `Provider` union type + metadata table (`providers/registry.ts`) |
26+| `WireFormat` (`openAIChatCompletions` \| `anthropicMessages`) | same two-value union |
27+| `ProviderRegistry.client(for:)` | `getClient(model)` — Anthropic → `anthropic.ts`, everything else → `openaiCompatible.ts` |
28+| `OpenAICompatibleClient` (one client, 11 providers + custom) | `providers/openaiCompatible.ts` |
29+| `AnthropicClient` (native Messages API) | `providers/anthropic.ts` |
30+| `StreamingService` + `SSEParser` | `providers/sse.ts` (fetch + ReadableStream line splitter + SSE state machine) |
31+| `ChatRequest` / `ChatEvent` / `ProviderClient` protocol | `providers/types.ts` (`ChatRequest`, `ChatEvent`, `ProviderClient` interface: `streamChat`, `complete`, `listModelIDs`, `testKey`) |
32+| `ModelCatalogData.all` (170 models) | `providers/catalog.ts` (typed, complete port) |
33+| `ProviderError` (typed, human messages) | `ProviderError` class with the same cases + messages |
34+
35+**Key finding:** only TWO wire formats exist. 11 of 12 providers (OpenAI, xAI,
36+Mistral, **Gemini via its OpenAI-compat endpoint**, Qwen/DashScope, DeepSeek,
37+Kimi, Perplexity, Together, DeepInfra, Cerebras) speak OpenAI
38+`/chat/completions`; Anthropic alone speaks `/v1/messages`. The native app has
39+no separate Gemini `generateContent` client — Gemini rides the compat endpoint
40+with `Authorization: Bearer <API key>`, and the web app does the same.
41+
42+## 2. Base URLs & auth (exact, from `ProviderID.defaultBaseURL`)
43+
44+| Provider | Base URL | Auth |
45+|---|---|---|
46+| openai | `https://api.openai.com/v1` | `Authorization: Bearer` |
47+| anthropic | `https://api.anthropic.com/v1` | `x-api-key` + `anthropic-version: 2023-06-01` + **(web only)** `anthropic-dangerous-direct-browser-access: true` |
48+| xai | `https://api.x.ai/v1` | Bearer |
49+| mistral | `https://api.mistral.ai/v1` | Bearer |
50+| gemini | `https://generativelanguage.googleapis.com/v1beta/openai` | Bearer (the API key) |
51+| qwen | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | Bearer |
52+| deepseek | `https://api.deepseek.com` | Bearer |
53+| kimi | `https://api.moonshot.ai/v1` | Bearer |
54+| perplexity | `https://api.perplexity.ai` | Bearer |
55+| together | `https://api.together.xyz/v1` | Bearer |
56+| deepinfra | `https://api.deepinfra.com/v1/openai` | Bearer |
57+| cerebras | `https://api.cerebras.ai/v1` | Bearer |
58+
59+Paths appended to the base: `chat/completions` and `models` (OpenAI-compat);
60+`messages` and `models?limit=100` (Anthropic). Base path components must be
61+preserved when joining (`…/compatible-mode/v1`, `…/v1beta/openai`).
62+Per-provider base-URL override (proxy / Zyquo Router) plugs in exactly where
63+`baseURLOverride` does natively.
64+
65+## 3. OpenAI-compatible client — request construction rules
66+
67+Body fields (all optional unless noted): `model`, `messages`, `stream`,
68+`stream_options.include_usage`, `temperature`, `top_p`, `max_tokens` **or**
69+`max_completion_tokens`, `frequency_penalty`, `presence_penalty`,
70+`reasoning_effort`, `enable_thinking`. Rules ported verbatim:
71+
72+1. **System prompt** becomes the first message with `role: "system"`; stored
73+ messages with role `system` are skipped when building the array.
74+2. **Parameter gating:** a param is included only if the model's
75+ `ParameterSupport` allows it (providers reject unknown params).
76+ `usesMaxCompletionTokens` → send `max_completion_tokens` instead of
77+ `max_tokens` (OpenAI reasoning models, Kimi K-series, Cerebras).
78+3. **`stream_options: {include_usage: true}`** only for: openai, xai, gemini,
79+ deepseek, kimi, together, cerebras, custom. NOT for mistral (rejects unknown
80+ params), qwen/deepinfra/perplexity (usage included automatically).
81+4. **Mistral `reasoning_effort` mapping:** only accepts `"high"`/`"none"` —
82+ map `medium→high`, `low→none`.
83+5. **Qwen/DashScope `enable_thinking`:** only legal on **streaming** requests;
84+ omit on non-streaming.
85+6. **Vision:** user messages with images become
86+ `content: [{type:"text",text}, {type:"image_url", image_url:{url:"data:<mime>;base64,<data>"}}]`;
87+ otherwise `content` is a plain string. Images only on user messages of
88+ vision-capable models.
89+7. **Text-file attachments** are injected inline into the message text, fenced:
90+ ` ```<fileName>\n<contents>\n``` `.
91+8. **`requiresStreaming` models** (Qwen qwq/qvq, several Together-hosted):
92+ non-streaming `complete()` aggregates the stream instead.
93+
94+### Streamed response parsing (SSE `data:` lines, `[DONE]` terminator)
95+
96+Per chunk (`choices[0]`):
97+- `delta.reasoning_content ?? delta.reasoning` → reasoning delta (DeepSeek,
98+ Qwen, xAI, DeepInfra use `reasoning_content`; some use `reasoning`).
99+- `delta.content ?? choices[0].text` → text delta (**Together quirk:** some
100+ models stream completions-style with token text in `choices[].text`).
101+- **Mistral reasoning-model quirk:** `delta.content` may be an ARRAY of chunks
102+ `{type:"thinking"|"text", …}` — `thinking` chunks (each
103+ `{thinking:[{type:"text",text}]}`) join into reasoning; `text` chunks
104+ (`{text}`) join into content.
105+- `finish_reason` → finished reason.
106+- Top-level `citations` (array of URL strings) + `search_results`
107+ (`[{title,url}]`) → Perplexity numbered citations (emit once).
108+- Top-level `usage` (`prompt_tokens`, `completion_tokens`,
109+ `completion_tokens_details.reasoning_tokens`) → usage event.
110+- Malformed / unknown JSON chunks are tolerated and skipped (keep-alives).
111+
112+### `/models` listing quirks
113+- Together returns a **bare array**; everyone else wraps in `{"data": […]}`.
114+- Gemini compat prefixes ids with `models/` — strip it.
115+- Perplexity has **no** `/models` endpoint (`supportsModelListing = false`) —
116+ key testing there does a minimal non-streaming completion (`max_tokens: 16`
117+ minimum — probe-verified) against the provider's cheapest catalog model.
118+
119+## 4. Anthropic client — Messages API rules
120+
121+- POST `…/v1/messages`; headers `x-api-key`, `anthropic-version: 2023-06-01`,
122+ plus browser header (CORS-MATRIX). **`max_tokens` is mandatory** (default
123+ 8192 when the user hasn't set one).
124+- `system` is a top-level string param, never a message.
125+- Message content is block-structured: image blocks
126+ `{type:"image", source:{type:"base64", media_type, data}}` come **before** the
127+ text block; empty text becomes `" "` (API rejects empty).
128+- `thinking`: `{type:"enabled", budget_tokens: 8000}` / `{type:"disabled"}` for
129+ models with `thinkingToggle` (Claude Fable 5 has thinking always-on — no
130+ toggle sent). Claude 4.7+ models don't accept `temperature`/`top_p`
131+ (ParameterSupport encodes this per model).
132+- **Named SSE events** (`event:` field): `message_start` (input usage) →
133+ `content_block_delta` (`delta.text` → text; `delta.thinking` → reasoning) →
134+ `message_delta` (output usage + `stop_reason`) → `message_stop`; `ping` and
135+ `content_block_start/stop` ignored; `error` events raise. Usage is emitted at
136+ stream end (input from start + output from delta).
137+- Non-streaming: `content[]` blocks — join `type=="text"` for text,
138+ `type=="thinking"` for reasoning.
139+
140+## 5. SSE parser + networking (from `SSEParser`/`StreamingService`)
141+
142+- Line-level state machine: accumulate `event:`/`data:` fields; a **blank line**
143+ dispatches the event (multi-`data:` lines join with `\n`); `:` comment lines
144+ are keep-alives (DeepSeek sends `: keep-alive`) — skip; `id:`/`retry:`
145+ ignored; strip one leading space after `data:`; handle `\r\n`; flush a
146+ trailing unterminated event at stream end.
147+- Web implementation: `fetch(url, {signal})` → check `res.ok` (non-2xx: read
148+ full body, map via `ProviderError.from(status, body, provider)`) → pipe
149+ `res.body` through `TextDecoderStream` → split on newlines → feed the parser.
150+- **Error mapping:** 401/403 → invalid key (names the provider); 429 → rate
151+ limited (+ `Retry-After` if present); 400/404/422 → bad request with the
152+ provider's message; else server error. Error bodies are shape-sniffed:
153+ `{error:{message}}`, `{error:"…"}`, `{message}`, `{detail}`, Gemini arrays.
154+ A fetch `TypeError` (no HTTP status) is surfaced as a network/CORS-shaped
155+ error with the proxy/Router hint.
156+- **Retry:** non-streaming POSTs retry ×3 with exponential backoff (4s, 8s) on
157+ 429/5xx, honoring `Retry-After`. Streaming does not auto-retry.
158+- Cancellation: `AbortController` per in-flight generation; abort → typed
159+ `cancelled` error (maps to native `onTermination → task.cancel()`).
160+
161+## 6. Data model (ported to `types/`)
162+
163+- `AIModel`: `id` (exact wire ID), `provider`, `displayName`, `contextWindow`,
164+ `maxOutputTokens?`, `capabilities`, `pricing?`, `parameterSupport`,
165+ `isLegacy`, `isRecommended`, `customBaseURL?`. Context badge: `≥1M → "NM ctx"`,
166+ `≥1K → "NK ctx"`.
167+- `ModelCapabilities`: `vision, tools, reasoning, streaming(=true), jsonMode,
168+ citations` (defaults false).
169+- `ModelPricing`: `inputPerMTok/outputPerMTok` USD; `cost(in,out)` = base-rate
170+ estimate (cached/tiered pricing intentionally simplified; UI labels costs as
171+ estimates).
172+- `ParameterSupport`: `temperature(+), topP(+), frequencyPenalty(−),
173+ presencePenalty(−), usesMaxCompletionTokens(−), reasoningEffort(−),
174+ thinkingToggle(−), requiresStreaming(−)`; preset `openAIDefault` adds both
175+ penalties.
176+- `TokenUsage`: `inputTokens, outputTokens, reasoningTokens?` (+ addition).
177+- `ChatEvent`: `reasoningDelta | textDelta | citations | usage | finished`.
178+- `Citation`: `index (1-based), url, title?` (merge Perplexity `citations` +
179+ `search_results`).
180+
181+## 7. Model catalog (ported to `providers/catalog.ts`)
182+
183+**170 models** ported completely and faithfully from `ModelCatalogData.swift`
184+(NOT from the drifted `docs/research/catalog-summary.md` which claims 195):
185+openai 27, anthropic 11, xai 5, mistral 10, gemini 14, qwen 32, deepseek 2,
186+kimi 12, perplexity 4, together 16, deepinfra 34, cerebras 3. 23 recommended
187+models; legacies badged and ranked last. The full per-model data (IDs, names,
188+context, max output, capabilities, pricing, parameter support, flags) is
189+transcribed 1:1 in `catalog.ts` — that file IS the appendix.
190+
191+Catalog behavior ported from `ModelCatalog.swift`:
192+- Ranking in pickers: favorites (0) → recommended (1) → normal (2) → legacy (3).
193+- **Lookups keyed on `(provider, id)`** — ids duplicate across providers
194+ (e.g. `deepseek-v4-flash` exists under both deepseek and qwen;
195+ `moonshotai/Kimi-K2.6` under together and deepinfra).
196+- `cheapestModel(provider)`: non-legacy, prefer non-reasoning, min
197+ `outputPerMTok` (nil → ∞) — used for key tests and title generation.
198+- `defaultModel`: first recommended in catalog order → `gpt-5.6-sol`.
199+- Dynamic `/models` refresh overlays a `liveModelIDs` set per provider (never
200+ mutates built-ins); unknown live IDs are offered as custom-model candidates.
201+
202+## 8. Design tokens (ported to `design/` as CSS variables — from `ZyquoTheme.swift`)
203+
204+| Token | Light | Dark |
205+|---|---|---|
206+| background | `#FAFBFD` | `#14161E` |
207+| surface | `#FFFFFF` | `#1C1F2A` |
208+| surfaceSecondary | `#F2F4F8` | `#232734` |
209+| accent | `#4E6AF0` | `#6D84F5` |
210+| accentSubtle | `#EBEFFD` | `#28304C` |
211+| textPrimary | `#1A1C22` | `#E8EAF2` |
212+| textSecondary | `#6B7080` | `#9BA1B5` |
213+| textTertiary | `#9EA3B0` | `#6A7188` |
214+| border (0.5px hairlines) | `#E4E7EE` | `#2C3040` |
215+| success / warning / danger | `#2FA36B` / `#D9822B` / `#D64545` | `#43BD83` / `#E59A4D` / `#E36363` |
216+
217+Typography: system font stack (SF Pro on Apple); title 20/semibold; body
218+13.5px default (user-adjustable 12–18), line-height **1.45**; caption 11;
219+code mono 12.5. Spacing 4/8/12/16/20/24/32. Radii 6/10/14. Shadow: soft only
220+(`rgba(0,0,0,0.06)`, blur 12, y 2) on floating elements. Metrics: sidebar 260,
221+chat header 52, max message column 760, hairline 0.5. Motion: hover 80ms,
222+send/message-in 150ms ease-out, pressed scale 0.97.
223+
224+Full native UX study (layout, picker, bubbles, settings, palette, compare,
225+personas, prompt library, store behaviors, brand glyph) → `docs/NATIVE-UX.md`.
226+
227+## 9. Vault → localStorage (explicit difference)
228+
229+The native app stores keys in a machine-bound encrypted vault
230+(`SecureKeyStore`). A browser has no equivalent primitive, so the web edition
231+stores keys in **plain `localStorage`** (`zyquo.cloud.web.keys`), with a clear
232+first-run notice, masked display, and an **opt-in passphrase lock**
233+(WebCrypto AES-GCM, key derived from the passphrase) — honestly framed: once
234+unlocked, a compromised page can still read keys. This is a deliberate
235+bring-your-own-key local-only model, not a secure vault.
added scripts/cors-probe.sh +66 −0
@@ -0,0 +1,66 @@
1+#!/bin/bash
2+#
3+# cors-probe.sh
4+# Zyquo Cloud Web
5+#
6+# Author: Simon-Pierre Boucher
7+# Mail: contact@spboucher.ai
8+#
9+# Phase 0.A — empirical CORS probe for all 12 providers.
10+# For each provider: (1) OPTIONS preflight as a browser would send it,
11+# (2) a real minimal POST with an Origin header, checking the
12+# Access-Control-Allow-* response headers. Keys are read from
13+# .keys.local.json (gitignored) and never printed.
14+#
15+
16+set -u
17+KEYS_FILE="$(dirname "$0")/../.keys.local.json"
18+ORIGIN="https://www.zyquo.cloud"
19+OUT_DIR="${1:-/tmp/zyquo-cors}"
20+mkdir -p "$OUT_DIR"
21+
22+key() { python3 -c "import json,sys; print(json.load(open('$KEYS_FILE'))['$1'])"; }
23+
24+probe() {
25+ local name="$1" url="$2" auth_header="$3" model="$4" extra_header="${5:-}"
26+ local out="$OUT_DIR/$name.txt"
27+ {
28+ echo "=== $name — $url ==="
29+ echo "--- PREFLIGHT (OPTIONS) ---"
30+ local req_headers="authorization,content-type"
31+ [ "$name" = "anthropic" ] && req_headers="x-api-key,anthropic-version,anthropic-dangerous-direct-browser-access,content-type"
32+ curl -sS -o /dev/null -D - -X OPTIONS "$url" \
33+ -H "Origin: $ORIGIN" \
34+ -H "Access-Control-Request-Method: POST" \
35+ -H "Access-Control-Request-Headers: $req_headers" \
36+ --max-time 30 2>&1 | grep -iE "^(HTTP|access-control|allow)" || echo "(no CORS headers in preflight)"
37+ echo "--- POST with Origin ---"
38+ local body='{"model":"'"$model"'","messages":[{"role":"user","content":"Say OK"}],"max_tokens":8}'
39+ [ "$name" = "anthropic" ] && body='{"model":"'"$model"'","max_tokens":8,"messages":[{"role":"user","content":"Say OK"}]}'
40+ curl -sS -o "$OUT_DIR/$name.body.json" -D - -X POST "$url" \
41+ -H "Origin: $ORIGIN" \
42+ -H "Content-Type: application/json" \
43+ -H "$auth_header" \
44+ ${extra_header:+-H "$extra_header"} \
45+ -d "$body" \
46+ --max-time 60 2>&1 | grep -iE "^(HTTP|access-control)" || echo "(no CORS headers in POST)"
47+ echo "--- POST body (first 300 chars) ---"
48+ head -c 300 "$OUT_DIR/$name.body.json"; echo
49+ } > "$out" 2>&1
50+ echo "done: $name"
51+}
52+
53+probe openai "https://api.openai.com/v1/chat/completions" "Authorization: Bearer $(key openai)" "gpt-4.1-nano" &
54+probe anthropic "https://api.anthropic.com/v1/messages" "x-api-key: $(key anthropic)" "claude-haiku-4-5-20251001" "anthropic-version: 2023-06-01" &
55+probe xai "https://api.x.ai/v1/chat/completions" "Authorization: Bearer $(key xai)" "grok-3-mini" &
56+probe mistral "https://api.mistral.ai/v1/chat/completions" "Authorization: Bearer $(key mistral)" "mistral-small-latest" &
57+probe gemini "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions" "Authorization: Bearer $(key gemini)" "gemini-2.5-flash-lite" &
58+probe qwen "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions" "Authorization: Bearer $(key qwen)" "qwen-turbo" &
59+probe deepseek "https://api.deepseek.com/chat/completions" "Authorization: Bearer $(key deepseek)" "deepseek-chat" &
60+probe kimi "https://api.moonshot.ai/v1/chat/completions" "Authorization: Bearer $(key kimi)" "kimi-k2-0711-preview" &
61+probe perplexity "https://api.perplexity.ai/chat/completions" "Authorization: Bearer $(key perplexity)" "sonar" &
62+probe together "https://api.together.xyz/v1/chat/completions" "Authorization: Bearer $(key together)" "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo" &
63+probe deepinfra "https://api.deepinfra.com/v1/openai/chat/completions" "Authorization: Bearer $(key deepinfra)" "meta-llama/Meta-Llama-3.1-8B-Instruct" &
64+probe cerebras "https://api.cerebras.ai/v1/chat/completions" "Authorization: Bearer $(key cerebras)" "llama3.1-8b" &
65+wait
66+echo "All probes complete → $OUT_DIR"
67
Menu

Theme