/** * App.tsx – Root Application Component * * Assembles the entire application: * - Wraps everything in ErrorBoundary to catch unhandled render errors * - Uses useOnlineStatus for network detection (Req 11.3) * - Dual-panel desktop layout via ResizableLayout with 40/60 split (Req 12.1) * - Left panel: ChatPanel (fixed) * - Right panel: Dynamically switches between ExportRecordList (default) and EditorPanel (when document is opened) * - Uses React.lazy for code splitting of non-critical panels (Req 13.2, 13.8) * * Layout Structure: * ┌─────────────────────────────────────────────────────────────┐ * │ Toolbar │ * ├──────────────────────┬──────────────────────────────────────┤ * │ Chat Panel │ Export Records List (default) │ * │ (Left 40%) │ OR │ * │ │ Editor Panel (when document opened) │ * │ │ (Right 60%) │ * └──────────────────────┴──────────────────────────────────────┘ * * Requirements: 4.1, 4.6, 11.3, 12.1, 13.2, 13.8 * * @module App */ import React, { useCallback, useState, memo, lazy, Suspense, useMemo } from 'react'; import { ConfigProvider, Alert, Button, Drawer, Spin } from 'antd'; import { MessageOutlined } from '@ant-design/icons'; import zhCN from 'antd/locale/zh_CN'; import { ErrorBoundary } from './components/common'; import ResizableLayout from './components/Layout/ResizableLayout'; import { useUIStore } from './stores/uiStore'; // ── Lazy-loaded panel components (Req 13.2, 13.8) ──────────────────────────── // These non-first-screen panels are split into separate chunks by Vite, // reducing the initial JS bundle size. const ChatPanel = lazy(() => import('./components/ChatPanel/ChatPanel')); const EditorPanel = lazy(() => import('./components/EditorPanel/EditorPanel')); const ExportRecordList = lazy(() => import('./components/ExportRecordList').then((m) => ({ default: m.ExportRecordList })) ); const SessionList = lazy(() => import('./components/SessionList').then((m) => ({ default: m.SessionList })) ); import { useOnlineStatus } from './hooks/useOnlineStatus'; // ── Panel loading fallback ──────────────────────────────────────────────────── /** * Lightweight fallback rendered by while a lazy panel chunk is * being fetched. Keeps the layout stable by filling the full available area. */ const PanelFallback: React.FC = memo(() => (
)); PanelFallback.displayName = 'PanelFallback'; // ── Styles ──────────────────────────────────────────────────────────────────── const appStyle: React.CSSProperties = { display: 'flex', flexDirection: 'column', height: '100vh', width: '100vw', overflow: 'hidden', backgroundColor: '#f5f5f5', }; const offlineBannerStyle: React.CSSProperties = { flexShrink: 0, }; const mainContentStyle: React.CSSProperties = { flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column', }; // ── Subcomponents ───────────────────────────────────────────────────────────── /** * Offline banner shown at the top when network is unavailable. */ const OfflineBanner: React.FC = memo(() => (
)); OfflineBanner.displayName = 'OfflineBanner'; // ── App Component ───────────────────────────────────────────────────────────── /** * App * * The top-level React component. Handles layout orchestration. * * Uses a fixed dual-panel desktop layout with ResizableLayout (40% left / 60% right). * The left panel shows ChatPanel (fixed), and the right panel dynamically switches: * - Default: ExportRecordList (shows export history) * - When document opened: EditorPanel (shows document for preview and editing) */ const App: React.FC = () => { // ── Network status (Req 11.3) ──────────────────────────────────────────── const isOnline = useOnlineStatus(); // ── Editor state ────────────────────────────────────────────────────────── const previewDocumentId = useUIStore((state) => state.previewDocumentId); const previewDocumentName = useUIStore((state) => state.previewDocumentName); const closeDocumentPreview = useUIStore((state) => state.closeDocumentPreview); // ── Session history drawer ──────────────────────────────────────────────── const [sessionListOpen, setSessionListOpen] = useState(false); // ── User ID (in a real app, this would come from auth context) ─────────── const currentUserId = 'default-user'; /** Stable callback to close the session drawer */ const handleCloseSessionList = useCallback(() => setSessionListOpen(false), []); /** Stable callback to open the session drawer */ const handleOpenSessionList = useCallback(() => setSessionListOpen(true), []); /** * Handle close editor button click */ const handleCloseEditor = useCallback(() => { closeDocumentPreview(); }, [closeDocumentPreview]); // ── Panels ──────────────────────────────────────────────────────────────── // Each panel is wrapped in so the lazy chunk loads independently. // useMemo ensures the JSX elements are stable references and don't cause // Suspense to remount unnecessarily on parent re-renders (Req 13.7). const leftPanel = useMemo(() => { // Always show chat panel on the left return ( }> ); }, []); const rightPanel = useMemo(() => { // If a document is being previewed, show EditorPanel // Otherwise show ExportRecordList (default) if (previewDocumentId) { return ( }> ); } // Default: show export records list return ( }> ); }, [previewDocumentId, previewDocumentName, handleCloseEditor, currentUserId]); // ── Session history drawer ──────────────────────────────────────────────── const sessionListDrawer = sessionListOpen ? ( }> ) : null; // ── Session history toggle button ───────────────────────────────────────── const sessionListButton = (