diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..c505749 --- /dev/null +++ b/App.tsx @@ -0,0 +1,250 @@ + +import React, { useState, useEffect } from 'react'; +import { SCENARIOS } from './constants'; +import { Scenario, SessionMode } 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'; + +const App: React.FC = () => { + const [activeScenario, setActiveScenario] = useState(null); + const [sessionMode, setSessionMode] = useState('text'); + const [selectedDifficulty, setSelectedDifficulty] = useState('All'); + const [isPreparing, setIsPreparing] = useState(false); + + // Custom Scenarios State + const [customScenarios, setCustomScenarios] = useState([]); + const [showGenerator, setShowGenerator] = useState(false); + const [showReference, setShowReference] = useState(false); + + 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); + setIsPreparing(false); + }, 1400); + }; + + const handleExitSession = () => { + setActiveScenario(null); + }; + + const handleScenarioGenerated = (newScenario: Scenario) => { + setCustomScenarios(prev => [newScenario, ...prev]); + setShowGenerator(false); + }; + + 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 ( +
+
+
+
+
+ +
+
+ +
+
+ +

Initializing Session

+
+ + + +

Calibrating AI Parameters

+
+ +
+
+
+ +
+

+ + Configuring {sessionMode === 'voice' ? 'Voice Link' : 'Neural Chat Bridge'} +

+

+ + Injecting {activeScenario?.cbtFocus[0] || 'Clinical'} Logic +

+
+
+ + +
+ ); + } + + if (activeScenario) { + return ( +
+ {sessionMode === 'text' ? ( + + ) : ( +
+ +
+ )} +
+ ); + } + + return ( +
+ {/* Generator Modal */} + {showGenerator && ( + setShowGenerator(false)} + /> + )} + + {/* Reference Modal */} + {showReference && ( + setShowReference(false)} /> + )} + + {/* Hero Section */} +
+
+
+
+
+ +
+

Cognitive Clarity

+
+ + +
+ +

+ A clinical roleplaying platform utilizing CBT principles and AI to help you navigate social complexities and restructure internal dialogues. +

+ +
+
+ + Evidence-Based Framework +
+
+ + Personalized Adaptive Logic +
+
+
+
+ + {/* Main Content */} +
+
+

+ Training Scenarios +

+ +
+ {/* Filter Controls */} +
+
+ + Difficulty +
+
+ {difficulties.map((diff) => ( + + ))} +
+
+ + +
+
+ +
+ {filteredScenarios.map(scenario => ( + + ))} + + +
+
+ + +
+ ); +}; + +export default App; diff --git a/README.md b/README.md index 2241000..c69c3b1 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,20 @@
- GHBanner - -

Built with AI Studio

- -

The fastest path from prompt to production with Gemini.

- - Start building -
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1MTS72LTsm0ZY5GzQzl-a8Rpt72k1csd9 + +## Run Locally + +**Prerequisites:** Node.js + + +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` diff --git a/components/AudioVisualizer.tsx b/components/AudioVisualizer.tsx new file mode 100644 index 0000000..7109cd0 --- /dev/null +++ b/components/AudioVisualizer.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +interface AudioVisualizerProps { + level: number; + active: boolean; +} + +const AudioVisualizer: React.FC = ({ level, active }) => { + const barCount = 19; + return ( +
+ {Array.from({ length: barCount }).map((_, i) => { + const center = (barCount - 1) / 2; + const dist = Math.abs(i - center); + const multiplier = Math.pow(0.88, dist); + const jitter = 0.8 + (Math.sin(i * 1.5) * 0.2); + const height = active ? Math.max(8, level * multiplier * jitter) : 6; + + return ( +
+ ); + })} +
+ ); +}; + +export default React.memo(AudioVisualizer); \ No newline at end of file diff --git a/components/CBTReference.tsx b/components/CBTReference.tsx new file mode 100644 index 0000000..c35c23c --- /dev/null +++ b/components/CBTReference.tsx @@ -0,0 +1,129 @@ +import React, { useState } from 'react'; +import { CBT_GLOSSARY, GlossaryEntry } from '../constants'; +import { X, Search, BookOpen, Brain, Activity, HelpCircle } from 'lucide-react'; + +interface CBTReferenceProps { + onClose: () => void; +} + +const CBTReference: React.FC = ({ onClose }) => { + const [search, setSearch] = useState(''); + const [activeCategory, setActiveCategory] = useState<'All' | 'Distortion' | 'Technique' | 'Concept'>('All'); + + const filtered = CBT_GLOSSARY.filter(entry => { + const matchesSearch = entry.term.toLowerCase().includes(search.toLowerCase()) || + entry.definition.toLowerCase().includes(search.toLowerCase()); + const matchesCategory = activeCategory === 'All' || entry.category === activeCategory; + return matchesSearch && matchesCategory; + }); + + const categories: ('All' | 'Distortion' | 'Technique' | 'Concept')[] = ['All', 'Distortion', 'Technique', 'Concept']; + + const getCategoryIcon = (cat: string) => { + switch (cat) { + case 'Distortion': return ; + case 'Technique': return ; + case 'Concept': return ; + default: return ; + } + }; + + return ( +
+
+ {/* Header */} +
+
+
+ +
+
+

CBT Clinical Reference

+

Evidence-Based Concepts & Frameworks

+
+
+ +
+ + {/* Filters */} +
+
+ + setSearch(e.target.value)} + className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-4 focus:ring-teal-500/10 focus:border-teal-500 focus:outline-none transition-all text-sm" + /> +
+ +
+ {categories.map(cat => ( + + ))} +
+
+ + {/* Content */} +
+
+ {filtered.map((entry, idx) => ( +
+
+

{entry.term}

+
+ {getCategoryIcon(entry.category)} + {entry.category} +
+
+

+ {entry.definition} +

+ {entry.example && ( +
+ Clinical Example +

"{entry.example}"

+
+ )} +
+ ))} + + {filtered.length === 0 && ( +
+
+ +
+

No concepts matching your search parameters.

+
+ )} +
+
+ + {/* Footer */} +
+

+ References compiled from clinical CBT standard frameworks. +

+
+
+
+ ); +}; + +export default CBTReference; \ No newline at end of file diff --git a/components/ChatInput.tsx b/components/ChatInput.tsx new file mode 100644 index 0000000..5a251e2 --- /dev/null +++ b/components/ChatInput.tsx @@ -0,0 +1,43 @@ + +import React, { useState } from 'react'; +import { Send } from 'lucide-react'; + +interface ChatInputProps { + onSend: (text: string) => void; + disabled?: boolean; +} + +const ChatInput: React.FC = ({ onSend, disabled }) => { + const [input, setInput] = useState(''); + + const handleSend = () => { + if (!input.trim() || disabled) return; + onSend(input); + setInput(''); + }; + + return ( +
+
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSend()} + placeholder="Type your response in character..." + className="flex-grow px-5 py-3 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-teal-500 bg-white text-slate-900 placeholder:text-slate-400 text-sm shadow-inner" + disabled={disabled} + /> + +
+
+ ); +}; + +export default React.memo(ChatInput); diff --git a/components/ChatMessage.tsx b/components/ChatMessage.tsx new file mode 100644 index 0000000..6b6b72b --- /dev/null +++ b/components/ChatMessage.tsx @@ -0,0 +1,40 @@ + +import React from 'react'; +import { User, Bot } from 'lucide-react'; +import { Message } from '../types'; + +interface ChatMessageProps { + message: Message; + isLoading?: boolean; +} + +const ChatMessage: React.FC = ({ message, isLoading }) => { + const isModel = message.role === 'model'; + + return ( +
+
+
+ {!isModel ? : } +
+
+ {message.text || (isModel && isLoading ? ( +
+ + + +
+ ) : null)} +
+
+
+ ); +}; + +export default React.memo(ChatMessage); diff --git a/components/ChatSession.tsx b/components/ChatSession.tsx new file mode 100644 index 0000000..711fabf --- /dev/null +++ b/components/ChatSession.tsx @@ -0,0 +1,244 @@ + +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} + + CRITICAL ROLEPLAY PROTOCOL: + 1. Stay strictly in character as the persona defined in the scenario. + 2. NEVER provide clinical feedback, advice, or meta-commentary about the user's progress. + 3. NEVER use phrases like "Great job holding your boundary" or "Feedback:". + 4. Do not congratulate the user on using CBT techniques; simply react as the character would. + 5. If the user successfully concludes the scenario, end the dialogue naturally. + `; + + 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); diff --git a/components/LiveInsightPanel.tsx b/components/LiveInsightPanel.tsx new file mode 100644 index 0000000..bf5cd56 --- /dev/null +++ b/components/LiveInsightPanel.tsx @@ -0,0 +1,97 @@ + +import React from 'react'; +import { BrainCircuit, Loader2, CheckCircle2, AlertTriangle, Lightbulb, TrendingUp } from 'lucide-react'; +import { AnalysisResult } from '../types'; + +interface LiveInsightPanelProps { + analysis: AnalysisResult | null; + isAnalyzing: boolean; +} + +const LiveInsightPanel: React.FC = ({ analysis, isAnalyzing }) => { + return ( +
+
+
+ +

Live Clinical Insight

+
+ {isAnalyzing && } +
+ +
+ {analysis ? ( +
+
+
+ {analysis.analysis.isAdaptive ? ( + + ) : ( + + )} + + {analysis.analysis.isAdaptive ? 'Optimal Thinking' : 'Distortion Identified'} + +
+ + {!analysis.analysis.isAdaptive && analysis.analysis.distortions.length > 0 && ( +
+ {analysis.analysis.distortions.map((d, i) => ( + + {d} + + ))} +
+ )} + +

+ {analysis.analysis.evidence} +

+
+ +
+
+ Efficiency +
+ {[1,2,3,4,5].map(star => ( +
+ ))} +
+
+

{analysis.rating.reason}

+
+ +
+
+
+ + Master Reframe +
+

"{analysis.suggestions.alternativeThought}"

+
+ +
+
+ + Tactical Pivot +
+

{analysis.suggestions.behavioralStep}

+
+
+
+ ) : ( +
+
+ +
+

+ Monitoring Interaction...
Turn analysis pending +

+
+ )} +
+
+ ); +}; + +export default React.memo(LiveInsightPanel); diff --git a/components/ScenarioCard.tsx b/components/ScenarioCard.tsx new file mode 100644 index 0000000..090d69f --- /dev/null +++ b/components/ScenarioCard.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Scenario } from '../types'; +import { Mic, MessageSquare } from 'lucide-react'; + +interface ScenarioCardProps { + scenario: Scenario; + onSelect: (scenario: Scenario, mode: 'text' | 'voice') => void; +} + +const ScenarioCard: React.FC = ({ scenario, onSelect }) => { + const getDifficultyColor = (diff: string) => { + switch (diff) { + case 'Beginner': return 'bg-teal-50 text-teal-800 border-teal-100'; + case 'Intermediate': return 'bg-amber-50 text-amber-800 border-amber-100'; + case 'Advanced': return 'bg-rose-50 text-rose-800 border-rose-100'; + default: return 'bg-slate-50 text-slate-800 border-slate-100'; + } + }; + + return ( +
+
+ + {scenario.difficulty} + + + {scenario.category} + +
+ +

{scenario.title}

+

{scenario.description}

+ +
+
+ {scenario.cbtFocus.map(focus => ( + + {focus} + + ))} +
+
+ +
+ + +
+
+ ); +}; + +export default ScenarioCard; \ No newline at end of file diff --git a/components/ScenarioGenerator.tsx b/components/ScenarioGenerator.tsx new file mode 100644 index 0000000..3778273 --- /dev/null +++ b/components/ScenarioGenerator.tsx @@ -0,0 +1,232 @@ +import React, { useState } from 'react'; +import { GoogleGenAI, Type } from "@google/genai"; +import { Scenario } from '../types'; +import { CBT_GLOSSARY } from '../constants'; +import { Sparkles, X, Loader2, AlertCircle, Info, Target, UserCircle2, BrainCircuit, Check } from 'lucide-react'; + +interface ScenarioGeneratorProps { + onGenerate: (scenario: Scenario) => void; + onCancel: () => void; +} + +const ScenarioGenerator: React.FC = ({ onGenerate, onCancel }) => { + const [prompt, setPrompt] = useState(''); + const [selectedConcepts, setSelectedConcepts] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const toggleConcept = (term: string) => { + setSelectedConcepts(prev => + prev.includes(term) ? prev.filter(t => t !== term) : [...prev, term] + ); + }; + + const handleGenerate = async () => { + if (!prompt.trim() && selectedConcepts.length === 0) return; + + setIsLoading(true); + setError(null); + + try { + const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); + + const schema = { + type: Type.OBJECT, + properties: { + id: { type: Type.STRING }, + title: { 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', '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: ` + You are a Clinical CBT Scenario Architect. + Your goal is to transform a user's prompt into a structured clinical simulation. + + CORE INSTRUCTIONS: + 1. CLINICAL FOCUS: ${selectedContext} + 2. CATEGORIZATION: You MUST assign the scenario to exactly one of the following categories based on its primary context: + - 'Social Anxiety': Situations involving social performance, fear of judgment, or networking. + - 'Workplace': Scenarios involving colleagues, supervisors, career growth, or professional conflict. + - 'Relationships': Dynamics with family, friends, or romantic partners; boundary setting. + - 'Self-Talk': Purely internal cognitive restructuring, rumination, or self-compassion work. + 3. DIFFICULTY CALIBRATION: Assign a level based on cognitive load: + - 'Beginner': Low stakes, high support, clear-cut distortions. + - 'Intermediate': Ambiguous stakes, moderate social friction, subtle distortions. + - 'Advanced': High emotional triggers, persistent clinical resistance from AI character, deep-seated patterns. + 4. INTERNAL LOGIC: The 'systemInstruction' must include a detailed "INTERNAL CBT LOGIC" section. This tells the AI how to react to the user's progress or cognitive lapses. + 5. CONSISTENCY: Ensure 'cbtFocus' includes the selected concepts. + ` + }, + 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; + scenario.id = `custom-${Date.now()}-${scenario.id}`; + onGenerate(scenario); + } else { + throw new Error("No content generated"); + } + + } catch (err: any) { + console.error("Generation error:", err); + setError("Clinical scenario synthesis failed. Please refine your parameters."); + } finally { + setIsLoading(false); + } + }; + + const getConceptColor = (category: string, isSelected: boolean) => { + if (!isSelected) return 'bg-white border-slate-200 text-slate-500 hover:border-slate-300'; + switch (category) { + case 'Distortion': return 'bg-rose-600 border-rose-600 text-white shadow-md shadow-rose-100'; + case 'Technique': return 'bg-teal-600 border-teal-600 text-white shadow-md shadow-teal-100'; + case 'Concept': return 'bg-amber-600 border-amber-600 text-white shadow-md shadow-amber-100'; + default: return 'bg-slate-800 border-slate-800 text-white'; + } + }; + + return ( +
+
+ {/* Header */} +
+
+

+ + Scenario Synthesis +

+

+ Construct an evidence-based clinical environment. +

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

Clinical Blueprint

+
+
+
+
+ Character Role +
+

Define who the AI plays.

+
+
+
+ The Challenge +
+

Describe the social friction.

+
+
+
+ CBT Focus +
+

Target specific distortions.

+
+
+
+ + {/* Clinical Targets (Selection) */} +
+ +
+ {CBT_GLOSSARY.map(concept => { + const isSelected = selectedConcepts.includes(concept.term); + return ( + + ); + })} +
+
+ +
+ +