Initial commit

This commit is contained in:
Aodhan Collins
2025-10-06 00:33:04 +01:00
commit 66749a5ce7
71 changed files with 22041 additions and 0 deletions

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