mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user