Files
clarity-helper/docs/plans/PLAN_SVELTE_REWRITE.md
T
jpremill 1feea40684 feat(ui): redesign voice cockpit with clinical companion drawer, Svelte 5 migration, and Gemini 3.8 voice
- Re-architect VoiceSession into full-bleed studio cockpit with overlay slide-over companion drawer
- Eliminate width squishing defect on LiveInsightPanel and add native dark/light theme support
- Integrate authentic Google Gemini 3.8 Flash TTS with Fenrir persona and fallback audio pipeline
- Implement comprehensive Playwright E2E visual regression suite across Desktop, iPad, and iPhone (21 tests, 45 snapshots)
- Migrate core application to Svelte 5 runes (, , ) with reactive stores
- Maintain .NET 8 Azure Functions serverless endpoints and clean build hygiene
2026-09-29 23:35:05 -04:00

301 lines
21 KiB
Markdown

# Architectural Specification & Migration Plan: Svelte 5 Rewrite
**Project:** Cognitive Clarity (`clarity-helper`)
**Target Architecture:** Svelte 5 (Runes) + Vite 6 + Tailwind CSS + `bits-ui` + `lucide-svelte`
**Author:** Antigravity Autonomous Architecture Swarm (`[Svelte_Fullstack_Specialist]`, `[React_Frontend_Specialist]`, `[Adversarial_Auditor]`)
**Status:** Approved Blueprint
**Date:** 2026-09-29
---
## 1. Executive Summary & Architectural Rationale
Cognitive Clarity is currently built on React 19.2, Vite 6, Tailwind CSS 3.4, and Radix UI. While functional, the application suffers from React-specific architectural overhead:
1. **Audio DSP Re-render Friction:** WebAudio FFT frequency visualizers (`AudioVisualizer.tsx`) and mic/synthesis volume levels trigger frequent React state updates (`setAudioLevel`, `setAiAudioLevel`) at 60 FPS, causing unnecessary virtual DOM diffing cycles across parent components.
2. **Context & Prop Drilling Drag:** Settings and scenario parameters required explicit prop drilling down through `App.tsx` -> `ChatSession.tsx` / `VoiceSession.tsx`, leading to silent parameter dropping bugs.
3. **Bundle Weight:** React 19 + `react-dom` + `@radix-ui/*` primitives account for over 250 kB of the runtime JavaScript bundle.
### Why Svelte 5 (Runes)?
- **Fine-Grained Reactivity (Zero Virtual DOM):** Svelte 5 runes (`$state`, `$derived`, `$effect`) update the exact DOM elements bound to audio frequency bars without re-evaluating the component function or neighboring elements.
- **Vanishing Runtime & PWA Performance:** Eliminating `react` and `react-dom` reduces the vendor chunk footprint by over 60%, resulting in faster cold starts on mobile PWA standalone installs.
- **Universal Reactive Classes:** Complex state machines like WebAudio DSP capture and turn-by-turn clinical supervision can be modeled as pure TypeScript reactive classes (`AudioEngine.svelte.ts`, `SessionStore.svelte.ts`), cleanly decoupling audio hardware from UI presentation.
- **Bits UI Parity:** `bits-ui` provides accessible, unstyled headless primitives with 1:1 API parity to Radix UI.
---
## 2. Component-by-Component Mapping & Recomposition Matrix
Every single React component and hook is systematically evaluated, documented, and mapped to its Svelte 5 counterpart:
### 2.1 Application Shell & State Coordinators
| React Component | Svelte 5 Component | React Mechanics (`useState`, `useEffect`, etc.) | Svelte 5 Recomposition (`$state`, `$derived`, `$effect`, etc.) |
|:---|:---|:---|:---|
| **`App.tsx`** | **`src/App.svelte`** | Router state (`activeScenario`, `sessionMode`, `isPreparing`), modal toggles (`showGenerator`, `showReference`, `showSettings`), `localStorage` sync via `useEffect`. | Uses root `AppStore.svelte.ts` context. State managed with `$state`. Modals rendered via declarative snippets. PWA `ReloadPrompt` mounted at root. |
| **`index.tsx`** | **`src/main.ts`** | `createRoot(document.getElementById('root')!).render(<App />)`. | `mount(App, { target: document.getElementById('app')! })` using Svelte 5 `mount` API. |
| **`index.html`** | **`index.html`** | `<div id="root"></div>`, scripts pointing to `index.tsx`. | `<div id="app"></div>`, script pointing to `/src/main.ts`. Retains PWA manifest and Inter font. |
| **`sw.ts`** | **`src/sw.ts`** | Workbox injectManifest precaching + Google Fonts runtime caching. | 100% retained. `vite-plugin-pwa` operates identically with `@sveltejs/vite-plugin-svelte`. |
---
### 2.2 Design System UI Primitives (`components/ui/` -> `src/lib/components/ui/`)
All Radix UI primitives are mapped to `bits-ui` (or native accessible HTML where headless UI is redundant):
| React Component | Svelte 5 Component | Dependencies (React) | Dependencies (Svelte 5) | Recomposition Notes |
|:---|:---|:---|:---|:---|
| **`ui/button.tsx`** | **`Button.svelte`** | `@radix-ui/react-slot`, `class-variance-authority` | `bits-ui/button` or HTML `<button>` + `tailwind-variants` | Uses Svelte 5 `Snippet` for default children. Supports `variant` and `size` props. |
| **`ui/card.tsx`** | **`Card.svelte`**, `CardHeader.svelte`, `CardTitle.svelte`, `CardContent.svelte`, `CardFooter.svelte` | Native `div` containers with Tailwind styling | Native `div` components using `{#snippet children()}` | Clean composable snippets replacing sub-component JSX props. |
| **`ui/badge.tsx`** | **`Badge.svelte`** | `class-variance-authority`, `clsx`, `tailwind-merge` | `clsx` + `tailwind-merge` + `$props<{ variant?: 'default' \| 'secondary' \| 'destructive' \| 'outline' }>()` | Native `span` with typed variants. |
| **`ui/dialog.tsx`** | **`Dialog.svelte`** | `@radix-ui/react-dialog` | `bits-ui` (`Dialog.Root`, `Dialog.Trigger`, `Dialog.Portal`, `Dialog.Overlay`, `Dialog.Content`) | Retains exact backdrop blur, animations, and focus trapping. |
| **`ui/tabs.tsx`** | **`Tabs.svelte`** | `@radix-ui/react-tabs` | `bits-ui` (`Tabs.Root`, `Tabs.List`, `Tabs.Trigger`, `Tabs.Content`) | Controlled tab state via `$bindable()` or internal value. |
| **`ui/scroll-area.tsx`** | **`ScrollArea.svelte`** | `@radix-ui/react-scroll-area` | `bits-ui` (`ScrollArea.Root`, `ScrollArea.Viewport`, `ScrollArea.Scrollbar`) | Custom styled scrollbar matching dark/light themes. |
| **`ui/tooltip.tsx`** | **`Tooltip.svelte`** | `@radix-ui/react-tooltip` | `bits-ui` (`Tooltip.Root`, `Tooltip.Trigger`, `Tooltip.Content`) | Replaces bespoke `components/Tooltip.tsx` and Radix tooltip with unified Svelte 5 component. |
| **`ui/select.tsx`** | **`Select.svelte`** | `@radix-ui/react-select` | `bits-ui` (`Select.Root`, `Select.Trigger`, `Select.Content`, `Select.Item`) | Accessible dropdown selector for audio devices and difficulty. |
| **`ui/switch.tsx`** | **`Switch.svelte`** | `@radix-ui/react-switch` | `bits-ui` (`Switch.Root`, `Switch.Thumb`) | Accessible toggle switch. |
| **`ui/input.tsx`** | **`Input.svelte`** | Native `<input>` with Tailwind classes | Native `<input>` with `$bindable(value)` and rest props | Type-safe two-way binding. |
| **`ui/textarea.tsx`** | **`Textarea.svelte`** | Native `<textarea>` with Tailwind classes | Native `<textarea>` with `$bindable(value)` and auto-resize | Two-way reactive text binding. |
| **`ui/separator.tsx`** | **`Separator.svelte`** | `@radix-ui/react-separator` | `bits-ui` (`Separator.Root`) | Semantic divider. |
| **`ui/label.tsx`** | **`Label.svelte`** | `@radix-ui/react-label` | `bits-ui` (`Label.Root`) or `<label>` | Semantic form labeling. |
---
### 2.3 Interactive Domain Feature Components
| React Component | Svelte 5 Component | Core Mechanics & State | Svelte 5 Architectural Treatment |
|:---|:---|:---|:---|
| **`ScenarioCard.tsx`** | **`ScenarioCard.svelte`** | Displays scenario metadata, difficulty badge, CBT focus tags. Has "Chat" and "Voice" launcher buttons and custom delete trigger. | `$props<{ scenario: Scenario, onSelect: (s: Scenario, mode: SessionMode) => void, onDelete?: (id: string) => void }>()`. Uses `Badge` and `Button` snippets. Tooltip hover explanations. |
| **`ChatSession.tsx`** | **`ChatSession.svelte`** | Message stream rendering, SSE chunk accumulation, automatic scroll-to-bottom on new messages, out-of-band turn supervision, post-session summary. | Driven by `ChatSessionController.svelte.ts`. Message list auto-scroll handled via an action `use:autoScroll`. Sub-components `ChatMessage.svelte` and `ChatInput.svelte`. Out-of-band analysis drawer rendered in flex container. |
| **`ChatMessage.tsx`** | **`ChatMessage.svelte`** | Renders user vs AI character dialogue bubbles with timestamps and role avatar. | Pure presentational component. Formats timestamps via `$derived`. Supports markdown rendering through Svelte markdown component. |
| **`ChatInput.tsx`** | **`ChatInput.svelte`** | Text input with enter-key send shortcut and disabled state while waiting for AI turn. | Form binding with `$state(text)`. Enter key listener `onkeydown={(e) => e.key === 'Enter' && !e.shiftKey && submit()}`. |
| **`VoiceSession.tsx`** | **`VoiceSession.svelte`** | Duplex voice session orchestrator. Coordinates WebAudio capture, FFT visualizer, speech synthesis, live insight drawer, transcripts modal. | Uses `AudioEngine.svelte.ts` service class. Binds visualizer level directly to reactive state without re-rendering container. Coordinates sub-components `VoiceHeader`, `VoiceControls`, `AudioVisualizer`, `TranscriptPanel`. |
| **`VoiceHeader.tsx`** | **`VoiceHeader.svelte`** | Scenario title, microphone device selector button, finish session trigger. | Simple header receiving `$props<{ scenario: Scenario, onExit: () => void, onOpenDeviceSelector: () => void }>()`. |
| **`VoiceControls.tsx`** | **`VoiceControls.svelte`** | Mute button toggle, finish session button, transcript drawer button. | Reactive buttons bound to `audio.isMuted` and `$state(showTranscript)`. |
| **`AnalyzingState.tsx`** | **`AnalyzingState.svelte`** | Animated post-session clinical synthesis loading screen. | SVG animated spinners and pulsing status typography. |
| **`ConnectionState.tsx`** | **`ConnectionState.svelte`** | Pre-session audio hardware initialization and calibration screen. | Smooth CSS pulsing visualizer. |
| **`AudioVisualizer.tsx`** | **`AudioVisualizer.svelte`** | 19 animated frequency bars dynamically styled based on microphone level or AI vocalization level. | Directly mutates bar heights via `$derived` or inline style calculation. In Svelte 5, updates happen at microtask speed with zero VDOM diffing. |
| **`AudioDeviceSelector.tsx`** | **`AudioDeviceSelector.svelte`** | Queries `navigator.mediaDevices.enumerateDevices()`, selects input mic. | Device enumeration encapsulated in a reactive Svelte 5 class or effect. Accessible `Select` dropdown. |
| **`LiveInsightPanel.tsx`** | **`LiveInsightPanel.svelte`** | Out-of-band supervisor drawer displaying distortions detected, adaptiveness, evidence quote, efficiency score, reframes, and tactical pivots. | Animates into view via `transition:slide`. Renders structured `AnalysisResult` reactive prop. |
| **`SessionReport.tsx`** | **`SessionReport.svelte`** | Comprehensive post-session CBT assessment report with mastery score, trajectory, homework suggestions, transcript accordion, Markdown & JSON downloaders. | Generates download blobs via `sessionStorage.ts`. Accordion transcript drawer. Clipboard copy with reactive `$state(copied)` timeout. |
| **`ScenarioGenerator.tsx`** | **`ScenarioGenerator.svelte`** | AI scenario builder dialog. Prompts user for description, selects CBT concepts from glossary, calibrates difficulty, calls OpenRouter JSON API. | Dialog modal using `Dialog.svelte`. Reactive multiselect tag chips with `$state(selectedConcepts)`. Generates validated scenario and appends to `customScenarios`. |
| **`CBTReference.tsx`** | **`CBTReference.svelte`** | Interactive searchable CBT glossary covering all 18 clinical terms, definitions, categories, and real-world examples. | Filter tabs (`All`, `Distortions`, `Techniques`, `Concepts`) and search input. Filtered list calculated cleanly via `$derived(filteredGlossary)`. |
| **`SettingsModal.tsx`** | **`SettingsModal.svelte`** | Tabs for Voice & Audio (vocal persona, mic device) and OpenRouter AI (API key input, model selector with `google/gemini-3.8-flash`). | Accessible `Tabs.svelte`. Binds `$state(openRouterKey)` and `$state(selectedModel)`. Persists to `UserSettingsStore.svelte.ts`. |
| **`TranscriptPanel.tsx`** | **`TranscriptPanel.svelte`** | Slide-over drawer displaying conversation history during voice sessions. | Svelte transition drawer (`transition:fly={{ x: 300 }}`) with `ScrollArea`. |
| **`ReloadPrompt.svelte`** | **`ReloadPrompt.svelte`** | PWA update notification toast when Workbox detects a service worker update. | Uses `virtual:pwa-register/svelte` to trigger automatic or prompt-based reload. |
| **`VersionBadge.svelte`** | **`VersionBadge.svelte`** | Displays `__APP_VERSION__` and `__BUILD_TIMESTAMP__`. | Reads Vite compile-time definitions. |
---
## 3. Reactive Architecture & State Engineering (Svelte 5 Runes)
In Svelte 5, state management is vastly superior to React's `useState` / `useCallback` / `useRef` paradigm. We will implement three first-class reactive state stores:
### 3.1 `UserSettingsStore.svelte.ts` (Global Preferences)
```typescript
import { UserSettings } from './types';
import { DEFAULT_OPENROUTER_MODEL, DEFAULT_SUPERVISOR_MODEL } from './services/apiConfig';
const SETTINGS_KEY = 'cognitive-clarity-settings';
export class UserSettingsStore {
voiceName = $state<UserSettings['voiceName']>('Zephyr');
vocalStyle = $state('Helpful');
genderPreference = $state<'Male' | 'Female' | 'Neutral'>('Neutral');
preferredAudioDeviceId = $state<string | undefined>(undefined);
openRouterApiKey = $state<string | undefined>(undefined);
roleplayModel = $state(DEFAULT_OPENROUTER_MODEL);
supervisorModel = $state(DEFAULT_SUPERVISOR_MODEL);
constructor() {
this.load();
}
load() {
if (typeof localStorage === 'undefined') return;
try {
const saved = localStorage.getItem(SETTINGS_KEY);
if (saved) {
const parsed = JSON.parse(saved);
Object.assign(this, parsed);
}
} catch (e) {
console.error("Failed to load settings", e);
}
}
save() {
if (typeof localStorage === 'undefined') return;
const data: UserSettings = {
voiceName: this.voiceName,
vocalStyle: this.vocalStyle,
genderPreference: this.genderPreference,
preferredAudioDeviceId: this.preferredAudioDeviceId,
openRouterApiKey: this.openRouterApiKey,
roleplayModel: this.roleplayModel,
supervisorModel: this.supervisorModel,
};
localStorage.setItem(SETTINGS_KEY, JSON.stringify(data));
}
}
export const userSettings = new UserSettingsStore();
```
### 3.2 `AudioEngine.svelte.ts` (Isolated WebAudio DSP Controller)
Replaces `hooks/useLiveSession.ts`. Completely encapsulates hardware access, FFT frequency calculations, and speech synthesis:
```typescript
import { Scenario, Message, AnalysisResult } from './types';
import { UserSettingsStore } from './UserSettingsStore.svelte';
import { streamOpenRouterChat, analyzeTurnWithOpenRouter, synthesizeSpeechWithOpenRouter } from './services/openRouterService';
export class AudioEngine {
status = $state<'connecting' | 'active' | 'analyzing' | 'finished' | 'error'>('connecting');
errorMessage = $state<string | null>(null);
isMuted = $state(false);
micLevel = $state(0);
aiAudioLevel = $state(0);
isAiSpeaking = $state(false);
isUserSpeaking = $state(false);
messages = $state<Message[]>([]);
liveInsight = $state<AnalysisResult | null>(null);
isAnalyzingTurn = $state(false);
// Active level derived cleanly for visualizer
activeLevel = $derived(this.isAiSpeaking ? this.aiAudioLevel : this.micLevel);
private inputCtx: AudioContext | null = null;
private outputCtx: AudioContext | null = null;
private userAnalyser: AnalyserNode | null = null;
private aiAnalyser: AnalyserNode | null = null;
private activeSource: AudioBufferSourceNode | null = null;
private recognition: any = null;
private animFrameId: number | null = null;
constructor(private scenario: Scenario, private settings: UserSettingsStore) {}
async start() {
// 1. Initialize AudioContexts & Analysers
// 2. Setup Web Speech Recognition
// 3. Start RAF loop to update this.micLevel and this.aiAudioLevel
// 4. Initial scenario greeting
}
stopAllAudio() {
this.activeSource?.stop();
this.activeSource = null;
if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
window.speechSynthesis.cancel();
}
this.isAiSpeaking = false;
this.aiAudioLevel = 0;
}
destroy() {
this.stopAllAudio();
if (this.animFrameId) cancelAnimationFrame(this.animFrameId);
this.recognition?.stop();
this.inputCtx?.close();
this.outputCtx?.close();
}
}
```
### 3.3 Context Injection via Svelte 5 Context API
To avoid prop drilling, the root `App.svelte` provisions the stores using `setContext`:
```typescript
// src/lib/context.ts
import { getContext, setContext } from 'svelte';
import { UserSettingsStore } from './UserSettingsStore.svelte';
import { AppStateStore } from './AppStateStore.svelte';
const SETTINGS_KEY = Symbol('USER_SETTINGS');
const APP_STATE_KEY = Symbol('APP_STATE');
export const setUserSettingsContext = (store: UserSettingsStore) => setContext(SETTINGS_KEY, store);
export const getUserSettingsContext = () => getContext<UserSettingsStore>(SETTINGS_KEY);
export const setAppStateContext = (store: AppStateStore) => setContext(APP_STATE_KEY, store);
export const getAppStateContext = () => getContext<AppStateStore>(APP_STATE_KEY);
```
---
## 4. Tri-Lens Adversarial Swarm Audit (`--audit --swarm`)
### 4.1 Lens 1: Requirements & Blast Radius (`[Requirements_BlastRadius_Auditor]`)
- **Clinical Roleplay Invariant:** Svelte 5 implementation must preserve strict separation between character dialogue and supervisor feedback. System instructions in `ChatSession.svelte` and `AudioEngine.svelte.ts` must maintain anti-meta constraints:
- AI character NEVER congratulates the user or breaks character.
- Clinical supervision runs strictly out-of-band via parallel OpenRouter promises.
- **Scenario & Glossary Parity:** 100% preservation of all 14 scenarios and 18 glossary terms from `constants.ts`.
- **Backend API Neutrality:** The existing Azure Functions in `api/` (`/api/chat`, `/api/analysis`, `/api/scenario/generate`) remain 100% compatible and unchanged, since Svelte consumes the same HTTP JSON contracts.
### 4.2 Lens 2: Performance & Security (`[Performance_Security_Auditor]`)
- **WebAudio Garbage Collection & Context Limits:** Browsers enforce a hard cap of ~6 active `AudioContext` instances. `AudioEngine.svelte.ts` must implement strict `$effect` teardown lifecycles calling `audioCtx.close()` upon component unmount.
- **Token Streaming Throughput:** Server-Sent Events in `openRouterService.ts` parse chunks at wire speed. In Svelte 5, text accumulation directly appends to `$state(text)` without triggering full component tree re-renders.
- **Client Key Security:** OpenRouter API keys remain strictly local to `localStorage` and `Authorization: Bearer <key>` headers. Zero telemetry leakage.
### 4.3 Lens 3: Test Rigor & QA (`[TestRigor_QA_Auditor]`)
- **TDD Contract:** Establish Vitest test suites for:
1. `UserSettingsStore.test.ts`: LocalStorage read/write, 4-tier resolution hierarchy.
2. `AudioEngine.test.ts`: Mocked WebAudio lifecycle, turn progression, error handling.
3. `CBTReference.test.ts`: Reactive search filtering across distortions and techniques.
4. `ScenarioGenerator.test.ts`: Schema validation for AI-generated scenarios.
---
## 5. Phased Implementation Roadmap & Verification Gates
```mermaid
flowchart LR
Phase1[Phase 1: Setup & Dependencies] --> Phase2[Phase 2: Core Services & Types]
Phase2 --> Phase3[Phase 3: Svelte 5 Runes & Stores]
Phase3 --> Phase4[Phase 4: Design Primitives bits-ui]
Phase4 --> Phase5[Phase 5: Feature Components]
Phase5 --> Phase6[Phase 6: Audio & Chat Engines]
Phase6 --> Phase7[Phase 7: 5-Gate CI Validation]
```
### Phase 1: Tooling & Dual Workspace Harness
1. Scaffold Svelte 5 configuration in the repository:
- Add `@sveltejs/vite-plugin-svelte` and `svelte@^5.0.0` to `devDependencies`.
- Install `bits-ui` and `lucide-svelte`.
- Update `vite.config.ts` to support both Svelte and PWA compilation.
- Configure Vitest with `@testing-library/svelte` for Svelte component testing.
### Phase 2: Domain Services & Types Ingestion
1. Re-use 100% framework-agnostic TypeScript files directly:
- `types.ts`, `constants.ts`
- `services/apiConfig.ts`, `services/openRouterService.ts`, `services/sessionStorage.ts`, `services/audioUtils.ts`
2. Validate with existing test suite: `npm test` continues passing 12/12.
### Phase 3: Svelte 5 Reactive State Layer
1. Author `src/lib/stores/UserSettingsStore.svelte.ts`.
2. Author `src/lib/stores/AppStateStore.svelte.ts`.
3. Author `src/lib/audio/AudioEngine.svelte.ts`.
4. Author Vitest unit tests verifying state transitions, defaults (`google/gemini-3.8-flash`), and storage persistence.
### Phase 4: Svelte Design System (`src/lib/components/ui/`)
1. Implement `Button.svelte`, `Badge.svelte`, `Card.svelte`, `Dialog.svelte`, `Tabs.svelte`, `ScrollArea.svelte`, `Tooltip.svelte`, `Input.svelte`, `Textarea.svelte`, `Select.svelte`.
2. Verify visual token parity with Tailwind 3.4 / 4 utility classes.
### Phase 5: Feature Components Migration
1. Author `ScenarioCard.svelte`, `CBTReference.svelte`, `SettingsModal.svelte`, `ScenarioGenerator.svelte`.
2. Author `SessionReport.svelte` with Markdown and JSON report downloaders.
3. Author `LiveInsightPanel.svelte` with reactive distortion pill rendering and star rating.
### Phase 6: Session Controllers & Audio Visualizer
1. Author `ChatSession.svelte`, `ChatMessage.svelte`, `ChatInput.svelte`.
2. Author `VoiceSession.svelte`, `VoiceHeader.svelte`, `VoiceControls.svelte`, `AudioVisualizer.svelte`, `TranscriptPanel.svelte`.
3. Author `ConnectionState.svelte`, `AnalyzingState.svelte`.
4. Test real-time WebAudio microphone frequency analysis and OpenRouter speech playback.
### Phase 7: App Shell, PWA Precache & 5-Gate Validation
1. Compose `src/App.svelte` with hero section, difficulty filter pills, scenario grid, and modal dialogs.
2. Update `sw.ts` and `vite.config.ts` PWA injectManifest.
3. Run 5-Gate Validation:
- Gate 1: Svelte TypeScript check (`svelte-check` / `tsc --noEmit`)
- Gate 2: Vitest unit & component test suite (`npm test`)
- Gate 3: Scenario schema validator (`npm run validate:scenarios`)
- Gate 4: Production build (`npm run build:nobump`)
- Gate 5: Backend assembly compilation (`dotnet build api/api.csproj`)