From 24162d060bc19ef135a6234305f0e01505d1b506 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Fri, 1 Aug 2025 23:34:49 +0100 Subject: [PATCH] util for repair and compress as example --- frontend/public/locales/en/translation.json | 16 +- .../tools/compress/useCompressOperation.ts | 250 ++------------- .../hooks/tools/repair/useRepairOperation.ts | 33 ++ .../hooks/tools/shared/useToolOperation.ts | 303 ++++++++++++++++++ frontend/src/hooks/useToolManagement.tsx | 10 + frontend/src/tools/Repair.tsx | 158 +++++++++ frontend/src/types/fileContext.ts | 4 +- 7 files changed, 542 insertions(+), 232 deletions(-) create mode 100644 frontend/src/hooks/tools/repair/useRepairOperation.ts create mode 100644 frontend/src/hooks/tools/shared/useToolOperation.ts create mode 100644 frontend/src/tools/Repair.tsx diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 8dc3e4f90..db48b9650 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -25,5 +25,19 @@ "web": "Web", "text": "Text", "email": "Email" - } + }, + "home": { + "repair": { + "title": "Repair", + "desc": "Tries to repair a corrupt/broken PDF" + } + }, + "repair": { + "tags": "fix,restore,correction,recover", + "title": "Repair", + "header": "Repair PDFs", + "submit": "Repair" + }, + "loading": "Loading...", + "download": "Download" } \ No newline at end of file diff --git a/frontend/src/hooks/tools/compress/useCompressOperation.ts b/frontend/src/hooks/tools/compress/useCompressOperation.ts index e66b3f43f..4eae2e588 100644 --- a/frontend/src/hooks/tools/compress/useCompressOperation.ts +++ b/frontend/src/hooks/tools/compress/useCompressOperation.ts @@ -1,10 +1,4 @@ -import { useCallback, useState } from 'react'; -import axios from 'axios'; -import { useTranslation } from 'react-i18next'; -import { useFileContext } from '../../../contexts/FileContext'; -import { FileOperation } from '../../../types/fileContext'; -import { zipFileService } from '../../../services/zipFileService'; -import { generateThumbnailForFile } from '../../../utils/thumbnailUtils'; +import { useToolOperation } from '../shared/useToolOperation'; export interface CompressParameters { compressionLevel: number; @@ -37,232 +31,30 @@ export interface CompressOperationHook { } export const useCompressOperation = (): CompressOperationHook => { - const { t } = useTranslation(); - const { - recordOperation, - markOperationApplied, - markOperationFailed, - addFiles - } = useFileContext(); + const toolOperation = useToolOperation({ + operationType: 'compress', + endpoint: '/api/v1/misc/compress-pdf', + buildFormData: (file: File, parameters: CompressParameters) => { + const formData = new FormData(); - // Internal state management - const [files, setFiles] = useState([]); - const [thumbnails, setThumbnails] = useState([]); - const [isGeneratingThumbnails, setIsGeneratingThumbnails] = useState(false); - const [downloadUrl, setDownloadUrl] = useState(null); - const [downloadFilename, setDownloadFilename] = useState(''); - const [status, setStatus] = useState(''); - const [errorMessage, setErrorMessage] = useState(null); - const [isLoading, setIsLoading] = useState(false); + formData.append("fileInput", file); - // Track blob URLs for cleanup - const [blobUrls, setBlobUrls] = useState([]); - - const cleanupBlobUrls = useCallback(() => { - blobUrls.forEach(url => { - try { - URL.revokeObjectURL(url); - } catch (error) { - console.warn('Failed to revoke blob URL:', error); - } - }); - setBlobUrls([]); - }, [blobUrls]); - - const buildFormData = useCallback(( - parameters: CompressParameters, - file: File - ) => { - const formData = new FormData(); - - formData.append("fileInput", file); - - if (parameters.compressionMethod === 'quality') { - formData.append("optimizeLevel", parameters.compressionLevel.toString()); - } else { - // File size method - const fileSize = parameters.fileSizeValue ? `${parameters.fileSizeValue}${parameters.fileSizeUnit}` : ''; - if (fileSize) { - formData.append("expectedOutputSize", fileSize); - } - } - - formData.append("grayscale", parameters.grayscale.toString()); - - const endpoint = "/api/v1/misc/compress-pdf"; - - return { formData, endpoint }; - }, []); - - const createOperation = useCallback(( - parameters: CompressParameters, - selectedFiles: File[] - ): { operation: FileOperation; operationId: string; fileId: string } => { - const operationId = `compress-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - const fileId = selectedFiles.map(f => f.name).join(','); - - const operation: FileOperation = { - id: operationId, - type: 'compress', - timestamp: Date.now(), - fileIds: selectedFiles.map(f => f.name), - status: 'pending', - metadata: { - originalFileNames: selectedFiles.map(f => f.name), - parameters: { - compressionLevel: parameters.compressionLevel, - grayscale: parameters.grayscale, - expectedSize: parameters.expectedSize, - }, - totalFileSize: selectedFiles.reduce((sum, f) => sum + f.size, 0), - fileCount: selectedFiles.length - } - }; - - return { operation, operationId, fileId }; - }, []); - - - const executeOperation = useCallback(async ( - parameters: CompressParameters, - selectedFiles: File[] - ) => { - if (selectedFiles.length === 0) { - setStatus(t("noFileSelected")); - return; - } - const validFiles = selectedFiles.filter(file => file.size > 0); - if (validFiles.length === 0) { - setErrorMessage('No valid files to compress. All selected files are empty.'); - return; - } - - if (validFiles.length < selectedFiles.length) { - console.warn(`Skipping ${selectedFiles.length - validFiles.length} empty files`); - } - - const { operation, operationId, fileId } = createOperation(parameters, selectedFiles); - - recordOperation(fileId, operation); - - setStatus(t("loading")); - setIsLoading(true); - setErrorMessage(null); - setFiles([]); - setThumbnails([]); - - try { - const compressedFiles: File[] = []; - - const failedFiles: string[] = []; - - for (let i = 0; i < validFiles.length; i++) { - const file = validFiles[i]; - setStatus(`Compressing ${file.name} (${i + 1}/${validFiles.length})`); - - try { - const { formData, endpoint } = buildFormData(parameters, file); - const response = await axios.post(endpoint, formData, { responseType: "blob" }); - - const contentType = response.headers['content-type'] || 'application/pdf'; - const blob = new Blob([response.data], { type: contentType }); - const compressedFile = new File([blob], `compressed_${file.name}`, { type: contentType }); - - compressedFiles.push(compressedFile); - } catch (fileError) { - console.error(`Failed to compress ${file.name}:`, fileError); - failedFiles.push(file.name); + if (parameters.compressionMethod === 'quality') { + formData.append("optimizeLevel", parameters.compressionLevel.toString()); + } else { + // File size method + const fileSize = parameters.fileSizeValue ? `${parameters.fileSizeValue}${parameters.fileSizeUnit}` : ''; + if (fileSize) { + formData.append("expectedOutputSize", fileSize); } } - if (failedFiles.length > 0 && compressedFiles.length === 0) { - throw new Error(`Failed to compress all files: ${failedFiles.join(', ')}`); - } + formData.append("grayscale", parameters.grayscale.toString()); - if (failedFiles.length > 0) { - setStatus(`Compressed ${compressedFiles.length}/${validFiles.length} files. Failed: ${failedFiles.join(', ')}`); - } + return formData; + }, + filePrefix: 'compressed_' + }); - setFiles(compressedFiles); - setIsGeneratingThumbnails(true); - - await addFiles(compressedFiles); - - cleanupBlobUrls(); - - if (compressedFiles.length === 1) { - const url = window.URL.createObjectURL(compressedFiles[0]); - setDownloadUrl(url); - setBlobUrls([url]); - setDownloadFilename(`compressed_${selectedFiles[0].name}`); - } else { - const { zipFile } = await zipFileService.createZipFromFiles(compressedFiles, 'compressed_files.zip'); - const url = window.URL.createObjectURL(zipFile); - setDownloadUrl(url); - setBlobUrls([url]); - setDownloadFilename(`compressed_${validFiles.length}_files.zip`); - } - - const thumbnails = await Promise.all( - compressedFiles.map(async (file) => { - try { - const thumbnail = await generateThumbnailForFile(file); - return thumbnail || ''; - } catch (error) { - console.warn(`Failed to generate thumbnail for ${file.name}:`, error); - return ''; - } - }) - ); - - setThumbnails(thumbnails); - setIsGeneratingThumbnails(false); - setStatus(t("downloadComplete")); - markOperationApplied(fileId, operationId); - } catch (error: any) { - console.error(error); - let errorMsg = t("error.pdfPassword", "An error occurred while compressing the PDF."); - if (error.response?.data && typeof error.response.data === 'string') { - errorMsg = error.response.data; - } else if (error.message) { - errorMsg = error.message; - } - setErrorMessage(errorMsg); - setStatus(t("error._value", "Compression failed.")); - markOperationFailed(fileId, operationId, errorMsg); - } finally { - setIsLoading(false); - } - }, [t, createOperation, buildFormData, recordOperation, markOperationApplied, markOperationFailed, addFiles]); - - const resetResults = useCallback(() => { - cleanupBlobUrls(); - setFiles([]); - setThumbnails([]); - setIsGeneratingThumbnails(false); - setDownloadUrl(null); - setStatus(''); - setErrorMessage(null); - setIsLoading(false); - }, [cleanupBlobUrls]); - - const clearError = useCallback(() => { - setErrorMessage(null); - }, []); - - return { - executeOperation, - files, - thumbnails, - isGeneratingThumbnails, - downloadUrl, - downloadFilename, - status, - errorMessage, - isLoading, - - // Result management functions - resetResults, - clearError, - }; -}; + return toolOperation; +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/repair/useRepairOperation.ts b/frontend/src/hooks/tools/repair/useRepairOperation.ts new file mode 100644 index 000000000..600575a2c --- /dev/null +++ b/frontend/src/hooks/tools/repair/useRepairOperation.ts @@ -0,0 +1,33 @@ +import { useToolOperation, ToolOperationHook } from '../shared/useToolOperation'; + +export interface RepairOperationState { + files: File[]; + thumbnails: string[]; + isGeneratingThumbnails: boolean; + downloadUrl: string | null; + downloadFilename: string; + isLoading: boolean; + status: string; + errorMessage: string | null; + executeOperation: (selectedFiles: File[]) => Promise; + resetResults: () => void; + clearError: () => void; +} + +export const useRepairOperation = (): RepairOperationState => { + const toolOperation = useToolOperation({ + operationType: 'repair', + endpoint: '/api/v1/misc/repair', + buildFormData: (file: File) => { + const formData = new FormData(); + formData.append('fileInput', file); + return formData; + }, + filePrefix: 'repaired_' + }); + + return { + ...toolOperation, + executeOperation: (selectedFiles: File[]) => toolOperation.executeOperation(undefined, selectedFiles) + }; +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/shared/useToolOperation.ts b/frontend/src/hooks/tools/shared/useToolOperation.ts new file mode 100644 index 000000000..f9b093c73 --- /dev/null +++ b/frontend/src/hooks/tools/shared/useToolOperation.ts @@ -0,0 +1,303 @@ +import { useState, useCallback } from 'react'; +import axios from 'axios'; +import { useTranslation } from 'react-i18next'; +import { useFileContext } from '../../../contexts/FileContext'; +import { FileOperation } from '../../../types/fileContext'; +import { zipFileService } from '../../../services/zipFileService'; +import { generateThumbnailForFile } from '../../../utils/thumbnailUtils'; + +/** + * Configuration interface for tool operations that defines how files should be processed. + * @template TParams - Type of parameters required by the tool (use void for no parameters) + */ +export interface ToolOperationConfig { + /** Unique identifier for the operation type (e.g., 'compress', 'repair') */ + operationType: string; + /** Backend API endpoint for the operation */ + endpoint: string; + /** Function to build FormData from file and parameters for API submission */ + buildFormData: (file: File, params: TParams) => FormData; + /** Optional custom response processor for operations returning multiple files */ + processResponse?: (blob: Blob) => Promise; + /** Prefix added to processed file names (e.g., 'compressed_', 'repaired_') */ + filePrefix: string; + /** If true, processes single files directly; if false, processes files individually */ + singleFileMode?: boolean; +} + +/** + * State interface for tool operations containing all operation results and UI state. + */ +export interface ToolOperationState { + /** Array of processed files returned by the operation */ + files: File[]; + /** Array of thumbnail URLs corresponding to processed files */ + thumbnails: string[]; + /** Whether thumbnails are currently being generated */ + isGeneratingThumbnails: boolean; + /** Blob URL for downloading results (single file or zip) */ + downloadUrl: string | null; + /** Suggested filename for download */ + downloadFilename: string; + /** Whether the operation is currently in progress */ + isLoading: boolean; + /** Current operation status message for user feedback */ + status: string; + /** Error message if operation failed, null if no error */ + errorMessage: string | null; + /** Function to reset all operation results and state */ + resetResults: () => void; + /** Function to clear current error message */ + clearError: () => void; +} + +/** + * Complete hook interface for tool operations, extending state with execution capability. + * @template TParams - Type of parameters required by the tool + */ +export interface ToolOperationHook extends ToolOperationState { + /** Function to execute the tool operation with given parameters and files */ + executeOperation: (params: TParams, selectedFiles: File[]) => Promise; +} + +/** + * Shared hook for implementing tool operations with consistent behavior across all tools. + * Handles file processing, thumbnail generation, download URLs, error handling, and FileContext integration. + * + * @template TParams - Type of parameters required by the tool (use void for no parameters) + * @param config - Configuration object defining how the tool should process files + * @returns Hook with state and execution function for the tool operation + */ +export const useToolOperation = ( + config: ToolOperationConfig +): ToolOperationHook => { + const { t } = useTranslation(); + const { + recordOperation, + markOperationApplied, + markOperationFailed, + addFiles + } = useFileContext(); + + const [files, setFiles] = useState([]); + const [thumbnails, setThumbnails] = useState([]); + const [isGeneratingThumbnails, setIsGeneratingThumbnails] = useState(false); + const [downloadUrl, setDownloadUrl] = useState(null); + const [downloadFilename, setDownloadFilename] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [status, setStatus] = useState(''); + const [errorMessage, setErrorMessage] = useState(null); + + // Track blob URLs for cleanup + const [blobUrls, setBlobUrls] = useState([]); + + const cleanupBlobUrls = useCallback(() => { + blobUrls.forEach(url => { + try { + URL.revokeObjectURL(url); + } catch (error) { + console.warn('Failed to revoke blob URL:', error); + } + }); + setBlobUrls([]); + }, [blobUrls]); + + const createOperation = useCallback(( + params: TParams, + selectedFiles: File[] + ): { operation: FileOperation; operationId: string; fileId: string } => { + const operationId = `${config.operationType}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + const fileId = selectedFiles.map(f => f.name).join(','); + + const operation: FileOperation = { + id: operationId, + type: config.operationType, + timestamp: Date.now(), + fileIds: selectedFiles.map(f => f.name), + status: 'pending', + metadata: { + originalFileName: selectedFiles[0]?.name, + outputFileNames: selectedFiles.map(f => f.name), + parameters: params, + fileSize: selectedFiles.reduce((sum, f) => sum + f.size, 0) + } + }; + + return { operation, operationId, fileId }; + }, [config.operationType]); + + const processMultipleFiles = useCallback(async ( + params: TParams, + validFiles: File[] + ): Promise => { + const processedFiles: File[] = []; + const failedFiles: string[] = []; + + for (let i = 0; i < validFiles.length; i++) { + const file = validFiles[i]; + setStatus(`Processing ${file.name} (${i + 1}/${validFiles.length})`); + + try { + const formData = config.buildFormData(file, params); + const response = await axios.post(config.endpoint, formData, { + responseType: 'blob' + }); + + const contentType = response.headers['content-type'] || 'application/pdf'; + const blob = new Blob([response.data], { type: contentType }); + const processedFile = new File([blob], `${config.filePrefix}${file.name}`, { type: contentType }); + + processedFiles.push(processedFile); + } catch (fileError) { + console.error(`Failed to process ${file.name}:`, fileError); + failedFiles.push(file.name); + } + } + + if (failedFiles.length > 0 && processedFiles.length === 0) { + throw new Error(`Failed to process all files: ${failedFiles.join(', ')}`); + } + + if (failedFiles.length > 0) { + setStatus(`Processed ${processedFiles.length}/${validFiles.length} files. Failed: ${failedFiles.join(', ')}`); + } + + return processedFiles; + }, [config]); + + const handleDownloadAndThumbnails = useCallback(async ( + processedFiles: File[], + originalFiles: File[] + ) => { + setFiles(processedFiles); + setIsGeneratingThumbnails(true); + + await addFiles(processedFiles); + cleanupBlobUrls(); + + if (processedFiles.length === 1) { + const url = window.URL.createObjectURL(processedFiles[0]); + setDownloadUrl(url); + setBlobUrls([url]); + setDownloadFilename(`${config.filePrefix}${originalFiles[0].name}`); + } else { + const zipFilename = `${config.filePrefix}${processedFiles.length}_files.zip`; + const { zipFile } = await zipFileService.createZipFromFiles(processedFiles, zipFilename); + const url = window.URL.createObjectURL(zipFile); + setDownloadUrl(url); + setBlobUrls([url]); + setDownloadFilename(zipFilename); + } + + const thumbnails = await Promise.all( + processedFiles.map(async (file) => { + try { + const thumbnail = await generateThumbnailForFile(file); + return thumbnail || ''; + } catch (error) { + console.warn(`Failed to generate thumbnail for ${file.name}:`, error); + return ''; + } + }) + ); + + setThumbnails(thumbnails); + setIsGeneratingThumbnails(false); + }, [config.filePrefix, addFiles, cleanupBlobUrls]); + + const executeOperation = useCallback(async (params: TParams, selectedFiles: File[]) => { + if (selectedFiles.length === 0) { + setStatus(t("noFileSelected")); + return; + } + + const validFiles = selectedFiles.filter(file => file.size > 0); + if (validFiles.length === 0) { + setErrorMessage('No valid files to process. All selected files are empty.'); + return; + } + + if (validFiles.length < selectedFiles.length) { + console.warn(`Skipping ${selectedFiles.length - validFiles.length} empty files`); + } + + const { operation, operationId, fileId } = createOperation(params, selectedFiles); + recordOperation(fileId, operation); + + setIsLoading(true); + setErrorMessage(null); + setFiles([]); + setThumbnails([]); + setStatus(t("loading")); + + try { + let processedFiles: File[]; + + if (config.singleFileMode && validFiles.length === 1) { + // Single file mode - direct API call + const formData = config.buildFormData(validFiles[0], params); + const response = await axios.post(config.endpoint, formData, { responseType: 'blob' }); + + if (config.processResponse) { + processedFiles = await config.processResponse(response.data); + } else { + const contentType = response.headers['content-type'] || 'application/pdf'; + const blob = new Blob([response.data], { type: contentType }); + const processedFile = new File([blob], `${config.filePrefix}${validFiles[0].name}`, { type: contentType }); + processedFiles = [processedFile]; + } + } else { + // Multi-file mode - process each file individually + processedFiles = await processMultipleFiles(params, validFiles); + } + + await handleDownloadAndThumbnails(processedFiles, selectedFiles); + + setStatus(t("downloadComplete")); + markOperationApplied(fileId, operationId); + } catch (error: any) { + console.error(error); + let errorMsg = `An error occurred while processing the ${config.operationType} operation.`; + if (error.response?.data && typeof error.response.data === 'string') { + errorMsg = error.response.data; + } else if (error.message) { + errorMsg = error.message; + } + setErrorMessage(errorMsg); + setStatus(`${config.operationType} failed.`); + markOperationFailed(fileId, operationId, errorMsg); + } finally { + setIsLoading(false); + } + }, [t, config, createOperation, recordOperation, markOperationApplied, markOperationFailed, processMultipleFiles, handleDownloadAndThumbnails]); + + const resetResults = useCallback(() => { + cleanupBlobUrls(); + setFiles([]); + setThumbnails([]); + setIsGeneratingThumbnails(false); + setDownloadUrl(null); + setDownloadFilename(''); + setStatus(''); + setErrorMessage(null); + setIsLoading(false); + }, [cleanupBlobUrls]); + + const clearError = useCallback(() => { + setErrorMessage(null); + }, []); + + return { + files, + thumbnails, + isGeneratingThumbnails, + downloadUrl, + downloadFilename, + isLoading, + status, + errorMessage, + executeOperation, + resetResults, + clearError + }; +}; \ No newline at end of file diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx index debd3f5b1..afb2521b4 100644 --- a/frontend/src/hooks/useToolManagement.tsx +++ b/frontend/src/hooks/useToolManagement.tsx @@ -4,6 +4,7 @@ import ContentCutIcon from "@mui/icons-material/ContentCut"; import ZoomInMapIcon from "@mui/icons-material/ZoomInMap"; import SwapHorizIcon from "@mui/icons-material/SwapHoriz"; import ApiIcon from "@mui/icons-material/Api"; +import BuildIcon from "@mui/icons-material/Build"; import { useMultipleEndpointsEnabled } from "./useEndpointConfig"; import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool"; @@ -75,6 +76,15 @@ const toolDefinitions: Record = { description: "Extract text from images using OCR", endpoints: ["ocr-pdf"] }, + repair: { + id: "repair", + icon: , + component: React.lazy(() => import("../tools/Repair")), + maxFiles: -1, + category: "utility", + description: "Repair corrupted or broken PDF files", + endpoints: ["repair"] + }, }; diff --git a/frontend/src/tools/Repair.tsx b/frontend/src/tools/Repair.tsx new file mode 100644 index 000000000..a7197e19d --- /dev/null +++ b/frontend/src/tools/Repair.tsx @@ -0,0 +1,158 @@ +import React, { useEffect, useMemo } from "react"; +import { Button, Stack, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import DownloadIcon from "@mui/icons-material/Download"; +import { useEndpointEnabled } from "../hooks/useEndpointConfig"; +import { useFileContext } from "../contexts/FileContext"; +import { useToolFileSelection } from "../contexts/FileSelectionContext"; + +import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep"; +import OperationButton from "../components/tools/shared/OperationButton"; +import ErrorNotification from "../components/tools/shared/ErrorNotification"; +import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator"; +import ResultsPreview from "../components/tools/shared/ResultsPreview"; + +import { useRepairOperation } from "../hooks/tools/repair/useRepairOperation"; +import { BaseToolProps } from "../types/tool"; + +const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { + const { t } = useTranslation(); + const { setCurrentMode } = useFileContext(); + const { selectedFiles } = useToolFileSelection(); + + const repairOperation = useRepairOperation(); + + // Endpoint validation + const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("repair"); + + useEffect(() => { + repairOperation.resetResults(); + onPreviewFile?.(null); + }, [selectedFiles]); + + const handleRepair = async () => { + try { + await repairOperation.executeOperation(selectedFiles); + if (repairOperation.files && onComplete) { + onComplete(repairOperation.files); + } + } catch (error) { + if (onError) { + onError(error instanceof Error ? error.message : 'Repair operation failed'); + } + } + }; + + const handleThumbnailClick = (file: File) => { + onPreviewFile?.(file); + sessionStorage.setItem('previousMode', 'repair'); + setCurrentMode('viewer'); + }; + + const handleSettingsReset = () => { + repairOperation.resetResults(); + onPreviewFile?.(null); + setCurrentMode('repair'); + }; + + const hasFiles = selectedFiles.length > 0; + const hasResults = repairOperation.files.length > 0 || repairOperation.downloadUrl !== null; + const filesCollapsed = hasFiles; + const settingsCollapsed = hasResults; + + const previewResults = useMemo(() => + repairOperation.files?.map((file, index) => ({ + file, + thumbnail: repairOperation.thumbnails[index] + })) || [], + [repairOperation.files, repairOperation.thumbnails] + ); + + return ( + + + {/* Files Step */} + + + + + {/* Repair Step */} + + + + This tool attempts to repair corrupted or broken PDF files by fixing structural issues and recovering readable content. Each file is processed individually. + + + + + + + {/* Results Step */} + + + {repairOperation.status && ( + {repairOperation.status} + )} + + + + {repairOperation.downloadUrl && ( + + )} + + + + + + + ); +} + +export default Repair; \ No newline at end of file diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index d9c049ae7..4421962b8 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -5,9 +5,9 @@ import { ProcessedFile } from './processing'; import { PDFDocument, PDFPage, PageOperation } from './pageEditor'; -export type ModeType = 'viewer' | 'pageEditor' | 'fileEditor' | 'merge' | 'split' | 'compress' | 'ocr'; +export type ModeType = 'viewer' | 'pageEditor' | 'fileEditor' | 'merge' | 'split' | 'compress' | 'ocr' | 'repair'; -export type OperationType = 'merge' | 'split' | 'compress' | 'add' | 'remove' | 'replace' | 'convert' | 'upload' | 'ocr'; +export type OperationType = 'merge' | 'split' | 'compress' | 'add' | 'remove' | 'replace' | 'convert' | 'upload' | 'ocr' | 'repair'; export interface FileOperation { id: string;