diff --git a/.editorconfig b/.editorconfig index d45455a7a..9faac4bf7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -24,7 +24,7 @@ indent_size = 2 insert_final_newline = false trim_trailing_whitespace = false -[*.js] +[{*.js,*.jsx,*.ts,*.tsx}] indent_size = 2 [*.css] diff --git a/CLAUDE.md b/CLAUDE.md index 8bdd7c235..be4e92201 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,12 +59,73 @@ Frontend designed for **stateful document processing**: Without cleanup: browser crashes with memory leaks. #### Tool Development -- **Pattern**: Follow `src/tools/Split.tsx` as reference implementation -- **File Access**: Tools receive `selectedFiles` prop (computed from activeFiles based on user selection) -- **File Selection**: Users select files in FileEditor (tool mode) → stored as IDs → computed to File objects for tools -- **Integration**: All files are part of FileContext ecosystem - automatic memory management and operation tracking -- **Parameters**: Tool parameter handling patterns still being standardized -- **Preview Integration**: Tools can implement preview functionality (see Split tool's thumbnail preview) + +**Architecture**: Modular hook-based system with clear separation of concerns: + +- **useToolOperation** (`frontend/src/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook + - Coordinates all tool operations with consistent interface + - Integrates with FileContext for operation tracking + - Handles validation, error handling, and UI state management + +- **Supporting Hooks**: + - **useToolState**: UI state management (loading, progress, error, files) + - **useToolApiCalls**: HTTP requests and file processing + - **useToolResources**: Blob URLs, thumbnails, ZIP downloads + +- **Utilities**: + - **toolErrorHandler**: Standardized error extraction and i18n support + - **toolResponseProcessor**: API response handling (single/zip/custom) + - **toolOperationTracker**: FileContext integration utilities + +**Three Tool Patterns**: + +**Pattern 1: Single-File Tools** (Individual processing) +- Backend processes one file per API call +- Set `multiFileEndpoint: false` +- Examples: Compress, Rotate +```typescript +return useToolOperation({ + operationType: 'compress', + endpoint: '/api/v1/misc/compress-pdf', + buildFormData: (params, file: File) => { /* single file */ }, + multiFileEndpoint: false, + filePrefix: 'compressed_' +}); +``` + +**Pattern 2: Multi-File Tools** (Batch processing) +- Backend accepts `MultipartFile[]` arrays in single API call +- Set `multiFileEndpoint: true` +- Examples: Split, Merge, Overlay +```typescript +return useToolOperation({ + operationType: 'split', + endpoint: '/api/v1/general/split-pages', + buildFormData: (params, files: File[]) => { /* all files */ }, + multiFileEndpoint: true, + filePrefix: 'split_' +}); +``` + +**Pattern 3: Complex Tools** (Custom processing) +- Tools with complex routing logic or non-standard processing +- Provide `customProcessor` for full control +- Examples: Convert, OCR +```typescript +return useToolOperation({ + operationType: 'convert', + customProcessor: async (params, files) => { /* custom logic */ }, + filePrefix: 'converted_' +}); +``` + +**Benefits**: +- **No Timeouts**: Operations run until completion (supports 100GB+ files) +- **Consistent**: All tools follow same pattern and interface +- **Maintainable**: Single responsibility hooks, easy to test and modify +- **i18n Ready**: Built-in internationalization support +- **Type Safe**: Full TypeScript support with generic interfaces +- **Memory Safe**: Automatic resource cleanup and blob URL management ## Architecture Overview @@ -126,7 +187,10 @@ Without cleanup: browser crashes with memory leaks. - **Core Status**: React SPA architecture complete with multi-tool workflow support - **State Management**: FileContext handles all file operations and tool navigation - **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+) -- **Tool Integration**: Standardized tool interface - see `src/tools/Split.tsx` as reference +- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator + - Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources` + - Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker` + - Pattern: Each tool creates focused operation hook, UI consumes state/actions - **Preview System**: Tool results can be previewed without polluting file context (Split tool example) - **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing @@ -141,7 +205,7 @@ Without cleanup: browser crashes with memory leaks. - **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation - **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling - **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code -- **Tool Development**: New tools should follow Split tool pattern (`src/tools/Split.tsx`) +- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`) - **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes - **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation) diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index dbfceddea..976b4ae06 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1774,7 +1774,25 @@ "storageError": "Storage error occurred", "storageLow": "Storage is running low. Consider removing old files.", "supportMessage": "Powered by browser database storage for unlimited capacity", - "noFileSelected": "No files selected" + "noFileSelected": "No files selected", + "searchFiles": "Search files...", + "recent": "Recent", + "localFiles": "Local Files", + "googleDrive": "Google Drive", + "googleDriveShort": "Drive", + "myFiles": "My Files", + "noRecentFiles": "No recent files found", + "dropFilesHint": "Drop files here to upload", + "googleDriveNotAvailable": "Google Drive integration not available", + "openFiles": "Open Files", + "openFile": "Open File", + "details": "File Details", + "fileName": "Name", + "fileFormat": "Format", + "fileSize": "Size", + "fileVersion": "Version", + "totalSelected": "Total Selected", + "dropFilesHere": "Drop files here" }, "storage": { "temporaryNotice": "Files are stored temporarily in your browser and may be cleared automatically", diff --git a/frontend/src/components/FileManager.tsx b/frontend/src/components/FileManager.tsx new file mode 100644 index 000000000..02f9af5e4 --- /dev/null +++ b/frontend/src/components/FileManager.tsx @@ -0,0 +1,168 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import { Modal } from '@mantine/core'; +import { Dropzone } from '@mantine/dropzone'; +import { FileWithUrl } from '../types/file'; +import { useFileManager } from '../hooks/useFileManager'; +import { useFilesModalContext } from '../contexts/FilesModalContext'; +import { Tool } from '../types/tool'; +import MobileLayout from './fileManager/MobileLayout'; +import DesktopLayout from './fileManager/DesktopLayout'; +import DragOverlay from './fileManager/DragOverlay'; +import { FileManagerProvider } from '../contexts/FileManagerContext'; + +interface FileManagerProps { + selectedTool?: Tool | null; +} + +const FileManager: React.FC = ({ selectedTool }) => { + const { isFilesModalOpen, closeFilesModal, onFilesSelect } = useFilesModalContext(); + const [recentFiles, setRecentFiles] = useState([]); + const [isDragging, setIsDragging] = useState(false); + const [isMobile, setIsMobile] = useState(false); + + const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile } = useFileManager(); + + // File management handlers + const isFileSupported = useCallback((fileName: string) => { + if (!selectedTool?.supportedFormats) return true; + const extension = fileName.split('.').pop()?.toLowerCase(); + return selectedTool.supportedFormats.includes(extension || ''); + }, [selectedTool?.supportedFormats]); + + const refreshRecentFiles = useCallback(async () => { + const files = await loadRecentFiles(); + setRecentFiles(files); + }, [loadRecentFiles]); + + const handleFilesSelected = useCallback(async (files: FileWithUrl[]) => { + try { + const fileObjects = await Promise.all( + files.map(async (fileWithUrl) => { + return await convertToFile(fileWithUrl); + }) + ); + onFilesSelect(fileObjects); + } catch (error) { + console.error('Failed to process selected files:', error); + } + }, [convertToFile, onFilesSelect]); + + const handleNewFileUpload = useCallback(async (files: File[]) => { + if (files.length > 0) { + try { + // Files will get IDs assigned through onFilesSelect -> FileContext addFiles + onFilesSelect(files); + await refreshRecentFiles(); + } catch (error) { + console.error('Failed to process dropped files:', error); + } + } + }, [onFilesSelect, refreshRecentFiles]); + + const handleRemoveFileByIndex = useCallback(async (index: number) => { + await handleRemoveFile(index, recentFiles, setRecentFiles); + }, [handleRemoveFile, recentFiles]); + + useEffect(() => { + const checkMobile = () => setIsMobile(window.innerWidth < 1030); + checkMobile(); + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); + }, []); + + useEffect(() => { + if (isFilesModalOpen) { + refreshRecentFiles(); + } else { + // Reset state when modal is closed + setIsDragging(false); + } + }, [isFilesModalOpen, refreshRecentFiles]); + + // Cleanup any blob URLs when component unmounts + useEffect(() => { + return () => { + // Clean up blob URLs from recent files + recentFiles.forEach(file => { + if (file.url && file.url.startsWith('blob:')) { + URL.revokeObjectURL(file.url); + } + }); + }; + }, [recentFiles]); + + // Modal size constants for consistent scaling + const modalHeight = '80vh'; + const modalWidth = isMobile ? '100%' : '80vw'; + const modalMaxWidth = isMobile ? '100%' : '1200px'; + const modalMaxHeight = '1200px'; + const modalMinWidth = isMobile ? '320px' : '800px'; + + return ( + +
+ setIsDragging(true)} + onDragLeave={() => setIsDragging(false)} + accept={["*/*"]} + multiple={true} + activateOnClick={false} + style={{ + height: '100%', + width: '100%', + border: 'none', + borderRadius: '30px', + backgroundColor: 'var(--bg-file-manager)' + }} + styles={{ + inner: { pointerEvents: 'all' } + }} + > + + {isMobile ? : } + + + + +
+
+ ); +}; + +export default FileManager; \ No newline at end of file diff --git a/frontend/src/components/fileManagement/StorageStatsCard.tsx b/frontend/src/components/fileManagement/StorageStatsCard.tsx deleted file mode 100644 index 2d2488712..000000000 --- a/frontend/src/components/fileManagement/StorageStatsCard.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from "react"; -import { Card, Group, Text, Button, Progress, Alert, Stack } from "@mantine/core"; -import { useTranslation } from "react-i18next"; -import StorageIcon from "@mui/icons-material/Storage"; -import DeleteIcon from "@mui/icons-material/Delete"; -import WarningIcon from "@mui/icons-material/Warning"; -import { StorageStats } from "../../services/fileStorage"; -import { formatFileSize } from "../../utils/fileUtils"; -import { getStorageUsagePercent } from "../../utils/storageUtils"; -import { StorageConfig } from "../../types/file"; - -interface StorageStatsCardProps { - storageStats: StorageStats | null; - filesCount: number; - onClearAll: () => void; - onReloadFiles: () => void; - storageConfig: StorageConfig; -} - -const StorageStatsCard = ({ - storageStats, - filesCount, - onClearAll, - onReloadFiles, - storageConfig, -}: StorageStatsCardProps) => { - const { t } = useTranslation(); - - if (!storageStats) return null; - - const storageUsagePercent = getStorageUsagePercent(storageStats); - const totalUsed = storageStats.totalSize || storageStats.used; - const hardLimitPercent = (totalUsed / storageConfig.maxTotalStorage) * 100; - const isNearLimit = hardLimitPercent >= storageConfig.warningThreshold * 100; - - return ( - - - - -
- - {t("storage.storageUsed", "Storage used")}: {formatFileSize(totalUsed)} / {formatFileSize(storageConfig.maxTotalStorage)} - - 60 ? "yellow" : "blue"} - size="sm" - mt={4} - /> - - - {storageStats.fileCount} files • {t("storage.approximateSize", "Approximate size")} - - - {Math.round(hardLimitPercent)}% used - - - {isNearLimit && ( - - {t("storage.storageFull", "Storage is nearly full. Consider removing some files.")} - - )} -
- - {filesCount > 0 && ( - - )} - - -
-
-
- ); -}; - -export default StorageStatsCard; diff --git a/frontend/src/components/fileManager/CompactFileDetails.tsx b/frontend/src/components/fileManager/CompactFileDetails.tsx new file mode 100644 index 000000000..7f7c410b7 --- /dev/null +++ b/frontend/src/components/fileManager/CompactFileDetails.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { Stack, Box, Text, Button, ActionIcon, Center } from '@mantine/core'; +import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import { useTranslation } from 'react-i18next'; +import { getFileSize } from '../../utils/fileUtils'; +import { FileWithUrl } from '../../types/file'; + +interface CompactFileDetailsProps { + currentFile: FileWithUrl | null; + thumbnail: string | null; + selectedFiles: FileWithUrl[]; + currentFileIndex: number; + numberOfFiles: number; + isAnimating: boolean; + onPrevious: () => void; + onNext: () => void; + onOpenFiles: () => void; +} + +const CompactFileDetails: React.FC = ({ + currentFile, + thumbnail, + selectedFiles, + currentFileIndex, + numberOfFiles, + isAnimating, + onPrevious, + onNext, + onOpenFiles +}) => { + const { t } = useTranslation(); + const hasSelection = selectedFiles.length > 0; + const hasMultipleFiles = numberOfFiles > 1; + + return ( + + {/* Compact mobile layout */} + + {/* Small preview */} + + {currentFile && thumbnail ? ( + {currentFile.name} + ) : currentFile ? ( +
+ +
+ ) : null} +
+ + {/* File info */} + + + {currentFile ? currentFile.name : 'No file selected'} + + + {currentFile ? getFileSize(currentFile) : ''} + {selectedFiles.length > 1 && ` • ${selectedFiles.length} files`} + + {hasMultipleFiles && ( + + {currentFileIndex + 1} of {selectedFiles.length} + + )} + + + {/* Navigation arrows for multiple files */} + {hasMultipleFiles && ( + + + + + + + + + )} +
+ + {/* Action Button */} + +
+ ); +}; + +export default CompactFileDetails; \ No newline at end of file diff --git a/frontend/src/components/fileManager/DesktopLayout.tsx b/frontend/src/components/fileManager/DesktopLayout.tsx new file mode 100644 index 000000000..be701ff20 --- /dev/null +++ b/frontend/src/components/fileManager/DesktopLayout.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Grid } from '@mantine/core'; +import FileSourceButtons from './FileSourceButtons'; +import FileDetails from './FileDetails'; +import SearchInput from './SearchInput'; +import FileListArea from './FileListArea'; +import HiddenFileInput from './HiddenFileInput'; +import { useFileManagerContext } from '../../contexts/FileManagerContext'; + +const DesktopLayout: React.FC = () => { + const { + activeSource, + recentFiles, + modalHeight, + } = useFileManagerContext(); + + return ( + + {/* Column 1: File Sources */} + + + + + {/* Column 2: File List */} + +
+ {activeSource === 'recent' && ( +
+ +
+ )} + +
+ 0 ? modalHeight : '100%', + backgroundColor: 'transparent', + border: 'none', + borderRadius: 0 + }} + /> +
+
+
+ + {/* Column 3: File Details */} + +
+ +
+
+ + {/* Hidden file input for local file selection */} + +
+ ); +}; + +export default DesktopLayout; \ No newline at end of file diff --git a/frontend/src/components/fileManager/DragOverlay.tsx b/frontend/src/components/fileManager/DragOverlay.tsx new file mode 100644 index 000000000..976bb940e --- /dev/null +++ b/frontend/src/components/fileManager/DragOverlay.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Stack, Text, useMantineTheme, alpha } from '@mantine/core'; +import UploadFileIcon from '@mui/icons-material/UploadFile'; +import { useTranslation } from 'react-i18next'; + +interface DragOverlayProps { + isVisible: boolean; +} + +const DragOverlay: React.FC = ({ isVisible }) => { + const { t } = useTranslation(); + const theme = useMantineTheme(); + + if (!isVisible) return null; + + return ( +
+ + + + {t('fileManager.dropFilesHere', 'Drop files here to upload')} + + +
+ ); +}; + +export default DragOverlay; \ No newline at end of file diff --git a/frontend/src/components/fileManager/FileDetails.tsx b/frontend/src/components/fileManager/FileDetails.tsx new file mode 100644 index 000000000..9673d06ad --- /dev/null +++ b/frontend/src/components/fileManager/FileDetails.tsx @@ -0,0 +1,116 @@ +import React, { useState, useEffect } from 'react'; +import { Stack, Button } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { useIndexedDBThumbnail } from '../../hooks/useIndexedDBThumbnail'; +import { useFileManagerContext } from '../../contexts/FileManagerContext'; +import FilePreview from './FilePreview'; +import FileInfoCard from './FileInfoCard'; +import CompactFileDetails from './CompactFileDetails'; + +interface FileDetailsProps { + compact?: boolean; +} + +const FileDetails: React.FC = ({ + compact = false +}) => { + const { selectedFiles, onOpenFiles, modalHeight } = useFileManagerContext(); + const { t } = useTranslation(); + const [currentFileIndex, setCurrentFileIndex] = useState(0); + const [isAnimating, setIsAnimating] = useState(false); + + // Get the currently displayed file + const currentFile = selectedFiles.length > 0 ? selectedFiles[currentFileIndex] : null; + const hasSelection = selectedFiles.length > 0; + const hasMultipleFiles = selectedFiles.length > 1; + + // Use IndexedDB hook for the current file + const { thumbnail: currentThumbnail } = useIndexedDBThumbnail(currentFile); + + // Get thumbnail for current file + const getCurrentThumbnail = () => { + return currentThumbnail; + }; + + const handlePrevious = () => { + if (isAnimating) return; + setIsAnimating(true); + setTimeout(() => { + setCurrentFileIndex(prev => prev > 0 ? prev - 1 : selectedFiles.length - 1); + setIsAnimating(false); + }, 150); + }; + + const handleNext = () => { + if (isAnimating) return; + setIsAnimating(true); + setTimeout(() => { + setCurrentFileIndex(prev => prev < selectedFiles.length - 1 ? prev + 1 : 0); + setIsAnimating(false); + }, 150); + }; + + // Reset index when selection changes + React.useEffect(() => { + if (currentFileIndex >= selectedFiles.length) { + setCurrentFileIndex(0); + } + }, [selectedFiles.length, currentFileIndex]); + + if (compact) { + return ( + + ); + } + + return ( + + {/* Section 1: Thumbnail Preview */} + + + {/* Section 2: File Details */} + + + + + ); +}; + +export default FileDetails; \ No newline at end of file diff --git a/frontend/src/components/fileManager/FileInfoCard.tsx b/frontend/src/components/fileManager/FileInfoCard.tsx new file mode 100644 index 000000000..7e69dd2ed --- /dev/null +++ b/frontend/src/components/fileManager/FileInfoCard.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { Stack, Card, Box, Text, Badge, Group, Divider, ScrollArea } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { detectFileExtension, getFileSize } from '../../utils/fileUtils'; +import { FileWithUrl } from '../../types/file'; + +interface FileInfoCardProps { + currentFile: FileWithUrl | null; + modalHeight: string; +} + +const FileInfoCard: React.FC = ({ + currentFile, + modalHeight +}) => { + const { t } = useTranslation(); + + return ( + + + + {t('fileManager.details', 'File Details')} + + + + + + {t('fileManager.fileName', 'Name')} + + {currentFile ? currentFile.name : ''} + + + + + + {t('fileManager.fileFormat', 'Format')} + {currentFile ? ( + + {detectFileExtension(currentFile.name).toUpperCase()} + + ) : ( + + )} + + + + + {t('fileManager.fileSize', 'Size')} + + {currentFile ? getFileSize(currentFile) : ''} + + + + + + {t('fileManager.fileVersion', 'Version')} + + {currentFile ? '1.0' : ''} + + + + + + ); +}; + +export default FileInfoCard; \ No newline at end of file diff --git a/frontend/src/components/fileManager/FileListArea.tsx b/frontend/src/components/fileManager/FileListArea.tsx new file mode 100644 index 000000000..8e1975137 --- /dev/null +++ b/frontend/src/components/fileManager/FileListArea.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { Center, ScrollArea, Text, Stack } from '@mantine/core'; +import CloudIcon from '@mui/icons-material/Cloud'; +import HistoryIcon from '@mui/icons-material/History'; +import { useTranslation } from 'react-i18next'; +import FileListItem from './FileListItem'; +import { useFileManagerContext } from '../../contexts/FileManagerContext'; + +interface FileListAreaProps { + scrollAreaHeight: string; + scrollAreaStyle?: React.CSSProperties; +} + +const FileListArea: React.FC = ({ + scrollAreaHeight, + scrollAreaStyle = {}, +}) => { + const { + activeSource, + recentFiles, + filteredFiles, + selectedFileIds, + onFileSelect, + onFileRemove, + onFileDoubleClick, + isFileSupported, + } = useFileManagerContext(); + const { t } = useTranslation(); + + if (activeSource === 'recent') { + return ( + + + {recentFiles.length === 0 ? ( +
+ + + {t('fileManager.noRecentFiles', 'No recent files')} + + {t('fileManager.dropFilesHint', 'Drop files anywhere to upload')} + + +
+ ) : ( + filteredFiles.map((file, index) => ( + onFileSelect(file)} + onRemove={() => onFileRemove(index)} + onDoubleClick={() => onFileDoubleClick(file)} + /> + )) + )} +
+
+ ); + } + + // Google Drive placeholder + return ( +
+ + + {t('fileManager.googleDriveNotAvailable', 'Google Drive integration coming soon')} + +
+ ); +}; + +export default FileListArea; \ No newline at end of file diff --git a/frontend/src/components/fileManager/FileListItem.tsx b/frontend/src/components/fileManager/FileListItem.tsx new file mode 100644 index 000000000..147133009 --- /dev/null +++ b/frontend/src/components/fileManager/FileListItem.tsx @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; +import { Group, Box, Text, ActionIcon, Checkbox, Divider } from '@mantine/core'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { getFileSize, getFileDate } from '../../utils/fileUtils'; +import { FileWithUrl } from '../../types/file'; + +interface FileListItemProps { + file: FileWithUrl; + isSelected: boolean; + isSupported: boolean; + onSelect: () => void; + onRemove: () => void; + onDoubleClick?: () => void; + isLast?: boolean; +} + +const FileListItem: React.FC = ({ + file, + isSelected, + isSupported, + onSelect, + onRemove, + onDoubleClick +}) => { + const [isHovered, setIsHovered] = useState(false); + + return ( + <> + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + + {}} // Handled by parent onClick + size="sm" + pl="sm" + pr="xs" + styles={{ + input: { + cursor: 'pointer' + } + }} + /> + + + + {file.name} + {getFileSize(file)} • {getFileDate(file)} + + {/* Delete button - fades in/out on hover */} + { e.stopPropagation(); onRemove(); }} + style={{ + opacity: isHovered ? 1 : 0, + transform: isHovered ? 'scale(1)' : 'scale(0.8)', + transition: 'opacity 0.3s ease, transform 0.3s ease', + pointerEvents: isHovered ? 'auto' : 'none' + }} + > + + + + + { } + + ); +}; + +export default FileListItem; \ No newline at end of file diff --git a/frontend/src/components/fileManager/FilePreview.tsx b/frontend/src/components/fileManager/FilePreview.tsx new file mode 100644 index 000000000..deb4cc67b --- /dev/null +++ b/frontend/src/components/fileManager/FilePreview.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { Box, Center, ActionIcon, Image } from '@mantine/core'; +import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import { FileWithUrl } from '../../types/file'; + +interface FilePreviewProps { + currentFile: FileWithUrl | null; + thumbnail: string | null; + numberOfFiles: number; + isAnimating: boolean; + modalHeight: string; + onPrevious: () => void; + onNext: () => void; +} + +const FilePreview: React.FC = ({ + currentFile, + thumbnail, + numberOfFiles, + isAnimating, + modalHeight, + onPrevious, + onNext +}) => { + const hasMultipleFiles = numberOfFiles > 1; + // Common style objects + const navigationArrowStyle = { + position: 'absolute' as const, + top: '50%', + transform: 'translateY(-50%)', + zIndex: 10 + }; + + const stackDocumentBaseStyle = { + position: 'absolute' as const, + width: '100%', + height: '100%' + }; + + const animationStyle = { + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + transform: isAnimating ? 'scale(0.95) translateX(1.25rem)' : 'scale(1) translateX(0)', + opacity: isAnimating ? 0.7 : 1 + }; + + const mainDocumentShadow = '0 6px 16px rgba(0, 0, 0, 0.2)'; + const stackDocumentShadows = { + back: '0 2px 8px rgba(0, 0, 0, 0.1)', + middle: '0 3px 10px rgba(0, 0, 0, 0.12)' + }; + + return ( + + + {/* Left Navigation Arrow */} + {hasMultipleFiles && ( + + + + )} + + {/* Document Stack Container */} + + {/* Background documents (stack effect) */} + {/* Show 2 shadow pages for 3+ files */} + {numberOfFiles >= 3 && ( + + )} + + {/* Show 1 shadow page for 2+ files */} + {numberOfFiles >= 2 && ( + + )} + + {/* Main document */} + {currentFile && thumbnail ? ( + {currentFile.name} + ) : currentFile ? ( +
+ +
+ ) : null} +
+ + {/* Right Navigation Arrow */} + {hasMultipleFiles && ( + + + + )} +
+
+ ); +}; + +export default FilePreview; \ No newline at end of file diff --git a/frontend/src/components/fileManager/FileSourceButtons.tsx b/frontend/src/components/fileManager/FileSourceButtons.tsx new file mode 100644 index 000000000..a6870a661 --- /dev/null +++ b/frontend/src/components/fileManager/FileSourceButtons.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { Stack, Text, Button, Group } from '@mantine/core'; +import HistoryIcon from '@mui/icons-material/History'; +import FolderIcon from '@mui/icons-material/Folder'; +import CloudIcon from '@mui/icons-material/Cloud'; +import { useTranslation } from 'react-i18next'; +import { useFileManagerContext } from '../../contexts/FileManagerContext'; + +interface FileSourceButtonsProps { + horizontal?: boolean; +} + +const FileSourceButtons: React.FC = ({ + horizontal = false +}) => { + const { activeSource, onSourceChange, onLocalFileClick } = useFileManagerContext(); + const { t } = useTranslation(); + + const buttonProps = { + variant: (source: string) => activeSource === source ? 'filled' : 'subtle', + getColor: (source: string) => activeSource === source ? 'var(--mantine-color-gray-2)' : undefined, + getStyles: (source: string) => ({ + root: { + backgroundColor: activeSource === source ? undefined : 'transparent', + color: activeSource === source ? 'var(--mantine-color-gray-9)' : 'var(--mantine-color-gray-6)', + border: 'none', + '&:hover': { + backgroundColor: activeSource === source ? undefined : 'var(--mantine-color-gray-0)' + } + } + }) + }; + + const buttons = ( + <> + + + + + + + ); + + if (horizontal) { + return ( + + {buttons} + + ); + } + + return ( + + + {t('fileManager.myFiles', 'My Files')} + + {buttons} + + ); +}; + +export default FileSourceButtons; \ No newline at end of file diff --git a/frontend/src/components/fileManager/HiddenFileInput.tsx b/frontend/src/components/fileManager/HiddenFileInput.tsx new file mode 100644 index 000000000..6f2834267 --- /dev/null +++ b/frontend/src/components/fileManager/HiddenFileInput.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useFileManagerContext } from '../../contexts/FileManagerContext'; + +const HiddenFileInput: React.FC = () => { + const { fileInputRef, onFileInputChange } = useFileManagerContext(); + + return ( + + ); +}; + +export default HiddenFileInput; \ No newline at end of file diff --git a/frontend/src/components/fileManager/MobileLayout.tsx b/frontend/src/components/fileManager/MobileLayout.tsx new file mode 100644 index 000000000..30d1ad6b9 --- /dev/null +++ b/frontend/src/components/fileManager/MobileLayout.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { Stack, Box } from '@mantine/core'; +import FileSourceButtons from './FileSourceButtons'; +import FileDetails from './FileDetails'; +import SearchInput from './SearchInput'; +import FileListArea from './FileListArea'; +import HiddenFileInput from './HiddenFileInput'; +import { useFileManagerContext } from '../../contexts/FileManagerContext'; + +const MobileLayout: React.FC = () => { + const { + activeSource, + selectedFiles, + modalHeight, + } = useFileManagerContext(); + + // Calculate the height more accurately based on actual content + const calculateFileListHeight = () => { + // Base modal height minus padding and gaps + const baseHeight = `calc(${modalHeight} - 2rem)`; // Account for Stack padding + + // Estimate heights of fixed components + const fileSourceHeight = '3rem'; // FileSourceButtons height + const fileDetailsHeight = selectedFiles.length > 0 ? '10rem' : '8rem'; // FileDetails compact height + const searchHeight = activeSource === 'recent' ? '3rem' : '0rem'; // SearchInput height + const gapHeight = activeSource === 'recent' ? '3rem' : '2rem'; // Stack gaps + + return `calc(${baseHeight} - ${fileSourceHeight} - ${fileDetailsHeight} - ${searchHeight} - ${gapHeight})`; + }; + + return ( + + {/* Section 1: File Sources - Fixed at top */} + + + + + + + + + {/* Section 3 & 4: Search Bar + File List - Unified background extending to modal edge */} + + {activeSource === 'recent' && ( + + + + )} + + + + + + + {/* Hidden file input for local file selection */} + + + ); +}; + +export default MobileLayout; \ No newline at end of file diff --git a/frontend/src/components/fileManager/SearchInput.tsx b/frontend/src/components/fileManager/SearchInput.tsx new file mode 100644 index 000000000..f47da0dca --- /dev/null +++ b/frontend/src/components/fileManager/SearchInput.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { TextInput } from '@mantine/core'; +import SearchIcon from '@mui/icons-material/Search'; +import { useTranslation } from 'react-i18next'; +import { useFileManagerContext } from '../../contexts/FileManagerContext'; + +interface SearchInputProps { + style?: React.CSSProperties; +} + +const SearchInput: React.FC = ({ style }) => { + const { t } = useTranslation(); + const { searchTerm, onSearchChange } = useFileManagerContext(); + + return ( + } + value={searchTerm} + onChange={(e) => onSearchChange(e.target.value)} + + style={{ padding: '0.5rem', ...style }} + styles={{ + input: { + border: 'none', + backgroundColor: 'transparent' + } + }} + /> + ); +}; + +export default SearchInput; \ No newline at end of file diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx new file mode 100644 index 000000000..b0c984ee8 --- /dev/null +++ b/frontend/src/components/layout/Workbench.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import { Box } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; +import { useWorkbenchState, useToolSelection } from '../../contexts/ToolWorkflowContext'; +import { useFileHandler } from '../../hooks/useFileHandler'; +import { useFileContext } from '../../contexts/FileContext'; + +import TopControls from '../shared/TopControls'; +import FileEditor from '../fileEditor/FileEditor'; +import PageEditor from '../pageEditor/PageEditor'; +import PageEditorControls from '../pageEditor/PageEditorControls'; +import Viewer from '../viewer/Viewer'; +import ToolRenderer from '../tools/ToolRenderer'; +import LandingPage from '../shared/LandingPage'; + +// No props needed - component uses contexts directly +export default function Workbench() { + const { t } = useTranslation(); + const { isRainbowMode } = useRainbowThemeContext(); + + // Use context-based hooks to eliminate all prop drilling + const { activeFiles, currentView, setCurrentView } = useFileContext(); + const { + previewFile, + pageEditorFunctions, + sidebarsVisible, + setPreviewFile, + setPageEditorFunctions, + setSidebarsVisible + } = useWorkbenchState(); + + const { selectedToolKey, selectedTool, handleToolSelect } = useToolSelection(); + const { addToActiveFiles } = useFileHandler(); + + const handlePreviewClose = () => { + setPreviewFile(null); + const previousMode = sessionStorage.getItem('previousMode'); + if (previousMode === 'split') { + // Use context's handleToolSelect which coordinates tool selection and view changes + handleToolSelect('split'); + sessionStorage.removeItem('previousMode'); + } else if (previousMode === 'compress') { + handleToolSelect('compress'); + sessionStorage.removeItem('previousMode'); + } else if (previousMode === 'convert') { + handleToolSelect('convert'); + sessionStorage.removeItem('previousMode'); + } else { + setCurrentView('fileEditor' as any); + } + }; + + const renderMainContent = () => { + if (!activeFiles[0]) { + return ( + + ); + } + + switch (currentView) { + case "fileEditor": + return ( + { + setCurrentView("pageEditor" as any); + }, + onMergeFiles: (filesToMerge) => { + filesToMerge.forEach(addToActiveFiles); + setCurrentView("viewer" as any); + } + })} + /> + ); + + case "viewer": + return ( + + ); + + case "pageEditor": + return ( + <> + + {pageEditorFunctions && ( + + )} + + ); + + default: + // Check if it's a tool view + if (selectedToolKey && selectedTool) { + return ( + + ); + } + return ( + + ); + } + }; + + return ( + + {/* Top Controls */} + + + {/* Main content area */} + + {renderMainContent()} + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/fileManagement/FileCard.tsx b/frontend/src/components/shared/FileCard.tsx similarity index 99% rename from frontend/src/components/fileManagement/FileCard.tsx rename to frontend/src/components/shared/FileCard.tsx index d474a2f63..1b686ddaf 100644 --- a/frontend/src/components/fileManagement/FileCard.tsx +++ b/frontend/src/components/shared/FileCard.tsx @@ -6,6 +6,7 @@ import StorageIcon from "@mui/icons-material/Storage"; import VisibilityIcon from "@mui/icons-material/Visibility"; import EditIcon from "@mui/icons-material/Edit"; +import { FileWithUrl } from "../../types/file"; import { getFileSize, getFileDate } from "../../utils/fileUtils"; import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail"; import { fileStorage } from "../../services/fileStorage"; diff --git a/frontend/src/components/shared/FileGrid.tsx b/frontend/src/components/shared/FileGrid.tsx index 78b5a8f17..791a8a453 100644 --- a/frontend/src/components/shared/FileGrid.tsx +++ b/frontend/src/components/shared/FileGrid.tsx @@ -3,7 +3,7 @@ import { Box, Flex, Group, Text, Button, TextInput, Select, Badge } from "@manti import { useTranslation } from "react-i18next"; import SearchIcon from "@mui/icons-material/Search"; import SortIcon from "@mui/icons-material/Sort"; -import FileCard from "../fileManagement/FileCard"; +import FileCard from "./FileCard"; import { FileWithUrl } from "../../types/file"; interface FileGridProps { diff --git a/frontend/src/components/shared/FileUploadModal.tsx b/frontend/src/components/shared/FileUploadModal.tsx deleted file mode 100644 index a83e96e62..000000000 --- a/frontend/src/components/shared/FileUploadModal.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { Modal } from '@mantine/core'; -import FileUploadSelector from './FileUploadSelector'; -import { useFilesModalContext } from '../../contexts/FilesModalContext'; -import { Tool } from '../../types/tool'; - -interface FileUploadModalProps { - selectedTool?: Tool | null; -} - -const FileUploadModal: React.FC = ({ selectedTool }) => { - const { isFilesModalOpen, closeFilesModal, onFileSelect, onFilesSelect } = useFilesModalContext(); - - - return ( - - - - ); -}; - -export default FileUploadModal; \ No newline at end of file diff --git a/frontend/src/components/shared/FileUploadSelector.tsx b/frontend/src/components/shared/FileUploadSelector.tsx deleted file mode 100644 index 3f345f24b..000000000 --- a/frontend/src/components/shared/FileUploadSelector.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import React, { useState, useCallback, useRef, useEffect } from 'react'; -import { Stack, Button, Text, Center, Box, Divider } from '@mantine/core'; -import { Dropzone } from '@mantine/dropzone'; -import UploadFileIcon from '@mui/icons-material/UploadFile'; -import { useTranslation } from 'react-i18next'; -import { fileStorage } from '../../services/fileStorage'; -import { FileWithUrl } from '../../types/file'; -import { detectFileExtension } from '../../utils/fileUtils'; -import FileGrid from './FileGrid'; -import MultiSelectControls from './MultiSelectControls'; -import { useFileManager } from '../../hooks/useFileManager'; - -interface FileUploadSelectorProps { - // Appearance - title?: string; - subtitle?: string; - showDropzone?: boolean; - - // File handling - sharedFiles?: any[]; - onFileSelect?: (file: File) => void; - onFilesSelect: (files: File[]) => void; - accept?: string[]; - supportedExtensions?: string[]; // Extensions this tool supports (e.g., ['pdf', 'jpg', 'png']) - - // Loading state - loading?: boolean; - disabled?: boolean; - - // Recent files - showRecentFiles?: boolean; - maxRecentFiles?: number; -} - -const FileUploadSelector = ({ - title, - subtitle, - showDropzone = true, - sharedFiles = [], - onFileSelect, - onFilesSelect, - accept = ["application/pdf", "application/zip", "application/x-zip-compressed"], - supportedExtensions = ["pdf"], // Default to PDF only for most tools - loading = false, - disabled = false, - showRecentFiles = true, - maxRecentFiles = 8, -}: FileUploadSelectorProps) => { - const { t } = useTranslation(); - const fileInputRef = useRef(null); - - const [recentFiles, setRecentFiles] = useState([]); - const [selectedFiles, setSelectedFiles] = useState([]); - - const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile, createFileSelectionHandlers } = useFileManager(); - - // Utility function to check if a file extension is supported - const isFileSupported = useCallback((fileName: string): boolean => { - const extension = detectFileExtension(fileName); - return extension ? supportedExtensions.includes(extension) : false; - }, [supportedExtensions]); - - const refreshRecentFiles = useCallback(async () => { - const files = await loadRecentFiles(); - setRecentFiles(files); - }, [loadRecentFiles]); - - const handleNewFileUpload = useCallback(async (uploadedFiles: File[]) => { - if (uploadedFiles.length === 0) return; - - if (showRecentFiles) { - try { - for (const file of uploadedFiles) { - await storeFile(file); - } - refreshRecentFiles(); - } catch (error) { - console.error('Failed to save files to recent:', error); - } - } - - if (onFilesSelect) { - onFilesSelect(uploadedFiles); - } else if (onFileSelect) { - onFileSelect(uploadedFiles[0]); - } - }, [onFileSelect, onFilesSelect, showRecentFiles, storeFile, refreshRecentFiles]); - - const handleFileInputChange = useCallback((event: React.ChangeEvent) => { - const files = event.target.files; - if (files && files.length > 0) { - const fileArray = Array.from(files); - console.log('File input change:', fileArray.length, 'files'); - handleNewFileUpload(fileArray); - } - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - }, [handleNewFileUpload]); - - const openFileDialog = useCallback(() => { - fileInputRef.current?.click(); - }, []); - - const handleRecentFileSelection = useCallback(async (file: FileWithUrl) => { - try { - const fileObj = await convertToFile(file); - if (onFilesSelect) { - onFilesSelect([fileObj]); - } else if (onFileSelect) { - onFileSelect(fileObj); - } - } catch (error) { - console.error('Failed to load file from recent:', error); - } - }, [onFileSelect, onFilesSelect, convertToFile]); - - const selectionHandlers = createFileSelectionHandlers(selectedFiles, setSelectedFiles); - - const handleSelectedRecentFiles = useCallback(async () => { - if (onFilesSelect) { - await selectionHandlers.selectMultipleFiles(recentFiles, onFilesSelect); - } - }, [recentFiles, onFilesSelect, selectionHandlers]); - - const handleRemoveFileByIndex = useCallback(async (index: number) => { - await handleRemoveFile(index, recentFiles, setRecentFiles); - const file = recentFiles[index]; - setSelectedFiles(prev => prev.filter(id => id !== (file.id || file.name))); - }, [handleRemoveFile, recentFiles]); - - useEffect(() => { - if (showRecentFiles) { - refreshRecentFiles(); - } - }, [showRecentFiles, refreshRecentFiles]); - - // Get default title and subtitle from translations if not provided - const displayTitle = title || t("fileUpload.selectFiles", "Select files"); - const displaySubtitle = subtitle || t("fileUpload.chooseFromStorageMultiple", "Choose files from storage or upload new PDFs"); - - return ( - <> - - {/* Title and description */} - - - - {displayTitle} - - - {displaySubtitle} - - - - {/* Action buttons */} - - - {showDropzone ? ( - -
- - - {t("fileUpload.dropFilesHere", "Drop files here or click to upload")} - - - {accept.includes('application/pdf') && accept.includes('application/zip') - ? t("fileUpload.pdfAndZipFiles", "PDF and ZIP files") - : accept.includes('application/pdf') - ? t("fileUpload.pdfFilesOnly", "PDF files only") - : t("fileUpload.supportedFileTypes", "Supported file types") - } - - -
-
- ) : ( - - - - {/* Manual file input as backup */} - - - )} -
- - {/* Recent Files Section */} - {showRecentFiles && recentFiles.length > 0 && ( - - - - {t("fileUpload.recentFiles", "Recent Files")} - - { - await Promise.all(recentFiles.map(async (file) => { - await fileStorage.deleteFile(file.id || file.name); - })); - setRecentFiles([]); - setSelectedFiles([]); - }} - /> - - { - await Promise.all(recentFiles.map(async (file) => { - await fileStorage.deleteFile(file.id || file.name); - })); - setRecentFiles([]); - setSelectedFiles([]); - }} - /> - - )} -
- - ); -}; - -export default FileUploadSelector; diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx index 1ee6b60ee..53f0ba94e 100644 --- a/frontend/src/components/shared/QuickAccessBar.tsx +++ b/frontend/src/components/shared/QuickAccessBar.tsx @@ -11,20 +11,18 @@ import { useRainbowThemeContext } from "./RainbowThemeProvider"; import AppConfigModal from './AppConfigModal'; import { useIsOverflowing } from '../../hooks/useIsOverflowing'; import { useFilesModalContext } from '../../contexts/FilesModalContext'; -import { QuickAccessBarProps, ButtonConfig } from '../../types/sidebar'; +import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; +import { ButtonConfig } from '../../types/sidebar'; import './QuickAccessBar.css'; function NavHeader({ activeButton, - setActiveButton, - onReaderToggle, - onToolsClick + setActiveButton }: { activeButton: string; setActiveButton: (id: string) => void; - onReaderToggle: () => void; - onToolsClick: () => void; }) { + const { handleReaderToggle, handleBackToTools } = useToolWorkflow(); return ( <> {/* All Tools button below divider */} @@ -35,8 +33,8 @@ function NavHeader({ variant="subtle" onClick={() => { setActiveButton('tools'); - onReaderToggle(); - onToolsClick(); + handleReaderToggle(); + handleBackToTools(); }} style={{ backgroundColor: activeButton === 'tools' ? 'var(--icon-tools-bg)' : 'var(--icon-inactive-bg)', @@ -59,12 +57,11 @@ function NavHeader({ ); } -const QuickAccessBar = forwardRef(({ - onToolsClick, - onReaderToggle, +const QuickAccessBar = forwardRef(({ }, ref) => { const { isRainbowMode } = useRainbowThemeContext(); const { openFilesModal, isFilesModalOpen } = useFilesModalContext(); + const { handleReaderToggle } = useToolWorkflow(); const [configModalOpen, setConfigModalOpen] = useState(false); const [activeButton, setActiveButton] = useState('tools'); const scrollableRef = useRef(null); @@ -85,7 +82,7 @@ const QuickAccessBar = forwardRef(({ type: 'navigation', onClick: () => { setActiveButton('read'); - onReaderToggle(); + handleReaderToggle(); } }, { @@ -199,9 +196,7 @@ const QuickAccessBar = forwardRef(({
diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx new file mode 100644 index 000000000..1551ea6c9 --- /dev/null +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { TextInput } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; +import { useToolPanelState, useToolSelection, useWorkbenchState } from '../../contexts/ToolWorkflowContext'; +import ToolPicker from './ToolPicker'; +import ToolRenderer from './ToolRenderer'; +import { useSidebarContext } from "../../contexts/SidebarContext"; +import rainbowStyles from '../../styles/rainbow.module.css'; + +// No props needed - component uses context + +export default function ToolPanel() { + const { t } = useTranslation(); + const { isRainbowMode } = useRainbowThemeContext(); + const { sidebarRefs } = useSidebarContext(); + const { toolPanelRef } = sidebarRefs; + + + // Use context-based hooks to eliminate prop drilling + const { + leftPanelView, + isPanelVisible, + searchQuery, + filteredTools, + setSearchQuery, + handleBackToTools + } = useToolPanelState(); + + const { selectedToolKey, handleToolSelect } = useToolSelection(); + const { setPreviewFile } = useWorkbenchState(); + + return ( +
+
+ {/* Search Bar - Always visible at the top */} +
+ setSearchQuery(e.currentTarget.value)} + autoComplete="off" + size="sm" + /> +
+ + {leftPanelView === 'toolPicker' ? ( + // Tool Picker View +
+ +
+ ) : ( + // Selected Tool Content View +
+ {/* Tool content */} +
+ +
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/tools/split/SplitSettings.tsx b/frontend/src/components/tools/split/SplitSettings.tsx index 50ca49f20..95e972eaf 100644 --- a/frontend/src/components/tools/split/SplitSettings.tsx +++ b/frontend/src/components/tools/split/SplitSettings.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import { SPLIT_MODES, SPLIT_TYPES, type SplitMode, type SplitType } from '../../../constants/splitConstants'; export interface SplitParameters { + mode: SplitMode | ''; pages: string; hDiv: string; vDiv: string; @@ -15,16 +16,12 @@ export interface SplitParameters { } export interface SplitSettingsProps { - mode: SplitMode | ''; - onModeChange: (mode: SplitMode | '') => void; parameters: SplitParameters; onParameterChange: (parameter: keyof SplitParameters, value: string | boolean) => void; disabled?: boolean; } const SplitSettings = ({ - mode, - onModeChange, parameters, onParameterChange, disabled = false @@ -125,8 +122,8 @@ const SplitSettings = ({