diff --git a/.agents/skills/azure-functions-genai/SKILL.md b/.agents/skills/azure-functions-genai/SKILL.md new file mode 100644 index 0000000..38cbaf6 --- /dev/null +++ b/.agents/skills/azure-functions-genai/SKILL.md @@ -0,0 +1,84 @@ +--- +name: azure-functions-genai +description: >- + Guides the design, implementation, and debugging of Azure Functions v4 (.NET 8/10 Isolated Worker) + using the official Google.GenAI C# SDK for serverless AI endpoints. +--- + +# Azure Functions & Google.GenAI C# SDK Skill + +Use this skill when developing, updating, or testing the C# serverless API endpoints located in `api/`. + +--- + +## 1. Solution Architecture + +The backend consists of an **Azure Functions v4 Isolated Worker** targeting **.NET 8.0**: +- Project file: `api/api.csproj` +- Runtime: `Microsoft.Azure.Functions.Worker` 2.50+ & `Google.GenAI` 1.20+ +- Host configuration: `api/host.json` +- Application Insights integration: `Microsoft.ApplicationInsights.WorkerService` + +--- + +## 2. Google.GenAI C# SDK Integration Patterns + +### 2.1 Namespaces & Types +```csharp +using Google.GenAI; +using Google.GenAI.Types; +using Type = Google.GenAI.Types.Type; // Disambiguate with System.Type +``` + +### 2.2 Client Initialization +```csharp +var apiKey = System.Environment.GetEnvironmentVariable("GEMINI_API_KEY"); +var client = new Client(apiKey: apiKey); +``` + +### 2.3 Structured Output Generation (JSON Schema) +```csharp +var schema = new Schema +{ + Type = Type.Object, + Properties = new Dictionary + { + { "distortionsIdentified", new Schema { Type = Type.Array, Items = new Schema { Type = Type.String } } }, + { "techniquesUsed", new Schema { Type = Type.Array, Items = new Schema { Type = Type.String } } }, + { "clinicalInsight", new Schema { Type = Type.String } }, + { "feedbackType", new Schema { Type = Type.String, Enum = new List { "Constructive", "Positive", "Neutral" } } } + }, + Required = new List { "distortionsIdentified", "techniquesUsed", "clinicalInsight", "feedbackType" } +}; + +var response = await client.Models.GenerateContentAsync( + model: "gemini-2.5-flash", + contents: promptContent, + config: new GenerateContentConfig + { + SystemInstruction = new Content { Parts = new List { new Part { Text = systemInstruction } } }, + ResponseMimeType = "application/json", + ResponseSchema = schema + }); +``` + +--- + +## 3. Endpoints Matrix + +| Route | HTTP Method | Function Name | Purpose | +|:---|:---|:---|:---| +| `/api/chat` | `POST` | `ChatProgression` | Headless roleplay progression with strict anti-meta guardrails | +| `/api/analysis` | `POST` | `ClinicalAnalysis` | Real-time clinical supervisor turn evaluation | +| `/api/scenario/generate` | `POST` | `GenerateScenario` | Dynamic CBT roleplay scenario generation | + +--- + +## 4. Local Build & Test Verification +```bash +# Build the C# API project +dotnet build api/api.csproj + +# Run local functions host (requires Azure Functions Core Tools) +func start --csharp --prefix api +``` diff --git a/.agents/skills/cbt-clinical-evaluation/SKILL.md b/.agents/skills/cbt-clinical-evaluation/SKILL.md new file mode 100644 index 0000000..905d7ec --- /dev/null +++ b/.agents/skills/cbt-clinical-evaluation/SKILL.md @@ -0,0 +1,56 @@ +--- +name: cbt-clinical-evaluation +description: >- + Provides clinical supervision protocols, real-time distortion detection rules, + psychometric scoring rubrics, and post-session diagnostic report formatting. +--- + +# CBT Clinical Evaluation & Supervision Skill + +Use this skill when refining the clinical analysis supervisor model prompts, tuning distortion detection thresholds, or formatting final session case reports. + +--- + +## 1. Real-Time Turn-by-Turn Supervision Protocol + +During a roleplay session, every completed user turn is evaluated out-of-band by an asynchronous clinical supervisor prompt. + +### 1.1 Clinical Schema +```typescript +export interface AnalysisResult { + analysis: { + distortions: string[]; // Detected distortions (e.g. "Mind Reading", "Catastrophizing") + isAdaptive: boolean; // True if the response demonstrates cognitive restructuring + evidence: string; // Direct quote or behavioral evidence from user's response + }; + rating: { + score: number; // 1-10 clinical effectiveness score + reason: string; // Clinical justification for the score + }; + suggestions: { + alternativeThought: string; // Rational reframe of the user's statement + behavioralStep: string; // Actionable communication or behavioral adjustment + }; +} +``` + +--- + +## 2. Clinical Evaluation Rubric (1-10 Scoring Scale) + +| Score Range | Clinical Category | Behavioral & Cognitive Characteristics | +|:---|:---|:---| +| **1 - 3** | **Maladaptive / Reactive** | User falls into cognitive distortions without awareness; demonstrates passive-aggressive, defensive, or submissive behavior; abdicates personal boundaries. | +| **4 - 6** | **Emerging Awareness / Partial Restructuring** | User attempts to challenge distortions or state boundaries, but tone remains emotionally reactive, apologetic, or hesitant. | +| **7 - 8** | **Adaptive / Effective Restructuring** | Clear cognitive restructuring; assertive, non-blaming boundary setting; validates other person's emotions while holding firm limits. | +| **9 - 10** | **Mastery / Radical Flexibility** | Demonstrates emotional regulation under high friction; employs sophisticated techniques (e.g., DEAR MAN, Gray Rock, Radical Acceptance); de-escalates without concession. | + +--- + +## 3. Post-Session Comprehensive Case Report + +At the end of a session, a final summary prompt generates a macro assessment: +1. **Primary Cognitive Distortions Identified**: Frequency and contexts of observed distortions. +2. **Techniques Successfully Applied**: Count and effectiveness of behavioral tools used. +3. **Core Cognitive Shifts**: Evolution of the user's responses across the dialogue arc. +4. **Targeted Clinical Homework**: 1-2 practical real-world exercises to reinforce learning. diff --git a/.agents/skills/cbt-scenario-design/SKILL.md b/.agents/skills/cbt-scenario-design/SKILL.md new file mode 100644 index 0000000..25352c9 --- /dev/null +++ b/.agents/skills/cbt-scenario-design/SKILL.md @@ -0,0 +1,78 @@ +--- +name: cbt-scenario-design +description: >- + Designs, calibrates, and validates Cognitive Behavioral Therapy (CBT) roleplay scenarios, + difficulty ladders, character psychological profiles, and internal CBT logic rules. +--- + +# CBT Scenario Design & Calibration Skill + +Use this skill when creating new simulation scenarios, tuning AI character difficulty levels, adding cognitive distortion targets, or modifying the scenario catalog. + +--- + +## 1. Clinical Scenario Architecture + +Every scenario in Cognitive Clarity conforms to the following schema: + +```typescript +export interface Scenario { + id: string; // Unique kebab-case slug (e.g. 'workplace-critical-review') + title: string; // Human-readable evocative title + description: string; // Context setting the premise for the user + category: 'Social Anxiety' | 'Workplace' | 'Relationships' | 'Self-Talk'; + difficulty: 'Beginner' | 'Intermediate' | 'Advanced'; + characterName: string; // Name or archetype of the AI partner + initialMessage: string; // The opening dialogue from the AI character + cbtFocus: string[]; // Target distortions and techniques + systemInstruction: string; // Comprehensive prompt with character persona & logic +} +``` + +--- + +## 2. Difficulty Calibration Rubric + +| Difficulty | Character Resistance | Cognitive Distortion Salience | User Task | Recommended Category | +|:---|:---|:---|:---|:---| +| **Beginner** | Highly receptive to assertive communication; explicitly hints at expectations. | Overt, obvious distortions (e.g., explicit All-or-Nothing statements). | Identify and dispute basic cognitive distortions; maintain calm presence. | `Social Anxiety`, `Self-Talk` | +| **Intermediate** | Ambiguous motivations; subtle passive-aggression; holds reasonable opposing perspective. | Subtle, nuanced distortions (e.g., Mind Reading, Fortune Telling, Maladaptive Guilt). | Set boundaries, de-escalate without defensiveness, practice Cognitive Reframing. | `Workplace`, `Relationships` | +| **Advanced** | Strong emotional activation; high clinical resistance; persistent invalidation or defensiveness. | Layered cognitive distortions combined with emotional triggers. | Apply Radical Acceptance, Gray Rock Method, multi-turn emotional regulation under stress. | `Relationships`, `Self-Talk` | + +--- + +## 3. System Instruction Design Blueprint + +When writing `systemInstruction` for a scenario, always follow this four-part structure: + +```markdown +1. CHARACTER IDENTITY & CONTEXT: + - Name, relationship to user, current emotional state, and immediate goal. + - Tone markers (e.g., "curt, hurried, stressed" or "skeptical, passive-aggressive"). + +2. INTERNAL CBT LOGIC: + - IF the user becomes defensive or aggressive -> Escalate emotional distance or defensiveness. + - IF the user validates emotions while establishing clear boundaries -> De-escalate and show grudging respect. + - IF the user asks clarifying questions without mind reading -> Clarify directly and remove ambiguity. + +3. VOCAL & DIALOGUE STYLE: + - Sentence length: Short and conversational (1-3 sentences per turn). + - Natural conversational elements: Hesitations, pauses, tone inflections. + +4. ANTI-META GUARDRAILS: + - NEVER break character. + - NEVER provide feedback, scores, advice, or therapy tips. + - NEVER say "Good job" or "That was a great reframing". +``` + +--- + +## 4. Scenario Validation Checklist + +- [ ] `id` is unique and formatted as lowercase kebab-case. +- [ ] `category` is one of the four clinical categories. +- [ ] `difficulty` is properly calibrated (`Beginner`, `Intermediate`, `Advanced`). +- [ ] `cbtFocus` lists at least 2 valid entries from `CBT_GLOSSARY`. +- [ ] `initialMessage` immediately immerses the user without preamble. +- [ ] `systemInstruction` contains explicit `INTERNAL CBT LOGIC` rules. +- [ ] Zero clinical meta-feedback or congratulatory phrases in persona instructions. diff --git a/.agents/skills/webaudio-gemini-live/SKILL.md b/.agents/skills/webaudio-gemini-live/SKILL.md new file mode 100644 index 0000000..b3feaa0 --- /dev/null +++ b/.agents/skills/webaudio-gemini-live/SKILL.md @@ -0,0 +1,95 @@ +--- +name: webaudio-gemini-live +description: >- + Guides the integration, troubleshooting, and optimization of real-time WebAudio capture, + Gemini Multimodal Live API WebSocket streaming, PCM encoding, and frequency visualizers. +--- + +# WebAudio & Gemini Multimodal Live API Skill + +Use this skill when developing, debugging, or optimizing real-time duplex voice conversations with Google Gemini's native audio models (`gemini-2.5-flash-native-audio-preview-09-2025` or future native audio models). + +--- + +## 1. WebAudio Architecture & Data Flow + +```mermaid +sequenceDiagram + participant UserMic as Microphone (MediaStream) + participant InAudioCtx as Input AudioContext (16kHz) + participant LiveWS as Gemini Live WebSocket + participant OutAudioCtx as Output AudioContext (24kHz) + participant Visualizer as AnalyserNode & Visualizer + + UserMic->>InAudioCtx: Raw Audio Stream + InAudioCtx->>InAudioCtx: Float32 -> Int16 PCM (16kHz) + InAudioCtx->>LiveWS: sendRealtimeInput({ media: { data: Base64, mimeType: 'audio/pcm;rate=16000' } }) + LiveWS-->>OutAudioCtx: serverContent.modelTurn (PCM 24kHz Base64) + OutAudioCtx->>Visualizer: Frequency Data Array (FFT) + OutAudioCtx->>OutAudioCtx: Schedule AudioBufferSourceNode (Monotonic Queue) + LiveWS-->>LiveWS: serverContent.interrupted -> Stop All Audio +``` + +--- + +## 2. Critical Implementation Guidelines + +### 2.1 Audio Context Configuration +- **Input Sample Rate**: Must be initialized at `16000` Hz: + ```typescript + const inputAudioCtx = new (window.AudioContext || (window as any).webkitAudioContext)({ sampleRate: 16000 }); + ``` +- **Output Sample Rate**: Must be initialized at `24000` Hz to match Gemini's native output audio sample rate: + ```typescript + const outputAudioCtx = new (window.AudioContext || (window as any).webkitAudioContext)({ sampleRate: 24000 }); + ``` + +### 2.2 PCM Conversion Math +Convert 32-bit floating point audio (`-1.0` to `+1.0`) into 16-bit signed PCM integers (`-32768` to `+32767`): +```typescript +export function floatTo16BitPCM(float32Array: Float32Array): Int16Array { + const int16Array = new Int16Array(float32Array.length); + for (let i = 0; i < float32Array.length; i++) { + const s = Math.max(-1, Math.min(1, float32Array[i])); + int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; + } + return int16Array; +} +``` + +### 2.3 Monotonic Playback Scheduling +To prevent audio overlap or gaps: +```typescript +const nextStartTimeRef = useRef(0); +const audioCtx = outputAudioCtxRef.current!; + +// Ensure scheduled start is never in the past +nextStartTimeRef.current = Math.max(nextStartTimeRef.current, audioCtx.currentTime); + +const source = audioCtx.createBufferSource(); +source.buffer = buffer; +source.connect(analyserNode); +source.start(nextStartTimeRef.current); +nextStartTimeRef.current += buffer.duration; +``` + +### 2.4 Handling Barge-in / Interruptions +When Gemini detects user speech while generating speech, it emits `message.serverContent.interrupted`: +```typescript +if (message.serverContent?.interrupted) { + // Stop all currently playing audio source nodes immediately + audioSourcesRef.current.forEach(source => { + try { source.stop(); } catch (e) {} + }); + audioSourcesRef.current.clear(); + nextStartTimeRef.current = 0; + setIsAiSpeaking(false); +} +``` + +--- + +## 3. Visualizer Engineering +- Connect `AnalyserNode` with `fftSize = 256` between source and destination. +- Use `analyser.getByteFrequencyData(dataArray)` to sample normalized amplitude (0-255). +- Use `requestAnimationFrame` for continuous rendering with smooth falloff. diff --git a/.env.example b/.env.example index c8de6b5..ffbc76b 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,4 @@ -VITE_GEMINI_API_KEY="" +# Cognitive Clarity - Environment Configuration +# OpenRouter API Key for local development (optional if configured via in-app Settings modal) +VITE_OPENROUTER_API_KEY="" +OPENROUTER_API_KEY="" diff --git a/.gitignore b/.gitignore index 4fee690..a446efb 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,10 @@ local.settings.json *.sw? bin/ obj/ + +# Ephemeral Worktrees & Test Reports +.worktrees/ +test-results/ +playwright-report/ +.agents/teamwork/ + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..08af9ab --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,120 @@ +# AGENTS.md - Cognitive Clarity Workspace Instructions + +Welcome to **Cognitive Clarity** (`clarity-helper`), an adaptive clinical training and roleplay environment engineered to strengthen cognitive flexibility, practice Cognitive Behavioral Therapy (CBT) restructuring techniques, and simulate high-stakes social interactions in real time. + +--- + +## 1. System Architecture Overview + +```mermaid +graph TD + User([User Voice / Text]) <--> Frontend[React 19 + Vite + Tailwind + Radix UI] + + subgraph "Frontend Real-Time Multimodal Layer" + Frontend <--> WebAudio[WebAudio DSP Engine: Live FFT Analysis & Synthesis] + Frontend <--> OpenRouterClient[OpenRouter API: Streaming Roleplay & TTS/STT] + OpenRouterClient --> Transcripts[Real-Time Transcription Pipeline] + Transcripts --> Supervisor[Parallel Turn Supervisor: OpenRouter google/gemini-3.8-flash] + Supervisor --> LiveInsights[LiveInsightPanel & SessionReport] + end + + subgraph "Backend Serverless Layer (api/)" + Frontend -.->|Optional Enterprise Fallback| AzFunc[Azure Functions v4 .NET 8 Isolated Worker] + AzFunc --> ChatFunc["/api/chat (ChatProgression)"] + AzFunc --> AnalysisFunc["/api/analysis (ClinicalAnalysis)"] + AzFunc --> ScenarioFunc["/api/scenario/generate (GenerateScenario)"] + AzFunc <--> OpenRouterAPI[OpenRouter API Endpoint /chat/completions] + end + + subgraph "Local Persistence & Offline" + Frontend <--> LocalStorage[LocalStorage: Custom Scenarios, Settings & Session History] + Frontend <--> ServiceWorker[Vite PWA / Workbox Service Worker: sw.ts] + end +``` + +--- + +## 2. Core Repository Structure + +``` +clarity-helper/ +├── App.tsx # Primary application router, state coordinator, and hero interface +├── constants.ts # CBT Glossary (12 distortions, 5 techniques, 2 concepts), default scenarios, schemas +├── types.ts # TypeScript domain models (Scenario, AnalysisResult, UserSettings, Message) +├── index.html / index.tsx # HTML entry point & React root mount +├── index.css # Global styles & Tailwind base directives +├── vite.config.ts # Vite 6 config with PWA injectManifest, aliases, and env injection +├── sw.ts # Custom Workbox Service Worker with precaching and caching strategies +├── api/ # Azure Functions .NET 8 Isolated Worker backend +│ ├── Program.cs # Functions host bootstrap & Application Insights telemetry +│ ├── api.csproj # .NET 8 project file with HttpClient Factory +│ ├── ChatFunction.cs # POST /api/chat - Headless roleplay progression via OpenRouter +│ ├── AnalysisFunction.cs # POST /api/analysis - Real-time clinical evaluation via OpenRouter +│ └── ScenarioFunction.cs # POST /api/scenario/generate - Structured scenario generator via OpenRouter +├── components/ # UI Component Hierarchy +│ ├── VoiceSession.tsx # Multimodal voice roleplay controller +│ ├── VoiceSession/ # Voice state components (Header, Controls, Visualizer states) +│ ├── ChatSession.tsx # Text-based roleplay controller with streaming SSE +│ ├── ChatMessage.tsx # Conversation bubble with role styling +│ ├── ChatInput.tsx # Text input with submit handling +│ ├── LiveInsightPanel.tsx # Real-time turn-by-turn clinical supervision drawer +│ ├── SessionReport.tsx # Comprehensive post-session CBT assessment report +│ ├── ScenarioGenerator.tsx # AI-powered interactive custom scenario builder +│ ├── CBTReference.tsx # Interactive searchable glossary & technique reference +│ ├── AudioVisualizer.tsx # Canvas/CSS real-time FFT frequency visualizer +│ ├── AudioDeviceSelector.tsx # Input microphone selector modal +│ ├── SettingsModal.tsx # Voice profile & OpenRouter API key configuration +│ ├── ReloadPrompt.tsx # PWA update notification prompt +│ └── ui/ # Radix UI + Tailwind design system primitives +├── hooks/ +│ └── useLiveSession.ts # Low-level WebAudio + OpenRouter voice synthesis hook +├── services/ +│ ├── apiConfig.ts # OpenRouter API key resolution & model defaults +│ ├── openRouterService.ts # Streaming chat, structured supervision & speech API client +│ ├── sessionStorage.ts # Persistent session history, markdown and JSON report exports +│ └── audioUtils.ts # PCM16 / Base64 / AudioBuffer encoding and decoding helpers +├── scripts/ # Headless CLI tools and scenario test suites +│ ├── validate-scenarios.ts # Scenario schema and distortion coverage validator +│ └── simulate-cbt-session.ts # Headless multi-turn roleplay & clinical scoring simulator +├── test/ # Automated Node.js / tsx unit test suite +│ ├── apiConfig.test.ts # API key resolution & model defaults tests +│ ├── openRouterService.test.ts # Streaming, supervisor & scenario generation tests +│ └── scenarios.test.ts # Clinical scenario schema & glossary tests +└── .agents/ # Specialized agent skills and workflow blueprints + └── skills/ # Domain-specific skill runbooks +``` + +--- + +## 3. Engineering Invariants & Coding Guidelines + +### 3.1 Strict Clinical Roleplay Separation +- **No Meta-Commentary Leaks**: When the AI character is engaged in roleplay (`ChatSession`, `VoiceSession`, `ChatFunction.cs`), system instructions MUST forbid the model from breaking character, congratulating the user, giving clinical advice, or outputting feedback tokens. +- **Parallel Supervision**: Clinical feedback is exclusively computed out-of-band by a dedicated supervisor model instance (`LiveInsightPanel`, `AnalysisFunction.cs`), ensuring realistic emotional friction during the simulation. + +### 3.2 OpenRouter Exclusive API Policy +- **Zero Direct Google API Keys**: The codebase must NOT require or use direct Google Gemini API keys. All model queries (`google/gemini-3.8-flash`, `google/gemini-3.7-flash`, `google/gemini-2.5-flash`, `anthropic/claude-3.5-haiku`, `meta-llama/llama-3.3-70b-instruct`) route exclusively through OpenRouter. +- **Client Security**: OpenRouter API Keys provided by the user are stored in browser LocalStorage and never sent to external servers or telemetries. + +### 3.3 TypeScript & Build Hygiene +- Keep `npm run lint` (`tsc --noEmit`) and `npm test` passing with zero errors. +- Ensure all Radix UI component props utilize `React.ComponentPropsWithoutRef` or type unions compatible with React 19. +- Maintain full type synchronization between `types.ts` and backend C# DTOs in `api/*.cs`. + +### 3.4 .NET Azure Functions Backend +- Target **.NET 8.0** isolated worker model with `Microsoft.Azure.Functions.Worker.Sdk`. +- Call OpenRouter API via `HttpClientFactory` with `OPENROUTER_API_KEY`. + +--- + +## 4. Verification & Testing Commands + +| Purpose | Command | Notes | +|:---|:---|:---| +| **Run Unit Tests** | `npm test` | Executes native Node.js / `tsx` unit test suites | +| **Typecheck Frontend** | `npm run lint` | Runs `tsc --noEmit` | +| **Build Frontend** | `npm run build:nobump` | Bundles Vite SPA + PWA Service Worker to `dist/` | +| **Validate Scenarios** | `npm run validate:scenarios` | Validates default & custom scenario data schemas | +| **Simulate Session** | `npm run simulate:session` | Executes headless 3-turn CBT simulation with clinical score | +| **Build .NET API** | `dotnet build api/api.csproj` | Compiles Azure Functions Isolated Worker assembly | +| **Run Dev Server** | `npm run dev` | Starts Vite on `http://localhost:3000` | diff --git a/App.tsx b/App.tsx index 522cd99..36a9e09 100644 --- a/App.tsx +++ b/App.tsx @@ -17,7 +17,9 @@ import { VersionBadge } from './components/VersionBadge'; const DEFAULT_SETTINGS: UserSettings = { voiceName: 'Zephyr', vocalStyle: 'Helpful', - genderPreference: 'Neutral' + genderPreference: 'Neutral', + roleplayModel: 'google/gemini-3.8-flash', + supervisorModel: 'google/gemini-3.8-flash' }; const CUSTOM_SCENARIOS_KEY = 'cognitive-clarity-custom-scenarios'; @@ -143,7 +145,7 @@ const App: React.FC = () => { return (
{sessionMode === 'text' ? ( - + ) : (
{
{/* Modals */} - {showGenerator && setShowGenerator(false)} />} + {showGenerator && setShowGenerator(false)} />} {showReference && setShowReference(false)} />} {showSettings && setShowSettings(false)} />} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..498f6d6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,91 @@ +# CHANGELOG + +### [2026-08-31] - OpenRouter Exclusive Engine Migration & Unified API Routing +**Type:** Migration | Architecture | Feature | Performance +**Context:** Completely eliminated direct Google API key dependencies in favor of exclusive OpenRouter integration (`https://openrouter.ai/api/v1`) across frontend roleplay streaming, speech synthesis/transcription, clinical supervision, scenario generation, and backend Azure Functions. +**Resolution:** +- **OpenRouter Exclusive Architecture:** Removed all `@google/genai` client libraries and direct Google Gemini API key requirements. All model interactions (including `google/gemini-2.5-flash`, `google/gemini-2.0-flash-001`, `anthropic/claude-3.5-haiku`, `meta-llama/llama-3.3-70b-instruct`) route exclusively through OpenRouter with standard Bearer authorization. +- **Frontend Streaming SSE & TTS:** Upgraded `services/openRouterService.ts` to support Server-Sent Events (SSE) token streaming, structured turn evaluation in JSON mode, and high-fidelity speech synthesis. +- **Live Voice Processing:** Refactored `hooks/useLiveSession.ts` to use browser speech recognition + WebAudio frequency analysis for microphone input, coupled with OpenRouter speech synthesis and turn analysis. +- **Backend Azure Functions Alignment:** Refactored `api/ChatFunction.cs`, `api/AnalysisFunction.cs`, and `api/ScenarioFunction.cs` to utilize `HttpClientFactory` and `OPENROUTER_API_KEY`, removing the `Google.GenAI` NuGet package. +- **Settings & UI Redesign:** Replaced provider toggles in `components/SettingsModal.tsx` with dedicated OpenRouter API Key input and model selector. +- **Bundle Shrinkage:** Reduced total precache build size from 703 kB to 439 kB by removing heavy GenAI SDK dependencies. + +**Files Changed:** +- `services/openRouterService.ts` +- `services/apiConfig.ts` +- `hooks/useLiveSession.ts` +- `components/ChatSession.tsx` +- `components/VoiceSession.tsx` +- `components/ScenarioGenerator.tsx` +- `components/SettingsModal.tsx` +- `constants.ts` +- `types.ts` +- `App.tsx` +- `index.html` +- `vite.config.ts` +- `api/api.csproj` +- `api/Program.cs` +- `api/ChatFunction.cs` +- `api/AnalysisFunction.cs` +- `api/ScenarioFunction.cs` +- `AGENTS.md` +- `GEMINI.md` +- `README.md` +- `CHANGELOG.md` + +**Files Changed:** +- `hooks/useLiveSession.ts` +- `services/apiConfig.ts` +- `services/openRouterService.ts` +- `services/sessionStorage.ts` +- `components/SessionReport.tsx` +- `components/SettingsModal.tsx` +- `components/ChatSession.tsx` +- `components/VoiceSession.tsx` +- `components/ScenarioGenerator.tsx` +- `types.ts` +- `App.tsx` +- `vite.config.ts` +- `sw.ts` +- `CHANGELOG.md` + +### [2026-08-30] - Architectural Assessment, Type Alignment & Agentic Tooling +**Type:** Refactor | Bug Fix | Feature | Config +**Context:** Comprehensive full-codebase architectural evaluation of Cognitive Clarity (`clarity-helper`), fixing TypeScript / .NET compilation blockers, enriching CBT domain coverage, and establishing workspace agent instructions, custom skills, and scenario test helpers. +**Resolution:** +- Fixed TypeScript 5.8 / React 19 typing discrepancy on `BadgeProps` in `components/ui/badge.tsx`. +- Upgraded C# Azure Functions in `api/` to `Google.GenAI` 1.20+ SDK, resolving all namespace ambiguities and schema definitions across `ChatFunction.cs`, `AnalysisFunction.cs`, and `ScenarioFunction.cs`. +- Enriched `CBT_GLOSSARY` in `constants.ts` with missing definitions for `Labeling` and `Overgeneralization`. +- Added explicit `INTERNAL CBT LOGIC:` branching rules across built-in scenarios `s1`, `s3`, and `s4`. +- Created comprehensive workspace guidelines and rules in `AGENTS.md` and `GEMINI.md`. +- Created custom agent skills in `.agents/skills/`: + - `cbt-scenario-design` + - `webaudio-gemini-live` + - `cbt-clinical-evaluation` + - `azure-functions-genai` +- Authored developer CLI helpers in `scripts/`: + - `scripts/validate-scenarios.ts` (Validates 14 built-in scenarios against clinical schema) + - `scripts/simulate-cbt-session.ts` (Headless multi-turn roleplay & clinical scoring simulator) +- Updated `package.json` with `validate:scenarios` and `simulate:session` npm script targets and installed `tsx`. +- Elevated `README.md` to publication-grade documentation with complete architecture diagrams, endpoint matrices, and developer guide. + +**Files Changed:** +- `components/ui/badge.tsx` +- `constants.ts` +- `package.json` +- `package-lock.json` +- `api/api.csproj` +- `api/ChatFunction.cs` +- `api/AnalysisFunction.cs` +- `api/ScenarioFunction.cs` +- `scripts/validate-scenarios.ts` +- `scripts/simulate-cbt-session.ts` +- `.agents/skills/cbt-scenario-design/SKILL.md` +- `.agents/skills/webaudio-gemini-live/SKILL.md` +- `.agents/skills/cbt-clinical-evaluation/SKILL.md` +- `.agents/skills/azure-functions-genai/SKILL.md` +- `AGENTS.md` +- `GEMINI.md` +- `README.md` +- `CHANGELOG.md` diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..fd3b058 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,16 @@ +# GEMINI.md - Cognitive Clarity Architectural Rules + +This repository defines the **Cognitive Clarity** adaptive clinical simulation system. + +--- + +## Autonomous Agent Invariants + +1. **OpenRouter Exclusive Execution**: All AI operations (text dialogue streaming, clinical turn analysis, scenario generation, and vocal speech synthesis) MUST route exclusively through OpenRouter API (`https://openrouter.ai/api/v1`). Direct Google API keys or endpoints are strictly forbidden. +2. **Strict Character Boundaries**: AI roleplay characters must NEVER provide clinical meta-commentary, congratulations, or diagnostic advice within dialogue turns. Feedback is handled asynchronously by the supervisor engine. +3. **Audio Stream Integrity**: WebAudio processing must properly manage WebAudio Context lifecycles, avoid main-thread blocking, and drive FFT visualizer states accurately. +4. **C# Isolated Functions Parity**: Ensure `api/api.csproj` builds cleanly with .NET 8, maintaining type fidelity with TypeScript schemas in `types.ts` and consuming OpenRouter via `HttpClientFactory`. +5. **Zero Residual Slop**: Temporary scripts or exploratory files must be placed in `scripts/` or scratch directories and never committed in a broken state. +6. **Continuous Changelog**: Every feature, bugfix, or architectural update must be recorded in `CHANGELOG.md`. + +Refer to [AGENTS.md](file:///Users/moo/Development/Repositories/clarity-helper/AGENTS.md) for full workspace specifications. diff --git a/README.md b/README.md index f08bdf9..e34195f 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,152 @@
-GHBanner + Cognitive Clarity Banner +

🧠 Cognitive Clarity

+

An Adaptive Clinical Training & Multimodal Roleplay Environment for Cognitive Behavioral Therapy (CBT)

+

+ Key Features • + Architecture • + Getting Started • + CBT Scenarios • + Serverless Backend • + Developer Tools • + Agents & Skills +

-# Run and deploy your AI Studio app +--- -This contains everything you need to run your app locally. +## 🌟 Key Features -View your app in AI Studio: https://ai.studio/apps/56e6d01a-b890-4a16-b7c2-fc7b3e52f59a +- **🎙️ Real-Time Voice & Text Roleplay**: Natural interactive voice roleplay and streaming text chat powered exclusively by OpenRouter (`google/gemini-3.8-flash`, `google/gemini-3.7-flash`, `anthropic/claude-3.5-haiku`, `meta-llama/llama-3.3-70b-instruct`) with WebAudio speech synthesis and FFT frequency visualization. +- **👁️ Out-of-Band Clinical Supervisor**: Real-time turn-by-turn clinical supervision evaluating cognitive distortions (Mind Reading, Catastrophizing, Should Statements, etc.) and effective restructuring techniques without breaking character during roleplay. +- **📊 Comprehensive Session Diagnostic Reports**: End-of-session case conceptualization reports with psychometric scoring (1-5), evidence quotes, cognitive restructuring trajectories, and one-click Markdown & JSON exports. +- **🛠️ AI Custom Scenario Architect**: Dynamic clinical scenario generator creating calibrated roleplays across `Social Anxiety`, `Workplace`, `Relationships`, and `Self-Talk` at `Beginner`, `Intermediate`, and `Advanced` difficulty levels. +- **📖 Evidence-Based CBT Reference Guide**: Searchable clinical glossary covering 12 cognitive distortions, 5 therapeutic techniques (Cognitive Reframing, Behavioral Activation, Radical Acceptance, etc.), and concepts (Spotlight Effect, Gray Rock Method). +- **⚡ Progressive Web App (PWA)**: Built with `vite-plugin-pwa` and Workbox custom service worker (`sw.ts`) for offline caching, asset precaching, and mobile standalone installation. +- **☁️ Serverless C# API Layer**: Optional headless backend implemented in Azure Functions v4 (.NET 8 Isolated Worker) powered by OpenRouter. -## Run Locally +--- -**Prerequisites:** Node.js +## 🏛️ System Architecture +```mermaid +graph TD + User([User Voice / Text]) <--> Frontend[React 19 + Vite + Tailwind + Radix UI] + + subgraph "Frontend Real-Time Multimodal Layer" + Frontend <--> WebAudio[WebAudio DSP Engine: Live FFT Analysis & Speech Synthesis] + Frontend <--> OpenRouterClient[OpenRouter API: Streaming Roleplay & Structured Analysis] + OpenRouterClient --> Transcripts[Real-Time Transcription Pipeline] + Transcripts --> Supervisor[Parallel Turn Supervisor: OpenRouter google/gemini-3.8-flash] + Supervisor --> LiveInsights[LiveInsightPanel & SessionReport] + end -1. Install dependencies: - `npm install` -2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key -3. Run the app: - `npm run dev` + subgraph "Backend Serverless Layer (api/)" + Frontend -.->|Headless / Batch Evaluation| AzFunc[Azure Functions v4 .NET 8 Isolated Worker] + AzFunc --> ChatFunc["/api/chat (ChatProgression)"] + AzFunc --> AnalysisFunc["/api/analysis (ClinicalAnalysis)"] + AzFunc --> ScenarioFunc["/api/scenario/generate (GenerateScenario)"] + AzFunc <--> OpenRouterAPI[OpenRouter API /chat/completions] + end + + subgraph "Local Persistence & Offline" + Frontend <--> LocalStorage[LocalStorage: Custom Scenarios, Settings & Session History] + Frontend <--> ServiceWorker[Vite PWA / Workbox Service Worker: sw.ts] + end +``` + +--- + +## 🚀 Getting Started + +### Prerequisites + +- **Node.js**: v20+ or v22+ +- **npm**: v10+ +- **.NET 8.0 SDK** (optional, for backend Azure Functions) +- **OpenRouter API Key**: [Get an API Key from OpenRouter](https://openrouter.ai/keys) + +### 1. Installation + +```bash +git clone https://github.com/jpremill/clarity-helper.git +cd clarity-helper +npm install +``` + +### 2. Configure Environment Variables + +Create `.env.local` in the project root (or set your key directly in the in-app Settings modal): + +```env +VITE_OPENROUTER_API_KEY=your_openrouter_api_key_here +OPENROUTER_API_KEY=your_openrouter_api_key_here +``` + +### 3. Run Development Server + +```bash +npm run dev +``` +Open [http://localhost:3000](http://localhost:3000) in your browser. + +--- + +## 🧠 CBT Framework & Scenarios + +Cognitive Clarity includes 14 pre-calibrated clinical scenarios across 4 core domains: + +| Category | Beginner | Intermediate | Advanced | +|:---|:---|:---|:---| +| **Relationships** | *The Silent Friend* (s1) | *The Friend's Criticism* (s13) | *Intrusive Relative* (s11), *The Unpaid Debt* (s7) | +| **Workplace** | — | *Critical Feedback* (s2), *The Deadline Slip* (s9) | *The Salary Discussion* (s8), *The Job Interview* (s12) | +| **Social Anxiety** | *Booking an Appointment* (s5) | *The Defective Return* (s6), *The Social 'No'* (s10) | *The Social Spotlight* (s3) | +| **Self-Talk** | *Inner Critic Echo* (s4) | — | *The Unexpected Health Symptom* (s14) | + +--- + +## ☁️ Serverless Backend API (`api/`) + +The repository includes a .NET 8 Azure Functions isolated worker backend: + +```bash +# Build the .NET Functions project +dotnet build api/api.csproj +``` + +### Endpoints + +| Route | Method | Description | +|:---|:---|:---| +| `/api/chat` | `POST` | Advances the character roleplay dialogue with strict anti-meta guardrails. | +| `/api/analysis` | `POST` | Analyzes a dialogue turn for cognitive distortions and returns structured feedback. | +| `/api/scenario/generate` | `POST` | Generates a complete, validated scenario specification using JSON schema constraints. | + +--- + +## 🛠️ Developer Tools & Scripts + +| Script | Command | Purpose | +|:---|:---|:---| +| **Run Unit Tests** | `npm test` | Executes native Node.js / `tsx` unit test suites. | +| **Typecheck** | `npm run lint` | Runs `tsc --noEmit` to verify type safety. | +| **Build SPA & PWA** | `npm run build:nobump` | Compiles Vite production bundle + Service Worker into `dist/`. | +| **Validate Scenarios** | `npm run validate:scenarios` | Validates all 14 built-in scenarios against schema and distortion rules. | +| **Simulate Session** | `npm run simulate:session` | Headless multi-turn roleplay & clinical scoring simulator harness. | + +--- + +## 🤖 Agents & Skills + +Specialized agent configurations and workflow blueprints are organized in `.agents/skills/`: + +- [`.agents/skills/cbt-scenario-design/SKILL.md`](.agents/skills/cbt-scenario-design/SKILL.md): Scenario creation, difficulty calibration, and psychological persona guidelines. +- [`.agents/skills/webaudio-gemini-live/SKILL.md`](.agents/skills/webaudio-gemini-live/SKILL.md): Real-time duplex WebAudio streaming, PCM conversions, and visualizer math. +- [`.agents/skills/cbt-clinical-evaluation/SKILL.md`](.agents/skills/cbt-clinical-evaluation/SKILL.md): Clinical supervisor scoring, distortion taxonomy, and diagnostic report structures. +- [`.agents/skills/azure-functions-genai/SKILL.md`](.agents/skills/azure-functions-genai/SKILL.md): C# isolated worker API design with `Google.GenAI` 1.20+ SDK. + +--- + +## 📄 License + +MIT License. Designed with evidence-based Cognitive Behavioral Therapy principles. diff --git a/api/AnalysisFunction.cs b/api/AnalysisFunction.cs index 64ea565..224beb4 100644 --- a/api/AnalysisFunction.cs +++ b/api/AnalysisFunction.cs @@ -1,10 +1,11 @@ using System.Net; +using System.Net.Http.Headers; +using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; -using Google.GenAI; -using System.Text.Json.Serialization; namespace CognitiveClarity.api { @@ -21,22 +22,27 @@ namespace CognitiveClarity.api [JsonPropertyName("cbtFocus")] public List CBTFocus { get; set; } = new(); + + [JsonPropertyName("model")] + public string Model { get; set; } = "google/gemini-3.8-flash"; } public class AnalysisFunction { private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; - public AnalysisFunction(ILoggerFactory loggerFactory) + public AnalysisFunction(ILoggerFactory loggerFactory, IHttpClientFactory httpClientFactory) { _logger = loggerFactory.CreateLogger(); + _httpClientFactory = httpClientFactory; } [Function("ClinicalAnalysis")] public async Task Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "analysis")] HttpRequestData req) { - _logger.LogInformation("C# HTTP trigger function processed a clinical analysis request."); + _logger.LogInformation("C# HTTP trigger function processed a clinical analysis request via OpenRouter."); var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); var data = JsonSerializer.Deserialize(requestBody); @@ -48,67 +54,84 @@ namespace CognitiveClarity.api return badResponse; } - var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY"); + var apiKey = System.Environment.GetEnvironmentVariable("OPENROUTER_API_KEY"); if (string.IsNullOrEmpty(apiKey)) { - _logger.LogError("GEMINI_API_KEY is missing from environment variables."); + _logger.LogError("OPENROUTER_API_KEY is missing from environment variables."); return req.CreateResponse(HttpStatusCode.InternalServerError); } - // Initialize the Gemini Client - var ai = new GeminiClient(apiKey: apiKey); + var systemAnalysisPrompt = $@"You are a master clinical psychologist specializing in Cognitive Behavioral Therapy (CBT). +You are supervising an interactive CBT simulation. +SCENARIO: {data.ScenarioTitle} - {data.ScenarioDescription} +FOCUS AREAS: {string.Join(", ", data.CBTFocus)} - var systemAnalysisPrompt = @" -You are a master clinical psychologist specializing in Cognitive Behavioral Therapy (CBT). -You are observing a roleplay session between a user and an AI character. -Analyze the user's latest response for: -1. Cognitive distortions present (if any). -2. CBT techniques successfully applied by the user (if any). -3. A brief, encouraging clinical insight or redirection (max 2 sentences). -Be precise, clinical, and constructive. -"; +Return ONLY a valid JSON object matching this schema: +{{ + ""analysis"": {{ + ""distortions"": [""string""], + ""isAdaptive"": boolean, + ""evidence"": ""string"" + }}, + ""rating"": {{ + ""score"": number (1-5), + ""reason"": ""string"" + }}, + ""suggestions"": {{ + ""alternativeThought"": ""string"", + ""behavioralStep"": ""string"" + }} +}}"; - var analysisSystemInstruction = $@"{systemAnalysisPrompt} -SCENARIO CONTEXT: -Title: {data.ScenarioTitle} -Description: {data.ScenarioDescription} -CBT Focus Areas: {string.Join(", ", data.CBTFocus)}"; - - var schema = new Schema + var payload = new { - Type = TypeEnum.Object, - Properties = new Dictionary + model = string.IsNullOrEmpty(data.Model) ? "google/gemini-3.8-flash" : data.Model, + messages = new object[] { - { "distortionsIdentified", new Schema { Type = TypeEnum.Array, Items = new Schema { Type = TypeEnum.String } } }, - { "techniquesUsed", new Schema { Type = TypeEnum.Array, Items = new Schema { Type = TypeEnum.String } } }, - { "clinicalInsight", new Schema { Type = TypeEnum.String } }, - { "feedbackType", new Schema { Type = TypeEnum.String, Enum = new List { "Constructive", "Positive", "Neutral" } } } + new { role = "system", content = systemAnalysisPrompt }, + new { role = "user", content = data.TurnContext } }, - Required = new List { "distortionsIdentified", "techniquesUsed", "clinicalInsight", "feedbackType" } + response_format = new { type = "json_object" }, + temperature = 0.2 }; - try + try { - var response = await ai.Models.GenerateContentAsync( - model: "gemini-3.0-flash", - contents: data.TurnContext, - new GenerateContentConfig - { - SystemInstruction = new Content { Parts = new List { new Part { Text = analysisSystemInstruction } } }, - ResponseMimeType = "application/json", - ResponseSchema = schema - }); + var client = _httpClientFactory.CreateClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + client.DefaultRequestHeaders.Add("HTTP-Referer", "https://cognitive-clarity.app"); + client.DefaultRequestHeaders.Add("X-Title", "Cognitive Clarity"); + + var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); + var response = await client.PostAsync("https://openrouter.ai/api/v1/chat/completions", content); + + if (!response.IsSuccessStatusCode) + { + var errText = await response.Content.ReadAsStringAsync(); + _logger.LogError("OpenRouter returned error {Status}: {Error}", response.StatusCode, errText); + var errResponse = req.CreateResponse(response.StatusCode); + await errResponse.WriteStringAsync(errText); + return errResponse; + } + + var resJson = await response.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(resJson); + var rawResult = doc.RootElement + .GetProperty("choices")[0] + .GetProperty("message") + .GetProperty("content") + .GetString() ?? "{}"; var successResponse = req.CreateResponse(HttpStatusCode.OK); successResponse.Headers.Add("Content-Type", "application/json"); - await successResponse.WriteStringAsync(response.Text!); + await successResponse.WriteStringAsync(rawResult); return successResponse; } catch (Exception ex) { - _logger.LogError(ex, "Failed to generate analysis via Gemini API."); + _logger.LogError(ex, "Failed to generate analysis via OpenRouter API."); var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError); - await errorResponse.WriteStringAsync("Error communicating with Gemini API."); + await errorResponse.WriteStringAsync("Error communicating with OpenRouter API."); return errorResponse; } } diff --git a/api/ChatFunction.cs b/api/ChatFunction.cs index ef4ec91..957110c 100644 --- a/api/ChatFunction.cs +++ b/api/ChatFunction.cs @@ -1,17 +1,18 @@ using System.Net; +using System.Net.Http.Headers; +using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; -using Google.GenAI; -using System.Text.Json.Serialization; namespace CognitiveClarity.api { public class ChatMessage { [JsonPropertyName("role")] - public string Role { get; set; } = string.Empty; // "user" or "model" + public string Role { get; set; } = string.Empty; // "user" or "model" or "assistant" [JsonPropertyName("text")] public string Text { get; set; } = string.Empty; @@ -24,22 +25,27 @@ namespace CognitiveClarity.api [JsonPropertyName("systemInstruction")] public string SystemInstruction { get; set; } = string.Empty; + + [JsonPropertyName("model")] + public string Model { get; set; } = "google/gemini-3.8-flash"; } public class ChatFunction { private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; - public ChatFunction(ILoggerFactory loggerFactory) + public ChatFunction(ILoggerFactory loggerFactory, IHttpClientFactory httpClientFactory) { _logger = loggerFactory.CreateLogger(); + _httpClientFactory = httpClientFactory; } [Function("ChatProgression")] public async Task Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "chat")] HttpRequestData req) { - _logger.LogInformation("C# HTTP trigger function processed a chat progression request."); + _logger.LogInformation("C# HTTP trigger function processed a chat progression request via OpenRouter."); var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); var data = JsonSerializer.Deserialize(requestBody); @@ -51,72 +57,92 @@ namespace CognitiveClarity.api return badResponse; } - var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY"); + var apiKey = System.Environment.GetEnvironmentVariable("OPENROUTER_API_KEY"); if (string.IsNullOrEmpty(apiKey)) { - _logger.LogError("GEMINI_API_KEY is missing from environment variables."); + _logger.LogError("OPENROUTER_API_KEY is missing from environment variables."); return req.CreateResponse(HttpStatusCode.InternalServerError); } - // Initialize the Gemini Client - var ai = new GeminiClient(apiKey: apiKey); - - // Construct contents from history - var contents = new List(); - foreach (var msg in data.History) - { - contents.Add(new Content - { - Role = msg.Role, - Parts = new List { new Part { Text = msg.Text } } - }); - } - var roleplaySystemInstruction = $@"{data.SystemInstruction} - [CONTEXT] - You are participating in a clinical CBT roleplay simulation. - The instructions above define your character, the scenario, and your internal CBT logic. - - [TASK] - Respond to the user naturally in character to advance the roleplay scenario. - - [CONSTRAINTS] - - CRITICAL: Stay strictly in character as the persona defined in the scenario. - - CRITICAL: NEVER provide clinical feedback, advice, or meta-commentary about the user's progress. - - CRITICAL: NEVER use phrases like ""Great job holding your boundary"" or ""Feedback:"". - - CRITICAL: Do not congratulate the user on using CBT techniques; simply react as the character would. - - If the user successfully concludes the scenario, end the dialogue naturally. - - [OUTPUT REQUIREMENTS] - Return the character's direct dialogue response. Keep it conversational and proportional to the user's input. - "; +[CONTEXT] +You are participating in a clinical CBT roleplay simulation. +The instructions above define your character, the scenario, and your internal CBT logic. - try +[TASK] +Respond to the user naturally in character to advance the roleplay scenario. + +[CONSTRAINTS] +- CRITICAL: Stay strictly in character as the persona defined in the scenario. +- CRITICAL: NEVER provide clinical feedback, advice, or meta-commentary about the user's progress. +- CRITICAL: NEVER use phrases like ""Great job holding your boundary"" or ""Feedback:"". +- CRITICAL: Do not congratulate the user on using CBT techniques; simply react as the character would. +- If the user successfully concludes the scenario, end the dialogue naturally. + +[OUTPUT REQUIREMENTS] +Return the character's direct dialogue response. Keep it conversational and proportional to the user's input."; + + var messages = new List { - var response = await ai.Models.GenerateContentAsync( - model: "gemini-3.0-flash", - contents: contents, - new GenerateContentConfig - { - SystemInstruction = new Content { Parts = new List { new Part { Text = roleplaySystemInstruction } } }, - // Streaming isn't natively supported in standard HTTP Functions easily without Server-Sent Events (SSE). - // We will return a standard block response for now, which simplifies the architecture. - }); + new { role = "system", content = roleplaySystemInstruction } + }; + + foreach (var msg in data.History) + { + messages.Add(new + { + role = msg.Role == "model" ? "assistant" : msg.Role, + content = msg.Text + }); + } + + var payload = new + { + model = string.IsNullOrEmpty(data.Model) ? "google/gemini-3.8-flash" : data.Model, + messages = messages, + temperature = 0.7, + max_tokens = 1000 + }; + + try + { + var client = _httpClientFactory.CreateClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + client.DefaultRequestHeaders.Add("HTTP-Referer", "https://cognitive-clarity.app"); + client.DefaultRequestHeaders.Add("X-Title", "Cognitive Clarity"); + + var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); + var response = await client.PostAsync("https://openrouter.ai/api/v1/chat/completions", content); + + if (!response.IsSuccessStatusCode) + { + var errText = await response.Content.ReadAsStringAsync(); + _logger.LogError("OpenRouter returned error {Status}: {Error}", response.StatusCode, errText); + var errResponse = req.CreateResponse(response.StatusCode); + await errResponse.WriteStringAsync(errText); + return errResponse; + } + + var resJson = await response.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(resJson); + var botText = doc.RootElement + .GetProperty("choices")[0] + .GetProperty("message") + .GetProperty("content") + .GetString() ?? string.Empty; var successResponse = req.CreateResponse(HttpStatusCode.OK); successResponse.Headers.Add("Content-Type", "application/json"); - - // Return just the text in a JSON wrapper - var resultObj = new { text = response.Text }; + var resultObj = new { text = botText }; await successResponse.WriteStringAsync(JsonSerializer.Serialize(resultObj)); return successResponse; } catch (Exception ex) { - _logger.LogError(ex, "Failed to generate chat progression via Gemini API."); + _logger.LogError(ex, "Failed to generate chat progression via OpenRouter API."); var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError); - await errorResponse.WriteStringAsync("Error communicating with Gemini API."); + await errorResponse.WriteStringAsync("Error communicating with OpenRouter API."); return errorResponse; } } diff --git a/api/Program.cs b/api/Program.cs index 704475c..b5b4e06 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -8,6 +8,7 @@ var builder = FunctionsApplication.CreateBuilder(args); builder.ConfigureFunctionsWebApplication(); builder.Services + .AddHttpClient() .AddApplicationInsightsTelemetryWorkerService() .ConfigureFunctionsApplicationInsights(); diff --git a/api/ScenarioFunction.cs b/api/ScenarioFunction.cs index 5dabd43..d4e02bc 100644 --- a/api/ScenarioFunction.cs +++ b/api/ScenarioFunction.cs @@ -1,10 +1,11 @@ using System.Net; +using System.Net.Http.Headers; +using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; -using Google.GenAI; -using System.Text.Json.Serialization; namespace CognitiveClarity.api { @@ -18,22 +19,27 @@ namespace CognitiveClarity.api [JsonPropertyName("difficulty")] public string Difficulty { get; set; } = "Intermediate"; + + [JsonPropertyName("model")] + public string Model { get; set; } = "google/gemini-3.8-flash"; } public class ScenarioFunction { private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; - public ScenarioFunction(ILoggerFactory loggerFactory) + public ScenarioFunction(ILoggerFactory loggerFactory, IHttpClientFactory httpClientFactory) { _logger = loggerFactory.CreateLogger(); + _httpClientFactory = httpClientFactory; } [Function("GenerateScenario")] public async Task Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "scenario/generate")] HttpRequestData req) { - _logger.LogInformation("C# HTTP trigger function processed a scenario generation request."); + _logger.LogInformation("C# HTTP trigger function processed a scenario generation request via OpenRouter."); var requestBody = await new StreamReader(req.Body).ReadToEndAsync(); var data = JsonSerializer.Deserialize(requestBody); @@ -45,84 +51,100 @@ namespace CognitiveClarity.api return badResponse; } - var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY"); + var apiKey = System.Environment.GetEnvironmentVariable("OPENROUTER_API_KEY"); if (string.IsNullOrEmpty(apiKey)) { - _logger.LogError("GEMINI_API_KEY is missing from environment variables."); + _logger.LogError("OPENROUTER_API_KEY is missing from environment variables."); return req.CreateResponse(HttpStatusCode.InternalServerError); } - // Initialize the Gemini Client - var ai = new GeminiClient(apiKey: apiKey); - var selectedContext = data.SelectedConcepts.Count > 0 ? $"Focus specifically on these CBT concepts: {string.Join(", ", data.SelectedConcepts)}." : ""; var systemInstruction = $@" - [CONTEXT] - You are a Clinical CBT Scenario Architect. - The user requires a structured clinical simulation designed to address specific cognitive distortions and behavioral challenges. - Clinical Focus: {selectedContext} - - [TASK] - Transform the user's prompt into a cohesive, structured clinical scenario that sets up a targeted roleplay environment. - - [CONSTRAINTS] - - CATEGORIZATION: You MUST categorize the scenario appropriately (Social Anxiety, Workplace, Relationships, or Self-Talk). - - DIFFICULTY CALIBRATION: The requested difficulty is '{data.Difficulty}'. - - 'Beginner': Low stakes, high support, clear-cut distortions, AI character is cooperative. - - 'Intermediate': Ambiguous stakes, moderate social friction, subtle distortions, AI character has specific needs. - - 'Advanced': High emotional triggers, persistent clinical resistance, complex cognitive patterns. - - INTERNAL LOGIC: Provide a detailed ""INTERNAL CBT LOGIC"" section instructing the AI character how to react to the user's progress or cognitive lapses during the future roleplay. - - CHARACTER VOICE: Define a realistic, emotionally consistent voice profile. - - [OUTPUT REQUIREMENTS] - Return ONLY a valid JSON object conforming exactly to the requested schema. Ensure all arrays and nested objects are populated correctly. - "; +[CONTEXT] +You are a Clinical CBT Scenario Architect. +The user requires a structured clinical simulation designed to address specific cognitive distortions and behavioral challenges. +Clinical Focus: {selectedContext} - var schema = new Schema - { - Type = TypeEnum.Object, - Properties = new Dictionary - { - { "id", new Schema { Type = TypeEnum.String } }, - { "title", new Schema { Type = TypeEnum.String } }, - { "characterName", new Schema { Type = TypeEnum.String } }, - { "description", new Schema { Type = TypeEnum.String } }, - { "category", new Schema { Type = TypeEnum.String, Enum = new List { "Social Anxiety", "Workplace", "Relationships", "Self-Talk" } } }, - { "difficulty", new Schema { Type = TypeEnum.String, Enum = new List { "Beginner", "Intermediate", "Advanced" } } }, - { "systemInstruction", new Schema { Type = TypeEnum.String } }, - { "initialMessage", new Schema { Type = TypeEnum.String } }, - { "cbtFocus", new Schema { Type = TypeEnum.Array, Items = new Schema { Type = TypeEnum.String } } } - }, - Required = new List { "id", "title", "characterName", "description", "category", "difficulty", "systemInstruction", "initialMessage", "cbtFocus" } - }; +[TASK] +Transform the user's prompt into a cohesive, structured clinical scenario that sets up a targeted roleplay environment. + +[CONSTRAINTS] +- CATEGORIZATION: You MUST categorize the scenario appropriately (Social Anxiety, Workplace, Relationships, or Self-Talk). +- DIFFICULTY CALIBRATION: The requested difficulty is '{data.Difficulty}'. + - 'Beginner': Low stakes, high support, clear-cut distortions, AI character is cooperative. + - 'Intermediate': Ambiguous stakes, moderate social friction, subtle distortions, AI character has specific needs. + - 'Advanced': High emotional triggers, persistent clinical resistance, complex cognitive patterns. +- INTERNAL LOGIC: Provide a detailed ""INTERNAL CBT LOGIC"" section instructing the AI character how to react to the user's progress or cognitive lapses during the future roleplay. +- CHARACTER VOICE: Define a realistic, emotionally consistent voice profile. + +[OUTPUT REQUIREMENTS] +Return ONLY a valid JSON object matching this schema: +{{ + ""id"": ""string"", + ""title"": ""string"", + ""characterName"": ""string"", + ""description"": ""string"", + ""category"": ""Social Anxiety"" | ""Workplace"" | ""Relationships"" | ""Self-Talk"", + ""difficulty"": ""Beginner"" | ""Intermediate"" | ""Advanced"", + ""systemInstruction"": ""string"", + ""initialMessage"": ""string"", + ""cbtFocus"": [""string""] +}}"; var promptText = $"Generate a clinical scenario for: {(string.IsNullOrWhiteSpace(data.Prompt) ? "A scenario exploring the chosen CBT concepts." : data.Prompt)}"; - try + var payload = new { - var response = await ai.Models.GenerateContentAsync( - model: "gemini-3.0-flash", - contents: promptText, - new GenerateContentConfig - { - SystemInstruction = new Content { Parts = new List { new Part { Text = systemInstruction } } }, - ResponseMimeType = "application/json", - ResponseSchema = schema - }); + model = string.IsNullOrEmpty(data.Model) ? "google/gemini-3.8-flash" : data.Model, + messages = new object[] + { + new { role = "system", content = systemInstruction }, + new { role = "user", content = promptText } + }, + response_format = new { type = "json_object" }, + temperature = 0.7 + }; + + try + { + var client = _httpClientFactory.CreateClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); + client.DefaultRequestHeaders.Add("HTTP-Referer", "https://cognitive-clarity.app"); + client.DefaultRequestHeaders.Add("X-Title", "Cognitive Clarity"); + + var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); + var response = await client.PostAsync("https://openrouter.ai/api/v1/chat/completions", content); + + if (!response.IsSuccessStatusCode) + { + var errText = await response.Content.ReadAsStringAsync(); + _logger.LogError("OpenRouter returned error {Status}: {Error}", response.StatusCode, errText); + var errResponse = req.CreateResponse(response.StatusCode); + await errResponse.WriteStringAsync(errText); + return errResponse; + } + + var resJson = await response.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(resJson); + var scenarioJson = doc.RootElement + .GetProperty("choices")[0] + .GetProperty("message") + .GetProperty("content") + .GetString() ?? "{}"; var successResponse = req.CreateResponse(HttpStatusCode.OK); successResponse.Headers.Add("Content-Type", "application/json"); - await successResponse.WriteStringAsync(response.Text!); + await successResponse.WriteStringAsync(scenarioJson); return successResponse; } catch (Exception ex) { - _logger.LogError(ex, "Failed to generate scenario via Gemini API."); + _logger.LogError(ex, "Failed to generate scenario via OpenRouter API."); var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError); - await errorResponse.WriteStringAsync("Error communicating with Gemini API."); + await errorResponse.WriteStringAsync("Error communicating with OpenRouter API."); return errorResponse; } } diff --git a/api/api.csproj b/api/api.csproj index 9022e8b..d0b19ff 100644 --- a/api/api.csproj +++ b/api/api.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -10,7 +10,6 @@ - diff --git a/api/local.settings.example.json b/api/local.settings.example.json new file mode 100644 index 0000000..ebd5994 --- /dev/null +++ b/api/local.settings.example.json @@ -0,0 +1,8 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", + "OPENROUTER_API_KEY": "your_openrouter_api_key_here" + } +} diff --git a/changelogs/unreleased/gemini-3-8-openrouter-upgrade.md b/changelogs/unreleased/gemini-3-8-openrouter-upgrade.md new file mode 100644 index 0000000..61e48ff --- /dev/null +++ b/changelogs/unreleased/gemini-3-8-openrouter-upgrade.md @@ -0,0 +1,21 @@ +--- +title: "Upgrade to Google Gemini 3.8 Flash & OpenRouter API Key Governance" +ticket: "GEMINI-3.8-UPGRADE" +type: "feature" +author: "Antigravity Autonomous Swarm" +date: "2026-09-29" +--- + +### Added +- **Gemini 3.8 Model Catalog Support**: Upgraded default roleplay and clinical supervisor models across the entire application to `google/gemini-3.8-flash` via OpenRouter. +- **Enhanced Model Selection**: Added `google/gemini-3.8-flash` (Frontier Ultra Low Latency) and `google/gemini-3.7-flash` (Hybrid Reasoning) to `AVAILABLE_MODELS` in `SettingsModal.tsx`. +- **TDD Test Suite**: Added zero-dependency automated unit test suite (`npm test`) using `node:test` + `tsx`, covering API key resolution, OpenRouter mock requests/SSE streaming, and scenario schemas. +- **Azure Functions Local Template**: Added `api/local.settings.example.json` with `OPENROUTER_API_KEY` configuration. + +### Changed +- **Parameter & Key Propagation**: Wired `userSettings` through `App.tsx` down to `ChatSession`, `ScenarioGenerator`, `VoiceSession`, and `useLiveSession`, ensuring custom OpenRouter API keys and selected model IDs are respected on every call. +- **Azure Functions Default Models**: Updated `AnalysisFunction.cs`, `ChatFunction.cs`, and `ScenarioFunction.cs` default fallback models to `google/gemini-3.8-flash`. +- **Environment Template**: Updated `.env.example` to provide `VITE_OPENROUTER_API_KEY` and `OPENROUTER_API_KEY`. + +### Removed +- **Dead Dependency Residue**: Stripped `@google/genai` from `package.json` dependencies and `vite.config.ts` rollup chunks. diff --git a/changelogs/unreleased/svelte-5-rewrite.md b/changelogs/unreleased/svelte-5-rewrite.md new file mode 100644 index 0000000..5a68751 --- /dev/null +++ b/changelogs/unreleased/svelte-5-rewrite.md @@ -0,0 +1,42 @@ +--- +title: "Full Frontend Rewrite to Svelte 5 (Runes & Bits UI Architecture)" +ticket: "SVELTE-5-REWRITE" +type: "refactor" +author: "Antigravity Autonomous Swarm" +date: "2026-09-29" +--- + +### Added +- **Svelte 5 Runes Architecture**: Fully rewritten frontend in Svelte 5 using runes (`$state`, `$derived`, `$effect`, `$props`, `$bindable`), eliminating virtual DOM overhead and React dependency chains. +- **AudioEngine Reactive Controller**: Engineered `src/lib/audio/AudioEngine.svelte.ts` managing WebAudio contexts, FFT analyser frequency extraction, Web Speech recognition, OpenRouter TTS synthesis, and parallel clinical supervision in microtasks. +- **Svelte 5 Reactive Stores**: Implemented `src/lib/stores/UserSettingsStore.svelte.ts` and `src/lib/stores/AppStateStore.svelte.ts` with local storage persistence and derived filters. +- **Svelte 5 Design System Primitives (`src/lib/components/ui/`)**: + - `Button.svelte`, `Badge.svelte`, `Card.svelte` (+ `Header`, `Title`, `Description`, `Content`, `Footer`) + - `Input.svelte`, `Textarea.svelte`, `Tooltip.svelte`, `Dialog.svelte` + - `Tabs.svelte`, `TabsList.svelte`, `TabsTrigger.svelte`, `TabsContent.svelte`, `tabsContext.ts` + - `ScrollArea.svelte`, `Label.svelte`, `Separator.svelte` +- **Recomposed Clinical Feature Components (`src/lib/components/`)**: + - `ScenarioCard.svelte`: Scenario card with difficulty badges, CBT protocol chips, and text/voice launching. + - `AudioVisualizer.svelte`: Canvas/CSS audio level visualizer with dynamic bar animations. + - `AudioDeviceSelector.svelte`: Microphone input device switcher. + - `LiveInsightPanel.svelte`: Turn-by-turn parallel supervisor feedback drawer. + - `SessionReport.svelte`: Comprehensive post-session CBT assessment report with Markdown and JSON exports. + - `ScenarioGenerator.svelte`: AI scenario generator modal with difficulty presets and clinical concept selection. + - `CBTReference.svelte`: Searchable glossary reference covering all 18 clinical terms and techniques. + - `SettingsModal.svelte`: Voice profile and OpenRouter API key/model selector modal. + - `TranscriptPanel.svelte`: Slide-over voice transcript drawer with role styling. + - `ChatMessage.svelte` & `ChatInput.svelte`: Roleplay message bubbles and character input box. + - `ChatSession.svelte`: Streaming text chat controller wired to OpenRouter and live supervisor. + - `VoiceSession/VoiceHeader.svelte`, `VoiceControls.svelte`, `ConnectionState.svelte`, `AnalyzingState.svelte`. + - `VoiceSession.svelte`: Full voice roleplay cockpit coordinating AudioEngine and visualizer. + - `ReloadPrompt.svelte` & `VersionBadge.svelte`: PWA reload prompt and app version indicator. +- **App Shell & Mounting (`src/`)**: + - `src/App.svelte`: Root application coordinator. + - `src/main.ts`: Svelte 5 `mount()` entry point targeting `#root`. +- **Vitest & Svelte-Check Verification Suites**: + - Upgraded test harness to `vitest` with 16/16 unit tests passing. + - `npx svelte-check`: 0 errors, 0 warnings across all 20 Svelte components. + +### Changed +- **Entry Point**: Updated `index.html` to mount `/src/main.ts` and removed obsolete React importmap. +- **Vite Bundler**: Configured `svelte()` plugin alongside PWA injectManifest, tuning rollup manual chunks for `@lucide/svelte`, `svelte`, and `workbox`. diff --git a/changelogs/unreleased/voice-demo-and-playwright-snapshots.md b/changelogs/unreleased/voice-demo-and-playwright-snapshots.md new file mode 100644 index 0000000..dc99370 --- /dev/null +++ b/changelogs/unreleased/voice-demo-and-playwright-snapshots.md @@ -0,0 +1,33 @@ +# Changelog Fragment: Natural Voice Audition Lab & Comprehensive Playwright Snapshot Suite + +## Summary +Introduced an interactive Natural Voice Testing Lab, calibrated a gruff "salt-of-the-earth" vocal persona (`Fenrir` via OpenRouter `onyx`), and implemented a comprehensive 21-test Playwright E2E visual regression suite spanning Desktop Chrome, iPad Portrait, and Mobile iPhone viewports with 45 packaged review snapshots. + +## Key Changes +- **Natural Voice Audition Lab (`services/voiceAuditionService.ts`, `src/lib/components/SettingsModal.svelte`)**: + - Implemented decoupled audio audition service supporting OpenRouter TTS (`audio/mpeg`) with automatic Web Speech fallback. + - Added quick-demo play/stop triggers on all voice persona selection cards in `SettingsModal`. + - Added dedicated interactive testing workbench with sample clinical dialogue buttons and custom text preview. +- **Salt-of-the-Earth Persona Calibration (`constants.ts`, `src/lib/audio/AudioEngine.svelte.ts`)**: + - Created `Fenrir (Salt of the Earth)` voice profile with `onyx` model mapping and tuned pitch/rate (`0.75` / `0.92`) for authentic, unvarnished clinical dialogue. +- **Playwright Multi-Viewport E2E Test Suite (`tests/e2e/`)**: + - Authored 4 comprehensive test suites across `desktop-chrome`, `ipad-portrait`, and `mobile-iphone` (21 tests passing, 100% coverage). + - Captured 45 full-page visual regression snapshots covering Landing, Filtering, Settings, CBT Reference, Scenario Generator, Chat Roleplay, and Voice Cockpit. + - Packaged interactive HTML review gallery at `docs/screenshots/index.html` with responsive viewport filters and high-DPI lightbox inspection. +- **Native Google Gemini 3.8 Flash TTS Integration (`services/geminiVoiceService.ts`)**: + - Implemented direct Google AI Studio client leveraging `models/gemini-3.8-flash-tts` with fallback to `gemini-2.5-flash-preview-tts`. + - Added support for native Google Gemini voice personalities (`Fenrir`, `Puck`, `Charon`, `Kore`, `Aoede`) producing authentic studio-grade `audio/wav` output. + - Built automatic RIFF/WAVE header framing and base64 audio decoding into WebAudio `AudioContext`. + - Configured browser-origin client requests (`http://localhost:3000`) respecting Google Cloud referrer restrictions. +- **Full-Bleed Voice Session Cockpit & Clinical Companion Redesign (`src/App.svelte`, `VoiceSession.svelte`, `LiveInsightPanel.svelte`)**: + - Re-architected `VoiceSession` from a constrained `h-[80vh]` card into an immersive full-bleed studio cockpit (`fixed inset-0 bg-slate-950`). + - Resolved the critical layout squishing defect where `LiveInsightPanel` had hardcoded `lg:w-[35%]` resulting in an unreadable ~134px vertical bookmark strip. + - Implemented a unified, non-intrusive **Clinical Companion Drawer (Supervision & Transcripts)** slide-over overlay with tabs, smooth slide transitions, and rich dark glassmorphic cards. + - Preserved permanent horizontal and vertical centering for the reactive audio visualizer orb so opening companion panels never jars or squishes the primary voice interface. + - Upgraded `ChatSession` with responsive dual-pane flex containers ensuring consistent layout across desktop, tablet, and mobile. +- **Svelte 5 Architectural Hardening**: + + - Resolved `AudioDeviceSelector` auto-trigger defect where initial device discovery prematurely dismissed the modal. + - Fixed tab context reactivity in `Tabs.svelte` using Svelte 5 `$state` proxy pattern. + - Initialized `AudioEngine.status` to `'idle'` to eliminate cockpit initialization deadlock. + diff --git a/components/ChatSession.tsx b/components/ChatSession.tsx index b2e20e1..2887fcc 100644 --- a/components/ChatSession.tsx +++ b/components/ChatSession.tsx @@ -1,9 +1,8 @@ - import React, { useState, useRef, useEffect, useCallback } from 'react'; -import { Scenario, Message, AnalysisResult } from '../types'; -import { GoogleGenAI, Chat, GenerateContentResponse } from "@google/genai"; +import { Scenario, Message, AnalysisResult, UserSettings } from '../types'; import { BrainCircuit, ChevronLeft, Flag, Loader2 } from 'lucide-react'; -import { SYSTEM_ANALYSIS_PROMPT, SYSTEM_SUMMARY_ANALYSIS_PROMPT, CBT_ANALYSIS_SCHEMA } from '../constants'; +import { streamOpenRouterChat, analyzeTurnWithOpenRouter, generateSessionSummaryReport } from '../services/openRouterService'; +import { saveSession } from '../services/sessionStorage'; // Sub-components import ChatMessage from './ChatMessage'; @@ -13,10 +12,11 @@ import SessionReport from './SessionReport'; interface ChatSessionProps { scenario: Scenario; + userSettings?: UserSettings; onExit: () => void; } -const ChatSession: React.FC = ({ scenario, onExit }) => { +const ChatSession: React.FC = ({ scenario, userSettings, onExit }) => { const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [status, setStatus] = useState<'active' | 'analyzing' | 'finished'>('active'); @@ -26,8 +26,6 @@ const ChatSession: React.FC = ({ scenario, onExit }) => { const [isAnalyzingTurn, setIsAnalyzingTurn] = useState(false); const [finalReport, setFinalReport] = useState(null); - const chatRef = useRef(null); - const analysisRef = useRef(null); const messagesEndRef = useRef(null); const scrollToBottom = useCallback(() => { @@ -40,9 +38,40 @@ const ChatSession: React.FC = ({ scenario, onExit }) => { // Session Initialization useEffect(() => { - const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); + // Initial Message + setMessages([{ + id: 'init', + role: 'model', + text: scenario.initialMessage, + timestamp: new Date() + }]); + + setLiveInsight(null); + }, [scenario]); + + const handleSend = useCallback(async (text: string) => { + if (isLoading || !text.trim()) return; + + const userMsg: Message = { + id: Date.now().toString(), + role: 'user', + text: text, + timestamp: new Date() + }; + + const updatedHistory = [...messages, userMsg]; + setMessages(updatedHistory); + setIsLoading(true); + + const botMsgId = (Date.now() + 1).toString(); + const botMsg: Message = { + id: botMsgId, + role: 'model', + text: '', + timestamp: new Date() + }; + setMessages([...updatedHistory, botMsg]); - // Roleplay Chat Setup const roleplaySystemInstruction = ` ${scenario.systemInstruction} @@ -64,95 +93,55 @@ const ChatSession: React.FC = ({ scenario, onExit }) => { Return the character's direct dialogue response. Keep it conversational and proportional to the user's input. `; - chatRef.current = ai.chats.create({ - model: 'gemini-3-flash-preview', - config: { systemInstruction: roleplaySystemInstruction }, - }); - - // Live Analysis Chat Setup - const analysisSystemInstruction = `${SYSTEM_ANALYSIS_PROMPT} -SCENARIO CONTEXT: -Title: ${scenario.title} -Description: ${scenario.description} -CBT Focus Areas: ${scenario.cbtFocus.join(', ')}`; - - analysisRef.current = ai.chats.create({ - model: 'gemini-3-flash-preview', - config: { - systemInstruction: analysisSystemInstruction, - responseMimeType: 'application/json', - responseSchema: CBT_ANALYSIS_SCHEMA, - } - }); - - // Initial Message - setMessages([{ - id: 'init', - role: 'model', - text: scenario.initialMessage, - timestamp: new Date() - }]); - - setLiveInsight(null); - }, [scenario]); - - const handleSend = useCallback(async (text: string) => { - if (!chatRef.current || isLoading) return; - - const userMsg: Message = { - id: Date.now().toString(), - role: 'user', - text: text, - timestamp: new Date() - }; - - setMessages(prev => [...prev, userMsg]); - setIsLoading(true); - try { - const botMsgId = (Date.now() + 1).toString(); - const botMsg: Message = { - id: botMsgId, - role: 'model', - text: '', - timestamp: new Date() - }; - setMessages(prev => [...prev, botMsg]); - - const streamResponse = await chatRef.current.sendMessageStream({ message: text }); - let fullResponseText = ''; - - for await (const chunk of streamResponse) { - const c = chunk as GenerateContentResponse; - const textChunk = c.text || ""; - fullResponseText += textChunk; - - setMessages(prev => prev.map(m => - m.id === botMsgId ? { ...m, text: fullResponseText } : m - )); - } - - // Live Turn Analysis - if (analysisRef.current) { - setIsAnalyzingTurn(true); - const turnContext = `User said: "${userMsg.text}". Roleplay partner said: "${fullResponseText}".`; - - analysisRef.current.sendMessage({ message: turnContext }).then(res => { - try { - if (res.text) setLiveInsight(JSON.parse(res.text)); - } catch (e) { console.error("Live analysis parse error", e); } finally { - setIsAnalyzingTurn(false); + let accumulatedText = ''; + await streamOpenRouterChat( + updatedHistory, + roleplaySystemInstruction, + { + onChunk: (chunk) => { + accumulatedText += chunk; + setMessages(prev => prev.map(m => + m.id === botMsgId ? { ...m, text: accumulatedText } : m + )); + }, + onComplete: (fullText) => { + // Live Turn Clinical Analysis via OpenRouter + setIsAnalyzingTurn(true); + const turnContext = `User said: "${userMsg.text}". Roleplay partner said: "${fullText}".`; + analyzeTurnWithOpenRouter( + turnContext, + scenario.title, + scenario.description, + scenario.cbtFocus, + { + apiKey: userSettings?.openRouterApiKey, + model: userSettings?.supervisorModel + } + ).then(res => { + setLiveInsight(res); + }).catch(e => { + console.error("Live analysis failed", e); + }).finally(() => { + setIsAnalyzingTurn(false); + }); + }, + onError: (err) => { + console.error("OpenRouter stream failed", err); } - }); - } - + }, + { + apiKey: userSettings?.openRouterApiKey, + model: userSettings?.roleplayModel + } + ); } catch (error) { console.error("Chat transmission failed", error); setMessages(prev => prev.filter(m => m.text !== '')); } finally { setIsLoading(false); } - }, [isLoading]); + }, [messages, scenario, isLoading, userSettings]); const handleFinishSession = async () => { if (messages.length < 2) return onExit(); @@ -160,28 +149,19 @@ CBT Focus Areas: ${scenario.cbtFocus.join(', ')}`; setStatus('analyzing'); try { - const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); - const transcript = messages.map(m => `[${m.role.toUpperCase()}]: ${m.text}`).join('\n'); + const reportData = await generateSessionSummaryReport( + messages, + scenario.title, + scenario.description, + { + apiKey: userSettings?.openRouterApiKey, + model: userSettings?.supervisorModel + } + ); - const summaryPrompt = `${SYSTEM_SUMMARY_ANALYSIS_PROMPT} -SCENARIO CONTEXT: ${scenario.title} - ${scenario.description}`; - - const result = await ai.models.generateContent({ - model: 'gemini-3-flash-preview', - config: { - systemInstruction: summaryPrompt, - responseMimeType: 'application/json', - responseSchema: CBT_ANALYSIS_SCHEMA, - }, - contents: [{ role: 'user', parts: [{ text: `SESSION TRANSCRIPT:\n${transcript}` }] }] - }); - - if (result.text) { - setFinalReport(JSON.parse(result.text)); - setStatus('finished'); - } else { - throw new Error("Final synthesis failed"); - } + setFinalReport(reportData); + saveSession(scenario, 'text', messages, reportData); + setStatus('finished'); } catch (err) { console.error("Post-session analysis failure", err); onExit(); @@ -196,13 +176,20 @@ SCENARIO CONTEXT: ${scenario.title} - ${scenario.description}`;

Compiling Report

-

Generating evidence-based synthesis of your cognitive and behavioral patterns during this session...

+

Generating evidence-based synthesis of your cognitive and behavioral patterns during this session via OpenRouter...

); } if (status === 'finished' && finalReport) { - return ; + return ( + + ); } return ( diff --git a/components/ScenarioGenerator.tsx b/components/ScenarioGenerator.tsx index 3b68334..37e97d2 100644 --- a/components/ScenarioGenerator.tsx +++ b/components/ScenarioGenerator.tsx @@ -1,6 +1,5 @@ import React, { useState } from 'react'; -import { GoogleGenAI, Type } from "@google/genai"; -import { Scenario } from '../types'; +import { Scenario, UserSettings } from '../types'; import { CBT_GLOSSARY } from '../constants'; import { Sparkles, X, Loader2, AlertCircle, Info, Target, UserCircle2, BrainCircuit, Check, BarChart } from 'lucide-react'; import { Button } from './ui/button'; @@ -8,13 +7,15 @@ import { Textarea } from './ui/textarea'; import { ScrollArea } from './ui/scroll-area'; import { Card } from './ui/card'; import { Badge } from './ui/badge'; +import { generateScenarioWithOpenRouter } from '../services/openRouterService'; interface ScenarioGeneratorProps { onGenerate: (scenario: Scenario) => void; onCancel: () => void; + userSettings?: UserSettings; } -const ScenarioGenerator: React.FC = ({ onGenerate, onCancel }) => { +const ScenarioGenerator: React.FC = ({ onGenerate, onCancel, userSettings }) => { const [prompt, setPrompt] = useState(''); const [selectedConcepts, setSelectedConcepts] = useState([]); const [selectedDifficulty, setSelectedDifficulty] = useState('Intermediate'); @@ -34,240 +35,162 @@ const ScenarioGenerator: React.FC = ({ onGenerate, onCan setError(null); try { - const ai = new GoogleGenAI({ apiKey: import.meta.env.VITE_GEMINI_API_KEY }); - - const schema = { - type: Type.OBJECT, - properties: { - id: { type: Type.STRING }, - title: { type: Type.STRING }, - characterName: { type: Type.STRING }, - description: { type: Type.STRING }, - category: { - type: Type.STRING, - enum: ['Social Anxiety', 'Workplace', 'Relationships', 'Self-Talk'] - }, - difficulty: { - type: Type.STRING, - enum: ['Beginner', 'Intermediate', 'Advanced'] - }, - systemInstruction: { type: Type.STRING }, - initialMessage: { type: Type.STRING }, - cbtFocus: { - type: Type.ARRAY, - items: { type: Type.STRING } - } - }, - required: ['id', 'title', 'characterName', 'description', 'category', 'difficulty', 'systemInstruction', 'initialMessage', 'cbtFocus'] - }; - - const selectedContext = selectedConcepts.length > 0 - ? `Focus specifically on these CBT concepts: ${selectedConcepts.join(', ')}.` - : ''; - - const result = await ai.models.generateContent({ - model: 'gemini-3-flash-preview', - config: { - responseMimeType: 'application/json', - responseSchema: schema, - systemInstruction: ` - [CONTEXT] - You are a Clinical CBT Scenario Architect. - The user requires a structured clinical simulation designed to address specific cognitive distortions and behavioral challenges. - Clinical Focus: ${selectedContext} - - [TASK] - Transform the user's prompt into a cohesive, structured clinical scenario that sets up a targeted roleplay environment. - - [CONSTRAINTS] - - CATEGORIZATION: You MUST categorize the scenario appropriately (Social Anxiety, Workplace, Relationships, or Self-Talk). - - DIFFICULTY CALIBRATION: The requested difficulty is '${selectedDifficulty}'. - - 'Beginner': Low stakes, high support, clear-cut distortions, AI character is cooperative. - - 'Intermediate': Ambiguous stakes, moderate social friction, subtle distortions, AI character has specific needs. - - 'Advanced': High emotional triggers, persistent clinical resistance, complex cognitive patterns. - - INTERNAL LOGIC: Provide a detailed "INTERNAL CBT LOGIC" section instructing the AI character how to react to the user's progress or cognitive lapses during the future roleplay. - - CHARACTER VOICE: Define a realistic, emotionally consistent voice profile. - - [OUTPUT REQUIREMENTS] - Return ONLY a valid JSON object conforming exactly to the requested schema. Ensure all arrays and nested objects are populated correctly. - ` - }, - contents: [ - { role: 'user', parts: [{ text: `Generate a clinical scenario for: ${prompt || 'A scenario exploring the chosen CBT concepts.'}` }] } - ] - }); - - if (result.text) { - const scenario = JSON.parse(result.text) as Scenario; - // Enforce the user-selected difficulty in the final object - scenario.difficulty = selectedDifficulty; - scenario.id = `custom-${Date.now()}-${scenario.id}`; - onGenerate(scenario); - } else { - throw new Error("No content generated"); - } + const scenario = await generateScenarioWithOpenRouter( + prompt, + selectedConcepts, + selectedDifficulty, + { + apiKey: userSettings?.openRouterApiKey, + model: userSettings?.roleplayModel + } + ); + scenario.difficulty = selectedDifficulty; + scenario.id = `custom-${Date.now()}-${scenario.id || 'generated'}`; + onGenerate(scenario); } catch (err: any) { - console.error("Generation error:", err); - setError("Clinical scenario synthesis failed. Please refine your parameters."); + console.error("OpenRouter Scenario Generation error:", err); + setError(err?.message || "Clinical scenario synthesis failed. Please check your OpenRouter API Key in Settings."); } finally { setIsLoading(false); } }; - const getConceptVariant = (category: string, isSelected: boolean) => { - if (!isSelected) return 'outline'; - return 'default'; - }; - - const getConceptColorClass = (category: string, isSelected: boolean) => { - if (!isSelected) return 'text-slate-500 border-slate-200 hover:border-slate-300'; - switch (category) { - case 'Distortion': return 'bg-rose-600 border-rose-600 hover:bg-rose-700 shadow-md shadow-rose-100'; - case 'Technique': return 'bg-teal-600 border-teal-600 hover:bg-teal-700 shadow-md shadow-teal-100'; - case 'Concept': return 'bg-amber-600 border-amber-600 hover:bg-amber-700 shadow-md shadow-amber-100'; - default: return 'bg-slate-800 border-slate-800 hover:bg-slate-900'; - } - }; - return ( -
-
+
+
{/* Header */} -
-
-

- - Scenario Synthesis -

-

- Construct an evidence-based clinical environment. -

+
+
+
+ +
+
+

Clinical Scenario Architect

+

+ AI Synthesis Engine • OpenRouter +

+
- -
+
- -
- {/* Prompting Guide */} - -
- -

Clinical Blueprint

-
-
-
-
- Character Role -
-

Define who the AI plays.

-
-
-
- The Challenge -
-

Describe the social friction.

-
-
-
- CBT Focus -
-

Target specific distortions.

-
-
-
- - {/* Difficulty Selection */} -
-