mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 06:09:23 +00:00
feat: Add support for generating thumbnails with metadata and enhance file processing capabilities
- Introduced `generateThumbnailWithMetadata` function to generate thumbnails and extract page counts for PDF files. - Updated `FileContextProvider` to handle processed files with pre-existing thumbnails and metadata. - Enhanced `useToolOperation` to utilize new thumbnail generation method and manage processed files efficiently. - Refactored `useToolResources` to include the new thumbnail generation method. - Updated relevant components to improve logging and error handling during thumbnail generation.
This commit is contained in:
parent
f691e690e4
commit
c8eb7ac647
@ -46,6 +46,7 @@ import { EnhancedPDFProcessingService } from '../services/enhancedPDFProcessingS
|
||||
import { thumbnailGenerationService } from '../services/thumbnailGenerationService';
|
||||
import { fileStorage } from '../services/fileStorage';
|
||||
import { fileProcessingService } from '../services/fileProcessingService';
|
||||
import { generateThumbnailWithMetadata } from '../utils/thumbnailUtils';
|
||||
|
||||
// Get service instances
|
||||
const enhancedPDFProcessingService = EnhancedPDFProcessingService.getInstance();
|
||||
@ -419,7 +420,7 @@ export function FileContextProvider({
|
||||
|
||||
// Action implementations
|
||||
const addFiles = useCallback(async (files: File[]): Promise<File[]> => {
|
||||
// Three-tier deduplication: UUID (primary key) + quickKey (soft dedupe) + contentHash (hard dedupe)
|
||||
console.log(`📄 addFiles: Adding ${files.length} files with immediate thumbnail generation`);
|
||||
const fileRecords: FileRecord[] = [];
|
||||
const addedFiles: File[] = [];
|
||||
|
||||
@ -443,8 +444,41 @@ export function FileContextProvider({
|
||||
// Store File in ref map
|
||||
filesRef.current.set(fileId, file);
|
||||
|
||||
// Create record
|
||||
// Generate thumbnail and page count immediately
|
||||
let thumbnail: string | undefined;
|
||||
let pageCount: number = 1;
|
||||
try {
|
||||
console.log(`📄 Generating immediate thumbnail and metadata for ${file.name}`);
|
||||
const result = await generateThumbnailWithMetadata(file);
|
||||
thumbnail = result.thumbnail;
|
||||
pageCount = result.pageCount;
|
||||
console.log(`📄 Generated immediate metadata for ${file.name}: ${pageCount} pages, thumbnail: ${!!thumbnail}`);
|
||||
} catch (error) {
|
||||
console.warn(`📄 Failed to generate immediate metadata for ${file.name}:`, error);
|
||||
// Continue with defaults
|
||||
}
|
||||
|
||||
// Create record with immediate thumbnail and page metadata
|
||||
const record = toFileRecord(file, fileId);
|
||||
if (thumbnail) {
|
||||
record.thumbnailUrl = thumbnail;
|
||||
}
|
||||
|
||||
// Create initial processedFile metadata with page count
|
||||
if (pageCount > 0) {
|
||||
record.processedFile = {
|
||||
totalPages: pageCount,
|
||||
pages: Array.from({ length: pageCount }, (_, index) => ({
|
||||
pageNumber: index + 1,
|
||||
thumbnail: index === 0 ? thumbnail : undefined, // Only first page gets thumbnail initially
|
||||
rotation: 0,
|
||||
splitBefore: false
|
||||
})),
|
||||
thumbnailUrl: thumbnail,
|
||||
lastProcessed: Date.now()
|
||||
};
|
||||
console.log(`📄 addFiles: Created initial processedFile metadata for ${file.name} with ${pageCount} pages`);
|
||||
}
|
||||
|
||||
// Add to deduplication tracking
|
||||
existingQuickKeys.add(quickKey);
|
||||
@ -452,29 +486,40 @@ export function FileContextProvider({
|
||||
fileRecords.push(record);
|
||||
addedFiles.push(file);
|
||||
|
||||
// Start centralized file processing (async, non-blocking) - SINGLE CALL
|
||||
// Start background processing for validation only (we already have thumbnail and page count)
|
||||
fileProcessingService.processFile(file, fileId).then(result => {
|
||||
// Only update if file still exists in context
|
||||
if (filesRef.current.has(fileId)) {
|
||||
if (result.success && result.metadata) {
|
||||
// Update with processed metadata using dispatch directly
|
||||
dispatch({
|
||||
type: 'UPDATE_FILE_RECORD',
|
||||
payload: {
|
||||
id: fileId,
|
||||
updates: {
|
||||
processedFile: result.metadata,
|
||||
thumbnailUrl: result.metadata.thumbnailUrl
|
||||
// Only log if page count differs from our immediate calculation
|
||||
const initialPageCount = pageCount;
|
||||
if (result.metadata.totalPages !== initialPageCount) {
|
||||
console.log(`📄 Page count validation: ${file.name} initial=${initialPageCount} → final=${result.metadata.totalPages} pages`);
|
||||
// Update with the validated page count, but preserve existing thumbnail
|
||||
dispatch({
|
||||
type: 'UPDATE_FILE_RECORD',
|
||||
payload: {
|
||||
id: fileId,
|
||||
updates: {
|
||||
processedFile: {
|
||||
...result.metadata,
|
||||
// Preserve our immediate thumbnail if we have one
|
||||
thumbnailUrl: thumbnail || result.metadata.thumbnailUrl
|
||||
},
|
||||
// Keep existing thumbnailUrl if we have one
|
||||
thumbnailUrl: thumbnail || result.metadata.thumbnailUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(`✅ File processing complete for ${file.name}: ${result.metadata.totalPages} pages`);
|
||||
});
|
||||
} else {
|
||||
console.log(`✅ Page count validation passed for ${file.name}: ${result.metadata.totalPages} pages (immediate generation was correct)`);
|
||||
}
|
||||
|
||||
// Optional: Persist to IndexedDB if enabled (reuse the same result)
|
||||
// Optional: Persist to IndexedDB if enabled
|
||||
if (enablePersistence) {
|
||||
try {
|
||||
const thumbnail = result.metadata.thumbnailUrl;
|
||||
fileStorage.storeFile(file, fileId, thumbnail).then(() => {
|
||||
const finalThumbnail = thumbnail || result.metadata.thumbnailUrl;
|
||||
fileStorage.storeFile(file, fileId, finalThumbnail).then(() => {
|
||||
console.log('File persisted to IndexedDB:', fileId);
|
||||
}).catch(error => {
|
||||
console.warn('Failed to persist file to IndexedDB:', error);
|
||||
@ -484,11 +529,11 @@ export function FileContextProvider({
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn(`❌ File processing failed for ${file.name}:`, result.error);
|
||||
console.warn(`❌ Background file processing failed for ${file.name}:`, result.error);
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error(`❌ File processing error for ${file.name}:`, error);
|
||||
console.error(`❌ Background file processing error for ${file.name}:`, error);
|
||||
});
|
||||
|
||||
}
|
||||
@ -502,7 +547,118 @@ export function FileContextProvider({
|
||||
return addedFiles;
|
||||
}, [enablePersistence]); // Remove updateFileRecord dependency
|
||||
|
||||
// NEW: Add processed files with pre-existing thumbnails and metadata (for tool outputs)
|
||||
const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise<File[]> => {
|
||||
console.log(`📄 addProcessedFiles: Adding ${filesWithThumbnails.length} processed files with pre-existing thumbnails`);
|
||||
const fileRecords: FileRecord[] = [];
|
||||
const addedFiles: File[] = [];
|
||||
|
||||
// Build quickKey lookup from existing files for deduplication
|
||||
const existingQuickKeys = new Set<string>();
|
||||
Object.values(stateRef.current.files.byId).forEach(record => {
|
||||
existingQuickKeys.add(record.quickKey);
|
||||
});
|
||||
|
||||
for (const { file, thumbnail, pageCount } of filesWithThumbnails) {
|
||||
const quickKey = createQuickKey(file);
|
||||
|
||||
// Soft deduplication: Check if file already exists by metadata
|
||||
if (existingQuickKeys.has(quickKey)) {
|
||||
console.log(`📄 Skipping duplicate processed file: ${file.name} (already exists)`);
|
||||
continue; // Skip duplicate file
|
||||
}
|
||||
|
||||
const fileId = createFileId(); // UUID-based, zero collisions
|
||||
|
||||
// Store File in ref map
|
||||
filesRef.current.set(fileId, file);
|
||||
|
||||
// Create record with pre-existing thumbnail and page metadata
|
||||
const record = toFileRecord(file, fileId);
|
||||
if (thumbnail) {
|
||||
record.thumbnailUrl = thumbnail;
|
||||
}
|
||||
|
||||
// If we have page count, create initial processedFile metadata
|
||||
if (pageCount && pageCount > 0) {
|
||||
record.processedFile = {
|
||||
totalPages: pageCount,
|
||||
pages: Array.from({ length: pageCount }, (_, index) => ({
|
||||
pageNumber: index + 1,
|
||||
thumbnail: index === 0 ? thumbnail : undefined, // Only first page gets thumbnail initially
|
||||
rotation: 0,
|
||||
splitBefore: false
|
||||
})),
|
||||
thumbnailUrl: thumbnail,
|
||||
lastProcessed: Date.now()
|
||||
};
|
||||
console.log(`📄 addProcessedFiles: Created initial processedFile metadata for ${file.name} with ${pageCount} pages`);
|
||||
}
|
||||
|
||||
// Add to deduplication tracking
|
||||
existingQuickKeys.add(quickKey);
|
||||
|
||||
fileRecords.push(record);
|
||||
addedFiles.push(file);
|
||||
|
||||
// Start background processing for page metadata only (thumbnail already provided)
|
||||
fileProcessingService.processFile(file, fileId).then(result => {
|
||||
// Only update if file still exists in context
|
||||
if (filesRef.current.has(fileId)) {
|
||||
if (result.success && result.metadata) {
|
||||
// Update with processed metadata but preserve existing thumbnail
|
||||
dispatch({
|
||||
type: 'UPDATE_FILE_RECORD',
|
||||
payload: {
|
||||
id: fileId,
|
||||
updates: {
|
||||
processedFile: result.metadata,
|
||||
// Keep existing thumbnail if we already have one, otherwise use processed one
|
||||
thumbnailUrl: thumbnail || result.metadata.thumbnailUrl
|
||||
}
|
||||
}
|
||||
});
|
||||
// Only log if page count changed (meaning our initial guess was wrong)
|
||||
const initialPageCount = pageCount || 1;
|
||||
if (result.metadata.totalPages !== initialPageCount) {
|
||||
console.log(`📄 Page count updated for ${file.name}: ${initialPageCount} → ${result.metadata.totalPages} pages`);
|
||||
} else {
|
||||
console.log(`✅ Processed file metadata complete for ${file.name}: ${result.metadata.totalPages} pages (thumbnail: ${thumbnail ? 'PRE-EXISTING' : 'GENERATED'})`);
|
||||
}
|
||||
|
||||
// Optional: Persist to IndexedDB if enabled
|
||||
if (enablePersistence) {
|
||||
try {
|
||||
const finalThumbnail = thumbnail || result.metadata.thumbnailUrl;
|
||||
fileStorage.storeFile(file, fileId, finalThumbnail).then(() => {
|
||||
console.log('Processed file persisted to IndexedDB:', fileId);
|
||||
}).catch(error => {
|
||||
console.warn('Failed to persist processed file to IndexedDB:', error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to initiate processed file persistence:', error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn(`❌ Processed file background processing failed for ${file.name}:`, result.error);
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error(`❌ Processed file background processing error for ${file.name}:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
// Only dispatch if we have new files
|
||||
if (fileRecords.length > 0) {
|
||||
dispatch({ type: 'ADD_FILES', payload: { fileRecords } });
|
||||
}
|
||||
|
||||
console.log(`📄 Added ${fileRecords.length} processed files with pre-existing thumbnails`);
|
||||
return addedFiles;
|
||||
}, [enablePersistence]);
|
||||
|
||||
// NEW: Add stored files with preserved IDs to prevent duplicates across sessions
|
||||
// This is the CORRECT way to handle files from IndexedDB storage - no File object mutation
|
||||
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>): Promise<File[]> => {
|
||||
const fileRecords: FileRecord[] = [];
|
||||
const addedFiles: File[] = [];
|
||||
@ -635,6 +791,7 @@ export function FileContextProvider({
|
||||
// Memoized actions to prevent re-renders
|
||||
const actions = useMemo<FileContextActions>(() => ({
|
||||
addFiles,
|
||||
addProcessedFiles,
|
||||
addStoredFiles,
|
||||
removeFiles,
|
||||
updateFileRecord,
|
||||
@ -658,7 +815,7 @@ export function FileContextProvider({
|
||||
setMode: (mode: ModeType) => dispatch({ type: 'SET_CURRENT_MODE', payload: mode }),
|
||||
confirmNavigation,
|
||||
cancelNavigation
|
||||
}), [addFiles, addStoredFiles, removeFiles, cleanupAllFiles, setHasUnsavedChanges, confirmNavigation, cancelNavigation]);
|
||||
}), [addFiles, addProcessedFiles, addStoredFiles, removeFiles, cleanupAllFiles, setHasUnsavedChanges, confirmNavigation, cancelNavigation]);
|
||||
|
||||
// Split context values to minimize re-renders
|
||||
const stateValue = useMemo<FileContextStateValue>(() => ({
|
||||
@ -677,6 +834,7 @@ export function FileContextProvider({
|
||||
...state.ui,
|
||||
// Action compatibility layer
|
||||
addFiles,
|
||||
addProcessedFiles,
|
||||
addStoredFiles,
|
||||
removeFiles,
|
||||
updateFileRecord,
|
||||
@ -701,7 +859,7 @@ export function FileContextProvider({
|
||||
get activeFiles() { return selectors.getFiles(); }, // Getter to avoid creating new arrays on every render
|
||||
// Selectors
|
||||
...selectors
|
||||
}), [state, actions, addFiles, addStoredFiles, removeFiles, updateFileRecord, setHasUnsavedChanges, requestNavigation, confirmNavigation, cancelNavigation, trackBlobUrl, trackPdfDocument, cleanupFile, scheduleCleanup]); // Removed selectors dependency
|
||||
}), [state, actions, addFiles, addProcessedFiles, addStoredFiles, removeFiles, updateFileRecord, setHasUnsavedChanges, requestNavigation, confirmNavigation, cancelNavigation, trackBlobUrl, trackPdfDocument, cleanupFile, scheduleCleanup]); // Removed selectors dependency
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
|
@ -121,7 +121,7 @@ export const useToolOperation = <TParams = void>(
|
||||
// Composed hooks
|
||||
const { state, actions } = useToolState();
|
||||
const { processFiles, cancelOperation: cancelApiCalls } = useToolApiCalls<TParams>();
|
||||
const { generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles } = useToolResources();
|
||||
const { generateThumbnails, generateThumbnailsWithMetadata, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles } = useToolResources();
|
||||
|
||||
const executeOperation = useCallback(async (
|
||||
params: TParams,
|
||||
@ -167,23 +167,31 @@ export const useToolOperation = <TParams = void>(
|
||||
// Use explicit multiFileEndpoint flag to determine processing approach
|
||||
if (config.multiFileEndpoint) {
|
||||
// Multi-file processing - single API call with all files
|
||||
console.log(`🚀 useToolOperation: Multi-file processing for ${config.operationType} with ${validFiles.length} files`);
|
||||
actions.setStatus('Processing files...');
|
||||
const formData = (config.buildFormData as (params: TParams, files: File[]) => FormData)(params, validFiles);
|
||||
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
|
||||
console.log(`🚀 Calling endpoint: ${endpoint}`);
|
||||
|
||||
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
||||
console.log(`🚀 Received response: ${response.data.size} bytes, type: ${response.data.type}`);
|
||||
|
||||
// Multi-file responses are typically ZIP files that need extraction
|
||||
if (config.responseHandler) {
|
||||
console.log(`🚀 Using custom responseHandler for ${config.operationType}`);
|
||||
// Use custom responseHandler for multi-file (handles ZIP extraction)
|
||||
processedFiles = await config.responseHandler(response.data, validFiles);
|
||||
} else {
|
||||
console.log(`🚀 Using default ZIP extraction for ${config.operationType}`);
|
||||
// Default: assume ZIP response for multi-file endpoints
|
||||
processedFiles = await extractZipFiles(response.data);
|
||||
console.log(`🚀 Extracted ${processedFiles.length} files from ZIP`);
|
||||
|
||||
if (processedFiles.length === 0) {
|
||||
console.log(`🚀 ZIP extraction failed, trying generic fallback`);
|
||||
// Try the generic extraction as fallback
|
||||
processedFiles = await extractAllZipFiles(response.data);
|
||||
console.log(`🚀 Generic fallback extracted ${processedFiles.length} files`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -205,21 +213,35 @@ export const useToolOperation = <TParams = void>(
|
||||
}
|
||||
|
||||
if (processedFiles.length > 0) {
|
||||
console.log(`🚀 useToolOperation: Processing complete. ${processedFiles.length} files ready for thumbnails:`,
|
||||
processedFiles.map((f, i) => `[${i}]: ${f.name} (${f.type}, ${f.size} bytes)`));
|
||||
actions.setFiles(processedFiles);
|
||||
|
||||
// Generate thumbnails and download URL concurrently
|
||||
// Generate thumbnails with metadata and download URL concurrently
|
||||
actions.setGeneratingThumbnails(true);
|
||||
const [thumbnails, downloadInfo] = await Promise.all([
|
||||
generateThumbnails(processedFiles),
|
||||
const [thumbnailResults, downloadInfo] = await Promise.all([
|
||||
generateThumbnailsWithMetadata(processedFiles),
|
||||
createDownloadInfo(processedFiles, config.operationType)
|
||||
]);
|
||||
actions.setGeneratingThumbnails(false);
|
||||
|
||||
// Extract thumbnails for tool state and page counts for context
|
||||
const thumbnails = thumbnailResults.map(r => r.thumbnail || '');
|
||||
const pageCounts = thumbnailResults.map(r => r.pageCount);
|
||||
|
||||
console.log(`⚡ useToolOperation: Generated ${thumbnails.length} thumbnails with page counts for ${config.operationType}:`,
|
||||
thumbnailResults.map((r, i) => `[${i}]: ${r.thumbnail ? 'PRESENT' : 'MISSING'} (${r.pageCount} pages)`));
|
||||
actions.setThumbnails(thumbnails);
|
||||
actions.setDownloadInfo(downloadInfo.url, downloadInfo.filename);
|
||||
|
||||
// Add to file context
|
||||
await fileActions.addFiles(processedFiles);
|
||||
// Add to file context WITH pre-existing thumbnails AND page counts to avoid duplicate processing
|
||||
const filesWithMetadata = processedFiles.map((file, index) => ({
|
||||
file,
|
||||
thumbnail: thumbnails[index] || undefined,
|
||||
pageCount: pageCounts[index] || undefined
|
||||
}));
|
||||
console.log(`📄 useToolOperation: Adding ${filesWithMetadata.length} processed files with pre-existing thumbnails and page counts to context`);
|
||||
await fileActions.addProcessedFiles(filesWithMetadata);
|
||||
|
||||
markOperationApplied(fileId, operationId);
|
||||
}
|
||||
@ -233,7 +255,7 @@ export const useToolOperation = <TParams = void>(
|
||||
actions.setLoading(false);
|
||||
actions.setProgress(null);
|
||||
}
|
||||
}, [t, config, actions, processFiles, generateThumbnails, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles, fileActions.addFiles]);
|
||||
}, [t, config, actions, processFiles, generateThumbnailsWithMetadata, createDownloadInfo, cleanupBlobUrls, extractZipFiles, extractAllZipFiles, fileActions.addProcessedFiles]);
|
||||
|
||||
const cancelOperation = useCallback(() => {
|
||||
cancelApiCalls();
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
|
||||
import { generateThumbnailForFile, generateThumbnailWithMetadata, ThumbnailWithMetadata } from '../../../utils/thumbnailUtils';
|
||||
import { zipFileService } from '../../../services/zipFileService';
|
||||
|
||||
|
||||
@ -43,23 +43,52 @@ export const useToolResources = () => {
|
||||
}, []); // No dependencies - use ref to access current URLs
|
||||
|
||||
const generateThumbnails = useCallback(async (files: File[]): Promise<string[]> => {
|
||||
console.log(`🖼️ useToolResources.generateThumbnails: Starting for ${files.length} files`);
|
||||
const thumbnails: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
console.log(`🖼️ Generating thumbnail for: ${file.name} (${file.type}, ${file.size} bytes)`);
|
||||
const thumbnail = await generateThumbnailForFile(file);
|
||||
console.log(`🖼️ Generated thumbnail for ${file.name}:`, thumbnail ? 'SUCCESS' : 'FAILED (no thumbnail returned)');
|
||||
if (thumbnail) {
|
||||
thumbnails.push(thumbnail);
|
||||
} else {
|
||||
console.warn(`🖼️ No thumbnail returned for ${file.name}`);
|
||||
thumbnails.push('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to generate thumbnail for ${file.name}:`, error);
|
||||
console.warn(`🖼️ Failed to generate thumbnail for ${file.name}:`, error);
|
||||
thumbnails.push('');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🖼️ useToolResources.generateThumbnails: Complete. Generated ${thumbnails.filter(t => t).length}/${files.length} thumbnails`);
|
||||
return thumbnails;
|
||||
}, []);
|
||||
|
||||
const generateThumbnailsWithMetadata = useCallback(async (files: File[]): Promise<ThumbnailWithMetadata[]> => {
|
||||
console.log(`🖼️ useToolResources.generateThumbnailsWithMetadata: Starting for ${files.length} files`);
|
||||
const results: ThumbnailWithMetadata[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
console.log(`🖼️ Generating thumbnail with metadata for: ${file.name} (${file.type}, ${file.size} bytes)`);
|
||||
const result = await generateThumbnailWithMetadata(file);
|
||||
console.log(`🖼️ Generated thumbnail with metadata for ${file.name}:`,
|
||||
result.thumbnail ? 'SUCCESS' : 'FAILED (no thumbnail)',
|
||||
`${result.pageCount} pages`);
|
||||
results.push(result);
|
||||
} catch (error) {
|
||||
console.warn(`🖼️ Failed to generate thumbnail with metadata for ${file.name}:`, error);
|
||||
results.push({ thumbnail: '', pageCount: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🖼️ useToolResources.generateThumbnailsWithMetadata: Complete. Generated ${results.filter(r => r.thumbnail).length}/${files.length} thumbnails with metadata`);
|
||||
return results;
|
||||
}, []);
|
||||
|
||||
const extractZipFiles = useCallback(async (zipBlob: Blob): Promise<File[]> => {
|
||||
try {
|
||||
const zipFile = new File([zipBlob], 'temp.zip', { type: 'application/zip' });
|
||||
@ -116,6 +145,7 @@ export const useToolResources = () => {
|
||||
|
||||
return {
|
||||
generateThumbnails,
|
||||
generateThumbnailsWithMetadata,
|
||||
createDownloadInfo,
|
||||
extractZipFiles,
|
||||
extractAllZipFiles,
|
||||
|
@ -88,6 +88,8 @@ export const useToolState = () => {
|
||||
}, []);
|
||||
|
||||
const setThumbnails = useCallback((thumbnails: string[]) => {
|
||||
console.log(`🔧 useToolState.setThumbnails: Setting ${thumbnails.length} thumbnails:`,
|
||||
thumbnails.map((t, i) => `[${i}]: ${t ? 'PRESENT' : 'MISSING'}`));
|
||||
dispatch({ type: 'SET_THUMBNAILS', payload: thumbnails });
|
||||
}, []);
|
||||
|
||||
|
@ -47,7 +47,7 @@ export const fileOperationsService = {
|
||||
}
|
||||
|
||||
try {
|
||||
await fileStorage.init();
|
||||
// fileStorage.init() no longer needed - using centralized IndexedDB manager
|
||||
const storedFiles = await fileStorage.getAllFileMetadata();
|
||||
|
||||
// Detect if IndexedDB was purged by comparing with current UI state
|
||||
@ -189,7 +189,7 @@ export const fileOperationsService = {
|
||||
if (currentFiles.length === 0) return false;
|
||||
|
||||
try {
|
||||
await fileStorage.init();
|
||||
// fileStorage.init() no longer needed - using centralized IndexedDB manager
|
||||
const storedFiles = await fileStorage.getAllFileMetadata();
|
||||
return storedFiles.length === 0; // Purge detected if no files in storage but UI shows files
|
||||
} catch (error) {
|
||||
|
@ -412,7 +412,8 @@ class FileStorageService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert StoredFile back to File object for compatibility
|
||||
* Convert StoredFile back to pure File object without mutations
|
||||
* Returns a clean File object - use FileContext.addStoredFiles() for proper metadata handling
|
||||
*/
|
||||
createFileFromStored(storedFile: StoredFile): File {
|
||||
if (!storedFile || !storedFile.data) {
|
||||
@ -429,13 +430,27 @@ class FileStorageService {
|
||||
lastModified: storedFile.lastModified
|
||||
});
|
||||
|
||||
// Add custom properties for compatibility
|
||||
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
|
||||
Object.defineProperty(file, 'thumbnail', { value: storedFile.thumbnail, writable: false });
|
||||
|
||||
// Returns pure File object - no mutations
|
||||
// Use FileContext.addStoredFiles() to properly associate with metadata
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert StoredFile to the format expected by FileContext.addStoredFiles()
|
||||
* This is the recommended way to load stored files into FileContext
|
||||
*/
|
||||
createFileWithMetadata(storedFile: StoredFile): { file: File; originalId: string; metadata: { thumbnail?: string } } {
|
||||
const file = this.createFileFromStored(storedFile);
|
||||
|
||||
return {
|
||||
file,
|
||||
originalId: storedFile.id,
|
||||
metadata: {
|
||||
thumbnail: storedFile.thumbnail
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create blob URL for stored file
|
||||
*/
|
||||
|
@ -69,13 +69,17 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const filesCollapsed = hasFiles;
|
||||
const settingsCollapsed = hasResults;
|
||||
|
||||
const previewResults = useMemo(() =>
|
||||
splitOperation.files?.map((file, index) => ({
|
||||
const previewResults = useMemo(() => {
|
||||
const results = splitOperation.files?.map((file, index) => ({
|
||||
file,
|
||||
thumbnail: splitOperation.thumbnails[index]
|
||||
})) || [],
|
||||
[splitOperation.files, splitOperation.thumbnails]
|
||||
);
|
||||
})) || [];
|
||||
|
||||
console.log(`🔧 Split tool preview: ${results.length} files, thumbnails:`,
|
||||
splitOperation.thumbnails.map((t, i) => `[${i}]: ${t ? 'PRESENT' : 'MISSING'}`));
|
||||
|
||||
return results;
|
||||
}, [splitOperation.files, splitOperation.thumbnails]);
|
||||
|
||||
return (
|
||||
<ToolStepContainer>
|
||||
|
@ -191,6 +191,7 @@ export type FileContextAction =
|
||||
export interface FileContextActions {
|
||||
// File management - lightweight actions only
|
||||
addFiles: (files: File[]) => Promise<File[]>;
|
||||
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<File[]>;
|
||||
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => Promise<File[]>;
|
||||
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => void;
|
||||
updateFileRecord: (id: FileId, updates: Partial<FileRecord>) => void;
|
||||
|
@ -1,8 +1,34 @@
|
||||
import { FileWithUrl } from "../types/file";
|
||||
import { StoredFile, fileStorage } from "../services/fileStorage";
|
||||
|
||||
/**
|
||||
* @deprecated File objects no longer have mutated ID properties.
|
||||
* The new system maintains pure File objects and tracks IDs separately in FileContext.
|
||||
* Use FileContext selectors to get file IDs instead.
|
||||
*/
|
||||
export function getFileId(file: File): string | null {
|
||||
return (file as File & { id?: string }).id || null;
|
||||
const legacyId = (file as File & { id?: string }).id;
|
||||
if (legacyId) {
|
||||
console.warn('DEPRECATED: getFileId() found legacy mutated File object. Use FileContext selectors instead.');
|
||||
}
|
||||
return legacyId || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file ID for a File object using FileContext state (new system)
|
||||
* @param file File object to find ID for
|
||||
* @param fileState Current FileContext state
|
||||
* @returns File ID or null if not found
|
||||
*/
|
||||
export function getFileIdFromContext(file: File, fileIds: string[], getFile: (id: string) => File | undefined): string | null {
|
||||
// Find the file ID by comparing File objects
|
||||
for (const id of fileIds) {
|
||||
const contextFile = getFile(id);
|
||||
if (contextFile === file) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,7 +107,7 @@ export function createEnhancedFileFromStored(storedFile: StoredFile, thumbnail?:
|
||||
*/
|
||||
export async function loadFilesFromIndexedDB(): Promise<FileWithUrl[]> {
|
||||
try {
|
||||
await fileStorage.init();
|
||||
// fileStorage.init() no longer needed - using centralized IndexedDB manager
|
||||
const storedFiles = await fileStorage.getAllFileMetadata();
|
||||
|
||||
if (storedFiles.length === 0) {
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { getDocument } from "pdfjs-dist";
|
||||
|
||||
export interface ThumbnailWithMetadata {
|
||||
thumbnail: string | undefined;
|
||||
pageCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate thumbnail scale based on file size
|
||||
* Smaller files get higher quality, larger files get lower quality
|
||||
@ -166,27 +171,36 @@ function formatFileSize(bytes: number): string {
|
||||
* Returns base64 data URL or undefined if generation fails
|
||||
*/
|
||||
export async function generateThumbnailForFile(file: File): Promise<string | undefined> {
|
||||
console.log(`🎯 generateThumbnailForFile: Starting for ${file.name} (${file.type}, ${file.size} bytes)`);
|
||||
|
||||
// Skip thumbnail generation for very large files to avoid memory issues
|
||||
if (file.size >= 100 * 1024 * 1024) { // 100MB limit
|
||||
console.log('Skipping thumbnail generation for large file:', file.name);
|
||||
return generatePlaceholderThumbnail(file);
|
||||
console.log('🎯 Skipping thumbnail generation for large file:', file.name);
|
||||
const placeholder = generatePlaceholderThumbnail(file);
|
||||
console.log('🎯 Generated placeholder thumbnail for large file:', file.name);
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
// Handle image files - use original file directly
|
||||
if (file.type.startsWith('image/')) {
|
||||
return URL.createObjectURL(file);
|
||||
console.log('🎯 Creating blob URL for image file:', file.name);
|
||||
const url = URL.createObjectURL(file);
|
||||
console.log('🎯 Created image blob URL:', url);
|
||||
return url;
|
||||
}
|
||||
|
||||
// Handle PDF files
|
||||
if (!file.type.startsWith('application/pdf')) {
|
||||
console.log('File is not a PDF or image, generating placeholder:', file.name);
|
||||
return generatePlaceholderThumbnail(file);
|
||||
console.log('🎯 File is not a PDF or image, generating placeholder:', file.name);
|
||||
const placeholder = generatePlaceholderThumbnail(file);
|
||||
console.log('🎯 Generated placeholder thumbnail for non-PDF file:', file.name);
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
// Calculate quality scale based on file size
|
||||
console.log('Generating thumbnail for', file.name);
|
||||
console.log('🎯 Generating PDF thumbnail for', file.name);
|
||||
const scale = calculateScaleFromFileSize(file.size);
|
||||
console.log(`Using scale ${scale} for ${file.name} (${(file.size / 1024 / 1024).toFixed(1)}MB)`);
|
||||
console.log(`🎯 Using scale ${scale} for ${file.name} (${(file.size / 1024 / 1024).toFixed(1)}MB)`);
|
||||
try {
|
||||
// Only read first 2MB for thumbnail generation to save memory
|
||||
const chunkSize = 2 * 1024 * 1024; // 2MB
|
||||
@ -215,13 +229,14 @@ export async function generateThumbnailForFile(file: File): Promise<string | und
|
||||
|
||||
// Immediately clean up memory after thumbnail generation
|
||||
pdf.destroy();
|
||||
console.log('Thumbnail generated and PDF destroyed for', file.name);
|
||||
console.log('🎯 PDF thumbnail successfully generated for', file.name, 'size:', thumbnail.length);
|
||||
|
||||
return thumbnail;
|
||||
} catch (error) {
|
||||
console.warn('🎯 Error generating PDF thumbnail for', file.name, ':', error);
|
||||
if (error instanceof Error) {
|
||||
if (error.name === 'InvalidPDFException') {
|
||||
console.warn(`PDF structure issue for ${file.name} - using fallback thumbnail`);
|
||||
console.warn(`🎯 PDF structure issue for ${file.name} - trying fallback with full file`);
|
||||
// Return a placeholder or try with full file instead of chunk
|
||||
try {
|
||||
const fullArrayBuffer = await file.arrayBuffer();
|
||||
@ -247,17 +262,132 @@ export async function generateThumbnailForFile(file: File): Promise<string | und
|
||||
const thumbnail = canvas.toDataURL();
|
||||
|
||||
pdf.destroy();
|
||||
console.log('🎯 Fallback PDF thumbnail generation succeeded for', file.name);
|
||||
return thumbnail;
|
||||
} catch (fallbackError) {
|
||||
console.warn('Fallback thumbnail generation also failed for', file.name, fallbackError);
|
||||
return undefined;
|
||||
console.warn('🎯 Fallback thumbnail generation also failed for', file.name, fallbackError);
|
||||
console.log('🎯 Using placeholder thumbnail for', file.name);
|
||||
return generatePlaceholderThumbnail(file);
|
||||
}
|
||||
} else {
|
||||
console.warn('Failed to generate thumbnail for', file.name, error);
|
||||
return undefined;
|
||||
console.warn('🎯 Non-PDF error generating thumbnail for', file.name, error);
|
||||
console.log('🎯 Using placeholder thumbnail for', file.name);
|
||||
return generatePlaceholderThumbnail(file);
|
||||
}
|
||||
}
|
||||
console.warn('Unknown error generating thumbnail for', file.name, error);
|
||||
return undefined;
|
||||
console.warn('🎯 Unknown error generating thumbnail for', file.name, error);
|
||||
console.log('🎯 Using placeholder thumbnail for', file.name);
|
||||
return generatePlaceholderThumbnail(file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate thumbnail and extract page count for a PDF file
|
||||
* Returns both thumbnail and metadata in a single pass
|
||||
*/
|
||||
export async function generateThumbnailWithMetadata(file: File): Promise<ThumbnailWithMetadata> {
|
||||
console.log(`🎯 generateThumbnailWithMetadata: Starting for ${file.name} (${file.type}, ${file.size} bytes)`);
|
||||
|
||||
// Non-PDF files default to 1 page
|
||||
if (!file.type.startsWith('application/pdf')) {
|
||||
console.log('🎯 File is not a PDF, generating placeholder with pageCount=1:', file.name);
|
||||
const thumbnail = await generateThumbnailForFile(file);
|
||||
return { thumbnail, pageCount: 1 };
|
||||
}
|
||||
|
||||
// Skip thumbnail generation for very large files to avoid memory issues
|
||||
if (file.size >= 100 * 1024 * 1024) { // 100MB limit
|
||||
console.log('🎯 Skipping processing for large PDF file:', file.name);
|
||||
const thumbnail = generatePlaceholderThumbnail(file);
|
||||
return { thumbnail, pageCount: 1 }; // Default to 1 for large files
|
||||
}
|
||||
|
||||
// Calculate quality scale based on file size
|
||||
const scale = calculateScaleFromFileSize(file.size);
|
||||
console.log(`🎯 Using scale ${scale} for ${file.name} (${(file.size / 1024 / 1024).toFixed(1)}MB)`);
|
||||
|
||||
try {
|
||||
// Read file chunk for processing
|
||||
const chunkSize = 2 * 1024 * 1024; // 2MB
|
||||
const chunk = file.slice(0, Math.min(chunkSize, file.size));
|
||||
const arrayBuffer = await chunk.arrayBuffer();
|
||||
|
||||
const pdf = await getDocument({
|
||||
data: arrayBuffer,
|
||||
disableAutoFetch: true,
|
||||
disableStream: true,
|
||||
verbosity: 0
|
||||
}).promise;
|
||||
|
||||
const pageCount = pdf.numPages;
|
||||
console.log(`🎯 PDF ${file.name} has ${pageCount} pages`);
|
||||
|
||||
// Generate thumbnail for first page
|
||||
const page = await pdf.getPage(1);
|
||||
const viewport = page.getViewport({ scale });
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
if (!context) {
|
||||
pdf.destroy();
|
||||
throw new Error('Could not get canvas context');
|
||||
}
|
||||
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
const thumbnail = canvas.toDataURL();
|
||||
|
||||
// Clean up
|
||||
pdf.destroy();
|
||||
|
||||
console.log('🎯 Successfully generated thumbnail with metadata for', file.name, `${pageCount} pages, thumbnail size:`, thumbnail.length);
|
||||
return { thumbnail, pageCount };
|
||||
|
||||
} catch (error) {
|
||||
console.warn('🎯 Error generating PDF thumbnail with metadata for', file.name, ':', error);
|
||||
|
||||
// Try fallback with full file if chunk approach failed
|
||||
if (error instanceof Error && error.name === 'InvalidPDFException') {
|
||||
try {
|
||||
console.warn(`🎯 Trying fallback with full file for ${file.name}`);
|
||||
const fullArrayBuffer = await file.arrayBuffer();
|
||||
const pdf = await getDocument({
|
||||
data: fullArrayBuffer,
|
||||
disableAutoFetch: true,
|
||||
disableStream: true,
|
||||
verbosity: 0
|
||||
}).promise;
|
||||
|
||||
const pageCount = pdf.numPages;
|
||||
const page = await pdf.getPage(1);
|
||||
const viewport = page.getViewport({ scale });
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
if (!context) {
|
||||
pdf.destroy();
|
||||
throw new Error('Could not get canvas context');
|
||||
}
|
||||
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
const thumbnail = canvas.toDataURL();
|
||||
|
||||
pdf.destroy();
|
||||
|
||||
console.log('🎯 Fallback successful for', file.name, `${pageCount} pages`);
|
||||
return { thumbnail, pageCount };
|
||||
|
||||
} catch (fallbackError) {
|
||||
console.warn('🎯 Fallback also failed for', file.name, fallbackError);
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback: placeholder thumbnail with default page count
|
||||
console.log('🎯 Using placeholder thumbnail with default pageCount=1 for', file.name);
|
||||
const thumbnail = generatePlaceholderThumbnail(file);
|
||||
return { thumbnail, pageCount: 1 };
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user