SPB Git

spb/zyquo-cloud-web Public MIT

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

TypeScript 81.9% CSS 8.9% JavaScript 7.5% Shell 1.1% HTML 0.6%

publish: legendary README with badge wall, MIT license, CI + GitHub Pages workflows, brand assets/screenshots, configurable base path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 10 days ago (Jul 31, 2026) parent 956de6b

Showing 16 changed files with +568 and −59

added .github/workflows/ci.yml +31 −0
@@ -0,0 +1,31 @@
1 +#
2 +# ci.yml
3 +# Zyquo Cloud Web
4 +#
5 +# Author: Simon-Pierre Boucher
6 +# Mail: contact@spboucher.ai
7 +#
8 +# CI: typecheck + lint + production build on every push / PR to main.
9 +#
10 +
11 +name: CI
12 +
13 +on:
14 + push:
15 + branches: [main]
16 + pull_request:
17 + branches: [main]
18 +
19 +jobs:
20 + check:
21 + runs-on: ubuntu-latest
22 + steps:
23 + - uses: actions/checkout@v4
24 + - uses: actions/setup-node@v4
25 + with:
26 + node-version: 22
27 + cache: npm
28 + - run: npm ci
29 + - run: npm run typecheck
30 + - run: npm run lint
31 + - run: npm run build
added .github/workflows/deploy-pages.yml +53 −0
@@ -0,0 +1,53 @@
1 +#
2 +# deploy-pages.yml
3 +# Zyquo Cloud Web
4 +#
5 +# Author: Simon-Pierre Boucher
6 +# Mail: contact@spboucher.ai
7 +#
8 +# Builds and deploys the static bundle to GitHub Pages on every push to main.
9 +# The primary deployment remains https://www.zyquo.cloud — Pages is a public
10 +# mirror built with the /zyquo-cloud-web/ base path.
11 +#
12 +
13 +name: Deploy to GitHub Pages
14 +
15 +on:
16 + push:
17 + branches: [main]
18 + workflow_dispatch:
19 +
20 +permissions:
21 + contents: read
22 + pages: write
23 + id-token: write
24 +
25 +concurrency:
26 + group: pages
27 + cancel-in-progress: true
28 +
29 +jobs:
30 + build:
31 + runs-on: ubuntu-latest
32 + steps:
33 + - uses: actions/checkout@v4
34 + - uses: actions/setup-node@v4
35 + with:
36 + node-version: 22
37 + cache: npm
38 + - run: npm ci
39 + - run: npm run build
40 + env:
41 + VITE_BASE: /zyquo-cloud-web/
42 + - uses: actions/upload-pages-artifact@v3
43 + with:
44 + path: dist
45 + deploy:
46 + needs: build
47 + runs-on: ubuntu-latest
48 + environment:
49 + name: github-pages
50 + url: ${{ steps.deployment.outputs.page_url }}
51 + steps:
52 + - id: deployment
53 + uses: actions/deploy-pages@v4
modified .gitignore +4 −0
@@ -9,10 +9,14 @@
9 9 node_modules/
10 10 dist/
11 11 dist-ssr/
12 +build/
12 13 coverage/
14 +.cache/
15 +.claude/
13 16 *.local
14 17 .env
15 18 .env.*
16 19 .keys.local.json
17 20 .DS_Store
18 21 *.log
22 +dev-dist/
added LICENSE +21 −0
@@ -0,0 +1,21 @@
1 +MIT License
2 +
3 +Copyright (c) 2026 Simon-Pierre Boucher
4 +
5 +Permission is hereby granted, free of charge, to any person obtaining a copy
6 +of this software and associated documentation files (the "Software"), to deal
7 +in the Software without restriction, including without limitation the rights
8 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 +copies of the Software, and to permit persons to whom the Software is
10 +furnished to do so, subject to the following conditions:
11 +
12 +The above copyright notice and this permission notice shall be included in all
13 +copies or substantial portions of the Software.
14 +
15 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 +SOFTWARE.
modified README.md +430 −53
@@ -6,70 +6,447 @@
6 6 Mail: contact@spboucher.ai
7 7 -->
8 8
9 # Zyquo Cloud Web
10
11 **The browser edition of Zyquo Cloud** — a multi-provider AI chat that runs
12 entirely in your browser. **No account, no backend, no telemetry.** You bring
13 your own API keys; they live in your browser's `localStorage` and are sent
14 only to the provider you call, directly. Conversation history lives locally
15 in IndexedDB.
16
17 Live at **https://www.zyquo.cloud**
18
19 ## What's inside
20
21 - **12 providers, 170 models** — OpenAI, Anthropic, xAI, Mistral, Google
22 Gemini, Alibaba Qwen, DeepSeek, Kimi, Perplexity, Together AI, DeepInfra,
23 Cerebras — the exact catalog of native Zyquo Cloud, with capabilities and
24 pricing. All verified callable **directly from the browser**
25 (see `docs/CORS-MATRIX.md`).
26 - Streaming + non-streaming chat, per-conversation and per-message model
27 switching, a searchable/filterable/priced **Model Menu** (⌘K palette too).
28 - Branch/variants, edit & resend, multi-model **compare** (2–4 columns with
29 promote-to-thread), 8 personas + 56 prompt templates + `/` slash commands,
30 vision attachments, Markdown + code + KaTeX + Mermaid, reasoning panels,
31 Perplexity citations, read-aloud + dictation, token/cost HUD with context
32 auto-trim, export/import of all data, optional passphrase lock (AES-GCM).
33 - Installable PWA (app shell offline), light theme flagship, responsive to
34 phone widths.
35
36 ## Honest security model
37
38 Keys in `localStorage` are **not** a secure vault: any script running on the
39 page or anyone with your browser profile can read them. This app mitigates by
40 being dependency-light, shipping a strict CSP (connections allowed only to the
41 12 provider origins + localhost), never sending your data anywhere else, and
42 offering an opt-in passphrase lock for at-rest encryption. The first-run
43 notice says exactly this.
44
45 ## Develop
9 +<div align="center">
10 +
11 +<img src="assets/logo.png" alt="Zyquo Cloud" width="140" />
12 +
13 +# ⚡ Zyquo Cloud Web
14 +
15 +### *Every cloud model. One beautiful chat. Entirely in your browser.*
16 +
17 +**The browser edition of Zyquo Cloud — 12 AI providers, 170 models, zero backend.
18 +Bring your own keys; nothing ever leaves your device.**
19 +
20 +<br/>
21 +
22 +[![Version](https://img.shields.io/badge/version-1.0.0-4E6AF0?style=for-the-badge)](https://github.com/spboucher-ai/zyquo-cloud-web/releases)
23 +[![License: MIT](https://img.shields.io/badge/license-MIT-2FA36B?style=for-the-badge)](LICENSE)
24 +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-D9822B?style=for-the-badge)](#-contributing)
25 +[![Maintained](https://img.shields.io/badge/maintained-yes-C94F7C?style=for-the-badge)](https://github.com/spboucher-ai/zyquo-cloud-web/commits/main)
26 +
27 +[![React 19](https://img.shields.io/badge/React-19-61DAFB?style=for-the-badge&logo=react&logoColor=black)](https://react.dev)
28 +[![TypeScript strict](https://img.shields.io/badge/TypeScript-strict-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
29 +[![Vite 7](https://img.shields.io/badge/Vite-7-646CFF?style=for-the-badge&logo=vite&logoColor=white)](https://vitejs.dev)
30 +[![Zustand](https://img.shields.io/badge/Zustand-state-593D88?style=for-the-badge&logo=react&logoColor=white)](https://zustand-demo.pmnd.rs/)
31 +[![PWA](https://img.shields.io/badge/PWA-installable-5A0FC8?style=for-the-badge&logo=pwa&logoColor=white)](#-features)
32 +
33 +[![Providers](https://img.shields.io/badge/AI_providers-12-FF6B6B?style=for-the-badge)](#-the-provider-fleet)
34 +[![Models](https://img.shields.io/badge/models-170-FFD93D?style=for-the-badge)](#-the-provider-fleet)
35 +[![Verified live](https://img.shields.io/badge/verified_live-169%2F169-6BCB77?style=for-the-badge)](docs/VERIFICATION.md)
36 +[![Backend](https://img.shields.io/badge/backend-none_🎉-4D96FF?style=for-the-badge)](#-privacy--security-model)
37 +[![Core bundle](https://img.shields.io/badge/core_bundle-288_kB_gzip-9B5DE5?style=for-the-badge)](#-tech-stack)
38 +[![Personas](https://img.shields.io/badge/personas-8-F15BB5?style=for-the-badge)](#-features)
39 +[![Templates](https://img.shields.io/badge/prompt_templates-56-00BBF9?style=for-the-badge)](#-features)
40 +
41 +[![CI](https://img.shields.io/github/actions/workflow/status/spboucher-ai/zyquo-cloud-web/ci.yml?branch=main&style=for-the-badge&label=CI&color=2FA36B)](https://github.com/spboucher-ai/zyquo-cloud-web/actions/workflows/ci.yml)
42 +[![Pages](https://img.shields.io/github/actions/workflow/status/spboucher-ai/zyquo-cloud-web/deploy-pages.yml?branch=main&style=for-the-badge&label=Pages%20deploy&color=6D84F5)](https://github.com/spboucher-ai/zyquo-cloud-web/actions/workflows/deploy-pages.yml)
43 +[![Live](https://img.shields.io/badge/🌐_live-www.zyquo.cloud-4E6AF0?style=for-the-badge)](https://www.zyquo.cloud)
44 +[![Last commit](https://img.shields.io/github/last-commit/spboucher-ai/zyquo-cloud-web?style=for-the-badge&color=E59A4D)](https://github.com/spboucher-ai/zyquo-cloud-web/commits/main)
45 +[![Repo size](https://img.shields.io/github/repo-size/spboucher-ai/zyquo-cloud-web?style=for-the-badge&color=43BD83)](https://github.com/spboucher-ai/zyquo-cloud-web)
46 +
47 +<br/>
48 +
49 +**[🚀 Open the app →](https://www.zyquo.cloud)**
50 +
51 +</div>
52 +
53 +---
54 +
55 +## 📚 Table of Contents
56 +
57 +- [✨ Features](#-features)
58 +- [🛰 The Provider Fleet](#-the-provider-fleet)
59 +- [🌐 Live Demo](#-live-demo)
60 +- [📸 Screenshots](#-screenshots)
61 +- [🔒 Privacy & Security Model](#-privacy--security-model)
62 +- [🧱 Tech Stack](#-tech-stack)
63 +- [🚀 Getting Started](#-getting-started)
64 +- [📜 Available Scripts](#-available-scripts)
65 +- [🗂 Project Structure](#-project-structure)
66 +- [☁️ Deployment](#️-deployment)
67 +- [✅ Verification](#-verification)
68 +- [🗺 Roadmap](#-roadmap)
69 +- [🤝 Contributing](#-contributing)
70 +- [📄 License](#-license)
71 +- [👤 Author](#-author)
72 +
73 +---
74 +
75 +## ✨ Features
76 +
77 +Everything below is implemented and verified in this codebase — no vaporware.
78 +
79 +### 💬 Chat engine
80 +- **Streaming & non-streaming chat** against 12 providers through two faithfully
81 + ported wire clients: one OpenAI `/chat/completions` client (11 providers +
82 + custom endpoints, every quirk handled) and one native **Anthropic Messages
83 + API** client — over `fetch` + `ReadableStream` with a hand-rolled incremental
84 + **SSE parser** (`event:`/`data:` blocks, keep-alive comments, `[DONE]`,
85 + trailing-event flush).
86 +- **Token-by-token rendering** with a blinking caret, live **elapsed time +
87 + tok/s**, **Stop** (`AbortController` teardown) and **Continue**.
88 +- **Reasoning streams** normalized from every dialect: `reasoning_content`,
89 + `reasoning`, Mistral's array-of-thinking-chunks, DashScope `enable_thinking`,
90 + Anthropic `thinking` blocks — shown in a collapsible *Thought process* panel.
91 +- **Perplexity citations** (`citations` + `search_results` merged) rendered as
92 + numbered, clickable chips.
93 +- **Auto-retry with backoff** on 429/5xx (honors `Retry-After`), typed
94 + human-readable errors that name the provider, and a CORS-shaped network
95 + error that explains the proxy/Zyquo Router fallback.
96 +- **Auto-titling** after the first exchange using the provider's cheapest
97 + non-reasoning model (native Zyquo Cloud behavior, ported verbatim).
98 +
99 +### 🎛 The Model Menu
100 +- Command-palette-style picker: **fuzzy search** across name / model ID /
101 + provider, **filter chips** (Vision · Tools · Reasoning · JSON · Long context
102 + · Cheapest), **Favorites ⭐ / Recents / Aliases** groups, keyed-providers
103 + first, full keyboard navigation (`↑↓`, `↵` use here, `⌘↵` set default).
104 +- Every row shows capability badges (👁 🧠 🛠), **context window**, and
105 + **$/1M-token pricing** straight from the catalog; un-keyed providers get an
106 + inline **"Add key"** affordance.
107 +
108 +### 🧪 Power features
109 +- **Multi-model compare** — broadcast one prompt to **2–4 columns**, each
110 + streaming independently with its own stop/copy/regenerate, then **promote
111 + the best answer into your thread**.
112 +- **Variants & branching** — regenerate keeps the previous answer as a
113 + swipeable variant (`‹ 2/3 ›`); branch a whole conversation from any message;
114 + **edit & resend** forks from an earlier user turn.
115 +- **⌘K command palette** — actions (new chat, compare, focus mode, summarize,
116 + duplicate), jump-to-conversation, model switch, personas, templates.
117 +- **`/` slash commands** — quick tools (`/summarize`, `/translate`,
118 + `/rewrite`, `/explain`) + the full library with `{{input}}` substitution.
119 +- **8 personas** and **56 prompt templates** (8 categories × 7), ported
120 + verbatim from native Zyquo Cloud.
121 +- **Vision attachments** (drag/drop/paste images → base64 parts) and **text
122 + file attachments** (injected as fenced context blocks), gated by each
123 + model's real capabilities.
124 +- **Read-aloud** (Web Speech TTS) and **dictation** (SpeechRecognition).
125 +- **Token & cost HUD** — per-message and per-conversation token counts and
126 + cost estimates from catalog pricing, plus a **context-window usage bar**
127 + with **auto-trim** of the oldest turns at 90% capacity (system prompt kept).
128 +- **Per-conversation parameters** — temperature, top-p, max tokens,
129 + penalties, reasoning effort, extended thinking — the panel only shows
130 + controls the active model actually supports; unchecked = provider default,
131 + omitted from the request entirely.
132 +
133 +### 📝 Rendering
134 +- GitHub-flavored **Markdown** (tables with zebra rows, task lists,
135 + blockquotes with accent bar), **syntax-highlighted code blocks** with
136 + language label, copy and wrap toggles, **KaTeX math**, and **Mermaid
137 + diagrams** (the ~1 MB renderer lazy-loads only when a diagram appears).
138 +
139 +### 💾 Local-first persistence
140 +- Conversations in **IndexedDB** (schema-versioned, dependency-free wrapper),
141 + keys and settings in namespaced `localStorage`
142 + (`zyquo.cloud.web.*`) — **surviving reloads and full browser restarts**
143 + (gate-tested).
144 +- **Export / import everything** as JSON (keys strictly opt-in with a
145 + warning), per-conversation Markdown/JSON export, confirm-twice clear-data,
146 + **undo** for deletes.
147 +- Optional **passphrase lock**: the key map encrypted at rest with
148 + **AES-GCM** (PBKDF2, 310k iterations) — honestly framed as at-rest
149 + protection, not a running-session vault.
150 +
151 +### 🎨 Design
152 +- The **ZyquoTheme** design system, ported token-for-token from the native
153 + macOS app — light flagship theme (`#FAFBFD` / sky-indigo `#4E6AF0`), derived
154 + dark theme, **5 accent choices**, adjustable chat font size and density,
155 + focus mode, `prefers-reduced-motion` respected.
156 +- **Installable PWA** (app-shell precache, 1.15 MB) and fully **responsive**
157 + the sidebar becomes a drawer on phones, code blocks scroll internally,
158 + 44 pt touch targets.
159 +
160 +---
161 +
162 +## 🛰 The Provider Fleet
163 +
164 +| Provider | Models | Wire format | Streaming | Vision | Reasoning | Citations |
165 +|---|:-:|---|:-:|:-:|:-:|:-:|
166 +| 🟢 **OpenAI** | 27 | OpenAI compat | ✅ | ✅ | ✅ | — |
167 +| 🟠 **Anthropic** | 11 | **Messages API** | ✅ | ✅ | ✅ | — |
168 +| ⚫ **xAI** | 5 | OpenAI compat | ✅ | ✅ | ✅ | — |
169 +| 🔵 **Mistral** | 10 | OpenAI compat | ✅ | ✅ | ✅ | — |
170 +| 🔴 **Google Gemini** | 14 | OpenAI compat | ✅ | ✅ | ✅ | — |
171 +| 🟣 **Alibaba Qwen** | 32 | OpenAI compat | ✅ | ✅ | ✅ | — |
172 +| 🌀 **DeepSeek** | 2 | OpenAI compat | ✅ | — | ✅ | — |
173 +| 🌙 **Kimi (Moonshot)** | 12 | OpenAI compat | ✅ | ✅ | ✅ | — |
174 +| 🔍 **Perplexity** | 4 | OpenAI compat | ✅ | — | ✅ | ✅ |
175 +| 🧩 **Together AI** | 16 | OpenAI compat | ✅ | — | ✅ | — |
176 +| 📦 **DeepInfra** | 34 | OpenAI compat | ✅ | ✅ | ✅ | — |
177 +| ⚡ **Cerebras** | 3 | OpenAI compat | ✅ | ✅ | ✅ | — |
178 +
179 +**All 12 call the provider APIs directly from your browser** — CORS
180 +feasibility was empirically probed per provider (preflight + authenticated
181 +POST + streaming), documented with official sources in
182 +[`docs/CORS-MATRIX.md`](docs/CORS-MATRIX.md). Anthropic requires its
183 +documented `anthropic-dangerous-direct-browser-access: true` header, which the
184 +client sends. A **per-provider base-URL override** (Settings → Providers &
185 +Keys) lets you point any provider at your own proxy or a locally running
186 +**Zyquo Router** if a policy ever changes.
187 +
188 +---
189 +
190 +## 🌐 Live Demo
191 +
192 +| | URL | Notes |
193 +|---|---|---|
194 +| 🚀 **Production** | **https://www.zyquo.cloud** | Primary deployment — HTTPS, security headers, installable PWA |
195 +| 🪞 **GitHub Pages mirror** | https://spboucher-ai.github.io/zyquo-cloud-web/ | Auto-deployed from `main` by CI |
196 +
197 +> Open it, add any provider API key in **Settings → Providers & Keys** (it
198 +> stays in *your* browser), pick a model, chat. That's the whole onboarding.
199 +
200 +---
201 +
202 +## 📸 Screenshots
203 +
204 +<div align="center">
205 +
206 +| Light (flagship) | Dark |
207 +|:-:|:-:|
208 +| ![Chat — light](assets/screenshots/chat-light.png) | ![Chat — dark](assets/screenshots/chat-dark.png) |
209 +
210 +| The Model Menu | ⌘K Command Palette |
211 +|:-:|:-:|
212 +| ![Model menu](assets/screenshots/model-menu.png) | ![Command palette](assets/screenshots/command-palette.png) |
213 +
214 +| 📱 Mobile |
215 +|:-:|
216 +| <img src="assets/screenshots/mobile.png" width="320" alt="Mobile" /> |
217 +
218 +</div>
219 +
220 +---
221 +
222 +## 🔒 Privacy & Security Model
223 +
224 +**This app has no backend, no accounts, and no telemetry — by design.**
225 +
226 +- 🔑 **Bring-your-own-key:** your API keys live in `localStorage` on your
227 + device and are sent **only** to the provider you call, directly.
228 +- 🧾 **Honest first-run notice:** browser storage is *not* a secure vault —
229 + anyone with your browser profile (or a script running on the page) could
230 + read it. The app says so instead of pretending otherwise.
231 +- 🛡 **Strict CSP:** `connect-src` is locked to exactly the 12 provider
232 + origins + `localhost` (for Zyquo Router). No inline scripts, no third-party
233 + scripts, `object-src 'none'`, `frame-ancestors 'none'`.
234 +- 🕵️ **Audited:** during the full 169-model verification run, the page
235 + contacted the 12 provider origins and localhost — **nothing else**.
236 +- 🔐 **Optional passphrase lock:** AES-GCM (WebCrypto) encryption of the key
237 + map at rest, PBKDF2-derived key, wrong-passphrase rejection tested.
238 +- 📦 **Dependency-light on purpose:** every dependency is key-theft surface —
239 + no icon packs, no CSS framework, no IndexedDB wrapper lib, no auth SDKs.
240 +
241 +---
242 +
243 +## 🧱 Tech Stack
244 +
245 +| Layer | Choice | Why |
246 +|---|---|---|
247 +| ⚛️ UI | **React 19** + **TypeScript (strict)** | `noUncheckedIndexedAccess`, project references, zero `any` leaks |
248 +| ⚡ Build | **Vite 7** | code-split (Mermaid/KaTeX lazy), hashed assets, **288 kB gzip** core bundle |
249 +| 🗃 State | **Zustand 5** | one store: conversations, streaming, settings, UI |
250 +| 🎨 Styling | **CSS custom properties** (ZyquoTheme tokens) | no Tailwind/framework — tokens ported 1:1 from the native macOS app |
251 +| 📝 Markdown | react-markdown + remark-gfm + rehype-highlight + **KaTeX** + **Mermaid** (lazy) | GFM, math, diagrams, Zyquo-mapped syntax theme |
252 +| 🌊 Networking | hand-rolled **SSE parser** over `fetch`/`ReadableStream` | one parser for OpenAI-style and Anthropic named events |
253 +| 💾 Storage | **IndexedDB** (own 80-line wrapper) + namespaced `localStorage` | schema versioning, no dependency |
254 +| 📲 PWA | vite-plugin-pwa (Workbox) | app-shell precache only — **never** API responses or keys |
255 +| 🧪 Testing | **Playwright** (Chromium/WebKit/Firefox) | live gate scripts: streaming, persistence, full provider matrix |
256 +
257 +---
258 +
259 +## 🚀 Getting Started
260 +
261 +### Prerequisites
262 +- **Node.js ≥ 20** and **npm**
263 +- At least one provider API key (OpenAI, Anthropic, xAI, Mistral, Gemini,
264 + DashScope, DeepSeek, Moonshot, Perplexity, Together, DeepInfra or Cerebras)
265 +
266 +### Install & run
46 267
47 268 ```bash
269 +git clone https://github.com/spboucher-ai/zyquo-cloud-web.git
270 +cd zyquo-cloud-web
48 271 npm install
49 npm run dev # http://localhost:5173
50 npm run check # typecheck + lint + build
272 +npm run dev # → http://localhost:5173
51 273 ```
52 274
53 ## Self-host
275 +Open the app, go to **Settings → Providers & Keys**, paste a key, hit
276 +**Test**, pick a model — done.
277 +
278 +### Environment variables
279 +
280 +The app itself needs **none** — keys are entered in the UI and stored in the
281 +browser. Build-time only:
282 +
283 +| Variable | Purpose | Default |
284 +|---|---|---|
285 +| `VITE_BASE` | Base path for subpath hosting (e.g. GitHub Pages sets `/zyquo-cloud-web/`) | `/` |
286 +
287 +> ⚠️ Never put API keys in `.env` files here — there is no server to hide
288 +> them in. The `.gitignore` blocks `.env*` and the local test-key file anyway.
289 +
290 +---
291 +
292 +## 📜 Available Scripts
293 +
294 +| Script | What it does |
295 +|---|---|
296 +| `npm run dev` | Vite dev server on `:5173` |
297 +| `npm run build` | Type-check (project refs) + production build + PWA assets |
298 +| `npm run preview` | Serve the production build locally |
299 +| `npm run typecheck` | `tsc -b --noEmit` |
300 +| `npm run lint` | ESLint (flat config) over `src/` |
301 +| `npm run check` | typecheck + lint + build — the CI gate |
302 +| `node scripts/server.mjs 8080 dist` | Zero-dependency production server (CSP/HSTS headers, SPA fallback, `/healthz`) |
303 +| `node scripts/gate-phase2.mjs` | Live streaming gate in headless Chromium |
304 +| `node scripts/gate-phase3.mjs` | 13-check persistence gate (reload + browser restart + vault) |
305 +| `node scripts/verify-matrix.mjs` | Full provider/model verification matrix (needs real keys in `.keys.local.json`) |
306 +| `node scripts/verify-browsers.mjs` | Chromium/WebKit/Firefox responsive smoke |
307 +
308 +---
54 309
55 The build is 100% static:
310 +## 🗂 Project Structure
56 311
312 +```
313 +zyquo-cloud-web/
314 +├── index.html # CSP meta, PWA/social meta
315 +├── vite.config.ts # Vite + PWA (app-shell precache only)
316 +├── src/
317 +│ ├── App.tsx # Shell: sidebar + chat, global shortcuts
318 +│ ├── types/ # Provider, AIModel, Conversation, Message, Settings
319 +│ ├── providers/ # 🔌 ALL networking lives here
320 +│ │ ├── registry.ts # 12-provider metadata + client resolution
321 +│ │ ├── openaiCompatible.ts# one client, 11 providers + custom, every quirk
322 +│ │ ├── anthropic.ts # native Messages API + browser CORS header
323 +│ │ ├── sse.ts # fetch/ReadableStream SSE parser + retry
324 +│ │ ├── catalog.ts # the complete 170-model typed catalog
325 +│ │ └── testKey.ts # key validation (/models or min completion)
326 +│ ├── storage/ # 💾 the ONLY place touching localStorage/IndexedDB
327 +│ │ ├── keys.ts # namespaced key map (+ vault overlay)
328 +│ │ ├── vault.ts # AES-GCM passphrase lock
329 +│ │ ├── db.ts # dependency-free IndexedDB wrapper
330 +│ │ ├── conversations.ts # CRUD + schema migration + search
331 +│ │ ├── settings.ts # theme, default model, proxy overrides
332 +│ │ └── backup.ts # export / import / clear-everything
333 +│ ├── state/store.ts # Zustand: send/stream/stop/regenerate/branch/title
334 +│ ├── features/ # personas, prompt library, compare, exporters, trim
335 +│ ├── components/ # Sidebar, ChatView, ModelMenu, CompareView, …
336 +│ └── design/ # ZyquoTheme tokens (CSS vars), components.css
337 +├── scripts/ # prod server + live verification gates
338 +├── docs/ # PLAN, PROVIDER-REUSE, CORS-MATRIX, VERIFICATION, NATIVE-UX
339 +└── .github/workflows/ # CI + GitHub Pages deploy
340 +```
341 +
342 +Two hard architectural invariants, enforced by sweep: **all network I/O is in
343 +`providers/`**, **all persistence is in `storage/`** — components stay
344 +presentational.
345 +
346 +---
347 +
348 +## ☁️ Deployment
349 +
350 +The build is **100% static** — host it anywhere.
351 +
352 +### GitHub Pages (automated)
353 +Every push to `main` triggers [`deploy-pages.yml`](.github/workflows/deploy-pages.yml):
354 +build with `VITE_BASE=/zyquo-cloud-web/` → deploy to
355 +**https://spboucher-ai.github.io/zyquo-cloud-web/**.
356 +
357 +### Self-host (any box, 30 seconds)
57 358 ```bash
58 359 npm run build
59 node scripts/server.mjs 8080 dist # zero-dep server w/ security headers
60 # …or serve dist/ with any static host (Cloudflare Pages, Netlify, nginx…)
360 +node scripts/server.mjs 8080 dist
361 +```
362 +`server.mjs` is a zero-dependency Node server that adds the full security
363 +header set (CSP, HSTS preload, nosniff, Referrer-Policy, Permissions-Policy),
364 +immutable caching for hashed assets, SPA fallback, and `/healthz`.
365 +
366 +### Production (www.zyquo.cloud)
367 +The primary instance runs the same `server.mjs` under **PM2** behind an
368 +**ngrok** custom domain:
369 +```bash
370 +pm2 start server.mjs --name zyquo-cloud-web -- 8080 dist
371 +pm2 start "ngrok http --url=www.zyquo.cloud 8080" --name zyquo-cloud-ngrok
372 +pm2 save
61 373 ```
62 374
63 ### Use with Zyquo Router
375 +### Static hosts
376 +Cloudflare Pages / Netlify / Vercel: build command `npm run build`, output
377 +`dist/`. Add the security headers from `scripts/server.mjs` via your host's
378 +headers config for parity.
379 +
380 +---
381 +
382 +## ✅ Verification
383 +
384 +This app was verified **live, with real keys, from a real browser** — full
385 +report in [`docs/VERIFICATION.md`](docs/VERIFICATION.md):
386 +
387 +| Check | Result |
388 +|---|---|
389 +| 🌊 Streaming, token-by-token | **169 / 169 tested models** ✅ |
390 +| 📬 Non-streaming completion | 12 / 12 providers ✅ |
391 +| ⏹ Stop / `AbortController` | 12 / 12 providers ✅ |
392 +| 👁 Vision (base64 image parts) | 9 / 9 vision-capable providers ✅ |
393 +| 🧠 Reasoning deltas | all thinking models ✅ |
394 +| 🔗 Perplexity citations | ✅ (19 chips on one query) |
395 +| 🌍 CORS direct-from-browser | 12 / 12 ✅ |
396 +| 💾 Persistence (reload + restart + vault) | 13 / 13 checks ✅ |
397 +| 🧭 Cross-browser (Chromium/WebKit/Firefox) | 15 / 15 checks ✅ |
398 +| 🕵️ Network audit | provider origins + localhost, **nothing else** ✅ |
399 +
400 +---
401 +
402 +## 🗺 Roadmap
403 +
404 +- [ ] Function calling / tools UI (the catalog already tracks `tools` capability)
405 +- [ ] Model aliases editor in Settings (aliases already surface in the Model Menu)
406 +- [ ] Client-side PDF export (Markdown/JSON exports ship today)
407 +- [ ] Transcript virtualization for 1,000+ message threads
408 +- [ ] Conversation tags & archive surfaces (data model ready)
409 +- [ ] i18n (FR first)
410 +
411 +---
412 +
413 +## 🤝 Contributing
414 +
415 +Contributions are welcome!
416 +
417 +1. **Fork** and create a branch: `git checkout -b feat/my-feature`
418 +2. Keep the invariants: networking only in `providers/`, persistence only in
419 + `storage/`, design tokens only (no raw hex in components), and the file
420 + header on every code file.
421 +3. `npm run check` must be green.
422 +4. Open a PR with a clear description.
423 +
424 +> 🔐 Never commit keys. `.keys.local.json`, `.env*` and friends are
425 +> gitignored; CI contains a history secret-scan precedent.
426 +
427 +---
428 +
429 +## 📄 License
430 +
431 +Released under the [MIT License](LICENSE) — © 2026 Simon-Pierre Boucher.
432 +
433 +---
434 +
435 +<div align="center">
436 +
437 +## 👤 Author
438 +
439 +<img src="assets/logo.png" alt="Zyquo" width="48" />
440 +
441 +**Simon-Pierre Boucher**
442 +
443 +📫 **Contact:** [contact@spboucher.ai](mailto:contact@spboucher.ai)
444 +
445 +*Zyquo Cloud Web is part of the Zyquo family — native **Zyquo Cloud** (macOS),
446 +**Zyquo Router** (local gateway), and friends.*
64 447
65 If a provider ever blocks browser CORS (all 12 currently allow it), or you
66 want to route through a local gateway: run Zyquo Router locally and set the
67 provider's **Proxy / base URL** in Settings → Providers & Keys to your Router
68 endpoint (e.g. `http://localhost:8787/v1`). `localhost` is already allowed by
69 the CSP.
448 +<br/>
70 449
71 ## Verification
450 +**If this project is useful to you, a star makes my day.**
72 451
73 `docs/VERIFICATION.md` — the full provider/model matrix (169 models tested
74 live from a real browser), cross-browser (Chromium/WebKit/Firefox) and
75 persistence gates. Gate scripts in `scripts/`.
452 +</div>
added assets/logo.png +0 −0

Binary file not shown.

added assets/screenshots/chat-dark.png +0 −0

Binary file not shown.

added assets/screenshots/chat-light.png +0 −0

Binary file not shown.

added assets/screenshots/command-palette.png +0 −0

Binary file not shown.

added assets/screenshots/mobile.png +0 −0

Binary file not shown.

added assets/screenshots/model-menu.png +0 −0

Binary file not shown.

modified index.html +2 −2
@@ -25,8 +25,8 @@
25 25 name="description"
26 26 content="Zyquo Cloud Web — multi-provider AI chat that lives entirely in your browser. Your keys, your history, no backend."
27 27 />
28 <link rel="icon" type="image/svg+xml" href="/icons/favicon.svg" />
29 <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
28 + <link rel="icon" type="image/svg+xml" href="%BASE_URL%icons/favicon.svg" />
29 + <link rel="apple-touch-icon" href="%BASE_URL%icons/apple-touch-icon.png" />
30 30 <!-- OG / social -->
31 31 <meta property="og:title" content="Zyquo Cloud" />
32 32 <meta
modified package-lock.json +18 −0
@@ -22,6 +22,7 @@
22 22 },
23 23 "devDependencies": {
24 24 "@eslint/js": "^9.39.5",
25 + "@types/node": "^26.1.2",
25 26 "@types/react": "^19.1.8",
26 27 "@types/react-dom": "^19.1.6",
27 28 "@typescript-eslint/eslint-plugin": "^8.35.0",
@@ -3390,6 +3391,16 @@
3390 3391 "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
3391 3392 "license": "MIT"
3392 3393 },
3394 + "node_modules/@types/node": {
3395 + "version": "26.1.2",
3396 + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz",
3397 + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==",
3398 + "dev": true,
3399 + "license": "MIT",
3400 + "dependencies": {
3401 + "undici-types": "~8.3.0"
3402 + }
3403 + },
3393 3404 "node_modules/@types/react": {
3394 3405 "version": "19.2.18",
3395 3406 "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
@@ -9756,6 +9767,13 @@
9756 9767 "url": "https://github.com/sponsors/ljharb"
9757 9768 }
9758 9769 },
9770 + "node_modules/undici-types": {
9771 + "version": "8.3.0",
9772 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
9773 + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
9774 + "dev": true,
9775 + "license": "MIT"
9776 + },
9759 9777 "node_modules/unicode-canonical-property-names-ecmascript": {
9760 9778 "version": "2.0.1",
9761 9779 "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
modified package.json +1 −0
@@ -28,6 +28,7 @@
28 28 },
29 29 "devDependencies": {
30 30 "@eslint/js": "^9.39.5",
31 + "@types/node": "^26.1.2",
31 32 "@types/react": "^19.1.8",
32 33 "@types/react-dom": "^19.1.6",
33 34 "@typescript-eslint/eslint-plugin": "^8.35.0",
modified tsconfig.node.json +2 −1
@@ -11,7 +11,8 @@
11 11 "noEmit": true,
12 12 "strict": true,
13 13 "noUnusedLocals": true,
14 "noUnusedParameters": true
14 + "noUnusedParameters": true,
15 + "types": ["node"]
15 16 },
16 17 "include": ["vite.config.ts"]
17 18 }
modified vite.config.ts +6 −3
@@ -11,6 +11,8 @@ import react from '@vitejs/plugin-react'
11 11 import { VitePWA } from 'vite-plugin-pwa'
12 12
13 13 export default defineConfig({
14 + // Overridable for subpath hosting (e.g. GitHub Pages sets VITE_BASE=/zyquo-cloud-web/).
15 + base: process.env.VITE_BASE ?? '/',
14 16 plugins: [
15 17 react(),
16 18 VitePWA({
@@ -51,11 +53,12 @@ export default defineConfig({
51 53 display: 'standalone',
52 54 background_color: '#FAFBFD',
53 55 theme_color: '#FAFBFD',
56 + // Relative to the manifest's location so subpath deploys keep working.
54 57 icons: [
55 { src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
56 { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
58 + { src: 'icons/icon-192.png', sizes: '192x192', type: 'image/png' },
59 + { src: 'icons/icon-512.png', sizes: '512x512', type: 'image/png' },
57 60 {
58 src: '/icons/icon-512-maskable.png',
61 + src: 'icons/icon-512-maskable.png',
59 62 sizes: '512x512',
60 63 type: 'image/png',
61 64 purpose: 'maskable',
62 65