Restructure frontend code to allow for extensions (#4721)

# Description of Changes
Move frontend code into `core` folder and add infrastructure for
`proprietary` folder to include premium, non-OSS features
This commit is contained in:
James Brunton
2025-10-28 10:29:36 +00:00
committed by GitHub
parent 960d48f80c
commit d2b38ef4b8
725 changed files with 2485 additions and 2226 deletions
@@ -0,0 +1,47 @@
import { ToolType, useToolOperation } from '@app/hooks/tools/shared/useToolOperation';
import { useCallback } from 'react';
import { executeAutomationSequence } from '@app/utils/automationExecutor';
import { useToolRegistry } from '@app/contexts/ToolRegistryContext';
import { AutomateParameters } from '@app/types/automation';
export function useAutomateOperation() {
const { allTools } = useToolRegistry();
const toolRegistry = allTools;
const customProcessor = useCallback(async (params: AutomateParameters, files: File[]) => {
console.log('🚀 Starting automation execution via customProcessor', { params, files });
if (!params.automationConfig) {
throw new Error('No automation configuration provided');
}
// Execute the automation sequence and return the final results
const finalResults = await executeAutomationSequence(
params.automationConfig!,
files,
toolRegistry,
(stepIndex: number, operationName: string) => {
console.log(`Step ${stepIndex + 1} started: ${operationName}`);
params.onStepStart?.(stepIndex, operationName);
},
(stepIndex: number, resultFiles: File[]) => {
console.log(`Step ${stepIndex + 1} completed with ${resultFiles.length} files`);
params.onStepComplete?.(stepIndex, resultFiles);
},
(stepIndex: number, error: string) => {
console.error(`Step ${stepIndex + 1} failed:`, error);
params.onStepError?.(stepIndex, error);
throw new Error(`Automation step ${stepIndex + 1} failed: ${error}`);
}
);
console.log(`✅ Automation completed, returning ${finalResults.length} files`);
return finalResults;
}, [toolRegistry]);
return useToolOperation<AutomateParameters>({
toolType: ToolType.custom,
operationType: 'automate',
customProcessor,
});
}
@@ -0,0 +1,132 @@
import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { AutomationTool, AutomationConfig, AutomationMode } from '@app/types/automation';
import { AUTOMATION_CONSTANTS } from '@app/constants/automation';
import { ToolRegistry } from '@app/data/toolsTaxonomy';
import { ToolId } from "@app/types/toolId";
interface UseAutomationFormProps {
mode: AutomationMode;
existingAutomation?: AutomationConfig;
toolRegistry: Partial<ToolRegistry>;
}
export function useAutomationForm({ mode, existingAutomation, toolRegistry }: UseAutomationFormProps) {
const { t } = useTranslation();
const [automationName, setAutomationName] = useState('');
const [automationDescription, setAutomationDescription] = useState('');
const [automationIcon, setAutomationIcon] = useState<string>('');
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
const getToolName = useCallback((operation: string) => {
const tool = toolRegistry?.[operation as ToolId] as any;
return tool?.name || t(`tools.${operation}.name`, operation);
}, [toolRegistry, t]);
const getToolDefaultParameters = useCallback((operation: string): Record<string, any> => {
const config = toolRegistry[operation as ToolId]?.operationConfig;
if (config?.defaultParameters) {
return { ...config.defaultParameters };
}
return {};
}, [toolRegistry]);
// Initialize based on mode and existing automation
useEffect(() => {
if ((mode === AutomationMode.SUGGESTED || mode === AutomationMode.EDIT) && existingAutomation) {
setAutomationName(existingAutomation.name || '');
setAutomationDescription(existingAutomation.description || '');
setAutomationIcon(existingAutomation.icon || '');
const operations = existingAutomation.operations || [];
const tools = operations.map((op, index) => {
const operation = typeof op === 'string' ? op : op.operation;
const toolEntry = toolRegistry[operation as ToolId];
// If tool has no settingsComponent, it's automatically configured
const isConfigured = mode === AutomationMode.EDIT ? true : !toolEntry?.automationSettings;
return {
id: `${operation}-${Date.now()}-${index}`,
operation: operation,
name: getToolName(operation),
configured: isConfigured,
parameters: typeof op === 'object' ? op.parameters || {} : {}
};
});
setSelectedTools(tools);
} else if (mode === AutomationMode.CREATE && selectedTools.length === 0) {
// Initialize with default empty tools for new automation
const defaultTools = Array.from({ length: AUTOMATION_CONSTANTS.DEFAULT_TOOL_COUNT }, (_, index) => ({
id: `tool-${index + 1}-${Date.now()}`,
operation: '',
name: t('automate.creation.tools.selectTool', 'Select a tool...'),
configured: false,
parameters: {}
}));
setSelectedTools(defaultTools);
}
}, [mode, existingAutomation, t, getToolName]);
const addTool = (operation: string) => {
const toolEntry = toolRegistry[operation as ToolId];
// If tool has no settingsComponent, it's automatically configured
const isConfigured = !toolEntry?.automationSettings;
const newTool: AutomationTool = {
id: `${operation}-${Date.now()}`,
operation,
name: getToolName(operation),
configured: isConfigured,
parameters: getToolDefaultParameters(operation)
};
setSelectedTools([...selectedTools, newTool]);
};
const removeTool = (index: number) => {
if (selectedTools.length <= AUTOMATION_CONSTANTS.MIN_TOOL_COUNT) return;
setSelectedTools(selectedTools.filter((_, i) => i !== index));
};
const updateTool = (index: number, updates: Partial<AutomationTool>) => {
const updatedTools = [...selectedTools];
updatedTools[index] = { ...updatedTools[index], ...updates };
setSelectedTools(updatedTools);
};
const hasUnsavedChanges = () => {
return (
automationName.trim() !== '' ||
selectedTools.some(tool => tool.operation !== '' || tool.configured)
);
};
const canSaveAutomation = () => {
return (
automationName.trim() !== '' &&
selectedTools.length > 0 &&
selectedTools.every(tool => tool.configured && tool.operation !== '')
);
};
return {
automationName,
setAutomationName,
automationDescription,
setAutomationDescription,
automationIcon,
setAutomationIcon,
selectedTools,
setSelectedTools,
addTool,
removeTool,
updateTool,
hasUnsavedChanges,
canSaveAutomation,
getToolName,
getToolDefaultParameters
};
}
@@ -0,0 +1,94 @@
import { useState, useEffect, useCallback } from 'react';
import { AutomationConfig } from '@app/services/automationStorage';
import { SuggestedAutomation } from '@app/types/automation';
export interface SavedAutomation extends AutomationConfig {}
export function useSavedAutomations() {
const [savedAutomations, setSavedAutomations] = useState<SavedAutomation[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const loadSavedAutomations = useCallback(async () => {
try {
setLoading(true);
setError(null);
const { automationStorage } = await import('@app/services/automationStorage');
const automations = await automationStorage.getAllAutomations();
setSavedAutomations(automations);
} catch (err) {
console.error('Error loading saved automations:', err);
setError(err as Error);
setSavedAutomations([]);
} finally {
setLoading(false);
}
}, []);
const refreshAutomations = useCallback(() => {
loadSavedAutomations();
}, [loadSavedAutomations]);
const deleteAutomation = useCallback(async (id: string) => {
try {
const { automationStorage } = await import('@app/services/automationStorage');
await automationStorage.deleteAutomation(id);
// Refresh the list after deletion
refreshAutomations();
} catch (err) {
console.error('Error deleting automation:', err);
throw err;
}
}, [refreshAutomations]);
const copyFromSuggested = useCallback(async (suggestedAutomation: SuggestedAutomation) => {
try {
const { automationStorage } = await import('@app/services/automationStorage');
// Map suggested automation icons to MUI icon keys
const getIconKey = (_suggestedIcon: {id: string}): string => {
// Check the automation ID or name to determine the appropriate icon
switch (suggestedAutomation.id) {
case 'secure-pdf-ingestion':
case 'secure-workflow':
return 'SecurityIcon'; // Security icon for security workflows
case 'email-preparation':
return 'CompressIcon'; // Compression icon
case 'process-images':
return 'StarIcon'; // Star icon for process images
default:
return 'SettingsIcon'; // Default fallback
}
};
// Convert suggested automation to saved automation format
const savedAutomation = {
name: suggestedAutomation.name,
description: suggestedAutomation.description,
icon: getIconKey(suggestedAutomation.icon),
operations: suggestedAutomation.operations
};
await automationStorage.saveAutomation(savedAutomation);
// Refresh the list after saving
refreshAutomations();
} catch (err) {
console.error('Error copying suggested automation:', err);
throw err;
}
}, [refreshAutomations]);
// Load automations on mount
useEffect(() => {
loadSavedAutomations();
}, [loadSavedAutomations]);
return {
savedAutomations,
loading,
error,
refreshAutomations,
deleteAutomation,
copyFromSuggested
};
}
@@ -0,0 +1,194 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import React from 'react';
import LocalIcon from '@app/components/shared/LocalIcon';
import { SuggestedAutomation } from '@app/types/automation';
import { SPLIT_METHODS } from '@app/constants/splitConstants';
// Create icon components
const CompressIcon = () => React.createElement(LocalIcon, { icon: 'compress', width: '1.5rem', height: '1.5rem' });
const SecurityIcon = () => React.createElement(LocalIcon, { icon: 'security', width: '1.5rem', height: '1.5rem' });
const StarIcon = () => React.createElement(LocalIcon, { icon: 'star', width: '1.5rem', height: '1.5rem' });
export function useSuggestedAutomations(): SuggestedAutomation[] {
const { t } = useTranslation();
const suggestedAutomations = useMemo<SuggestedAutomation[]>(() => {
const now = new Date().toISOString();
return [
{
id: "secure-pdf-ingestion",
name: t("automation.suggested.securePdfIngestion", "Secure PDF Ingestion"),
description: t("automation.suggested.securePdfIngestionDesc", "Comprehensive PDF processing workflow that sanitizes documents, applies OCR with cleanup, converts to PDF/A format for long-term archival, and optimizes file size."),
operations: [
{
operation: "sanitize",
parameters: {
removeJavaScript: true,
removeEmbeddedFiles: true,
removeXMPMetadata: true,
removeMetadata: true,
removeLinks: false,
removeFonts: false,
}
},
{
operation: "ocr",
parameters: {
languages: ['eng'],
ocrType: 'skip-text',
ocrRenderType: 'hocr',
additionalOptions: ['clean', 'cleanFinal'],
}
},
{
operation: "convert",
parameters: {
fromExtension: 'pdf',
toExtension: 'pdfa',
pdfaOptions: {
outputFormat: 'pdfa-1',
}
}
},
{
operation: "compress",
parameters: {
compressionLevel: 5,
grayscale: false,
expectedSize: '',
compressionMethod: 'quality',
fileSizeValue: '',
fileSizeUnit: 'MB',
}
}
],
createdAt: now,
updatedAt: now,
icon: SecurityIcon,
},
{
id: "email-preparation",
name: t("automation.suggested.emailPreparation", "Email Preparation"),
description: t("automation.suggested.emailPreparationDesc", "Optimizes PDFs for email distribution by compressing files, splitting large documents into 20MB chunks for email compatibility, and removing metadata for privacy."),
operations: [
{
operation: "compress",
parameters: {
compressionLevel: 5,
grayscale: false,
expectedSize: '',
compressionMethod: 'quality',
fileSizeValue: '',
fileSizeUnit: 'MB',
}
},
{
operation: "split",
parameters: {
method: SPLIT_METHODS.BY_SIZE,
pages: '',
hDiv: '1',
vDiv: '1',
merge: false,
splitValue: '20MB',
bookmarkLevel: '1',
includeMetadata: false,
allowDuplicates: false,
duplexMode: false,
}
},
{
operation: "sanitize",
parameters: {
removeJavaScript: false,
removeEmbeddedFiles: false,
removeXMPMetadata: true,
removeMetadata: true,
removeLinks: false,
removeFonts: false,
}
}
],
createdAt: now,
updatedAt: now,
icon: CompressIcon,
},
{
id: "secure-workflow",
name: t("automation.suggested.secureWorkflow", "Security Workflow"),
description: t("automation.suggested.secureWorkflowDesc", "Secures PDF documents by removing potentially malicious content like JavaScript and embedded files, then adds password protection to prevent unauthorized access. Password is set to 'password' by default."),
operations: [
{
operation: "sanitize",
parameters: {
removeJavaScript: true,
removeEmbeddedFiles: true,
removeXMPMetadata: false,
removeMetadata: false,
removeLinks: false,
removeFonts: false,
}
},
{
operation: "addPassword",
parameters: {
password: 'password',
ownerPassword: '',
keyLength: 128,
permissions: {
preventAssembly: false,
preventExtractContent: false,
preventExtractForAccessibility: false,
preventFillInForm: false,
preventModify: false,
preventModifyAnnotations: false,
preventPrinting: false,
preventPrintingFaithful: false,
}
}
}
],
createdAt: now,
updatedAt: now,
icon: SecurityIcon,
},
{
id: "process-images",
name: t("automation.suggested.processImages", "Process Images"),
description: t("automation.suggested.processImagesDesc", "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."),
operations: [
{
operation: "convert",
parameters: {
fromExtension: 'image',
toExtension: 'pdf',
imageOptions: {
colorType: 'color',
dpi: 300,
singleOrMultiple: 'multiple',
fitOption: 'maintainAspectRatio',
autoRotate: true,
combineImages: true,
}
}
},
{
operation: "ocr",
parameters: {
languages: ['eng'],
ocrType: 'skip-text',
ocrRenderType: 'hocr',
additionalOptions: [],
}
}
],
createdAt: now,
updatedAt: now,
icon: StarIcon,
},
];
}, [t]);
return suggestedAutomations;
}