diff --git a/App.tsx b/App.tsx index c505749..eb4a26e 100644 --- a/App.tsx +++ b/App.tsx @@ -1,13 +1,21 @@ import React, { useState, useEffect } from 'react'; import { SCENARIOS } from './constants'; -import { Scenario, SessionMode } from './types'; +import { Scenario, SessionMode, UserSettings } from './types'; import ScenarioCard from './components/ScenarioCard'; import ChatSession from './components/ChatSession'; import VoiceSession from './components/VoiceSession'; import ScenarioGenerator from './components/ScenarioGenerator'; import CBTReference from './components/CBTReference'; -import { Brain, Info, ShieldCheck, Filter, Plus, Sparkles, BookOpen, Loader2, Activity, Zap } from 'lucide-react'; +import SettingsModal from './components/SettingsModal'; +import Tooltip from './components/Tooltip'; +import { Brain, Info, ShieldCheck, Filter, Plus, BookOpen, Activity, Zap, Settings } from 'lucide-react'; + +const DEFAULT_SETTINGS: UserSettings = { + voiceName: 'Zephyr', + vocalStyle: 'Helpful', + genderPreference: 'Neutral' +}; const App: React.FC = () => { const [activeScenario, setActiveScenario] = useState(null); @@ -19,11 +27,20 @@ const App: React.FC = () => { const [customScenarios, setCustomScenarios] = useState([]); const [showGenerator, setShowGenerator] = useState(false); const [showReference, setShowReference] = useState(false); + const [showSettings, setShowSettings] = useState(false); + + // User Settings State + const [userSettings, setUserSettings] = useState(() => { + const saved = localStorage.getItem('cognitive-clarity-settings'); + return saved ? JSON.parse(saved) : DEFAULT_SETTINGS; + }); + + useEffect(() => { + localStorage.setItem('cognitive-clarity-settings', JSON.stringify(userSettings)); + }, [userSettings]); const handleSelectScenario = (scenario: Scenario, mode: SessionMode) => { setIsPreparing(true); - // Simulate "Clinical Environment Calibration" - // This provides a clearer boundary between dashboard and the specialized session environment setTimeout(() => { setActiveScenario(scenario); setSessionMode(mode); @@ -41,14 +58,12 @@ const App: React.FC = () => { }; const allScenarios = [...customScenarios, ...SCENARIOS]; - const filteredScenarios = allScenarios.filter(scenario => selectedDifficulty === 'All' || scenario.difficulty === selectedDifficulty ); const difficulties = ['All', 'Beginner', 'Intermediate', 'Advanced']; - // Robust Session Preparation UI if (isPreparing) { return (
@@ -82,7 +97,7 @@ const App: React.FC = () => {

- Injecting {activeScenario?.cbtFocus[0] || 'Clinical'} Logic + Applying {userSettings.voiceName} Vocal Profile

@@ -106,7 +121,11 @@ const App: React.FC = () => { ) : (
- +
)} @@ -115,18 +134,10 @@ const App: React.FC = () => { return (
- {/* Generator Modal */} - {showGenerator && ( - setShowGenerator(false)} - /> - )} - - {/* Reference Modal */} - {showReference && ( - setShowReference(false)} /> - )} + {/* Modals */} + {showGenerator && setShowGenerator(false)} />} + {showReference && setShowReference(false)} />} + {showSettings && setShowSettings(false)} />} {/* Hero Section */}
@@ -139,41 +150,53 @@ const App: React.FC = () => {

Cognitive Clarity

- +
+ + + + + + +

- A clinical roleplaying platform utilizing CBT principles and AI to help you navigate social complexities and restructure internal dialogues. + An adaptive clinical training environment designed to strengthen cognitive flexibility and navigate social friction.

-
- - Evidence-Based Framework -
-
- - Personalized Adaptive Logic -
+ +
+ + Evidence-Based Framework +
+
+ +
+ + Vocal Partner: {userSettings.voiceName} +
+
{/* Main Content */}
-
-

- Training Scenarios -

- +
+

Training Scenarios

- {/* Filter Controls */}
@@ -196,32 +219,27 @@ const App: React.FC = () => {
- + + +
{filteredScenarios.map(scenario => ( - + ))} - - + + + + + +
); }; -export default ScenarioCard; \ No newline at end of file +export default React.memo(ScenarioCard); diff --git a/components/SettingsModal.tsx b/components/SettingsModal.tsx new file mode 100644 index 0000000..050d4de --- /dev/null +++ b/components/SettingsModal.tsx @@ -0,0 +1,120 @@ + +import React from 'react'; +import { UserSettings } from '../types'; +import { X, Volume2, Check, Info, Mic2 } from 'lucide-react'; +import Tooltip from './Tooltip'; + +interface SettingsModalProps { + settings: UserSettings; + onUpdate: (settings: UserSettings) => void; + onClose: () => void; +} + +interface VoiceOption { + id: UserSettings['voiceName']; + label: string; + gender: 'Male' | 'Female' | 'Neutral'; + style: string; + description: string; +} + +const VOICE_OPTIONS: VoiceOption[] = [ + { id: 'Zephyr', label: 'Zephyr', gender: 'Neutral', style: 'Helpful', description: 'A balanced, clear, and professional tone suitable for most scenarios.' }, + { id: 'Puck', label: 'Puck', gender: 'Male', style: 'Youthful', description: 'Energetic and friendly, ideal for peer-to-peer or informal interactions.' }, + { id: 'Kore', label: 'Kore', gender: 'Female', style: 'Calm', description: 'Soothing and professional, perfect for clinical or high-stress roleplay.' }, + { id: 'Charon', label: 'Charon', gender: 'Male', style: 'Authoritative', description: 'Deep and commanding, useful for workplace or boundary-testing scenarios.' }, + { id: 'Fenrir', label: 'Fenrir', gender: 'Male', style: 'Rugged', description: 'Direct and matter-of-fact tone with a slightly more serious edge.' }, +]; + +const SettingsModal: React.FC = ({ settings, onUpdate, onClose }) => { + return ( +
+
+
+
+
+ +
+
+

Voice & Persona Settings

+

Configure your AI clinical partner

+
+
+ +
+ +
+
+
+

+ + Vocal Profile +

+ + + +
+ +
+ {VOICE_OPTIONS.map((voice) => ( + + ))} +
+
+ +
+
+ +
+

+ These settings apply to Voice Mode interactions only. Text-based roleplay personas are dynamically generated based on each specific scenario. +

+
+
+ +
+ +
+
+
+ ); +}; + +export default SettingsModal; diff --git a/components/Tooltip.tsx b/components/Tooltip.tsx new file mode 100644 index 0000000..aa6f199 --- /dev/null +++ b/components/Tooltip.tsx @@ -0,0 +1,46 @@ + +import React, { useState } from 'react'; + +interface TooltipProps { + content: string; + children: React.ReactNode; + position?: 'top' | 'bottom' | 'left' | 'right'; +} + +const Tooltip: React.FC = ({ content, children, position = 'top' }) => { + const [isVisible, setIsVisible] = useState(false); + + const positionClasses = { + top: 'bottom-full left-1/2 -translate-x-1/2 mb-2', + bottom: 'top-full left-1/2 -translate-x-1/2 mt-2', + left: 'right-full top-1/2 -translate-y-1/2 mr-2', + right: 'left-full top-1/2 -translate-y-1/2 ml-2', + }; + + const arrowClasses = { + top: 'top-full left-1/2 -translate-x-1/2 border-t-slate-800', + bottom: 'bottom-full left-1/2 -translate-x-1/2 border-b-slate-800', + left: 'left-full top-1/2 -translate-y-1/2 border-l-slate-800', + right: 'right-full top-1/2 -translate-y-1/2 border-r-slate-800', + }; + + return ( +
setIsVisible(true)} + onMouseLeave={() => setIsVisible(false)} + onFocus={() => setIsVisible(true)} + onBlur={() => setIsVisible(false)} + > + {children} + {isVisible && ( +
+ {content} +
+
+ )} +
+ ); +}; + +export default React.memo(Tooltip); diff --git a/components/TranscriptPanel.tsx b/components/TranscriptPanel.tsx new file mode 100644 index 0000000..a47f6cd --- /dev/null +++ b/components/TranscriptPanel.tsx @@ -0,0 +1,84 @@ + +import React, { useEffect, useRef } from 'react'; +import { MessageSquare, X, User, Bot } from 'lucide-react'; + +interface TranscriptPanelProps { + messages: { role: 'user' | 'model'; text: string; id: string }[]; + onClose: () => void; +} + +const TranscriptPanel: React.FC = ({ messages, onClose }) => { + const scrollRef = useRef(null); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [messages]); + + return ( +
+
+
+ +

Live Conversation

+
+ +
+ +
+ {messages.length === 0 ? ( +
+
+ +
+

+ Listening for conversation...
Transcript will appear here +

+
+ ) : ( + messages.map((m) => ( +
+
+ {m.role === 'user' ? ( + <> + You + + + ) : ( + <> + + AI Partner + + )} +
+
+ {m.text} +
+
+ )) + )} +
+ +
+
+
+

+ Real-time Analysis +

+
+
+
+ ); +}; + +export default React.memo(TranscriptPanel); diff --git a/components/VoiceSession.tsx b/components/VoiceSession.tsx index 6bc1a23..b07be86 100644 --- a/components/VoiceSession.tsx +++ b/components/VoiceSession.tsx @@ -1,48 +1,54 @@ + import React, { useEffect, useRef, useState, useCallback } from 'react'; -// Corrected import: AnalysisResult is defined in types.ts, not LiveInsightPanel.tsx -import { Scenario, AnalysisResult } from '../types'; +import { Scenario, AnalysisResult, UserSettings } from '../types'; import { GoogleGenAI, LiveServerMessage, Modality, Chat } from "@google/genai"; -import { Mic, MicOff, X, Radio, AlertCircle, Headphones, BrainCircuit, LayoutPanelLeft } from 'lucide-react'; +import { Mic, MicOff, X, Radio, AlertCircle, Headphones, BrainCircuit, LayoutPanelLeft, Play, Info, CheckCircle2, MessageSquare, Sparkles, Activity } from 'lucide-react'; import { arrayBufferToBase64, base64ToUint8Array, decodeAudioData, floatTo16BitPCM } from '../services/audioUtils'; import { CBT_ANALYSIS_SCHEMA, SYSTEM_SUMMARY_ANALYSIS_PROMPT, SYSTEM_ANALYSIS_PROMPT } from '../constants'; // Sub-components import AudioVisualizer from './AudioVisualizer'; -// Corrected import: LiveInsightPanel is a default export, AnalysisResult is already imported from types import LiveInsightPanel from './LiveInsightPanel'; import SessionReport from './SessionReport'; +import Tooltip from './Tooltip'; +import TranscriptPanel from './TranscriptPanel'; interface VoiceSessionProps { scenario: Scenario; onExit: () => void; + voiceSettings: UserSettings; } -const VoiceSession: React.FC = ({ scenario, onExit }) => { - const [status, setStatus] = useState<'disconnected' | 'connecting' | 'connected' | 'error' | 'analyzing' | 'finished'>('disconnected'); +interface DisplayMessage { + id: string; + role: 'user' | 'model'; + text: string; +} + +const VoiceSession: React.FC = ({ scenario, onExit, voiceSettings }) => { + const [sessionPhase, setSessionPhase] = useState<'briefing' | 'session' | 'analyzing' | 'finished'>('briefing'); + const [status, setStatus] = useState<'disconnected' | 'connecting' | 'active' | 'error'>('disconnected'); const [errorMessage, setErrorMessage] = useState(null); const [finalAnalysis, setFinalAnalysis] = useState(null); - // Live Analysis State + const [showLiveInsights, setShowLiveInsights] = useState(false); + const [showTranscript, setShowTranscript] = useState(false); + const [liveAnalysis, setLiveAnalysis] = useState(null); const [isAnalyzingTurn, setIsAnalyzingTurn] = useState(false); - const [showLiveInsights, setShowLiveInsights] = useState(false); + const [liveMessages, setLiveMessages] = useState([]); - // Audio Refs const inputContextRef = useRef(null); const outputContextRef = useRef(null); const nextStartTimeRef = useRef(0); const sourcesRef = useRef>(new Set()); - // Transcript accumulation const transcriptRef = useRef(""); const currentTurnUserText = useRef(""); const currentTurnPartnerText = useRef(""); - // AI Refs const sessionRef = useRef(null); const analysisChatRef = useRef(null); - - // Visualization const [volumeLevel, setVolumeLevel] = useState(0); useEffect(() => { @@ -52,336 +58,248 @@ const VoiceSession: React.FC = ({ scenario, onExit }) => { }, []); const disconnectSession = useCallback(async () => { - sourcesRef.current.forEach(source => { - try { source.stop(); } catch(e) {} - }); + sourcesRef.current.forEach(source => { try { source.stop(); } catch(e) {} }); sourcesRef.current.clear(); - try { - if (inputContextRef.current && inputContextRef.current.state !== 'closed') { - await inputContextRef.current.close(); - } - if (outputContextRef.current && outputContextRef.current.state !== 'closed') { - await outputContextRef.current.close(); - } - } catch (e) { - console.error("Error closing contexts", e); - } + if (inputContextRef.current && inputContextRef.current.state !== 'closed') await inputContextRef.current.close(); + if (outputContextRef.current && outputContextRef.current.state !== 'closed') await outputContextRef.current.close(); + } catch (e) { console.error("Error closing contexts", e); } inputContextRef.current = null; outputContextRef.current = null; - if (sessionRef.current) { try { const session = await sessionRef.current; - if (session && typeof session.close === 'function') { - session.close(); - } - } catch (e) { - console.error("Error closing session:", e); - } + if (session && typeof session.close === 'function') session.close(); + } catch (e) { console.error("Error closing session:", e); } sessionRef.current = null; } }, []); const handleTerminateAndAnalyze = async () => { await disconnectSession(); - - if (transcriptRef.current.length < 10) { - setStatus('disconnected'); - onExit(); - return; - } - - setStatus('analyzing'); + if (transcriptRef.current.length < 10) return onExit(); + setSessionPhase('analyzing'); try { const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); - const systemPrompt = `${SYSTEM_SUMMARY_ANALYSIS_PROMPT} - - SCENARIO CONTEXT: - Title: ${scenario.title} - Description: ${scenario.description} - CBT Focus: ${scenario.cbtFocus.join(', ')}`; - const result = await ai.models.generateContent({ model: 'gemini-3-flash-preview', config: { - systemInstruction: systemPrompt, + systemInstruction: `${SYSTEM_SUMMARY_ANALYSIS_PROMPT}\n\nSCENARIO: ${scenario.title}\nCBT: ${scenario.cbtFocus.join(', ')}`, responseMimeType: 'application/json', responseSchema: CBT_ANALYSIS_SCHEMA, }, - contents: [{ role: 'user', parts: [{ text: `SESSION TRANSCRIPT:\n${transcriptRef.current}` }] }] + contents: [{ role: 'user', parts: [{ text: `TRANSCRIPT:\n${transcriptRef.current}` }] }] }); - if (result.text) { setFinalAnalysis(JSON.parse(result.text)); - setStatus('finished'); - } else { - throw new Error("Analysis synthesis failed."); - } - } catch (err) { - console.error("Post-session analysis error", err); - setStatus('disconnected'); - onExit(); - } + setSessionPhase('finished'); + } else { throw new Error("Analysis failed."); } + } catch (err) { onExit(); } }; const startSession = async () => { try { + setSessionPhase('session'); setStatus('connecting'); setErrorMessage(null); transcriptRef.current = ""; currentTurnUserText.current = ""; currentTurnPartnerText.current = ""; setLiveAnalysis(null); + setLiveMessages([]); const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext; outputContextRef.current = new AudioContextClass({ sampleRate: 24000 }); inputContextRef.current = new AudioContextClass({ sampleRate: 16000 }); - - if (outputContextRef.current.state === 'suspended') await outputContextRef.current.resume(); - if (inputContextRef.current.state === 'suspended') await inputContextRef.current.resume(); - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); analysisChatRef.current = ai.chats.create({ model: 'gemini-3-flash-preview', config: { - systemInstruction: `${SYSTEM_ANALYSIS_PROMPT}\n\nSCENARIO CONTEXT:\nTitle: ${scenario.title}\nDescription: ${scenario.description}\nCBT Focus: ${scenario.cbtFocus.join(', ')}`, + systemInstruction: `${SYSTEM_ANALYSIS_PROMPT}\nSCENARIO: ${scenario.title}\nObjectives: ${scenario.cbtFocus.join(', ')}`, responseMimeType: 'application/json', responseSchema: CBT_ANALYSIS_SCHEMA, } }); - const effectiveSystemInstruction = ` - ${scenario.systemInstruction} - IMPORTANT: This is a voice conversation. Keep responses concise (1-2 sentences). You are strictly in character. - START THE CONVERSATION IMMEDIATELY by saying: "${scenario.initialMessage}" - `; - const sessionPromise = ai.live.connect({ model: 'gemini-2.5-flash-native-audio-preview-09-2025', config: { responseModalities: [Modality.AUDIO], - systemInstruction: effectiveSystemInstruction, + systemInstruction: ` + ${scenario.systemInstruction} + VOICE MODE: Keep responses short and empathetic. + INITIATION: When the user says "Let's start", "Hello", or initiates speech, begin with your opening: "${scenario.initialMessage}". + `, outputAudioTranscription: {}, inputAudioTranscription: {}, - speechConfig: { - voiceConfig: { prebuiltVoiceConfig: { voiceName: 'Puck' } }, - }, + speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: voiceSettings.voiceName } } }, }, callbacks: { onopen: () => { - setStatus('connected'); + setStatus('active'); if (!inputContextRef.current) return; - const source = inputContextRef.current.createMediaStreamSource(stream); const processor = inputContextRef.current.createScriptProcessor(4096, 1, 1); - const gainNode = inputContextRef.current.createGain(); - gainNode.gain.value = 0; - processor.onaudioprocess = (e) => { - const inputData = e.inputBuffer.getChannelData(0); + const data = e.inputBuffer.getChannelData(0); let sum = 0; - for(let i=0; i { - session.sendRealtimeInput({ - media: { mimeType: 'audio/pcm;rate=16000', data: b64Data } - }); - }).catch(err => console.error("Error sending audio:", err)); + for(let i=0; i s.sendRealtimeInput({ media: { mimeType: 'audio/pcm;rate=16000', data: b64 } })); }; - source.connect(processor); - processor.connect(gainNode); - gainNode.connect(inputContextRef.current.destination); + processor.connect(inputContextRef.current.destination); }, onmessage: async (msg: LiveServerMessage) => { if (msg.serverContent?.inputTranscription) { const text = msg.serverContent.inputTranscription.text; currentTurnUserText.current += text; - transcriptRef.current += `[USER]: ${text}\n`; + setLiveMessages(prev => { + const last = prev[prev.length - 1]; + if (last && last.role === 'user') return [...prev.slice(0, -1), { ...last, text: last.text + text }]; + return [...prev, { id: Date.now().toString(), role: 'user', text }]; + }); } if (msg.serverContent?.outputTranscription) { const text = msg.serverContent.outputTranscription.text; currentTurnPartnerText.current += text; - transcriptRef.current += `[PARTNER]: ${text}\n`; + setLiveMessages(prev => { + const last = prev[prev.length - 1]; + if (last && last.role === 'model') return [...prev.slice(0, -1), { ...last, text: last.text + text }]; + return [...prev, { id: Date.now().toString(), role: 'model', text }]; + }); } - const audioData = msg.serverContent?.modelTurn?.parts?.[0]?.inlineData?.data; if (audioData && outputContextRef.current) { - const ctx = outputContextRef.current; const bytes = base64ToUint8Array(audioData); try { - const audioBuffer = await decodeAudioData(bytes, ctx, 24000, 1); - const source = ctx.createBufferSource(); + const audioBuffer = await decodeAudioData(bytes, outputContextRef.current, 24000, 1); + const source = outputContextRef.current.createBufferSource(); source.buffer = audioBuffer; - source.connect(ctx.destination); - const now = ctx.currentTime; - if (nextStartTimeRef.current < now) nextStartTimeRef.current = now; + source.connect(outputContextRef.current.destination); + const now = outputContextRef.current.currentTime; + if (nextStartTimeRef.current < now) nextStartTimeRef.current = now + 0.05; source.start(nextStartTimeRef.current); nextStartTimeRef.current += audioBuffer.duration; sourcesRef.current.add(source); - source.addEventListener('ended', () => sourcesRef.current.delete(source)); - } catch (decodingErr) { console.error("Audio decoding error", decodingErr); } + source.onended = () => sourcesRef.current.delete(source); + } catch (e) { console.error("Audio decoding error:", e); } } - if (msg.serverContent?.interrupted) { sourcesRef.current.forEach(src => { try { src.stop(); } catch(e){} }); sourcesRef.current.clear(); if (outputContextRef.current) nextStartTimeRef.current = outputContextRef.current.currentTime; } - if (msg.serverContent?.turnComplete) { - if (analysisChatRef.current && (currentTurnUserText.current || currentTurnPartnerText.current)) { + transcriptRef.current += `[USER]: ${currentTurnUserText.current}\n[PARTNER]: ${currentTurnPartnerText.current}\n`; + if (analysisChatRef.current) { setIsAnalyzingTurn(true); - const turnContext = `User said: "${currentTurnUserText.current}". Roleplay partner said: "${currentTurnPartnerText.current}".`; - - analysisChatRef.current.sendMessage({ message: turnContext }).then(res => { - try { - if (res.text) { - setLiveAnalysis(JSON.parse(res.text)); - if (!showLiveInsights) setShowLiveInsights(true); - } - } catch (e) { console.error("Failed to parse live analysis", e); } finally { - setIsAnalyzingTurn(false); - currentTurnUserText.current = ""; - currentTurnPartnerText.current = ""; - } - }); + analysisChatRef.current.sendMessage({ message: `Evaluation Turn: User("${currentTurnUserText.current}") Partner("${currentTurnPartnerText.current}")` }).then(res => { + if (res.text) setLiveAnalysis(JSON.parse(res.text)); + setIsAnalyzingTurn(false); + currentTurnUserText.current = ""; + currentTurnPartnerText.current = ""; + }).catch(() => setIsAnalyzingTurn(false)); } } }, - onclose: () => { if (status !== 'error' && status !== 'analyzing' && status !== 'finished') setStatus('disconnected'); }, - onerror: (err) => { - console.error("Session error", err); - setErrorMessage("Clinical bridge interrupted. Resetting connection."); - setStatus('error'); - disconnectSession(); - } + onerror: () => { setStatus('error'); setErrorMessage("Voice bridge interrupted."); disconnectSession(); } } }); sessionRef.current = sessionPromise; - } catch (err: any) { - console.error("Failed to start session", err); - setErrorMessage(err.message || "Microphone access denied or link failure."); - setStatus('error'); - } + } catch (err: any) { setStatus('error'); } }; - if (status === 'analyzing') { + if (sessionPhase === 'briefing') { return ( -
-
- -
+
+
+
+
+
+
+

Ready to start?

+

Vocal Partner: {voiceSettings.voiceName} ({voiceSettings.vocalStyle})

+
+
+
+
+

Narrative Setting

+
"{scenario.description}"
+
+
+
+

Techniques

+
+ {scenario.cbtFocus.map(f => ({f}))} +
+
+
+

Profile

+
{voiceSettings.genderPreference} Speaker
+
+
+
+
Protocol
+

Once active, say "Let's start" to begin the interaction with your {voiceSettings.vocalStyle.toLowerCase()} partner.

+
+
+
+
+ +
-

Synthesizing Clinical Insights

-

Please wait while the engine analyzes your vocal patterns and cognitive restructuring efficiency.

); } - if (status === 'finished' && finalAnalysis) { - return ; - } + if (sessionPhase === 'analyzing') return ( +
+
+

Analyzing Session

+

Generating your clinical progress report...

+
+ ); + + if (sessionPhase === 'finished' && finalAnalysis) return ; return ( -
- {/* Main Voice Hub */} +
- - {status === 'connected' && ( -
-
-
- )} - - {/* Header */} + {status === 'active' && (
)}
-
- -
+
-

{scenario.title}

-
-
- {status} -
+

{scenario.title}

+
{status}
- {status === 'connected' && ( - - )} - + + +
- -
-
- -
-
- -
-
-
- -
- {status === 'disconnected' &&

System ready for audio link.
Please proceed when in a quiet environment.

} - {status === 'connecting' &&

Establishing Bi-Directional Bridge...

} - {status === 'connected' &&

Active Session. Listening...

} - {status === 'error' && ( -
- -

{errorMessage}

-
- )} +
+ +
+
+ {status === 'connecting' &&

Initializing {voiceSettings.voiceName}...

} + {status === 'active' && (

Listening...

Partner: {voiceSettings.voiceName}

)}
- -
- {status !== 'connected' ? ( - - ) : ( - - )} +
+ {status === 'active' && ()}
+ {showTranscript && setShowTranscript(false)} />}
- {showLiveInsights && }
); }; -export default VoiceSession; \ No newline at end of file +export default VoiceSession; diff --git a/constants.ts b/constants.ts index 44bbe7b..3c16ad8 100644 --- a/constants.ts +++ b/constants.ts @@ -1,4 +1,3 @@ - import { Scenario } from './types'; import { Type } from "@google/genai"; @@ -126,7 +125,7 @@ export const SCENARIOS: Scenario[] = [ description: "Your manager asks for a quick sync. You immediately assume the worst about your performance and upcoming review.", category: 'Workplace', difficulty: 'Intermediate', - systemInstruction: "You are a professional manager. You have one piece of constructive feedback regarding a recent report, but overall you're happy with the user's performance.", + systemInstruction: "You are a professional manager. You have one piece of constructive feedback regarding a recent report, but overall you're happy with the user's performance. INTERNAL CBT LOGIC: If the user catastrophizes, reassure them professionally but maintain your feedback.", initialMessage: "Hi there, thanks for jumping on. I wanted to chat briefly about the Q3 project report.", cbtFocus: ["Catastrophizing", "All-or-Nothing Thinking"] }, @@ -149,6 +148,106 @@ export const SCENARIOS: Scenario[] = [ systemInstruction: "You are the user's personified Inner Critic. You are harsh, cynical, and use common distortions. Your goal is to represent the user's negative self-talk so they can practice refuting it.", initialMessage: "I can't believe you forgot to buy milk again. You're completely disorganized and you'll never get your life together.", cbtFocus: ["Labeling", "Overgeneralization"] + }, + { + id: 's5', + title: "Booking an Appointment", + description: "You need to call a busy medical clinic to book a follow-up. You're worried about being a nuisance or stuttering during the call.", + category: 'Social Anxiety', + difficulty: 'Beginner', + systemInstruction: "You are a busy, professional medical receptionist. You are efficient and somewhat direct, but not unkind. INTERNAL CBT LOGIC: If the user is overly apologetic or hesitates excessively, stay professional but move the process forward quickly.", + initialMessage: "City Health Clinic, how can I help you today?", + cbtFocus: ["Mind Reading", "Catastrophizing"] + }, + { + id: 's6', + title: "The Defective Return", + description: "You bought an expensive espresso machine that is leaking. You're at the store to return it, but you're afraid the clerk will be dismissive or think you're lying.", + category: 'Social Anxiety', + difficulty: 'Intermediate', + systemInstruction: "You are a retail clerk at a high-end appliance store. You follow company policy strictly. You aren't trying to be difficult, but you must verify the defect. INTERNAL CBT LOGIC: If the user shows 'Emotional Reasoning' (I feel like I'm doing something wrong), stay neutral and ask for the receipt.", + initialMessage: "Hi there. Returning something today? Can I see your receipt and the item?", + cbtFocus: ["Personalization", "Emotional Reasoning"] + }, + { + id: 's7', + title: "The Unpaid Debt", + description: "A close friend borrowed $200 three months ago and hasn't mentioned it since. You need the money for bills but feel guilty for asking.", + category: 'Relationships', + difficulty: 'Advanced', + systemInstruction: "You are a well-meaning but somewhat forgetful friend. You value the friendship and haven't intentionally avoided the debt; you just genuinely forgot. INTERNAL CBT LOGIC: If the user uses 'Should Statements' about themselves (I shouldn't have to ask), don't get defensive, but wait for them to express the need directly.", + initialMessage: "Hey! It was so good to see you at brunch last week. What's on your mind today?", + cbtFocus: ["Maladaptive Guilt", "Should Statements"] + }, + { + id: 's8', + title: "The Salary Discussion", + description: "You've been exceeding all your targets for a year. You've prepared a case for a raise, but you're battling thoughts that you're being 'greedy' or 'ungrateful'.", + category: 'Workplace', + difficulty: 'Advanced', + systemInstruction: "You are a firm but fair Department Head. You have a limited budget but respect high performance. INTERNAL CBT LOGIC: If the user sounds apologetic or uses 'Shoulds', push back slightly on the budget. If they use objective evidence and stay regulated, be more collaborative.", + initialMessage: "Thanks for coming in. I know you wanted to discuss your compensation. What's your proposal?", + cbtFocus: ["Should Statements", "All-or-Nothing Thinking"] + }, + { + id: 's9', + title: "The Deadline Slip", + description: "You missed a major deadline because you were waiting on a partner's input. You feel like a total failure and are sure your boss will fire you.", + category: 'Workplace', + difficulty: 'Intermediate', + systemInstruction: "You are a result-oriented Team Lead. You are disappointed the deadline was missed but want to solve the problem and prevent a recurrence. INTERNAL CBT LOGIC: If the user uses 'Labeling' (I am a failure), focus them back on the specific behavior and the external variables.", + initialMessage: "The client is asking where the Q1 projection is. We missed the 9 AM cutoff. What happened?", + cbtFocus: ["Labeling", "Overgeneralization"] + }, + { + id: 's10', + title: "The Social 'No'", + description: "A friend has invited you to a high-energy party. You are exhausted and need rest, but you're sure they will stop liking you if you don't attend.", + category: 'Social Anxiety', + difficulty: 'Intermediate', + systemInstruction: "You are an extroverted friend who is excited about a party. You want the user to come because you enjoy their company, but you aren't trying to be manipulative. INTERNAL CBT LOGIC: If the user 'Mind Reads' (You're going to be mad at me), respond with slight confusion about why they'd think that.", + initialMessage: "You are definitely coming to Sarah's party tonight, right? Everyone is going to be there!", + cbtFocus: ["Mind Reading", "Fortune Telling"] + }, + { + id: 's11', + title: "Intrusive Relative", + description: "A relative is making critical comments about your career choices during a family dinner. Practice setting a boundary without spiraling into anger.", + category: 'Relationships', + difficulty: 'Advanced', + systemInstruction: "You are a critical, traditional-minded aunt/uncle. You think you're 'just being helpful' with your advice. INTERNAL CBT LOGIC: If the user sets a clear boundary, try to guilt-trip them slightly to see if they can maintain it.", + initialMessage: "I saw your latest post. Are you still doing that freelance work? It's been a while, shouldn't you look for something more... stable?", + cbtFocus: ["Healthy Boundaries", "Gray Rock Method"] + }, + { + id: 's12', + title: "The Job Interview", + description: "You're in the waiting room for a job you really want. Your mind is already 'Fortune Telling' that you will stutter and look incompetent.", + category: 'Social Anxiety', + difficulty: 'Advanced', + systemInstruction: "You are an HR Director at a top firm. You are professional, observant, and asking standard but probing questions. INTERNAL CBT LOGIC: If the user shows 'Fortune Telling' behaviors (pre-apologizing), maintain a neutral professional stance.", + initialMessage: "Welcome. Please, have a seat. Why don't we start by you telling me about a time you handled a difficult conflict at work?", + cbtFocus: ["Fortune Telling", "Catastrophizing"] + }, + { + id: 's13', + title: "The Friend's Criticism", + description: "A friend tells you that you've been 'acting distant' lately. Your immediate reaction is to take it as a personal attack on your character.", + category: 'Relationships', + difficulty: 'Intermediate', + systemInstruction: "You are a friend who feels a bit neglected and is expressing their feelings. You aren't attacking the user, just sharing your perspective. INTERNAL CBT LOGIC: If the user 'Personalizes' (I'm a bad friend), re-center the conversation on the specific dynamic.", + initialMessage: "I feel like we haven't really talked in weeks. Is everything okay? It feels like you've been avoiding my calls.", + cbtFocus: ["Personalization", "Emotional Reasoning"] + }, + { + id: 's14', + title: "The Unexpected Health Symptom", + description: "You've noticed a small rash on your arm. Your mind has already jumped to a terminal illness. Practice radical acceptance and de-catastrophizing.", + category: 'Self-Talk', + difficulty: 'Advanced', + systemInstruction: "You are the user's personified Anxiety. You are insistent, bringing up 'what if' scenarios and suggesting they search for symptoms online. INTERNAL CBT LOGIC: If the user practices 'Radical Acceptance', become slightly quieter but try one more 'what if'.", + initialMessage: "Look at that mark. That wasn't there yesterday. What if it's something serious? We should probably check the internet right now.", + cbtFocus: ["Catastrophizing", "Radical Acceptance"] } ]; @@ -200,4 +299,4 @@ export const SYSTEM_SUMMARY_ANALYSIS_PROMPT = ` Synthesize the entire interaction to identify recurring cognitive patterns, evaluate overall progress, and assign "homework" for behavioral activation. Analyze the transcript for patterns, not just individual messages. Focus on how the user handles the "Internal CBT Logic" of the specific scenario. -`; +`; \ No newline at end of file diff --git a/types.ts b/types.ts index 8cd7c46..827c297 100644 --- a/types.ts +++ b/types.ts @@ -10,12 +10,18 @@ export interface Scenario { cbtFocus: string[]; } +export interface UserSettings { + voiceName: 'Puck' | 'Charon' | 'Kore' | 'Fenrir' | 'Zephyr'; + vocalStyle: string; + genderPreference: 'Male' | 'Female' | 'Neutral'; +} + export interface Message { id: string; role: 'user' | 'model' | 'system'; text: string; timestamp: Date; - isAnalysis?: boolean; // If true, this is a CBT analysis, not part of roleplay + isAnalysis?: boolean; } export type SessionMode = 'text' | 'voice';