import React, { useState, useRef, useEffect, useCallback } from 'react'; import { Scenario, Message, AnalysisResult } from '../types'; import { GoogleGenAI, Chat, GenerateContentResponse } from "@google/genai"; import { BrainCircuit, ChevronLeft, Flag, Loader2 } from 'lucide-react'; import { SYSTEM_ANALYSIS_PROMPT, SYSTEM_SUMMARY_ANALYSIS_PROMPT, CBT_ANALYSIS_SCHEMA } from '../constants'; // Sub-components import ChatMessage from './ChatMessage'; import ChatInput from './ChatInput'; import LiveInsightPanel from './LiveInsightPanel'; import SessionReport from './SessionReport'; interface ChatSessionProps { scenario: Scenario; onExit: () => void; } const ChatSession: React.FC = ({ scenario, onExit }) => { const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [status, setStatus] = useState<'active' | 'analyzing' | 'finished'>('active'); // Clinical Feedback State const [liveInsight, setLiveInsight] = useState(null); 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(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, []); useEffect(() => { scrollToBottom(); }, [messages, scrollToBottom]); // Session Initialization useEffect(() => { const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); // Roleplay Chat Setup const roleplaySystemInstruction = ` ${scenario.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. `; 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); } }); } } catch (error) { console.error("Chat transmission failed", error); setMessages(prev => prev.filter(m => m.text !== '')); } finally { setIsLoading(false); } }, [isLoading]); const handleFinishSession = async () => { if (messages.length < 2) return onExit(); 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 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"); } } catch (err) { console.error("Post-session analysis failure", err); onExit(); } }; if (status === 'analyzing') { return (

Compiling Report

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

); } if (status === 'finished' && finalReport) { return ; } return (
{/* Interaction Header */}

{scenario.title}

{scenario.cbtFocus.slice(0, 3).map(f => ( {f} ))}
{/* Core Chat Module */}
{messages.map((msg) => ( ))}
{/* Real-time Insights Module */}
); }; export default React.memo(ChatSession);