mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 11:00:47 +02:00
Restructure frontend code to allow for extensions (#4721)
# Description of Changes Move frontend code into `core` folder and add infrastructure for `proprietary` folder to include premium, non-OSS features
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { ProcessedFile, ProcessingState, ProcessingConfig } from '@app/types/processing';
|
||||
import { enhancedPDFProcessingService } from '@app/services/enhancedPDFProcessingService';
|
||||
import { FileHasher } from '@app/utils/fileHash';
|
||||
|
||||
interface UseEnhancedProcessedFilesResult {
|
||||
processedFiles: Map<File, ProcessedFile>;
|
||||
processingStates: Map<string, ProcessingState>;
|
||||
isProcessing: boolean;
|
||||
hasProcessingErrors: boolean;
|
||||
processingProgress: {
|
||||
overall: number;
|
||||
fileProgress: Map<string, number>;
|
||||
estimatedTimeRemaining: number;
|
||||
};
|
||||
cacheStats: {
|
||||
entries: number;
|
||||
totalSizeBytes: number;
|
||||
maxSizeBytes: number;
|
||||
};
|
||||
metrics: {
|
||||
totalFiles: number;
|
||||
completedFiles: number;
|
||||
failedFiles: number;
|
||||
averageProcessingTime: number;
|
||||
cacheHitRate: number;
|
||||
};
|
||||
actions: {
|
||||
cancelProcessing: (fileKey: string) => void;
|
||||
retryProcessing: (file: File) => void;
|
||||
clearCache: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function useEnhancedProcessedFiles(
|
||||
activeFiles: File[],
|
||||
config?: Partial<ProcessingConfig>
|
||||
): UseEnhancedProcessedFilesResult {
|
||||
const [processedFiles, setProcessedFiles] = useState<Map<File, ProcessedFile>>(new Map());
|
||||
const fileHashMapRef = useRef<Map<File, string>>(new Map()); // Use ref to avoid state update loops
|
||||
const [processingStates, setProcessingStates] = useState<Map<string, ProcessingState>>(new Map());
|
||||
|
||||
// Subscribe to processing state changes once
|
||||
useEffect(() => {
|
||||
const unsubscribe = enhancedPDFProcessingService.onProcessingChange(setProcessingStates);
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
// Process files when activeFiles changes
|
||||
useEffect(() => {
|
||||
console.log('useEnhancedProcessedFiles: activeFiles changed', activeFiles.length, 'files');
|
||||
|
||||
if (activeFiles.length === 0) {
|
||||
console.log('useEnhancedProcessedFiles: No active files, clearing processed cache');
|
||||
setProcessedFiles(new Map());
|
||||
// Clear any ongoing processing when no files
|
||||
enhancedPDFProcessingService.clearAllProcessing();
|
||||
return;
|
||||
}
|
||||
|
||||
const processFiles = async () => {
|
||||
const newProcessedFiles = new Map<File, ProcessedFile>();
|
||||
|
||||
for (const file of activeFiles) {
|
||||
// Generate hash for this file
|
||||
const fileHash = await FileHasher.generateHybridHash(file);
|
||||
fileHashMapRef.current.set(file, fileHash);
|
||||
|
||||
// First, check if we have this exact File object cached
|
||||
let existing = processedFiles.get(file);
|
||||
|
||||
// If not found by File object, try to find by hash in case File was recreated
|
||||
if (!existing) {
|
||||
for (const [cachedFile, processed] of processedFiles.entries()) {
|
||||
const cachedHash = fileHashMapRef.current.get(cachedFile);
|
||||
if (cachedHash === fileHash) {
|
||||
existing = processed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
newProcessedFiles.set(file, existing);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const processed = await enhancedPDFProcessingService.processFile(file, config);
|
||||
if (processed) {
|
||||
newProcessedFiles.set(file, processed);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to start processing for ${file.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Only update if the content actually changed
|
||||
const hasChanged = newProcessedFiles.size !== processedFiles.size ||
|
||||
Array.from(newProcessedFiles.keys()).some(file => !processedFiles.has(file));
|
||||
|
||||
if (hasChanged) {
|
||||
setProcessedFiles(newProcessedFiles);
|
||||
}
|
||||
};
|
||||
|
||||
processFiles();
|
||||
}, [activeFiles]); // Only depend on activeFiles to avoid infinite loops
|
||||
|
||||
// Listen for processing completion
|
||||
useEffect(() => {
|
||||
const checkForCompletedFiles = async () => {
|
||||
let hasNewFiles = false;
|
||||
const updatedFiles = new Map(processedFiles);
|
||||
|
||||
// Generate file keys for all files first
|
||||
const fileKeyPromises = activeFiles.map(async (file) => ({
|
||||
file,
|
||||
key: await FileHasher.generateHybridHash(file)
|
||||
}));
|
||||
|
||||
const fileKeyPairs = await Promise.all(fileKeyPromises);
|
||||
|
||||
for (const { file, key } of fileKeyPairs) {
|
||||
// Only check files that don't have processed results yet
|
||||
if (!updatedFiles.has(file)) {
|
||||
const processingState = processingStates.get(key);
|
||||
|
||||
// Check for both processing and recently completed files
|
||||
// This ensures we catch completed files before they're cleaned up
|
||||
if (processingState?.status === 'processing' || processingState?.status === 'completed') {
|
||||
try {
|
||||
const processed = await enhancedPDFProcessingService.processFile(file, config);
|
||||
if (processed) {
|
||||
updatedFiles.set(file, processed);
|
||||
hasNewFiles = true;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors in completion check
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNewFiles) {
|
||||
setProcessedFiles(updatedFiles);
|
||||
}
|
||||
};
|
||||
|
||||
// Check every 500ms for completed processing
|
||||
const interval = setInterval(checkForCompletedFiles, 500);
|
||||
return () => clearInterval(interval);
|
||||
}, [activeFiles, processingStates]);
|
||||
|
||||
|
||||
// Cleanup when activeFiles changes
|
||||
useEffect(() => {
|
||||
const currentFiles = new Set(activeFiles);
|
||||
const previousFiles = Array.from(processedFiles.keys());
|
||||
const removedFiles = previousFiles.filter(file => !currentFiles.has(file));
|
||||
|
||||
if (removedFiles.length > 0) {
|
||||
// Clean up processing service cache
|
||||
enhancedPDFProcessingService.cleanup(removedFiles);
|
||||
|
||||
// Update local state
|
||||
setProcessedFiles(prev => {
|
||||
const updated = new Map();
|
||||
for (const [file, processed] of prev) {
|
||||
if (currentFiles.has(file)) {
|
||||
updated.set(file, processed);
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
}, [activeFiles]);
|
||||
|
||||
// Calculate derived state
|
||||
const isProcessing = processingStates.size > 0;
|
||||
const hasProcessingErrors = Array.from(processingStates.values()).some(state => state.status === 'error');
|
||||
|
||||
// Calculate overall progress
|
||||
const processingProgress = calculateProcessingProgress(processingStates);
|
||||
|
||||
// Get cache stats and metrics
|
||||
const cacheStats = enhancedPDFProcessingService.getCacheStats();
|
||||
const metrics = enhancedPDFProcessingService.getMetrics();
|
||||
|
||||
// Action handlers
|
||||
const actions = {
|
||||
cancelProcessing: (fileKey: string) => {
|
||||
enhancedPDFProcessingService.cancelProcessing(fileKey);
|
||||
},
|
||||
|
||||
retryProcessing: async (file: File) => {
|
||||
try {
|
||||
await enhancedPDFProcessingService.processFile(file, config);
|
||||
} catch (error) {
|
||||
console.error(`Failed to retry processing for ${file.name}:`, error);
|
||||
}
|
||||
},
|
||||
|
||||
clearCache: () => {
|
||||
enhancedPDFProcessingService.clearAll();
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
enhancedPDFProcessingService.clearAllProcessing();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
processedFiles,
|
||||
processingStates,
|
||||
isProcessing,
|
||||
hasProcessingErrors,
|
||||
processingProgress,
|
||||
cacheStats,
|
||||
metrics,
|
||||
actions
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate overall processing progress from individual file states
|
||||
*/
|
||||
function calculateProcessingProgress(states: Map<string, ProcessingState>): {
|
||||
overall: number;
|
||||
fileProgress: Map<string, number>;
|
||||
estimatedTimeRemaining: number;
|
||||
} {
|
||||
if (states.size === 0) {
|
||||
return {
|
||||
overall: 100,
|
||||
fileProgress: new Map(),
|
||||
estimatedTimeRemaining: 0
|
||||
};
|
||||
}
|
||||
|
||||
const fileProgress = new Map<string, number>();
|
||||
let totalProgress = 0;
|
||||
let totalEstimatedTime = 0;
|
||||
|
||||
for (const [fileKey, state] of states) {
|
||||
fileProgress.set(fileKey, state.progress);
|
||||
totalProgress += state.progress;
|
||||
totalEstimatedTime += state.estimatedTimeRemaining || 0;
|
||||
}
|
||||
|
||||
const overall = totalProgress / states.size;
|
||||
const estimatedTimeRemaining = totalEstimatedTime;
|
||||
|
||||
return {
|
||||
overall,
|
||||
fileProgress,
|
||||
estimatedTimeRemaining
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for getting a single processed file with enhanced features
|
||||
*/
|
||||
export function useEnhancedProcessedFile(
|
||||
file: File | null,
|
||||
config?: Partial<ProcessingConfig>
|
||||
): {
|
||||
processedFile: ProcessedFile | null;
|
||||
isProcessing: boolean;
|
||||
processingState: ProcessingState | null;
|
||||
error: string | null;
|
||||
canRetry: boolean;
|
||||
actions: {
|
||||
cancel: () => void;
|
||||
retry: () => void;
|
||||
};
|
||||
} {
|
||||
const result = useEnhancedProcessedFiles(file ? [file] : [], config);
|
||||
|
||||
const processedFile = file ? result.processedFiles.get(file) || null : null;
|
||||
// Note: This is async but we can't await in hook return - consider refactoring if needed
|
||||
const fileKey = file ? '' : '';
|
||||
const processingState = fileKey ? result.processingStates.get(fileKey) || null : null;
|
||||
const isProcessing = !!processingState;
|
||||
const error = processingState?.error?.message || null;
|
||||
const canRetry = processingState?.error?.recoverable || false;
|
||||
|
||||
const actions = {
|
||||
cancel: () => {
|
||||
if (fileKey) {
|
||||
result.actions.cancelProcessing(fileKey);
|
||||
}
|
||||
},
|
||||
retry: () => {
|
||||
if (file) {
|
||||
result.actions.retryProcessing(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
processedFile,
|
||||
isProcessing,
|
||||
processingState,
|
||||
error,
|
||||
canRetry,
|
||||
actions
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user