Files
clarity-helper/docs/plans/PLAN_SVELTE_REWRITE.md
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

21 KiB

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)

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:

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:

// 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

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)