import React, { useState, useEffect, useCallback } from 'react'; import { Box, Container, Typography, Paper, Tabs, Tab, FormControl, FormControlLabel, InputLabel, Select, MenuItem, Button, Snackbar, Alert, TextField, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, Grid, Card, CardContent, Divider, Switch, SelectChangeEvent, Chip, LinearProgress, CircularProgress, } from '@mui/material'; import { Edit as EditIcon, Delete as DeleteIcon, Add as AddIcon, CloudSync as CloudSyncIcon, Folder as FolderIcon, Assessment as AssessmentIcon, PlayArrow as PlayArrowIcon, Pause as PauseIcon, Stop as StopIcon } from '@mui/icons-material'; import { useAuth } from '../contexts/AuthContext'; import api, { queueService } from '../services/api'; interface User { id: string; username: string; email: string; created_at: string; } interface Settings { ocrLanguage: string; concurrentOcrJobs: number; ocrTimeoutSeconds: number; maxFileSizeMb: number; allowedFileTypes: string[]; autoRotateImages: boolean; enableImagePreprocessing: boolean; searchResultsPerPage: number; searchSnippetLength: number; fuzzySearchThreshold: number; retentionDays: number | null; enableAutoCleanup: boolean; enableCompression: boolean; memoryLimitMb: number; cpuPriority: string; enableBackgroundOcr: boolean; ocrPageSegmentationMode: number; ocrEngineMode: number; ocrMinConfidence: number; ocrDpi: number; ocrEnhanceContrast: boolean; ocrRemoveNoise: boolean; ocrDetectOrientation: boolean; ocrWhitelistChars: string; ocrBlacklistChars: string; ocrBrightnessBoost: number; ocrContrastMultiplier: number; ocrNoiseReductionLevel: number; ocrSharpeningStrength: number; ocrMorphologicalOperations: boolean; ocrAdaptiveThresholdWindowSize: number; ocrHistogramEqualization: boolean; ocrUpscaleFactor: number; ocrMaxImageWidth: number; ocrMaxImageHeight: number; saveProcessedImages: boolean; ocrQualityThresholdBrightness: number; ocrQualityThresholdContrast: number; ocrQualityThresholdNoise: number; ocrQualityThresholdSharpness: number; ocrSkipEnhancement: boolean; } interface SnackbarState { open: boolean; message: string; severity: 'success' | 'error' | 'warning' | 'info'; } interface UserDialogState { open: boolean; mode: 'create' | 'edit'; user: User | null; } interface UserFormData { username: string; email: string; password: string; } interface OcrLanguage { code: string; name: string; } interface WebDAVFolderInfo { path: string; total_files: number; supported_files: number; estimated_time_hours: number; total_size_mb: number; } interface WebDAVCrawlEstimate { folders: WebDAVFolderInfo[]; total_files: number; total_supported_files: number; total_estimated_time_hours: number; total_size_mb: number; } interface WebDAVConnectionResult { success: boolean; message: string; server_version?: string; server_type?: string; } // Debounce utility function function useDebounce any>(func: T, delay: number): T { const timeoutRef = React.useRef(null); const debouncedFunc = useCallback((...args: Parameters) => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => func(...args), delay); }, [func, delay]) as T; // Cleanup on unmount useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); return debouncedFunc; } const SettingsPage: React.FC = () => { const { user: currentUser } = useAuth(); const [tabValue, setTabValue] = useState(0); const [settings, setSettings] = useState({ ocrLanguage: 'eng', concurrentOcrJobs: 4, ocrTimeoutSeconds: 300, maxFileSizeMb: 50, allowedFileTypes: ['pdf', 'png', 'jpg', 'jpeg', 'tiff', 'bmp', 'txt'], autoRotateImages: true, enableImagePreprocessing: true, searchResultsPerPage: 25, searchSnippetLength: 200, fuzzySearchThreshold: 0.8, retentionDays: null, enableAutoCleanup: false, enableCompression: false, memoryLimitMb: 512, cpuPriority: 'normal', enableBackgroundOcr: true, ocrPageSegmentationMode: 3, ocrEngineMode: 3, ocrMinConfidence: 30.0, ocrDpi: 300, ocrEnhanceContrast: true, ocrRemoveNoise: true, ocrDetectOrientation: true, ocrWhitelistChars: '', ocrBlacklistChars: '', ocrBrightnessBoost: 0.0, ocrContrastMultiplier: 1.0, ocrNoiseReductionLevel: 1, ocrSharpeningStrength: 0.0, ocrMorphologicalOperations: true, ocrAdaptiveThresholdWindowSize: 15, ocrHistogramEqualization: false, ocrUpscaleFactor: 1.0, ocrMaxImageWidth: 10000, ocrMaxImageHeight: 10000, saveProcessedImages: false, ocrQualityThresholdBrightness: 40.0, ocrQualityThresholdContrast: 0.15, ocrQualityThresholdNoise: 0.3, ocrQualityThresholdSharpness: 0.15, ocrSkipEnhancement: false, }); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: 'success' }); const [userDialog, setUserDialog] = useState({ open: false, mode: 'create', user: null }); const [userForm, setUserForm] = useState({ username: '', email: '', password: '' }); // OCR Admin Controls State const [ocrStatus, setOcrStatus] = useState<{ is_paused: boolean; status: 'paused' | 'running' } | null>(null); const [ocrActionLoading, setOcrActionLoading] = useState(false); const ocrLanguages: OcrLanguage[] = [ { code: 'eng', name: 'English' }, { code: 'spa', name: 'Spanish' }, { code: 'fra', name: 'French' }, { code: 'deu', name: 'German' }, { code: 'ita', name: 'Italian' }, { code: 'por', name: 'Portuguese' }, { code: 'rus', name: 'Russian' }, { code: 'jpn', name: 'Japanese' }, { code: 'chi_sim', name: 'Chinese (Simplified)' }, { code: 'chi_tra', name: 'Chinese (Traditional)' }, { code: 'kor', name: 'Korean' }, { code: 'ara', name: 'Arabic' }, { code: 'hin', name: 'Hindi' }, { code: 'nld', name: 'Dutch' }, { code: 'pol', name: 'Polish' }, ]; useEffect(() => { fetchSettings(); fetchUsers(); fetchOcrStatus(); }, []); const fetchSettings = async (): Promise => { try { const response = await api.get('/settings'); setSettings({ ocrLanguage: response.data.ocr_language || 'eng', concurrentOcrJobs: response.data.concurrent_ocr_jobs || 4, ocrTimeoutSeconds: response.data.ocr_timeout_seconds || 300, maxFileSizeMb: response.data.max_file_size_mb || 50, allowedFileTypes: response.data.allowed_file_types || ['pdf', 'png', 'jpg', 'jpeg', 'tiff', 'bmp', 'txt'], autoRotateImages: response.data.auto_rotate_images !== undefined ? response.data.auto_rotate_images : true, enableImagePreprocessing: response.data.enable_image_preprocessing !== undefined ? response.data.enable_image_preprocessing : true, searchResultsPerPage: response.data.search_results_per_page || 25, searchSnippetLength: response.data.search_snippet_length || 200, fuzzySearchThreshold: response.data.fuzzy_search_threshold || 0.8, retentionDays: response.data.retention_days, enableAutoCleanup: response.data.enable_auto_cleanup || false, enableCompression: response.data.enable_compression || false, memoryLimitMb: response.data.memory_limit_mb || 512, cpuPriority: response.data.cpu_priority || 'normal', enableBackgroundOcr: response.data.enable_background_ocr !== undefined ? response.data.enable_background_ocr : true, ocrPageSegmentationMode: response.data.ocr_page_segmentation_mode || 3, ocrEngineMode: response.data.ocr_engine_mode || 3, ocrMinConfidence: response.data.ocr_min_confidence || 30.0, ocrDpi: response.data.ocr_dpi || 300, ocrEnhanceContrast: response.data.ocr_enhance_contrast !== undefined ? response.data.ocr_enhance_contrast : true, ocrRemoveNoise: response.data.ocr_remove_noise !== undefined ? response.data.ocr_remove_noise : true, ocrDetectOrientation: response.data.ocr_detect_orientation !== undefined ? response.data.ocr_detect_orientation : true, ocrWhitelistChars: response.data.ocr_whitelist_chars || '', ocrBlacklistChars: response.data.ocr_blacklist_chars || '', ocrBrightnessBoost: response.data.ocr_brightness_boost || 0.0, ocrContrastMultiplier: response.data.ocr_contrast_multiplier || 1.0, ocrNoiseReductionLevel: response.data.ocr_noise_reduction_level || 1, ocrSharpeningStrength: response.data.ocr_sharpening_strength || 0.0, ocrMorphologicalOperations: response.data.ocr_morphological_operations !== undefined ? response.data.ocr_morphological_operations : true, ocrAdaptiveThresholdWindowSize: response.data.ocr_adaptive_threshold_window_size || 15, ocrHistogramEqualization: response.data.ocr_histogram_equalization || false, ocrUpscaleFactor: response.data.ocr_upscale_factor || 1.0, ocrMaxImageWidth: response.data.ocr_max_image_width || 10000, ocrMaxImageHeight: response.data.ocr_max_image_height || 10000, saveProcessedImages: response.data.save_processed_images || false, ocrQualityThresholdBrightness: response.data.ocr_quality_threshold_brightness || 40.0, ocrQualityThresholdContrast: response.data.ocr_quality_threshold_contrast || 0.15, ocrQualityThresholdNoise: response.data.ocr_quality_threshold_noise || 0.3, ocrQualityThresholdSharpness: response.data.ocr_quality_threshold_sharpness || 0.15, ocrSkipEnhancement: response.data.ocr_skip_enhancement || false, }); } catch (error: any) { console.error('Error fetching settings:', error); if (error.response?.status !== 404) { showSnackbar('Failed to load settings', 'error'); } } }; const fetchUsers = async (): Promise => { try { const response = await api.get('/users'); setUsers(response.data); } catch (error: any) { console.error('Error fetching users:', error); if (error.response?.status !== 404) { showSnackbar('Failed to load users', 'error'); } } }; const handleSettingsChange = async (key: keyof Settings, value: any): Promise => { try { // Convert camelCase to snake_case for API const snakeCase = (str: string): string => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); const apiKey = snakeCase(key); // Build the update payload with only the changed field const updatePayload = { [apiKey]: value }; await api.put('/settings', updatePayload); // Only update state after successful API call setSettings(prevSettings => ({ ...prevSettings, [key]: value })); // Only show success message for non-text inputs to reduce noise if (typeof value !== 'string') { showSnackbar('Settings updated successfully', 'success'); } } catch (error) { console.error('Error updating settings:', error); showSnackbar('Failed to update settings', 'error'); } }; const handleUserSubmit = async (): Promise => { setLoading(true); try { if (userDialog.mode === 'create') { await api.post('/users', userForm); showSnackbar('User created successfully', 'success'); } else { const { password, ...updateData } = userForm; const payload: any = updateData; if (password) { payload.password = password; } await api.put(`/users/${userDialog.user?.id}`, payload); showSnackbar('User updated successfully', 'success'); } fetchUsers(); handleCloseUserDialog(); } catch (error: any) { console.error('Error saving user:', error); showSnackbar(error.response?.data?.message || 'Failed to save user', 'error'); } finally { setLoading(false); } }; const handleDeleteUser = async (userId: string): Promise => { if (userId === currentUser?.id) { showSnackbar('You cannot delete your own account', 'error'); return; } if (window.confirm('Are you sure you want to delete this user?')) { setLoading(true); try { await api.delete(`/users/${userId}`); showSnackbar('User deleted successfully', 'success'); fetchUsers(); } catch (error) { console.error('Error deleting user:', error); showSnackbar('Failed to delete user', 'error'); } finally { setLoading(false); } } }; const handleOpenUserDialog = (mode: 'create' | 'edit', user: User | null = null): void => { setUserDialog({ open: true, mode, user }); if (mode === 'edit' && user) { setUserForm({ username: user.username, email: user.email, password: '' }); } else { setUserForm({ username: '', email: '', password: '' }); } }; const handleCloseUserDialog = (): void => { setUserDialog({ open: false, mode: 'create', user: null }); setUserForm({ username: '', email: '', password: '' }); }; const showSnackbar = (message: string, severity: SnackbarState['severity']): void => { setSnackbar({ open: true, message, severity }); }; const handleTabChange = (event: React.SyntheticEvent, newValue: number): void => { setTabValue(newValue); }; const handleOcrLanguageChange = (event: SelectChangeEvent): void => { handleSettingsChange('ocrLanguage', event.target.value); }; const handleCpuPriorityChange = (event: SelectChangeEvent): void => { handleSettingsChange('cpuPriority', event.target.value); }; const handleResultsPerPageChange = (event: SelectChangeEvent): void => { handleSettingsChange('searchResultsPerPage', event.target.value); }; const fetchOcrStatus = async (): Promise => { try { const response = await queueService.getOcrStatus(); setOcrStatus(response.data); } catch (error: any) { console.error('Error fetching OCR status:', error); // Don't show error for OCR status since it might not be available for non-admin users } }; const handlePauseOcr = async (): Promise => { setOcrActionLoading(true); try { await queueService.pauseOcr(); showSnackbar('OCR processing paused successfully', 'success'); fetchOcrStatus(); // Refresh status } catch (error: any) { console.error('Error pausing OCR:', error); if (error.response?.status === 403) { showSnackbar('Admin access required to pause OCR processing', 'error'); } else { showSnackbar('Failed to pause OCR processing', 'error'); } } finally { setOcrActionLoading(false); } }; const handleResumeOcr = async (): Promise => { setOcrActionLoading(true); try { await queueService.resumeOcr(); showSnackbar('OCR processing resumed successfully', 'success'); fetchOcrStatus(); // Refresh status } catch (error: any) { console.error('Error resuming OCR:', error); if (error.response?.status === 403) { showSnackbar('Admin access required to resume OCR processing', 'error'); } else { showSnackbar('Failed to resume OCR processing', 'error'); } } finally { setOcrActionLoading(false); } }; return ( Settings {tabValue === 0 && ( General Settings OCR Configuration OCR Language handleSettingsChange('concurrentOcrJobs', parseInt(e.target.value))} disabled={loading} inputProps={{ min: 1, max: 16 }} helperText="Number of OCR jobs that can run simultaneously" /> handleSettingsChange('ocrTimeoutSeconds', parseInt(e.target.value))} disabled={loading} inputProps={{ min: 30, max: 3600 }} helperText="Maximum time for OCR processing per file" /> CPU Priority {/* Admin OCR Controls */} OCR Processing Controls (Admin Only) Control OCR processing to manage CPU usage and allow users to use the application without performance impact. {ocrStatus && ( : } size="medium" /> {ocrStatus.is_paused ? 'OCR processing is paused. No new jobs will be processed.' : 'OCR processing is active. Documents will be processed automatically.'} )} {ocrStatus?.is_paused && ( OCR Processing Paused
New documents will not be processed for OCR text extraction until processing is resumed. Users can still upload and view documents, but search functionality may be limited.
)}
File Processing handleSettingsChange('maxFileSizeMb', parseInt(e.target.value))} disabled={loading} inputProps={{ min: 1, max: 500 }} helperText="Maximum allowed file size for uploads" /> handleSettingsChange('memoryLimitMb', parseInt(e.target.value))} disabled={loading} inputProps={{ min: 128, max: 4096 }} helperText="Memory limit per OCR job" /> handleSettingsChange('autoRotateImages', e.target.checked)} disabled={loading} /> } label="Auto-rotate Images" /> Automatically detect and correct image orientation handleSettingsChange('enableImagePreprocessing', e.target.checked)} disabled={loading} /> } label="Enable Image Preprocessing" /> Enhance images for better OCR accuracy (deskew, denoise, contrast) handleSettingsChange('enableBackgroundOcr', e.target.checked)} disabled={loading} /> } label="Enable Background OCR" /> Process OCR in the background after file upload Search Configuration Results Per Page handleSettingsChange('searchSnippetLength', parseInt(e.target.value))} disabled={loading} inputProps={{ min: 50, max: 500 }} helperText="Characters to show in search result previews" /> handleSettingsChange('fuzzySearchThreshold', parseFloat(e.target.value))} disabled={loading} inputProps={{ min: 0, max: 1, step: 0.1 }} helperText="Tolerance for spelling mistakes (0.0-1.0)" /> Storage Management handleSettingsChange('retentionDays', e.target.value ? parseInt(e.target.value) : null)} disabled={loading} inputProps={{ min: 1 }} helperText="Auto-delete documents after X days (leave empty to disable)" /> handleSettingsChange('enableAutoCleanup', e.target.checked)} disabled={loading} /> } label="Enable Auto Cleanup" /> Automatically remove orphaned files and clean up storage handleSettingsChange('enableCompression', e.target.checked)} disabled={loading} /> } label="Enable Compression" /> Compress stored documents to save disk space
)} {tabValue === 1 && ( OCR Image Processing Settings Enhancement Controls handleSettingsChange('ocrSkipEnhancement', e.target.checked)} /> } label="Skip All Image Enhancement (Use Original Images Only)" sx={{ mb: 2 }} /> handleSettingsChange('ocrBrightnessBoost', parseFloat(e.target.value) || 0)} helperText="Manual brightness adjustment (0 = auto, >0 = boost amount)" inputProps={{ step: 0.1, min: 0, max: 100 }} /> handleSettingsChange('ocrContrastMultiplier', parseFloat(e.target.value) || 1)} helperText="Manual contrast adjustment (1.0 = auto, >1.0 = increase)" inputProps={{ step: 0.1, min: 0.1, max: 5 }} /> Noise Reduction Level handleSettingsChange('ocrSharpeningStrength', parseFloat(e.target.value) || 0)} helperText="Image sharpening amount (0 = auto, >0 = manual)" inputProps={{ step: 0.1, min: 0, max: 2 }} /> Quality Thresholds (when to apply enhancements) handleSettingsChange('ocrQualityThresholdBrightness', parseFloat(e.target.value) || 40)} helperText="Enhance if brightness below this value (0-255)" inputProps={{ step: 1, min: 0, max: 255 }} /> handleSettingsChange('ocrQualityThresholdContrast', parseFloat(e.target.value) || 0.15)} helperText="Enhance if contrast below this value (0-1)" inputProps={{ step: 0.01, min: 0, max: 1 }} /> handleSettingsChange('ocrQualityThresholdNoise', parseFloat(e.target.value) || 0.3)} helperText="Enhance if noise above this value (0-1)" inputProps={{ step: 0.01, min: 0, max: 1 }} /> handleSettingsChange('ocrQualityThresholdSharpness', parseFloat(e.target.value) || 0.15)} helperText="Enhance if sharpness below this value (0-1)" inputProps={{ step: 0.01, min: 0, max: 1 }} /> Advanced Processing Options handleSettingsChange('ocrMorphologicalOperations', e.target.checked)} /> } label="Morphological Operations (text cleanup)" /> handleSettingsChange('ocrHistogramEqualization', e.target.checked)} /> } label="Histogram Equalization" /> handleSettingsChange('saveProcessedImages', e.target.checked)} /> } label="Save Processed Images for Review" /> handleSettingsChange('ocrAdaptiveThresholdWindowSize', parseInt(e.target.value) || 15)} helperText="Window size for contrast enhancement (odd number)" inputProps={{ step: 2, min: 3, max: 101 }} /> Image Size and Scaling handleSettingsChange('ocrMaxImageWidth', parseInt(e.target.value) || 10000)} helperText="Maximum image width in pixels" inputProps={{ step: 100, min: 100, max: 50000 }} /> handleSettingsChange('ocrMaxImageHeight', parseInt(e.target.value) || 10000)} helperText="Maximum image height in pixels" inputProps={{ step: 100, min: 100, max: 50000 }} /> handleSettingsChange('ocrUpscaleFactor', parseFloat(e.target.value) || 1.0)} helperText="Image scaling factor (1.0 = no scaling)" inputProps={{ step: 0.1, min: 0.1, max: 5 }} /> )} {tabValue === 2 && ( User Management Username Email Created At Actions {users.map((user) => ( {user.username} {user.email} {new Date(user.created_at).toLocaleDateString()} handleOpenUserDialog('edit', user)} disabled={loading} > handleDeleteUser(user.id)} disabled={loading || user.id === currentUser?.id} > ))}
)}
{userDialog.mode === 'create' ? 'Create New User' : 'Edit User'} setUserForm({ ...userForm, username: e.target.value })} required /> setUserForm({ ...userForm, email: e.target.value })} required /> setUserForm({ ...userForm, password: e.target.value })} required={userDialog.mode === 'create'} /> setSnackbar({ ...snackbar, open: false })} > setSnackbar({ ...snackbar, open: false })} severity={snackbar.severity} sx={{ width: '100%' }} > {snackbar.message}
); }; export default SettingsPage;