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
+24 -45
View File
@@ -7,55 +7,34 @@ declare const tag: unique symbol;
export type FileId = string & { readonly [tag]: 'FileId' };
/**
* File metadata for efficient operations without loading full file data
* Used by IndexedDBContext and FileContext for lazy file loading
* Tool operation metadata for history tracking
* Note: Parameters removed for security - sensitive data like passwords should not be stored in history
*/
export interface FileMetadata {
export interface ToolOperation {
toolName: string;
timestamp: number;
}
/**
* Base file metadata shared between storage and runtime layers
* Contains all common file properties and history tracking
*/
export interface BaseFileMetadata {
id: FileId;
name: string;
type: string;
size: number;
lastModified: number;
thumbnail?: string;
isDraft?: boolean; // Marks files as draft versions
createdAt?: number; // When file was added to system
// File history tracking
isLeaf: boolean; // True if this file hasn't been processed yet
originalFileId: string; // Root file ID for grouping versions
versionNumber: number; // Version number in chain
parentFileId?: FileId; // Immediate parent file ID
toolHistory?: Array<{
toolName: string;
timestamp: number;
}>; // Tool chain for history tracking
}
export interface StorageConfig {
useIndexedDB: boolean;
maxFileSize: number; // Maximum size per file in bytes
maxTotalStorage: number; // Maximum total storage in bytes
warningThreshold: number; // Warning threshold (percentage 0-1)
}
export const defaultStorageConfig: StorageConfig = {
useIndexedDB: true,
maxFileSize: 100 * 1024 * 1024, // 100MB per file
maxTotalStorage: 1024 * 1024 * 1024, // 1GB default, will be updated dynamically
warningThreshold: 0.8, // Warn at 80% capacity
};
// Calculate and update storage limit: half of available storage or 10GB, whichever is smaller
export const initializeStorageConfig = async (): Promise<StorageConfig> => {
const tenGB = 10 * 1024 * 1024 * 1024; // 10GB in bytes
const oneGB = 1024 * 1024 * 1024; // 1GB fallback
let maxTotalStorage = oneGB; // Default fallback
// Try to estimate available storage
if ('storage' in navigator && 'estimate' in navigator.storage) {
try {
const estimate = await navigator.storage.estimate();
if (estimate.quota) {
const halfQuota = estimate.quota / 2;
maxTotalStorage = Math.min(halfQuota, tenGB);
}
} catch (error) {
console.warn('Could not estimate storage quota, using 1GB default:', error);
}
}
return {
...defaultStorageConfig,
maxTotalStorage
};
};
+26 -16
View File
@@ -3,7 +3,7 @@
*/
import { PageOperation } from './pageEditor';
import { FileId, FileMetadata } from './file';
import { FileId, BaseFileMetadata } from './file';
// Re-export FileId for convenience
export type { FileId };
@@ -40,7 +40,6 @@ export interface ProcessedFilePage {
export interface ProcessedFileMetadata {
pages: ProcessedFilePage[];
totalPages?: number;
thumbnailUrl?: string;
lastProcessed?: number;
[key: string]: any;
}
@@ -52,16 +51,17 @@ export interface ProcessedFileMetadata {
* 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
/**
* StirlingFileStub - Runtime UI metadata 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 extends BaseFileMetadata {
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)
@@ -107,6 +107,11 @@ export function isStirlingFile(file: File): file is StirlingFile {
// Create a StirlingFile from a regular File object
export function createStirlingFile(file: File, id?: FileId): StirlingFile {
// Check if file is already a StirlingFile to avoid property redefinition
if (isStirlingFile(file)) {
return file; // Already has fileId and quickKey properties
}
const fileId = id || createFileId();
const quickKey = createQuickKey(file);
@@ -151,9 +156,11 @@ export function isFileObject(obj: any): obj is File | StirlingFile {
export function toStirlingFileStub(
export function createNewStirlingFileStub(
file: File,
id?: FileId
id?: FileId,
thumbnail?: string,
processedFileMetadata?: ProcessedFileMetadata
): StirlingFileStub {
const fileId = id || createFileId();
return {
@@ -162,8 +169,13 @@ export function toStirlingFileStub(
size: file.size,
type: file.type,
lastModified: file.lastModified,
originalFileId: fileId,
quickKey: createQuickKey(file),
createdAt: Date.now()
createdAt: Date.now(),
isLeaf: true, // New files are leaf nodes by default
versionNumber: 1, // New files start at version 1
thumbnailUrl: thumbnail,
processedFile: processedFileMetadata
};
}
@@ -209,7 +221,6 @@ export interface FileOperation {
metadata?: {
originalFileName?: string;
outputFileNames?: string[];
parameters?: Record<string, any>;
fileSize?: number;
pageCount?: number;
error?: string;
@@ -286,8 +297,7 @@ export type FileContextAction =
export interface FileContextActions {
// File management - lightweight actions only
addFiles: (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise<StirlingFile[]>;
addProcessedFiles: (filesWithThumbnails: Array<{ file: File; thumbnail?: string; pageCount?: number }>) => Promise<StirlingFile[]>;
addStoredFiles: (filesWithMetadata: Array<{ file: File; originalId: FileId; metadata: FileMetadata }>, options?: { selectFiles?: boolean }) => Promise<StirlingFile[]>;
addStirlingFileStubs: (stirlingFileStubs: StirlingFileStub[], options?: { insertAfterPageId?: string; selectFiles?: boolean }) => Promise<StirlingFile[]>;
removeFiles: (fileIds: FileId[], deleteFromStorage?: boolean) => Promise<void>;
updateStirlingFileStub: (id: FileId, updates: Partial<StirlingFileStub>) => void;
reorderFiles: (orderedFileIds: FileId[]) => void;
@@ -299,7 +309,7 @@ export interface FileContextActions {
unpinFile: (file: StirlingFile) => void;
// File consumption (replace unpinned files with outputs)
consumeFiles: (inputFileIds: FileId[], outputFiles: File[]) => Promise<FileId[]>;
consumeFiles: (inputFileIds: FileId[], outputStirlingFiles: StirlingFile[], outputStirlingFileStubs: StirlingFileStub[]) => Promise<FileId[]>;
undoConsumeFiles: (inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[]) => Promise<void>;
// Selection management
setSelectedFiles: (fileIds: FileId[]) => void;