Introduces a new `UserSettings` interface and default settings. This allows users to customize voice preferences (name, style, gender) and persona characteristics within the application. This enhances personalization and user experience by tailoring interactions to individual needs and preferences.
263 lines
12 KiB
TypeScript
263 lines
12 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
|
import { SCENARIOS } from './constants';
|
|
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 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<Scenario | null>(null);
|
|
const [sessionMode, setSessionMode] = useState<SessionMode>('text');
|
|
const [selectedDifficulty, setSelectedDifficulty] = useState<string>('All');
|
|
const [isPreparing, setIsPreparing] = useState(false);
|
|
|
|
// Custom Scenarios State
|
|
const [customScenarios, setCustomScenarios] = useState<Scenario[]>([]);
|
|
const [showGenerator, setShowGenerator] = useState(false);
|
|
const [showReference, setShowReference] = useState(false);
|
|
const [showSettings, setShowSettings] = useState(false);
|
|
|
|
// User Settings State
|
|
const [userSettings, setUserSettings] = useState<UserSettings>(() => {
|
|
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);
|
|
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'];
|
|
|
|
if (isPreparing) {
|
|
return (
|
|
<div className="min-h-screen bg-slate-900 flex flex-col items-center justify-center p-6 text-white overflow-hidden relative">
|
|
<div className="absolute inset-0 overflow-hidden pointer-events-none opacity-20">
|
|
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-teal-500 rounded-full blur-[120px] animate-pulse"></div>
|
|
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-blue-500 rounded-full blur-[120px] animate-pulse" style={{ animationDelay: '1s' }}></div>
|
|
</div>
|
|
|
|
<div className="relative z-10 flex flex-col items-center max-w-md text-center">
|
|
<div className="w-24 h-24 bg-white/5 rounded-3xl border border-white/10 flex items-center justify-center mb-8 relative">
|
|
<Brain size={48} className="text-teal-400" />
|
|
<div className="absolute inset-0 border-2 border-teal-500/30 rounded-3xl animate-ping opacity-20"></div>
|
|
</div>
|
|
|
|
<h2 className="text-2xl font-bold mb-3 tracking-tight">Initializing Session</h2>
|
|
<div className="flex items-center gap-2 mb-8">
|
|
<span className="h-1.5 w-1.5 bg-teal-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></span>
|
|
<span className="h-1.5 w-1.5 bg-teal-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></span>
|
|
<span className="h-1.5 w-1.5 bg-teal-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></span>
|
|
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest ml-2">Calibrating AI Parameters</p>
|
|
</div>
|
|
|
|
<div className="w-full bg-white/5 h-1.5 rounded-full overflow-hidden mb-4 border border-white/5">
|
|
<div className="h-full bg-teal-500 rounded-full w-0 animate-[loading_1.4s_ease-in-out_forwards]"></div>
|
|
</div>
|
|
|
|
<div className="space-y-2 opacity-60">
|
|
<p className="text-[10px] uppercase font-black tracking-tighter flex items-center justify-center gap-2">
|
|
<Zap size={10} className="text-teal-400" />
|
|
Configuring {sessionMode === 'voice' ? 'Voice Link' : 'Neural Chat Bridge'}
|
|
</p>
|
|
<p className="text-[10px] uppercase font-black tracking-tighter flex items-center justify-center gap-2">
|
|
<Activity size={10} className="text-blue-400" />
|
|
Applying {userSettings.voiceName} Vocal Profile
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<style>{`
|
|
@keyframes loading {
|
|
0% { width: 0%; }
|
|
20% { width: 15%; }
|
|
50% { width: 70%; }
|
|
100% { width: 100%; }
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (activeScenario) {
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 flex flex-col items-center pt-8 px-4 pb-8 animate-in fade-in duration-700">
|
|
{sessionMode === 'text' ? (
|
|
<ChatSession scenario={activeScenario} onExit={handleExitSession} />
|
|
) : (
|
|
<div className="w-full max-w-4xl h-[80vh]">
|
|
<VoiceSession
|
|
scenario={activeScenario}
|
|
onExit={handleExitSession}
|
|
voiceSettings={userSettings}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#FDFDFF] text-slate-900 font-sans relative animate-in fade-in duration-500">
|
|
{/* Modals */}
|
|
{showGenerator && <ScenarioGenerator onGenerate={handleScenarioGenerated} onCancel={() => setShowGenerator(false)} />}
|
|
{showReference && <CBTReference onClose={() => setShowReference(false)} />}
|
|
{showSettings && <SettingsModal settings={userSettings} onUpdate={setUserSettings} onClose={() => setShowSettings(false)} />}
|
|
|
|
{/* Hero Section */}
|
|
<div className="bg-white border-b border-slate-200">
|
|
<div className="max-w-5xl mx-auto px-6 py-12">
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
|
|
<div className="flex items-center gap-3">
|
|
<div className="bg-teal-600 p-2 rounded-lg text-white shadow-sm shadow-teal-200">
|
|
<Brain size={32} />
|
|
</div>
|
|
<h1 className="text-3xl font-bold tracking-tight text-slate-900">Cognitive Clarity</h1>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Tooltip content="Adjust voice settings for audio sessions.">
|
|
<button
|
|
onClick={() => setShowSettings(true)}
|
|
className="p-2.5 rounded-xl bg-slate-50 border border-slate-200 text-slate-400 hover:text-slate-800 transition-all"
|
|
>
|
|
<Settings size={20} />
|
|
</button>
|
|
</Tooltip>
|
|
<Tooltip content="Explore the glossary of CBT distortions, techniques, and concepts.">
|
|
<button
|
|
onClick={() => setShowReference(true)}
|
|
className="flex items-center justify-center gap-2 bg-slate-50 text-slate-600 hover:text-teal-700 hover:bg-teal-50 px-5 py-2.5 rounded-xl border border-slate-200 transition-all font-bold text-sm"
|
|
>
|
|
<BookOpen size={18} />
|
|
<span>Reference Guide</span>
|
|
</button>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-xl text-slate-600 max-w-2xl leading-relaxed font-light">
|
|
An adaptive clinical training environment designed to strengthen cognitive flexibility and navigate social friction.
|
|
</p>
|
|
|
|
<div className="flex flex-wrap gap-4 mt-8">
|
|
<Tooltip content="Our scenarios are built on the foundations of Cognitive Behavioral Therapy.">
|
|
<div className="flex items-center gap-2 text-sm text-slate-500 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-100 cursor-help">
|
|
<ShieldCheck className="text-teal-600" size={18} />
|
|
<span>Evidence-Based Framework</span>
|
|
</div>
|
|
</Tooltip>
|
|
<Tooltip content="The AI character responds dynamically to your level of cognitive restructuring.">
|
|
<div className="flex items-center gap-2 text-sm text-slate-500 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-100 cursor-help">
|
|
<Info className="text-teal-600" size={18} />
|
|
<span>Vocal Partner: {userSettings.voiceName}</span>
|
|
</div>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<main className="max-w-5xl mx-auto px-6 py-12">
|
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-10">
|
|
<h2 className="text-2xl font-medium text-slate-800">Training Scenarios</h2>
|
|
<div className="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
|
|
<div className="flex items-center bg-white border border-slate-200 rounded-lg p-1 shadow-sm overflow-x-auto">
|
|
<div className="px-3 text-slate-400 flex items-center gap-2 text-sm font-medium border-r border-slate-100 mr-1">
|
|
<Filter size={14} />
|
|
<span className="hidden sm:inline">Difficulty</span>
|
|
</div>
|
|
<div className="flex gap-1">
|
|
{difficulties.map((diff) => (
|
|
<button
|
|
key={diff}
|
|
onClick={() => setSelectedDifficulty(diff)}
|
|
className={`px-3 py-1.5 text-xs rounded-md transition-all whitespace-nowrap ${
|
|
selectedDifficulty === diff
|
|
? 'bg-teal-50 text-teal-700 font-semibold border border-teal-100'
|
|
: 'text-slate-500 hover:bg-slate-50 border border-transparent'
|
|
}`}
|
|
>
|
|
{diff}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<Tooltip content="Generate a unique roleplay situation based on your specific needs using AI." position="bottom">
|
|
<button
|
|
onClick={() => setShowGenerator(true)}
|
|
className="flex items-center justify-center gap-2 bg-teal-600 text-white px-5 py-2.5 rounded-lg hover:bg-teal-700 transition-all font-bold text-sm shadow-md shadow-teal-100"
|
|
>
|
|
<Plus size={18} />
|
|
<span>Create Custom</span>
|
|
</button>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{filteredScenarios.map(scenario => (
|
|
<ScenarioCard key={scenario.id} scenario={scenario} onSelect={handleSelectScenario} />
|
|
))}
|
|
<button
|
|
onClick={() => setShowGenerator(true)}
|
|
className="flex flex-col items-center justify-center gap-4 p-6 rounded-xl border-2 border-dashed border-slate-200 text-slate-400 hover:border-teal-300 hover:text-teal-600 hover:bg-teal-50/50 transition-all h-full min-h-[250px] group"
|
|
>
|
|
<div className="bg-slate-50 p-4 rounded-full group-hover:bg-white transition-colors"><Plus size={32} /></div>
|
|
<div className="text-center">
|
|
<span className="block font-bold text-sm uppercase tracking-widest mb-1">Custom Scenario</span>
|
|
<span className="text-xs font-light">Generate a specific roleplay session</span>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</main>
|
|
|
|
<footer className="bg-white border-t border-slate-200 py-12">
|
|
<div className="max-w-5xl mx-auto px-6 flex flex-col md:flex-row justify-between items-center gap-8">
|
|
<div className="flex items-center gap-2 text-slate-400">
|
|
<Brain size={20} /><span className="text-sm font-bold uppercase tracking-widest">Cognitive Clarity v1.2</span>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|