diff --git a/.github/workflows/deploy-on-v2-commit.yml b/.github/workflows/deploy-on-v2-commit.yml
new file mode 100644
index 000000000..eb12d5cd9
--- /dev/null
+++ b/.github/workflows/deploy-on-v2-commit.yml
@@ -0,0 +1,183 @@
+name: Auto V2 Deploy on Push
+
+on:
+ push:
+ branches:
+ - V2
+ - deploy-on-v2-commit
+
+permissions:
+ contents: read
+
+jobs:
+ deploy-v2-on-push:
+ runs-on: ubuntu-latest
+ concurrency:
+ group: deploy-v2-push-V2
+ cancel-in-progress: true
+
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
+ with:
+ egress-policy: audit
+
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Get commit hashes for frontend and backend
+ id: commit-hashes
+ run: |
+ # Get last commit that touched the frontend folder, docker/frontend, or docker/compose
+ FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
+ if [ -z "$FRONTEND_HASH" ]; then
+ FRONTEND_HASH="no-frontend-changes"
+ fi
+
+ # Get last commit that touched backend code, docker/backend, or docker/compose
+ BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
+ if [ -z "$BACKEND_HASH" ]; then
+ BACKEND_HASH="no-backend-changes"
+ fi
+
+ echo "Frontend hash: $FRONTEND_HASH"
+ echo "Backend hash: $BACKEND_HASH"
+
+ echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
+ echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
+
+ # Short hashes for tags
+ if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
+ echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
+ else
+ echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
+ fi
+
+ if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
+ echo "backend_short=no-backend" >> $GITHUB_OUTPUT
+ else
+ echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Check if frontend image exists
+ id: check-frontend
+ run: |
+ if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
+ echo "exists=true" >> $GITHUB_OUTPUT
+ echo "Frontend image already exists, skipping build"
+ else
+ echo "exists=false" >> $GITHUB_OUTPUT
+ echo "Frontend image needs to be built"
+ fi
+
+ - name: Check if backend image exists
+ id: check-backend
+ run: |
+ if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
+ echo "exists=true" >> $GITHUB_OUTPUT
+ echo "Backend image already exists, skipping build"
+ else
+ echo "exists=false" >> $GITHUB_OUTPUT
+ echo "Backend image needs to be built"
+ fi
+
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_HUB_USERNAME }}
+ password: ${{ secrets.DOCKER_HUB_API }}
+
+ - name: Build and push frontend image
+ if: steps.check-frontend.outputs.exists == 'false'
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: ./docker/frontend/Dockerfile
+ push: true
+ tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
+ build-args: VERSION_TAG=v2-alpha
+ platforms: linux/amd64
+
+ - name: Build and push backend image
+ if: steps.check-backend.outputs.exists == 'false'
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: ./docker/backend/Dockerfile
+ push: true
+ tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
+ build-args: VERSION_TAG=v2-alpha
+ platforms: linux/amd64
+
+
+ - name: Set up SSH
+ run: |
+ mkdir -p ~/.ssh/
+ echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
+ chmod 600 ../private.key
+
+
+ - name: Deploy to VPS on port 3000
+ run: |
+ export UNIQUE_NAME=docker-compose-v2-$GITHUB_RUN_ID.yml
+
+ cat > $UNIQUE_NAME << EOF
+ version: '3.3'
+ services:
+ backend:
+ container_name: stirling-v2-backend
+ image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
+ ports:
+ - "13000:8080"
+ volumes:
+ - /stirling/V2/data:/usr/share/tessdata:rw
+ - /stirling/V2/config:/configs:rw
+ - /stirling/V2/logs:/logs:rw
+ environment:
+ DISABLE_ADDITIONAL_FEATURES: "true"
+ SECURITY_ENABLELOGIN: "false"
+ SYSTEM_DEFAULTLOCALE: en-GB
+ UI_APPNAME: "Stirling-PDF V2"
+ UI_HOMEDESCRIPTION: "V2 Frontend/Backend Split"
+ UI_APPNAMENAVBAR: "V2 Deployment"
+ SYSTEM_MAXFILESIZE: "100"
+ METRICS_ENABLED: "true"
+ SYSTEM_GOOGLEVISIBILITY: "false"
+ restart: on-failure:5
+
+ frontend:
+ container_name: stirling-v2-frontend
+ image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
+ ports:
+ - "3000:80"
+ environment:
+ VITE_API_BASE_URL: "http://${{ secrets.VPS_HOST }}:13000"
+ depends_on:
+ - backend
+ restart: on-failure:5
+ EOF
+
+ # Copy to remote with unique name
+ scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/$UNIQUE_NAME
+
+ # SSH and rename/move atomically to avoid interference
+ ssh -i ../private.key -o StrictHostKeyChecking=no ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH
+ mkdir -p /stirling/V2/{data,config,logs}
+ mv /tmp/$UNIQUE_NAME /stirling/V2/docker-compose.yml
+ cd /stirling/V2
+ docker-compose down || true
+ docker-compose pull
+ docker-compose up -d
+ docker system prune -af --volumes
+ docker image prune -af --filter "until=336h" --filter "label!=keep=true"
+ ENDSSH
+
+ - name: Cleanup temporary files
+ if: always()
+ run: |
+ rm -f ../private.key
+
diff --git a/frontend/index.html b/frontend/index.html
index 0fc165c66..c4a808349 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -7,12 +7,12 @@
-
Vite App
+ Stirling PDF
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index e82d7349c..060a51d64 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -27,6 +27,7 @@
"i18next-browser-languagedetector": "^8.1.0",
"i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1",
+ "material-symbols": "^0.33.0",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^3.11.174",
"react": "^19.1.0",
@@ -5092,6 +5093,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/material-symbols": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.33.0.tgz",
+ "integrity": "sha512-t9/Gz+14fClRgN7oVOt5CBuwsjFLxSNP9BRDyMrI5el3IZNvoD94IDGJha0YYivyAow24rCS0WOkAv4Dp+YjNg==",
+ "license": "Apache-2.0"
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 7207248b1..f95b43bd3 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -23,6 +23,7 @@
"i18next-browser-languagedetector": "^8.1.0",
"i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1",
+ "material-symbols": "^0.33.0",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^3.11.174",
"react": "^19.1.0",
diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico
index a11777cc4..6d6c8521c 100644
Binary files a/frontend/public/favicon.ico and b/frontend/public/favicon.ico differ
diff --git a/frontend/public/logo192.png b/frontend/public/logo192.png
index fc44b0a37..2994ca293 100644
Binary files a/frontend/public/logo192.png and b/frontend/public/logo192.png differ
diff --git a/frontend/public/logo512.png b/frontend/public/logo512.png
index a4e47a654..b48155073 100644
Binary files a/frontend/public/logo512.png and b/frontend/public/logo512.png differ
diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx
index c0badafd8..9e0dc2171 100644
--- a/frontend/src/components/fileEditor/FileEditor.tsx
+++ b/frontend/src/components/fileEditor/FileEditor.tsx
@@ -7,6 +7,7 @@ import { Dropzone } from '@mantine/dropzone';
import { useTranslation } from 'react-i18next';
import UploadFileIcon from '@mui/icons-material/UploadFile';
import { useFileContext } from '../../contexts/FileContext';
+import { useFileSelection } from '../../contexts/FileSelectionContext';
import { FileOperation } from '../../types/fileContext';
import { fileStorage } from '../../services/fileStorage';
import { generateThumbnailForFile } from '../../utils/thumbnailUtils';
@@ -31,20 +32,16 @@ interface FileEditorProps {
onOpenPageEditor?: (file: File) => void;
onMergeFiles?: (files: File[]) => void;
toolMode?: boolean;
- multiSelect?: boolean;
showUpload?: boolean;
showBulkActions?: boolean;
- onFileSelect?: (files: File[]) => void;
}
const FileEditor = ({
onOpenPageEditor,
onMergeFiles,
toolMode = false,
- multiSelect = true,
showUpload = true,
- showBulkActions = true,
- onFileSelect
+ showBulkActions = true
}: FileEditorProps) => {
const { t } = useTranslation();
@@ -63,6 +60,14 @@ const FileEditor = ({
markOperationApplied
} = fileContext;
+ // Get file selection context
+ const {
+ selectedFiles: toolSelectedFiles,
+ setSelectedFiles: setToolSelectedFiles,
+ maxFiles,
+ isToolMode
+ } = useFileSelection();
+
const [files, setFiles] = useState([]);
const [status, setStatus] = useState(null);
const [error, setError] = useState(null);
@@ -99,14 +104,14 @@ const FileEditor = ({
const lastActiveFilesRef = useRef([]);
const lastProcessedFilesRef = useRef(0);
- // Map context selected file names to local file IDs
- // Defensive programming: ensure selectedFileIds is always an array
- const safeSelectedFileIds = Array.isArray(selectedFileIds) ? selectedFileIds : [];
+ // Get selected file IDs from context (defensive programming)
+ const contextSelectedIds = Array.isArray(selectedFileIds) ? selectedFileIds : [];
- const localSelectedFiles = files
+ // Map context selections to local file IDs for UI display
+ const localSelectedIds = files
.filter(file => {
const fileId = (file.file as any).id || file.name;
- return safeSelectedFileIds.includes(fileId);
+ return contextSelectedIds.includes(fileId);
})
.map(file => file.id);
@@ -396,44 +401,41 @@ const FileEditor = ({
if (!targetFile) return;
const contextFileId = (targetFile.file as any).id || targetFile.name;
+ const isSelected = contextSelectedIds.includes(contextFileId);
- if (!multiSelect) {
- // Single select mode for tools - toggle on/off
- const isCurrentlySelected = safeSelectedFileIds.includes(contextFileId);
- if (isCurrentlySelected) {
- // Deselect the file
- setContextSelectedFiles([]);
- if (onFileSelect) {
- onFileSelect([]);
- }
- } else {
- // Select the file
- setContextSelectedFiles([contextFileId]);
- if (onFileSelect) {
- onFileSelect([targetFile.file]);
- }
- }
+ let newSelection: string[];
+
+ if (isSelected) {
+ // Remove file from selection
+ newSelection = contextSelectedIds.filter(id => id !== contextFileId);
} else {
- // Multi select mode (default)
- setContextSelectedFiles(prev => {
- const safePrev = Array.isArray(prev) ? prev : [];
- return safePrev.includes(contextFileId)
- ? safePrev.filter(id => id !== contextFileId)
- : [...safePrev, contextFileId];
- });
-
- // Notify parent with selected files
- if (onFileSelect) {
- const selectedFiles = files
- .filter(f => {
- const fId = (f.file as any).id || f.name;
- return safeSelectedFileIds.includes(fId) || fId === contextFileId;
- })
- .map(f => f.file);
- onFileSelect(selectedFiles);
+ // Add file to selection
+ if (maxFiles === 1) {
+ newSelection = [contextFileId];
+ } else {
+ // Check if we've hit the selection limit
+ if (maxFiles > 1 && contextSelectedIds.length >= maxFiles) {
+ setStatus(`Maximum ${maxFiles} files can be selected`);
+ return;
+ }
+ newSelection = [...contextSelectedIds, contextFileId];
}
}
- }, [files, setContextSelectedFiles, multiSelect, onFileSelect, safeSelectedFileIds]);
+
+ // Update context
+ setContextSelectedFiles(newSelection);
+
+ // Update tool selection context if in tool mode
+ if (isToolMode || toolMode) {
+ const selectedFiles = files
+ .filter(f => {
+ const fId = (f.file as any).id || f.name;
+ return newSelection.includes(fId);
+ })
+ .map(f => f.file);
+ setToolSelectedFiles(selectedFiles);
+ }
+ }, [files, setContextSelectedFiles, maxFiles, contextSelectedIds, setStatus, isToolMode, toolMode, setToolSelectedFiles]);
const toggleSelectionMode = useCallback(() => {
setSelectionMode(prev => {
@@ -450,15 +452,15 @@ const FileEditor = ({
const handleDragStart = useCallback((fileId: string) => {
setDraggedFile(fileId);
- if (selectionMode && localSelectedFiles.includes(fileId) && localSelectedFiles.length > 1) {
+ if (selectionMode && localSelectedIds.includes(fileId) && localSelectedIds.length > 1) {
setMultiFileDrag({
- fileIds: localSelectedFiles,
- count: localSelectedFiles.length
+ fileIds: localSelectedIds,
+ count: localSelectedIds.length
});
} else {
setMultiFileDrag(null);
}
- }, [selectionMode, localSelectedFiles]);
+ }, [selectionMode, localSelectedIds]);
const handleDragEnd = useCallback(() => {
setDraggedFile(null);
@@ -519,8 +521,8 @@ const FileEditor = ({
if (targetIndex === -1) return;
}
- const filesToMove = selectionMode && localSelectedFiles.includes(draggedFile)
- ? localSelectedFiles
+ const filesToMove = selectionMode && localSelectedIds.includes(draggedFile)
+ ? localSelectedIds
: [draggedFile];
// Update the local files state and sync with activeFiles
@@ -545,7 +547,7 @@ const FileEditor = ({
const moveCount = multiFileDrag ? multiFileDrag.count : 1;
setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`);
- }, [draggedFile, files, selectionMode, localSelectedFiles, multiFileDrag]);
+ }, [draggedFile, files, selectionMode, localSelectedIds, multiFileDrag]);
const handleEndZoneDragEnter = useCallback(() => {
if (draggedFile) {
@@ -764,7 +766,7 @@ const FileEditor = ({
) : (
void;
@@ -16,6 +22,86 @@ interface QuickAccessBarProps {
readerMode: boolean;
}
+interface ButtonConfig {
+ id: string;
+ name: string;
+ icon: React.ReactNode;
+ tooltip: string;
+ isRound?: boolean;
+ size?: 'sm' | 'md' | 'lg' | 'xl';
+ onClick: () => void;
+}
+
+function NavHeader({
+ activeButton,
+ setActiveButton,
+ onReaderToggle,
+ onToolsClick
+}: {
+ activeButton: string;
+ setActiveButton: (id: string) => void;
+ onReaderToggle: () => void;
+ onToolsClick: () => void;
+}) {
+ return (
+ <>
+
+ {/* Divider after top icons */}
+
+ {/* All Tools button below divider */}
+
+
+
{
+ setActiveButton('tools');
+ onReaderToggle();
+ onToolsClick();
+ }}
+ style={{
+ backgroundColor: activeButton === 'tools' ? 'var(--icon-tools-bg)' : 'var(--icon-inactive-bg)',
+ color: activeButton === 'tools' ? 'var(--icon-tools-color)' : 'var(--icon-inactive-color)',
+ border: 'none',
+ borderRadius: '8px',
+ }}
+ className={activeButton === 'tools' ? 'activeIconScale' : ''}
+ >
+
+
+
+
+
+ All Tools
+
+
+
+ >
+ );
+}
+
const QuickAccessBar = ({
onToolsClick,
onReaderToggle,
@@ -26,55 +112,201 @@ const QuickAccessBar = ({
}: QuickAccessBarProps) => {
const { isRainbowMode } = useRainbowThemeContext();
const [configModalOpen, setConfigModalOpen] = useState(false);
+ const [activeButton, setActiveButton] = useState('tools');
+ const scrollableRef = useRef(null);
+ const isOverflow = useIsOverflowing(scrollableRef);
+
+ const buttonConfigs: ButtonConfig[] = [
+ {
+ id: 'read',
+ name: 'Read',
+ icon: ,
+ tooltip: 'Read documents',
+ size: 'lg',
+ isRound: false,
+ onClick: () => {
+ setActiveButton('read');
+ onReaderToggle();
+ }
+ },
+ {
+ id: 'sign',
+ name: 'Sign',
+ icon:
+
+ signature
+ ,
+ tooltip: 'Sign your document',
+ size: 'lg',
+ isRound: false,
+ onClick: () => setActiveButton('sign')
+ },
+ {
+ id: 'automate',
+ name: 'Automate',
+ icon: ,
+ tooltip: 'Automate workflows',
+ size: 'lg',
+ isRound: false,
+ onClick: () => setActiveButton('automate')
+ },
+ {
+ id: 'files',
+ name: 'Files',
+ icon: ,
+ tooltip: 'Manage files',
+ isRound: true,
+ size: 'lg',
+ onClick: () => setActiveButton('files')
+ },
+ {
+ id: 'activity',
+ name: 'Activity',
+ icon:
+
+ vital_signs
+ ,
+ tooltip: 'View activity and analytics',
+ isRound: true,
+ size: 'lg',
+ onClick: () => setActiveButton('activity')
+ },
+ {
+ id: 'config',
+ name: 'Config',
+ icon: ,
+ tooltip: 'Configure settings',
+ size: 'lg',
+ onClick: () => {
+ setConfigModalOpen(true);
+ }
+ }
+ ];
+
+ const CIRCULAR_BORDER_RADIUS = '50%';
+ const ROUND_BORDER_RADIUS = '8px';
+
+ const getBorderRadius = (config: ButtonConfig): string => {
+ return config.isRound ? CIRCULAR_BORDER_RADIUS : ROUND_BORDER_RADIUS;
+ };
+
+ const getButtonStyle = (config: ButtonConfig) => {
+ const isActive = activeButton === config.id;
+
+ if (isActive) {
+ return {
+ backgroundColor: `var(--icon-${config.id}-bg)`,
+ color: `var(--icon-${config.id}-color)`,
+ border: 'none',
+ borderRadius: getBorderRadius(config),
+ };
+ }
+
+ // Inactive state - use consistent inactive colors
+ return {
+ backgroundColor: 'var(--icon-inactive-bg)',
+ color: 'var(--icon-inactive-color)',
+ border: 'none',
+ borderRadius: getBorderRadius(config),
+ };
+ };
return (
-
- {/* All Tools Button */}
-
+ {/* Fixed header outside scrollable area */}
+
+
+
- {/* Reader Mode Button */}
-
+ {/* Conditional divider when overflowing */}
+ {isOverflow && (
+
+ )}
- {/* Spacer */}
-
-
- {/* Config Modal Button (for testing) */}
-
-
setConfigModalOpen(true)}
- >
-
-
-
Config
+ {/* Scrollable content area */}
+
{
+ // Prevent the wheel event from bubbling up to parent containers
+ e.stopPropagation();
+ }}
+ >
+
+ {/* Top section with main buttons */}
+
+ {buttonConfigs.slice(0, -1).map((config, index) => (
+
+
+
+
+
+ {config.icon}
+
+
+
+ {config.name}
+
+
+
+
+ {/* Add divider after Automate button (index 2) */}
+ {index === 2 && (
+
+ )}
+
+ ))}
+
+
+ {/* Spacer to push Config button to bottom */}
+
+
+ {/* Config button at the bottom */}
+
+
+
{
+ setConfigModalOpen(true);
+ }}
+ style={{
+ backgroundColor: 'var(--icon-inactive-bg)',
+ color: 'var(--icon-inactive-color)',
+ border: 'none',
+ borderRadius: '8px',
+ }}
+ >
+
+
+
+
+
+ Config
+
+
+
-
+
+
+
+
+ {toolName ? `Loading ${toolName}...` : "Loading tool..."}
+
+
+
+ )
+}
diff --git a/frontend/src/components/tools/ToolPicker.tsx b/frontend/src/components/tools/ToolPicker.tsx
index 0bb0ea566..7b678de98 100644
--- a/frontend/src/components/tools/ToolPicker.tsx
+++ b/frontend/src/components/tools/ToolPicker.tsx
@@ -1,15 +1,7 @@
import React, { useState } from "react";
import { Box, Text, Stack, Button, TextInput, Group } from "@mantine/core";
import { useTranslation } from "react-i18next";
-
-type Tool = {
- icon: React.ReactNode;
- name: string;
-};
-
-type ToolRegistry = {
- [id: string]: Tool;
-};
+import { ToolRegistry } from "../../types/tool";
interface ToolPickerProps {
selectedToolKey: string | null;
diff --git a/frontend/src/components/tools/ToolRenderer.tsx b/frontend/src/components/tools/ToolRenderer.tsx
index 06c5157a8..493470935 100644
--- a/frontend/src/components/tools/ToolRenderer.tsx
+++ b/frontend/src/components/tools/ToolRenderer.tsx
@@ -1,23 +1,18 @@
-import { FileWithUrl } from "../../types/file";
+import React, { Suspense } from "react";
import { useToolManagement } from "../../hooks/useToolManagement";
+import { BaseToolProps } from "../../types/tool";
+import ToolLoadingFallback from "./ToolLoadingFallback";
-interface ToolRendererProps {
+interface ToolRendererProps extends BaseToolProps {
selectedToolKey: string;
- pdfFile: any;
- files: FileWithUrl[];
- toolParams: any;
- updateParams: (params: any) => void;
- toolSelectedFiles?: File[];
- onPreviewFile?: (file: File | null) => void;
}
+
const ToolRenderer = ({
selectedToolKey,
-files,
- toolParams,
- updateParams,
- toolSelectedFiles = [],
onPreviewFile,
+ onComplete,
+ onError,
}: ToolRendererProps) => {
// Get the tool from registry
const { toolRegistry } = useToolManagement();
@@ -29,46 +24,16 @@ files,
const ToolComponent = selectedTool.component;
- // Pass tool-specific props
- switch (selectedToolKey) {
- case "split":
- return (
-
- );
- case "compress":
- return (
-
- );
- case "convert":
- return (
-
- );
- case "merge":
- return (
-
- );
- default:
- return (
-
- );
- }
+ // Wrap lazy-loaded component with Suspense
+ return (
+ }>
+
+
+ );
};
export default ToolRenderer;
diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx
index 811a49db7..6e8a42fab 100644
--- a/frontend/src/contexts/FileContext.tsx
+++ b/frontend/src/contexts/FileContext.tsx
@@ -22,6 +22,7 @@ import { useEnhancedProcessedFiles } from '../hooks/useEnhancedProcessedFiles';
import { fileStorage } from '../services/fileStorage';
import { enhancedPDFProcessingService } from '../services/enhancedPDFProcessingService';
import { thumbnailGenerationService } from '../services/thumbnailGenerationService';
+import { getFileId } from '../utils/fileUtils';
// Initial state
const initialViewerConfig: ViewerConfig = {
@@ -98,7 +99,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
case 'REMOVE_FILES':
const remainingFiles = state.activeFiles.filter(file => {
- const fileId = (file as any).id || file.name;
+ const fileId = getFileId(file);
return !action.payload.includes(fileId);
});
const safeSelectedFileIds = Array.isArray(state.selectedFileIds) ? state.selectedFileIds : [];
@@ -347,7 +348,7 @@ export function FileContextProvider({
// Cleanup timers and refs
const cleanupTimers = useRef