Initial commit
This commit is contained in:
729
src/components/SettingsPanel.tsx
Normal file
729
src/components/SettingsPanel.tsx
Normal file
@@ -0,0 +1,729 @@
|
||||
import { X, Key, Zap, Palette, User, Volume2, Moon, Sun, Monitor } 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,
|
||||
setOpenRouterApiKey,
|
||||
setElevenLabsApiKey,
|
||||
setTemperature,
|
||||
setMaxTokens,
|
||||
setCurrentCharacter,
|
||||
setCustomSystemPrompt,
|
||||
setVoiceEnabled,
|
||||
setTtsVoice,
|
||||
setTtsModel,
|
||||
setTtsSpeed,
|
||||
setTtsStability,
|
||||
setTtsSimilarityBoost,
|
||||
setTtsConversationMode,
|
||||
setSttLanguage,
|
||||
setSttMode,
|
||||
setTheme,
|
||||
} = 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('💾 LocalStorage contents:', localStorage.getItem('eve-settings'))
|
||||
}, [])
|
||||
|
||||
// Load browser voices
|
||||
useEffect(() => {
|
||||
const loadVoices = () => {
|
||||
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)])
|
||||
}
|
||||
}
|
||||
|
||||
loadVoices()
|
||||
window.speechSynthesis.addEventListener('voiceschanged', loadVoices)
|
||||
|
||||
return () => {
|
||||
window.speechSynthesis.removeEventListener('voiceschanged', loadVoices)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 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>
|
||||
|
||||
{/* 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user