From 9565b4e285c2bd1543aa5fea6cf0e2cc5368699c Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:07:40 +0200 Subject: [PATCH 1/5] fix: address deferred CodeRabbit findings from PR #297 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four findings deferred from PR #297 to keep that PR's merge unblocked, addressed here first per explicit instruction before starting the Grok/Claude/Ollama plan execution: 1. GROK-PROVIDER-INTEGRATION-PLAN.md: the addendum was numbered "## 7" but its own subsections said "8.1"-"8.4", and a "§§1-7 above" self-reference included the addendum itself. Renumbered to 7.1-7.4, fixed both self-references, and fixed a second pre-existing ambiguous "§5" cross-reference (meant "item 5 within Phase 1's own list", not top-level section 5) while in the area. 2. tests/unit/listenerMiddleware.test.ts: added the required QNBS-v3 comment next to the new featureFlagsActions import. 3. Added the required QNBS-v3 comment before the new "local-first shadow sync" describe block. 4. Rewrote the local-first sync test to assert the actual selector-to-binding boundary directly instead of inferring success from an absence of logger calls in an unmocked-dependency, environment-dependent setup. Added real mocks for services/localFirst/{projectDoc,docBinding,docPersistence} and services/storage/storageEncryptionService so the test can assert the exact object selectProjectData(state) returns is what reaches ProjectDocBinding's constructor and syncFromProject/verify. Hit two real bugs while building this: (a) an arrow-function mock implementation can never be used as a constructor -- switched to a real class, which Biome's useArrowFunction rule doesn't flag either (a function-expression fix would have been immediately reverted by the same linter that was satisfied by removing the arrow function in the first place); (b) vi.fn().mockImplementation() can't type a class constructor against its generic (...args) => any signature -- mocked the class directly instead of wrapping it in vi.fn(). --- GROK-PROVIDER-INTEGRATION-PLAN.md | 18 ++++----- tests/unit/listenerMiddleware.test.ts | 54 +++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/GROK-PROVIDER-INTEGRATION-PLAN.md b/GROK-PROVIDER-INTEGRATION-PLAN.md index 744d6d37..4eb79887 100644 --- a/GROK-PROVIDER-INTEGRATION-PLAN.md +++ b/GROK-PROVIDER-INTEGRATION-PLAN.md @@ -72,7 +72,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 @@ -287,11 +287,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 +301,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 +322,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 +360,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/tests/unit/listenerMiddleware.test.ts b/tests/unit/listenerMiddleware.test.ts index 57d6cf86..867f08bb 100644 --- a/tests/unit/listenerMiddleware.test.ts +++ b/tests/unit/listenerMiddleware.test.ts @@ -2,10 +2,13 @@ import { configureStore, type Reducer } from '@reduxjs/toolkit'; import undoable from 'redux-undo'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { listenerMiddleware } from '../../app/listenerMiddleware'; +import type { RootState } from '../../app/store'; import { useTransientUiStore } from '../../app/transientUiStore'; +// QNBS-v3: featureFlagsActions dispatches setEnableLocalFirstSync to exercise the local-first listener below import featureFlagsReducer, { featureFlagsActions, } from '../../features/featureFlags/featureFlagsSlice'; +import { selectProjectData } from '../../features/project/projectSelectors'; import projectReducer, { projectActions } from '../../features/project/projectSlice'; import settingsReducer, { settingsActions } from '../../features/settings/settingsSlice'; import statusReducer, { statusActions } from '../../features/status/statusSlice'; @@ -56,6 +59,37 @@ vi.mock('../../services/storageBackend', () => ({ saveEnvelopeFromProjectData: (data: unknown) => mockSaveEnvelope(data), })); +// QNBS-v3: stub the real Yjs doc/binding/persistence chain so the local-first sync test can assert +// the exact payload selectProjectData hands to the binding, instead of depending on real Yjs behavior. +const mockSyncFromProject = vi.fn(); +const mockVerify = vi.fn().mockReturnValue({ ok: true, mismatches: [] }); +const mockReproject = vi.fn(); +let lastBindingCtorArg: unknown; +vi.mock('../../services/localFirst/projectDoc', () => ({ + createBlankProjectDoc: vi.fn(() => ({})), +})); +// QNBS-v3: a class, not a function expression — the real code calls `new ProjectDocBinding(...)`, +// and Biome's useArrowFunction rule would otherwise rewrite a function expression into a +// non-constructible arrow function here. +class MockProjectDocBinding { + syncFromProject = mockSyncFromProject; + verify = mockVerify; + reproject = mockReproject; + constructor(initial: unknown) { + lastBindingCtorArg = initial; + } +} +vi.mock('../../services/localFirst/docBinding', () => ({ + ProjectDocBinding: MockProjectDocBinding, +})); +vi.mock('../../services/localFirst/docPersistence', () => ({ + NOOP_PERSISTENCE: { active: false, whenSynced: Promise.resolve() }, + persistProjectDoc: vi.fn(() => ({ active: true, whenSynced: Promise.resolve() })), +})); +vi.mock('../../services/storage/storageEncryptionService', () => ({ + isIdbEncryptionReady: vi.fn(() => true), +})); + // QNBS-v3: Phase 0 audit — duckdbListenerLoader is lazily imported in listenerMiddleware. // Mock all loader fns so listener effects complete cleanly in stress tests. vi.mock('../../services/duckdb/duckdbListenerLoader', () => ({ @@ -454,18 +488,22 @@ describe('analytics privacy opt-out gating', () => { }); }); +// QNBS-v3: covers canonical selector use during local-first sync — prevents project-data selection regressions describe('local-first shadow sync (B1.1)', () => { - it('reads project data via the canonical selector when the flag flips on', async () => { + it('passes the canonical selector output straight through to the binding', async () => { const store = makeFullStore(); + store.dispatch(projectActions.updateTitle('Local-First Title')); store.dispatch(featureFlagsActions.setEnableLocalFirstSync(true)); await vi.advanceTimersByTimeAsync(100); - // getLocalFirstHandle's dynamic imports (Yjs doc/binding/persistence) aren't mocked in this - // suite; whatever happens next is caught internally and logged as non-critical (see - // listenerMiddleware.ts's runLocalFirstShadowSync catch block) — this test only needs to prove - // the selector line runs and the listener doesn't throw out to the store. - // Real dynamic-imported Yjs doc/binding modules run for real here (not mocked) — a freshly - // created shadow doc synced from the same present data has nothing to drift against, so a - // clean run logs neither a warning (self-heal) nor an error. + + // Asserts the actual selector→sync boundary directly (per review feedback) instead of + // inferring success from an absence of logger calls: the exact object selectProjectData(state) + // returns must be what reaches ProjectDocBinding's constructor and syncFromProject/verify. + const presentData = selectProjectData(store.getState() as unknown as RootState); + expect(lastBindingCtorArg).toBe(presentData); + expect(mockSyncFromProject).toHaveBeenCalledWith(presentData); + expect(mockVerify).toHaveBeenCalledWith(presentData); + expect(mockReproject).not.toHaveBeenCalled(); expect(mockLoggerWarn).not.toHaveBeenCalled(); expect(mockLoggerError).not.toHaveBeenCalled(); }); From ef8c4a5533bc8c2907631de44840cc89989004a4 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:36:41 +0200 Subject: [PATCH 2/5] docs: Phase 0 re-verification finds Claude split -- two tracks, not one Re-checked the plan's own findings against current main before starting implementation (Phase 0's explicit purpose), and found the original draft's Claude analysis was incomplete in a way that changes the whole approach: - AiProviderCard.tsx's primary provider dropdown already has an 'anthropic' entry with a dedicated warning block -- missed originally because the investigation searched for the literal string "claude", never "anthropic" (the actual identifier), when checking that file. Its existing i18n copy already half-promises desktop support ("...or use the Tauri desktop app for Anthropic calls"). - streamAnthropic() throws unconditionally on EVERY platform, including desktop -- but CORS is a browser-only restriction. It never even checks isTauriRuntime() before throwing. Tauri's native HTTP plugin (@tauri-apps/plugin-http, services/localServerHttp.ts) already solves exactly this class of problem for Ollama/LM Studio/vLLM (ADR-0012); the same escape hatch works for any HTTPS endpoint, including api.anthropic.com, since native networking isn't subject to browser CORS at all. Restructured Phase 2 into two independent tracks: Track A (desktop) is a narrow bug fix reusing ADR-0012's established pattern, no new infrastructure, ships first. Track B (web/PWA) is the actual new architecture -- the serverless proxy -- and is now correctly scoped to only the three web deploy targets (desktop no longer needs it at all). Updated the Executive Summary, both Definition of Done checklists, and all internal cross-references accordingly. --- GROK-PROVIDER-INTEGRATION-PLAN.md | 211 ++++++++++++++++++++---------- 1 file changed, 140 insertions(+), 71 deletions(-) diff --git a/GROK-PROVIDER-INTEGRATION-PLAN.md b/GROK-PROVIDER-INTEGRATION-PLAN.md index 4eb79887..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. --- @@ -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 --- From 1921d154d5fe3804648a7875697fe04131bed538 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:44:47 +0200 Subject: [PATCH 3/5] docs(adr): add ADR-0016 for native Grok provider + split Claude fix Formalizes GROK-PROVIDER-INTEGRATION-PLAN.md's Phase 0 decision record: Grok is a pure UI-wiring fix (backend already works); Claude splits into Track A (desktop, native-HTTP via the ADR-0012 pattern, ships first, no new infrastructure) and Track B (web/PWA, this app's first serverless backend dependency, ships second). Also documents why the Ollama-in-PWA follow-up (Issue #266) is a separate decision, not a Track-B variant -- a hosted proxy can't reach a user's own localhost. --- .../0016-native-grok-and-claude-providers.md | 118 ++++++++++++++++++ docs/adr/README.md | 1 + 2 files changed, 119 insertions(+) create mode 100644 docs/adr/0016-native-grok-and-claude-providers.md diff --git a/docs/adr/0016-native-grok-and-claude-providers.md b/docs/adr/0016-native-grok-and-claude-providers.md new file mode 100644 index 00000000..18281054 --- /dev/null +++ b/docs/adr/0016-native-grok-and-claude-providers.md @@ -0,0 +1,118 @@ +# ADR 0016 — Native Grok provider + split Claude fix (desktop native-HTTP, web serverless proxy) + +- **Status:** Accepted +- **Date:** 2026-07-30 +- **Deciders:** Maintainer + Claude Code +- **Context tags:** ai, grok, anthropic, claude, cors, tauri, proxy, backend +- **Related:** [`GROK-PROVIDER-INTEGRATION-PLAN.md`](../../GROK-PROVIDER-INTEGRATION-PLAN.md) (execution plan this ADR formalizes) · extends [ADR 0012](0012-local-server-connectivity-tauri-http.md) · see also [ADR 0004](0004-csp-connect-src-byok-tradeoff.md) + +## Context + +README documents Grok (xAI, "Cloud 4") and Claude (Anthropic, "Cloud 3") as working cloud AI +providers. Neither was actually selectable as a primary provider with its own API key in +`components/settings/AiProviderCard.tsx`'s main flow, prompting an investigation into why. + +Verification found **two structurally different problems**, not one: + +- **Grok**: `services/aiProviderService.ts`'s `streamGrok()` already has a real, working + implementation — a genuine `fetch('https://api.x.ai/v1/chat/completions', ...)`, BYOK key + storage, and a working `/v1/models` connection test. It simply was never added to + `AiProviderCard.tsx`'s primary provider dropdown (only reachable as a hybrid-fallback-chain + option) or to the newer Vercel-AI-SDK layer (`services/ai/providerFactory.ts`'s + `providerToKind()`). **Pure UI/wiring gap.** +- **Claude**: `streamAnthropic()` unconditionally throws — *"Direct browser requests are blocked by + Anthropic (CORS). Please use a backend proxy or switch to Gemini/OpenAI/Ollama."* — on **every** + platform, including desktop, without ever checking which platform it's running on. A first + investigation pass concluded this needed a serverless proxy everywhere. **A second, more careful + pass corrected that**: `AiProviderCard.tsx` already has an `'anthropic'` entry in its primary + dropdown with a dedicated warning block, whose own shipped i18n copy already says *"...or use the + Tauri desktop app for Anthropic calls"* — a promise the code doesn't keep. CORS is a **browser** + security mechanism. It does not apply to Tauri's native HTTP plugin + (`@tauri-apps/plugin-http`), which `services/localServerHttp.ts` already uses to bypass exactly + this class of restriction for Ollama/LM Studio/vLLM (ADR-0012). `streamAnthropic()` never + attempts that escape hatch before throwing. + +## Decision + +### Grok — native UI wiring (no new architecture) + +Add Grok to `AiProviderCard.tsx`'s primary provider dropdown with its own API-key field and model +selector, wired to the existing, already-functional `streamGrok()` backend. Add a `case 'grok'` to +`providerFactory.ts`'s `providerToKind()` (xAI's API is OpenAI-compatible) so the newer +Vercel-AI-SDK Writer-streaming path gets Grok too, not just legacy thunks. No new feature flag — +Grok becomes a regular provider option exactly like Gemini/OpenAI. No CSP change needed: +`https://api.x.ai` is already present in `src-tauri/tauri.conf.json`, and the web CSP's `https:` +scheme-source (ADR-0004) already covers it. + +### Claude — two independent tracks, not one + +**Track A (desktop, ships first, no new infrastructure):** fix `streamAnthropic()` to branch on +`isTauriRuntime()` before throwing. On desktop, call `https://api.anthropic.com` directly via the +same native-HTTP pattern ADR-0012 established for local servers — native networking is not subject +to browser CORS at all. Add `https://api.anthropic.com` to the Tauri CSP's `connect-src` (mirrors +the existing Grok entry). Desktop Settings UI swaps the warning-only block for a real API-key/model +flow. This is a narrow bug fix, not new architecture, and ships independently of Track B. + +**Track B (web/PWA, ships second, the actual new architecture):** browser `fetch()` has no +native-HTTP escape hatch — Anthropic's API sends no CORS headers permitting direct browser access, +so the only way to make Claude work in a browser tab is a server-side relay. This is the app's +**first backend dependency** ever (previously a purely static SPA across GitHub Pages, Vercel, and +Cloudflare Pages, plus a Tauri desktop bundle with no local server of its own). A Vercel Function +(`api/claude-proxy.ts`) relays `{ apiKey, model, messages }` to `https://api.anthropic.com/v1/messages` +statelessly — it must never log, persist, or cache the API key or message content. A Cloudflare +Pages Function equivalent follows for that deploy target. **GitHub Pages cannot host a serverless +function at all** (pure static hosting) — Claude shows an honest "unavailable on this deployment" +state there rather than silently failing or defaulting to a hosted proxy the user didn't choose. + +The proxy is public and unauthenticated by construction (it relays whatever API key the caller +supplies) — this makes it an externally-reachable resource-exhaustion surface (CWE-400) if shipped +without controls. Schema validation, a request body size limit, an origin check, rate limiting, and +timeouts on both legs (inbound and the outbound call to Anthropic) are **mandatory**, not optional +hardening to consider later. + +### Trust-model consequence (Track B only) + +Every other cloud provider in this app is a direct browser→provider call — the user's API key +never transits infrastructure WorldScript controls. Track B's proxy is the **first exception**: a +Claude request on the web now flows browser → WorldScript's Vercel/Cloudflare deployment → +Anthropic. The proxy is stateless (nothing is persisted or logged), but the key still passes +through code the maintainer runs, for the duration of that one request. This is documented +explicitly in `docs/SECURITY-THREAT-MODEL.md` and in README's privacy framing — it is not hidden +behind "just like BYOK for other providers," because it isn't quite the same guarantee. + +### Ollama-in-PWA (adjacent follow-up, Issue #266, not part of this decision's core scope) + +A related but separate question came up during this work: could the same proxy pattern let the PWA +reach the user's own local Ollama server? **No** — a hosted proxy can only reach itself, never a +user's private `localhost`, so this is not a variant of Track B. The actual answer (already +described by the maintainer in Issue #266's own comment thread) is an opt-in feature flag using +direct browser `fetch()`, matching NovelCrafter's real (non-magic) model: it works only when the +user configures their own Ollama server's `OLLAMA_ORIGINS` to the exact PWA origin. This narrowly +widens ADR-0012's "PWA stays desktop-only" decision for users who explicitly opt in and accept the +setup burden — it does not reverse that decision as the default. See the plan document's addendum +for the full design. + +## Consequences + +- **Positive:** Grok becomes fully usable with zero new infrastructure. Claude becomes usable on + desktop immediately (Track A) with a small, low-risk fix reusing an established pattern, and on + the web (Track B) once the proxy ships — closing a real gap between README's claims and actual + behavior for both providers. +- **Negative (accepted):** Track B introduces this app's first backend dependency, a genuine + architectural shift from "purely static SPA" — deliberately scoped as narrowly as possible (one + relay endpoint, no general backend-for-frontend layer) and documented as a trust-model change, + not glossed over. +- **Deliberately deferred:** a hardcoded fallback proxy URL for GitHub Pages (so that deployment + could have Claude too, at the cost of routing every such request through a fixed external host + the user didn't choose) is not part of this decision — it's called out in the plan as a choice + for the maintainer to make explicitly later, not a default. +- **Not yet resolved:** whether Claude's image-generation call site should get an equivalent fix + depends on whether Anthropic's API even offers image generation — verify before wiring UI for a + capability that may not exist. + +## References + +- [`GROK-PROVIDER-INTEGRATION-PLAN.md`](../../GROK-PROVIDER-INTEGRATION-PLAN.md) — full phased execution plan +- [[0012-local-server-connectivity-tauri-http]] — the native-HTTP pattern Track A reuses +- [[0004-csp-connect-src-byok-tradeoff]] — why the web CSP's `https:` scheme-source already covers Grok +- [Issue #266](https://github.com/qnbs/WorldScript-Studio/issues/266) — origin of both the ADR-0012 pattern and the Ollama-in-PWA follow-up diff --git a/docs/adr/README.md b/docs/adr/README.md index 14e4d557..c13421d6 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -21,6 +21,7 @@ than editing history. | [0013](0013-csp-wasm-and-blob-frames.md) | CSP `'wasm-unsafe-eval'` and `frame-src blob:` | Accepted | | [0014](0014-worker-generation-duplication.md) | Two live worker generations (v1 and WorkerBus v2) | Superseded by 0015 | | [0015](0015-worker-generation-consolidation.md) | Worker-generation consolidation: v1 retired, WorkerBus v2 is the sole generation | Accepted | +| [0016](0016-native-grok-and-claude-providers.md) | Native Grok provider + split Claude fix (desktop native-HTTP, web serverless proxy) | Accepted | **Format:** Context → Decision → Consequences (incl. rejected alternatives). Keep each ADR to one decision. Link related records with `[[slug]]`. From 2689f6d23e7ccebcebde5cd57b24a24a10d82417 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:18:24 +0200 Subject: [PATCH 4/5] feat(ai): Phase 1 -- native Grok provider wiring Grok (xAI, Cloud 4) had a real, working backend (aiProviderService.ts's streamGrok(), key storage, a live /v1/models connection test) but was never selectable as a primary provider -- reachable only as a hybrid- fallback-chain option. This closes that UI/wiring gap; no new backend work needed. - AiProviderCard.tsx: added Grok to the primary provider dropdown with its own API-key input + model selector (grok-3 / grok-3-mini), reusing the existing storageService key-storage plumbing and the already-working testAIConnection() grok case for Test Connection. Also fixed the whole provider array's i18n while touching it -- gemini/openai/ollama/anthropic labels were hardcoded literals too, not just Grok's gap. - providerFactory.ts: providerToKind() gets a distinct 'grok' kind (not folded into 'openaiCompatible') -- Grok needs a fixed baseURL (https://api.x.ai/v1) and a real stored key, unlike the Ollama default that also maps to 'openaiCompatible'; folding it in would have silently sent Grok requests through Ollama's localhost default. - worldScriptCompletionFetch.ts: resolveModelConfig() branches on the new 'grok' kind, bringing Grok into the newer Writer-streaming path (useWorldScriptAI), not just legacy thunks. - i18n: added settings.ai.grokKey + settings.ai.provider.{gemini, openai,ollama,anthropic,grok} across all 19 locales -- hand- translated for the 5 production locales (de/en/es/fr/it), EN fallback seeded for the other 14 pending the established bulk- translate workflow. Bundles rebuilt; README's key-count badge/table updated 2849 -> 2855. - Tests: providerFactory (grok maps to 'grok', not 'unsupported'), worldScriptCompletionFetch (grok success path + missing-key 401), AiProviderCard (renders key input + model selector, saves via storageService.saveApiKey('grok', ...)). No CSP change needed: https://api.x.ai is already in src-tauri/tauri.conf.json, and the web CSP's https: scheme-source (ADR-0004) already covers it. No new feature flag -- confirmed via `pnpm exec tsx scripts/audit-feature-parity.ts` (22 flags, 0 drifts). The many locales/*/{characters,common,dashboard,desktop,export, outline,worlds,writer}.json changes in this commit are incidental -- running the i18n tooling's --fix pass re-sorted keys in files it touched along the way. No content was added, removed, or changed in any of those files, only key order. --- README.md | 8 +- components/settings/AiProviderCard.tsx | 68 +++++- docs/i18n/AUDIT_AND_IMPROVEMENT_PLAN.md | 2 +- locales/ar/settings.json | 6 + locales/de/settings.json | 6 + locales/el/settings.json | 6 + locales/en/settings.json | 6 + locales/es/settings.json | 6 + locales/eu/settings.json | 6 + locales/fa/settings.json | 6 + locales/fi/settings.json | 6 + locales/fr/settings.json | 6 + locales/he/settings.json | 6 + locales/hu/settings.json | 6 + locales/is/settings.json | 6 + locales/it/settings.json | 6 + locales/ja/settings.json | 6 + locales/ko/settings.json | 6 + locales/pt/settings.json | 6 + locales/ru/settings.json | 6 + locales/sv/settings.json | 6 + locales/zh/settings.json | 6 + public/locales/ar/bundle.json | 150 ++++++------ public/locales/de/bundle.json | 228 +++++++++--------- public/locales/el/bundle.json | 150 ++++++------ public/locales/en/bundle.json | 6 + public/locales/es/bundle.json | 228 +++++++++--------- public/locales/eu/bundle.json | 150 ++++++------ public/locales/fa/bundle.json | 150 ++++++------ public/locales/fi/bundle.json | 150 ++++++------ public/locales/fr/bundle.json | 228 +++++++++--------- public/locales/he/bundle.json | 150 ++++++------ public/locales/hu/bundle.json | 150 ++++++------ public/locales/is/bundle.json | 150 ++++++------ public/locales/it/bundle.json | 228 +++++++++--------- public/locales/ja/bundle.json | 150 ++++++------ public/locales/ko/bundle.json | 150 ++++++------ public/locales/pt/bundle.json | 150 ++++++------ public/locales/ru/bundle.json | 150 ++++++------ public/locales/sv/bundle.json | 150 ++++++------ public/locales/zh/bundle.json | 150 ++++++------ services/ai/providerFactory.ts | 10 +- services/ai/worldScriptCompletionFetch.ts | 12 + .../ai/worldScriptCompletionFetch.test.ts | 42 ++++ tests/unit/providerFactory.test.ts | 6 +- tests/unit/settings/AiProviderCard.test.tsx | 51 +++- 46 files changed, 1862 insertions(+), 1469 deletions(-) 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..caf89c49 100644 --- a/components/settings/AiProviderCard.tsx +++ b/components/settings/AiProviderCard.tsx @@ -46,6 +46,8 @@ export const AiProviderCard: FC = ({ // imply a verified connection next to the "desktop app required" banner. const ollamaUntestable = provider === 'ollama' && !isDesktop; const [openaiKey, setOpenaiKey] = useState(''); + 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 +68,25 @@ export const AiProviderCard: FC = ({ .getApiKey('openai') .then((k) => setOpenaiKey(k ?? '')) .catch(() => {}); + storageService + .getApiKey('grok') + .then((k) => setGrokKey(k ?? '')) + .catch(() => {}); }, []); + 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 +190,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 +342,46 @@ export const AiProviderCard: FC = ({ )} + {provider === 'grok' && ( +
+ +
+ setGrokKey(e.target.value)} + className="flex-1 font-mono text-sm" + /> + +
+

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

+ +