diff --git a/frontend/src/components/fileManager/FileListItem.tsx b/frontend/src/components/fileManager/FileListItem.tsx index 784b55f00..09e508c50 100644 --- a/frontend/src/components/fileManager/FileListItem.tsx +++ b/frontend/src/components/fileManager/FileListItem.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Group, Box, Text, ActionIcon, Checkbox, Divider } from '@mantine/core'; +import { Group, Box, Text, ActionIcon, Checkbox, Divider, Badge } from '@mantine/core'; import DeleteIcon from '@mui/icons-material/Delete'; import PushPinIcon from '@mui/icons-material/PushPin'; import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined'; @@ -64,7 +64,14 @@ const FileListItem: React.FC = ({ - {file.name} + + {file.name} + {file.isDraft && ( + + DRAFT + + )} + {getFileSize(file)} • {getFileDate(file)} diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx index 505294786..9dae4d68c 100644 --- a/frontend/src/components/pageEditor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -556,10 +556,34 @@ const PageEditor = ({ // Enhanced draft save using centralized IndexedDB manager const saveDraftToIndexedDB = useCallback(async (doc: PDFDocument) => { const draftKey = `draft-${doc.id || 'merged'}`; + + // Convert PDF document to bytes for storage + const pdfBytes = await doc.save(); + const originalFileNames = activeFileIds.map(id => selectors.getFileRecord(id)?.name).filter(Boolean); + + // Create a temporary file for thumbnail generation + const tempFile = new File([pdfBytes], `Draft - ${originalFileNames.join(', ') || 'Untitled'}.pdf`, { + type: 'application/pdf', + lastModified: Date.now() + }); + + // Generate thumbnail for the draft + let thumbnail: string | undefined; + try { + const { generateThumbnailForFile } = await import('../../utils/thumbnailUtils'); + thumbnail = await generateThumbnailForFile(tempFile); + } catch (error) { + console.warn('Failed to generate thumbnail for draft:', error); + } + const draftData = { - document: doc, + id: draftKey, + name: `Draft - ${originalFileNames.join(', ') || 'Untitled'}`, + pdfData: pdfBytes, + size: pdfBytes.length, timestamp: Date.now(), - originalFiles: activeFileIds.map(id => selectors.getFileRecord(id)?.name).filter(Boolean) + thumbnail, + originalFiles: originalFileNames }; try { diff --git a/frontend/src/components/shared/LandingPage.tsx b/frontend/src/components/shared/LandingPage.tsx index 4af3d1202..6c1668a43 100644 --- a/frontend/src/components/shared/LandingPage.tsx +++ b/frontend/src/components/shared/LandingPage.tsx @@ -33,7 +33,7 @@ const LandingPage = () => { {/* White PDF Page Background */} { ref={fileInputRef} type="file" multiple - accept="*/*" + accept=".pdf,.zip" onChange={handleFileSelect} style={{ display: 'none' }} /> diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx index 421c78e33..7bca656a0 100644 --- a/frontend/src/contexts/FileContext.tsx +++ b/frontend/src/contexts/FileContext.tsx @@ -208,48 +208,12 @@ function FileContextInner({ dispatch }), [actions]); - // Load files from persistence on mount - useEffect(() => { - if (!enablePersistence || !indexedDB) return; - - const loadFromPersistence = async () => { - try { - // Load metadata to populate file list (actual File objects loaded on-demand) - const metadata = await indexedDB.loadAllMetadata(); - if (metadata.length === 0) { - if (DEBUG) console.log('📄 No files found in persistence'); - return; - } - - if (DEBUG) { - console.log(`📄 Loading ${metadata.length} files from persistence`); - } - - // Create FileRecords from metadata - File objects loaded when needed - const fileRecords = metadata.map(meta => ({ - id: meta.id, - name: meta.name, - size: meta.size, - type: meta.type, - lastModified: meta.lastModified, - thumbnailUrl: meta.thumbnail, - isPinned: false, - createdAt: Date.now() - })); - - // Add to state so file manager can show them - dispatch({ - type: 'ADD_FILES', - payload: { fileRecords } - }); - - } catch (error) { - console.error('Failed to load files from persistence:', error); - } - }; - - loadFromPersistence(); - }, [enablePersistence, indexedDB]); // Only run when these change + // Persistence loading disabled - files only loaded on explicit user action + // useEffect(() => { + // if (!enablePersistence || !indexedDB) return; + // const loadFromPersistence = async () => { /* loading logic removed */ }; + // loadFromPersistence(); + // }, [enablePersistence, indexedDB]); // Cleanup on unmount useEffect(() => { diff --git a/frontend/src/contexts/IndexedDBContext.tsx b/frontend/src/contexts/IndexedDBContext.tsx index 55885ca86..ab3ed09f5 100644 --- a/frontend/src/contexts/IndexedDBContext.tsx +++ b/frontend/src/contexts/IndexedDBContext.tsx @@ -25,6 +25,9 @@ interface IndexedDBContextValue { // Utilities getStorageStats: () => Promise<{ used: number; available: number; fileCount: number }>; + + // Draft operations + loadAllDraftMetadata: () => Promise; } const IndexedDBContext = createContext(null); @@ -56,6 +59,22 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) { }, []); const saveFile = useCallback(async (file: File, fileId: FileId, existingThumbnail?: string): Promise => { + // DEBUG: Check original file before saving + if (DEBUG && file.type === 'application/pdf') { + try { + const { getDocument } = await import('pdfjs-dist'); + const arrayBuffer = await file.arrayBuffer(); + const pdf = await getDocument({ data: arrayBuffer }).promise; + console.log(`🔍 Saving file to IndexedDB:`, { + name: file.name, + size: file.size, + pages: pdf.numPages + }); + } catch (error) { + console.error(`🔍 Error validating file before save:`, error); + } + } + // Use existing thumbnail or generate new one const thumbnail = existingThumbnail || await generateThumbnailForFile(file); @@ -99,6 +118,27 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) { lastModified: storedFile.lastModified }); + // DEBUG: Check if file reconstruction is working + if (DEBUG && file.type === 'application/pdf') { + console.log(`🔍 File loaded from IndexedDB:`, { + name: file.name, + originalSize: storedFile.size, + reconstructedSize: file.size, + dataLength: storedFile.data.byteLength, + sizesMatch: storedFile.size === file.size + }); + + // Quick PDF validation + try { + const { getDocument } = await import('pdfjs-dist'); + const arrayBuffer = await file.arrayBuffer(); + const pdf = await getDocument({ data: arrayBuffer }).promise; + console.log(`🔍 PDF validation: ${pdf.numPages} pages in reconstructed file`); + } catch (error) { + console.error(`🔍 PDF reconstruction error:`, error); + } + } + // Cache for future use with LRU eviction fileCache.current.set(fileId, { file, lastAccessed: Date.now() }); evictLRUEntries(); @@ -177,6 +217,38 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) { return await fileStorage.getStorageStats(); }, []); + const loadAllDraftMetadata = useCallback(async (): Promise => { + try { + const { indexedDBManager, DATABASE_CONFIGS } = await import('../services/indexedDBManager'); + const db = await indexedDBManager.openDatabase(DATABASE_CONFIGS.DRAFTS); + + return new Promise((resolve, reject) => { + const transaction = db.transaction(['drafts'], 'readonly'); + const store = transaction.objectStore('drafts'); + const request = store.getAll(); + + request.onsuccess = () => { + const drafts = request.result || []; + const draftMetadata: FileMetadata[] = drafts.map((draft: any) => ({ + id: draft.id, + name: draft.name || `Draft ${draft.id}`, + type: 'application/pdf', + size: draft.size || 0, + lastModified: draft.timestamp || Date.now(), + thumbnail: draft.thumbnail, + isDraft: true + })); + resolve(draftMetadata); + }; + + request.onerror = () => reject(request.error); + }); + } catch (error) { + console.warn('Failed to load draft metadata:', error); + return []; + } + }, []); + // No periodic cleanup needed - LRU eviction happens on-demand when cache fills const value: IndexedDBContextValue = { @@ -187,7 +259,8 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) { loadAllMetadata, deleteMultiple, clearAll, - getStorageStats + getStorageStats, + loadAllDraftMetadata }; return ( diff --git a/frontend/src/hooks/useFileManager.ts b/frontend/src/hooks/useFileManager.ts index 541b4ead5..b16fa6f4f 100644 --- a/frontend/src/hooks/useFileManager.ts +++ b/frontend/src/hooks/useFileManager.ts @@ -12,7 +12,39 @@ export const useFileManager = () => { throw new Error('IndexedDB context not available'); } - // Try ID first (preferred) + // Handle drafts differently from regular files + if (fileMetadata.isDraft) { + // Load draft from the drafts database + try { + const { indexedDBManager, DATABASE_CONFIGS } = await import('../services/indexedDBManager'); + const db = await indexedDBManager.openDatabase(DATABASE_CONFIGS.DRAFTS); + + return new Promise((resolve, reject) => { + const transaction = db.transaction(['drafts'], 'readonly'); + const store = transaction.objectStore('drafts'); + const request = store.get(fileMetadata.id); + + request.onsuccess = () => { + const draft = request.result; + if (draft && draft.pdfData) { + const file = new File([draft.pdfData], fileMetadata.name, { + type: 'application/pdf', + lastModified: fileMetadata.lastModified + }); + resolve(file); + } else { + reject(new Error('Draft data not found')); + } + }; + + request.onerror = () => reject(request.error); + }); + } catch (error) { + throw new Error(`Failed to load draft: ${fileMetadata.name} (${error})`); + } + } + + // Regular file loading if (fileMetadata.id) { const file = await indexedDB.loadFile(fileMetadata.id); if (file) { @@ -29,11 +61,16 @@ export const useFileManager = () => { return []; } - // Get metadata only (no file data) for performance - const storedFileMetadata = await indexedDB.loadAllMetadata(); - const sortedFiles = storedFileMetadata.sort((a, b) => (b.lastModified || 0) - (a.lastModified || 0)); + // Load both regular files and drafts + const [storedFileMetadata, draftMetadata] = await Promise.all([ + indexedDB.loadAllMetadata(), + indexedDB.loadAllDraftMetadata() + ]); + + // Combine and sort by last modified + const allFiles = [...storedFileMetadata, ...draftMetadata]; + const sortedFiles = allFiles.sort((a, b) => (b.lastModified || 0) - (a.lastModified || 0)); - // Already in correct FileMetadata format return sortedFiles; } catch (error) { console.error('Failed to load recent files:', error); diff --git a/frontend/src/services/fileStorage.ts b/frontend/src/services/fileStorage.ts index 9ce9c1647..cdbe183a5 100644 --- a/frontend/src/services/fileStorage.ts +++ b/frontend/src/services/fileStorage.ts @@ -156,7 +156,7 @@ class FileStorageService { } cursor.continue(); } else { - console.log('Loaded metadata for', files.length, 'files without loading data'); + // Metadata loaded efficiently without file data resolve(files); } }; diff --git a/frontend/src/types/file.ts b/frontend/src/types/file.ts index 462490fdf..0f8a5ec19 100644 --- a/frontend/src/types/file.ts +++ b/frontend/src/types/file.ts @@ -25,6 +25,7 @@ export interface FileMetadata { size: number; lastModified: number; thumbnail?: string; + isDraft?: boolean; // Marks files as draft versions /** @deprecated Legacy compatibility - will be removed */ storedInIndexedDB?: boolean; }