Additional clean up

This commit is contained in:
Reece Browne 2025-08-05 14:29:57 +01:00
parent dcadada7d3
commit 70bebb9a56
9 changed files with 144 additions and 75 deletions

View File

@ -77,31 +77,51 @@ Without cleanup: browser crashes with memory leaks.
- **toolResponseProcessor**: API response handling (single/zip/custom) - **toolResponseProcessor**: API response handling (single/zip/custom)
- **toolOperationTracker**: FileContext integration utilities - **toolOperationTracker**: FileContext integration utilities
**Tool Implementation Pattern**: **Three Tool Patterns**:
1. Create hook in `frontend/src/hooks/tools/[toolname]/use[ToolName]Operation.ts`
2. Define parameters interface and validation
3. Implement `buildFormData` function for API requests
4. Configure `useToolOperation` with endpoints and settings
5. UI components consume the hook's state and actions
**Example Pattern** (see `useCompressOperation.ts`): **Pattern 1: Single-File Tools** (Individual processing)
- Backend processes one file per API call
- Set `multiFileEndpoint: false`
- Examples: Compress, Rotate
```typescript ```typescript
export const useCompressOperation = () => { return useToolOperation({
const { t } = useTranslation(); operationType: 'compress',
endpoint: '/api/v1/misc/compress-pdf',
return useToolOperation<CompressParameters>({ buildFormData: (params, file: File) => { /* single file */ },
operationType: 'compress', multiFileEndpoint: false,
endpoint: '/api/v1/misc/compress-pdf', filePrefix: 'compressed_'
buildFormData, });
filePrefix: 'compressed_', ```
validateParams: (params) => { /* validation logic */ },
getErrorMessage: createStandardErrorHandler(t('compress.error.failed')) **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**: **Benefits**:
- **Consistent**: All tools follow same pattern and interface - **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 - **Maintainable**: Single responsibility hooks, easy to test and modify
- **i18n Ready**: Built-in internationalization support - **i18n Ready**: Built-in internationalization support
- **Type Safe**: Full TypeScript support with generic interfaces - **Type Safe**: Full TypeScript support with generic interfaces

View File

@ -31,14 +31,13 @@ const buildFormData = (parameters: CompressParameters, file: File): FormData =>
export const useCompressOperation = () => { export const useCompressOperation = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return useToolOperation<CompressParameters>({ return useToolOperation<CompressParameters>({
operationType: 'compress', operationType: 'compress',
endpoint: '/api/v1/misc/compress-pdf', endpoint: '/api/v1/misc/compress-pdf',
buildFormData, buildFormData,
filePrefix: 'compressed_', filePrefix: 'compressed_',
singleFileMode: false, // Process files individually 1 multiFileEndpoint: false, // Individual API calls per file
timeout: 60000, // 1 minute timeout per file
validateParams: (params) => { validateParams: (params) => {
if (params.compressionMethod === 'filesize' && !params.fileSizeValue) { if (params.compressionMethod === 'filesize' && !params.fileSizeValue) {
return { valid: false, errors: [t('compress.validation.fileSizeRequired', 'File size value is required when using filesize method')] }; return { valid: false, errors: [t('compress.validation.fileSizeRequired', 'File size value is required when using filesize method')] };

View File

@ -5,10 +5,8 @@ import { ConvertParameters } from './useConvertParameters';
import { detectFileExtension } from '../../../utils/fileUtils'; import { detectFileExtension } from '../../../utils/fileUtils';
import { createFileFromApiResponse } from '../../../utils/fileResponseUtils'; import { createFileFromApiResponse } from '../../../utils/fileResponseUtils';
import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation'; import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperation';
import { getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils'; import { getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
const shouldProcessFilesSeparately = ( const shouldProcessFilesSeparately = (
selectedFiles: File[], selectedFiles: File[],
parameters: ConvertParameters parameters: ConvertParameters
@ -31,18 +29,6 @@ const shouldProcessFilesSeparately = (
); );
}; };
const createFileFromResponse = (
responseData: any,
headers: any,
originalFileName: string,
targetExtension: string
): File => {
const originalName = originalFileName.split('.')[0];
const fallbackFilename = `${originalName}_converted.${targetExtension}`;
return createFileFromApiResponse(responseData, headers, fallbackFilename);
};
const buildFormData = (parameters: ConvertParameters, selectedFiles: File[]): FormData => { const buildFormData = (parameters: ConvertParameters, selectedFiles: File[]): FormData => {
const formData = new FormData(); const formData = new FormData();
@ -83,20 +69,66 @@ const buildFormData = (parameters: ConvertParameters, selectedFiles: File[]): Fo
return formData; return formData;
}; };
const createFileFromResponse = (
responseData: any,
headers: any,
originalFileName: string,
targetExtension: string
): File => {
const originalName = originalFileName.split('.')[0];
const fallbackFilename = `${originalName}_converted.${targetExtension}`;
return createFileFromApiResponse(responseData, headers, fallbackFilename);
};
export const useConvertOperation = () => { export const useConvertOperation = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const customConvertProcessor = useCallback(async (
parameters: ConvertParameters,
selectedFiles: File[]
): Promise<File[]> => {
const processedFiles: File[] = [];
const endpoint = getEndpointUrl(parameters.fromExtension, parameters.toExtension);
if (!endpoint) {
throw new Error('Unsupported conversion format');
}
// Convert-specific routing logic: decide batch vs individual processing
if (shouldProcessFilesSeparately(selectedFiles, parameters)) {
// Individual processing for complex cases (PDF→image, smart detection, etc.)
for (const file of selectedFiles) {
const formData = buildFormData(parameters, [file]);
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
const convertedFile = createFileFromResponse(response.data, response.headers, file.name, parameters.toExtension);
processedFiles.push(convertedFile);
}
} else {
// Batch processing for simple cases (image→PDF combine)
const formData = buildFormData(parameters, selectedFiles);
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
const baseFilename = selectedFiles.length === 1
? selectedFiles[0].name
: 'converted_files';
const convertedFile = createFileFromResponse(response.data, response.headers, baseFilename, parameters.toExtension);
processedFiles.push(convertedFile);
}
return processedFiles;
}, [t]);
return useToolOperation<ConvertParameters>({ return useToolOperation<ConvertParameters>({
operationType: 'convert', operationType: 'convert',
endpoint: (params) => getEndpointUrl(params.fromExtension, params.toExtension) || '', endpoint: '', // Not used with customProcessor but required
buildFormData: buildFormData, // Clean multi-file signature: (params, selectedFiles) => FormData buildFormData, // Not used with customProcessor but required
filePrefix: 'converted_', filePrefix: 'converted_',
responseHandler: { customProcessor: customConvertProcessor, // Convert handles its own routing
type: 'single'
},
validateParams: (params) => { validateParams: (params) => {
// Add any validation if needed
return { valid: true }; return { valid: true };
}, },
getErrorMessage: (error) => { getErrorMessage: (error) => {

View File

@ -87,8 +87,7 @@ export const useOCROperation = () => {
try { try {
const formData = buildFormData(file, parameters); const formData = buildFormData(file, parameters);
const response = await axios.post('/api/v1/misc/ocr-pdf', formData, { const response = await axios.post('/api/v1/misc/ocr-pdf', formData, {
responseType: "blob", responseType: "blob"
timeout: 300000 // 5 minute timeout for OCR
}); });
// Check for HTTP errors // Check for HTTP errors
@ -174,7 +173,6 @@ export const useOCROperation = () => {
buildFormData, // Not used with customProcessor but required buildFormData, // Not used with customProcessor but required
filePrefix: 'ocr_', filePrefix: 'ocr_',
customProcessor: customOCRProcessor, customProcessor: customOCRProcessor,
timeout: 300000, // 5 minute timeout for OCR
validateParams: (params) => { validateParams: (params) => {
if (params.languages.length === 0) { if (params.languages.length === 0) {
return { valid: false, errors: [t('ocr.validation.languageRequired', 'Please select at least one language for OCR processing.')] }; return { valid: false, errors: [t('ocr.validation.languageRequired', 'Please select at least one language for OCR processing.')] };

View File

@ -8,7 +8,6 @@ export interface ApiCallsConfig<TParams = void> {
buildFormData: (file: File, params: TParams) => FormData; buildFormData: (file: File, params: TParams) => FormData;
filePrefix: string; filePrefix: string;
responseHandler?: ResponseHandler; responseHandler?: ResponseHandler;
timeout?: number;
} }
export const useToolApiCalls = <TParams = void>() => { export const useToolApiCalls = <TParams = void>() => {
@ -39,7 +38,6 @@ export const useToolApiCalls = <TParams = void>() => {
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint; const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
const response = await axios.post(endpoint, formData, { const response = await axios.post(endpoint, formData, {
responseType: 'blob', responseType: 'blob',
timeout: config.timeout || 120000,
cancelToken: cancelTokenRef.current.token cancelToken: cancelTokenRef.current.token
}); });

View File

@ -18,38 +18,57 @@ export interface ValidationResult {
export type { ProcessingProgress, ResponseHandler }; export type { ProcessingProgress, ResponseHandler };
/** /**
* Configuration for tool operations defining processing behavior and API integration * Configuration for tool operations defining processing behavior and API integration.
*
* Supports three patterns:
* 1. Single-file tools: multiFileEndpoint: false, processes files individually
* 2. Multi-file tools: multiFileEndpoint: true, single API call with all files
* 3. Complex tools: customProcessor handles all processing logic
*/ */
export interface ToolOperationConfig<TParams = void> { export interface ToolOperationConfig<TParams = void> {
/** Operation identifier for tracking and logging */ /** Operation identifier for tracking and logging */
operationType: string; operationType: string;
/** API endpoint for the operation (can be string or function for dynamic endpoints) */ /**
* API endpoint for the operation. Can be static string or function for dynamic routing.
* Not used when customProcessor is provided.
*/
endpoint: string | ((params: TParams) => string); endpoint: string | ((params: TParams) => string);
/** Builds FormData for API request - signature indicates single-file vs multi-file capability */ /**
* Builds FormData for API request. Signature determines processing approach:
* - (params, file: File) => FormData: Single-file processing
* - (params, files: File[]) => FormData: Multi-file processing
* Not used when customProcessor is provided.
*/
buildFormData: ((params: TParams, file: File) => FormData) | ((params: TParams, files: File[]) => FormData); buildFormData: ((params: TParams, file: File) => FormData) | ((params: TParams, files: File[]) => FormData);
/** Prefix for processed filenames (e.g., 'compressed_', 'repaired_') */ /** Prefix added to processed filenames (e.g., 'compressed_', 'split_') */
filePrefix: string; filePrefix: string;
/** How to handle API responses */ /**
* Whether this tool uses backends that accept MultipartFile[] arrays.
* - true: Single API call with all files (backend uses MultipartFile[])
* - false/undefined: Individual API calls per file (backend uses single MultipartFile)
* Ignored when customProcessor is provided.
*/
multiFileEndpoint?: boolean;
/** How to handle API responses (e.g., ZIP extraction, single file response) */
responseHandler?: ResponseHandler; responseHandler?: ResponseHandler;
/** Process files individually or as a batch */ /**
singleFileMode?: boolean; * Custom processing logic that completely bypasses standard file processing.
* When provided, tool handles all API calls, response processing, and file creation.
/** Custom processing logic that bypasses default file processing */ * Use for tools with complex routing logic or non-standard processing requirements.
*/
customProcessor?: (params: TParams, files: File[]) => Promise<File[]>; customProcessor?: (params: TParams, files: File[]) => Promise<File[]>;
/** Validate parameters before execution */ /** Validate parameters before execution. Return validation errors if invalid. */
validateParams?: (params: TParams) => ValidationResult; validateParams?: (params: TParams) => ValidationResult;
/** Extract user-friendly error messages */ /** Extract user-friendly error messages from API errors */
getErrorMessage?: (error: any) => string; getErrorMessage?: (error: any) => string;
/** Request timeout in milliseconds */
timeout?: number;
} }
/** /**
@ -78,8 +97,16 @@ export interface ToolOperationHook<TParams = void> {
export { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; export { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
/** /**
* Shared hook for tool operations with consistent error handling, progress tracking, * Shared hook for tool operations providing consistent error handling, progress tracking,
* and FileContext integration. Eliminates boilerplate while maintaining flexibility. * and FileContext integration. Eliminates boilerplate while maintaining flexibility.
*
* Supports three tool patterns:
* 1. Single-file tools: Set multiFileEndpoint: false, processes files individually
* 2. Multi-file tools: Set multiFileEndpoint: true, single API call with all files
* 3. Complex tools: Provide customProcessor for full control over processing logic
*
* @param config - Tool operation configuration
* @returns Hook interface with state and execution methods
*/ */
export const useToolOperation = <TParams = void>( export const useToolOperation = <TParams = void>(
config: ToolOperationConfig<TParams> config: ToolOperationConfig<TParams>
@ -133,11 +160,8 @@ export const useToolOperation = <TParams = void>(
actions.setStatus('Processing files...'); actions.setStatus('Processing files...');
processedFiles = await config.customProcessor(params, validFiles); processedFiles = await config.customProcessor(params, validFiles);
} else { } else {
// Detect if buildFormData signature is multi-file or single-file // Use explicit multiFileEndpoint flag to determine processing approach
// Both have 2 params now, so check if second param expects an array if (config.multiFileEndpoint) {
const isMultiFileFormData = /files|selectedFiles/.test(config.buildFormData.toString());
if (isMultiFileFormData) {
// Multi-file processing - single API call with all files // Multi-file processing - single API call with all files
actions.setStatus('Processing files...'); actions.setStatus('Processing files...');
const formData = (config.buildFormData as (params: TParams, files: File[]) => FormData)(params, validFiles); const formData = (config.buildFormData as (params: TParams, files: File[]) => FormData)(params, validFiles);
@ -164,8 +188,7 @@ export const useToolOperation = <TParams = void>(
endpoint: config.endpoint, endpoint: config.endpoint,
buildFormData: (file: File, params: TParams) => (config.buildFormData as (params: TParams, file: File) => FormData)(params, file), buildFormData: (file: File, params: TParams) => (config.buildFormData as (params: TParams, file: File) => FormData)(params, file),
filePrefix: config.filePrefix, filePrefix: config.filePrefix,
responseHandler: config.responseHandler, responseHandler: config.responseHandler
timeout: config.timeout
}; };
processedFiles = await processFiles( processedFiles = await processFiles(
params, params,

View File

@ -63,8 +63,9 @@ export const useSplitOperation = () => {
return useToolOperation<SplitParameters>({ return useToolOperation<SplitParameters>({
operationType: 'split', operationType: 'split',
endpoint: (params) => getEndpoint(params), endpoint: (params) => getEndpoint(params),
buildFormData: buildFormData, // Clean multi-file signature: (params, selectedFiles) => FormData buildFormData: buildFormData, // Multi-file signature: (params, selectedFiles) => FormData
filePrefix: 'split_', filePrefix: 'split_',
multiFileEndpoint: true, // Single API call with all files
responseHandler: { responseHandler: {
type: 'zip', type: 'zip',
useZipExtractor: true useZipExtractor: true

View File

@ -28,8 +28,7 @@ export class EnhancedPDFProcessingService {
thumbnailQuality: 'medium', thumbnailQuality: 'medium',
priorityPageCount: 10, priorityPageCount: 10,
useWebWorker: false, useWebWorker: false,
maxRetries: 3, maxRetries: 3
timeoutMs: 300000 // 5 minutes
}; };
private constructor() {} private constructor() {}
@ -87,7 +86,7 @@ export class EnhancedPDFProcessingService {
estimatedTime: number estimatedTime: number
): Promise<void> { ): Promise<void> {
// Create cancellation token // Create cancellation token
const cancellationToken = ProcessingErrorHandler.createTimeoutController(config.timeoutMs); const cancellationToken = new AbortController();
// Set initial state // Set initial state
const state: ProcessingState = { const state: ProcessingState = {

View File

@ -69,7 +69,6 @@ export interface ProcessingConfig {
priorityPageCount: number; // Number of priority pages to process first priorityPageCount: number; // Number of priority pages to process first
useWebWorker: boolean; useWebWorker: boolean;
maxRetries: number; maxRetries: number;
timeoutMs: number;
} }
export interface FileAnalysis { export interface FileAnalysis {