diff --git a/frontend/src/components/fileManager/FileListArea.tsx b/frontend/src/components/fileManager/FileListArea.tsx index 3353064d3..ab447a50c 100644 --- a/frontend/src/components/fileManager/FileListArea.tsx +++ b/frontend/src/components/fileManager/FileListArea.tsx @@ -53,12 +53,10 @@ const FileListArea: React.FC = ({ ) : ( filteredFiles.map((file, index) => { - // Check if this file is a leaf (appears in group keys) or a history file - const isLeafFile = fileGroups.has(file.id); - const lineagePath = fileGroups.get(file.id) || []; - const isHistoryFile = !isLeafFile; // If not a leaf, it's a history file - const isLatestVersion = isLeafFile; // Leaf files are the latest in their branch - const hasVersionHistory = lineagePath.length > 1; + // Determine if this is a history file based on whether it's in the recent files or loaded as history + const isLeafFile = recentFiles.some(rf => rf.id === file.id); + const isHistoryFile = !isLeafFile; // If not in recent files, it's a loaded history file + const isLatestVersion = isLeafFile; // Only leaf files (from recent files) are latest versions return ( = ({ onDownload={() => onDownloadSingle(file)} onDoubleClick={() => onFileDoubleClick(file)} isHistoryFile={isHistoryFile} - isLatestVersion={isLatestVersion && hasVersionHistory} + isLatestVersion={isLatestVersion} /> ); }) diff --git a/frontend/src/components/fileManager/FileListItem.tsx b/frontend/src/components/fileManager/FileListItem.tsx index 0a40be0e6..f2ea9a425 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, Menu, Badge, Button } from '@mantine/core'; +import { Group, Box, Text, ActionIcon, Checkbox, Divider, Menu, Badge, Button, Loader } from '@mantine/core'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import DeleteIcon from '@mui/icons-material/Delete'; import DownloadIcon from '@mui/icons-material/Download'; @@ -38,7 +38,7 @@ const FileListItem: React.FC = ({ const [isHovered, setIsHovered] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); const { t } = useTranslation(); - const { fileGroups, expandedFileIds, onToggleExpansion, onAddToRecents } = useFileManagerContext(); + const { fileGroups, expandedFileIds, onToggleExpansion, onAddToRecents, isLoadingHistory, getHistoryError } = useFileManagerContext(); // Keep item in hovered state if menu is open const shouldShowHovered = isHovered || isMenuOpen; @@ -46,9 +46,13 @@ const FileListItem: React.FC = ({ // Get version information for this file const leafFileId = isLatestVersion ? file.id : (file.originalFileId || file.id); const lineagePath = fileGroups.get(leafFileId) || []; - const hasVersionHistory = lineagePath.length > 1; + const hasVersionHistory = (file.versionNumber || 0) > 0; // Show history for any processed file (v1+) const currentVersion = file.versionNumber || 0; // Display original files as v0 const isExpanded = expandedFileIds.has(leafFileId); + + // Get loading state for this file's history + const isLoadingFileHistory = isLoadingHistory(file.id); + const historyError = getHistoryError(file.id); return ( <> @@ -91,6 +95,7 @@ const FileListItem: React.FC = ({ {file.name} + {isLoadingFileHistory && } 0 ? "blue" : "gray"}> v{currentVersion} @@ -100,7 +105,7 @@ const FileListItem: React.FC = ({ {getFileSize(file)} • {getFileDate(file)} {hasVersionHistory && ( - • {lineagePath.length} versions + • has history )} @@ -157,17 +162,30 @@ const FileListItem: React.FC = ({ {isLatestVersion && hasVersionHistory && ( <> } + leftSection={ + isLoadingFileHistory ? + : + + } onClick={(e) => { e.stopPropagation(); onToggleExpansion(leafFileId); }} + disabled={isLoadingFileHistory} > - {isExpanded ? - t('fileManager.hideHistory', 'Hide History') : - t('fileManager.showHistory', 'Show History') + {isLoadingFileHistory ? + t('fileManager.loadingHistory', 'Loading History...') : + (isExpanded ? + t('fileManager.hideHistory', 'Hide History') : + t('fileManager.showHistory', 'Show History') + ) } + {historyError && ( + + {t('fileManager.historyError', 'Error loading history')} + + )} )} diff --git a/frontend/src/contexts/FileManagerContext.tsx b/frontend/src/contexts/FileManagerContext.tsx index 8bed49bba..860b7e3d5 100644 --- a/frontend/src/contexts/FileManagerContext.tsx +++ b/frontend/src/contexts/FileManagerContext.tsx @@ -3,7 +3,8 @@ import { FileMetadata } from '../types/file'; import { StoredFile, fileStorage } from '../services/fileStorage'; import { downloadFiles } from '../utils/downloadUtils'; import { FileId } from '../types/file'; -import { getLatestVersions, groupFilesByOriginal, getVersionHistory } from '../utils/fileHistoryUtils'; +import { getLatestVersions, groupFilesByOriginal, getVersionHistory, createFileMetadataWithHistory } from '../utils/fileHistoryUtils'; +import { useMultiFileHistory } from '../hooks/useFileHistory'; // Type for the context value - now contains everything directly interface FileManagerContextValue { @@ -18,6 +19,10 @@ interface FileManagerContextValue { expandedFileIds: Set; fileGroups: Map; + // History loading state + isLoadingHistory: (fileId: FileId) => boolean; + getHistoryError: (fileId: FileId) => string | null; + // Handlers onSourceChange: (source: 'recent' | 'local' | 'drive') => void; onLocalFileClick: () => void; @@ -75,11 +80,20 @@ export const FileManagerProvider: React.FC = ({ const [searchTerm, setSearchTerm] = useState(''); const [lastClickedIndex, setLastClickedIndex] = useState(null); const [expandedFileIds, setExpandedFileIds] = useState>(new Set()); + const [loadedHistoryFiles, setLoadedHistoryFiles] = useState>(new Map()); // Cache for loaded history const fileInputRef = useRef(null); // Track blob URLs for cleanup const createdBlobUrls = useRef>(new Set()); + // History loading hook + const { + loadFileHistory, + getHistory, + isLoadingHistory, + getError: getHistoryError + } = useMultiFileHistory(); + // Computed values (with null safety) const selectedFilesSet = new Set(selectedFileIds); @@ -101,36 +115,24 @@ export const FileManagerProvider: React.FC = ({ const displayFiles = useMemo(() => { if (!recentFiles || recentFiles.length === 0) return []; - const recordsForGrouping = recentFiles.map(file => ({ - ...file, - originalFileId: file.originalFileId, - versionNumber: file.versionNumber || 0 - })); - - // Get branch groups (leaf files with their lineage paths) - const branchGroups = groupFilesByOriginal(recordsForGrouping); - - // Show only leaf files (end of branches) in main list const expandedFiles = []; - for (const [leafFileId, lineagePath] of branchGroups) { - const leafFile = recentFiles.find(f => f.id === leafFileId); - if (!leafFile) continue; - - // Add the leaf file (shown in main list) + + // Since we now only load leaf files, iterate through recent files directly + for (const leafFile of recentFiles) { + // Add the leaf file (main file shown in list) expandedFiles.push(leafFile); - // If expanded, add the lineage history (except the leaf itself) - if (expandedFileIds.has(leafFileId)) { - const historyFiles = lineagePath - .filter((record: any) => record.id !== leafFileId) - .map((record: any) => recentFiles.find(f => f.id === record.id)) - .filter((f): f is FileMetadata => f !== undefined); - expandedFiles.push(...historyFiles); + // If expanded, add the loaded history files + if (expandedFileIds.has(leafFile.id)) { + const historyFiles = loadedHistoryFiles.get(leafFile.id) || []; + // Sort history files by version number (oldest first) + const sortedHistory = historyFiles.sort((a, b) => (a.versionNumber || 0) - (b.versionNumber || 0)); + expandedFiles.push(...sortedHistory); } } return expandedFiles; - }, [recentFiles, expandedFileIds, fileGroups]); + }, [recentFiles, expandedFileIds, loadedHistoryFiles]); const selectedFiles = selectedFileIds.length === 0 ? [] : displayFiles.filter(file => selectedFilesSet.has(file.id)); @@ -331,7 +333,10 @@ export const FileManagerProvider: React.FC = ({ } }, []); - const handleToggleExpansion = useCallback((fileId: string) => { + const handleToggleExpansion = useCallback(async (fileId: string) => { + const isCurrentlyExpanded = expandedFileIds.has(fileId); + + // Update expansion state setExpandedFileIds(prev => { const newSet = new Set(prev); if (newSet.has(fileId)) { @@ -341,7 +346,124 @@ export const FileManagerProvider: React.FC = ({ } return newSet; }); - }, []); + + // Load complete history chain if expanding + if (!isCurrentlyExpanded) { + const currentFileMetadata = recentFiles.find(f => f.id === fileId); + if (currentFileMetadata && (currentFileMetadata.versionNumber || 0) > 0) { + try { + // Load the current file to get its full history + const storedFile = await fileStorage.getFile(fileId); + if (storedFile) { + const file = new File([storedFile.data], storedFile.name, { + type: storedFile.type, + lastModified: storedFile.lastModified + }); + + // Get the complete history metadata (this will give us original/parent IDs) + const historyData = await loadFileHistory(file, fileId); + + if (historyData?.originalFileId) { + // Load complete history chain by traversing parent relationships + const historyFiles: FileMetadata[] = []; + + // Get all stored files for chain traversal + const allStoredMetadata = await fileStorage.getAllFileMetadata(); + const fileMap = new Map(allStoredMetadata.map(f => [f.id, f])); + + // Build complete chain by following parent relationships backwards + const visitedIds = new Set([fileId]); // Don't include the current file + const toProcess = [historyData]; // Start with current file's history data + + while (toProcess.length > 0) { + const currentHistoryData = toProcess.shift()!; + + // Add original file if we haven't seen it + if (currentHistoryData.originalFileId && !visitedIds.has(currentHistoryData.originalFileId)) { + visitedIds.add(currentHistoryData.originalFileId); + const originalMeta = fileMap.get(currentHistoryData.originalFileId); + if (originalMeta) { + try { + const origStoredFile = await fileStorage.getFile(originalMeta.id); + if (origStoredFile) { + const origFile = new File([origStoredFile.data], origStoredFile.name, { + type: origStoredFile.type, + lastModified: origStoredFile.lastModified + }); + const origMetadata = await createFileMetadataWithHistory(origFile, originalMeta.id, originalMeta.thumbnail); + historyFiles.push(origMetadata); + } + } catch (error) { + console.warn(`Failed to load original file ${originalMeta.id}:`, error); + } + } + } + + // Add parent file if we haven't seen it + if (currentHistoryData.parentFileId && !visitedIds.has(currentHistoryData.parentFileId)) { + visitedIds.add(currentHistoryData.parentFileId); + const parentMeta = fileMap.get(currentHistoryData.parentFileId); + if (parentMeta) { + try { + const parentStoredFile = await fileStorage.getFile(parentMeta.id); + if (parentStoredFile) { + const parentFile = new File([parentStoredFile.data], parentStoredFile.name, { + type: parentStoredFile.type, + lastModified: parentStoredFile.lastModified + }); + const parentMetadata = await createFileMetadataWithHistory(parentFile, parentMeta.id, parentMeta.thumbnail); + historyFiles.push(parentMetadata); + + // Load parent's history to continue the chain + const parentHistoryData = await loadFileHistory(parentFile, parentMeta.id); + if (parentHistoryData) { + toProcess.push(parentHistoryData); + } + } + } catch (error) { + console.warn(`Failed to load parent file ${parentMeta.id}:`, error); + } + } + } + } + + // Also find any files that have the current file as their original (siblings/alternatives) + for (const [metaId, meta] of fileMap) { + if (!visitedIds.has(metaId) && meta.originalFileId === historyData.originalFileId) { + visitedIds.add(metaId); + try { + const siblingStoredFile = await fileStorage.getFile(meta.id); + if (siblingStoredFile) { + const siblingFile = new File([siblingStoredFile.data], siblingStoredFile.name, { + type: siblingStoredFile.type, + lastModified: siblingStoredFile.lastModified + }); + const siblingMetadata = await createFileMetadataWithHistory(siblingFile, meta.id, meta.thumbnail); + historyFiles.push(siblingMetadata); + } + } catch (error) { + console.warn(`Failed to load sibling file ${meta.id}:`, error); + } + } + } + + // Cache the loaded history files + setLoadedHistoryFiles(prev => new Map(prev.set(fileId, historyFiles))); + } + } + } catch (error) { + console.warn(`Failed to load history chain for file ${fileId}:`, error); + } + } + } else { + // Clear loaded history when collapsing + setLoadedHistoryFiles(prev => { + const newMap = new Map(prev); + newMap.delete(fileId); + return newMap; + }); + } + }, [expandedFileIds, recentFiles, loadFileHistory]); const handleAddToRecents = useCallback(async (file: FileMetadata) => { try { @@ -399,6 +521,10 @@ export const FileManagerProvider: React.FC = ({ expandedFileIds, fileGroups, + // History loading state + isLoadingHistory, + getHistoryError, + // Handlers onSourceChange: handleSourceChange, onLocalFileClick: handleLocalFileClick, @@ -429,6 +555,8 @@ export const FileManagerProvider: React.FC = ({ fileInputRef, expandedFileIds, fileGroups, + isLoadingHistory, + getHistoryError, handleSourceChange, handleLocalFileClick, handleFileSelect, diff --git a/frontend/src/contexts/IndexedDBContext.tsx b/frontend/src/contexts/IndexedDBContext.tsx index 14f3f7939..0934bb91e 100644 --- a/frontend/src/contexts/IndexedDBContext.tsx +++ b/frontend/src/contexts/IndexedDBContext.tsx @@ -21,6 +21,7 @@ interface IndexedDBContextValue { // Batch operations loadAllMetadata: () => Promise; + loadLeafMetadata: () => Promise; // Only leaf files for recent files list deleteMultiple: (fileIds: FileId[]) => Promise; clearAll: () => Promise; @@ -140,6 +141,64 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) { await fileStorage.deleteFile(fileId); }, []); + const loadLeafMetadata = useCallback(async (): Promise => { + const metadata = await fileStorage.getLeafFileMetadata(); // Only get leaf files + + // Separate PDF and non-PDF files for different processing + const pdfFiles = metadata.filter(m => m.type.includes('pdf')); + const nonPdfFiles = metadata.filter(m => !m.type.includes('pdf')); + + // Process non-PDF files immediately (no history extraction needed) + const nonPdfMetadata: FileMetadata[] = nonPdfFiles.map(m => ({ + id: m.id, + name: m.name, + type: m.type, + size: m.size, + lastModified: m.lastModified, + thumbnail: m.thumbnail, + isLeaf: m.isLeaf + })); + + // Process PDF files with controlled concurrency to avoid memory issues + const BATCH_SIZE = 5; // Process 5 PDFs at a time to avoid overwhelming memory + const pdfMetadata: FileMetadata[] = []; + + for (let i = 0; i < pdfFiles.length; i += BATCH_SIZE) { + const batch = pdfFiles.slice(i, i + BATCH_SIZE); + + const batchResults = await Promise.all(batch.map(async (m) => { + try { + // For PDF files, load and extract basic history for display only + const storedFile = await fileStorage.getFile(m.id); + if (storedFile?.data) { + const file = new File([storedFile.data], m.name, { + type: m.type, + lastModified: m.lastModified + }); + return await createFileMetadataWithHistory(file, m.id, m.thumbnail); + } + } catch (error) { + if (DEBUG) console.warn('🗂️ Failed to extract basic metadata from leaf file:', m.name, error); + } + + // Fallback to basic metadata without history + return { + id: m.id, + name: m.name, + type: m.type, + size: m.size, + lastModified: m.lastModified, + thumbnail: m.thumbnail, + isLeaf: m.isLeaf + }; + })); + + pdfMetadata.push(...batchResults); + } + + return [...nonPdfMetadata, ...pdfMetadata]; + }, []); + const loadAllMetadata = useCallback(async (): Promise => { const metadata = await fileStorage.getAllFileMetadata(); @@ -230,6 +289,7 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) { loadMetadata, deleteFile, loadAllMetadata, + loadLeafMetadata, deleteMultiple, clearAll, getStorageStats, diff --git a/frontend/src/contexts/file/fileActions.ts b/frontend/src/contexts/file/fileActions.ts index 94812f37b..ac3e83c77 100644 --- a/frontend/src/contexts/file/fileActions.ts +++ b/frontend/src/contexts/file/fileActions.ts @@ -15,7 +15,7 @@ import { generateThumbnailWithMetadata } from '../../utils/thumbnailUtils'; import { FileLifecycleManager } from './lifecycle'; import { fileProcessingService } from '../../services/fileProcessingService'; import { buildQuickKeySet, buildQuickKeySetFromMetadata } from './fileSelectors'; -import { extractFileHistory } from '../../utils/fileHistoryUtils'; +import { extractFileHistory, extractBasicFileMetadata } from '../../utils/fileHistoryUtils'; const DEBUG = process.env.NODE_ENV === 'development'; @@ -184,25 +184,23 @@ export async function addFiles( if (DEBUG) console.log(`📄 addFiles(raw): Created initial processedFile metadata for ${file.name} with ${pageCount} pages`); } - // Extract file history from PDF metadata (async) - extractFileHistory(file, record).then(updatedRecord => { - if (updatedRecord !== record && (updatedRecord.originalFileId || updatedRecord.versionNumber)) { - // History was found, dispatch update to trigger re-render + // Extract basic metadata (version number and tool chain) for display + extractBasicFileMetadata(file, record).then(updatedRecord => { + if (updatedRecord !== record && (updatedRecord.versionNumber || updatedRecord.toolHistory)) { + // Basic metadata found, dispatch update to trigger re-render dispatch({ type: 'UPDATE_FILE_RECORD', payload: { id: fileId, updates: { - originalFileId: updatedRecord.originalFileId, versionNumber: updatedRecord.versionNumber, - parentFileId: updatedRecord.parentFileId, toolHistory: updatedRecord.toolHistory } } }); } }).catch(error => { - if (DEBUG) console.warn(`📄 Failed to extract history for ${file.name}:`, error); + if (DEBUG) console.warn(`📄 Failed to extract basic metadata for ${file.name}:`, error); }); existingQuickKeys.add(quickKey); @@ -247,36 +245,23 @@ export async function addFiles( if (DEBUG) console.log(`📄 addFiles(processed): Created initial processedFile metadata for ${file.name} with ${pageCount} pages`); } - // Extract file history from PDF metadata (async) - if (DEBUG) console.log(`📄 addFiles(processed): Starting async history extraction for ${file.name}`); - extractFileHistory(file, record).then(updatedRecord => { - if (DEBUG) console.log(`📄 addFiles(processed): History extraction completed for ${file.name}:`, { - hasChanges: updatedRecord !== record, - originalFileId: updatedRecord.originalFileId, - versionNumber: updatedRecord.versionNumber, - toolHistoryLength: updatedRecord.toolHistory?.length || 0 - }); - - if (updatedRecord !== record && (updatedRecord.originalFileId || updatedRecord.versionNumber)) { - // History was found, dispatch update to trigger re-render - if (DEBUG) console.log(`📄 addFiles(processed): Dispatching UPDATE_FILE_RECORD for ${file.name}`); + // Extract basic metadata (version number and tool chain) for display + extractBasicFileMetadata(file, record).then(updatedRecord => { + if (updatedRecord !== record && (updatedRecord.versionNumber || updatedRecord.toolHistory)) { + // Basic metadata found, dispatch update to trigger re-render dispatch({ type: 'UPDATE_FILE_RECORD', payload: { id: fileId, updates: { - originalFileId: updatedRecord.originalFileId, versionNumber: updatedRecord.versionNumber, - parentFileId: updatedRecord.parentFileId, toolHistory: updatedRecord.toolHistory } } }); - } else { - if (DEBUG) console.log(`📄 addFiles(processed): No history found for ${file.name}, skipping update`); } }).catch(error => { - if (DEBUG) console.error(`📄 addFiles(processed): Failed to extract history for ${file.name}:`, error); + if (DEBUG) console.warn(`📄 Failed to extract basic metadata for ${file.name}:`, error); }); existingQuickKeys.add(quickKey); @@ -354,25 +339,23 @@ export async function addFiles( if (DEBUG) console.log(`📄 addFiles(stored): Created processedFile metadata for ${file.name} with ${pageCount} pages`); } - // Extract file history from PDF metadata (async) - same as raw files - extractFileHistory(file, record).then(updatedRecord => { - if (updatedRecord !== record && (updatedRecord.originalFileId || updatedRecord.versionNumber)) { - // History was found, dispatch update to trigger re-render + // Extract basic metadata (version number and tool chain) for display + extractBasicFileMetadata(file, record).then(updatedRecord => { + if (updatedRecord !== record && (updatedRecord.versionNumber || updatedRecord.toolHistory)) { + // Basic metadata found, dispatch update to trigger re-render dispatch({ type: 'UPDATE_FILE_RECORD', payload: { id: fileId, updates: { - originalFileId: updatedRecord.originalFileId, versionNumber: updatedRecord.versionNumber, - parentFileId: updatedRecord.parentFileId, toolHistory: updatedRecord.toolHistory } } }); } }).catch(error => { - if (DEBUG) console.warn(`📄 Failed to extract history for ${file.name}:`, error); + if (DEBUG) console.warn(`📄 Failed to extract basic metadata for ${file.name}:`, error); }); existingQuickKeys.add(quickKey); @@ -431,22 +414,20 @@ async function processFilesIntoRecords( record.processedFile = createProcessedFile(pageCount, thumbnail); } - // Extract file history from PDF metadata (synchronous during consumeFiles) + // Extract basic metadata synchronously during consumeFiles for immediate display if (file.type.includes('pdf')) { try { - const updatedRecord = await extractFileHistory(file, record); + const updatedRecord = await extractBasicFileMetadata(file, record); - if (updatedRecord !== record && (updatedRecord.originalFileId || updatedRecord.versionNumber)) { - // Update the record directly with history data + if (updatedRecord !== record && (updatedRecord.versionNumber || updatedRecord.toolHistory)) { + // Update the record directly with basic metadata Object.assign(record, { - originalFileId: updatedRecord.originalFileId, versionNumber: updatedRecord.versionNumber, - parentFileId: updatedRecord.parentFileId, toolHistory: updatedRecord.toolHistory }); } } catch (error) { - if (DEBUG) console.warn(`📄 Failed to extract history for ${file.name}:`, error); + if (DEBUG) console.warn(`📄 Failed to extract basic metadata for ${file.name}:`, error); } } diff --git a/frontend/src/hooks/useFileHistory.ts b/frontend/src/hooks/useFileHistory.ts new file mode 100644 index 000000000..3af9c5231 --- /dev/null +++ b/frontend/src/hooks/useFileHistory.ts @@ -0,0 +1,160 @@ +/** + * Custom hook for on-demand file history loading + * Replaces automatic history extraction during file loading + */ + +import { useState, useCallback } from 'react'; +import { FileId } from '../types/file'; +import { FileRecord } from '../types/fileContext'; +import { loadFileHistoryOnDemand } from '../utils/fileHistoryUtils'; + +interface FileHistoryState { + originalFileId?: string; + versionNumber?: number; + parentFileId?: FileId; + toolHistory?: Array<{ + toolName: string; + timestamp: number; + parameters?: Record; + }>; +} + +interface UseFileHistoryResult { + historyData: FileHistoryState | null; + isLoading: boolean; + error: string | null; + loadHistory: (file: File, fileId: FileId, updateFileRecord?: (id: FileId, updates: Partial) => void) => Promise; + clearHistory: () => void; +} + +export function useFileHistory(): UseFileHistoryResult { + const [historyData, setHistoryData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const loadHistory = useCallback(async ( + file: File, + fileId: FileId, + updateFileRecord?: (id: FileId, updates: Partial) => void + ) => { + setIsLoading(true); + setError(null); + + try { + const history = await loadFileHistoryOnDemand(file, fileId, updateFileRecord); + setHistoryData(history); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to load file history'; + setError(errorMessage); + setHistoryData(null); + } finally { + setIsLoading(false); + } + }, []); + + const clearHistory = useCallback(() => { + setHistoryData(null); + setError(null); + setIsLoading(false); + }, []); + + return { + historyData, + isLoading, + error, + loadHistory, + clearHistory + }; +} + +/** + * Hook for managing history state of multiple files + */ +export function useMultiFileHistory() { + const [historyCache, setHistoryCache] = useState>(new Map()); + const [loadingFiles, setLoadingFiles] = useState>(new Set()); + const [errors, setErrors] = useState>(new Map()); + + const loadFileHistory = useCallback(async ( + file: File, + fileId: FileId, + updateFileRecord?: (id: FileId, updates: Partial) => void + ) => { + // Don't reload if already loaded or currently loading + if (historyCache.has(fileId) || loadingFiles.has(fileId)) { + return historyCache.get(fileId) || null; + } + + setLoadingFiles(prev => new Set(prev).add(fileId)); + setErrors(prev => { + const newErrors = new Map(prev); + newErrors.delete(fileId); + return newErrors; + }); + + try { + const history = await loadFileHistoryOnDemand(file, fileId, updateFileRecord); + + if (history) { + setHistoryCache(prev => new Map(prev).set(fileId, history)); + } + + return history; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to load file history'; + setErrors(prev => new Map(prev).set(fileId, errorMessage)); + return null; + } finally { + setLoadingFiles(prev => { + const newSet = new Set(prev); + newSet.delete(fileId); + return newSet; + }); + } + }, [historyCache, loadingFiles]); + + const getHistory = useCallback((fileId: FileId) => { + return historyCache.get(fileId) || null; + }, [historyCache]); + + const isLoadingHistory = useCallback((fileId: FileId) => { + return loadingFiles.has(fileId); + }, [loadingFiles]); + + const getError = useCallback((fileId: FileId) => { + return errors.get(fileId) || null; + }, [errors]); + + const clearHistory = useCallback((fileId: FileId) => { + setHistoryCache(prev => { + const newCache = new Map(prev); + newCache.delete(fileId); + return newCache; + }); + setErrors(prev => { + const newErrors = new Map(prev); + newErrors.delete(fileId); + return newErrors; + }); + setLoadingFiles(prev => { + const newSet = new Set(prev); + newSet.delete(fileId); + return newSet; + }); + }, []); + + const clearAllHistory = useCallback(() => { + setHistoryCache(new Map()); + setLoadingFiles(new Set()); + setErrors(new Map()); + }, []); + + return { + loadFileHistory, + getHistory, + isLoadingHistory, + getError, + clearHistory, + clearAllHistory + }; +} \ No newline at end of file diff --git a/frontend/src/hooks/useFileManager.ts b/frontend/src/hooks/useFileManager.ts index 99b4b6c46..ba51c9f89 100644 --- a/frontend/src/hooks/useFileManager.ts +++ b/frontend/src/hooks/useFileManager.ts @@ -30,8 +30,8 @@ export const useFileManager = () => { return []; } - // Load regular files metadata only - const storedFileMetadata = await indexedDB.loadAllMetadata(); + // Load only leaf files metadata (processed files that haven't been used as input for other tools) + const storedFileMetadata = await indexedDB.loadLeafMetadata(); // For now, only regular files - drafts will be handled separately in the future const allFiles = storedFileMetadata; diff --git a/frontend/src/services/fileStorage.ts b/frontend/src/services/fileStorage.ts index 37c78f08d..7271ec3c3 100644 --- a/frontend/src/services/fileStorage.ts +++ b/frontend/src/services/fileStorage.ts @@ -62,13 +62,11 @@ class FileStorageService { const store = transaction.objectStore(this.storeName); // Debug logging - console.log('Object store keyPath:', store.keyPath); - console.log('Storing file with UUID:', { - id: storedFile.id, // Now a UUID from FileContext + console.log('📄 LEAF FLAG DEBUG - Storing file:', { + id: storedFile.id, name: storedFile.name, - hasData: !!storedFile.data, - dataSize: storedFile.data.byteLength, - isLeaf: storedFile.isLeaf + isLeaf: storedFile.isLeaf, + dataSize: storedFile.data.byteLength }); const request = store.add(storedFile); @@ -222,11 +220,18 @@ class FileStorageService { getRequest.onsuccess = () => { const file = getRequest.result; if (file) { + console.log('📄 LEAF FLAG DEBUG - Marking as processed:', { + id: file.id, + name: file.name, + wasLeaf: file.isLeaf, + nowLeaf: false + }); file.isLeaf = false; const updateRequest = store.put(file); updateRequest.onsuccess = () => resolve(true); updateRequest.onerror = () => reject(updateRequest.error); } else { + console.warn('📄 LEAF FLAG DEBUG - File not found for processing:', id); resolve(false); // File not found } }; @@ -293,6 +298,7 @@ class FileStorageService { } cursor.continue(); } else { + console.log('📄 LEAF FLAG DEBUG - Found leaf files:', files.map(f => ({ id: f.id, name: f.name, isLeaf: f.isLeaf }))); resolve(files); } }; diff --git a/frontend/src/utils/fileHistoryUtils.ts b/frontend/src/utils/fileHistoryUtils.ts index 21232182b..9857c08e7 100644 --- a/frontend/src/utils/fileHistoryUtils.ts +++ b/frontend/src/utils/fileHistoryUtils.ts @@ -334,6 +334,97 @@ export async function getRecentLeafFileMetadata(): Promise { + // Only process PDF files + if (!file.type.includes('pdf')) { + return record; + } + + try { + const arrayBuffer = await file.arrayBuffer(); + const historyMetadata = await pdfMetadataService.extractHistoryMetadata(arrayBuffer); + + if (historyMetadata) { + const history = historyMetadata.stirlingHistory; + + // Update record with essential metadata only (no parent/original relationships) + return { + ...record, + versionNumber: history.versionNumber, + toolHistory: history.toolChain + }; + } + } catch (error) { + if (DEBUG) console.warn('📄 Failed to extract basic metadata:', file.name, error); + } + + return record; +} + +/** + * Load file history on-demand for a specific file + * This replaces the automatic history extraction during file loading + */ +export async function loadFileHistoryOnDemand( + file: File, + fileId: FileId, + updateFileRecord?: (id: FileId, updates: Partial) => void +): Promise<{ + originalFileId?: string; + versionNumber?: number; + parentFileId?: FileId; + toolHistory?: Array<{ + toolName: string; + timestamp: number; + parameters?: Record; + }>; +} | null> { + // Only process PDF files + if (!file.type.includes('pdf')) { + return null; + } + + try { + const baseRecord: FileRecord = { + id: fileId, + name: file.name, + size: file.size, + type: file.type, + lastModified: file.lastModified + }; + + const updatedRecord = await extractFileHistory(file, baseRecord); + + if (updatedRecord !== baseRecord && (updatedRecord.originalFileId || updatedRecord.versionNumber)) { + const historyData = { + originalFileId: updatedRecord.originalFileId, + versionNumber: updatedRecord.versionNumber, + parentFileId: updatedRecord.parentFileId, + toolHistory: updatedRecord.toolHistory + }; + + // Update the file record if update function is provided + if (updateFileRecord) { + updateFileRecord(fileId, historyData); + } + + return historyData; + } + + return null; + } catch (error) { + console.warn(`Failed to load history for ${file.name}:`, error); + return null; + } +} + /** * Create metadata for storing files with history information */ @@ -368,12 +459,11 @@ export async function createFileMetadataWithHistory( result.pdfMetadata = standardMetadata; } - // Add history metadata if available + // Add history metadata if available (basic version for display) if (historyMetadata) { const history = historyMetadata.stirlingHistory; - result.originalFileId = history.originalFileId; + // Only add basic metadata needed for display, not full history relationships result.versionNumber = history.versionNumber; - result.parentFileId = history.parentFileId as FileId | undefined; result.historyInfo = { originalFileId: history.originalFileId, parentFileId: history.parentFileId,