mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 01:19:24 +00:00
show history
This commit is contained in:
parent
d0c6ae2c31
commit
02740b2741
@ -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}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
@ -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 />
|
||||
</>
|
||||
)}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
160
frontend/src/hooks/useFileHistory.ts
Normal file
160
frontend/src/hooks/useFileHistory.ts
Normal 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
|
||||
};
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user