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>
) : (
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 (
<FileListItem
@ -71,7 +69,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
onDownload={() => onDownloadSingle(file)}
onDoubleClick={() => onFileDoubleClick(file)}
isHistoryFile={isHistoryFile}
isLatestVersion={isLatestVersion && hasVersionHistory}
isLatestVersion={isLatestVersion}
/>
);
})

View File

@ -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<FileListItemProps> = ({
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<FileListItemProps> = ({
// 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<FileListItemProps> = ({
<Box style={{ flex: 1, minWidth: 0 }}>
<Group gap="xs" align="center">
<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"}>
v{currentVersion}
</Badge>
@ -100,7 +105,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
<Text size="xs" c="dimmed">
{getFileSize(file)} {getFileDate(file)}
{hasVersionHistory && (
<Text span c="dimmed"> {lineagePath.length} versions</Text>
<Text span c="dimmed"> has history</Text>
)}
</Text>
@ -157,17 +162,30 @@ const FileListItem: React.FC<FileListItemProps> = ({
{isLatestVersion && hasVersionHistory && (
<>
<Menu.Item
leftSection={<HistoryIcon style={{ fontSize: 16 }} />}
leftSection={
isLoadingFileHistory ?
<Loader size={16} /> :
<HistoryIcon style={{ fontSize: 16 }} />
}
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')
)
}
</Menu.Item>
{historyError && (
<Menu.Item disabled c="red" style={{ fontSize: '12px' }}>
{t('fileManager.historyError', 'Error loading history')}
</Menu.Item>
)}
<Menu.Divider />
</>
)}

View File

@ -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<string>;
fileGroups: Map<string, FileMetadata[]>;
// 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<FileManagerProviderProps> = ({
const [searchTerm, setSearchTerm] = useState('');
const [lastClickedIndex, setLastClickedIndex] = useState<number | null>(null);
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);
// Track blob URLs for cleanup
const createdBlobUrls = useRef<Set<string>>(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<FileManagerProviderProps> = ({
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<FileManagerProviderProps> = ({
}
}, []);
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<FileManagerProviderProps> = ({
}
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<FileManagerProviderProps> = ({
expandedFileIds,
fileGroups,
// History loading state
isLoadingHistory,
getHistoryError,
// Handlers
onSourceChange: handleSourceChange,
onLocalFileClick: handleLocalFileClick,
@ -429,6 +555,8 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
fileInputRef,
expandedFileIds,
fileGroups,
isLoadingHistory,
getHistoryError,
handleSourceChange,
handleLocalFileClick,
handleFileSelect,

View File

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

View File

@ -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);
}
}

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 [];
}
// 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;

View File

@ -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);
}
};

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
*/
@ -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,