Compare commits

..

No commits in common. "9a8110a7cb4cd306ee343b684bc429c84bfd8fb5" and "9eae2573fd4bff5db6e7f543aeaee73e3b2830cc" have entirely different histories.

16 changed files with 67 additions and 68 deletions

View File

@ -15,16 +15,17 @@ export default defineConfig(
}, },
{ {
rules: { rules: {
"no-empty": "off", // Temporarily disabled until codebase conformant
"no-empty-pattern": "off", // Temporarily disabled until codebase conformant
"no-undef": "off", // Temporarily disabled until codebase conformant "no-undef": "off", // Temporarily disabled until codebase conformant
"@typescript-eslint/no-empty-object-type": [ "no-useless-escape": "off", // Temporarily disabled until codebase conformant
"error", "no-case-declarations": "off", // Temporarily disabled until codebase conformant
{ "prefer-const": "off", // Temporarily disabled until codebase conformant
// Allow empty extending interfaces because there's no real reason not to, and it makes it obvious where to put extra attributes in the future "@typescript-eslint/ban-ts-comment": "off", // Temporarily disabled until codebase conformant
allowInterfaces: 'with-single-extends', "@typescript-eslint/no-empty-object-type": "off", // Temporarily disabled until codebase conformant
},
],
"@typescript-eslint/no-explicit-any": "off", // Temporarily disabled until codebase conformant "@typescript-eslint/no-explicit-any": "off", // Temporarily disabled until codebase conformant
"@typescript-eslint/no-require-imports": "off", // Temporarily disabled until codebase conformant "@typescript-eslint/no-require-imports": "off", // Temporarily disabled until codebase conformant
"@typescript-eslint/no-unused-expressions": "off", // Temporarily disabled until codebase conformant
"@typescript-eslint/no-unused-vars": "off", // Temporarily disabled until codebase conformant "@typescript-eslint/no-unused-vars": "off", // Temporarily disabled until codebase conformant
}, },
} }

View File

@ -224,7 +224,7 @@ function getLicenseUrl(licenseType) {
// Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)" // Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)"
if (licenseType.includes('AND') || licenseType.includes('OR')) { if (licenseType.includes('AND') || licenseType.includes('OR')) {
// Extract the first license from compound expressions for URL // Extract the first license from compound expressions for URL
const match = licenseType.match(/\(?\s*([A-Za-z0-9\-.]+)/); const match = licenseType.match(/\(?\s*([A-Za-z0-9\-\.]+)/);
if (match && licenseUrls[match[1]]) { if (match && licenseUrls[match[1]]) {
return licenseUrls[match[1]]; return licenseUrls[match[1]];
} }

View File

@ -17,7 +17,8 @@ import {
getActiveNavButton, getActiveNavButton,
} from './quickAccessBar/QuickAccessBar'; } from './quickAccessBar/QuickAccessBar';
const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => { const QuickAccessBar = forwardRef<HTMLDivElement>(({
}, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isRainbowMode } = useRainbowThemeContext(); const { isRainbowMode } = useRainbowThemeContext();
const { openFilesModal, isFilesModalOpen } = useFilesModalContext(); const { openFilesModal, isFilesModalOpen } = useFilesModalContext();

View File

@ -82,8 +82,8 @@ export function adjustFontSizeToFit(
return () => { return () => {
cancelAnimationFrame(raf); cancelAnimationFrame(raf);
try { ro.disconnect(); } catch { /* Ignore errors */ } try { ro.disconnect(); } catch {}
try { mo.disconnect(); } catch { /* Ignore errors */ } try { mo.disconnect(); } catch {}
}; };
} }

View File

@ -3,6 +3,8 @@ import { Stack, Text, Divider, Card, Group } from '@mantine/core';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSuggestedTools } from '../../../hooks/useSuggestedTools'; import { useSuggestedTools } from '../../../hooks/useSuggestedTools';
export interface SuggestedToolsSectionProps {}
export function SuggestedToolsSection(): React.ReactElement { export function SuggestedToolsSection(): React.ReactElement {
const { t } = useTranslation(); const { t } = useTranslation();
const suggestedTools = useSuggestedTools(); const suggestedTools = useSuggestedTools();

View File

@ -126,7 +126,7 @@ const ToolSearch = ({
key={id} key={id}
variant="subtle" variant="subtle"
onClick={() => { onClick={() => {
onToolSelect?.(id); onToolSelect && onToolSelect(id);
setDropdownOpen(false); setDropdownOpen(false);
}} }}
leftSection={<div style={{ color: "var(--tools-text-and-icon-color)" }}>{tool.icon}</div>} leftSection={<div style={{ color: "var(--tools-text-and-icon-color)" }}>{tool.icon}</div>}

View File

@ -6,7 +6,7 @@ import {
StirlingFileStub, StirlingFileStub,
FileContextAction, FileContextAction,
FileContextState, FileContextState,
toStirlingFileStub, toWorkbenchFile,
createFileId, createFileId,
createQuickKey createQuickKey
} from '../../types/fileContext'; } from '../../types/fileContext';
@ -163,7 +163,7 @@ export async function addFiles(
} }
// Create record with immediate thumbnail and page metadata // Create record with immediate thumbnail and page metadata
const record = toStirlingFileStub(file, fileId); const record = toWorkbenchFile(file, fileId);
if (thumbnail) { if (thumbnail) {
record.thumbnailUrl = thumbnail; record.thumbnailUrl = thumbnail;
// Track blob URLs for cleanup (images return blob URLs that need revocation) // Track blob URLs for cleanup (images return blob URLs that need revocation)
@ -205,7 +205,7 @@ export async function addFiles(
const fileId = createFileId(); const fileId = createFileId();
filesRef.current.set(fileId, file); filesRef.current.set(fileId, file);
const record = toStirlingFileStub(file, fileId); const record = toWorkbenchFile(file, fileId);
if (thumbnail) { if (thumbnail) {
record.thumbnailUrl = thumbnail; record.thumbnailUrl = thumbnail;
// Track blob URLs for cleanup (images return blob URLs that need revocation) // Track blob URLs for cleanup (images return blob URLs that need revocation)
@ -254,7 +254,7 @@ export async function addFiles(
filesRef.current.set(fileId, file); filesRef.current.set(fileId, file);
const record = toStirlingFileStub(file, fileId); const record = toWorkbenchFile(file, fileId);
// Generate processedFile metadata for stored files // Generate processedFile metadata for stored files
let pageCount: number = 1; let pageCount: number = 1;
@ -347,7 +347,7 @@ async function processFilesIntoRecords(
if (DEBUG) console.warn(`📄 Failed to generate thumbnail for file ${file.name}:`, error); if (DEBUG) console.warn(`📄 Failed to generate thumbnail for file ${file.name}:`, error);
} }
const record = toStirlingFileStub(file, fileId); const record = toWorkbenchFile(file, fileId);
if (thumbnail) { if (thumbnail) {
record.thumbnailUrl = thumbnail; record.thumbnailUrl = thumbnail;
} }

View File

@ -347,9 +347,9 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
const malformedFiles: Array<{name: string}> = [ const malformedFiles: Array<{name: string}> = [
{ name: 'valid.pdf' }, { name: 'valid.pdf' },
// @ts-expect-error - Testing runtime resilience // @ts-ignore - Testing runtime resilience
{ name: null }, { name: null },
// @ts-expect-error - Testing runtime resilience // @ts-ignore
{ name: undefined } { name: undefined }
]; ];

View File

@ -172,7 +172,7 @@ export const useToolOperation = <TParams>(
const validRegularFiles = extractFiles(validFiles); const validRegularFiles = extractFiles(validFiles);
switch (config.toolType) { switch (config.toolType) {
case ToolType.singleFile: { case ToolType.singleFile:
// Individual file processing - separate API call per file // Individual file processing - separate API call per file
const apiCallsConfig: ApiCallsConfig<TParams> = { const apiCallsConfig: ApiCallsConfig<TParams> = {
endpoint: config.endpoint, endpoint: config.endpoint,
@ -188,9 +188,8 @@ export const useToolOperation = <TParams>(
actions.setStatus actions.setStatus
); );
break; break;
}
case ToolType.multiFile: { case ToolType.multiFile:
// Multi-file processing - single API call with all files // Multi-file processing - single API call with all files
actions.setStatus('Processing files...'); actions.setStatus('Processing files...');
const formData = config.buildFormData(params, validRegularFiles); const formData = config.buildFormData(params, validRegularFiles);
@ -203,7 +202,7 @@ export const useToolOperation = <TParams>(
// Use custom responseHandler for multi-file (handles ZIP extraction) // Use custom responseHandler for multi-file (handles ZIP extraction)
processedFiles = await config.responseHandler(response.data, validRegularFiles); processedFiles = await config.responseHandler(response.data, validRegularFiles);
} else if (response.data.type === 'application/pdf' || } else if (response.data.type === 'application/pdf' ||
(response.headers && response.headers['content-type'] === 'application/pdf')) { (response.headers && response.headers['content-type'] === 'application/pdf')) {
// Single PDF response (e.g. split with merge option) - use original filename // Single PDF response (e.g. split with merge option) - use original filename
const originalFileName = validRegularFiles[0]?.name || 'document.pdf'; const originalFileName = validRegularFiles[0]?.name || 'document.pdf';
const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' }); const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' });
@ -218,7 +217,6 @@ export const useToolOperation = <TParams>(
} }
} }
break; break;
}
case ToolType.custom: case ToolType.custom:
actions.setStatus('Processing files...'); actions.setStatus('Processing files...');
@ -252,12 +250,12 @@ export const useToolOperation = <TParams>(
inputFileIds.push(fileId); inputFileIds.push(fileId);
inputStirlingFileStubs.push(record); inputStirlingFileStubs.push(record);
} else { } else {
console.warn(`No file stub found for file: ${file.name}`); console.warn(`No file record found for file: ${file.name}`);
} }
} }
const outputFileIds = await consumeFiles(inputFileIds, processedFiles); const outputFileIds = await consumeFiles(inputFileIds, processedFiles);
// Store operation data for undo (only store what we need to avoid memory bloat) // Store operation data for undo (only store what we need to avoid memory bloat)
lastOperationRef.current = { lastOperationRef.current = {
inputFiles: extractFiles(validFiles), // Convert to File objects for undo inputFiles: extractFiles(validFiles), // Convert to File objects for undo
@ -321,17 +319,16 @@ export const useToolOperation = <TParams>(
// Undo the consume operation // Undo the consume operation
await undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds); await undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds);
// Clear results and operation tracking // Clear results and operation tracking
resetResults(); resetResults();
lastOperationRef.current = null; lastOperationRef.current = null;
// Show success message // Show success message
actions.setStatus(t('undoSuccess', 'Operation undone successfully')); actions.setStatus(t('undoSuccess', 'Operation undone successfully'));
} catch (error: any) { } catch (error: any) {
let errorMessage = extractErrorMessage(error); let errorMessage = extractErrorMessage(error);
// Provide more specific error messages based on error type // Provide more specific error messages based on error type
if (error.message?.includes('Mismatch between input files')) { if (error.message?.includes('Mismatch between input files')) {
errorMessage = t('undoDataMismatch', 'Cannot undo: operation data is corrupted'); errorMessage = t('undoDataMismatch', 'Cannot undo: operation data is corrupted');
@ -340,9 +337,9 @@ export const useToolOperation = <TParams>(
} else if (error.name === 'QuotaExceededError') { } else if (error.name === 'QuotaExceededError') {
errorMessage = t('undoQuotaError', 'Cannot undo: insufficient storage space'); errorMessage = t('undoQuotaError', 'Cannot undo: insufficient storage space');
} }
actions.setError(`${t('undoFailed', 'Failed to undo operation')}: ${errorMessage}`); actions.setError(`${t('undoFailed', 'Failed to undo operation')}: ${errorMessage}`);
// Don't clear the operation data if undo failed - user might want to try again // Don't clear the operation data if undo failed - user might want to try again
} }
}, [undoConsumeFiles, resetResults, actions, t]); }, [undoConsumeFiles, resetResults, actions, t]);

View File

@ -35,11 +35,8 @@ function updatePosthogConsent(){
return; return;
} }
const optIn = (window.CookieConsent as any).acceptedCategory('analytics'); const optIn = (window.CookieConsent as any).acceptedCategory('analytics');
if (optIn) { optIn?
posthog.opt_in_capturing(); posthog.opt_in_capturing() : posthog.opt_out_capturing();
} else {
posthog.opt_out_capturing();
}
console.log("Updated analytics consent: ", optIn? "opted in" : "opted out"); console.log("Updated analytics consent: ", optIn? "opted in" : "opted out");
} }

View File

@ -183,7 +183,7 @@ export class EnhancedPDFProcessingService {
): Promise<ProcessedFile> { ): Promise<ProcessedFile> {
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfWorkerManager.createDocument(arrayBuffer); const pdf = await pdfWorkerManager.createDocument(arrayBuffer);
try { try {
const totalPages = pdf.numPages; const totalPages = pdf.numPages;
@ -460,12 +460,11 @@ export class EnhancedPDFProcessingService {
case 'failed': case 'failed':
this.metrics.failedFiles++; this.metrics.failedFiles++;
break; break;
case 'cacheHit': { case 'cacheHit':
// Update cache hit rate // Update cache hit rate
const totalAttempts = this.metrics.totalFiles + 1; const totalAttempts = this.metrics.totalFiles + 1;
this.metrics.cacheHitRate = (this.metrics.cacheHitRate * this.metrics.totalFiles + 1) / totalAttempts; this.metrics.cacheHitRate = (this.metrics.cacheHitRate * this.metrics.totalFiles + 1) / totalAttempts;
break; break;
}
} }
} }
@ -521,7 +520,10 @@ export class EnhancedPDFProcessingService {
this.notifyListeners(); this.notifyListeners();
// Force memory cleanup hint // Force memory cleanup hint
setTimeout(() => window.gc?.(), 100); if (typeof window !== 'undefined' && window.gc) {
let gc = window.gc;
setTimeout(() => gc(), 100);
}
} }
/** /**

View File

@ -148,17 +148,15 @@ export class FileAnalyzer {
case 'immediate_full': case 'immediate_full':
return pageCount * baseTime; return pageCount * baseTime;
case 'priority_pages': { case 'priority_pages':
// Estimate time for priority pages (first 10) // Estimate time for priority pages (first 10)
const priorityPages = Math.min(pageCount, 10); const priorityPages = Math.min(pageCount, 10);
return priorityPages * baseTime; return priorityPages * baseTime;
}
case 'progressive_chunked': { case 'progressive_chunked':
// Estimate time for first chunk (20 pages) // Estimate time for first chunk (20 pages)
const firstChunk = Math.min(pageCount, 20); const firstChunk = Math.min(pageCount, 20);
return firstChunk * baseTime; return firstChunk * baseTime;
}
default: default:
return pageCount * baseTime; return pageCount * baseTime;

View File

@ -73,7 +73,7 @@ class IndexedDBManager {
request.onsuccess = () => { request.onsuccess = () => {
const db = request.result; const db = request.result;
console.log(`Successfully opened ${config.name}`); console.log(`Successfully opened ${config.name}`);
// Set up close handler to clean up our references // Set up close handler to clean up our references
db.onclose = () => { db.onclose = () => {
console.log(`Database ${config.name} closed`); console.log(`Database ${config.name} closed`);
@ -87,11 +87,13 @@ class IndexedDBManager {
request.onupgradeneeded = (event) => { request.onupgradeneeded = (event) => {
const db = request.result; const db = request.result;
const oldVersion = event.oldVersion; const oldVersion = event.oldVersion;
console.log(`Upgrading ${config.name} from v${oldVersion} to v${config.version}`); console.log(`Upgrading ${config.name} from v${oldVersion} to v${config.version}`);
// Create or update object stores // Create or update object stores
config.stores.forEach(storeConfig => { config.stores.forEach(storeConfig => {
let store: IDBObjectStore;
if (db.objectStoreNames.contains(storeConfig.name)) { if (db.objectStoreNames.contains(storeConfig.name)) {
// Store exists - for now, just continue (could add migration logic here) // Store exists - for now, just continue (could add migration logic here)
console.log(`Object store '${storeConfig.name}' already exists`); console.log(`Object store '${storeConfig.name}' already exists`);
@ -107,7 +109,7 @@ class IndexedDBManager {
options.autoIncrement = storeConfig.autoIncrement; options.autoIncrement = storeConfig.autoIncrement;
} }
const store = db.createObjectStore(storeConfig.name, options); store = db.createObjectStore(storeConfig.name, options);
console.log(`Created object store '${storeConfig.name}'`); console.log(`Created object store '${storeConfig.name}'`);
// Create indexes // Create indexes
@ -166,7 +168,7 @@ class IndexedDBManager {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const deleteRequest = indexedDB.deleteDatabase(name); const deleteRequest = indexedDB.deleteDatabase(name);
deleteRequest.onerror = () => reject(deleteRequest.error); deleteRequest.onerror = () => reject(deleteRequest.error);
deleteRequest.onsuccess = () => { deleteRequest.onsuccess = () => {
console.log(`Deleted database: ${name}`); console.log(`Deleted database: ${name}`);
@ -222,4 +224,4 @@ export const DATABASE_CONFIGS = {
} as DatabaseConfig } as DatabaseConfig
} as const; } as const;
export const indexedDBManager = IndexedDBManager.getInstance(); export const indexedDBManager = IndexedDBManager.getInstance();

View File

@ -1,6 +1,6 @@
/** /**
* PDF.js Worker Manager - Centralized worker lifecycle management * PDF.js Worker Manager - Centralized worker lifecycle management
* *
* Prevents infinite worker creation by managing PDF.js workers globally * Prevents infinite worker creation by managing PDF.js workers globally
* and ensuring proper cleanup when operations complete. * and ensuring proper cleanup when operations complete.
*/ */
@ -86,7 +86,7 @@ class PDFWorkerManager {
const pdf = await loadingTask.promise; const pdf = await loadingTask.promise;
this.activeDocuments.add(pdf); this.activeDocuments.add(pdf);
this.workerCount++; this.workerCount++;
return pdf; return pdf;
} catch (error) { } catch (error) {
// If document creation fails, make sure to clean up the loading task // If document creation fails, make sure to clean up the loading task
@ -94,7 +94,6 @@ class PDFWorkerManager {
try { try {
loadingTask.destroy(); loadingTask.destroy();
} catch (destroyError) { } catch (destroyError) {
// Ignore errors
} }
} }
throw error; throw error;
@ -126,7 +125,7 @@ class PDFWorkerManager {
documentsToDestroy.forEach(pdf => { documentsToDestroy.forEach(pdf => {
this.destroyDocument(pdf); this.destroyDocument(pdf);
}); });
this.activeDocuments.clear(); this.activeDocuments.clear();
this.workerCount = 0; this.workerCount = 0;
} }
@ -167,10 +166,9 @@ class PDFWorkerManager {
try { try {
pdf.destroy(); pdf.destroy();
} catch (error) { } catch (error) {
// Ignore errors
} }
}); });
this.activeDocuments.clear(); this.activeDocuments.clear();
this.workerCount = 0; this.workerCount = 0;
} }
@ -184,4 +182,4 @@ class PDFWorkerManager {
} }
// Export singleton instance // Export singleton instance
export const pdfWorkerManager = PDFWorkerManager.getInstance(); export const pdfWorkerManager = PDFWorkerManager.getInstance();

View File

@ -46,7 +46,7 @@ export interface ProcessedFileMetadata {
/** /**
* StirlingFileStub - Metadata record for files in the active workbench session * StirlingFileStub - Metadata record for files in the active workbench session
* *
* Contains UI display data and processing state. Actual File objects stored * Contains UI display data and processing state. Actual File objects stored
* separately in refs for memory efficiency. Supports multi-tool workflows * separately in refs for memory efficiency. Supports multi-tool workflows
* where files persist across tool operations. * where files persist across tool operations.
@ -117,7 +117,7 @@ export function createStirlingFile(file: File, id?: FileId): StirlingFile {
enumerable: true, enumerable: true,
configurable: false configurable: false
}); });
Object.defineProperty(file, 'quickKey', { Object.defineProperty(file, 'quickKey', {
value: quickKey, value: quickKey,
writable: false, writable: false,
@ -150,10 +150,7 @@ export function isFileObject(obj: any): obj is File | StirlingFile {
export function toStirlingFileStub( export function toWorkbenchFile(file: File, id?: FileId): StirlingFileStub {
file: File,
id?: FileId
): StirlingFileStub {
const fileId = id || createFileId(); const fileId = id || createFileId();
return { return {
id: fileId, id: fileId,
@ -353,3 +350,6 @@ export interface FileContextActionsValue {
actions: FileContextActions; actions: FileContextActions;
dispatch: (action: FileContextAction) => void; dispatch: (action: FileContextAction) => void;
} }
// TODO: URL parameter types will be redesigned for new routing system

View File

@ -1,6 +1,7 @@
// Base parameter interfaces for reusable patterns // Base parameter interfaces for reusable patterns
// Base interface that all tool parameters should extend export interface BaseParameters {
// Provides a foundation for adding common properties across all tools // Base interface that all tool parameters should extend
// Examples of future additions: userId, sessionId, commonFlags, etc. // Provides a foundation for adding common properties across all tools
export type BaseParameters = object // Examples of future additions: userId, sessionId, commonFlags, etc.
}