From 9766949f769b4586c68a6a0bf6cfdf431acce3df Mon Sep 17 00:00:00 2001 From: Reece Browne Date: Fri, 5 Sep 2025 00:23:55 +0100 Subject: [PATCH] stirlingFileStub instead of workbench file --- .../src/components/fileEditor/FileEditor.tsx | 46 +++++++------- .../pageEditor/hooks/usePageDocument.ts | 22 +++---- frontend/src/components/shared/FileCard.tsx | 4 +- frontend/src/components/shared/FileGrid.tsx | 12 ++-- .../tools/shared/FileStatusIndicator.tsx | 4 +- frontend/src/contexts/FileContext.tsx | 12 ++-- frontend/src/contexts/file/FileReducer.ts | 18 +++--- frontend/src/contexts/file/fileActions.ts | 48 +++++++-------- frontend/src/contexts/file/fileHooks.ts | 20 +++--- frontend/src/contexts/file/fileSelectors.ts | 16 ++--- frontend/src/contexts/file/lifecycle.ts | 4 +- .../hooks/tools/shared/useToolOperation.ts | 18 +++--- frontend/src/types/fileContext.ts | 61 +++++++++++-------- 13 files changed, 146 insertions(+), 139 deletions(-) diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index 383010257..901eb20ca 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -50,7 +50,7 @@ const FileEditor = ({ // Extract needed values from state (memoized to prevent infinite loops) const activeFiles = useMemo(() => selectors.getFiles(), [selectors.getFilesSignature()]); - const activeWorkbenchFiles = useMemo(() => selectors.getWorkbenchFiles(), [selectors.getFilesSignature()]); + const activeStirlingFileStubs = useMemo(() => selectors.getStirlingFileStubs(), [selectors.getFilesSignature()]); const selectedFileIds = state.ui.selectedFileIds; const isProcessing = state.ui.isProcessing; @@ -92,10 +92,10 @@ const FileEditor = ({ const contextSelectedIdsRef = useRef([]); contextSelectedIdsRef.current = contextSelectedIds; - // Use activeWorkbenchFiles directly - no conversion needed + // Use activeStirlingFileStubs directly - no conversion needed const localSelectedIds = contextSelectedIds; - // Helper to convert WorkbenchFile to FileThumbnail format + // Helper to convert StirlingFileStub to FileThumbnail format const recordToFileItem = useCallback((record: any) => { const file = selectors.getFile(record.id); if (!file) return null; @@ -253,26 +253,26 @@ const FileEditor = ({ }, [addFiles]); const selectAll = useCallback(() => { - setSelectedFiles(activeWorkbenchFiles.map(r => r.id)); // Use WorkbenchFile IDs directly - }, [activeWorkbenchFiles, setSelectedFiles]); + setSelectedFiles(activeStirlingFileStubs.map(r => r.id)); // Use StirlingFileStub IDs directly + }, [activeStirlingFileStubs, setSelectedFiles]); const deselectAll = useCallback(() => setSelectedFiles([]), [setSelectedFiles]); const closeAllFiles = useCallback(() => { - if (activeWorkbenchFiles.length === 0) return; + if (activeStirlingFileStubs.length === 0) return; // Remove all files from context but keep in storage - const allFileIds = activeWorkbenchFiles.map(record => record.id); + const allFileIds = activeStirlingFileStubs.map(record => record.id); removeFiles(allFileIds, false); // false = keep in storage // Clear selections setSelectedFiles([]); - }, [activeWorkbenchFiles, removeFiles, setSelectedFiles]); + }, [activeStirlingFileStubs, removeFiles, setSelectedFiles]); const toggleFile = useCallback((fileId: FileId) => { const currentSelectedIds = contextSelectedIdsRef.current; - const targetRecord = activeWorkbenchFiles.find(r => r.id === fileId); + const targetRecord = activeStirlingFileStubs.find(r => r.id === fileId); if (!targetRecord) return; const contextFileId = fileId; // No need to create a new ID @@ -302,7 +302,7 @@ const FileEditor = ({ // Update context (this automatically updates tool selection since they use the same action) setSelectedFiles(newSelection); - }, [setSelectedFiles, toolMode, setStatus, activeWorkbenchFiles]); + }, [setSelectedFiles, toolMode, setStatus, activeStirlingFileStubs]); const toggleSelectionMode = useCallback(() => { setSelectionMode(prev => { @@ -316,7 +316,7 @@ const FileEditor = ({ // File reordering handler for drag and drop const handleReorderFiles = useCallback((sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => { - const currentIds = activeWorkbenchFiles.map(r => r.id); + const currentIds = activeStirlingFileStubs.map(r => r.id); // Find indices const sourceIndex = currentIds.findIndex(id => id === sourceFileId); @@ -368,13 +368,13 @@ const FileEditor = ({ // Update status const moveCount = filesToMove.length; setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`); - }, [activeWorkbenchFiles, reorderFiles, setStatus]); + }, [activeStirlingFileStubs, reorderFiles, setStatus]); // File operations using context const handleDeleteFile = useCallback((fileId: FileId) => { - const record = activeWorkbenchFiles.find(r => r.id === fileId); + const record = activeStirlingFileStubs.find(r => r.id === fileId); const file = record ? selectors.getFile(record.id) : null; if (record && file) { @@ -405,27 +405,27 @@ const FileEditor = ({ const currentSelected = selectedFileIds.filter(id => id !== contextFileId); setSelectedFiles(currentSelected); } - }, [activeWorkbenchFiles, selectors, removeFiles, setSelectedFiles, selectedFileIds]); + }, [activeStirlingFileStubs, selectors, removeFiles, setSelectedFiles, selectedFileIds]); const handleViewFile = useCallback((fileId: FileId) => { - const record = activeWorkbenchFiles.find(r => r.id === fileId); + const record = activeStirlingFileStubs.find(r => r.id === fileId); if (record) { // Set the file as selected in context and switch to viewer for preview setSelectedFiles([fileId]); navActions.setWorkbench('viewer'); } - }, [activeWorkbenchFiles, setSelectedFiles, navActions.setWorkbench]); + }, [activeStirlingFileStubs, setSelectedFiles, navActions.setWorkbench]); const handleMergeFromHere = useCallback((fileId: FileId) => { - const startIndex = activeWorkbenchFiles.findIndex(r => r.id === fileId); + const startIndex = activeStirlingFileStubs.findIndex(r => r.id === fileId); if (startIndex === -1) return; - const recordsToMerge = activeWorkbenchFiles.slice(startIndex); + const recordsToMerge = activeStirlingFileStubs.slice(startIndex); const filesToMerge = recordsToMerge.map(r => selectors.getFile(r.id)).filter(Boolean) as StirlingFile[]; if (onMergeFiles) { onMergeFiles(filesToMerge); } - }, [activeWorkbenchFiles, selectors, onMergeFiles]); + }, [activeStirlingFileStubs, selectors, onMergeFiles]); const handleSplitFile = useCallback((fileId: FileId) => { const file = selectors.getFile(fileId); @@ -467,7 +467,7 @@ const FileEditor = ({ - {activeWorkbenchFiles.length === 0 && !zipExtractionProgress.isExtracting ? ( + {activeStirlingFileStubs.length === 0 && !zipExtractionProgress.isExtracting ? (
📁 @@ -475,7 +475,7 @@ const FileEditor = ({ Upload PDF files, ZIP archives, or load from storage to get started
- ) : activeWorkbenchFiles.length === 0 && zipExtractionProgress.isExtracting ? ( + ) : activeStirlingFileStubs.length === 0 && zipExtractionProgress.isExtracting ? ( @@ -522,7 +522,7 @@ const FileEditor = ({ pointerEvents: 'auto' }} > - {activeWorkbenchFiles.map((record, index) => { + {activeStirlingFileStubs.map((record, index) => { const fileItem = recordToFileItem(record); if (!fileItem) return null; @@ -531,7 +531,7 @@ const FileEditor = ({ key={record.id} file={fileItem} index={index} - totalFiles={activeWorkbenchFiles.length} + totalFiles={activeStirlingFileStubs.length} selectedFiles={localSelectedIds} selectionMode={selectionMode} onToggleFile={toggleFile} diff --git a/frontend/src/components/pageEditor/hooks/usePageDocument.ts b/frontend/src/components/pageEditor/hooks/usePageDocument.ts index 2a17f56e8..3a4d49053 100644 --- a/frontend/src/components/pageEditor/hooks/usePageDocument.ts +++ b/frontend/src/components/pageEditor/hooks/usePageDocument.ts @@ -27,9 +27,9 @@ export function usePageDocument(): PageDocumentHook { const globalProcessing = state.ui.isProcessing; // Get primary file record outside useMemo to track processedFile changes - const primaryWorkbenchFile = primaryFileId ? selectors.getWorkbenchFile(primaryFileId) : null; - const processedFilePages = primaryWorkbenchFile?.processedFile?.pages; - const processedFileTotalPages = primaryWorkbenchFile?.processedFile?.totalPages; + const primaryStirlingFileStub = primaryFileId ? selectors.getStirlingFileStub(primaryFileId) : null; + const processedFilePages = primaryStirlingFileStub?.processedFile?.pages; + const processedFileTotalPages = primaryStirlingFileStub?.processedFile?.totalPages; // Compute merged document with stable signature (prevents infinite loops) const mergedPdfDocument = useMemo((): PDFDocument | null => { @@ -38,16 +38,16 @@ export function usePageDocument(): PageDocumentHook { const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null; // If we have file IDs but no file record, something is wrong - return null to show loading - if (!primaryWorkbenchFile) { + if (!primaryStirlingFileStub) { console.log('🎬 PageEditor: No primary file record found, showing loading'); return null; } const name = activeFileIds.length === 1 - ? (primaryWorkbenchFile.name ?? 'document.pdf') + ? (primaryStirlingFileStub.name ?? 'document.pdf') : activeFileIds - .map(id => (selectors.getWorkbenchFile(id)?.name ?? 'file').replace(/\.pdf$/i, '')) + .map(id => (selectors.getStirlingFileStub(id)?.name ?? 'file').replace(/\.pdf$/i, '')) .join(' + '); // Build page insertion map from files with insertion positions @@ -55,7 +55,7 @@ export function usePageDocument(): PageDocumentHook { const originalFileIds: FileId[] = []; activeFileIds.forEach(fileId => { - const record = selectors.getWorkbenchFile(fileId); + const record = selectors.getStirlingFileStub(fileId); if (record?.insertAfterPageId !== undefined) { if (!insertionMap.has(record.insertAfterPageId)) { insertionMap.set(record.insertAfterPageId, []); @@ -72,12 +72,12 @@ export function usePageDocument(): PageDocumentHook { // Helper function to create pages from a file const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => { - const workbenchFiles = selectors.getWorkbenchFile(fileId); - if (!workbenchFiles) { + const stirlingFileStub = selectors.getStirlingFileStub(fileId); + if (!stirlingFileStub) { return []; } - const processedFile = workbenchFiles.processedFile; + const processedFile = stirlingFileStub.processedFile; let filePages: PDFPage[] = []; if (processedFile?.pages && processedFile.pages.length > 0) { @@ -159,7 +159,7 @@ export function usePageDocument(): PageDocumentHook { }; return mergedDoc; - }, [activeFileIds, primaryFileId, primaryWorkbenchFile, processedFilePages, processedFileTotalPages, selectors, filesSignature]); + }, [activeFileIds, primaryFileId, primaryStirlingFileStub, processedFilePages, processedFileTotalPages, selectors, filesSignature]); // Large document detection for smart loading const isVeryLargeDocument = useMemo(() => { diff --git a/frontend/src/components/shared/FileCard.tsx b/frontend/src/components/shared/FileCard.tsx index d24395125..6c63af42e 100644 --- a/frontend/src/components/shared/FileCard.tsx +++ b/frontend/src/components/shared/FileCard.tsx @@ -6,13 +6,13 @@ import StorageIcon from "@mui/icons-material/Storage"; import VisibilityIcon from "@mui/icons-material/Visibility"; import EditIcon from "@mui/icons-material/Edit"; -import { WorkbenchFile } from "../../types/fileContext"; +import { StirlingFileStub } from "../../types/fileContext"; import { getFileSize, getFileDate } from "../../utils/fileUtils"; import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail"; interface FileCardProps { file: File; - record?: WorkbenchFile; + record?: StirlingFileStub; onRemove: () => void; onDoubleClick?: () => void; onView?: () => void; diff --git a/frontend/src/components/shared/FileGrid.tsx b/frontend/src/components/shared/FileGrid.tsx index 03270c5dd..2d9cba640 100644 --- a/frontend/src/components/shared/FileGrid.tsx +++ b/frontend/src/components/shared/FileGrid.tsx @@ -4,15 +4,15 @@ import { useTranslation } from "react-i18next"; import SearchIcon from "@mui/icons-material/Search"; import SortIcon from "@mui/icons-material/Sort"; import FileCard from "./FileCard"; -import { WorkbenchFile } from "../../types/fileContext"; +import { StirlingFileStub } from "../../types/fileContext"; import { FileId } from "../../types/file"; interface FileGridProps { - files: Array<{ file: File; record?: WorkbenchFile }>; + files: Array<{ file: File; record?: StirlingFileStub }>; onRemove?: (index: number) => void; - onDoubleClick?: (item: { file: File; record?: WorkbenchFile }) => void; - onView?: (item: { file: File; record?: WorkbenchFile }) => void; - onEdit?: (item: { file: File; record?: WorkbenchFile }) => void; + onDoubleClick?: (item: { file: File; record?: StirlingFileStub }) => void; + onView?: (item: { file: File; record?: StirlingFileStub }) => void; + onEdit?: (item: { file: File; record?: StirlingFileStub }) => void; onSelect?: (fileId: FileId) => void; selectedFiles?: FileId[]; showSearch?: boolean; @@ -126,7 +126,7 @@ const FileGrid = ({ {displayFiles .filter(item => { if (!item.record?.id) { - console.error('FileGrid: File missing WorkbenchFile with proper ID:', item.file.name); + console.error('FileGrid: File missing StirlingFileStub with proper ID:', item.file.name); return false; } return true; diff --git a/frontend/src/components/tools/shared/FileStatusIndicator.tsx b/frontend/src/components/tools/shared/FileStatusIndicator.tsx index 261c9eae8..a083d65ef 100644 --- a/frontend/src/components/tools/shared/FileStatusIndicator.tsx +++ b/frontend/src/components/tools/shared/FileStatusIndicator.tsx @@ -18,7 +18,7 @@ const FileStatusIndicator = ({ }: FileStatusIndicatorProps) => { const { t } = useTranslation(); const { openFilesModal, onFilesSelect } = useFilesModalContext(); - const { files: workbenchFiles } = useAllFiles(); + const { files: stirlingFileStubs } = useAllFiles(); const { loadRecentFiles } = useFileManager(); const [hasRecentFiles, setHasRecentFiles] = useState(null); @@ -56,7 +56,7 @@ const FileStatusIndicator = ({ } // Check if there are no files in the workbench - if (workbenchFiles.length === 0) { + if (stirlingFileStubs.length === 0) { // If no recent files, show upload button if (!hasRecentFiles) { return ( diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx index a79303c33..80735de58 100644 --- a/frontend/src/contexts/FileContext.tsx +++ b/frontend/src/contexts/FileContext.tsx @@ -20,7 +20,7 @@ import { FileContextActionsValue, FileContextActions, FileId, - WorkbenchFile, + StirlingFileStub, StirlingFile, createStirlingFile } from '../types/fileContext'; @@ -127,8 +127,8 @@ function FileContextInner({ return consumeFiles(inputFileIds, outputFiles, filesRef, dispatch, indexedDB); }, [indexedDB]); - const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputWorkbenchFiles: WorkbenchFile[], outputFileIds: FileId[]): Promise => { - return undoConsumeFiles(inputFiles, inputWorkbenchFiles, outputFileIds, stateRef, filesRef, dispatch, indexedDB); + const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[]): Promise => { + return undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds, stateRef, filesRef, dispatch, indexedDB); }, [indexedDB]); // Helper to find FileId from File object @@ -170,8 +170,8 @@ function FileContextInner({ } } }, - updateWorkbenchFile: (fileId: FileId, updates: Partial) => - lifecycleManager.updateWorkbenchFile(fileId, updates, stateRef), + updateStirlingFileStub: (fileId: FileId, updates: Partial) => + lifecycleManager.updateStirlingFileStub(fileId, updates, stateRef), reorderFiles: (orderedFileIds: FileId[]) => { dispatch({ type: 'REORDER_FILES', payload: { orderedFileIds } }); }, @@ -295,7 +295,7 @@ export { useFileSelection, useFileManagement, useFileUI, - useWorkbenchFile, + useStirlingFileStub, useAllFiles, useSelectedFiles, // Primary API hooks for tools diff --git a/frontend/src/contexts/file/FileReducer.ts b/frontend/src/contexts/file/FileReducer.ts index c3dfa82e6..4c4196764 100644 --- a/frontend/src/contexts/file/FileReducer.ts +++ b/frontend/src/contexts/file/FileReducer.ts @@ -6,7 +6,7 @@ import { FileId } from '../../types/file'; import { FileContextState, FileContextAction, - WorkbenchFile + StirlingFileStub } from '../../types/fileContext'; // Initial state @@ -29,7 +29,7 @@ export const initialFileContextState: FileContextState = { function processFileSwap( state: FileContextState, filesToRemove: FileId[], - filesToAdd: WorkbenchFile[] + filesToAdd: StirlingFileStub[] ): FileContextState { // Only remove unpinned files const unpinnedRemoveIds = filesToRemove.filter(id => !state.pinnedFiles.has(id)); @@ -70,11 +70,11 @@ function processFileSwap( export function fileContextReducer(state: FileContextState, action: FileContextAction): FileContextState { switch (action.type) { case 'ADD_FILES': { - const { workbenchFiles } = action.payload; + const { stirlingFileStubs } = action.payload; const newIds: FileId[] = []; - const newById: Record = { ...state.files.byId }; + const newById: Record = { ...state.files.byId }; - workbenchFiles.forEach(record => { + stirlingFileStubs.forEach(record => { // Only add if not already present (dedupe by stable ID) if (!newById[record.id]) { newIds.push(record.id); @@ -233,13 +233,13 @@ export function fileContextReducer(state: FileContextState, action: FileContextA } case 'CONSUME_FILES': { - const { inputFileIds, outputWorkbenchFiles } = action.payload; - return processFileSwap(state, inputFileIds, outputWorkbenchFiles); + const { inputFileIds, outputStirlingFileStubs } = action.payload; + return processFileSwap(state, inputFileIds, outputStirlingFileStubs); } case 'UNDO_CONSUME_FILES': { - const { inputWorkbenchFiles, outputFileIds } = action.payload; - return processFileSwap(state, outputFileIds, inputWorkbenchFiles); + const { inputStirlingFileStubs, outputFileIds } = action.payload; + return processFileSwap(state, outputFileIds, inputStirlingFileStubs); } case 'RESET_CONTEXT': { diff --git a/frontend/src/contexts/file/fileActions.ts b/frontend/src/contexts/file/fileActions.ts index db33e6885..373a28a6a 100644 --- a/frontend/src/contexts/file/fileActions.ts +++ b/frontend/src/contexts/file/fileActions.ts @@ -3,7 +3,7 @@ */ import { - WorkbenchFile, + StirlingFileStub, FileContextAction, FileContextState, toWorkbenchFile, @@ -109,7 +109,7 @@ export async function addFiles( await addFilesMutex.lock(); try { - const workbenchFiles: WorkbenchFile[] = []; + const stirlingFileStubs: StirlingFileStub[] = []; const addedFiles: AddedFile[] = []; // Build quickKey lookup from existing files for deduplication @@ -184,7 +184,7 @@ export async function addFiles( } existingQuickKeys.add(quickKey); - workbenchFiles.push(record); + stirlingFileStubs.push(record); addedFiles.push({ file, id: fileId, thumbnail }); } break; @@ -226,7 +226,7 @@ export async function addFiles( } existingQuickKeys.add(quickKey); - workbenchFiles.push(record); + stirlingFileStubs.push(record); addedFiles.push({ file, id: fileId, thumbnail }); } break; @@ -301,7 +301,7 @@ export async function addFiles( } existingQuickKeys.add(quickKey); - workbenchFiles.push(record); + stirlingFileStubs.push(record); addedFiles.push({ file, id: fileId, thumbnail: metadata.thumbnail }); } @@ -310,9 +310,9 @@ export async function addFiles( } // Dispatch ADD_FILES action if we have new files - if (workbenchFiles.length > 0) { - dispatch({ type: 'ADD_FILES', payload: { workbenchFiles } }); - if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${workbenchFiles.length} files`); + if (stirlingFileStubs.length > 0) { + dispatch({ type: 'ADD_FILES', payload: { stirlingFileStubs } }); + if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${stirlingFileStubs.length} files`); } return addedFiles; @@ -328,7 +328,7 @@ export async function addFiles( async function processFilesIntoRecords( files: File[], filesRef: React.MutableRefObject> -): Promise> { +): Promise> { return Promise.all( files.map(async (file) => { const fileId = createFileId(); @@ -365,10 +365,10 @@ async function processFilesIntoRecords( * Helper function to persist files to IndexedDB */ async function persistFilesToIndexedDB( - workbenchFiles: Array<{ file: File; fileId: FileId; thumbnail?: string }>, + stirlingFileStubs: Array<{ file: File; fileId: FileId; thumbnail?: string }>, indexedDB: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise } ): Promise { - await Promise.all(workbenchFiles.map(async ({ file, fileId, thumbnail }) => { + await Promise.all(stirlingFileStubs.map(async ({ file, fileId, thumbnail }) => { try { await indexedDB.saveFile(file, fileId, thumbnail); } catch (error) { @@ -390,11 +390,11 @@ export async function consumeFiles( if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputFiles.length} output files`); // Process output files with thumbnails and metadata - const outputWorkbenchFiles = await processFilesIntoRecords(outputFiles, filesRef); + const outputStirlingFileStubs = await processFilesIntoRecords(outputFiles, filesRef); // Persist output files to IndexedDB if available if (indexedDB) { - await persistFilesToIndexedDB(outputWorkbenchFiles, indexedDB); + await persistFilesToIndexedDB(outputStirlingFileStubs, indexedDB); } // Dispatch the consume action @@ -402,21 +402,21 @@ export async function consumeFiles( type: 'CONSUME_FILES', payload: { inputFileIds, - outputWorkbenchFiles: outputWorkbenchFiles.map(({ record }) => record) + outputStirlingFileStubs: outputStirlingFileStubs.map(({ record }) => record) } }); - if (DEBUG) console.log(`📄 consumeFiles: Successfully consumed files - removed ${inputFileIds.length} inputs, added ${outputWorkbenchFiles.length} outputs`); + if (DEBUG) console.log(`📄 consumeFiles: Successfully consumed files - removed ${inputFileIds.length} inputs, added ${outputStirlingFileStubs.length} outputs`); // Return the output file IDs for undo tracking - return outputWorkbenchFiles.map(({ fileId }) => fileId); + return outputStirlingFileStubs.map(({ fileId }) => fileId); } /** * Helper function to restore files to filesRef and manage IndexedDB cleanup */ async function restoreFilesAndCleanup( - filesToRestore: Array<{ file: File; record: WorkbenchFile }>, + filesToRestore: Array<{ file: File; record: StirlingFileStub }>, fileIdsToRemove: FileId[], filesRef: React.MutableRefObject>, indexedDB?: { deleteFile: (fileId: FileId) => Promise } | null @@ -465,18 +465,18 @@ async function restoreFilesAndCleanup( */ export async function undoConsumeFiles( inputFiles: File[], - inputWorkbenchFiles: WorkbenchFile[], + inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[], stateRef: React.MutableRefObject, filesRef: React.MutableRefObject>, dispatch: React.Dispatch, indexedDB?: { saveFile: (file: File, fileId: FileId, existingThumbnail?: string) => Promise; deleteFile: (fileId: FileId) => Promise } | null ): Promise { - if (DEBUG) console.log(`📄 undoConsumeFiles: Restoring ${inputWorkbenchFiles.length} input files, removing ${outputFileIds.length} output files`); + if (DEBUG) console.log(`📄 undoConsumeFiles: Restoring ${inputStirlingFileStubs.length} input files, removing ${outputFileIds.length} output files`); // Validate inputs - if (inputFiles.length !== inputWorkbenchFiles.length) { - throw new Error(`Mismatch between input files (${inputFiles.length}) and records (${inputWorkbenchFiles.length})`); + if (inputFiles.length !== inputStirlingFileStubs.length) { + throw new Error(`Mismatch between input files (${inputFiles.length}) and records (${inputStirlingFileStubs.length})`); } // Create a backup of current filesRef state for rollback @@ -486,7 +486,7 @@ export async function undoConsumeFiles( // Prepare files to restore const filesToRestore = inputFiles.map((file, index) => ({ file, - record: inputWorkbenchFiles[index] + record: inputStirlingFileStubs[index] })); // Restore input files and clean up output files @@ -501,12 +501,12 @@ export async function undoConsumeFiles( dispatch({ type: 'UNDO_CONSUME_FILES', payload: { - inputWorkbenchFiles, + inputStirlingFileStubs, outputFileIds } }); - if (DEBUG) console.log(`📄 undoConsumeFiles: Successfully undone consume operation - restored ${inputWorkbenchFiles.length} inputs, removed ${outputFileIds.length} outputs`); + if (DEBUG) console.log(`📄 undoConsumeFiles: Successfully undone consume operation - restored ${inputStirlingFileStubs.length} inputs, removed ${outputFileIds.length} outputs`); } catch (error) { // Rollback filesRef to previous state diff --git a/frontend/src/contexts/file/fileHooks.ts b/frontend/src/contexts/file/fileHooks.ts index c78c10681..1907c8fb8 100644 --- a/frontend/src/contexts/file/fileHooks.ts +++ b/frontend/src/contexts/file/fileHooks.ts @@ -9,7 +9,7 @@ import { FileContextStateValue, FileContextActionsValue } from './contexts'; -import { WorkbenchFile, StirlingFile } from '../../types/fileContext'; +import { StirlingFileStub, StirlingFile } from '../../types/fileContext'; import { FileId } from '../../types/file'; /** @@ -38,13 +38,13 @@ export function useFileActions(): FileContextActionsValue { /** * Hook for current/primary file (first in list) */ -export function useCurrentFile(): { file?: File; record?: WorkbenchFile } { +export function useCurrentFile(): { file?: File; record?: StirlingFileStub } { const { state, selectors } = useFileState(); const primaryFileId = state.files.ids[0]; return useMemo(() => ({ file: primaryFileId ? selectors.getFile(primaryFileId) : undefined, - record: primaryFileId ? selectors.getWorkbenchFile(primaryFileId) : undefined + record: primaryFileId ? selectors.getStirlingFileStub(primaryFileId) : undefined }), [primaryFileId, selectors]); } @@ -87,7 +87,7 @@ export function useFileManagement() { addFiles: actions.addFiles, removeFiles: actions.removeFiles, clearAllFiles: actions.clearAllFiles, - updateWorkbenchFile: actions.updateWorkbenchFile, + updateStirlingFileStub: actions.updateStirlingFileStub, reorderFiles: actions.reorderFiles }), [actions]); } @@ -111,24 +111,24 @@ export function useFileUI() { /** * Hook for specific file by ID (optimized for individual file access) */ -export function useWorkbenchFile(fileId: FileId): { file?: File; record?: WorkbenchFile } { +export function useStirlingFileStub(fileId: FileId): { file?: File; record?: StirlingFileStub } { const { selectors } = useFileState(); return useMemo(() => ({ file: selectors.getFile(fileId), - record: selectors.getWorkbenchFile(fileId) + record: selectors.getStirlingFileStub(fileId) }), [fileId, selectors]); } /** * Hook for all files (use sparingly - causes re-renders on file list changes) */ -export function useAllFiles(): { files: StirlingFile[]; records: WorkbenchFile[]; fileIds: FileId[] } { +export function useAllFiles(): { files: StirlingFile[]; records: StirlingFileStub[]; fileIds: FileId[] } { const { state, selectors } = useFileState(); return useMemo(() => ({ files: selectors.getFiles(), - records: selectors.getWorkbenchFiles(), + records: selectors.getStirlingFileStubs(), fileIds: state.files.ids }), [state.files.ids, selectors]); } @@ -136,12 +136,12 @@ export function useAllFiles(): { files: StirlingFile[]; records: WorkbenchFile[] /** * Hook for selected files (optimized for selection-based UI) */ -export function useSelectedFiles(): { files: StirlingFile[]; records: WorkbenchFile[]; fileIds: FileId[] } { +export function useSelectedFiles(): { files: StirlingFile[]; records: StirlingFileStub[]; fileIds: FileId[] } { const { state, selectors } = useFileState(); return useMemo(() => ({ files: selectors.getSelectedFiles(), - records: selectors.getSelectedWorkbenchFiles(), + records: selectors.getSelectedStirlingFileStubs(), fileIds: state.ui.selectedFileIds }), [state.ui.selectedFileIds, selectors]); } diff --git a/frontend/src/contexts/file/fileSelectors.ts b/frontend/src/contexts/file/fileSelectors.ts index 6842a157f..a004831cc 100644 --- a/frontend/src/contexts/file/fileSelectors.ts +++ b/frontend/src/contexts/file/fileSelectors.ts @@ -4,7 +4,7 @@ import { FileId } from '../../types/file'; import { - WorkbenchFile, + StirlingFileStub, FileContextState, FileContextSelectors, StirlingFile, @@ -34,9 +34,9 @@ export function createFileSelectors( .filter(Boolean) as StirlingFile[]; }, - getWorkbenchFile: (id: FileId) => stateRef.current.files.byId[id], + getStirlingFileStub: (id: FileId) => stateRef.current.files.byId[id], - getWorkbenchFiles: (ids?: FileId[]) => { + getStirlingFileStubs: (ids?: FileId[]) => { const currentIds = ids || stateRef.current.files.ids; return currentIds.map(id => stateRef.current.files.byId[id]).filter(Boolean); }, @@ -52,7 +52,7 @@ export function createFileSelectors( .filter(Boolean) as StirlingFile[]; }, - getSelectedWorkbenchFiles: () => { + getSelectedStirlingFileStubs: () => { return stateRef.current.ui.selectedFileIds .map(id => stateRef.current.files.byId[id]) .filter(Boolean); @@ -72,7 +72,7 @@ export function createFileSelectors( .filter(Boolean) as StirlingFile[]; }, - getPinnedWorkbenchFiles: () => { + getPinnedStirlingFileStubs: () => { return Array.from(stateRef.current.pinnedFiles) .map(id => stateRef.current.files.byId[id]) .filter(Boolean); @@ -98,9 +98,9 @@ export function createFileSelectors( /** * Helper for building quickKey sets for deduplication */ -export function buildQuickKeySet(workbenchFiles: Record): Set { +export function buildQuickKeySet(stirlingFileStubs: Record): Set { const quickKeys = new Set(); - Object.values(workbenchFiles).forEach(record => { + Object.values(stirlingFileStubs).forEach(record => { if (record.quickKey) { quickKeys.add(record.quickKey); } @@ -127,7 +127,7 @@ export function buildQuickKeySetFromMetadata(metadata: Array<{ name: string; siz export function getPrimaryFile( stateRef: React.MutableRefObject, filesRef: React.MutableRefObject> -): { file?: File; record?: WorkbenchFile } { +): { file?: File; record?: StirlingFileStub } { const primaryFileId = stateRef.current.files.ids[0]; if (!primaryFileId) return {}; diff --git a/frontend/src/contexts/file/lifecycle.ts b/frontend/src/contexts/file/lifecycle.ts index 01b02b8c3..901a16943 100644 --- a/frontend/src/contexts/file/lifecycle.ts +++ b/frontend/src/contexts/file/lifecycle.ts @@ -3,7 +3,7 @@ */ import { FileId } from '../../types/file'; -import { FileContextAction, WorkbenchFile, ProcessedFilePage } from '../../types/fileContext'; +import { FileContextAction, StirlingFileStub, ProcessedFilePage } from '../../types/fileContext'; const DEBUG = process.env.NODE_ENV === 'development'; @@ -166,7 +166,7 @@ export class FileLifecycleManager { /** * Update file record with race condition guards */ - updateWorkbenchFile = (fileId: FileId, updates: Partial, stateRef?: React.MutableRefObject): void => { + updateStirlingFileStub = (fileId: FileId, updates: Partial, stateRef?: React.MutableRefObject): void => { // Guard against updating removed files (race condition protection) if (!this.filesRef.current.has(fileId)) { if (DEBUG) console.warn(`🗂️ Attempted to update removed file (filesRef): ${fileId}`); diff --git a/frontend/src/hooks/tools/shared/useToolOperation.ts b/frontend/src/hooks/tools/shared/useToolOperation.ts index 05d7929f7..3cc24c30f 100644 --- a/frontend/src/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/hooks/tools/shared/useToolOperation.ts @@ -6,7 +6,7 @@ import { useToolState, type ProcessingProgress } from './useToolState'; import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls'; import { useToolResources } from './useToolResources'; import { extractErrorMessage } from '../../../utils/toolErrorHandler'; -import { StirlingFile, extractFiles, FileId, WorkbenchFile } from '../../../types/fileContext'; +import { StirlingFile, extractFiles, FileId, StirlingFileStub } from '../../../types/fileContext'; import { ResponseHandler } from '../../../utils/toolResponseProcessor'; // Re-export for backwards compatibility @@ -138,7 +138,7 @@ export const useToolOperation = ( // Track last operation for undo functionality const lastOperationRef = useRef<{ inputFiles: File[]; - inputWorkbenchFiles: WorkbenchFile[]; + inputStirlingFileStubs: StirlingFileStub[]; outputFileIds: FileId[]; } | null>(null); @@ -240,15 +240,15 @@ export const useToolOperation = ( // Replace input files with processed files (consumeFiles handles pinning) const inputFileIds: FileId[] = []; - const inputWorkbenchFiles: WorkbenchFile[] = []; + const inputStirlingFileStubs: StirlingFileStub[] = []; // Build parallel arrays of IDs and records for undo tracking for (const file of validFiles) { const fileId = file.fileId; - const record = selectors.getWorkbenchFile(fileId); + const record = selectors.getStirlingFileStub(fileId); if (record) { inputFileIds.push(fileId); - inputWorkbenchFiles.push(record); + inputStirlingFileStubs.push(record); } else { console.warn(`No file record found for file: ${file.name}`); } @@ -259,7 +259,7 @@ export const useToolOperation = ( // Store operation data for undo (only store what we need to avoid memory bloat) lastOperationRef.current = { inputFiles: extractFiles(validFiles), // Convert to File objects for undo - inputWorkbenchFiles: inputWorkbenchFiles.map(record => ({ ...record })), // Deep copy to avoid reference issues + inputStirlingFileStubs: inputStirlingFileStubs.map(record => ({ ...record })), // Deep copy to avoid reference issues outputFileIds }; @@ -302,10 +302,10 @@ export const useToolOperation = ( return; } - const { inputFiles, inputWorkbenchFiles, outputFileIds } = lastOperationRef.current; + const { inputFiles, inputStirlingFileStubs, outputFileIds } = lastOperationRef.current; // Validate that we have data to undo - if (inputFiles.length === 0 || inputWorkbenchFiles.length === 0) { + if (inputFiles.length === 0 || inputStirlingFileStubs.length === 0) { actions.setError(t('invalidUndoData', 'Cannot undo: invalid operation data')); return; } @@ -317,7 +317,7 @@ export const useToolOperation = ( try { // Undo the consume operation - await undoConsumeFiles(inputFiles, inputWorkbenchFiles, outputFileIds); + await undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds); // Clear results and operation tracking resetResults(); diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index e55ef3df8..7ea8bc733 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -44,25 +44,32 @@ export interface ProcessedFileMetadata { [key: string]: any; } -export interface WorkbenchFile { - id: FileId; - name: string; - size: number; - type: string; - lastModified: number; - quickKey?: string; // Fast deduplication key: name|size|lastModified - thumbnailUrl?: string; - blobUrl?: string; - createdAt?: number; - processedFile?: ProcessedFileMetadata; - insertAfterPageId?: string; // Page ID after which this file should be inserted - isPinned?: boolean; +/** + * StirlingFileStub - Metadata record for files in the active workbench session + * + * Contains UI display data and processing state. Actual File objects stored + * separately in refs for memory efficiency. Supports multi-tool workflows + * where files persist across tool operations. + */ +export interface StirlingFileStub { + id: FileId; // UUID primary key for collision-free operations + name: string; // Display name for UI + size: number; // File size for progress indicators + type: string; // MIME type for format validation + lastModified: number; // Original timestamp for deduplication + quickKey?: string; // Fast deduplication key: name|size|lastModified + thumbnailUrl?: string; // Generated thumbnail blob URL for visual display + 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 + insertAfterPageId?: string; // Page ID after which this file should be inserted + isPinned?: boolean; // Protected from tool consumption (replace/remove) // Note: File object stored in provider ref, not in state } export interface FileContextNormalizedFiles { ids: FileId[]; - byId: Record; + byId: Record; } // Helper functions - UUID-based primary keys (zero collisions, synchronous) @@ -143,7 +150,7 @@ export function isFileObject(obj: any): obj is File | StirlingFile { -export function toWorkbenchFile(file: File, id?: FileId): WorkbenchFile { +export function toWorkbenchFile(file: File, id?: FileId): StirlingFileStub { const fileId = id || createFileId(); return { id: fileId, @@ -156,7 +163,7 @@ export function toWorkbenchFile(file: File, id?: FileId): WorkbenchFile { }; } -export function revokeFileResources(record: WorkbenchFile): void { +export function revokeFileResources(record: StirlingFileStub): void { // Only revoke blob: URLs to prevent errors on other schemes if (record.thumbnailUrl && record.thumbnailUrl.startsWith('blob:')) { try { @@ -230,7 +237,7 @@ export interface FileContextState { // Core file management - lightweight file IDs only files: { ids: FileId[]; - byId: Record; + byId: Record; }; // Pinned files - files that won't be consumed by tools @@ -249,16 +256,16 @@ export interface FileContextState { // Action types for reducer pattern export type FileContextAction = // File management actions - | { type: 'ADD_FILES'; payload: { workbenchFiles: WorkbenchFile[] } } + | { type: 'ADD_FILES'; payload: { stirlingFileStubs: StirlingFileStub[] } } | { type: 'REMOVE_FILES'; payload: { fileIds: FileId[] } } - | { type: 'UPDATE_FILE_RECORD'; payload: { id: FileId; updates: Partial } } + | { type: 'UPDATE_FILE_RECORD'; payload: { id: FileId; updates: Partial } } | { type: 'REORDER_FILES'; payload: { orderedFileIds: FileId[] } } // Pinned files actions | { type: 'PIN_FILE'; payload: { fileId: FileId } } | { type: 'UNPIN_FILE'; payload: { fileId: FileId } } - | { type: 'CONSUME_FILES'; payload: { inputFileIds: FileId[]; outputWorkbenchFiles: WorkbenchFile[] } } - | { type: 'UNDO_CONSUME_FILES'; payload: { inputWorkbenchFiles: WorkbenchFile[]; outputFileIds: FileId[] } } + | { type: 'CONSUME_FILES'; payload: { inputFileIds: FileId[]; outputStirlingFileStubs: StirlingFileStub[] } } + | { type: 'UNDO_CONSUME_FILES'; payload: { inputStirlingFileStubs: StirlingFileStub[]; outputFileIds: FileId[] } } // UI actions | { type: 'SET_SELECTED_FILES'; payload: { fileIds: FileId[] } } @@ -278,7 +285,7 @@ export interface FileContextActions { addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise; addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise; removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise; - updateWorkbenchFile: (id: FileId, updates: Partial) => void; + updateStirlingFileStub: (id: FileId, updates: Partial) => void; reorderFiles: (orderedFileIds: FileId[]) => void; clearAllFiles: () => Promise; clearAllData: () => Promise; @@ -289,7 +296,7 @@ export interface FileContextActions { // File consumption (replace unpinned files with outputs) consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise; - undoConsumeFiles: (inputFiles: File[], inputWorkbenchFiles: WorkbenchFile[], outputFileIds: FileId[]) => Promise; + undoConsumeFiles: (inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[]) => Promise; // Selection management setSelectedFiles: (fileIds: FileId[]) => void; setSelectedPages: (pageNumbers: number[]) => void; @@ -314,14 +321,14 @@ export interface FileContextActions { export interface FileContextSelectors { getFile: (id: FileId) => StirlingFile | undefined; getFiles: (ids?: FileId[]) => StirlingFile[]; - getWorkbenchFile: (id: FileId) => WorkbenchFile | undefined; - getWorkbenchFiles: (ids?: FileId[]) => WorkbenchFile[]; + getStirlingFileStub: (id: FileId) => StirlingFileStub | undefined; + getStirlingFileStubs: (ids?: FileId[]) => StirlingFileStub[]; getAllFileIds: () => FileId[]; getSelectedFiles: () => StirlingFile[]; - getSelectedWorkbenchFiles: () => WorkbenchFile[]; + getSelectedStirlingFileStubs: () => StirlingFileStub[]; getPinnedFileIds: () => FileId[]; getPinnedFiles: () => StirlingFile[]; - getPinnedWorkbenchFiles: () => WorkbenchFile[]; + getPinnedStirlingFileStubs: () => StirlingFileStub[]; isFilePinned: (file: StirlingFile) => boolean; getFilesSignature: () => string; }