From 5bf024be4826af26d44d353652917fd6bc279b33 Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Fri, 5 Sep 2025 17:41:53 +0100 Subject: [PATCH] Builds --- .../fileEditor/FileEditorThumbnail.tsx | 2 +- .../hooks/tools/shared/useToolOperation.ts | 10 +- frontend/src/hooks/useFileHistory.ts | 18 +- frontend/src/services/pdfMetadataService.ts | 6 +- frontend/src/types/fileContext.ts | 3 +- frontend/src/utils/fileHistoryUtils.ts | 156 +++++++++--------- 6 files changed, 98 insertions(+), 97 deletions(-) diff --git a/frontend/src/components/fileEditor/FileEditorThumbnail.tsx b/frontend/src/components/fileEditor/FileEditorThumbnail.tsx index 85ccc41bc..4e6561b4d 100644 --- a/frontend/src/components/fileEditor/FileEditorThumbnail.tsx +++ b/frontend/src/components/fileEditor/FileEditorThumbnail.tsx @@ -66,7 +66,7 @@ const FileEditorThumbnail = ({ const isPinned = actualFile ? isFilePinned(actualFile) : false; // Get file record to access tool history - const fileRecord = selectors.getFileRecord(file.id); + const fileRecord = selectors.getStirlingFileStub(file.id); const toolHistory = fileRecord?.toolHistory || []; const hasToolHistory = toolHistory.length > 0; const versionNumber = fileRecord?.versionNumber || 0; diff --git a/frontend/src/hooks/tools/shared/useToolOperation.ts b/frontend/src/hooks/tools/shared/useToolOperation.ts index fc29e96bf..7bcfecace 100644 --- a/frontend/src/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/hooks/tools/shared/useToolOperation.ts @@ -129,7 +129,7 @@ export const useToolOperation = ( config: ToolOperationConfig ): ToolOperationHook => { const { t } = useTranslation(); - const { addFiles, consumeFiles, undoConsumeFiles, selectors } = useFileContext(); + const { addFiles, consumeFiles, undoConsumeFiles, selectors, findFileId } = useFileContext(); // Composed hooks const { state, actions } = useToolState(); @@ -168,14 +168,14 @@ export const useToolOperation = ( // Prepare files with history metadata injection (for PDFs) actions.setStatus('Preparing files...'); - const getFileRecord = (file: File) => { + const getFileStub = (file: File) => { const fileId = findFileId(file); - return fileId ? selectors.getFileRecord(fileId) : undefined; + return fileId ? selectors.getStirlingFileStub(fileId) : undefined; }; const filesWithHistory = await prepareFilesWithHistory( validFiles, - getFileRecord, + getFileStub, config.operationType, params as Record ); @@ -197,7 +197,6 @@ export const useToolOperation = ( }; processedFiles = await processFiles( params, - filesWithHistory, validRegularFiles, apiCallsConfig, actions.setProgress, @@ -205,7 +204,6 @@ export const useToolOperation = ( ); break; } - case ToolType.multiFile: { // Multi-file processing - single API call with all files actions.setStatus('Processing files...'); diff --git a/frontend/src/hooks/useFileHistory.ts b/frontend/src/hooks/useFileHistory.ts index 3af9c5231..881d9e6f5 100644 --- a/frontend/src/hooks/useFileHistory.ts +++ b/frontend/src/hooks/useFileHistory.ts @@ -5,7 +5,7 @@ import { useState, useCallback } from 'react'; import { FileId } from '../types/file'; -import { FileRecord } from '../types/fileContext'; +import { StirlingFileStub } from '../types/fileContext'; import { loadFileHistoryOnDemand } from '../utils/fileHistoryUtils'; interface FileHistoryState { @@ -23,7 +23,7 @@ interface UseFileHistoryResult { historyData: FileHistoryState | null; isLoading: boolean; error: string | null; - loadHistory: (file: File, fileId: FileId, updateFileRecord?: (id: FileId, updates: Partial) => void) => Promise; + loadHistory: (file: File, fileId: FileId, updateFileStub?: (id: FileId, updates: Partial) => void) => Promise; clearHistory: () => void; } @@ -35,13 +35,13 @@ export function useFileHistory(): UseFileHistoryResult { const loadHistory = useCallback(async ( file: File, fileId: FileId, - updateFileRecord?: (id: FileId, updates: Partial) => void + updateFileStub?: (id: FileId, updates: Partial) => void ) => { setIsLoading(true); setError(null); try { - const history = await loadFileHistoryOnDemand(file, fileId, updateFileRecord); + const history = await loadFileHistoryOnDemand(file, fileId, updateFileStub); setHistoryData(history); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to load file history'; @@ -78,7 +78,7 @@ export function useMultiFileHistory() { const loadFileHistory = useCallback(async ( file: File, fileId: FileId, - updateFileRecord?: (id: FileId, updates: Partial) => void + updateFileStub?: (id: FileId, updates: Partial) => void ) => { // Don't reload if already loaded or currently loading if (historyCache.has(fileId) || loadingFiles.has(fileId)) { @@ -93,12 +93,12 @@ export function useMultiFileHistory() { }); try { - const history = await loadFileHistoryOnDemand(file, fileId, updateFileRecord); - + const history = await loadFileHistoryOnDemand(file, fileId, updateFileStub); + if (history) { setHistoryCache(prev => new Map(prev).set(fileId, history)); } - + return history; } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to load file history'; @@ -157,4 +157,4 @@ export function useMultiFileHistory() { clearHistory, clearAllHistory }; -} \ No newline at end of file +} diff --git a/frontend/src/services/pdfMetadataService.ts b/frontend/src/services/pdfMetadataService.ts index 87a689bb1..9cf407db8 100644 --- a/frontend/src/services/pdfMetadataService.ts +++ b/frontend/src/services/pdfMetadataService.ts @@ -119,7 +119,11 @@ export class PDFMetadataService { }); } - return await pdfDoc.save(); + const savedPdfBytes = await pdfDoc.save(); + // Convert Uint8Array to ArrayBuffer + const arrayBuffer = new ArrayBuffer(savedPdfBytes.byteLength); + new Uint8Array(arrayBuffer).set(savedPdfBytes); + return arrayBuffer; } catch (error) { if (DEBUG) console.error('📄 Failed to inject PDF metadata:', error); // Return original bytes if metadata injection fails diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index b6feb8423..2277f612c 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -64,8 +64,7 @@ export interface StirlingFileStub { 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) - - isLeaf?: boolean; // True if this file is a leaf node (hasn't been processed yet) + 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 diff --git a/frontend/src/utils/fileHistoryUtils.ts b/frontend/src/utils/fileHistoryUtils.ts index 9857c08e7..5a8a71d33 100644 --- a/frontend/src/utils/fileHistoryUtils.ts +++ b/frontend/src/utils/fileHistoryUtils.ts @@ -6,19 +6,19 @@ */ import { pdfMetadataService, type ToolOperation } from '../services/pdfMetadataService'; -import { FileRecord } from '../types/fileContext'; +import { StirlingFileStub } from '../types/fileContext'; import { FileId, FileMetadata } from '../types/file'; import { createFileId } from '../types/fileContext'; const DEBUG = process.env.NODE_ENV === 'development'; /** - * Extract history information from a PDF file and update FileRecord + * Extract history information from a PDF file and update StirlingFileStub */ export async function extractFileHistory( file: File, - record: FileRecord -): Promise { + record: StirlingFileStub +): Promise { // Only process PDF files if (!file.type.includes('pdf')) { return record; @@ -52,7 +52,7 @@ export async function extractFileHistory( */ export async function injectHistoryForTool( file: File, - sourceFileRecord: FileRecord, + sourceStirlingFileStub: StirlingFileStub, toolName: string, parameters?: Record ): Promise { @@ -86,20 +86,20 @@ export async function injectHistoryForTool( const history = existingHistoryMetadata.stirlingHistory; newVersionNumber = history.versionNumber + 1; originalFileId = history.originalFileId; - parentFileId = sourceFileRecord.id; // This file becomes the parent + parentFileId = sourceStirlingFileStub.id; // This file becomes the parent parentToolChain = history.toolChain || []; - } else if (sourceFileRecord.originalFileId && sourceFileRecord.versionNumber) { + } else if (sourceStirlingFileStub.originalFileId && sourceStirlingFileStub.versionNumber) { // File record has history but PDF doesn't (shouldn't happen, but fallback) - newVersionNumber = sourceFileRecord.versionNumber + 1; - originalFileId = sourceFileRecord.originalFileId; - parentFileId = sourceFileRecord.id; - parentToolChain = sourceFileRecord.toolHistory || []; + 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 = sourceFileRecord.id; // Use source file ID as original - parentFileId = sourceFileRecord.id; // Parent is the source file + originalFileId = sourceStirlingFileStub.id; // Use source file ID as original + parentFileId = sourceStirlingFileStub.id; // Parent is the source file parentToolChain = []; // No previous tools } @@ -127,14 +127,14 @@ export async function injectHistoryForTool( */ export async function prepareFilesWithHistory( files: File[], - getFileRecord: (file: File) => FileRecord | undefined, + getStirlingFileStub: (file: File) => StirlingFileStub | undefined, toolName: string, parameters?: Record ): Promise { const processedFiles: File[] = []; for (const file of files) { - const record = getFileRecord(file); + const record = getStirlingFileStub(file); if (!record) { processedFiles.push(file); continue; @@ -180,57 +180,57 @@ export async function verifyToolMetadataPreservation( * Group files by processing branches - each branch ends in a leaf file * Returns Map where fileId is the leaf and lineagePath is the path back to original */ -export function groupFilesByOriginal(fileRecords: FileRecord[]): Map { - const groups = new Map(); +export function groupFilesByOriginal(StirlingFileStubs: StirlingFileStub[]): Map { + const groups = new Map(); // Create a map for quick lookups - const fileMap = new Map(); - for (const record of fileRecords) { + const fileMap = new Map(); + for (const record of StirlingFileStubs) { fileMap.set(record.id, record); } // Find leaf files (files that are not parents of any other files AND have version history) // Original files (v0) should only be leaves if they have no processed versions at all - const leafFiles = fileRecords.filter(record => { - const isParentOfOthers = fileRecords.some(otherRecord => otherRecord.parentFileId === record.id); - const isOriginalOfOthers = fileRecords.some(otherRecord => otherRecord.originalFileId === record.id); - + const leafFiles = StirlingFileStubs.filter(stub => { + const isParentOfOthers = StirlingFileStubs.some(otherStub => otherStub.parentFileId === stub.id); + const isOriginalOfOthers = StirlingFileStubs.some(otherStub => otherStub.originalFileId === stub.id); + // A file is a leaf if: // 1. It's not a parent of any other files, AND // 2. It has processing history (versionNumber > 0) OR it's not referenced as original by others - return !isParentOfOthers && (record.versionNumber && record.versionNumber > 0 || !isOriginalOfOthers); + return !isParentOfOthers && (stub.versionNumber && stub.versionNumber > 0 || !isOriginalOfOthers); }); // For each leaf file, build its complete lineage path back to original for (const leafFile of leafFiles) { - const lineagePath: FileRecord[] = []; - let currentFile: FileRecord | undefined = leafFile; - + const lineagePath: StirlingFileStub[] = []; + let currentFile: StirlingFileStub | undefined = leafFile; + // Trace back through parentFileId chain to build this specific branch while (currentFile) { lineagePath.push(currentFile); - + // Move to parent file in this branch - let nextFile: FileRecord | undefined = undefined; - + let nextFile: StirlingFileStub | undefined = undefined; + if (currentFile.parentFileId) { nextFile = fileMap.get(currentFile.parentFileId); } else if (currentFile.originalFileId && currentFile.originalFileId !== currentFile.id) { // For v1 files, the original file might be referenced by originalFileId nextFile = fileMap.get(currentFile.originalFileId); } - + // Check for infinite loops before moving to next if (nextFile && lineagePath.some(file => file.id === nextFile!.id)) { break; } - + currentFile = nextFile; } - + // Sort lineage with latest version first (leaf at top) lineagePath.sort((a, b) => (b.versionNumber || 0) - (a.versionNumber || 0)); - + // Use leaf file ID as the group key - each branch gets its own group groups.set(leafFile.id, lineagePath); } @@ -241,22 +241,22 @@ export function groupFilesByOriginal(fileRecords: FileRecord[]): Map record.isLeaf !== undefined); - + const hasLeafFlags = fileStubs.some(fileStub => fileStub.isLeaf !== undefined); + if (hasLeafFlags) { // Fast path: just return files marked as leaf nodes - return fileRecords.filter(record => record.isLeaf !== false); // Default to true if undefined + return fileStubs.filter(fileStub => fileStub.isLeaf !== false); // Default to true if undefined } else { // Fallback to expensive calculation for backward compatibility - const groups = groupFilesByOriginal(fileRecords); - const latestVersions: FileRecord[] = []; + const groups = groupFilesByOriginal(fileStubs); + const latestVersions: StirlingFileStub[] = []; - for (const [_, records] of groups) { - if (records.length > 0) { + for (const [_, fileStubs] of groups) { + if (fileStubs.length > 0) { // First item is the latest version (sorted desc by version number) - latestVersions.push(records[0]); + latestVersions.push(fileStubs[0]); } } @@ -268,15 +268,15 @@ export function getLatestVersions(fileRecords: FileRecord[]): FileRecord[] { * Get version history for a file */ export function getVersionHistory( - targetRecord: FileRecord, - allRecords: FileRecord[] -): FileRecord[] { - const originalId = targetRecord.originalFileId || targetRecord.id; + targetFileStub: StirlingFileStub, + allFileStubs: StirlingFileStub[] +): StirlingFileStub[] { + const originalId = targetFileStub.originalFileId || targetFileStub.id; - return allRecords - .filter(record => { - const recordOriginalId = record.originalFileId || record.id; - return recordOriginalId === originalId; + return allFileStubs + .filter(fileStub => { + const fileStubOriginalId = fileStub.originalFileId || fileStub.id; + return fileStubOriginalId === originalId; }) .sort((a, b) => (b.versionNumber || 0) - (a.versionNumber || 0)); } @@ -284,23 +284,23 @@ export function getVersionHistory( /** * Check if a file has version history */ -export function hasVersionHistory(record: FileRecord): boolean { - return !!(record.originalFileId && record.versionNumber && record.versionNumber > 0); +export function hasVersionHistory(fileStub: StirlingFileStub): boolean { + return !!(fileStub.originalFileId && fileStub.versionNumber && fileStub.versionNumber > 0); } /** * Generate a descriptive name for a file version */ -export function generateVersionName(record: FileRecord): string { - const baseName = record.name.replace(/\.pdf$/i, ''); +export function generateVersionName(fileStub: StirlingFileStub): string { + const baseName = fileStub.name.replace(/\.pdf$/i, ''); - if (!hasVersionHistory(record)) { - return record.name; + if (!hasVersionHistory(fileStub)) { + return fileStub.name; } - const versionInfo = record.versionNumber ? ` (v${record.versionNumber})` : ''; - const toolInfo = record.toolHistory && record.toolHistory.length > 0 - ? ` - ${record.toolHistory[record.toolHistory.length - 1].toolName}` + const versionInfo = fileStub.versionNumber ? ` (v${fileStub.versionNumber})` : ''; + const toolInfo = fileStub.toolHistory && fileStub.toolHistory.length > 0 + ? ` - ${fileStub.toolHistory[fileStub.toolHistory.length - 1].toolName}` : ''; return `${baseName}${versionInfo}${toolInfo}.pdf`; @@ -340,11 +340,11 @@ export async function getRecentLeafFileMetadata(): Promise { + fileStub: StirlingFileStub +): Promise { // Only process PDF files if (!file.type.includes('pdf')) { - return record; + return fileStub; } try { @@ -354,9 +354,9 @@ export async function extractBasicFileMetadata( if (historyMetadata) { const history = historyMetadata.stirlingHistory; - // Update record with essential metadata only (no parent/original relationships) + // Update fileStub with essential metadata only (no parent/original relationships) return { - ...record, + ...fileStub, versionNumber: history.versionNumber, toolHistory: history.toolChain }; @@ -365,7 +365,7 @@ export async function extractBasicFileMetadata( if (DEBUG) console.warn('📄 Failed to extract basic metadata:', file.name, error); } - return record; + return fileStub; } /** @@ -375,7 +375,7 @@ export async function extractBasicFileMetadata( export async function loadFileHistoryOnDemand( file: File, fileId: FileId, - updateFileRecord?: (id: FileId, updates: Partial) => void + updateFileStub?: (id: FileId, updates: Partial) => void ): Promise<{ originalFileId?: string; versionNumber?: number; @@ -392,7 +392,7 @@ export async function loadFileHistoryOnDemand( } try { - const baseRecord: FileRecord = { + const baseFileStub: StirlingFileStub = { id: fileId, name: file.name, size: file.size, @@ -400,19 +400,19 @@ export async function loadFileHistoryOnDemand( lastModified: file.lastModified }; - const updatedRecord = await extractFileHistory(file, baseRecord); - - if (updatedRecord !== baseRecord && (updatedRecord.originalFileId || updatedRecord.versionNumber)) { + const updatedFileStub = await extractFileHistory(file, baseFileStub); + + if (updatedFileStub !== baseFileStub && (updatedFileStub.originalFileId || updatedFileStub.versionNumber)) { const historyData = { - originalFileId: updatedRecord.originalFileId, - versionNumber: updatedRecord.versionNumber, - parentFileId: updatedRecord.parentFileId, - toolHistory: updatedRecord.toolHistory + originalFileId: updatedFileStub.originalFileId, + versionNumber: updatedFileStub.versionNumber, + parentFileId: updatedFileStub.parentFileId, + toolHistory: updatedFileStub.toolHistory }; - // Update the file record if update function is provided - if (updateFileRecord) { - updateFileRecord(fileId, historyData); + // Update the file stub if update function is provided + if (updateFileStub) { + updateFileStub(fileId, historyData); } return historyData;