mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
add tools
This commit is contained in:
parent
a85ab4a832
commit
c6bd7a1bfc
@ -8,15 +8,19 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
TextInput,
|
TextInput,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Divider
|
Divider,
|
||||||
|
Modal
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
import CheckIcon from '@mui/icons-material/Check';
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
import AddCircleOutline from '@mui/icons-material/AddCircleOutline';
|
||||||
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
import ToolConfigurationModal from './ToolConfigurationModal';
|
import ToolConfigurationModal from './ToolConfigurationModal';
|
||||||
import ToolSelector from './ToolSelector';
|
import ToolSelector from './ToolSelector';
|
||||||
|
import AutomationEntry from './AutomationEntry';
|
||||||
|
|
||||||
interface AutomationCreationProps {
|
interface AutomationCreationProps {
|
||||||
mode: 'custom' | 'suggested' | 'create';
|
mode: 'custom' | 'suggested' | 'create';
|
||||||
@ -41,6 +45,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
|
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
|
||||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||||
const [configuraingToolIndex, setConfiguringToolIndex] = useState(-1);
|
const [configuraingToolIndex, setConfiguringToolIndex] = useState(-1);
|
||||||
|
const [unsavedWarningOpen, setUnsavedWarningOpen] = useState(false);
|
||||||
|
|
||||||
// Initialize based on mode and existing automation
|
// Initialize based on mode and existing automation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -56,8 +61,27 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
setSelectedTools(tools);
|
setSelectedTools(tools);
|
||||||
|
} else if (mode === 'create' && selectedTools.length === 0) {
|
||||||
|
// Initialize with 2 empty tools for new automation
|
||||||
|
const defaultTools = [
|
||||||
|
{
|
||||||
|
id: `tool-1-${Date.now()}`,
|
||||||
|
operation: '',
|
||||||
|
name: t('automate.creation.tools.selectTool', 'Select a tool...'),
|
||||||
|
configured: false,
|
||||||
|
parameters: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `tool-2-${Date.now() + 1}`,
|
||||||
|
operation: '',
|
||||||
|
name: t('automate.creation.tools.selectTool', 'Select a tool...'),
|
||||||
|
configured: false,
|
||||||
|
parameters: {}
|
||||||
}
|
}
|
||||||
}, [mode, existingAutomation]);
|
];
|
||||||
|
setSelectedTools(defaultTools);
|
||||||
|
}
|
||||||
|
}, [mode, existingAutomation, selectedTools.length, t]);
|
||||||
|
|
||||||
const getToolName = (operation: string) => {
|
const getToolName = (operation: string) => {
|
||||||
const tool = toolRegistry?.[operation] as any;
|
const tool = toolRegistry?.[operation] as any;
|
||||||
@ -78,6 +102,8 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removeTool = (index: number) => {
|
const removeTool = (index: number) => {
|
||||||
|
// Don't allow removing tools if only 2 remain
|
||||||
|
if (selectedTools.length <= 2) return;
|
||||||
setSelectedTools(selectedTools.filter((_, i) => i !== index));
|
setSelectedTools(selectedTools.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -105,14 +131,38 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
setConfiguringToolIndex(-1);
|
setConfiguringToolIndex(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasUnsavedChanges = () => {
|
||||||
|
return (
|
||||||
|
automationName.trim() !== '' ||
|
||||||
|
selectedTools.some(tool => tool.operation !== '' || tool.configured)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const canSaveAutomation = () => {
|
const canSaveAutomation = () => {
|
||||||
return (
|
return (
|
||||||
automationName.trim() !== '' &&
|
automationName.trim() !== '' &&
|
||||||
selectedTools.length > 0 &&
|
selectedTools.length > 0 &&
|
||||||
selectedTools.every(tool => tool.configured)
|
selectedTools.every(tool => tool.configured && tool.operation !== '')
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBackClick = () => {
|
||||||
|
if (hasUnsavedChanges()) {
|
||||||
|
setUnsavedWarningOpen(true);
|
||||||
|
} else {
|
||||||
|
onBack();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmBack = () => {
|
||||||
|
setUnsavedWarningOpen(false);
|
||||||
|
onBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelBack = () => {
|
||||||
|
setUnsavedWarningOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
const saveAutomation = async () => {
|
const saveAutomation = async () => {
|
||||||
if (!canSaveAutomation()) return;
|
if (!canSaveAutomation()) return;
|
||||||
|
|
||||||
@ -152,48 +202,70 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Add Tool Selector */}
|
|
||||||
|
{/* Selected Tools List */}
|
||||||
|
{selectedTools.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500} mb="xs" style={{ color: 'var(--mantine-color-text)' }}>
|
||||||
|
{t('automate.creation.tools.selected', 'Selected Tools')} ({selectedTools.length})
|
||||||
|
</Text>
|
||||||
|
<Stack gap="0" style={{
|
||||||
|
}}>
|
||||||
|
{selectedTools.map((tool, index) => (
|
||||||
|
<React.Fragment key={tool.id}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: '1px solid var(--mantine-color-gray-2)',
|
||||||
|
borderRadius: 'var(--mantine-radius-sm)',
|
||||||
|
backgroundColor: 'white'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap="xs" align="center" wrap="nowrap" style={{ width: '100%' }}>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
|
||||||
|
{/* Tool Selection Dropdown */}
|
||||||
<ToolSelector
|
<ToolSelector
|
||||||
onSelect={addTool}
|
key={`tool-selector-${tool.id}`}
|
||||||
|
onSelect={(newOperation) => {
|
||||||
|
const updatedTools = [...selectedTools];
|
||||||
|
updatedTools[index] = {
|
||||||
|
...updatedTools[index],
|
||||||
|
operation: newOperation,
|
||||||
|
name: getToolName(newOperation),
|
||||||
|
configured: false,
|
||||||
|
parameters: {}
|
||||||
|
};
|
||||||
|
setSelectedTools(updatedTools);
|
||||||
|
}}
|
||||||
excludeTools={['automate']}
|
excludeTools={['automate']}
|
||||||
toolRegistry={toolRegistry}
|
toolRegistry={toolRegistry}
|
||||||
|
selectedValue={tool.operation}
|
||||||
|
placeholder={tool.name}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Selected Tools */}
|
<Group gap="xs" style={{ flexShrink: 0 }}>
|
||||||
{selectedTools.length > 0 && (
|
|
||||||
<Stack gap="xs">
|
|
||||||
{selectedTools.map((tool, index) => (
|
|
||||||
<Group key={tool.id} gap="xs" align="center">
|
|
||||||
<Text size="xs" c="dimmed" style={{ minWidth: '1rem', textAlign: 'center' }}>
|
|
||||||
{index + 1}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Group gap="xs" align="center">
|
|
||||||
<Text size="sm" style={{ color: 'var(--mantine-color-text)' }}>
|
|
||||||
{tool.name}
|
|
||||||
</Text>
|
|
||||||
{tool.configured ? (
|
{tool.configured ? (
|
||||||
<CheckIcon style={{ fontSize: 14, color: 'green' }} />
|
<CheckIcon style={{ fontSize: 14, color: 'green' }} />
|
||||||
) : (
|
) : (
|
||||||
<CloseIcon style={{ fontSize: 14, color: 'orange' }} />
|
<CloseIcon style={{ fontSize: 14, color: 'orange' }} />
|
||||||
)}
|
)}
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group gap="xs">
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => configureTool(index)}
|
onClick={() => configureTool(index)}
|
||||||
|
title={t('automate.creation.tools.configure', 'Configure tool')}
|
||||||
>
|
>
|
||||||
<SettingsIcon style={{ fontSize: 16 }} />
|
<SettingsIcon style={{ fontSize: 16 }} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="sm"
|
size="sm"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => removeTool(index)}
|
onClick={() => removeTool(index)}
|
||||||
|
title={t('automate.creation.tools.remove', 'Remove tool')}
|
||||||
>
|
>
|
||||||
<DeleteIcon style={{ fontSize: 16 }} />
|
<DeleteIcon style={{ fontSize: 16 }} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@ -202,11 +274,45 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{index < selectedTools.length - 1 && (
|
{index < selectedTools.length - 1 && (
|
||||||
<Text size="xs" c="dimmed">→</Text>
|
<div style={{ textAlign: 'center', padding: '8px 0' }}>
|
||||||
|
<Text size="xs" c="dimmed">↓</Text>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Arrow before Add Tool Button */}
|
||||||
|
{selectedTools.length > 0 && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '8px 0' }}>
|
||||||
|
<Text size="xs" c="dimmed">↓</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add Tool Button */}
|
||||||
|
<div style={{
|
||||||
|
border: '1px solid var(--mantine-color-gray-2)',
|
||||||
|
borderRadius: 'var(--mantine-radius-sm)',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
<AutomationEntry
|
||||||
|
title={t('automate.creation.tools.addTool', 'Add Tool')}
|
||||||
|
badgeIcon={AddCircleOutline}
|
||||||
|
operations={[]}
|
||||||
|
onClick={() => {
|
||||||
|
const newTool: AutomationTool = {
|
||||||
|
id: `tool-${Date.now()}`,
|
||||||
|
operation: '',
|
||||||
|
name: t('automate.creation.tools.selectTool', 'Select a tool...'),
|
||||||
|
configured: false,
|
||||||
|
parameters: {}
|
||||||
|
};
|
||||||
|
setSelectedTools([...selectedTools, newTool]);
|
||||||
|
}}
|
||||||
|
keepIconColor={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
@ -231,6 +337,28 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
onCancel={handleToolConfigCancel}
|
onCancel={handleToolConfigCancel}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Unsaved Changes Warning Modal */}
|
||||||
|
<Modal
|
||||||
|
opened={unsavedWarningOpen}
|
||||||
|
onClose={handleCancelBack}
|
||||||
|
title={t('automate.creation.unsavedChanges.title', 'Unsaved Changes')}
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text>
|
||||||
|
{t('automate.creation.unsavedChanges.message', 'You have unsaved changes. Are you sure you want to go back? All changes will be lost.')}
|
||||||
|
</Text>
|
||||||
|
<Group gap="md" justify="flex-end">
|
||||||
|
<Button variant="outline" onClick={handleCancelBack}>
|
||||||
|
{t('automate.creation.unsavedChanges.cancel', 'Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button color="red" onClick={handleConfirmBack}>
|
||||||
|
{t('automate.creation.unsavedChanges.confirm', 'Go Back')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,17 @@ interface ToolSelectorProps {
|
|||||||
onSelect: (toolKey: string) => void;
|
onSelect: (toolKey: string) => void;
|
||||||
excludeTools?: string[];
|
excludeTools?: string[];
|
||||||
toolRegistry: Record<string, ToolRegistryEntry>; // Pass registry as prop to break circular dependency
|
toolRegistry: Record<string, ToolRegistryEntry>; // Pass registry as prop to break circular dependency
|
||||||
|
selectedValue?: string; // For showing current selection when editing existing tool
|
||||||
|
placeholder?: string; // Custom placeholder text
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ToolSelector({ onSelect, excludeTools = [], toolRegistry }: ToolSelectorProps) {
|
export default function ToolSelector({
|
||||||
|
onSelect,
|
||||||
|
excludeTools = [],
|
||||||
|
toolRegistry,
|
||||||
|
selectedValue,
|
||||||
|
placeholder
|
||||||
|
}: ToolSelectorProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
@ -83,6 +91,14 @@ export default function ToolSelector({ onSelect, excludeTools = [], toolRegistry
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get display value for selected tool
|
||||||
|
const getDisplayValue = () => {
|
||||||
|
if (selectedValue && toolRegistry[selectedValue]) {
|
||||||
|
return toolRegistry[selectedValue].name;
|
||||||
|
}
|
||||||
|
return placeholder || t('automate.creation.tools.add', 'Add a tool...');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
opened={opened}
|
opened={opened}
|
||||||
@ -98,7 +114,8 @@ export default function ToolSelector({ onSelect, excludeTools = [], toolRegistry
|
|||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
toolRegistry={filteredToolRegistry}
|
toolRegistry={filteredToolRegistry}
|
||||||
mode="filter"
|
mode="filter"
|
||||||
placeholder={t('automate.creation.tools.add', 'Add a tool...')}
|
placeholder={getDisplayValue()}
|
||||||
|
hideIcon={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
|
@ -13,6 +13,7 @@ interface ToolSearchProps {
|
|||||||
mode: 'filter' | 'dropdown';
|
mode: 'filter' | 'dropdown';
|
||||||
selectedToolKey?: string | null;
|
selectedToolKey?: string | null;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
hideIcon?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolSearch = ({
|
const ToolSearch = ({
|
||||||
@ -22,7 +23,8 @@ const ToolSearch = ({
|
|||||||
onToolSelect,
|
onToolSelect,
|
||||||
mode = 'filter',
|
mode = 'filter',
|
||||||
selectedToolKey,
|
selectedToolKey,
|
||||||
placeholder
|
placeholder,
|
||||||
|
hideIcon = false
|
||||||
}: ToolSearchProps) => {
|
}: ToolSearchProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
@ -64,7 +66,7 @@ const ToolSearch = ({
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")}
|
placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")}
|
||||||
icon={<span className="material-symbols-rounded">search</span>}
|
icon={hideIcon ? undefined : <span className="material-symbols-rounded">search</span>}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -81,6 +81,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
{
|
{
|
||||||
title: t('automate.stepTitle', 'Automations'),
|
title: t('automate.stepTitle', 'Automations'),
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
|
onCollapsedClick: ()=> setCurrentStep('selection'),
|
||||||
content: currentStep === 'selection' ? renderCurrentStep() : null
|
content: currentStep === 'selection' ? renderCurrentStep() : null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user