# Tool_OCR Frontend API Documentation > **Version**: 0.1.0 > **Last Updated**: 2025-01-13 > **Purpose**: Complete documentation of frontend architecture, component structure, API integration, and dependencies --- ## Table of Contents 1. [Project Overview](#project-overview) 2. [Technology Stack](#technology-stack) 3. [Component Architecture](#component-architecture) 4. [Page → API Dependency Matrix](#page--api-dependency-matrix) 5. [Component Tree Structure](#component-tree-structure) 6. [State Management Strategy](#state-management-strategy) 7. [Route Configuration](#route-configuration) 8. [API Integration Patterns](#api-integration-patterns) 9. [UI/UX Design System](#uiux-design-system) 10. [Error Handling Patterns](#error-handling-patterns) 11. [Deployment Configuration](#deployment-configuration) --- ## Project Overview Tool_OCR 前端是一個基於 React 18 + Vite 的現代化 OCR 文件處理系統,提供企業級的使用者介面和體驗。 ### Key Features - **批次檔案上傳**: 支援拖放上傳,多檔案批次處理 - **即時進度追蹤**: 使用輪詢機制顯示 OCR 處理進度 - **結果預覽**: Markdown 和 JSON 雙格式預覽 - **靈活匯出**: 支援 TXT、JSON、Excel、Markdown、PDF、ZIP 多種格式 - **規則管理**: 可自訂匯出規則和 CSS 模板 - **響應式設計**: 適配桌面和平板裝置 --- ## Technology Stack ### Core Dependencies ```json { "@tanstack/react-query": "^5.90.7", // Server state management "react": "^19.2.0", // UI framework "react-dom": "^19.2.0", "react-router-dom": "^7.9.5", // Routing "vite": "^7.2.2", // Build tool "typescript": "~5.9.3" // Type safety } ``` ### UI & Styling ```json { "tailwindcss": "^4.1.17", // CSS framework "class-variance-authority": "^0.7.0", // Component variants "clsx": "^2.1.1", // Class name utility "tailwind-merge": "^3.4.0", // Tailwind class merge "lucide-react": "^0.553.0" // Icon library } ``` ### State & Data ```json { "zustand": "^5.0.8", // Client state "axios": "^1.13.2", // HTTP client "react-dropzone": "^14.3.8", // File upload "react-markdown": "^9.0.1" // Markdown rendering } ``` ### Internationalization ```json { "i18next": "^25.6.2", "react-i18next": "^16.3.0" } ``` --- ## Component Architecture ### Atomic Design Structure ``` frontend/src/ ├── components/ │ ├── ui/ # Atomic components (shadcn/ui) │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── select.tsx │ │ ├── badge.tsx │ │ ├── progress.tsx │ │ ├── alert.tsx │ │ ├── dialog.tsx │ │ ├── tabs.tsx │ │ ├── table.tsx │ │ └── toast.tsx │ ├── FileUpload.tsx # Drag-and-drop upload component │ ├── ResultsTable.tsx # OCR results display table │ ├── MarkdownPreview.tsx # Markdown content renderer │ └── Layout.tsx # Main app layout with sidebar ├── pages/ │ ├── LoginPage.tsx # Authentication │ ├── UploadPage.tsx # File upload and selection │ ├── ProcessingPage.tsx # OCR processing status │ ├── ResultsPage.tsx # Results viewing and preview │ ├── ExportPage.tsx # Export configuration and download │ └── SettingsPage.tsx # User settings and rules management ├── store/ │ ├── authStore.ts # Authentication state (Zustand) │ └── uploadStore.ts # Upload batch state (Zustand) ├── services/ │ └── api.ts # API client (Axios) ├── types/ │ └── api.ts # TypeScript type definitions ├── lib/ │ └── utils.ts # Utility functions ├── i18n/ │ └── index.ts # i18n configuration └── styles/ └── index.css # Global styles and CSS variables ``` --- ## Page → API Dependency Matrix | Page/Component | API Endpoints Used | HTTP Method | Purpose | Polling | |----------------|-------------------|-------------|---------|---------| | **LoginPage** | `/api/v1/auth/login` | POST | User authentication | No | | **UploadPage** | `/api/v1/upload` | POST | Upload files for OCR | No | | **ProcessingPage** | `/api/v1/ocr/process` | POST | Start OCR processing | No | | | `/api/v1/batch/{batch_id}/status` | GET | Poll batch status | Yes (2s) | | **ResultsPage** | `/api/v1/batch/{batch_id}/status` | GET | Load completed files | No | | | `/api/v1/ocr/result/{file_id}` | GET | Get OCR result details | No | | | `/api/v1/export/pdf/{file_id}` | GET | Download PDF export | No | | **ExportPage** | `/api/v1/export` | POST | Export batch results | No | | | `/api/v1/export/rules` | GET | List export rules | No | | | `/api/v1/export/rules` | POST | Create new rule | No | | | `/api/v1/export/rules/{rule_id}` | PUT | Update existing rule | No | | | `/api/v1/export/rules/{rule_id}` | DELETE | Delete rule | No | | | `/api/v1/export/css-templates` | GET | List CSS templates | No | | **SettingsPage** | `/api/v1/export/rules` | GET | Manage export rules | No | --- ## Component Tree Structure ``` App ├── Router (React Router) │ ├── PublicRoute │ │ └── LoginPage │ │ ├── Form (username + password) │ │ ├── Button (submit) │ │ └── Alert (error display) │ └── ProtectedRoute (requires authentication) │ └── Layout │ ├── Sidebar │ │ ├── Logo │ │ ├── Navigation Links │ │ │ ├── UploadPage link │ │ │ ├── ProcessingPage link │ │ │ ├── ResultsPage link │ │ │ ├── ExportPage link │ │ │ └── SettingsPage link │ │ └── User Section + Logout │ ├── TopBar │ │ ├── SearchInput │ │ └── NotificationBell │ └── MainContent (Outlet) │ ├── UploadPage │ │ ├── FileUpload (react-dropzone) │ │ ├── FileList (selected files) │ │ └── UploadButton │ ├── ProcessingPage │ │ ├── ProgressBar │ │ ├── StatsCards (completed/processing/failed) │ │ ├── FileStatusList │ │ └── ActionButtons │ ├── ResultsPage │ │ ├── FileList (left sidebar) │ │ │ ├── SearchInput │ │ │ └── FileItems │ │ └── PreviewPanel (right) │ │ ├── StatsCards │ │ ├── Tabs (Markdown/JSON) │ │ ├── MarkdownPreview │ │ └── JSONViewer │ ├── ExportPage │ │ ├── FormatSelector │ │ ├── RuleSelector │ │ ├── CSSTemplateSelector │ │ ├── OptionsForm │ │ └── ExportButton │ └── SettingsPage │ ├── UserInfo │ ├── ExportRulesManager │ │ ├── RuleList │ │ ├── CreateRuleDialog │ │ ├── EditRuleDialog │ │ └── DeleteConfirmDialog │ └── SystemSettings ``` --- ## State Management Strategy ### Client State (Zustand) **authStore.ts** - Authentication State ```typescript interface AuthState { user: User | null isAuthenticated: boolean setUser: (user: User | null) => void logout: () => void } ``` **uploadStore.ts** - Upload Batch State ```typescript interface UploadState { batchId: number | null files: FileInfo[] uploadProgress: number setBatchId: (id: number) => void setFiles: (files: FileInfo[]) => void setUploadProgress: (progress: number) => void reset: () => void } ``` ### Server State (React Query) - **Caching**: Automatic caching with stale-while-revalidate strategy - **Polling**: Automatic refetch for batch status every 2 seconds during processing - **Error Handling**: Built-in error retry and error state management - **Optimistic Updates**: For export rules CRUD operations ### Query Keys ```typescript // Batch status polling ['batchStatus', batchId] // OCR result for specific file ['ocrResult', fileId] // Export rules list ['exportRules'] // CSS templates list ['cssTemplates'] ``` --- ## Route Configuration | Route | Component | Access Level | Description | Protected | |-------|-----------|--------------|-------------|-----------| | `/login` | LoginPage | Public | User authentication | No | | `/` | Layout (redirect to /upload) | Private | Main layout wrapper | Yes | | `/upload` | UploadPage | Private | File upload interface | Yes | | `/processing` | ProcessingPage | Private | OCR processing status | Yes | | `/results` | ResultsPage | Private | View OCR results | Yes | | `/export` | ExportPage | Private | Export configuration | Yes | | `/settings` | SettingsPage | Private | User settings | Yes | ### Protected Route Implementation ```typescript function ProtectedRoute({ children }: { children: React.ReactNode }) { const isAuthenticated = useAuthStore((state) => state.isAuthenticated) if (!isAuthenticated) { return } return <>{children} } ``` --- ## API Integration Patterns ### API Client Configuration **Base URL**: `http://localhost:12010/api/v1` **Request Interceptor**: Adds JWT token to Authorization header ```typescript this.client.interceptors.request.use((config) => { if (this.token) { config.headers.Authorization = `Bearer ${this.token}` } return config }) ``` **Response Interceptor**: Handles 401 errors and redirects to login ```typescript this.client.interceptors.response.use( (response) => response, (error: AxiosError) => { if (error.response?.status === 401) { this.clearToken() window.location.href = '/login' } return Promise.reject(error) } ) ``` ### Authentication Flow ```typescript // 1. Login const response = await apiClient.login({ username, password }) // Response: { access_token, token_type, expires_in } // 2. Store token localStorage.setItem('auth_token', response.access_token) // 3. Set user in store setUser({ id: 1, username }) // 4. Navigate to /upload navigate('/upload') ``` ### File Upload Flow ```typescript // 1. Prepare FormData const formData = new FormData() files.forEach((file) => formData.append('files', file)) // 2. Upload files const response = await apiClient.uploadFiles(files) // Response: { batch_id, files: FileInfo[] } // 3. Store batch info setBatchId(response.batch_id) setFiles(response.files) // 4. Navigate to /processing navigate('/processing') ``` ### OCR Processing Flow ```typescript // 1. Start OCR processing await apiClient.processOCR({ batch_id, lang: 'ch', detect_layout: true }) // Response: { message, batch_id, total_files, status } // 2. Poll batch status every 2 seconds const { data: batchStatus } = useQuery({ queryKey: ['batchStatus', batchId], queryFn: () => apiClient.getBatchStatus(batchId), refetchInterval: (query) => { const status = query.state.data?.batch.status if (status === 'completed' || status === 'failed') return false return 2000 // Poll every 2 seconds }, }) // 3. Auto-redirect when completed useEffect(() => { if (batchStatus?.batch.status === 'completed') { navigate('/results') } }, [batchStatus?.batch.status]) ``` ### Results Viewing Flow ```typescript // 1. Load batch status const { data: batchStatus } = useQuery({ queryKey: ['batchStatus', batchId], queryFn: () => apiClient.getBatchStatus(batchId), }) // 2. Select a file setSelectedFileId(fileId) // 3. Load OCR result for selected file const { data: ocrResult } = useQuery({ queryKey: ['ocrResult', selectedFileId], queryFn: () => apiClient.getOCRResult(selectedFileId), enabled: !!selectedFileId, }) // 4. Display in Markdown or JSON format {ocrResult.markdown_content}
{JSON.stringify(ocrResult.json_data, null, 2)}
``` ### Export Flow ```typescript // 1. Select export format and options const exportData = { batch_id: batchId, format: 'pdf', rule_id: selectedRuleId, css_template: 'academic', options: { include_metadata: true } } // 2. Request export const blob = await apiClient.exportResults(exportData) // 3. Trigger download downloadBlob(blob, `ocr-results-${batchId}.pdf`) ``` --- ## UI/UX Design System ### Color Palette (CSS Variables) ```css /* Primary - Professional Blue */ --primary: 217 91% 60%; /* #3b82f6 */ --primary-foreground: 0 0% 100%; /* Secondary - Gray-Blue */ --secondary: 220 15% 95%; --secondary-foreground: 220 15% 25%; /* Accent - Vibrant Teal */ --accent: 173 80% 50%; --accent-foreground: 0 0% 100%; /* Success */ --success: 142 72% 45%; /* #16a34a */ --success-foreground: 0 0% 100%; /* Destructive */ --destructive: 0 85% 60%; /* #ef4444 */ --destructive-foreground: 0 0% 100%; /* Warning */ --warning: 38 92% 50%; --warning-foreground: 0 0% 100%; /* Background */ --background: 220 15% 97%; /* #fafafa */ --card: 0 0% 100%; /* #ffffff */ --sidebar: 220 25% 12%; /* Dark blue-gray */ /* Borders */ --border: 220 13% 88%; --radius: 0.5rem; ``` ### Typography - **Font Family**: System font stack (native) - **Page Title**: 1.875rem (30px), font-weight: 700 - **Section Title**: 1.125rem (18px), font-weight: 600 - **Body Text**: 0.875rem (14px), font-weight: 400 - **Small Text**: 0.75rem (12px) ### Spacing Scale ```css --spacing-xs: 0.25rem; /* 4px */ --spacing-sm: 0.5rem; /* 8px */ --spacing-md: 1rem; /* 16px */ --spacing-lg: 1.5rem; /* 24px */ --spacing-xl: 2rem; /* 32px */ ``` ### Component Variants **Button Variants**: - `default`: Primary blue background - `outline`: Border only - `secondary`: Muted background - `destructive`: Red for delete actions - `ghost`: No background, hover effect **Alert Variants**: - `default`: Neutral gray - `info`: Blue - `success`: Green - `warning`: Yellow - `destructive`: Red **Badge Variants**: - `default`: Gray - `success`: Green - `warning`: Yellow - `destructive`: Red - `secondary`: Muted ### Responsive Breakpoints ```typescript // Tailwind breakpoints sm: '640px', // Mobile landscape md: '768px', // Tablet lg: '1024px', // Desktop (primary support) xl: '1280px', // Large desktop 2xl: '1536px' // Extra large ``` **Primary Support**: Desktop (>= 1024px) **Secondary Support**: Tablet (768px - 1023px) **Optional**: Mobile (< 768px) --- ## Error Handling Patterns ### Global Error Boundary ```typescript class ErrorBoundary extends Component { static getDerivedStateFromError(error: Error): State { return { hasError: true, error } } componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error('Uncaught error:', error, errorInfo) } render() { if (this.state.hasError) { return } return this.props.children } } ``` ### API Error Handling ```typescript try { await apiClient.uploadFiles(files) } catch (err: any) { const errorDetail = err.response?.data?.detail toast({ title: t('upload.uploadError'), description: Array.isArray(errorDetail) ? errorDetail.map(e => e.msg || e.message).join(', ') : errorDetail || t('errors.networkError'), variant: 'destructive', }) } ``` ### Form Validation ```typescript // Client-side validation if (selectedFiles.length === 0) { toast({ title: t('errors.validationError'), description: '請選擇至少一個檔案', variant: 'destructive', }) return } // Backend validation errors if (err.response?.status === 422) { const errors = err.response.data.detail // Display validation errors to user } ``` ### Loading States ```typescript // Query loading state const { data, isLoading, error } = useQuery({ queryKey: ['batchStatus', batchId], queryFn: () => apiClient.getBatchStatus(batchId), }) if (isLoading) return if (error) return if (!data) return // Mutation loading state const mutation = useMutation({ mutationFn: apiClient.uploadFiles, onSuccess: () => { /* success */ }, onError: () => { /* error */ }, }) ``` --- ## Deployment Configuration ### Environment Variables ```bash # .env.production VITE_API_BASE_URL=http://localhost:12010 VITE_APP_NAME=Tool_OCR VITE_APP_VERSION=0.1.0 ``` ### Build Configuration **vite.config.ts**: ```typescript export default defineConfig({ plugins: [react()], server: { port: 12011, proxy: { '/api': { target: 'http://localhost:12010', changeOrigin: true, }, }, }, build: { outDir: 'dist', sourcemap: false, rollupOptions: { output: { manualChunks: { vendor: ['react', 'react-dom', 'react-router-dom'], ui: ['@tanstack/react-query', 'zustand', 'lucide-react'], }, }, }, }, }) ``` ### Build Commands ```bash # Development npm run dev # Production build npm run build # Preview production build npm run preview ``` ### Nginx Configuration ```nginx server { listen 80; server_name tool-ocr.example.com; root /path/to/Tool_OCR/frontend/dist; # Frontend static files location / { try_files $uri $uri/ /index.html; } # API reverse proxy location /api { proxy_pass http://127.0.0.1:12010; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # Static assets caching location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { expires 1y; add_header Cache-Control "public, immutable"; } } ``` --- ## Performance Optimization ### Code Splitting - **Vendor Bundle**: React, React Router, React Query (separate chunk) - **UI Bundle**: Zustand, Lucide React, UI components - **Route-based Splitting**: Lazy load pages with `React.lazy()` ### Caching Strategy - **React Query Cache**: 5 minutes stale time for most queries - **Polling Interval**: 2 seconds during OCR processing - **Infinite Cache**: Export rules (rarely change) ### Asset Optimization - **Images**: Convert to WebP format, use appropriate sizes - **Fonts**: System font stack (no custom fonts) - **Icons**: Lucide React (tree-shakeable) --- ## Testing Strategy ### Component Testing (Planned) ```typescript // Example: UploadPage.test.tsx import { render, screen, fireEvent } from '@testing-library/react' import { UploadPage } from '@/pages/UploadPage' describe('UploadPage', () => { it('should display file upload area', () => { render() expect(screen.getByText(/拖放檔案/i)).toBeInTheDocument() }) it('should allow file selection', async () => { render() const file = new File(['content'], 'test.pdf', { type: 'application/pdf' }) // Test file upload }) }) ``` ### API Integration Testing - **Mock API Responses**: Use MSW (Mock Service Worker) - **Error Scenarios**: Test 401, 404, 500 responses - **Loading States**: Test skeleton/spinner display --- ## Accessibility Standards ### WCAG 2.1 AA Compliance - **Keyboard Navigation**: All interactive elements accessible via keyboard - **Focus Indicators**: Visible focus states on all inputs and buttons - **ARIA Labels**: Proper labels for screen readers - **Color Contrast**: Minimum 4.5:1 ratio for text - **Alt Text**: All images have descriptive alt attributes ### Semantic HTML ```typescript // Use semantic elements