'use client'; import { Fragment, useState, useEffect, useCallback, useMemo } from 'react'; import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Listbox, ListboxButton, ListboxOptions, ListboxOption, Button, Input, TabGroup, TabList, Tab, TabPanels, TabPanel, } from '@headlessui/react'; import { useTheme } from '@/contexts/ThemeContext'; import { useConfig } from '@/contexts/ConfigContext'; import { ChevronUpDownIcon, CheckIcon, SettingsIcon } from '@/components/icons/Icons'; import { indexedDBService } from '@/utils/indexedDB'; import { useDocuments } from '@/contexts/DocumentContext'; import { setItem, getItem } from '@/utils/indexedDB'; import { ConfirmDialog } from '@/components/ConfirmDialog'; import { ProgressPopup } from '@/components/ProgressPopup'; import { useTimeEstimation } from '@/hooks/useTimeEstimation'; import { THEMES } from '@/contexts/ThemeContext'; const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null; const themes = THEMES.map(id => ({ id, name: id.charAt(0).toUpperCase() + id.slice(1) })); export function SettingsModal() { const [isOpen, setIsOpen] = useState(false); const { theme, setTheme } = useTheme(); const { apiKey, baseUrl, ttsModel, ttsInstructions, updateConfig, updateConfigKey } = useConfig(); const { refreshPDFs, refreshEPUBs, clearPDFs, clearEPUBs } = useDocuments(); const [localApiKey, setLocalApiKey] = useState(apiKey); const [localBaseUrl, setLocalBaseUrl] = useState(baseUrl); const [localTTSModel, setLocalTTSModel] = useState(ttsModel); const [customModel, setCustomModel] = useState(''); const [localTTSInstructions, setLocalTTSInstructions] = useState(ttsInstructions); const [isSyncing, setIsSyncing] = useState(false); const [isLoading, setIsLoading] = useState(false); const [showProgress, setShowProgress] = useState(false); const [statusMessage, setStatusMessage] = useState(''); const [operationType, setOperationType] = useState<'sync' | 'load'>('sync'); const [abortController, setAbortController] = useState(null); const selectedTheme = themes.find(t => t.id === theme) || themes[0]; const [showClearLocalConfirm, setShowClearLocalConfirm] = useState(false); const [showClearServerConfirm, setShowClearServerConfirm] = useState(false); const { progress, setProgress, estimatedTimeRemaining } = useTimeEstimation(); const ttsModels = useMemo(() => [ { id: 'tts-1', name: 'TTS-1' }, { id: 'tts-1-hd', name: '($$) TTS-1-HD' }, { id: 'gpt-4o-mini-tts', name: '($$$) GPT-4o Mini TTS' }, { id: 'kokoro', name: 'Kokoro' }, { id: 'custom', name: 'Custom Model' } ], []); // set firstVisit on initial load const checkFirstVist = useCallback(async () => { if (!isDev) return; const firstVisit = await getItem('firstVisit'); if (firstVisit == null) { await setItem('firstVisit', 'true'); setIsOpen(true); } }, [setIsOpen]); useEffect(() => { checkFirstVist(); setLocalApiKey(apiKey); setLocalBaseUrl(baseUrl); setLocalTTSModel(ttsModel); setLocalTTSInstructions(ttsInstructions); // Set custom model if current model is not in predefined list if (!ttsModels.some(m => m.id === ttsModel) && ttsModel !== '') { setCustomModel(ttsModel); setLocalTTSModel('custom'); } }, [apiKey, baseUrl, ttsModel, ttsModels, ttsInstructions, checkFirstVist]); const handleSync = async () => { const controller = new AbortController(); setAbortController(controller); try { setIsSyncing(true); setShowProgress(true); setProgress(0); setOperationType('sync'); setStatusMessage('Preparing documents...'); await indexedDBService.syncToServer((progress, status) => { if (controller.signal.aborted) return; setProgress(progress); if (status) setStatusMessage(status); }, controller.signal); } catch (error) { if (controller.signal.aborted) { console.log('Sync operation cancelled'); setStatusMessage('Operation cancelled'); } else { console.error('Sync failed:', error); setStatusMessage('Sync failed. Please try again.'); } } finally { setIsSyncing(false); setShowProgress(false); setProgress(0); setStatusMessage(''); setAbortController(null); } }; const handleLoad = async () => { const controller = new AbortController(); setAbortController(controller); try { setIsLoading(true); setShowProgress(true); setProgress(0); setOperationType('load'); setStatusMessage('Downloading documents from server...'); await indexedDBService.loadFromServer((progress, status) => { if (controller.signal.aborted) return; setProgress(progress); if (status) setStatusMessage(status); }, controller.signal); if (controller.signal.aborted) return; setStatusMessage('Refreshing document list...'); await Promise.all([refreshPDFs(), refreshEPUBs()]); } catch (error) { if (controller.signal.aborted) { console.log('Load operation cancelled'); setStatusMessage('Operation cancelled'); } else { console.error('Load failed:', error); setStatusMessage('Load failed. Please try again.'); } } finally { setIsLoading(false); setShowProgress(false); setProgress(0); setStatusMessage(''); setAbortController(null); } }; const handleClearLocal = async () => { await clearPDFs(); await clearEPUBs(); setShowClearLocalConfirm(false); }; const handleClearServer = async () => { try { const response = await fetch('/api/documents', { method: 'DELETE', }); if (!response.ok) { throw new Error('Failed to delete server documents'); } } catch (error) { console.error('Delete failed:', error); } setShowClearServerConfirm(false); }; const handleInputChange = (type: 'apiKey' | 'baseUrl' | 'ttsModel', value: string) => { if (type === 'apiKey') { setLocalApiKey(value === '' ? '' : value); } else if (type === 'baseUrl') { setLocalBaseUrl(value === '' ? '' : value); } else if (type === 'ttsModel') { setLocalTTSModel(value === '' ? 'tts-1' : value); } }; const resetToCurrent = useCallback(() => { setIsOpen(false); setLocalApiKey(apiKey); setLocalBaseUrl(baseUrl); setLocalTTSModel(ttsModel); setLocalTTSInstructions(ttsInstructions); if (!ttsModels.some(m => m.id === ttsModel) && ttsModel !== '') { setCustomModel(ttsModel); setLocalTTSModel('custom'); } }, [apiKey, baseUrl, ttsModel, ttsInstructions, ttsModels]); const tabs = [ { name: 'Appearance', icon: '✨' }, { name: 'API', icon: '🔑' }, { name: 'Documents', icon: '📄' } ]; return ( <>
Settings {tabs.map((tab) => ( `w-full rounded-lg py-1 text-sm font-medium ring-accent/60 ring-offset-2 ring-offset-base ${selected ? 'bg-accent text-white shadow' : 'text-foreground hover:bg-accent/[0.12] hover:text-accent' }` } > {tab.icon} {tab.name} ))}
setTheme(newTheme.id)}> {selectedTheme.name} {themes.map((theme) => ( `relative cursor-pointer select-none py-2 pl-10 pr-4 ${active ? 'bg-accent/10 text-accent' : 'text-foreground' }` } value={theme} > {({ selected }) => ( <> {theme.name} {selected ? ( ) : null} )} ))}
handleInputChange('apiKey', e.target.value)} placeholder="Using environment variable" className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent" />
handleInputChange('baseUrl', e.target.value)} placeholder="Using environment variable" className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent" />
m.id === localTTSModel) || ttsModels[0]} onChange={(model) => { setLocalTTSModel(model.id); if (model.id !== 'custom') { setCustomModel(''); } }} > {ttsModels.find(m => m.id === localTTSModel)?.name || 'Select Model'} {ttsModels.map((model) => ( `relative cursor-pointer select-none py-2 pl-10 pr-4 ${ active ? 'bg-accent/10 text-accent' : 'text-foreground' }` } value={model} > {({ selected }) => ( <> {model.name} {selected ? ( ) : null} )} ))} {localTTSModel === 'custom' && ( setCustomModel(e.target.value)} placeholder="Enter custom model name" className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent" /> )}
{localTTSModel === 'gpt-4o-mini-tts' && (