diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx index f2f304b1e..921c88333 100644 --- a/frontend/src/contexts/FileContext.tsx +++ b/frontend/src/contexts/FileContext.tsx @@ -22,13 +22,12 @@ import { FileId, StirlingFileStub, StirlingFile, - createStirlingFile } from '../types/fileContext'; // Import modular components import { fileContextReducer, initialFileContextState } from './file/FileReducer'; import { createFileSelectors } from './file/fileSelectors'; -import { AddedFile, addFiles, addStirlingFileStubs, consumeFiles, undoConsumeFiles, createFileActions } from './file/fileActions'; +import { addFiles, addStirlingFileStubs, consumeFiles, undoConsumeFiles, createFileActions } from './file/fileActions'; import { FileLifecycleManager } from './file/lifecycle'; import { FileStateContext, FileActionsContext } from './file/contexts'; import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext'; @@ -73,56 +72,23 @@ function FileContextInner({ dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } }); }, []); - const selectFiles = (addedFilesWithIds: AddedFile[]) => { + const selectFiles = (stirlingFiles: StirlingFile[]) => { const currentSelection = stateRef.current.ui.selectedFileIds; - const newFileIds = addedFilesWithIds.map(({ id }) => id); + const newFileIds = stirlingFiles.map(stirlingFile => stirlingFile.fileId); dispatch({ type: 'SET_SELECTED_FILES', payload: { fileIds: [...currentSelection, ...newFileIds] } }); } // File operations using unified addFiles helper with persistence const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise => { - const addedFilesWithIds = await addFiles({ files, ...options }, stateRef, filesRef, dispatch, lifecycleManager); + const stirlingFiles = await addFiles({ files, ...options }, stateRef, filesRef, dispatch, lifecycleManager, enablePersistence); // Auto-select the newly added files if requested - if (options?.selectFiles && addedFilesWithIds.length > 0) { - selectFiles(addedFilesWithIds); + if (options?.selectFiles && stirlingFiles.length > 0) { + selectFiles(stirlingFiles); } - // Persist to IndexedDB if enabled and update StirlingFileStub with version info - if (indexedDB && enablePersistence && addedFilesWithIds.length > 0) { - await Promise.all(addedFilesWithIds.map(async ({ file, id, thumbnail }) => { - try { - const metadata = await indexedDB.saveFile(file, id, thumbnail); - - // Update StirlingFileStub with version information from IndexedDB - if (metadata.versionNumber || metadata.originalFileId) { - dispatch({ - type: 'UPDATE_FILE_RECORD', - payload: { - id, - updates: { - versionNumber: metadata.versionNumber, - originalFileId: metadata.originalFileId, - parentFileId: metadata.parentFileId, - toolHistory: metadata.toolHistory - } - } - }); - - if (DEBUG) console.log(`📄 FileContext: Updated raw file ${file.name} with IndexedDB history data:`, { - versionNumber: metadata.versionNumber, - originalFileId: metadata.originalFileId, - toolChainLength: metadata.toolHistory?.length || 0 - }); - } - } catch (error) { - console.error('Failed to persist file to IndexedDB:', file.name, error); - } - })); - } - - return addedFilesWithIds.map(({ file, id }) => createStirlingFile(file, id)); - }, [indexedDB, enablePersistence]); + return stirlingFiles; + }, [enablePersistence]); const addStirlingFileStubsAction = useCallback(async (stirlingFileStubs: StirlingFileStub[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise => { // StirlingFileStubs preserve all metadata - perfect for FileManager use case! @@ -130,12 +96,7 @@ function FileContextInner({ // Auto-select the newly added files if requested if (options?.selectFiles && result.length > 0) { - // Convert StirlingFile[] to AddedFile[] format for selectFiles - const addedFilesWithIds = result.map(stirlingFile => ({ - file: stirlingFile, - id: stirlingFile.fileId - })); - selectFiles(addedFilesWithIds); + selectFiles(result); } return result; diff --git a/frontend/src/contexts/IndexedDBContext.tsx b/frontend/src/contexts/IndexedDBContext.tsx index e3659ac97..b916b247c 100644 --- a/frontend/src/contexts/IndexedDBContext.tsx +++ b/frontend/src/contexts/IndexedDBContext.tsx @@ -64,7 +64,24 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) { // Store in IndexedDB (no history data - that's handled by direct fileStorage calls now) const stirlingFile = createStirlingFile(file, fileId); - await fileStorage.storeStirlingFile(stirlingFile, thumbnail, true); + + // Create minimal stub for storage + const stub: StirlingFileStub = { + id: fileId, + name: file.name, + size: file.size, + type: file.type, + lastModified: file.lastModified, + quickKey: `${file.name}|${file.size}|${file.lastModified}`, + thumbnailUrl: thumbnail, + isLeaf: true, + createdAt: Date.now(), + versionNumber: 1, + originalFileId: fileId, + toolHistory: [] + }; + + await fileStorage.storeStirlingFile(stirlingFile, stub); const storedFile = await fileStorage.getStirlingFileStub(fileId); // Cache the file object for immediate reuse diff --git a/frontend/src/contexts/file/fileActions.ts b/frontend/src/contexts/file/fileActions.ts index cde32ee58..8ca04444e 100644 --- a/frontend/src/contexts/file/fileActions.ts +++ b/frontend/src/contexts/file/fileActions.ts @@ -8,7 +8,8 @@ import { FileContextState, toStirlingFileStub, createFileId, - createQuickKey + createQuickKey, + createStirlingFile, } from '../../types/fileContext'; import { FileId } from '../../types/file'; import { generateThumbnailWithMetadata } from '../../utils/thumbnailUtils'; @@ -16,7 +17,6 @@ import { FileLifecycleManager } from './lifecycle'; import { buildQuickKeySet } from './fileSelectors'; import { StirlingFile } from '../../types/fileContext'; import { fileStorage } from '../../services/fileStorage'; - const DEBUG = process.env.NODE_ENV === 'development'; /** @@ -120,13 +120,7 @@ export function createChildStub( }; } -/** - * File addition types - */ -type AddFileKind = 'raw' | 'processed'; - interface AddFileOptions { - // For 'raw' files files?: File[]; // For 'processed' files @@ -139,12 +133,6 @@ interface AddFileOptions { selectFiles?: boolean; } -export interface AddedFile { - file: File; - id: FileId; - thumbnail?: string; -} - /** * Unified file addition helper - replaces addFiles */ @@ -153,95 +141,114 @@ export async function addFiles( stateRef: React.MutableRefObject, filesRef: React.MutableRefObject>, dispatch: React.Dispatch, - lifecycleManager: FileLifecycleManager -): Promise { + lifecycleManager: FileLifecycleManager, + enablePersistence: boolean = false +): Promise { // Acquire mutex to prevent race conditions await addFilesMutex.lock(); try { const stirlingFileStubs: StirlingFileStub[] = []; - const addedFiles: AddedFile[] = []; + const stirlingFiles: StirlingFile[] = []; // Build quickKey lookup from existing files for deduplication const existingQuickKeys = buildQuickKeySet(stateRef.current.files.byId); - const { files = [] } = options; - if (DEBUG) console.log(`📄 addFiles(raw): Adding ${files.length} files with immediate thumbnail generation`); + const { files = [] } = options; + if (DEBUG) console.log(`📄 addFiles(raw): Adding ${files.length} files with immediate thumbnail generation`); - for (const file of files) { - const quickKey = createQuickKey(file); + for (const file of files) { + const quickKey = createQuickKey(file); - // Soft deduplication: Check if file already exists by metadata - if (existingQuickKeys.has(quickKey)) { - if (DEBUG) console.log(`📄 Skipping duplicate file: ${file.name} (quickKey: ${quickKey})`); - continue; - } - if (DEBUG) console.log(`📄 Adding new file: ${file.name} (quickKey: ${quickKey})`); + // Soft deduplication: Check if file already exists by metadata + if (existingQuickKeys.has(quickKey)) { + if (DEBUG) console.log(`📄 Skipping duplicate file: ${file.name} (quickKey: ${quickKey})`); + continue; + } + if (DEBUG) console.log(`📄 Adding new file: ${file.name} (quickKey: ${quickKey})`); - const fileId = createFileId(); - filesRef.current.set(fileId, file); + const fileId = createFileId(); + filesRef.current.set(fileId, file); - // Generate thumbnail and page count immediately - let thumbnail: string | undefined; - let pageCount: number = 1; + // Generate thumbnail and page count immediately + let thumbnail: string | undefined; + let pageCount: number = 1; - // Route based on file type - PDFs through full metadata pipeline, non-PDFs through simple path - if (file.type.startsWith('application/pdf')) { - try { - if (DEBUG) console.log(`📄 Generating PDF metadata for ${file.name}`); - const result = await generateThumbnailWithMetadata(file); - thumbnail = result.thumbnail; - pageCount = result.pageCount; - if (DEBUG) console.log(`📄 Generated PDF metadata for ${file.name}: ${pageCount} pages, thumbnail: SUCCESS`); - } catch (error) { - if (DEBUG) console.warn(`📄 Failed to generate PDF metadata for ${file.name}:`, error); - } - } else { - // Non-PDF files: simple thumbnail generation, no page count - try { - if (DEBUG) console.log(`📄 Generating simple thumbnail for non-PDF file ${file.name}`); - const { generateThumbnailForFile } = await import('../../utils/thumbnailUtils'); - thumbnail = await generateThumbnailForFile(file); - pageCount = 0; // Non-PDFs have no page count - if (DEBUG) console.log(`📄 Generated simple thumbnail for ${file.name}: no page count, thumbnail: SUCCESS`); - } catch (error) { - if (DEBUG) console.warn(`📄 Failed to generate simple thumbnail for ${file.name}:`, error); - } - } + // Route based on file type - PDFs through full metadata pipeline, non-PDFs through simple path + if (file.type.startsWith('application/pdf')) { + try { + if (DEBUG) console.log(`📄 Generating PDF metadata for ${file.name}`); + const result = await generateThumbnailWithMetadata(file); + thumbnail = result.thumbnail; + pageCount = result.pageCount; + if (DEBUG) console.log(`📄 Generated PDF metadata for ${file.name}: ${pageCount} pages, thumbnail: SUCCESS`); + } catch (error) { + if (DEBUG) console.warn(`📄 Failed to generate PDF metadata for ${file.name}:`, error); + } + } else { + // Non-PDF files: simple thumbnail generation, no page count + try { + if (DEBUG) console.log(`📄 Generating simple thumbnail for non-PDF file ${file.name}`); + const { generateThumbnailForFile } = await import('../../utils/thumbnailUtils'); + thumbnail = await generateThumbnailForFile(file); + pageCount = 0; // Non-PDFs have no page count + if (DEBUG) console.log(`📄 Generated simple thumbnail for ${file.name}: no page count, thumbnail: SUCCESS`); + } catch (error) { + if (DEBUG) console.warn(`📄 Failed to generate simple thumbnail for ${file.name}:`, error); + } + } - // Create record with immediate thumbnail and page metadata - const record = toStirlingFileStub(file, fileId, thumbnail); - if (thumbnail) { - // Track blob URLs for cleanup (images return blob URLs that need revocation) - if (thumbnail.startsWith('blob:')) { - lifecycleManager.trackBlobUrl(thumbnail); - } - } + // Create record with immediate thumbnail and page metadata + const record = toStirlingFileStub(file, fileId, thumbnail); + if (thumbnail) { + // Track blob URLs for cleanup (images return blob URLs that need revocation) + if (thumbnail.startsWith('blob:')) { + lifecycleManager.trackBlobUrl(thumbnail); + } + } - // Store insertion position if provided - if (options.insertAfterPageId !== undefined) { - record.insertAfterPageId = options.insertAfterPageId; - } + // Store insertion position if provided + if (options.insertAfterPageId !== undefined) { + record.insertAfterPageId = options.insertAfterPageId; + } - // Create initial processedFile metadata with page count - if (pageCount > 0) { - record.processedFile = createProcessedFile(pageCount, thumbnail); - if (DEBUG) console.log(`📄 addFiles(raw): Created initial processedFile metadata for ${file.name} with ${pageCount} pages`); - } + // Create initial processedFile metadata with page count + if (pageCount > 0) { + record.processedFile = createProcessedFile(pageCount, thumbnail); + if (DEBUG) console.log(`📄 addFiles(raw): Created initial processedFile metadata for ${file.name} with ${pageCount} pages`); + } - // History metadata is now managed in IndexedDB, not in PDF metadata + existingQuickKeys.add(quickKey); + stirlingFileStubs.push(record); - existingQuickKeys.add(quickKey); - stirlingFileStubs.push(record); - addedFiles.push({ file, id: fileId, thumbnail }); - } + // Create StirlingFile directly + const stirlingFile = createStirlingFile(file, fileId); + stirlingFiles.push(stirlingFile); + } + + // Persist to storage if enabled using fileStorage service + if (enablePersistence && stirlingFiles.length > 0) { + await Promise.all(stirlingFiles.map(async (stirlingFile, index) => { + try { + // Get corresponding stub with all metadata + const fileStub = stirlingFileStubs[index]; + + // Store using the cleaner signature - pass StirlingFile + StirlingFileStub directly + await fileStorage.storeStirlingFile(stirlingFile, fileStub); + + if (DEBUG) console.log(`📄 addFiles: Stored file ${stirlingFile.name} with metadata:`, fileStub); + } catch (error) { + console.error('Failed to persist file to storage:', stirlingFile.name, error); + } + })); + } // Dispatch ADD_FILES action if we have new files if (stirlingFileStubs.length > 0) { dispatch({ type: 'ADD_FILES', payload: { stirlingFileStubs } }); } - return addedFiles; + return stirlingFiles; } finally { // Always release mutex even if error occurs addFilesMutex.unlock(); @@ -300,17 +307,7 @@ export async function consumeFiles( try { // Use fileStorage directly with complete metadata from stub - await fileStorage.storeStirlingFile( - stirlingFile, - stub.thumbnailUrl, - true, // isLeaf - new files are leaf nodes - { - versionNumber: stub.versionNumber || 1, - originalFileId: stub.originalFileId || stub.id, - parentFileId: stub.parentFileId, - toolHistory: stub.toolHistory || [] - } - ); + await fileStorage.storeStirlingFile(stirlingFile, stub); if (DEBUG) console.log(`📄 Saved StirlingFile ${stirlingFile.name} directly to storage with complete metadata:`, { fileId: stirlingFile.fileId, diff --git a/frontend/src/services/fileStorage.ts b/frontend/src/services/fileStorage.ts index 3b6e0e319..b4b128164 100644 --- a/frontend/src/services/fileStorage.ts +++ b/frontend/src/services/fileStorage.ts @@ -39,22 +39,9 @@ class FileStorageService { } /** - * Store a StirlingFile with its metadata + * Store a StirlingFile with its metadata from StirlingFileStub */ - async storeStirlingFile( - stirlingFile: StirlingFile, - thumbnail?: string, - isLeaf: boolean = true, - historyData?: { - versionNumber: number; - originalFileId: string; - parentFileId: FileId | undefined; - toolHistory: Array<{ - toolName: string; - timestamp: number; - }>; - } - ): Promise { + async storeStirlingFile(stirlingFile: StirlingFile, stub: StirlingFileStub): Promise { const db = await this.getDatabase(); const arrayBuffer = await stirlingFile.arrayBuffer(); @@ -67,14 +54,14 @@ class FileStorageService { size: stirlingFile.size, lastModified: stirlingFile.lastModified, data: arrayBuffer, - thumbnail, - isLeaf, + thumbnail: stub.thumbnailUrl, + isLeaf: stub.isLeaf ?? true, - // History data - use provided data or defaults for original files - versionNumber: historyData?.versionNumber ?? 1, - originalFileId: historyData?.originalFileId ?? stirlingFile.fileId, - parentFileId: historyData?.parentFileId ?? undefined, - toolHistory: historyData?.toolHistory ?? [] + // History data from stub + versionNumber: stub.versionNumber ?? 1, + originalFileId: stub.originalFileId ?? stirlingFile.fileId, + parentFileId: stub.parentFileId ?? undefined, + toolHistory: stub.toolHistory ?? [] }; return new Promise((resolve, reject) => {