phase0: provider reuse study, CORS matrix (empirical + documented), native UX reference
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Showing 7 changed files with +932 and −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 | ||