import { createFileRoute } from '@tanstack/react-router' import { route } from '@/constants/routes' import SettingsMenu from '@/containers/SettingsMenu' import HeaderPage from '@/containers/HeaderPage' import { Switch } from '@/components/ui/switch' import { Button } from '@/components/ui/button' import { Card, CardItem } from '@/containers/Card' import LanguageSwitcher from '@/containers/LanguageSwitcher' import { useTranslation } from '@/i18n/react-i18next-compat' import { useGeneralSetting } from '@/hooks/useGeneralSetting' import { useAppUpdater } from '@/hooks/useAppUpdater' import { useEffect, useState, useCallback } from 'react' import { open } from '@tauri-apps/plugin-dialog' import { revealItemInDir } from '@tauri-apps/plugin-opener' import ChangeDataFolderLocation from '@/containers/dialogs/ChangeDataFolderLocation' import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { factoryReset, getJanDataFolder, relocateJanDataFolder, } from '@/services/app' import { IconBrandDiscord, IconBrandGithub, IconExternalLink, IconFolder, IconLogs, IconCopy, IconCopyCheck, } from '@tabler/icons-react' import { WebviewWindow } from '@tauri-apps/api/webviewWindow' import { windowKey } from '@/constants/windows' import { toast } from 'sonner' import { isDev } from '@/lib/utils' import { emit } from '@tauri-apps/api/event' import { stopAllModels } from '@/services/models' import { SystemEvent } from '@/types/events' // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute(route.settings.general as any)({ component: General, }) function General() { const { t } = useTranslation() const { spellCheckChatInput, setSpellCheckChatInput, experimentalFeatures, setExperimentalFeatures, } = useGeneralSetting() const openFileTitle = (): string => { if (IS_MACOS) { return t('settings:general.showInFinder') } else if (IS_WINDOWS) { return t('settings:general.showInFileExplorer') } else { return t('settings:general.openContainingFolder') } } const { checkForUpdate } = useAppUpdater() const [janDataFolder, setJanDataFolder] = useState() const [isCopied, setIsCopied] = useState(false) const [selectedNewPath, setSelectedNewPath] = useState(null) const [isDialogOpen, setIsDialogOpen] = useState(false) const [isCheckingUpdate, setIsCheckingUpdate] = useState(false) useEffect(() => { const fetchDataFolder = async () => { const path = await getJanDataFolder() setJanDataFolder(path) } fetchDataFolder() }, []) const resetApp = async () => { // TODO: Loading indicator await factoryReset() } const handleOpenLogs = async () => { try { // Check if logs window already exists const existingWindow = await WebviewWindow.getByLabel( windowKey.logsAppWindow ) if (existingWindow) { // If window exists, focus it await existingWindow.setFocus() console.log('Focused existing logs window') } else { // Create a new logs window using Tauri v2 WebviewWindow API const logsWindow = new WebviewWindow(windowKey.logsAppWindow, { url: route.appLogs, title: 'App Logs - Jan', width: 800, height: 600, resizable: true, center: true, }) // Listen for window creation logsWindow.once('tauri://created', () => { console.log('Logs window created') }) // Listen for window errors logsWindow.once('tauri://error', (e) => { console.error('Error creating logs window:', e) }) } } catch (error) { console.error('Failed to open logs window:', error) } } const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text) setIsCopied(true) setTimeout(() => setIsCopied(false), 2000) // Reset after 2 seconds } catch (error) { console.error('Failed to copy to clipboard:', error) } } const handleDataFolderChange = async () => { const selectedPath = await open({ multiple: false, directory: true, defaultPath: janDataFolder, }) if (selectedPath === janDataFolder) return if (selectedPath !== null) { setSelectedNewPath(selectedPath) setIsDialogOpen(true) } } const confirmDataFolderChange = async () => { if (selectedNewPath) { try { await stopAllModels() emit(SystemEvent.KILL_SIDECAR) setTimeout(async () => { try { await relocateJanDataFolder(selectedNewPath) setJanDataFolder(selectedNewPath) // Only relaunch if relocation was successful window.core?.api?.relaunch() setSelectedNewPath(null) setIsDialogOpen(false) } catch (error) { toast.error( error instanceof Error ? error.message : t('settings:general.failedToRelocateDataFolder') ) } }, 1000) } catch (error) { console.error('Failed to relocate data folder:', error) // Revert the data folder path on error const originalPath = await getJanDataFolder() setJanDataFolder(originalPath) toast.error(t('settings:general.failedToRelocateDataFolderDesc')) } } } const handleCheckForUpdate = useCallback(async () => { setIsCheckingUpdate(true) try { if (isDev()) return toast.info(t('settings:general.devVersion')) const update = await checkForUpdate(true) if (!update) { toast.info(t('settings:general.noUpdateAvailable')) } // If update is available, the AppUpdater dialog will automatically show } catch (error) { console.error('Failed to check for updates:', error) toast.error(t('settings:general.updateError')) } finally { setIsCheckingUpdate(false) } }, [t, checkForUpdate]) return (

{t('common:settings')}

{/* General */} v{VERSION} } />
{isCheckingUpdate ? t('settings:general.checkingForUpdates') : t('settings:general.checkForUpdates')}
} /> } />
{/* Advanced */} setExperimentalFeatures(e)} /> } /> {/* Data folder */} {t('settings:dataFolder.appDataDesc', { ns: 'settings', })}  
{janDataFolder}
} actions={ <> {selectedNewPath && ( { setIsDialogOpen(open) if (!open) { setSelectedNewPath(null) } }} >
)} } />
} />
{/* Other */} setSpellCheckChatInput(e)} /> } /> {t('settings:general.factoryResetTitle')} {t('settings:general.factoryResetDesc')} } /> {/* Resources */}
{t('settings:general.viewDocs')}
} />
{t('settings:general.viewReleases')}
} />
{/* Community */}
} />
} />
{/* Support */}
{t('settings:general.reportIssue')}
} />
{/* Credits */}

{t('settings:general.creditsDesc1')}

{t('settings:general.creditsDesc2')}

} />
) }