mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 09:29:24 +00:00
4 types
This commit is contained in:
parent
f8bdeabe35
commit
f88c3e25d1
@ -2151,6 +2151,7 @@
|
|||||||
"loadingHistory": "Loading History...",
|
"loadingHistory": "Loading History...",
|
||||||
"lastModified": "Last Modified",
|
"lastModified": "Last Modified",
|
||||||
"toolChain": "Tools Applied",
|
"toolChain": "Tools Applied",
|
||||||
|
"addToRecents": "Add to Recents",
|
||||||
"searchFiles": "Search files...",
|
"searchFiles": "Search files...",
|
||||||
"recent": "Recent",
|
"recent": "Recent",
|
||||||
"localFiles": "Local Files",
|
"localFiles": "Local Files",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useCallback, useEffect } from 'react';
|
import React, { useState, useCallback, useEffect } from 'react';
|
||||||
import { Modal } from '@mantine/core';
|
import { Modal } from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { FileMetadata } from '../types/file';
|
import { StoredFileMetadata, fileStorage } from '../services/fileStorage';
|
||||||
import { useFileManager } from '../hooks/useFileManager';
|
import { useFileManager } from '../hooks/useFileManager';
|
||||||
import { useFilesModalContext } from '../contexts/FilesModalContext';
|
import { useFilesModalContext } from '../contexts/FilesModalContext';
|
||||||
import { Tool } from '../types/tool';
|
import { Tool } from '../types/tool';
|
||||||
@ -16,11 +16,11 @@ interface FileManagerProps {
|
|||||||
|
|
||||||
const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||||
const { isFilesModalOpen, closeFilesModal, onFilesSelect, onStoredFilesSelect } = useFilesModalContext();
|
const { isFilesModalOpen, closeFilesModal, onFilesSelect, onStoredFilesSelect } = useFilesModalContext();
|
||||||
const [recentFiles, setRecentFiles] = useState<FileMetadata[]>([]);
|
const [recentFiles, setRecentFiles] = useState<StoredFileMetadata[]>([]);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
|
||||||
const { loadRecentFiles, handleRemoveFile, convertToFile } = useFileManager();
|
const { loadRecentFiles, handleRemoveFile } = useFileManager();
|
||||||
|
|
||||||
// File management handlers
|
// File management handlers
|
||||||
const isFileSupported = useCallback((fileName: string) => {
|
const isFileSupported = useCallback((fileName: string) => {
|
||||||
@ -34,21 +34,24 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
setRecentFiles(files);
|
setRecentFiles(files);
|
||||||
}, [loadRecentFiles]);
|
}, [loadRecentFiles]);
|
||||||
|
|
||||||
const handleFilesSelected = useCallback(async (files: FileMetadata[]) => {
|
const handleFilesSelected = useCallback(async (files: StoredFileMetadata[]) => {
|
||||||
try {
|
try {
|
||||||
// Use stored files flow that preserves original IDs
|
// Use stored files flow that preserves original IDs
|
||||||
const filesWithMetadata = await Promise.all(
|
// Load full StoredFile objects for selected files
|
||||||
files.map(async (metadata) => ({
|
const storedFiles = await Promise.all(
|
||||||
file: await convertToFile(metadata),
|
files.map(async (metadata) => {
|
||||||
originalId: metadata.id,
|
const storedFile = await fileStorage.getFile(metadata.id);
|
||||||
metadata
|
if (!storedFile) {
|
||||||
}))
|
throw new Error(`File not found in storage: ${metadata.name}`);
|
||||||
|
}
|
||||||
|
return storedFile;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
onStoredFilesSelect(filesWithMetadata);
|
onStoredFilesSelect(storedFiles);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to process selected files:', error);
|
console.error('Failed to process selected files:', error);
|
||||||
}
|
}
|
||||||
}, [convertToFile, onStoredFilesSelect]);
|
}, [onStoredFilesSelect]);
|
||||||
|
|
||||||
const handleNewFileUpload = useCallback(async (files: File[]) => {
|
const handleNewFileUpload = useCallback(async (files: File[]) => {
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
@ -85,7 +88,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
// Cleanup any blob URLs when component unmounts
|
// Cleanup any blob URLs when component unmounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
// FileMetadata doesn't have blob URLs, so no cleanup needed
|
// StoredFileMetadata doesn't have blob URLs, so no cleanup needed
|
||||||
// Blob URLs are managed by FileContext and tool operations
|
// Blob URLs are managed by FileContext and tool operations
|
||||||
console.log('FileManager unmounting - FileContext handles blob URL cleanup');
|
console.log('FileManager unmounting - FileContext handles blob URL cleanup');
|
||||||
};
|
};
|
||||||
@ -148,6 +151,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
recentFiles={recentFiles}
|
recentFiles={recentFiles}
|
||||||
onFilesSelected={handleFilesSelected}
|
onFilesSelected={handleFilesSelected}
|
||||||
onNewFilesSelect={handleNewFileUpload}
|
onNewFilesSelect={handleNewFileUpload}
|
||||||
|
onStoredFilesSelect={onStoredFilesSelect}
|
||||||
onClose={closeFilesModal}
|
onClose={closeFilesModal}
|
||||||
isFileSupported={isFileSupported}
|
isFileSupported={isFileSupported}
|
||||||
isOpen={isFilesModalOpen}
|
isOpen={isFilesModalOpen}
|
||||||
|
@ -69,7 +69,7 @@ const FileEditorThumbnail = ({
|
|||||||
const fileRecord = selectors.getStirlingFileStub(file.id);
|
const fileRecord = selectors.getStirlingFileStub(file.id);
|
||||||
const toolHistory = fileRecord?.toolHistory || [];
|
const toolHistory = fileRecord?.toolHistory || [];
|
||||||
const hasToolHistory = toolHistory.length > 0;
|
const hasToolHistory = toolHistory.length > 0;
|
||||||
const versionNumber = fileRecord?.versionNumber || 0;
|
const versionNumber = fileRecord?.versionNumber || 1;
|
||||||
|
|
||||||
|
|
||||||
const downloadSelectedFile = useCallback(() => {
|
const downloadSelectedFile = useCallback(() => {
|
||||||
|
@ -5,12 +5,12 @@ import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
|||||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { getFileSize } from '../../utils/fileUtils';
|
import { getFileSize } from '../../utils/fileUtils';
|
||||||
import { FileMetadata } from '../../types/file';
|
import { StoredFileMetadata } from '../../services/fileStorage';
|
||||||
|
|
||||||
interface CompactFileDetailsProps {
|
interface CompactFileDetailsProps {
|
||||||
currentFile: FileMetadata | null;
|
currentFile: StoredFileMetadata | null;
|
||||||
thumbnail: string | null;
|
thumbnail: string | null;
|
||||||
selectedFiles: FileMetadata[];
|
selectedFiles: StoredFileMetadata[];
|
||||||
currentFileIndex: number;
|
currentFileIndex: number;
|
||||||
numberOfFiles: number;
|
numberOfFiles: number;
|
||||||
isAnimating: boolean;
|
isAnimating: boolean;
|
||||||
@ -72,7 +72,7 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
|
|||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
{currentFile ? getFileSize(currentFile) : ''}
|
{currentFile ? getFileSize(currentFile) : ''}
|
||||||
{selectedFiles.length > 1 && ` • ${selectedFiles.length} files`}
|
{selectedFiles.length > 1 && ` • ${selectedFiles.length} files`}
|
||||||
{currentFile && ` • v${currentFile.versionNumber || 0}`}
|
{currentFile && ` • v${currentFile.versionNumber || 1}`}
|
||||||
</Text>
|
</Text>
|
||||||
{hasMultipleFiles && (
|
{hasMultipleFiles && (
|
||||||
<Text size="xs" c="blue">
|
<Text size="xs" c="blue">
|
||||||
@ -80,9 +80,9 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{/* Compact tool chain for mobile */}
|
{/* Compact tool chain for mobile */}
|
||||||
{currentFile?.historyInfo?.toolChain && currentFile.historyInfo.toolChain.length > 0 && (
|
{currentFile?.toolHistory && currentFile.toolHistory.length > 0 && (
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
{currentFile.historyInfo.toolChain.map(tool => tool.toolName).join(' → ')}
|
{currentFile.toolHistory.map((tool: any) => tool.toolName).join(' → ')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -2,11 +2,11 @@ import React from 'react';
|
|||||||
import { Stack, Card, Box, Text, Badge, Group, Divider, ScrollArea } from '@mantine/core';
|
import { Stack, Card, Box, Text, Badge, Group, Divider, ScrollArea } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { detectFileExtension, getFileSize } from '../../utils/fileUtils';
|
import { detectFileExtension, getFileSize } from '../../utils/fileUtils';
|
||||||
import { FileMetadata } from '../../types/file';
|
import { StoredFileMetadata } from '../../services/fileStorage';
|
||||||
import ToolChain from '../shared/ToolChain';
|
import ToolChain from '../shared/ToolChain';
|
||||||
|
|
||||||
interface FileInfoCardProps {
|
interface FileInfoCardProps {
|
||||||
currentFile: FileMetadata | null;
|
currentFile: StoredFileMetadata | null;
|
||||||
modalHeight: string;
|
modalHeight: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,19 +114,19 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
|
|||||||
<Text size="sm" c="dimmed">{t('fileManager.fileVersion', 'Version')}</Text>
|
<Text size="sm" c="dimmed">{t('fileManager.fileVersion', 'Version')}</Text>
|
||||||
{currentFile &&
|
{currentFile &&
|
||||||
<Badge size="sm" variant="light" color={currentFile?.versionNumber ? 'blue' : 'gray'}>
|
<Badge size="sm" variant="light" color={currentFile?.versionNumber ? 'blue' : 'gray'}>
|
||||||
v{currentFile ? (currentFile.versionNumber || 0) : ''}
|
v{currentFile ? (currentFile.versionNumber || 1) : ''}
|
||||||
</Badge>}
|
</Badge>}
|
||||||
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Tool Chain Display */}
|
{/* Tool Chain Display */}
|
||||||
{currentFile?.historyInfo?.toolChain && currentFile.historyInfo.toolChain.length > 0 && (
|
{currentFile?.toolHistory && currentFile.toolHistory.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Box py="xs">
|
<Box py="xs">
|
||||||
<Text size="xs" c="dimmed" mb="xs">{t('fileManager.toolChain', 'Tools Applied')}</Text>
|
<Text size="xs" c="dimmed" mb="xs">{t('fileManager.toolChain', 'Tools Applied')}</Text>
|
||||||
<ToolChain
|
<ToolChain
|
||||||
toolChain={currentFile.historyInfo.toolChain}
|
toolChain={currentFile.toolHistory}
|
||||||
displayStyle="badges"
|
displayStyle="badges"
|
||||||
size="xs"
|
size="xs"
|
||||||
maxWidth={'180px'}
|
maxWidth={'180px'}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Group, Box, Text, ActionIcon, Checkbox, Divider, Menu, Badge, Button, Loader } from '@mantine/core';
|
import { Group, Box, Text, ActionIcon, Checkbox, Divider, Menu, Badge, 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';
|
||||||
@ -7,12 +7,12 @@ import AddIcon from '@mui/icons-material/Add';
|
|||||||
import HistoryIcon from '@mui/icons-material/History';
|
import HistoryIcon from '@mui/icons-material/History';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { getFileSize, getFileDate } from '../../utils/fileUtils';
|
import { getFileSize, getFileDate } from '../../utils/fileUtils';
|
||||||
import { FileMetadata } from '../../types/file';
|
import { StoredFileMetadata } from '../../services/fileStorage';
|
||||||
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
||||||
import ToolChain from '../shared/ToolChain';
|
import ToolChain from '../shared/ToolChain';
|
||||||
|
|
||||||
interface FileListItemProps {
|
interface FileListItemProps {
|
||||||
file: FileMetadata;
|
file: StoredFileMetadata;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isSupported: boolean;
|
isSupported: boolean;
|
||||||
onSelect: (shiftKey?: boolean) => void;
|
onSelect: (shiftKey?: boolean) => void;
|
||||||
@ -38,22 +38,17 @@ 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, isLoadingHistory, getHistoryError } = useFileManagerContext();
|
const {expandedFileIds, onToggleExpansion, onAddToRecents } = 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;
|
||||||
|
|
||||||
// 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 hasVersionHistory = (file.versionNumber || 1) > 1; // Show history for any processed file (v2+)
|
||||||
const hasVersionHistory = (file.versionNumber || 0) > 0; // Show history for any processed file (v1+)
|
const currentVersion = file.versionNumber || 1; // Display original files as v1
|
||||||
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 (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box
|
||||||
@ -95,8 +90,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 > 1 ? "blue" : "gray"}>
|
||||||
<Badge size="xs" variant="light" color={currentVersion > 0 ? "blue" : "gray"}>
|
|
||||||
v{currentVersion}
|
v{currentVersion}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
@ -104,15 +98,12 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
|||||||
<Group gap="xs" align="center">
|
<Group gap="xs" align="center">
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
{getFileSize(file)} • {getFileDate(file)}
|
{getFileSize(file)} • {getFileDate(file)}
|
||||||
{hasVersionHistory && (
|
|
||||||
<Text span c="dimmed"> • has history</Text>
|
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Tool chain for processed files */}
|
{/* Tool chain for processed files */}
|
||||||
{file.historyInfo?.toolChain && file.historyInfo.toolChain.length > 0 && (
|
{file.toolHistory && file.toolHistory.length > 0 && (
|
||||||
<ToolChain
|
<ToolChain
|
||||||
toolChain={file.historyInfo.toolChain}
|
toolChain={file.toolHistory}
|
||||||
maxWidth={'150px'}
|
maxWidth={'150px'}
|
||||||
displayStyle="text"
|
displayStyle="text"
|
||||||
size="xs"
|
size="xs"
|
||||||
@ -163,29 +154,20 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
|||||||
<>
|
<>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={
|
leftSection={
|
||||||
isLoadingFileHistory ?
|
|
||||||
<Loader size={16} /> :
|
|
||||||
<HistoryIcon style={{ fontSize: 16 }} />
|
<HistoryIcon style={{ fontSize: 16 }} />
|
||||||
}
|
}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onToggleExpansion(leafFileId);
|
onToggleExpansion(leafFileId);
|
||||||
}}
|
}}
|
||||||
disabled={isLoadingFileHistory}
|
|
||||||
>
|
>
|
||||||
{isLoadingFileHistory ?
|
{
|
||||||
t('fileManager.loadingHistory', 'Loading History...') :
|
|
||||||
(isExpanded ?
|
(isExpanded ?
|
||||||
t('fileManager.hideHistory', 'Hide History') :
|
t('fileManager.hideHistory', 'Hide History') :
|
||||||
t('fileManager.showHistory', 'Show 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 />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Center } from '@mantine/core';
|
import { Box, Center } from '@mantine/core';
|
||||||
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
|
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
|
||||||
import { FileMetadata } from '../../types/file';
|
import { StoredFileMetadata } from '../../services/fileStorage';
|
||||||
import DocumentThumbnail from './filePreview/DocumentThumbnail';
|
import DocumentThumbnail from './filePreview/DocumentThumbnail';
|
||||||
import DocumentStack from './filePreview/DocumentStack';
|
import DocumentStack from './filePreview/DocumentStack';
|
||||||
import HoverOverlay from './filePreview/HoverOverlay';
|
import HoverOverlay from './filePreview/HoverOverlay';
|
||||||
@ -9,7 +9,7 @@ import NavigationArrows from './filePreview/NavigationArrows';
|
|||||||
|
|
||||||
export interface FilePreviewProps {
|
export interface FilePreviewProps {
|
||||||
// Core file data
|
// Core file data
|
||||||
file: File | FileMetadata | null;
|
file: File | StoredFileMetadata | null;
|
||||||
thumbnail?: string | null;
|
thumbnail?: string | null;
|
||||||
|
|
||||||
// Optional features
|
// Optional features
|
||||||
@ -22,7 +22,7 @@ export interface FilePreviewProps {
|
|||||||
isAnimating?: boolean;
|
isAnimating?: boolean;
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
onFileClick?: (file: File | FileMetadata | null) => void;
|
onFileClick?: (file: File | StoredFileMetadata | null) => void;
|
||||||
onPrevious?: () => void;
|
onPrevious?: () => void;
|
||||||
onNext?: () => void;
|
onNext?: () => void;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Center, Image } from '@mantine/core';
|
import { Box, Center, Image } from '@mantine/core';
|
||||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||||
import { FileMetadata } from '../../../types/file';
|
import { StoredFileMetadata } from '../../../services/fileStorage';
|
||||||
|
|
||||||
export interface DocumentThumbnailProps {
|
export interface DocumentThumbnailProps {
|
||||||
file: File | FileMetadata | null;
|
file: File | StoredFileMetadata | null;
|
||||||
thumbnail?: string | null;
|
thumbnail?: string | null;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
@ -32,6 +32,7 @@ import { AddedFile, addFiles, consumeFiles, undoConsumeFiles, createFileActions
|
|||||||
import { FileLifecycleManager } from './file/lifecycle';
|
import { FileLifecycleManager } from './file/lifecycle';
|
||||||
import { FileStateContext, FileActionsContext } from './file/contexts';
|
import { FileStateContext, FileActionsContext } from './file/contexts';
|
||||||
import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext';
|
import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext';
|
||||||
|
import { StoredFile, StoredFileMetadata } from '../services/fileStorage';
|
||||||
|
|
||||||
const DEBUG = process.env.NODE_ENV === 'development';
|
const DEBUG = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
@ -88,11 +89,33 @@ function FileContextInner({
|
|||||||
selectFiles(addedFilesWithIds);
|
selectFiles(addedFilesWithIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist to IndexedDB if enabled
|
// Persist to IndexedDB if enabled and update StirlingFileStub with version info
|
||||||
if (indexedDB && enablePersistence && addedFilesWithIds.length > 0) {
|
if (indexedDB && enablePersistence && addedFilesWithIds.length > 0) {
|
||||||
await Promise.all(addedFilesWithIds.map(async ({ file, id, thumbnail }) => {
|
await Promise.all(addedFilesWithIds.map(async ({ file, id, thumbnail }) => {
|
||||||
try {
|
try {
|
||||||
await indexedDB.saveFile(file, id, thumbnail);
|
const metadata = await indexedDB.saveFile(file, id, thumbnail);
|
||||||
|
|
||||||
|
// Update StirlingFileStub with version information from IndexedDB
|
||||||
|
if (metadata.versionNumber || metadata.originalFileId) {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_FILE_RECORD',
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
updates: {
|
||||||
|
versionNumber: metadata.versionNumber,
|
||||||
|
originalFileId: metadata.originalFileId,
|
||||||
|
parentFileId: metadata.parentFileId,
|
||||||
|
toolHistory: metadata.toolHistory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (DEBUG) console.log(`📄 FileContext: Updated raw file ${file.name} with IndexedDB history data:`, {
|
||||||
|
versionNumber: metadata.versionNumber,
|
||||||
|
originalFileId: metadata.originalFileId,
|
||||||
|
toolChainLength: metadata.toolHistory?.length || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to persist file to IndexedDB:', file.name, error);
|
console.error('Failed to persist file to IndexedDB:', file.name, error);
|
||||||
}
|
}
|
||||||
@ -107,7 +130,20 @@ function FileContextInner({
|
|||||||
return result.map(({ file, id }) => createStirlingFile(file, id));
|
return result.map(({ file, id }) => createStirlingFile(file, id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise<StirlingFile[]> => {
|
const addStoredFiles = useCallback(async (storedFiles: StoredFile[], options?: { selectFiles?: boolean }): Promise<StirlingFile[]> => {
|
||||||
|
// Convert StoredFile[] to the format expected by addFiles
|
||||||
|
const filesWithMetadata = storedFiles.map(storedFile => ({
|
||||||
|
file: new File([storedFile.data], storedFile.name, {
|
||||||
|
type: storedFile.type,
|
||||||
|
lastModified: storedFile.lastModified
|
||||||
|
}),
|
||||||
|
originalId: storedFile.id,
|
||||||
|
metadata: {
|
||||||
|
...storedFile,
|
||||||
|
data: undefined // Remove data field for metadata
|
||||||
|
} as StoredFileMetadata
|
||||||
|
}));
|
||||||
|
|
||||||
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager);
|
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager);
|
||||||
|
|
||||||
// Auto-select the newly added files if requested
|
// Auto-select the newly added files if requested
|
||||||
@ -118,6 +154,7 @@ function FileContextInner({
|
|||||||
return result.map(({ file, id }) => createStirlingFile(file, id));
|
return result.map(({ file, id }) => createStirlingFile(file, id));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// Action creators
|
// Action creators
|
||||||
const baseActions = useMemo(() => createFileActions(dispatch), []);
|
const baseActions = useMemo(() => createFileActions(dispatch), []);
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import React, { createContext, useContext, useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
import React, { createContext, useContext, useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { FileMetadata } from '../types/file';
|
import { StoredFileMetadata, StoredFile } from '../services/fileStorage';
|
||||||
import { fileStorage } from '../services/fileStorage';
|
import { 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, createFileMetadataWithHistory } from '../utils/fileHistoryUtils';
|
import { groupFilesByOriginal } 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 {
|
||||||
@ -12,36 +11,32 @@ interface FileManagerContextValue {
|
|||||||
activeSource: 'recent' | 'local' | 'drive';
|
activeSource: 'recent' | 'local' | 'drive';
|
||||||
selectedFileIds: FileId[];
|
selectedFileIds: FileId[];
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
selectedFiles: FileMetadata[];
|
selectedFiles: StoredFileMetadata[];
|
||||||
filteredFiles: FileMetadata[];
|
filteredFiles: StoredFileMetadata[];
|
||||||
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||||
selectedFilesSet: Set<string>;
|
selectedFilesSet: Set<string>;
|
||||||
expandedFileIds: Set<string>;
|
expandedFileIds: Set<string>;
|
||||||
fileGroups: Map<string, FileMetadata[]>;
|
fileGroups: Map<string, StoredFileMetadata[]>;
|
||||||
|
|
||||||
// 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;
|
||||||
onFileSelect: (file: FileMetadata, index: number, shiftKey?: boolean) => void;
|
onFileSelect: (file: StoredFileMetadata, index: number, shiftKey?: boolean) => void;
|
||||||
onFileRemove: (index: number) => void;
|
onFileRemove: (index: number) => void;
|
||||||
onFileDoubleClick: (file: FileMetadata) => void;
|
onFileDoubleClick: (file: StoredFileMetadata) => void;
|
||||||
onOpenFiles: () => void;
|
onOpenFiles: () => void;
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
onFileInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
onFileInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
onSelectAll: () => void;
|
onSelectAll: () => void;
|
||||||
onDeleteSelected: () => void;
|
onDeleteSelected: () => void;
|
||||||
onDownloadSelected: () => void;
|
onDownloadSelected: () => void;
|
||||||
onDownloadSingle: (file: FileMetadata) => void;
|
onDownloadSingle: (file: StoredFileMetadata) => void;
|
||||||
onToggleExpansion: (fileId: string) => void;
|
onToggleExpansion: (fileId: string) => void;
|
||||||
onAddToRecents: (file: FileMetadata) => void;
|
onAddToRecents: (file: StoredFileMetadata) => void;
|
||||||
onNewFilesSelect: (files: File[]) => void;
|
onNewFilesSelect: (files: File[]) => void;
|
||||||
|
|
||||||
// External props
|
// External props
|
||||||
recentFiles: FileMetadata[];
|
recentFiles: StoredFileMetadata[];
|
||||||
isFileSupported: (fileName: string) => boolean;
|
isFileSupported: (fileName: string) => boolean;
|
||||||
modalHeight: string;
|
modalHeight: string;
|
||||||
}
|
}
|
||||||
@ -52,9 +47,10 @@ const FileManagerContext = createContext<FileManagerContextValue | null>(null);
|
|||||||
// Provider component props
|
// Provider component props
|
||||||
interface FileManagerProviderProps {
|
interface FileManagerProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
recentFiles: FileMetadata[];
|
recentFiles: StoredFileMetadata[];
|
||||||
onFilesSelected: (files: FileMetadata[]) => void; // For selecting stored files
|
onFilesSelected: (files: StoredFileMetadata[]) => void; // For selecting stored files
|
||||||
onNewFilesSelect: (files: File[]) => void; // For uploading new local files
|
onNewFilesSelect: (files: File[]) => void; // For uploading new local files
|
||||||
|
onStoredFilesSelect: (storedFiles: StoredFile[]) => void; // For adding stored files directly
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
isFileSupported: (fileName: string) => boolean;
|
isFileSupported: (fileName: string) => boolean;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -68,6 +64,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
recentFiles,
|
recentFiles,
|
||||||
onFilesSelected,
|
onFilesSelected,
|
||||||
onNewFilesSelect,
|
onNewFilesSelect,
|
||||||
|
onStoredFilesSelect: onStoredFilesSelect,
|
||||||
onClose,
|
onClose,
|
||||||
isFileSupported,
|
isFileSupported,
|
||||||
isOpen,
|
isOpen,
|
||||||
@ -80,19 +77,12 @@ 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 [loadedHistoryFiles, setLoadedHistoryFiles] = useState<Map<FileId, StoredFileMetadata[]>>(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,11 +91,11 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
const fileGroups = useMemo(() => {
|
const fileGroups = useMemo(() => {
|
||||||
if (!recentFiles || recentFiles.length === 0) return new Map();
|
if (!recentFiles || recentFiles.length === 0) return new Map();
|
||||||
|
|
||||||
// Convert FileMetadata to FileRecord-like objects for grouping utility
|
// Convert StoredFileMetadata to FileRecord-like objects for grouping utility
|
||||||
const recordsForGrouping = recentFiles.map(file => ({
|
const recordsForGrouping = recentFiles.map(file => ({
|
||||||
...file,
|
...file,
|
||||||
originalFileId: file.originalFileId,
|
originalFileId: file.originalFileId,
|
||||||
versionNumber: file.versionNumber || 0
|
versionNumber: file.versionNumber || 1
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return groupFilesByOriginal(recordsForGrouping);
|
return groupFilesByOriginal(recordsForGrouping);
|
||||||
@ -126,7 +116,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
if (expandedFileIds.has(leafFile.id)) {
|
if (expandedFileIds.has(leafFile.id)) {
|
||||||
const historyFiles = loadedHistoryFiles.get(leafFile.id) || [];
|
const historyFiles = loadedHistoryFiles.get(leafFile.id) || [];
|
||||||
// Sort history files by version number (oldest first)
|
// Sort history files by version number (oldest first)
|
||||||
const sortedHistory = historyFiles.sort((a, b) => (a.versionNumber || 0) - (b.versionNumber || 0));
|
const sortedHistory = historyFiles.sort((a, b) => (a.versionNumber || 1) - (b.versionNumber || 1));
|
||||||
expandedFiles.push(...sortedHistory);
|
expandedFiles.push(...sortedHistory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,7 +145,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleFileSelect = useCallback((file: FileMetadata, currentIndex: number, shiftKey?: boolean) => {
|
const handleFileSelect = useCallback((file: StoredFileMetadata, currentIndex: number, shiftKey?: boolean) => {
|
||||||
const fileId = file.id;
|
const fileId = file.id;
|
||||||
if (!fileId) return;
|
if (!fileId) return;
|
||||||
|
|
||||||
@ -196,33 +186,99 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
}
|
}
|
||||||
}, [filteredFiles, lastClickedIndex]);
|
}, [filteredFiles, lastClickedIndex]);
|
||||||
|
|
||||||
|
// Helper function to safely determine which files can be deleted
|
||||||
|
const getSafeFilesToDelete = useCallback((
|
||||||
|
leafFileIds: string[],
|
||||||
|
allStoredMetadata: Omit<import('../services/fileStorage').StoredFile, 'data'>[]
|
||||||
|
): string[] => {
|
||||||
|
const fileMap = new Map(allStoredMetadata.map(f => [f.id as string, f]));
|
||||||
|
const filesToDelete = new Set<string>();
|
||||||
|
const filesToPreserve = new Set<string>();
|
||||||
|
|
||||||
|
// First, identify all files in the lineages of the leaf files being deleted
|
||||||
|
for (const leafFileId of leafFileIds) {
|
||||||
|
const currentFile = fileMap.get(leafFileId);
|
||||||
|
if (!currentFile) continue;
|
||||||
|
|
||||||
|
// Always include the leaf file itself for deletion
|
||||||
|
filesToDelete.add(leafFileId);
|
||||||
|
|
||||||
|
// If this is a processed file with history, trace back through its lineage
|
||||||
|
if (currentFile.versionNumber && currentFile.versionNumber > 1) {
|
||||||
|
const originalFileId = currentFile.originalFileId || currentFile.id;
|
||||||
|
|
||||||
|
// Find all files in this history chain
|
||||||
|
const chainFiles = allStoredMetadata.filter(file =>
|
||||||
|
(file.originalFileId || file.id) === originalFileId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add all files in this lineage as candidates for deletion
|
||||||
|
chainFiles.forEach(file => filesToDelete.add(file.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now identify files that must be preserved because they're referenced by OTHER lineages
|
||||||
|
for (const file of allStoredMetadata) {
|
||||||
|
const fileOriginalId = file.originalFileId || file.id;
|
||||||
|
|
||||||
|
// If this file is a leaf node (not being deleted) and its lineage overlaps with files we want to delete
|
||||||
|
if (file.isLeaf !== false && !leafFileIds.includes(file.id)) {
|
||||||
|
// Find all files in this preserved lineage
|
||||||
|
const preservedChainFiles = allStoredMetadata.filter(chainFile =>
|
||||||
|
(chainFile.originalFileId || chainFile.id) === fileOriginalId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mark all files in this preserved lineage as must-preserve
|
||||||
|
preservedChainFiles.forEach(chainFile => filesToPreserve.add(chainFile.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final list: files to delete minus files that must be preserved
|
||||||
|
const safeToDelete = Array.from(filesToDelete).filter(fileId => !filesToPreserve.has(fileId));
|
||||||
|
|
||||||
|
console.log('Deletion analysis:', {
|
||||||
|
candidatesForDeletion: Array.from(filesToDelete),
|
||||||
|
mustPreserve: Array.from(filesToPreserve),
|
||||||
|
safeToDelete
|
||||||
|
});
|
||||||
|
|
||||||
|
return safeToDelete;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleFileRemove = useCallback(async (index: number) => {
|
const handleFileRemove = useCallback(async (index: number) => {
|
||||||
const fileToRemove = filteredFiles[index];
|
const fileToRemove = filteredFiles[index];
|
||||||
if (fileToRemove) {
|
if (fileToRemove) {
|
||||||
const deletedFileId = fileToRemove.id;
|
const deletedFileId = fileToRemove.id;
|
||||||
|
|
||||||
|
// Get all stored files to analyze lineages
|
||||||
|
const allStoredMetadata = await fileStorage.getAllFileMetadata();
|
||||||
|
|
||||||
|
// Get safe files to delete (respecting shared lineages)
|
||||||
|
const filesToDelete = getSafeFilesToDelete([deletedFileId as string], allStoredMetadata);
|
||||||
|
|
||||||
|
console.log(`Safely deleting files for ${fileToRemove.name}:`, filesToDelete);
|
||||||
|
|
||||||
// Clear from selection immediately
|
// Clear from selection immediately
|
||||||
setSelectedFileIds(prev => prev.filter(id => id !== deletedFileId));
|
setSelectedFileIds(prev => prev.filter(id => !filesToDelete.includes(id)));
|
||||||
|
|
||||||
// Clear from expanded state to prevent ghost entries
|
// Clear from expanded state to prevent ghost entries
|
||||||
setExpandedFileIds(prev => {
|
setExpandedFileIds(prev => {
|
||||||
const newExpanded = new Set(prev);
|
const newExpanded = new Set(prev);
|
||||||
newExpanded.delete(deletedFileId);
|
filesToDelete.forEach(id => newExpanded.delete(id));
|
||||||
return newExpanded;
|
return newExpanded;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear from history cache - need to remove this file from any cached history
|
// Clear from history cache - remove all files in the chain
|
||||||
setLoadedHistoryFiles(prev => {
|
setLoadedHistoryFiles(prev => {
|
||||||
const newCache = new Map(prev);
|
const newCache = new Map(prev);
|
||||||
|
|
||||||
// If the deleted file was a main file with cached history, remove its cache
|
// Remove cache entries for all deleted files
|
||||||
newCache.delete(deletedFileId);
|
filesToDelete.forEach(id => newCache.delete(id as FileId));
|
||||||
|
|
||||||
// Also remove the deleted file from any other file's history cache
|
// Also remove deleted files from any other file's history cache
|
||||||
for (const [mainFileId, historyFiles] of newCache.entries()) {
|
for (const [mainFileId, historyFiles] of newCache.entries()) {
|
||||||
const filteredHistory = historyFiles.filter(histFile => histFile.id !== deletedFileId);
|
const filteredHistory = historyFiles.filter(histFile => !filesToDelete.includes(histFile.id as string));
|
||||||
if (filteredHistory.length !== historyFiles.length) {
|
if (filteredHistory.length !== historyFiles.length) {
|
||||||
// The deleted file was in this history, update the cache
|
|
||||||
newCache.set(mainFileId, filteredHistory);
|
newCache.set(mainFileId, filteredHistory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,15 +286,24 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
return newCache;
|
return newCache;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call the parent's deletion logic
|
// Delete safe files from IndexedDB
|
||||||
|
try {
|
||||||
|
for (const fileId of filesToDelete) {
|
||||||
|
await fileStorage.deleteFile(fileId as FileId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete files from chain:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the parent's deletion logic for the main file only
|
||||||
await onFileRemove(index);
|
await onFileRemove(index);
|
||||||
|
|
||||||
// Refresh to ensure consistent state
|
// Refresh to ensure consistent state
|
||||||
await refreshRecentFiles();
|
await refreshRecentFiles();
|
||||||
}
|
}
|
||||||
}, [filteredFiles, onFileRemove, refreshRecentFiles]);
|
}, [filteredFiles, onFileRemove, refreshRecentFiles, getSafeFilesToDelete]);
|
||||||
|
|
||||||
const handleFileDoubleClick = useCallback((file: FileMetadata) => {
|
const handleFileDoubleClick = useCallback((file: StoredFileMetadata) => {
|
||||||
if (isFileSupported(file.name)) {
|
if (isFileSupported(file.name)) {
|
||||||
onFilesSelected([file]);
|
onFilesSelected([file]);
|
||||||
onClose();
|
onClose();
|
||||||
@ -288,40 +353,28 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
if (selectedFileIds.length === 0) return;
|
if (selectedFileIds.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use the same logic as individual file deletion for consistency
|
// Get all stored files to analyze lineages
|
||||||
// Delete each selected file individually using the same cache update logic
|
const allStoredMetadata = await fileStorage.getAllFileMetadata();
|
||||||
const allFilesToDelete = filteredFiles.filter(file =>
|
|
||||||
selectedFileIds.includes(file.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Deduplicate by file ID since shared files can appear multiple times in the display
|
// Get safe files to delete (respecting shared lineages)
|
||||||
const uniqueFilesToDelete = allFilesToDelete.reduce((unique: typeof allFilesToDelete, file) => {
|
const filesToDelete = getSafeFilesToDelete(selectedFileIds, allStoredMetadata);
|
||||||
if (!unique.some(f => f.id === file.id)) {
|
|
||||||
unique.push(file);
|
|
||||||
}
|
|
||||||
return unique;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const filesToDelete = uniqueFilesToDelete;
|
console.log(`Bulk safely deleting files and their history chains:`, filesToDelete);
|
||||||
const deletedFileIds = new Set(filesToDelete.map(f => f.id));
|
|
||||||
|
|
||||||
// Update history cache synchronously
|
// Update history cache synchronously
|
||||||
setLoadedHistoryFiles(prev => {
|
setLoadedHistoryFiles(prev => {
|
||||||
const newCache = new Map(prev);
|
const newCache = new Map(prev);
|
||||||
|
|
||||||
for (const fileToDelete of filesToDelete) {
|
// Remove cache entries for all deleted files
|
||||||
// If the deleted file was a main file with cached history, remove its cache
|
filesToDelete.forEach(id => newCache.delete(id as FileId));
|
||||||
newCache.delete(fileToDelete.id);
|
|
||||||
|
|
||||||
// Also remove the deleted file from any other file's history cache
|
// Also remove deleted files from any other file's history cache
|
||||||
for (const [mainFileId, historyFiles] of newCache.entries()) {
|
for (const [mainFileId, historyFiles] of newCache.entries()) {
|
||||||
const filteredHistory = historyFiles.filter(histFile => histFile.id !== fileToDelete.id);
|
const filteredHistory = historyFiles.filter(histFile => !filesToDelete.includes(histFile.id as string));
|
||||||
if (filteredHistory.length !== historyFiles.length) {
|
if (filteredHistory.length !== historyFiles.length) {
|
||||||
// The deleted file was in this history, update the cache
|
|
||||||
newCache.set(mainFileId, filteredHistory);
|
newCache.set(mainFileId, filteredHistory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return newCache;
|
return newCache;
|
||||||
});
|
});
|
||||||
@ -329,18 +382,16 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
// Also clear any expanded state for deleted files to prevent ghost entries
|
// Also clear any expanded state for deleted files to prevent ghost entries
|
||||||
setExpandedFileIds(prev => {
|
setExpandedFileIds(prev => {
|
||||||
const newExpanded = new Set(prev);
|
const newExpanded = new Set(prev);
|
||||||
for (const deletedId of deletedFileIds) {
|
filesToDelete.forEach(id => newExpanded.delete(id));
|
||||||
newExpanded.delete(deletedId);
|
|
||||||
}
|
|
||||||
return newExpanded;
|
return newExpanded;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear selection immediately to prevent ghost selections
|
// Clear selection immediately to prevent ghost selections
|
||||||
setSelectedFileIds(prev => prev.filter(id => !deletedFileIds.has(id)));
|
setSelectedFileIds(prev => prev.filter(id => !filesToDelete.includes(id)));
|
||||||
|
|
||||||
// Delete files from IndexedDB
|
// Delete safe files from IndexedDB
|
||||||
for (const file of filesToDelete) {
|
for (const fileId of filesToDelete) {
|
||||||
await fileStorage.deleteFile(file.id);
|
await fileStorage.deleteFile(fileId as FileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the file list to get updated data
|
// Refresh the file list to get updated data
|
||||||
@ -348,7 +399,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete selected files:', error);
|
console.error('Failed to delete selected files:', error);
|
||||||
}
|
}
|
||||||
}, [selectedFileIds, filteredFiles, refreshRecentFiles]);
|
}, [selectedFileIds, filteredFiles, refreshRecentFiles, getSafeFilesToDelete]);
|
||||||
|
|
||||||
|
|
||||||
const handleDownloadSelected = useCallback(async () => {
|
const handleDownloadSelected = useCallback(async () => {
|
||||||
@ -369,7 +420,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
}
|
}
|
||||||
}, [selectedFileIds, filteredFiles]);
|
}, [selectedFileIds, filteredFiles]);
|
||||||
|
|
||||||
const handleDownloadSingle = useCallback(async (file: FileMetadata) => {
|
const handleDownloadSingle = useCallback(async (file: StoredFileMetadata) => {
|
||||||
try {
|
try {
|
||||||
await downloadFiles([file]);
|
await downloadFiles([file]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -394,107 +445,55 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
// Load complete history chain if expanding
|
// Load complete history chain if expanding
|
||||||
if (!isCurrentlyExpanded) {
|
if (!isCurrentlyExpanded) {
|
||||||
const currentFileMetadata = recentFiles.find(f => f.id === fileId);
|
const currentFileMetadata = recentFiles.find(f => f.id === fileId);
|
||||||
if (currentFileMetadata && (currentFileMetadata.versionNumber || 0) > 0) {
|
if (currentFileMetadata && (currentFileMetadata.versionNumber || 1) > 1) {
|
||||||
try {
|
try {
|
||||||
// Load the current file to get its full history
|
// Get all stored file metadata for chain traversal
|
||||||
const storedFile = await fileStorage.getFile(fileId as 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 as 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 allStoredMetadata = await fileStorage.getAllFileMetadata();
|
||||||
const fileMap = new Map(allStoredMetadata.map(f => [f.id, f]));
|
const fileMap = new Map(allStoredMetadata.map(f => [f.id, f]));
|
||||||
|
|
||||||
// Build complete chain by following parent relationships backwards
|
// Get the current file's IndexedDB data
|
||||||
const visitedIds = new Set([fileId]); // Don't include the current file
|
const currentStoredFile = fileMap.get(fileId as FileId);
|
||||||
const toProcess = [historyData]; // Start with current file's history data
|
if (!currentStoredFile) {
|
||||||
|
console.warn(`No stored file found for ${fileId}`);
|
||||||
while (toProcess.length > 0) {
|
return;
|
||||||
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 as FileId);
|
|
||||||
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
|
// Build complete history chain using IndexedDB metadata
|
||||||
if (currentHistoryData.parentFileId && !visitedIds.has(currentHistoryData.parentFileId)) {
|
const historyFiles: StoredFileMetadata[] = [];
|
||||||
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
|
// Find the original file
|
||||||
const parentHistoryData = await loadFileHistory(parentFile, parentMeta.id);
|
const originalFileId = currentStoredFile.originalFileId || currentStoredFile.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)
|
// Collect all files in this history chain
|
||||||
for (const [metaId, meta] of fileMap) {
|
const chainFiles = Array.from(fileMap.values()).filter(file =>
|
||||||
if (!visitedIds.has(metaId) && (meta as any).originalFileId === historyData.originalFileId) {
|
(file.originalFileId || file.id) === originalFileId && file.id !== fileId
|
||||||
visitedIds.add(metaId);
|
);
|
||||||
try {
|
|
||||||
const siblingStoredFile = await fileStorage.getFile(meta.id);
|
// Sort by version number (oldest first for history display)
|
||||||
if (siblingStoredFile) {
|
chainFiles.sort((a, b) => (a.versionNumber || 1) - (b.versionNumber || 1));
|
||||||
const siblingFile = new File([siblingStoredFile.data], siblingStoredFile.name, {
|
|
||||||
type: siblingStoredFile.type,
|
// Convert stored files to StoredFileMetadata format with proper history info
|
||||||
lastModified: siblingStoredFile.lastModified
|
for (const storedFile of chainFiles) {
|
||||||
});
|
// Load the actual file to extract PDF metadata if available
|
||||||
const siblingMetadata = await createFileMetadataWithHistory(siblingFile, meta.id, meta.thumbnail);
|
const historyMetadata: StoredFileMetadata = {
|
||||||
historyFiles.push(siblingMetadata);
|
id: storedFile.id,
|
||||||
}
|
name: storedFile.name,
|
||||||
} catch (error) {
|
type: storedFile.type,
|
||||||
console.warn(`Failed to load sibling file ${meta.id}:`, error);
|
size: storedFile.size,
|
||||||
}
|
lastModified: storedFile.lastModified,
|
||||||
}
|
thumbnail: storedFile.thumbnail,
|
||||||
|
versionNumber: storedFile.versionNumber,
|
||||||
|
isLeaf: storedFile.isLeaf,
|
||||||
|
// Use IndexedDB data directly - it's more reliable than re-parsing PDF
|
||||||
|
originalFileId: storedFile.originalFileId,
|
||||||
|
parentFileId: storedFile.parentFileId,
|
||||||
|
toolHistory: storedFile.toolHistory
|
||||||
|
};
|
||||||
|
historyFiles.push(historyMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the loaded history files
|
// Cache the loaded history files
|
||||||
setLoadedHistoryFiles(prev => new Map(prev.set(fileId as FileId, historyFiles)));
|
setLoadedHistoryFiles(prev => new Map(prev.set(fileId as FileId, historyFiles)));
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to load history chain for file ${fileId}:`, error);
|
console.warn(`Failed to load history chain for file ${fileId}:`, error);
|
||||||
}
|
}
|
||||||
@ -507,30 +506,26 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
return newMap;
|
return newMap;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [expandedFileIds, recentFiles, loadFileHistory]);
|
}, [expandedFileIds, recentFiles]);
|
||||||
|
|
||||||
const handleAddToRecents = useCallback(async (file: FileMetadata) => {
|
const handleAddToRecents = useCallback(async (file: StoredFileMetadata) => {
|
||||||
try {
|
try {
|
||||||
console.log('Promoting to recents:', file.name, 'version:', file.versionNumber);
|
console.log('Adding to recents:', file.name, 'version:', file.versionNumber);
|
||||||
|
|
||||||
// Load the file from storage and create a copy with new ID and timestamp
|
// Load file from storage and use addStoredFiles pattern
|
||||||
const storedFile = await fileStorage.getFile(file.id);
|
const storedFile = await fileStorage.getFile(file.id);
|
||||||
if (storedFile) {
|
if (!storedFile) {
|
||||||
// Create new file with current timestamp to appear at top
|
throw new Error(`File not found in storage: ${file.name}`);
|
||||||
const promotedFile = new File([storedFile.data], file.name, {
|
|
||||||
type: file.type,
|
|
||||||
lastModified: Date.now() // Current timestamp makes it appear at top
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add as new file through the normal flow (creates new ID)
|
|
||||||
onNewFilesSelect([promotedFile]);
|
|
||||||
|
|
||||||
console.log('Successfully promoted to recents:', file.name, 'v' + file.versionNumber);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use direct StoredFile approach - much more efficient
|
||||||
|
onStoredFilesSelect([storedFile]);
|
||||||
|
|
||||||
|
console.log('Successfully added to recents:', file.name, 'v' + file.versionNumber);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to promote to recents:', error);
|
console.error('Failed to add to recents:', error);
|
||||||
}
|
}
|
||||||
}, [onNewFilesSelect]);
|
}, [onStoredFilesSelect]);
|
||||||
|
|
||||||
// Cleanup blob URLs when component unmounts
|
// Cleanup blob URLs when component unmounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -565,10 +560,6 @@ 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,
|
||||||
@ -599,8 +590,6 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
fileInputRef,
|
fileInputRef,
|
||||||
expandedFileIds,
|
expandedFileIds,
|
||||||
fileGroups,
|
fileGroups,
|
||||||
isLoadingHistory,
|
|
||||||
getHistoryError,
|
|
||||||
handleSourceChange,
|
handleSourceChange,
|
||||||
handleLocalFileClick,
|
handleLocalFileClick,
|
||||||
handleFileSelect,
|
handleFileSelect,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { createContext, useContext, useState, useCallback, useMemo } from 'react';
|
import React, { createContext, useContext, useState, useCallback, useMemo } from 'react';
|
||||||
import { useFileHandler } from '../hooks/useFileHandler';
|
import { useFileHandler } from '../hooks/useFileHandler';
|
||||||
import { FileMetadata } from '../types/file';
|
import { StoredFileMetadata, StoredFile } from '../services/fileStorage';
|
||||||
import { FileId } from '../types/file';
|
import { FileId } from '../types/file';
|
||||||
|
|
||||||
interface FilesModalContextType {
|
interface FilesModalContextType {
|
||||||
@ -9,7 +9,7 @@ interface FilesModalContextType {
|
|||||||
closeFilesModal: () => void;
|
closeFilesModal: () => void;
|
||||||
onFileSelect: (file: File) => void;
|
onFileSelect: (file: File) => void;
|
||||||
onFilesSelect: (files: File[]) => void;
|
onFilesSelect: (files: File[]) => void;
|
||||||
onStoredFilesSelect: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => void;
|
onStoredFilesSelect: (storedFiles: StoredFile[]) => void;
|
||||||
onModalClose?: () => void;
|
onModalClose?: () => void;
|
||||||
setOnModalClose: (callback: () => void) => void;
|
setOnModalClose: (callback: () => void) => void;
|
||||||
}
|
}
|
||||||
@ -58,14 +58,14 @@ export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||||||
closeFilesModal();
|
closeFilesModal();
|
||||||
}, [addMultipleFiles, closeFilesModal, insertAfterPage, customHandler]);
|
}, [addMultipleFiles, closeFilesModal, insertAfterPage, customHandler]);
|
||||||
|
|
||||||
const handleStoredFilesSelect = useCallback((filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => {
|
const handleStoredFilesSelect = useCallback((storedFiles: StoredFile[]) => {
|
||||||
if (customHandler) {
|
if (customHandler) {
|
||||||
// Use custom handler for special cases (like page insertion)
|
// Use custom handler for special cases (like page insertion)
|
||||||
const files = filesWithMetadata.map(item => item.file);
|
const files = storedFiles.map(storedFile => new File([storedFile.data], storedFile.name, { type: storedFile.type, lastModified: storedFile.lastModified }));
|
||||||
customHandler(files, insertAfterPage);
|
customHandler(files, insertAfterPage);
|
||||||
} else {
|
} else {
|
||||||
// Use normal file handling
|
// Use normal file handling
|
||||||
addStoredFiles(filesWithMetadata);
|
addStoredFiles(storedFiles);
|
||||||
}
|
}
|
||||||
closeFilesModal();
|
closeFilesModal();
|
||||||
}, [addStoredFiles, closeFilesModal, insertAfterPage, customHandler]);
|
}, [addStoredFiles, closeFilesModal, insertAfterPage, customHandler]);
|
||||||
|
@ -8,20 +8,19 @@ import React, { createContext, useContext, useCallback, useRef } from 'react';
|
|||||||
const DEBUG = process.env.NODE_ENV === 'development';
|
const DEBUG = process.env.NODE_ENV === 'development';
|
||||||
import { fileStorage } from '../services/fileStorage';
|
import { fileStorage } from '../services/fileStorage';
|
||||||
import { FileId } from '../types/file';
|
import { FileId } from '../types/file';
|
||||||
import { FileMetadata } from '../types/file';
|
import { StoredFileMetadata } from '../services/fileStorage';
|
||||||
import { generateThumbnailForFile } from '../utils/thumbnailUtils';
|
import { generateThumbnailForFile } from '../utils/thumbnailUtils';
|
||||||
import { createFileMetadataWithHistory } from '../utils/fileHistoryUtils';
|
|
||||||
|
|
||||||
interface IndexedDBContextValue {
|
interface IndexedDBContextValue {
|
||||||
// Core CRUD operations
|
// Core CRUD operations
|
||||||
saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<FileMetadata>;
|
saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<StoredFileMetadata>;
|
||||||
loadFile: (fileId: FileId) => Promise<File | null>;
|
loadFile: (fileId: FileId) => Promise<File | null>;
|
||||||
loadMetadata: (fileId: FileId) => Promise<FileMetadata | null>;
|
loadMetadata: (fileId: FileId) => Promise<StoredFileMetadata | null>;
|
||||||
deleteFile: (fileId: FileId) => Promise<void>;
|
deleteFile: (fileId: FileId) => Promise<void>;
|
||||||
|
|
||||||
// Batch operations
|
// Batch operations
|
||||||
loadAllMetadata: () => Promise<FileMetadata[]>;
|
loadAllMetadata: () => Promise<StoredFileMetadata[]>;
|
||||||
loadLeafMetadata: () => Promise<FileMetadata[]>; // Only leaf files for recent files list
|
loadLeafMetadata: () => Promise<StoredFileMetadata[]>; // Only leaf files for recent files list
|
||||||
deleteMultiple: (fileIds: FileId[]) => Promise<void>;
|
deleteMultiple: (fileIds: FileId[]) => Promise<void>;
|
||||||
clearAll: () => Promise<void>;
|
clearAll: () => Promise<void>;
|
||||||
|
|
||||||
@ -59,20 +58,49 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
|
|||||||
if (DEBUG) console.log(`🗂️ Evicted ${toRemove.length} LRU cache entries`);
|
if (DEBUG) console.log(`🗂️ Evicted ${toRemove.length} LRU cache entries`);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const saveFile = useCallback(async (file: File, fileId: FileId, existingThumbnail?: string): Promise<FileMetadata> => {
|
const saveFile = useCallback(async (file: File, fileId: FileId, existingThumbnail?: string): Promise<StoredFileMetadata> => {
|
||||||
// Use existing thumbnail or generate new one if none provided
|
// Use existing thumbnail or generate new one if none provided
|
||||||
const thumbnail = existingThumbnail || await generateThumbnailForFile(file);
|
const thumbnail = existingThumbnail || await generateThumbnailForFile(file);
|
||||||
|
|
||||||
// Store in IndexedDB
|
// Extract history data if attached to the file by tool operations
|
||||||
await fileStorage.storeFile(file, fileId, thumbnail);
|
const historyData = (file as any).__historyData as {
|
||||||
|
versionNumber: number;
|
||||||
|
originalFileId: string;
|
||||||
|
parentFileId: FileId | undefined;
|
||||||
|
toolHistory: Array<{ toolName: string; timestamp: number; }>;
|
||||||
|
} | undefined;
|
||||||
|
|
||||||
|
if (historyData) {
|
||||||
|
console.log('🏛️ INDEXEDDB CONTEXT - Found history data on file:', {
|
||||||
|
fileName: file.name,
|
||||||
|
versionNumber: historyData.versionNumber,
|
||||||
|
originalFileId: historyData.originalFileId,
|
||||||
|
parentFileId: historyData.parentFileId,
|
||||||
|
toolChainLength: historyData.toolHistory.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in IndexedDB with history data
|
||||||
|
const storedFile = await fileStorage.storeFile(file, fileId, thumbnail, true, historyData);
|
||||||
|
|
||||||
// Cache the file object for immediate reuse
|
// Cache the file object for immediate reuse
|
||||||
fileCache.current.set(fileId, { file, lastAccessed: Date.now() });
|
fileCache.current.set(fileId, { file, lastAccessed: Date.now() });
|
||||||
evictLRUEntries();
|
evictLRUEntries();
|
||||||
|
|
||||||
// Extract history metadata for PDFs and return enhanced metadata
|
// Return metadata with history information from the stored file
|
||||||
const metadata = await createFileMetadataWithHistory(file, fileId, thumbnail);
|
const metadata: StoredFileMetadata = {
|
||||||
|
id: fileId,
|
||||||
|
name: file.name,
|
||||||
|
type: file.type,
|
||||||
|
size: file.size,
|
||||||
|
lastModified: file.lastModified,
|
||||||
|
thumbnail,
|
||||||
|
isLeaf: true,
|
||||||
|
versionNumber: storedFile.versionNumber,
|
||||||
|
originalFileId: storedFile.originalFileId,
|
||||||
|
parentFileId: storedFile.parentFileId || undefined,
|
||||||
|
toolHistory: storedFile.toolHistory
|
||||||
|
};
|
||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
}, []);
|
}, []);
|
||||||
@ -103,7 +131,7 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
|
|||||||
return file;
|
return file;
|
||||||
}, [evictLRUEntries]);
|
}, [evictLRUEntries]);
|
||||||
|
|
||||||
const loadMetadata = useCallback(async (fileId: FileId): Promise<FileMetadata | null> => {
|
const loadMetadata = useCallback(async (fileId: FileId): Promise<StoredFileMetadata | null> => {
|
||||||
// Try to get from cache first (no IndexedDB hit)
|
// Try to get from cache first (no IndexedDB hit)
|
||||||
const cached = fileCache.current.get(fileId);
|
const cached = fileCache.current.get(fileId);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
@ -141,7 +169,7 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
|
|||||||
await fileStorage.deleteFile(fileId);
|
await fileStorage.deleteFile(fileId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadLeafMetadata = useCallback(async (): Promise<FileMetadata[]> => {
|
const loadLeafMetadata = useCallback(async (): Promise<StoredFileMetadata[]> => {
|
||||||
const metadata = await fileStorage.getLeafFileMetadata(); // Only get leaf files
|
const metadata = await fileStorage.getLeafFileMetadata(); // Only get leaf files
|
||||||
|
|
||||||
// Separate PDF and non-PDF files for different processing
|
// Separate PDF and non-PDF files for different processing
|
||||||
@ -149,7 +177,7 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
|
|||||||
const nonPdfFiles = 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)
|
// Process non-PDF files immediately (no history extraction needed)
|
||||||
const nonPdfMetadata: FileMetadata[] = nonPdfFiles.map(m => ({
|
const nonPdfMetadata: StoredFileMetadata[] = nonPdfFiles.map(m => ({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
name: m.name,
|
name: m.name,
|
||||||
type: m.type,
|
type: m.type,
|
||||||
@ -161,24 +189,35 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
|
|||||||
|
|
||||||
// Process PDF files with controlled concurrency to avoid memory issues
|
// 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 BATCH_SIZE = 5; // Process 5 PDFs at a time to avoid overwhelming memory
|
||||||
const pdfMetadata: FileMetadata[] = [];
|
const pdfMetadata: StoredFileMetadata[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < pdfFiles.length; i += BATCH_SIZE) {
|
for (let i = 0; i < pdfFiles.length; i += BATCH_SIZE) {
|
||||||
const batch = pdfFiles.slice(i, i + BATCH_SIZE);
|
const batch = pdfFiles.slice(i, i + BATCH_SIZE);
|
||||||
|
|
||||||
const batchResults = await Promise.all(batch.map(async (m) => {
|
const batchResults = await Promise.all(batch.map(async (m) => {
|
||||||
try {
|
try {
|
||||||
// For PDF files, load and extract basic history for display only
|
// For PDF files, use history data from IndexedDB instead of extracting from PDF
|
||||||
const storedFile = await fileStorage.getFile(m.id);
|
const storedFile = await fileStorage.getFile(m.id);
|
||||||
if (storedFile?.data) {
|
if (storedFile) {
|
||||||
const file = new File([storedFile.data], m.name, {
|
return {
|
||||||
|
id: m.id,
|
||||||
|
name: m.name,
|
||||||
type: m.type,
|
type: m.type,
|
||||||
lastModified: m.lastModified
|
size: m.size,
|
||||||
});
|
lastModified: m.lastModified,
|
||||||
return await createFileMetadataWithHistory(file, m.id, m.thumbnail);
|
thumbnail: m.thumbnail,
|
||||||
|
isLeaf: m.isLeaf,
|
||||||
|
versionNumber: storedFile.versionNumber,
|
||||||
|
historyInfo: {
|
||||||
|
originalFileId: storedFile.originalFileId,
|
||||||
|
parentFileId: storedFile.parentFileId || undefined,
|
||||||
|
versionNumber: storedFile.versionNumber,
|
||||||
|
toolChain: storedFile.toolHistory
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (DEBUG) console.warn('🗂️ Failed to extract basic metadata from leaf file:', m.name, error);
|
if (DEBUG) console.warn('🗂️ Failed to load stored file data for leaf file:', m.name, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to basic metadata without history
|
// Fallback to basic metadata without history
|
||||||
@ -199,7 +238,7 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
|
|||||||
return [...nonPdfMetadata, ...pdfMetadata];
|
return [...nonPdfMetadata, ...pdfMetadata];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadAllMetadata = useCallback(async (): Promise<FileMetadata[]> => {
|
const loadAllMetadata = useCallback(async (): Promise<StoredFileMetadata[]> => {
|
||||||
const metadata = await fileStorage.getAllFileMetadata();
|
const metadata = await fileStorage.getAllFileMetadata();
|
||||||
|
|
||||||
// Separate PDF and non-PDF files for different processing
|
// Separate PDF and non-PDF files for different processing
|
||||||
@ -207,7 +246,7 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
|
|||||||
const nonPdfFiles = 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)
|
// Process non-PDF files immediately (no history extraction needed)
|
||||||
const nonPdfMetadata: FileMetadata[] = nonPdfFiles.map(m => ({
|
const nonPdfMetadata: StoredFileMetadata[] = nonPdfFiles.map(m => ({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
name: m.name,
|
name: m.name,
|
||||||
type: m.type,
|
type: m.type,
|
||||||
@ -218,27 +257,37 @@ export function IndexedDBProvider({ children }: IndexedDBProviderProps) {
|
|||||||
|
|
||||||
// Process PDF files with controlled concurrency to avoid memory issues
|
// 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 BATCH_SIZE = 5; // Process 5 PDFs at a time to avoid overwhelming memory
|
||||||
const pdfMetadata: FileMetadata[] = [];
|
const pdfMetadata: StoredFileMetadata[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < pdfFiles.length; i += BATCH_SIZE) {
|
for (let i = 0; i < pdfFiles.length; i += BATCH_SIZE) {
|
||||||
const batch = pdfFiles.slice(i, i + BATCH_SIZE);
|
const batch = pdfFiles.slice(i, i + BATCH_SIZE);
|
||||||
|
|
||||||
const batchResults = await Promise.all(batch.map(async (m) => {
|
const batchResults = await Promise.all(batch.map(async (m) => {
|
||||||
try {
|
try {
|
||||||
// For PDF files, load and extract history with timeout
|
// For PDF files, use history data from IndexedDB instead of extracting from PDF
|
||||||
const storedFile = await fileStorage.getFile(m.id);
|
const storedFile = await fileStorage.getFile(m.id);
|
||||||
if (storedFile?.data) {
|
if (storedFile) {
|
||||||
const file = new File([storedFile.data], m.name, {
|
return {
|
||||||
|
id: m.id,
|
||||||
|
name: m.name,
|
||||||
type: m.type,
|
type: m.type,
|
||||||
lastModified: m.lastModified
|
size: m.size,
|
||||||
});
|
lastModified: m.lastModified,
|
||||||
return await createFileMetadataWithHistory(file, m.id, m.thumbnail);
|
thumbnail: m.thumbnail,
|
||||||
|
versionNumber: storedFile.versionNumber,
|
||||||
|
historyInfo: {
|
||||||
|
originalFileId: storedFile.originalFileId,
|
||||||
|
parentFileId: storedFile.parentFileId || undefined,
|
||||||
|
versionNumber: storedFile.versionNumber,
|
||||||
|
toolChain: storedFile.toolHistory
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (DEBUG) console.warn('🗂️ Failed to extract history from stored file:', m.name, error);
|
if (DEBUG) console.warn('🗂️ Failed to load stored file data for metadata:', m.name, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to basic metadata if history extraction fails
|
// Fallback to basic metadata if history loading fails
|
||||||
return {
|
return {
|
||||||
id: m.id,
|
id: m.id,
|
||||||
name: m.name,
|
name: m.name,
|
||||||
|
@ -10,11 +10,11 @@ import {
|
|||||||
createFileId,
|
createFileId,
|
||||||
createQuickKey
|
createQuickKey
|
||||||
} from '../../types/fileContext';
|
} from '../../types/fileContext';
|
||||||
import { FileId, FileMetadata } from '../../types/file';
|
import { FileId } from '../../types/file';
|
||||||
|
import { StoredFileMetadata } from '../../services/fileStorage';
|
||||||
import { generateThumbnailWithMetadata } from '../../utils/thumbnailUtils';
|
import { generateThumbnailWithMetadata } from '../../utils/thumbnailUtils';
|
||||||
import { FileLifecycleManager } from './lifecycle';
|
import { FileLifecycleManager } from './lifecycle';
|
||||||
import { buildQuickKeySet } from './fileSelectors';
|
import { buildQuickKeySet } from './fileSelectors';
|
||||||
import { extractBasicFileMetadata } from '../../utils/fileHistoryUtils';
|
|
||||||
|
|
||||||
const DEBUG = process.env.NODE_ENV === 'development';
|
const DEBUG = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ interface AddFileOptions {
|
|||||||
filesWithThumbnails?: Array<{ file: File; thumbnail?: string; pageCount?: number }>;
|
filesWithThumbnails?: Array<{ file: File; thumbnail?: string; pageCount?: number }>;
|
||||||
|
|
||||||
// For 'stored' files
|
// For 'stored' files
|
||||||
filesWithMetadata?: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>;
|
filesWithMetadata?: Array<{ file: File; originalId: FileId; metadata: StoredFileMetadata }>;
|
||||||
|
|
||||||
// Insertion position
|
// Insertion position
|
||||||
insertAfterPageId?: string;
|
insertAfterPageId?: string;
|
||||||
@ -183,24 +183,7 @@ 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 basic metadata (version number and tool chain) for display
|
// History metadata is now managed in IndexedDB, not in PDF metadata
|
||||||
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: {
|
|
||||||
versionNumber: updatedRecord.versionNumber,
|
|
||||||
toolHistory: updatedRecord.toolHistory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
if (DEBUG) console.warn(`📄 Failed to extract basic metadata for ${file.name}:`, error);
|
|
||||||
});
|
|
||||||
|
|
||||||
existingQuickKeys.add(quickKey);
|
existingQuickKeys.add(quickKey);
|
||||||
stirlingFileStubs.push(record);
|
stirlingFileStubs.push(record);
|
||||||
@ -244,24 +227,7 @@ 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 basic metadata (version number and tool chain) for display
|
// History metadata is now managed in IndexedDB, not in PDF metadata
|
||||||
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: {
|
|
||||||
versionNumber: updatedRecord.versionNumber,
|
|
||||||
toolHistory: updatedRecord.toolHistory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
if (DEBUG) console.warn(`📄 Failed to extract basic metadata for ${file.name}:`, error);
|
|
||||||
});
|
|
||||||
|
|
||||||
existingQuickKeys.add(quickKey);
|
existingQuickKeys.add(quickKey);
|
||||||
stirlingFileStubs.push(record);
|
stirlingFileStubs.push(record);
|
||||||
@ -338,24 +304,20 @@ 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 basic metadata (version number and tool chain) for display
|
// Use history data from IndexedDB instead of extracting from PDF metadata
|
||||||
extractBasicFileMetadata(file, record).then(updatedRecord => {
|
if (metadata.versionNumber || metadata.toolHistory) {
|
||||||
if (updatedRecord !== record && (updatedRecord.versionNumber || updatedRecord.toolHistory)) {
|
record.versionNumber = metadata.versionNumber;
|
||||||
// Basic metadata found, dispatch update to trigger re-render
|
record.originalFileId = metadata.originalFileId;
|
||||||
dispatch({
|
record.parentFileId = metadata.parentFileId;
|
||||||
type: 'UPDATE_FILE_RECORD',
|
record.toolHistory = metadata.toolHistory;
|
||||||
payload: {
|
|
||||||
id: fileId,
|
if (DEBUG) console.log(`📄 addFiles(stored): Applied IndexedDB history data to ${file.name}:`, {
|
||||||
updates: {
|
versionNumber: record.versionNumber,
|
||||||
versionNumber: updatedRecord.versionNumber,
|
originalFileId: record.originalFileId,
|
||||||
toolHistory: updatedRecord.toolHistory
|
parentFileId: record.parentFileId,
|
||||||
}
|
toolHistoryLength: record.toolHistory?.length || 0
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
|
||||||
if (DEBUG) console.warn(`📄 Failed to extract basic metadata for ${file.name}:`, error);
|
|
||||||
});
|
|
||||||
|
|
||||||
existingQuickKeys.add(quickKey);
|
existingQuickKeys.add(quickKey);
|
||||||
stirlingFileStubs.push(record);
|
stirlingFileStubs.push(record);
|
||||||
@ -413,43 +375,13 @@ async function processFilesIntoRecords(
|
|||||||
record.processedFile = createProcessedFile(pageCount, thumbnail);
|
record.processedFile = createProcessedFile(pageCount, thumbnail);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract basic metadata synchronously during consumeFiles for immediate display
|
// History metadata is now managed in IndexedDB, not in PDF metadata
|
||||||
if (file.type.includes('pdf')) {
|
|
||||||
try {
|
|
||||||
const updatedRecord = await extractBasicFileMetadata(file, record);
|
|
||||||
|
|
||||||
if (updatedRecord !== record && (updatedRecord.versionNumber || updatedRecord.toolHistory)) {
|
|
||||||
// Update the record directly with basic metadata
|
|
||||||
Object.assign(record, {
|
|
||||||
versionNumber: updatedRecord.versionNumber,
|
|
||||||
toolHistory: updatedRecord.toolHistory
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (DEBUG) console.warn(`📄 Failed to extract basic metadata for ${file.name}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { record, file, fileId, thumbnail };
|
return { record, file, fileId, thumbnail };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to persist files to IndexedDB
|
|
||||||
*/
|
|
||||||
async function persistFilesToIndexedDB(
|
|
||||||
stirlingFileStubs: Array<{ file: File; fileId: FileId; thumbnail?: string }>,
|
|
||||||
indexedDB: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise<any> }
|
|
||||||
): Promise<void> {
|
|
||||||
await Promise.all(stirlingFileStubs.map(async ({ file, fileId, thumbnail }) => {
|
|
||||||
try {
|
|
||||||
await indexedDB.saveFile(file, fileId, thumbnail);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to persist file to IndexedDB:', file.name, error);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consume files helper - replace unpinned input files with output files
|
* Consume files helper - replace unpinned input files with output files
|
||||||
@ -480,11 +412,31 @@ export async function consumeFiles(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Save output files to IndexedDB
|
// Save output files to IndexedDB and update the StirlingFileStub records with version info
|
||||||
await persistFilesToIndexedDB(outputStirlingFileStubs, indexedDB);
|
await Promise.all(outputStirlingFileStubs.map(async ({ file, fileId, record }) => {
|
||||||
|
try {
|
||||||
|
const metadata = await indexedDB.saveFile(file, fileId, record.thumbnailUrl);
|
||||||
|
|
||||||
|
// Update the record directly with version information from IndexedDB
|
||||||
|
if (metadata.versionNumber || metadata.historyInfo) {
|
||||||
|
record.versionNumber = metadata.versionNumber;
|
||||||
|
record.originalFileId = metadata.historyInfo?.originalFileId;
|
||||||
|
record.parentFileId = metadata.historyInfo?.parentFileId;
|
||||||
|
record.toolHistory = metadata.historyInfo?.toolChain;
|
||||||
|
|
||||||
|
if (DEBUG) console.log(`📄 Updated output record for ${file.name} with IndexedDB history data:`, {
|
||||||
|
versionNumber: metadata.versionNumber,
|
||||||
|
originalFileId: metadata.historyInfo?.originalFileId,
|
||||||
|
toolChainLength: metadata.historyInfo?.toolChain?.length || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to persist output file to IndexedDB:', file.name, error);
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch the consume action
|
// Dispatch the consume action with updated records
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'CONSUME_FILES',
|
type: 'CONSUME_FILES',
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -8,7 +8,6 @@ import { useToolResources } from './useToolResources';
|
|||||||
import { extractErrorMessage } from '../../../utils/toolErrorHandler';
|
import { extractErrorMessage } from '../../../utils/toolErrorHandler';
|
||||||
import { StirlingFile, extractFiles, FileId, StirlingFileStub } from '../../../types/fileContext';
|
import { StirlingFile, extractFiles, FileId, StirlingFileStub } from '../../../types/fileContext';
|
||||||
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
|
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
|
||||||
import { prepareStirlingFilesWithHistory, verifyToolMetadataPreservation } from '../../../utils/fileHistoryUtils';
|
|
||||||
|
|
||||||
// Re-export for backwards compatibility
|
// Re-export for backwards compatibility
|
||||||
export type { ProcessingProgress, ResponseHandler };
|
export type { ProcessingProgress, ResponseHandler };
|
||||||
@ -129,7 +128,7 @@ export const useToolOperation = <TParams>(
|
|||||||
config: ToolOperationConfig<TParams>
|
config: ToolOperationConfig<TParams>
|
||||||
): ToolOperationHook<TParams> => {
|
): ToolOperationHook<TParams> => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { addFiles, consumeFiles, undoConsumeFiles, selectors, findFileId } = useFileContext();
|
const { addFiles, consumeFiles, undoConsumeFiles, selectors } = useFileContext();
|
||||||
|
|
||||||
// Composed hooks
|
// Composed hooks
|
||||||
const { state, actions } = useToolState();
|
const { state, actions } = useToolState();
|
||||||
@ -166,24 +165,13 @@ export const useToolOperation = <TParams>(
|
|||||||
cleanupBlobUrls();
|
cleanupBlobUrls();
|
||||||
|
|
||||||
// Prepare files with history metadata injection (for PDFs)
|
// Prepare files with history metadata injection (for PDFs)
|
||||||
actions.setStatus('Preparing files...');
|
actions.setStatus('Processing files...');
|
||||||
const getFileStubById = (fileId: FileId) => {
|
|
||||||
return selectors.getStirlingFileStub(fileId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filesWithHistory = await prepareStirlingFilesWithHistory(
|
|
||||||
validFiles,
|
|
||||||
getFileStubById,
|
|
||||||
config.operationType,
|
|
||||||
params as Record<string, any>
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let processedFiles: File[];
|
let processedFiles: File[];
|
||||||
|
|
||||||
// Convert StirlingFiles with history to regular Files for API processing
|
// Use original files directly (no PDF metadata injection - history stored in IndexedDB)
|
||||||
// The history is already injected into the File data, we just need to extract the File objects
|
const filesForAPI = extractFiles(validFiles);
|
||||||
const filesForAPI = extractFiles(filesWithHistory);
|
|
||||||
|
|
||||||
switch (config.toolType) {
|
switch (config.toolType) {
|
||||||
case ToolType.singleFile: {
|
case ToolType.singleFile: {
|
||||||
@ -242,8 +230,6 @@ export const useToolOperation = <TParams>(
|
|||||||
if (processedFiles.length > 0) {
|
if (processedFiles.length > 0) {
|
||||||
actions.setFiles(processedFiles);
|
actions.setFiles(processedFiles);
|
||||||
|
|
||||||
// Verify metadata preservation for backend quality tracking
|
|
||||||
await verifyToolMetadataPreservation(validFiles, processedFiles, config.operationType);
|
|
||||||
|
|
||||||
// Generate thumbnails and download URL concurrently
|
// Generate thumbnails and download URL concurrently
|
||||||
actions.setGeneratingThumbnails(true);
|
actions.setGeneratingThumbnails(true);
|
||||||
@ -272,7 +258,46 @@ export const useToolOperation = <TParams>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputFileIds = await consumeFiles(inputFileIds, processedFiles);
|
// Prepare output files with history data before saving
|
||||||
|
const processedFilesWithHistory = processedFiles.map(file => {
|
||||||
|
// Find the corresponding input file for history chain
|
||||||
|
const inputStub = inputStirlingFileStubs.find(stub =>
|
||||||
|
inputFileIds.includes(stub.id)
|
||||||
|
) || inputStirlingFileStubs[0]; // Fallback to first input if not found
|
||||||
|
|
||||||
|
// Create new tool operation
|
||||||
|
const newToolOperation = {
|
||||||
|
toolName: config.operationType,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build complete tool chain
|
||||||
|
const existingToolChain = inputStub?.toolHistory || [];
|
||||||
|
const toolHistory = [...existingToolChain, newToolOperation];
|
||||||
|
|
||||||
|
// Calculate version number
|
||||||
|
const versionNumber = inputStub?.versionNumber ? inputStub.versionNumber + 1 : 1;
|
||||||
|
|
||||||
|
// Attach history data to file
|
||||||
|
(file as any).__historyData = {
|
||||||
|
versionNumber,
|
||||||
|
originalFileId: inputStub?.originalFileId || inputStub?.id,
|
||||||
|
parentFileId: inputStub?.id || null,
|
||||||
|
toolHistory
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🏛️ FILE HISTORY - Prepared file with history:', {
|
||||||
|
fileName: file.name,
|
||||||
|
versionNumber,
|
||||||
|
originalFileId: inputStub?.originalFileId || inputStub?.id,
|
||||||
|
parentFileId: inputStub?.id,
|
||||||
|
toolChainLength: toolHistory.length
|
||||||
|
});
|
||||||
|
|
||||||
|
return file;
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputFileIds = await consumeFiles(inputFileIds, processedFilesWithHistory);
|
||||||
|
|
||||||
// Store operation data for undo (only store what we need to avoid memory bloat)
|
// Store operation data for undo (only store what we need to avoid memory bloat)
|
||||||
lastOperationRef.current = {
|
lastOperationRef.current = {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useFileState, useFileActions } from '../contexts/FileContext';
|
import { useFileState, useFileActions } from '../contexts/FileContext';
|
||||||
import { FileMetadata } from '../types/file';
|
import { StoredFileMetadata, StoredFile } from '../services/fileStorage';
|
||||||
import { FileId } from '../types/file';
|
import { FileId } from '../types/file';
|
||||||
|
|
||||||
export const useFileHandler = () => {
|
export const useFileHandler = () => {
|
||||||
@ -18,17 +18,17 @@ export const useFileHandler = () => {
|
|||||||
}, [actions.addFiles]);
|
}, [actions.addFiles]);
|
||||||
|
|
||||||
// Add stored files preserving their original IDs to prevent session duplicates
|
// Add stored files preserving their original IDs to prevent session duplicates
|
||||||
const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => {
|
const addStoredFiles = useCallback(async (storedFiles: StoredFile[]) => {
|
||||||
// Filter out files that already exist with the same ID (exact match)
|
// Filter out files that already exist with the same ID (exact match)
|
||||||
const newFiles = filesWithMetadata.filter(({ originalId }) => {
|
const newFiles = storedFiles.filter(({ id }) => {
|
||||||
return state.files.byId[originalId] === undefined;
|
return state.files.byId[id] === undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newFiles.length > 0) {
|
if (newFiles.length > 0) {
|
||||||
await actions.addStoredFiles(newFiles, { selectFiles: true });
|
await actions.addStoredFiles(newFiles, { selectFiles: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📁 Added ${newFiles.length} stored files (${filesWithMetadata.length - newFiles.length} skipped as duplicates)`);
|
console.log(`📁 Added ${newFiles.length} stored files (${storedFiles.length - newFiles.length} skipped as duplicates)`);
|
||||||
}, [state.files.byId, actions.addStoredFiles]);
|
}, [state.files.byId, actions.addStoredFiles]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { FileId } from '../types/file';
|
import { FileId } from '../types/file';
|
||||||
import { StirlingFileStub } from '../types/fileContext';
|
import { StirlingFileStub } from '../types/fileContext';
|
||||||
import { loadFileHistoryOnDemand } from '../utils/fileHistoryUtils';
|
// loadFileHistoryOnDemand removed - history now comes from IndexedDB directly
|
||||||
|
|
||||||
interface FileHistoryState {
|
interface FileHistoryState {
|
||||||
originalFileId?: string;
|
originalFileId?: string;
|
||||||
@ -33,16 +33,17 @@ export function useFileHistory(): UseFileHistoryResult {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadHistory = useCallback(async (
|
const loadHistory = useCallback(async (
|
||||||
file: File,
|
_file: File,
|
||||||
fileId: FileId,
|
_fileId: FileId,
|
||||||
updateFileStub?: (id: FileId, updates: Partial<StirlingFileStub>) => void
|
_updateFileStub?: (id: FileId, updates: Partial<StirlingFileStub>) => void
|
||||||
) => {
|
) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const history = await loadFileHistoryOnDemand(file, fileId, updateFileStub);
|
// History is now loaded from IndexedDB, not PDF metadata
|
||||||
setHistoryData(history);
|
// This function is deprecated
|
||||||
|
throw new Error('loadFileHistoryOnDemand is deprecated - use IndexedDB history directly');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Failed to load file history';
|
const errorMessage = err instanceof Error ? err.message : 'Failed to load file history';
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
@ -76,9 +77,9 @@ export function useMultiFileHistory() {
|
|||||||
const [errors, setErrors] = useState<Map<FileId, string>>(new Map());
|
const [errors, setErrors] = useState<Map<FileId, string>>(new Map());
|
||||||
|
|
||||||
const loadFileHistory = useCallback(async (
|
const loadFileHistory = useCallback(async (
|
||||||
file: File,
|
_file: File,
|
||||||
fileId: FileId,
|
fileId: FileId,
|
||||||
updateFileStub?: (id: FileId, updates: Partial<StirlingFileStub>) => void
|
_updateFileStub?: (id: FileId, updates: Partial<StirlingFileStub>) => void
|
||||||
) => {
|
) => {
|
||||||
// Don't reload if already loaded or currently loading
|
// Don't reload if already loaded or currently loading
|
||||||
if (historyCache.has(fileId) || loadingFiles.has(fileId)) {
|
if (historyCache.has(fileId) || loadingFiles.has(fileId)) {
|
||||||
@ -93,13 +94,9 @@ export function useMultiFileHistory() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const history = await loadFileHistoryOnDemand(file, fileId, updateFileStub);
|
// History is now loaded from IndexedDB, not PDF metadata
|
||||||
|
// This function is deprecated
|
||||||
if (history) {
|
throw new Error('loadFileHistoryOnDemand is deprecated - use IndexedDB history directly');
|
||||||
setHistoryCache(prev => new Map(prev).set(fileId, history));
|
|
||||||
}
|
|
||||||
|
|
||||||
return history;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Failed to load file history';
|
const errorMessage = err instanceof Error ? err.message : 'Failed to load file history';
|
||||||
setErrors(prev => new Map(prev).set(fileId, errorMessage));
|
setErrors(prev => new Map(prev).set(fileId, errorMessage));
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useIndexedDB } from '../contexts/IndexedDBContext';
|
import { useIndexedDB } from '../contexts/IndexedDBContext';
|
||||||
import { FileMetadata } from '../types/file';
|
import { StoredFileMetadata, StoredFile, fileStorage } from '../services/fileStorage';
|
||||||
import { FileId } from '../types/fileContext';
|
import { FileId } from '../types/fileContext';
|
||||||
|
|
||||||
export const useFileManager = () => {
|
export const useFileManager = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const indexedDB = useIndexedDB();
|
const indexedDB = useIndexedDB();
|
||||||
|
|
||||||
const convertToFile = useCallback(async (fileMetadata: FileMetadata): Promise<File> => {
|
const convertToFile = useCallback(async (fileMetadata: StoredFileMetadata): Promise<File> => {
|
||||||
if (!indexedDB) {
|
if (!indexedDB) {
|
||||||
throw new Error('IndexedDB context not available');
|
throw new Error('IndexedDB context not available');
|
||||||
}
|
}
|
||||||
@ -22,7 +22,7 @@ export const useFileManager = () => {
|
|||||||
throw new Error(`File not found in storage: ${fileMetadata.name} (ID: ${fileMetadata.id})`);
|
throw new Error(`File not found in storage: ${fileMetadata.name} (ID: ${fileMetadata.id})`);
|
||||||
}, [indexedDB]);
|
}, [indexedDB]);
|
||||||
|
|
||||||
const loadRecentFiles = useCallback(async (): Promise<FileMetadata[]> => {
|
const loadRecentFiles = useCallback(async (): Promise<StoredFileMetadata[]> => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
if (!indexedDB) {
|
if (!indexedDB) {
|
||||||
@ -45,7 +45,7 @@ export const useFileManager = () => {
|
|||||||
}
|
}
|
||||||
}, [indexedDB]);
|
}, [indexedDB]);
|
||||||
|
|
||||||
const handleRemoveFile = useCallback(async (index: number, files: FileMetadata[], setFiles: (files: FileMetadata[]) => void) => {
|
const handleRemoveFile = useCallback(async (index: number, files: StoredFileMetadata[], setFiles: (files: StoredFileMetadata[]) => void) => {
|
||||||
const file = files[index];
|
const file = files[index];
|
||||||
if (!file.id) {
|
if (!file.id) {
|
||||||
throw new Error('File ID is required for removal');
|
throw new Error('File ID is required for removal');
|
||||||
@ -105,23 +105,24 @@ export const useFileManager = () => {
|
|||||||
setSelectedFiles([]);
|
setSelectedFiles([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectMultipleFiles = async (files: FileMetadata[], onStoredFilesSelect: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>) => void) => {
|
const selectMultipleFiles = async (files: StoredFileMetadata[], onStoredFilesSelect: (storedFiles: StoredFile[]) => void) => {
|
||||||
if (selectedFiles.length === 0) return;
|
if (selectedFiles.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Filter by UUID and convert to File objects
|
// Filter by UUID and load full StoredFile objects directly
|
||||||
const selectedFileObjects = files.filter(f => selectedFiles.includes(f.id));
|
const selectedFileObjects = files.filter(f => selectedFiles.includes(f.id));
|
||||||
|
|
||||||
// Use stored files flow that preserves IDs
|
const storedFiles = await Promise.all(
|
||||||
const filesWithMetadata = await Promise.all(
|
selectedFileObjects.map(async (metadata) => {
|
||||||
selectedFileObjects.map(async (metadata) => ({
|
const storedFile = await fileStorage.getFile(metadata.id);
|
||||||
file: await convertToFile(metadata),
|
if (!storedFile) {
|
||||||
originalId: metadata.id,
|
throw new Error(`File not found in storage: ${metadata.name}`);
|
||||||
metadata
|
}
|
||||||
}))
|
return storedFile;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
onStoredFilesSelect(filesWithMetadata);
|
|
||||||
|
|
||||||
|
onStoredFilesSelect(storedFiles);
|
||||||
clearSelection();
|
clearSelection();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load selected files:', error);
|
console.error('Failed to load selected files:', error);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { FileMetadata } from "../types/file";
|
import { StoredFileMetadata } from "../services/fileStorage";
|
||||||
import { useIndexedDB } from "../contexts/IndexedDBContext";
|
import { useIndexedDB } from "../contexts/IndexedDBContext";
|
||||||
import { generateThumbnailForFile } from "../utils/thumbnailUtils";
|
import { generateThumbnailForFile } from "../utils/thumbnailUtils";
|
||||||
import { FileId } from "../types/fileContext";
|
import { FileId } from "../types/fileContext";
|
||||||
@ -9,7 +9,7 @@ import { FileId } from "../types/fileContext";
|
|||||||
* Hook for IndexedDB-aware thumbnail loading
|
* Hook for IndexedDB-aware thumbnail loading
|
||||||
* Handles thumbnail generation for files not in IndexedDB
|
* Handles thumbnail generation for files not in IndexedDB
|
||||||
*/
|
*/
|
||||||
export function useIndexedDBThumbnail(file: FileMetadata | undefined | null): {
|
export function useIndexedDBThumbnail(file: StoredFileMetadata | undefined | null): {
|
||||||
thumbnail: string | null;
|
thumbnail: string | null;
|
||||||
isGenerating: boolean
|
isGenerating: boolean
|
||||||
} {
|
} {
|
||||||
|
@ -4,21 +4,21 @@
|
|||||||
* Now uses centralized IndexedDB manager
|
* Now uses centralized IndexedDB manager
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FileId } from '../types/file';
|
import { FileId, BaseFileMetadata } from '../types/file';
|
||||||
import { indexedDBManager, DATABASE_CONFIGS } from './indexedDBManager';
|
import { indexedDBManager, DATABASE_CONFIGS } from './indexedDBManager';
|
||||||
|
|
||||||
export interface StoredFile {
|
export interface StoredFile extends BaseFileMetadata {
|
||||||
id: FileId;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
size: number;
|
|
||||||
lastModified: number;
|
|
||||||
data: ArrayBuffer;
|
data: ArrayBuffer;
|
||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
url?: string; // For compatibility with existing components
|
url?: string; // For compatibility with existing components
|
||||||
isLeaf?: boolean; // True if this file is a leaf node (hasn't been processed yet)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight metadata version of StoredFile (without ArrayBuffer data)
|
||||||
|
* Used for efficient file browsing in FileManager without loading file data
|
||||||
|
*/
|
||||||
|
export type StoredFileMetadata = Omit<StoredFile, 'data'>;
|
||||||
|
|
||||||
export interface StorageStats {
|
export interface StorageStats {
|
||||||
used: number;
|
used: number;
|
||||||
available: number;
|
available: number;
|
||||||
@ -40,7 +40,21 @@ class FileStorageService {
|
|||||||
/**
|
/**
|
||||||
* Store a file in IndexedDB with external UUID
|
* Store a file in IndexedDB with external UUID
|
||||||
*/
|
*/
|
||||||
async storeFile(file: File, fileId: FileId, thumbnail?: string, isLeaf: boolean = true): Promise<StoredFile> {
|
async storeFile(
|
||||||
|
file: File,
|
||||||
|
fileId: FileId,
|
||||||
|
thumbnail?: string,
|
||||||
|
isLeaf: boolean = true,
|
||||||
|
historyData?: {
|
||||||
|
versionNumber: number;
|
||||||
|
originalFileId: string;
|
||||||
|
parentFileId: FileId | undefined;
|
||||||
|
toolHistory: Array<{
|
||||||
|
toolName: string;
|
||||||
|
timestamp: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
): Promise<StoredFile> {
|
||||||
const db = await this.getDatabase();
|
const db = await this.getDatabase();
|
||||||
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
@ -53,7 +67,13 @@ class FileStorageService {
|
|||||||
lastModified: file.lastModified,
|
lastModified: file.lastModified,
|
||||||
data: arrayBuffer,
|
data: arrayBuffer,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
isLeaf
|
isLeaf,
|
||||||
|
|
||||||
|
// History data - use provided data or defaults for original files
|
||||||
|
versionNumber: historyData?.versionNumber ?? 1,
|
||||||
|
originalFileId: historyData?.originalFileId ?? fileId,
|
||||||
|
parentFileId: historyData?.parentFileId ?? undefined,
|
||||||
|
toolHistory: historyData?.toolHistory ?? []
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -131,14 +151,14 @@ class FileStorageService {
|
|||||||
/**
|
/**
|
||||||
* Get metadata of all stored files (without loading data into memory)
|
* Get metadata of all stored files (without loading data into memory)
|
||||||
*/
|
*/
|
||||||
async getAllFileMetadata(): Promise<Omit<StoredFile, 'data'>[]> {
|
async getAllFileMetadata(): Promise<StoredFileMetadata[]> {
|
||||||
const db = await this.getDatabase();
|
const db = await this.getDatabase();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = db.transaction([this.storeName], 'readonly');
|
const transaction = db.transaction([this.storeName], 'readonly');
|
||||||
const store = transaction.objectStore(this.storeName);
|
const store = transaction.objectStore(this.storeName);
|
||||||
const request = store.openCursor();
|
const request = store.openCursor();
|
||||||
const files: Omit<StoredFile, 'data'>[] = [];
|
const files: StoredFileMetadata[] = [];
|
||||||
|
|
||||||
request.onerror = () => reject(request.error);
|
request.onerror = () => reject(request.error);
|
||||||
request.onsuccess = (event) => {
|
request.onsuccess = (event) => {
|
||||||
@ -153,7 +173,11 @@ class FileStorageService {
|
|||||||
type: storedFile.type,
|
type: storedFile.type,
|
||||||
size: storedFile.size,
|
size: storedFile.size,
|
||||||
lastModified: storedFile.lastModified,
|
lastModified: storedFile.lastModified,
|
||||||
thumbnail: storedFile.thumbnail
|
thumbnail: storedFile.thumbnail,
|
||||||
|
versionNumber: storedFile.versionNumber || 1,
|
||||||
|
originalFileId: storedFile.originalFileId || storedFile.id,
|
||||||
|
parentFileId: storedFile.parentFileId || undefined,
|
||||||
|
toolHistory: storedFile.toolHistory || []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
cursor.continue();
|
cursor.continue();
|
||||||
@ -270,14 +294,14 @@ class FileStorageService {
|
|||||||
/**
|
/**
|
||||||
* Get metadata of only leaf files (without loading data into memory)
|
* Get metadata of only leaf files (without loading data into memory)
|
||||||
*/
|
*/
|
||||||
async getLeafFileMetadata(): Promise<Omit<StoredFile, 'data'>[]> {
|
async getLeafFileMetadata(): Promise<StoredFileMetadata[]> {
|
||||||
const db = await this.getDatabase();
|
const db = await this.getDatabase();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = db.transaction([this.storeName], 'readonly');
|
const transaction = db.transaction([this.storeName], 'readonly');
|
||||||
const store = transaction.objectStore(this.storeName);
|
const store = transaction.objectStore(this.storeName);
|
||||||
const request = store.openCursor();
|
const request = store.openCursor();
|
||||||
const files: Omit<StoredFile, 'data'>[] = [];
|
const files: StoredFileMetadata[] = [];
|
||||||
|
|
||||||
request.onerror = () => reject(request.error);
|
request.onerror = () => reject(request.error);
|
||||||
request.onsuccess = (event) => {
|
request.onsuccess = (event) => {
|
||||||
@ -293,12 +317,15 @@ class FileStorageService {
|
|||||||
size: storedFile.size,
|
size: storedFile.size,
|
||||||
lastModified: storedFile.lastModified,
|
lastModified: storedFile.lastModified,
|
||||||
thumbnail: storedFile.thumbnail,
|
thumbnail: storedFile.thumbnail,
|
||||||
isLeaf: storedFile.isLeaf
|
isLeaf: storedFile.isLeaf,
|
||||||
|
versionNumber: storedFile.versionNumber || 1,
|
||||||
|
originalFileId: storedFile.originalFileId || storedFile.id,
|
||||||
|
parentFileId: storedFile.parentFileId || undefined,
|
||||||
|
toolHistory: storedFile.toolHistory || []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -534,21 +561,6 @@ class FileStorageService {
|
|||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert StoredFile to the format expected by FileContext.addStoredFiles()
|
|
||||||
* This is the recommended way to load stored files into FileContext
|
|
||||||
*/
|
|
||||||
createFileWithMetadata(storedFile: StoredFile): { file: File; originalId: FileId; metadata: { thumbnail?: string } } {
|
|
||||||
const file = this.createFileFromStored(storedFile);
|
|
||||||
|
|
||||||
return {
|
|
||||||
file,
|
|
||||||
originalId: storedFile.id,
|
|
||||||
metadata: {
|
|
||||||
thumbnail: storedFile.thumbnail
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create blob URL for stored file
|
* Create blob URL for stored file
|
||||||
|
@ -201,13 +201,16 @@ class IndexedDBManager {
|
|||||||
export const DATABASE_CONFIGS = {
|
export const DATABASE_CONFIGS = {
|
||||||
FILES: {
|
FILES: {
|
||||||
name: 'stirling-pdf-files',
|
name: 'stirling-pdf-files',
|
||||||
version: 2,
|
version: 3,
|
||||||
stores: [{
|
stores: [{
|
||||||
name: 'files',
|
name: 'files',
|
||||||
keyPath: 'id',
|
keyPath: 'id',
|
||||||
indexes: [
|
indexes: [
|
||||||
{ name: 'name', keyPath: 'name', unique: false },
|
{ name: 'name', keyPath: 'name', unique: false },
|
||||||
{ name: 'lastModified', keyPath: 'lastModified', unique: false }
|
{ name: 'lastModified', keyPath: 'lastModified', unique: false },
|
||||||
|
{ name: 'originalFileId', keyPath: 'originalFileId', unique: false },
|
||||||
|
{ name: 'parentFileId', keyPath: 'parentFileId', unique: false },
|
||||||
|
{ name: 'versionNumber', keyPath: 'versionNumber', unique: false }
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
} as DatabaseConfig,
|
} as DatabaseConfig,
|
||||||
@ -219,7 +222,8 @@ export const DATABASE_CONFIGS = {
|
|||||||
name: 'drafts',
|
name: 'drafts',
|
||||||
keyPath: 'id'
|
keyPath: 'id'
|
||||||
}]
|
}]
|
||||||
} as DatabaseConfig
|
} as DatabaseConfig,
|
||||||
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const indexedDBManager = IndexedDBManager.getInstance();
|
export const indexedDBManager = IndexedDBManager.getInstance();
|
||||||
|
@ -7,18 +7,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { PDFDocument } from 'pdf-lib';
|
||||||
import { FileId } from '../types/file';
|
|
||||||
import { ContentCache, type CacheConfig } from '../utils/ContentCache';
|
import { ContentCache, type CacheConfig } from '../utils/ContentCache';
|
||||||
|
|
||||||
const DEBUG = process.env.NODE_ENV === 'development';
|
const DEBUG = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tool operation metadata for history tracking
|
* Tool operation metadata for history tracking
|
||||||
|
* Note: Parameters removed for security - sensitive data like passwords should not be stored
|
||||||
*/
|
*/
|
||||||
export interface ToolOperation {
|
export interface ToolOperation {
|
||||||
toolName: string;
|
toolName: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
parameters?: Record<string, any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -182,7 +181,7 @@ export class PDFMetadataService {
|
|||||||
latestVersionNumber = parsed.stirlingHistory.versionNumber;
|
latestVersionNumber = parsed.stirlingHistory.versionNumber;
|
||||||
historyJson = json;
|
historyJson = json;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Silent fallback for corrupted history
|
// Silent fallback for corrupted history
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,11 @@ export type FileId = string & { readonly [tag]: 'FileId' };
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Tool operation metadata for history tracking
|
* Tool operation metadata for history tracking
|
||||||
|
* Note: Parameters removed for security - sensitive data like passwords should not be stored in history
|
||||||
*/
|
*/
|
||||||
export interface ToolOperation {
|
export interface ToolOperation {
|
||||||
toolName: string;
|
toolName: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
parameters?: Record<string, any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,31 +21,32 @@ export interface ToolOperation {
|
|||||||
*/
|
*/
|
||||||
export interface FileHistoryInfo {
|
export interface FileHistoryInfo {
|
||||||
originalFileId: string;
|
originalFileId: string;
|
||||||
parentFileId?: string;
|
parentFileId?: FileId;
|
||||||
versionNumber: number;
|
versionNumber: number;
|
||||||
toolChain: ToolOperation[];
|
toolChain: ToolOperation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File metadata for efficient operations without loading full file data
|
* Base file metadata shared between storage and runtime layers
|
||||||
* Used by IndexedDBContext and FileContext for lazy file loading
|
* Contains all common file properties and history tracking
|
||||||
*/
|
*/
|
||||||
export interface FileMetadata {
|
export interface BaseFileMetadata {
|
||||||
id: FileId;
|
id: FileId;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
size: number;
|
size: number;
|
||||||
lastModified: number;
|
lastModified: number;
|
||||||
thumbnail?: string;
|
createdAt?: number; // When file was added to system
|
||||||
isLeaf?: boolean; // True if this file is a leaf node (hasn't been processed yet)
|
|
||||||
|
|
||||||
// File history tracking (extracted from PDF metadata)
|
// File history tracking
|
||||||
historyInfo?: FileHistoryInfo;
|
isLeaf?: boolean; // True if this file hasn't been processed yet
|
||||||
|
|
||||||
// Quick access version information
|
|
||||||
originalFileId?: string; // Root file ID for grouping versions
|
originalFileId?: string; // Root file ID for grouping versions
|
||||||
versionNumber?: number; // Version number in chain
|
versionNumber?: number; // Version number in chain
|
||||||
parentFileId?: FileId; // Immediate parent file ID
|
parentFileId?: FileId; // Immediate parent file ID
|
||||||
|
toolHistory?: Array<{
|
||||||
|
toolName: string;
|
||||||
|
timestamp: number;
|
||||||
|
}>; // Tool chain for history tracking
|
||||||
|
|
||||||
// Standard PDF document metadata
|
// Standard PDF document metadata
|
||||||
pdfMetadata?: {
|
pdfMetadata?: {
|
||||||
@ -59,6 +60,10 @@ export interface FileMetadata {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileMetadata has been replaced with StoredFileMetadata from '../services/fileStorage'
|
||||||
|
// This ensures clear type relationships and eliminates duplication
|
||||||
|
|
||||||
|
|
||||||
export interface StorageConfig {
|
export interface StorageConfig {
|
||||||
useIndexedDB: boolean;
|
useIndexedDB: boolean;
|
||||||
maxFileSize: number; // Maximum size per file in bytes
|
maxFileSize: number; // Maximum size per file in bytes
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { PageOperation } from './pageEditor';
|
import { PageOperation } from './pageEditor';
|
||||||
import { FileId, FileMetadata } from './file';
|
import { FileId, BaseFileMetadata } from './file';
|
||||||
|
import { StoredFileMetadata, StoredFile } from '../services/fileStorage';
|
||||||
|
|
||||||
// Re-export FileId for convenience
|
// Re-export FileId for convenience
|
||||||
export type { FileId };
|
export type { FileId };
|
||||||
@ -51,30 +52,20 @@ export interface ProcessedFileMetadata {
|
|||||||
* separately in refs for memory efficiency. Supports multi-tool workflows
|
* separately in refs for memory efficiency. Supports multi-tool workflows
|
||||||
* where files persist across tool operations.
|
* where files persist across tool operations.
|
||||||
*/
|
*/
|
||||||
export interface StirlingFileStub {
|
/**
|
||||||
id: FileId; // UUID primary key for collision-free operations
|
* StirlingFileStub - Runtime UI metadata for files in the active workbench session
|
||||||
name: string; // Display name for UI
|
*
|
||||||
size: number; // File size for progress indicators
|
* Contains UI display data and processing state. Actual File objects stored
|
||||||
type: string; // MIME type for format validation
|
* separately in refs for memory efficiency. Supports multi-tool workflows
|
||||||
lastModified: number; // Original timestamp for deduplication
|
* where files persist across tool operations.
|
||||||
|
*/
|
||||||
|
export interface StirlingFileStub extends BaseFileMetadata {
|
||||||
quickKey?: string; // Fast deduplication key: name|size|lastModified
|
quickKey?: string; // Fast deduplication key: name|size|lastModified
|
||||||
thumbnailUrl?: string; // Generated thumbnail blob URL for visual display
|
thumbnailUrl?: string; // Generated thumbnail blob URL for visual display
|
||||||
blobUrl?: string; // File access blob URL for downloads/processing
|
blobUrl?: string; // File access blob URL for downloads/processing
|
||||||
createdAt?: number; // When added to workbench for sorting
|
|
||||||
processedFile?: ProcessedFileMetadata; // PDF page data and processing results
|
processedFile?: ProcessedFileMetadata; // PDF page data and processing results
|
||||||
insertAfterPageId?: string; // Page ID after which this file should be inserted
|
insertAfterPageId?: string; // Page ID after which this file should be inserted
|
||||||
isPinned?: boolean; // Protected from tool consumption (replace/remove)
|
isPinned?: boolean; // Protected from tool consumption (replace/remove)
|
||||||
isLeaf?: boolean; // True if this file is a leaf node (hasn't been processed yet)
|
|
||||||
|
|
||||||
// File history tracking (from PDF metadata)
|
|
||||||
originalFileId?: string; // Root file ID for grouping versions
|
|
||||||
versionNumber?: number; // Version number in chain
|
|
||||||
parentFileId?: FileId; // Immediate parent file ID
|
|
||||||
toolHistory?: Array<{
|
|
||||||
toolName: string;
|
|
||||||
timestamp: number;
|
|
||||||
parameters?: Record<string, any>;
|
|
||||||
}>;
|
|
||||||
// Note: File object stored in provider ref, not in state
|
// Note: File object stored in provider ref, not in state
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,6 +108,11 @@ export function isStirlingFile(file: File): file is StirlingFile {
|
|||||||
|
|
||||||
// Create a StirlingFile from a regular File object
|
// Create a StirlingFile from a regular File object
|
||||||
export function createStirlingFile(file: File, id?: FileId): StirlingFile {
|
export function createStirlingFile(file: File, id?: FileId): StirlingFile {
|
||||||
|
// Check if file is already a StirlingFile to avoid property redefinition
|
||||||
|
if (isStirlingFile(file)) {
|
||||||
|
return file; // Already has fileId and quickKey properties
|
||||||
|
}
|
||||||
|
|
||||||
const fileId = id || createFileId();
|
const fileId = id || createFileId();
|
||||||
const quickKey = createQuickKey(file);
|
const quickKey = createQuickKey(file);
|
||||||
|
|
||||||
@ -220,7 +216,6 @@ export interface FileOperation {
|
|||||||
metadata?: {
|
metadata?: {
|
||||||
originalFileName?: string;
|
originalFileName?: string;
|
||||||
outputFileNames?: string[];
|
outputFileNames?: string[];
|
||||||
parameters?: Record<string, any>;
|
|
||||||
fileSize?: number;
|
fileSize?: number;
|
||||||
pageCount?: number;
|
pageCount?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
@ -298,7 +293,7 @@ export interface FileContextActions {
|
|||||||
// File management - lightweight actions only
|
// File management - lightweight actions only
|
||||||
addFiles: (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise<StirlingFile[]>;
|
addFiles: (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise<StirlingFile[]>;
|
||||||
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<StirlingFile[]>;
|
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<StirlingFile[]>;
|
||||||
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise<StirlingFile[]>;
|
addStoredFiles: (storedFiles: StoredFile[], options?: { selectFiles?: boolean }) => Promise<StirlingFile[]>;
|
||||||
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise<void>;
|
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise<void>;
|
||||||
updateStirlingFileStub: (id: FileId, updates: Partial<StirlingFileStub>) => void;
|
updateStirlingFileStub: (id: FileId, updates: Partial<StirlingFileStub>) => void;
|
||||||
reorderFiles: (orderedFileIds: FileId[]) => void;
|
reorderFiles: (orderedFileIds: FileId[]) => void;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FileMetadata } from '../types/file';
|
import { StoredFileMetadata } from '../services/fileStorage';
|
||||||
import { fileStorage } from '../services/fileStorage';
|
import { fileStorage } from '../services/fileStorage';
|
||||||
import { zipFileService } from '../services/zipFileService';
|
import { zipFileService } from '../services/zipFileService';
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ export function downloadBlob(blob: Blob, filename: string): void {
|
|||||||
* @param file - The file object with storage information
|
* @param file - The file object with storage information
|
||||||
* @throws Error if file cannot be retrieved from storage
|
* @throws Error if file cannot be retrieved from storage
|
||||||
*/
|
*/
|
||||||
export async function downloadFileFromStorage(file: FileMetadata): Promise<void> {
|
export async function downloadFileFromStorage(file: StoredFileMetadata): Promise<void> {
|
||||||
const lookupKey = file.id;
|
const lookupKey = file.id;
|
||||||
const storedFile = await fileStorage.getFile(lookupKey);
|
const storedFile = await fileStorage.getFile(lookupKey);
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ export async function downloadFileFromStorage(file: FileMetadata): Promise<void>
|
|||||||
* Downloads multiple files as individual downloads
|
* Downloads multiple files as individual downloads
|
||||||
* @param files - Array of files to download
|
* @param files - Array of files to download
|
||||||
*/
|
*/
|
||||||
export async function downloadMultipleFiles(files: FileMetadata[]): Promise<void> {
|
export async function downloadMultipleFiles(files: StoredFileMetadata[]): Promise<void> {
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
await downloadFileFromStorage(file);
|
await downloadFileFromStorage(file);
|
||||||
}
|
}
|
||||||
@ -53,7 +53,7 @@ export async function downloadMultipleFiles(files: FileMetadata[]): Promise<void
|
|||||||
* @param files - Array of files to include in ZIP
|
* @param files - Array of files to include in ZIP
|
||||||
* @param zipFilename - Optional custom ZIP filename (defaults to timestamped name)
|
* @param zipFilename - Optional custom ZIP filename (defaults to timestamped name)
|
||||||
*/
|
*/
|
||||||
export async function downloadFilesAsZip(files: FileMetadata[], zipFilename?: string): Promise<void> {
|
export async function downloadFilesAsZip(files: StoredFileMetadata[], zipFilename?: string): Promise<void> {
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
throw new Error('No files provided for ZIP download');
|
throw new Error('No files provided for ZIP download');
|
||||||
}
|
}
|
||||||
@ -94,7 +94,7 @@ export async function downloadFilesAsZip(files: FileMetadata[], zipFilename?: st
|
|||||||
* @param options - Download options
|
* @param options - Download options
|
||||||
*/
|
*/
|
||||||
export async function downloadFiles(
|
export async function downloadFiles(
|
||||||
files: FileMetadata[],
|
files: StoredFileMetadata[],
|
||||||
options: {
|
options: {
|
||||||
forceZip?: boolean;
|
forceZip?: boolean;
|
||||||
zipFilename?: string;
|
zipFilename?: string;
|
||||||
|
@ -1,206 +1,17 @@
|
|||||||
/**
|
/**
|
||||||
* File History Utilities
|
* File History Utilities
|
||||||
*
|
*
|
||||||
* Helper functions for integrating PDF metadata service with FileContext operations.
|
* Helper functions for IndexedDB-based file history management.
|
||||||
* Handles extraction of history from files and preparation for metadata injection.
|
* Handles file history operations and lineage tracking.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { pdfMetadataService, type ToolOperation } from '../services/pdfMetadataService';
|
|
||||||
import { StirlingFileStub } from '../types/fileContext';
|
import { StirlingFileStub } from '../types/fileContext';
|
||||||
import { FileId, FileMetadata } from '../types/file';
|
import { FileId } from '../types/file';
|
||||||
import { createFileId } from '../types/fileContext';
|
import { StoredFileMetadata } from '../services/fileStorage';
|
||||||
|
|
||||||
const DEBUG = process.env.NODE_ENV === 'development';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract history information from a PDF file and update StirlingFileStub
|
|
||||||
*/
|
|
||||||
export async function extractFileHistory(
|
|
||||||
file: File,
|
|
||||||
record: StirlingFileStub
|
|
||||||
): Promise<StirlingFileStub> {
|
|
||||||
// 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 history information
|
|
||||||
return {
|
|
||||||
...record,
|
|
||||||
originalFileId: history.originalFileId,
|
|
||||||
versionNumber: history.versionNumber,
|
|
||||||
parentFileId: history.parentFileId as FileId | undefined,
|
|
||||||
toolHistory: history.toolChain
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (DEBUG) console.warn('📄 Failed to extract file history:', file.name, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject history metadata into a PDF file for tool operations
|
|
||||||
*/
|
|
||||||
export async function injectHistoryForTool(
|
|
||||||
file: File,
|
|
||||||
sourceStirlingFileStub: StirlingFileStub,
|
|
||||||
toolName: string,
|
|
||||||
parameters?: Record<string, any>
|
|
||||||
): Promise<File> {
|
|
||||||
// Only process PDF files
|
|
||||||
if (!file.type.includes('pdf')) {
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
|
||||||
|
|
||||||
// Create tool operation record
|
|
||||||
const toolOperation: ToolOperation = {
|
|
||||||
toolName,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
parameters
|
|
||||||
};
|
|
||||||
|
|
||||||
let modifiedBytes: ArrayBuffer;
|
|
||||||
|
|
||||||
// Extract version info directly from the PDF metadata to ensure accuracy
|
|
||||||
const existingHistoryMetadata = await pdfMetadataService.extractHistoryMetadata(arrayBuffer);
|
|
||||||
|
|
||||||
let newVersionNumber: number;
|
|
||||||
let originalFileId: string;
|
|
||||||
let parentFileId: string;
|
|
||||||
let parentToolChain: ToolOperation[];
|
|
||||||
|
|
||||||
if (existingHistoryMetadata) {
|
|
||||||
// File already has embedded history - increment version
|
|
||||||
const history = existingHistoryMetadata.stirlingHistory;
|
|
||||||
newVersionNumber = history.versionNumber + 1;
|
|
||||||
originalFileId = history.originalFileId;
|
|
||||||
parentFileId = sourceStirlingFileStub.id; // This file becomes the parent
|
|
||||||
parentToolChain = history.toolChain || [];
|
|
||||||
|
|
||||||
} else if (sourceStirlingFileStub.originalFileId && sourceStirlingFileStub.versionNumber) {
|
|
||||||
// File record has history but PDF doesn't (shouldn't happen, but fallback)
|
|
||||||
newVersionNumber = sourceStirlingFileStub.versionNumber + 1;
|
|
||||||
originalFileId = sourceStirlingFileStub.originalFileId;
|
|
||||||
parentFileId = sourceStirlingFileStub.id;
|
|
||||||
parentToolChain = sourceStirlingFileStub.toolHistory || [];
|
|
||||||
} else {
|
|
||||||
// File has no history - this becomes version 1
|
|
||||||
newVersionNumber = 1;
|
|
||||||
originalFileId = sourceStirlingFileStub.id; // Use source file ID as original
|
|
||||||
parentFileId = sourceStirlingFileStub.id; // Parent is the source file
|
|
||||||
parentToolChain = []; // No previous tools
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new tool chain with the new operation
|
|
||||||
const newToolChain = [...parentToolChain, toolOperation];
|
|
||||||
|
|
||||||
modifiedBytes = await pdfMetadataService.injectHistoryMetadata(
|
|
||||||
arrayBuffer,
|
|
||||||
originalFileId,
|
|
||||||
parentFileId,
|
|
||||||
newToolChain,
|
|
||||||
newVersionNumber
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create new file with updated metadata
|
|
||||||
return new File([modifiedBytes], file.name, { type: file.type });
|
|
||||||
} catch (error) {
|
|
||||||
if (DEBUG) console.warn('📄 Failed to inject history for tool operation:', error);
|
|
||||||
return file; // Return original file if injection fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepare StirlingFiles with history-injected PDFs for tool operations
|
|
||||||
* Preserves fileId and all StirlingFile metadata while injecting history
|
|
||||||
*/
|
|
||||||
export async function prepareStirlingFilesWithHistory(
|
|
||||||
stirlingFiles: import('../types/fileContext').StirlingFile[],
|
|
||||||
getStirlingFileStub: (fileId: import('../types/file').FileId) => StirlingFileStub | undefined,
|
|
||||||
toolName: string,
|
|
||||||
parameters?: Record<string, any>
|
|
||||||
): Promise<import('../types/fileContext').StirlingFile[]> {
|
|
||||||
const processedFiles: import('../types/fileContext').StirlingFile[] = [];
|
|
||||||
|
|
||||||
for (const stirlingFile of stirlingFiles) {
|
|
||||||
const fileStub = getStirlingFileStub(stirlingFile.fileId);
|
|
||||||
|
|
||||||
if (!fileStub) {
|
|
||||||
// If no stub found, keep original file
|
|
||||||
processedFiles.push(stirlingFile);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject history into the file data
|
|
||||||
const fileWithHistory = await injectHistoryForTool(stirlingFile, fileStub, toolName, parameters);
|
|
||||||
|
|
||||||
// Create new StirlingFile with the updated file data but preserve fileId and quickKey
|
|
||||||
const updatedStirlingFile = new File([fileWithHistory], fileWithHistory.name, {
|
|
||||||
type: fileWithHistory.type,
|
|
||||||
lastModified: fileWithHistory.lastModified
|
|
||||||
}) as import('../types/fileContext').StirlingFile;
|
|
||||||
|
|
||||||
// Preserve the original fileId and quickKey
|
|
||||||
Object.defineProperty(updatedStirlingFile, 'fileId', {
|
|
||||||
value: stirlingFile.fileId,
|
|
||||||
writable: false,
|
|
||||||
enumerable: true,
|
|
||||||
configurable: false
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.defineProperty(updatedStirlingFile, 'quickKey', {
|
|
||||||
value: stirlingFile.quickKey,
|
|
||||||
writable: false,
|
|
||||||
enumerable: true,
|
|
||||||
configurable: false
|
|
||||||
});
|
|
||||||
|
|
||||||
processedFiles.push(updatedStirlingFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
return processedFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that processed files preserved metadata from originals
|
|
||||||
* Logs warnings for tools that strip standard PDF metadata
|
|
||||||
*/
|
|
||||||
export async function verifyToolMetadataPreservation(
|
|
||||||
originalFiles: File[],
|
|
||||||
processedFiles: File[],
|
|
||||||
toolName: string
|
|
||||||
): Promise<void> {
|
|
||||||
if (originalFiles.length === 0 || processedFiles.length === 0) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// For single-file tools, compare the original with the processed file
|
|
||||||
if (originalFiles.length === 1 && processedFiles.length === 1) {
|
|
||||||
const originalBytes = await originalFiles[0].arrayBuffer();
|
|
||||||
const processedBytes = await processedFiles[0].arrayBuffer();
|
|
||||||
|
|
||||||
await pdfMetadataService.verifyMetadataPreservation(
|
|
||||||
originalBytes,
|
|
||||||
processedBytes,
|
|
||||||
toolName
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// For multi-file tools, we could add more complex verification later
|
|
||||||
} catch (error) {
|
|
||||||
if (DEBUG) console.warn(`📄 Failed to verify metadata preservation for ${toolName}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group files by processing branches - each branch ends in a leaf file
|
* Group files by processing branches - each branch ends in a leaf file
|
||||||
@ -350,7 +161,7 @@ export async function getRecentLeafFiles(): Promise<import('../services/fileStor
|
|||||||
* Get recent file metadata efficiently using leaf flags from IndexedDB
|
* Get recent file metadata efficiently using leaf flags from IndexedDB
|
||||||
* This is much faster than loading all files and calculating leaf nodes
|
* This is much faster than loading all files and calculating leaf nodes
|
||||||
*/
|
*/
|
||||||
export async function getRecentLeafFileMetadata(): Promise<Omit<import('../services/fileStorage').StoredFile, 'data'>[]> {
|
export async function getRecentLeafFileMetadata(): Promise<StoredFileMetadata[]> {
|
||||||
try {
|
try {
|
||||||
const { fileStorage } = await import('../services/fileStorage');
|
const { fileStorage } = await import('../services/fileStorage');
|
||||||
return await fileStorage.getLeafFileMetadata();
|
return await fileStorage.getLeafFileMetadata();
|
||||||
@ -360,106 +171,18 @@ 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,
|
|
||||||
fileStub: StirlingFileStub
|
|
||||||
): Promise<StirlingFileStub> {
|
|
||||||
// Only process PDF files
|
|
||||||
if (!file.type.includes('pdf')) {
|
|
||||||
return fileStub;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
|
||||||
const historyMetadata = await pdfMetadataService.extractHistoryMetadata(arrayBuffer);
|
|
||||||
|
|
||||||
if (historyMetadata) {
|
|
||||||
const history = historyMetadata.stirlingHistory;
|
|
||||||
|
|
||||||
// Update fileStub with essential metadata only (no parent/original relationships)
|
|
||||||
return {
|
|
||||||
...fileStub,
|
|
||||||
versionNumber: history.versionNumber,
|
|
||||||
toolHistory: history.toolChain
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (DEBUG) console.warn('📄 Failed to extract basic metadata:', file.name, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileStub;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load file history on-demand for a specific file
|
* Create basic metadata for storing files
|
||||||
* This replaces the automatic history extraction during file loading
|
* History information is managed separately in IndexedDB
|
||||||
*/
|
|
||||||
export async function loadFileHistoryOnDemand(
|
|
||||||
file: File,
|
|
||||||
fileId: FileId,
|
|
||||||
updateFileStub?: (id: FileId, updates: Partial<StirlingFileStub>) => 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 baseFileStub: StirlingFileStub = {
|
|
||||||
id: fileId,
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
type: file.type,
|
|
||||||
lastModified: file.lastModified
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedFileStub = await extractFileHistory(file, baseFileStub);
|
|
||||||
|
|
||||||
if (updatedFileStub !== baseFileStub && (updatedFileStub.originalFileId || updatedFileStub.versionNumber)) {
|
|
||||||
const historyData = {
|
|
||||||
originalFileId: updatedFileStub.originalFileId,
|
|
||||||
versionNumber: updatedFileStub.versionNumber,
|
|
||||||
parentFileId: updatedFileStub.parentFileId,
|
|
||||||
toolHistory: updatedFileStub.toolHistory
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update the file stub if update function is provided
|
|
||||||
if (updateFileStub) {
|
|
||||||
updateFileStub(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
|
|
||||||
*/
|
*/
|
||||||
export async function createFileMetadataWithHistory(
|
export async function createFileMetadataWithHistory(
|
||||||
file: File,
|
file: File,
|
||||||
fileId: FileId,
|
fileId: FileId,
|
||||||
thumbnail?: string
|
thumbnail?: string
|
||||||
): Promise<FileMetadata> {
|
): Promise<StoredFileMetadata> {
|
||||||
const baseMetadata: FileMetadata = {
|
return {
|
||||||
id: fileId,
|
id: fileId,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
type: file.type,
|
type: file.type,
|
||||||
@ -468,41 +191,4 @@ export async function createFileMetadataWithHistory(
|
|||||||
thumbnail,
|
thumbnail,
|
||||||
isLeaf: true // New files are leaf nodes by default
|
isLeaf: true // New files are leaf nodes by default
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extract metadata for PDF files
|
|
||||||
if (file.type.includes('pdf')) {
|
|
||||||
try {
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
|
||||||
const [historyMetadata, standardMetadata] = await Promise.all([
|
|
||||||
pdfMetadataService.extractHistoryMetadata(arrayBuffer),
|
|
||||||
pdfMetadataService.extractStandardMetadata(arrayBuffer)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = { ...baseMetadata };
|
|
||||||
|
|
||||||
// Add standard PDF metadata if available
|
|
||||||
if (standardMetadata) {
|
|
||||||
result.pdfMetadata = standardMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add history metadata if available (basic version for display)
|
|
||||||
if (historyMetadata) {
|
|
||||||
const history = historyMetadata.stirlingHistory;
|
|
||||||
// Only add basic metadata needed for display, not full history relationships
|
|
||||||
result.versionNumber = history.versionNumber;
|
|
||||||
result.historyInfo = {
|
|
||||||
originalFileId: history.originalFileId,
|
|
||||||
parentFileId: history.parentFileId,
|
|
||||||
versionNumber: history.versionNumber,
|
|
||||||
toolChain: history.toolChain
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
if (DEBUG) console.warn('📄 Failed to extract metadata:', file.name, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseMetadata;
|
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import { FileOperation } from '../types/fileContext';
|
|||||||
*/
|
*/
|
||||||
export const createOperation = <TParams = void>(
|
export const createOperation = <TParams = void>(
|
||||||
operationType: string,
|
operationType: string,
|
||||||
params: TParams,
|
_params: TParams,
|
||||||
selectedFiles: File[]
|
selectedFiles: File[]
|
||||||
): { operation: FileOperation; operationId: string; fileId: FileId } => {
|
): { operation: FileOperation; operationId: string; fileId: FileId } => {
|
||||||
const operationId = `${operationType}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
const operationId = `${operationType}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
@ -20,7 +20,6 @@ export const createOperation = <TParams = void>(
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
metadata: {
|
metadata: {
|
||||||
originalFileName: selectedFiles[0]?.name,
|
originalFileName: selectedFiles[0]?.name,
|
||||||
parameters: params,
|
|
||||||
fileSize: selectedFiles.reduce((sum, f) => sum + f.size, 0)
|
fileSize: selectedFiles.reduce((sum, f) => sum + f.size, 0)
|
||||||
}
|
}
|
||||||
} as any /* FIX ME*/;
|
} as any /* FIX ME*/;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user