feat: Editable safety settings in SettingsModal

Backend:
- New POST /api/image-gen/config endpoint to update image generation config
  in-memory (applies immediately, resets on server restart)
- Accepts: model, safety_harassment, safety_hate_speech,
  safety_sexually_explicit, safety_dangerous

Frontend:
- SettingsModal safety filters are now editable dropdowns (no longer disabled)
- Model selection is also editable
- Added 'Save Changes' button with visual feedback:
  * Green 'Settings saved!' on success
  * Red 'Failed to save' on error
  * Disabled when no changes made
- Tracks edited state vs. saved state
- Note: changes apply immediately but reset on server restart
  (set .env for persistence)

Fixed TypeScript issues:
- getConfigValue() properly typed to return string only
- Proper union type for config keys excluding has_api_key
This commit is contained in:
rin.kagamine committed 2026-05-29 22:15:34 -04:00
1 parent 6410e16912
commit a79b2806dc
2 files changed
+81 -14

No files matched your search

+64 -14
View File
@@ -35,14 +35,19 @@ const SAFETY_OPTIONS = [
export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDeleteAll, clipCount }: Props) {
const [confirmDelete, setConfirmDelete] = useState(false)
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [imageGenModels, setImageGenModels] = useState<ImageGenModel[]>([])
const [imageGenConfig, setImageGenConfig] = useState<ImageGenConfig | null>(null)
const [editedConfig, setEditedConfig] = useState<Partial<ImageGenConfig>>({})
const [showImageGenSettings, setShowImageGenSettings] = useState(false)
const [saveMessage, setSaveMessage] = useState('')
useEffect(() => {
if (isOpen) {
fetchImageGenModels()
fetchImageGenConfig()
setEditedConfig({})
setSaveMessage('')
}
}, [isOpen])
@@ -95,6 +100,42 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
}
}
const handleSaveImageGenConfig = async () => {
if (!editedConfig || Object.keys(editedConfig).length === 0) return
setSaving(true)
try {
const res = await fetch('/api/image-gen/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(editedConfig),
})
if (res.ok) {
setSaveMessage('Settings saved!')
setImageGenConfig(prev => prev ? { ...prev, ...editedConfig } : prev)
setEditedConfig({})
setTimeout(() => setSaveMessage(''), 2000)
} else {
setSaveMessage('Failed to save')
}
} catch (e) {
setSaveMessage('Failed to save')
} finally {
setSaving(false)
}
}
const updateEditedConfig = (key: string, value: string) => {
setEditedConfig(prev => ({ ...prev, [key]: value }))
setSaveMessage('')
}
const getConfigValue = (key: 'model' | 'safety_harassment' | 'safety_hate_speech' | 'safety_sexually_explicit' | 'safety_dangerous'): string => {
const val = editedConfig[key] ?? imageGenConfig?.[key]
return typeof val === 'string' ? val : 'BLOCK_ONLY_HIGH'
}
const hasChanges = Object.keys(editedConfig).length > 0
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
@@ -150,12 +191,8 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
<label className="block text-xs text-lyria-muted mb-1.5">Model</label>
<select
className="w-full bg-black/30 border border-white/10 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:border-lyria-accent"
value={imageGenConfig?.model || 'gemini-3.1-flash-image-preview'}
onChange={(e) => {
// This would need a backend endpoint to update config
// For now, it's read-only from .env
console.log('Model selection changed:', e.target.value)
}}
value={getConfigValue('model')}
onChange={(e) => updateEditedConfig('model', e.target.value)}
>
{imageGenModels.map((model) => (
<option key={model.id} value={model.id}>
@@ -163,9 +200,6 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
</option>
))}
</select>
<p className="text-xs text-lyria-muted/60 mt-1">
Configure via IMAGE_GEN_MODEL in .env
</p>
</div>
{/* API Key Status */}
@@ -189,8 +223,8 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
<label className="block text-xs text-lyria-muted mb-1">{label}</label>
<select
className="w-full bg-black/30 border border-white/10 rounded-md px-3 py-1.5 text-sm text-white focus:outline-none focus:border-lyria-accent"
value={imageGenConfig?.[key as keyof ImageGenConfig] as string || 'BLOCK_ONLY_HIGH'}
disabled
value={getConfigValue(key as 'safety_harassment' | 'safety_hate_speech' | 'safety_sexually_explicit' | 'safety_dangerous')}
onChange={(e) => updateEditedConfig(key, e.target.value)}
>
{SAFETY_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
@@ -198,10 +232,26 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
</select>
</div>
))}
<p className="text-xs text-lyria-muted/60">
Configure via IMAGE_GEN_SAFETY_* in .env
</p>
</div>
{/* Save Button */}
<div className="flex items-center gap-3 pt-2">
<button
onClick={handleSaveImageGenConfig}
disabled={!hasChanges || saving}
className="px-4 py-2 text-sm font-medium rounded-md transition-colors disabled:opacity-50 bg-lyria-accent text-black hover:bg-lyria-accent/90"
>
{saving ? 'Saving...' : 'Save Changes'}
</button>
{saveMessage && (
<span className={`text-xs ${saveMessage.includes('Failed') ? 'text-red-400' : 'text-green-400'}`}>
{saveMessage}
</span>
)}
</div>
<p className="text-xs text-lyria-muted/60">
Changes apply immediately but reset on server restart. Set .env for persistence.
</p>
</div>
)}
</div>