830 lines
38 KiB
TypeScript
830 lines
38 KiB
TypeScript
import { X, Key, Zap, Palette, User, Volume2, Moon, Sun, Monitor, Bell, Minimize2 } from 'lucide-react'
|
||
import { useSettingsStore } from '../stores/settingsStore'
|
||
import { getAllCharacters, getCharacter } from '../lib/characters'
|
||
import { getElevenLabsClient, Voice } from '../lib/elevenlabs'
|
||
import { useEffect, useState } from 'react'
|
||
|
||
interface SettingsPanelProps {
|
||
onClose: () => void
|
||
}
|
||
|
||
export function SettingsPanel({ onClose }: SettingsPanelProps) {
|
||
const {
|
||
openrouterApiKey,
|
||
elevenLabsApiKey,
|
||
temperature,
|
||
maxTokens,
|
||
currentCharacter,
|
||
customSystemPrompt,
|
||
voiceEnabled,
|
||
ttsVoice,
|
||
ttsModel,
|
||
ttsSpeed,
|
||
ttsStability,
|
||
ttsSimilarityBoost,
|
||
ttsConversationMode,
|
||
sttLanguage,
|
||
sttMode,
|
||
theme,
|
||
notificationsEnabled,
|
||
minimizeToTray,
|
||
setOpenRouterApiKey,
|
||
setElevenLabsApiKey,
|
||
setTemperature,
|
||
setMaxTokens,
|
||
setCurrentCharacter,
|
||
setCustomSystemPrompt,
|
||
setVoiceEnabled,
|
||
setTtsVoice,
|
||
setTtsModel,
|
||
setTtsSpeed,
|
||
setTtsStability,
|
||
setTtsSimilarityBoost,
|
||
setTtsConversationMode,
|
||
setSttLanguage,
|
||
setSttMode,
|
||
setTheme,
|
||
setNotificationsEnabled,
|
||
setMinimizeToTray,
|
||
} = useSettingsStore()
|
||
|
||
const [browserVoices, setBrowserVoices] = useState<SpeechSynthesisVoice[]>([])
|
||
const [elevenLabsVoices, setElevenLabsVoices] = useState<Voice[]>([])
|
||
const [loadingVoices, setLoadingVoices] = useState(false)
|
||
const [voiceError, setVoiceError] = useState<string | null>(null)
|
||
|
||
const characters = getAllCharacters()
|
||
const selectedCharacter = getCharacter(currentCharacter)
|
||
|
||
// Debug: Log current settings on mount
|
||
useEffect(() => {
|
||
console.log('⚙️ SettingsPanel mounted')
|
||
console.log('📥 Current ttsVoice from store:', ttsVoice)
|
||
console.log('🔑 OpenRouter API Key exists:', !!openrouterApiKey, 'Length:', openrouterApiKey?.length || 0)
|
||
console.log('🔑 ElevenLabs API Key exists:', !!elevenLabsApiKey, 'Length:', elevenLabsApiKey?.length || 0)
|
||
console.log('💾 LocalStorage contents:', localStorage.getItem('eve-settings'))
|
||
}, [openrouterApiKey, elevenLabsApiKey, ttsVoice])
|
||
|
||
// Load browser voices
|
||
useEffect(() => {
|
||
// Check if speechSynthesis is available (may not be in Tauri WebView)
|
||
if (typeof window === 'undefined' || !window.speechSynthesis) {
|
||
console.warn('⚠️ Speech Synthesis API not available in this environment')
|
||
return
|
||
}
|
||
|
||
const loadVoices = () => {
|
||
try {
|
||
const voices = window.speechSynthesis.getVoices()
|
||
setBrowserVoices(voices)
|
||
console.log(`🔊 Loaded ${voices.length} browser voices`)
|
||
|
||
// Check for duplicate voiceURIs
|
||
const voiceURIs = voices.map(v => v.voiceURI)
|
||
const duplicates = voiceURIs.filter((uri, index) => voiceURIs.indexOf(uri) !== index)
|
||
if (duplicates.length > 0) {
|
||
console.warn('⚠️ Found duplicate voice URIs:', [...new Set(duplicates)])
|
||
}
|
||
} catch (error) {
|
||
console.warn('⚠️ Failed to load browser voices:', error)
|
||
}
|
||
}
|
||
|
||
loadVoices()
|
||
|
||
try {
|
||
window.speechSynthesis.addEventListener('voiceschanged', loadVoices)
|
||
} catch (error) {
|
||
console.warn('⚠️ Failed to add voiceschanged listener:', error)
|
||
}
|
||
|
||
return () => {
|
||
try {
|
||
if (window.speechSynthesis) {
|
||
window.speechSynthesis.removeEventListener('voiceschanged', loadVoices)
|
||
}
|
||
} catch (error) {
|
||
// Ignore cleanup errors
|
||
}
|
||
}
|
||
}, [])
|
||
|
||
// Load ElevenLabs voices when API key is configured
|
||
useEffect(() => {
|
||
const loadElevenLabsVoices = async () => {
|
||
if (!elevenLabsApiKey) {
|
||
setElevenLabsVoices([])
|
||
return
|
||
}
|
||
|
||
setLoadingVoices(true)
|
||
setVoiceError(null)
|
||
|
||
try {
|
||
const client = getElevenLabsClient(elevenLabsApiKey)
|
||
const voices = await client.getVoices()
|
||
console.log('🎵 ElevenLabs voices loaded:', voices.length)
|
||
console.log('🎵 Sample voice:', voices[0])
|
||
console.log('🎵 All voice IDs:', voices.map(v => `${v.name}: ${v.voice_id}`))
|
||
setElevenLabsVoices(voices)
|
||
} catch (error) {
|
||
console.error('Failed to load ElevenLabs voices:', error)
|
||
setVoiceError('Failed to load ElevenLabs voices. Check your API key.')
|
||
setElevenLabsVoices([])
|
||
} finally {
|
||
setLoadingVoices(false)
|
||
}
|
||
}
|
||
|
||
loadElevenLabsVoices()
|
||
}, [elevenLabsApiKey])
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
|
||
<h2 className="text-2xl font-bold text-gray-800 dark:text-white">Settings</h2>
|
||
<button
|
||
onClick={onClose}
|
||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="p-6 overflow-y-auto max-h-[calc(90vh-80px)]">
|
||
{/* API Keys Section */}
|
||
<section className="mb-8">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<Key className="w-5 h-5 text-blue-500" />
|
||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">API Keys</h3>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
OpenRouter API Key
|
||
<a
|
||
href="https://openrouter.ai/keys"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="ml-2 text-blue-500 hover:underline text-xs"
|
||
>
|
||
Get key →
|
||
</a>
|
||
</label>
|
||
{openrouterApiKey ? (
|
||
<div className="px-4 py-2 bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 rounded-lg text-sm">
|
||
Key loaded from environment.
|
||
</div>
|
||
) : (
|
||
<input
|
||
type="password"
|
||
value={openrouterApiKey}
|
||
onChange={(e) => setOpenRouterApiKey(e.target.value)}
|
||
placeholder="sk-or-v1-..."
|
||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||
bg-white dark:bg-gray-700 text-gray-800 dark:text-white
|
||
focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
)}
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
Required for AI chat functionality
|
||
</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
ElevenLabs API Key (Optional)
|
||
<a
|
||
href="https://elevenlabs.io"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="ml-2 text-blue-500 hover:underline text-xs"
|
||
>
|
||
Get key →
|
||
</a>
|
||
</label>
|
||
{elevenLabsApiKey ? (
|
||
<div className="px-4 py-2 bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 rounded-lg text-sm">
|
||
Key loaded from environment.
|
||
</div>
|
||
) : (
|
||
<input
|
||
type="password"
|
||
value={elevenLabsApiKey}
|
||
onChange={(e) => setElevenLabsApiKey(e.target.value)}
|
||
placeholder="..."
|
||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||
bg-white dark:bg-gray-700 text-gray-800 dark:text-white
|
||
focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
)}
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
For text-to-speech features
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Appearance Section */}
|
||
<section className="mb-8">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<Palette className="w-5 h-5 text-indigo-500" />
|
||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
|
||
Appearance
|
||
</h3>
|
||
</div>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||
Theme
|
||
</label>
|
||
<div className="grid grid-cols-3 gap-3">
|
||
<button
|
||
onClick={() => setTheme('light')}
|
||
className={`flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-all ${
|
||
theme === 'light'
|
||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
|
||
}`}
|
||
>
|
||
<Sun className={`w-6 h-6 ${theme === 'light' ? 'text-blue-500' : 'text-gray-600 dark:text-gray-400'}`} />
|
||
<span className={`text-sm font-medium ${theme === 'light' ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300'}`}>
|
||
Light
|
||
</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setTheme('dark')}
|
||
className={`flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-all ${
|
||
theme === 'dark'
|
||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
|
||
}`}
|
||
>
|
||
<Moon className={`w-6 h-6 ${theme === 'dark' ? 'text-blue-500' : 'text-gray-600 dark:text-gray-400'}`} />
|
||
<span className={`text-sm font-medium ${theme === 'dark' ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300'}`}>
|
||
Dark
|
||
</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setTheme('system')}
|
||
className={`flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-all ${
|
||
theme === 'system'
|
||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
|
||
}`}
|
||
>
|
||
<Monitor className={`w-6 h-6 ${theme === 'system' ? 'text-blue-500' : 'text-gray-600 dark:text-gray-400'}`} />
|
||
<span className={`text-sm font-medium ${theme === 'system' ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300'}`}>
|
||
System
|
||
</span>
|
||
</button>
|
||
</div>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||
Choose your preferred color theme. System follows your OS settings.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Character/Personality Section */}
|
||
<section className="mb-8">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<User className="w-5 h-5 text-purple-500" />
|
||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
|
||
Character & Personality
|
||
</h3>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
Character Preset
|
||
</label>
|
||
<select
|
||
value={currentCharacter}
|
||
onChange={(e) => setCurrentCharacter(e.target.value)}
|
||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||
bg-white dark:bg-gray-700 text-gray-800 dark:text-white
|
||
focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||
>
|
||
{characters.map((char) => (
|
||
<option key={char.id} value={char.id}>
|
||
{char.name} - {char.description}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
|
||
<h4 className="font-medium text-sm text-gray-800 dark:text-white mb-2">
|
||
Current Personality
|
||
</h4>
|
||
<p className="text-xs text-gray-600 dark:text-gray-300 whitespace-pre-wrap">
|
||
{selectedCharacter.systemPrompt.slice(0, 200)}...
|
||
</p>
|
||
</div>
|
||
|
||
{currentCharacter === 'custom' && (
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
Custom System Prompt
|
||
</label>
|
||
<textarea
|
||
value={customSystemPrompt}
|
||
onChange={(e) => setCustomSystemPrompt(e.target.value)}
|
||
placeholder="Enter your custom system prompt for EVE..."
|
||
rows={6}
|
||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||
bg-white dark:bg-gray-700 text-gray-800 dark:text-white
|
||
focus:outline-none focus:ring-2 focus:ring-purple-500 resize-none"
|
||
/>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
Define how EVE should behave and respond
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Voice Settings Section */}
|
||
<section className="mb-8">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<Volume2 className="w-5 h-5 text-green-500" />
|
||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
|
||
Voice Settings
|
||
</h3>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div className="flex items-center gap-2">
|
||
<input
|
||
type="checkbox"
|
||
id="voiceEnabled"
|
||
checked={voiceEnabled}
|
||
onChange={(e) => setVoiceEnabled(e.target.checked)}
|
||
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
<label htmlFor="voiceEnabled" className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
Enable text-to-speech for assistant messages
|
||
</label>
|
||
</div>
|
||
|
||
{voiceEnabled && (
|
||
<div className="flex items-center gap-2 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||
<input
|
||
type="checkbox"
|
||
id="ttsConversationMode"
|
||
checked={ttsConversationMode}
|
||
onChange={(e) => setTtsConversationMode(e.target.checked)}
|
||
className="w-4 h-4 text-purple-600 rounded focus:ring-2 focus:ring-purple-500"
|
||
/>
|
||
<label htmlFor="ttsConversationMode" className="text-sm font-medium text-gray-700 dark:text-gray-300 flex-1">
|
||
🎧 Audio Conversation Mode
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||
Auto-play responses in audio, hide text by default (can toggle)
|
||
</p>
|
||
</label>
|
||
</div>
|
||
)}
|
||
|
||
{voiceEnabled && (
|
||
<>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
TTS Voice Selection
|
||
{loadingVoices && (
|
||
<span className="ml-2 text-xs text-blue-400 animate-pulse">Loading voices...</span>
|
||
)}
|
||
</label>
|
||
<select
|
||
value={ttsVoice}
|
||
onChange={(e) => {
|
||
const selectedValue = e.target.value
|
||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||
console.log('🎛️ Settings: Voice dropdown changed')
|
||
console.log('📥 Selected value:', selectedValue)
|
||
console.log('🔍 Value breakdown:', {
|
||
hasPrefix: selectedValue.includes(':'),
|
||
prefix: selectedValue.split(':')[0],
|
||
voiceId: selectedValue.split(':')[1],
|
||
})
|
||
|
||
setTtsVoice(selectedValue)
|
||
|
||
// Verify it's saved to localStorage
|
||
setTimeout(() => {
|
||
const stored = localStorage.getItem('eve-settings')
|
||
const parsed = stored ? JSON.parse(stored) : null
|
||
console.log('💾 LocalStorage ttsVoice:', parsed?.state?.ttsVoice)
|
||
console.log('💾 Full LocalStorage:', stored)
|
||
}, 100)
|
||
}}
|
||
disabled={loadingVoices}
|
||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||
bg-white dark:bg-gray-700 text-gray-800 dark:text-white
|
||
focus:outline-none focus:ring-2 focus:ring-green-500
|
||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
<option value="default">Default Voice</option>
|
||
|
||
{elevenLabsVoices.length > 0 && (
|
||
<optgroup label="ElevenLabs Voices (Premium)">
|
||
{elevenLabsVoices.map((voice, index) => {
|
||
const optionValue = `elevenlabs:${voice.voice_id}`
|
||
|
||
if (index === 0) {
|
||
console.log('📋 Sample ElevenLabs dropdown option:', {
|
||
name: voice.name,
|
||
voice_id: voice.voice_id,
|
||
optionValue: optionValue
|
||
})
|
||
}
|
||
|
||
if (!voice.voice_id) {
|
||
console.error('❌ Voice missing voice_id:', voice)
|
||
}
|
||
|
||
return (
|
||
<option key={`elevenlabs-${index}-${voice.voice_id}`} value={optionValue}>
|
||
{voice.name}
|
||
{voice.labels?.accent && ` - ${voice.labels.accent}`}
|
||
{voice.labels?.age && ` (${voice.labels.age})`}
|
||
</option>
|
||
)
|
||
})}
|
||
</optgroup>
|
||
)}
|
||
|
||
<optgroup label="Browser Voices (Free)">
|
||
{browserVoices.map((voice, index) => (
|
||
<option key={`browser-${index}-${voice.voiceURI}`} value={`browser:${voice.voiceURI}`}>
|
||
{voice.name} ({voice.lang})
|
||
</option>
|
||
))}
|
||
</optgroup>
|
||
</select>
|
||
|
||
{voiceError && (
|
||
<p className="text-xs text-red-400 mt-1">{voiceError}</p>
|
||
)}
|
||
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
{ttsVoice === 'default' && 'Using system default voice'}
|
||
{ttsVoice.startsWith('browser:') && 'Using browser voice'}
|
||
{ttsVoice.startsWith('elevenlabs:') && 'Using ElevenLabs voice'}
|
||
{' • '}
|
||
{elevenLabsVoices.length > 0
|
||
? `${elevenLabsVoices.length} ElevenLabs voices available`
|
||
: elevenLabsApiKey
|
||
? 'Loading ElevenLabs voices...'
|
||
: 'Add ElevenLabs API key above to access premium voices'}
|
||
</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
STT Language
|
||
</label>
|
||
<select
|
||
value={sttLanguage}
|
||
onChange={(e) => setSttLanguage(e.target.value)}
|
||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||
bg-white dark:bg-gray-700 text-gray-800 dark:text-white
|
||
focus:outline-none focus:ring-2 focus:ring-green-500"
|
||
>
|
||
<option value="en-US">English (US)</option>
|
||
<option value="en-GB">English (UK)</option>
|
||
<option value="en-AU">English (Australia)</option>
|
||
<option value="en-CA">English (Canada)</option>
|
||
<option value="es-ES">Spanish (Spain)</option>
|
||
<option value="es-MX">Spanish (Mexico)</option>
|
||
<option value="fr-FR">French (France)</option>
|
||
<option value="fr-CA">French (Canada)</option>
|
||
<option value="de-DE">German</option>
|
||
<option value="it-IT">Italian</option>
|
||
<option value="pt-BR">Portuguese (Brazil)</option>
|
||
<option value="pt-PT">Portuguese (Portugal)</option>
|
||
<option value="ru-RU">Russian</option>
|
||
<option value="ja-JP">Japanese</option>
|
||
<option value="ko-KR">Korean</option>
|
||
<option value="zh-CN">Chinese (Simplified)</option>
|
||
<option value="zh-TW">Chinese (Traditional)</option>
|
||
<option value="ar-SA">Arabic</option>
|
||
<option value="hi-IN">Hindi</option>
|
||
<option value="nl-NL">Dutch</option>
|
||
<option value="pl-PL">Polish</option>
|
||
<option value="tr-TR">Turkish</option>
|
||
<option value="sv-SE">Swedish</option>
|
||
<option value="da-DK">Danish</option>
|
||
<option value="fi-FI">Finnish</option>
|
||
<option value="no-NO">Norwegian</option>
|
||
</select>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
Language for speech recognition
|
||
</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
STT Input Mode
|
||
</label>
|
||
<select
|
||
value={sttMode}
|
||
onChange={(e) => setSttMode(e.target.value as 'push-to-talk' | 'continuous')}
|
||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||
bg-white dark:bg-gray-700 text-gray-800 dark:text-white
|
||
focus:outline-none focus:ring-2 focus:ring-green-500"
|
||
>
|
||
<option value="push-to-talk">Push to Talk</option>
|
||
<option value="continuous">Continuous Listening</option>
|
||
</select>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
Push to talk: Click to start/stop. Continuous: Always listening until stopped.
|
||
</p>
|
||
</div>
|
||
|
||
{/* TTS Model Selection */}
|
||
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||
ElevenLabs Model
|
||
</h4>
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
TTS Model
|
||
{!ttsVoice.startsWith('elevenlabs:') && (
|
||
<span className="ml-2 text-xs text-gray-400">(ElevenLabs only)</span>
|
||
)}
|
||
</label>
|
||
<select
|
||
value={ttsModel}
|
||
onChange={(e) => setTtsModel(e.target.value)}
|
||
disabled={!ttsVoice.startsWith('elevenlabs:')}
|
||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||
bg-white dark:bg-gray-700 text-gray-800 dark:text-white
|
||
focus:outline-none focus:ring-2 focus:ring-green-500
|
||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
<optgroup label="Real-Time Models (Recommended)">
|
||
<option value="eleven_turbo_v2_5">Turbo v2.5 - Best Balance</option>
|
||
<option value="eleven_flash_v2_5">Flash v2.5 - Fastest (~75ms)</option>
|
||
<option value="eleven_multilingual_v2">Multilingual v2 - High Quality</option>
|
||
</optgroup>
|
||
<optgroup label="High Quality Models (Slower)">
|
||
<option value="eleven_turbo_v2">Turbo v2 - Legacy</option>
|
||
<option value="eleven_flash_v2">Flash v2 - Legacy</option>
|
||
<option value="eleven_monolingual_v1">Monolingual v1 - English Only</option>
|
||
</optgroup>
|
||
<optgroup label="Alpha Models (Not for Real-Time)">
|
||
<option value="eleven_v3">V3 Alpha - Highest Quality ⚠️ Slow</option>
|
||
</optgroup>
|
||
</select>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
{ttsModel === 'eleven_v3' && '⚠️ V3 not optimized for real-time conversation'}
|
||
{ttsModel === 'eleven_turbo_v2_5' && '⭐ Recommended for conversational AI'}
|
||
{ttsModel === 'eleven_flash_v2_5' && '⚡ Ultra-low latency for instant responses'}
|
||
{ttsModel === 'eleven_multilingual_v2' && '🌍 Best quality for multiple languages'}
|
||
{!['eleven_v3', 'eleven_turbo_v2_5', 'eleven_flash_v2_5', 'eleven_multilingual_v2'].includes(ttsModel) && 'Legacy model - consider upgrading'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* TTS Quality Controls */}
|
||
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||
Voice Quality Settings
|
||
</h4>
|
||
|
||
{/* Speed Control */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
Speed: {ttsSpeed.toFixed(2)}x
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min="0.25"
|
||
max="4"
|
||
step="0.25"
|
||
value={ttsSpeed}
|
||
onChange={(e) => setTtsSpeed(parseFloat(e.target.value))}
|
||
className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-green-500"
|
||
/>
|
||
<div className="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
<span>0.25x (Slow)</span>
|
||
<span>1.0x (Normal)</span>
|
||
<span>4.0x (Fast)</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Stability Control (ElevenLabs) */}
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
Stability: {(ttsStability * 100).toFixed(0)}%
|
||
{!ttsVoice.startsWith('elevenlabs:') && (
|
||
<span className="ml-2 text-xs text-gray-400">(ElevenLabs only)</span>
|
||
)}
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="1"
|
||
step="0.05"
|
||
value={ttsStability}
|
||
onChange={(e) => setTtsStability(parseFloat(e.target.value))}
|
||
disabled={!ttsVoice.startsWith('elevenlabs:')}
|
||
className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-green-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
/>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
Higher = more consistent, Lower = more expressive
|
||
</p>
|
||
</div>
|
||
|
||
{/* Similarity Boost Control (ElevenLabs) */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
Clarity: {(ttsSimilarityBoost * 100).toFixed(0)}%
|
||
{!ttsVoice.startsWith('elevenlabs:') && (
|
||
<span className="ml-2 text-xs text-gray-400">(ElevenLabs only)</span>
|
||
)}
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="1"
|
||
step="0.05"
|
||
value={ttsSimilarityBoost}
|
||
onChange={(e) => setTtsSimilarityBoost(parseFloat(e.target.value))}
|
||
disabled={!ttsVoice.startsWith('elevenlabs:')}
|
||
className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-green-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||
/>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
Higher = more similar to original voice, enhances clarity
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Model Settings Section */}
|
||
<section className="mb-8">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<Zap className="w-5 h-5 text-purple-500" />
|
||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
|
||
Model Parameters
|
||
</h3>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
Temperature: {temperature.toFixed(2)}
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="2"
|
||
step="0.1"
|
||
value={temperature}
|
||
onChange={(e) => setTemperature(parseFloat(e.target.value))}
|
||
className="w-full"
|
||
/>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
Lower = more focused, Higher = more creative
|
||
</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
Max Tokens
|
||
</label>
|
||
<input
|
||
type="number"
|
||
value={maxTokens}
|
||
onChange={(e) => setMaxTokens(parseInt(e.target.value))}
|
||
min="100"
|
||
max="4096"
|
||
step="100"
|
||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg
|
||
bg-white dark:bg-gray-700 text-gray-800 dark:text-white
|
||
focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
Maximum length of responses
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* System Integration Section */}
|
||
<section className="mb-8">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<Bell className="w-5 h-5 text-green-500" />
|
||
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
|
||
System Integration
|
||
</h3>
|
||
</div>
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2">
|
||
<Bell className="w-4 h-4 text-green-500" />
|
||
<label className="font-medium text-gray-800 dark:text-white">
|
||
Desktop Notifications
|
||
</label>
|
||
</div>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||
Get notified when EVE responds (even when window is hidden)
|
||
</p>
|
||
</div>
|
||
<input
|
||
type="checkbox"
|
||
checked={notificationsEnabled}
|
||
onChange={(e) => setNotificationsEnabled(e.target.checked)}
|
||
className="w-5 h-5 text-green-500 rounded focus:ring-2 focus:ring-green-500"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg opacity-50">
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2">
|
||
<Minimize2 className="w-4 h-4 text-gray-500" />
|
||
<label className="font-medium text-gray-800 dark:text-white">
|
||
Minimize to Tray
|
||
</label>
|
||
<span className="text-xs bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 px-2 py-0.5 rounded">
|
||
Linux: Coming Soon
|
||
</span>
|
||
</div>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||
System tray temporarily unavailable on Linux (icon format issue)
|
||
</p>
|
||
</div>
|
||
<input
|
||
type="checkbox"
|
||
checked={minimizeToTray}
|
||
onChange={(e) => setMinimizeToTray(e.target.checked)}
|
||
disabled
|
||
className="w-5 h-5 text-gray-400 rounded cursor-not-allowed"
|
||
/>
|
||
</div>
|
||
|
||
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800">
|
||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||
<strong className="text-yellow-700 dark:text-yellow-400">⌨️ Global Shortcut:</strong> Press{' '}
|
||
<kbd className="px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded text-xs font-mono">
|
||
Ctrl+Shift+E
|
||
</kbd>{' '}
|
||
(or{' '}
|
||
<kbd className="px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded text-xs font-mono">
|
||
Cmd+Shift+E
|
||
</kbd>{' '}
|
||
on Mac) to quickly show/hide EVE.
|
||
<br />
|
||
<span className="text-xs text-yellow-600 dark:text-yellow-400 mt-1 inline-block">
|
||
Note: May not work on some Linux desktop environments due to permission restrictions.
|
||
</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Info Section */}
|
||
<section className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
|
||
<div className="flex items-start gap-2">
|
||
<Palette className="w-5 h-5 text-blue-500 mt-0.5" />
|
||
<div>
|
||
<h4 className="font-semibold text-gray-800 dark:text-white mb-1">
|
||
About OpenRouter
|
||
</h4>
|
||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||
OpenRouter provides unified access to multiple AI models including GPT-4, Claude,
|
||
Gemini, Llama, and more. You only need one API key to access all models.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
{/* Footer */}
|
||
<div className="p-6 border-t border-gray-200 dark:border-gray-700 flex justify-end">
|
||
<button
|
||
onClick={onClose}
|
||
className="px-6 py-2 bg-gradient-to-r from-blue-500 to-indigo-600
|
||
text-white rounded-lg hover:from-blue-600 hover:to-indigo-700
|
||
transition font-medium"
|
||
>
|
||
Save & Close
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|