diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 8a30d3869..c3cbf3e89 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -14,6 +14,9 @@ import "./styles/cookieconsent.css";
import "./index.css";
import { RightRailProvider } from "./contexts/RightRailContext";
+// Import file ID debugging helpers (development only)
+import "./utils/fileIdSafety";
+
// Loading component for i18next suspense
const LoadingFallback = () => (
= ({ selectedTool }) => {
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile } = useFileManager();
// Wrapper for storeFile that generates UUID
- const storeFileWithId = useCallback(async (file: File) => {
+ const storeStirlingFile = useCallback(async (file: File) => {
const fileId = createFileId(); // Generate UUID for storage
return await storeFile(file, fileId);
}, [storeFile]);
diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx
index bf95d9796..901eb20ca 100644
--- a/frontend/src/components/fileEditor/FileEditor.tsx
+++ b/frontend/src/components/fileEditor/FileEditor.tsx
@@ -16,12 +16,12 @@ import styles from './FileEditor.module.css';
import FileEditorThumbnail from './FileEditorThumbnail';
import FilePickerModal from '../shared/FilePickerModal';
import SkeletonLoader from '../shared/SkeletonLoader';
-import { FileId } from '../../types/file';
+import { FileId, StirlingFile } from '../../types/fileContext';
interface FileEditorProps {
- onOpenPageEditor?: (file: File) => void;
- onMergeFiles?: (files: File[]) => void;
+ onOpenPageEditor?: (file: StirlingFile) => void;
+ onMergeFiles?: (files: StirlingFile[]) => void;
toolMode?: boolean;
showUpload?: boolean;
showBulkActions?: boolean;
@@ -50,7 +50,7 @@ const FileEditor = ({
// Extract needed values from state (memoized to prevent infinite loops)
const activeFiles = useMemo(() => selectors.getFiles(), [selectors.getFilesSignature()]);
- const activeFileRecords = useMemo(() => selectors.getFileRecords(), [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 activeFileRecords directly - no conversion needed
+ // Use activeStirlingFileStubs directly - no conversion needed
const localSelectedIds = contextSelectedIds;
- // Helper to convert FileRecord 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(activeFileRecords.map(r => r.id)); // Use FileRecord IDs directly
- }, [activeFileRecords, setSelectedFiles]);
+ setSelectedFiles(activeStirlingFileStubs.map(r => r.id)); // Use StirlingFileStub IDs directly
+ }, [activeStirlingFileStubs, setSelectedFiles]);
const deselectAll = useCallback(() => setSelectedFiles([]), [setSelectedFiles]);
const closeAllFiles = useCallback(() => {
- if (activeFileRecords.length === 0) return;
+ if (activeStirlingFileStubs.length === 0) return;
// Remove all files from context but keep in storage
- const allFileIds = activeFileRecords.map(record => record.id);
+ const allFileIds = activeStirlingFileStubs.map(record => record.id);
removeFiles(allFileIds, false); // false = keep in storage
// Clear selections
setSelectedFiles([]);
- }, [activeFileRecords, removeFiles, setSelectedFiles]);
+ }, [activeStirlingFileStubs, removeFiles, setSelectedFiles]);
const toggleFile = useCallback((fileId: FileId) => {
const currentSelectedIds = contextSelectedIdsRef.current;
- const targetRecord = activeFileRecords.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, activeFileRecords]);
+ }, [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 = activeFileRecords.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`);
- }, [activeFileRecords, reorderFiles, setStatus]);
+ }, [activeStirlingFileStubs, reorderFiles, setStatus]);
// File operations using context
const handleDeleteFile = useCallback((fileId: FileId) => {
- const record = activeFileRecords.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);
}
- }, [activeFileRecords, selectors, removeFiles, setSelectedFiles, selectedFileIds]);
+ }, [activeStirlingFileStubs, selectors, removeFiles, setSelectedFiles, selectedFileIds]);
const handleViewFile = useCallback((fileId: FileId) => {
- const record = activeFileRecords.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');
}
- }, [activeFileRecords, setSelectedFiles, navActions.setWorkbench]);
+ }, [activeStirlingFileStubs, setSelectedFiles, navActions.setWorkbench]);
const handleMergeFromHere = useCallback((fileId: FileId) => {
- const startIndex = activeFileRecords.findIndex(r => r.id === fileId);
+ const startIndex = activeStirlingFileStubs.findIndex(r => r.id === fileId);
if (startIndex === -1) return;
- const recordsToMerge = activeFileRecords.slice(startIndex);
- const filesToMerge = recordsToMerge.map(r => selectors.getFile(r.id)).filter(Boolean) as File[];
+ const recordsToMerge = activeStirlingFileStubs.slice(startIndex);
+ const filesToMerge = recordsToMerge.map(r => selectors.getFile(r.id)).filter(Boolean) as StirlingFile[];
if (onMergeFiles) {
onMergeFiles(filesToMerge);
}
- }, [activeFileRecords, selectors, onMergeFiles]);
+ }, [activeStirlingFileStubs, selectors, onMergeFiles]);
const handleSplitFile = useCallback((fileId: FileId) => {
const file = selectors.getFile(fileId);
@@ -467,7 +467,7 @@ const FileEditor = ({
- {activeFileRecords.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
- ) : activeFileRecords.length === 0 && zipExtractionProgress.isExtracting ? (
+ ) : activeStirlingFileStubs.length === 0 && zipExtractionProgress.isExtracting ? (
@@ -522,7 +522,7 @@ const FileEditor = ({
pointerEvents: 'auto'
}}
>
- {activeFileRecords.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={activeFileRecords.length}
+ totalFiles={activeStirlingFileStubs.length}
selectedFiles={localSelectedIds}
selectionMode={selectionMode}
onToggleFile={toggleFile}
diff --git a/frontend/src/components/fileEditor/FileEditorThumbnail.tsx b/frontend/src/components/fileEditor/FileEditorThumbnail.tsx
index e82483898..bfeb404c5 100644
--- a/frontend/src/components/fileEditor/FileEditorThumbnail.tsx
+++ b/frontend/src/components/fileEditor/FileEditorThumbnail.tsx
@@ -61,8 +61,8 @@ const FileEditorThumbnail = ({
// Resolve the actual File object for pin/unpin operations
const actualFile = useMemo(() => {
- return activeFiles.find((f: File) => f.name === file.name && f.size === file.size);
- }, [activeFiles, file.name, file.size]);
+ return activeFiles.find(f => f.fileId === file.id);
+ }, [activeFiles, file.id]);
const isPinned = actualFile ? isFilePinned(actualFile) : false;
const downloadSelectedFile = useCallback(() => {
diff --git a/frontend/src/components/pageEditor/FileThumbnail.tsx b/frontend/src/components/pageEditor/FileThumbnail.tsx
index 1eda1f6c8..ad81ce463 100644
--- a/frontend/src/components/pageEditor/FileThumbnail.tsx
+++ b/frontend/src/components/pageEditor/FileThumbnail.tsx
@@ -61,8 +61,8 @@ const FileThumbnail = ({
// Resolve the actual File object for pin/unpin operations
const actualFile = useMemo(() => {
- return activeFiles.find((f: File) => f.name === file.name && f.size === file.size);
- }, [activeFiles, file.name, file.size]);
+ return activeFiles.find(f => f.fileId === file.id);
+ }, [activeFiles, file.id]);
const isPinned = actualFile ? isFilePinned(actualFile) : false;
const downloadSelectedFile = useCallback(() => {
diff --git a/frontend/src/components/pageEditor/hooks/usePageDocument.ts b/frontend/src/components/pageEditor/hooks/usePageDocument.ts
index b620c87b8..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 primaryFileRecord = primaryFileId ? selectors.getFileRecord(primaryFileId) : null;
- const processedFilePages = primaryFileRecord?.processedFile?.pages;
- const processedFileTotalPages = primaryFileRecord?.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 (!primaryFileRecord) {
+ if (!primaryStirlingFileStub) {
console.log('🎬 PageEditor: No primary file record found, showing loading');
return null;
}
const name =
activeFileIds.length === 1
- ? (primaryFileRecord.name ?? 'document.pdf')
+ ? (primaryStirlingFileStub.name ?? 'document.pdf')
: activeFileIds
- .map(id => (selectors.getFileRecord(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.getFileRecord(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 fileRecord = selectors.getFileRecord(fileId);
- if (!fileRecord) {
+ const stirlingFileStub = selectors.getStirlingFileStub(fileId);
+ if (!stirlingFileStub) {
return [];
}
- const processedFile = fileRecord.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, primaryFileRecord, 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 73ae01dba..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 { FileRecord } from "../../types/fileContext";
+import { StirlingFileStub } from "../../types/fileContext";
import { getFileSize, getFileDate } from "../../utils/fileUtils";
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
interface FileCardProps {
file: File;
- record?: FileRecord;
+ record?: StirlingFileStub;
onRemove: () => void;
onDoubleClick?: () => void;
onView?: () => void;
@@ -25,7 +25,7 @@ interface FileCardProps {
const FileCard = ({ file, record, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect, isSupported = true }: FileCardProps) => {
const { t } = useTranslation();
// Use record thumbnail if available, otherwise fall back to IndexedDB lookup
- const fileMetadata = record ? { id: record.id, name: file.name, type: file.type, size: file.size, lastModified: file.lastModified } : null;
+ const fileMetadata = record ? { id: record.id, name: record.name, type: record.type, size: record.size, lastModified: record.lastModified } : null;
const { thumbnail: indexedDBThumb, isGenerating } = useIndexedDBThumbnail(fileMetadata);
const thumb = record?.thumbnailUrl || indexedDBThumb;
const [isHovered, setIsHovered] = useState(false);
diff --git a/frontend/src/components/shared/FileGrid.tsx b/frontend/src/components/shared/FileGrid.tsx
index 1a43196d6..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 { FileRecord } from "../../types/fileContext";
+import { StirlingFileStub } from "../../types/fileContext";
import { FileId } from "../../types/file";
interface FileGridProps {
- files: Array<{ file: File; record?: FileRecord }>;
+ files: Array<{ file: File; record?: StirlingFileStub }>;
onRemove?: (index: number) => void;
- onDoubleClick?: (item: { file: File; record?: FileRecord }) => void;
- onView?: (item: { file: File; record?: FileRecord }) => void;
- onEdit?: (item: { file: File; record?: FileRecord }) => 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;
@@ -123,9 +123,17 @@ const FileGrid = ({
h="30rem"
style={{ overflowY: "auto", width: "100%" }}
>
- {displayFiles.map((item, idx) => {
- const fileId = item.record?.id || item.file.name as FileId /* FIX ME: This doesn't seem right */;
- const originalIdx = files.findIndex(f => (f.record?.id || f.file.name) === fileId);
+ {displayFiles
+ .filter(item => {
+ if (!item.record?.id) {
+ console.error('FileGrid: File missing StirlingFileStub with proper ID:', item.file.name);
+ return false;
+ }
+ return true;
+ })
+ .map((item, idx) => {
+ const fileId = item.record!.id; // Safe to assert after filter
+ const originalIdx = files.findIndex(f => f.record?.id === fileId);
const supported = isFileSupported ? isFileSupported(item.file.name) : true;
return (
{
@@ -85,7 +84,7 @@ export default function RightRail() {
if (currentView === 'fileEditor' || currentView === 'viewer') {
// Download selected files (or all if none selected)
const filesToDownload = selectedFiles.length > 0 ? selectedFiles : activeFiles;
-
+
filesToDownload.forEach(file => {
const link = document.createElement('a');
link.href = URL.createObjectURL(file);
@@ -206,8 +205,8 @@ export default function RightRail() {
)}
{/* Group: Selection controls + Close, animate as one unit when entering/leaving viewer */}
-
@@ -358,14 +357,14 @@ export default function RightRail() {
0 ? t('rightRail.downloadSelected', 'Download Selected Files') : t('rightRail.downloadAll', 'Download All'))
} position="left" offset={12} arrow>
-
void;
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
- selectedFiles: File[];
+ selectedFiles: StirlingFile[];
disabled?: boolean;
}
@@ -129,7 +129,7 @@ const ConvertSettings = ({
};
const filterFilesByExtension = (extension: string) => {
- const files = activeFiles.map(fileId => selectors.getFile(fileId)).filter(Boolean) as File[];
+ const files = activeFiles.map(fileId => selectors.getFile(fileId)).filter(Boolean) as StirlingFile[];
return files.filter(file => {
const fileExtension = detectFileExtension(file.name);
@@ -143,21 +143,8 @@ const ConvertSettings = ({
});
};
- const updateFileSelection = (files: File[]) => {
- // Map File objects to their actual IDs in FileContext
- const fileIds = files.map(file => {
- // Find the file ID by matching file properties
- const fileRecord = state.files.ids
- .map(id => selectors.getFileRecord(id))
- .find(record =>
- record &&
- record.name === file.name &&
- record.size === file.size &&
- record.lastModified === file.lastModified
- );
- return fileRecord?.id;
- }).filter((id): id is FileId => id !== undefined); // Type guard to ensure only strings
-
+ const updateFileSelection = (files: StirlingFile[]) => {
+ const fileIds = files.map(file => file.fileId);
setSelectedFiles(fileIds);
};
diff --git a/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx b/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx
index e1a662bd2..49e057a1c 100644
--- a/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx
+++ b/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx
@@ -3,11 +3,12 @@ import { Stack, Text, Select, Alert } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
import { usePdfSignatureDetection } from '../../../hooks/usePdfSignatureDetection';
+import { StirlingFile } from '../../../types/fileContext';
interface ConvertToPdfaSettingsProps {
parameters: ConvertParameters;
onParameterChange: (key: keyof ConvertParameters, value: any) => void;
- selectedFiles: File[];
+ selectedFiles: StirlingFile[];
disabled?: boolean;
}
diff --git a/frontend/src/components/tools/shared/FileStatusIndicator.tsx b/frontend/src/components/tools/shared/FileStatusIndicator.tsx
index 9b375fc2f..a083d65ef 100644
--- a/frontend/src/components/tools/shared/FileStatusIndicator.tsx
+++ b/frontend/src/components/tools/shared/FileStatusIndicator.tsx
@@ -6,9 +6,10 @@ import UploadIcon from '@mui/icons-material/Upload';
import { useFilesModalContext } from "../../../contexts/FilesModalContext";
import { useAllFiles } from "../../../contexts/FileContext";
import { useFileManager } from "../../../hooks/useFileManager";
+import { StirlingFile } from "../../../types/fileContext";
export interface FileStatusIndicatorProps {
- selectedFiles?: File[];
+ selectedFiles?: StirlingFile[];
placeholder?: string;
}
@@ -17,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);
@@ -55,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/components/tools/shared/FilesToolStep.tsx b/frontend/src/components/tools/shared/FilesToolStep.tsx
index b062e9c02..8c188d4a9 100644
--- a/frontend/src/components/tools/shared/FilesToolStep.tsx
+++ b/frontend/src/components/tools/shared/FilesToolStep.tsx
@@ -1,9 +1,10 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import FileStatusIndicator from './FileStatusIndicator';
+import { StirlingFile } from '../../../types/fileContext';
export interface FilesToolStepProps {
- selectedFiles: File[];
+ selectedFiles: StirlingFile[];
isCollapsed?: boolean;
onCollapsedClick?: () => void;
placeholder?: string;
diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx
index b6a7594c6..83057c13a 100644
--- a/frontend/src/components/tools/shared/createToolFlow.tsx
+++ b/frontend/src/components/tools/shared/createToolFlow.tsx
@@ -4,9 +4,10 @@ import { createToolSteps, ToolStepProvider } from './ToolStep';
import OperationButton from './OperationButton';
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
import { ToolWorkflowTitle, ToolWorkflowTitleProps } from './ToolWorkflowTitle';
+import { StirlingFile } from '../../../types/fileContext';
export interface FilesStepConfig {
- selectedFiles: File[];
+ selectedFiles: StirlingFile[];
isCollapsed?: boolean;
placeholder?: string;
onCollapsedClick?: () => void;
diff --git a/frontend/src/components/viewer/Viewer.tsx b/frontend/src/components/viewer/Viewer.tsx
index 0932e995b..53d9e98e2 100644
--- a/frontend/src/components/viewer/Viewer.tsx
+++ b/frontend/src/components/viewer/Viewer.tsx
@@ -15,6 +15,7 @@ import { fileStorage } from "../../services/fileStorage";
import SkeletonLoader from '../shared/SkeletonLoader';
import { useFileState, useFileActions, useCurrentFile } from "../../contexts/FileContext";
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
+import { isFileObject } from "../../types/fileContext";
import { FileId } from "../../types/file";
@@ -201,7 +202,7 @@ const Viewer = ({
const effectiveFile = React.useMemo(() => {
if (previewFile) {
// Validate the preview file
- if (!(previewFile instanceof File)) {
+ if (!isFileObject(previewFile)) {
return null;
}
diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx
index 39faa0643..80735de58 100644
--- a/frontend/src/contexts/FileContext.tsx
+++ b/frontend/src/contexts/FileContext.tsx
@@ -19,7 +19,10 @@ import {
FileContextStateValue,
FileContextActionsValue,
FileContextActions,
- FileRecord
+ FileId,
+ StirlingFileStub,
+ StirlingFile,
+ createStirlingFile
} from '../types/fileContext';
// Import modular components
@@ -29,7 +32,6 @@ import { AddedFile, addFiles, consumeFiles, undoConsumeFiles, createFileActions
import { FileLifecycleManager } from './file/lifecycle';
import { FileStateContext, FileActionsContext } from './file/contexts';
import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext';
-import { FileId } from '../types/file';
const DEBUG = process.env.NODE_ENV === 'development';
@@ -79,7 +81,7 @@ function FileContextInner({
}
// File operations using unified addFiles helper with persistence
- const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise => {
+ const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise => {
const addedFilesWithIds = await addFiles('raw', { files, ...options }, stateRef, filesRef, dispatch, lifecycleManager);
// Auto-select the newly added files if requested
@@ -98,15 +100,15 @@ function FileContextInner({
}));
}
- return addedFilesWithIds.map(({ file }) => file);
+ return addedFilesWithIds.map(({ file, id }) => createStirlingFile(file, id));
}, [indexedDB, enablePersistence]);
- const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise => {
+ const addProcessedFiles = useCallback(async (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>): Promise => {
const result = await addFiles('processed', { filesWithThumbnails }, stateRef, filesRef, dispatch, lifecycleManager);
- return result.map(({ file }) => file);
+ return result.map(({ file, id }) => createStirlingFile(file, id));
}, []);
- const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise => {
+ const addStoredFiles = useCallback(async (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: any }>, options?: { selectFiles?: boolean }): Promise => {
const result = await addFiles('stored', { filesWithMetadata }, stateRef, filesRef, dispatch, lifecycleManager);
// Auto-select the newly added files if requested
@@ -114,7 +116,7 @@ function FileContextInner({
selectFiles(result);
}
- return result.map(({ file }) => file);
+ return result.map(({ file, id }) => createStirlingFile(file, id));
}, []);
// Action creators
@@ -122,11 +124,11 @@ function FileContextInner({
// Helper functions for pinned files
const consumeFilesWrapper = useCallback(async (inputFileIds: FileId[], outputFiles: File[]): Promise => {
- return consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch, indexedDB);
+ return consumeFiles(inputFileIds, outputFiles, filesRef, dispatch, indexedDB);
}, [indexedDB]);
- const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputFileRecords: FileRecord[], outputFileIds: FileId[]): Promise => {
- return undoConsumeFiles(inputFiles, inputFileRecords, 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
@@ -140,24 +142,14 @@ function FileContextInner({
});
}, []);
- // File-to-ID wrapper functions for pinning
- const pinFileWrapper = useCallback((file: File) => {
- const fileId = findFileId(file);
- if (fileId) {
- baseActions.pinFile(fileId);
- } else {
- console.warn('File not found for pinning:', file.name);
- }
- }, [baseActions, findFileId]);
+ // File pinning functions - use StirlingFile directly
+ const pinFileWrapper = useCallback((file: StirlingFile) => {
+ baseActions.pinFile(file.fileId);
+ }, [baseActions]);
- const unpinFileWrapper = useCallback((file: File) => {
- const fileId = findFileId(file);
- if (fileId) {
- baseActions.unpinFile(fileId);
- } else {
- console.warn('File not found for unpinning:', file.name);
- }
- }, [baseActions, findFileId]);
+ const unpinFileWrapper = useCallback((file: StirlingFile) => {
+ baseActions.unpinFile(file.fileId);
+ }, [baseActions]);
// Complete actions object
const actions = useMemo(() => ({
@@ -178,8 +170,8 @@ function FileContextInner({
}
}
},
- updateFileRecord: (fileId: FileId, updates: Partial) =>
- lifecycleManager.updateFileRecord(fileId, updates, stateRef),
+ updateStirlingFileStub: (fileId: FileId, updates: Partial) =>
+ lifecycleManager.updateStirlingFileStub(fileId, updates, stateRef),
reorderFiles: (orderedFileIds: FileId[]) => {
dispatch({ type: 'REORDER_FILES', payload: { orderedFileIds } });
},
@@ -303,7 +295,7 @@ export {
useFileSelection,
useFileManagement,
useFileUI,
- useFileRecord,
+ 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 93c06c4d2..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,
- FileRecord
+ StirlingFileStub
} from '../../types/fileContext';
// Initial state
@@ -29,7 +29,7 @@ export const initialFileContextState: FileContextState = {
function processFileSwap(
state: FileContextState,
filesToRemove: FileId[],
- filesToAdd: FileRecord[]
+ 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 { fileRecords } = action.payload;
+ const { stirlingFileStubs } = action.payload;
const newIds: FileId[] = [];
- const newById: Record = { ...state.files.byId };
+ const newById: Record = { ...state.files.byId };
- fileRecords.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, outputFileRecords } = action.payload;
- return processFileSwap(state, inputFileIds, outputFileRecords);
+ const { inputFileIds, outputStirlingFileStubs } = action.payload;
+ return processFileSwap(state, inputFileIds, outputStirlingFileStubs);
}
case 'UNDO_CONSUME_FILES': {
- const { inputFileRecords, outputFileIds } = action.payload;
- return processFileSwap(state, outputFileIds, inputFileRecords);
+ 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 e55108553..3901fabee 100644
--- a/frontend/src/contexts/file/fileActions.ts
+++ b/frontend/src/contexts/file/fileActions.ts
@@ -3,10 +3,10 @@
*/
import {
- FileRecord,
+ StirlingFileStub,
FileContextAction,
FileContextState,
- toFileRecord,
+ toStirlingFileStub,
createFileId,
createQuickKey
} from '../../types/fileContext';
@@ -109,8 +109,8 @@ export async function addFiles(
await addFilesMutex.lock();
try {
- const fileRecords: FileRecord[] = [];
- const addedFiles: AddedFile[] = [];
+ const stirlingFileStubs: StirlingFileStub[] = [];
+ const addedFiles: AddedFile[] = [];
// Build quickKey lookup from existing files for deduplication
const existingQuickKeys = buildQuickKeySet(stateRef.current.files.byId);
@@ -163,7 +163,7 @@ export async function addFiles(
}
// Create record with immediate thumbnail and page metadata
- const record = toFileRecord(file, fileId);
+ const record = toStirlingFileStub(file, fileId);
if (thumbnail) {
record.thumbnailUrl = thumbnail;
// Track blob URLs for cleanup (images return blob URLs that need revocation)
@@ -184,7 +184,7 @@ export async function addFiles(
}
existingQuickKeys.add(quickKey);
- fileRecords.push(record);
+ stirlingFileStubs.push(record);
addedFiles.push({ file, id: fileId, thumbnail });
}
break;
@@ -205,7 +205,7 @@ export async function addFiles(
const fileId = createFileId();
filesRef.current.set(fileId, file);
- const record = toFileRecord(file, fileId);
+ const record = toStirlingFileStub(file, fileId);
if (thumbnail) {
record.thumbnailUrl = thumbnail;
// Track blob URLs for cleanup (images return blob URLs that need revocation)
@@ -226,7 +226,7 @@ export async function addFiles(
}
existingQuickKeys.add(quickKey);
- fileRecords.push(record);
+ stirlingFileStubs.push(record);
addedFiles.push({ file, id: fileId, thumbnail });
}
break;
@@ -254,7 +254,7 @@ export async function addFiles(
filesRef.current.set(fileId, file);
- const record = toFileRecord(file, fileId);
+ const record = toStirlingFileStub(file, fileId);
// Generate processedFile metadata for stored files
let pageCount: number = 1;
@@ -301,7 +301,7 @@ export async function addFiles(
}
existingQuickKeys.add(quickKey);
- fileRecords.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 (fileRecords.length > 0) {
- dispatch({ type: 'ADD_FILES', payload: { fileRecords } });
- if (DEBUG) console.log(`📄 addFiles(${kind}): Successfully added ${fileRecords.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