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:
1 parent
6410e16912
commit
a79b2806dc
2 files changed
+81
-14
No files matched your search
@@ -298,6 +298,23 @@ async def get_image_gen_configuration():
|
|||||||
"has_api_key": bool(config.get("api_key")),
|
"has_api_key": bool(config.get("api_key")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@app.post("/api/image-gen/config")
|
||||||
|
async def update_image_gen_configuration(request: dict):
|
||||||
|
"""Update image generation configuration (in-memory only, not persisted to .env)."""
|
||||||
|
# Update environment variables in-memory for this process
|
||||||
|
if "model" in request:
|
||||||
|
os.environ["IMAGE_GEN_MODEL"] = request["model"]
|
||||||
|
if "safety_harassment" in request:
|
||||||
|
os.environ["IMAGE_GEN_SAFETY_HARASSMENT"] = request["safety_harassment"]
|
||||||
|
if "safety_hate_speech" in request:
|
||||||
|
os.environ["IMAGE_GEN_SAFETY_HATE_SPEECH"] = request["safety_hate_speech"]
|
||||||
|
if "safety_sexually_explicit" in request:
|
||||||
|
os.environ["IMAGE_GEN_SAFETY_SEXUALLY_EXPLICIT"] = request["safety_sexually_explicit"]
|
||||||
|
if "safety_dangerous" in request:
|
||||||
|
os.environ["IMAGE_GEN_SAFETY_DANGEROUS"] = request["safety_dangerous"]
|
||||||
|
|
||||||
|
return {"status": "updated"}
|
||||||
|
|
||||||
@app.patch("/api/clips/{clip_id}")
|
@app.patch("/api/clips/{clip_id}")
|
||||||
async def rename_clip_endpoint(clip_id: str, request: RenameRequest):
|
async def rename_clip_endpoint(clip_id: str, request: RenameRequest):
|
||||||
clip = await get_clip(clip_id)
|
clip = await get_clip(clip_id)
|
||||||
|
|||||||
@@ -35,14 +35,19 @@ const SAFETY_OPTIONS = [
|
|||||||
export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDeleteAll, clipCount }: Props) {
|
export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDeleteAll, clipCount }: Props) {
|
||||||
const [confirmDelete, setConfirmDelete] = useState(false)
|
const [confirmDelete, setConfirmDelete] = useState(false)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
const [imageGenModels, setImageGenModels] = useState<ImageGenModel[]>([])
|
const [imageGenModels, setImageGenModels] = useState<ImageGenModel[]>([])
|
||||||
const [imageGenConfig, setImageGenConfig] = useState<ImageGenConfig | null>(null)
|
const [imageGenConfig, setImageGenConfig] = useState<ImageGenConfig | null>(null)
|
||||||
|
const [editedConfig, setEditedConfig] = useState<Partial<ImageGenConfig>>({})
|
||||||
const [showImageGenSettings, setShowImageGenSettings] = useState(false)
|
const [showImageGenSettings, setShowImageGenSettings] = useState(false)
|
||||||
|
const [saveMessage, setSaveMessage] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
fetchImageGenModels()
|
fetchImageGenModels()
|
||||||
fetchImageGenConfig()
|
fetchImageGenConfig()
|
||||||
|
setEditedConfig({})
|
||||||
|
setSaveMessage('')
|
||||||
}
|
}
|
||||||
}, [isOpen])
|
}, [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 (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
{/* Backdrop */}
|
{/* 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>
|
<label className="block text-xs text-lyria-muted mb-1.5">Model</label>
|
||||||
<select
|
<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"
|
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'}
|
value={getConfigValue('model')}
|
||||||
onChange={(e) => {
|
onChange={(e) => updateEditedConfig('model', e.target.value)}
|
||||||
// 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)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{imageGenModels.map((model) => (
|
{imageGenModels.map((model) => (
|
||||||
<option key={model.id} value={model.id}>
|
<option key={model.id} value={model.id}>
|
||||||
@@ -163,9 +200,6 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<p className="text-xs text-lyria-muted/60 mt-1">
|
|
||||||
Configure via IMAGE_GEN_MODEL in .env
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* API Key Status */}
|
{/* 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>
|
<label className="block text-xs text-lyria-muted mb-1">{label}</label>
|
||||||
<select
|
<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"
|
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'}
|
value={getConfigValue(key as 'safety_harassment' | 'safety_hate_speech' | 'safety_sexually_explicit' | 'safety_dangerous')}
|
||||||
disabled
|
onChange={(e) => updateEditedConfig(key, e.target.value)}
|
||||||
>
|
>
|
||||||
{SAFETY_OPTIONS.map((opt) => (
|
{SAFETY_OPTIONS.map((opt) => (
|
||||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||||
@@ -198,10 +232,26 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<p className="text-xs text-lyria-muted/60">
|
|
||||||
Configure via IMAGE_GEN_SAFETY_* in .env
|
|
||||||
</p>
|
|
||||||
</div>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user