'use client'; import { useState, useCallback, useMemo, useEffect } from 'react'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { useI18n } from '@/lib/hooks/use-i18n'; import { useSettingsStore } from '@/lib/store/settings'; import { IMAGE_PROVIDERS } from '@/lib/media/image-providers'; import { Loader2, CheckCircle2, XCircle, Eye, EyeOff, Zap, Plus, Settings2, Trash2, RefreshCw, } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { ImageProviderId } from '@/lib/media/types'; interface ImageSettingsProps { selectedProviderId: ImageProviderId; } interface WorkflowEntry { id: string; name: string; } export function ImageSettings({ selectedProviderId }: ImageSettingsProps) { const { t } = useI18n(); const imageModelId = useSettingsStore((state) => state.imageModelId); const imageProvidersConfig = useSettingsStore((state) => state.imageProvidersConfig); const _setImageModelId = useSettingsStore((state) => state.setImageModelId); const setImageProvider = useSettingsStore((state) => state.setImageProvider); const setImageProviderConfig = useSettingsStore((state) => state.setImageProviderConfig); const [showApiKey, setShowApiKey] = useState(false); const [testLoading, setTestLoading] = useState(false); const [testStatus, setTestStatus] = useState<'idle' | 'success' | 'error'>('idle'); const [testMessage, setTestMessage] = useState(''); // Model dialog state const [showModelDialog, setShowModelDialog] = useState(false); const [editingModelIndex, setEditingModelIndex] = useState(null); const [modelForm, setModelForm] = useState({ id: '', name: '' }); // ComfyUI workflow list state const [workflows, setWorkflows] = useState([]); const [workflowsLoading, setWorkflowsLoading] = useState(false); const [workflowsError, setWorkflowsError] = useState(null); const isComfyUI = selectedProviderId === 'comfyui-image'; // Reset test state when provider changes (derived state pattern) const [prevSelectedProviderId, setPrevSelectedProviderId] = useState(selectedProviderId); if (selectedProviderId !== prevSelectedProviderId) { setPrevSelectedProviderId(selectedProviderId); setTestStatus('idle'); setTestMessage(''); } // Fetch ComfyUI workflows when the provider is selected const fetchWorkflows = useCallback(async () => { setWorkflowsLoading(true); setWorkflowsError(null); try { const res = await fetch('/api/comfyui-workflows'); const data = await res.json(); setWorkflows(data.workflows || []); } catch (err) { setWorkflowsError(t('settings.comfyuiLoadError').replace('{error}', String(err))); setWorkflows([]); } finally { setWorkflowsLoading(false); } }, [t]); useEffect(() => { if (isComfyUI) { fetchWorkflows(); } }, [isComfyUI, fetchWorkflows]); const currentConfig = imageProvidersConfig[selectedProviderId]; const currentProvider = IMAGE_PROVIDERS[selectedProviderId]; const builtInModels = currentProvider?.models || []; const customModels = useMemo( () => currentConfig?.customModels || [], [currentConfig?.customModels], ); const isServerConfigured = !!currentConfig?.isServerConfigured; const requiresApiKey = currentProvider?.requiresApiKey ?? true; const handleApiKeyChange = (apiKey: string) => { setImageProviderConfig(selectedProviderId, { apiKey }); }; const handleBaseUrlChange = (baseUrl: string) => { setImageProviderConfig(selectedProviderId, { baseUrl }); }; const handleTest = async () => { setTestLoading(true); setTestStatus('idle'); setTestMessage(''); try { const response = await fetch('/api/verify-image-provider', { method: 'POST', headers: { 'x-image-provider': selectedProviderId, 'x-image-model': imageModelId || '', 'x-api-key': currentConfig?.apiKey || '', 'x-base-url': currentConfig?.baseUrl || '', }, }); const data = await response.json(); if (data.success) { setTestStatus('success'); setTestMessage(t('settings.imageConnectivitySuccess')); } else { setTestStatus('error'); setTestMessage(`${t('settings.imageConnectivityFailed')}: ${data.message}`); } } catch (err) { setTestStatus('error'); setTestMessage(`${t('settings.imageConnectivityFailed')}: ${err}`); } finally { setTestLoading(false); } }; // Model CRUD const handleOpenAddModel = () => { setEditingModelIndex(null); setModelForm({ id: '', name: '' }); setShowModelDialog(true); }; const handleOpenEditModel = (index: number) => { setEditingModelIndex(index); setModelForm({ ...customModels[index] }); setShowModelDialog(true); }; const handleSaveModel = useCallback(() => { if (!modelForm.id.trim()) return; const newCustomModels = [...customModels]; if (editingModelIndex !== null) { newCustomModels[editingModelIndex] = { id: modelForm.id.trim(), name: modelForm.name.trim() || modelForm.id.trim(), }; } else { newCustomModels.push({ id: modelForm.id.trim(), name: modelForm.name.trim() || modelForm.id.trim(), }); } setImageProviderConfig(selectedProviderId, { customModels: newCustomModels, }); setShowModelDialog(false); }, [modelForm, editingModelIndex, customModels, selectedProviderId, setImageProviderConfig]); const handleDeleteModel = (index: number) => { const newCustomModels = customModels.filter((_, i) => i !== index); setImageProviderConfig(selectedProviderId, { customModels: newCustomModels, }); }; return (
{/* Server-configured notice */} {isServerConfigured && (
{t('settings.serverConfiguredNotice')}
)} {/* Managed providers are admin-owned: the operator's key and base URL are authoritative and not overridable here, so the editing inputs are hidden (server ignores client-sent overrides for these). */} {!isServerConfigured && ( <> {/* API Key + Test inline */}
handleApiKeyChange(e.target.value)} className="h-8 pr-8" />
{testMessage && (
{testStatus === 'success' && } {testStatus === 'error' && }

{testMessage}

)}
{/* Base URL */}
handleBaseUrlChange(e.target.value)} placeholder={ currentConfig?.baseUrl || currentProvider?.defaultBaseUrl || t('settings.enterCustomBaseUrl') } className="h-8" /> {(() => { const effectiveBaseUrl = currentConfig?.baseUrl || currentProvider?.defaultBaseUrl || ''; if (!effectiveBaseUrl) return null; return (

{t('settings.requestUrl')}: {effectiveBaseUrl}

); })()}
)} {/* ── ComfyUI: Workflow list ── */} {isComfyUI ? (
{workflowsError && (
{workflowsError}
)} {!workflowsLoading && !workflowsError && workflows.length === 0 && (
{t('settings.comfyuiNoWorkflowsFoundPrefix')}{' '} public/.
{t('settings.comfyuiAddWorkflowPrefix')}{' '} comfyui-*.json{' '} {t('settings.comfyuiAddWorkflowSuffix')}
)}
{workflows.map((workflow) => (
{ // Selecting a workflow here must also make ComfyUI the // active image provider — otherwise the workflow filename // gets written into imageModelId while a different // provider (e.g. Seedream) stays active, and that // provider's next generation call sends this filename as // its model id. Mirrors the atomic setImageProvider + // setImageModelId pairing used in media-popover.tsx. setImageProvider(selectedProviderId); _setImageModelId(workflow.id); }} >
{workflow.name}
{workflow.id}
{imageModelId === workflow.id && ( )}
))}

{t('settings.comfyuiFolderHintPrefix')} public/{' '} {t('settings.comfyuiFolderHintMiddle')}{' '} comfyui-anime-style.json → "Anime Style".

) : ( /* ── All other providers: standard model list ── */
{/* Built-in models */} {builtInModels.map((model) => (
{model.name}
{model.id}
))} {/* Custom models */} {customModels.map((model, index) => (
{model.name}
{model.id}
))}
)} {/* Add/Edit Model Dialog — only used for non-ComfyUI providers */} {editingModelIndex !== null ? t('settings.editModel') : t('settings.addNewModel')} {editingModelIndex !== null ? t('settings.editModel') : t('settings.addNewModel')}
setModelForm((prev) => ({ ...prev, id: e.target.value }))} placeholder="e.g. my-custom-model-v1" className="h-8 font-mono text-sm" />
setModelForm((prev) => ({ ...prev, name: e.target.value }))} placeholder="e.g. My Custom Model" className="h-8 text-sm" />
); }