mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Merge branch 'V2' of github.com:Stirling-Tools/Stirling-PDF into UIUX/V2/StylingSnags
This commit is contained in:
commit
6d72878b4b
@ -48,7 +48,11 @@
|
|||||||
"filesSelected": "{{count}} files selected",
|
"filesSelected": "{{count}} files selected",
|
||||||
"files": {
|
"files": {
|
||||||
"title": "Files",
|
"title": "Files",
|
||||||
"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",
|
||||||
|
"addFiles": "Add files",
|
||||||
|
"noFiles": "No files uploaded. ",
|
||||||
|
"selectFromWorkbench": "Select files from the workbench or "
|
||||||
},
|
},
|
||||||
"noFavourites": "No favourites added",
|
"noFavourites": "No favourites added",
|
||||||
"downloadComplete": "Download Complete",
|
"downloadComplete": "Download Complete",
|
||||||
@ -2275,6 +2279,20 @@
|
|||||||
"description": "Configure the settings for this tool. These settings will be applied when the automation runs.",
|
"description": "Configure the settings for this tool. These settings will be applied when the automation runs.",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"save": "Save Configuration"
|
"save": "Save Configuration"
|
||||||
}
|
},
|
||||||
|
"copyToSaved": "Copy to Saved"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"automation": {
|
||||||
|
"suggested": {
|
||||||
|
"securePdfIngestion": "Secure PDF Ingestion",
|
||||||
|
"securePdfIngestionDesc": "Comprehensive PDF processing workflow that sanitises documents, applies OCR with cleanup, converts to PDF/A format for long-term archival, and optimises file size.",
|
||||||
|
"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.",
|
||||||
|
"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.",
|
||||||
|
"processImages": "Process Images",
|
||||||
|
"processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2106,5 +2106,20 @@
|
|||||||
"results": {
|
"results": {
|
||||||
"title": "Decrypted PDFs"
|
"title": "Decrypted PDFs"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"automation": {
|
||||||
|
"suggested": {
|
||||||
|
"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.",
|
||||||
|
"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.",
|
||||||
|
"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.",
|
||||||
|
"processImages": "Process Images",
|
||||||
|
"processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"automate": {
|
||||||
|
"copyToSaved": "Copy to Saved"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,8 @@ export interface TextInputProps {
|
|||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
/** Accessibility label */
|
/** Accessibility label */
|
||||||
'aria-label'?: string;
|
'aria-label'?: string;
|
||||||
|
/** Focus event handler */
|
||||||
|
onFocus?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
|
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
|
||||||
@ -46,6 +48,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
'aria-label': ariaLabel,
|
'aria-label': ariaLabel,
|
||||||
|
onFocus,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
@ -63,7 +66,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
|
|||||||
return (
|
return (
|
||||||
<div className={`${styles.container} ${className}`} style={style}>
|
<div className={`${styles.container} ${className}`} style={style}>
|
||||||
{icon && (
|
{icon && (
|
||||||
<span
|
<span
|
||||||
className={styles.icon}
|
className={styles.icon}
|
||||||
style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }}
|
style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }}
|
||||||
>
|
>
|
||||||
@ -81,6 +84,7 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
|
onFocus={onFocus}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF',
|
backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF',
|
||||||
color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382',
|
color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382',
|
||||||
|
@ -8,6 +8,7 @@ import ToolRenderer from './ToolRenderer';
|
|||||||
import ToolSearch from './toolPicker/ToolSearch';
|
import ToolSearch from './toolPicker/ToolSearch';
|
||||||
import { useSidebarContext } from "../../contexts/SidebarContext";
|
import { useSidebarContext } from "../../contexts/SidebarContext";
|
||||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||||
|
import { Stack, ScrollArea } from '@mantine/core';
|
||||||
|
|
||||||
// No props needed - component uses context
|
// No props needed - component uses context
|
||||||
|
|
||||||
@ -91,15 +92,17 @@ export default function ToolPanel() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Selected Tool Content View
|
// Selected Tool Content View
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* Tool content */}
|
{/* Tool content */}
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
{selectedToolKey && (
|
<ScrollArea h="100%">
|
||||||
<ToolRenderer
|
{selectedToolKey && (
|
||||||
selectedToolKey={selectedToolKey}
|
<ToolRenderer
|
||||||
onPreviewFile={setPreviewFile}
|
selectedToolKey={selectedToolKey}
|
||||||
/>
|
onPreviewFile={setPreviewFile}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -98,7 +98,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
const saveAutomation = async () => {
|
const saveAutomation = async () => {
|
||||||
if (!canSaveAutomation()) return;
|
if (!canSaveAutomation()) return;
|
||||||
|
|
||||||
const automation = {
|
const automationData = {
|
||||||
name: automationName.trim(),
|
name: automationName.trim(),
|
||||||
description: '',
|
description: '',
|
||||||
operations: selectedTools.map(tool => ({
|
operations: selectedTools.map(tool => ({
|
||||||
@ -109,7 +109,30 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { automationStorage } = await import('../../../services/automationStorage');
|
const { automationStorage } = await import('../../../services/automationStorage');
|
||||||
const savedAutomation = await automationStorage.saveAutomation(automation);
|
let savedAutomation;
|
||||||
|
|
||||||
|
if (mode === AutomationMode.EDIT && existingAutomation) {
|
||||||
|
// For edit mode, check if name has changed
|
||||||
|
const nameChanged = automationName.trim() !== existingAutomation.name;
|
||||||
|
|
||||||
|
if (nameChanged) {
|
||||||
|
// Name changed - create new automation
|
||||||
|
savedAutomation = await automationStorage.saveAutomation(automationData);
|
||||||
|
} else {
|
||||||
|
// Name unchanged - update existing automation
|
||||||
|
const updatedAutomation = {
|
||||||
|
...existingAutomation,
|
||||||
|
...automationData,
|
||||||
|
id: existingAutomation.id,
|
||||||
|
createdAt: existingAutomation.createdAt
|
||||||
|
};
|
||||||
|
savedAutomation = await automationStorage.updateAutomation(updatedAutomation);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create mode - always create new automation
|
||||||
|
savedAutomation = await automationStorage.saveAutomation(automationData);
|
||||||
|
}
|
||||||
|
|
||||||
onComplete(savedAutomation);
|
onComplete(savedAutomation);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving automation:', error);
|
console.error('Error saving automation:', error);
|
||||||
|
@ -4,10 +4,14 @@ import { Button, Group, Text, ActionIcon, Menu, Box } from '@mantine/core';
|
|||||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
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 { Tooltip } from '../../shared/Tooltip';
|
||||||
|
|
||||||
interface AutomationEntryProps {
|
interface AutomationEntryProps {
|
||||||
/** Optional title for the automation (usually for custom ones) */
|
/** Optional title for the automation (usually for custom ones) */
|
||||||
title?: string;
|
title?: string;
|
||||||
|
/** Optional description for tooltip */
|
||||||
|
description?: string;
|
||||||
/** MUI Icon component for the badge */
|
/** MUI Icon component for the badge */
|
||||||
badgeIcon?: React.ComponentType<any>;
|
badgeIcon?: React.ComponentType<any>;
|
||||||
/** Array of tool operation names in the workflow */
|
/** Array of tool operation names in the workflow */
|
||||||
@ -22,17 +26,21 @@ interface AutomationEntryProps {
|
|||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
/** Delete handler */
|
/** Delete handler */
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
|
/** Copy handler (for suggested automations) */
|
||||||
|
onCopy?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutomationEntry({
|
export default function AutomationEntry({
|
||||||
title,
|
title,
|
||||||
|
description,
|
||||||
badgeIcon: BadgeIcon,
|
badgeIcon: BadgeIcon,
|
||||||
operations,
|
operations,
|
||||||
onClick,
|
onClick,
|
||||||
keepIconColor = false,
|
keepIconColor = false,
|
||||||
showMenu = false,
|
showMenu = false,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete
|
onDelete,
|
||||||
|
onCopy
|
||||||
}: AutomationEntryProps) {
|
}: AutomationEntryProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
@ -41,6 +49,47 @@ 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;
|
||||||
|
|
||||||
|
// Create tooltip content with description and tool chain
|
||||||
|
const createTooltipContent = () => {
|
||||||
|
if (!description) return null;
|
||||||
|
|
||||||
|
const toolChain = operations.map((op, index) => (
|
||||||
|
<React.Fragment key={`${op}-${index}`}>
|
||||||
|
<Text
|
||||||
|
component="span"
|
||||||
|
size="sm"
|
||||||
|
fw={600}
|
||||||
|
style={{
|
||||||
|
color: 'var(--mantine-primary-color-filled)',
|
||||||
|
background: 'var(--mantine-primary-color-light)',
|
||||||
|
padding: '2px 6px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(`${op}.title`, op)}
|
||||||
|
</Text>
|
||||||
|
{index < operations.length - 1 && (
|
||||||
|
<Text component="span" size="sm" mx={4}>
|
||||||
|
→
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ minWidth: '400px', width: 'auto' }}>
|
||||||
|
<Text size="sm" mb={8} style={{ whiteSpace: 'normal', wordWrap: 'break-word' }}>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', whiteSpace: 'nowrap' }}>
|
||||||
|
{toolChain}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (title) {
|
if (title) {
|
||||||
// Custom automation with title
|
// Custom automation with title
|
||||||
@ -89,7 +138,7 @@ export default function AutomationEntry({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const boxContent = (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: shouldShowHovered ? 'var(--mantine-color-gray-1)' : 'transparent',
|
backgroundColor: shouldShowHovered ? 'var(--mantine-color-gray-1)' : 'transparent',
|
||||||
@ -132,6 +181,17 @@ export default function AutomationEntry({
|
|||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
|
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
|
{onCopy && (
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<ContentCopyIcon style={{ fontSize: 16 }} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onCopy();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('automate.copyToSaved', 'Copy to Saved')}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
{onEdit && (
|
{onEdit && (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<EditIcon style={{ fontSize: 16 }} />}
|
leftSection={<EditIcon style={{ fontSize: 16 }} />}
|
||||||
@ -160,4 +220,18 @@ export default function AutomationEntry({
|
|||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Only show tooltip if description exists, otherwise return plain content
|
||||||
|
return description ? (
|
||||||
|
<Tooltip
|
||||||
|
content={createTooltipContent()}
|
||||||
|
position="right"
|
||||||
|
arrow={true}
|
||||||
|
delay={500}
|
||||||
|
>
|
||||||
|
{boxContent}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
boxContent
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import AddCircleOutline from "@mui/icons-material/AddCircleOutline";
|
|||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
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 } from "../../../types/automation";
|
import { AutomationConfig, SuggestedAutomation } from "../../../types/automation";
|
||||||
|
|
||||||
interface AutomationSelectionProps {
|
interface AutomationSelectionProps {
|
||||||
savedAutomations: AutomationConfig[];
|
savedAutomations: AutomationConfig[];
|
||||||
@ -13,6 +13,7 @@ interface AutomationSelectionProps {
|
|||||||
onRun: (automation: AutomationConfig) => void;
|
onRun: (automation: AutomationConfig) => void;
|
||||||
onEdit: (automation: AutomationConfig) => void;
|
onEdit: (automation: AutomationConfig) => void;
|
||||||
onDelete: (automation: AutomationConfig) => void;
|
onDelete: (automation: AutomationConfig) => void;
|
||||||
|
onCopyFromSuggested: (automation: SuggestedAutomation) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutomationSelection({
|
export default function AutomationSelection({
|
||||||
@ -20,7 +21,8 @@ export default function AutomationSelection({
|
|||||||
onCreateNew,
|
onCreateNew,
|
||||||
onRun,
|
onRun,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete
|
onDelete,
|
||||||
|
onCopyFromSuggested
|
||||||
}: AutomationSelectionProps) {
|
}: AutomationSelectionProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const suggestedAutomations = useSuggestedAutomations();
|
const suggestedAutomations = useSuggestedAutomations();
|
||||||
@ -63,9 +65,13 @@ export default function AutomationSelection({
|
|||||||
{suggestedAutomations.map((automation) => (
|
{suggestedAutomations.map((automation) => (
|
||||||
<AutomationEntry
|
<AutomationEntry
|
||||||
key={automation.id}
|
key={automation.id}
|
||||||
|
title={automation.name}
|
||||||
|
description={automation.description}
|
||||||
badgeIcon={automation.icon}
|
badgeIcon={automation.icon}
|
||||||
operations={automation.operations.map(op => op.operation)}
|
operations={automation.operations.map(op => op.operation)}
|
||||||
onClick={() => onRun(automation)}
|
onClick={() => onRun(automation)}
|
||||||
|
showMenu={true}
|
||||||
|
onCopy={() => onCopyFromSuggested(automation)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { Text, Stack, Group, ActionIcon } from '@mantine/core';
|
import { Text, Stack, Group, ActionIcon } from "@mantine/core";
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import AddCircleOutline from "@mui/icons-material/AddCircleOutline";
|
||||||
import AddCircleOutline from '@mui/icons-material/AddCircleOutline';
|
import { AutomationTool } from "../../../types/automation";
|
||||||
import { AutomationTool } from '../../../types/automation';
|
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
|
||||||
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
import ToolSelector from "./ToolSelector";
|
||||||
import ToolSelector from './ToolSelector';
|
import AutomationEntry from "./AutomationEntry";
|
||||||
import AutomationEntry from './AutomationEntry';
|
|
||||||
|
|
||||||
interface ToolListProps {
|
interface ToolListProps {
|
||||||
tools: AutomationTool[];
|
tools: AutomationTool[];
|
||||||
@ -29,35 +28,39 @@ export default function ToolList({
|
|||||||
onToolConfigure,
|
onToolConfigure,
|
||||||
onToolAdd,
|
onToolAdd,
|
||||||
getToolName,
|
getToolName,
|
||||||
getToolDefaultParameters
|
getToolDefaultParameters,
|
||||||
}: ToolListProps) {
|
}: ToolListProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleToolSelect = (index: number, newOperation: string) => {
|
const handleToolSelect = (index: number, newOperation: string) => {
|
||||||
const defaultParams = getToolDefaultParameters(newOperation);
|
const defaultParams = getToolDefaultParameters(newOperation);
|
||||||
|
|
||||||
onToolUpdate(index, {
|
onToolUpdate(index, {
|
||||||
operation: newOperation,
|
operation: newOperation,
|
||||||
name: getToolName(newOperation),
|
name: getToolName(newOperation),
|
||||||
configured: false,
|
configured: false,
|
||||||
parameters: defaultParams
|
parameters: defaultParams,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Text size="sm" fw={500} mb="xs" style={{ color: 'var(--mantine-color-text)' }}>
|
<Text size="sm" fw={500} mb="xs" style={{ color: "var(--mantine-color-text)" }}>
|
||||||
{t('automate.creation.tools.selected', 'Selected Tools')} ({tools.length})
|
{t("automate.creation.tools.selected", "Selected Tools")} ({tools.length})
|
||||||
</Text>
|
</Text>
|
||||||
<Stack gap="0">
|
<Stack gap="0">
|
||||||
{tools.map((tool, index) => (
|
{tools.map((tool, index) => (
|
||||||
<React.Fragment key={tool.id}>
|
<React.Fragment key={tool.id}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid var(--mantine-color-gray-2)',
|
border: "1px solid var(--mantine-color-gray-2)",
|
||||||
borderRadius: 'var(--mantine-radius-sm)',
|
borderRadius: tool.operation && !tool.configured
|
||||||
position: 'relative',
|
? "var(--mantine-radius-lg) var(--mantine-radius-lg) 0 0"
|
||||||
padding: 'var(--mantine-spacing-xs)'
|
: "var(--mantine-radius-lg)",
|
||||||
|
backgroundColor: "var(--mantine-color-gray-2)",
|
||||||
|
position: "relative",
|
||||||
|
padding: "var(--mantine-spacing-xs)",
|
||||||
|
borderBottomWidth: tool.operation && !tool.configured ? "0" : "1px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Delete X in top right */}
|
{/* Delete X in top right */}
|
||||||
@ -65,26 +68,26 @@ export default function ToolList({
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => onToolRemove(index)}
|
onClick={() => onToolRemove(index)}
|
||||||
title={t('automate.creation.tools.remove', 'Remove tool')}
|
title={t("automate.creation.tools.remove", "Remove tool")}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
top: '4px',
|
top: "4px",
|
||||||
right: '4px',
|
right: "4px",
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
color: 'var(--mantine-color-gray-6)'
|
color: "var(--mantine-color-gray-6)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CloseIcon style={{ fontSize: 12 }} />
|
<CloseIcon style={{ fontSize: 16 }} />
|
||||||
</ActionIcon>
|
</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 */}
|
||||||
<Group gap="xs" align="center" wrap="nowrap">
|
<Group gap="xs" align="center" wrap="nowrap">
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<ToolSelector
|
<ToolSelector
|
||||||
key={`tool-selector-${tool.id}`}
|
key={`tool-selector-${tool.id}`}
|
||||||
onSelect={(newOperation) => handleToolSelect(index, newOperation)}
|
onSelect={(newOperation) => handleToolSelect(index, newOperation)}
|
||||||
excludeTools={['automate']}
|
excludeTools={["automate"]}
|
||||||
toolRegistry={toolRegistry}
|
toolRegistry={toolRegistry}
|
||||||
selectedValue={tool.operation}
|
selectedValue={tool.operation}
|
||||||
placeholder={tool.name}
|
placeholder={tool.name}
|
||||||
@ -97,26 +100,37 @@ export default function ToolList({
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onToolConfigure(index)}
|
onClick={() => onToolConfigure(index)}
|
||||||
title={t('automate.creation.tools.configure', 'Configure tool')}
|
title={t("automate.creation.tools.configure", "Configure tool")}
|
||||||
style={{ color: 'var(--mantine-color-gray-6)' }}
|
style={{ color: "var(--mantine-color-gray-6)" }}
|
||||||
>
|
>
|
||||||
<SettingsIcon style={{ fontSize: 16 }} />
|
<SettingsIcon style={{ fontSize: 16 }} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Configuration status underneath */}
|
|
||||||
{tool.operation && !tool.configured && (
|
|
||||||
<Text pl="md" size="xs" c="dimmed" mt="xs">
|
|
||||||
{t('automate.creation.tools.notConfigured', "! Not Configured")}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Configuration status underneath */}
|
||||||
|
{tool.operation && !tool.configured && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
border: "1px solid var(--mantine-color-gray-2)",
|
||||||
|
borderTop: "none",
|
||||||
|
borderRadius: "0 0 var(--mantine-radius-lg) var(--mantine-radius-lg)",
|
||||||
|
backgroundColor: "var(--active-bg)",
|
||||||
|
padding: "var(--mantine-spacing-xs)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text pl="md" size="xs" >
|
||||||
|
{t("automate.creation.tools.notConfigured", "! Not Configured")}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{index < tools.length - 1 && (
|
{index < tools.length - 1 && (
|
||||||
<div style={{ textAlign: 'center', padding: '8px 0' }}>
|
<div style={{ textAlign: "center", padding: "8px 0" }}>
|
||||||
<Text size="xs" c="dimmed">↓</Text>
|
<Text size="xs" c="dimmed">
|
||||||
|
↓
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
@ -124,19 +138,23 @@ export default function ToolList({
|
|||||||
|
|
||||||
{/* Arrow before Add Tool Button */}
|
{/* Arrow before Add Tool Button */}
|
||||||
{tools.length > 0 && (
|
{tools.length > 0 && (
|
||||||
<div style={{ textAlign: 'center', padding: '8px 0' }}>
|
<div style={{ textAlign: "center", padding: "8px 0" }}>
|
||||||
<Text size="xs" c="dimmed">↓</Text>
|
<Text size="xs" c="dimmed">
|
||||||
|
↓
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add Tool Button */}
|
{/* Add Tool Button */}
|
||||||
<div style={{
|
<div
|
||||||
border: '1px solid var(--mantine-color-gray-2)',
|
style={{
|
||||||
borderRadius: 'var(--mantine-radius-sm)',
|
border: "1px solid var(--mantine-color-gray-2)",
|
||||||
overflow: 'hidden'
|
borderRadius: "var(--mantine-radius-sm)",
|
||||||
}}>
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<AutomationEntry
|
<AutomationEntry
|
||||||
title={t('automate.creation.tools.addTool', 'Add Tool')}
|
title={t("automate.creation.tools.addTool", "Add Tool")}
|
||||||
badgeIcon={AddCircleOutline}
|
badgeIcon={AddCircleOutline}
|
||||||
operations={[]}
|
operations={[]}
|
||||||
onClick={onToolAdd}
|
onClick={onToolAdd}
|
||||||
@ -146,4 +164,4 @@ export default function ToolList({
|
|||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import React, { useState, useMemo, useCallback } from 'react';
|
import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Menu, Stack, Text, ScrollArea } from '@mantine/core';
|
import { Stack, Text, ScrollArea } from '@mantine/core';
|
||||||
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
import { useToolSections } from '../../../hooks/useToolSections';
|
import { useToolSections } from '../../../hooks/useToolSections';
|
||||||
import { renderToolButtons } from '../shared/renderToolButtons';
|
import { renderToolButtons } from '../shared/renderToolButtons';
|
||||||
import ToolSearch from '../toolPicker/ToolSearch';
|
import ToolSearch from '../toolPicker/ToolSearch';
|
||||||
|
import ToolButton from '../toolPicker/ToolButton';
|
||||||
|
|
||||||
interface ToolSelectorProps {
|
interface ToolSelectorProps {
|
||||||
onSelect: (toolKey: string) => void;
|
onSelect: (toolKey: string) => void;
|
||||||
@ -24,6 +25,8 @@ export default function ToolSelector({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [shouldAutoFocus, setShouldAutoFocus] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Filter out excluded tools (like 'automate' itself)
|
// Filter out excluded tools (like 'automate' itself)
|
||||||
const baseFilteredTools = useMemo(() => {
|
const baseFilteredTools = useMemo(() => {
|
||||||
@ -66,13 +69,21 @@ export default function ToolSelector({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!sections || sections.length === 0) {
|
if (!sections || sections.length === 0) {
|
||||||
|
// If no sections, create a simple group from filtered tools
|
||||||
|
if (baseFilteredTools.length > 0) {
|
||||||
|
return [{
|
||||||
|
name: 'All Tools',
|
||||||
|
subcategoryId: 'all' as any,
|
||||||
|
tools: baseFilteredTools.map(([key, tool]) => ({ id: key, tool }))
|
||||||
|
}];
|
||||||
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the "all" section which contains all tools without duplicates
|
// Find the "all" section which contains all tools without duplicates
|
||||||
const allSection = sections.find(s => (s as any).key === 'all');
|
const allSection = sections.find(s => (s as any).key === 'all');
|
||||||
return allSection?.subcategories || [];
|
return allSection?.subcategories || [];
|
||||||
}, [isSearching, searchGroups, sections]);
|
}, [isSearching, searchGroups, sections, baseFilteredTools]);
|
||||||
|
|
||||||
const handleToolSelect = useCallback((toolKey: string) => {
|
const handleToolSelect = useCallback((toolKey: string) => {
|
||||||
onSelect(toolKey);
|
onSelect(toolKey);
|
||||||
@ -88,8 +99,25 @@ export default function ToolSelector({
|
|||||||
|
|
||||||
const handleSearchFocus = () => {
|
const handleSearchFocus = () => {
|
||||||
setOpened(true);
|
setOpened(true);
|
||||||
|
setShouldAutoFocus(true); // Request auto-focus for the input
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle click outside to close dropdown
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||||
|
setOpened(false);
|
||||||
|
setSearchTerm('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opened) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
}, [opened]);
|
||||||
|
|
||||||
|
|
||||||
const handleSearchChange = (value: string) => {
|
const handleSearchChange = (value: string) => {
|
||||||
setSearchTerm(value);
|
setSearchTerm(value);
|
||||||
if (!opened) {
|
if (!opened) {
|
||||||
@ -97,6 +125,14 @@ export default function ToolSelector({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInputFocus = () => {
|
||||||
|
if (!opened) {
|
||||||
|
setOpened(true);
|
||||||
|
}
|
||||||
|
// Clear auto-focus flag since input is now focused
|
||||||
|
setShouldAutoFocus(false);
|
||||||
|
};
|
||||||
|
|
||||||
// Get display value for selected tool
|
// Get display value for selected tool
|
||||||
const getDisplayValue = () => {
|
const getDisplayValue = () => {
|
||||||
if (selectedValue && toolRegistry[selectedValue]) {
|
if (selectedValue && toolRegistry[selectedValue]) {
|
||||||
@ -106,77 +142,63 @@ export default function ToolSelector({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative', width: '100%' }}>
|
<div ref={containerRef} className='rounded-xl'>
|
||||||
<Menu
|
{/* Always show the target - either selected tool or search input */}
|
||||||
opened={opened}
|
|
||||||
onChange={(isOpen) => {
|
|
||||||
setOpened(isOpen);
|
|
||||||
// Clear search term when menu closes to show proper display
|
|
||||||
if (!isOpen) {
|
|
||||||
setSearchTerm('');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
closeOnClickOutside={true}
|
|
||||||
closeOnEscape={true}
|
|
||||||
position="bottom-start"
|
|
||||||
offset={4}
|
|
||||||
withinPortal={false}
|
|
||||||
trapFocus={false}
|
|
||||||
shadow="sm"
|
|
||||||
transitionProps={{ duration: 0 }}
|
|
||||||
>
|
|
||||||
<Menu.Target>
|
|
||||||
<div style={{ width: '100%' }}>
|
|
||||||
{selectedValue && toolRegistry[selectedValue] && !opened ? (
|
|
||||||
// Show selected tool in AutomationEntry style when tool is selected and not searching
|
|
||||||
<div onClick={handleSearchFocus} style={{ cursor: 'pointer' }}>
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 'var(--mantine-spacing-sm)',
|
|
||||||
padding: '0 0.5rem',
|
|
||||||
borderRadius: 'var(--mantine-radius-sm)',
|
|
||||||
}}>
|
|
||||||
<div style={{ color: 'var(--mantine-color-text)', fontSize: '1.2rem' }}>
|
|
||||||
{toolRegistry[selectedValue].icon}
|
|
||||||
</div>
|
|
||||||
<Text size="sm" style={{ flex: 1, color: 'var(--mantine-color-text)' }}>
|
|
||||||
{toolRegistry[selectedValue].name}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
// Show search input when no tool selected or actively searching
|
|
||||||
<ToolSearch
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
toolRegistry={filteredToolRegistry}
|
|
||||||
mode="filter"
|
|
||||||
placeholder={getDisplayValue()}
|
|
||||||
hideIcon={true}
|
|
||||||
onFocus={handleSearchFocus}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Menu.Target>
|
|
||||||
|
|
||||||
<Menu.Dropdown p={0} style={{ minWidth: '16rem' }}>
|
{selectedValue && toolRegistry[selectedValue] && !opened ? (
|
||||||
<ScrollArea h={350}>
|
// Show selected tool in AutomationEntry style when tool is selected and dropdown closed
|
||||||
<Stack gap="sm" p="sm">
|
<div onClick={handleSearchFocus} style={{ cursor: 'pointer',
|
||||||
{displayGroups.length === 0 ? (
|
borderRadius: "var(--mantine-radius-lg)" }}>
|
||||||
<Text size="sm" c="dimmed" ta="center" p="md">
|
<ToolButton id='tool' tool={toolRegistry[selectedValue]} isSelected={false}
|
||||||
{isSearching
|
onSelect={()=>{}} rounded={true}></ToolButton>
|
||||||
? t('tools.noSearchResults', 'No tools found')
|
</div>
|
||||||
: t('tools.noTools', 'No tools available')
|
) : (
|
||||||
}
|
// Show search input when no tool selected OR when dropdown is opened
|
||||||
</Text>
|
<ToolSearch
|
||||||
) : (
|
value={searchTerm}
|
||||||
renderedTools
|
onChange={handleSearchChange}
|
||||||
)}
|
toolRegistry={filteredToolRegistry}
|
||||||
</Stack>
|
mode="unstyled"
|
||||||
</ScrollArea>
|
placeholder={getDisplayValue()}
|
||||||
</Menu.Dropdown>
|
hideIcon={true}
|
||||||
</Menu>
|
onFocus={handleInputFocus}
|
||||||
|
autoFocus={shouldAutoFocus}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Custom dropdown */}
|
||||||
|
{opened && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '100%',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 1000,
|
||||||
|
backgroundColor: 'var(--mantine-color-body)',
|
||||||
|
border: '1px solid var(--mantine-color-gray-3)',
|
||||||
|
borderRadius: 'var(--mantine-radius-sm)',
|
||||||
|
boxShadow: 'var(--mantine-shadow-sm)',
|
||||||
|
marginTop: '4px',
|
||||||
|
minWidth: '16rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollArea h={350}>
|
||||||
|
<Stack gap="sm" p="sm">
|
||||||
|
{displayGroups.length === 0 ? (
|
||||||
|
<Text size="sm" c="dimmed" ta="center" p="md">
|
||||||
|
{isSearching
|
||||||
|
? t('tools.noSearchResults', 'No tools found')
|
||||||
|
: t('tools.noTools', 'No tools available')
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
renderedTools
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Text, Anchor } from "@mantine/core";
|
import { Text, Anchor } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import FolderIcon from '@mui/icons-material/Folder';
|
import FolderIcon from '@mui/icons-material/Folder';
|
||||||
|
import UploadIcon from '@mui/icons-material/Upload';
|
||||||
import { useFilesModalContext } from "../../../contexts/FilesModalContext";
|
import { useFilesModalContext } from "../../../contexts/FilesModalContext";
|
||||||
import { useAllFiles } from "../../../contexts/FileContext";
|
import { useAllFiles } from "../../../contexts/FileContext";
|
||||||
|
import { useFileManager } from "../../../hooks/useFileManager";
|
||||||
|
|
||||||
export interface FileStatusIndicatorProps {
|
export interface FileStatusIndicatorProps {
|
||||||
selectedFiles?: File[];
|
selectedFiles?: File[];
|
||||||
@ -14,40 +16,110 @@ const FileStatusIndicator = ({
|
|||||||
selectedFiles = [],
|
selectedFiles = [],
|
||||||
}: FileStatusIndicatorProps) => {
|
}: FileStatusIndicatorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { openFilesModal } = useFilesModalContext();
|
const { openFilesModal, onFilesSelect } = useFilesModalContext();
|
||||||
const { files: workbenchFiles } = useAllFiles();
|
const { files: workbenchFiles } = useAllFiles();
|
||||||
|
const { loadRecentFiles } = useFileManager();
|
||||||
|
const [hasRecentFiles, setHasRecentFiles] = useState<boolean | null>(null);
|
||||||
|
|
||||||
|
// Check if there are recent files
|
||||||
|
useEffect(() => {
|
||||||
|
const checkRecentFiles = async () => {
|
||||||
|
try {
|
||||||
|
const recentFiles = await loadRecentFiles();
|
||||||
|
setHasRecentFiles(recentFiles.length > 0);
|
||||||
|
} catch (error) {
|
||||||
|
setHasRecentFiles(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkRecentFiles();
|
||||||
|
}, [loadRecentFiles]);
|
||||||
|
|
||||||
|
// Handle native file picker
|
||||||
|
const handleNativeUpload = () => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.multiple = true;
|
||||||
|
input.accept = '.pdf,application/pdf';
|
||||||
|
input.onchange = (event) => {
|
||||||
|
const files = Array.from((event.target as HTMLInputElement).files || []);
|
||||||
|
if (files.length > 0) {
|
||||||
|
onFilesSelect(files);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Don't render until we know if there are recent files
|
||||||
|
if (hasRecentFiles === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if there are no files in the workbench
|
// Check if there are no files in the workbench
|
||||||
if (workbenchFiles.length === 0) {
|
if (workbenchFiles.length === 0) {
|
||||||
return (
|
// If no recent files, show upload button
|
||||||
<Text size="sm" c="dimmed">
|
if (!hasRecentFiles) {
|
||||||
<Anchor
|
return (
|
||||||
size="sm"
|
<Text size="sm" c="dimmed">
|
||||||
onClick={openFilesModal}
|
<Anchor
|
||||||
style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '4px' }}
|
size="sm"
|
||||||
>
|
onClick={handleNativeUpload}
|
||||||
<FolderIcon style={{ fontSize: '14px' }} />
|
style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '0.25rem' }}
|
||||||
{t("files.addFiles", "Add files")}
|
>
|
||||||
</Anchor>
|
<UploadIcon style={{ fontSize: '0.875rem' }} />
|
||||||
</Text>
|
{t("files.upload", "Upload")}
|
||||||
);
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// If there are recent files, show add files button
|
||||||
|
return (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
<Anchor
|
||||||
|
size="sm"
|
||||||
|
onClick={openFilesModal}
|
||||||
|
style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '0.25rem' }}
|
||||||
|
>
|
||||||
|
<FolderIcon style={{ fontSize: '0.875rem' }} />
|
||||||
|
{t("files.addFiles", "Add files")}
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show selection status when there are files in workbench
|
// Show selection status when there are files in workbench
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
return (
|
// If no recent files, show upload option
|
||||||
<Text size="sm" c="dimmed">
|
if (!hasRecentFiles) {
|
||||||
{t("files.selectFromWorkbench", "Select files from the workbench or ") + " "}
|
return (
|
||||||
<Anchor
|
<Text size="sm" c="dimmed">
|
||||||
size="sm"
|
{t("files.selectFromWorkbench", "Select files from the workbench or ") + " "}
|
||||||
onClick={openFilesModal}
|
<Anchor
|
||||||
style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '4px' }}
|
size="sm"
|
||||||
>
|
onClick={handleNativeUpload}
|
||||||
<FolderIcon style={{ fontSize: '14px' }} />
|
style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '0.25rem' }}
|
||||||
{t("files.addFiles", "Add files")}
|
>
|
||||||
</Anchor>
|
<UploadIcon style={{ fontSize: '0.875rem' }} />
|
||||||
</Text>
|
{t("files.upload", "Upload")}
|
||||||
);
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// If there are recent files, show add files option
|
||||||
|
return (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t("files.selectFromWorkbench", "Select files from the workbench or ") + " "}
|
||||||
|
<Anchor
|
||||||
|
size="sm"
|
||||||
|
onClick={openFilesModal}
|
||||||
|
style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '0.25rem' }}
|
||||||
|
>
|
||||||
|
<FolderIcon style={{ fontSize: '0.875rem' }} />
|
||||||
|
{t("files.addFiles", "Add files")}
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -65,7 +65,8 @@ export function createToolFlow(config: ToolFlowConfig) {
|
|||||||
const steps = createToolSteps();
|
const steps = createToolSteps();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="sm" p="sm" h="95vh" w="100%" style={{ overflow: 'auto' }}>
|
<Stack gap="sm" p="sm" >
|
||||||
|
{/* <Stack gap="sm" p="sm" h="100%" w="100%" style={{ overflow: 'auto' }}> */}
|
||||||
<ToolStepProvider forceStepNumbers={config.forceStepNumbers}>
|
<ToolStepProvider forceStepNumbers={config.forceStepNumbers}>
|
||||||
{config.title && <ToolWorkflowTitle {...config.title} />}
|
{config.title && <ToolWorkflowTitle {...config.title} />}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ interface ToolButtonProps {
|
|||||||
tool: ToolRegistryEntry;
|
tool: ToolRegistryEntry;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
|
rounded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect }) => {
|
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect }) => {
|
||||||
|
@ -76,4 +76,4 @@
|
|||||||
.search-input-container {
|
.search-input-container {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
@ -4,18 +4,19 @@ import { useTranslation } from "react-i18next";
|
|||||||
import LocalIcon from '../../shared/LocalIcon';
|
import LocalIcon from '../../shared/LocalIcon';
|
||||||
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
|
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
|
||||||
import { TextInput } from "../../shared/TextInput";
|
import { TextInput } from "../../shared/TextInput";
|
||||||
import './ToolPicker.css';
|
import "./ToolPicker.css";
|
||||||
|
|
||||||
interface ToolSearchProps {
|
interface ToolSearchProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
toolRegistry: Readonly<Record<string, ToolRegistryEntry>>;
|
toolRegistry: Readonly<Record<string, ToolRegistryEntry>>;
|
||||||
onToolSelect?: (toolId: string) => void;
|
onToolSelect?: (toolId: string) => void;
|
||||||
mode: 'filter' | 'dropdown';
|
mode: "filter" | "dropdown" | "unstyled";
|
||||||
selectedToolKey?: string | null;
|
selectedToolKey?: string | null;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
hideIcon?: boolean;
|
hideIcon?: boolean;
|
||||||
onFocus?: () => void;
|
onFocus?: () => void;
|
||||||
|
autoFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolSearch = ({
|
const ToolSearch = ({
|
||||||
@ -23,11 +24,12 @@ const ToolSearch = ({
|
|||||||
onChange,
|
onChange,
|
||||||
toolRegistry,
|
toolRegistry,
|
||||||
onToolSelect,
|
onToolSelect,
|
||||||
mode = 'filter',
|
mode = "filter",
|
||||||
selectedToolKey,
|
selectedToolKey,
|
||||||
placeholder,
|
placeholder,
|
||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
onFocus
|
onFocus,
|
||||||
|
autoFocus = false,
|
||||||
}: ToolSearchProps) => {
|
}: ToolSearchProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
@ -38,9 +40,10 @@ const ToolSearch = ({
|
|||||||
if (!value.trim()) return [];
|
if (!value.trim()) return [];
|
||||||
return Object.entries(toolRegistry)
|
return Object.entries(toolRegistry)
|
||||||
.filter(([id, tool]) => {
|
.filter(([id, tool]) => {
|
||||||
if (mode === 'dropdown' && id === selectedToolKey) return false;
|
if (mode === "dropdown" && id === selectedToolKey) return false;
|
||||||
return tool.name.toLowerCase().includes(value.toLowerCase()) ||
|
return (
|
||||||
tool.description.toLowerCase().includes(value.toLowerCase());
|
tool.name.toLowerCase().includes(value.toLowerCase()) || tool.description.toLowerCase().includes(value.toLowerCase())
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.slice(0, 6)
|
.slice(0, 6)
|
||||||
.map(([id, tool]) => ({ id, tool }));
|
.map(([id, tool]) => ({ id, tool }));
|
||||||
@ -48,7 +51,7 @@ const ToolSearch = ({
|
|||||||
|
|
||||||
const handleSearchChange = (searchValue: string) => {
|
const handleSearchChange = (searchValue: string) => {
|
||||||
onChange(searchValue);
|
onChange(searchValue);
|
||||||
if (mode === 'dropdown') {
|
if (mode === "dropdown") {
|
||||||
setDropdownOpen(searchValue.trim().length > 0 && filteredTools.length > 0);
|
setDropdownOpen(searchValue.trim().length > 0 && filteredTools.length > 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -64,12 +67,20 @@ const ToolSearch = ({
|
|||||||
setDropdownOpen(false);
|
setDropdownOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Auto-focus the input when requested
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoFocus && searchRef.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
searchRef.current?.focus();
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
}, [autoFocus]);
|
||||||
|
|
||||||
const searchInput = (
|
const searchInput = (
|
||||||
<div className="search-input-container">
|
|
||||||
<TextInput
|
<TextInput
|
||||||
ref={searchRef}
|
ref={searchRef}
|
||||||
value={value}
|
value={value}
|
||||||
@ -77,36 +88,39 @@ const ToolSearch = ({
|
|||||||
placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")}
|
placeholder={placeholder || t("toolPicker.searchPlaceholder", "Search tools...")}
|
||||||
icon={hideIcon ? undefined : <LocalIcon icon="search-rounded" width="1.5rem" height="1.5rem" />}
|
icon={hideIcon ? undefined : <LocalIcon icon="search-rounded" width="1.5rem" height="1.5rem" />}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
|
onFocus={onFocus}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mode === 'filter') {
|
if (mode === "filter") {
|
||||||
|
return <div className="search-input-container">{searchInput}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "unstyled") {
|
||||||
return searchInput;
|
return searchInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={searchRef} style={{ position: 'relative' }}>
|
<div ref={searchRef} style={{ position: "relative" }}>
|
||||||
{searchInput}
|
{searchInput}
|
||||||
{dropdownOpen && filteredTools.length > 0 && (
|
{dropdownOpen && filteredTools.length > 0 && (
|
||||||
<div
|
<div
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
top: '100%',
|
top: "100%",
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
backgroundColor: 'var(--mantine-color-body)',
|
backgroundColor: "var(--mantine-color-body)",
|
||||||
border: '1px solid var(--mantine-color-gray-3)',
|
border: "1px solid var(--mantine-color-gray-3)",
|
||||||
borderRadius: '6px',
|
borderRadius: "6px",
|
||||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
|
||||||
maxHeight: '300px',
|
maxHeight: "300px",
|
||||||
overflowY: 'auto'
|
overflowY: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack gap="xs" style={{ padding: '8px' }}>
|
<Stack gap="xs" style={{ padding: "8px" }}>
|
||||||
{filteredTools.map(({ id, tool }) => (
|
{filteredTools.map(({ id, tool }) => (
|
||||||
<Button
|
<Button
|
||||||
key={id}
|
key={id}
|
||||||
@ -115,22 +129,18 @@ const ToolSearch = ({
|
|||||||
onToolSelect && onToolSelect(id);
|
onToolSelect && onToolSelect(id);
|
||||||
setDropdownOpen(false);
|
setDropdownOpen(false);
|
||||||
}}
|
}}
|
||||||
leftSection={
|
leftSection={<div style={{ color: "var(--tools-text-and-icon-color)" }}>{tool.icon}</div>}
|
||||||
<div style={{ color: 'var(--tools-text-and-icon-color)' }}>
|
|
||||||
{tool.icon}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
justify="flex-start"
|
justify="flex-start"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: '6px',
|
borderRadius: "6px",
|
||||||
color: 'var(--tools-text-and-icon-color)',
|
color: "var(--tools-text-and-icon-color)",
|
||||||
padding: '8px 12px'
|
padding: "8px 12px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ textAlign: 'left' }}>
|
<div style={{ textAlign: "left" }}>
|
||||||
<div style={{ fontWeight: 500 }}>{tool.name}</div>
|
<div style={{ fontWeight: 500 }}>{tool.name}</div>
|
||||||
<Text size="xs" c="dimmed" style={{ marginTop: '2px' }}>
|
<Text size="xs" c="dimmed" style={{ marginTop: "2px" }}>
|
||||||
{tool.description}
|
{tool.description}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
@ -134,7 +134,10 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
|
|
||||||
const setPreviewFile = useCallback((file: File | null) => {
|
const setPreviewFile = useCallback((file: File | null) => {
|
||||||
dispatch({ type: 'SET_PREVIEW_FILE', payload: file });
|
dispatch({ type: 'SET_PREVIEW_FILE', payload: file });
|
||||||
}, []);
|
if (file) {
|
||||||
|
actions.setMode('viewer');
|
||||||
|
}
|
||||||
|
}, [actions]);
|
||||||
|
|
||||||
const setPageEditorFunctions = useCallback((functions: PageEditorFunctions | null) => {
|
const setPageEditorFunctions = useCallback((functions: PageEditorFunctions | null) => {
|
||||||
dispatch({ type: 'SET_PAGE_EDITOR_FUNCTIONS', payload: functions });
|
dispatch({ type: 'SET_PAGE_EDITOR_FUNCTIONS', payload: functions });
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation';
|
import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation';
|
||||||
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
|
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
|
||||||
@ -16,18 +16,18 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
|
|||||||
const [automationName, setAutomationName] = useState('');
|
const [automationName, setAutomationName] = useState('');
|
||||||
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
|
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
|
||||||
|
|
||||||
const getToolName = (operation: string) => {
|
const getToolName = useCallback((operation: string) => {
|
||||||
const tool = toolRegistry?.[operation] as any;
|
const tool = toolRegistry?.[operation] as any;
|
||||||
return tool?.name || t(`tools.${operation}.name`, operation);
|
return tool?.name || t(`tools.${operation}.name`, operation);
|
||||||
};
|
}, [toolRegistry, t]);
|
||||||
|
|
||||||
const getToolDefaultParameters = (operation: string): Record<string, any> => {
|
const getToolDefaultParameters = useCallback((operation: string): Record<string, any> => {
|
||||||
const config = toolRegistry[operation]?.operationConfig;
|
const config = toolRegistry[operation]?.operationConfig;
|
||||||
if (config?.defaultParameters) {
|
if (config?.defaultParameters) {
|
||||||
return { ...config.defaultParameters };
|
return { ...config.defaultParameters };
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
};
|
}, [toolRegistry]);
|
||||||
|
|
||||||
// Initialize based on mode and existing automation
|
// Initialize based on mode and existing automation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -58,7 +58,7 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
|
|||||||
}));
|
}));
|
||||||
setSelectedTools(defaultTools);
|
setSelectedTools(defaultTools);
|
||||||
}
|
}
|
||||||
}, [mode, existingAutomation, selectedTools.length, t, getToolName]);
|
}, [mode, existingAutomation, t, getToolName]);
|
||||||
|
|
||||||
const addTool = (operation: string) => {
|
const addTool = (operation: string) => {
|
||||||
const newTool: AutomationTool = {
|
const newTool: AutomationTool = {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { AutomationConfig } from '../../../services/automationStorage';
|
import { AutomationConfig } from '../../../services/automationStorage';
|
||||||
|
import { SuggestedAutomation } from '../../../types/automation';
|
||||||
|
|
||||||
export interface SavedAutomation extends AutomationConfig {}
|
export interface SavedAutomation extends AutomationConfig {}
|
||||||
|
|
||||||
@ -40,6 +41,26 @@ export function useSavedAutomations() {
|
|||||||
}
|
}
|
||||||
}, [refreshAutomations]);
|
}, [refreshAutomations]);
|
||||||
|
|
||||||
|
const copyFromSuggested = useCallback(async (suggestedAutomation: SuggestedAutomation) => {
|
||||||
|
try {
|
||||||
|
const { automationStorage } = await import('../../../services/automationStorage');
|
||||||
|
|
||||||
|
// Convert suggested automation to saved automation format
|
||||||
|
const savedAutomation = {
|
||||||
|
name: suggestedAutomation.name,
|
||||||
|
description: suggestedAutomation.description,
|
||||||
|
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
|
// Load automations on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSavedAutomations();
|
loadSavedAutomations();
|
||||||
@ -50,6 +71,7 @@ export function useSavedAutomations() {
|
|||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
refreshAutomations,
|
refreshAutomations,
|
||||||
deleteAutomation
|
deleteAutomation,
|
||||||
|
copyFromSuggested
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -17,9 +17,60 @@ export function useSuggestedAutomations(): SuggestedAutomation[] {
|
|||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: "compress-and-split",
|
id: "secure-pdf-ingestion",
|
||||||
name: t("automation.suggested.compressAndSplit", "Compress & Split"),
|
name: t("automation.suggested.securePdfIngestion", "Secure PDF Ingestion"),
|
||||||
description: t("automation.suggested.compressAndSplitDesc", "Compress PDFs and split them by pages"),
|
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: [
|
operations: [
|
||||||
{
|
{
|
||||||
operation: "compress",
|
operation: "compress",
|
||||||
@ -36,45 +87,37 @@ export function useSuggestedAutomations(): SuggestedAutomation[] {
|
|||||||
operation: "splitPdf",
|
operation: "splitPdf",
|
||||||
parameters: {
|
parameters: {
|
||||||
mode: 'bySizeOrCount',
|
mode: 'bySizeOrCount',
|
||||||
pages: '1',
|
pages: '',
|
||||||
hDiv: '2',
|
hDiv: '1',
|
||||||
vDiv: '2',
|
vDiv: '1',
|
||||||
merge: false,
|
merge: false,
|
||||||
splitType: 'pages',
|
splitType: 'size',
|
||||||
splitValue: '1',
|
splitValue: '20MB',
|
||||||
bookmarkLevel: '1',
|
bookmarkLevel: '1',
|
||||||
includeMetadata: false,
|
includeMetadata: false,
|
||||||
allowDuplicates: false,
|
allowDuplicates: false,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operation: "sanitize",
|
||||||
|
parameters: {
|
||||||
|
removeJavaScript: false,
|
||||||
|
removeEmbeddedFiles: false,
|
||||||
|
removeXMPMetadata: true,
|
||||||
|
removeMetadata: true,
|
||||||
|
removeLinks: false,
|
||||||
|
removeFonts: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
icon: CompressIcon,
|
icon: CompressIcon,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "ocr-workflow",
|
|
||||||
name: t("automation.suggested.ocrWorkflow", "OCR Processing"),
|
|
||||||
description: t("automation.suggested.ocrWorkflowDesc", "Extract text from PDFs using OCR technology"),
|
|
||||||
operations: [
|
|
||||||
{
|
|
||||||
operation: "ocr",
|
|
||||||
parameters: {
|
|
||||||
languages: ['eng'],
|
|
||||||
ocrType: 'skip-text',
|
|
||||||
ocrRenderType: 'hocr',
|
|
||||||
additionalOptions: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
icon: TextFieldsIcon,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
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", "Sanitize PDFs and add password protection"),
|
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."),
|
||||||
operations: [
|
operations: [
|
||||||
{
|
{
|
||||||
operation: "sanitize",
|
operation: "sanitize",
|
||||||
@ -111,23 +154,32 @@ export function useSuggestedAutomations(): SuggestedAutomation[] {
|
|||||||
icon: SecurityIcon,
|
icon: SecurityIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "optimization-workflow",
|
id: "process-images",
|
||||||
name: t("automation.suggested.optimizationWorkflow", "Optimization Workflow"),
|
name: t("automation.suggested.processImages", "Process Images"),
|
||||||
description: t("automation.suggested.optimizationWorkflowDesc", "Repair and compress PDFs for better performance"),
|
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: [
|
operations: [
|
||||||
{
|
{
|
||||||
operation: "repair",
|
operation: "convert",
|
||||||
parameters: {}
|
parameters: {
|
||||||
|
fromExtension: 'image',
|
||||||
|
toExtension: 'pdf',
|
||||||
|
imageOptions: {
|
||||||
|
colorType: 'color',
|
||||||
|
dpi: 300,
|
||||||
|
singleOrMultiple: 'multiple',
|
||||||
|
fitOption: 'maintainAspectRatio',
|
||||||
|
autoRotate: true,
|
||||||
|
combineImages: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
operation: "compress",
|
operation: "ocr",
|
||||||
parameters: {
|
parameters: {
|
||||||
compressionLevel: 7,
|
languages: ['eng'],
|
||||||
grayscale: false,
|
ocrType: 'skip-text',
|
||||||
expectedSize: '',
|
ocrRenderType: 'hocr',
|
||||||
compressionMethod: 'quality',
|
additionalOptions: [],
|
||||||
fileSizeValue: '',
|
|
||||||
fileSizeUnit: 'MB',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -2,6 +2,7 @@ import React, { useState } 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 { 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";
|
||||||
@ -13,20 +14,21 @@ import { useAutomateOperation } from "../hooks/tools/automate/useAutomateOperati
|
|||||||
import { BaseToolProps } from "../types/tool";
|
import { BaseToolProps } from "../types/tool";
|
||||||
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
||||||
import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations";
|
import { useSavedAutomations } from "../hooks/tools/automate/useSavedAutomations";
|
||||||
import { AutomationConfig, AutomationStepData, AutomationMode } from "../types/automation";
|
import { AutomationConfig, AutomationStepData, AutomationMode, AutomationStep } from "../types/automation";
|
||||||
import { AUTOMATION_STEPS } from "../constants/automation";
|
import { AUTOMATION_STEPS } from "../constants/automation";
|
||||||
|
|
||||||
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { selectedFiles } = useFileSelection();
|
const { selectedFiles } = useFileSelection();
|
||||||
|
const { setMode } = useNavigation();
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState<'selection' | 'creation' | 'run'>(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 });
|
||||||
|
|
||||||
const automateOperation = useAutomateOperation();
|
const automateOperation = useAutomateOperation();
|
||||||
const toolRegistry = useFlatToolRegistry();
|
const toolRegistry = useFlatToolRegistry();
|
||||||
const hasResults = automateOperation.files.length > 0 || automateOperation.downloadUrl !== null;
|
const hasResults = automateOperation.files.length > 0 || automateOperation.downloadUrl !== null;
|
||||||
const { savedAutomations, deleteAutomation, refreshAutomations } = useSavedAutomations();
|
const { savedAutomations, deleteAutomation, refreshAutomations, copyFromSuggested } = useSavedAutomations();
|
||||||
|
|
||||||
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
|
||||||
@ -62,7 +64,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
|
|
||||||
const renderCurrentStep = () => {
|
const renderCurrentStep = () => {
|
||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
case 'selection':
|
case AUTOMATION_STEPS.SELECTION:
|
||||||
return (
|
return (
|
||||||
<AutomationSelection
|
<AutomationSelection
|
||||||
savedAutomations={savedAutomations}
|
savedAutomations={savedAutomations}
|
||||||
@ -77,10 +79,18 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
onError?.(`Failed to delete automation: ${automation.name}`);
|
onError?.(`Failed to delete automation: ${automation.name}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onCopyFromSuggested={async (suggestedAutomation) => {
|
||||||
|
try {
|
||||||
|
await copyFromSuggested(suggestedAutomation);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to copy suggested automation:', error);
|
||||||
|
onError?.(`Failed to copy automation: ${suggestedAutomation.name}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'creation':
|
case AUTOMATION_STEPS.CREATION:
|
||||||
if (!stepData.mode) {
|
if (!stepData.mode) {
|
||||||
console.error('Creation mode is undefined');
|
console.error('Creation mode is undefined');
|
||||||
return null;
|
return null;
|
||||||
@ -98,7 +108,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'run':
|
case AUTOMATION_STEPS.RUN:
|
||||||
if (!stepData.automation) {
|
if (!stepData.automation) {
|
||||||
console.error('Automation config is undefined');
|
console.error('Automation config is undefined');
|
||||||
return null;
|
return null;
|
||||||
@ -171,7 +181,11 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
review: {
|
review: {
|
||||||
isVisible: hasResults && currentStep === AUTOMATION_STEPS.RUN,
|
isVisible: hasResults && currentStep === AUTOMATION_STEPS.RUN,
|
||||||
operation: automateOperation,
|
operation: automateOperation,
|
||||||
title: t('automate.reviewTitle', 'Automation Results')
|
title: t('automate.reviewTitle', 'Automation Results'),
|
||||||
|
onFileClick: (file: File) => {
|
||||||
|
onPreviewFile?.(file);
|
||||||
|
setMode('viewer');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -24,8 +24,10 @@ export interface AutomationTool {
|
|||||||
parameters?: Record<string, any>;
|
parameters?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AutomationStep = typeof import('../constants/automation').AUTOMATION_STEPS[keyof typeof import('../constants/automation').AUTOMATION_STEPS];
|
||||||
|
|
||||||
export interface AutomationStepData {
|
export interface AutomationStepData {
|
||||||
step: 'selection' | 'creation' | 'run';
|
step: AutomationStep;
|
||||||
mode?: AutomationMode;
|
mode?: AutomationMode;
|
||||||
automation?: AutomationConfig;
|
automation?: AutomationConfig;
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ export const isValidMode = (mode: string): mode is ModeType => {
|
|||||||
return validModes.includes(mode as ModeType);
|
return validModes.includes(mode as ModeType);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDefaultMode = (): ModeType => 'pageEditor';
|
export const getDefaultMode = (): ModeType => 'fileEditor';
|
||||||
|
|
||||||
// Route parsing result
|
// Route parsing result
|
||||||
export interface ToolRoute {
|
export interface ToolRoute {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user