import { useEffect, useRef } from 'react'; import { readStoredValue, writeStoredValue } from './api.js'; import { DEFAULT_STATUS, relayDisabledReason } from './relay-status.js'; import { PairingScreen } from './PairingScreen.jsx'; import { TopBar } from './TopBar.jsx'; import { DEFAULT_REASONING_EFFORT, REASONING_DEFAULT_VERSION, REASONING_DEFAULT_VERSION_KEY, REASONING_EFFORT_KEY, THEME_KEY } from './app-core-utils.js'; import { useAppWebSocket } from './hooks/useAppWebSocket.js'; import { useAppState } from './hooks/useAppState.js'; import { useChatActions } from './hooks/useChatActions.js'; import { useDocsActions } from './hooks/useDocsActions.js'; import { useMessageActions } from './hooks/useMessageActions.js'; import { useProjectController } from './hooks/useProjectController.js'; import { useRelayOperationLocks } from './hooks/useRelayOperationLocks.js'; import { useRunRegistry } from './hooks/useRunRegistry.js'; import { useMessageSpeech } from './hooks/useMessageSpeech.js'; import { useTurnPolling } from './hooks/useTurnPolling.js'; import { useTurnRefresh } from './hooks/useTurnRefresh.js'; import { useViewportMetrics } from './hooks/useViewportMetrics.js'; import { useVoiceDialogController } from './hooks/useVoiceDialogController.js'; import { Drawer } from './components/Drawer.jsx'; import { DocsPanel } from './components/DocsPanel.jsx'; import { ChatPane, ImagePreviewModal } from './components/MessageViews.jsx'; import { VoiceDialogPanel } from './components/VoiceDialogPanel.jsx'; import { Composer } from './components/Composer.jsx'; export default function App() { const app = useAppState(); const menuButtonRef = useRef(null); const wasDrawerOpenRef = useRef(false); const { status, authenticated, drawerOpen, setDrawerOpen, projects, selectedProject, expandedProjectIds, hiddenProjectIds, sessionsByProject, loadingProjectId, selectedSession, messages, previewImage, setPreviewImage, docsOpen, setDocsOpen, docsBusy, docsError, input, setInput, attachments, uploading, permissionMode, setPermissionMode, selectedModel, setSelectedModel, selectedReasoningEffort, setSelectedReasoningEffort, runningById, theme, setTheme, syncing, connectionState, selectedProjectRef, selectedSessionRef } = app; useViewportMetrics(); const { rememberLock: rememberRelayOperationLock, disabledReasons: actionDisabledReasons } = useRelayOperationLocks(); const runRegistry = useRunRegistry(app); const { running } = runRegistry; const turnRefresh = useTurnRefresh(app, runRegistry); const turnPolling = useTurnPolling(app, runRegistry, turnRefresh); const chatActions = useChatActions(app, runRegistry, turnPolling, rememberRelayOperationLock); const { submitCodexMessage, handleSubmit, handleVoiceSubmit, handleAbort } = chatActions; const projectController = useProjectController(app, runRegistry); const { loadStatus, bootstrap, handleSync, handleToggleProject, handleHideProject, handleSelectSession, handleRenameSession, handleDeleteSession, handleNewConversation } = projectController; const messageSpeech = useMessageSpeech(selectedSession?.id); const { handleDeleteMessage, handleUploadFiles, handleRemoveAttachment } = useMessageActions( app, rememberRelayOperationLock, { onDeleteMessageConfirmed: (message) => { const messageId = String(message?.id || ''); if ( messageId && (messageSpeech.speakingMessageId === messageId || messageSpeech.speechLoadingMessageId === messageId) ) { messageSpeech.stopSpeech(); } } } ); const { handleConnectDocs, handleDisconnectDocs, handleRefreshDocs, handleOpenDocsHome, handleOpenDocsAuth } = useDocsActions(app, loadStatus); const voiceDialog = useVoiceDialogController({ status, selectedProject, selectedProjectRef, messages, runningById, rememberRelayOperationLock, onVoiceSubmit: handleVoiceSubmit, submitCodexMessage, onStopMessageSpeech: messageSpeech.stopSpeech }); useAppWebSocket(app, runRegistry, turnRefresh); useEffect(() => { selectedProjectRef.current = selectedProject; }, [selectedProject]); useEffect(() => { bootstrap(); }, []); useEffect(() => { selectedSessionRef.current = selectedSession; }, [selectedSession]); useEffect(() => { app.hiddenProjectIdsRef.current = hiddenProjectIds; }, [app.hiddenProjectIdsRef, hiddenProjectIds]); useEffect(() => { writeStoredValue(THEME_KEY, theme); document.documentElement.dataset.theme = theme; document.documentElement.style.colorScheme = theme; document .querySelector('meta[name="theme-color"]') ?.setAttribute('content', theme === 'dark' ? '#171717' : '#f7f7f4'); }, [theme]); useEffect(() => { if (readStoredValue(REASONING_DEFAULT_VERSION_KEY) !== REASONING_DEFAULT_VERSION) { writeStoredValue(REASONING_DEFAULT_VERSION_KEY, REASONING_DEFAULT_VERSION); writeStoredValue(REASONING_EFFORT_KEY, DEFAULT_REASONING_EFFORT); setSelectedReasoningEffort(DEFAULT_REASONING_EFFORT); } }, [setSelectedReasoningEffort]); useEffect(() => { if (selectedReasoningEffort) { writeStoredValue(REASONING_EFFORT_KEY, selectedReasoningEffort); } }, [selectedReasoningEffort]); useEffect(() => { if (status.model && selectedModel === DEFAULT_STATUS.model) { setSelectedModel(status.model); } }, [selectedModel, status.model]); useEffect(() => { const saved = readStoredValue(REASONING_EFFORT_KEY); if (!saved && status.reasoningEffort && !selectedReasoningEffort) { setSelectedReasoningEffort(status.reasoningEffort); } }, [selectedReasoningEffort, status.reasoningEffort]); const projectDisabledReason = selectedProject ? '' : syncing || !projects.length ? '正在同步项目...' : '未找到可用项目'; const composerDisabledReason = relayDisabledReason(connectionState) || projectDisabledReason; const modalBackgroundInert = docsOpen || voiceDialog.open || Boolean(previewImage); const appContentInert = drawerOpen || modalBackgroundInert; useEffect(() => { let focusFrame = null; if (wasDrawerOpenRef.current && !drawerOpen && !modalBackgroundInert) { focusFrame = window.requestAnimationFrame(() => { menuButtonRef.current?.focus(); }); } wasDrawerOpenRef.current = drawerOpen; return () => { if (focusFrame !== null) { window.cancelAnimationFrame(focusFrame); } }; }, [drawerOpen, modalBackgroundInert]); if (!authenticated) { return ; } const closeDrawer = () => { setDrawerOpen(false); }; return (
setDrawerOpen(true)} onOpenDocs={() => setDocsOpen(true)} backgroundInert={appContentInert} menuButtonRef={menuButtonRef} /> setDocsOpen(false)} onConnect={handleConnectDocs} onDisconnect={handleDisconnectDocs} onOpenHome={handleOpenDocsHome} onOpenAuth={handleOpenDocsAuth} onRefresh={handleRefreshDocs} /> setPreviewImage(null)} />
); }