Files
eve-alpha/src/components/SettingsPanel.tsx
Aodhan Collins 8d6a681baa Phase 2 complete.
2025-10-06 21:08:25 +01:00

830 lines
38 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)
}