Feature/v2/filehistory (#4370)

File History

---------

Co-authored-by: Connor Yoh <[email protected]>
This commit is contained in:
ConnorYoh
2025-09-16 15:08:11 +01:00
committed by GitHub
co-authored by Connor Yoh
parent 8e8b417f5e
commit 190178a471
61 changed files with 2279 additions and 1245 deletions
@@ -6,8 +6,9 @@ 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, StirlingFileStub } from '../../../types/fileContext';
import { StirlingFile, extractFiles, FileId, StirlingFileStub, createStirlingFile, createNewStirlingFileStub } from '../../../types/fileContext';
import { ResponseHandler } from '../../../utils/toolResponseProcessor';
import { createChildStub, generateProcessedFileMetadata } from '../../../contexts/file/fileActions';
// Re-export for backwards compatibility
export type { ProcessingProgress, ResponseHandler };
@@ -31,7 +32,7 @@ interface BaseToolOperationConfig<TParams> {
operationType: string;
/** Prefix added to processed filenames (e.g., 'compressed_', 'split_') */
filePrefix: string;
filePrefix?: string;
/**
* Whether to preserve the filename provided by the backend in response headers.
@@ -165,18 +166,20 @@ export const useToolOperation = <TParams>(
return;
}
// Reset state
actions.setLoading(true);
actions.setError(null);
actions.resetResults();
cleanupBlobUrls();
// Prepare files with history metadata injection (for PDFs)
actions.setStatus('Processing files...');
try {
let processedFiles: File[];
// Convert StirlingFile to regular File objects for API processing
const validRegularFiles = extractFiles(validFiles);
// Use original files directly (no PDF metadata injection - history stored in IndexedDB)
const filesForAPI = extractFiles(validFiles);
switch (config.toolType) {
case ToolType.singleFile: {
@@ -190,18 +193,17 @@ export const useToolOperation = <TParams>(
};
processedFiles = await processFiles(
params,
validRegularFiles,
filesForAPI,
apiCallsConfig,
actions.setProgress,
actions.setStatus
);
break;
}
case ToolType.multiFile: {
// Multi-file processing - single API call with all files
actions.setStatus('Processing files...');
const formData = config.buildFormData(params, validRegularFiles);
const formData = config.buildFormData(params, filesForAPI);
const endpoint = typeof config.endpoint === 'function' ? config.endpoint(params) : config.endpoint;
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
@@ -209,11 +211,11 @@ export const useToolOperation = <TParams>(
// Multi-file responses are typically ZIP files that need extraction, but some may return single PDFs
if (config.responseHandler) {
// Use custom responseHandler for multi-file (handles ZIP extraction)
processedFiles = await config.responseHandler(response.data, validRegularFiles);
processedFiles = await config.responseHandler(response.data, filesForAPI);
} else if (response.data.type === 'application/pdf' ||
(response.headers && response.headers['content-type'] === 'application/pdf')) {
// Single PDF response (e.g. split with merge option) - use original filename
const originalFileName = validRegularFiles[0]?.name || 'document.pdf';
const originalFileName = filesForAPI[0]?.name || 'document.pdf';
const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' });
processedFiles = [singleFile];
} else {
@@ -230,13 +232,14 @@ export const useToolOperation = <TParams>(
case ToolType.custom:
actions.setStatus('Processing files...');
processedFiles = await config.customProcessor(params, validRegularFiles);
processedFiles = await config.customProcessor(params, filesForAPI);
break;
}
if (processedFiles.length > 0) {
actions.setFiles(processedFiles);
// Generate thumbnails and download URL concurrently
actions.setGeneratingThumbnails(true);
const [thumbnails, downloadInfo] = await Promise.all([
@@ -264,7 +267,40 @@ export const useToolOperation = <TParams>(
}
}
const outputFileIds = await consumeFiles(inputFileIds, processedFiles);
// Create new tool operation
const newToolOperation = {
toolName: config.operationType,
timestamp: Date.now()
};
// Generate fresh processedFileMetadata for all processed files to ensure accuracy
actions.setStatus('Generating metadata for processed files...');
const processedFileMetadataArray = await Promise.all(
processedFiles.map(file => generateProcessedFileMetadata(file))
);
const shouldBranchHistory = processedFiles.length != inputStirlingFileStubs.length;
// Create output stubs with fresh metadata (no inheritance of stale processedFile data)
const outputStirlingFileStubs = shouldBranchHistory
? processedFiles.map((file, index) =>
createNewStirlingFileStub(file, undefined, thumbnails[index], processedFileMetadataArray[index])
)
: processedFiles.map((resultingFile, index) =>
createChildStub(
inputStirlingFileStubs[index],
newToolOperation,
resultingFile,
thumbnails[index],
processedFileMetadataArray[index]
)
);
// Create StirlingFile objects from processed files and child stubs
const outputStirlingFiles = processedFiles.map((file, index) => {
const childStub = outputStirlingFileStubs[index];
return createStirlingFile(file, childStub.id);
});
const outputFileIds = await consumeFiles(inputFileIds, outputStirlingFiles, outputStirlingFileStubs);
// Store operation data for undo (only store what we need to avoid memory bloat)
lastOperationRef.current = {