import { MessageNotice } from './appNotice'; import { useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; import { Bot, Check, ChevronUp, ExternalLink, History, House, Languages, Lock, LogIn, MessageCircle, Network, PackageOpen, ServerCog, Settings, X, } from 'lucide-react'; import appLogo from './assets/logo.jpg'; import { CoreRuntimeProvider, useCoreRuntime } from './coreRuntime'; import { CoreUpdateProvider, useCoreUpdate } from './coreUpdate'; import { ConfigPanelPage } from './pages/ConfigPanel'; import { ApiAccessPage } from './pages/ApiAccessPage'; import { KernelPage } from './pages/Kernel'; import { VersionManagementPage } from './pages/VersionManagementPage'; import { OAuthManagementPage } from './pages/ManagementPages'; import { AgentsPage } from './pages/AgentsPage'; import { EasyModePage } from './pages/EasyModePage'; import { UsageRecordsPage } from './pages/UsageRecordsPage'; import { languageOptions, useI18n } from './i18n'; import { AppUpdateDialog, AppUpdateProvider, useAppUpdate } from './appUpdate'; import { appUpdateIndicatorState } from './appUpdateModel'; import { canOpenAppPage, isAlwaysAvailablePage } from './navigation'; import { useThemePreference } from './theme'; import { useDialogFocusTrap } from './components/useDialogFocusTrap'; const CONTACT_URL = 'https://qm.qq.com/q/3queDaIG'; const pages = [ { id: 'easy', labelKey: 'app.nav.easy', icon: House, component: HomePage, }, { id: 'home', labelKey: 'app.nav.home', icon: House, component: HomePage, }, { id: 'api', labelKey: 'app.nav.api', icon: Network, component: ApiAccessPage, }, { id: 'oauth', labelKey: 'app.nav.oauth', icon: LogIn, component: OAuthManagementPage, }, { id: 'agents', labelKey: 'app.nav.agents', icon: Bot, component: AgentsPage, }, { id: 'usage-records', labelKey: 'app.nav.usageRecords', icon: History, component: UsageRecordsPage, }, { id: 'config', labelKey: 'app.nav.config', icon: Settings, component: ConfigPanelPage, }, { id: 'versions', labelKey: 'app.nav.versions', icon: PackageOpen, component: VersionManagementPageWrapper, }, ] as const; type PageId = (typeof pages)[number]['id']; type WindowsCloseAction = 'exit' | 'minimize-to-tray'; type WindowsCloseBehavior = 'ask' | WindowsCloseAction; type WindowsClosePrompt = { resolvingAction: WindowsCloseAction | null; rememberChoice: boolean; error: string | null; }; type GuiSettings = { closeBehavior: WindowsCloseBehavior; }; function HomePage() { return ; } function VersionManagementPageWrapper() { return ; } function App() { return ( ); } function AppContent() { const { locale, setLocale, t } = useI18n(); const { info: appUpdateInfo, hasUpdate, processing: appUpdateProcessing } = useAppUpdate(); const { latest: coreLatest, hasUpdate: coreHasUpdate } = useCoreUpdate(); const [active, setActive] = useState('home'); const [languageMenuOpen, setLanguageMenuOpen] = useState(false); const [theme, setTheme] = useThemePreference(); const [windowsClosePrompt, setWindowsClosePrompt] = useState(null); const closeDialogRef = useDialogFocusTrap({ active: Boolean(windowsClosePrompt), onEscape: windowsClosePrompt?.resolvingAction ? undefined : () => setWindowsClosePrompt(null), preventEscape: Boolean(windowsClosePrompt?.resolvingAction), }); const languageMenuRef = useRef(null); const languageButtonRef = useRef(null); const { status } = useCoreRuntime(); const coreReady = Boolean(status?.ready); const activePage = pages.find((page) => page.id === active) ?? pages[0]; const ActivePage = activePage.component; const selectedLanguage = languageOptions.find((option) => option.value === locale) ?? languageOptions[0]; const availableUpdateLabel = [ hasUpdate ? t('appUpdate.badgeAvailable', { version: appUpdateInfo?.latestVersion ?? '' }) : '', coreHasUpdate ? `${t('kernel.versions.coreCardTitle')}: ${t('kernel.update.available')} ${coreLatest?.version ?? ''}`.trim() : '', ].filter(Boolean).join(' · '); useEffect(() => { if (!canOpenAppPage(active, coreReady)) { setActive('home'); } }, [active, coreReady]); useEffect(() => { if (!languageMenuOpen) return undefined; const closeFromOutside = (event: PointerEvent) => { if (!languageMenuRef.current?.contains(event.target as Node)) { setLanguageMenuOpen(false); } }; const closeFromKeyboard = (event: KeyboardEvent) => { if (event.key !== 'Escape') return; setLanguageMenuOpen(false); languageButtonRef.current?.focus(); }; document.addEventListener('pointerdown', closeFromOutside); document.addEventListener('keydown', closeFromKeyboard); return () => { document.removeEventListener('pointerdown', closeFromOutside); document.removeEventListener('keydown', closeFromKeyboard); }; }, [languageMenuOpen]); useEffect(() => { let disposed = false; let stopListening: (() => void) | undefined; const handleWindowsCloseRequest = async () => { try { const settings = await invoke('get_gui_settings'); if (settings.closeBehavior !== 'ask') { await resolveWindowsCloseRequest(settings.closeBehavior, false); return; } } catch (error) { console.error('读取关闭行为设置失败', error); } setWindowsClosePrompt((current) => current ?? { resolvingAction: null, rememberChoice: false, error: null, }, ); }; void listen('windows-close-requested', () => { void handleWindowsCloseRequest(); }) .then((stop) => { if (disposed) { stop(); } else { stopListening = stop; } }) .catch((error) => { console.error('监听 Windows 关闭确认事件失败', error); }); return () => { disposed = true; stopListening?.(); }; }, []); const handleLanguageListKeyDown = (event: ReactKeyboardEvent) => { if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return; const options = Array.from(event.currentTarget.querySelectorAll('[role="option"]')); if (!options.length) return; const current = options.indexOf(document.activeElement as HTMLButtonElement); const next = event.key === 'Home' ? 0 : event.key === 'End' ? options.length - 1 : event.key === 'ArrowDown' ? (Math.max(current, -1) + 1) % options.length : (current <= 0 ? options.length : current) - 1; event.preventDefault(); options[next]?.focus(); }; const select = (pageId: PageId) => { if (!canOpenAppPage(pageId, coreReady)) { return; } setActive(pageId); }; const openContact = async () => { try { await invoke('open_external_url', { url: CONTACT_URL }); } catch (error) { console.error('打开联系我们链接失败', error); } }; const resolveWindowsCloseRequest = async ( action: WindowsCloseAction, remember = windowsClosePrompt?.rememberChoice ?? false, ) => { setWindowsClosePrompt((current) => current ? { ...current, resolvingAction: action, error: null, } : current, ); try { await invoke('resolve_windows_close_request', { action, remember }); setWindowsClosePrompt(null); } catch (error) { setWindowsClosePrompt((current) => current ? { ...current, resolvingAction: null, error: error instanceof Error ? error.message : String(error), } : { resolvingAction: null, rememberChoice: false, error: error instanceof Error ? error.message : String(error), }, ); } }; return ( <>
{active !== "easy" ? ( ) : null}
{isAlwaysAvailablePage(activePage.id) || coreReady ? ( activePage.id === 'easy' ? ( select('home')} theme={theme} setTheme={setTheme} locale={locale} setLocale={setLocale} /> ) : ( ) ) : ( )}
{windowsClosePrompt ? (

{t('app.close.title')}

{t('app.close.description')}

{windowsClosePrompt.error ? ( setWindowsClosePrompt(current => current ? { ...current, error: null } : current)} /> ) : null}
) : null} ); } function CoreLockedPage() { const { t } = useI18n(); return (
); } export default App;