'use client'; import { Fragment, useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Listbox, ListboxButton, ListboxOptions, ListboxOption, Button, Input, } from '@headlessui/react'; import Link from 'next/link'; import { useTheme } from '@/contexts/ThemeContext'; import { useConfig } from '@/contexts/ConfigContext'; import { ChevronUpDownIcon, CheckIcon, SettingsIcon, KeyIcon, PaletteIcon, DocumentIcon, UserIcon, DownloadIcon, ChevronRightIcon } from '@/components/icons/Icons'; import { getAppConfig, getFirstVisit, setFirstVisit } from '@/lib/client/dexie'; import { useDocuments } from '@/contexts/DocumentContext'; import { ConfirmDialog } from '@/components/ConfirmDialog'; import { ProgressPopup } from '@/components/ProgressPopup'; import { useTimeEstimation } from '@/hooks/useTimeEstimation'; import { THEMES, getCustomThemeColors, type CustomThemeColors } from '@/contexts/ThemeContext'; import { ColorPicker } from '@/components/ColorPicker'; import { DocumentSelectionModal } from '@/components/documents/DocumentSelectionModal'; import { BaseDocument } from '@/types/documents'; import { getAuthClient } from '@/lib/client/auth-client'; import { useAuthSession } from '@/hooks/useAuthSession'; import { useAuthConfig } from '@/contexts/AuthRateLimitContext'; import { useRouter } from 'next/navigation'; import { showPrivacyModal } from '@/components/PrivacyModal'; import { deleteDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client/api/documents'; import { postChangelogVersionCheck } from '@/lib/client/api/user-state'; import { cacheStoredDocumentFromBytes, clearDocumentCache } from '@/lib/client/cache/documents'; import { clearAllDocumentPreviewCaches, clearInMemoryDocumentPreviewCache } from '@/lib/client/cache/previews'; import { resolveTtsSettingsViewModel } from '@/lib/client/settings/tts-settings'; import { isBuiltInTtsProviderId, type TtsProviderType, } from '@/lib/shared/tts-provider-catalog'; import { defaultBaseUrlForProviderType, defaultModelForProviderType, resolveProviderDefaults, resolveEffectiveProviderType, resolveTtsProviderModelPolicy, } from '@/lib/shared/tts-provider-policy'; import { useRuntimeConfig } from '@/contexts/RuntimeConfigContext'; import { AdminProvidersPanel } from '@/components/admin/AdminProvidersPanel'; import { AdminFeaturesPanel } from '@/components/admin/AdminFeaturesPanel'; import { useSharedProviders } from '@/hooks/useSharedProviders'; import { useLibraryDocumentsQuery } from '@/hooks/useLibraryDocumentsQuery'; import { btnDanger, btnOutline, btnPrimary, btnSecondary, inputClass, listboxButtonClass, listboxOptionClass, listboxOptionsClass, segmentedButtonClass, segmentedGroupClass, } from '@/components/formPrimitives'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { fetchChangelogManifest, fetchChangelogReleaseBody } from '@/lib/client/changelog'; import { findCurrentVersionIndex, normalizeVersion, type ChangelogManifestEntry, type ChangelogReleaseBody, } from '@/lib/shared/changelog'; // Hard-coded theme color palettes for the visual theme selector type ThemeColorSet = { background: string; base: string; offbase: string; accent: string; secondaryAccent: string; foreground: string; muted: string }; const THEME_COLORS: Record = { light: { background: '#ffffff', base: '#f7fafc', offbase: '#e2e8f0', accent: '#ef4444', secondaryAccent: '#ed6868', foreground: '#2d3748', muted: '#718096' }, dark: { background: '#111111', base: '#171717', offbase: '#343434', accent: '#f87171', secondaryAccent: '#eb6262', foreground: '#ededed', muted: '#a3a3a3' }, ocean: { background: '#020617', base: '#0f172a', offbase: '#1e293b', accent: '#38bdf8', secondaryAccent: '#22d3ee', foreground: '#e2e8f0', muted: '#94a3b8' }, forest: { background: '#0a0f0c', base: '#111a15', offbase: '#1a2820', accent: '#4ade80', secondaryAccent: '#22c55e', foreground: '#d4e8d0', muted: '#7c8f85' }, sunset: { background: '#1a0f0f', base: '#2c1810', offbase: '#3d1f14', accent: '#ff6b6b', secondaryAccent: '#f59e0b', foreground: '#ffe4d6', muted: '#bc8f8f' }, sea: { background: '#0c1922', base: '#102c3d', offbase: '#1a3c52', accent: '#06b6d4', secondaryAccent: '#0ea5e9', foreground: '#e0f2fe', muted: '#7ca7c4' }, mint: { background: '#0f1916', base: '#132d27', offbase: '#1c3d35', accent: '#2dd4bf', secondaryAccent: '#10b981', foreground: '#dcfce7', muted: '#75a99c' }, lavender: { background: '#faf8ff', base: '#f3effb', offbase: '#e4daf0', accent: '#7c3aed', secondaryAccent: '#a78bfa', foreground: '#3b2e5a', muted: '#8e7bab' }, rose: { background: '#fff8f8', base: '#fef1f1', offbase: '#f5dada', accent: '#e11d48', secondaryAccent: '#f472b6', foreground: '#4a2c2c', muted: '#b08a8a' }, sand: { background: '#fdfbf7', base: '#f7f2e8', offbase: '#e8dfc9', accent: '#b45309', secondaryAccent: '#d97706', foreground: '#44392a', muted: '#9a8b74' }, sky: { background: '#f6faff', base: '#edf4fc', offbase: '#d5e3f5', accent: '#2563eb', secondaryAccent: '#3b82f6', foreground: '#1e3a5f', muted: '#6b8db5' }, slate: { background: '#e8ecf0', base: '#dde2e8', offbase: '#c8ced6', accent: '#5b7a9d', secondaryAccent: '#7393b0', foreground: '#2c3440', muted: '#7a8694' }, }; const LIGHT_THEME_IDS = new Set(['light', 'lavender', 'rose', 'sand', 'sky', 'slate']); const allThemes = THEMES.filter(id => id !== 'custom').map(id => ({ id, name: id.charAt(0).toUpperCase() + id.slice(1), })); const systemTheme = allThemes.find(t => t.id === 'system')!; const lightThemes = allThemes.filter(t => LIGHT_THEME_IDS.has(t.id)); const darkThemes = allThemes.filter(t => t.id !== 'system' && !LIGHT_THEME_IDS.has(t.id)); const CUSTOM_COLOR_FIELDS: { key: keyof CustomThemeColors; label: string }[] = [ { key: 'background', label: 'Background' }, { key: 'base', label: 'Base' }, { key: 'offbase', label: 'Off-base' }, { key: 'accent', label: 'Accent' }, { key: 'secondaryAccent', label: 'Accent 2' }, { key: 'foreground', label: 'Foreground' }, { key: 'muted', label: 'Muted' }, ]; type SectionId = 'api' | 'theme' | 'docs' | 'account' | 'admin'; type SidebarSection = { id: SectionId; label: string; icon: React.ComponentType>; authOnly?: boolean; adminOnly?: boolean; }; const SIDEBAR_SECTIONS: SidebarSection[] = [ { id: 'api', label: 'TTS Provider', icon: KeyIcon }, { id: 'theme', label: 'Appearance', icon: PaletteIcon }, { id: 'docs', label: 'Documents', icon: DocumentIcon }, { id: 'account', label: 'Account', icon: UserIcon, authOnly: true }, { id: 'admin', label: 'Admin', icon: SettingsIcon, authOnly: true, adminOnly: true }, ]; type AdminSubTab = 'providers' | 'features'; export function SettingsModal({ className = '' }: { className?: string }) { const runtimeConfig = useRuntimeConfig(); const enableDestructiveDelete = runtimeConfig.enableDestructiveDeleteActions; const showAllProviderModels = runtimeConfig.showAllProviderModels; const enableTTSProvidersTab = runtimeConfig.enableTtsProvidersTab; const restrictUserApiKeys = runtimeConfig.restrictUserApiKeys; const [isOpen, setIsOpen] = useState(false); const [isChangelogOpen, setIsChangelogOpen] = useState(false); const [activeSection, setActiveSection] = useState(enableTTSProvidersTab ? 'api' : 'theme'); const { theme, setTheme, applyCustomColors } = useTheme(); const [customColors, setCustomColors] = useState(getCustomThemeColors); const [isCustomExpanded, setIsCustomExpanded] = useState(false); const { apiKey, baseUrl, providerRef, providerType, ttsModel, ttsInstructions, updateConfig, updateConfigKey } = useConfig(); const { refreshDocuments } = useDocuments(); const [localApiKey, setLocalApiKey] = useState(apiKey); const [localBaseUrl, setLocalBaseUrl] = useState(baseUrl); const [localProviderRef, setLocalProviderRef] = useState(providerRef); const [localProviderType, setLocalProviderType] = useState(providerType); const [modelValue, setModelValue] = useState(ttsModel); const [customModelInput, setCustomModelInput] = useState(''); const [localTTSInstructions, setLocalTTSInstructions] = useState(ttsInstructions); const [isImportingLibrary, setIsImportingLibrary] = useState(false); const [isSelectionModalOpen, setIsSelectionModalOpen] = useState(false); const [selectionModalProps, setSelectionModalProps] = useState<{ title: string; confirmLabel: string; defaultSelected: boolean; }>({ title: '', confirmLabel: '', defaultSelected: false }); const [showProgress, setShowProgress] = useState(false); const [statusMessage, setStatusMessage] = useState(''); const [abortController, setAbortController] = useState(null); const [showDeleteDocsConfirm, setShowDeleteDocsConfirm] = useState(false); const [showDeleteAccountConfirm, setShowDeleteAccountConfirm] = useState(false); const { progress, setProgress, estimatedTimeRemaining } = useTimeEstimation(); const { authEnabled, baseUrl: authBaseUrl } = useAuthConfig(); const { data: session, isPending: isSessionPending } = useAuthSession(); const changelogVersionCheckKeyRef = useRef(null); const changelogVersionCheckInFlightRef = useRef(null); const router = useRouter(); const isBusy = isImportingLibrary; const { documents: libraryDocuments, isLoading: isLibraryDocumentsLoading, errorMessage: libraryDocumentsErrorMessage, prefetch: prefetchLibraryDocuments, } = useLibraryDocumentsQuery(isSelectionModalOpen); const { providers: sharedProviders } = useSharedProviders(); const { providers: ttsProviders, models: ttsModels, supportsCustomModel: supportsCustom, selectedModelId, canSubmit, selectedSharedProvider, selectedProviderRef, selectedProviderType, } = useMemo(() => resolveTtsSettingsViewModel({ providerRef: localProviderRef, providerType: localProviderType, apiKey: localApiKey, modelValue, customModelInput, showAllProviderModels, sharedProviders, allowBuiltInProviders: !restrictUserApiKeys, }), [localProviderRef, localProviderType, localApiKey, modelValue, customModelInput, showAllProviderModels, sharedProviders, restrictUserApiKeys]); const isSharedSelected = Boolean(selectedSharedProvider); const selectedProviderOption = ttsProviders.find((p) => p.id === localProviderRef) ?? ttsProviders[0]; const checkFirstVist = useCallback(async () => { const appConfig = await getAppConfig(); if (authEnabled && !appConfig?.privacyAccepted) { return; } const firstVisit = await getFirstVisit(); if (!firstVisit) { await setFirstVisit(true); setIsOpen(true); } }, [authEnabled, setIsOpen]); useEffect(() => { checkFirstVist().catch((err) => { console.error('First visit check failed:', err); }); }, [checkFirstVist]); useEffect(() => { setLocalApiKey(apiKey); setLocalBaseUrl(baseUrl); setLocalProviderRef(providerRef); setLocalProviderType(providerType); setModelValue(ttsModel); setLocalTTSInstructions(ttsInstructions); }, [apiKey, baseUrl, providerRef, providerType, ttsModel, ttsInstructions]); useEffect(() => { if (!authEnabled) { return; } const onPrivacyAccepted = () => { checkFirstVist().catch((err) => { console.error('First visit check after privacy acceptance failed:', err); }); }; window.addEventListener('openreader:privacyAccepted', onPrivacyAccepted); return () => { window.removeEventListener('openreader:privacyAccepted', onPrivacyAccepted); }; }, [authEnabled, checkFirstVist]); useEffect(() => { let active = true; let timer: ReturnType | null = null; const run = async () => { const sessionUserId = session?.user?.id ?? null; if (authEnabled && (isSessionPending || !sessionUserId)) return; const currentVersion = normalizeVersion(runtimeConfig.appVersion || ''); if (!currentVersion) return; const userKey = sessionUserId ?? 'server-unclaimed'; const checkKey = `${userKey}:${currentVersion}`; if (changelogVersionCheckKeyRef.current === checkKey) return; if (changelogVersionCheckInFlightRef.current === checkKey) return; changelogVersionCheckInFlightRef.current = checkKey; try { for (let attempt = 0; attempt < 2; attempt += 1) { try { const result = await postChangelogVersionCheck(currentVersion); changelogVersionCheckKeyRef.current = checkKey; if (result.shouldOpen && active) { setIsOpen(true); setIsChangelogOpen(true); } return; } catch (error) { if (attempt === 1) throw error; await new Promise((resolve) => setTimeout(resolve, 400)); } } } catch (error) { console.warn('Failed to check changelog version:', error); } finally { if (changelogVersionCheckInFlightRef.current === checkKey) { changelogVersionCheckInFlightRef.current = null; } } }; // In React Strict Mode (dev), effects mount/unmount once before the real mount. // Deferring the network mutation avoids writing lastSeenVersion from the throwaway pass. timer = setTimeout(() => { void run(); }, 120); return () => { active = false; if (timer) clearTimeout(timer); }; }, [authEnabled, isSessionPending, session?.user?.id, runtimeConfig.appVersion]); useEffect(() => { if (!ttsModels.some(m => m.id === modelValue) && modelValue !== '') { setCustomModelInput(modelValue); } else { setCustomModelInput(''); } }, [modelValue, ttsModels]); useEffect(() => { if (selectedProviderOption) return; if (ttsProviders.length === 0) return; const fallback = ttsProviders[0]; setLocalProviderRef(fallback.id); setLocalProviderType(fallback.providerType); if (fallback.shared) { const shared = sharedProviders.find((p) => p.slug === fallback.id); if (shared?.defaultModel) { setModelValue(shared.defaultModel); } setLocalTTSInstructions(shared?.defaultInstructions ?? ''); setLocalApiKey(''); setLocalBaseUrl(''); setCustomModelInput(''); return; } if (isBuiltInTtsProviderId(fallback.providerType)) { setModelValue(defaultModelForProviderType(fallback.providerType)); setLocalBaseUrl(defaultBaseUrlForProviderType(fallback.providerType)); setCustomModelInput(''); } }, [selectedProviderOption, ttsProviders, sharedProviders]); const handleRefresh = async () => { try { clearInMemoryDocumentPreviewCache(); await refreshDocuments(); } catch (error) { console.error('Failed to refresh documents:', error); } }; const handleClearCache = async () => { try { await Promise.all([ clearDocumentCache(), clearAllDocumentPreviewCaches(), ]); } catch (error) { console.error('Failed to clear cache:', error); } }; const handleImportLibrary = async () => { // Start fetching as soon as the user opens the picker so cached data is // often ready before the modal asks for it. void prefetchLibraryDocuments(); setSelectionModalProps({ title: 'Import from Library', confirmLabel: 'Import', defaultSelected: false, }); setIsSelectionModalOpen(true); }; const handleModalConfirm = async (selectedFiles: BaseDocument[]) => { const controller = new AbortController(); setAbortController(controller); setIsSelectionModalOpen(false); try { setShowProgress(true); setProgress(0); setIsImportingLibrary(true); for (let i = 0; i < selectedFiles.length; i++) { if (controller.signal.aborted) break; const doc = selectedFiles[i]; setStatusMessage(`Importing ${i + 1}/${selectedFiles.length}: ${doc.name}`); setProgress((i / Math.max(1, selectedFiles.length)) * 90); const contentResponse = await fetch(`/api/documents/library/content?id=${encodeURIComponent(doc.id)}`, { signal: controller.signal, }); if (!contentResponse.ok) { console.warn(`Failed to download library document: ${doc.name}`); continue; } const bytes = await contentResponse.arrayBuffer(); const file = new File([bytes], doc.name, { type: mimeTypeForDoc(doc), lastModified: doc.lastModified, }); const uploaded = await uploadDocuments([file], { signal: controller.signal }); const stored = uploaded[0]; if (stored) { await cacheStoredDocumentFromBytes(stored, bytes).catch((err) => { console.warn('Failed to cache imported document:', stored.id, err); }); } } if (!controller.signal.aborted) { setProgress(95); await refreshDocuments(); setProgress(100); setStatusMessage('Import complete'); } } catch (error) { if (controller.signal.aborted) { console.log('library import cancelled'); setStatusMessage('Operation cancelled'); } else { console.error('library import failed:', error); setStatusMessage('Import failed. Please try again.'); } } finally { setIsImportingLibrary(false); setShowProgress(false); setProgress(0); setStatusMessage(''); setAbortController(null); } }; const handleDeleteDocs = async () => { try { await deleteDocuments(); await refreshDocuments().catch(() => { }); } catch (error) { console.error('Delete failed:', error); } finally { setShowDeleteDocsConfirm(false); } }; const handleSignOut = async () => { const client = getAuthClient(authBaseUrl); await client.signOut(); router.push('/signin'); }; const handleDeleteAccount = async () => { try { const res = await fetch('/api/account/delete', { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete account'); const client = getAuthClient(authBaseUrl); await client.signOut(); window.location.href = '/signup'; } catch (error) { console.error('Failed to delete account:', error); } setShowDeleteAccountConfirm(false); }; const handleInputChange = (type: 'apiKey' | 'baseUrl', value: string) => { if (type === 'apiKey') { setLocalApiKey(value === '' ? '' : value); } else if (type === 'baseUrl') { setLocalBaseUrl(value === '' ? '' : value); } }; const resetToCurrent = useCallback(() => { setIsOpen(false); setIsChangelogOpen(false); setLocalApiKey(apiKey); setLocalBaseUrl(baseUrl); setLocalProviderRef(providerRef); setLocalProviderType(providerType); setModelValue(ttsModel); setLocalTTSInstructions(ttsInstructions); if (!ttsModels.some(m => m.id === ttsModel) && ttsModel !== '') { setCustomModelInput(ttsModel); } else { setCustomModelInput(''); } }, [apiKey, baseUrl, providerRef, providerType, ttsModel, ttsInstructions, ttsModels]); const [systemIsDark, setSystemIsDark] = useState( typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches ); useEffect(() => { const mq = window.matchMedia('(prefers-color-scheme: dark)'); const handler = (e: MediaQueryListEvent) => setSystemIsDark(e.matches); mq.addEventListener('change', handler); return () => mq.removeEventListener('change', handler); }, []); const getThemeColors = useCallback((id: string): ThemeColorSet => { if (id === 'system') return THEME_COLORS[systemIsDark ? 'dark' : 'light']; if (id === 'custom') { return { background: customColors.background, base: customColors.base, offbase: customColors.offbase, accent: customColors.accent, secondaryAccent: customColors.secondaryAccent, foreground: customColors.foreground, muted: customColors.muted, }; } return THEME_COLORS[id] || THEME_COLORS.light; }, [systemIsDark, customColors]); const isAdmin = Boolean( (session?.user as unknown as { isAdmin?: boolean } | undefined)?.isAdmin, ); const [adminSubTab, setAdminSubTab] = useState('providers'); const visibleSections = useMemo( () => SIDEBAR_SECTIONS.filter((section) => { if (section.id === 'api' && !enableTTSProvidersTab) { return false; } if (section.authOnly && !authEnabled) return false; if (section.adminOnly && !isAdmin) return false; return true; }), [authEnabled, isAdmin, enableTTSProvidersTab] ); useEffect(() => { if (visibleSections.some(section => section.id === activeSection)) { return; } setActiveSection(visibleSections[0]?.id ?? 'theme'); }, [activeSection, visibleSections]); const fieldLabelClass = 'block text-[11px] font-semibold uppercase tracking-wide text-muted'; const sectionShellClass = 'space-y-2 pb-3 border-b border-offbase px-0.5'; const sectionHeadingClass = 'text-sm font-semibold text-foreground'; const accountBtnBase = 'inline-flex items-center justify-center rounded-lg text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 transform transition-transform duration-200 ease-in-out'; const accountBtnPrimary = `${accountBtnBase} bg-accent text-background hover:bg-secondary-accent hover:scale-[1.04]`; const accountBtnOutline = `${accountBtnBase} bg-background border border-offbase text-foreground hover:bg-offbase hover:text-accent hover:scale-[1.04]`; const accountBtnDanger = `${accountBtnBase} bg-red-600 text-white border border-red-700 hover:bg-red-700 hover:scale-[1.02]`; const effectiveProviderType = resolveEffectiveProviderType({ providerRef: selectedProviderRef, providerType: localProviderType, sharedProviders, }); const providerModelPolicy = resolveTtsProviderModelPolicy({ providerRef: selectedProviderRef, providerType: effectiveProviderType, model: modelValue, sharedProviders, }); const shouldShowBaseUrl = !restrictUserApiKeys && !isSharedSelected && providerModelPolicy.isResolvedProviderType && providerModelPolicy.providerType !== 'replicate' && (providerModelPolicy.providerType === 'custom-openai' || !localBaseUrl || localBaseUrl === ''); const shouldShowApiKey = !restrictUserApiKeys && !isSharedSelected; const selectedModel = ttsModels.find(m => m.id === selectedModelId) || ttsModels[0]; const selectedModelVersion = selectedModel?.id?.includes(':') ? selectedModel.id.slice(selectedModel.id.indexOf(':')) : ''; const displayVersion = normalizeVersion(runtimeConfig.appVersion || ''); return ( <>
{/* Header */}
Settings
{authEnabled && ( )}
{isChangelogOpen ? ( setIsChangelogOpen(false)} /> ) : ( <> {/* Mobile: 2x2 grid nav */}
{visibleSections.map((section) => { const Icon = section.icon; return ( ); })}
{/* Desktop: vertical sidebar */} {/* Content */}
{/* API Section */} {activeSection === 'api' && (
{ttsProviders.length === 0 ? (

User API keys are restricted and no shared provider is configured. Ask an admin to add one.

) : ( { const defaults = resolveProviderDefaults({ providerRef: provider.id, providerType: provider.providerType, sharedProviders, }); setLocalProviderRef(provider.id); setLocalProviderType(defaults.providerType); setModelValue(defaults.defaultModel); setLocalTTSInstructions(defaults.defaultInstructions); if (provider.shared) { // Shared admin provider — credentials live on the server. setLocalApiKey(''); setLocalBaseUrl(''); } else if (isBuiltInTtsProviderId(provider.providerType)) { setLocalBaseUrl(defaultBaseUrlForProviderType(provider.providerType)); } setCustomModelInput(''); }} > {selectedProviderOption?.name || 'Select Provider'} {ttsProviders.map((provider) => ( listboxOptionClass(active)} value={provider} > {({ selected }) => ( <> {provider.name} {selected && ( )} )} ))} )}
{restrictUserApiKeys && (

This instance restricts user API keys. TTS runs through admin-configured shared providers only.

)} {shouldShowBaseUrl && (
handleInputChange('baseUrl', e.target.value)} placeholder="Using environment variable" className={inputClass} />
)} {shouldShowApiKey && (
handleInputChange('apiKey', e.target.value)} placeholder="Using environment variable" className={inputClass} />
)} {isSharedSelected && (

This is a shared provider configured by an admin. API key and base URL are managed server-side.

)}
{!showAllProviderModels && (

This instance restricts model selection to each provider's default model.

)}
m.id === selectedModelId) || ttsModels[0]} onChange={(model) => { if (model.id === 'custom') { setModelValue(customModelInput); } else { setModelValue(model.id); setCustomModelInput(''); } }} > {selectedModel ? ( {selectedModel.name} {selectedModelVersion && ( {selectedModelVersion} )} ) : ( Select Model )} {ttsModels.map((model) => ( listboxOptionClass(active)} value={model} > {({ selected }) => ( <> {model.name} {model.id.includes(':') && ( {model.id.slice(model.id.indexOf(':'))} )} {selected && ( )} )} ))} {supportsCustom && selectedModelId === 'custom' && ( { setCustomModelInput(e.target.value); setModelValue(e.target.value); }} placeholder="Enter custom model name" className={inputClass} /> )}
{providerModelPolicy.supportsInstructions && (