Codex
deploy: CodexMobile Relay
90f0300
Raw
History Blame Contribute Delete
10.5 kB
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 <PairingScreen onPaired={bootstrap} />;
}
const closeDrawer = () => {
setDrawerOpen(false);
};
return (
<div className="app-shell">
<TopBar
selectedProject={selectedProject}
selectedSession={selectedSession}
connectionState={connectionState}
onMenu={() => setDrawerOpen(true)}
onOpenDocs={() => setDocsOpen(true)}
backgroundInert={appContentInert}
menuButtonRef={menuButtonRef}
/>
<Drawer
open={drawerOpen}
onClose={closeDrawer}
projects={projects}
selectedProject={selectedProject}
selectedSession={selectedSession}
expandedProjectIds={expandedProjectIds}
sessionsByProject={sessionsByProject}
loadingProjectId={loadingProjectId}
onToggleProject={handleToggleProject}
onHideProject={handleHideProject}
onSelectSession={handleSelectSession}
onRenameSession={handleRenameSession}
onDeleteSession={handleDeleteSession}
onNewConversation={handleNewConversation}
onSync={handleSync}
syncing={syncing}
hiddenProjectIds={hiddenProjectIds}
theme={theme}
setTheme={setTheme}
backgroundInert={modalBackgroundInert}
/>
<DocsPanel
open={docsOpen}
docs={status.docs}
busy={docsBusy}
error={docsError}
onClose={() => setDocsOpen(false)}
onConnect={handleConnectDocs}
onDisconnect={handleDisconnectDocs}
onOpenHome={handleOpenDocsHome}
onOpenAuth={handleOpenDocsAuth}
onRefresh={handleRefreshDocs}
/>
<ChatPane
messages={messages}
selectedSession={selectedSession}
running={running}
onPreviewImage={setPreviewImage}
onDeleteMessage={handleDeleteMessage}
onSpeakMessage={messageSpeech.speakMessage}
speakingMessageId={messageSpeech.speakingMessageId}
speechLoadingMessageId={messageSpeech.speechLoadingMessageId}
backgroundInert={appContentInert}
/>
<VoiceDialogPanel
open={voiceDialog.open}
state={voiceDialog.state}
error={voiceDialog.error}
transcript={voiceDialog.transcript}
assistantText={voiceDialog.assistantText}
handoffDraft={voiceDialog.handoffDraft}
onHandoffDraftChange={voiceDialog.setHandoffDraft}
onHandoffSubmit={voiceDialog.submitHandoffToCodex}
onHandoffContinue={voiceDialog.continueHandoffCollection}
onHandoffCancel={voiceDialog.cancelHandoffConfirmation}
onStart={voiceDialog.startRecording}
onStop={voiceDialog.stopRecording}
onClose={voiceDialog.closeDialog}
/>
<Composer
input={input}
setInput={setInput}
onSubmit={handleSubmit}
running={running}
onAbort={handleAbort}
models={status.models}
selectedModel={selectedModel}
onSelectModel={setSelectedModel}
selectedReasoningEffort={selectedReasoningEffort}
onSelectReasoningEffort={setSelectedReasoningEffort}
permissionMode={permissionMode}
onSelectPermission={setPermissionMode}
attachments={attachments}
onUploadFiles={handleUploadFiles}
onRemoveAttachment={handleRemoveAttachment}
onRateLimit={rememberRelayOperationLock}
uploading={uploading}
onVoiceSubmit={handleVoiceSubmit}
onOpenVoiceDialog={voiceDialog.openDialog}
voiceDialogActive={voiceDialog.open}
disabled={Boolean(composerDisabledReason)}
disabledReason={composerDisabledReason}
actionDisabledReasons={actionDisabledReasons}
backgroundInert={appContentInert}
/>
<ImagePreviewModal image={previewImage} onClose={() => setPreviewImage(null)} />
</div>
);
}