mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 22:29:24 +00:00
improvement/v2/automate/tweaks (#4293)
- [x] Cleanup Automation output name garbage - [x] Remove Cross button on first two tools - [x] Automation creation name title to make clearer to the user - [x] Colours for dark mode on automation tool settings are bad - [x] Fix tool names not using correct translated ones - [x] suggested Automation Password needs adding to description - [x] Allow different filetypes in automation - [x] Custom Icons for automation - [x] split Tool wasn't working with merge to single pdf --------- Co-authored-by: Connor Yoh <connor@stirlingpdf.com> Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
parent
3d26b054f1
commit
47ccb6a6ed
@ -51,7 +51,6 @@
|
|||||||
"placeholder": "Select a PDF file in the main view to get started",
|
"placeholder": "Select a PDF file in the main view to get started",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"addFiles": "Add files",
|
"addFiles": "Add files",
|
||||||
"noFiles": "No files uploaded. ",
|
|
||||||
"selectFromWorkbench": "Select files from the workbench or "
|
"selectFromWorkbench": "Select files from the workbench or "
|
||||||
},
|
},
|
||||||
"noFavourites": "No favourites added",
|
"noFavourites": "No favourites added",
|
||||||
@ -2320,9 +2319,14 @@
|
|||||||
"creation": {
|
"creation": {
|
||||||
"createTitle": "Create Automation",
|
"createTitle": "Create Automation",
|
||||||
"editTitle": "Edit Automation",
|
"editTitle": "Edit Automation",
|
||||||
"description": "Automations run tools sequentially. To get started, add tools in the order you want them to run.",
|
"intro": "Automations run tools sequentially. To get started, add tools in the order you want them to run.",
|
||||||
"name": {
|
"name": {
|
||||||
"placeholder": "Automation name"
|
"label": "Automation Name",
|
||||||
|
"placeholder": "My Automation"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"label": "Description (optional)",
|
||||||
|
"placeholder": "Describe what this automation does..."
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"selectTool": "Select a tool...",
|
"selectTool": "Select a tool...",
|
||||||
@ -2339,6 +2343,9 @@
|
|||||||
"message": "You have unsaved changes. Are you sure you want to go back? All changes will be lost.",
|
"message": "You have unsaved changes. Are you sure you want to go back? All changes will be lost.",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"confirm": "Go Back"
|
"confirm": "Go Back"
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"label": "Icon"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"run": {
|
"run": {
|
||||||
@ -2369,7 +2376,7 @@
|
|||||||
"emailPreparation": "Email Preparation",
|
"emailPreparation": "Email Preparation",
|
||||||
"emailPreparationDesc": "Optimises PDFs for email distribution by compressing files, splitting large documents into 20MB chunks for email compatibility, and removing metadata for privacy.",
|
"emailPreparationDesc": "Optimises PDFs for email distribution by compressing files, splitting large documents into 20MB chunks for email compatibility, and removing metadata for privacy.",
|
||||||
"secureWorkflow": "Security Workflow",
|
"secureWorkflow": "Security Workflow",
|
||||||
"secureWorkflowDesc": "Secures PDF documents by removing potentially malicious content like JavaScript and embedded files, then adds password protection to prevent unauthorised access.",
|
"secureWorkflowDesc": "Secures PDF documents by removing potentially malicious content like JavaScript and embedded files, then adds password protection to prevent unauthorised access. Password is set to 'password' by default.",
|
||||||
"processImages": "Process Images",
|
"processImages": "Process Images",
|
||||||
"processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."
|
"processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."
|
||||||
}
|
}
|
||||||
|
@ -2111,10 +2111,10 @@
|
|||||||
"suggested": {
|
"suggested": {
|
||||||
"securePdfIngestion": "Secure PDF Ingestion",
|
"securePdfIngestion": "Secure PDF Ingestion",
|
||||||
"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.",
|
"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.",
|
||||||
"emailPreparation": "Email Preparation",
|
"emailPreparation": "Email Preparation",
|
||||||
"emailPreparationDesc": "Optimizes PDFs for email distribution by compressing files, splitting large documents into 20MB chunks for email compatibility, and removing metadata for privacy.",
|
"emailPreparationDesc": "Optimizes PDFs for email distribution by compressing files, splitting large documents into 20MB chunks for email compatibility, and removing metadata for privacy.",
|
||||||
"secureWorkflow": "Security Workflow",
|
"secureWorkflow": "Security Workflow",
|
||||||
"secureWorkflowDesc": "Secures PDF documents by removing potentially malicious content like JavaScript and embedded files, then adds password protection to prevent unauthorized access.",
|
"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.",
|
||||||
"processImages": "Process Images",
|
"processImages": "Process Images",
|
||||||
"processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."
|
"processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."
|
||||||
}
|
}
|
||||||
|
@ -32,9 +32,10 @@ const FileUploadButton = ({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
accept={accept}
|
accept={accept}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
||||||
>
|
>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Button {...props} variant={variant} fullWidth={fullWidth}>
|
<Button {...props} variant={variant} fullWidth={fullWidth} color="blue">
|
||||||
{file ? file.name : (placeholder || defaultPlaceholder)}
|
{file ? file.name : (placeholder || defaultPlaceholder)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
@ -22,7 +22,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>(({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
|
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
|
||||||
const { handleReaderToggle, handleBackToTools, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode } = useToolWorkflow();
|
const { handleReaderToggle, handleBackToTools, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode, resetTool } = useToolWorkflow();
|
||||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||||
const [activeButton, setActiveButton] = useState<string>('tools');
|
const [activeButton, setActiveButton] = useState<string>('tools');
|
||||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||||
@ -74,7 +74,12 @@ const QuickAccessBar = forwardRef<HTMLDivElement>(({
|
|||||||
type: 'navigation',
|
type: 'navigation',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setActiveButton('automate');
|
setActiveButton('automate');
|
||||||
handleToolSelect('automate');
|
// If already on automate tool, reset it directly
|
||||||
|
if (selectedToolKey === 'automate') {
|
||||||
|
resetTool('automate');
|
||||||
|
} else {
|
||||||
|
handleToolSelect('automate');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -16,7 +16,7 @@ const WatermarkTypeSettings = ({ watermarkType, onWatermarkTypeChange, disabled
|
|||||||
<div style={{ display: 'flex', gap: '4px' }}>
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
<Button
|
<Button
|
||||||
variant={watermarkType === 'text' ? 'filled' : 'outline'}
|
variant={watermarkType === 'text' ? 'filled' : 'outline'}
|
||||||
color={watermarkType === 'text' ? 'blue' : 'gray'}
|
color={watermarkType === 'text' ? 'blue' : 'var(--text-muted)'}
|
||||||
onClick={() => onWatermarkTypeChange('text')}
|
onClick={() => onWatermarkTypeChange('text')}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
@ -27,7 +27,7 @@ const WatermarkTypeSettings = ({ watermarkType, onWatermarkTypeChange, disabled
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={watermarkType === 'image' ? 'filled' : 'outline'}
|
variant={watermarkType === 'image' ? 'filled' : 'outline'}
|
||||||
color={watermarkType === 'image' ? 'blue' : 'gray'}
|
color={watermarkType === 'image' ? 'blue' : 'var(--text-muted)'}
|
||||||
onClick={() => onWatermarkTypeChange('image')}
|
onClick={() => onWatermarkTypeChange('image')}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Group,
|
Group,
|
||||||
TextInput,
|
TextInput,
|
||||||
|
Textarea,
|
||||||
Divider,
|
Divider,
|
||||||
Modal
|
Modal
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
@ -13,6 +14,7 @@ import CheckIcon from '@mui/icons-material/Check';
|
|||||||
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
import ToolConfigurationModal from './ToolConfigurationModal';
|
import ToolConfigurationModal from './ToolConfigurationModal';
|
||||||
import ToolList from './ToolList';
|
import ToolList from './ToolList';
|
||||||
|
import IconSelector from './IconSelector';
|
||||||
import { AutomationConfig, AutomationMode, AutomationTool } from '../../../types/automation';
|
import { AutomationConfig, AutomationMode, AutomationTool } from '../../../types/automation';
|
||||||
import { useAutomationForm } from '../../../hooks/tools/automate/useAutomationForm';
|
import { useAutomationForm } from '../../../hooks/tools/automate/useAutomationForm';
|
||||||
|
|
||||||
@ -31,6 +33,10 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
const {
|
const {
|
||||||
automationName,
|
automationName,
|
||||||
setAutomationName,
|
setAutomationName,
|
||||||
|
automationDescription,
|
||||||
|
setAutomationDescription,
|
||||||
|
automationIcon,
|
||||||
|
setAutomationIcon,
|
||||||
selectedTools,
|
selectedTools,
|
||||||
addTool,
|
addTool,
|
||||||
removeTool,
|
removeTool,
|
||||||
@ -100,7 +106,8 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
|
|
||||||
const automationData = {
|
const automationData = {
|
||||||
name: automationName.trim(),
|
name: automationName.trim(),
|
||||||
description: '',
|
description: automationDescription.trim(),
|
||||||
|
icon: automationIcon,
|
||||||
operations: selectedTools.map(tool => ({
|
operations: selectedTools.map(tool => ({
|
||||||
operation: tool.operation,
|
operation: tool.operation,
|
||||||
parameters: tool.parameters || {}
|
parameters: tool.parameters || {}
|
||||||
@ -114,7 +121,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
if (mode === AutomationMode.EDIT && existingAutomation) {
|
if (mode === AutomationMode.EDIT && existingAutomation) {
|
||||||
// For edit mode, check if name has changed
|
// For edit mode, check if name has changed
|
||||||
const nameChanged = automationName.trim() !== existingAutomation.name;
|
const nameChanged = automationName.trim() !== existingAutomation.name;
|
||||||
|
|
||||||
if (nameChanged) {
|
if (nameChanged) {
|
||||||
// Name changed - create new automation
|
// Name changed - create new automation
|
||||||
savedAutomation = await automationStorage.saveAutomation(automationData);
|
savedAutomation = await automationStorage.saveAutomation(automationData);
|
||||||
@ -144,17 +151,39 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Text size="sm" mb="md" p="md" style={{borderRadius:'var(--mantine-radius-md)', background: 'var(--color-gray-200)', color: 'var(--mantine-color-text)' }}>
|
<Text size="sm" mb="md" p="md" style={{borderRadius:'var(--mantine-radius-md)', background: 'var(--color-gray-200)', color: 'var(--mantine-color-text)' }}>
|
||||||
{t("automate.creation.description", "Automations run tools sequentially. To get started, add tools in the order you want them to run.")}
|
{t("automate.creation.intro", "Automations run tools sequentially. To get started, add tools in the order you want them to run.")}
|
||||||
</Text>
|
</Text>
|
||||||
<Divider mb="md" />
|
<Divider mb="md" />
|
||||||
|
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{/* Automation Name */}
|
{/* Automation Name and Icon */}
|
||||||
<TextInput
|
<Group gap="xs" align="flex-end">
|
||||||
placeholder={t('automate.creation.name.placeholder', 'Automation name')}
|
<Stack gap="xs" style={{ flex: 1 }}>
|
||||||
value={automationName}
|
<TextInput
|
||||||
onChange={(e) => setAutomationName(e.currentTarget.value)}
|
placeholder={t('automate.creation.name.placeholder', 'My Automation')}
|
||||||
|
value={automationName}
|
||||||
|
withAsterisk
|
||||||
|
label={t('automate.creation.name.label', 'Automation Name')}
|
||||||
|
onChange={(e) => setAutomationName(e.currentTarget.value)}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<IconSelector
|
||||||
|
value={automationIcon || 'SettingsIcon'}
|
||||||
|
onChange={setAutomationIcon}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Automation Description */}
|
||||||
|
<Textarea
|
||||||
|
placeholder={t('automate.creation.description.placeholder', 'Describe what this automation does...')}
|
||||||
|
value={automationDescription}
|
||||||
|
label={t('automate.creation.description.label', 'Description')}
|
||||||
|
onChange={(e) => setAutomationDescription(e.currentTarget.value)}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
rows={3}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import EditIcon from '@mui/icons-material/Edit';
|
|||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||||
import { Tooltip } from '../../shared/Tooltip';
|
import { Tooltip } from '../../shared/Tooltip';
|
||||||
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
|
|
||||||
interface AutomationEntryProps {
|
interface AutomationEntryProps {
|
||||||
/** Optional title for the automation (usually for custom ones) */
|
/** Optional title for the automation (usually for custom ones) */
|
||||||
@ -28,6 +29,8 @@ interface AutomationEntryProps {
|
|||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
/** Copy handler (for suggested automations) */
|
/** Copy handler (for suggested automations) */
|
||||||
onCopy?: () => void;
|
onCopy?: () => void;
|
||||||
|
/** Tool registry to resolve operation names */
|
||||||
|
toolRegistry?: Record<string, ToolRegistryEntry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutomationEntry({
|
export default function AutomationEntry({
|
||||||
@ -40,7 +43,8 @@ export default function AutomationEntry({
|
|||||||
showMenu = false,
|
showMenu = false,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onCopy
|
onCopy,
|
||||||
|
toolRegistry
|
||||||
}: AutomationEntryProps) {
|
}: AutomationEntryProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
@ -49,9 +53,19 @@ export default function AutomationEntry({
|
|||||||
// Keep item in hovered state if menu is open
|
// Keep item in hovered state if menu is open
|
||||||
const shouldShowHovered = isHovered || isMenuOpen;
|
const shouldShowHovered = isHovered || isMenuOpen;
|
||||||
|
|
||||||
|
// Helper function to resolve tool display names
|
||||||
|
const getToolDisplayName = (operation: string): string => {
|
||||||
|
if (toolRegistry?.[operation]?.name) {
|
||||||
|
return toolRegistry[operation].name;
|
||||||
|
}
|
||||||
|
// Fallback to translation or operation key
|
||||||
|
return t(`${operation}.title`, operation);
|
||||||
|
};
|
||||||
|
|
||||||
// Create tooltip content with description and tool chain
|
// Create tooltip content with description and tool chain
|
||||||
const createTooltipContent = () => {
|
const createTooltipContent = () => {
|
||||||
if (!description) return null;
|
// Show tooltip if there's a description OR if there are operations to show in the chain
|
||||||
|
if (!description && operations.length === 0) return null;
|
||||||
|
|
||||||
const toolChain = operations.map((op, index) => (
|
const toolChain = operations.map((op, index) => (
|
||||||
<React.Fragment key={`${op}-${index}`}>
|
<React.Fragment key={`${op}-${index}`}>
|
||||||
@ -68,7 +82,7 @@ export default function AutomationEntry({
|
|||||||
whiteSpace: 'nowrap'
|
whiteSpace: 'nowrap'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t(`${op}.title`, op)}
|
{getToolDisplayName(op)}
|
||||||
</Text>
|
</Text>
|
||||||
{index < operations.length - 1 && (
|
{index < operations.length - 1 && (
|
||||||
<Text component="span" size="sm" mx={4}>
|
<Text component="span" size="sm" mx={4}>
|
||||||
@ -80,12 +94,16 @@ export default function AutomationEntry({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ minWidth: '400px', width: 'auto' }}>
|
<div style={{ minWidth: '400px', width: 'auto' }}>
|
||||||
<Text size="sm" mb={8} style={{ whiteSpace: 'normal', wordWrap: 'break-word' }}>
|
{description && (
|
||||||
{description}
|
<Text size="sm" mb={8} style={{ whiteSpace: 'normal', wordWrap: 'break-word' }}>
|
||||||
</Text>
|
{description}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', whiteSpace: 'nowrap' }}>
|
</Text>
|
||||||
{toolChain}
|
)}
|
||||||
</div>
|
{operations.length > 0 && (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', whiteSpace: 'nowrap' }}>
|
||||||
|
{toolChain}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -122,7 +140,7 @@ export default function AutomationEntry({
|
|||||||
{operations.map((op, index) => (
|
{operations.map((op, index) => (
|
||||||
<React.Fragment key={`${op}-${index}`}>
|
<React.Fragment key={`${op}-${index}`}>
|
||||||
<Text size="xs" style={{ color: 'var(--mantine-color-text)' }}>
|
<Text size="xs" style={{ color: 'var(--mantine-color-text)' }}>
|
||||||
{t(`${op}.title`, op)}
|
{getToolDisplayName(op)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{index < operations.length - 1 && (
|
{index < operations.length - 1 && (
|
||||||
@ -221,8 +239,10 @@ export default function AutomationEntry({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only show tooltip if description exists, otherwise return plain content
|
// Show tooltip if there's a description OR operations to display
|
||||||
return description ? (
|
const shouldShowTooltip = description || operations.length > 0;
|
||||||
|
|
||||||
|
return shouldShowTooltip ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={createTooltipContent()}
|
content={createTooltipContent()}
|
||||||
position="right"
|
position="right"
|
||||||
|
@ -6,6 +6,8 @@ import SettingsIcon from "@mui/icons-material/Settings";
|
|||||||
import AutomationEntry from "./AutomationEntry";
|
import AutomationEntry from "./AutomationEntry";
|
||||||
import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSuggestedAutomations";
|
import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSuggestedAutomations";
|
||||||
import { AutomationConfig, SuggestedAutomation } from "../../../types/automation";
|
import { AutomationConfig, SuggestedAutomation } from "../../../types/automation";
|
||||||
|
import { iconMap } from './iconMap';
|
||||||
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
|
|
||||||
interface AutomationSelectionProps {
|
interface AutomationSelectionProps {
|
||||||
savedAutomations: AutomationConfig[];
|
savedAutomations: AutomationConfig[];
|
||||||
@ -14,6 +16,7 @@ interface AutomationSelectionProps {
|
|||||||
onEdit: (automation: AutomationConfig) => void;
|
onEdit: (automation: AutomationConfig) => void;
|
||||||
onDelete: (automation: AutomationConfig) => void;
|
onDelete: (automation: AutomationConfig) => void;
|
||||||
onCopyFromSuggested: (automation: SuggestedAutomation) => void;
|
onCopyFromSuggested: (automation: SuggestedAutomation) => void;
|
||||||
|
toolRegistry: Record<string, ToolRegistryEntry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutomationSelection({
|
export default function AutomationSelection({
|
||||||
@ -22,7 +25,8 @@ export default function AutomationSelection({
|
|||||||
onRun,
|
onRun,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onCopyFromSuggested
|
onCopyFromSuggested,
|
||||||
|
toolRegistry
|
||||||
}: AutomationSelectionProps) {
|
}: AutomationSelectionProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const suggestedAutomations = useSuggestedAutomations();
|
const suggestedAutomations = useSuggestedAutomations();
|
||||||
@ -40,20 +44,26 @@ export default function AutomationSelection({
|
|||||||
operations={[]}
|
operations={[]}
|
||||||
onClick={onCreateNew}
|
onClick={onCreateNew}
|
||||||
keepIconColor={true}
|
keepIconColor={true}
|
||||||
|
toolRegistry={toolRegistry}
|
||||||
/>
|
/>
|
||||||
{/* Saved Automations */}
|
{/* Saved Automations */}
|
||||||
{savedAutomations.map((automation) => (
|
{savedAutomations.map((automation) => {
|
||||||
<AutomationEntry
|
const IconComponent = automation.icon ? iconMap[automation.icon as keyof typeof iconMap] : SettingsIcon;
|
||||||
key={automation.id}
|
return (
|
||||||
title={automation.name}
|
<AutomationEntry
|
||||||
badgeIcon={SettingsIcon}
|
key={automation.id}
|
||||||
operations={automation.operations.map(op => typeof op === 'string' ? op : op.operation)}
|
title={automation.name}
|
||||||
onClick={() => onRun(automation)}
|
description={automation.description}
|
||||||
showMenu={true}
|
badgeIcon={IconComponent || SettingsIcon}
|
||||||
onEdit={() => onEdit(automation)}
|
operations={automation.operations.map(op => typeof op === 'string' ? op : op.operation)}
|
||||||
onDelete={() => onDelete(automation)}
|
onClick={() => onRun(automation)}
|
||||||
/>
|
showMenu={true}
|
||||||
))}
|
onEdit={() => onEdit(automation)}
|
||||||
|
onDelete={() => onDelete(automation)}
|
||||||
|
toolRegistry={toolRegistry}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<Divider pb='sm' />
|
<Divider pb='sm' />
|
||||||
|
|
||||||
{/* Suggested Automations */}
|
{/* Suggested Automations */}
|
||||||
@ -72,6 +82,7 @@ export default function AutomationSelection({
|
|||||||
onClick={() => onRun(automation)}
|
onClick={() => onRun(automation)}
|
||||||
showMenu={true}
|
showMenu={true}
|
||||||
onCopy={() => onCopyFromSuggested(automation)}
|
onCopy={() => onCopyFromSuggested(automation)}
|
||||||
|
toolRegistry={toolRegistry}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
116
frontend/src/components/tools/automate/IconSelector.tsx
Normal file
116
frontend/src/components/tools/automate/IconSelector.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Box, Text, Stack, Button, SimpleGrid, Tooltip, Popover } from "@mantine/core";
|
||||||
|
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
|
import { iconMap, iconOptions } from './iconMap';
|
||||||
|
|
||||||
|
interface IconSelectorProps {
|
||||||
|
value?: string;
|
||||||
|
onChange?: (iconKey: string) => void;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function IconSelector({ value = "SettingsIcon", onChange, size = "sm" }: IconSelectorProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
|
||||||
|
const selectedIconComponent = iconMap[value as keyof typeof iconMap] || iconMap.SettingsIcon;
|
||||||
|
|
||||||
|
const handleIconSelect = (iconKey: string) => {
|
||||||
|
onChange?.(iconKey);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconSize = size === "sm" ? 16 : size === "md" ? 18 : 20;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="1px">
|
||||||
|
<Text size="sm" fw={600} style={{ color: "var(--mantine-color-primary)" }}>
|
||||||
|
{t("automate.creation.icon.label", "Icon")}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
opened={isDropdownOpen}
|
||||||
|
onClose={() => setIsDropdownOpen(false)}
|
||||||
|
onDismiss={() => setIsDropdownOpen(false)}
|
||||||
|
position="bottom-start"
|
||||||
|
withArrow
|
||||||
|
trapFocus
|
||||||
|
>
|
||||||
|
<Popover.Target>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size={size}
|
||||||
|
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||||
|
style={{
|
||||||
|
width: size === "sm" ? "3.75rem" : "4.375rem",
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
paddingLeft: "0.5rem",
|
||||||
|
borderColor: "var(--mantine-color-gray-3)",
|
||||||
|
color: "var(--mantine-color-text)",
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{React.createElement(selectedIconComponent, { style: { fontSize: iconSize } })}
|
||||||
|
<KeyboardArrowDownIcon
|
||||||
|
style={{
|
||||||
|
fontSize: iconSize * 0.8,
|
||||||
|
position: "absolute",
|
||||||
|
right: "0.25rem",
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</Popover.Target>
|
||||||
|
|
||||||
|
<Popover.Dropdown>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<SimpleGrid cols={4} spacing="xs">
|
||||||
|
{iconOptions.map((option) => {
|
||||||
|
const IconComponent = iconMap[option.value as keyof typeof iconMap];
|
||||||
|
const isSelected = value === option.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip key={option.value} label={option.label}>
|
||||||
|
<Box
|
||||||
|
onClick={() => handleIconSelect(option.value)}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: "0.5rem",
|
||||||
|
borderRadius: "0.25rem",
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor: isSelected ? "var(--mantine-color-gray-1)" : "transparent",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (!isSelected) {
|
||||||
|
e.currentTarget.style.backgroundColor = "var(--mantine-color-gray-0)";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (!isSelected) {
|
||||||
|
e.currentTarget.style.backgroundColor = "transparent";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconComponent
|
||||||
|
style={{
|
||||||
|
fontSize: iconSize,
|
||||||
|
color: isSelected ? "var(--mantine-color-gray-9)" : "var(--mantine-color-gray-7)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Stack>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
@ -63,22 +63,24 @@ export default function ToolList({
|
|||||||
borderBottomWidth: tool.operation && !tool.configured ? "0" : "1px",
|
borderBottomWidth: tool.operation && !tool.configured ? "0" : "1px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Delete X in top right */}
|
{/* Delete X in top right - only show for tools after the first 2 */}
|
||||||
<ActionIcon
|
{index > 1 && (
|
||||||
variant="subtle"
|
<ActionIcon
|
||||||
size="xs"
|
variant="subtle"
|
||||||
onClick={() => onToolRemove(index)}
|
size="xs"
|
||||||
title={t("automate.creation.tools.remove", "Remove tool")}
|
onClick={() => onToolRemove(index)}
|
||||||
style={{
|
title={t("automate.creation.tools.remove", "Remove tool")}
|
||||||
position: "absolute",
|
style={{
|
||||||
top: "4px",
|
position: "absolute",
|
||||||
right: "4px",
|
top: "4px",
|
||||||
zIndex: 1,
|
right: "4px",
|
||||||
color: "var(--mantine-color-gray-6)",
|
zIndex: 1,
|
||||||
}}
|
color: "var(--mantine-color-gray-6)",
|
||||||
>
|
}}
|
||||||
<CloseIcon style={{ fontSize: 16 }} />
|
>
|
||||||
</ActionIcon>
|
<CloseIcon style={{ fontSize: 16 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
|
||||||
<div style={{ paddingRight: "1.25rem" }}>
|
<div style={{ paddingRight: "1.25rem" }}>
|
||||||
{/* Tool Selection Dropdown with inline settings cog */}
|
{/* Tool Selection Dropdown with inline settings cog */}
|
||||||
|
92
frontend/src/components/tools/automate/iconMap.ts
Normal file
92
frontend/src/components/tools/automate/iconMap.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
|
import CompressIcon from '@mui/icons-material/Compress';
|
||||||
|
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
||||||
|
import CleaningServicesIcon from '@mui/icons-material/CleaningServices';
|
||||||
|
import CropIcon from '@mui/icons-material/Crop';
|
||||||
|
import TextFieldsIcon from '@mui/icons-material/TextFields';
|
||||||
|
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import FolderIcon from '@mui/icons-material/Folder';
|
||||||
|
import CloudIcon from '@mui/icons-material/Cloud';
|
||||||
|
import StorageIcon from '@mui/icons-material/Storage';
|
||||||
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
|
import UploadIcon from '@mui/icons-material/Upload';
|
||||||
|
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||||
|
import RotateLeftIcon from '@mui/icons-material/RotateLeft';
|
||||||
|
import RotateRightIcon from '@mui/icons-material/RotateRight';
|
||||||
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
|
import ContentCutIcon from '@mui/icons-material/ContentCut';
|
||||||
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||||
|
import WorkIcon from '@mui/icons-material/Work';
|
||||||
|
import BuildIcon from '@mui/icons-material/Build';
|
||||||
|
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||||
|
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
||||||
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
import SecurityIcon from '@mui/icons-material/Security';
|
||||||
|
import StarIcon from '@mui/icons-material/Star';
|
||||||
|
|
||||||
|
export const iconMap = {
|
||||||
|
SettingsIcon,
|
||||||
|
CompressIcon,
|
||||||
|
SwapHorizIcon,
|
||||||
|
CleaningServicesIcon,
|
||||||
|
CropIcon,
|
||||||
|
TextFieldsIcon,
|
||||||
|
PictureAsPdfIcon,
|
||||||
|
EditIcon,
|
||||||
|
DeleteIcon,
|
||||||
|
FolderIcon,
|
||||||
|
CloudIcon,
|
||||||
|
StorageIcon,
|
||||||
|
SearchIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
UploadIcon,
|
||||||
|
PlayArrowIcon,
|
||||||
|
RotateLeftIcon,
|
||||||
|
RotateRightIcon,
|
||||||
|
VisibilityIcon,
|
||||||
|
ContentCutIcon,
|
||||||
|
ContentCopyIcon,
|
||||||
|
WorkIcon,
|
||||||
|
BuildIcon,
|
||||||
|
AutoAwesomeIcon,
|
||||||
|
SmartToyIcon,
|
||||||
|
CheckIcon,
|
||||||
|
SecurityIcon,
|
||||||
|
StarIcon
|
||||||
|
};
|
||||||
|
|
||||||
|
export const iconOptions = [
|
||||||
|
{ value: 'SettingsIcon', label: 'Settings' },
|
||||||
|
{ value: 'CompressIcon', label: 'Compress' },
|
||||||
|
{ value: 'SwapHorizIcon', label: 'Convert' },
|
||||||
|
{ value: 'CleaningServicesIcon', label: 'Clean' },
|
||||||
|
{ value: 'CropIcon', label: 'Crop' },
|
||||||
|
{ value: 'TextFieldsIcon', label: 'Text' },
|
||||||
|
{ value: 'PictureAsPdfIcon', label: 'PDF' },
|
||||||
|
{ value: 'EditIcon', label: 'Edit' },
|
||||||
|
{ value: 'DeleteIcon', label: 'Delete' },
|
||||||
|
{ value: 'FolderIcon', label: 'Folder' },
|
||||||
|
{ value: 'CloudIcon', label: 'Cloud' },
|
||||||
|
{ value: 'StorageIcon', label: 'Storage' },
|
||||||
|
{ value: 'SearchIcon', label: 'Search' },
|
||||||
|
{ value: 'DownloadIcon', label: 'Download' },
|
||||||
|
{ value: 'UploadIcon', label: 'Upload' },
|
||||||
|
{ value: 'PlayArrowIcon', label: 'Play' },
|
||||||
|
{ value: 'RotateLeftIcon', label: 'Rotate Left' },
|
||||||
|
{ value: 'RotateRightIcon', label: 'Rotate Right' },
|
||||||
|
{ value: 'VisibilityIcon', label: 'View' },
|
||||||
|
{ value: 'ContentCutIcon', label: 'Cut' },
|
||||||
|
{ value: 'ContentCopyIcon', label: 'Copy' },
|
||||||
|
{ value: 'WorkIcon', label: 'Work' },
|
||||||
|
{ value: 'BuildIcon', label: 'Build' },
|
||||||
|
{ value: 'AutoAwesomeIcon', label: 'Magic' },
|
||||||
|
{ value: 'SmartToyIcon', label: 'Robot' },
|
||||||
|
{ value: 'CheckIcon', label: 'Check' },
|
||||||
|
{ value: 'SecurityIcon', label: 'Security' },
|
||||||
|
{ value: 'StarIcon', label: 'Star' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export type IconKey = keyof typeof iconMap;
|
@ -23,7 +23,7 @@ const CompressSettings = ({ parameters, onParameterChange, disabled = false }: C
|
|||||||
<div style={{ display: 'flex', gap: '4px' }}>
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
<Button
|
<Button
|
||||||
variant={parameters.compressionMethod === 'quality' ? 'filled' : 'outline'}
|
variant={parameters.compressionMethod === 'quality' ? 'filled' : 'outline'}
|
||||||
color={parameters.compressionMethod === 'quality' ? 'blue' : 'gray'}
|
color={parameters.compressionMethod === 'quality' ? 'blue' : 'var(--text-muted)'}
|
||||||
onClick={() => onParameterChange('compressionMethod', 'quality')}
|
onClick={() => onParameterChange('compressionMethod', 'quality')}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
@ -34,7 +34,7 @@ const CompressSettings = ({ parameters, onParameterChange, disabled = false }: C
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={parameters.compressionMethod === 'filesize' ? 'filled' : 'outline'}
|
variant={parameters.compressionMethod === 'filesize' ? 'filled' : 'outline'}
|
||||||
color={parameters.compressionMethod === 'filesize' ? 'blue' : 'gray'}
|
color={parameters.compressionMethod === 'filesize' ? 'blue' : 'var(--text-muted)'}
|
||||||
onClick={() => onParameterChange('compressionMethod', 'filesize')}
|
onClick={() => onParameterChange('compressionMethod', 'filesize')}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
|
@ -86,6 +86,10 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
|
|||||||
selectTool: (toolId: string) => void;
|
selectTool: (toolId: string) => void;
|
||||||
clearToolSelection: () => void;
|
clearToolSelection: () => void;
|
||||||
|
|
||||||
|
// Tool Reset Actions
|
||||||
|
registerToolReset: (toolId: string, resetFunction: () => void) => void;
|
||||||
|
resetTool: (toolId: string) => void;
|
||||||
|
|
||||||
// Workflow Actions (compound actions)
|
// Workflow Actions (compound actions)
|
||||||
handleToolSelect: (toolId: string) => void;
|
handleToolSelect: (toolId: string) => void;
|
||||||
handleBackToTools: () => void;
|
handleBackToTools: () => void;
|
||||||
@ -106,16 +110,19 @@ interface ToolWorkflowProviderProps {
|
|||||||
export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||||
const [state, dispatch] = useReducer(toolWorkflowReducer, initialState);
|
const [state, dispatch] = useReducer(toolWorkflowReducer, initialState);
|
||||||
|
|
||||||
|
// Store reset functions for tools
|
||||||
|
const [toolResetFunctions, setToolResetFunctions] = React.useState<Record<string, () => void>>({});
|
||||||
|
|
||||||
// Navigation actions and state are available since we're inside NavigationProvider
|
// Navigation actions and state are available since we're inside NavigationProvider
|
||||||
const { actions } = useNavigationActions();
|
const { actions } = useNavigationActions();
|
||||||
const navigationState = useNavigationState();
|
const navigationState = useNavigationState();
|
||||||
|
|
||||||
// Tool management hook
|
// Tool management hook
|
||||||
const {
|
const {
|
||||||
toolRegistry,
|
toolRegistry,
|
||||||
getSelectedTool,
|
getSelectedTool,
|
||||||
} = useToolManagement();
|
} = useToolManagement();
|
||||||
|
|
||||||
// Get selected tool from navigation context
|
// Get selected tool from navigation context
|
||||||
const selectedTool = getSelectedTool(navigationState.selectedToolKey);
|
const selectedTool = getSelectedTool(navigationState.selectedToolKey);
|
||||||
|
|
||||||
@ -147,13 +154,29 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
dispatch({ type: 'SET_SEARCH_QUERY', payload: query });
|
dispatch({ type: 'SET_SEARCH_QUERY', payload: query });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Tool reset methods
|
||||||
|
const registerToolReset = useCallback((toolId: string, resetFunction: () => void) => {
|
||||||
|
setToolResetFunctions(prev => ({ ...prev, [toolId]: resetFunction }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetTool = useCallback((toolId: string) => {
|
||||||
|
// Use the current state directly instead of depending on the state in the closure
|
||||||
|
setToolResetFunctions(current => {
|
||||||
|
const resetFunction = current[toolId];
|
||||||
|
if (resetFunction) {
|
||||||
|
resetFunction();
|
||||||
|
}
|
||||||
|
return current; // Return the same state to avoid unnecessary updates
|
||||||
|
});
|
||||||
|
}, []); // Empty dependency array makes this stable
|
||||||
|
|
||||||
// Workflow actions (compound actions that coordinate multiple state changes)
|
// Workflow actions (compound actions that coordinate multiple state changes)
|
||||||
const handleToolSelect = useCallback((toolId: string) => {
|
const handleToolSelect = useCallback((toolId: string) => {
|
||||||
actions.handleToolSelect(toolId);
|
actions.handleToolSelect(toolId);
|
||||||
|
|
||||||
// Clear search query when selecting a tool
|
// Clear search query when selecting a tool
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
|
|
||||||
// Handle view switching logic
|
// Handle view switching logic
|
||||||
if (toolId === 'allTools' || toolId === 'read' || toolId === 'view-pdf') {
|
if (toolId === 'allTools' || toolId === 'read' || toolId === 'view-pdf') {
|
||||||
setLeftPanelView('toolPicker');
|
setLeftPanelView('toolPicker');
|
||||||
@ -212,6 +235,10 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
selectTool: actions.selectTool,
|
selectTool: actions.selectTool,
|
||||||
clearToolSelection: actions.clearToolSelection,
|
clearToolSelection: actions.clearToolSelection,
|
||||||
|
|
||||||
|
// Tool Reset Actions
|
||||||
|
registerToolReset,
|
||||||
|
resetTool,
|
||||||
|
|
||||||
// Workflow Actions
|
// Workflow Actions
|
||||||
handleToolSelect,
|
handleToolSelect,
|
||||||
handleBackToTools,
|
handleBackToTools,
|
||||||
@ -233,6 +260,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
actions.selectTool,
|
actions.selectTool,
|
||||||
actions.clearToolSelection,
|
actions.clearToolSelection,
|
||||||
|
registerToolReset,
|
||||||
|
resetTool,
|
||||||
handleToolSelect,
|
handleToolSelect,
|
||||||
handleBackToTools,
|
handleBackToTools,
|
||||||
handleReaderToggle,
|
handleReaderToggle,
|
||||||
@ -251,36 +280,6 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
export function useToolWorkflow(): ToolWorkflowContextValue {
|
export function useToolWorkflow(): ToolWorkflowContextValue {
|
||||||
const context = useContext(ToolWorkflowContext);
|
const context = useContext(ToolWorkflowContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
// During development hot reload, temporarily return a safe fallback
|
|
||||||
if (false && process.env.NODE_ENV === 'development') {
|
|
||||||
console.warn('ToolWorkflowContext temporarily unavailable during hot reload, using fallback');
|
|
||||||
|
|
||||||
// Return minimal safe fallback to prevent crashes
|
|
||||||
return {
|
|
||||||
sidebarsVisible: true,
|
|
||||||
leftPanelView: 'toolPicker',
|
|
||||||
readerMode: false,
|
|
||||||
previewFile: null,
|
|
||||||
pageEditorFunctions: null,
|
|
||||||
searchQuery: '',
|
|
||||||
selectedToolKey: null,
|
|
||||||
selectedTool: null,
|
|
||||||
toolRegistry: {},
|
|
||||||
filteredTools: [],
|
|
||||||
isPanelVisible: true,
|
|
||||||
setSidebarsVisible: () => {},
|
|
||||||
setLeftPanelView: () => {},
|
|
||||||
setReaderMode: () => {},
|
|
||||||
setPreviewFile: () => {},
|
|
||||||
setPageEditorFunctions: () => {},
|
|
||||||
setSearchQuery: () => {},
|
|
||||||
selectTool: () => {},
|
|
||||||
clearToolSelection: () => {},
|
|
||||||
handleToolSelect: () => {},
|
|
||||||
handleBackToTools: () => {},
|
|
||||||
handleReaderToggle: () => {}
|
|
||||||
} as ToolWorkflowContextValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('ToolWorkflowContext not found. Current stack:', new Error().stack);
|
console.error('ToolWorkflowContext not found. Current stack:', new Error().stack);
|
||||||
throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider');
|
throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider');
|
||||||
|
@ -42,6 +42,26 @@ import ChangePermissionsSettings from '../components/tools/changePermissions/Cha
|
|||||||
|
|
||||||
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
||||||
|
|
||||||
|
// Convert tool supported file formats
|
||||||
|
export const CONVERT_SUPPORTED_FORMATS = [
|
||||||
|
// Microsoft Office
|
||||||
|
"doc", "docx", "dot", "dotx", "csv", "xls", "xlsx", "xlt", "xltx", "slk", "dif", "ppt", "pptx",
|
||||||
|
// OpenDocument
|
||||||
|
"odt", "ott", "ods", "ots", "odp", "otp", "odg", "otg",
|
||||||
|
// Text formats
|
||||||
|
"txt", "text", "xml", "rtf", "html", "lwp", "md",
|
||||||
|
// Images
|
||||||
|
"bmp", "gif", "jpeg", "jpg", "png", "tif", "tiff", "pbm", "pgm", "ppm", "ras", "xbm", "xpm", "svg", "svm", "wmf", "webp",
|
||||||
|
// StarOffice
|
||||||
|
"sda", "sdc", "sdd", "sdw", "stc", "std", "sti", "stw", "sxd", "sxg", "sxi", "sxw",
|
||||||
|
// Email formats
|
||||||
|
"eml",
|
||||||
|
// Archive formats
|
||||||
|
"zip",
|
||||||
|
// Other
|
||||||
|
"dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf"
|
||||||
|
];
|
||||||
|
|
||||||
// Hook to get the translated tool registry
|
// Hook to get the translated tool registry
|
||||||
export function useFlatToolRegistry(): ToolRegistry {
|
export function useFlatToolRegistry(): ToolRegistry {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -394,6 +414,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.AUTOMATION,
|
subcategoryId: SubcategoryId.AUTOMATION,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
|
supportedFormats: CONVERT_SUPPORTED_FORMATS,
|
||||||
endpoints: ["handleData"]
|
endpoints: ["handleData"]
|
||||||
},
|
},
|
||||||
"auto-rename-pdf-file": {
|
"auto-rename-pdf-file": {
|
||||||
@ -589,6 +610,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.GENERAL,
|
subcategoryId: SubcategoryId.GENERAL,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
|
supportedFormats: CONVERT_SUPPORTED_FORMATS,
|
||||||
endpoints: [
|
endpoints: [
|
||||||
"pdf-to-img",
|
"pdf-to-img",
|
||||||
"img-to-pdf",
|
"img-to-pdf",
|
||||||
@ -605,24 +627,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
"pdf-to-pdfa",
|
"pdf-to-pdfa",
|
||||||
"eml-to-pdf"
|
"eml-to-pdf"
|
||||||
],
|
],
|
||||||
supportedFormats: [
|
|
||||||
// Microsoft Office
|
|
||||||
"doc", "docx", "dot", "dotx", "csv", "xls", "xlsx", "xlt", "xltx", "slk", "dif", "ppt", "pptx",
|
|
||||||
// OpenDocument
|
|
||||||
"odt", "ott", "ods", "ots", "odp", "otp", "odg", "otg",
|
|
||||||
// Text formats
|
|
||||||
"txt", "text", "xml", "rtf", "html", "lwp", "md",
|
|
||||||
// Images
|
|
||||||
"bmp", "gif", "jpeg", "jpg", "png", "tif", "tiff", "pbm", "pgm", "ppm", "ras", "xbm", "xpm", "svg", "svm", "wmf", "webp",
|
|
||||||
// StarOffice
|
|
||||||
"sda", "sdc", "sdd", "sdw", "stc", "std", "sti", "stw", "sxd", "sxg", "sxi", "sxw",
|
|
||||||
// Email formats
|
|
||||||
"eml",
|
|
||||||
// Archive formats
|
|
||||||
"zip",
|
|
||||||
// Other
|
|
||||||
"dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf"
|
|
||||||
],
|
|
||||||
operationConfig: convertOperationConfig,
|
operationConfig: convertOperationConfig,
|
||||||
settingsComponent: ConvertSettings
|
settingsComponent: ConvertSettings
|
||||||
},
|
},
|
||||||
|
@ -44,6 +44,6 @@ export function useAutomateOperation() {
|
|||||||
endpoint: '/api/v1/pipeline/handleData', // Not used with customProcessor
|
endpoint: '/api/v1/pipeline/handleData', // Not used with customProcessor
|
||||||
buildFormData: () => new FormData(), // Not used with customProcessor
|
buildFormData: () => new FormData(), // Not used with customProcessor
|
||||||
customProcessor,
|
customProcessor,
|
||||||
filePrefix: AUTOMATION_CONSTANTS.FILE_PREFIX
|
filePrefix: '' // No prefix needed since automation handles naming internally
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -14,6 +14,8 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [automationName, setAutomationName] = useState('');
|
const [automationName, setAutomationName] = useState('');
|
||||||
|
const [automationDescription, setAutomationDescription] = useState('');
|
||||||
|
const [automationIcon, setAutomationIcon] = useState<string>('');
|
||||||
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
|
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
|
||||||
|
|
||||||
const getToolName = useCallback((operation: string) => {
|
const getToolName = useCallback((operation: string) => {
|
||||||
@ -33,6 +35,8 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((mode === AutomationMode.SUGGESTED || mode === AutomationMode.EDIT) && existingAutomation) {
|
if ((mode === AutomationMode.SUGGESTED || mode === AutomationMode.EDIT) && existingAutomation) {
|
||||||
setAutomationName(existingAutomation.name || '');
|
setAutomationName(existingAutomation.name || '');
|
||||||
|
setAutomationDescription(existingAutomation.description || '');
|
||||||
|
setAutomationIcon(existingAutomation.icon || '');
|
||||||
|
|
||||||
const operations = existingAutomation.operations || [];
|
const operations = existingAutomation.operations || [];
|
||||||
const tools = operations.map((op, index) => {
|
const tools = operations.map((op, index) => {
|
||||||
@ -101,6 +105,10 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
|
|||||||
return {
|
return {
|
||||||
automationName,
|
automationName,
|
||||||
setAutomationName,
|
setAutomationName,
|
||||||
|
automationDescription,
|
||||||
|
setAutomationDescription,
|
||||||
|
automationIcon,
|
||||||
|
setAutomationIcon,
|
||||||
selectedTools,
|
selectedTools,
|
||||||
setSelectedTools,
|
setSelectedTools,
|
||||||
addTool,
|
addTool,
|
||||||
|
@ -45,10 +45,27 @@ export function useSavedAutomations() {
|
|||||||
try {
|
try {
|
||||||
const { automationStorage } = await import('../../../services/automationStorage');
|
const { automationStorage } = await import('../../../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
|
// Convert suggested automation to saved automation format
|
||||||
const savedAutomation = {
|
const savedAutomation = {
|
||||||
name: suggestedAutomation.name,
|
name: suggestedAutomation.name,
|
||||||
description: suggestedAutomation.description,
|
description: suggestedAutomation.description,
|
||||||
|
icon: getIconKey(suggestedAutomation.icon),
|
||||||
operations: suggestedAutomation.operations
|
operations: suggestedAutomation.operations
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ export function useSuggestedAutomations(): SuggestedAutomation[] {
|
|||||||
{
|
{
|
||||||
id: "secure-workflow",
|
id: "secure-workflow",
|
||||||
name: t("automation.suggested.secureWorkflow", "Security 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."),
|
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: [
|
operations: [
|
||||||
{
|
{
|
||||||
operation: "sanitize",
|
operation: "sanitize",
|
||||||
|
@ -156,10 +156,16 @@ export const useToolOperation = <TParams = void>(
|
|||||||
|
|
||||||
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
||||||
|
|
||||||
// Multi-file responses are typically ZIP files that need extraction
|
// Multi-file responses are typically ZIP files that need extraction, but some may return single PDFs
|
||||||
if (config.responseHandler) {
|
if (config.responseHandler) {
|
||||||
// Use custom responseHandler for multi-file (handles ZIP extraction)
|
// Use custom responseHandler for multi-file (handles ZIP extraction)
|
||||||
processedFiles = await config.responseHandler(response.data, validFiles);
|
processedFiles = await config.responseHandler(response.data, validFiles);
|
||||||
|
} 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 = validFiles[0]?.name || 'document.pdf';
|
||||||
|
const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' });
|
||||||
|
processedFiles = [singleFile];
|
||||||
} else {
|
} else {
|
||||||
// Default: assume ZIP response for multi-file endpoints
|
// Default: assume ZIP response for multi-file endpoints
|
||||||
processedFiles = await extractZipFiles(response.data);
|
processedFiles = await extractZipFiles(response.data);
|
||||||
|
@ -107,9 +107,8 @@ export const mantineTheme = createTheme({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Textarea: {
|
||||||
TextInput: {
|
styles: (theme: any) => ({
|
||||||
styles: {
|
|
||||||
input: {
|
input: {
|
||||||
backgroundColor: 'var(--bg-surface)',
|
backgroundColor: 'var(--bg-surface)',
|
||||||
borderColor: 'var(--border-default)',
|
borderColor: 'var(--border-default)',
|
||||||
@ -123,7 +122,43 @@ export const mantineTheme = createTheme({
|
|||||||
color: 'var(--text-secondary)',
|
color: 'var(--text-secondary)',
|
||||||
fontWeight: 'var(--font-weight-medium)',
|
fontWeight: 'var(--font-weight-medium)',
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
TextInput: {
|
||||||
|
styles: (theme: any) => ({
|
||||||
|
input: {
|
||||||
|
backgroundColor: 'var(--bg-surface)',
|
||||||
|
borderColor: 'var(--border-default)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
'&:focus': {
|
||||||
|
borderColor: 'var(--color-primary-500)',
|
||||||
|
boxShadow: '0 0 0 1px var(--color-primary-500)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
fontWeight: 'var(--font-weight-medium)',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
PasswordInput: {
|
||||||
|
styles: (theme: any) => ({
|
||||||
|
input: {
|
||||||
|
backgroundColor: 'var(--bg-surface)',
|
||||||
|
borderColor: 'var(--border-default)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
'&:focus': {
|
||||||
|
borderColor: 'var(--color-primary-500)',
|
||||||
|
boxShadow: '0 0 0 1px var(--color-primary-500)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: 'var(--text-secondary)',
|
||||||
|
fontWeight: 'var(--font-weight-medium)',
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
Select: {
|
Select: {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useMemo, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFileContext } from "../contexts/FileContext";
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
import { useFileSelection } from "../contexts/FileContext";
|
import { useFileSelection } from "../contexts/FileContext";
|
||||||
import { useNavigation } from "../contexts/NavigationContext";
|
import { useNavigation } from "../contexts/NavigationContext";
|
||||||
|
import { useToolWorkflow } from "../contexts/ToolWorkflowContext";
|
||||||
|
|
||||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
import { createFilesToolStep } from "../components/tools/shared/FilesToolStep";
|
import { createFilesToolStep } from "../components/tools/shared/FilesToolStep";
|
||||||
@ -21,6 +22,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { selectedFiles } = useFileSelection();
|
const { selectedFiles } = useFileSelection();
|
||||||
const { setMode } = useNavigation();
|
const { setMode } = useNavigation();
|
||||||
|
const { registerToolReset } = useToolWorkflow();
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState<AutomationStep>(AUTOMATION_STEPS.SELECTION);
|
const [currentStep, setCurrentStep] = useState<AutomationStep>(AUTOMATION_STEPS.SELECTION);
|
||||||
const [stepData, setStepData] = useState<AutomationStepData>({ step: AUTOMATION_STEPS.SELECTION });
|
const [stepData, setStepData] = useState<AutomationStepData>({ step: AUTOMATION_STEPS.SELECTION });
|
||||||
@ -30,6 +32,28 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const hasResults = automateOperation.files.length > 0 || automateOperation.downloadUrl !== null;
|
const hasResults = automateOperation.files.length > 0 || automateOperation.downloadUrl !== null;
|
||||||
const { savedAutomations, deleteAutomation, refreshAutomations, copyFromSuggested } = useSavedAutomations();
|
const { savedAutomations, deleteAutomation, refreshAutomations, copyFromSuggested } = useSavedAutomations();
|
||||||
|
|
||||||
|
// Use ref to store the latest reset function to avoid closure issues
|
||||||
|
const resetFunctionRef = React.useRef<() => void>(null);
|
||||||
|
|
||||||
|
// Update ref with latest reset function
|
||||||
|
resetFunctionRef.current = () => {
|
||||||
|
automateOperation.resetResults();
|
||||||
|
automateOperation.clearError();
|
||||||
|
setCurrentStep(AUTOMATION_STEPS.SELECTION);
|
||||||
|
setStepData({ step: AUTOMATION_STEPS.SELECTION });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register reset function with the tool workflow context - only once on mount
|
||||||
|
React.useEffect(() => {
|
||||||
|
const stableResetFunction = () => {
|
||||||
|
if (resetFunctionRef.current) {
|
||||||
|
resetFunctionRef.current();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerToolReset('automate', stableResetFunction);
|
||||||
|
}, [registerToolReset]); // Only depend on registerToolReset which should be stable
|
||||||
|
|
||||||
const handleStepChange = (data: AutomationStepData) => {
|
const handleStepChange = (data: AutomationStepData) => {
|
||||||
// If navigating away from run step, reset automation results
|
// If navigating away from run step, reset automation results
|
||||||
if (currentStep === AUTOMATION_STEPS.RUN && data.step !== AUTOMATION_STEPS.RUN) {
|
if (currentStep === AUTOMATION_STEPS.RUN && data.step !== AUTOMATION_STEPS.RUN) {
|
||||||
@ -87,6 +111,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
onError?.(`Failed to copy automation: ${suggestedAutomation.name}`);
|
onError?.(`Failed to copy automation: ${suggestedAutomation.name}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
toolRegistry={toolRegistry}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -132,11 +157,25 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
content
|
content
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Dynamic file placeholder based on supported types
|
||||||
|
const filesPlaceholder = useMemo(() => {
|
||||||
|
if (currentStep === AUTOMATION_STEPS.RUN && stepData.automation?.operations?.length) {
|
||||||
|
const firstOperation = stepData.automation.operations[0];
|
||||||
|
const toolConfig = toolRegistry[firstOperation.operation];
|
||||||
|
|
||||||
|
// Check if the tool has supportedFormats that include non-PDF formats
|
||||||
|
if (toolConfig?.supportedFormats && toolConfig.supportedFormats.length > 1) {
|
||||||
|
return t('automate.files.placeholder.multiFormat', 'Select files to process (supports various formats)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t('automate.files.placeholder', 'Select PDF files to process with this automation');
|
||||||
|
}, [currentStep, stepData.automation, toolRegistry, t]);
|
||||||
|
|
||||||
// Always create files step to avoid conditional hook calls
|
// Always create files step to avoid conditional hook calls
|
||||||
const filesStep = createFilesToolStep(createStep, {
|
const filesStep = createFilesToolStep(createStep, {
|
||||||
selectedFiles,
|
selectedFiles,
|
||||||
isCollapsed: hasResults,
|
isCollapsed: hasResults,
|
||||||
placeholder: t('automate.files.placeholder', 'Select files to process with this automation')
|
placeholder: filesPlaceholder
|
||||||
});
|
});
|
||||||
|
|
||||||
const automationSteps = [
|
const automationSteps = [
|
||||||
|
@ -23,9 +23,18 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(splitParams.getEndpointName());
|
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(splitParams.getEndpointName());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Only reset results when parameters change, not when files change
|
||||||
splitOperation.resetResults();
|
splitOperation.resetResults();
|
||||||
onPreviewFile?.(null);
|
onPreviewFile?.(null);
|
||||||
}, [splitParams.parameters, selectedFiles]);
|
}, [splitParams.parameters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset results when selected files change (user selected different files)
|
||||||
|
if (selectedFiles.length > 0) {
|
||||||
|
splitOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
}
|
||||||
|
}, [selectedFiles]);
|
||||||
const handleSplit = async () => {
|
const handleSplit = async () => {
|
||||||
try {
|
try {
|
||||||
await splitOperation.executeOperation(splitParams.parameters, selectedFiles);
|
await splitOperation.executeOperation(splitParams.parameters, selectedFiles);
|
||||||
@ -51,7 +60,7 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hasFiles = selectedFiles.length > 0;
|
const hasFiles = selectedFiles.length > 0;
|
||||||
const hasResults = splitOperation.downloadUrl !== null;
|
const hasResults = splitOperation.files.length > 0 || splitOperation.downloadUrl !== null;
|
||||||
const settingsCollapsed = !hasFiles || hasResults;
|
const settingsCollapsed = !hasFiles || hasResults;
|
||||||
|
|
||||||
return createToolFlow({
|
return createToolFlow({
|
||||||
|
@ -11,6 +11,7 @@ export interface AutomationConfig {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
operations: AutomationOperation[];
|
operations: AutomationOperation[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
@ -14,6 +14,19 @@ export const executeToolOperation = async (
|
|||||||
parameters: any,
|
parameters: any,
|
||||||
files: File[],
|
files: File[],
|
||||||
toolRegistry: ToolRegistry
|
toolRegistry: ToolRegistry
|
||||||
|
): Promise<File[]> => {
|
||||||
|
return executeToolOperationWithPrefix(operationName, parameters, files, toolRegistry, AUTOMATION_CONSTANTS.FILE_PREFIX);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a tool operation with custom prefix
|
||||||
|
*/
|
||||||
|
export const executeToolOperationWithPrefix = async (
|
||||||
|
operationName: string,
|
||||||
|
parameters: any,
|
||||||
|
files: File[],
|
||||||
|
toolRegistry: ToolRegistry,
|
||||||
|
filePrefix: string = AUTOMATION_CONSTANTS.FILE_PREFIX
|
||||||
): Promise<File[]> => {
|
): Promise<File[]> => {
|
||||||
console.log(`🔧 Executing tool: ${operationName}`, { parameters, fileCount: files.length });
|
console.log(`🔧 Executing tool: ${operationName}`, { parameters, fileCount: files.length });
|
||||||
|
|
||||||
@ -51,15 +64,37 @@ export const executeToolOperation = async (
|
|||||||
|
|
||||||
console.log(`📥 Response status: ${response.status}, size: ${response.data.size} bytes`);
|
console.log(`📥 Response status: ${response.status}, size: ${response.data.size} bytes`);
|
||||||
|
|
||||||
// Multi-file responses are typically ZIP files, but may be single files
|
// Multi-file responses are typically ZIP files, but may be single files (e.g. split with merge=true)
|
||||||
const result = await AutomationFileProcessor.extractAutomationZipFiles(response.data);
|
let result;
|
||||||
|
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 = files[0]?.name || 'document.pdf';
|
||||||
|
const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' });
|
||||||
|
result = {
|
||||||
|
success: true,
|
||||||
|
files: [singleFile],
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// ZIP response
|
||||||
|
result = await AutomationFileProcessor.extractAutomationZipFiles(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
if (result.errors.length > 0) {
|
if (result.errors.length > 0) {
|
||||||
console.warn(`⚠️ File processing warnings:`, result.errors);
|
console.warn(`⚠️ File processing warnings:`, result.errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📁 Processed ${result.files.length} files from response`);
|
// Apply prefix to files, replacing any existing prefix
|
||||||
return result.files;
|
const processedFiles = filePrefix
|
||||||
|
? result.files.map(file => {
|
||||||
|
const nameWithoutPrefix = file.name.replace(/^[^_]*_/, '');
|
||||||
|
return new File([file], `${filePrefix}${nameWithoutPrefix}`, { type: file.type });
|
||||||
|
})
|
||||||
|
: result.files;
|
||||||
|
|
||||||
|
console.log(`📁 Processed ${processedFiles.length} files from response`);
|
||||||
|
return processedFiles;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Single-file processing - separate API call per file
|
// Single-file processing - separate API call per file
|
||||||
@ -83,11 +118,12 @@ export const executeToolOperation = async (
|
|||||||
|
|
||||||
console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`);
|
console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`);
|
||||||
|
|
||||||
// Create result file
|
// Create result file with automation prefix
|
||||||
|
|
||||||
const resultFile = ResourceManager.createResultFile(
|
const resultFile = ResourceManager.createResultFile(
|
||||||
response.data,
|
response.data,
|
||||||
file.name,
|
file.name,
|
||||||
AUTOMATION_CONSTANTS.FILE_PREFIX
|
filePrefix
|
||||||
);
|
);
|
||||||
resultFiles.push(resultFile);
|
resultFiles.push(resultFile);
|
||||||
console.log(`✅ Created result file: ${resultFile.name}`);
|
console.log(`✅ Created result file: ${resultFile.name}`);
|
||||||
@ -123,6 +159,7 @@ export const executeAutomationSequence = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
let currentFiles = [...initialFiles];
|
let currentFiles = [...initialFiles];
|
||||||
|
const automationPrefix = automation.name ? `${automation.name}_` : 'automated_';
|
||||||
|
|
||||||
for (let i = 0; i < automation.operations.length; i++) {
|
for (let i = 0; i < automation.operations.length; i++) {
|
||||||
const operation = automation.operations[i];
|
const operation = automation.operations[i];
|
||||||
@ -134,11 +171,12 @@ export const executeAutomationSequence = async (
|
|||||||
try {
|
try {
|
||||||
onStepStart?.(i, operation.operation);
|
onStepStart?.(i, operation.operation);
|
||||||
|
|
||||||
const resultFiles = await executeToolOperation(
|
const resultFiles = await executeToolOperationWithPrefix(
|
||||||
operation.operation,
|
operation.operation,
|
||||||
operation.parameters || {},
|
operation.parameters || {},
|
||||||
currentFiles,
|
currentFiles,
|
||||||
toolRegistry
|
toolRegistry,
|
||||||
|
i === automation.operations.length - 1 ? automationPrefix : '' // Only add prefix to final step
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`✅ Step ${i + 1} completed: ${resultFiles.length} result files`);
|
console.log(`✅ Step ${i + 1} completed: ${resultFiles.length} result files`);
|
||||||
|
@ -10,7 +10,15 @@
|
|||||||
export const getFilenameFromHeaders = (contentDisposition: string = ''): string | null => {
|
export const getFilenameFromHeaders = (contentDisposition: string = ''): string | null => {
|
||||||
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||||
if (match && match[1]) {
|
if (match && match[1]) {
|
||||||
return match[1].replace(/['"]/g, '');
|
const filename = match[1].replace(/['"]/g, '');
|
||||||
|
|
||||||
|
// Decode URL-encoded characters (e.g., %20 -> space)
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(filename);
|
||||||
|
} catch (error) {
|
||||||
|
// If decoding fails, return the original filename
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user