diff --git a/GROK-PROVIDER-INTEGRATION-PLAN.md b/GROK-PROVIDER-INTEGRATION-PLAN.md index 744d6d37..2d318880 100644 --- a/GROK-PROVIDER-INTEGRATION-PLAN.md +++ b/GROK-PROVIDER-INTEGRATION-PLAN.md @@ -15,26 +15,32 @@ now and executed only after every other workstream in the current post-recovery --- -## 0. Executive summary — verified current state (corrects the original draft's premise) +## 0. Executive summary — verified current state (corrects the original draft's premise, twice) -The original request assumed both Grok and Claude were simply "not yet implemented." Verification -against the actual code shows two **structurally different** problems: +The original request assumed both Grok and Claude were simply "not yet implemented." A first +verification pass (below, still accurate for Grok) found Claude's backend deliberately stubbed for +a real reason (CORS) rather than just unwired. **A second, more careful pass — re-checking +`AiProviderCard.tsx` for the literal string `anthropic`, not `claude` (a search-term mistake in the +first pass) — found the picture is more nuanced still:** | | Grok (xAI) | Claude (Anthropic) | |---|---|---| | `AIProvider` / `AiModel` types | Already present (`'grok'`, `'grok-3'`, `'grok-3-mini'`) | Already present (`'anthropic'`, Claude model ids) | | README | Lists as Cloud 4 | Lists as Cloud 3 | -| Legacy provider service (`services/aiProviderService.ts`) | **Real, working implementation** — `streamGrok()` does an actual `fetch('https://api.x.ai/v1/chat/completions', ...)`, key lookup via `storageService.getApiKey('grok')`, and a real `/v1/models` connectivity test (lines 260, 314, 380, 827) | **Deliberately stubbed** — `streamAnthropic()` (line 244) unconditionally throws: *"Claude/Anthropic: Direct browser requests are blocked by Anthropic (CORS). Please use a backend proxy or switch to Gemini/OpenAI/Ollama."* Same pattern at the image-generation and connection-test call sites (lines 590, 821-824). | +| Legacy provider service (`services/aiProviderService.ts`) | **Real, working implementation** — `streamGrok()` does an actual `fetch('https://api.x.ai/v1/chat/completions', ...)`, key lookup via `storageService.getApiKey('grok')`, and a real `/v1/models` connectivity test (lines 260, 314, 380, 827) | `streamAnthropic()` (line 244) **unconditionally throws on every platform, including desktop**: *"Claude/Anthropic: Direct browser requests are blocked by Anthropic (CORS). Please use a backend proxy or switch to Gemini/OpenAI/Ollama."* Same pattern at the image-generation and connection-test call sites (lines 590, 821-824). **This is the actual bug** — CORS is a *browser* security mechanism; it does not apply to Tauri's native HTTP plugin (`@tauri-apps/plugin-http`, `services/localServerHttp.ts`, ADR-0012), which already exists in this codebase for exactly this class of problem (local server discovery). `streamAnthropic()` never even tries the native path — it throws before checking `isTauriRuntime()` at all. | | New Vercel AI SDK layer (`services/ai/providerFactory.ts`) | `providerToKind()` has no `'grok'` case — falls through to `'unsupported'`. Comment: *"anthropic/grok are reserved in the type union but not yet implemented here."* | Same — no `'anthropic'` case, falls through to `'unsupported'`. | -| Settings UI (`components/settings/AiProviderCard.tsx`) | **Not in the primary provider dropdown** (`{gemini, openai, ollama, webllm}` only, lines 174-177). Reachable only as a **hybrid-fallback-chain** option in `AiSections.tsx` (lines 149, 178) — never as a primary selection with its own API-key field. | **Not in the primary provider dropdown either.** No fallback-chain entry, no dedicated UI anywhere. | -| CSP — Tauri desktop (`src-tauri/tauri.conf.json`) | `https://api.x.ai` **already present** in `connect-src` | `https://api.anthropic.com` **absent** | -| CSP — web (`index.html`, `vercel.json`) | Already permissive: `connect-src` includes a bare `https:` scheme-source by design (ADR-0004, BYOK custom-base-URL tradeoff) — any HTTPS origin is already allowed on web builds. | Same — already permissive on web. | -| Root cause | **UI wiring gap only.** The backend already works; it's simply unreachable from the Settings UI. | **Architectural constraint, not a wiring gap.** Anthropic's API does not send CORS headers permitting direct browser `fetch()`. This app has zero backend/serverless infrastructure today (no `api/` directory, no Vercel Functions, no Cloudflare Workers) — it is a purely static SPA across all three of its deployment targets (GitHub Pages, Vercel, Cloudflare Pages) plus a Tauri desktop bundle. Claude genuinely cannot work without adding a server-side relay. | - -**Decision (made 2026-07-30, by the maintainer, mid-audit-sprint):** do both in one plan/PR — -Grok gets the straightforward UI-wiring fix; Claude gets a purpose-built serverless CORS-proxy -**in addition to** the same UI wiring, since this is the app's first backend dependency and needs -its own architectural treatment, not just a UI patch. +| Settings UI (`components/settings/AiProviderCard.tsx`) | **Not in the primary provider dropdown** (`{gemini, openai, ollama, webllm, onnx, transformers}`, lines 174-179). Reachable only as a **hybrid-fallback-chain** option in `AiSections.tsx` (lines 149, 178) — never as a primary selection with its own API-key field. | **Already in the primary provider dropdown** (`{ id: 'anthropic', label: 'Anthropic Claude' }`, line 180) — this part is *not* missing. Selecting it renders a dedicated warning block (line 587) with existing, already-shipped i18n strings (`settings.ai.corsRestriction`, `settings.ai.anthropicCorsNote` — the latter's copy *already says* "...or use the Tauri desktop app for Anthropic calls", a promise the code doesn't yet keep). There is no API-key input for it today — the block is warning-only. | +| CSP — Tauri desktop (`src-tauri/tauri.conf.json`) | `https://api.x.ai` **already present** in `connect-src` | `https://api.anthropic.com` **absent** — needed once desktop calls it directly | +| CSP — web (`index.html`, `vercel.json`) | Already permissive: `connect-src` includes a bare `https:` scheme-source by design (ADR-0004, BYOK custom-base-URL tradeoff) — any HTTPS origin is already allowed on web builds. | Same — already permissive on web (irrelevant either way, since the proxy call from the browser is same-origin). | +| Root cause | **UI wiring gap only.** The backend already works; it's simply unreachable from the Settings UI. | **Two separate gaps, one per platform.** Desktop: `streamAnthropic()` never attempts the native-HTTP escape hatch that ADR-0012 already built for exactly this CORS class of problem — a **narrow bug fix**, not new architecture. Web/PWA: browser `fetch()` has no such escape hatch; Anthropic's API sends no CORS headers permitting it, so the **only** way to make Claude work in a browser tab is a server-side relay — this app's first backend dependency. | + +**Decision (made 2026-07-30, by the maintainer, mid-audit-sprint):** do both providers in one +plan/PR. Grok gets the straightforward UI-wiring fix. Claude gets **two independent tracks**: a +small native-HTTP fix for desktop (reusing ADR-0012's established pattern — ships first, no new +infrastructure) and a purpose-built serverless CORS-proxy for web/PWA (the actual new architecture, +ships second, larger and riskier). Treating these as one lump "Claude proxy" task — as the first +draft of this plan did — would have meant the *easier, already-half-promised* desktop fix waited +behind the *harder* web fix for no real reason. --- @@ -72,7 +78,7 @@ its own architectural treatment, not just a UI patch. **Goal:** make Grok selectable as a primary provider with its own API-key field, exactly like Gemini/OpenAI. -1. **`components/settings/AiProviderCard.tsx`**: add `{ id: 'grok', label: t('settings.ai.provider.grok') }` (translation key, never a hardcoded literal — see §5 below) to the +1. **`components/settings/AiProviderCard.tsx`**: add `{ id: 'grok', label: t('settings.ai.provider.grok') }` (translation key, never a hardcoded literal — see item 5 below) to the primary options array (currently `gemini | openai | ollama | webllm`, lines 174-177). Add a `provider === 'grok'` UI block mirroring the existing `provider === 'openai'` block (lines 257+): API-key input (`storageService.saveApiKey('grok', ...)` / `clearApiKey('grok')` — the @@ -118,40 +124,91 @@ Gemini/OpenAI. --- -## 4. Phase 2 — Claude: serverless CORS proxy (new architecture) - -This is the substantial new piece. **Read this whole phase before starting** — the deployment-target -constraint below affects the entire design. - -### 4.1 The deployment-target problem - -WorldScript Studio ships to **four** targets, and a serverless function is not equally available -on all of them: +## 4. Phase 2 — Claude: two independent tracks, desktop first + +Claude splits cleanly into two tracks with very different sizes, because CORS is a **browser** +security mechanism — it does not apply to Tauri's native HTTP plugin at all. **Ship Track A first**: +it is a narrow bug fix reusing infrastructure that already exists (ADR-0012), not new architecture, +and it fulfills a promise the shipped UI copy already makes (`settings.ai.anthropicCorsNote`: *"...or +use the Tauri desktop app for Anthropic calls"*). Track B (the actual new architecture) follows. + +### 4.0 Track A — Desktop: native-HTTP fix (small, ships first, no new infrastructure) + +1. **`services/aiProviderService.ts`**: `streamAnthropic()` (and the image-generation / connection-test + call sites at lines 590, 821-824) currently throw unconditionally, before ever checking the + runtime. Branch on `isTauriRuntime()` (`services/tauriRuntime.ts`) first: + - **Desktop**: call `https://api.anthropic.com/v1/messages` directly via a Tauri-native fetch — + the same `@tauri-apps/plugin-http` escape hatch `services/localServerHttp.ts` already uses for + Ollama/LM Studio/vLLM (ADR-0012). That module is scoped and named for *local* servers + specifically (`LocalServerError`, `DEFAULT_OLLAMA_BASE_URL`); reusing it as-is for a cloud + endpoint would be a naming/scope mismatch. Extract the ~10-line `isTauriRuntime() ? dynamic + plugin-http import : global fetch` resolution into a small shared helper (or duplicate it + narrowly here with a comment pointing at `localServerHttp.ts` as the sibling pattern) — decide + which at execution time based on how it reads in context; either is defensible, over-abstracting + two call sites into a shared module is not required. + - **Web/browser** (including the Tauri WebView's own dev-mode web preview, if distinct from the + packaged app — verify `isTauriRuntime()`'s detection covers exactly the packaged case): still + throws, now pointing at Track B once it ships (or the existing message if Track B isn't done + yet — sequence the throw message's wording to whichever is true at the time this ships). +2. **CSP**: add `https://api.anthropic.com` to `src-tauri/tauri.conf.json`'s `connect-src` (mirrors + the existing `https://api.x.ai` entry for Grok). No web CSP change needed (native fetch isn't + subject to it; irrelevant for the desktop-only path anyway). +3. **UI**: on desktop, `AiProviderCard.tsx`'s existing `provider === 'anthropic'` warning block + (line 587) becomes conditional on `!isTauriRuntime()` — desktop instead renders the same + API-key + model-selector + Test Connection pattern as every other cloud provider, calling straight + through to the now-fixed `streamAnthropic()`. Web/PWA keeps the existing warning block (updated + per Track B's actual shipped state — see §4.2 below). +4. **i18n**: no *new* strings needed for the desktop UI block itself — reuse the API-key/model-selector + copy already shared with Gemini/OpenAI/Grok. The existing `anthropicCorsNote` string's wording may + need a small revision once desktop actually works (it currently frames desktop as a suggestion, + not a fact) — treat this as a content update, not a new key, across all 19 locales. +5. **Tests**: unit tests for `streamAnthropic()`'s new `isTauriRuntime()` branch (both sides mocked), + and for the UI's desktop-vs-web conditional rendering. + +**Track A Definition of Done:** +- [ ] `streamAnthropic()` (+ image-gen + connection-test) branch on `isTauriRuntime()`; desktop calls Anthropic natively, no CORS error +- [ ] `src-tauri/tauri.conf.json` CSP includes `https://api.anthropic.com` +- [ ] Desktop Settings UI shows a real API-key/model-selector flow for Claude instead of the warning block +- [ ] Web/PWA UI unchanged by this track (still shows the warning, pointing at Track B once it exists) +- [ ] i18n: any copy changes across all 19 locales; no hardcoded literals +- [ ] Unit tests for both runtime branches +- [ ] This is explicitly **not** gated behind Track B — it can ship and be useful on its own even if Track B is deferred + +### 4.1 Track B — Web/PWA: serverless CORS proxy (the actual new architecture) + +This is the substantial new piece, and the only one that needs a backend. **Read this whole +section before starting** — the deployment-target constraint below affects the entire design, and +it is now **narrower than the first draft assumed**: Track A already covers desktop, so this track +only needs to reach browser tabs (Vercel, Cloudflare Pages, GitHub Pages). + +#### 4.1.1 The deployment-target problem + +A serverless function is not equally available across WorldScript Studio's three *web* deploy +targets: | Target | Can host a serverless function? | Consequence for Claude | |---|---|---| | **Vercel** (primary) | Yes — Vercel Functions (`api/*.ts`) | Claude works when the app is served from Vercel | | **Cloudflare Pages** | Yes — Cloudflare Pages Functions (`functions/*.ts`) | Claude works when served from Cloudflare, but needs a **second, separately-written** function (different runtime/handler shape from Vercel's) | | **GitHub Pages** | **No** — pure static hosting, no server-side execution at all | Claude **cannot** work on the GitHub Pages mirror, ever, without pointing at an externally-hosted proxy | -| **Tauri desktop** | No — static bundled app, no local server | Claude on desktop would have to call out to a **hosted** proxy (e.g., the maintainer's own Vercel deployment) — a materially different trust/privacy story than "your key never leaves your device except to the provider you configure," since now a request also transits the maintainer's infrastructure | **This is the crux of the decision the maintainer needs to be aware of at execution time, restated -clearly in this plan rather than glossed over:** enabling Claude means either (a) accepting that it -silently doesn't work on 2 of 4 targets (GitHub Pages, desktop) unless those builds are configured -to point at a hosted proxy URL, with clear UI messaging about that, or (b) writing and maintaining -proxy functions for both Vercel *and* Cloudflare Pages, and deciding whether desktop/GitHub-Pages -builds get a hardcoded fallback proxy URL (introducing a real dependency on a specific external -host that isn't "your own deployment" for those users) or simply show Claude as unavailable there. +clearly in this plan rather than glossed over:** enabling Claude on the web means either (a) +accepting that it silently doesn't work on GitHub Pages unless that build is configured to point at +a hosted proxy URL, with clear UI messaging about that, or (b) hardcoding a fallback proxy URL for +the GitHub Pages build specifically (introducing a real dependency on a specific external host that +isn't "this exact deployment" for those users, and a genuine trust-model question worth flagging to +the maintainer rather than deciding silently). **Recommended default for this plan:** implement the Vercel Function (primary target) first; -Cloudflare Pages Function as a fast-follow using the same relay logic; GitHub Pages and Tauri -desktop show Claude as **unavailable** with a clear, honest message (reusing the existing -`settings.ai.providerStatusUnavailableBrowser` i18n key's *pattern* — a new, more specific key — -rather than silently failing or defaulting to a hosted proxy the user didn't choose). Revisit -hardcoding a fallback proxy URL only if the maintainer explicitly wants GitHub-Pages/desktop users -to have Claude too, understanding the trust-model change that implies. +Cloudflare Pages Function as a fast-follow using the same relay logic; GitHub Pages shows Claude as +**unavailable** with a clear, honest message (a new, precise i18n key — not a reuse of +`settings.ai.providerStatusUnavailableBrowser`, since that key's existing use case is a different +reason) rather than silently failing or defaulting to a hosted proxy the user didn't choose. Revisit +a hardcoded fallback proxy URL only if the maintainer explicitly wants GitHub-Pages users to have +Claude too, understanding the trust-model change that implies. -### 4.2 Proxy design +#### 4.1.2 Proxy design - **New file:** `api/claude-proxy.ts` (Vercel Function, Node.js runtime — check whether Vercel's Edge Runtime or Node runtime is the better fit; Edge is lighter but has stricter API surface). @@ -189,31 +246,34 @@ to have Claude too, understanding the trust-model change that implies. routing convention differs from Vercel's `api/` — check Cloudflare Pages Functions docs for the exact handler signature at execution time, since this plan predates writing the code). -### 4.3 UI wiring (same pattern as Grok, once the proxy exists) - -1. `components/settings/AiProviderCard.tsx`: add `{ id: 'anthropic', label: t('settings.ai.provider.anthropic') }` — - **use the existing `'anthropic'` identifier consistently** (the `AIProvider` type, `aiProviderService.ts`'s - `case 'anthropic':`, and every other switch/lookup already use `'anthropic'`; do not introduce a - parallel `'claude'` id anywhere — selection, key storage, service dispatch, and factory routing - must all key off the same identifier), - with the same API-key + model-selector + test-connection pattern as Grok — but the "backend" the - Test Connection button calls is now the proxy endpoint, not a direct Anthropic call. -2. **Availability detection:** on GitHub Pages / Tauri desktop builds (however that's detected at - runtime — check `services/tauriRuntime.ts` and any existing hostname-based deploy-target - detection), show Claude as disabled/unavailable per §4.1's recommended default, with a - `settings.ai.providerStatusUnavailableProxy`-style i18n string explaining why (not just a generic - "unavailable in browser" reuse — this is a different reason than the existing key's original - use case, so a new, precise key is more honest). -3. `services/aiProviderService.ts`: replace `streamAnthropic()`'s unconditional throw with a real - implementation that calls the proxy endpoint (`fetch('/api/claude-proxy', ...)`) instead of - `https://api.anthropic.com` directly. Same for the image-generation and connection-test call - sites (lines 590, 821-824) — decide whether Claude image generation is in scope at all (Anthropic - may not offer an image-gen endpoint; verify before wiring UI for something that doesn't exist). -4. `services/ai/providerFactory.ts`: add an `'anthropic'` case if the new Vercel-AI-SDK layer should - also support Claude — likely wants its own `'anthropicProxy'` kind rather than reusing - `'openaiCompatible'`, since the request/response shape differs from OpenAI's. - -### 4.4 Security & privacy documentation (mandatory, not optional) +### 4.2 UI wiring (web/PWA — the dropdown entry and warning block already exist, see §0) + +Unlike Grok, **no new dropdown entry is needed** — `{ id: 'anthropic', label: 'Anthropic Claude' }` +and its warning block already ship (`AiProviderCard.tsx` lines 180, 587). The identifier is already +correctly `'anthropic'` throughout (`AIProvider` type, `aiProviderService.ts`'s `case 'anthropic':`) +— just don't introduce a parallel `'claude'` id anywhere while touching this. + +1. **On web builds where the proxy is reachable** (Vercel, Cloudflare Pages): replace the existing + warning-only block with the same API-key + model-selector + Test Connection pattern every other + cloud provider gets — but the "backend" the Test Connection button calls is the proxy endpoint, + not a direct Anthropic call. Existing labels are already real strings, not hardcoded literals + (verify `t('settings.ai.provider.anthropic')`-equivalent is actually used, per the mandatory + i18n rule in §3.5 — the current line reads `label: 'Anthropic Claude'`, a literal; fix this as + part of the same change even though it predates this plan). +2. **On GitHub Pages** (no proxy possible — §4.1.1): keep a warning block, but update its copy to + the new, precise "no proxy on this deployment" reasoning rather than the current generic CORS + note, once Track A ships and the note's "use the desktop app" half becomes literally true. +3. **`services/aiProviderService.ts`**: replace `streamAnthropic()`'s web-path throw (the desktop + path was already fixed in Track A) with a real implementation that calls the proxy endpoint + (`fetch('/api/claude-proxy', ...)`) instead of `https://api.anthropic.com` directly. Same for the + image-generation and connection-test call sites (lines 590, 821-824) — decide whether Claude + image generation is in scope at all (Anthropic may not offer an image-gen endpoint; verify before + wiring UI for something that doesn't exist). +4. **`services/ai/providerFactory.ts`**: add an `'anthropic'` case if the new Vercel-AI-SDK layer + should also support Claude on the web — likely wants its own `'anthropicProxy'` kind rather than + reusing `'openaiCompatible'`, since the request/response shape differs from OpenAI's. + +### 4.3 Security & privacy documentation (mandatory, not optional) - `docs/SECURITY-THREAT-MODEL.md`: add a new row/section for the Claude proxy — it is the **first** place in this app's architecture where a BYOK key transits infrastructure the user doesn't @@ -227,7 +287,7 @@ to have Claude too, understanding the trust-model change that implies. - Settings UI: the Claude API-key input should carry a brief, honest inline note about this distinction — not hidden in docs only. -### 4.5 Testing +### 4.4 Testing - Unit tests for the proxy handler logic (request forwarding, no logging of secrets) — likely needs a lightweight Vercel Function test harness or at minimum a pure-function extraction of the relay @@ -238,20 +298,29 @@ to have Claude too, understanding the trust-model change that implies. mock the proxy endpoint, verify the Settings UI flow end-to-end. Cannot realistically hit the real Anthropic API in CI (costs money, needs a real key) — mock-only, consistent with how other cloud-provider E2E specs in this repo already avoid real API calls. -- Manual smoke test matrix: Vercel (real proxy, real Anthropic key) — Cloudflare Pages (real proxy) - — GitHub Pages (confirm Claude shows unavailable, doesn't silently break) — Tauri desktop (same). +- Manual smoke test matrix: Tauri desktop (Track A, real Anthropic key, no proxy) — Vercel (Track B, + real proxy, real Anthropic key) — Cloudflare Pages (Track B, real proxy) — GitHub Pages (Track B + unavailable state, confirm it doesn't silently break). -**Claude Definition of Done:** +**Claude Definition of Done (Track A — desktop, ships first):** +- [ ] `streamAnthropic()` (+ image-gen + connection-test) branch on `isTauriRuntime()`; desktop calls Anthropic natively, no CORS error +- [ ] `src-tauri/tauri.conf.json` CSP includes `https://api.anthropic.com` +- [ ] Desktop Settings UI shows a real API-key/model-selector flow for Claude instead of the warning block +- [ ] Unit tests for both runtime branches + +**Claude Definition of Done (Track B — web/PWA, ships second):** - [ ] Vercel Function proxy deployed and relaying requests statelessly (no key/prompt logging) -- [ ] Proxy enforces schema validation, body-size limit, origin policy, rate limiting, and timeouts (§4.2) — tested +- [ ] Proxy enforces schema validation, body-size limit, origin policy, rate limiting, and timeouts (§4.1.2) — tested - [ ] Cloudflare Pages Function equivalent (or explicit decision to defer, documented in the ADR) -- [ ] Claude selectable as primary provider in Settings → AI Provider on proxy-capable deployments -- [ ] GitHub Pages and Tauri desktop show an honest "unavailable" state, not a silent failure +- [ ] Claude's existing warning-only block replaced with a real API-key/model-selector flow on proxy-capable web deployments +- [ ] GitHub Pages shows an honest "unavailable" state, not a silent failure - [ ] `docs/SECURITY-THREAT-MODEL.md` and README's privacy framing updated with the proxy caveat -- [ ] i18n: all provider/model/status/error strings use translation keys (no hardcoded literals); all 19 locales -- [ ] No new feature flag - [ ] Unit + E2E tests (mocked, no real Anthropic calls in CI) -- [ ] New ADR 0016 covers both Grok and Claude decisions + +**Both tracks:** +- [ ] i18n: all provider/model/status/error strings use translation keys (no hardcoded literals) — including fixing the pre-existing `label: 'Anthropic Claude'` literal; all 19 locales +- [ ] No new feature flag +- [ ] New ADR 0016 covers both Grok and Claude decisions (and the Track A/B split) - [ ] README/CHANGELOG updated; TODO.md/AUDIT.md reconciled --- @@ -287,11 +356,11 @@ to have Claude too, understanding the trust-model change that implies. ## 7. Addendum — opt-in browser-fetch Ollama for the PWA (Issue #266 follow-up) -**This is a separate initiative from §§1-7 above** — different issue, different mechanism, no +**This is a separate initiative from §§0-6 above** — different issue, different mechanism, no backend involved — bundled into this same plan file at the maintainer's request rather than as a second document. -### 8.1 Why this is *not* the same pattern as the Claude proxy +### 7.1 Why this is *not* the same pattern as the Claude proxy A hosted serverless proxy (§4) works for Claude because the proxy's *destination* — `api.anthropic.com` — is a fixed host reachable from anywhere on the internet, including from @@ -301,7 +370,7 @@ is `http://localhost:11434` **on the end user's own machine**. A serverless func bridges "an internet server reaching into a user's private machine." This is a hard networking fact, not a policy choice, and it's why no part of this addendum proposes a proxy. -### 8.2 What actually exists: the maintainer's own "technically: yes" answer in Issue #266 +### 7.2 What actually exists: the maintainer's own "technically: yes" answer in Issue #266 [Issue #266](https://github.com/qnbs/WorldScript-Studio/issues/266) already contains a full, correct analysis (comment thread, 2026-07-28/29) of why the PWA doesn't attempt Ollama connections @@ -322,7 +391,7 @@ plan operationalizes exactly that answer, not a new invention: reason this was never made the *default* — but that's an argument against defaulting it on, not against offering it as an explicit, clearly-labeled opt-in for users who understand the tradeoff. -### 8.3 Implementation sketch +### 7.3 Implementation sketch 1. **New feature flag** — e.g. `enableBrowserOllama` — default **off**, opt-in, added through the normal `featureFlagsSlice.ts` + `featureCatalog.ts` process this sprint's WS-2 already @@ -360,11 +429,11 @@ plan operationalizes exactly that answer, not a new invention: 7. **Docs**: update the existing "desktop app required" banner copy, README, TODO.md, and the in-app help article to mention the new opt-in path without implying it's the recommended one — desktop remains the recommended, zero-config path for local servers. -8. **i18n**: same mandatory-translation-key requirement as §5/§8 above — the toggle label, the - `OLLAMA_ORIGINS` instructional copy, and the new `cors`-suspected error state all need real - `t()` keys across all 19 locales; no hardcoded literals. +8. **i18n**: same mandatory-translation-key requirement as Phase 1's i18n item (§3.5) above — the + toggle label, the `OLLAMA_ORIGINS` instructional copy, and the new `cors`-suspected error state + all need real `t()` keys across all 19 locales; no hardcoded literals. -### 8.4 Definition of Done +### 7.4 Definition of Done - [ ] New feature flag, default off, follows this repo's existing flag conventions (`pnpm exec tsx scripts/audit-feature-parity.ts` reports 0 drifts) - [ ] PWA can optionally attempt a direct Ollama fetch when the flag is on, clearly labeled experimental/unsupported in the UI diff --git a/README.md b/README.md index 16da7fce..5446c0fa 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ v1.24.3 IndexedDB v8 PWA v3.0 - i18n 19 locales — 2849 keys + i18n 19 locales — 2855 keys 6477 tests / 529 files Codecov Coverage License MIT @@ -383,7 +383,7 @@ Infrastructure-level features that keep the app fast and extensible as projects ### 🌐 Full Multi-Language Support -Shipped UI locales with **2849 i18n keys** across all 19 languages — zero hardcoded user-facing strings: +Shipped UI locales with **2855 i18n keys** across all 19 languages — zero hardcoded user-facing strings: - 🇩🇪 **German** (Deutsch) - 🇬🇧 **English** @@ -492,7 +492,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and | **PDF Export** | jsPDF | Client-side, configurable PDF document generation | | **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) | | **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking | -| **i18n** | Custom React Context (`I18nContext.tsx`) | 2849 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta); EN fallback; `localStorage` persistence | +| **i18n** | Custom React Context (`I18nContext.tsx`) | 2855 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta); EN fallback; `localStorage` persistence | | **Testing** | Vitest 4.x (6477 tests / 529 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | | **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy | | **Visualization** | Force-directed graph | Interactive character relationship network | @@ -694,7 +694,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt **Current test metrics (2026-07-30, CI-reported):** - **6477 unit tests** across **529 test files** — all passing - Coverage thresholds: lines ≥ 74 · branches ≥ 60 · functions ≥ 67 · statements ≥ 72 — enforced in CI (see Codecov badge for live metrics) -- i18n: **2849 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta) +- i18n: **2855 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta) **CI-cloud-first workflow (recommended):** On constrained hardware run **`pnpm run lint && pnpm run i18n:check && pnpm run typecheck`** locally, then push and let CI handle coverage, E2E, Lighthouse, and Stryker. Authoritative numbers come from CI artifacts (Codecov, JUnit). After CI goes green, update the README badges and `AUDIT.md` quality-gate line from the reported metrics. See **[`docs/CI.md`](docs/CI.md) § Cloud CI-first vs local development** for the full post-merge doc-update checklist. diff --git a/components/settings/AiProviderCard.tsx b/components/settings/AiProviderCard.tsx index 7a1543c7..57707f66 100644 --- a/components/settings/AiProviderCard.tsx +++ b/components/settings/AiProviderCard.tsx @@ -46,6 +46,9 @@ export const AiProviderCard: FC = ({ // imply a verified connection next to the "desktop app required" banner. const ollamaUntestable = provider === 'ollama' && !isDesktop; const [openaiKey, setOpenaiKey] = useState(''); + // QNBS-v3: Grok's own key input state, mirroring OpenAI's pattern above. + const [grokKey, setGrokKey] = useState(''); + const [isSavingGrokKey, setIsSavingGrokKey] = useState(false); const [ollamaModels, setOllamaModels] = useState([]); const [testStatus, setTestStatus] = useState<'idle' | 'loading' | 'ok' | 'error'>('idle'); const [testError, setTestError] = useState(''); @@ -66,8 +69,26 @@ export const AiProviderCard: FC = ({ .getApiKey('openai') .then((k) => setOpenaiKey(k ?? '')) .catch(() => {}); + storageService + .getApiKey('grok') + .then((k) => setGrokKey(k ?? '')) + .catch(() => {}); }, []); + // QNBS-v3: save/clear via storageService, matching every other provider's key persistence. + const handleSaveGrokKey = useCallback(async () => { + setIsSavingGrokKey(true); + try { + if (grokKey.trim()) { + await storageService.saveApiKey('grok', grokKey.trim()); + } else { + await storageService.clearApiKey('grok'); + } + } finally { + setIsSavingGrokKey(false); + } + }, [grokKey]); + const handleSaveOpenAiKey = useCallback(async () => { setIsSavingKey(true); try { @@ -171,13 +192,14 @@ export const AiProviderCard: FC = ({ }, [provider, isDesktop, handleLoadOllamaModels, handleTest]); const providers: { id: AIProvider; label: string }[] = [ - { id: 'gemini', label: 'Google Gemini' }, - { id: 'openai', label: 'OpenAI' }, - { id: 'ollama', label: 'Ollama (lokal)' }, + { id: 'gemini', label: t('settings.ai.provider.gemini') }, + { id: 'openai', label: t('settings.ai.provider.openai') }, + { id: 'ollama', label: t('settings.ai.provider.ollama') }, { id: 'webllm', label: t('settings.ai.providerWebllm') }, { id: 'onnx', label: t('settings.ai.providerOnnx') }, { id: 'transformers', label: t('settings.ai.providerTransformers') }, - { id: 'anthropic', label: 'Anthropic Claude' }, + { id: 'anthropic', label: t('settings.ai.provider.anthropic') }, + { id: 'grok', label: t('settings.ai.provider.grok') }, ]; const presetOptions: { id: LocalBackendPreset; labelKey: string }[] = [ @@ -322,6 +344,46 @@ export const AiProviderCard: FC = ({ )} + {provider === 'grok' && ( +
+ +
+ setGrokKey(e.target.value)} + className="flex-1 font-mono text-sm" + /> + +
+

{t('settings.ai.keysEncrypted')}

+ +