Introduces `characterName` to the `Scenario` type, enabling AI personas to be explicitly named. This change also updates the `ScenarioCard` component to display the character name and adds foundational support for potential future scenario deletion functionality. The README has been updated with a new AI Studio link.
136 lines
6.4 KiB
TypeScript
136 lines
6.4 KiB
TypeScript
|
|
import React from 'react';
|
|
import { UserSettings } from '../types';
|
|
import { X, Volume2, Check, Info, Mic2, Settings2 } from 'lucide-react';
|
|
import Tooltip from './Tooltip';
|
|
import AudioDeviceSelector from './AudioDeviceSelector';
|
|
|
|
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<SettingsModalProps> = ({ settings, onUpdate, onClose }) => {
|
|
return (
|
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-md">
|
|
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-xl overflow-hidden animate-in fade-in zoom-in duration-300 border border-slate-100 max-h-[90vh] flex flex-col">
|
|
<div className="p-6 bg-slate-50 border-b border-slate-200 flex justify-between items-center flex-shrink-0">
|
|
<div className="flex items-center gap-3">
|
|
<div className="bg-slate-800 p-2 rounded-xl text-white">
|
|
<Volume2 size={20} />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-lg font-bold text-slate-800">Voice & Persona Settings</h2>
|
|
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest">Configure your AI clinical partner</p>
|
|
</div>
|
|
</div>
|
|
<button onClick={onClose} className="p-2 hover:bg-slate-200 rounded-xl transition-colors text-slate-400">
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-8 space-y-8 overflow-y-auto">
|
|
<section className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-xs font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
|
|
<Mic2 size={14} className="text-teal-600" />
|
|
Vocal Profile
|
|
</h3>
|
|
<Tooltip content="Different voices can help simulate specific social or professional contexts.">
|
|
<Info size={14} className="text-slate-300" />
|
|
</Tooltip>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-3">
|
|
{VOICE_OPTIONS.map((voice) => (
|
|
<button
|
|
key={voice.id}
|
|
onClick={() => onUpdate({ ...settings, voiceName: voice.id, genderPreference: voice.gender, vocalStyle: voice.style })}
|
|
className={`flex items-center justify-between p-4 rounded-2xl border-2 transition-all text-left ${
|
|
settings.voiceName === voice.id
|
|
? 'bg-teal-50 border-teal-500 shadow-md shadow-teal-100'
|
|
: 'bg-white border-slate-100 hover:border-slate-200'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
|
settings.voiceName === voice.id ? 'bg-teal-600 text-white' : 'bg-slate-100 text-slate-400'
|
|
}`}>
|
|
<Volume2 size={18} />
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-bold text-slate-800">{voice.label}</span>
|
|
<span className="text-[9px] font-black uppercase text-slate-400 bg-slate-100 px-1.5 py-0.5 rounded">
|
|
{voice.gender} • {voice.style}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-slate-500 font-light mt-0.5">{voice.description}</p>
|
|
</div>
|
|
</div>
|
|
{settings.voiceName === voice.id && (
|
|
<div className="bg-teal-600 text-white p-1 rounded-full">
|
|
<Check size={14} />
|
|
</div>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-xs font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
|
|
<Settings2 size={14} className="text-teal-600" />
|
|
Hardware Configuration
|
|
</h3>
|
|
</div>
|
|
|
|
<AudioDeviceSelector
|
|
selectedDeviceId={settings.preferredAudioDeviceId}
|
|
onSelect={(deviceId) => onUpdate({ ...settings, preferredAudioDeviceId: deviceId })}
|
|
/>
|
|
</section>
|
|
|
|
<div className="bg-amber-50 border border-amber-100 rounded-2xl p-4 flex gap-3">
|
|
<div className="text-amber-600 mt-0.5">
|
|
<Info size={18} />
|
|
</div>
|
|
<p className="text-xs text-amber-800 leading-relaxed">
|
|
These settings apply to <strong>Voice Mode</strong> interactions only. Text-based roleplay personas are dynamically generated based on each specific scenario.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6 bg-slate-50 border-t border-slate-200 flex justify-end flex-shrink-0">
|
|
<button
|
|
onClick={onClose}
|
|
className="bg-slate-800 text-white px-8 py-3 rounded-2xl font-bold hover:bg-slate-900 shadow-lg shadow-slate-200 transition-all uppercase tracking-widest text-[10px]"
|
|
>
|
|
Save Preferences
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SettingsModal;
|