This commit is contained in:
Typer_Body
2026-05-07 23:33:54 +08:00
parent d176a448e0
commit fc40d3c949
34 changed files with 2297 additions and 1075 deletions
+18 -9
View File
@@ -74,15 +74,24 @@ class ConditionNode(WorkflowNode):
left_num = float(left) left_num = float(left)
right_num = float(right) right_num = float(right)
if operator == "==": return left_num == right_num if operator == "==":
elif operator == "!=": return left_num != right_num return left_num == right_num
elif operator == ">": return left_num > right_num elif operator == "!=":
elif operator == "<": return left_num < right_num return left_num != right_num
elif operator == ">=": return left_num >= right_num elif operator == ">":
elif operator == "<=": return left_num <= right_num return left_num > right_num
elif operator == "<":
return left_num < right_num
elif operator == ">=":
return left_num >= right_num
elif operator == "<=":
return left_num <= right_num
except ValueError: except ValueError:
if operator == "==": return left == right if operator == "==":
elif operator == "!=": return left != right return left == right
elif operator in (">", "<", ">=", "<="): return False elif operator == "!=":
return left != right
elif operator in (">", "<", ">=", "<="):
return False
return False return False
@@ -31,7 +31,6 @@ class DatabaseQueryNode(WorkflowNode):
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]: async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
connection_type = self.get_config("connection_type", "postgresql") connection_type = self.get_config("connection_type", "postgresql")
connection_string = self.get_config("connection_string", "")
query = self.get_config("query", "") query = self.get_config("query", "")
query_type = self.get_config("query_type", "select") query_type = self.get_config("query_type", "select")
timeout = self.get_config("timeout", 30) timeout = self.get_config("timeout", 30)
@@ -30,7 +30,6 @@ class ParameterExtractorNode(WorkflowNode):
config_schema: ClassVar[list[NodeConfig]] = [] config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]: async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
text = inputs.get("text", "")
param_defs = self.get_config("parameters", []) param_defs = self.get_config("parameters", [])
extracted = {} extracted = {}
@@ -30,7 +30,6 @@ class QuestionClassifierNode(WorkflowNode):
config_schema: ClassVar[list[NodeConfig]] = [] config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]: async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
question = inputs.get("question", "")
categories = self.get_config("categories", []) categories = self.get_config("categories", [])
if categories: if categories:
@@ -30,7 +30,4 @@ class SendMessageNode(WorkflowNode):
config_schema: ClassVar[list[NodeConfig]] = [] config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]: async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
message = inputs.get("message", "")
target = inputs.get("target") or self.get_config("target_id", "")
return {"status": "sent", "message_id": f"msg_{context.execution_id}"} return {"status": "sent", "message_id": f"msg_{context.execution_id}"}
@@ -161,7 +161,12 @@ function isEntityCategory(id: string): id is EntityCategoryId {
// Map sidebar config IDs to SidebarDataContext keys // Map sidebar config IDs to SidebarDataContext keys
const ENTITY_KEY_MAP: Record< const ENTITY_KEY_MAP: Record<
EntityCategoryId, EntityCategoryId,
'bots' | 'pipelines' | 'workflows' | 'knowledgeBases' | 'plugins' | 'mcpServers' | 'bots'
| 'pipelines'
| 'workflows'
| 'knowledgeBases'
| 'plugins'
| 'mcpServers'
> = { > = {
bots: 'bots', bots: 'bots',
pipelines: 'pipelines', pipelines: 'pipelines',
@@ -1,6 +1,9 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSidebarData, SidebarEntityItem } from '../home-sidebar/SidebarDataContext'; import {
useSidebarData,
SidebarEntityItem,
} from '../home-sidebar/SidebarDataContext';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
@@ -43,7 +46,8 @@ export default function UnifiedBindingSelector({
className, className,
}: UnifiedBindingSelectorProps) { }: UnifiedBindingSelectorProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { pipelines, workflows, refreshPipelines, refreshWorkflows } = useSidebarData(); const { pipelines, workflows, refreshPipelines, refreshWorkflows } =
useSidebarData();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
useEffect(() => { useEffect(() => {
@@ -137,7 +141,9 @@ export default function UnifiedBindingSelector({
{/* Entity selection */} {/* Entity selection */}
<div className="space-y-2"> <div className="space-y-2">
<Label> <Label>
{value.type === 'pipeline' ? t('bots.selectPipeline') : t('bots.selectWorkflow')} {value.type === 'pipeline'
? t('bots.selectPipeline')
: t('bots.selectWorkflow')}
</Label> </Label>
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@@ -171,7 +177,9 @@ export default function UnifiedBindingSelector({
<Check <Check
className={cn( className={cn(
'mr-2 h-4 w-4', 'mr-2 h-4 w-4',
value.id === pipeline.id ? 'opacity-100' : 'opacity-0' value.id === pipeline.id
? 'opacity-100'
: 'opacity-0',
)} )}
/> />
<div className="flex items-center gap-2 flex-1 text-left"> <div className="flex items-center gap-2 flex-1 text-left">
@@ -186,37 +194,37 @@ export default function UnifiedBindingSelector({
</Button> </Button>
)) ))
) )
) : workflows.length === 0 ? (
<div className="py-6 text-center text-sm text-muted-foreground">
{t('bots.noWorkflowsFound')}
</div>
) : ( ) : (
workflows.length === 0 ? ( workflows.map((workflow) => (
<div className="py-6 text-center text-sm text-muted-foreground"> <Button
{t('bots.noWorkflowsFound')} key={workflow.id}
</div> variant="ghost"
) : ( className="w-full justify-start"
workflows.map((workflow) => ( onClick={() => handleSelect(workflow.id, 'workflow')}
<Button >
key={workflow.id} <Check
variant="ghost" className={cn(
className="w-full justify-start" 'mr-2 h-4 w-4',
onClick={() => handleSelect(workflow.id, 'workflow')} value.id === workflow.id
> ? 'opacity-100'
<Check : 'opacity-0',
className={cn(
'mr-2 h-4 w-4',
value.id === workflow.id ? 'opacity-100' : 'opacity-0'
)}
/>
<div className="flex items-center gap-2 flex-1 text-left">
{workflow.emoji && <span>{workflow.emoji}</span>}
<span className="truncate">{workflow.name}</span>
</div>
{workflow.description && (
<span className="text-xs text-muted-foreground truncate max-w-[120px]">
{workflow.description}
</span>
)} )}
</Button> />
)) <div className="flex items-center gap-2 flex-1 text-left">
) {workflow.emoji && <span>{workflow.emoji}</span>}
<span className="truncate">{workflow.name}</span>
</div>
{workflow.description && (
<span className="text-xs text-muted-foreground truncate max-w-[120px]">
{workflow.description}
</span>
)}
</Button>
))
)} )}
</div> </div>
</ScrollArea> </ScrollArea>
@@ -2,7 +2,13 @@ import { useState, useEffect, useCallback, useRef } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
@@ -12,7 +18,15 @@ import WorkflowExecutionsTab from './components/workflow-executions/WorkflowExec
import WorkflowDebugDialog from './components/workflow-debug-dialog/WorkflowDebugDialog'; import WorkflowDebugDialog from './components/workflow-debug-dialog/WorkflowDebugDialog';
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext'; import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Settings, Play, BarChart3, GitBranch, Download, Upload, Bug } from 'lucide-react'; import {
Settings,
Play,
BarChart3,
GitBranch,
Download,
Upload,
Bug,
} from 'lucide-react';
import { backendClient } from '@/app/infra/http'; import { backendClient } from '@/app/infra/http';
import { Workflow } from '@/app/infra/entities/api'; import { Workflow } from '@/app/infra/entities/api';
import { useWorkflowStore } from './store/useWorkflowStore'; import { useWorkflowStore } from './store/useWorkflowStore';
@@ -24,7 +38,7 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
const { refreshWorkflows, workflows, setDetailEntityName } = useSidebarData(); const { refreshWorkflows, workflows, setDetailEntityName } = useSidebarData();
const { const {
currentWorkflow, currentWorkflow,
setCurrentWorkflow, setCurrentWorkflow,
@@ -43,7 +57,11 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
const [activeTab, setActiveTab] = useState('editor'); const [activeTab, setActiveTab] = useState('editor');
const [workflow, setWorkflow] = useState<Workflow | null>(null); const [workflow, setWorkflow] = useState<Workflow | null>(null);
const [createStep, setCreateStep] = useState<'basic' | 'editor'>('basic'); const [createStep, setCreateStep] = useState<'basic' | 'editor'>('basic');
const [basicInfo, setBasicInfo] = useState<{ name: string; description: string; emoji: string }>({ const [basicInfo, setBasicInfo] = useState<{
name: string;
description: string;
emoji: string;
}>({
name: '', name: '',
description: '', description: '',
emoji: '🔄', emoji: '🔄',
@@ -65,11 +83,14 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
// Load node types // Load node types
useEffect(() => { useEffect(() => {
if (nodeTypes.length === 0) { if (nodeTypes.length === 0) {
backendClient.getWorkflowNodeTypes().then((resp) => { backendClient
setNodeTypes(resp.node_types, resp.categories); .getWorkflowNodeTypes()
}).catch((err) => { .then((resp) => {
console.error('Failed to load node types:', err); setNodeTypes(resp.node_types, resp.categories);
}); })
.catch((err) => {
console.error('Failed to load node types:', err);
});
} }
}, [nodeTypes.length, setNodeTypes]); }, [nodeTypes.length, setNodeTypes]);
@@ -82,16 +103,23 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
} }
setLoading(true); setLoading(true);
backendClient.getWorkflow(id).then((resp) => { backendClient
setWorkflow(resp.workflow); .getWorkflow(id)
setCurrentWorkflow(resp.workflow); .then((resp) => {
fromWorkflowDefinition(resp.workflow.nodes || [], resp.workflow.edges || []); setWorkflow(resp.workflow);
}).catch((err) => { setCurrentWorkflow(resp.workflow);
console.error('Failed to load workflow:', err); fromWorkflowDefinition(
toast.error(t('workflows.loadError')); resp.workflow.nodes || [],
}).finally(() => { resp.workflow.edges || [],
setLoading(false); );
}); })
.catch((err) => {
console.error('Failed to load workflow:', err);
toast.error(t('workflows.loadError'));
})
.finally(() => {
setLoading(false);
});
return () => { return () => {
reset(); reset();
@@ -105,7 +133,7 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
setSaving(true); setSaving(true);
try { try {
const { nodes, edges } = toWorkflowDefinition(); const { nodes, edges } = toWorkflowDefinition();
if (isCreateMode) { if (isCreateMode) {
const resp = await backendClient.createWorkflow({ const resp = await backendClient.createWorkflow({
name: basicInfo.name || t('workflows.newWorkflow'), name: basicInfo.name || t('workflows.newWorkflow'),
@@ -138,12 +166,22 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
} finally { } finally {
setSaving(false); setSaving(false);
} }
}, [id, isCreateMode, workflow, isSaving, toWorkflowDefinition, refreshWorkflows, navigate, t, basicInfo]); }, [
id,
isCreateMode,
workflow,
isSaving,
toWorkflowDefinition,
refreshWorkflows,
navigate,
t,
basicInfo,
]);
// Export workflow handler // Export workflow handler
const handleExport = useCallback(() => { const handleExport = useCallback(() => {
const { nodes, edges } = toWorkflowDefinition(); const { nodes, edges } = toWorkflowDefinition();
const exportData = { const exportData = {
name: workflow?.name || t('workflows.newWorkflow'), name: workflow?.name || t('workflows.newWorkflow'),
description: workflow?.description || '', description: workflow?.description || '',
@@ -155,8 +193,10 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
version: '1.0', version: '1.0',
exportedAt: new Date().toISOString(), exportedAt: new Date().toISOString(),
}; };
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const blob = new Blob([JSON.stringify(exportData, null, 2)], {
type: 'application/json',
});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
@@ -165,84 +205,103 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
toast.success(t('workflows.exportSuccess')); toast.success(t('workflows.exportSuccess'));
}, [workflow, toWorkflowDefinition, t]); }, [workflow, toWorkflowDefinition, t]);
// Import workflow handler // Import workflow handler
const handleImport = useCallback((file: File) => { const handleImport = useCallback(
const reader = new FileReader(); (file: File) => {
reader.onload = (e) => { const reader = new FileReader();
try { reader.onload = (e) => {
const importData = JSON.parse(e.target?.result as string); try {
const importData = JSON.parse(e.target?.result as string);
// Validate imported data structure
if (!importData.nodes || !Array.isArray(importData.nodes)) { // Validate imported data structure
throw new Error('Invalid workflow file: missing nodes'); if (!importData.nodes || !Array.isArray(importData.nodes)) {
} throw new Error('Invalid workflow file: missing nodes');
if (!importData.edges || !Array.isArray(importData.edges)) {
throw new Error('Invalid workflow file: missing edges');
}
// Validate each node has required fields
const nodeIds = new Set<string>();
for (const node of importData.nodes) {
if (!node.id || !node.type) {
throw new Error(`Invalid node: missing id or type`);
} }
if (!node.position || typeof node.position.x !== 'number' || typeof node.position.y !== 'number') { if (!importData.edges || !Array.isArray(importData.edges)) {
throw new Error(`Invalid node "${node.id}": missing or invalid position`); throw new Error('Invalid workflow file: missing edges');
} }
nodeIds.add(node.id);
// Validate each node has required fields
const nodeIds = new Set<string>();
for (const node of importData.nodes) {
if (!node.id || !node.type) {
throw new Error(`Invalid node: missing id or type`);
}
if (
!node.position ||
typeof node.position.x !== 'number' ||
typeof node.position.y !== 'number'
) {
throw new Error(
`Invalid node "${node.id}": missing or invalid position`,
);
}
nodeIds.add(node.id);
}
// Validate each edge has required fields and references existing nodes
for (const edge of importData.edges) {
if (!edge.id || !edge.source || !edge.target) {
throw new Error(`Invalid edge: missing id, source, or target`);
}
if (!nodeIds.has(edge.source)) {
throw new Error(
`Edge "${edge.id}" references unknown source node "${edge.source}"`,
);
}
if (!nodeIds.has(edge.target)) {
throw new Error(
`Edge "${edge.id}" references unknown target node "${edge.target}"`,
);
}
}
// Load nodes and edges into the store
fromWorkflowDefinition(importData.nodes, importData.edges);
// Update workflow metadata if available
if (
workflow &&
(importData.name || importData.description || importData.emoji)
) {
setWorkflow({
...workflow,
name: importData.name || workflow.name,
description: importData.description || workflow.description,
emoji: importData.emoji || workflow.emoji,
variables: importData.variables || workflow.variables,
settings: importData.settings || workflow.settings,
});
}
setDirty(true);
toast.success(t('workflows.importSuccess'));
} catch (error) {
console.error('Failed to import workflow:', error);
toast.error(t('workflows.importError'));
} }
};
// Validate each edge has required fields and references existing nodes reader.readAsText(file);
for (const edge of importData.edges) { },
if (!edge.id || !edge.source || !edge.target) { [workflow, fromWorkflowDefinition, setDirty, t],
throw new Error(`Invalid edge: missing id, source, or target`); );
}
if (!nodeIds.has(edge.source)) {
throw new Error(`Edge "${edge.id}" references unknown source node "${edge.source}"`);
}
if (!nodeIds.has(edge.target)) {
throw new Error(`Edge "${edge.id}" references unknown target node "${edge.target}"`);
}
}
// Load nodes and edges into the store
fromWorkflowDefinition(importData.nodes, importData.edges);
// Update workflow metadata if available
if (workflow && (importData.name || importData.description || importData.emoji)) {
setWorkflow({
...workflow,
name: importData.name || workflow.name,
description: importData.description || workflow.description,
emoji: importData.emoji || workflow.emoji,
variables: importData.variables || workflow.variables,
settings: importData.settings || workflow.settings,
});
}
setDirty(true);
toast.success(t('workflows.importSuccess'));
} catch (error) {
console.error('Failed to import workflow:', error);
toast.error(t('workflows.importError'));
}
};
reader.readAsText(file);
}, [workflow, fromWorkflowDefinition, setDirty, t]);
// Handle file input change // Handle file input change
const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = useCallback(
const file = e.target.files?.[0]; (e: React.ChangeEvent<HTMLInputElement>) => {
if (file) { const file = e.target.files?.[0];
handleImport(file); if (file) {
// Reset file input handleImport(file);
e.target.value = ''; // Reset file input
} e.target.value = '';
}, [handleImport]); }
},
[handleImport],
);
// Publish handler // Publish handler
const handlePublish = useCallback(async () => { const handlePublish = useCallback(async () => {
@@ -289,7 +348,10 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
style={{ display: 'none' }} style={{ display: 'none' }}
ref={fileInputRef} ref={fileInputRef}
/> />
<Button variant="outline" onClick={() => fileInputRef.current?.click()}> <Button
variant="outline"
onClick={() => fileInputRef.current?.click()}
>
<Upload className="size-4 mr-1" /> <Upload className="size-4 mr-1" />
{t('workflows.import')} {t('workflows.import')}
</Button> </Button>
@@ -307,17 +369,26 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>{t('workflows.basicInfo')}</CardTitle> <CardTitle>{t('workflows.basicInfo')}</CardTitle>
<CardDescription>{t('workflows.basicInfoDesc')}</CardDescription> <CardDescription>
{t('workflows.basicInfoDesc')}
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="workflow-name">{t('workflows.name')}</Label> <Label htmlFor="workflow-name">{t('workflows.name')}</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<EmojiPicker value={basicInfo.emoji} onChange={(emoji: string) => setBasicInfo({ ...basicInfo, emoji })} /> <EmojiPicker
value={basicInfo.emoji}
onChange={(emoji: string) =>
setBasicInfo({ ...basicInfo, emoji })
}
/>
<Input <Input
id="workflow-name" id="workflow-name"
value={basicInfo.name} value={basicInfo.name}
onChange={(e) => setBasicInfo({ ...basicInfo, name: e.target.value })} onChange={(e) =>
setBasicInfo({ ...basicInfo, name: e.target.value })
}
placeholder={t('workflows.namePlaceholder')} placeholder={t('workflows.namePlaceholder')}
className="flex-1" className="flex-1"
/> />
@@ -325,11 +396,18 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="workflow-description">{t('workflows.description')}</Label> <Label htmlFor="workflow-description">
{t('workflows.description')}
</Label>
<Textarea <Textarea
id="workflow-description" id="workflow-description"
value={basicInfo.description} value={basicInfo.description}
onChange={(e) => setBasicInfo({ ...basicInfo, description: e.target.value })} onChange={(e) =>
setBasicInfo({
...basicInfo,
description: e.target.value,
})
}
placeholder={t('workflows.descriptionPlaceholder')} placeholder={t('workflows.descriptionPlaceholder')}
rows={3} rows={3}
/> />
@@ -377,12 +455,15 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
style={{ display: 'none' }} style={{ display: 'none' }}
ref={fileInputRef} ref={fileInputRef}
/> />
{/* Sticky Header: title + save button */} {/* Sticky Header: title + save button */}
<div className="flex items-center justify-between pb-4 shrink-0"> <div className="flex items-center justify-between pb-4 shrink-0">
<h1 className="text-xl font-semibold">{t('workflows.editWorkflow')}</h1> <h1 className="text-xl font-semibold">{t('workflows.editWorkflow')}</h1>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="outline" onClick={() => fileInputRef.current?.click()}> <Button
variant="outline"
onClick={() => fileInputRef.current?.click()}
>
<Upload className="size-4 mr-1" /> <Upload className="size-4 mr-1" />
{t('workflows.import')} {t('workflows.import')}
</Button> </Button>
@@ -434,10 +515,7 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
</TabsList> </TabsList>
{/* Tab: Editor */} {/* Tab: Editor */}
<TabsContent <TabsContent value="editor" className="flex-1 min-h-0 mt-4">
value="editor"
className="flex-1 min-h-0 mt-4"
>
<WorkflowEditorComponent /> <WorkflowEditorComponent />
</TabsContent> </TabsContent>
@@ -1,6 +1,10 @@
import { useState, useCallback, useEffect, useRef } from 'react'; import { useState, useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useWorkflowStore, DebugLog, NodeExecutionResult } from '../../store/useWorkflowStore'; import {
useWorkflowStore,
DebugLog,
NodeExecutionResult,
} from '../../store/useWorkflowStore';
import { backendClient } from '@/app/infra/http'; import { backendClient } from '@/app/infra/http';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -10,7 +14,13 @@ import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
@@ -66,7 +76,10 @@ const logLevelColors: Record<string, string> = {
debug: 'text-gray-400', debug: 'text-gray-400',
}; };
export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebuggerProps) { export default function WorkflowDebugger({
workflowId,
onClose,
}: WorkflowDebuggerProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const logsEndRef = useRef<HTMLDivElement>(null); const logsEndRef = useRef<HTMLDivElement>(null);
const [activeTab, setActiveTab] = useState<string>('context'); const [activeTab, setActiveTab] = useState<string>('context');
@@ -123,9 +136,9 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
setDebugState('running'); setDebugState('running');
clearNodeExecutionResults(); clearNodeExecutionResults();
clearDebugLogs(); clearDebugLogs();
addDebugLog({ level: 'info', message: t('workflows.debug.starting') }); addDebugLog({ level: 'info', message: t('workflows.debug.starting') });
const response = await backendClient.startWorkflowDebug(workflowId, { const response = await backendClient.startWorkflowDebug(workflowId, {
context: { context: {
message_content: debugContext.messageContent, message_content: debugContext.messageContent,
@@ -136,125 +149,169 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
is_group: debugContext.isGroup, is_group: debugContext.isGroup,
}, },
variables: debugContext.customVariables, variables: debugContext.customVariables,
breakpoints: Object.keys(breakpoints).filter(k => breakpoints[k]), breakpoints: Object.keys(breakpoints).filter((k) => breakpoints[k]),
}); });
setDebugExecutionId(response.execution_id); setDebugExecutionId(response.execution_id);
addDebugLog({ level: 'info', message: t('workflows.debug.started', { id: response.execution_id }) }); addDebugLog({
level: 'info',
message: t('workflows.debug.started', { id: response.execution_id }),
});
// Start polling for state updates // Start polling for state updates
pollDebugState(response.execution_id); pollDebugState(response.execution_id);
} catch (error) { } catch (error) {
setDebugState('error'); setDebugState('error');
addDebugLog({ level: 'error', message: `${t('workflows.debug.startError')}: ${error}` }); addDebugLog({
level: 'error',
message: `${t('workflows.debug.startError')}: ${error}`,
});
} }
}, [workflowId, debugContext, breakpoints, t]); }, [workflowId, debugContext, breakpoints, t]);
// Poll debug state // Poll debug state
const pollDebugState = useCallback(async (executionId: string) => { const pollDebugState = useCallback(
pollCancelledRef.current = false; async (executionId: string) => {
const poll = async () => { pollCancelledRef.current = false;
if (pollCancelledRef.current) return; const poll = async () => {
try {
const state = await backendClient.getWorkflowDebugState(workflowId, executionId);
if (pollCancelledRef.current) return; if (pollCancelledRef.current) return;
try {
setDebugState(state.status as typeof debugState); const state = await backendClient.getWorkflowDebugState(
setCurrentNodeId(state.current_node_id || null); workflowId,
executionId,
// Update node execution results );
if (state.node_states) { if (pollCancelledRef.current) return;
for (const [nodeId, nodeState] of Object.entries(state.node_states)) {
updateNodeExecutionResult(nodeId, nodeState as Partial<NodeExecutionResult>); setDebugState(state.status as typeof debugState);
setCurrentNodeId(state.current_node_id || null);
// Update node execution results
if (state.node_states) {
for (const [nodeId, nodeState] of Object.entries(
state.node_states,
)) {
updateNodeExecutionResult(
nodeId,
nodeState as Partial<NodeExecutionResult>,
);
}
} }
}
// Add new logs
// Add new logs if (state.new_logs) {
if (state.new_logs) { for (const log of state.new_logs) {
for (const log of state.new_logs) { addDebugLog(log);
addDebugLog(log); }
} }
// Continue polling if still running or paused
if (
!pollCancelledRef.current &&
(state.status === 'running' || state.status === 'paused')
) {
setTimeout(poll, 500);
} else if (state.status === 'completed') {
addDebugLog({
level: 'info',
message: t('workflows.debug.completed'),
});
} else if (state.status === 'error') {
addDebugLog({
level: 'error',
message: state.error || t('workflows.debug.unknownError'),
});
}
} catch (error) {
console.error('Failed to poll debug state:', error);
} }
};
// Continue polling if still running or paused
if (!pollCancelledRef.current && (state.status === 'running' || state.status === 'paused')) { poll();
setTimeout(poll, 500); },
} else if (state.status === 'completed') { [workflowId, t],
addDebugLog({ level: 'info', message: t('workflows.debug.completed') }); );
} else if (state.status === 'error') {
addDebugLog({ level: 'error', message: state.error || t('workflows.debug.unknownError') });
}
} catch (error) {
console.error('Failed to poll debug state:', error);
}
};
poll();
}, [workflowId, t]);
// Pause execution // Pause execution
const handlePause = useCallback(async () => { const handlePause = useCallback(async () => {
if (!debugExecutionId) return; if (!debugExecutionId) return;
try { try {
await backendClient.pauseWorkflowDebug(workflowId, debugExecutionId); await backendClient.pauseWorkflowDebug(workflowId, debugExecutionId);
setDebugState('paused'); setDebugState('paused');
addDebugLog({ level: 'info', message: t('workflows.debug.paused') }); addDebugLog({ level: 'info', message: t('workflows.debug.paused') });
} catch (error) { } catch (error) {
addDebugLog({ level: 'error', message: `${t('workflows.debug.pauseError')}: ${error}` }); addDebugLog({
level: 'error',
message: `${t('workflows.debug.pauseError')}: ${error}`,
});
} }
}, [workflowId, debugExecutionId, t]); }, [workflowId, debugExecutionId, t]);
// Resume execution // Resume execution
const handleResume = useCallback(async () => { const handleResume = useCallback(async () => {
if (!debugExecutionId) return; if (!debugExecutionId) return;
try { try {
await backendClient.resumeWorkflowDebug(workflowId, debugExecutionId); await backendClient.resumeWorkflowDebug(workflowId, debugExecutionId);
setDebugState('running'); setDebugState('running');
addDebugLog({ level: 'info', message: t('workflows.debug.resumed') }); addDebugLog({ level: 'info', message: t('workflows.debug.resumed') });
pollDebugState(debugExecutionId); pollDebugState(debugExecutionId);
} catch (error) { } catch (error) {
addDebugLog({ level: 'error', message: `${t('workflows.debug.resumeError')}: ${error}` }); addDebugLog({
level: 'error',
message: `${t('workflows.debug.resumeError')}: ${error}`,
});
} }
}, [workflowId, debugExecutionId, t, pollDebugState]); }, [workflowId, debugExecutionId, t, pollDebugState]);
// Step execution // Step execution
const handleStep = useCallback(async () => { const handleStep = useCallback(async () => {
if (!debugExecutionId) return; if (!debugExecutionId) return;
try { try {
const result = await backendClient.stepWorkflowDebug(workflowId, debugExecutionId); const result = await backendClient.stepWorkflowDebug(
workflowId,
debugExecutionId,
);
if (result.node_id) { if (result.node_id) {
setCurrentNodeId(result.node_id); setCurrentNodeId(result.node_id);
updateNodeExecutionResult(result.node_id, result.node_state as Partial<NodeExecutionResult>); updateNodeExecutionResult(
addDebugLog({ result.node_id,
level: 'info', result.node_state as Partial<NodeExecutionResult>,
);
addDebugLog({
level: 'info',
message: t('workflows.debug.steppedTo', { node: result.node_id }), message: t('workflows.debug.steppedTo', { node: result.node_id }),
nodeId: result.node_id, nodeId: result.node_id,
}); });
} }
if (result.completed) { if (result.completed) {
setDebugState('completed'); setDebugState('completed');
addDebugLog({ level: 'info', message: t('workflows.debug.completed') }); addDebugLog({ level: 'info', message: t('workflows.debug.completed') });
} }
} catch (error) { } catch (error) {
addDebugLog({ level: 'error', message: `${t('workflows.debug.stepError')}: ${error}` }); addDebugLog({
level: 'error',
message: `${t('workflows.debug.stepError')}: ${error}`,
});
} }
}, [workflowId, debugExecutionId, t]); }, [workflowId, debugExecutionId, t]);
// Stop execution // Stop execution
const handleStop = useCallback(async () => { const handleStop = useCallback(async () => {
if (!debugExecutionId) return; if (!debugExecutionId) return;
try { try {
await backendClient.stopWorkflowDebug(workflowId, debugExecutionId); await backendClient.stopWorkflowDebug(workflowId, debugExecutionId);
resetDebugState(); resetDebugState();
addDebugLog({ level: 'info', message: t('workflows.debug.stopped') }); addDebugLog({ level: 'info', message: t('workflows.debug.stopped') });
} catch (error) { } catch (error) {
addDebugLog({ level: 'error', message: `${t('workflows.debug.stopError')}: ${error}` }); addDebugLog({
level: 'error',
message: `${t('workflows.debug.stopError')}: ${error}`,
});
} }
}, [workflowId, debugExecutionId, t, resetDebugState]); }, [workflowId, debugExecutionId, t, resetDebugState]);
@@ -303,7 +360,10 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
const isRunning = debugState === 'running'; const isRunning = debugState === 'running';
const isPaused = debugState === 'paused'; const isPaused = debugState === 'paused';
const canStart = debugState === 'idle' || debugState === 'completed' || debugState === 'error'; const canStart =
debugState === 'idle' ||
debugState === 'completed' ||
debugState === 'error';
return ( return (
<div className="flex flex-col h-full bg-background border-l"> <div className="flex flex-col h-full bg-background border-l">
@@ -313,7 +373,11 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
<Bug className="size-5 text-primary" /> <Bug className="size-5 text-primary" />
<span className="font-semibold">{t('workflows.debug.title')}</span> <span className="font-semibold">{t('workflows.debug.title')}</span>
{debugState !== 'idle' && ( {debugState !== 'idle' && (
<Badge variant={isRunning ? 'default' : isPaused ? 'secondary' : 'outline'}> <Badge
variant={
isRunning ? 'default' : isPaused ? 'secondary' : 'outline'
}
>
{t(`workflows.debug.state.${debugState}`)} {t(`workflows.debug.state.${debugState}`)}
</Badge> </Badge>
)} )}
@@ -335,7 +399,12 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
) : ( ) : (
<> <>
{isRunning ? ( {isRunning ? (
<Button size="sm" variant="secondary" onClick={handlePause} className="gap-1"> <Button
size="sm"
variant="secondary"
onClick={handlePause}
className="gap-1"
>
<Pause className="size-4" /> <Pause className="size-4" />
{t('workflows.debug.pause')} {t('workflows.debug.pause')}
</Button> </Button>
@@ -345,29 +414,54 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
{t('workflows.debug.resume')} {t('workflows.debug.resume')}
</Button> </Button>
)} )}
<Button size="sm" variant="outline" onClick={handleStep} disabled={isRunning} className="gap-1"> <Button
size="sm"
variant="outline"
onClick={handleStep}
disabled={isRunning}
className="gap-1"
>
<StepForward className="size-4" /> <StepForward className="size-4" />
{t('workflows.debug.step')} {t('workflows.debug.step')}
</Button> </Button>
<Button size="sm" variant="destructive" onClick={handleStop} className="gap-1"> <Button
size="sm"
variant="destructive"
onClick={handleStop}
className="gap-1"
>
<Square className="size-4" /> <Square className="size-4" />
{t('workflows.debug.stop')} {t('workflows.debug.stop')}
</Button> </Button>
</> </>
)} )}
<div className="flex-1" /> <div className="flex-1" />
<Button size="sm" variant="ghost" onClick={clearBreakpoints} title={t('workflows.debug.clearBreakpoints')}> <Button
size="sm"
variant="ghost"
onClick={clearBreakpoints}
title={t('workflows.debug.clearBreakpoints')}
>
<Circle className="size-4" /> <Circle className="size-4" />
</Button> </Button>
<Button size="sm" variant="ghost" onClick={clearDebugLogs} title={t('workflows.debug.clearLogs')}> <Button
size="sm"
variant="ghost"
onClick={clearDebugLogs}
title={t('workflows.debug.clearLogs')}
>
<Trash2 className="size-4" /> <Trash2 className="size-4" />
</Button> </Button>
</div> </div>
{/* Main Content */} {/* Main Content */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col min-h-0"> <Tabs
value={activeTab}
onValueChange={setActiveTab}
className="flex-1 flex flex-col min-h-0"
>
<TabsList className="mx-4 mt-2 justify-start"> <TabsList className="mx-4 mt-2 justify-start">
<TabsTrigger value="context" className="gap-1"> <TabsTrigger value="context" className="gap-1">
<Terminal className="size-4" /> <Terminal className="size-4" />
@@ -399,19 +493,23 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
<Label>{t('workflows.debug.messageContent')}</Label> <Label>{t('workflows.debug.messageContent')}</Label>
<Textarea <Textarea
value={debugContext.messageContent} value={debugContext.messageContent}
onChange={(e) => setDebugContext({ messageContent: e.target.value })} onChange={(e) =>
setDebugContext({ messageContent: e.target.value })
}
placeholder={t('workflows.debug.messageContentPlaceholder')} placeholder={t('workflows.debug.messageContentPlaceholder')}
className="min-h-[80px]" className="min-h-[80px]"
disabled={!canStart} disabled={!canStart}
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>{t('workflows.debug.senderId')}</Label> <Label>{t('workflows.debug.senderId')}</Label>
<Input <Input
value={debugContext.senderId} value={debugContext.senderId}
onChange={(e) => setDebugContext({ senderId: e.target.value })} onChange={(e) =>
setDebugContext({ senderId: e.target.value })
}
placeholder={t('workflows.debug.senderIdPlaceholder')} placeholder={t('workflows.debug.senderIdPlaceholder')}
disabled={!canStart} disabled={!canStart}
/> />
@@ -420,19 +518,23 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
<Label>{t('workflows.debug.senderName')}</Label> <Label>{t('workflows.debug.senderName')}</Label>
<Input <Input
value={debugContext.senderName} value={debugContext.senderName}
onChange={(e) => setDebugContext({ senderName: e.target.value })} onChange={(e) =>
setDebugContext({ senderName: e.target.value })
}
placeholder={t('workflows.debug.senderNamePlaceholder')} placeholder={t('workflows.debug.senderNamePlaceholder')}
disabled={!canStart} disabled={!canStart}
/> />
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>{t('workflows.debug.platform')}</Label> <Label>{t('workflows.debug.platform')}</Label>
<Input <Input
value={debugContext.platform} value={debugContext.platform}
onChange={(e) => setDebugContext({ platform: e.target.value })} onChange={(e) =>
setDebugContext({ platform: e.target.value })
}
placeholder={t('workflows.debug.platformPlaceholder')} placeholder={t('workflows.debug.platformPlaceholder')}
disabled={!canStart} disabled={!canStart}
/> />
@@ -441,17 +543,21 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
<Label>{t('workflows.debug.conversationId')}</Label> <Label>{t('workflows.debug.conversationId')}</Label>
<Input <Input
value={debugContext.conversationId} value={debugContext.conversationId}
onChange={(e) => setDebugContext({ conversationId: e.target.value })} onChange={(e) =>
setDebugContext({ conversationId: e.target.value })
}
placeholder={t('workflows.debug.conversationIdPlaceholder')} placeholder={t('workflows.debug.conversationIdPlaceholder')}
disabled={!canStart} disabled={!canStart}
/> />
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Switch <Switch
checked={debugContext.isGroup} checked={debugContext.isGroup}
onCheckedChange={(checked) => setDebugContext({ isGroup: checked })} onCheckedChange={(checked) =>
setDebugContext({ isGroup: checked })
}
disabled={!canStart} disabled={!canStart}
/> />
<Label>{t('workflows.debug.isGroup')}</Label> <Label>{t('workflows.debug.isGroup')}</Label>
@@ -460,42 +566,53 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
{/* Custom Variables */} {/* Custom Variables */}
<Card> <Card>
<CardHeader className="py-3"> <CardHeader className="py-3">
<CardTitle className="text-sm">{t('workflows.debug.customVariables')}</CardTitle> <CardTitle className="text-sm">
{t('workflows.debug.customVariables')}
</CardTitle>
<CardDescription className="text-xs"> <CardDescription className="text-xs">
{t('workflows.debug.customVariablesDesc')} {t('workflows.debug.customVariablesDesc')}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="py-2"> <CardContent className="py-2">
<div className="space-y-2"> <div className="space-y-2">
{Object.entries(debugContext.customVariables).map(([key, value]) => ( {Object.entries(debugContext.customVariables).map(
<div key={key} className="flex items-center gap-2"> ([key, value]) => (
<code className="text-xs bg-muted px-2 py-1 rounded flex-1"> <div key={key} className="flex items-center gap-2">
{key}: {JSON.stringify(value)} <code className="text-xs bg-muted px-2 py-1 rounded flex-1">
</code> {key}: {JSON.stringify(value)}
<Button </code>
size="icon" <Button
variant="ghost" size="icon"
className="size-6" variant="ghost"
onClick={() => handleRemoveVariable(key)} className="size-6"
disabled={!canStart} onClick={() => handleRemoveVariable(key)}
> disabled={!canStart}
<X className="size-3" /> >
</Button> <X className="size-3" />
</div> </Button>
))} </div>
),
)}
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
placeholder={t('workflows.debug.variableKey')} placeholder={t('workflows.debug.variableKey')}
value={newVariable.key} value={newVariable.key}
onChange={(e) => setNewVariable({ ...newVariable, key: e.target.value })} onChange={(e) =>
setNewVariable({ ...newVariable, key: e.target.value })
}
className="text-xs" className="text-xs"
disabled={!canStart} disabled={!canStart}
/> />
<Input <Input
placeholder={t('workflows.debug.variableValue')} placeholder={t('workflows.debug.variableValue')}
value={newVariable.value} value={newVariable.value}
onChange={(e) => setNewVariable({ ...newVariable, value: e.target.value })} onChange={(e) =>
setNewVariable({
...newVariable,
value: e.target.value,
})
}
className="text-xs" className="text-xs"
disabled={!canStart} disabled={!canStart}
/> />
@@ -512,7 +629,12 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
</CardContent> </CardContent>
</Card> </Card>
<Button variant="outline" size="sm" onClick={resetDebugContext} disabled={!canStart}> <Button
variant="outline"
size="sm"
onClick={resetDebugContext}
disabled={!canStart}
>
<RefreshCw className="size-4 mr-2" /> <RefreshCw className="size-4 mr-2" />
{t('workflows.debug.resetContext')} {t('workflows.debug.resetContext')}
</Button> </Button>
@@ -524,7 +646,9 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
<div className="space-y-4"> <div className="space-y-4">
<Card> <Card>
<CardHeader className="py-3"> <CardHeader className="py-3">
<CardTitle className="text-sm">{t('workflows.debug.watchedVariables')}</CardTitle> <CardTitle className="text-sm">
{t('workflows.debug.watchedVariables')}
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="py-2"> <CardContent className="py-2">
{watchedVariables.length === 0 ? ( {watchedVariables.length === 0 ? (
@@ -549,7 +673,10 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
return ( return (
<div key={variable} className="flex items-center gap-2"> <div key={variable} className="flex items-center gap-2">
<code className="text-xs bg-muted px-2 py-1 rounded flex-1 font-mono"> <code className="text-xs bg-muted px-2 py-1 rounded flex-1 font-mono">
{variable} = {value !== undefined ? JSON.stringify(value) : 'undefined'} {variable} ={' '}
{value !== undefined
? JSON.stringify(value)
: 'undefined'}
</code> </code>
<Button <Button
size="icon" size="icon"
@@ -570,7 +697,9 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
{/* Node Outputs */} {/* Node Outputs */}
<Card> <Card>
<CardHeader className="py-3"> <CardHeader className="py-3">
<CardTitle className="text-sm">{t('workflows.debug.nodeOutputs')}</CardTitle> <CardTitle className="text-sm">
{t('workflows.debug.nodeOutputs')}
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="py-2"> <CardContent className="py-2">
{Object.keys(nodeExecutionResults).length === 0 ? ( {Object.keys(nodeExecutionResults).length === 0 ? (
@@ -579,33 +708,41 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
</p> </p>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{Object.entries(nodeExecutionResults).map(([nodeId, result]) => { {Object.entries(nodeExecutionResults).map(
const node = nodes.find((n) => n.id === nodeId); ([nodeId, result]) => {
return ( const node = nodes.find((n) => n.id === nodeId);
<Collapsible key={nodeId}> return (
<CollapsibleTrigger className="flex items-center gap-2 w-full text-left"> <Collapsible key={nodeId}>
<ChevronRight className="size-4 transition-transform data-[state=open]:rotate-90" /> <CollapsibleTrigger className="flex items-center gap-2 w-full text-left">
<span className="text-sm font-medium"> <ChevronRight className="size-4 transition-transform data-[state=open]:rotate-90" />
{node?.data.label || nodeId} <span className="text-sm font-medium">
</span> {node?.data.label || nodeId}
{result.outputs && Object.keys(result.outputs).length > 0 && ( </span>
<Badge variant="secondary" className="text-xs"> {result.outputs &&
{Object.keys(result.outputs).length} outputs Object.keys(result.outputs).length > 0 && (
</Badge> <Badge
)} variant="secondary"
</CollapsibleTrigger> className="text-xs"
<CollapsibleContent className="pl-6 pt-2"> >
{result.outputs ? ( {Object.keys(result.outputs).length} outputs
<pre className="text-xs bg-muted p-2 rounded overflow-auto max-h-40"> </Badge>
{JSON.stringify(result.outputs, null, 2)} )}
</pre> </CollapsibleTrigger>
) : ( <CollapsibleContent className="pl-6 pt-2">
<p className="text-xs text-muted-foreground">No outputs</p> {result.outputs ? (
)} <pre className="text-xs bg-muted p-2 rounded overflow-auto max-h-40">
</CollapsibleContent> {JSON.stringify(result.outputs, null, 2)}
</Collapsible> </pre>
); ) : (
})} <p className="text-xs text-muted-foreground">
No outputs
</p>
)}
</CollapsibleContent>
</Collapsible>
);
},
)}
</div> </div>
)} )}
</CardContent> </CardContent>
@@ -618,8 +755,12 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
<div className="space-y-2"> <div className="space-y-2">
{nodes.map((node) => { {nodes.map((node) => {
const result = nodeExecutionResults[node.id]; const result = nodeExecutionResults[node.id];
const StatusIcon = result ? statusIcons[result.status] || Clock : Clock; const StatusIcon = result
const statusColor = result ? statusColors[result.status] || '' : 'text-gray-400'; ? statusIcons[result.status] || Clock
: Clock;
const statusColor = result
? statusColors[result.status] || ''
: 'text-gray-400';
const isCurrentNode = currentNodeId === node.id; const isCurrentNode = currentNodeId === node.id;
const hasBreakpoint = !!breakpoints[node.id]; const hasBreakpoint = !!breakpoints[node.id];
@@ -641,7 +782,9 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
title={t('workflows.debug.toggleBreakpoint')} title={t('workflows.debug.toggleBreakpoint')}
/> />
<StatusIcon className={`size-4 ${statusColor}`} /> <StatusIcon className={`size-4 ${statusColor}`} />
<span className="text-sm font-medium">{node.data.label}</span> <span className="text-sm font-medium">
{node.data.label}
</span>
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{node.data.type} {node.data.type}
</Badge> </Badge>
@@ -675,7 +818,9 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
onCheckedChange={setAutoScroll} onCheckedChange={setAutoScroll}
className="scale-75" className="scale-75"
/> />
<Label className="text-xs">{t('workflows.debug.autoScroll')}</Label> <Label className="text-xs">
{t('workflows.debug.autoScroll')}
</Label>
</div> </div>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{debugLogs.length} {t('workflows.debug.logEntries')} {debugLogs.length} {t('workflows.debug.logEntries')}
@@ -684,18 +829,24 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
<ScrollArea className="flex-1 p-4"> <ScrollArea className="flex-1 p-4">
<div className="space-y-1 font-mono text-xs"> <div className="space-y-1 font-mono text-xs">
{debugLogs.length === 0 ? ( {debugLogs.length === 0 ? (
<p className="text-muted-foreground">{t('workflows.debug.noLogs')}</p> <p className="text-muted-foreground">
{t('workflows.debug.noLogs')}
</p>
) : ( ) : (
debugLogs.map((log) => ( debugLogs.map((log) => (
<div key={log.id} className="flex gap-2"> <div key={log.id} className="flex gap-2">
<span className="text-muted-foreground shrink-0"> <span className="text-muted-foreground shrink-0">
{new Date(log.timestamp).toLocaleTimeString()} {new Date(log.timestamp).toLocaleTimeString()}
</span> </span>
<span className={`shrink-0 uppercase ${logLevelColors[log.level]}`}> <span
className={`shrink-0 uppercase ${logLevelColors[log.level]}`}
>
[{log.level}] [{log.level}]
</span> </span>
{log.nodeId && ( {log.nodeId && (
<span className="text-purple-400 shrink-0">[{log.nodeId}]</span> <span className="text-purple-400 shrink-0">
[{log.nodeId}]
</span>
)} )}
<span className="text-foreground">{log.message}</span> <span className="text-foreground">{log.message}</span>
{log.data && ( {log.data && (
@@ -46,7 +46,9 @@ interface NodeTypeForUI {
} }
// Default node types generated from shared constants // Default node types generated from shared constants
const defaultNodeTypes: NodeTypeForUI[] = Object.entries(NODE_TYPE_I18N_KEYS).map(([type, keys]) => ({ const defaultNodeTypes: NodeTypeForUI[] = Object.entries(
NODE_TYPE_I18N_KEYS,
).map(([type, keys]) => ({
type, type,
category: type.split('.')[0], category: type.split('.')[0],
labelKey: keys.labelKey, labelKey: keys.labelKey,
@@ -56,45 +58,51 @@ const defaultNodeTypes: NodeTypeForUI[] = Object.entries(NODE_TYPE_I18N_KEYS).ma
export default function NodePalette() { export default function NodePalette() {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const { nodeTypes: backendNodeTypes, nodeCategories } = useWorkflowStore(); const { nodeTypes: backendNodeTypes, nodeCategories } = useWorkflowStore();
// Search state // Search state
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
// Expanded categories state // Expanded categories state
const [expandedCategories, setExpandedCategories] = useState<Set<string>>( const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
new Set(['trigger', 'process', 'control', 'action', 'integration']) new Set(['trigger', 'process', 'control', 'action', 'integration']),
); );
// Helper: get label string for a node using i18n // Helper: get label string for a node using i18n
const getNodeLabel = useCallback((node: NodeTypeForUI): string => { const getNodeLabel = useCallback(
if (node.labelKey) { (node: NodeTypeForUI): string => {
return t(node.labelKey, { defaultValue: node.labelKey }); if (node.labelKey) {
} return t(node.labelKey, { defaultValue: node.labelKey });
if (node.label) { }
const labelValue = resolveI18nLabel(node.label); if (node.label) {
return labelValue || node.type; const labelValue = resolveI18nLabel(node.label);
} return labelValue || node.type;
return node.type; }
}, [t]); return node.type;
},
[t],
);
// Helper: get description string for a node using i18n // Helper: get description string for a node using i18n
const getNodeDescription = useCallback((node: NodeTypeForUI): string => { const getNodeDescription = useCallback(
if (node.descriptionKey) { (node: NodeTypeForUI): string => {
return t(node.descriptionKey, { defaultValue: '' }); if (node.descriptionKey) {
} return t(node.descriptionKey, { defaultValue: '' });
if (node.description) { }
const descValue = resolveI18nLabel(node.description); if (node.description) {
return descValue || ''; const descValue = resolveI18nLabel(node.description);
} return descValue || '';
return ''; }
}, [t]); return '';
},
[t],
);
// Use backend node types if available, otherwise use defaults // Use backend node types if available, otherwise use defaults
const nodeTypes = useMemo((): NodeTypeForUI[] => { const nodeTypes = useMemo((): NodeTypeForUI[] => {
if (backendNodeTypes && backendNodeTypes.length > 0) { if (backendNodeTypes && backendNodeTypes.length > 0) {
return backendNodeTypes.map((node) => { return backendNodeTypes.map((node) => {
const i18nKeys = findNodeI18nKeys(node.type); const i18nKeys = findNodeI18nKeys(node.type);
return { return {
type: node.type, type: node.type,
category: node.category, category: node.category,
@@ -129,13 +137,19 @@ export default function NodePalette() {
// Group filtered nodes by category // Group filtered nodes by category
const groupedNodes = useMemo(() => { const groupedNodes = useMemo(() => {
const groups: Record<string, typeof nodeTypes> = {}; const groups: Record<string, typeof nodeTypes> = {};
const categoryOrder = ['trigger', 'process', 'control', 'action', 'integration']; const categoryOrder = [
'trigger',
'process',
'control',
'action',
'integration',
];
// Initialize groups in order // Initialize groups in order
categoryOrder.forEach((cat) => { categoryOrder.forEach((cat) => {
groups[cat] = []; groups[cat] = [];
}); });
for (const node of filteredNodes) { for (const node of filteredNodes) {
const category = node.category || node.type.split('.')[0]; const category = node.category || node.type.split('.')[0];
if (!groups[category]) { if (!groups[category]) {
@@ -143,14 +157,14 @@ export default function NodePalette() {
} }
groups[category].push(node); groups[category].push(node);
} }
// Remove empty categories // Remove empty categories
Object.keys(groups).forEach((key) => { Object.keys(groups).forEach((key) => {
if (groups[key].length === 0) { if (groups[key].length === 0) {
delete groups[key]; delete groups[key];
} }
}); });
return groups; return groups;
}, [filteredNodes]); }, [filteredNodes]);
@@ -174,7 +188,7 @@ export default function NodePalette() {
event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', nodeType); event.dataTransfer.setData('text/plain', nodeType);
}, },
[] [],
); );
// Get category label using i18n // Get category label using i18n
@@ -189,13 +203,18 @@ export default function NodePalette() {
if (category?.label) { if (category?.label) {
const lang = i18n.language; const lang = i18n.language;
if (lang.startsWith('zh')) { if (lang.startsWith('zh')) {
return category.label['zh-CN'] || category.label['zh-Hans'] || category.label['en'] || categoryName; return (
category.label['zh-CN'] ||
category.label['zh-Hans'] ||
category.label['en'] ||
categoryName
);
} }
return category.label['en'] || category.label['en-US'] || categoryName; return category.label['en'] || category.label['en-US'] || categoryName;
} }
return categoryName; return categoryName;
}, },
[nodeCategories, t, i18n.language] [nodeCategories, t, i18n.language],
); );
// Clear search // Clear search
@@ -204,131 +223,142 @@ export default function NodePalette() {
}, []); }, []);
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
{/* Header */} {/* Header */}
<div className="p-3 border-b"> <div className="p-3 border-b">
<h3 className="font-semibold text-sm mb-3 flex items-center gap-2"> <h3 className="font-semibold text-sm mb-3 flex items-center gap-2">
<Layers className="size-4" /> <Layers className="size-4" />
{t('workflows.nodePalette')} {t('workflows.nodePalette')}
</h3> </h3>
{/* Search input */} {/* Search input */}
<div className="relative"> <div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" /> <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input <Input
type="text" type="text"
placeholder={t('workflows.searchNodes')} placeholder={t('workflows.searchNodes')}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8 pr-8 h-8 text-sm" className="pl-8 pr-8 h-8 text-sm"
/> />
{searchQuery && ( {searchQuery && (
<button
onClick={clearSearch}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
×
</button>
)}
</div>
</div>
{/* Node list */}
<div className="flex-1 min-h-0 overflow-y-auto">
<div className="p-2 space-y-1">
{/* Show loading state if no nodes */}
{Object.keys(groupedNodes).length === 0 && !searchQuery && (
<div className="text-sm text-muted-foreground text-center py-8">
<Cpu className="size-8 mx-auto mb-2 opacity-50" />
<p>{t('workflows.loadingNodeTypes')}</p>
</div>
)}
{/* Show no results message */}
{Object.keys(groupedNodes).length === 0 && searchQuery && (
<div className="text-sm text-muted-foreground text-center py-8">
<Search className="size-8 mx-auto mb-2 opacity-50" />
<p>{t('workflows.noNodesFound')}</p>
<button <button
onClick={clearSearch} onClick={clearSearch}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" className="text-primary hover:underline mt-2"
> >
× {t('workflows.clearSearch')}
</button> </button>
)} </div>
</div> )}
</div>
{/* Category groups */}
{/* Node list */} {Object.entries(groupedNodes).map(([category, nodes]) => {
<div className="flex-1 min-h-0 overflow-y-auto"> const isExpanded = expandedCategories.has(category);
<div className="p-2 space-y-1"> const CategoryIcon = categoryIcons[category] || Settings;
{/* Show loading state if no nodes */} const ChevronIcon = isExpanded ? ChevronDown : ChevronRight;
{Object.keys(groupedNodes).length === 0 && !searchQuery && (
<div className="text-sm text-muted-foreground text-center py-8"> return (
<Cpu className="size-8 mx-auto mb-2 opacity-50" /> <div key={category} className="mb-1">
<p>{t('workflows.loadingNodeTypes')}</p> {/* Category header */}
</div>
)}
{/* Show no results message */}
{Object.keys(groupedNodes).length === 0 && searchQuery && (
<div className="text-sm text-muted-foreground text-center py-8">
<Search className="size-8 mx-auto mb-2 opacity-50" />
<p>{t('workflows.noNodesFound')}</p>
<button <button
onClick={clearSearch} onClick={() => toggleCategory(category)}
className="text-primary hover:underline mt-2" className={cn(
'w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-sm font-medium transition-colors',
categoryBgColors[category],
categoryColors[category],
)}
> >
{t('workflows.clearSearch')} <ChevronIcon className="size-4 flex-shrink-0" />
<CategoryIcon className="size-4 flex-shrink-0" />
<span className="flex-1 text-left">
{getCategoryLabel(category)}
</span>
<Badge variant="secondary" className="text-xs px-1.5 py-0">
{nodes.length}
</Badge>
</button> </button>
</div>
)}
{/* Category groups */}
{Object.entries(groupedNodes).map(([category, nodes]) => {
const isExpanded = expandedCategories.has(category);
const CategoryIcon = categoryIcons[category] || Settings;
const ChevronIcon = isExpanded ? ChevronDown : ChevronRight;
return ( {/* Node list */}
<div key={category} className="mb-1"> {isExpanded && (
{/* Category header */} <div className="mt-1 space-y-0.5 ml-2">
<button {nodes.map((node) => {
onClick={() => toggleCategory(category)} const Icon = nodeIcons[node.type] || Settings;
className={cn( const label = getNodeLabel(node);
'w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-sm font-medium transition-colors', const description = getNodeDescription(node);
categoryBgColors[category],
categoryColors[category]
)}
>
<ChevronIcon className="size-4 flex-shrink-0" />
<CategoryIcon className="size-4 flex-shrink-0" />
<span className="flex-1 text-left">{getCategoryLabel(category)}</span>
<Badge variant="secondary" className="text-xs px-1.5 py-0">
{nodes.length}
</Badge>
</button>
{/* Node list */} return (
{isExpanded && ( <div
<div className="mt-1 space-y-0.5 ml-2"> key={node.type}
{nodes.map((node) => { draggable
const Icon = nodeIcons[node.type] || Settings; onDragStart={(e) => onDragStart(e, node.type)}
const label = getNodeLabel(node); className={cn(
const description = getNodeDescription(node); 'flex items-center gap-2 px-2 py-1.5 rounded-md cursor-grab select-none',
'hover:bg-muted/80 active:cursor-grabbing transition-colors',
return ( 'border border-transparent hover:border-border',
'group',
)}
title={description || label}
>
<div <div
key={node.type}
draggable
onDragStart={(e) => onDragStart(e, node.type)}
className={cn( className={cn(
'flex items-center gap-2 px-2 py-1.5 rounded-md cursor-grab select-none',
'hover:bg-muted/80 active:cursor-grabbing transition-colors',
'border border-transparent hover:border-border',
'group'
)}
title={description || label}
>
<div className={cn(
'p-1 rounded', 'p-1 rounded',
categoryBgColors[category], categoryBgColors[category],
categoryBorderColors[category], categoryBorderColors[category],
'border' 'border',
)}> )}
<Icon className={cn('size-3.5', categoryColors[category])} /> >
</div> <Icon
<span className="text-sm truncate flex-1">{label}</span> className={cn(
<ExternalLink className="size-3 opacity-0 group-hover:opacity-50 transition-opacity" /> 'size-3.5',
categoryColors[category],
)}
/>
</div> </div>
); <span className="text-sm truncate flex-1">
})} {label}
</div> </span>
)} <ExternalLink className="size-3 opacity-0 group-hover:opacity-50 transition-opacity" />
</div> </div>
); );
})} })}
</div> </div>
</div> )}
</div>
{/* Footer hint */} );
<div className="p-2 border-t text-xs text-muted-foreground text-center"> })}
{t('workflows.dragToAdd')}
</div> </div>
</div> </div>
{/* Footer hint */}
<div className="p-2 border-t text-xs text-muted-foreground text-center">
{t('workflows.dragToAdd')}
</div>
</div>
); );
} }
@@ -60,7 +60,9 @@ const translateIfKey = (value: string | undefined): string | undefined => {
}; };
// Delegate to shared utility // Delegate to shared utility
const extractI18nLabel = (obj: Record<string, string> | I18nObject | undefined): string | undefined => { const extractI18nLabel = (
obj: Record<string, string> | I18nObject | undefined,
): string | undefined => {
if (!obj) return undefined; if (!obj) return undefined;
const result = resolveI18nLabel(obj as Record<string, string>); const result = resolveI18nLabel(obj as Record<string, string>);
return result || undefined; return result || undefined;
@@ -112,23 +114,28 @@ const resolvePortDisplayLabel = (
function VariableReference({ function VariableReference({
variable, variable,
onCopy onCopy,
}: { }: {
variable: { name: string; label?: string; type?: string }; variable: { name: string; label?: string; type?: string };
onCopy: (ref: string) => void; onCopy: (ref: string) => void;
}) { }) {
const ref = `{{${variable.name}}}`; const ref = `{{${variable.name}}}`;
const displayName = variable.label || formatVariableName(variable.name); const displayName = variable.label || formatVariableName(variable.name);
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="flex items-center justify-between py-1 px-2 rounded bg-muted/50 text-sm group overflow-hidden w-full cursor-default"> <div className="flex items-center justify-between py-1 px-2 rounded bg-muted/50 text-sm group overflow-hidden w-full cursor-default">
<div className="flex items-center gap-2 min-w-0 flex-1 overflow-hidden"> <div className="flex items-center gap-2 min-w-0 flex-1 overflow-hidden">
<Variable className="size-3.5 text-muted-foreground flex-shrink-0" /> <Variable className="size-3.5 text-muted-foreground flex-shrink-0" />
<span className="truncate font-mono text-xs min-w-0">{displayName}</span> <span className="truncate font-mono text-xs min-w-0">
{displayName}
</span>
{variable.type && ( {variable.type && (
<Badge variant="outline" className="text-[10px] px-1 py-0 flex-shrink-0"> <Badge
variant="outline"
className="text-[10px] px-1 py-0 flex-shrink-0"
>
{getTypeLabel(variable.type)} {getTypeLabel(variable.type)}
</Badge> </Badge>
)} )}
@@ -165,9 +172,13 @@ function CollapsibleSection({
badge?: React.ReactNode; badge?: React.ReactNode;
}) { }) {
const [isOpen, setIsOpen] = useState(defaultOpen); const [isOpen, setIsOpen] = useState(defaultOpen);
return ( return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="w-full overflow-hidden"> <Collapsible
open={isOpen}
onOpenChange={setIsOpen}
className="w-full overflow-hidden"
>
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<button className="w-full flex items-center gap-2 py-2 px-1 hover:bg-muted/50 rounded transition-colors min-w-0 overflow-hidden"> <button className="w-full flex items-center gap-2 py-2 px-1 hover:bg-muted/50 rounded transition-colors min-w-0 overflow-hidden">
{isOpen ? ( {isOpen ? (
@@ -176,14 +187,14 @@ function CollapsibleSection({
<ChevronRight className="size-4 text-muted-foreground flex-shrink-0" /> <ChevronRight className="size-4 text-muted-foreground flex-shrink-0" />
)} )}
<Icon className="size-4 text-muted-foreground flex-shrink-0" /> <Icon className="size-4 text-muted-foreground flex-shrink-0" />
<span className="font-medium text-sm flex-1 text-left truncate min-w-0">{title}</span> <span className="font-medium text-sm flex-1 text-left truncate min-w-0">
{title}
</span>
{badge} {badge}
</button> </button>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="pl-6 pr-1 pb-2 w-full overflow-hidden"> <CollapsibleContent className="pl-6 pr-1 pb-2 w-full overflow-hidden">
<div className="w-full overflow-hidden"> <div className="w-full overflow-hidden">{children}</div>
{children}
</div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
); );
@@ -243,9 +254,12 @@ export default function PropertyPanel({
// form item definitions, i18n labels/descriptions and option labels consistently. // form item definitions, i18n labels/descriptions and option labels consistently.
// Fall back to backend metadata for nodes that do not exist in the local registry. // Fall back to backend metadata for nodes that do not exist in the local registry.
const configSchema = useMemo(() => { const configSchema = useMemo(() => {
const localConfigSchema = (localNodeConfig?.configSchema as IDynamicFormItemSchema[]) || []; const localConfigSchema =
const backendConfigSchema = (nodeTypeMeta?.config_schema as IDynamicFormItemSchema[]) || []; (localNodeConfig?.configSchema as IDynamicFormItemSchema[]) || [];
const rawConfigSchema = localConfigSchema.length > 0 ? localConfigSchema : backendConfigSchema; const backendConfigSchema =
(nodeTypeMeta?.config_schema as IDynamicFormItemSchema[]) || [];
const rawConfigSchema =
localConfigSchema.length > 0 ? localConfigSchema : backendConfigSchema;
return rawConfigSchema.map((item) => { return rawConfigSchema.map((item) => {
const backendItem = backendConfigSchema.find( const backendItem = backendConfigSchema.find(
@@ -255,14 +269,16 @@ export default function PropertyPanel({
return { return {
...(backendItem || {}), ...(backendItem || {}),
...item, ...item,
label: item.label || backendItem?.label || { label: item.label ||
en_US: item.name, backendItem?.label || {
zh_Hans: item.name, en_US: item.name,
}, zh_Hans: item.name,
description: item.description || backendItem?.description || { },
en_US: '', description: item.description ||
zh_Hans: '', backendItem?.description || {
}, en_US: '',
zh_Hans: '',
},
options: item.options || backendItem?.options, options: item.options || backendItem?.options,
show_if: item.show_if || backendItem?.show_if, show_if: item.show_if || backendItem?.show_if,
}; };
@@ -272,13 +288,17 @@ export default function PropertyPanel({
// Get available input variables from connected upstream nodes // Get available input variables from connected upstream nodes
const availableInputVariables = useMemo(() => { const availableInputVariables = useMemo(() => {
if (!selectedNode) return []; if (!selectedNode) return [];
const variables: { nodeId: string; nodeLabel: string; outputs: { name: string; label?: string; type?: string }[] }[] = []; const variables: {
nodeId: string;
nodeLabel: string;
outputs: { name: string; label?: string; type?: string }[];
}[] = [];
// Find all upstream nodes // Find all upstream nodes
const incomingEdges = edges.filter((e) => e.target === selectedNode.id); const incomingEdges = edges.filter((e) => e.target === selectedNode.id);
const upstreamNodeIds = incomingEdges.map((e) => e.source); const upstreamNodeIds = incomingEdges.map((e) => e.source);
for (const nodeId of upstreamNodeIds) { for (const nodeId of upstreamNodeIds) {
const node = nodes.find((n) => n.id === nodeId); const node = nodes.find((n) => n.id === nodeId);
if (node) { if (node) {
@@ -294,20 +314,40 @@ export default function PropertyPanel({
}); });
} }
} }
// Add global variables // Add global variables
variables.push({ variables.push({
nodeId: '__global__', nodeId: '__global__',
nodeLabel: t('workflows.globalVariables'), nodeLabel: t('workflows.globalVariables'),
outputs: [ outputs: [
{ name: 'message.content', label: t('workflows.messageContent'), type: 'string' }, {
{ name: 'message.sender', label: t('workflows.messageSender'), type: 'string' }, name: 'message.content',
{ name: 'context.platform', label: t('workflows.platform'), type: 'string' }, label: t('workflows.messageContent'),
{ name: 'context.session_id', label: t('workflows.sessionId'), type: 'string' }, type: 'string',
{ name: 'context.timestamp', label: t('workflows.timestamp'), type: 'datetime' }, },
{
name: 'message.sender',
label: t('workflows.messageSender'),
type: 'string',
},
{
name: 'context.platform',
label: t('workflows.platform'),
type: 'string',
},
{
name: 'context.session_id',
label: t('workflows.sessionId'),
type: 'string',
},
{
name: 'context.timestamp',
label: t('workflows.timestamp'),
type: 'datetime',
},
], ],
}); });
return variables; return variables;
}, [selectedNode, edges, nodes, t]); }, [selectedNode, edges, nodes, t]);
@@ -318,7 +358,7 @@ export default function PropertyPanel({
updateNodeLabel(selectedNodeId, e.target.value); updateNodeLabel(selectedNodeId, e.target.value);
} }
}, },
[selectedNodeId, updateNodeLabel] [selectedNodeId, updateNodeLabel],
); );
// Handle config change from dynamic form // Handle config change from dynamic form
@@ -330,7 +370,7 @@ export default function PropertyPanel({
} }
return undefined; return undefined;
}, },
[selectedNodeId, updateNodeConfig, pushHistory] [selectedNodeId, updateNodeConfig, pushHistory],
); );
// Handle node delete // Handle node delete
@@ -354,14 +394,17 @@ export default function PropertyPanel({
updateEdgeCondition(selectedEdgeId, e.target.value); updateEdgeCondition(selectedEdgeId, e.target.value);
} }
}, },
[selectedEdgeId, updateEdgeCondition] [selectedEdgeId, updateEdgeCondition],
); );
// Copy variable reference // Copy variable reference
const handleCopyVariable = useCallback((ref: string) => { const handleCopyVariable = useCallback(
navigator.clipboard.writeText(ref); (ref: string) => {
toast.success(t('common.copySuccess')); navigator.clipboard.writeText(ref);
}, [t]); toast.success(t('common.copySuccess'));
},
[t],
);
// No selection // No selection
if (!selectedNodeId && !selectedEdgeId) { if (!selectedNodeId && !selectedEdgeId) {
@@ -408,11 +451,17 @@ export default function PropertyPanel({
{/* Connection info */} {/* Connection info */}
<div className="bg-muted/50 p-3 rounded-lg w-full overflow-hidden"> <div className="bg-muted/50 p-3 rounded-lg w-full overflow-hidden">
<div className="flex items-center gap-2 text-sm min-w-0 w-full overflow-hidden"> <div className="flex items-center gap-2 text-sm min-w-0 w-full overflow-hidden">
<Badge variant="outline" className="font-mono text-xs truncate max-w-[35%] flex-shrink min-w-0"> <Badge
variant="outline"
className="font-mono text-xs truncate max-w-[35%] flex-shrink min-w-0"
>
{sourceNode?.data.label || selectedEdge.source} {sourceNode?.data.label || selectedEdge.source}
</Badge> </Badge>
<ArrowRight className="size-4 text-muted-foreground flex-shrink-0" /> <ArrowRight className="size-4 text-muted-foreground flex-shrink-0" />
<Badge variant="outline" className="font-mono text-xs truncate max-w-[35%] flex-shrink min-w-0"> <Badge
variant="outline"
className="font-mono text-xs truncate max-w-[35%] flex-shrink min-w-0"
>
{targetNode?.data.label || selectedEdge.target} {targetNode?.data.label || selectedEdge.target}
</Badge> </Badge>
</div> </div>
@@ -422,11 +471,16 @@ export default function PropertyPanel({
<CollapsibleSection <CollapsibleSection
title={t('workflows.condition')} title={t('workflows.condition')}
icon={Code} icon={Code}
badge={selectedEdge.data?.condition ? ( badge={
<Badge variant="secondary" className="text-xs flex-shrink-0"> selectedEdge.data?.condition ? (
{t('workflows.hasCondition')} <Badge
</Badge> variant="secondary"
) : null} className="text-xs flex-shrink-0"
>
{t('workflows.hasCondition')}
</Badge>
) : null
}
> >
<div className="space-y-2 w-full overflow-hidden"> <div className="space-y-2 w-full overflow-hidden">
<Textarea <Textarea
@@ -451,19 +505,28 @@ export default function PropertyPanel({
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button variant="destructive" size="sm" className="w-full"> <Button variant="destructive" size="sm" className="w-full">
<Trash2 className="size-4 mr-2 flex-shrink-0" /> <Trash2 className="size-4 mr-2 flex-shrink-0" />
<span className="truncate">{t('workflows.deleteEdge')}</span> <span className="truncate">
{t('workflows.deleteEdge')}
</span>
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>{t('workflows.deleteEdgeConfirm')}</AlertDialogTitle> <AlertDialogTitle>
{t('workflows.deleteEdgeConfirm')}
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{t('workflows.deleteEdgeConfirmDesc')} {t('workflows.deleteEdgeConfirmDesc')}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter className="flex-wrap gap-2"> <AlertDialogFooter className="flex-wrap gap-2">
<AlertDialogCancel className="flex-1 min-w-[80px]">{t('common.cancel')}</AlertDialogCancel> <AlertDialogCancel className="flex-1 min-w-[80px]">
<AlertDialogAction onClick={handleDeleteEdge} className="flex-1 min-w-[80px]"> {t('common.cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteEdge}
className="flex-1 min-w-[80px]"
>
{t('common.delete')} {t('common.delete')}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
@@ -479,9 +542,11 @@ export default function PropertyPanel({
// Node selected // Node selected
if (selectedNode) { if (selectedNode) {
const nodeInputs = selectedNode.data.inputs || nodeTypeMeta?.inputs || [{ name: 'input', type: 'any' }]; const nodeInputs = selectedNode.data.inputs ||
const nodeOutputs = selectedNode.data.outputs || nodeTypeMeta?.outputs || [{ name: 'output', type: 'any' }]; nodeTypeMeta?.inputs || [{ name: 'input', type: 'any' }];
const nodeOutputs = selectedNode.data.outputs ||
nodeTypeMeta?.outputs || [{ name: 'output', type: 'any' }];
// Extract i18n labels using extractI18nLabel // Extract i18n labels using extractI18nLabel
const nodeLabel = nodeTypeMeta?.label const nodeLabel = nodeTypeMeta?.label
? extractI18nLabel(nodeTypeMeta.label) ? extractI18nLabel(nodeTypeMeta.label)
@@ -489,7 +554,7 @@ export default function PropertyPanel({
const nodeDescription = nodeTypeMeta?.description const nodeDescription = nodeTypeMeta?.description
? extractI18nLabel(nodeTypeMeta.description) ? extractI18nLabel(nodeTypeMeta.description)
: undefined; : undefined;
// Get node category color from local config // Get node category color from local config
const nodeColor = localNodeConfig?.color || nodeTypeMeta?.color; const nodeColor = localNodeConfig?.color || nodeTypeMeta?.color;
@@ -514,7 +579,9 @@ export default function PropertyPanel({
</Badge> </Badge>
</div> </div>
{nodeDescription && ( {nodeDescription && (
<p className="text-xs text-muted-foreground">{nodeDescription}</p> <p className="text-xs text-muted-foreground">
{nodeDescription}
</p>
)} )}
</div> </div>
@@ -536,7 +603,7 @@ export default function PropertyPanel({
className="h-8 w-full" className="h-8 w-full"
/> />
</div> </div>
<div className="space-y-1.5 w-full overflow-hidden"> <div className="space-y-1.5 w-full overflow-hidden">
<Label className="text-xs text-muted-foreground"> <Label className="text-xs text-muted-foreground">
{t('workflows.nodeId')} {t('workflows.nodeId')}
@@ -583,10 +650,16 @@ export default function PropertyPanel({
> >
<Variable className="size-3.5 text-blue-600 dark:text-blue-400 flex-shrink-0" /> <Variable className="size-3.5 text-blue-600 dark:text-blue-400 flex-shrink-0" />
<span className="font-mono text-xs truncate min-w-0 flex-1"> <span className="font-mono text-xs truncate min-w-0 flex-1">
{resolvePortDisplayLabel(input, 'workflows.nodeInputs')} {resolvePortDisplayLabel(
input,
'workflows.nodeInputs',
)}
</span> </span>
{input.type && ( {input.type && (
<Badge variant="outline" className="text-[10px] px-1 py-0 flex-shrink-0"> <Badge
variant="outline"
className="text-[10px] px-1 py-0 flex-shrink-0"
>
{getTypeLabel(input.type)} {getTypeLabel(input.type)}
</Badge> </Badge>
)} )}
@@ -607,7 +680,10 @@ export default function PropertyPanel({
key={output.name} key={output.name}
variable={{ variable={{
name: `nodes.${selectedNode.id}.${output.name}`, name: `nodes.${selectedNode.id}.${output.name}`,
label: resolvePortDisplayLabel(output, 'workflows.nodeOutputs'), label: resolvePortDisplayLabel(
output,
'workflows.nodeOutputs',
),
type: output.type, type: output.type,
}} }}
onCopy={handleCopyVariable} onCopy={handleCopyVariable}
@@ -627,7 +703,10 @@ export default function PropertyPanel({
> >
<div className="space-y-3 w-full overflow-hidden"> <div className="space-y-3 w-full overflow-hidden">
{availableInputVariables.map((group) => ( {availableInputVariables.map((group) => (
<div key={group.nodeId} className="space-y-1.5 w-full overflow-hidden"> <div
key={group.nodeId}
className="space-y-1.5 w-full overflow-hidden"
>
<div className="text-xs font-medium text-muted-foreground"> <div className="text-xs font-medium text-muted-foreground">
{group.nodeLabel} {group.nodeLabel}
</div> </div>
@@ -652,7 +731,10 @@ export default function PropertyPanel({
title={t('workflows.nodeConfig')} title={t('workflows.nodeConfig')}
icon={Settings} icon={Settings}
badge={ badge={
<Badge variant="secondary" className="text-xs flex-shrink-0"> <Badge
variant="secondary"
className="text-xs flex-shrink-0"
>
{configSchema.length} {configSchema.length}
</Badge> </Badge>
} }
@@ -660,7 +742,9 @@ export default function PropertyPanel({
<div className="space-y-2 w-full overflow-hidden box-border"> <div className="space-y-2 w-full overflow-hidden box-border">
<DynamicFormComponent <DynamicFormComponent
itemConfigList={configSchema} itemConfigList={configSchema}
initialValues={selectedNode.data.config as Record<string, object>} initialValues={
selectedNode.data.config as Record<string, object>
}
onSubmit={handleConfigChange} onSubmit={handleConfigChange}
/> />
</div> </div>
@@ -683,19 +767,28 @@ export default function PropertyPanel({
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button variant="destructive" size="sm" className="w-full"> <Button variant="destructive" size="sm" className="w-full">
<Trash2 className="size-4 mr-2 flex-shrink-0" /> <Trash2 className="size-4 mr-2 flex-shrink-0" />
<span className="truncate">{t('workflows.deleteNode')}</span> <span className="truncate">
{t('workflows.deleteNode')}
</span>
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>{t('workflows.deleteNodeConfirm')}</AlertDialogTitle> <AlertDialogTitle>
{t('workflows.deleteNodeConfirm')}
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{t('workflows.deleteNodeConfirmDesc')} {t('workflows.deleteNodeConfirmDesc')}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter className="flex-wrap gap-2"> <AlertDialogFooter className="flex-wrap gap-2">
<AlertDialogCancel className="flex-1 min-w-[80px]">{t('common.cancel')}</AlertDialogCancel> <AlertDialogCancel className="flex-1 min-w-[80px]">
<AlertDialogAction onClick={handleDeleteNode} className="flex-1 min-w-[80px]"> {t('common.cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteNode}
className="flex-1 min-w-[80px]"
>
{t('common.delete')} {t('common.delete')}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
@@ -18,7 +18,11 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip'; } from '@/components/ui/tooltip';
import { NODE_ICONS, NODE_TYPE_I18N_KEYS, getNodeTypeLabel } from './workflow-constants'; import {
NODE_ICONS,
NODE_TYPE_I18N_KEYS,
getNodeTypeLabel,
} from './workflow-constants';
import { resolveI18nLabel, maybeTranslateKey } from './workflow-i18n'; import { resolveI18nLabel, maybeTranslateKey } from './workflow-i18n';
import type { I18nObject } from '@/app/infra/entities/common'; import type { I18nObject } from '@/app/infra/entities/common';
@@ -31,14 +35,17 @@ const nodeTypeI18nKeys: Record<string, string> = Object.fromEntries(
); );
// Category colors with improved design // Category colors with improved design
const categoryColors: Record<string, { const categoryColors: Record<
bg: string; string,
border: string; {
text: string; bg: string;
icon: string; border: string;
gradient: string; text: string;
handleBg: string; icon: string;
}> = { gradient: string;
handleBg: string;
}
> = {
trigger: { trigger: {
bg: 'bg-amber-50 dark:bg-amber-950/40', bg: 'bg-amber-50 dark:bg-amber-950/40',
border: 'border-amber-400 dark:border-amber-600', border: 'border-amber-400 dark:border-amber-600',
@@ -82,15 +89,24 @@ const categoryColors: Record<string, {
}; };
// Node execution status // Node execution status
export type NodeExecutionStatus = 'idle' | 'pending' | 'running' | 'completed' | 'failed' | 'skipped'; export type NodeExecutionStatus =
| 'idle'
| 'pending'
| 'running'
| 'completed'
| 'failed'
| 'skipped';
// Status colors and icons // Status colors and icons
const statusConfig: Record<NodeExecutionStatus, { const statusConfig: Record<
icon: React.ElementType; NodeExecutionStatus,
color: string; {
bgColor: string; icon: React.ElementType;
animate?: boolean; color: string;
}> = { bgColor: string;
animate?: boolean;
}
> = {
idle: { idle: {
icon: Play, icon: Play,
color: 'text-muted-foreground', color: 'text-muted-foreground',
@@ -128,8 +144,16 @@ export interface WorkflowNodeData extends Record<string, unknown> {
label: string; label: string;
type: string; type: string;
config: Record<string, unknown>; config: Record<string, unknown>;
inputs?: { name: string; label?: string | Record<string, string> | I18nObject; type?: string }[]; inputs?: {
outputs?: { name: string; label?: string | Record<string, string> | I18nObject; type?: string }[]; name: string;
label?: string | Record<string, string> | I18nObject;
type?: string;
}[];
outputs?: {
name: string;
label?: string | Record<string, string> | I18nObject;
type?: string;
}[];
// Execution state // Execution state
executionStatus?: NodeExecutionStatus; executionStatus?: NodeExecutionStatus;
executionError?: string; executionError?: string;
@@ -152,7 +176,10 @@ function getPortLabel(
} }
if (typeof label === 'string' && label) { if (typeof label === 'string' && label) {
if (label.startsWith('workflows.nodeOutputs.') || label.startsWith('workflows.nodeInputs.')) { if (
label.startsWith('workflows.nodeOutputs.') ||
label.startsWith('workflows.nodeInputs.')
) {
return t(label, { defaultValue: fallbackName }); return t(label, { defaultValue: fallbackName });
} }
return label; return label;
@@ -164,17 +191,22 @@ function getPortLabel(
} }
// Helper function to extract i18n value from I18nObject (delegates to shared utility) // Helper function to extract i18n value from I18nObject (delegates to shared utility)
function extractI18nValue(i18nObj: Record<string, string> | undefined, _t: (key: string) => string): string { function extractI18nValue(
i18nObj: Record<string, string> | undefined,
_t: (key: string) => string,
): string {
return resolveI18nLabel(i18nObj); return resolveI18nLabel(i18nObj);
} }
// Helper function to get node type description: show the raw type name after the dot // Helper function to get node type description: show the raw type name after the dot
function getNodeTypeDescription( function getNodeTypeDescription(
nodeType: string, nodeType: string,
t: (key: string, options?: { defaultValue: string }) => string, t: (key: string, options?: { defaultValue: string }) => string,
nodeTypeLabel?: Record<string, string> nodeTypeLabel?: Record<string, string>,
): string { ): string {
return nodeType.includes('.') ? nodeType.split('.').slice(1).join('.') : nodeType; return nodeType.includes('.')
? nodeType.split('.').slice(1).join('.')
: nodeType;
} }
function WorkflowNodeComponent({ data, selected }: NodeProps) { function WorkflowNodeComponent({ data, selected }: NodeProps) {
@@ -191,11 +223,19 @@ function WorkflowNodeComponent({ data, selected }: NodeProps) {
// Get inputs and outputs with defaults (use i18n keys for default labels) // Get inputs and outputs with defaults (use i18n keys for default labels)
const inputs = useMemo(() => { const inputs = useMemo(() => {
return nodeData.inputs || [{ name: 'input', label: 'workflows.nodeInputs.input', type: 'any' }]; return (
nodeData.inputs || [
{ name: 'input', label: 'workflows.nodeInputs.input', type: 'any' },
]
);
}, [nodeData.inputs]); }, [nodeData.inputs]);
const outputs = useMemo(() => { const outputs = useMemo(() => {
return nodeData.outputs || [{ name: 'output', label: 'workflows.nodeOutputs.output', type: 'any' }]; return (
nodeData.outputs || [
{ name: 'output', label: 'workflows.nodeOutputs.output', type: 'any' },
]
);
}, [nodeData.outputs]); }, [nodeData.outputs]);
// Determine if this is a trigger node (no inputs) // Determine if this is a trigger node (no inputs)
@@ -217,73 +257,95 @@ function WorkflowNodeComponent({ data, selected }: NodeProps) {
'min-w-[200px] max-w-[280px] rounded-xl border-2 shadow-lg transition-all duration-200', 'min-w-[200px] max-w-[280px] rounded-xl border-2 shadow-lg transition-all duration-200',
colors.bg, colors.bg,
colors.border, colors.border,
selected && 'ring-2 ring-primary ring-offset-2 ring-offset-background shadow-xl scale-[1.02]', selected &&
'ring-2 ring-primary ring-offset-2 ring-offset-background shadow-xl scale-[1.02]',
status === 'running' && 'shadow-blue-200 dark:shadow-blue-900/50', status === 'running' && 'shadow-blue-200 dark:shadow-blue-900/50',
status === 'failed' && 'shadow-red-200 dark:shadow-red-900/50 border-red-500', status === 'failed' &&
'shadow-red-200 dark:shadow-red-900/50 border-red-500',
)} )}
> >
{/* Input handles - only show if not trigger */} {/* Input handles - only show if not trigger */}
{!isTrigger && inputs.map((input, index) => ( {!isTrigger &&
<Tooltip key={`input-${input.name}`}> inputs.map((input, index) => (
<TooltipTrigger asChild> <Tooltip key={`input-${input.name}`}>
<Handle <TooltipTrigger asChild>
type="target" <Handle
position={Position.Left} type="target"
id={input.name} position={Position.Left}
style={{ id={input.name}
top: inputs.length === 1 ? '50%' : `${((index + 1) / (inputs.length + 1)) * 100}%`, style={{
background: colors.handleBg, top:
width: 12, inputs.length === 1
height: 12, ? '50%'
border: '2px solid white', : `${((index + 1) / (inputs.length + 1)) * 100}%`,
boxShadow: '0 2px 4px rgba(0,0,0,0.1)', background: colors.handleBg,
}} width: 12,
className="!transition-transform hover:!scale-125" height: 12,
/> border: '2px solid white',
</TooltipTrigger> boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
<TooltipContent side="left"> }}
<p className="font-medium">{getPortLabel(input.label, input.name, 'workflows.nodeInputs', t)}</p> className="!transition-transform hover:!scale-125"
{input.type && <p className="text-xs text-muted-foreground">{input.type}</p>} />
</TooltipContent> </TooltipTrigger>
</Tooltip> <TooltipContent side="left">
))} <p className="font-medium">
{getPortLabel(
input.label,
input.name,
'workflows.nodeInputs',
t,
)}
</p>
{input.type && (
<p className="text-xs text-muted-foreground">{input.type}</p>
)}
</TooltipContent>
</Tooltip>
))}
{/* Node content */} {/* Node content */}
<div className={cn('p-3 bg-gradient-to-b', colors.gradient)}> <div className={cn('p-3 bg-gradient-to-b', colors.gradient)}>
{/* Header row with icon and status */} {/* Header row with icon and status */}
<div className="flex items-start gap-2.5"> <div className="flex items-start gap-2.5">
{/* Node icon */} {/* Node icon */}
<div className={cn( <div
'p-2 rounded-lg shrink-0', className={cn(
colors.icon, 'p-2 rounded-lg shrink-0',
'bg-white/60 dark:bg-black/20', colors.icon,
'shadow-sm' 'bg-white/60 dark:bg-black/20',
)}> 'shadow-sm',
)}
>
<Icon className="size-5" /> <Icon className="size-5" />
</div> </div>
{/* Title and type */} {/* Title and type */}
<div className="flex-1 min-w-0 pt-0.5"> <div className="flex-1 min-w-0 pt-0.5">
<div className={cn('font-semibold text-sm truncate', colors.text)}> <div
className={cn('font-semibold text-sm truncate', colors.text)}
>
{nodeData.label} {nodeData.label}
</div> </div>
<div className="text-xs text-muted-foreground truncate mt-0.5"> <div className="text-xs text-muted-foreground truncate mt-0.5">
{getNodeTypeDescription(nodeData.type, t, nodeData.nodeTypeLabel)} {getNodeTypeDescription(
nodeData.type,
t,
nodeData.nodeTypeLabel,
)}
</div> </div>
</div> </div>
{/* Status indicator */} {/* Status indicator */}
{status !== 'idle' && ( {status !== 'idle' && (
<div className={cn( <div
'p-1 rounded-full shrink-0', className={cn('p-1 rounded-full shrink-0', statusInfo.bgColor)}
statusInfo.bgColor >
)}> <StatusIcon
<StatusIcon
className={cn( className={cn(
'size-4', 'size-4',
statusInfo.color, statusInfo.color,
statusInfo.animate && 'animate-spin' statusInfo.animate && 'animate-spin',
)} )}
/> />
</div> </div>
)} )}
@@ -291,16 +353,20 @@ function WorkflowNodeComponent({ data, selected }: NodeProps) {
{/* Execution info */} {/* Execution info */}
{(status === 'completed' || status === 'failed') && ( {(status === 'completed' || status === 'failed') && (
<div className={cn( <div
'mt-2 pt-2 border-t flex items-center justify-between text-xs', className={cn(
'border-black/5 dark:border-white/5' 'mt-2 pt-2 border-t flex items-center justify-between text-xs',
)}> 'border-black/5 dark:border-white/5',
)}
>
<div className={cn('flex items-center gap-1', statusInfo.color)}> <div className={cn('flex items-center gap-1', statusInfo.color)}>
<StatusIcon className="size-3" /> <StatusIcon className="size-3" />
<span className="capitalize">{status}</span> <span className="capitalize">{status}</span>
</div> </div>
{formattedDuration && ( {formattedDuration && (
<span className="text-muted-foreground">{formattedDuration}</span> <span className="text-muted-foreground">
{formattedDuration}
</span>
)} )}
</div> </div>
)} )}
@@ -329,7 +395,10 @@ function WorkflowNodeComponent({ data, selected }: NodeProps) {
position={Position.Right} position={Position.Right}
id={output.name} id={output.name}
style={{ style={{
top: outputs.length === 1 ? '50%' : `${((index + 1) / (outputs.length + 1)) * 100}%`, top:
outputs.length === 1
? '50%'
: `${((index + 1) / (outputs.length + 1)) * 100}%`,
background: colors.handleBg, background: colors.handleBg,
width: 12, width: 12,
height: 12, height: 12,
@@ -340,12 +409,20 @@ function WorkflowNodeComponent({ data, selected }: NodeProps) {
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right"> <TooltipContent side="right">
<p className="font-medium">{getPortLabel(output.label, output.name, 'workflows.nodeOutputs', t)}</p> <p className="font-medium">
{output.type && <p className="text-xs text-muted-foreground">{output.type}</p>} {getPortLabel(
output.label,
output.name,
'workflows.nodeOutputs',
t,
)}
</p>
{output.type && (
<p className="text-xs text-muted-foreground">{output.type}</p>
)}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
))} ))}
</div> </div>
</TooltipProvider> </TooltipProvider>
); );
@@ -1,5 +1,8 @@
export { default as WorkflowEditorComponent } from './WorkflowEditorComponent'; export { default as WorkflowEditorComponent } from './WorkflowEditorComponent';
export { default as WorkflowNodeComponent, type WorkflowNodeData } from './WorkflowNodeComponent'; export {
default as WorkflowNodeComponent,
type WorkflowNodeData,
} from './WorkflowNodeComponent';
export { default as NodePalette } from './NodePalette'; export { default as NodePalette } from './NodePalette';
export { default as PropertyPanel } from './PropertyPanel'; export { default as PropertyPanel } from './PropertyPanel';
@@ -1,6 +1,6 @@
/** /**
* AI Node Configurations * AI Node Configurations
* *
* Defines configurations for all AI-related node types: * Defines configurations for all AI-related node types:
* - llm_call: Call a large language model * - llm_call: Call a large language model
* - question_classifier: Classify user questions into categories * - question_classifier: Classify user questions into categories
@@ -80,8 +80,10 @@ export const llmCallConfig: NodeConfigMeta = {
zh_Hans: '系统提示词', zh_Hans: '系统提示词',
}, },
description: { description: {
en_US: 'System prompt to set the model behavior (supports variable interpolation with {{variable}})', en_US:
zh_Hans: '设置模型行为的系统提示词(支持使用 {{variable}} 进行变量插值)', 'System prompt to set the model behavior (supports variable interpolation with {{variable}})',
zh_Hans:
'设置模型行为的系统提示词(支持使用 {{variable}} 进行变量插值)',
}, },
required: false, required: false,
default: '', default: '',
@@ -95,8 +97,10 @@ export const llmCallConfig: NodeConfigMeta = {
zh_Hans: '用户提示词模板', zh_Hans: '用户提示词模板',
}, },
description: { description: {
en_US: 'User prompt template with variable placeholders (e.g., {{input}}, {{context.key}})', en_US:
zh_Hans: '带有变量占位符的用户提示词模板(例如 {{input}}{{context.key}}', 'User prompt template with variable placeholders (e.g., {{input}}, {{context.key}})',
zh_Hans:
'带有变量占位符的用户提示词模板(例如 {{input}}、{{context.key}}',
}, },
required: true, required: true,
default: '{{input}}', default: '{{input}}',
@@ -110,7 +114,8 @@ export const llmCallConfig: NodeConfigMeta = {
zh_Hans: '温度', zh_Hans: '温度',
}, },
description: { description: {
en_US: 'Controls randomness in responses (0.0 = deterministic, 2.0 = very random)', en_US:
'Controls randomness in responses (0.0 = deterministic, 2.0 = very random)',
zh_Hans: '控制响应的随机性(0.0 = 确定性,2.0 = 非常随机)', zh_Hans: '控制响应的随机性(0.0 = 确定性,2.0 = 非常随机)',
}, },
required: false, required: false,
@@ -125,7 +130,8 @@ export const llmCallConfig: NodeConfigMeta = {
zh_Hans: '最大令牌数', zh_Hans: '最大令牌数',
}, },
description: { description: {
en_US: 'Maximum number of tokens to generate (leave 0 for model default)', en_US:
'Maximum number of tokens to generate (leave 0 for model default)',
zh_Hans: '生成的最大令牌数(设为 0 使用模型默认值)', zh_Hans: '生成的最大令牌数(设为 0 使用模型默认值)',
}, },
required: false, required: false,
@@ -148,7 +154,10 @@ export const llmCallConfig: NodeConfigMeta = {
options: [ options: [
{ name: 'text', label: { en_US: 'Plain Text', zh_Hans: '纯文本' } }, { name: 'text', label: { en_US: 'Plain Text', zh_Hans: '纯文本' } },
{ name: 'json', label: { en_US: 'JSON', zh_Hans: 'JSON' } }, { name: 'json', label: { en_US: 'JSON', zh_Hans: 'JSON' } },
{ name: 'markdown', label: { en_US: 'Markdown', zh_Hans: 'Markdown 文本' } }, {
name: 'markdown',
label: { en_US: 'Markdown', zh_Hans: 'Markdown 文本' },
},
], ],
}, },
{ {
@@ -282,8 +291,10 @@ export const questionClassifierConfig: NodeConfigMeta = {
zh_Hans: '分类定义', zh_Hans: '分类定义',
}, },
description: { description: {
en_US: 'Define categories in JSON format: [{"name": "category1", "description": "...", "examples": ["..."]}]', en_US:
zh_Hans: '使用 JSON 格式定义分类: [{"name": "分类1", "description": "...", "examples": ["..."]}]', 'Define categories in JSON format: [{"name": "category1", "description": "...", "examples": ["..."]}]',
zh_Hans:
'使用 JSON 格式定义分类: [{"name": "分类1", "description": "...", "examples": ["..."]}]',
}, },
required: true, required: true,
default: '[]', default: '[]',
@@ -389,8 +400,10 @@ export const parameterExtractorConfig: NodeConfigMeta = {
zh_Hans: '参数架构', zh_Hans: '参数架构',
}, },
description: { description: {
en_US: 'JSON array defining expected parameters: [{"name": "date", "type": "string", "description": "Meeting date", "required": true}]', en_US:
zh_Hans: '定义期望参数的 JSON 数组: [{"name": "日期", "type": "string", "description": "会议日期", "required": true}]', 'JSON array defining expected parameters: [{"name": "date", "type": "string", "description": "Meeting date", "required": true}]',
zh_Hans:
'定义期望参数的 JSON 数组: [{"name": "日期", "type": "string", "description": "会议日期", "required": true}]',
}, },
required: true, required: true,
default: '[]', default: '[]',
@@ -445,7 +458,8 @@ export const knowledgeRetrievalConfig: NodeConfigMeta = {
zh_Hans: '知识检索', zh_Hans: '知识检索',
}, },
description: { description: {
en_US: 'Retrieve relevant information from knowledge bases using semantic search', en_US:
'Retrieve relevant information from knowledge bases using semantic search',
zh_Hans: '使用语义搜索从知识库中检索相关信息', zh_Hans: '使用语义搜索从知识库中检索相关信息',
}, },
icon: 'BookOpen', icon: 'BookOpen',
@@ -532,9 +546,18 @@ export const knowledgeRetrievalConfig: NodeConfigMeta = {
required: false, required: false,
default: 'vector', default: 'vector',
options: [ options: [
{ name: 'vector', label: { en_US: 'Vector Search', zh_Hans: '向量检索' } }, {
{ name: 'hybrid', label: { en_US: 'Hybrid Search', zh_Hans: '混合检索' } }, name: 'vector',
{ name: 'keyword', label: { en_US: 'Keyword Search', zh_Hans: '关键词检索' } }, label: { en_US: 'Vector Search', zh_Hans: '向量检索' },
},
{
name: 'hybrid',
label: { en_US: 'Hybrid Search', zh_Hans: '混合检索' },
},
{
name: 'keyword',
label: { en_US: 'Keyword Search', zh_Hans: '关键词检索' },
},
], ],
}, },
{ {
@@ -700,8 +723,10 @@ export const intentRecognitionConfig: NodeConfigMeta = {
zh_Hans: '意图定义', zh_Hans: '意图定义',
}, },
description: { description: {
en_US: 'Define intents in JSON format: [{"name": "intent1", "description": "...", "examples": ["..."]}]', en_US:
zh_Hans: '使用 JSON 格式定义意图: [{"name": "意图1", "description": "...", "examples": ["..."]}]', 'Define intents in JSON format: [{"name": "intent1", "description": "...", "examples": ["..."]}]',
zh_Hans:
'使用 JSON 格式定义意图: [{"name": "意图1", "description": "...", "examples": ["..."]}]',
}, },
required: true, required: true,
default: '[]', default: '[]',
@@ -1,6 +1,6 @@
/** /**
* Control Node Configurations * Control Node Configurations
* *
* Defines configurations for flow control node types: * Defines configurations for flow control node types:
* - condition: Conditional branching * - condition: Conditional branching
* - switch_case: Multi-way branching * - switch_case: Multi-way branching
@@ -62,10 +62,16 @@ export const conditionConfig: NodeConfigMeta = {
required: true, required: true,
default: 'expression', default: 'expression',
options: [ options: [
{ name: 'expression', label: { en_US: 'Expression', zh_Hans: '表达式' } }, {
name: 'expression',
label: { en_US: 'Expression', zh_Hans: '表达式' },
},
{ name: 'comparison', label: { en_US: 'Comparison', zh_Hans: '比较' } }, { name: 'comparison', label: { en_US: 'Comparison', zh_Hans: '比较' } },
{ name: 'exists', label: { en_US: 'Value Exists', zh_Hans: '值存在' } }, { name: 'exists', label: { en_US: 'Value Exists', zh_Hans: '值存在' } },
{ name: 'type_check', label: { en_US: 'Type Check', zh_Hans: '类型检查' } }, {
name: 'type_check',
label: { en_US: 'Type Check', zh_Hans: '类型检查' },
},
], ],
}, },
{ {
@@ -124,15 +130,36 @@ export const conditionConfig: NodeConfigMeta = {
default: 'eq', default: 'eq',
options: [ options: [
{ name: 'eq', label: { en_US: 'Equals (==)', zh_Hans: '等于 (==)' } }, { name: 'eq', label: { en_US: 'Equals (==)', zh_Hans: '等于 (==)' } },
{ name: 'neq', label: { en_US: 'Not Equals (!=)', zh_Hans: '不等于 (!=)' } }, {
{ name: 'gt', label: { en_US: 'Greater Than (>)', zh_Hans: '大于 (>)' } }, name: 'neq',
{ name: 'gte', label: { en_US: 'Greater or Equal (>=)', zh_Hans: '大于等于 (>=)' } }, label: { en_US: 'Not Equals (!=)', zh_Hans: '等于 (!=)' },
},
{
name: 'gt',
label: { en_US: 'Greater Than (>)', zh_Hans: '大于 (>)' },
},
{
name: 'gte',
label: { en_US: 'Greater or Equal (>=)', zh_Hans: '大于等于 (>=)' },
},
{ name: 'lt', label: { en_US: 'Less Than (<)', zh_Hans: '小于 (<)' } }, { name: 'lt', label: { en_US: 'Less Than (<)', zh_Hans: '小于 (<)' } },
{ name: 'lte', label: { en_US: 'Less or Equal (<=)', zh_Hans: '小于等于 (<=)' } }, {
name: 'lte',
label: { en_US: 'Less or Equal (<=)', zh_Hans: '小于等于 (<=)' },
},
{ name: 'contains', label: { en_US: 'Contains', zh_Hans: '包含' } }, { name: 'contains', label: { en_US: 'Contains', zh_Hans: '包含' } },
{ name: 'starts_with', label: { en_US: 'Starts With', zh_Hans: '以...开头' } }, {
{ name: 'ends_with', label: { en_US: 'Ends With', zh_Hans: '以...结尾' } }, name: 'starts_with',
{ name: 'matches', label: { en_US: 'Matches Regex', zh_Hans: '匹配正则' } }, label: { en_US: 'Starts With', zh_Hans: '以...开头' },
},
{
name: 'ends_with',
label: { en_US: 'Ends With', zh_Hans: '以...结尾' },
},
{
name: 'matches',
label: { en_US: 'Matches Regex', zh_Hans: '匹配正则' },
},
], ],
show_if: { show_if: {
field: 'condition_type', field: 'condition_type',
@@ -261,11 +288,14 @@ export const switchCaseConfig: NodeConfigMeta = {
zh_Hans: '情况', zh_Hans: '情况',
}, },
description: { description: {
en_US: 'Define cases as JSON array: [{"name": "case_1", "value": "value1"}, {"name": "case_2", "values": ["v1", "v2"]}]', en_US:
zh_Hans: '使用 JSON 数组定义情况: [{"name": "case_1", "value": "value1"}, {"name": "case_2", "values": ["v1", "v2"]}]', 'Define cases as JSON array: [{"name": "case_1", "value": "value1"}, {"name": "case_2", "values": ["v1", "v2"]}]',
zh_Hans:
'使用 JSON 数组定义情况: [{"name": "case_1", "value": "value1"}, {"name": "case_2", "values": ["v1", "v2"]}]',
}, },
required: true, required: true,
default: '[{"name": "case_1", "value": ""}, {"name": "case_2", "value": ""}]', default:
'[{"name": "case_1", "value": ""}, {"name": "case_2", "value": ""}]',
}, },
{ {
id: 'case_sensitive', id: 'case_sensitive',
@@ -502,8 +532,10 @@ export const parallelConfig: NodeConfigMeta = {
zh_Hans: '分支', zh_Hans: '分支',
}, },
description: { description: {
en_US: 'Define branches as JSON array: [{"name": "branch_1"}, {"name": "branch_2"}]', en_US:
zh_Hans: '使用 JSON 数组定义分支: [{"name": "branch_1"}, {"name": "branch_2"}]', 'Define branches as JSON array: [{"name": "branch_1"}, {"name": "branch_2"}]',
zh_Hans:
'使用 JSON 数组定义分支: [{"name": "branch_1"}, {"name": "branch_2"}]',
}, },
required: true, required: true,
default: '[{"name": "branch_1"}, {"name": "branch_2"}]', default: '[{"name": "branch_1"}, {"name": "branch_2"}]',
@@ -763,7 +795,10 @@ export const iteratorConfig: NodeConfigMeta = {
name: 'parallel', name: 'parallel',
type: DynamicFormItemType.BOOLEAN, type: DynamicFormItemType.BOOLEAN,
label: { en_US: 'Parallel Processing', zh_Hans: '并行处理' }, label: { en_US: 'Parallel Processing', zh_Hans: '并行处理' },
description: { en_US: 'Process items in parallel', zh_Hans: '并行处理项目' }, description: {
en_US: 'Process items in parallel',
zh_Hans: '并行处理项目',
},
required: false, required: false,
default: false, default: false,
}, },
@@ -772,7 +807,10 @@ export const iteratorConfig: NodeConfigMeta = {
name: 'max_concurrency', name: 'max_concurrency',
type: DynamicFormItemType.INT, type: DynamicFormItemType.INT,
label: { en_US: 'Max Concurrency', zh_Hans: '最大并发数' }, label: { en_US: 'Max Concurrency', zh_Hans: '最大并发数' },
description: { en_US: 'Maximum number of concurrent iterations', zh_Hans: '最大并发迭代数' }, description: {
en_US: 'Maximum number of concurrent iterations',
zh_Hans: '最大并发迭代数',
},
required: false, required: false,
default: 5, default: 5,
show_if: { field: 'parallel', operator: 'eq', value: true }, show_if: { field: 'parallel', operator: 'eq', value: true },
@@ -782,7 +820,10 @@ export const iteratorConfig: NodeConfigMeta = {
name: 'max_iterations', name: 'max_iterations',
type: DynamicFormItemType.INT, type: DynamicFormItemType.INT,
label: { en_US: 'Max Iterations', zh_Hans: '最大迭代次数' }, label: { en_US: 'Max Iterations', zh_Hans: '最大迭代次数' },
description: { en_US: 'Safety limit on iterations', zh_Hans: '迭代次数安全限制' }, description: {
en_US: 'Safety limit on iterations',
zh_Hans: '迭代次数安全限制',
},
required: false, required: false,
default: 1000, default: 1000,
}, },
@@ -831,14 +872,29 @@ export const mergeConfig: NodeConfigMeta = {
name: 'merge_strategy', name: 'merge_strategy',
type: DynamicFormItemType.SELECT, type: DynamicFormItemType.SELECT,
label: { en_US: 'Merge Strategy', zh_Hans: '合并策略' }, label: { en_US: 'Merge Strategy', zh_Hans: '合并策略' },
description: { en_US: 'How to merge inputs from branches', zh_Hans: '如何合并分支输入' }, description: {
en_US: 'How to merge inputs from branches',
zh_Hans: '如何合并分支输入',
},
required: true, required: true,
default: 'wait_all', default: 'wait_all',
options: [ options: [
{ name: 'wait_all', label: { en_US: 'Wait for All', zh_Hans: '等待全部' } }, {
{ name: 'first_completed', label: { en_US: 'First Completed', zh_Hans: '第一个完成' } }, name: 'wait_all',
{ name: 'combine', label: { en_US: 'Combine to Object', zh_Hans: '合并为对象' } }, label: { en_US: 'Wait for All', zh_Hans: '等待全部' },
{ name: 'array', label: { en_US: 'Collect to Array', zh_Hans: '收集为数组' } }, },
{
name: 'first_completed',
label: { en_US: 'First Completed', zh_Hans: '第一个完成' },
},
{
name: 'combine',
label: { en_US: 'Combine to Object', zh_Hans: '合并为对象' },
},
{
name: 'array',
label: { en_US: 'Collect to Array', zh_Hans: '收集为数组' },
},
], ],
}, },
], ],
@@ -881,7 +937,11 @@ export const variableAggregatorConfig: NodeConfigMeta = {
name: 'variable_mappings', name: 'variable_mappings',
type: DynamicFormItemType.TEXT, type: DynamicFormItemType.TEXT,
label: { en_US: 'Variable Mappings', zh_Hans: '变量映射' }, label: { en_US: 'Variable Mappings', zh_Hans: '变量映射' },
description: { en_US: 'JSON mapping of output variables: {"out_key": "{{nodes.xxx.value}}"}', zh_Hans: 'JSON 格式的输出变量映射: {"out_key": "{{nodes.xxx.value}}"}' }, description: {
en_US:
'JSON mapping of output variables: {"out_key": "{{nodes.xxx.value}}"}',
zh_Hans: 'JSON 格式的输出变量映射: {"out_key": "{{nodes.xxx.value}}"}',
},
required: true, required: true,
default: '{}', default: '{}',
}, },
@@ -890,13 +950,25 @@ export const variableAggregatorConfig: NodeConfigMeta = {
name: 'aggregation_mode', name: 'aggregation_mode',
type: DynamicFormItemType.SELECT, type: DynamicFormItemType.SELECT,
label: { en_US: 'Aggregation Mode', zh_Hans: '聚合模式' }, label: { en_US: 'Aggregation Mode', zh_Hans: '聚合模式' },
description: { en_US: 'How to aggregate the variables', zh_Hans: '如何聚合变量' }, description: {
en_US: 'How to aggregate the variables',
zh_Hans: '如何聚合变量',
},
required: true, required: true,
default: 'merge', default: 'merge',
options: [ options: [
{ name: 'merge', label: { en_US: 'Merge Objects', zh_Hans: '合并对象' } }, {
{ name: 'array', label: { en_US: 'Collect to Array', zh_Hans: '收集为数组' } }, name: 'merge',
{ name: 'first', label: { en_US: 'First Non-null', zh_Hans: '第一个非空' } }, label: { en_US: 'Merge Objects', zh_Hans: '合并对象' },
},
{
name: 'array',
label: { en_US: 'Collect to Array', zh_Hans: '收集为数组' },
},
{
name: 'first',
label: { en_US: 'First Non-null', zh_Hans: '第一个非空' },
},
], ],
}, },
], ],
@@ -1,6 +1,6 @@
/** /**
* Node Configurations Index * Node Configurations Index
* *
* This module exports all node configuration metadata and provides * This module exports all node configuration metadata and provides
* utility functions for accessing node configurations. * utility functions for accessing node configurations.
*/ */
@@ -9,7 +9,7 @@
export * from './types'; export * from './types';
// Trigger Nodes // Trigger Nodes
export { export {
triggerConfigs, triggerConfigs,
getTriggerConfig, getTriggerConfig,
messageTriggerConfig, messageTriggerConfig,
@@ -147,7 +147,9 @@ export function getNodeConfig(nodeType: string): NodeConfigMeta | undefined {
/** /**
* Get all node configurations for a category * Get all node configurations for a category
*/ */
export function getNodeConfigsByCategory(category: NodeCategory): NodeConfigMeta[] { export function getNodeConfigsByCategory(
category: NodeCategory,
): NodeConfigMeta[] {
return allNodeConfigs.filter((config) => config.category === category); return allNodeConfigs.filter((config) => config.category === category);
} }
@@ -171,18 +173,18 @@ export function isValidNodeType(nodeType: string): boolean {
export function getDefaultConfig(nodeType: string): Record<string, unknown> { export function getDefaultConfig(nodeType: string): Record<string, unknown> {
const config = getNodeConfig(nodeType); const config = getNodeConfig(nodeType);
if (!config) return {}; if (!config) return {};
// Build default config from schema defaults // Build default config from schema defaults
const defaults: Record<string, unknown> = {}; const defaults: Record<string, unknown> = {};
for (const field of config.configSchema) { for (const field of config.configSchema) {
defaults[field.name] = field.default; defaults[field.name] = field.default;
} }
// Override with explicit defaultConfig if provided // Override with explicit defaultConfig if provided
if (config.defaultConfig) { if (config.defaultConfig) {
Object.assign(defaults, config.defaultConfig); Object.assign(defaults, config.defaultConfig);
} }
return defaults; return defaults;
} }
@@ -191,32 +193,35 @@ export function getDefaultConfig(nodeType: string): Record<string, unknown> {
*/ */
export function validateNodeConfig( export function validateNodeConfig(
nodeType: string, nodeType: string,
config: Record<string, unknown> config: Record<string, unknown>,
): { valid: boolean; errors: string[] } { ): { valid: boolean; errors: string[] } {
const nodeConfig = getNodeConfig(nodeType); const nodeConfig = getNodeConfig(nodeType);
if (!nodeConfig) { if (!nodeConfig) {
return { valid: false, errors: [`Unknown node type: ${nodeType}`] }; return { valid: false, errors: [`Unknown node type: ${nodeType}`] };
} }
const errors: string[] = []; const errors: string[] = [];
for (const field of nodeConfig.configSchema) { for (const field of nodeConfig.configSchema) {
const value = config[field.name]; const value = config[field.name];
// Check required fields // Check required fields
if (field.required && (value === undefined || value === null || value === '')) { if (
field.required &&
(value === undefined || value === null || value === '')
) {
errors.push(`Field "${field.name}" is required`); errors.push(`Field "${field.name}" is required`);
continue; continue;
} }
// Skip validation for optional empty fields // Skip validation for optional empty fields
if (!field.required && (value === undefined || value === null)) { if (!field.required && (value === undefined || value === null)) {
continue; continue;
} }
// Type-specific validation could be added here // Type-specific validation could be added here
} }
return { valid: errors.length === 0, errors }; return { valid: errors.length === 0, errors };
} }
@@ -1,6 +1,6 @@
/** /**
* Integration Node Configurations * Integration Node Configurations
* *
* Defines configurations for integration node types: * Defines configurations for integration node types:
* - database_query: Query databases * - database_query: Query databases
* - redis_operation: Redis operations * - redis_operation: Redis operations
@@ -64,7 +64,10 @@ export const databaseQueryConfig: NodeConfigMeta = {
required: true, required: true,
default: 'postgresql', default: 'postgresql',
options: [ options: [
{ name: 'postgresql', label: { en_US: 'PostgreSQL', zh_Hans: 'PostgreSQL' } }, {
name: 'postgresql',
label: { en_US: 'PostgreSQL', zh_Hans: 'PostgreSQL' },
},
{ name: 'mysql', label: { en_US: 'MySQL', zh_Hans: 'MySQL' } }, { name: 'mysql', label: { en_US: 'MySQL', zh_Hans: 'MySQL' } },
{ name: 'sqlite', label: { en_US: 'SQLite', zh_Hans: 'SQLite' } }, { name: 'sqlite', label: { en_US: 'SQLite', zh_Hans: 'SQLite' } },
], ],
@@ -372,7 +375,8 @@ export const mcpToolConfig: NodeConfigMeta = {
zh_Hans: '参数模板', zh_Hans: '参数模板',
}, },
description: { description: {
en_US: 'Tool arguments as JSON (supports variable interpolation). Leave empty to use input.', en_US:
'Tool arguments as JSON (supports variable interpolation). Leave empty to use input.',
zh_Hans: '工具参数(JSON 格式,支持变量插值)。留空则使用输入。', zh_Hans: '工具参数(JSON 格式,支持变量插值)。留空则使用输入。',
}, },
required: false, required: false,
@@ -537,27 +541,78 @@ export const memoryStoreConfig: NodeConfigMeta = {
export const difyWorkflowConfig: NodeConfigMeta = { export const difyWorkflowConfig: NodeConfigMeta = {
nodeType: 'dify_workflow', nodeType: 'dify_workflow',
label: { en_US: 'Dify Workflow', zh_Hans: 'Dify 工作流' }, label: { en_US: 'Dify Workflow', zh_Hans: 'Dify 工作流' },
description: { en_US: 'Call a Dify platform workflow', zh_Hans: '调用 Dify 平台工作流' }, description: {
en_US: 'Call a Dify platform workflow',
zh_Hans: '调用 Dify 平台工作流',
},
icon: 'Bot', icon: 'Bot',
category: 'integration', category: 'integration',
color: '#ec4899', color: '#ec4899',
inputs: [ inputs: [
createInput('input', 'any', { description: 'Input data', label: { en_US: 'Input', zh_Hans: '输入' }, required: false }), createInput('input', 'any', {
description: 'Input data',
label: { en_US: 'Input', zh_Hans: '输入' },
required: false,
}),
], ],
outputs: [ outputs: [
createOutput('result', 'any', { description: 'Workflow result', label: { en_US: 'Result', zh_Hans: '结果' } }), createOutput('result', 'any', {
createOutput('success', 'boolean', { description: 'Whether call was successful', label: { en_US: 'Success', zh_Hans: '成功' } }), description: 'Workflow result',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether call was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
], ],
configSchema: [ configSchema: [
{ id: 'base-url', name: 'base-url', type: DynamicFormItemType.STRING, label: { en_US: 'Base URL', zh_Hans: 'Base URL' }, description: { en_US: 'Dify API base URL', zh_Hans: 'Dify API 基础 URL' }, required: true, default: '' }, {
{ id: 'api-key', name: 'api-key', type: DynamicFormItemType.STRING, label: { en_US: 'API Key', zh_Hans: 'API Key' }, description: { en_US: 'Dify API key', zh_Hans: 'Dify API 密钥' }, required: true, default: '' }, id: 'base-url',
{ id: 'app-type', name: 'app-type', type: DynamicFormItemType.SELECT, label: { en_US: 'App Type', zh_Hans: '应用类型' }, description: { en_US: 'Dify application type', zh_Hans: 'Dify 应用类型' }, required: true, default: 'workflow', options: [ name: 'base-url',
{ name: 'workflow', label: { en_US: 'Workflow', zh_Hans: '工作流' } }, type: DynamicFormItemType.STRING,
{ name: 'chatbot', label: { en_US: 'Chatbot', zh_Hans: '聊天机器人' } }, label: { en_US: 'Base URL', zh_Hans: 'Base URL' },
] }, description: { en_US: 'Dify API base URL', zh_Hans: 'Dify API 基础 URL' },
{ id: 'timeout', name: 'timeout', type: DynamicFormItemType.INT, label: { en_US: 'Timeout (seconds)', zh_Hans: '超时时间(秒)' }, description: { en_US: 'Request timeout', zh_Hans: '请求超时时间' }, required: false, default: 60 }, required: true,
default: '',
},
{
id: 'api-key',
name: 'api-key',
type: DynamicFormItemType.STRING,
label: { en_US: 'API Key', zh_Hans: 'API Key' },
description: { en_US: 'Dify API key', zh_Hans: 'Dify API 密钥' },
required: true,
default: '',
},
{
id: 'app-type',
name: 'app-type',
type: DynamicFormItemType.SELECT,
label: { en_US: 'App Type', zh_Hans: '应用类型' },
description: { en_US: 'Dify application type', zh_Hans: 'Dify 应用类型' },
required: true,
default: 'workflow',
options: [
{ name: 'workflow', label: { en_US: 'Workflow', zh_Hans: '工作流' } },
{ name: 'chatbot', label: { en_US: 'Chatbot', zh_Hans: '聊天机器人' } },
],
},
{
id: 'timeout',
name: 'timeout',
type: DynamicFormItemType.INT,
label: { en_US: 'Timeout (seconds)', zh_Hans: '超时时间(秒)' },
description: { en_US: 'Request timeout', zh_Hans: '请求超时时间' },
required: false,
default: 60,
},
], ],
defaultConfig: { 'base-url': '', 'api-key': '', 'app-type': 'workflow', timeout: 60 }, defaultConfig: {
'base-url': '',
'api-key': '',
'app-type': 'workflow',
timeout: 60,
},
}; };
/** /**
@@ -566,22 +621,69 @@ export const difyWorkflowConfig: NodeConfigMeta = {
export const difyKnowledgeQueryConfig: NodeConfigMeta = { export const difyKnowledgeQueryConfig: NodeConfigMeta = {
nodeType: 'dify_knowledge_query', nodeType: 'dify_knowledge_query',
label: { en_US: 'Dify Knowledge Query', zh_Hans: 'Dify 知识库查询' }, label: { en_US: 'Dify Knowledge Query', zh_Hans: 'Dify 知识库查询' },
description: { en_US: 'Query Dify knowledge base', zh_Hans: '查询 Dify 知识库' }, description: {
en_US: 'Query Dify knowledge base',
zh_Hans: '查询 Dify 知识库',
},
icon: 'Search', icon: 'Search',
category: 'integration', category: 'integration',
color: '#ec4899', color: '#ec4899',
inputs: [ inputs: [
createInput('query', 'string', { description: 'Search query', label: { en_US: 'Query', zh_Hans: '查询' } }), createInput('query', 'string', {
description: 'Search query',
label: { en_US: 'Query', zh_Hans: '查询' },
}),
], ],
outputs: [ outputs: [
createOutput('results', 'array', { description: 'Search results', label: { en_US: 'Results', zh_Hans: '结果' } }), createOutput('results', 'array', {
createOutput('success', 'boolean', { description: 'Whether query was successful', label: { en_US: 'Success', zh_Hans: '成功' } }), description: 'Search results',
label: { en_US: 'Results', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether query was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
], ],
configSchema: [ configSchema: [
{ id: 'base-url', name: 'base-url', type: DynamicFormItemType.STRING, label: { en_US: 'Base URL', zh_Hans: 'Base URL' }, description: { en_US: 'Dify API base URL', zh_Hans: 'Dify API 基础 URL' }, required: true, default: '' }, {
{ id: 'api-key', name: 'api-key', type: DynamicFormItemType.STRING, label: { en_US: 'API Key', zh_Hans: 'API Key' }, description: { en_US: 'Dify API key', zh_Hans: 'Dify API 密钥' }, required: true, default: '' }, id: 'base-url',
{ id: 'dataset_id', name: 'dataset_id', type: DynamicFormItemType.STRING, label: { en_US: 'Dataset ID', zh_Hans: '数据集 ID' }, description: { en_US: 'Dify dataset ID', zh_Hans: 'Dify 数据集 ID' }, required: true, default: '' }, name: 'base-url',
{ id: 'top_k', name: 'top_k', type: DynamicFormItemType.INT, label: { en_US: 'Top K', zh_Hans: 'Top K' }, description: { en_US: 'Number of results to return', zh_Hans: '返回结果数量' }, required: false, default: 5 }, type: DynamicFormItemType.STRING,
label: { en_US: 'Base URL', zh_Hans: 'Base URL' },
description: { en_US: 'Dify API base URL', zh_Hans: 'Dify API 基础 URL' },
required: true,
default: '',
},
{
id: 'api-key',
name: 'api-key',
type: DynamicFormItemType.STRING,
label: { en_US: 'API Key', zh_Hans: 'API Key' },
description: { en_US: 'Dify API key', zh_Hans: 'Dify API 密钥' },
required: true,
default: '',
},
{
id: 'dataset_id',
name: 'dataset_id',
type: DynamicFormItemType.STRING,
label: { en_US: 'Dataset ID', zh_Hans: '数据集 ID' },
description: { en_US: 'Dify dataset ID', zh_Hans: 'Dify 数据集 ID' },
required: true,
default: '',
},
{
id: 'top_k',
name: 'top_k',
type: DynamicFormItemType.INT,
label: { en_US: 'Top K', zh_Hans: 'Top K' },
description: {
en_US: 'Number of results to return',
zh_Hans: '返回结果数量',
},
required: false,
default: 5,
},
], ],
defaultConfig: { 'base-url': '', 'api-key': '', dataset_id: '', top_k: 5 }, defaultConfig: { 'base-url': '', 'api-key': '', dataset_id: '', top_k: 5 },
}; };
@@ -592,20 +694,49 @@ export const difyKnowledgeQueryConfig: NodeConfigMeta = {
export const n8nWorkflowConfig: NodeConfigMeta = { export const n8nWorkflowConfig: NodeConfigMeta = {
nodeType: 'n8n_workflow', nodeType: 'n8n_workflow',
label: { en_US: 'N8n Workflow', zh_Hans: 'n8n 工作流' }, label: { en_US: 'N8n Workflow', zh_Hans: 'n8n 工作流' },
description: { en_US: 'Call an n8n workflow via webhook', zh_Hans: '通过 webhook 调用 n8n 工作流' }, description: {
en_US: 'Call an n8n workflow via webhook',
zh_Hans: '通过 webhook 调用 n8n 工作流',
},
icon: 'Settings', icon: 'Settings',
category: 'integration', category: 'integration',
color: '#ec4899', color: '#ec4899',
inputs: [ inputs: [
createInput('input', 'any', { description: 'Input data', label: { en_US: 'Input', zh_Hans: '输入' }, required: false }), createInput('input', 'any', {
description: 'Input data',
label: { en_US: 'Input', zh_Hans: '输入' },
required: false,
}),
], ],
outputs: [ outputs: [
createOutput('result', 'any', { description: 'Workflow result', label: { en_US: 'Result', zh_Hans: '结果' } }), createOutput('result', 'any', {
createOutput('success', 'boolean', { description: 'Whether call was successful', label: { en_US: 'Success', zh_Hans: '成功' } }), description: 'Workflow result',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether call was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
], ],
configSchema: [ configSchema: [
{ id: 'webhook-url', name: 'webhook-url', type: DynamicFormItemType.STRING, label: { en_US: 'Webhook URL', zh_Hans: 'Webhook URL' }, description: { en_US: 'N8n webhook URL', zh_Hans: 'n8n Webhook URL' }, required: true, default: '' }, {
{ id: 'timeout', name: 'timeout', type: DynamicFormItemType.INT, label: { en_US: 'Timeout (seconds)', zh_Hans: '超时时间(秒)' }, description: { en_US: 'Request timeout', zh_Hans: '请求超时时间' }, required: false, default: 60 }, id: 'webhook-url',
name: 'webhook-url',
type: DynamicFormItemType.STRING,
label: { en_US: 'Webhook URL', zh_Hans: 'Webhook URL' },
description: { en_US: 'N8n webhook URL', zh_Hans: 'n8n Webhook URL' },
required: true,
default: '',
},
{
id: 'timeout',
name: 'timeout',
type: DynamicFormItemType.INT,
label: { en_US: 'Timeout (seconds)', zh_Hans: '超时时间(秒)' },
description: { en_US: 'Request timeout', zh_Hans: '请求超时时间' },
required: false,
default: 60,
},
], ],
defaultConfig: { 'webhook-url': '', timeout: 60 }, defaultConfig: { 'webhook-url': '', timeout: 60 },
}; };
@@ -621,17 +752,65 @@ export const langflowFlowConfig: NodeConfigMeta = {
category: 'integration', category: 'integration',
color: '#ec4899', color: '#ec4899',
inputs: [ inputs: [
createInput('input', 'any', { description: 'Input data', label: { en_US: 'Input', zh_Hans: '输入' }, required: false }), createInput('input', 'any', {
description: 'Input data',
label: { en_US: 'Input', zh_Hans: '输入' },
required: false,
}),
], ],
outputs: [ outputs: [
createOutput('result', 'any', { description: 'Flow result', label: { en_US: 'Result', zh_Hans: '结果' } }), createOutput('result', 'any', {
createOutput('success', 'boolean', { description: 'Whether call was successful', label: { en_US: 'Success', zh_Hans: '成功' } }), description: 'Flow result',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether call was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
], ],
configSchema: [ configSchema: [
{ id: 'base-url', name: 'base-url', type: DynamicFormItemType.STRING, label: { en_US: 'Base URL', zh_Hans: 'Base URL' }, description: { en_US: 'Langflow API base URL', zh_Hans: 'Langflow API 基础 URL' }, required: true, default: '' }, {
{ id: 'flow-id', name: 'flow-id', type: DynamicFormItemType.STRING, label: { en_US: 'Flow ID', zh_Hans: '流程 ID' }, description: { en_US: 'Langflow flow ID', zh_Hans: 'Langflow 流程 ID' }, required: true, default: '' }, id: 'base-url',
{ id: 'api-key', name: 'api-key', type: DynamicFormItemType.STRING, label: { en_US: 'API Key', zh_Hans: 'API Key' }, description: { en_US: 'Langflow API key (optional)', zh_Hans: 'Langflow API 密钥(可选)' }, required: false, default: '' }, name: 'base-url',
{ id: 'timeout', name: 'timeout', type: DynamicFormItemType.INT, label: { en_US: 'Timeout (seconds)', zh_Hans: '超时时间(秒)' }, description: { en_US: 'Request timeout', zh_Hans: '请求超时时间' }, required: false, default: 60 }, type: DynamicFormItemType.STRING,
label: { en_US: 'Base URL', zh_Hans: 'Base URL' },
description: {
en_US: 'Langflow API base URL',
zh_Hans: 'Langflow API 基础 URL',
},
required: true,
default: '',
},
{
id: 'flow-id',
name: 'flow-id',
type: DynamicFormItemType.STRING,
label: { en_US: 'Flow ID', zh_Hans: '流程 ID' },
description: { en_US: 'Langflow flow ID', zh_Hans: 'Langflow 流程 ID' },
required: true,
default: '',
},
{
id: 'api-key',
name: 'api-key',
type: DynamicFormItemType.STRING,
label: { en_US: 'API Key', zh_Hans: 'API Key' },
description: {
en_US: 'Langflow API key (optional)',
zh_Hans: 'Langflow API 密钥(可选)',
},
required: false,
default: '',
},
{
id: 'timeout',
name: 'timeout',
type: DynamicFormItemType.INT,
label: { en_US: 'Timeout (seconds)', zh_Hans: '超时时间(秒)' },
description: { en_US: 'Request timeout', zh_Hans: '请求超时时间' },
required: false,
default: 60,
},
], ],
defaultConfig: { 'base-url': '', 'flow-id': '', 'api-key': '', timeout: 60 }, defaultConfig: { 'base-url': '', 'flow-id': '', 'api-key': '', timeout: 60 },
}; };
@@ -647,19 +826,65 @@ export const cozeBotConfig: NodeConfigMeta = {
category: 'integration', category: 'integration',
color: '#ec4899', color: '#ec4899',
inputs: [ inputs: [
createInput('message', 'string', { description: 'Message to send', label: { en_US: 'Message', zh_Hans: '消息' } }), createInput('message', 'string', {
description: 'Message to send',
label: { en_US: 'Message', zh_Hans: '消息' },
}),
], ],
outputs: [ outputs: [
createOutput('result', 'any', { description: 'Bot response', label: { en_US: 'Result', zh_Hans: '结果' } }), createOutput('result', 'any', {
createOutput('success', 'boolean', { description: 'Whether call was successful', label: { en_US: 'Success', zh_Hans: '成功' } }), description: 'Bot response',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether call was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
], ],
configSchema: [ configSchema: [
{ id: 'api-base', name: 'api-base', type: DynamicFormItemType.STRING, label: { en_US: 'API Base URL', zh_Hans: 'API 基础 URL' }, description: { en_US: 'Coze API base URL', zh_Hans: 'Coze API 基础 URL' }, required: true, default: 'https://api.coze.com' }, {
{ id: 'bot-id', name: 'bot-id', type: DynamicFormItemType.STRING, label: { en_US: 'Bot ID', zh_Hans: 'Bot ID' }, description: { en_US: 'Coze Bot ID', zh_Hans: 'Coze Bot ID' }, required: true, default: '' }, id: 'api-base',
{ id: 'api-key', name: 'api-key', type: DynamicFormItemType.STRING, label: { en_US: 'API Key', zh_Hans: 'API Key' }, description: { en_US: 'Coze API key', zh_Hans: 'Coze API 密钥' }, required: true, default: '' }, name: 'api-base',
{ id: 'timeout', name: 'timeout', type: DynamicFormItemType.INT, label: { en_US: 'Timeout (seconds)', zh_Hans: '超时时间(秒)' }, description: { en_US: 'Request timeout', zh_Hans: '请求超时时间' }, required: false, default: 60 }, type: DynamicFormItemType.STRING,
label: { en_US: 'API Base URL', zh_Hans: 'API 基础 URL' },
description: { en_US: 'Coze API base URL', zh_Hans: 'Coze API 基础 URL' },
required: true,
default: 'https://api.coze.com',
},
{
id: 'bot-id',
name: 'bot-id',
type: DynamicFormItemType.STRING,
label: { en_US: 'Bot ID', zh_Hans: 'Bot ID' },
description: { en_US: 'Coze Bot ID', zh_Hans: 'Coze Bot ID' },
required: true,
default: '',
},
{
id: 'api-key',
name: 'api-key',
type: DynamicFormItemType.STRING,
label: { en_US: 'API Key', zh_Hans: 'API Key' },
description: { en_US: 'Coze API key', zh_Hans: 'Coze API 密钥' },
required: true,
default: '',
},
{
id: 'timeout',
name: 'timeout',
type: DynamicFormItemType.INT,
label: { en_US: 'Timeout (seconds)', zh_Hans: '超时时间(秒)' },
description: { en_US: 'Request timeout', zh_Hans: '请求超时时间' },
required: false,
default: 60,
},
], ],
defaultConfig: { 'api-base': 'https://api.coze.com', 'bot-id': '', 'api-key': '', timeout: 60 }, defaultConfig: {
'api-base': 'https://api.coze.com',
'bot-id': '',
'api-key': '',
timeout: 60,
},
}; };
/** /**
@@ -680,6 +905,8 @@ export const integrationConfigs: NodeConfigMeta[] = [
/** /**
* Get integration config by type * Get integration config by type
*/ */
export function getIntegrationConfig(nodeType: string): NodeConfigMeta | undefined { export function getIntegrationConfig(
nodeType: string,
): NodeConfigMeta | undefined {
return integrationConfigs.find((config) => config.nodeType === nodeType); return integrationConfigs.find((config) => config.nodeType === nodeType);
} }
@@ -1,6 +1,6 @@
/** /**
* Process Node Configurations * Process Node Configurations
* *
* Defines configurations for general processing node types: * Defines configurations for general processing node types:
* - text_template: Generate text using templates * - text_template: Generate text using templates
* - json_transform: Transform JSON data * - json_transform: Transform JSON data
@@ -52,7 +52,8 @@ export const textTemplateConfig: NodeConfigMeta = {
zh_Hans: '模板', zh_Hans: '模板',
}, },
description: { description: {
en_US: 'Text template with variable placeholders (e.g., {{variable_name}})', en_US:
'Text template with variable placeholders (e.g., {{variable_name}})',
zh_Hans: '带有变量占位符的文本模板(例如 {{variable_name}}', zh_Hans: '带有变量占位符的文本模板(例如 {{variable_name}}',
}, },
required: true, required: true,
@@ -141,9 +142,18 @@ export const jsonTransformConfig: NodeConfigMeta = {
required: true, required: true,
default: 'jmespath', default: 'jmespath',
options: [ options: [
{ name: 'jmespath', label: { en_US: 'JMESPath Expression', zh_Hans: 'JMESPath 表达式' } }, {
{ name: 'jsonpath', label: { en_US: 'JSONPath Expression', zh_Hans: 'JSONPath 表达式' } }, name: 'jmespath',
{ name: 'mapping', label: { en_US: 'Field Mapping', zh_Hans: '字段映射' } }, label: { en_US: 'JMESPath Expression', zh_Hans: 'JMESPath 表达式' },
},
{
name: 'jsonpath',
label: { en_US: 'JSONPath Expression', zh_Hans: 'JSONPath 表达式' },
},
{
name: 'mapping',
label: { en_US: 'Field Mapping', zh_Hans: '字段映射' },
},
], ],
}, },
{ {
@@ -175,8 +185,10 @@ export const jsonTransformConfig: NodeConfigMeta = {
zh_Hans: '字段映射', zh_Hans: '字段映射',
}, },
description: { description: {
en_US: 'JSON object defining field mappings: {"output_field": "input.path.to.field"}', en_US:
zh_Hans: '定义字段映射的 JSON 对象: {"output_field": "input.path.to.field"}', 'JSON object defining field mappings: {"output_field": "input.path.to.field"}',
zh_Hans:
'定义字段映射的 JSON 对象: {"output_field": "input.path.to.field"}',
}, },
required: true, required: true,
default: '{}', default: '{}',
@@ -243,7 +255,10 @@ export const codeExecutorConfig: NodeConfigMeta = {
required: true, required: true,
default: 'javascript', default: 'javascript',
options: [ options: [
{ name: 'javascript', label: { en_US: 'JavaScript', zh_Hans: 'JavaScript' } }, {
name: 'javascript',
label: { en_US: 'JavaScript', zh_Hans: 'JavaScript' },
},
{ name: 'python', label: { en_US: 'Python', zh_Hans: 'Python' } }, { name: 'python', label: { en_US: 'Python', zh_Hans: 'Python' } },
], ],
}, },
@@ -256,11 +271,13 @@ export const codeExecutorConfig: NodeConfigMeta = {
zh_Hans: '代码', zh_Hans: '代码',
}, },
description: { description: {
en_US: 'Code to execute. Use `input` to access input data and return the result.', en_US:
'Code to execute. Use `input` to access input data and return the result.',
zh_Hans: '要执行的代码。使用 `input` 访问输入数据,并返回结果。', zh_Hans: '要执行的代码。使用 `input` 访问输入数据,并返回结果。',
}, },
required: true, required: true,
default: '// Access input with: input\n// Return result with: return result;\n\nreturn input;', default:
'// Access input with: input\n// Return result with: return result;\n\nreturn input;',
}, },
{ {
id: 'timeout', id: 'timeout',
@@ -334,13 +351,25 @@ export const dataAggregatorConfig: NodeConfigMeta = {
required: true, required: true,
default: 'array', default: 'array',
options: [ options: [
{ name: 'array', label: { en_US: 'Collect to Array', zh_Hans: '收集为数组' } }, {
{ name: 'concat', label: { en_US: 'Concatenate Strings', zh_Hans: '连接字符串' } }, name: 'array',
label: { en_US: 'Collect to Array', zh_Hans: '收集为数组' },
},
{
name: 'concat',
label: { en_US: 'Concatenate Strings', zh_Hans: '连接字符串' },
},
{ name: 'sum', label: { en_US: 'Sum Numbers', zh_Hans: '求和' } }, { name: 'sum', label: { en_US: 'Sum Numbers', zh_Hans: '求和' } },
{ name: 'average', label: { en_US: 'Average Numbers', zh_Hans: '求平均' } }, {
name: 'average',
label: { en_US: 'Average Numbers', zh_Hans: '求平均' },
},
{ name: 'min', label: { en_US: 'Minimum', zh_Hans: '最小值' } }, { name: 'min', label: { en_US: 'Minimum', zh_Hans: '最小值' } },
{ name: 'max', label: { en_US: 'Maximum', zh_Hans: '最大值' } }, { name: 'max', label: { en_US: 'Maximum', zh_Hans: '最大值' } },
{ name: 'merge', label: { en_US: 'Merge Objects', zh_Hans: '合并对象' } }, {
name: 'merge',
label: { en_US: 'Merge Objects', zh_Hans: '合并对象' },
},
{ name: 'first', label: { en_US: 'First Item', zh_Hans: '第一项' } }, { name: 'first', label: { en_US: 'First Item', zh_Hans: '第一项' } },
{ name: 'last', label: { en_US: 'Last Item', zh_Hans: '最后一项' } }, { name: 'last', label: { en_US: 'Last Item', zh_Hans: '最后一项' } },
], ],
@@ -437,11 +466,23 @@ export const textSplitterConfig: NodeConfigMeta = {
required: true, required: true,
default: 'separator', default: 'separator',
options: [ options: [
{ name: 'separator', label: { en_US: 'By Separator', zh_Hans: '按分隔符' } }, {
name: 'separator',
label: { en_US: 'By Separator', zh_Hans: '按分隔符' },
},
{ name: 'length', label: { en_US: 'By Length', zh_Hans: '按长度' } }, { name: 'length', label: { en_US: 'By Length', zh_Hans: '按长度' } },
{ name: 'sentences', label: { en_US: 'By Sentences', zh_Hans: '按句子' } }, {
{ name: 'paragraphs', label: { en_US: 'By Paragraphs', zh_Hans: '按段落' } }, name: 'sentences',
{ name: 'regex', label: { en_US: 'By Regex', zh_Hans: '按正则表达式' } }, label: { en_US: 'By Sentences', zh_Hans: '按句子' },
},
{
name: 'paragraphs',
label: { en_US: 'By Paragraphs', zh_Hans: '按段落' },
},
{
name: 'regex',
label: { en_US: 'By Regex', zh_Hans: '按正则表达式' },
},
], ],
}, },
{ {
@@ -613,7 +654,10 @@ export const variableAssignmentConfig: NodeConfigMeta = {
options: [ options: [
{ name: 'input', label: { en_US: 'From Input', zh_Hans: '来自输入' } }, { name: 'input', label: { en_US: 'From Input', zh_Hans: '来自输入' } },
{ name: 'static', label: { en_US: 'Static Value', zh_Hans: '静态值' } }, { name: 'static', label: { en_US: 'Static Value', zh_Hans: '静态值' } },
{ name: 'expression', label: { en_US: 'Expression', zh_Hans: '表达式' } }, {
name: 'expression',
label: { en_US: 'Expression', zh_Hans: '表达式' },
},
], ],
}, },
{ {
@@ -714,7 +758,10 @@ export const dataTransformConfig: NodeConfigMeta = {
{ name: 'template', label: { en_US: 'Template', zh_Hans: '模板' } }, { name: 'template', label: { en_US: 'Template', zh_Hans: '模板' } },
{ name: 'jsonpath', label: { en_US: 'JSONPath', zh_Hans: 'JSONPath' } }, { name: 'jsonpath', label: { en_US: 'JSONPath', zh_Hans: 'JSONPath' } },
{ name: 'jmespath', label: { en_US: 'JMESPath', zh_Hans: 'JMESPath' } }, { name: 'jmespath', label: { en_US: 'JMESPath', zh_Hans: 'JMESPath' } },
{ name: 'expression', label: { en_US: 'Expression', zh_Hans: '表达式' } }, {
name: 'expression',
label: { en_US: 'Expression', zh_Hans: '表达式' },
},
], ],
}, },
{ {
@@ -1,6 +1,6 @@
/** /**
* Trigger Node Configurations * Trigger Node Configurations
* *
* Defines configurations for all trigger node types: * Defines configurations for all trigger node types:
* - message_trigger: Triggered by incoming messages * - message_trigger: Triggered by incoming messages
* - cron_trigger: Triggered by scheduled time * - cron_trigger: Triggered by scheduled time
@@ -62,9 +62,15 @@ export const messageTriggerConfig: NodeConfigMeta = {
default: 'all', default: 'all',
options: [ options: [
{ name: 'all', label: { en_US: 'All Messages', zh_Hans: '所有消息' } }, { name: 'all', label: { en_US: 'All Messages', zh_Hans: '所有消息' } },
{ name: 'prefix', label: { en_US: 'Prefix Match', zh_Hans: '前缀匹配' } }, {
name: 'prefix',
label: { en_US: 'Prefix Match', zh_Hans: '前缀匹配' },
},
{ name: 'regex', label: { en_US: 'Regex Match', zh_Hans: '正则匹配' } }, { name: 'regex', label: { en_US: 'Regex Match', zh_Hans: '正则匹配' } },
{ name: 'contains', label: { en_US: 'Contains Keyword', zh_Hans: '包含关键词' } }, {
name: 'contains',
label: { en_US: 'Contains Keyword', zh_Hans: '包含关键词' },
},
{ name: 'exact', label: { en_US: 'Exact Match', zh_Hans: '精确匹配' } }, { name: 'exact', label: { en_US: 'Exact Match', zh_Hans: '精确匹配' } },
], ],
}, },
@@ -77,7 +83,8 @@ export const messageTriggerConfig: NodeConfigMeta = {
zh_Hans: '匹配模式', zh_Hans: '匹配模式',
}, },
description: { description: {
en_US: 'The pattern to match against the message (prefix, regex, keyword, or exact text)', en_US:
'The pattern to match against the message (prefix, regex, keyword, or exact text)',
zh_Hans: '用于匹配消息的模式(前缀、正则表达式、关键词或精确文本)', zh_Hans: '用于匹配消息的模式(前缀、正则表达式、关键词或精确文本)',
}, },
required: false, required: false,
@@ -172,12 +179,39 @@ export const cronTriggerConfig: NodeConfigMeta = {
default: 'Asia/Shanghai', default: 'Asia/Shanghai',
options: [ options: [
{ name: 'UTC', label: { en_US: 'UTC', zh_Hans: 'UTC' } }, { name: 'UTC', label: { en_US: 'UTC', zh_Hans: 'UTC' } },
{ name: 'Asia/Shanghai', label: { en_US: 'Asia/Shanghai (UTC+8)', zh_Hans: '亚洲/上海 (UTC+8)' } }, {
{ name: 'Asia/Tokyo', label: { en_US: 'Asia/Tokyo (UTC+9)', zh_Hans: '亚洲/东京 (UTC+9)' } }, name: 'Asia/Shanghai',
{ name: 'America/New_York', label: { en_US: 'America/New_York (EST)', zh_Hans: '美国/纽约 (EST)' } }, label: {
{ name: 'America/Los_Angeles', label: { en_US: 'America/Los_Angeles (PST)', zh_Hans: '美国/洛杉矶 (PST)' } }, en_US: 'Asia/Shanghai (UTC+8)',
{ name: 'Europe/London', label: { en_US: 'Europe/London (GMT)', zh_Hans: '洲/伦敦 (GMT)' } }, zh_Hans: '洲/上海 (UTC+8)',
{ name: 'Europe/Berlin', label: { en_US: 'Europe/Berlin (CET)', zh_Hans: '欧洲/柏林 (CET)' } }, },
},
{
name: 'Asia/Tokyo',
label: { en_US: 'Asia/Tokyo (UTC+9)', zh_Hans: '亚洲/东京 (UTC+9)' },
},
{
name: 'America/New_York',
label: {
en_US: 'America/New_York (EST)',
zh_Hans: '美国/纽约 (EST)',
},
},
{
name: 'America/Los_Angeles',
label: {
en_US: 'America/Los_Angeles (PST)',
zh_Hans: '美国/洛杉矶 (PST)',
},
},
{
name: 'Europe/London',
label: { en_US: 'Europe/London (GMT)', zh_Hans: '欧洲/伦敦 (GMT)' },
},
{
name: 'Europe/Berlin',
label: { en_US: 'Europe/Berlin (CET)', zh_Hans: '欧洲/柏林 (CET)' },
},
], ],
}, },
{ {
@@ -230,7 +264,8 @@ export const webhookTriggerConfig: NodeConfigMeta = {
zh_Hans: 'Webhook 触发', zh_Hans: 'Webhook 触发',
}, },
description: { description: {
en_US: 'Trigger workflow when an HTTP request is received at the webhook URL', en_US:
'Trigger workflow when an HTTP request is received at the webhook URL',
zh_Hans: '当在 Webhook URL 收到 HTTP 请求时触发工作流', zh_Hans: '当在 Webhook URL 收到 HTTP 请求时触发工作流',
}, },
icon: 'Webhook', icon: 'Webhook',
@@ -288,8 +323,14 @@ export const webhookTriggerConfig: NodeConfigMeta = {
default: 'none', default: 'none',
options: [ options: [
{ name: 'none', label: { en_US: 'None', zh_Hans: '无' } }, { name: 'none', label: { en_US: 'None', zh_Hans: '无' } },
{ name: 'token', label: { en_US: 'Bearer Token', zh_Hans: 'Bearer 令牌' } }, {
{ name: 'signature', label: { en_US: 'Signature', zh_Hans: '签名验证' } }, name: 'token',
label: { en_US: 'Bearer Token', zh_Hans: 'Bearer 令牌' },
},
{
name: 'signature',
label: { en_US: 'Signature', zh_Hans: '签名验证' },
},
{ name: 'basic', label: { en_US: 'Basic Auth', zh_Hans: '基本认证' } }, { name: 'basic', label: { en_US: 'Basic Auth', zh_Hans: '基本认证' } },
], ],
}, },
@@ -328,10 +369,25 @@ export const webhookTriggerConfig: NodeConfigMeta = {
required: false, required: false,
default: 'application/json', default: 'application/json',
options: [ options: [
{ name: 'application/json', label: { en_US: 'application/json', zh_Hans: 'JSON' } }, {
{ name: 'application/x-www-form-urlencoded', label: { en_US: 'application/x-www-form-urlencoded', zh_Hans: '表单编码' } }, name: 'application/json',
{ name: 'multipart/form-data', label: { en_US: 'multipart/form-data', zh_Hans: '表单数据' } }, label: { en_US: 'application/json', zh_Hans: 'JSON' },
{ name: 'text/plain', label: { en_US: 'text/plain', zh_Hans: '纯文本' } }, },
{
name: 'application/x-www-form-urlencoded',
label: {
en_US: 'application/x-www-form-urlencoded',
zh_Hans: '表单编码',
},
},
{
name: 'multipart/form-data',
label: { en_US: 'multipart/form-data', zh_Hans: '表单数据' },
},
{
name: 'text/plain',
label: { en_US: 'text/plain', zh_Hans: '纯文本' },
},
], ],
}, },
{ {
@@ -424,15 +480,42 @@ export const eventTriggerConfig: NodeConfigMeta = {
required: true, required: true,
default: 'member_join', default: 'member_join',
options: [ options: [
{ name: 'member_join', label: { en_US: 'Member Join', zh_Hans: '成员加入' } }, {
{ name: 'member_leave', label: { en_US: 'Member Leave', zh_Hans: '成员离开' } }, name: 'member_join',
{ name: 'message_recall', label: { en_US: 'Message Recall', zh_Hans: '消息撤回' } }, label: { en_US: 'Member Join', zh_Hans: '成员加入' },
{ name: 'group_created', label: { en_US: 'Group Created', zh_Hans: '群组创建' } }, },
{ name: 'group_disbanded', label: { en_US: 'Group Disbanded', zh_Hans: '群组解散' } }, {
{ name: 'bot_added', label: { en_US: 'Bot Added to Group', zh_Hans: '机器人被添加到群' } }, name: 'member_leave',
{ name: 'bot_removed', label: { en_US: 'Bot Removed from Group', zh_Hans: '机器人被移出群' } }, label: { en_US: 'Member Leave', zh_Hans: '成员离开' },
{ name: 'friend_request', label: { en_US: 'Friend Request', zh_Hans: '好友请求' } }, },
{ name: 'group_request', label: { en_US: 'Group Join Request', zh_Hans: '入群请求' } }, {
name: 'message_recall',
label: { en_US: 'Message Recall', zh_Hans: '消息撤回' },
},
{
name: 'group_created',
label: { en_US: 'Group Created', zh_Hans: '群组创建' },
},
{
name: 'group_disbanded',
label: { en_US: 'Group Disbanded', zh_Hans: '群组解散' },
},
{
name: 'bot_added',
label: { en_US: 'Bot Added to Group', zh_Hans: '机器人被添加到群' },
},
{
name: 'bot_removed',
label: { en_US: 'Bot Removed from Group', zh_Hans: '机器人被移出群' },
},
{
name: 'friend_request',
label: { en_US: 'Friend Request', zh_Hans: '好友请求' },
},
{
name: 'group_request',
label: { en_US: 'Group Join Request', zh_Hans: '入群请求' },
},
], ],
}, },
], ],
@@ -1,6 +1,6 @@
/** /**
* Workflow Node Configuration Types * Workflow Node Configuration Types
* *
* This module defines the types used for node configuration metadata. * This module defines the types used for node configuration metadata.
* It extends the existing dynamic form system to support workflow-specific features. * It extends the existing dynamic form system to support workflow-specific features.
*/ */
@@ -23,40 +23,40 @@ export interface ExtendedPortDefinition extends PortDefinition {
export interface NodeConfigMeta { export interface NodeConfigMeta {
/** Unique node type identifier */ /** Unique node type identifier */
nodeType: string; nodeType: string;
/** Display name for the node */ /** Display name for the node */
label: I18nObject; label: I18nObject;
/** Description of what the node does */ /** Description of what the node does */
description: I18nObject; description: I18nObject;
/** Icon name (from lucide-react) */ /** Icon name (from lucide-react) */
icon: string; icon: string;
/** Node category for organization */ /** Node category for organization */
category: NodeCategory; category: NodeCategory;
/** Color for the node header */ /** Color for the node header */
color?: string; color?: string;
/** Input port definitions */ /** Input port definitions */
inputs: ExtendedPortDefinition[]; inputs: ExtendedPortDefinition[];
/** Output port definitions */ /** Output port definitions */
outputs: ExtendedPortDefinition[]; outputs: ExtendedPortDefinition[];
/** Configuration schema using the dynamic form system */ /** Configuration schema using the dynamic form system */
configSchema: IDynamicFormItemSchema[]; configSchema: IDynamicFormItemSchema[];
/** Default configuration values */ /** Default configuration values */
defaultConfig?: Record<string, unknown>; defaultConfig?: Record<string, unknown>;
/** Whether this node can be the starting point of a workflow */ /** Whether this node can be the starting point of a workflow */
isEntryPoint?: boolean; isEntryPoint?: boolean;
/** Maximum number of this node type allowed in a workflow (undefined = unlimited) */ /** Maximum number of this node type allowed in a workflow (undefined = unlimited) */
maxInstances?: number; maxInstances?: number;
/** Documentation URL */ /** Documentation URL */
docsUrl?: string; docsUrl?: string;
} }
@@ -76,7 +76,7 @@ export function createPort(
description?: string; description?: string;
required?: boolean; required?: boolean;
label?: I18nObject; label?: I18nObject;
} },
): ExtendedPortDefinition { ): ExtendedPortDefinition {
return { return {
name, name,
@@ -97,9 +97,12 @@ export function createInput(
description?: string; description?: string;
required?: boolean; required?: boolean;
label?: I18nObject; label?: I18nObject;
} },
): ExtendedPortDefinition { ): ExtendedPortDefinition {
return createPort(name, type, { ...options, required: options?.required ?? true }); return createPort(name, type, {
...options,
required: options?.required ?? true,
});
} }
/** /**
@@ -111,7 +114,7 @@ export function createOutput(
options?: { options?: {
description?: string; description?: string;
label?: I18nObject; label?: I18nObject;
} },
): ExtendedPortDefinition { ): ExtendedPortDefinition {
return createPort(name, type, { ...options, required: false }); return createPort(name, type, { ...options, required: false });
} }
@@ -48,53 +48,176 @@ import { resolveI18nLabel, maybeTranslateKey } from './workflow-i18n';
// Single source of truth. Used by WorkflowNodeComponent, // Single source of truth. Used by WorkflowNodeComponent,
// NodePalette, and useWorkflowStore. // NodePalette, and useWorkflowStore.
export const NODE_TYPE_I18N_KEYS: Record<string, { labelKey: string; descriptionKey: string }> = { export const NODE_TYPE_I18N_KEYS: Record<
string,
{ labelKey: string; descriptionKey: string }
> = {
// Trigger // Trigger
'trigger.message_trigger': { labelKey: 'workflows.nodes.messageTrigger', descriptionKey: 'workflows.nodes.messageTriggerDescription' }, 'trigger.message_trigger': {
'trigger.cron_trigger': { labelKey: 'workflows.nodes.cronTrigger', descriptionKey: 'workflows.nodes.cronTriggerDescription' }, labelKey: 'workflows.nodes.messageTrigger',
'trigger.webhook_trigger': { labelKey: 'workflows.nodes.webhookTrigger', descriptionKey: 'workflows.nodes.webhookTriggerDescription' }, descriptionKey: 'workflows.nodes.messageTriggerDescription',
'trigger.event_trigger': { labelKey: 'workflows.nodes.eventTrigger', descriptionKey: 'workflows.nodes.eventTriggerDescription' }, },
'trigger.cron_trigger': {
labelKey: 'workflows.nodes.cronTrigger',
descriptionKey: 'workflows.nodes.cronTriggerDescription',
},
'trigger.webhook_trigger': {
labelKey: 'workflows.nodes.webhookTrigger',
descriptionKey: 'workflows.nodes.webhookTriggerDescription',
},
'trigger.event_trigger': {
labelKey: 'workflows.nodes.eventTrigger',
descriptionKey: 'workflows.nodes.eventTriggerDescription',
},
// Process / AI // Process / AI
'process.llm_call': { labelKey: 'workflows.nodes.llmCall', descriptionKey: 'workflows.nodes.llmCallDescription' }, 'process.llm_call': {
'process.question_classifier': { labelKey: 'workflows.nodes.questionClassifier', descriptionKey: 'workflows.nodes.questionClassifierDescription' }, labelKey: 'workflows.nodes.llmCall',
'process.parameter_extractor': { labelKey: 'workflows.nodes.parameterExtractor', descriptionKey: 'workflows.nodes.parameterExtractorDescription' }, descriptionKey: 'workflows.nodes.llmCallDescription',
'process.knowledge_retrieval': { labelKey: 'workflows.nodes.knowledgeRetrieval', descriptionKey: 'workflows.nodes.knowledgeRetrievalDescription' }, },
'process.code_executor': { labelKey: 'workflows.nodes.codeExecutor', descriptionKey: 'workflows.nodes.codeExecutorDescription' }, 'process.question_classifier': {
'process.http_request': { labelKey: 'workflows.nodes.httpRequest', descriptionKey: 'workflows.nodes.httpRequestDescription' }, labelKey: 'workflows.nodes.questionClassifier',
'process.data_transform': { labelKey: 'workflows.nodes.dataTransform', descriptionKey: 'workflows.nodes.dataTransformDescription' }, descriptionKey: 'workflows.nodes.questionClassifierDescription',
'process.text_template': { labelKey: 'workflows.nodes.textTemplate', descriptionKey: 'workflows.nodes.textTemplateDescription' }, },
'process.json_transform': { labelKey: 'workflows.nodes.jsonTransform', descriptionKey: 'workflows.nodes.jsonTransformDescription' }, 'process.parameter_extractor': {
'process.data_aggregator': { labelKey: 'workflows.nodes.dataAggregator', descriptionKey: 'workflows.nodes.dataAggregatorDescription' }, labelKey: 'workflows.nodes.parameterExtractor',
'process.text_splitter': { labelKey: 'workflows.nodes.textSplitter', descriptionKey: 'workflows.nodes.textSplitterDescription' }, descriptionKey: 'workflows.nodes.parameterExtractorDescription',
'process.variable_assignment': { labelKey: 'workflows.nodes.variableAssignment', descriptionKey: 'workflows.nodes.variableAssignmentDescription' }, },
'process.knowledge_retrieval': {
labelKey: 'workflows.nodes.knowledgeRetrieval',
descriptionKey: 'workflows.nodes.knowledgeRetrievalDescription',
},
'process.code_executor': {
labelKey: 'workflows.nodes.codeExecutor',
descriptionKey: 'workflows.nodes.codeExecutorDescription',
},
'process.http_request': {
labelKey: 'workflows.nodes.httpRequest',
descriptionKey: 'workflows.nodes.httpRequestDescription',
},
'process.data_transform': {
labelKey: 'workflows.nodes.dataTransform',
descriptionKey: 'workflows.nodes.dataTransformDescription',
},
'process.text_template': {
labelKey: 'workflows.nodes.textTemplate',
descriptionKey: 'workflows.nodes.textTemplateDescription',
},
'process.json_transform': {
labelKey: 'workflows.nodes.jsonTransform',
descriptionKey: 'workflows.nodes.jsonTransformDescription',
},
'process.data_aggregator': {
labelKey: 'workflows.nodes.dataAggregator',
descriptionKey: 'workflows.nodes.dataAggregatorDescription',
},
'process.text_splitter': {
labelKey: 'workflows.nodes.textSplitter',
descriptionKey: 'workflows.nodes.textSplitterDescription',
},
'process.variable_assignment': {
labelKey: 'workflows.nodes.variableAssignment',
descriptionKey: 'workflows.nodes.variableAssignmentDescription',
},
// Control // Control
'control.condition': { labelKey: 'workflows.nodes.condition', descriptionKey: 'workflows.nodes.conditionDescription' }, 'control.condition': {
'control.switch': { labelKey: 'workflows.nodes.switch', descriptionKey: 'workflows.nodes.switchDescription' }, labelKey: 'workflows.nodes.condition',
'control.loop': { labelKey: 'workflows.nodes.loop', descriptionKey: 'workflows.nodes.loopDescription' }, descriptionKey: 'workflows.nodes.conditionDescription',
'control.iterator': { labelKey: 'workflows.nodes.iterator', descriptionKey: 'workflows.nodes.iteratorDescription' }, },
'control.parallel': { labelKey: 'workflows.nodes.parallel', descriptionKey: 'workflows.nodes.parallelDescription' }, 'control.switch': {
'control.wait': { labelKey: 'workflows.nodes.wait', descriptionKey: 'workflows.nodes.waitDescription' }, labelKey: 'workflows.nodes.switch',
'control.merge': { labelKey: 'workflows.nodes.merge', descriptionKey: 'workflows.nodes.mergeDescription' }, descriptionKey: 'workflows.nodes.switchDescription',
'control.variable_aggregator': { labelKey: 'workflows.nodes.variableAggregator', descriptionKey: 'workflows.nodes.variableAggregatorDescription' }, },
'control.loop': {
labelKey: 'workflows.nodes.loop',
descriptionKey: 'workflows.nodes.loopDescription',
},
'control.iterator': {
labelKey: 'workflows.nodes.iterator',
descriptionKey: 'workflows.nodes.iteratorDescription',
},
'control.parallel': {
labelKey: 'workflows.nodes.parallel',
descriptionKey: 'workflows.nodes.parallelDescription',
},
'control.wait': {
labelKey: 'workflows.nodes.wait',
descriptionKey: 'workflows.nodes.waitDescription',
},
'control.merge': {
labelKey: 'workflows.nodes.merge',
descriptionKey: 'workflows.nodes.mergeDescription',
},
'control.variable_aggregator': {
labelKey: 'workflows.nodes.variableAggregator',
descriptionKey: 'workflows.nodes.variableAggregatorDescription',
},
// Action // Action
'action.send_message': { labelKey: 'workflows.nodes.sendMessage', descriptionKey: 'workflows.nodes.sendMessageDescription' }, 'action.send_message': {
'action.reply_message': { labelKey: 'workflows.nodes.replyMessage', descriptionKey: 'workflows.nodes.replyMessageDescription' }, labelKey: 'workflows.nodes.sendMessage',
'action.store_data': { labelKey: 'workflows.nodes.storeData', descriptionKey: 'workflows.nodes.storeDataDescription' }, descriptionKey: 'workflows.nodes.sendMessageDescription',
'action.call_pipeline': { labelKey: 'workflows.nodes.callPipeline', descriptionKey: 'workflows.nodes.callPipelineDescription' }, },
'action.set_variable': { labelKey: 'workflows.nodes.setVariable', descriptionKey: 'workflows.nodes.setVariableDescription' }, 'action.reply_message': {
'action.opening_statement': { labelKey: 'workflows.nodes.openingStatement', descriptionKey: 'workflows.nodes.openingStatementDescription' }, labelKey: 'workflows.nodes.replyMessage',
'action.end': { labelKey: 'workflows.nodes.end', descriptionKey: 'workflows.nodes.endDescription' }, descriptionKey: 'workflows.nodes.replyMessageDescription',
},
'action.store_data': {
labelKey: 'workflows.nodes.storeData',
descriptionKey: 'workflows.nodes.storeDataDescription',
},
'action.call_pipeline': {
labelKey: 'workflows.nodes.callPipeline',
descriptionKey: 'workflows.nodes.callPipelineDescription',
},
'action.set_variable': {
labelKey: 'workflows.nodes.setVariable',
descriptionKey: 'workflows.nodes.setVariableDescription',
},
'action.opening_statement': {
labelKey: 'workflows.nodes.openingStatement',
descriptionKey: 'workflows.nodes.openingStatementDescription',
},
'action.end': {
labelKey: 'workflows.nodes.end',
descriptionKey: 'workflows.nodes.endDescription',
},
// Integration external services // Integration external services
'integration.dify_workflow': { labelKey: 'workflows.nodes.difyWorkflow', descriptionKey: 'workflows.nodes.difyWorkflowDescription' }, 'integration.dify_workflow': {
'integration.dify_knowledge_query': { labelKey: 'workflows.nodes.difyKnowledgeQuery', descriptionKey: 'workflows.nodes.difyKnowledgeQueryDescription' }, labelKey: 'workflows.nodes.difyWorkflow',
'integration.n8n_workflow': { labelKey: 'workflows.nodes.n8nWorkflow', descriptionKey: 'workflows.nodes.n8nWorkflowDescription' }, descriptionKey: 'workflows.nodes.difyWorkflowDescription',
'integration.langflow_flow': { labelKey: 'workflows.nodes.langflowFlow', descriptionKey: 'workflows.nodes.langflowFlowDescription' }, },
'integration.coze_bot': { labelKey: 'workflows.nodes.cozeBot', descriptionKey: 'workflows.nodes.cozeBotDescription' }, 'integration.dify_knowledge_query': {
labelKey: 'workflows.nodes.difyKnowledgeQuery',
descriptionKey: 'workflows.nodes.difyKnowledgeQueryDescription',
},
'integration.n8n_workflow': {
labelKey: 'workflows.nodes.n8nWorkflow',
descriptionKey: 'workflows.nodes.n8nWorkflowDescription',
},
'integration.langflow_flow': {
labelKey: 'workflows.nodes.langflowFlow',
descriptionKey: 'workflows.nodes.langflowFlowDescription',
},
'integration.coze_bot': {
labelKey: 'workflows.nodes.cozeBot',
descriptionKey: 'workflows.nodes.cozeBotDescription',
},
// Integration data & tools // Integration data & tools
'integration.database_query': { labelKey: 'workflows.nodes.databaseQuery', descriptionKey: 'workflows.nodes.databaseQueryDescription' }, 'integration.database_query': {
'integration.redis_operation': { labelKey: 'workflows.nodes.redisOperation', descriptionKey: 'workflows.nodes.redisOperationDescription' }, labelKey: 'workflows.nodes.databaseQuery',
'integration.mcp_tool': { labelKey: 'workflows.nodes.mcpTool', descriptionKey: 'workflows.nodes.mcpToolDescription' }, descriptionKey: 'workflows.nodes.databaseQueryDescription',
'integration.memory_store': { labelKey: 'workflows.nodes.memoryStore', descriptionKey: 'workflows.nodes.memoryStoreDescription' }, },
'integration.redis_operation': {
labelKey: 'workflows.nodes.redisOperation',
descriptionKey: 'workflows.nodes.redisOperationDescription',
},
'integration.mcp_tool': {
labelKey: 'workflows.nodes.mcpTool',
descriptionKey: 'workflows.nodes.mcpToolDescription',
},
'integration.memory_store': {
labelKey: 'workflows.nodes.memoryStore',
descriptionKey: 'workflows.nodes.memoryStoreDescription',
},
}; };
// Flat version: type → label key only (convenience for store / node component) // Flat version: type → label key only (convenience for store / node component)
@@ -104,10 +227,10 @@ export const NODE_TYPE_LABEL_KEYS: Record<string, string> = Object.fromEntries(
// Category i18n // Category i18n
export const CATEGORY_I18N_KEYS: Record<string, { labelKey: string }> = { export const CATEGORY_I18N_KEYS: Record<string, { labelKey: string }> = {
trigger: { labelKey: 'workflows.nodes.trigger' }, trigger: { labelKey: 'workflows.nodes.trigger' },
process: { labelKey: 'workflows.nodes.process' }, process: { labelKey: 'workflows.nodes.process' },
control: { labelKey: 'workflows.nodes.control' }, control: { labelKey: 'workflows.nodes.control' },
action: { labelKey: 'workflows.nodes.action' }, action: { labelKey: 'workflows.nodes.action' },
integration: { labelKey: 'workflows.nodes.integration' }, integration: { labelKey: 'workflows.nodes.integration' },
}; };
@@ -195,11 +318,16 @@ export const PALETTE_CATEGORY_COLORS: Record<string, string> = {
}; };
export const PALETTE_CATEGORY_BG: Record<string, string> = { export const PALETTE_CATEGORY_BG: Record<string, string> = {
trigger: 'bg-amber-100 dark:bg-amber-900/30 hover:bg-amber-200 dark:hover:bg-amber-900/50', trigger:
process: 'bg-blue-100 dark:bg-blue-900/30 hover:bg-blue-200 dark:hover:bg-blue-900/50', 'bg-amber-100 dark:bg-amber-900/30 hover:bg-amber-200 dark:hover:bg-amber-900/50',
control: 'bg-purple-100 dark:bg-purple-900/30 hover:bg-purple-200 dark:hover:bg-purple-900/50', process:
action: 'bg-green-100 dark:bg-green-900/30 hover:bg-green-200 dark:hover:bg-green-900/50', 'bg-blue-100 dark:bg-blue-900/30 hover:bg-blue-200 dark:hover:bg-blue-900/50',
integration: 'bg-pink-100 dark:bg-pink-900/30 hover:bg-pink-200 dark:hover:bg-pink-900/50', control:
'bg-purple-100 dark:bg-purple-900/30 hover:bg-purple-200 dark:hover:bg-purple-900/50',
action:
'bg-green-100 dark:bg-green-900/30 hover:bg-green-200 dark:hover:bg-green-900/50',
integration:
'bg-pink-100 dark:bg-pink-900/30 hover:bg-pink-200 dark:hover:bg-pink-900/50',
}; };
export const PALETTE_CATEGORY_BORDER: Record<string, string> = { export const PALETTE_CATEGORY_BORDER: Record<string, string> = {
@@ -224,7 +352,9 @@ export const CATEGORY_ICONS: Record<string, React.ElementType> = {
* Find the i18n keys for a node type, with fuzzy matching so both * Find the i18n keys for a node type, with fuzzy matching so both
* "trigger.message_trigger" and "message_trigger" work. * "trigger.message_trigger" and "message_trigger" work.
*/ */
export function findNodeI18nKeys(type: string): { labelKey: string; descriptionKey: string } | undefined { export function findNodeI18nKeys(
type: string,
): { labelKey: string; descriptionKey: string } | undefined {
let keys = NODE_TYPE_I18N_KEYS[type]; let keys = NODE_TYPE_I18N_KEYS[type];
if (keys) return keys; if (keys) return keys;
@@ -234,7 +364,13 @@ export function findNodeI18nKeys(type: string): { labelKey: string; descriptionK
if (parts.length > 1) { if (parts.length > 1) {
keys = NODE_TYPE_I18N_KEYS[type]; // already tried keys = NODE_TYPE_I18N_KEYS[type]; // already tried
} else { } else {
for (const cat of ['trigger', 'process', 'control', 'action', 'integration']) { for (const cat of [
'trigger',
'process',
'control',
'action',
'integration',
]) {
keys = NODE_TYPE_I18N_KEYS[`${cat}.${typeName}`]; keys = NODE_TYPE_I18N_KEYS[`${cat}.${typeName}`];
if (keys) return keys; if (keys) return keys;
} }
@@ -3,10 +3,7 @@ import type {
WorkflowPortDefinition, WorkflowPortDefinition,
} from '@/app/infra/entities/api'; } from '@/app/infra/entities/api';
import type { I18nObject } from '@/app/infra/entities/common'; import type { I18nObject } from '@/app/infra/entities/common';
import { import { getNodeConfig, type NodeConfigMeta } from './node-configs';
getNodeConfig,
type NodeConfigMeta,
} from './node-configs';
export const WORKFLOW_NODE_CATEGORIES = [ export const WORKFLOW_NODE_CATEGORIES = [
'trigger', 'trigger',
@@ -59,7 +56,9 @@ function normalizePort(
}; };
} }
function toBackendI18nObject(value?: I18nObject): Record<string, string> | undefined { function toBackendI18nObject(
value?: I18nObject,
): Record<string, string> | undefined {
if (!value) return undefined; if (!value) return undefined;
return { return {
@@ -103,7 +102,9 @@ function getLocalConfigVariants(type: string): string[] {
return [...variants]; return [...variants];
} }
export function getLocalNodeTypeMeta(type: string): WorkflowNodeTypeMetadata | null { export function getLocalNodeTypeMeta(
type: string,
): WorkflowNodeTypeMetadata | null {
let localConfig: NodeConfigMeta | undefined; let localConfig: NodeConfigMeta | undefined;
for (const variant of getLocalConfigVariants(type)) { for (const variant of getLocalConfigVariants(type)) {
@@ -138,30 +139,27 @@ export function normalizeWorkflowNodeTypeMeta(
const category = const category =
nodeType?.category || localMeta?.category || resolveNodeTypeCategory(type); nodeType?.category || localMeta?.category || resolveNodeTypeCategory(type);
const inputs = const inputs = nodeType?.inputs?.length
nodeType?.inputs?.length ? nodeType.inputs.map((input) =>
? nodeType.inputs.map((input) => normalizePort('workflows.nodeInputs', input),
normalizePort('workflows.nodeInputs', input), )
) : localMeta?.inputs?.length
: localMeta?.inputs?.length ? localMeta.inputs
? localMeta.inputs : [DEFAULT_INPUT_PORT];
: [DEFAULT_INPUT_PORT];
const outputs = const outputs = nodeType?.outputs?.length
nodeType?.outputs?.length ? nodeType.outputs.map((output) =>
? nodeType.outputs.map((output) => normalizePort('workflows.nodeOutputs', output),
normalizePort('workflows.nodeOutputs', output), )
) : localMeta?.outputs?.length
: localMeta?.outputs?.length ? localMeta.outputs
? localMeta.outputs : [DEFAULT_OUTPUT_PORT];
: [DEFAULT_OUTPUT_PORT];
const configSchema = const configSchema = nodeType?.config_schema?.length
nodeType?.config_schema?.length ? nodeType.config_schema
? nodeType.config_schema : localMeta?.config_schema?.length
: localMeta?.config_schema?.length ? localMeta.config_schema
? localMeta.config_schema : [];
: [];
return { return {
type, type,
@@ -6,7 +6,13 @@ import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Workflow } from '@/app/infra/entities/api'; import { Workflow } from '@/app/infra/entities/api';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Trash2, AlertTriangle } from 'lucide-react'; import { Trash2, AlertTriangle } from 'lucide-react';
import { import {
AlertDialog, AlertDialog,
@@ -101,7 +107,9 @@ export default function WorkflowFormComponent({
{/* Description */} {/* Description */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="workflow-description">{t('workflows.description')}</Label> <Label htmlFor="workflow-description">
{t('workflows.description')}
</Label>
<Textarea <Textarea
id="workflow-description" id="workflow-description"
value={description} value={description}
@@ -119,10 +127,7 @@ export default function WorkflowFormComponent({
{t('workflows.enabledDesc')} {t('workflows.enabledDesc')}
</p> </p>
</div> </div>
<Switch <Switch checked={isEnabled} onCheckedChange={setIsEnabled} />
checked={isEnabled}
onCheckedChange={setIsEnabled}
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -135,15 +140,21 @@ export default function WorkflowFormComponent({
<CardContent> <CardContent>
<div className="grid grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-2 gap-4 text-sm">
<div> <div>
<span className="text-muted-foreground">{t('workflows.uuid')}:</span> <span className="text-muted-foreground">
{t('workflows.uuid')}:
</span>
<span className="ml-2 font-mono">{workflow.uuid}</span> <span className="ml-2 font-mono">{workflow.uuid}</span>
</div> </div>
<div> <div>
<span className="text-muted-foreground">{t('workflows.version')}:</span> <span className="text-muted-foreground">
{t('workflows.version')}:
</span>
<span className="ml-2">{workflow.version || 1}</span> <span className="ml-2">{workflow.version || 1}</span>
</div> </div>
<div> <div>
<span className="text-muted-foreground">{t('workflows.createdAt')}:</span> <span className="text-muted-foreground">
{t('workflows.createdAt')}:
</span>
<span className="ml-2"> <span className="ml-2">
{workflow.created_at {workflow.created_at
? new Date(workflow.created_at).toLocaleString() ? new Date(workflow.created_at).toLocaleString()
@@ -151,7 +162,9 @@ export default function WorkflowFormComponent({
</span> </span>
</div> </div>
<div> <div>
<span className="text-muted-foreground">{t('workflows.updatedAt')}:</span> <span className="text-muted-foreground">
{t('workflows.updatedAt')}:
</span>
<span className="ml-2"> <span className="ml-2">
{workflow.updated_at {workflow.updated_at
? new Date(workflow.updated_at).toLocaleString() ? new Date(workflow.updated_at).toLocaleString()
@@ -181,7 +194,9 @@ export default function WorkflowFormComponent({
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>{t('workflows.deleteConfirm')}</AlertDialogTitle> <AlertDialogTitle>
{t('workflows.deleteConfirm')}
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{t('workflows.deleteConfirmDesc', { name: workflow.name })} {t('workflows.deleteConfirmDesc', { name: workflow.name })}
</AlertDialogDescription> </AlertDialogDescription>
@@ -1,5 +1,14 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { Node, Edge, Connection, addEdge, applyNodeChanges, applyEdgeChanges, NodeChange, EdgeChange } from '@xyflow/react'; import {
Node,
Edge,
Connection,
addEdge,
applyNodeChanges,
applyEdgeChanges,
NodeChange,
EdgeChange,
} from '@xyflow/react';
import { import {
Workflow, Workflow,
WorkflowNodeDefinition, WorkflowNodeDefinition,
@@ -64,28 +73,28 @@ export interface DebugContext {
interface WorkflowState { interface WorkflowState {
// Current workflow being edited // Current workflow being edited
currentWorkflow: Workflow | null; currentWorkflow: Workflow | null;
// React Flow nodes and edges // React Flow nodes and edges
nodes: WorkflowNode[]; nodes: WorkflowNode[];
edges: WorkflowEdge[]; edges: WorkflowEdge[];
// Node type metadata from backend // Node type metadata from backend
nodeTypes: WorkflowNodeTypeMetadata[]; nodeTypes: WorkflowNodeTypeMetadata[];
nodeCategories: WorkflowNodeCategory[]; nodeCategories: WorkflowNodeCategory[];
// Selection state // Selection state
selectedNodeId: string | null; selectedNodeId: string | null;
selectedEdgeId: string | null; selectedEdgeId: string | null;
// UI state // UI state
isDirty: boolean; isDirty: boolean;
isSaving: boolean; isSaving: boolean;
isLoading: boolean; isLoading: boolean;
// Undo/Redo history // Undo/Redo history
history: { nodes: WorkflowNode[]; edges: WorkflowEdge[] }[]; history: { nodes: WorkflowNode[]; edges: WorkflowEdge[] }[];
historyIndex: number; historyIndex: number;
// Debug state // Debug state
debugMode: boolean; debugMode: boolean;
debugState: DebugState; debugState: DebugState;
@@ -96,55 +105,67 @@ interface WorkflowState {
debugLogs: DebugLog[]; debugLogs: DebugLog[];
debugContext: DebugContext; debugContext: DebugContext;
watchedVariables: string[]; watchedVariables: string[];
// Actions // Actions
setCurrentWorkflow: (workflow: Workflow | null) => void; setCurrentWorkflow: (workflow: Workflow | null) => void;
setNodes: (nodes: WorkflowNode[]) => void; setNodes: (nodes: WorkflowNode[]) => void;
setEdges: (edges: WorkflowEdge[]) => void; setEdges: (edges: WorkflowEdge[]) => void;
setNodeTypes: (types: WorkflowNodeTypeMetadata[], categories: WorkflowNodeCategory[]) => void; setNodeTypes: (
types: WorkflowNodeTypeMetadata[],
categories: WorkflowNodeCategory[],
) => void;
// Node operations // Node operations
onNodesChange: (changes: NodeChange<WorkflowNode>[]) => void; onNodesChange: (changes: NodeChange<WorkflowNode>[]) => void;
onEdgesChange: (changes: EdgeChange<WorkflowEdge>[]) => void; onEdgesChange: (changes: EdgeChange<WorkflowEdge>[]) => void;
onConnect: (connection: Connection) => void; onConnect: (connection: Connection) => void;
addNode: (type: string, position: { x: number; y: number }) => void; addNode: (type: string, position: { x: number; y: number }) => void;
updateNodeConfig: (nodeId: string, config: Record<string, unknown>) => void; updateNodeConfig: (nodeId: string, config: Record<string, unknown>) => void;
updateNodeLabel: (nodeId: string, label: string) => void; updateNodeLabel: (nodeId: string, label: string) => void;
deleteNode: (nodeId: string) => void; deleteNode: (nodeId: string) => void;
// Edge operations // Edge operations
deleteEdge: (edgeId: string) => void; deleteEdge: (edgeId: string) => void;
updateEdgeCondition: (edgeId: string, condition: string) => void; updateEdgeCondition: (edgeId: string, condition: string) => void;
// Selection // Selection
selectNode: (nodeId: string | null) => void; selectNode: (nodeId: string | null) => void;
selectEdge: (edgeId: string | null) => void; selectEdge: (edgeId: string | null) => void;
clearSelection: () => void; clearSelection: () => void;
// State management // State management
setDirty: (dirty: boolean) => void; setDirty: (dirty: boolean) => void;
setSaving: (saving: boolean) => void; setSaving: (saving: boolean) => void;
setLoading: (loading: boolean) => void; setLoading: (loading: boolean) => void;
// Undo/Redo // Undo/Redo
undo: () => void; undo: () => void;
redo: () => void; redo: () => void;
pushHistory: () => void; pushHistory: () => void;
// Convert to/from API format // Convert to/from API format
toWorkflowDefinition: () => { nodes: WorkflowNodeDefinition[]; edges: WorkflowEdgeDefinition[] }; toWorkflowDefinition: () => {
fromWorkflowDefinition: (nodes: WorkflowNodeDefinition[], edges: WorkflowEdgeDefinition[]) => void; nodes: WorkflowNodeDefinition[];
edges: WorkflowEdgeDefinition[];
};
fromWorkflowDefinition: (
nodes: WorkflowNodeDefinition[],
edges: WorkflowEdgeDefinition[],
) => void;
// Reset // Reset
reset: () => void; reset: () => void;
// Debug actions // Debug actions
setDebugMode: (enabled: boolean) => void; setDebugMode: (enabled: boolean) => void;
setDebugState: (state: DebugState) => void; setDebugState: (state: DebugState) => void;
setDebugExecutionId: (executionId: string | null) => void; setDebugExecutionId: (executionId: string | null) => void;
setCurrentNodeId: (nodeId: string | null) => void; setCurrentNodeId: (nodeId: string | null) => void;
updateNodeExecutionResult: (nodeId: string, result: Partial<NodeExecutionResult>) => void; updateNodeExecutionResult: (
nodeId: string,
result: Partial<NodeExecutionResult>,
) => void;
clearNodeExecutionResults: () => void; clearNodeExecutionResults: () => void;
toggleBreakpoint: (nodeId: string) => void; toggleBreakpoint: (nodeId: string) => void;
clearBreakpoints: () => void; clearBreakpoints: () => void;
@@ -159,7 +180,10 @@ interface WorkflowState {
} }
const generateUuidLikeId = () => { const generateUuidLikeId = () => {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { if (
typeof crypto !== 'undefined' &&
typeof crypto.randomUUID === 'function'
) {
return crypto.randomUUID(); return crypto.randomUUID();
} }
@@ -195,7 +219,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
isLoading: false, isLoading: false,
history: [], history: [],
historyIndex: -1, historyIndex: -1,
// Debug initial state // Debug initial state
debugMode: false, debugMode: false,
debugState: 'idle', debugState: 'idle',
@@ -206,13 +230,14 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
debugLogs: [], debugLogs: [],
debugContext: { ...defaultDebugContext }, debugContext: { ...defaultDebugContext },
watchedVariables: [], watchedVariables: [],
// Setters // Setters
setCurrentWorkflow: (workflow) => set({ currentWorkflow: workflow }), setCurrentWorkflow: (workflow) => set({ currentWorkflow: workflow }),
setNodes: (nodes) => set({ nodes, isDirty: true }), setNodes: (nodes) => set({ nodes, isDirty: true }),
setEdges: (edges) => set({ edges, isDirty: true }), setEdges: (edges) => set({ edges, isDirty: true }),
setNodeTypes: (types, categories) => set({ nodeTypes: types, nodeCategories: categories }), setNodeTypes: (types, categories) =>
set({ nodeTypes: types, nodeCategories: categories }),
// Node change handlers // Node change handlers
onNodesChange: (changes) => { onNodesChange: (changes) => {
set((state) => ({ set((state) => ({
@@ -220,28 +245,28 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
isDirty: true, isDirty: true,
})); }));
}, },
onEdgesChange: (changes) => { onEdgesChange: (changes) => {
set((state) => ({ set((state) => ({
edges: applyEdgeChanges(changes, state.edges) as WorkflowEdge[], edges: applyEdgeChanges(changes, state.edges) as WorkflowEdge[],
isDirty: true, isDirty: true,
})); }));
}, },
onConnect: (connection) => { onConnect: (connection) => {
const newEdge: WorkflowEdge = { const newEdge: WorkflowEdge = {
...connection, ...connection,
id: generateEdgeId(), id: generateEdgeId(),
type: 'smoothstep', type: 'smoothstep',
} as WorkflowEdge; } as WorkflowEdge;
set((state) => ({ set((state) => ({
edges: addEdge(newEdge, state.edges) as WorkflowEdge[], edges: addEdge(newEdge, state.edges) as WorkflowEdge[],
isDirty: true, isDirty: true,
})); }));
get().pushHistory(); get().pushHistory();
}, },
// Add new node // Add new node
addNode: (type, position) => { addNode: (type, position) => {
const { nodeTypes } = get(); const { nodeTypes } = get();
@@ -277,83 +302,81 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
})), })),
}, },
}; };
set((state) => ({ set((state) => ({
nodes: [...state.nodes, newNode], nodes: [...state.nodes, newNode],
isDirty: true, isDirty: true,
})); }));
get().pushHistory(); get().pushHistory();
}, },
// Update node config // Update node config
updateNodeConfig: (nodeId, config) => { updateNodeConfig: (nodeId, config) => {
set((state) => ({ set((state) => ({
nodes: state.nodes.map((node) => nodes: state.nodes.map((node) =>
node.id === nodeId node.id === nodeId ? { ...node, data: { ...node.data, config } } : node,
? { ...node, data: { ...node.data, config } }
: node
), ),
isDirty: true, isDirty: true,
})); }));
}, },
// Update node label // Update node label
updateNodeLabel: (nodeId, label) => { updateNodeLabel: (nodeId, label) => {
set((state) => ({ set((state) => ({
nodes: state.nodes.map((node) => nodes: state.nodes.map((node) =>
node.id === nodeId node.id === nodeId ? { ...node, data: { ...node.data, label } } : node,
? { ...node, data: { ...node.data, label } }
: node
), ),
isDirty: true, isDirty: true,
})); }));
}, },
// Delete node // Delete node
deleteNode: (nodeId) => { deleteNode: (nodeId) => {
set((state) => ({ set((state) => ({
nodes: state.nodes.filter((node) => node.id !== nodeId), nodes: state.nodes.filter((node) => node.id !== nodeId),
edges: state.edges.filter( edges: state.edges.filter(
(edge) => edge.source !== nodeId && edge.target !== nodeId (edge) => edge.source !== nodeId && edge.target !== nodeId,
), ),
selectedNodeId: state.selectedNodeId === nodeId ? null : state.selectedNodeId, selectedNodeId:
state.selectedNodeId === nodeId ? null : state.selectedNodeId,
isDirty: true, isDirty: true,
})); }));
get().pushHistory(); get().pushHistory();
}, },
// Delete edge // Delete edge
deleteEdge: (edgeId) => { deleteEdge: (edgeId) => {
set((state) => ({ set((state) => ({
edges: state.edges.filter((edge) => edge.id !== edgeId), edges: state.edges.filter((edge) => edge.id !== edgeId),
selectedEdgeId: state.selectedEdgeId === edgeId ? null : state.selectedEdgeId, selectedEdgeId:
state.selectedEdgeId === edgeId ? null : state.selectedEdgeId,
isDirty: true, isDirty: true,
})); }));
get().pushHistory(); get().pushHistory();
}, },
// Update edge condition // Update edge condition
updateEdgeCondition: (edgeId, condition) => { updateEdgeCondition: (edgeId, condition) => {
set((state) => ({ set((state) => ({
edges: state.edges.map((edge) => edges: state.edges.map((edge) =>
edge.id === edgeId edge.id === edgeId
? { ...edge, data: { ...edge.data, condition } } ? { ...edge, data: { ...edge.data, condition } }
: edge : edge,
), ),
isDirty: true, isDirty: true,
})); }));
}, },
// Selection // Selection
selectNode: (nodeId) => set({ selectedNodeId: nodeId, selectedEdgeId: null }), selectNode: (nodeId) => set({ selectedNodeId: nodeId, selectedEdgeId: null }),
selectEdge: (edgeId) => set({ selectedEdgeId: edgeId, selectedNodeId: null }), selectEdge: (edgeId) => set({ selectedEdgeId: edgeId, selectedNodeId: null }),
clearSelection: () => set({ selectedNodeId: null, selectedEdgeId: null }), clearSelection: () => set({ selectedNodeId: null, selectedEdgeId: null }),
// State management // State management
setDirty: (dirty) => set({ isDirty: dirty }), setDirty: (dirty) => set({ isDirty: dirty }),
setSaving: (saving) => set({ isSaving: saving }), setSaving: (saving) => set({ isSaving: saving }),
setLoading: (loading) => set({ isLoading: loading }), setLoading: (loading) => set({ isLoading: loading }),
// Undo // Undo
undo: () => { undo: () => {
const { history, historyIndex } = get(); const { history, historyIndex } = get();
@@ -363,7 +386,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
set({ nodes, edges, historyIndex: newIndex, isDirty: true }); set({ nodes, edges, historyIndex: newIndex, isDirty: true });
} }
}, },
// Redo // Redo
redo: () => { redo: () => {
const { history, historyIndex } = get(); const { history, historyIndex } = get();
@@ -373,25 +396,25 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
set({ nodes, edges, historyIndex: newIndex, isDirty: true }); set({ nodes, edges, historyIndex: newIndex, isDirty: true });
} }
}, },
// Push current state to history // Push current state to history
pushHistory: () => { pushHistory: () => {
const { nodes, edges, history, historyIndex } = get(); const { nodes, edges, history, historyIndex } = get();
const newHistory = history.slice(0, historyIndex + 1); const newHistory = history.slice(0, historyIndex + 1);
newHistory.push({ nodes: [...nodes], edges: [...edges] }); newHistory.push({ nodes: [...nodes], edges: [...edges] });
// Keep history limited to 50 entries // Keep history limited to 50 entries
if (newHistory.length > 50) { if (newHistory.length > 50) {
newHistory.shift(); newHistory.shift();
} }
set({ history: newHistory, historyIndex: newHistory.length - 1 }); set({ history: newHistory, historyIndex: newHistory.length - 1 });
}, },
// Convert to API format // Convert to API format
toWorkflowDefinition: () => { toWorkflowDefinition: () => {
const { nodes, edges } = get(); const { nodes, edges } = get();
const workflowNodes: WorkflowNodeDefinition[] = nodes.map((node) => ({ const workflowNodes: WorkflowNodeDefinition[] = nodes.map((node) => ({
id: node.id, id: node.id,
type: node.data.type, type: node.data.type,
@@ -401,7 +424,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
inputs: node.data.inputs, inputs: node.data.inputs,
outputs: node.data.outputs, outputs: node.data.outputs,
})); }));
const workflowEdges: WorkflowEdgeDefinition[] = edges.map((edge) => ({ const workflowEdges: WorkflowEdgeDefinition[] = edges.map((edge) => ({
id: edge.id, id: edge.id,
source: edge.source, source: edge.source,
@@ -411,10 +434,10 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
label: edge.data?.label, label: edge.data?.label,
condition: edge.data?.condition, condition: edge.data?.condition,
})); }));
return { nodes: workflowNodes, edges: workflowEdges }; return { nodes: workflowNodes, edges: workflowEdges };
}, },
// Load from API format // Load from API format
fromWorkflowDefinition: (apiNodes, apiEdges) => { fromWorkflowDefinition: (apiNodes, apiEdges) => {
const nodes: WorkflowNode[] = apiNodes.map((node) => ({ const nodes: WorkflowNode[] = apiNodes.map((node) => ({
@@ -429,7 +452,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
outputs: node.outputs, outputs: node.outputs,
}, },
})); }));
const edges: WorkflowEdge[] = apiEdges.map((edge) => ({ const edges: WorkflowEdge[] = apiEdges.map((edge) => ({
id: edge.id, id: edge.id,
source: edge.source, source: edge.source,
@@ -442,58 +465,62 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
condition: edge.condition, condition: edge.condition,
}, },
})); }));
set({ nodes, edges, isDirty: false }); set({ nodes, edges, isDirty: false });
get().pushHistory(); get().pushHistory();
}, },
// Reset store // Reset store
reset: () => set({ reset: () =>
currentWorkflow: null, set({
nodes: [], currentWorkflow: null,
edges: [], nodes: [],
selectedNodeId: null, edges: [],
selectedEdgeId: null, selectedNodeId: null,
isDirty: false, selectedEdgeId: null,
isSaving: false, isDirty: false,
isLoading: false, isSaving: false,
history: [], isLoading: false,
historyIndex: -1, history: [],
// Reset debug state historyIndex: -1,
debugMode: false, // Reset debug state
debugState: 'idle', debugMode: false,
debugExecutionId: null, debugState: 'idle',
currentNodeId: null, debugExecutionId: null,
nodeExecutionResults: {}, currentNodeId: null,
breakpoints: {} as Record<string, boolean>, nodeExecutionResults: {},
debugLogs: [], breakpoints: {} as Record<string, boolean>,
debugContext: { ...defaultDebugContext }, debugLogs: [],
watchedVariables: [], debugContext: { ...defaultDebugContext },
}), watchedVariables: [],
}),
// Debug actions // Debug actions
setDebugMode: (enabled) => set({ debugMode: enabled }), setDebugMode: (enabled) => set({ debugMode: enabled }),
setDebugState: (state) => set({ debugState: state }), setDebugState: (state) => set({ debugState: state }),
setDebugExecutionId: (executionId) => set({ debugExecutionId: executionId }), setDebugExecutionId: (executionId) => set({ debugExecutionId: executionId }),
setCurrentNodeId: (nodeId) => set({ currentNodeId: nodeId }), setCurrentNodeId: (nodeId) => set({ currentNodeId: nodeId }),
updateNodeExecutionResult: (nodeId, result) => { updateNodeExecutionResult: (nodeId, result) => {
set((state) => ({ set((state) => ({
nodeExecutionResults: { nodeExecutionResults: {
...state.nodeExecutionResults, ...state.nodeExecutionResults,
[nodeId]: { [nodeId]: {
...(state.nodeExecutionResults[nodeId] || { nodeId, status: 'pending' }), ...(state.nodeExecutionResults[nodeId] || {
nodeId,
status: 'pending',
}),
...result, ...result,
}, },
}, },
})); }));
}, },
clearNodeExecutionResults: () => set({ nodeExecutionResults: {} }), clearNodeExecutionResults: () => set({ nodeExecutionResults: {} }),
toggleBreakpoint: (nodeId) => { toggleBreakpoint: (nodeId) => {
set((state) => { set((state) => {
const newBreakpoints = { ...state.breakpoints }; const newBreakpoints = { ...state.breakpoints };
@@ -505,9 +532,9 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
return { breakpoints: newBreakpoints }; return { breakpoints: newBreakpoints };
}); });
}, },
clearBreakpoints: () => set({ breakpoints: {} as Record<string, boolean> }), clearBreakpoints: () => set({ breakpoints: {} as Record<string, boolean> }),
addDebugLog: (log) => { addDebugLog: (log) => {
set((state) => ({ set((state) => ({
debugLogs: [ debugLogs: [
@@ -520,27 +547,28 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
].slice(-500), // Keep only last 500 logs ].slice(-500), // Keep only last 500 logs
})); }));
}, },
clearDebugLogs: () => set({ debugLogs: [] }), clearDebugLogs: () => set({ debugLogs: [] }),
setDebugContext: (context) => { setDebugContext: (context) => {
set((state) => ({ set((state) => ({
debugContext: { ...state.debugContext, ...context }, debugContext: { ...state.debugContext, ...context },
})); }));
}, },
resetDebugContext: () => set({ resetDebugContext: () =>
debugContext: { set({
messageContent: '', debugContext: {
senderId: `user_${Date.now().toString(36)}`, messageContent: '',
senderName: '', senderId: `user_${Date.now().toString(36)}`,
platform: '', senderName: '',
conversationId: `session_${Date.now().toString(36)}`, platform: '',
isGroup: false, conversationId: `session_${Date.now().toString(36)}`,
customVariables: {}, isGroup: false,
}, customVariables: {},
}), },
}),
addWatchedVariable: (variable) => { addWatchedVariable: (variable) => {
set((state) => ({ set((state) => ({
watchedVariables: state.watchedVariables.includes(variable) watchedVariables: state.watchedVariables.includes(variable)
@@ -548,20 +576,21 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
: [...state.watchedVariables, variable], : [...state.watchedVariables, variable],
})); }));
}, },
removeWatchedVariable: (variable) => { removeWatchedVariable: (variable) => {
set((state) => ({ set((state) => ({
watchedVariables: state.watchedVariables.filter((v) => v !== variable), watchedVariables: state.watchedVariables.filter((v) => v !== variable),
})); }));
}, },
clearWatchedVariables: () => set({ watchedVariables: [] }), clearWatchedVariables: () => set({ watchedVariables: [] }),
resetDebugState: () => set({ resetDebugState: () =>
debugState: 'idle', set({
debugExecutionId: null, debugState: 'idle',
currentNodeId: null, debugExecutionId: null,
nodeExecutionResults: {}, currentNodeId: null,
debugLogs: [], nodeExecutionResults: {},
}), debugLogs: [],
}),
})); }));
+12 -2
View File
@@ -82,7 +82,12 @@ export interface NodeTypeMetadata {
} }
// 节点类别 // 节点类别
export type NodeCategory = 'trigger' | 'process' | 'control' | 'action' | 'integration'; export type NodeCategory =
| 'trigger'
| 'process'
| 'control'
| 'action'
| 'integration';
// 节点类别信息 // 节点类别信息
export interface NodeCategoryInfo { export interface NodeCategoryInfo {
@@ -93,7 +98,12 @@ export interface NodeCategoryInfo {
} }
// 工作流执行状态 // 工作流执行状态
export type ExecutionStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; export type ExecutionStatus =
| 'pending'
| 'running'
| 'completed'
| 'failed'
| 'cancelled';
// 工作流执行记录 // 工作流执行记录
export interface WorkflowExecution { export interface WorkflowExecution {
+8 -2
View File
@@ -1290,11 +1290,17 @@ export class BackendClient extends BaseHttpClient {
return this.post(`/api/v1/workflows/${uuid}/debug/start`, options); return this.post(`/api/v1/workflows/${uuid}/debug/start`, options);
} }
public pauseWorkflowDebug(uuid: string, executionId: string): Promise<object> { public pauseWorkflowDebug(
uuid: string,
executionId: string,
): Promise<object> {
return this.post(`/api/v1/workflows/${uuid}/debug/${executionId}/pause`); return this.post(`/api/v1/workflows/${uuid}/debug/${executionId}/pause`);
} }
public resumeWorkflowDebug(uuid: string, executionId: string): Promise<object> { public resumeWorkflowDebug(
uuid: string,
executionId: string,
): Promise<object> {
return this.post(`/api/v1/workflows/${uuid}/debug/${executionId}/resume`); return this.post(`/api/v1/workflows/${uuid}/debug/${executionId}/resume`);
} }
+3 -1
View File
@@ -18,7 +18,9 @@ export default function I18nProvider({ children }: I18nProviderProps) {
// return i18nLabel.en_US; // return i18nLabel.en_US;
// } // }
export const extractI18nObject = (i18nObject: I18nObject | undefined | null): string => { export const extractI18nObject = (
i18nObject: I18nObject | undefined | null,
): string => {
// 根据当前语言返回对应的值, fallback优先级:en_US、zh_Hans、zh_Hant、ja_JP // 根据当前语言返回对应的值, fallback优先级:en_US、zh_Hans、zh_Hant、ja_JP
if (!i18nObject || typeof i18nObject !== 'object') { if (!i18nObject || typeof i18nObject !== 'object') {
return ''; return '';
+38 -19
View File
@@ -380,14 +380,17 @@ const enUS = {
sessionTypeGroup: 'Group Chat', sessionTypeGroup: 'Group Chat',
// Unified binding (replacing routing rules) // Unified binding (replacing routing rules)
bindTarget: 'Bind Target', bindTarget: 'Bind Target',
bindTargetDescription: 'Select the Pipeline or Workflow to process messages for this bot', bindTargetDescription:
'Select the Pipeline or Workflow to process messages for this bot',
bindingType: 'Binding Type', bindingType: 'Binding Type',
selectBinding: 'Select binding target', selectBinding: 'Select binding target',
selectWorkflow: 'Select Workflow', selectWorkflow: 'Select Workflow',
noPipelinesFound: 'No pipelines available', noPipelinesFound: 'No pipelines available',
noWorkflowsFound: 'No workflows available', noWorkflowsFound: 'No workflows available',
pipelineBindingHelp: 'Pipeline is the traditional message processing method using predefined stages.', pipelineBindingHelp:
workflowBindingHelp: 'Workflow provides visual node orchestration for more flexible message processing.', 'Pipeline is the traditional message processing method using predefined stages.',
workflowBindingHelp:
'Workflow provides visual node orchestration for more flexible message processing.',
adapterConfigDescription: 'Configure the selected platform adapter', adapterConfigDescription: 'Configure the selected platform adapter',
dangerZone: 'Danger Zone', dangerZone: 'Danger Zone',
dangerZoneDescription: 'Irreversible and destructive actions', dangerZoneDescription: 'Irreversible and destructive actions',
@@ -1355,7 +1358,8 @@ const enUS = {
}, },
workflows: { workflows: {
title: 'Workflows', title: 'Workflows',
description: 'Create and manage visual workflows for complex message processing logic', description:
'Create and manage visual workflows for complex message processing logic',
createWorkflow: 'Create Workflow', createWorkflow: 'Create Workflow',
selectFromSidebar: 'Select a workflow from the sidebar', selectFromSidebar: 'Select a workflow from the sidebar',
editWorkflow: 'Edit Workflow', editWorkflow: 'Edit Workflow',
@@ -1395,10 +1399,12 @@ const enUS = {
dangerZoneDesc: 'Irreversible operations', dangerZoneDesc: 'Irreversible operations',
dangerZoneDescription: 'Irreversible operations', dangerZoneDescription: 'Irreversible operations',
deleteWorkflowAction: 'Delete this workflow', deleteWorkflowAction: 'Delete this workflow',
deleteWorkflowHint: 'Once deleted, all associated configurations will be permanently removed and cannot be recovered.', deleteWorkflowHint:
'Once deleted, all associated configurations will be permanently removed and cannot be recovered.',
deleteWorkflow: 'Delete Workflow', deleteWorkflow: 'Delete Workflow',
deleteConfirm: 'Confirm Delete', deleteConfirm: 'Confirm Delete',
deleteConfirmDesc: 'Are you sure you want to delete workflow "{{name}}"? This action cannot be undone.', deleteConfirmDesc:
'Are you sure you want to delete workflow "{{name}}"? This action cannot be undone.',
// Form component // Form component
name: 'Name', name: 'Name',
namePlaceholder: 'Enter workflow name', namePlaceholder: 'Enter workflow name',
@@ -1461,13 +1467,16 @@ const enUS = {
dragToAdd: 'Drag nodes to add to canvas', dragToAdd: 'Drag nodes to add to canvas',
// Property panel // Property panel
selectNodeOrEdge: 'Select a node or edge', selectNodeOrEdge: 'Select a node or edge',
selectNodeOrEdgeHint: 'Click on a node or edge in the canvas to view and edit its properties', selectNodeOrEdgeHint:
'Click on a node or edge in the canvas to view and edit its properties',
edgeProperties: 'Edge Properties', edgeProperties: 'Edge Properties',
nodeProperties: 'Node Properties', nodeProperties: 'Node Properties',
condition: 'Condition', condition: 'Condition',
hasCondition: 'Set', hasCondition: 'Set',
conditionPlaceholder: 'Enter condition expression, e.g. output.success == true', conditionPlaceholder:
conditionHelp: 'When condition is empty, this edge will always be executed. Use {{variable}} to reference context variables.', 'Enter condition expression, e.g. output.success == true',
conditionHelp:
'When condition is empty, this edge will always be executed. Use {{variable}} to reference context variables.',
deleteEdge: 'Delete Edge', deleteEdge: 'Delete Edge',
deleteEdgeConfirm: 'Confirm Delete Edge', deleteEdgeConfirm: 'Confirm Delete Edge',
deleteEdgeConfirmDesc: 'This edge will be permanently removed.', deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
@@ -1488,7 +1497,8 @@ const enUS = {
noConfigOptions: 'No configuration options for this node type', noConfigOptions: 'No configuration options for this node type',
deleteNode: 'Delete Node', deleteNode: 'Delete Node',
deleteNodeConfirm: 'Confirm Delete Node', deleteNodeConfirm: 'Confirm Delete Node',
deleteNodeConfirmDesc: 'This node and all its connections will be permanently removed.', deleteNodeConfirmDesc:
'This node and all its connections will be permanently removed.',
// Node inputs/outputs i18n (for port labels) // Node inputs/outputs i18n (for port labels)
nodeInputs: { nodeInputs: {
// Common inputs // Common inputs
@@ -1551,7 +1561,8 @@ const enUS = {
aiProcess: 'AI Processing', aiProcess: 'AI Processing',
aiProcessDescription: 'Process messages using AI models', aiProcessDescription: 'Process messages using AI models',
llmCall: 'LLM Call', llmCall: 'LLM Call',
llmCallDescription: 'Invoke large language model for conversation or generation', llmCallDescription:
'Invoke large language model for conversation or generation',
codeProcess: 'Code Processing', codeProcess: 'Code Processing',
codeProcessDescription: 'Execute custom code', codeProcessDescription: 'Execute custom code',
codeExecutor: 'Code Executor', codeExecutor: 'Code Executor',
@@ -1563,13 +1574,17 @@ const enUS = {
dataTransform: 'Data Transform', dataTransform: 'Data Transform',
dataTransformDescription: 'Transform data format', dataTransformDescription: 'Transform data format',
questionClassifier: 'Question Classifier', questionClassifier: 'Question Classifier',
questionClassifierDescription: 'Classify user questions into predefined categories using LLM', questionClassifierDescription:
'Classify user questions into predefined categories using LLM',
parameterExtractor: 'Parameter Extractor', parameterExtractor: 'Parameter Extractor',
parameterExtractorDescription: 'Extract structured parameters from text using LLM', parameterExtractorDescription:
'Extract structured parameters from text using LLM',
knowledgeRetrieval: 'Knowledge Retrieval', knowledgeRetrieval: 'Knowledge Retrieval',
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base', knowledgeRetrievalDescription:
'Retrieve relevant content from knowledge base',
textTemplate: 'Text Template', textTemplate: 'Text Template',
textTemplateDescription: 'Generate text using templates with variable interpolation', textTemplateDescription:
'Generate text using templates with variable interpolation',
jsonTransform: 'JSON Transform', jsonTransform: 'JSON Transform',
jsonTransformDescription: 'Transform JSON data using expressions', jsonTransformDescription: 'Transform JSON data using expressions',
dataAggregator: 'Data Aggregator', dataAggregator: 'Data Aggregator',
@@ -1597,13 +1612,15 @@ const enUS = {
merge: 'Merge', merge: 'Merge',
mergeDescription: 'Merge multiple branches', mergeDescription: 'Merge multiple branches',
variableAggregator: 'Variable Aggregator', variableAggregator: 'Variable Aggregator',
variableAggregatorDescription: 'Aggregate variable outputs from multiple branches', variableAggregatorDescription:
'Aggregate variable outputs from multiple branches',
action: 'Actions', action: 'Actions',
actionDescription: 'Action execution nodes', actionDescription: 'Action execution nodes',
sendMessage: 'Send Message', sendMessage: 'Send Message',
sendMessageDescription: 'Send message to platform', sendMessageDescription: 'Send message to platform',
replyMessage: 'Reply Message', replyMessage: 'Reply Message',
replyMessageDescription: 'Reply to the message that triggered the workflow', replyMessageDescription:
'Reply to the message that triggered the workflow',
storeData: 'Store Data', storeData: 'Store Data',
storeDataDescription: 'Store data to database', storeDataDescription: 'Store data to database',
callPipeline: 'Call Pipeline', callPipeline: 'Call Pipeline',
@@ -1611,7 +1628,8 @@ const enUS = {
setVariable: 'Set Variable', setVariable: 'Set Variable',
setVariableDescription: 'Set context variable', setVariableDescription: 'Set context variable',
openingStatement: 'Opening Statement', openingStatement: 'Opening Statement',
openingStatementDescription: 'Provide conversation opener and suggested questions', openingStatementDescription:
'Provide conversation opener and suggested questions',
end: 'End', end: 'End',
endDescription: 'Mark the end of workflow execution', endDescription: 'Mark the end of workflow execution',
log: 'Log', log: 'Log',
@@ -1657,7 +1675,8 @@ const enUS = {
title: 'Version History', title: 'Version History',
current: 'Current Version', current: 'Current Version',
rollback: 'Rollback to this version', rollback: 'Rollback to this version',
rollbackConfirm: 'Are you sure you want to rollback to this version? Current changes will be lost.', rollbackConfirm:
'Are you sure you want to rollback to this version? Current changes will be lost.',
rollbackSuccess: 'Rollback successful', rollbackSuccess: 'Rollback successful',
rollbackError: 'Failed to rollback: ', rollbackError: 'Failed to rollback: ',
}, },
+32 -16
View File
@@ -1373,7 +1373,8 @@ const esES = {
}, },
workflows: { workflows: {
title: 'Workflows', title: 'Workflows',
description: 'Create and manage visual workflows for complex message processing logic', description:
'Create and manage visual workflows for complex message processing logic',
createWorkflow: 'Create Workflow', createWorkflow: 'Create Workflow',
selectFromSidebar: 'Select a workflow from the sidebar', selectFromSidebar: 'Select a workflow from the sidebar',
editWorkflow: 'Edit Workflow', editWorkflow: 'Edit Workflow',
@@ -1413,10 +1414,12 @@ const esES = {
dangerZoneDesc: 'Irreversible operations', dangerZoneDesc: 'Irreversible operations',
dangerZoneDescription: 'Irreversible operations', dangerZoneDescription: 'Irreversible operations',
deleteWorkflowAction: 'Delete this workflow', deleteWorkflowAction: 'Delete this workflow',
deleteWorkflowHint: 'Once deleted, all associated configurations will be permanently removed and cannot be recovered.', deleteWorkflowHint:
'Once deleted, all associated configurations will be permanently removed and cannot be recovered.',
deleteWorkflow: 'Delete Workflow', deleteWorkflow: 'Delete Workflow',
deleteConfirm: 'Confirm Delete', deleteConfirm: 'Confirm Delete',
deleteConfirmDesc: 'Are you sure you want to delete workflow "{{name}}"? This action cannot be undone.', deleteConfirmDesc:
'Are you sure you want to delete workflow "{{name}}"? This action cannot be undone.',
// Form component // Form component
name: 'Name', name: 'Name',
namePlaceholder: 'Enter workflow name', namePlaceholder: 'Enter workflow name',
@@ -1479,13 +1482,16 @@ const esES = {
dragToAdd: 'Drag nodes to add to canvas', dragToAdd: 'Drag nodes to add to canvas',
// Property panel // Property panel
selectNodeOrEdge: 'Select a node or edge', selectNodeOrEdge: 'Select a node or edge',
selectNodeOrEdgeHint: 'Click on a node or edge in the canvas to view and edit its properties', selectNodeOrEdgeHint:
'Click on a node or edge in the canvas to view and edit its properties',
edgeProperties: 'Edge Properties', edgeProperties: 'Edge Properties',
nodeProperties: 'Node Properties', nodeProperties: 'Node Properties',
condition: 'Condition', condition: 'Condition',
hasCondition: 'Set', hasCondition: 'Set',
conditionPlaceholder: 'Enter condition expression, e.g. output.success == true', conditionPlaceholder:
conditionHelp: 'When condition is empty, this edge will always be executed. Use {{variable}} to reference context variables.', 'Enter condition expression, e.g. output.success == true',
conditionHelp:
'When condition is empty, this edge will always be executed. Use {{variable}} to reference context variables.',
deleteEdge: 'Delete Edge', deleteEdge: 'Delete Edge',
deleteEdgeConfirm: 'Confirm Delete Edge', deleteEdgeConfirm: 'Confirm Delete Edge',
deleteEdgeConfirmDesc: 'This edge will be permanently removed.', deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
@@ -1506,7 +1512,8 @@ const esES = {
noConfigOptions: 'No configuration options for this node type', noConfigOptions: 'No configuration options for this node type',
deleteNode: 'Delete Node', deleteNode: 'Delete Node',
deleteNodeConfirm: 'Confirm Delete Node', deleteNodeConfirm: 'Confirm Delete Node',
deleteNodeConfirmDesc: 'This node and all its connections will be permanently removed.', deleteNodeConfirmDesc:
'This node and all its connections will be permanently removed.',
// Node inputs/outputs i18n (for port labels) // Node inputs/outputs i18n (for port labels)
nodeInputs: { nodeInputs: {
// Common inputs // Common inputs
@@ -1569,7 +1576,8 @@ const esES = {
aiProcess: 'AI Processing', aiProcess: 'AI Processing',
aiProcessDescription: 'Process messages using AI models', aiProcessDescription: 'Process messages using AI models',
llmCall: 'LLM Call', llmCall: 'LLM Call',
llmCallDescription: 'Invoke large language model for conversation or generation', llmCallDescription:
'Invoke large language model for conversation or generation',
codeProcess: 'Code Processing', codeProcess: 'Code Processing',
codeProcessDescription: 'Execute custom code', codeProcessDescription: 'Execute custom code',
codeExecutor: 'Code Executor', codeExecutor: 'Code Executor',
@@ -1581,13 +1589,17 @@ const esES = {
dataTransform: 'Data Transform', dataTransform: 'Data Transform',
dataTransformDescription: 'Transform data format', dataTransformDescription: 'Transform data format',
questionClassifier: 'Question Classifier', questionClassifier: 'Question Classifier',
questionClassifierDescription: 'Classify user questions into predefined categories using LLM', questionClassifierDescription:
'Classify user questions into predefined categories using LLM',
parameterExtractor: 'Parameter Extractor', parameterExtractor: 'Parameter Extractor',
parameterExtractorDescription: 'Extract structured parameters from text using LLM', parameterExtractorDescription:
'Extract structured parameters from text using LLM',
knowledgeRetrieval: 'Knowledge Retrieval', knowledgeRetrieval: 'Knowledge Retrieval',
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base', knowledgeRetrievalDescription:
'Retrieve relevant content from knowledge base',
textTemplate: 'Text Template', textTemplate: 'Text Template',
textTemplateDescription: 'Generate text using templates with variable interpolation', textTemplateDescription:
'Generate text using templates with variable interpolation',
jsonTransform: 'JSON Transform', jsonTransform: 'JSON Transform',
jsonTransformDescription: 'Transform JSON data using expressions', jsonTransformDescription: 'Transform JSON data using expressions',
dataAggregator: 'Data Aggregator', dataAggregator: 'Data Aggregator',
@@ -1615,13 +1627,15 @@ const esES = {
merge: 'Merge', merge: 'Merge',
mergeDescription: 'Merge multiple branches', mergeDescription: 'Merge multiple branches',
variableAggregator: 'Variable Aggregator', variableAggregator: 'Variable Aggregator',
variableAggregatorDescription: 'Aggregate variable outputs from multiple branches', variableAggregatorDescription:
'Aggregate variable outputs from multiple branches',
action: 'Actions', action: 'Actions',
actionDescription: 'Action execution nodes', actionDescription: 'Action execution nodes',
sendMessage: 'Send Message', sendMessage: 'Send Message',
sendMessageDescription: 'Send message to platform', sendMessageDescription: 'Send message to platform',
replyMessage: 'Reply Message', replyMessage: 'Reply Message',
replyMessageDescription: 'Reply to the message that triggered the workflow', replyMessageDescription:
'Reply to the message that triggered the workflow',
storeData: 'Store Data', storeData: 'Store Data',
storeDataDescription: 'Store data to database', storeDataDescription: 'Store data to database',
callPipeline: 'Call Pipeline', callPipeline: 'Call Pipeline',
@@ -1629,7 +1643,8 @@ const esES = {
setVariable: 'Set Variable', setVariable: 'Set Variable',
setVariableDescription: 'Set context variable', setVariableDescription: 'Set context variable',
openingStatement: 'Opening Statement', openingStatement: 'Opening Statement',
openingStatementDescription: 'Provide conversation opener and suggested questions', openingStatementDescription:
'Provide conversation opener and suggested questions',
end: 'End', end: 'End',
endDescription: 'Mark the end of workflow execution', endDescription: 'Mark the end of workflow execution',
log: 'Log', log: 'Log',
@@ -1675,7 +1690,8 @@ const esES = {
title: 'Version History', title: 'Version History',
current: 'Current Version', current: 'Current Version',
rollback: 'Rollback to this version', rollback: 'Rollback to this version',
rollbackConfirm: 'Are you sure you want to rollback to this version? Current changes will be lost.', rollbackConfirm:
'Are you sure you want to rollback to this version? Current changes will be lost.',
rollbackSuccess: 'Rollback successful', rollbackSuccess: 'Rollback successful',
rollbackError: 'Failed to rollback: ', rollbackError: 'Failed to rollback: ',
}, },
+34 -17
View File
@@ -1341,7 +1341,8 @@
}, },
workflows: { workflows: {
title: 'Workflows', title: 'Workflows',
description: 'Create and manage visual workflows for complex message processing logic', description:
'Create and manage visual workflows for complex message processing logic',
createWorkflow: 'Create Workflow', createWorkflow: 'Create Workflow',
selectFromSidebar: 'Select a workflow from the sidebar', selectFromSidebar: 'Select a workflow from the sidebar',
editWorkflow: 'Edit Workflow', editWorkflow: 'Edit Workflow',
@@ -1381,10 +1382,12 @@
dangerZoneDesc: 'Irreversible operations', dangerZoneDesc: 'Irreversible operations',
dangerZoneDescription: 'Irreversible operations', dangerZoneDescription: 'Irreversible operations',
deleteWorkflowAction: 'Delete this workflow', deleteWorkflowAction: 'Delete this workflow',
deleteWorkflowHint: 'Once deleted, all associated configurations will be permanently removed and cannot be recovered.', deleteWorkflowHint:
'Once deleted, all associated configurations will be permanently removed and cannot be recovered.',
deleteWorkflow: 'Delete Workflow', deleteWorkflow: 'Delete Workflow',
deleteConfirm: 'Confirm Delete', deleteConfirm: 'Confirm Delete',
deleteConfirmDesc: 'Are you sure you want to delete workflow "{{name}}"? This action cannot be undone.', deleteConfirmDesc:
'Are you sure you want to delete workflow "{{name}}"? This action cannot be undone.',
// Form component // Form component
name: 'Name', name: 'Name',
namePlaceholder: 'Enter workflow name', namePlaceholder: 'Enter workflow name',
@@ -1447,13 +1450,16 @@
dragToAdd: 'Drag nodes to add to canvas', dragToAdd: 'Drag nodes to add to canvas',
// Property panel // Property panel
selectNodeOrEdge: 'Select a node or edge', selectNodeOrEdge: 'Select a node or edge',
selectNodeOrEdgeHint: 'Click on a node or edge in the canvas to view and edit its properties', selectNodeOrEdgeHint:
'Click on a node or edge in the canvas to view and edit its properties',
edgeProperties: 'Edge Properties', edgeProperties: 'Edge Properties',
nodeProperties: 'Node Properties', nodeProperties: 'Node Properties',
condition: 'Condition', condition: 'Condition',
hasCondition: 'Set', hasCondition: 'Set',
conditionPlaceholder: 'Enter condition expression, e.g. output.success == true', conditionPlaceholder:
conditionHelp: 'When condition is empty, this edge will always be executed. Use {{variable}} to reference context variables.', 'Enter condition expression, e.g. output.success == true',
conditionHelp:
'When condition is empty, this edge will always be executed. Use {{variable}} to reference context variables.',
deleteEdge: 'Delete Edge', deleteEdge: 'Delete Edge',
deleteEdgeConfirm: 'Confirm Delete Edge', deleteEdgeConfirm: 'Confirm Delete Edge',
deleteEdgeConfirmDesc: 'This edge will be permanently removed.', deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
@@ -1474,7 +1480,8 @@
noConfigOptions: 'No configuration options for this node type', noConfigOptions: 'No configuration options for this node type',
deleteNode: 'Delete Node', deleteNode: 'Delete Node',
deleteNodeConfirm: 'Confirm Delete Node', deleteNodeConfirm: 'Confirm Delete Node',
deleteNodeConfirmDesc: 'This node and all its connections will be permanently removed.', deleteNodeConfirmDesc:
'This node and all its connections will be permanently removed.',
// Node inputs/outputs i18n (for port labels) // Node inputs/outputs i18n (for port labels)
nodeInputs: { nodeInputs: {
// Common inputs // Common inputs
@@ -1537,7 +1544,8 @@
aiProcess: 'AI Processing', aiProcess: 'AI Processing',
aiProcessDescription: 'Process messages using AI models', aiProcessDescription: 'Process messages using AI models',
llmCall: 'LLM Call', llmCall: 'LLM Call',
llmCallDescription: 'Invoke large language model for conversation or generation', llmCallDescription:
'Invoke large language model for conversation or generation',
codeProcess: 'Code Processing', codeProcess: 'Code Processing',
codeProcessDescription: 'Execute custom code', codeProcessDescription: 'Execute custom code',
codeExecutor: 'Code Executor', codeExecutor: 'Code Executor',
@@ -1549,13 +1557,17 @@
dataTransform: 'Data Transform', dataTransform: 'Data Transform',
dataTransformDescription: 'Transform data format', dataTransformDescription: 'Transform data format',
questionClassifier: 'Question Classifier', questionClassifier: 'Question Classifier',
questionClassifierDescription: 'Classify user questions into predefined categories using LLM', questionClassifierDescription:
'Classify user questions into predefined categories using LLM',
parameterExtractor: 'Parameter Extractor', parameterExtractor: 'Parameter Extractor',
parameterExtractorDescription: 'Extract structured parameters from text using LLM', parameterExtractorDescription:
'Extract structured parameters from text using LLM',
knowledgeRetrieval: 'Knowledge Retrieval', knowledgeRetrieval: 'Knowledge Retrieval',
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base', knowledgeRetrievalDescription:
'Retrieve relevant content from knowledge base',
textTemplate: 'Text Template', textTemplate: 'Text Template',
textTemplateDescription: 'Generate text using templates with variable interpolation', textTemplateDescription:
'Generate text using templates with variable interpolation',
jsonTransform: 'JSON Transform', jsonTransform: 'JSON Transform',
jsonTransformDescription: 'Transform JSON data using expressions', jsonTransformDescription: 'Transform JSON data using expressions',
dataAggregator: 'Data Aggregator', dataAggregator: 'Data Aggregator',
@@ -1583,13 +1595,15 @@
merge: 'Merge', merge: 'Merge',
mergeDescription: 'Merge multiple branches', mergeDescription: 'Merge multiple branches',
variableAggregator: 'Variable Aggregator', variableAggregator: 'Variable Aggregator',
variableAggregatorDescription: 'Aggregate variable outputs from multiple branches', variableAggregatorDescription:
'Aggregate variable outputs from multiple branches',
action: 'Actions', action: 'Actions',
actionDescription: 'Action execution nodes', actionDescription: 'Action execution nodes',
sendMessage: 'Send Message', sendMessage: 'Send Message',
sendMessageDescription: 'Send message to platform', sendMessageDescription: 'Send message to platform',
replyMessage: 'Reply Message', replyMessage: 'Reply Message',
replyMessageDescription: 'Reply to the message that triggered the workflow', replyMessageDescription:
'Reply to the message that triggered the workflow',
storeData: 'Store Data', storeData: 'Store Data',
storeDataDescription: 'Store data to database', storeDataDescription: 'Store data to database',
callPipeline: 'Call Pipeline', callPipeline: 'Call Pipeline',
@@ -1597,7 +1611,8 @@
setVariable: 'Set Variable', setVariable: 'Set Variable',
setVariableDescription: 'Set context variable', setVariableDescription: 'Set context variable',
openingStatement: 'Opening Statement', openingStatement: 'Opening Statement',
openingStatementDescription: 'Provide conversation opener and suggested questions', openingStatementDescription:
'Provide conversation opener and suggested questions',
end: 'End', end: 'End',
endDescription: 'Mark the end of workflow execution', endDescription: 'Mark the end of workflow execution',
log: 'Log', log: 'Log',
@@ -1643,7 +1658,8 @@
title: 'Version History', title: 'Version History',
current: 'Current Version', current: 'Current Version',
rollback: 'Rollback to this version', rollback: 'Rollback to this version',
rollbackConfirm: 'Are you sure you want to rollback to this version? Current changes will be lost.', rollbackConfirm:
'Are you sure you want to rollback to this version? Current changes will be lost.',
rollbackSuccess: 'Rollback successful', rollbackSuccess: 'Rollback successful',
rollbackError: 'Failed to rollback: ', rollbackError: 'Failed to rollback: ',
}, },
@@ -1719,7 +1735,8 @@
disconnected: 'WebSocket未接続', disconnected: 'WebSocket未接続',
connectionError: 'WebSocket接続エラー', connectionError: 'WebSocket接続エラー',
connectionFailed: 'WebSocket接続失敗', connectionFailed: 'WebSocket接続失敗',
notConnected: 'WebSocket未接続です。しばらくしてからもう一度お試しください', notConnected:
'WebSocket未接続です。しばらくしてからもう一度お試しください',
imageUploadFailed: '画像アップロード失敗', imageUploadFailed: '画像アップロード失敗',
reply: '返信', reply: '返信',
replyTo: '返信先', replyTo: '返信先',
+32 -16
View File
@@ -1344,7 +1344,8 @@ const ruRU = {
}, },
workflows: { workflows: {
title: 'Workflows', title: 'Workflows',
description: 'Create and manage visual workflows for complex message processing logic', description:
'Create and manage visual workflows for complex message processing logic',
createWorkflow: 'Create Workflow', createWorkflow: 'Create Workflow',
selectFromSidebar: 'Select a workflow from the sidebar', selectFromSidebar: 'Select a workflow from the sidebar',
editWorkflow: 'Edit Workflow', editWorkflow: 'Edit Workflow',
@@ -1384,10 +1385,12 @@ const ruRU = {
dangerZoneDesc: 'Irreversible operations', dangerZoneDesc: 'Irreversible operations',
dangerZoneDescription: 'Irreversible operations', dangerZoneDescription: 'Irreversible operations',
deleteWorkflowAction: 'Delete this workflow', deleteWorkflowAction: 'Delete this workflow',
deleteWorkflowHint: 'Once deleted, all associated configurations will be permanently removed and cannot be recovered.', deleteWorkflowHint:
'Once deleted, all associated configurations will be permanently removed and cannot be recovered.',
deleteWorkflow: 'Delete Workflow', deleteWorkflow: 'Delete Workflow',
deleteConfirm: 'Confirm Delete', deleteConfirm: 'Confirm Delete',
deleteConfirmDesc: 'Are you sure you want to delete workflow "{{name}}"? This action cannot be undone.', deleteConfirmDesc:
'Are you sure you want to delete workflow "{{name}}"? This action cannot be undone.',
// Form component // Form component
name: 'Name', name: 'Name',
namePlaceholder: 'Enter workflow name', namePlaceholder: 'Enter workflow name',
@@ -1450,13 +1453,16 @@ const ruRU = {
dragToAdd: 'Drag nodes to add to canvas', dragToAdd: 'Drag nodes to add to canvas',
// Property panel // Property panel
selectNodeOrEdge: 'Select a node or edge', selectNodeOrEdge: 'Select a node or edge',
selectNodeOrEdgeHint: 'Click on a node or edge in the canvas to view and edit its properties', selectNodeOrEdgeHint:
'Click on a node or edge in the canvas to view and edit its properties',
edgeProperties: 'Edge Properties', edgeProperties: 'Edge Properties',
nodeProperties: 'Node Properties', nodeProperties: 'Node Properties',
condition: 'Condition', condition: 'Condition',
hasCondition: 'Set', hasCondition: 'Set',
conditionPlaceholder: 'Enter condition expression, e.g. output.success == true', conditionPlaceholder:
conditionHelp: 'When condition is empty, this edge will always be executed. Use {{variable}} to reference context variables.', 'Enter condition expression, e.g. output.success == true',
conditionHelp:
'When condition is empty, this edge will always be executed. Use {{variable}} to reference context variables.',
deleteEdge: 'Delete Edge', deleteEdge: 'Delete Edge',
deleteEdgeConfirm: 'Confirm Delete Edge', deleteEdgeConfirm: 'Confirm Delete Edge',
deleteEdgeConfirmDesc: 'This edge will be permanently removed.', deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
@@ -1477,7 +1483,8 @@ const ruRU = {
noConfigOptions: 'No configuration options for this node type', noConfigOptions: 'No configuration options for this node type',
deleteNode: 'Delete Node', deleteNode: 'Delete Node',
deleteNodeConfirm: 'Confirm Delete Node', deleteNodeConfirm: 'Confirm Delete Node',
deleteNodeConfirmDesc: 'This node and all its connections will be permanently removed.', deleteNodeConfirmDesc:
'This node and all its connections will be permanently removed.',
// Node inputs/outputs i18n (for port labels) // Node inputs/outputs i18n (for port labels)
nodeInputs: { nodeInputs: {
// Common inputs // Common inputs
@@ -1540,7 +1547,8 @@ const ruRU = {
aiProcess: 'AI Processing', aiProcess: 'AI Processing',
aiProcessDescription: 'Process messages using AI models', aiProcessDescription: 'Process messages using AI models',
llmCall: 'LLM Call', llmCall: 'LLM Call',
llmCallDescription: 'Invoke large language model for conversation or generation', llmCallDescription:
'Invoke large language model for conversation or generation',
codeProcess: 'Code Processing', codeProcess: 'Code Processing',
codeProcessDescription: 'Execute custom code', codeProcessDescription: 'Execute custom code',
codeExecutor: 'Code Executor', codeExecutor: 'Code Executor',
@@ -1552,13 +1560,17 @@ const ruRU = {
dataTransform: 'Data Transform', dataTransform: 'Data Transform',
dataTransformDescription: 'Transform data format', dataTransformDescription: 'Transform data format',
questionClassifier: 'Question Classifier', questionClassifier: 'Question Classifier',
questionClassifierDescription: 'Classify user questions into predefined categories using LLM', questionClassifierDescription:
'Classify user questions into predefined categories using LLM',
parameterExtractor: 'Parameter Extractor', parameterExtractor: 'Parameter Extractor',
parameterExtractorDescription: 'Extract structured parameters from text using LLM', parameterExtractorDescription:
'Extract structured parameters from text using LLM',
knowledgeRetrieval: 'Knowledge Retrieval', knowledgeRetrieval: 'Knowledge Retrieval',
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base', knowledgeRetrievalDescription:
'Retrieve relevant content from knowledge base',
textTemplate: 'Text Template', textTemplate: 'Text Template',
textTemplateDescription: 'Generate text using templates with variable interpolation', textTemplateDescription:
'Generate text using templates with variable interpolation',
jsonTransform: 'JSON Transform', jsonTransform: 'JSON Transform',
jsonTransformDescription: 'Transform JSON data using expressions', jsonTransformDescription: 'Transform JSON data using expressions',
dataAggregator: 'Data Aggregator', dataAggregator: 'Data Aggregator',
@@ -1586,13 +1598,15 @@ const ruRU = {
merge: 'Merge', merge: 'Merge',
mergeDescription: 'Merge multiple branches', mergeDescription: 'Merge multiple branches',
variableAggregator: 'Variable Aggregator', variableAggregator: 'Variable Aggregator',
variableAggregatorDescription: 'Aggregate variable outputs from multiple branches', variableAggregatorDescription:
'Aggregate variable outputs from multiple branches',
action: 'Actions', action: 'Actions',
actionDescription: 'Action execution nodes', actionDescription: 'Action execution nodes',
sendMessage: 'Send Message', sendMessage: 'Send Message',
sendMessageDescription: 'Send message to platform', sendMessageDescription: 'Send message to platform',
replyMessage: 'Reply Message', replyMessage: 'Reply Message',
replyMessageDescription: 'Reply to the message that triggered the workflow', replyMessageDescription:
'Reply to the message that triggered the workflow',
storeData: 'Store Data', storeData: 'Store Data',
storeDataDescription: 'Store data to database', storeDataDescription: 'Store data to database',
callPipeline: 'Call Pipeline', callPipeline: 'Call Pipeline',
@@ -1600,7 +1614,8 @@ const ruRU = {
setVariable: 'Set Variable', setVariable: 'Set Variable',
setVariableDescription: 'Set context variable', setVariableDescription: 'Set context variable',
openingStatement: 'Opening Statement', openingStatement: 'Opening Statement',
openingStatementDescription: 'Provide conversation opener and suggested questions', openingStatementDescription:
'Provide conversation opener and suggested questions',
end: 'End', end: 'End',
endDescription: 'Mark the end of workflow execution', endDescription: 'Mark the end of workflow execution',
log: 'Log', log: 'Log',
@@ -1646,7 +1661,8 @@ const ruRU = {
title: 'Version History', title: 'Version History',
current: 'Current Version', current: 'Current Version',
rollback: 'Rollback to this version', rollback: 'Rollback to this version',
rollbackConfirm: 'Are you sure you want to rollback to this version? Current changes will be lost.', rollbackConfirm:
'Are you sure you want to rollback to this version? Current changes will be lost.',
rollbackSuccess: 'Rollback successful', rollbackSuccess: 'Rollback successful',
rollbackError: 'Failed to rollback: ', rollbackError: 'Failed to rollback: ',
}, },
+32 -16
View File
@@ -1313,7 +1313,8 @@ const thTH = {
}, },
workflows: { workflows: {
title: 'Workflows', title: 'Workflows',
description: 'Create and manage visual workflows for complex message processing logic', description:
'Create and manage visual workflows for complex message processing logic',
createWorkflow: 'Create Workflow', createWorkflow: 'Create Workflow',
selectFromSidebar: 'Select a workflow from the sidebar', selectFromSidebar: 'Select a workflow from the sidebar',
editWorkflow: 'Edit Workflow', editWorkflow: 'Edit Workflow',
@@ -1353,10 +1354,12 @@ const thTH = {
dangerZoneDesc: 'Irreversible operations', dangerZoneDesc: 'Irreversible operations',
dangerZoneDescription: 'Irreversible operations', dangerZoneDescription: 'Irreversible operations',
deleteWorkflowAction: 'Delete this workflow', deleteWorkflowAction: 'Delete this workflow',
deleteWorkflowHint: 'Once deleted, all associated configurations will be permanently removed and cannot be recovered.', deleteWorkflowHint:
'Once deleted, all associated configurations will be permanently removed and cannot be recovered.',
deleteWorkflow: 'Delete Workflow', deleteWorkflow: 'Delete Workflow',
deleteConfirm: 'Confirm Delete', deleteConfirm: 'Confirm Delete',
deleteConfirmDesc: 'Are you sure you want to delete workflow "{{name}}"? This action cannot be undone.', deleteConfirmDesc:
'Are you sure you want to delete workflow "{{name}}"? This action cannot be undone.',
// Form component // Form component
name: 'Name', name: 'Name',
namePlaceholder: 'Enter workflow name', namePlaceholder: 'Enter workflow name',
@@ -1419,13 +1422,16 @@ const thTH = {
dragToAdd: 'Drag nodes to add to canvas', dragToAdd: 'Drag nodes to add to canvas',
// Property panel // Property panel
selectNodeOrEdge: 'Select a node or edge', selectNodeOrEdge: 'Select a node or edge',
selectNodeOrEdgeHint: 'Click on a node or edge in the canvas to view and edit its properties', selectNodeOrEdgeHint:
'Click on a node or edge in the canvas to view and edit its properties',
edgeProperties: 'Edge Properties', edgeProperties: 'Edge Properties',
nodeProperties: 'Node Properties', nodeProperties: 'Node Properties',
condition: 'Condition', condition: 'Condition',
hasCondition: 'Set', hasCondition: 'Set',
conditionPlaceholder: 'Enter condition expression, e.g. output.success == true', conditionPlaceholder:
conditionHelp: 'When condition is empty, this edge will always be executed. Use {{variable}} to reference context variables.', 'Enter condition expression, e.g. output.success == true',
conditionHelp:
'When condition is empty, this edge will always be executed. Use {{variable}} to reference context variables.',
deleteEdge: 'Delete Edge', deleteEdge: 'Delete Edge',
deleteEdgeConfirm: 'Confirm Delete Edge', deleteEdgeConfirm: 'Confirm Delete Edge',
deleteEdgeConfirmDesc: 'This edge will be permanently removed.', deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
@@ -1446,7 +1452,8 @@ const thTH = {
noConfigOptions: 'No configuration options for this node type', noConfigOptions: 'No configuration options for this node type',
deleteNode: 'Delete Node', deleteNode: 'Delete Node',
deleteNodeConfirm: 'Confirm Delete Node', deleteNodeConfirm: 'Confirm Delete Node',
deleteNodeConfirmDesc: 'This node and all its connections will be permanently removed.', deleteNodeConfirmDesc:
'This node and all its connections will be permanently removed.',
// Node inputs/outputs i18n (for port labels) // Node inputs/outputs i18n (for port labels)
nodeInputs: { nodeInputs: {
// Common inputs // Common inputs
@@ -1509,7 +1516,8 @@ const thTH = {
aiProcess: 'AI Processing', aiProcess: 'AI Processing',
aiProcessDescription: 'Process messages using AI models', aiProcessDescription: 'Process messages using AI models',
llmCall: 'LLM Call', llmCall: 'LLM Call',
llmCallDescription: 'Invoke large language model for conversation or generation', llmCallDescription:
'Invoke large language model for conversation or generation',
codeProcess: 'Code Processing', codeProcess: 'Code Processing',
codeProcessDescription: 'Execute custom code', codeProcessDescription: 'Execute custom code',
codeExecutor: 'Code Executor', codeExecutor: 'Code Executor',
@@ -1521,13 +1529,17 @@ const thTH = {
dataTransform: 'Data Transform', dataTransform: 'Data Transform',
dataTransformDescription: 'Transform data format', dataTransformDescription: 'Transform data format',
questionClassifier: 'Question Classifier', questionClassifier: 'Question Classifier',
questionClassifierDescription: 'Classify user questions into predefined categories using LLM', questionClassifierDescription:
'Classify user questions into predefined categories using LLM',
parameterExtractor: 'Parameter Extractor', parameterExtractor: 'Parameter Extractor',
parameterExtractorDescription: 'Extract structured parameters from text using LLM', parameterExtractorDescription:
'Extract structured parameters from text using LLM',
knowledgeRetrieval: 'Knowledge Retrieval', knowledgeRetrieval: 'Knowledge Retrieval',
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base', knowledgeRetrievalDescription:
'Retrieve relevant content from knowledge base',
textTemplate: 'Text Template', textTemplate: 'Text Template',
textTemplateDescription: 'Generate text using templates with variable interpolation', textTemplateDescription:
'Generate text using templates with variable interpolation',
jsonTransform: 'JSON Transform', jsonTransform: 'JSON Transform',
jsonTransformDescription: 'Transform JSON data using expressions', jsonTransformDescription: 'Transform JSON data using expressions',
dataAggregator: 'Data Aggregator', dataAggregator: 'Data Aggregator',
@@ -1555,13 +1567,15 @@ const thTH = {
merge: 'Merge', merge: 'Merge',
mergeDescription: 'Merge multiple branches', mergeDescription: 'Merge multiple branches',
variableAggregator: 'Variable Aggregator', variableAggregator: 'Variable Aggregator',
variableAggregatorDescription: 'Aggregate variable outputs from multiple branches', variableAggregatorDescription:
'Aggregate variable outputs from multiple branches',
action: 'Actions', action: 'Actions',
actionDescription: 'Action execution nodes', actionDescription: 'Action execution nodes',
sendMessage: 'Send Message', sendMessage: 'Send Message',
sendMessageDescription: 'Send message to platform', sendMessageDescription: 'Send message to platform',
replyMessage: 'Reply Message', replyMessage: 'Reply Message',
replyMessageDescription: 'Reply to the message that triggered the workflow', replyMessageDescription:
'Reply to the message that triggered the workflow',
storeData: 'Store Data', storeData: 'Store Data',
storeDataDescription: 'Store data to database', storeDataDescription: 'Store data to database',
callPipeline: 'Call Pipeline', callPipeline: 'Call Pipeline',
@@ -1569,7 +1583,8 @@ const thTH = {
setVariable: 'Set Variable', setVariable: 'Set Variable',
setVariableDescription: 'Set context variable', setVariableDescription: 'Set context variable',
openingStatement: 'Opening Statement', openingStatement: 'Opening Statement',
openingStatementDescription: 'Provide conversation opener and suggested questions', openingStatementDescription:
'Provide conversation opener and suggested questions',
end: 'End', end: 'End',
endDescription: 'Mark the end of workflow execution', endDescription: 'Mark the end of workflow execution',
log: 'Log', log: 'Log',
@@ -1615,7 +1630,8 @@ const thTH = {
title: 'Version History', title: 'Version History',
current: 'Current Version', current: 'Current Version',
rollback: 'Rollback to this version', rollback: 'Rollback to this version',
rollbackConfirm: 'Are you sure you want to rollback to this version? Current changes will be lost.', rollbackConfirm:
'Are you sure you want to rollback to this version? Current changes will be lost.',
rollbackSuccess: 'Rollback successful', rollbackSuccess: 'Rollback successful',
rollbackError: 'Failed to rollback: ', rollbackError: 'Failed to rollback: ',
}, },
+32 -16
View File
@@ -1335,7 +1335,8 @@ const viVN = {
}, },
workflows: { workflows: {
title: 'Workflows', title: 'Workflows',
description: 'Create and manage visual workflows for complex message processing logic', description:
'Create and manage visual workflows for complex message processing logic',
createWorkflow: 'Create Workflow', createWorkflow: 'Create Workflow',
selectFromSidebar: 'Select a workflow from the sidebar', selectFromSidebar: 'Select a workflow from the sidebar',
editWorkflow: 'Edit Workflow', editWorkflow: 'Edit Workflow',
@@ -1375,10 +1376,12 @@ const viVN = {
dangerZoneDesc: 'Irreversible operations', dangerZoneDesc: 'Irreversible operations',
dangerZoneDescription: 'Irreversible operations', dangerZoneDescription: 'Irreversible operations',
deleteWorkflowAction: 'Delete this workflow', deleteWorkflowAction: 'Delete this workflow',
deleteWorkflowHint: 'Once deleted, all associated configurations will be permanently removed and cannot be recovered.', deleteWorkflowHint:
'Once deleted, all associated configurations will be permanently removed and cannot be recovered.',
deleteWorkflow: 'Delete Workflow', deleteWorkflow: 'Delete Workflow',
deleteConfirm: 'Confirm Delete', deleteConfirm: 'Confirm Delete',
deleteConfirmDesc: 'Are you sure you want to delete workflow "{{name}}"? This action cannot be undone.', deleteConfirmDesc:
'Are you sure you want to delete workflow "{{name}}"? This action cannot be undone.',
// Form component // Form component
name: 'Name', name: 'Name',
namePlaceholder: 'Enter workflow name', namePlaceholder: 'Enter workflow name',
@@ -1441,13 +1444,16 @@ const viVN = {
dragToAdd: 'Drag nodes to add to canvas', dragToAdd: 'Drag nodes to add to canvas',
// Property panel // Property panel
selectNodeOrEdge: 'Select a node or edge', selectNodeOrEdge: 'Select a node or edge',
selectNodeOrEdgeHint: 'Click on a node or edge in the canvas to view and edit its properties', selectNodeOrEdgeHint:
'Click on a node or edge in the canvas to view and edit its properties',
edgeProperties: 'Edge Properties', edgeProperties: 'Edge Properties',
nodeProperties: 'Node Properties', nodeProperties: 'Node Properties',
condition: 'Condition', condition: 'Condition',
hasCondition: 'Set', hasCondition: 'Set',
conditionPlaceholder: 'Enter condition expression, e.g. output.success == true', conditionPlaceholder:
conditionHelp: 'When condition is empty, this edge will always be executed. Use {{variable}} to reference context variables.', 'Enter condition expression, e.g. output.success == true',
conditionHelp:
'When condition is empty, this edge will always be executed. Use {{variable}} to reference context variables.',
deleteEdge: 'Delete Edge', deleteEdge: 'Delete Edge',
deleteEdgeConfirm: 'Confirm Delete Edge', deleteEdgeConfirm: 'Confirm Delete Edge',
deleteEdgeConfirmDesc: 'This edge will be permanently removed.', deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
@@ -1468,7 +1474,8 @@ const viVN = {
noConfigOptions: 'No configuration options for this node type', noConfigOptions: 'No configuration options for this node type',
deleteNode: 'Delete Node', deleteNode: 'Delete Node',
deleteNodeConfirm: 'Confirm Delete Node', deleteNodeConfirm: 'Confirm Delete Node',
deleteNodeConfirmDesc: 'This node and all its connections will be permanently removed.', deleteNodeConfirmDesc:
'This node and all its connections will be permanently removed.',
// Node inputs/outputs i18n (for port labels) // Node inputs/outputs i18n (for port labels)
nodeInputs: { nodeInputs: {
// Common inputs // Common inputs
@@ -1531,7 +1538,8 @@ const viVN = {
aiProcess: 'AI Processing', aiProcess: 'AI Processing',
aiProcessDescription: 'Process messages using AI models', aiProcessDescription: 'Process messages using AI models',
llmCall: 'LLM Call', llmCall: 'LLM Call',
llmCallDescription: 'Invoke large language model for conversation or generation', llmCallDescription:
'Invoke large language model for conversation or generation',
codeProcess: 'Code Processing', codeProcess: 'Code Processing',
codeProcessDescription: 'Execute custom code', codeProcessDescription: 'Execute custom code',
codeExecutor: 'Code Executor', codeExecutor: 'Code Executor',
@@ -1543,13 +1551,17 @@ const viVN = {
dataTransform: 'Data Transform', dataTransform: 'Data Transform',
dataTransformDescription: 'Transform data format', dataTransformDescription: 'Transform data format',
questionClassifier: 'Question Classifier', questionClassifier: 'Question Classifier',
questionClassifierDescription: 'Classify user questions into predefined categories using LLM', questionClassifierDescription:
'Classify user questions into predefined categories using LLM',
parameterExtractor: 'Parameter Extractor', parameterExtractor: 'Parameter Extractor',
parameterExtractorDescription: 'Extract structured parameters from text using LLM', parameterExtractorDescription:
'Extract structured parameters from text using LLM',
knowledgeRetrieval: 'Knowledge Retrieval', knowledgeRetrieval: 'Knowledge Retrieval',
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base', knowledgeRetrievalDescription:
'Retrieve relevant content from knowledge base',
textTemplate: 'Text Template', textTemplate: 'Text Template',
textTemplateDescription: 'Generate text using templates with variable interpolation', textTemplateDescription:
'Generate text using templates with variable interpolation',
jsonTransform: 'JSON Transform', jsonTransform: 'JSON Transform',
jsonTransformDescription: 'Transform JSON data using expressions', jsonTransformDescription: 'Transform JSON data using expressions',
dataAggregator: 'Data Aggregator', dataAggregator: 'Data Aggregator',
@@ -1577,13 +1589,15 @@ const viVN = {
merge: 'Merge', merge: 'Merge',
mergeDescription: 'Merge multiple branches', mergeDescription: 'Merge multiple branches',
variableAggregator: 'Variable Aggregator', variableAggregator: 'Variable Aggregator',
variableAggregatorDescription: 'Aggregate variable outputs from multiple branches', variableAggregatorDescription:
'Aggregate variable outputs from multiple branches',
action: 'Actions', action: 'Actions',
actionDescription: 'Action execution nodes', actionDescription: 'Action execution nodes',
sendMessage: 'Send Message', sendMessage: 'Send Message',
sendMessageDescription: 'Send message to platform', sendMessageDescription: 'Send message to platform',
replyMessage: 'Reply Message', replyMessage: 'Reply Message',
replyMessageDescription: 'Reply to the message that triggered the workflow', replyMessageDescription:
'Reply to the message that triggered the workflow',
storeData: 'Store Data', storeData: 'Store Data',
storeDataDescription: 'Store data to database', storeDataDescription: 'Store data to database',
callPipeline: 'Call Pipeline', callPipeline: 'Call Pipeline',
@@ -1591,7 +1605,8 @@ const viVN = {
setVariable: 'Set Variable', setVariable: 'Set Variable',
setVariableDescription: 'Set context variable', setVariableDescription: 'Set context variable',
openingStatement: 'Opening Statement', openingStatement: 'Opening Statement',
openingStatementDescription: 'Provide conversation opener and suggested questions', openingStatementDescription:
'Provide conversation opener and suggested questions',
end: 'End', end: 'End',
endDescription: 'Mark the end of workflow execution', endDescription: 'Mark the end of workflow execution',
log: 'Log', log: 'Log',
@@ -1637,7 +1652,8 @@ const viVN = {
title: 'Version History', title: 'Version History',
current: 'Current Version', current: 'Current Version',
rollback: 'Rollback to this version', rollback: 'Rollback to this version',
rollbackConfirm: 'Are you sure you want to rollback to this version? Current changes will be lost.', rollbackConfirm:
'Are you sure you want to rollback to this version? Current changes will be lost.',
rollbackSuccess: 'Rollback successful', rollbackSuccess: 'Rollback successful',
rollbackError: 'Failed to rollback: ', rollbackError: 'Failed to rollback: ',
}, },
+32 -16
View File
@@ -1276,7 +1276,8 @@ const zhHant = {
}, },
workflows: { workflows: {
title: 'Workflows', title: 'Workflows',
description: 'Create and manage visual workflows for complex message processing logic', description:
'Create and manage visual workflows for complex message processing logic',
createWorkflow: 'Create Workflow', createWorkflow: 'Create Workflow',
selectFromSidebar: 'Select a workflow from the sidebar', selectFromSidebar: 'Select a workflow from the sidebar',
editWorkflow: 'Edit Workflow', editWorkflow: 'Edit Workflow',
@@ -1316,10 +1317,12 @@ const zhHant = {
dangerZoneDesc: 'Irreversible operations', dangerZoneDesc: 'Irreversible operations',
dangerZoneDescription: 'Irreversible operations', dangerZoneDescription: 'Irreversible operations',
deleteWorkflowAction: 'Delete this workflow', deleteWorkflowAction: 'Delete this workflow',
deleteWorkflowHint: 'Once deleted, all associated configurations will be permanently removed and cannot be recovered.', deleteWorkflowHint:
'Once deleted, all associated configurations will be permanently removed and cannot be recovered.',
deleteWorkflow: 'Delete Workflow', deleteWorkflow: 'Delete Workflow',
deleteConfirm: 'Confirm Delete', deleteConfirm: 'Confirm Delete',
deleteConfirmDesc: 'Are you sure you want to delete workflow "{{name}}"? This action cannot be undone.', deleteConfirmDesc:
'Are you sure you want to delete workflow "{{name}}"? This action cannot be undone.',
// Form component // Form component
name: 'Name', name: 'Name',
namePlaceholder: 'Enter workflow name', namePlaceholder: 'Enter workflow name',
@@ -1382,13 +1385,16 @@ const zhHant = {
dragToAdd: 'Drag nodes to add to canvas', dragToAdd: 'Drag nodes to add to canvas',
// Property panel // Property panel
selectNodeOrEdge: 'Select a node or edge', selectNodeOrEdge: 'Select a node or edge',
selectNodeOrEdgeHint: 'Click on a node or edge in the canvas to view and edit its properties', selectNodeOrEdgeHint:
'Click on a node or edge in the canvas to view and edit its properties',
edgeProperties: 'Edge Properties', edgeProperties: 'Edge Properties',
nodeProperties: 'Node Properties', nodeProperties: 'Node Properties',
condition: 'Condition', condition: 'Condition',
hasCondition: 'Set', hasCondition: 'Set',
conditionPlaceholder: 'Enter condition expression, e.g. output.success == true', conditionPlaceholder:
conditionHelp: 'When condition is empty, this edge will always be executed. Use {{variable}} to reference context variables.', 'Enter condition expression, e.g. output.success == true',
conditionHelp:
'When condition is empty, this edge will always be executed. Use {{variable}} to reference context variables.',
deleteEdge: 'Delete Edge', deleteEdge: 'Delete Edge',
deleteEdgeConfirm: 'Confirm Delete Edge', deleteEdgeConfirm: 'Confirm Delete Edge',
deleteEdgeConfirmDesc: 'This edge will be permanently removed.', deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
@@ -1409,7 +1415,8 @@ const zhHant = {
noConfigOptions: 'No configuration options for this node type', noConfigOptions: 'No configuration options for this node type',
deleteNode: 'Delete Node', deleteNode: 'Delete Node',
deleteNodeConfirm: 'Confirm Delete Node', deleteNodeConfirm: 'Confirm Delete Node',
deleteNodeConfirmDesc: 'This node and all its connections will be permanently removed.', deleteNodeConfirmDesc:
'This node and all its connections will be permanently removed.',
// Node inputs/outputs i18n (for port labels) // Node inputs/outputs i18n (for port labels)
nodeInputs: { nodeInputs: {
// Common inputs // Common inputs
@@ -1472,7 +1479,8 @@ const zhHant = {
aiProcess: 'AI Processing', aiProcess: 'AI Processing',
aiProcessDescription: 'Process messages using AI models', aiProcessDescription: 'Process messages using AI models',
llmCall: 'LLM Call', llmCall: 'LLM Call',
llmCallDescription: 'Invoke large language model for conversation or generation', llmCallDescription:
'Invoke large language model for conversation or generation',
codeProcess: 'Code Processing', codeProcess: 'Code Processing',
codeProcessDescription: 'Execute custom code', codeProcessDescription: 'Execute custom code',
codeExecutor: 'Code Executor', codeExecutor: 'Code Executor',
@@ -1484,13 +1492,17 @@ const zhHant = {
dataTransform: 'Data Transform', dataTransform: 'Data Transform',
dataTransformDescription: 'Transform data format', dataTransformDescription: 'Transform data format',
questionClassifier: 'Question Classifier', questionClassifier: 'Question Classifier',
questionClassifierDescription: 'Classify user questions into predefined categories using LLM', questionClassifierDescription:
'Classify user questions into predefined categories using LLM',
parameterExtractor: 'Parameter Extractor', parameterExtractor: 'Parameter Extractor',
parameterExtractorDescription: 'Extract structured parameters from text using LLM', parameterExtractorDescription:
'Extract structured parameters from text using LLM',
knowledgeRetrieval: 'Knowledge Retrieval', knowledgeRetrieval: 'Knowledge Retrieval',
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base', knowledgeRetrievalDescription:
'Retrieve relevant content from knowledge base',
textTemplate: 'Text Template', textTemplate: 'Text Template',
textTemplateDescription: 'Generate text using templates with variable interpolation', textTemplateDescription:
'Generate text using templates with variable interpolation',
jsonTransform: 'JSON Transform', jsonTransform: 'JSON Transform',
jsonTransformDescription: 'Transform JSON data using expressions', jsonTransformDescription: 'Transform JSON data using expressions',
dataAggregator: 'Data Aggregator', dataAggregator: 'Data Aggregator',
@@ -1518,13 +1530,15 @@ const zhHant = {
merge: 'Merge', merge: 'Merge',
mergeDescription: 'Merge multiple branches', mergeDescription: 'Merge multiple branches',
variableAggregator: 'Variable Aggregator', variableAggregator: 'Variable Aggregator',
variableAggregatorDescription: 'Aggregate variable outputs from multiple branches', variableAggregatorDescription:
'Aggregate variable outputs from multiple branches',
action: 'Actions', action: 'Actions',
actionDescription: 'Action execution nodes', actionDescription: 'Action execution nodes',
sendMessage: 'Send Message', sendMessage: 'Send Message',
sendMessageDescription: 'Send message to platform', sendMessageDescription: 'Send message to platform',
replyMessage: 'Reply Message', replyMessage: 'Reply Message',
replyMessageDescription: 'Reply to the message that triggered the workflow', replyMessageDescription:
'Reply to the message that triggered the workflow',
storeData: 'Store Data', storeData: 'Store Data',
storeDataDescription: 'Store data to database', storeDataDescription: 'Store data to database',
callPipeline: 'Call Pipeline', callPipeline: 'Call Pipeline',
@@ -1532,7 +1546,8 @@ const zhHant = {
setVariable: 'Set Variable', setVariable: 'Set Variable',
setVariableDescription: 'Set context variable', setVariableDescription: 'Set context variable',
openingStatement: 'Opening Statement', openingStatement: 'Opening Statement',
openingStatementDescription: 'Provide conversation opener and suggested questions', openingStatementDescription:
'Provide conversation opener and suggested questions',
end: 'End', end: 'End',
endDescription: 'Mark the end of workflow execution', endDescription: 'Mark the end of workflow execution',
log: 'Log', log: 'Log',
@@ -1578,7 +1593,8 @@ const zhHant = {
title: 'Version History', title: 'Version History',
current: 'Current Version', current: 'Current Version',
rollback: 'Rollback to this version', rollback: 'Rollback to this version',
rollbackConfirm: 'Are you sure you want to rollback to this version? Current changes will be lost.', rollbackConfirm:
'Are you sure you want to rollback to this version? Current changes will be lost.',
rollbackSuccess: 'Rollback successful', rollbackSuccess: 'Rollback successful',
rollbackError: 'Failed to rollback: ', rollbackError: 'Failed to rollback: ',
}, },