show history

This commit is contained in:
Connor Yoh 2025-09-04 12:11:09 +01:00
parent d0c6ae2c31
commit 02740b2741
9 changed files with 533 additions and 92 deletions

View File

@ -53,12 +53,10 @@ const FileListArea: React.FC<FileListAreaProps> = ({
</Center> </Center>
) : ( ) : (
filteredFiles.map((file, index) => { filteredFiles.map((file, index) => {
// Check if this file is a leaf (appears in group keys) or a history file // Determine if this is a history file based on whether it's in the recent files or loaded as history
const isLeafFile = fileGroups.has(file.id); const isLeafFile = recentFiles.some(rf => rf.id === file.id);
const lineagePath = fileGroups.get(file.id) || []; const isHistoryFile = !isLeafFile; // If not in recent files, it's a loaded history file
const isHistoryFile = !isLeafFile; // If not a leaf, it's a history file const isLatestVersion = isLeafFile; // Only leaf files (from recent files) are latest versions
const isLatestVersion = isLeafFile; // Leaf files are the latest in their branch
const hasVersionHistory = lineagePath.length > 1;
return ( return (
<FileListItem <FileListItem
@ -71,7 +69,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
onDownload={() => onDownloadSingle(file)} onDownload={() => onDownloadSingle(file)}
onDoubleClick={() => onFileDoubleClick(file)} onDoubleClick={() => onFileDoubleClick(file)}
isHistoryFile={isHistoryFile} isHistoryFile={isHistoryFile}
isLatestVersion={isLatestVersion && hasVersionHistory} isLatestVersion={isLatestVersion}
/> />
); );
}) })

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react'; 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 MoreVertIcon from '@mui/icons-material/MoreVert';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import DownloadIcon from '@mui/icons-material/Download'; import DownloadIcon from '@mui/icons-material/Download';
@ -38,7 +38,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const { t } = useTranslation(); 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 // Keep item in hovered state if menu is open
const shouldShowHovered = isHovered || isMenuOpen; const shouldShowHovered = isHovered || isMenuOpen;
@ -46,9 +46,13 @@ const FileListItem: React.FC<FileListItemProps> = ({
// Get version information for this file // Get version information for this file
const leafFileId = isLatestVersion ? file.id : (file.originalFileId || file.id); const leafFileId = isLatestVersion ? file.id : (file.originalFileId || file.id);
const lineagePath = fileGroups.get(leafFileId) || []; 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 currentVersion = file.versionNumber || 0; // Display original files as v0
const isExpanded = expandedFileIds.has(leafFileId); const isExpanded = expandedFileIds.has(leafFileId);
// Get loading state for this file's history
const isLoadingFileHistory = isLoadingHistory(file.id);
const historyError = getHistoryError(file.id);
return ( return (
<> <>
@ -91,6 +95,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
<Box style={{ flex: 1, minWidth: 0 }}> <Box style={{ flex: 1, minWidth: 0 }}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Text size="sm" fw={500} truncate style={{ flex: 1 }}>{file.name}</Text> <Text size="sm" fw={500} truncate style={{ flex: 1 }}>{file.name}</Text>
{isLoadingFileHistory && <Loader size={14} />}
<Badge size="xs" variant="light" color={currentVersion > 0 ? "blue" : "gray"}> <Badge size="xs" variant="light" color={currentVersion > 0 ? "blue" : "gray"}>
v{currentVersion} v{currentVersion}
</Badge> </Badge>
@ -100,7 +105,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
{getFileSize(file)} {getFileDate(file)} {getFileSize(file)} {getFileDate(file)}
{hasVersionHistory && ( {hasVersionHistory && (
<Text span c="dimmed"> {lineagePath.length} versions</Text> <Text span c="dimmed"> has history</Text>
)} )}
</Text> </Text>
@ -157,17 +162,30 @@ const FileListItem: React.FC<FileListItemProps> = ({
{isLatestVersion && hasVersionHistory && ( {isLatestVersion && hasVersionHistory && (
<> <>
<Menu.Item <Menu.Item
leftSection={<HistoryIcon style={{ fontSize: 16 }} />} leftSection={
isLoadingFileHistory ?
<Loader size={16} /> :
<HistoryIcon style={{ fontSize: 16 }} />
}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onToggleExpansion(leafFileId); onToggleExpansion(leafFileId);
}} }}
disabled={isLoadingFileHistory}
> >
{isExpanded ? {isLoadingFileHistory ?
t('fileManager.hideHistory', 'Hide History') : t('fileManager.loadingHistory', 'Loading History...') :
t('fileManager.showHistory', 'Show History') (isExpanded ?
t('fileManager.hideHistory', 'Hide History') :
t('fileManager.showHistory', 'Show History')
)
} }
</Menu.Item> </Menu.Item>
{historyError && (
<Menu.Item disabled c="red" style={{ fontSize: '12px' }}>
{t('fileManager.historyError', 'Error loading history')}
</Menu.Item>
)}
<Menu.Divider /> <Menu.Divider />
</> </>
)} )}

View File

@ -3,7 +3,8 @@ import { FileMetadata } from '../types/file';
import { StoredFile, fileStorage } from '../services/fileStorage'; import { StoredFile, fileStorage } from '../services/fileStorage';
import { downloadFiles } from '../utils/downloadUtils'; import { downloadFiles } from '../utils/downloadUtils';
import { FileId } from '../types/file'; 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 // Type for the context value - now contains everything directly
interface FileManagerContextValue { interface FileManagerContextValue {
@ -18,6 +19,10 @@ interface FileManagerContextValue {
expandedFileIds: Set<string>; expandedFileIds: Set<string>;
fileGroups: Map<string, FileMetadata[]>; fileGroups: Map<string, FileMetadata[]>;
// History loading state
isLoadingHistory: (fileId: FileId) => boolean;
getHistoryError: (fileId: FileId) => string | null;
// Handlers // Handlers
onSourceChange: (source: 'recent' | 'local' | 'drive') => void; onSourceChange: (source: 'recent' | 'local' | 'drive') => void;
onLocalFileClick: () => void; onLocalFileClick: () => void;
@ -75,11 +80,20 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [lastClickedIndex, setLastClickedIndex] = useState<number | null>(null); const [lastClickedIndex, setLastClickedIndex] = useState<number | null>(null);
const [expandedFileIds, setExpandedFileIds] = useState<Set<string>>(new Set()); const [expandedFileIds, setExpandedFileIds] = useState<Set<string>>(new Set());
const [loadedHistoryFiles, setLoadedHistoryFiles] = useState<Map<FileId, FileMetadata[]>>(new Map()); // Cache for loaded history
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
// Track blob URLs for cleanup // Track blob URLs for cleanup
const createdBlobUrls = useRef<Set<string>>(new Set()); const createdBlobUrls = useRef<Set<string>>(new Set());
// History loading hook
const {
loadFileHistory,
getHistory,
isLoadingHistory,
getError: getHistoryError
} = useMultiFileHistory();
// Computed values (with null safety) // Computed values (with null safety)
const selectedFilesSet = new Set(selectedFileIds); const selectedFilesSet = new Set(selectedFileIds);
@ -101,36 +115,24 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
const displayFiles = useMemo(() => { const displayFiles = useMemo(() => {
if (!recentFiles || recentFiles.length === 0) return []; 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 = []; const expandedFiles = [];
for (const [leafFileId, lineagePath] of branchGroups) {
const leafFile = recentFiles.find(f => f.id === leafFileId); // Since we now only load leaf files, iterate through recent files directly
if (!leafFile) continue; for (const leafFile of recentFiles) {
// Add the leaf file (main file shown in list)
// Add the leaf file (shown in main list)
expandedFiles.push(leafFile); expandedFiles.push(leafFile);
// If expanded, add the lineage history (except the leaf itself) // If expanded, add the loaded history files
if (expandedFileIds.has(leafFileId)) { if (expandedFileIds.has(leafFile.id)) {
const historyFiles = lineagePath const historyFiles = loadedHistoryFiles.get(leafFile.id) || [];
.filter((record: any) => record.id !== leafFileId) // Sort history files by version number (oldest first)
.map((record: any) => recentFiles.find(f => f.id === record.id)) const sortedHistory = historyFiles.sort((a, b) => (a.versionNumber || 0) - (b.versionNumber || 0));
.filter((f): f is FileMetadata => f !== undefined); expandedFiles.push(...sortedHistory);
expandedFiles.push(...historyFiles);
} }
} }
return expandedFiles; return expandedFiles;
}, [recentFiles, expandedFileIds, fileGroups]); }, [recentFiles, expandedFileIds, loadedHistoryFiles]);
const selectedFiles = selectedFileIds.length === 0 ? [] : const selectedFiles = selectedFileIds.length === 0 ? [] :
displayFiles.filter(file => selectedFilesSet.has(file.id)); displayFiles.filter(file => selectedFilesSet.has(file.id));
@ -331,7 +333,10 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
} }
}, []); }, []);
const handleToggleExpansion = useCallback((fileId: string) => { const handleToggleExpansion = useCallback(async (fileId: string) => {
const isCurrentlyExpanded = expandedFileIds.has(fileId);
// Update expansion state
setExpandedFileIds(prev => { setExpandedFileIds(prev => {
const newSet = new Set(prev); const newSet = new Set(prev);
if (newSet.has(fileId)) { if (newSet.has(fileId)) {
@ -341,7 +346,124 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
} }
return newSet; 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) => { const handleAddToRecents = useCallback(async (file: FileMetadata) => {
try { try {
@ -399,6 +521,10 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
expandedFileIds, expandedFileIds,
fileGroups, fileGroups,
// History loading state
isLoadingHistory,
getHistoryError,
// Handlers // Handlers
onSourceChange: handleSourceChange, onSourceChange: handleSourceChange,
onLocalFileClick: handleLocalFileClick, onLocalFileClick: handleLocalFileClick,
@ -429,6 +555,8 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
fileInputRef, fileInputRef,
expandedFileIds, expandedFileIds,
fileGroups, fileGroups,
isLoadingHistory,
getHistoryError,
handleSourceChange, handleSourceChange,
handleLocalFileClick, handleLocalFileClick,
handleFileSelect, handleFileSelect,

View File

@ -21,6 +21,7 @@ interface IndexedDBContextValue {
// Batch operations // Batch operations
loadAllMetadata: () => Promise<FileMetadata[]>; loadAllMetadata: () => Promise<FileMetadata[]>;
loadLeafMetadata: () => Promise<FileMetadata[]>; // Only leaf files for recent files list
deleteMultiple: (fileIds: FileId[]) => Promise<void>; deleteMultiple: (fileIds: FileId[]) => Promise<void>;
clearAll: () => Promise<void>; clearAll: () => Promise<void>;
@ -140,6 +141,64 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
await fileStorage.deleteFile(fileId); await fileStorage.deleteFile(fileId);
}, []); }, []);
const loadLeafMetadata = useCallback(async (): Promise<FileMetadata[]> => {
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<FileMetadata[]> => { const loadAllMetadata = useCallback(async (): Promise<FileMetadata[]> => {
const metadata = await fileStorage.getAllFileMetadata(); const metadata = await fileStorage.getAllFileMetadata();
@ -230,6 +289,7 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
loadMetadata, loadMetadata,
deleteFile, deleteFile,
loadAllMetadata, loadAllMetadata,
loadLeafMetadata,
deleteMultiple, deleteMultiple,
clearAll, clearAll,
getStorageStats, getStorageStats,

View File

@ -15,7 +15,7 @@ import { generateThumbnailWithMetadata } from '../../utils/thumbnailUtils';
import { FileLifecycleManager } from './lifecycle'; import { FileLifecycleManager } from './lifecycle';
import { fileProcessingService } from '../../services/fileProcessingService'; import { fileProcessingService } from '../../services/fileProcessingService';
import { buildQuickKeySet, buildQuickKeySetFromMetadata } from './fileSelectors'; import { buildQuickKeySet, buildQuickKeySetFromMetadata } from './fileSelectors';
import { extractFileHistory } from '../../utils/fileHistoryUtils'; import { extractFileHistory, extractBasicFileMetadata } from '../../utils/fileHistoryUtils';
const DEBUG = process.env.NODE_ENV === 'development'; 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`); if (DEBUG) console.log(`📄 addFiles(raw): Created initial processedFile metadata for ${file.name} with ${pageCount} pages`);
} }
// Extract file history from PDF metadata (async) // Extract basic metadata (version number and tool chain) for display
extractFileHistory(file, record).then(updatedRecord => { extractBasicFileMetadata(file, record).then(updatedRecord => {
if (updatedRecord !== record && (updatedRecord.originalFileId || updatedRecord.versionNumber)) { if (updatedRecord !== record && (updatedRecord.versionNumber || updatedRecord.toolHistory)) {
// History was found, dispatch update to trigger re-render // Basic metadata found, dispatch update to trigger re-render
dispatch({ dispatch({
type: 'UPDATE_FILE_RECORD', type: 'UPDATE_FILE_RECORD',
payload: { payload: {
id: fileId, id: fileId,
updates: { updates: {
originalFileId: updatedRecord.originalFileId,
versionNumber: updatedRecord.versionNumber, versionNumber: updatedRecord.versionNumber,
parentFileId: updatedRecord.parentFileId,
toolHistory: updatedRecord.toolHistory toolHistory: updatedRecord.toolHistory
} }
} }
}); });
} }
}).catch(error => { }).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); 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`); if (DEBUG) console.log(`📄 addFiles(processed): Created initial processedFile metadata for ${file.name} with ${pageCount} pages`);
} }
// Extract file history from PDF metadata (async) // Extract basic metadata (version number and tool chain) for display
if (DEBUG) console.log(`📄 addFiles(processed): Starting async history extraction for ${file.name}`); extractBasicFileMetadata(file, record).then(updatedRecord => {
extractFileHistory(file, record).then(updatedRecord => { if (updatedRecord !== record && (updatedRecord.versionNumber || updatedRecord.toolHistory)) {
if (DEBUG) console.log(`📄 addFiles(processed): History extraction completed for ${file.name}:`, { // Basic metadata found, dispatch update to trigger re-render
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}`);
dispatch({ dispatch({
type: 'UPDATE_FILE_RECORD', type: 'UPDATE_FILE_RECORD',
payload: { payload: {
id: fileId, id: fileId,
updates: { updates: {
originalFileId: updatedRecord.originalFileId,
versionNumber: updatedRecord.versionNumber, versionNumber: updatedRecord.versionNumber,
parentFileId: updatedRecord.parentFileId,
toolHistory: updatedRecord.toolHistory toolHistory: updatedRecord.toolHistory
} }
} }
}); });
} else {
if (DEBUG) console.log(`📄 addFiles(processed): No history found for ${file.name}, skipping update`);
} }
}).catch(error => { }).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); 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`); 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 // Extract basic metadata (version number and tool chain) for display
extractFileHistory(file, record).then(updatedRecord => { extractBasicFileMetadata(file, record).then(updatedRecord => {
if (updatedRecord !== record && (updatedRecord.originalFileId || updatedRecord.versionNumber)) { if (updatedRecord !== record && (updatedRecord.versionNumber || updatedRecord.toolHistory)) {
// History was found, dispatch update to trigger re-render // Basic metadata found, dispatch update to trigger re-render
dispatch({ dispatch({
type: 'UPDATE_FILE_RECORD', type: 'UPDATE_FILE_RECORD',
payload: { payload: {
id: fileId, id: fileId,
updates: { updates: {
originalFileId: updatedRecord.originalFileId,
versionNumber: updatedRecord.versionNumber, versionNumber: updatedRecord.versionNumber,
parentFileId: updatedRecord.parentFileId,
toolHistory: updatedRecord.toolHistory toolHistory: updatedRecord.toolHistory
} }
} }
}); });
} }
}).catch(error => { }).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); existingQuickKeys.add(quickKey);
@ -431,22 +414,20 @@ async function processFilesIntoRecords(
record.processedFile = createProcessedFile(pageCount, thumbnail); 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')) { if (file.type.includes('pdf')) {
try { try {
const updatedRecord = await extractFileHistory(file, record); const updatedRecord = await extractBasicFileMetadata(file, record);
if (updatedRecord !== record && (updatedRecord.originalFileId || updatedRecord.versionNumber)) { if (updatedRecord !== record && (updatedRecord.versionNumber || updatedRecord.toolHistory)) {
// Update the record directly with history data // Update the record directly with basic metadata
Object.assign(record, { Object.assign(record, {
originalFileId: updatedRecord.originalFileId,
versionNumber: updatedRecord.versionNumber, versionNumber: updatedRecord.versionNumber,
parentFileId: updatedRecord.parentFileId,
toolHistory: updatedRecord.toolHistory toolHistory: updatedRecord.toolHistory
}); });
} }
} catch (error) { } 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);
} }
} }

View File

@ -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<string, any>;
}>;
}
interface UseFileHistoryResult {
historyData: FileHistoryState | null;
isLoading: boolean;
error: string | null;
loadHistory: (file: File, fileId: FileId, updateFileRecord?: (id: FileId, updates: Partial<FileRecord>) => void) => Promise<void>;
clearHistory: () => void;
}
export function useFileHistory(): UseFileHistoryResult {
const [historyData, setHistoryData] = useState<FileHistoryState | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadHistory = useCallback(async (
file: File,
fileId: FileId,
updateFileRecord?: (id: FileId, updates: Partial<FileRecord>) => 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<Map<FileId, FileHistoryState>>(new Map());
const [loadingFiles, setLoadingFiles] = useState<Set<FileId>>(new Set());
const [errors, setErrors] = useState<Map<FileId, string>>(new Map());
const loadFileHistory = useCallback(async (
file: File,
fileId: FileId,
updateFileRecord?: (id: FileId, updates: Partial<FileRecord>) => 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
};
}

View File

@ -30,8 +30,8 @@ export const useFileManager = () => {
return []; return [];
} }
// Load regular files metadata only // Load only leaf files metadata (processed files that haven't been used as input for other tools)
const storedFileMetadata = await indexedDB.loadAllMetadata(); const storedFileMetadata = await indexedDB.loadLeafMetadata();
// For now, only regular files - drafts will be handled separately in the future // For now, only regular files - drafts will be handled separately in the future
const allFiles = storedFileMetadata; const allFiles = storedFileMetadata;

View File

@ -62,13 +62,11 @@ class FileStorageService {
const store = transaction.objectStore(this.storeName); const store = transaction.objectStore(this.storeName);
// Debug logging // Debug logging
console.log('Object store keyPath:', store.keyPath); console.log('📄 LEAF FLAG DEBUG - Storing file:', {
console.log('Storing file with UUID:', { id: storedFile.id,
id: storedFile.id, // Now a UUID from FileContext
name: storedFile.name, name: storedFile.name,
hasData: !!storedFile.data, isLeaf: storedFile.isLeaf,
dataSize: storedFile.data.byteLength, dataSize: storedFile.data.byteLength
isLeaf: storedFile.isLeaf
}); });
const request = store.add(storedFile); const request = store.add(storedFile);
@ -222,11 +220,18 @@ class FileStorageService {
getRequest.onsuccess = () => { getRequest.onsuccess = () => {
const file = getRequest.result; const file = getRequest.result;
if (file) { if (file) {
console.log('📄 LEAF FLAG DEBUG - Marking as processed:', {
id: file.id,
name: file.name,
wasLeaf: file.isLeaf,
nowLeaf: false
});
file.isLeaf = false; file.isLeaf = false;
const updateRequest = store.put(file); const updateRequest = store.put(file);
updateRequest.onsuccess = () => resolve(true); updateRequest.onsuccess = () => resolve(true);
updateRequest.onerror = () => reject(updateRequest.error); updateRequest.onerror = () => reject(updateRequest.error);
} else { } else {
console.warn('📄 LEAF FLAG DEBUG - File not found for processing:', id);
resolve(false); // File not found resolve(false); // File not found
} }
}; };
@ -293,6 +298,7 @@ class FileStorageService {
} }
cursor.continue(); cursor.continue();
} else { } else {
console.log('📄 LEAF FLAG DEBUG - Found leaf files:', files.map(f => ({ id: f.id, name: f.name, isLeaf: f.isLeaf })));
resolve(files); resolve(files);
} }
}; };

View File

@ -334,6 +334,97 @@ export async function getRecentLeafFileMetadata(): Promise<Omit<import('../servi
} }
} }
/**
* Extract basic file metadata (version number and tool chain) without full history calculation
* This is lightweight and used for displaying essential info on file thumbnails
*/
export async function extractBasicFileMetadata(
file: File,
record: FileRecord
): Promise<FileRecord> {
// 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<FileRecord>) => void
): Promise<{
originalFileId?: string;
versionNumber?: number;
parentFileId?: FileId;
toolHistory?: Array<{
toolName: string;
timestamp: number;
parameters?: Record<string, any>;
}>;
} | 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 * Create metadata for storing files with history information
*/ */
@ -368,12 +459,11 @@ export async function createFileMetadataWithHistory(
result.pdfMetadata = standardMetadata; result.pdfMetadata = standardMetadata;
} }
// Add history metadata if available // Add history metadata if available (basic version for display)
if (historyMetadata) { if (historyMetadata) {
const history = historyMetadata.stirlingHistory; const history = historyMetadata.stirlingHistory;
result.originalFileId = history.originalFileId; // Only add basic metadata needed for display, not full history relationships
result.versionNumber = history.versionNumber; result.versionNumber = history.versionNumber;
result.parentFileId = history.parentFileId as FileId | undefined;
result.historyInfo = { result.historyInfo = {
originalFileId: history.originalFileId, originalFileId: history.originalFileId,
parentFileId: history.parentFileId, parentFileId: history.parentFileId,