- 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
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:
- 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. - 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. - 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
reactandreact-domreduces 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-uiprovides 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.svelteandAudioEngine.svelte.tsmust 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
AudioContextinstances.AudioEngine.svelte.tsmust implement strict$effectteardown lifecycles callingaudioCtx.close()upon component unmount. - Token Streaming Throughput: Server-Sent Events in
openRouterService.tsparse 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
localStorageandAuthorization: Bearer <key>headers. Zero telemetry leakage.
4.3 Lens 3: Test Rigor & QA ([TestRigor_QA_Auditor])
- TDD Contract: Establish Vitest test suites for:
UserSettingsStore.test.ts: LocalStorage read/write, 4-tier resolution hierarchy.AudioEngine.test.ts: Mocked WebAudio lifecycle, turn progression, error handling.CBTReference.test.ts: Reactive search filtering across distortions and techniques.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
- Scaffold Svelte 5 configuration in the repository:
- Add
@sveltejs/vite-plugin-svelteandsvelte@^5.0.0todevDependencies. - Install
bits-uiandlucide-svelte. - Update
vite.config.tsto support both Svelte and PWA compilation. - Configure Vitest with
@testing-library/sveltefor Svelte component testing.
- Add
Phase 2: Domain Services & Types Ingestion
- Re-use 100% framework-agnostic TypeScript files directly:
types.ts,constants.tsservices/apiConfig.ts,services/openRouterService.ts,services/sessionStorage.ts,services/audioUtils.ts
- Validate with existing test suite:
npm testcontinues passing 12/12.
Phase 3: Svelte 5 Reactive State Layer
- Author
src/lib/stores/UserSettingsStore.svelte.ts. - Author
src/lib/stores/AppStateStore.svelte.ts. - Author
src/lib/audio/AudioEngine.svelte.ts. - 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/)
- Implement
Button.svelte,Badge.svelte,Card.svelte,Dialog.svelte,Tabs.svelte,ScrollArea.svelte,Tooltip.svelte,Input.svelte,Textarea.svelte,Select.svelte. - Verify visual token parity with Tailwind 3.4 / 4 utility classes.
Phase 5: Feature Components Migration
- Author
ScenarioCard.svelte,CBTReference.svelte,SettingsModal.svelte,ScenarioGenerator.svelte. - Author
SessionReport.sveltewith Markdown and JSON report downloaders. - Author
LiveInsightPanel.sveltewith reactive distortion pill rendering and star rating.
Phase 6: Session Controllers & Audio Visualizer
- Author
ChatSession.svelte,ChatMessage.svelte,ChatInput.svelte. - Author
VoiceSession.svelte,VoiceHeader.svelte,VoiceControls.svelte,AudioVisualizer.svelte,TranscriptPanel.svelte. - Author
ConnectionState.svelte,AnalyzingState.svelte. - Test real-time WebAudio microphone frequency analysis and OpenRouter speech playback.
Phase 7: App Shell, PWA Precache & 5-Gate Validation
- Compose
src/App.sveltewith hero section, difficulty filter pills, scenario grid, and modal dialogs. - Update
sw.tsandvite.config.tsPWA injectManifest. - 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)
- Gate 1: Svelte TypeScript check (