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

View File

@@ -74,15 +74,24 @@ class ConditionNode(WorkflowNode):
left_num = float(left)
right_num = float(right)
if operator == "==": 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
elif operator == ">=": return left_num >= right_num
elif operator == "<=": return left_num <= right_num
if operator == "==":
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
elif operator == ">=":
return left_num >= right_num
elif operator == "<=":
return left_num <= right_num
except ValueError:
if operator == "==": return left == right
elif operator == "!=": return left != right
elif operator in (">", "<", ">=", "<="): return False
if operator == "==":
return left == right
elif operator == "!=":
return left != right
elif operator in (">", "<", ">=", "<="):
return False
return False

View File

@@ -31,7 +31,6 @@ class DatabaseQueryNode(WorkflowNode):
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
connection_type = self.get_config("connection_type", "postgresql")
connection_string = self.get_config("connection_string", "")
query = self.get_config("query", "")
query_type = self.get_config("query_type", "select")
timeout = self.get_config("timeout", 30)

View File

@@ -30,7 +30,6 @@ class ParameterExtractorNode(WorkflowNode):
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
text = inputs.get("text", "")
param_defs = self.get_config("parameters", [])
extracted = {}

View File

@@ -30,7 +30,6 @@ class QuestionClassifierNode(WorkflowNode):
config_schema: ClassVar[list[NodeConfig]] = []
async def execute(self, inputs: dict[str, Any], context: ExecutionContext) -> dict[str, Any]:
question = inputs.get("question", "")
categories = self.get_config("categories", [])
if categories:

View File

@@ -30,7 +30,4 @@ class SendMessageNode(WorkflowNode):
config_schema: ClassVar[list[NodeConfig]] = []
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}"}

View File

@@ -161,7 +161,12 @@ function isEntityCategory(id: string): id is EntityCategoryId {
// Map sidebar config IDs to SidebarDataContext keys
const ENTITY_KEY_MAP: Record<
EntityCategoryId,
'bots' | 'pipelines' | 'workflows' | 'knowledgeBases' | 'plugins' | 'mcpServers'
| 'bots'
| 'pipelines'
| 'workflows'
| 'knowledgeBases'
| 'plugins'
| 'mcpServers'
> = {
bots: 'bots',
pipelines: 'pipelines',

View File

@@ -1,6 +1,9 @@
import { useState, useEffect } from 'react';
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 { Button } from '@/components/ui/button';
import {
@@ -43,7 +46,8 @@ export default function UnifiedBindingSelector({
className,
}: UnifiedBindingSelectorProps) {
const { t } = useTranslation();
const { pipelines, workflows, refreshPipelines, refreshWorkflows } = useSidebarData();
const { pipelines, workflows, refreshPipelines, refreshWorkflows } =
useSidebarData();
const [open, setOpen] = useState(false);
useEffect(() => {
@@ -137,7 +141,9 @@ export default function UnifiedBindingSelector({
{/* Entity selection */}
<div className="space-y-2">
<Label>
{value.type === 'pipeline' ? t('bots.selectPipeline') : t('bots.selectWorkflow')}
{value.type === 'pipeline'
? t('bots.selectPipeline')
: t('bots.selectWorkflow')}
</Label>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
@@ -171,7 +177,9 @@ export default function UnifiedBindingSelector({
<Check
className={cn(
'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">
@@ -186,37 +194,37 @@ export default function UnifiedBindingSelector({
</Button>
))
)
) : workflows.length === 0 ? (
<div className="py-6 text-center text-sm text-muted-foreground">
{t('bots.noWorkflowsFound')}
</div>
) : (
workflows.length === 0 ? (
<div className="py-6 text-center text-sm text-muted-foreground">
{t('bots.noWorkflowsFound')}
</div>
) : (
workflows.map((workflow) => (
<Button
key={workflow.id}
variant="ghost"
className="w-full justify-start"
onClick={() => handleSelect(workflow.id, 'workflow')}
>
<Check
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>
workflows.map((workflow) => (
<Button
key={workflow.id}
variant="ghost"
className="w-full justify-start"
onClick={() => handleSelect(workflow.id, 'workflow')}
>
<Check
className={cn(
'mr-2 h-4 w-4',
value.id === workflow.id
? 'opacity-100'
: 'opacity-0',
)}
</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>
</ScrollArea>

View File

@@ -2,7 +2,13 @@ import { useState, useEffect, useCallback, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
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 { Input } from '@/components/ui/input';
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 { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
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 { Workflow } from '@/app/infra/entities/api';
import { useWorkflowStore } from './store/useWorkflowStore';
@@ -24,7 +38,7 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
const navigate = useNavigate();
const { t } = useTranslation();
const { refreshWorkflows, workflows, setDetailEntityName } = useSidebarData();
const {
currentWorkflow,
setCurrentWorkflow,
@@ -43,7 +57,11 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
const [activeTab, setActiveTab] = useState('editor');
const [workflow, setWorkflow] = useState<Workflow | null>(null);
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: '',
description: '',
emoji: '🔄',
@@ -65,11 +83,14 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
// Load node types
useEffect(() => {
if (nodeTypes.length === 0) {
backendClient.getWorkflowNodeTypes().then((resp) => {
setNodeTypes(resp.node_types, resp.categories);
}).catch((err) => {
console.error('Failed to load node types:', err);
});
backendClient
.getWorkflowNodeTypes()
.then((resp) => {
setNodeTypes(resp.node_types, resp.categories);
})
.catch((err) => {
console.error('Failed to load node types:', err);
});
}
}, [nodeTypes.length, setNodeTypes]);
@@ -82,16 +103,23 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
}
setLoading(true);
backendClient.getWorkflow(id).then((resp) => {
setWorkflow(resp.workflow);
setCurrentWorkflow(resp.workflow);
fromWorkflowDefinition(resp.workflow.nodes || [], resp.workflow.edges || []);
}).catch((err) => {
console.error('Failed to load workflow:', err);
toast.error(t('workflows.loadError'));
}).finally(() => {
setLoading(false);
});
backendClient
.getWorkflow(id)
.then((resp) => {
setWorkflow(resp.workflow);
setCurrentWorkflow(resp.workflow);
fromWorkflowDefinition(
resp.workflow.nodes || [],
resp.workflow.edges || [],
);
})
.catch((err) => {
console.error('Failed to load workflow:', err);
toast.error(t('workflows.loadError'));
})
.finally(() => {
setLoading(false);
});
return () => {
reset();
@@ -105,7 +133,7 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
setSaving(true);
try {
const { nodes, edges } = toWorkflowDefinition();
if (isCreateMode) {
const resp = await backendClient.createWorkflow({
name: basicInfo.name || t('workflows.newWorkflow'),
@@ -138,12 +166,22 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
} finally {
setSaving(false);
}
}, [id, isCreateMode, workflow, isSaving, toWorkflowDefinition, refreshWorkflows, navigate, t, basicInfo]);
}, [
id,
isCreateMode,
workflow,
isSaving,
toWorkflowDefinition,
refreshWorkflows,
navigate,
t,
basicInfo,
]);
// Export workflow handler
const handleExport = useCallback(() => {
const { nodes, edges } = toWorkflowDefinition();
const exportData = {
name: workflow?.name || t('workflows.newWorkflow'),
description: workflow?.description || '',
@@ -155,8 +193,10 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
version: '1.0',
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 a = document.createElement('a');
a.href = url;
@@ -165,84 +205,103 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success(t('workflows.exportSuccess'));
}, [workflow, toWorkflowDefinition, t]);
// Import workflow handler
const handleImport = useCallback((file: File) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const importData = JSON.parse(e.target?.result as string);
// Validate imported data structure
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`);
const handleImport = useCallback(
(file: File) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const importData = JSON.parse(e.target?.result as string);
// Validate imported data structure
if (!importData.nodes || !Array.isArray(importData.nodes)) {
throw new Error('Invalid workflow file: missing nodes');
}
if (!node.position || typeof node.position.x !== 'number' || typeof node.position.y !== 'number') {
throw new Error(`Invalid node "${node.id}": missing or invalid position`);
if (!importData.edges || !Array.isArray(importData.edges)) {
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
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'));
}
};
reader.readAsText(file);
}, [workflow, fromWorkflowDefinition, setDirty, t]);
};
reader.readAsText(file);
},
[workflow, fromWorkflowDefinition, setDirty, t],
);
// Handle file input change
const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
handleImport(file);
// Reset file input
e.target.value = '';
}
}, [handleImport]);
const handleFileChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
handleImport(file);
// Reset file input
e.target.value = '';
}
},
[handleImport],
);
// Publish handler
const handlePublish = useCallback(async () => {
@@ -289,7 +348,10 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
style={{ display: 'none' }}
ref={fileInputRef}
/>
<Button variant="outline" onClick={() => fileInputRef.current?.click()}>
<Button
variant="outline"
onClick={() => fileInputRef.current?.click()}
>
<Upload className="size-4 mr-1" />
{t('workflows.import')}
</Button>
@@ -307,17 +369,26 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
<Card>
<CardHeader>
<CardTitle>{t('workflows.basicInfo')}</CardTitle>
<CardDescription>{t('workflows.basicInfoDesc')}</CardDescription>
<CardDescription>
{t('workflows.basicInfoDesc')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="workflow-name">{t('workflows.name')}</Label>
<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
id="workflow-name"
value={basicInfo.name}
onChange={(e) => setBasicInfo({ ...basicInfo, name: e.target.value })}
onChange={(e) =>
setBasicInfo({ ...basicInfo, name: e.target.value })
}
placeholder={t('workflows.namePlaceholder')}
className="flex-1"
/>
@@ -325,11 +396,18 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
</div>
<div className="space-y-2">
<Label htmlFor="workflow-description">{t('workflows.description')}</Label>
<Label htmlFor="workflow-description">
{t('workflows.description')}
</Label>
<Textarea
id="workflow-description"
value={basicInfo.description}
onChange={(e) => setBasicInfo({ ...basicInfo, description: e.target.value })}
onChange={(e) =>
setBasicInfo({
...basicInfo,
description: e.target.value,
})
}
placeholder={t('workflows.descriptionPlaceholder')}
rows={3}
/>
@@ -377,12 +455,15 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
style={{ display: 'none' }}
ref={fileInputRef}
/>
{/* Sticky Header: title + save button */}
<div className="flex items-center justify-between pb-4 shrink-0">
<h1 className="text-xl font-semibold">{t('workflows.editWorkflow')}</h1>
<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" />
{t('workflows.import')}
</Button>
@@ -434,10 +515,7 @@ export default function WorkflowDetailContent({ id }: { id: string }) {
</TabsList>
{/* Tab: Editor */}
<TabsContent
value="editor"
className="flex-1 min-h-0 mt-4"
>
<TabsContent value="editor" className="flex-1 min-h-0 mt-4">
<WorkflowEditorComponent />
</TabsContent>

View File

@@ -1,6 +1,10 @@
import { useState, useCallback, useEffect, useRef } from 'react';
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 { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -10,7 +14,13 @@ import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { ScrollArea } from '@/components/ui/scroll-area';
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 {
Collapsible,
CollapsibleContent,
@@ -66,7 +76,10 @@ const logLevelColors: Record<string, string> = {
debug: 'text-gray-400',
};
export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebuggerProps) {
export default function WorkflowDebugger({
workflowId,
onClose,
}: WorkflowDebuggerProps) {
const { t } = useTranslation();
const logsEndRef = useRef<HTMLDivElement>(null);
const [activeTab, setActiveTab] = useState<string>('context');
@@ -123,9 +136,9 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
setDebugState('running');
clearNodeExecutionResults();
clearDebugLogs();
addDebugLog({ level: 'info', message: t('workflows.debug.starting') });
const response = await backendClient.startWorkflowDebug(workflowId, {
context: {
message_content: debugContext.messageContent,
@@ -136,125 +149,169 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
is_group: debugContext.isGroup,
},
variables: debugContext.customVariables,
breakpoints: Object.keys(breakpoints).filter(k => breakpoints[k]),
breakpoints: Object.keys(breakpoints).filter((k) => breakpoints[k]),
});
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
pollDebugState(response.execution_id);
} catch (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]);
// Poll debug state
const pollDebugState = useCallback(async (executionId: string) => {
pollCancelledRef.current = false;
const poll = async () => {
if (pollCancelledRef.current) return;
try {
const state = await backendClient.getWorkflowDebugState(workflowId, executionId);
const pollDebugState = useCallback(
async (executionId: string) => {
pollCancelledRef.current = false;
const poll = async () => {
if (pollCancelledRef.current) return;
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>);
try {
const state = await backendClient.getWorkflowDebugState(
workflowId,
executionId,
);
if (pollCancelledRef.current) return;
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
if (state.new_logs) {
for (const log of state.new_logs) {
addDebugLog(log);
// Add new logs
if (state.new_logs) {
for (const log of state.new_logs) {
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')) {
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);
}
};
poll();
}, [workflowId, t]);
};
poll();
},
[workflowId, t],
);
// Pause execution
const handlePause = useCallback(async () => {
if (!debugExecutionId) return;
try {
await backendClient.pauseWorkflowDebug(workflowId, debugExecutionId);
setDebugState('paused');
addDebugLog({ level: 'info', message: t('workflows.debug.paused') });
} catch (error) {
addDebugLog({ level: 'error', message: `${t('workflows.debug.pauseError')}: ${error}` });
addDebugLog({
level: 'error',
message: `${t('workflows.debug.pauseError')}: ${error}`,
});
}
}, [workflowId, debugExecutionId, t]);
// Resume execution
const handleResume = useCallback(async () => {
if (!debugExecutionId) return;
try {
await backendClient.resumeWorkflowDebug(workflowId, debugExecutionId);
setDebugState('running');
addDebugLog({ level: 'info', message: t('workflows.debug.resumed') });
pollDebugState(debugExecutionId);
} catch (error) {
addDebugLog({ level: 'error', message: `${t('workflows.debug.resumeError')}: ${error}` });
addDebugLog({
level: 'error',
message: `${t('workflows.debug.resumeError')}: ${error}`,
});
}
}, [workflowId, debugExecutionId, t, pollDebugState]);
// Step execution
const handleStep = useCallback(async () => {
if (!debugExecutionId) return;
try {
const result = await backendClient.stepWorkflowDebug(workflowId, debugExecutionId);
const result = await backendClient.stepWorkflowDebug(
workflowId,
debugExecutionId,
);
if (result.node_id) {
setCurrentNodeId(result.node_id);
updateNodeExecutionResult(result.node_id, result.node_state as Partial<NodeExecutionResult>);
addDebugLog({
level: 'info',
updateNodeExecutionResult(
result.node_id,
result.node_state as Partial<NodeExecutionResult>,
);
addDebugLog({
level: 'info',
message: t('workflows.debug.steppedTo', { node: result.node_id }),
nodeId: result.node_id,
});
}
if (result.completed) {
setDebugState('completed');
addDebugLog({ level: 'info', message: t('workflows.debug.completed') });
}
} catch (error) {
addDebugLog({ level: 'error', message: `${t('workflows.debug.stepError')}: ${error}` });
addDebugLog({
level: 'error',
message: `${t('workflows.debug.stepError')}: ${error}`,
});
}
}, [workflowId, debugExecutionId, t]);
// Stop execution
const handleStop = useCallback(async () => {
if (!debugExecutionId) return;
try {
await backendClient.stopWorkflowDebug(workflowId, debugExecutionId);
resetDebugState();
addDebugLog({ level: 'info', message: t('workflows.debug.stopped') });
} catch (error) {
addDebugLog({ level: 'error', message: `${t('workflows.debug.stopError')}: ${error}` });
addDebugLog({
level: 'error',
message: `${t('workflows.debug.stopError')}: ${error}`,
});
}
}, [workflowId, debugExecutionId, t, resetDebugState]);
@@ -303,7 +360,10 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
const isRunning = debugState === 'running';
const isPaused = debugState === 'paused';
const canStart = debugState === 'idle' || debugState === 'completed' || debugState === 'error';
const canStart =
debugState === 'idle' ||
debugState === 'completed' ||
debugState === 'error';
return (
<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" />
<span className="font-semibold">{t('workflows.debug.title')}</span>
{debugState !== 'idle' && (
<Badge variant={isRunning ? 'default' : isPaused ? 'secondary' : 'outline'}>
<Badge
variant={
isRunning ? 'default' : isPaused ? 'secondary' : 'outline'
}
>
{t(`workflows.debug.state.${debugState}`)}
</Badge>
)}
@@ -335,7 +399,12 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
) : (
<>
{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" />
{t('workflows.debug.pause')}
</Button>
@@ -345,29 +414,54 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
{t('workflows.debug.resume')}
</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" />
{t('workflows.debug.step')}
</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" />
{t('workflows.debug.stop')}
</Button>
</>
)}
<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" />
</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" />
</Button>
</div>
{/* 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">
<TabsTrigger value="context" className="gap-1">
<Terminal className="size-4" />
@@ -399,19 +493,23 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
<Label>{t('workflows.debug.messageContent')}</Label>
<Textarea
value={debugContext.messageContent}
onChange={(e) => setDebugContext({ messageContent: e.target.value })}
onChange={(e) =>
setDebugContext({ messageContent: e.target.value })
}
placeholder={t('workflows.debug.messageContentPlaceholder')}
className="min-h-[80px]"
disabled={!canStart}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>{t('workflows.debug.senderId')}</Label>
<Input
value={debugContext.senderId}
onChange={(e) => setDebugContext({ senderId: e.target.value })}
onChange={(e) =>
setDebugContext({ senderId: e.target.value })
}
placeholder={t('workflows.debug.senderIdPlaceholder')}
disabled={!canStart}
/>
@@ -420,19 +518,23 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
<Label>{t('workflows.debug.senderName')}</Label>
<Input
value={debugContext.senderName}
onChange={(e) => setDebugContext({ senderName: e.target.value })}
onChange={(e) =>
setDebugContext({ senderName: e.target.value })
}
placeholder={t('workflows.debug.senderNamePlaceholder')}
disabled={!canStart}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>{t('workflows.debug.platform')}</Label>
<Input
value={debugContext.platform}
onChange={(e) => setDebugContext({ platform: e.target.value })}
onChange={(e) =>
setDebugContext({ platform: e.target.value })
}
placeholder={t('workflows.debug.platformPlaceholder')}
disabled={!canStart}
/>
@@ -441,17 +543,21 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
<Label>{t('workflows.debug.conversationId')}</Label>
<Input
value={debugContext.conversationId}
onChange={(e) => setDebugContext({ conversationId: e.target.value })}
onChange={(e) =>
setDebugContext({ conversationId: e.target.value })
}
placeholder={t('workflows.debug.conversationIdPlaceholder')}
disabled={!canStart}
/>
</div>
</div>
<div className="flex items-center gap-2">
<Switch
checked={debugContext.isGroup}
onCheckedChange={(checked) => setDebugContext({ isGroup: checked })}
onCheckedChange={(checked) =>
setDebugContext({ isGroup: checked })
}
disabled={!canStart}
/>
<Label>{t('workflows.debug.isGroup')}</Label>
@@ -460,42 +566,53 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
{/* Custom Variables */}
<Card>
<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">
{t('workflows.debug.customVariablesDesc')}
</CardDescription>
</CardHeader>
<CardContent className="py-2">
<div className="space-y-2">
{Object.entries(debugContext.customVariables).map(([key, value]) => (
<div key={key} className="flex items-center gap-2">
<code className="text-xs bg-muted px-2 py-1 rounded flex-1">
{key}: {JSON.stringify(value)}
</code>
<Button
size="icon"
variant="ghost"
className="size-6"
onClick={() => handleRemoveVariable(key)}
disabled={!canStart}
>
<X className="size-3" />
</Button>
</div>
))}
{Object.entries(debugContext.customVariables).map(
([key, value]) => (
<div key={key} className="flex items-center gap-2">
<code className="text-xs bg-muted px-2 py-1 rounded flex-1">
{key}: {JSON.stringify(value)}
</code>
<Button
size="icon"
variant="ghost"
className="size-6"
onClick={() => handleRemoveVariable(key)}
disabled={!canStart}
>
<X className="size-3" />
</Button>
</div>
),
)}
<div className="flex gap-2">
<Input
placeholder={t('workflows.debug.variableKey')}
value={newVariable.key}
onChange={(e) => setNewVariable({ ...newVariable, key: e.target.value })}
onChange={(e) =>
setNewVariable({ ...newVariable, key: e.target.value })
}
className="text-xs"
disabled={!canStart}
/>
<Input
placeholder={t('workflows.debug.variableValue')}
value={newVariable.value}
onChange={(e) => setNewVariable({ ...newVariable, value: e.target.value })}
onChange={(e) =>
setNewVariable({
...newVariable,
value: e.target.value,
})
}
className="text-xs"
disabled={!canStart}
/>
@@ -512,7 +629,12 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
</CardContent>
</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" />
{t('workflows.debug.resetContext')}
</Button>
@@ -524,7 +646,9 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
<div className="space-y-4">
<Card>
<CardHeader className="py-3">
<CardTitle className="text-sm">{t('workflows.debug.watchedVariables')}</CardTitle>
<CardTitle className="text-sm">
{t('workflows.debug.watchedVariables')}
</CardTitle>
</CardHeader>
<CardContent className="py-2">
{watchedVariables.length === 0 ? (
@@ -549,7 +673,10 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
return (
<div key={variable} className="flex items-center gap-2">
<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>
<Button
size="icon"
@@ -570,7 +697,9 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
{/* Node Outputs */}
<Card>
<CardHeader className="py-3">
<CardTitle className="text-sm">{t('workflows.debug.nodeOutputs')}</CardTitle>
<CardTitle className="text-sm">
{t('workflows.debug.nodeOutputs')}
</CardTitle>
</CardHeader>
<CardContent className="py-2">
{Object.keys(nodeExecutionResults).length === 0 ? (
@@ -579,33 +708,41 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
</p>
) : (
<div className="space-y-2">
{Object.entries(nodeExecutionResults).map(([nodeId, result]) => {
const node = nodes.find((n) => n.id === nodeId);
return (
<Collapsible key={nodeId}>
<CollapsibleTrigger className="flex items-center gap-2 w-full text-left">
<ChevronRight className="size-4 transition-transform data-[state=open]:rotate-90" />
<span className="text-sm font-medium">
{node?.data.label || nodeId}
</span>
{result.outputs && Object.keys(result.outputs).length > 0 && (
<Badge variant="secondary" className="text-xs">
{Object.keys(result.outputs).length} outputs
</Badge>
)}
</CollapsibleTrigger>
<CollapsibleContent className="pl-6 pt-2">
{result.outputs ? (
<pre className="text-xs bg-muted p-2 rounded overflow-auto max-h-40">
{JSON.stringify(result.outputs, null, 2)}
</pre>
) : (
<p className="text-xs text-muted-foreground">No outputs</p>
)}
</CollapsibleContent>
</Collapsible>
);
})}
{Object.entries(nodeExecutionResults).map(
([nodeId, result]) => {
const node = nodes.find((n) => n.id === nodeId);
return (
<Collapsible key={nodeId}>
<CollapsibleTrigger className="flex items-center gap-2 w-full text-left">
<ChevronRight className="size-4 transition-transform data-[state=open]:rotate-90" />
<span className="text-sm font-medium">
{node?.data.label || nodeId}
</span>
{result.outputs &&
Object.keys(result.outputs).length > 0 && (
<Badge
variant="secondary"
className="text-xs"
>
{Object.keys(result.outputs).length} outputs
</Badge>
)}
</CollapsibleTrigger>
<CollapsibleContent className="pl-6 pt-2">
{result.outputs ? (
<pre className="text-xs bg-muted p-2 rounded overflow-auto max-h-40">
{JSON.stringify(result.outputs, null, 2)}
</pre>
) : (
<p className="text-xs text-muted-foreground">
No outputs
</p>
)}
</CollapsibleContent>
</Collapsible>
);
},
)}
</div>
)}
</CardContent>
@@ -618,8 +755,12 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
<div className="space-y-2">
{nodes.map((node) => {
const result = nodeExecutionResults[node.id];
const StatusIcon = result ? statusIcons[result.status] || Clock : Clock;
const statusColor = result ? statusColors[result.status] || '' : 'text-gray-400';
const StatusIcon = result
? statusIcons[result.status] || Clock
: Clock;
const statusColor = result
? statusColors[result.status] || ''
: 'text-gray-400';
const isCurrentNode = currentNodeId === node.id;
const hasBreakpoint = !!breakpoints[node.id];
@@ -641,7 +782,9 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
title={t('workflows.debug.toggleBreakpoint')}
/>
<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">
{node.data.type}
</Badge>
@@ -675,7 +818,9 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
onCheckedChange={setAutoScroll}
className="scale-75"
/>
<Label className="text-xs">{t('workflows.debug.autoScroll')}</Label>
<Label className="text-xs">
{t('workflows.debug.autoScroll')}
</Label>
</div>
<span className="text-xs text-muted-foreground">
{debugLogs.length} {t('workflows.debug.logEntries')}
@@ -684,18 +829,24 @@ export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebugg
<ScrollArea className="flex-1 p-4">
<div className="space-y-1 font-mono text-xs">
{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) => (
<div key={log.id} className="flex gap-2">
<span className="text-muted-foreground shrink-0">
{new Date(log.timestamp).toLocaleTimeString()}
</span>
<span className={`shrink-0 uppercase ${logLevelColors[log.level]}`}>
<span
className={`shrink-0 uppercase ${logLevelColors[log.level]}`}
>
[{log.level}]
</span>
{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>
{log.data && (

View File

@@ -46,7 +46,9 @@ interface NodeTypeForUI {
}
// 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,
category: type.split('.')[0],
labelKey: keys.labelKey,
@@ -56,45 +58,51 @@ const defaultNodeTypes: NodeTypeForUI[] = Object.entries(NODE_TYPE_I18N_KEYS).ma
export default function NodePalette() {
const { t, i18n } = useTranslation();
const { nodeTypes: backendNodeTypes, nodeCategories } = useWorkflowStore();
// Search state
const [searchQuery, setSearchQuery] = useState('');
// Expanded categories state
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
const getNodeLabel = useCallback((node: NodeTypeForUI): string => {
if (node.labelKey) {
return t(node.labelKey, { defaultValue: node.labelKey });
}
if (node.label) {
const labelValue = resolveI18nLabel(node.label);
return labelValue || node.type;
}
return node.type;
}, [t]);
const getNodeLabel = useCallback(
(node: NodeTypeForUI): string => {
if (node.labelKey) {
return t(node.labelKey, { defaultValue: node.labelKey });
}
if (node.label) {
const labelValue = resolveI18nLabel(node.label);
return labelValue || node.type;
}
return node.type;
},
[t],
);
// Helper: get description string for a node using i18n
const getNodeDescription = useCallback((node: NodeTypeForUI): string => {
if (node.descriptionKey) {
return t(node.descriptionKey, { defaultValue: '' });
}
if (node.description) {
const descValue = resolveI18nLabel(node.description);
return descValue || '';
}
return '';
}, [t]);
const getNodeDescription = useCallback(
(node: NodeTypeForUI): string => {
if (node.descriptionKey) {
return t(node.descriptionKey, { defaultValue: '' });
}
if (node.description) {
const descValue = resolveI18nLabel(node.description);
return descValue || '';
}
return '';
},
[t],
);
// Use backend node types if available, otherwise use defaults
const nodeTypes = useMemo((): NodeTypeForUI[] => {
if (backendNodeTypes && backendNodeTypes.length > 0) {
return backendNodeTypes.map((node) => {
const i18nKeys = findNodeI18nKeys(node.type);
return {
type: node.type,
category: node.category,
@@ -129,13 +137,19 @@ export default function NodePalette() {
// Group filtered nodes by category
const groupedNodes = useMemo(() => {
const groups: Record<string, typeof nodeTypes> = {};
const categoryOrder = ['trigger', 'process', 'control', 'action', 'integration'];
const categoryOrder = [
'trigger',
'process',
'control',
'action',
'integration',
];
// Initialize groups in order
categoryOrder.forEach((cat) => {
groups[cat] = [];
});
for (const node of filteredNodes) {
const category = node.category || node.type.split('.')[0];
if (!groups[category]) {
@@ -143,14 +157,14 @@ export default function NodePalette() {
}
groups[category].push(node);
}
// Remove empty categories
Object.keys(groups).forEach((key) => {
if (groups[key].length === 0) {
delete groups[key];
}
});
return groups;
}, [filteredNodes]);
@@ -174,7 +188,7 @@ export default function NodePalette() {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', nodeType);
},
[]
[],
);
// Get category label using i18n
@@ -189,13 +203,18 @@ export default function NodePalette() {
if (category?.label) {
const lang = i18n.language;
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 categoryName;
},
[nodeCategories, t, i18n.language]
[nodeCategories, t, i18n.language],
);
// Clear search
@@ -204,131 +223,142 @@ export default function NodePalette() {
}, []);
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="p-3 border-b">
<h3 className="font-semibold text-sm mb-3 flex items-center gap-2">
<Layers className="size-4" />
{t('workflows.nodePalette')}
</h3>
{/* Search input */}
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input
type="text"
placeholder={t('workflows.searchNodes')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8 pr-8 h-8 text-sm"
/>
{searchQuery && (
<div className="h-full flex flex-col">
{/* Header */}
<div className="p-3 border-b">
<h3 className="font-semibold text-sm mb-3 flex items-center gap-2">
<Layers className="size-4" />
{t('workflows.nodePalette')}
</h3>
{/* Search input */}
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input
type="text"
placeholder={t('workflows.searchNodes')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8 pr-8 h-8 text-sm"
/>
{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
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>
)}
</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>
</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 (
<div key={category} className="mb-1">
{/* Category header */}
<button
onClick={clearSearch}
className="text-primary hover:underline mt-2"
onClick={() => toggleCategory(category)}
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>
</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 (
<div key={category} className="mb-1">
{/* Category header */}
<button
onClick={() => toggleCategory(category)}
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]
)}
>
<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 */}
{isExpanded && (
<div className="mt-1 space-y-0.5 ml-2">
{nodes.map((node) => {
const Icon = nodeIcons[node.type] || Settings;
const label = getNodeLabel(node);
const description = getNodeDescription(node);
{/* Node list */}
{isExpanded && (
<div className="mt-1 space-y-0.5 ml-2">
{nodes.map((node) => {
const Icon = nodeIcons[node.type] || Settings;
const label = getNodeLabel(node);
const description = getNodeDescription(node);
return (
return (
<div
key={node.type}
draggable
onDragStart={(e) => onDragStart(e, node.type)}
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
key={node.type}
draggable
onDragStart={(e) => onDragStart(e, node.type)}
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',
categoryBgColors[category],
categoryBorderColors[category],
'border'
)}>
<Icon className={cn('size-3.5', categoryColors[category])} />
</div>
<span className="text-sm truncate flex-1">{label}</span>
<ExternalLink className="size-3 opacity-0 group-hover:opacity-50 transition-opacity" />
'border',
)}
>
<Icon
className={cn(
'size-3.5',
categoryColors[category],
)}
/>
</div>
);
})}
</div>
)}
</div>
);
})}
</div>
</div>
{/* Footer hint */}
<div className="p-2 border-t text-xs text-muted-foreground text-center">
{t('workflows.dragToAdd')}
<span className="text-sm truncate flex-1">
{label}
</span>
<ExternalLink className="size-3 opacity-0 group-hover:opacity-50 transition-opacity" />
</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>
);
}

View File

@@ -60,7 +60,9 @@ const translateIfKey = (value: string | undefined): string | undefined => {
};
// 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;
const result = resolveI18nLabel(obj as Record<string, string>);
return result || undefined;
@@ -112,23 +114,28 @@ const resolvePortDisplayLabel = (
function VariableReference({
variable,
onCopy
onCopy,
}: {
variable: { name: string; label?: string; type?: string };
onCopy: (ref: string) => void;
}) {
const ref = `{{${variable.name}}}`;
const displayName = variable.label || formatVariableName(variable.name);
return (
<Tooltip>
<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 gap-2 min-w-0 flex-1 overflow-hidden">
<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 && (
<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)}
</Badge>
)}
@@ -165,9 +172,13 @@ function CollapsibleSection({
badge?: React.ReactNode;
}) {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="w-full overflow-hidden">
<Collapsible
open={isOpen}
onOpenChange={setIsOpen}
className="w-full overflow-hidden"
>
<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">
{isOpen ? (
@@ -176,14 +187,14 @@ function CollapsibleSection({
<ChevronRight 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}
</button>
</CollapsibleTrigger>
<CollapsibleContent className="pl-6 pr-1 pb-2 w-full overflow-hidden">
<div className="w-full overflow-hidden">
{children}
</div>
<div className="w-full overflow-hidden">{children}</div>
</CollapsibleContent>
</Collapsible>
);
@@ -243,9 +254,12 @@ export default function PropertyPanel({
// 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.
const configSchema = useMemo(() => {
const localConfigSchema = (localNodeConfig?.configSchema as IDynamicFormItemSchema[]) || [];
const backendConfigSchema = (nodeTypeMeta?.config_schema as IDynamicFormItemSchema[]) || [];
const rawConfigSchema = localConfigSchema.length > 0 ? localConfigSchema : backendConfigSchema;
const localConfigSchema =
(localNodeConfig?.configSchema as IDynamicFormItemSchema[]) || [];
const backendConfigSchema =
(nodeTypeMeta?.config_schema as IDynamicFormItemSchema[]) || [];
const rawConfigSchema =
localConfigSchema.length > 0 ? localConfigSchema : backendConfigSchema;
return rawConfigSchema.map((item) => {
const backendItem = backendConfigSchema.find(
@@ -255,14 +269,16 @@ export default function PropertyPanel({
return {
...(backendItem || {}),
...item,
label: item.label || backendItem?.label || {
en_US: item.name,
zh_Hans: item.name,
},
description: item.description || backendItem?.description || {
en_US: '',
zh_Hans: '',
},
label: item.label ||
backendItem?.label || {
en_US: item.name,
zh_Hans: item.name,
},
description: item.description ||
backendItem?.description || {
en_US: '',
zh_Hans: '',
},
options: item.options || backendItem?.options,
show_if: item.show_if || backendItem?.show_if,
};
@@ -272,13 +288,17 @@ export default function PropertyPanel({
// Get available input variables from connected upstream nodes
const availableInputVariables = useMemo(() => {
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
const incomingEdges = edges.filter((e) => e.target === selectedNode.id);
const upstreamNodeIds = incomingEdges.map((e) => e.source);
for (const nodeId of upstreamNodeIds) {
const node = nodes.find((n) => n.id === nodeId);
if (node) {
@@ -294,20 +314,40 @@ export default function PropertyPanel({
});
}
}
// Add global variables
variables.push({
nodeId: '__global__',
nodeLabel: t('workflows.globalVariables'),
outputs: [
{ name: 'message.content', label: t('workflows.messageContent'), type: 'string' },
{ 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' },
{
name: 'message.content',
label: t('workflows.messageContent'),
type: 'string',
},
{
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;
}, [selectedNode, edges, nodes, t]);
@@ -318,7 +358,7 @@ export default function PropertyPanel({
updateNodeLabel(selectedNodeId, e.target.value);
}
},
[selectedNodeId, updateNodeLabel]
[selectedNodeId, updateNodeLabel],
);
// Handle config change from dynamic form
@@ -330,7 +370,7 @@ export default function PropertyPanel({
}
return undefined;
},
[selectedNodeId, updateNodeConfig, pushHistory]
[selectedNodeId, updateNodeConfig, pushHistory],
);
// Handle node delete
@@ -354,14 +394,17 @@ export default function PropertyPanel({
updateEdgeCondition(selectedEdgeId, e.target.value);
}
},
[selectedEdgeId, updateEdgeCondition]
[selectedEdgeId, updateEdgeCondition],
);
// Copy variable reference
const handleCopyVariable = useCallback((ref: string) => {
navigator.clipboard.writeText(ref);
toast.success(t('common.copySuccess'));
}, [t]);
const handleCopyVariable = useCallback(
(ref: string) => {
navigator.clipboard.writeText(ref);
toast.success(t('common.copySuccess'));
},
[t],
);
// No selection
if (!selectedNodeId && !selectedEdgeId) {
@@ -408,11 +451,17 @@ export default function PropertyPanel({
{/* Connection info */}
<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">
<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}
</Badge>
<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}
</Badge>
</div>
@@ -422,11 +471,16 @@ export default function PropertyPanel({
<CollapsibleSection
title={t('workflows.condition')}
icon={Code}
badge={selectedEdge.data?.condition ? (
<Badge variant="secondary" className="text-xs flex-shrink-0">
{t('workflows.hasCondition')}
</Badge>
) : null}
badge={
selectedEdge.data?.condition ? (
<Badge
variant="secondary"
className="text-xs flex-shrink-0"
>
{t('workflows.hasCondition')}
</Badge>
) : null
}
>
<div className="space-y-2 w-full overflow-hidden">
<Textarea
@@ -451,19 +505,28 @@ export default function PropertyPanel({
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm" className="w-full">
<Trash2 className="size-4 mr-2 flex-shrink-0" />
<span className="truncate">{t('workflows.deleteEdge')}</span>
<span className="truncate">
{t('workflows.deleteEdge')}
</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('workflows.deleteEdgeConfirm')}</AlertDialogTitle>
<AlertDialogTitle>
{t('workflows.deleteEdgeConfirm')}
</AlertDialogTitle>
<AlertDialogDescription>
{t('workflows.deleteEdgeConfirmDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="flex-wrap gap-2">
<AlertDialogCancel className="flex-1 min-w-[80px]">{t('common.cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteEdge} className="flex-1 min-w-[80px]">
<AlertDialogCancel className="flex-1 min-w-[80px]">
{t('common.cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteEdge}
className="flex-1 min-w-[80px]"
>
{t('common.delete')}
</AlertDialogAction>
</AlertDialogFooter>
@@ -479,9 +542,11 @@ export default function PropertyPanel({
// Node selected
if (selectedNode) {
const nodeInputs = selectedNode.data.inputs || nodeTypeMeta?.inputs || [{ name: 'input', type: 'any' }];
const nodeOutputs = selectedNode.data.outputs || nodeTypeMeta?.outputs || [{ name: 'output', type: 'any' }];
const nodeInputs = selectedNode.data.inputs ||
nodeTypeMeta?.inputs || [{ name: 'input', type: 'any' }];
const nodeOutputs = selectedNode.data.outputs ||
nodeTypeMeta?.outputs || [{ name: 'output', type: 'any' }];
// Extract i18n labels using extractI18nLabel
const nodeLabel = nodeTypeMeta?.label
? extractI18nLabel(nodeTypeMeta.label)
@@ -489,7 +554,7 @@ export default function PropertyPanel({
const nodeDescription = nodeTypeMeta?.description
? extractI18nLabel(nodeTypeMeta.description)
: undefined;
// Get node category color from local config
const nodeColor = localNodeConfig?.color || nodeTypeMeta?.color;
@@ -514,7 +579,9 @@ export default function PropertyPanel({
</Badge>
</div>
{nodeDescription && (
<p className="text-xs text-muted-foreground">{nodeDescription}</p>
<p className="text-xs text-muted-foreground">
{nodeDescription}
</p>
)}
</div>
@@ -536,7 +603,7 @@ export default function PropertyPanel({
className="h-8 w-full"
/>
</div>
<div className="space-y-1.5 w-full overflow-hidden">
<Label className="text-xs text-muted-foreground">
{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" />
<span className="font-mono text-xs truncate min-w-0 flex-1">
{resolvePortDisplayLabel(input, 'workflows.nodeInputs')}
{resolvePortDisplayLabel(
input,
'workflows.nodeInputs',
)}
</span>
{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)}
</Badge>
)}
@@ -607,7 +680,10 @@ export default function PropertyPanel({
key={output.name}
variable={{
name: `nodes.${selectedNode.id}.${output.name}`,
label: resolvePortDisplayLabel(output, 'workflows.nodeOutputs'),
label: resolvePortDisplayLabel(
output,
'workflows.nodeOutputs',
),
type: output.type,
}}
onCopy={handleCopyVariable}
@@ -627,7 +703,10 @@ export default function PropertyPanel({
>
<div className="space-y-3 w-full overflow-hidden">
{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">
{group.nodeLabel}
</div>
@@ -652,7 +731,10 @@ export default function PropertyPanel({
title={t('workflows.nodeConfig')}
icon={Settings}
badge={
<Badge variant="secondary" className="text-xs flex-shrink-0">
<Badge
variant="secondary"
className="text-xs flex-shrink-0"
>
{configSchema.length}
</Badge>
}
@@ -660,7 +742,9 @@ export default function PropertyPanel({
<div className="space-y-2 w-full overflow-hidden box-border">
<DynamicFormComponent
itemConfigList={configSchema}
initialValues={selectedNode.data.config as Record<string, object>}
initialValues={
selectedNode.data.config as Record<string, object>
}
onSubmit={handleConfigChange}
/>
</div>
@@ -683,19 +767,28 @@ export default function PropertyPanel({
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm" className="w-full">
<Trash2 className="size-4 mr-2 flex-shrink-0" />
<span className="truncate">{t('workflows.deleteNode')}</span>
<span className="truncate">
{t('workflows.deleteNode')}
</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('workflows.deleteNodeConfirm')}</AlertDialogTitle>
<AlertDialogTitle>
{t('workflows.deleteNodeConfirm')}
</AlertDialogTitle>
<AlertDialogDescription>
{t('workflows.deleteNodeConfirmDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="flex-wrap gap-2">
<AlertDialogCancel className="flex-1 min-w-[80px]">{t('common.cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteNode} className="flex-1 min-w-[80px]">
<AlertDialogCancel className="flex-1 min-w-[80px]">
{t('common.cancel')}
</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteNode}
className="flex-1 min-w-[80px]"
>
{t('common.delete')}
</AlertDialogAction>
</AlertDialogFooter>

View File

@@ -18,7 +18,11 @@ import {
TooltipProvider,
TooltipTrigger,
} 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 type { I18nObject } from '@/app/infra/entities/common';
@@ -31,14 +35,17 @@ const nodeTypeI18nKeys: Record<string, string> = Object.fromEntries(
);
// Category colors with improved design
const categoryColors: Record<string, {
bg: string;
border: string;
text: string;
icon: string;
gradient: string;
handleBg: string;
}> = {
const categoryColors: Record<
string,
{
bg: string;
border: string;
text: string;
icon: string;
gradient: string;
handleBg: string;
}
> = {
trigger: {
bg: 'bg-amber-50 dark:bg-amber-950/40',
border: 'border-amber-400 dark:border-amber-600',
@@ -82,15 +89,24 @@ const categoryColors: Record<string, {
};
// 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
const statusConfig: Record<NodeExecutionStatus, {
icon: React.ElementType;
color: string;
bgColor: string;
animate?: boolean;
}> = {
const statusConfig: Record<
NodeExecutionStatus,
{
icon: React.ElementType;
color: string;
bgColor: string;
animate?: boolean;
}
> = {
idle: {
icon: Play,
color: 'text-muted-foreground',
@@ -128,8 +144,16 @@ export interface WorkflowNodeData extends Record<string, unknown> {
label: string;
type: string;
config: Record<string, unknown>;
inputs?: { name: string; label?: string | Record<string, string> | I18nObject; type?: string }[];
outputs?: { name: string; label?: string | Record<string, string> | I18nObject; type?: string }[];
inputs?: {
name: string;
label?: string | Record<string, string> | I18nObject;
type?: string;
}[];
outputs?: {
name: string;
label?: string | Record<string, string> | I18nObject;
type?: string;
}[];
// Execution state
executionStatus?: NodeExecutionStatus;
executionError?: string;
@@ -152,7 +176,10 @@ function getPortLabel(
}
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 label;
@@ -164,17 +191,22 @@ function getPortLabel(
}
// 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);
}
// Helper function to get node type description: show the raw type name after the dot
function getNodeTypeDescription(
nodeType: string,
nodeType: string,
t: (key: string, options?: { defaultValue: string }) => string,
nodeTypeLabel?: Record<string, string>
nodeTypeLabel?: Record<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) {
@@ -191,11 +223,19 @@ function WorkflowNodeComponent({ data, selected }: NodeProps) {
// Get inputs and outputs with defaults (use i18n keys for default labels)
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]);
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]);
// 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',
colors.bg,
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 === '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 */}
{!isTrigger && inputs.map((input, index) => (
<Tooltip key={`input-${input.name}`}>
<TooltipTrigger asChild>
<Handle
type="target"
position={Position.Left}
id={input.name}
style={{
top: inputs.length === 1 ? '50%' : `${((index + 1) / (inputs.length + 1)) * 100}%`,
background: colors.handleBg,
width: 12,
height: 12,
border: '2px solid white',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
}}
className="!transition-transform hover:!scale-125"
/>
</TooltipTrigger>
<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>
))}
{!isTrigger &&
inputs.map((input, index) => (
<Tooltip key={`input-${input.name}`}>
<TooltipTrigger asChild>
<Handle
type="target"
position={Position.Left}
id={input.name}
style={{
top:
inputs.length === 1
? '50%'
: `${((index + 1) / (inputs.length + 1)) * 100}%`,
background: colors.handleBg,
width: 12,
height: 12,
border: '2px solid white',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
}}
className="!transition-transform hover:!scale-125"
/>
</TooltipTrigger>
<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 */}
<div className={cn('p-3 bg-gradient-to-b', colors.gradient)}>
{/* Header row with icon and status */}
<div className="flex items-start gap-2.5">
{/* Node icon */}
<div className={cn(
'p-2 rounded-lg shrink-0',
colors.icon,
'bg-white/60 dark:bg-black/20',
'shadow-sm'
)}>
<div
className={cn(
'p-2 rounded-lg shrink-0',
colors.icon,
'bg-white/60 dark:bg-black/20',
'shadow-sm',
)}
>
<Icon className="size-5" />
</div>
{/* Title and type */}
<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}
</div>
<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>
{/* Status indicator */}
{status !== 'idle' && (
<div className={cn(
'p-1 rounded-full shrink-0',
statusInfo.bgColor
)}>
<StatusIcon
<div
className={cn('p-1 rounded-full shrink-0', statusInfo.bgColor)}
>
<StatusIcon
className={cn(
'size-4',
statusInfo.color,
statusInfo.animate && 'animate-spin'
)}
statusInfo.animate && 'animate-spin',
)}
/>
</div>
)}
@@ -291,16 +353,20 @@ function WorkflowNodeComponent({ data, selected }: NodeProps) {
{/* Execution info */}
{(status === 'completed' || status === 'failed') && (
<div className={cn(
'mt-2 pt-2 border-t flex items-center justify-between text-xs',
'border-black/5 dark:border-white/5'
)}>
<div
className={cn(
'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)}>
<StatusIcon className="size-3" />
<span className="capitalize">{status}</span>
</div>
{formattedDuration && (
<span className="text-muted-foreground">{formattedDuration}</span>
<span className="text-muted-foreground">
{formattedDuration}
</span>
)}
</div>
)}
@@ -329,7 +395,10 @@ function WorkflowNodeComponent({ data, selected }: NodeProps) {
position={Position.Right}
id={output.name}
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,
width: 12,
height: 12,
@@ -340,12 +409,20 @@ function WorkflowNodeComponent({ data, selected }: NodeProps) {
/>
</TooltipTrigger>
<TooltipContent side="right">
<p className="font-medium">{getPortLabel(output.label, output.name, 'workflows.nodeOutputs', t)}</p>
{output.type && <p className="text-xs text-muted-foreground">{output.type}</p>}
<p className="font-medium">
{getPortLabel(
output.label,
output.name,
'workflows.nodeOutputs',
t,
)}
</p>
{output.type && (
<p className="text-xs text-muted-foreground">{output.type}</p>
)}
</TooltipContent>
</Tooltip>
))}
</div>
</TooltipProvider>
);

View File

@@ -1,5 +1,8 @@
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 PropertyPanel } from './PropertyPanel';

View File

@@ -1,6 +1,6 @@
/**
* AI Node Configurations
*
*
* Defines configurations for all AI-related node types:
* - llm_call: Call a large language model
* - question_classifier: Classify user questions into categories
@@ -80,8 +80,10 @@ export const llmCallConfig: NodeConfigMeta = {
zh_Hans: '系统提示词',
},
description: {
en_US: 'System prompt to set the model behavior (supports variable interpolation with {{variable}})',
zh_Hans: '设置模型行为的系统提示词(支持使用 {{variable}} 进行变量插值)',
en_US:
'System prompt to set the model behavior (supports variable interpolation with {{variable}})',
zh_Hans:
'设置模型行为的系统提示词(支持使用 {{variable}} 进行变量插值)',
},
required: false,
default: '',
@@ -95,8 +97,10 @@ export const llmCallConfig: NodeConfigMeta = {
zh_Hans: '用户提示词模板',
},
description: {
en_US: 'User prompt template with variable placeholders (e.g., {{input}}, {{context.key}})',
zh_Hans: '带有变量占位符的用户提示词模板(例如 {{input}}{{context.key}}',
en_US:
'User prompt template with variable placeholders (e.g., {{input}}, {{context.key}})',
zh_Hans:
'带有变量占位符的用户提示词模板(例如 {{input}}、{{context.key}}',
},
required: true,
default: '{{input}}',
@@ -110,7 +114,8 @@ export const llmCallConfig: NodeConfigMeta = {
zh_Hans: '温度',
},
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 = 非常随机)',
},
required: false,
@@ -125,7 +130,8 @@ export const llmCallConfig: NodeConfigMeta = {
zh_Hans: '最大令牌数',
},
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 使用模型默认值)',
},
required: false,
@@ -148,7 +154,10 @@ export const llmCallConfig: NodeConfigMeta = {
options: [
{ name: 'text', label: { en_US: 'Plain Text', zh_Hans: '纯文本' } },
{ 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: '分类定义',
},
description: {
en_US: 'Define categories in JSON format: [{"name": "category1", "description": "...", "examples": ["..."]}]',
zh_Hans: '使用 JSON 格式定义分类: [{"name": "分类1", "description": "...", "examples": ["..."]}]',
en_US:
'Define categories in JSON format: [{"name": "category1", "description": "...", "examples": ["..."]}]',
zh_Hans:
'使用 JSON 格式定义分类: [{"name": "分类1", "description": "...", "examples": ["..."]}]',
},
required: true,
default: '[]',
@@ -389,8 +400,10 @@ export const parameterExtractorConfig: NodeConfigMeta = {
zh_Hans: '参数架构',
},
description: {
en_US: 'JSON array defining expected parameters: [{"name": "date", "type": "string", "description": "Meeting date", "required": true}]',
zh_Hans: '定义期望参数的 JSON 数组: [{"name": "日期", "type": "string", "description": "会议日期", "required": true}]',
en_US:
'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,
default: '[]',
@@ -445,7 +458,8 @@ export const knowledgeRetrievalConfig: NodeConfigMeta = {
zh_Hans: '知识检索',
},
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: '使用语义搜索从知识库中检索相关信息',
},
icon: 'BookOpen',
@@ -532,9 +546,18 @@ export const knowledgeRetrievalConfig: NodeConfigMeta = {
required: false,
default: 'vector',
options: [
{ name: 'vector', 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: '关键词检索' } },
{
name: 'vector',
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: '意图定义',
},
description: {
en_US: 'Define intents in JSON format: [{"name": "intent1", "description": "...", "examples": ["..."]}]',
zh_Hans: '使用 JSON 格式定义意图: [{"name": "意图1", "description": "...", "examples": ["..."]}]',
en_US:
'Define intents in JSON format: [{"name": "intent1", "description": "...", "examples": ["..."]}]',
zh_Hans:
'使用 JSON 格式定义意图: [{"name": "意图1", "description": "...", "examples": ["..."]}]',
},
required: true,
default: '[]',

View File

@@ -1,6 +1,6 @@
/**
* Control Node Configurations
*
*
* Defines configurations for flow control node types:
* - condition: Conditional branching
* - switch_case: Multi-way branching
@@ -62,10 +62,16 @@ export const conditionConfig: NodeConfigMeta = {
required: true,
default: 'expression',
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: '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',
options: [
{ 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: 'gte', label: { en_US: 'Greater or Equal (>=)', zh_Hans: '大于等于 (>=)' } },
{
name: 'neq',
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: '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: 'starts_with', 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: '匹配正则' } },
{
name: 'starts_with',
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: {
field: 'condition_type',
@@ -261,11 +288,14 @@ export const switchCaseConfig: NodeConfigMeta = {
zh_Hans: '情况',
},
description: {
en_US: '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"]}]',
en_US:
'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,
default: '[{"name": "case_1", "value": ""}, {"name": "case_2", "value": ""}]',
default:
'[{"name": "case_1", "value": ""}, {"name": "case_2", "value": ""}]',
},
{
id: 'case_sensitive',
@@ -502,8 +532,10 @@ export const parallelConfig: NodeConfigMeta = {
zh_Hans: '分支',
},
description: {
en_US: 'Define branches as JSON array: [{"name": "branch_1"}, {"name": "branch_2"}]',
zh_Hans: '使用 JSON 数组定义分支: [{"name": "branch_1"}, {"name": "branch_2"}]',
en_US:
'Define branches as JSON array: [{"name": "branch_1"}, {"name": "branch_2"}]',
zh_Hans:
'使用 JSON 数组定义分支: [{"name": "branch_1"}, {"name": "branch_2"}]',
},
required: true,
default: '[{"name": "branch_1"}, {"name": "branch_2"}]',
@@ -763,7 +795,10 @@ export const iteratorConfig: NodeConfigMeta = {
name: 'parallel',
type: DynamicFormItemType.BOOLEAN,
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,
default: false,
},
@@ -772,7 +807,10 @@ export const iteratorConfig: NodeConfigMeta = {
name: 'max_concurrency',
type: DynamicFormItemType.INT,
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,
default: 5,
show_if: { field: 'parallel', operator: 'eq', value: true },
@@ -782,7 +820,10 @@ export const iteratorConfig: NodeConfigMeta = {
name: 'max_iterations',
type: DynamicFormItemType.INT,
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,
default: 1000,
},
@@ -831,14 +872,29 @@ export const mergeConfig: NodeConfigMeta = {
name: 'merge_strategy',
type: DynamicFormItemType.SELECT,
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,
default: 'wait_all',
options: [
{ name: 'wait_all', label: { en_US: 'Wait for All', 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: '收集为数组' } },
{
name: 'wait_all',
label: { en_US: 'Wait for All', 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',
type: DynamicFormItemType.TEXT,
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,
default: '{}',
},
@@ -890,13 +950,25 @@ export const variableAggregatorConfig: NodeConfigMeta = {
name: 'aggregation_mode',
type: DynamicFormItemType.SELECT,
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,
default: 'merge',
options: [
{ name: 'merge', 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: '第一个非空' } },
{
name: 'merge',
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: '第一个非空' },
},
],
},
],

View File

@@ -1,6 +1,6 @@
/**
* Node Configurations Index
*
*
* This module exports all node configuration metadata and provides
* utility functions for accessing node configurations.
*/
@@ -9,7 +9,7 @@
export * from './types';
// Trigger Nodes
export {
export {
triggerConfigs,
getTriggerConfig,
messageTriggerConfig,
@@ -147,7 +147,9 @@ export function getNodeConfig(nodeType: string): NodeConfigMeta | undefined {
/**
* 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);
}
@@ -171,18 +173,18 @@ export function isValidNodeType(nodeType: string): boolean {
export function getDefaultConfig(nodeType: string): Record<string, unknown> {
const config = getNodeConfig(nodeType);
if (!config) return {};
// Build default config from schema defaults
const defaults: Record<string, unknown> = {};
for (const field of config.configSchema) {
defaults[field.name] = field.default;
}
// Override with explicit defaultConfig if provided
if (config.defaultConfig) {
Object.assign(defaults, config.defaultConfig);
}
return defaults;
}
@@ -191,32 +193,35 @@ export function getDefaultConfig(nodeType: string): Record<string, unknown> {
*/
export function validateNodeConfig(
nodeType: string,
config: Record<string, unknown>
config: Record<string, unknown>,
): { valid: boolean; errors: string[] } {
const nodeConfig = getNodeConfig(nodeType);
if (!nodeConfig) {
return { valid: false, errors: [`Unknown node type: ${nodeType}`] };
}
const errors: string[] = [];
for (const field of nodeConfig.configSchema) {
const value = config[field.name];
// 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`);
continue;
}
// Skip validation for optional empty fields
if (!field.required && (value === undefined || value === null)) {
continue;
}
// Type-specific validation could be added here
}
return { valid: errors.length === 0, errors };
}

View File

@@ -1,6 +1,6 @@
/**
* Integration Node Configurations
*
*
* Defines configurations for integration node types:
* - database_query: Query databases
* - redis_operation: Redis operations
@@ -64,7 +64,10 @@ export const databaseQueryConfig: NodeConfigMeta = {
required: true,
default: 'postgresql',
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: 'sqlite', label: { en_US: 'SQLite', zh_Hans: 'SQLite' } },
],
@@ -372,7 +375,8 @@ export const mcpToolConfig: NodeConfigMeta = {
zh_Hans: '参数模板',
},
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 格式,支持变量插值)。留空则使用输入。',
},
required: false,
@@ -537,27 +541,78 @@ export const memoryStoreConfig: NodeConfigMeta = {
export const difyWorkflowConfig: NodeConfigMeta = {
nodeType: 'dify_workflow',
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',
category: 'integration',
color: '#ec4899',
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: [
createOutput('result', 'any', { description: 'Workflow result', label: { en_US: 'Result', zh_Hans: '结果' } }),
createOutput('success', 'boolean', { description: 'Whether call was successful', label: { en_US: 'Success', zh_Hans: '成功' } }),
createOutput('result', 'any', {
description: 'Workflow result',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether call was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
],
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: '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 },
{
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: '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 = {
nodeType: 'dify_knowledge_query',
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',
category: 'integration',
color: '#ec4899',
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: [
createOutput('results', 'array', { description: 'Search results', label: { en_US: 'Results', zh_Hans: '结果' } }),
createOutput('success', 'boolean', { description: 'Whether query was successful', label: { en_US: 'Success', zh_Hans: '成功' } }),
createOutput('results', 'array', {
description: 'Search results',
label: { en_US: 'Results', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether query was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
],
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: '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 },
{
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: '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 },
};
@@ -592,20 +694,49 @@ export const difyKnowledgeQueryConfig: NodeConfigMeta = {
export const n8nWorkflowConfig: NodeConfigMeta = {
nodeType: 'n8n_workflow',
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',
category: 'integration',
color: '#ec4899',
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: [
createOutput('result', 'any', { description: 'Workflow result', label: { en_US: 'Result', zh_Hans: '结果' } }),
createOutput('success', 'boolean', { description: 'Whether call was successful', label: { en_US: 'Success', zh_Hans: '成功' } }),
createOutput('result', 'any', {
description: 'Workflow result',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether call was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
],
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 },
};
@@ -621,17 +752,65 @@ export const langflowFlowConfig: NodeConfigMeta = {
category: 'integration',
color: '#ec4899',
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: [
createOutput('result', 'any', { description: 'Flow result', label: { en_US: 'Result', zh_Hans: '结果' } }),
createOutput('success', 'boolean', { description: 'Whether call was successful', label: { en_US: 'Success', zh_Hans: '成功' } }),
createOutput('result', 'any', {
description: 'Flow result',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether call was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
],
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: '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 },
{
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: '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 },
};
@@ -647,19 +826,65 @@ export const cozeBotConfig: NodeConfigMeta = {
category: 'integration',
color: '#ec4899',
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: [
createOutput('result', 'any', { description: 'Bot response', label: { en_US: 'Result', zh_Hans: '结果' } }),
createOutput('success', 'boolean', { description: 'Whether call was successful', label: { en_US: 'Success', zh_Hans: '成功' } }),
createOutput('result', 'any', {
description: 'Bot response',
label: { en_US: 'Result', zh_Hans: '结果' },
}),
createOutput('success', 'boolean', {
description: 'Whether call was successful',
label: { en_US: 'Success', zh_Hans: '成功' },
}),
],
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-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 },
{
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-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
*/
export function getIntegrationConfig(nodeType: string): NodeConfigMeta | undefined {
export function getIntegrationConfig(
nodeType: string,
): NodeConfigMeta | undefined {
return integrationConfigs.find((config) => config.nodeType === nodeType);
}

View File

@@ -1,6 +1,6 @@
/**
* Process Node Configurations
*
*
* Defines configurations for general processing node types:
* - text_template: Generate text using templates
* - json_transform: Transform JSON data
@@ -52,7 +52,8 @@ export const textTemplateConfig: NodeConfigMeta = {
zh_Hans: '模板',
},
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}}',
},
required: true,
@@ -141,9 +142,18 @@ export const jsonTransformConfig: NodeConfigMeta = {
required: true,
default: 'jmespath',
options: [
{ name: 'jmespath', 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: '字段映射' } },
{
name: 'jmespath',
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: '字段映射',
},
description: {
en_US: 'JSON object defining field mappings: {"output_field": "input.path.to.field"}',
zh_Hans: '定义字段映射的 JSON 对象: {"output_field": "input.path.to.field"}',
en_US:
'JSON object defining field mappings: {"output_field": "input.path.to.field"}',
zh_Hans:
'定义字段映射的 JSON 对象: {"output_field": "input.path.to.field"}',
},
required: true,
default: '{}',
@@ -243,7 +255,10 @@ export const codeExecutorConfig: NodeConfigMeta = {
required: true,
default: 'javascript',
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' } },
],
},
@@ -256,11 +271,13 @@ export const codeExecutorConfig: NodeConfigMeta = {
zh_Hans: '代码',
},
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` 访问输入数据,并返回结果。',
},
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',
@@ -334,13 +351,25 @@ export const dataAggregatorConfig: NodeConfigMeta = {
required: true,
default: 'array',
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: '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: '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: 'last', label: { en_US: 'Last Item', zh_Hans: '最后一项' } },
],
@@ -437,11 +466,23 @@ export const textSplitterConfig: NodeConfigMeta = {
required: true,
default: 'separator',
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: 'sentences', 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: '按正则表达式' } },
{
name: 'sentences',
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: [
{ name: 'input', label: { en_US: 'From Input', 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: 'jsonpath', label: { en_US: 'JSONPath', zh_Hans: 'JSONPath' } },
{ 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: '表达式' },
},
],
},
{

View File

@@ -1,6 +1,6 @@
/**
* Trigger Node Configurations
*
*
* Defines configurations for all trigger node types:
* - message_trigger: Triggered by incoming messages
* - cron_trigger: Triggered by scheduled time
@@ -62,9 +62,15 @@ export const messageTriggerConfig: NodeConfigMeta = {
default: 'all',
options: [
{ 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: '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: '精确匹配' } },
],
},
@@ -77,7 +83,8 @@ export const messageTriggerConfig: NodeConfigMeta = {
zh_Hans: '匹配模式',
},
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: '用于匹配消息的模式(前缀、正则表达式、关键词或精确文本)',
},
required: false,
@@ -172,12 +179,39 @@ export const cronTriggerConfig: NodeConfigMeta = {
default: 'Asia/Shanghai',
options: [
{ 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: '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)' } },
{
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: '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 触发',
},
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 请求时触发工作流',
},
icon: 'Webhook',
@@ -288,8 +323,14 @@ export const webhookTriggerConfig: NodeConfigMeta = {
default: 'none',
options: [
{ 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: '基本认证' } },
],
},
@@ -328,10 +369,25 @@ export const webhookTriggerConfig: NodeConfigMeta = {
required: false,
default: 'application/json',
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: 'multipart/form-data', label: { en_US: 'multipart/form-data', zh_Hans: '表单数据' } },
{ name: 'text/plain', label: { en_US: 'text/plain', zh_Hans: '纯文本' } },
{
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: '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,
default: 'member_join',
options: [
{ name: 'member_join', label: { en_US: 'Member Join', zh_Hans: '成员加入' } },
{ name: 'member_leave', label: { en_US: 'Member Leave', 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: '入群请求' } },
{
name: 'member_join',
label: { en_US: 'Member Join', zh_Hans: '成员加入' },
},
{
name: 'member_leave',
label: { en_US: 'Member Leave', 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: '入群请求' },
},
],
},
],

View File

@@ -1,6 +1,6 @@
/**
* Workflow Node Configuration Types
*
*
* This module defines the types used for node configuration metadata.
* It extends the existing dynamic form system to support workflow-specific features.
*/
@@ -23,40 +23,40 @@ export interface ExtendedPortDefinition extends PortDefinition {
export interface NodeConfigMeta {
/** Unique node type identifier */
nodeType: string;
/** Display name for the node */
label: I18nObject;
/** Description of what the node does */
description: I18nObject;
/** Icon name (from lucide-react) */
icon: string;
/** Node category for organization */
category: NodeCategory;
/** Color for the node header */
color?: string;
/** Input port definitions */
inputs: ExtendedPortDefinition[];
/** Output port definitions */
outputs: ExtendedPortDefinition[];
/** Configuration schema using the dynamic form system */
configSchema: IDynamicFormItemSchema[];
/** Default configuration values */
defaultConfig?: Record<string, unknown>;
/** Whether this node can be the starting point of a workflow */
isEntryPoint?: boolean;
/** Maximum number of this node type allowed in a workflow (undefined = unlimited) */
maxInstances?: number;
/** Documentation URL */
docsUrl?: string;
}
@@ -76,7 +76,7 @@ export function createPort(
description?: string;
required?: boolean;
label?: I18nObject;
}
},
): ExtendedPortDefinition {
return {
name,
@@ -97,9 +97,12 @@ export function createInput(
description?: string;
required?: boolean;
label?: I18nObject;
}
},
): 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?: {
description?: string;
label?: I18nObject;
}
},
): ExtendedPortDefinition {
return createPort(name, type, { ...options, required: false });
}

View File

@@ -48,53 +48,176 @@ import { resolveI18nLabel, maybeTranslateKey } from './workflow-i18n';
// Single source of truth. Used by WorkflowNodeComponent,
// 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.message_trigger': { labelKey: 'workflows.nodes.messageTrigger', descriptionKey: 'workflows.nodes.messageTriggerDescription' },
'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' },
'trigger.message_trigger': {
labelKey: 'workflows.nodes.messageTrigger',
descriptionKey: 'workflows.nodes.messageTriggerDescription',
},
'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.llm_call': { labelKey: 'workflows.nodes.llmCall', descriptionKey: 'workflows.nodes.llmCallDescription' },
'process.question_classifier': { labelKey: 'workflows.nodes.questionClassifier', descriptionKey: 'workflows.nodes.questionClassifierDescription' },
'process.parameter_extractor': { labelKey: 'workflows.nodes.parameterExtractor', descriptionKey: 'workflows.nodes.parameterExtractorDescription' },
'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' },
'process.llm_call': {
labelKey: 'workflows.nodes.llmCall',
descriptionKey: 'workflows.nodes.llmCallDescription',
},
'process.question_classifier': {
labelKey: 'workflows.nodes.questionClassifier',
descriptionKey: 'workflows.nodes.questionClassifierDescription',
},
'process.parameter_extractor': {
labelKey: 'workflows.nodes.parameterExtractor',
descriptionKey: 'workflows.nodes.parameterExtractorDescription',
},
'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.condition': { labelKey: 'workflows.nodes.condition', descriptionKey: 'workflows.nodes.conditionDescription' },
'control.switch': { labelKey: 'workflows.nodes.switch', descriptionKey: 'workflows.nodes.switchDescription' },
'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' },
'control.condition': {
labelKey: 'workflows.nodes.condition',
descriptionKey: 'workflows.nodes.conditionDescription',
},
'control.switch': {
labelKey: 'workflows.nodes.switch',
descriptionKey: 'workflows.nodes.switchDescription',
},
'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.send_message': { labelKey: 'workflows.nodes.sendMessage', descriptionKey: 'workflows.nodes.sendMessageDescription' },
'action.reply_message': { labelKey: 'workflows.nodes.replyMessage', 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' },
'action.send_message': {
labelKey: 'workflows.nodes.sendMessage',
descriptionKey: 'workflows.nodes.sendMessageDescription',
},
'action.reply_message': {
labelKey: 'workflows.nodes.replyMessage',
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.dify_workflow': { labelKey: 'workflows.nodes.difyWorkflow', descriptionKey: 'workflows.nodes.difyWorkflowDescription' },
'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.dify_workflow': {
labelKey: 'workflows.nodes.difyWorkflow',
descriptionKey: 'workflows.nodes.difyWorkflowDescription',
},
'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.database_query': { labelKey: 'workflows.nodes.databaseQuery', descriptionKey: 'workflows.nodes.databaseQueryDescription' },
'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' },
'integration.database_query': {
labelKey: 'workflows.nodes.databaseQuery',
descriptionKey: 'workflows.nodes.databaseQueryDescription',
},
'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)
@@ -104,10 +227,10 @@ export const NODE_TYPE_LABEL_KEYS: Record<string, string> = Object.fromEntries(
// Category i18n
export const CATEGORY_I18N_KEYS: Record<string, { labelKey: string }> = {
trigger: { labelKey: 'workflows.nodes.trigger' },
process: { labelKey: 'workflows.nodes.process' },
control: { labelKey: 'workflows.nodes.control' },
action: { labelKey: 'workflows.nodes.action' },
trigger: { labelKey: 'workflows.nodes.trigger' },
process: { labelKey: 'workflows.nodes.process' },
control: { labelKey: 'workflows.nodes.control' },
action: { labelKey: 'workflows.nodes.action' },
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> = {
trigger: 'bg-amber-100 dark:bg-amber-900/30 hover:bg-amber-200 dark:hover:bg-amber-900/50',
process: 'bg-blue-100 dark:bg-blue-900/30 hover:bg-blue-200 dark:hover:bg-blue-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',
trigger:
'bg-amber-100 dark:bg-amber-900/30 hover:bg-amber-200 dark:hover:bg-amber-900/50',
process:
'bg-blue-100 dark:bg-blue-900/30 hover:bg-blue-200 dark:hover:bg-blue-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> = {
@@ -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
* "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];
if (keys) return keys;
@@ -234,7 +364,13 @@ export function findNodeI18nKeys(type: string): { labelKey: string; descriptionK
if (parts.length > 1) {
keys = NODE_TYPE_I18N_KEYS[type]; // already tried
} 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}`];
if (keys) return keys;
}

View File

@@ -3,10 +3,7 @@ import type {
WorkflowPortDefinition,
} from '@/app/infra/entities/api';
import type { I18nObject } from '@/app/infra/entities/common';
import {
getNodeConfig,
type NodeConfigMeta,
} from './node-configs';
import { getNodeConfig, type NodeConfigMeta } from './node-configs';
export const WORKFLOW_NODE_CATEGORIES = [
'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;
return {
@@ -103,7 +102,9 @@ function getLocalConfigVariants(type: string): string[] {
return [...variants];
}
export function getLocalNodeTypeMeta(type: string): WorkflowNodeTypeMetadata | null {
export function getLocalNodeTypeMeta(
type: string,
): WorkflowNodeTypeMetadata | null {
let localConfig: NodeConfigMeta | undefined;
for (const variant of getLocalConfigVariants(type)) {
@@ -138,30 +139,27 @@ export function normalizeWorkflowNodeTypeMeta(
const category =
nodeType?.category || localMeta?.category || resolveNodeTypeCategory(type);
const inputs =
nodeType?.inputs?.length
? nodeType.inputs.map((input) =>
normalizePort('workflows.nodeInputs', input),
)
: localMeta?.inputs?.length
? localMeta.inputs
: [DEFAULT_INPUT_PORT];
const inputs = nodeType?.inputs?.length
? nodeType.inputs.map((input) =>
normalizePort('workflows.nodeInputs', input),
)
: localMeta?.inputs?.length
? localMeta.inputs
: [DEFAULT_INPUT_PORT];
const outputs =
nodeType?.outputs?.length
? nodeType.outputs.map((output) =>
normalizePort('workflows.nodeOutputs', output),
)
: localMeta?.outputs?.length
? localMeta.outputs
: [DEFAULT_OUTPUT_PORT];
const outputs = nodeType?.outputs?.length
? nodeType.outputs.map((output) =>
normalizePort('workflows.nodeOutputs', output),
)
: localMeta?.outputs?.length
? localMeta.outputs
: [DEFAULT_OUTPUT_PORT];
const configSchema =
nodeType?.config_schema?.length
? nodeType.config_schema
: localMeta?.config_schema?.length
? localMeta.config_schema
: [];
const configSchema = nodeType?.config_schema?.length
? nodeType.config_schema
: localMeta?.config_schema?.length
? localMeta.config_schema
: [];
return {
type,

View File

@@ -6,7 +6,13 @@ import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
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 {
AlertDialog,
@@ -101,7 +107,9 @@ export default function WorkflowFormComponent({
{/* Description */}
<div className="space-y-2">
<Label htmlFor="workflow-description">{t('workflows.description')}</Label>
<Label htmlFor="workflow-description">
{t('workflows.description')}
</Label>
<Textarea
id="workflow-description"
value={description}
@@ -119,10 +127,7 @@ export default function WorkflowFormComponent({
{t('workflows.enabledDesc')}
</p>
</div>
<Switch
checked={isEnabled}
onCheckedChange={setIsEnabled}
/>
<Switch checked={isEnabled} onCheckedChange={setIsEnabled} />
</div>
</CardContent>
</Card>
@@ -135,15 +140,21 @@ export default function WorkflowFormComponent({
<CardContent>
<div className="grid grid-cols-2 gap-4 text-sm">
<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>
</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>
</div>
<div>
<span className="text-muted-foreground">{t('workflows.createdAt')}:</span>
<span className="text-muted-foreground">
{t('workflows.createdAt')}:
</span>
<span className="ml-2">
{workflow.created_at
? new Date(workflow.created_at).toLocaleString()
@@ -151,7 +162,9 @@ export default function WorkflowFormComponent({
</span>
</div>
<div>
<span className="text-muted-foreground">{t('workflows.updatedAt')}:</span>
<span className="text-muted-foreground">
{t('workflows.updatedAt')}:
</span>
<span className="ml-2">
{workflow.updated_at
? new Date(workflow.updated_at).toLocaleString()
@@ -181,7 +194,9 @@ export default function WorkflowFormComponent({
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('workflows.deleteConfirm')}</AlertDialogTitle>
<AlertDialogTitle>
{t('workflows.deleteConfirm')}
</AlertDialogTitle>
<AlertDialogDescription>
{t('workflows.deleteConfirmDesc', { name: workflow.name })}
</AlertDialogDescription>

View File

@@ -1,5 +1,14 @@
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 {
Workflow,
WorkflowNodeDefinition,
@@ -64,28 +73,28 @@ export interface DebugContext {
interface WorkflowState {
// Current workflow being edited
currentWorkflow: Workflow | null;
// React Flow nodes and edges
nodes: WorkflowNode[];
edges: WorkflowEdge[];
// Node type metadata from backend
nodeTypes: WorkflowNodeTypeMetadata[];
nodeCategories: WorkflowNodeCategory[];
// Selection state
selectedNodeId: string | null;
selectedEdgeId: string | null;
// UI state
isDirty: boolean;
isSaving: boolean;
isLoading: boolean;
// Undo/Redo history
history: { nodes: WorkflowNode[]; edges: WorkflowEdge[] }[];
historyIndex: number;
// Debug state
debugMode: boolean;
debugState: DebugState;
@@ -96,55 +105,67 @@ interface WorkflowState {
debugLogs: DebugLog[];
debugContext: DebugContext;
watchedVariables: string[];
// Actions
setCurrentWorkflow: (workflow: Workflow | null) => void;
setNodes: (nodes: WorkflowNode[]) => void;
setEdges: (edges: WorkflowEdge[]) => void;
setNodeTypes: (types: WorkflowNodeTypeMetadata[], categories: WorkflowNodeCategory[]) => void;
setNodeTypes: (
types: WorkflowNodeTypeMetadata[],
categories: WorkflowNodeCategory[],
) => void;
// Node operations
onNodesChange: (changes: NodeChange<WorkflowNode>[]) => void;
onEdgesChange: (changes: EdgeChange<WorkflowEdge>[]) => void;
onConnect: (connection: Connection) => void;
addNode: (type: string, position: { x: number; y: number }) => void;
updateNodeConfig: (nodeId: string, config: Record<string, unknown>) => void;
updateNodeLabel: (nodeId: string, label: string) => void;
deleteNode: (nodeId: string) => void;
// Edge operations
deleteEdge: (edgeId: string) => void;
updateEdgeCondition: (edgeId: string, condition: string) => void;
// Selection
selectNode: (nodeId: string | null) => void;
selectEdge: (edgeId: string | null) => void;
clearSelection: () => void;
// State management
setDirty: (dirty: boolean) => void;
setSaving: (saving: boolean) => void;
setLoading: (loading: boolean) => void;
// Undo/Redo
undo: () => void;
redo: () => void;
pushHistory: () => void;
// Convert to/from API format
toWorkflowDefinition: () => { nodes: WorkflowNodeDefinition[]; edges: WorkflowEdgeDefinition[] };
fromWorkflowDefinition: (nodes: WorkflowNodeDefinition[], edges: WorkflowEdgeDefinition[]) => void;
toWorkflowDefinition: () => {
nodes: WorkflowNodeDefinition[];
edges: WorkflowEdgeDefinition[];
};
fromWorkflowDefinition: (
nodes: WorkflowNodeDefinition[],
edges: WorkflowEdgeDefinition[],
) => void;
// Reset
reset: () => void;
// Debug actions
setDebugMode: (enabled: boolean) => void;
setDebugState: (state: DebugState) => void;
setDebugExecutionId: (executionId: string | null) => void;
setCurrentNodeId: (nodeId: string | null) => void;
updateNodeExecutionResult: (nodeId: string, result: Partial<NodeExecutionResult>) => void;
updateNodeExecutionResult: (
nodeId: string,
result: Partial<NodeExecutionResult>,
) => void;
clearNodeExecutionResults: () => void;
toggleBreakpoint: (nodeId: string) => void;
clearBreakpoints: () => void;
@@ -159,7 +180,10 @@ interface WorkflowState {
}
const generateUuidLikeId = () => {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
if (
typeof crypto !== 'undefined' &&
typeof crypto.randomUUID === 'function'
) {
return crypto.randomUUID();
}
@@ -195,7 +219,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
isLoading: false,
history: [],
historyIndex: -1,
// Debug initial state
debugMode: false,
debugState: 'idle',
@@ -206,13 +230,14 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
debugLogs: [],
debugContext: { ...defaultDebugContext },
watchedVariables: [],
// Setters
setCurrentWorkflow: (workflow) => set({ currentWorkflow: workflow }),
setNodes: (nodes) => set({ nodes, 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
onNodesChange: (changes) => {
set((state) => ({
@@ -220,28 +245,28 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
isDirty: true,
}));
},
onEdgesChange: (changes) => {
set((state) => ({
edges: applyEdgeChanges(changes, state.edges) as WorkflowEdge[],
isDirty: true,
}));
},
onConnect: (connection) => {
const newEdge: WorkflowEdge = {
...connection,
id: generateEdgeId(),
type: 'smoothstep',
} as WorkflowEdge;
set((state) => ({
edges: addEdge(newEdge, state.edges) as WorkflowEdge[],
isDirty: true,
}));
get().pushHistory();
},
// Add new node
addNode: (type, position) => {
const { nodeTypes } = get();
@@ -277,83 +302,81 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
})),
},
};
set((state) => ({
nodes: [...state.nodes, newNode],
isDirty: true,
}));
get().pushHistory();
},
// Update node config
updateNodeConfig: (nodeId, config) => {
set((state) => ({
nodes: state.nodes.map((node) =>
node.id === nodeId
? { ...node, data: { ...node.data, config } }
: node
node.id === nodeId ? { ...node, data: { ...node.data, config } } : node,
),
isDirty: true,
}));
},
// Update node label
updateNodeLabel: (nodeId, label) => {
set((state) => ({
nodes: state.nodes.map((node) =>
node.id === nodeId
? { ...node, data: { ...node.data, label } }
: node
node.id === nodeId ? { ...node, data: { ...node.data, label } } : node,
),
isDirty: true,
}));
},
// Delete node
deleteNode: (nodeId) => {
set((state) => ({
nodes: state.nodes.filter((node) => node.id !== nodeId),
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,
}));
get().pushHistory();
},
// Delete edge
deleteEdge: (edgeId) => {
set((state) => ({
edges: state.edges.filter((edge) => edge.id !== edgeId),
selectedEdgeId: state.selectedEdgeId === edgeId ? null : state.selectedEdgeId,
selectedEdgeId:
state.selectedEdgeId === edgeId ? null : state.selectedEdgeId,
isDirty: true,
}));
get().pushHistory();
},
// Update edge condition
updateEdgeCondition: (edgeId, condition) => {
set((state) => ({
edges: state.edges.map((edge) =>
edge.id === edgeId
? { ...edge, data: { ...edge.data, condition } }
: edge
: edge,
),
isDirty: true,
}));
},
// Selection
selectNode: (nodeId) => set({ selectedNodeId: nodeId, selectedEdgeId: null }),
selectEdge: (edgeId) => set({ selectedEdgeId: edgeId, selectedNodeId: null }),
clearSelection: () => set({ selectedNodeId: null, selectedEdgeId: null }),
// State management
setDirty: (dirty) => set({ isDirty: dirty }),
setSaving: (saving) => set({ isSaving: saving }),
setLoading: (loading) => set({ isLoading: loading }),
// Undo
undo: () => {
const { history, historyIndex } = get();
@@ -363,7 +386,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
set({ nodes, edges, historyIndex: newIndex, isDirty: true });
}
},
// Redo
redo: () => {
const { history, historyIndex } = get();
@@ -373,25 +396,25 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
set({ nodes, edges, historyIndex: newIndex, isDirty: true });
}
},
// Push current state to history
pushHistory: () => {
const { nodes, edges, history, historyIndex } = get();
const newHistory = history.slice(0, historyIndex + 1);
newHistory.push({ nodes: [...nodes], edges: [...edges] });
// Keep history limited to 50 entries
if (newHistory.length > 50) {
newHistory.shift();
}
set({ history: newHistory, historyIndex: newHistory.length - 1 });
},
// Convert to API format
toWorkflowDefinition: () => {
const { nodes, edges } = get();
const workflowNodes: WorkflowNodeDefinition[] = nodes.map((node) => ({
id: node.id,
type: node.data.type,
@@ -401,7 +424,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
inputs: node.data.inputs,
outputs: node.data.outputs,
}));
const workflowEdges: WorkflowEdgeDefinition[] = edges.map((edge) => ({
id: edge.id,
source: edge.source,
@@ -411,10 +434,10 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
label: edge.data?.label,
condition: edge.data?.condition,
}));
return { nodes: workflowNodes, edges: workflowEdges };
},
// Load from API format
fromWorkflowDefinition: (apiNodes, apiEdges) => {
const nodes: WorkflowNode[] = apiNodes.map((node) => ({
@@ -429,7 +452,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
outputs: node.outputs,
},
}));
const edges: WorkflowEdge[] = apiEdges.map((edge) => ({
id: edge.id,
source: edge.source,
@@ -442,58 +465,62 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
condition: edge.condition,
},
}));
set({ nodes, edges, isDirty: false });
get().pushHistory();
},
// Reset store
reset: () => set({
currentWorkflow: null,
nodes: [],
edges: [],
selectedNodeId: null,
selectedEdgeId: null,
isDirty: false,
isSaving: false,
isLoading: false,
history: [],
historyIndex: -1,
// Reset debug state
debugMode: false,
debugState: 'idle',
debugExecutionId: null,
currentNodeId: null,
nodeExecutionResults: {},
breakpoints: {} as Record<string, boolean>,
debugLogs: [],
debugContext: { ...defaultDebugContext },
watchedVariables: [],
}),
reset: () =>
set({
currentWorkflow: null,
nodes: [],
edges: [],
selectedNodeId: null,
selectedEdgeId: null,
isDirty: false,
isSaving: false,
isLoading: false,
history: [],
historyIndex: -1,
// Reset debug state
debugMode: false,
debugState: 'idle',
debugExecutionId: null,
currentNodeId: null,
nodeExecutionResults: {},
breakpoints: {} as Record<string, boolean>,
debugLogs: [],
debugContext: { ...defaultDebugContext },
watchedVariables: [],
}),
// Debug actions
setDebugMode: (enabled) => set({ debugMode: enabled }),
setDebugState: (state) => set({ debugState: state }),
setDebugExecutionId: (executionId) => set({ debugExecutionId: executionId }),
setCurrentNodeId: (nodeId) => set({ currentNodeId: nodeId }),
updateNodeExecutionResult: (nodeId, result) => {
set((state) => ({
nodeExecutionResults: {
...state.nodeExecutionResults,
[nodeId]: {
...(state.nodeExecutionResults[nodeId] || { nodeId, status: 'pending' }),
...(state.nodeExecutionResults[nodeId] || {
nodeId,
status: 'pending',
}),
...result,
},
},
}));
},
clearNodeExecutionResults: () => set({ nodeExecutionResults: {} }),
toggleBreakpoint: (nodeId) => {
set((state) => {
const newBreakpoints = { ...state.breakpoints };
@@ -505,9 +532,9 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
return { breakpoints: newBreakpoints };
});
},
clearBreakpoints: () => set({ breakpoints: {} as Record<string, boolean> }),
addDebugLog: (log) => {
set((state) => ({
debugLogs: [
@@ -520,27 +547,28 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
].slice(-500), // Keep only last 500 logs
}));
},
clearDebugLogs: () => set({ debugLogs: [] }),
setDebugContext: (context) => {
set((state) => ({
debugContext: { ...state.debugContext, ...context },
}));
},
resetDebugContext: () => set({
debugContext: {
messageContent: '',
senderId: `user_${Date.now().toString(36)}`,
senderName: '',
platform: '',
conversationId: `session_${Date.now().toString(36)}`,
isGroup: false,
customVariables: {},
},
}),
resetDebugContext: () =>
set({
debugContext: {
messageContent: '',
senderId: `user_${Date.now().toString(36)}`,
senderName: '',
platform: '',
conversationId: `session_${Date.now().toString(36)}`,
isGroup: false,
customVariables: {},
},
}),
addWatchedVariable: (variable) => {
set((state) => ({
watchedVariables: state.watchedVariables.includes(variable)
@@ -548,20 +576,21 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
: [...state.watchedVariables, variable],
}));
},
removeWatchedVariable: (variable) => {
set((state) => ({
watchedVariables: state.watchedVariables.filter((v) => v !== variable),
}));
},
clearWatchedVariables: () => set({ watchedVariables: [] }),
resetDebugState: () => set({
debugState: 'idle',
debugExecutionId: null,
currentNodeId: null,
nodeExecutionResults: {},
debugLogs: [],
}),
resetDebugState: () =>
set({
debugState: 'idle',
debugExecutionId: null,
currentNodeId: null,
nodeExecutionResults: {},
debugLogs: [],
}),
}));

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 {
@@ -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 {

View File

@@ -1290,11 +1290,17 @@ export class BackendClient extends BaseHttpClient {
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`);
}
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`);
}

View File

@@ -18,7 +18,9 @@ export default function I18nProvider({ children }: I18nProviderProps) {
// 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
if (!i18nObject || typeof i18nObject !== 'object') {
return '';

View File

@@ -380,14 +380,17 @@ const enUS = {
sessionTypeGroup: 'Group Chat',
// Unified binding (replacing routing rules)
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',
selectBinding: 'Select binding target',
selectWorkflow: 'Select Workflow',
noPipelinesFound: 'No pipelines available',
noWorkflowsFound: 'No workflows available',
pipelineBindingHelp: 'Pipeline is the traditional message processing method using predefined stages.',
workflowBindingHelp: 'Workflow provides visual node orchestration for more flexible message processing.',
pipelineBindingHelp:
'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',
dangerZone: 'Danger Zone',
dangerZoneDescription: 'Irreversible and destructive actions',
@@ -1355,7 +1358,8 @@ const enUS = {
},
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',
selectFromSidebar: 'Select a workflow from the sidebar',
editWorkflow: 'Edit Workflow',
@@ -1395,10 +1399,12 @@ const enUS = {
dangerZoneDesc: 'Irreversible operations',
dangerZoneDescription: 'Irreversible operations',
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',
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
name: 'Name',
namePlaceholder: 'Enter workflow name',
@@ -1461,13 +1467,16 @@ const enUS = {
dragToAdd: 'Drag nodes to add to canvas',
// Property panel
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',
nodeProperties: 'Node Properties',
condition: 'Condition',
hasCondition: 'Set',
conditionPlaceholder: '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.',
conditionPlaceholder:
'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',
deleteEdgeConfirm: 'Confirm Delete Edge',
deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
@@ -1488,7 +1497,8 @@ const enUS = {
noConfigOptions: 'No configuration options for this node type',
deleteNode: '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)
nodeInputs: {
// Common inputs
@@ -1551,7 +1561,8 @@ const enUS = {
aiProcess: 'AI Processing',
aiProcessDescription: 'Process messages using AI models',
llmCall: 'LLM Call',
llmCallDescription: 'Invoke large language model for conversation or generation',
llmCallDescription:
'Invoke large language model for conversation or generation',
codeProcess: 'Code Processing',
codeProcessDescription: 'Execute custom code',
codeExecutor: 'Code Executor',
@@ -1563,13 +1574,17 @@ const enUS = {
dataTransform: 'Data Transform',
dataTransformDescription: 'Transform data format',
questionClassifier: 'Question Classifier',
questionClassifierDescription: 'Classify user questions into predefined categories using LLM',
questionClassifierDescription:
'Classify user questions into predefined categories using LLM',
parameterExtractor: 'Parameter Extractor',
parameterExtractorDescription: 'Extract structured parameters from text using LLM',
parameterExtractorDescription:
'Extract structured parameters from text using LLM',
knowledgeRetrieval: 'Knowledge Retrieval',
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base',
knowledgeRetrievalDescription:
'Retrieve relevant content from knowledge base',
textTemplate: 'Text Template',
textTemplateDescription: 'Generate text using templates with variable interpolation',
textTemplateDescription:
'Generate text using templates with variable interpolation',
jsonTransform: 'JSON Transform',
jsonTransformDescription: 'Transform JSON data using expressions',
dataAggregator: 'Data Aggregator',
@@ -1597,13 +1612,15 @@ const enUS = {
merge: 'Merge',
mergeDescription: 'Merge multiple branches',
variableAggregator: 'Variable Aggregator',
variableAggregatorDescription: 'Aggregate variable outputs from multiple branches',
variableAggregatorDescription:
'Aggregate variable outputs from multiple branches',
action: 'Actions',
actionDescription: 'Action execution nodes',
sendMessage: 'Send Message',
sendMessageDescription: 'Send message to platform',
replyMessage: 'Reply Message',
replyMessageDescription: 'Reply to the message that triggered the workflow',
replyMessageDescription:
'Reply to the message that triggered the workflow',
storeData: 'Store Data',
storeDataDescription: 'Store data to database',
callPipeline: 'Call Pipeline',
@@ -1611,7 +1628,8 @@ const enUS = {
setVariable: 'Set Variable',
setVariableDescription: 'Set context variable',
openingStatement: 'Opening Statement',
openingStatementDescription: 'Provide conversation opener and suggested questions',
openingStatementDescription:
'Provide conversation opener and suggested questions',
end: 'End',
endDescription: 'Mark the end of workflow execution',
log: 'Log',
@@ -1657,7 +1675,8 @@ const enUS = {
title: 'Version History',
current: 'Current 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',
rollbackError: 'Failed to rollback: ',
},

View File

@@ -1373,7 +1373,8 @@ const esES = {
},
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',
selectFromSidebar: 'Select a workflow from the sidebar',
editWorkflow: 'Edit Workflow',
@@ -1413,10 +1414,12 @@ const esES = {
dangerZoneDesc: 'Irreversible operations',
dangerZoneDescription: 'Irreversible operations',
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',
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
name: 'Name',
namePlaceholder: 'Enter workflow name',
@@ -1479,13 +1482,16 @@ const esES = {
dragToAdd: 'Drag nodes to add to canvas',
// Property panel
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',
nodeProperties: 'Node Properties',
condition: 'Condition',
hasCondition: 'Set',
conditionPlaceholder: '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.',
conditionPlaceholder:
'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',
deleteEdgeConfirm: 'Confirm Delete Edge',
deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
@@ -1506,7 +1512,8 @@ const esES = {
noConfigOptions: 'No configuration options for this node type',
deleteNode: '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)
nodeInputs: {
// Common inputs
@@ -1569,7 +1576,8 @@ const esES = {
aiProcess: 'AI Processing',
aiProcessDescription: 'Process messages using AI models',
llmCall: 'LLM Call',
llmCallDescription: 'Invoke large language model for conversation or generation',
llmCallDescription:
'Invoke large language model for conversation or generation',
codeProcess: 'Code Processing',
codeProcessDescription: 'Execute custom code',
codeExecutor: 'Code Executor',
@@ -1581,13 +1589,17 @@ const esES = {
dataTransform: 'Data Transform',
dataTransformDescription: 'Transform data format',
questionClassifier: 'Question Classifier',
questionClassifierDescription: 'Classify user questions into predefined categories using LLM',
questionClassifierDescription:
'Classify user questions into predefined categories using LLM',
parameterExtractor: 'Parameter Extractor',
parameterExtractorDescription: 'Extract structured parameters from text using LLM',
parameterExtractorDescription:
'Extract structured parameters from text using LLM',
knowledgeRetrieval: 'Knowledge Retrieval',
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base',
knowledgeRetrievalDescription:
'Retrieve relevant content from knowledge base',
textTemplate: 'Text Template',
textTemplateDescription: 'Generate text using templates with variable interpolation',
textTemplateDescription:
'Generate text using templates with variable interpolation',
jsonTransform: 'JSON Transform',
jsonTransformDescription: 'Transform JSON data using expressions',
dataAggregator: 'Data Aggregator',
@@ -1615,13 +1627,15 @@ const esES = {
merge: 'Merge',
mergeDescription: 'Merge multiple branches',
variableAggregator: 'Variable Aggregator',
variableAggregatorDescription: 'Aggregate variable outputs from multiple branches',
variableAggregatorDescription:
'Aggregate variable outputs from multiple branches',
action: 'Actions',
actionDescription: 'Action execution nodes',
sendMessage: 'Send Message',
sendMessageDescription: 'Send message to platform',
replyMessage: 'Reply Message',
replyMessageDescription: 'Reply to the message that triggered the workflow',
replyMessageDescription:
'Reply to the message that triggered the workflow',
storeData: 'Store Data',
storeDataDescription: 'Store data to database',
callPipeline: 'Call Pipeline',
@@ -1629,7 +1643,8 @@ const esES = {
setVariable: 'Set Variable',
setVariableDescription: 'Set context variable',
openingStatement: 'Opening Statement',
openingStatementDescription: 'Provide conversation opener and suggested questions',
openingStatementDescription:
'Provide conversation opener and suggested questions',
end: 'End',
endDescription: 'Mark the end of workflow execution',
log: 'Log',
@@ -1675,7 +1690,8 @@ const esES = {
title: 'Version History',
current: 'Current 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',
rollbackError: 'Failed to rollback: ',
},

View File

@@ -1341,7 +1341,8 @@
},
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',
selectFromSidebar: 'Select a workflow from the sidebar',
editWorkflow: 'Edit Workflow',
@@ -1381,10 +1382,12 @@
dangerZoneDesc: 'Irreversible operations',
dangerZoneDescription: 'Irreversible operations',
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',
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
name: 'Name',
namePlaceholder: 'Enter workflow name',
@@ -1447,13 +1450,16 @@
dragToAdd: 'Drag nodes to add to canvas',
// Property panel
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',
nodeProperties: 'Node Properties',
condition: 'Condition',
hasCondition: 'Set',
conditionPlaceholder: '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.',
conditionPlaceholder:
'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',
deleteEdgeConfirm: 'Confirm Delete Edge',
deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
@@ -1474,7 +1480,8 @@
noConfigOptions: 'No configuration options for this node type',
deleteNode: '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)
nodeInputs: {
// Common inputs
@@ -1537,7 +1544,8 @@
aiProcess: 'AI Processing',
aiProcessDescription: 'Process messages using AI models',
llmCall: 'LLM Call',
llmCallDescription: 'Invoke large language model for conversation or generation',
llmCallDescription:
'Invoke large language model for conversation or generation',
codeProcess: 'Code Processing',
codeProcessDescription: 'Execute custom code',
codeExecutor: 'Code Executor',
@@ -1549,13 +1557,17 @@
dataTransform: 'Data Transform',
dataTransformDescription: 'Transform data format',
questionClassifier: 'Question Classifier',
questionClassifierDescription: 'Classify user questions into predefined categories using LLM',
questionClassifierDescription:
'Classify user questions into predefined categories using LLM',
parameterExtractor: 'Parameter Extractor',
parameterExtractorDescription: 'Extract structured parameters from text using LLM',
parameterExtractorDescription:
'Extract structured parameters from text using LLM',
knowledgeRetrieval: 'Knowledge Retrieval',
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base',
knowledgeRetrievalDescription:
'Retrieve relevant content from knowledge base',
textTemplate: 'Text Template',
textTemplateDescription: 'Generate text using templates with variable interpolation',
textTemplateDescription:
'Generate text using templates with variable interpolation',
jsonTransform: 'JSON Transform',
jsonTransformDescription: 'Transform JSON data using expressions',
dataAggregator: 'Data Aggregator',
@@ -1583,13 +1595,15 @@
merge: 'Merge',
mergeDescription: 'Merge multiple branches',
variableAggregator: 'Variable Aggregator',
variableAggregatorDescription: 'Aggregate variable outputs from multiple branches',
variableAggregatorDescription:
'Aggregate variable outputs from multiple branches',
action: 'Actions',
actionDescription: 'Action execution nodes',
sendMessage: 'Send Message',
sendMessageDescription: 'Send message to platform',
replyMessage: 'Reply Message',
replyMessageDescription: 'Reply to the message that triggered the workflow',
replyMessageDescription:
'Reply to the message that triggered the workflow',
storeData: 'Store Data',
storeDataDescription: 'Store data to database',
callPipeline: 'Call Pipeline',
@@ -1597,7 +1611,8 @@
setVariable: 'Set Variable',
setVariableDescription: 'Set context variable',
openingStatement: 'Opening Statement',
openingStatementDescription: 'Provide conversation opener and suggested questions',
openingStatementDescription:
'Provide conversation opener and suggested questions',
end: 'End',
endDescription: 'Mark the end of workflow execution',
log: 'Log',
@@ -1643,7 +1658,8 @@
title: 'Version History',
current: 'Current 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',
rollbackError: 'Failed to rollback: ',
},
@@ -1719,7 +1735,8 @@
disconnected: 'WebSocket未接続',
connectionError: 'WebSocket接続エラー',
connectionFailed: 'WebSocket接続失敗',
notConnected: 'WebSocket未接続です。しばらくしてからもう一度お試しください',
notConnected:
'WebSocket未接続です。しばらくしてからもう一度お試しください',
imageUploadFailed: '画像アップロード失敗',
reply: '返信',
replyTo: '返信先',

View File

@@ -1344,7 +1344,8 @@ const ruRU = {
},
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',
selectFromSidebar: 'Select a workflow from the sidebar',
editWorkflow: 'Edit Workflow',
@@ -1384,10 +1385,12 @@ const ruRU = {
dangerZoneDesc: 'Irreversible operations',
dangerZoneDescription: 'Irreversible operations',
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',
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
name: 'Name',
namePlaceholder: 'Enter workflow name',
@@ -1450,13 +1453,16 @@ const ruRU = {
dragToAdd: 'Drag nodes to add to canvas',
// Property panel
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',
nodeProperties: 'Node Properties',
condition: 'Condition',
hasCondition: 'Set',
conditionPlaceholder: '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.',
conditionPlaceholder:
'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',
deleteEdgeConfirm: 'Confirm Delete Edge',
deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
@@ -1477,7 +1483,8 @@ const ruRU = {
noConfigOptions: 'No configuration options for this node type',
deleteNode: '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)
nodeInputs: {
// Common inputs
@@ -1540,7 +1547,8 @@ const ruRU = {
aiProcess: 'AI Processing',
aiProcessDescription: 'Process messages using AI models',
llmCall: 'LLM Call',
llmCallDescription: 'Invoke large language model for conversation or generation',
llmCallDescription:
'Invoke large language model for conversation or generation',
codeProcess: 'Code Processing',
codeProcessDescription: 'Execute custom code',
codeExecutor: 'Code Executor',
@@ -1552,13 +1560,17 @@ const ruRU = {
dataTransform: 'Data Transform',
dataTransformDescription: 'Transform data format',
questionClassifier: 'Question Classifier',
questionClassifierDescription: 'Classify user questions into predefined categories using LLM',
questionClassifierDescription:
'Classify user questions into predefined categories using LLM',
parameterExtractor: 'Parameter Extractor',
parameterExtractorDescription: 'Extract structured parameters from text using LLM',
parameterExtractorDescription:
'Extract structured parameters from text using LLM',
knowledgeRetrieval: 'Knowledge Retrieval',
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base',
knowledgeRetrievalDescription:
'Retrieve relevant content from knowledge base',
textTemplate: 'Text Template',
textTemplateDescription: 'Generate text using templates with variable interpolation',
textTemplateDescription:
'Generate text using templates with variable interpolation',
jsonTransform: 'JSON Transform',
jsonTransformDescription: 'Transform JSON data using expressions',
dataAggregator: 'Data Aggregator',
@@ -1586,13 +1598,15 @@ const ruRU = {
merge: 'Merge',
mergeDescription: 'Merge multiple branches',
variableAggregator: 'Variable Aggregator',
variableAggregatorDescription: 'Aggregate variable outputs from multiple branches',
variableAggregatorDescription:
'Aggregate variable outputs from multiple branches',
action: 'Actions',
actionDescription: 'Action execution nodes',
sendMessage: 'Send Message',
sendMessageDescription: 'Send message to platform',
replyMessage: 'Reply Message',
replyMessageDescription: 'Reply to the message that triggered the workflow',
replyMessageDescription:
'Reply to the message that triggered the workflow',
storeData: 'Store Data',
storeDataDescription: 'Store data to database',
callPipeline: 'Call Pipeline',
@@ -1600,7 +1614,8 @@ const ruRU = {
setVariable: 'Set Variable',
setVariableDescription: 'Set context variable',
openingStatement: 'Opening Statement',
openingStatementDescription: 'Provide conversation opener and suggested questions',
openingStatementDescription:
'Provide conversation opener and suggested questions',
end: 'End',
endDescription: 'Mark the end of workflow execution',
log: 'Log',
@@ -1646,7 +1661,8 @@ const ruRU = {
title: 'Version History',
current: 'Current 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',
rollbackError: 'Failed to rollback: ',
},

View File

@@ -1313,7 +1313,8 @@ const thTH = {
},
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',
selectFromSidebar: 'Select a workflow from the sidebar',
editWorkflow: 'Edit Workflow',
@@ -1353,10 +1354,12 @@ const thTH = {
dangerZoneDesc: 'Irreversible operations',
dangerZoneDescription: 'Irreversible operations',
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',
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
name: 'Name',
namePlaceholder: 'Enter workflow name',
@@ -1419,13 +1422,16 @@ const thTH = {
dragToAdd: 'Drag nodes to add to canvas',
// Property panel
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',
nodeProperties: 'Node Properties',
condition: 'Condition',
hasCondition: 'Set',
conditionPlaceholder: '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.',
conditionPlaceholder:
'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',
deleteEdgeConfirm: 'Confirm Delete Edge',
deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
@@ -1446,7 +1452,8 @@ const thTH = {
noConfigOptions: 'No configuration options for this node type',
deleteNode: '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)
nodeInputs: {
// Common inputs
@@ -1509,7 +1516,8 @@ const thTH = {
aiProcess: 'AI Processing',
aiProcessDescription: 'Process messages using AI models',
llmCall: 'LLM Call',
llmCallDescription: 'Invoke large language model for conversation or generation',
llmCallDescription:
'Invoke large language model for conversation or generation',
codeProcess: 'Code Processing',
codeProcessDescription: 'Execute custom code',
codeExecutor: 'Code Executor',
@@ -1521,13 +1529,17 @@ const thTH = {
dataTransform: 'Data Transform',
dataTransformDescription: 'Transform data format',
questionClassifier: 'Question Classifier',
questionClassifierDescription: 'Classify user questions into predefined categories using LLM',
questionClassifierDescription:
'Classify user questions into predefined categories using LLM',
parameterExtractor: 'Parameter Extractor',
parameterExtractorDescription: 'Extract structured parameters from text using LLM',
parameterExtractorDescription:
'Extract structured parameters from text using LLM',
knowledgeRetrieval: 'Knowledge Retrieval',
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base',
knowledgeRetrievalDescription:
'Retrieve relevant content from knowledge base',
textTemplate: 'Text Template',
textTemplateDescription: 'Generate text using templates with variable interpolation',
textTemplateDescription:
'Generate text using templates with variable interpolation',
jsonTransform: 'JSON Transform',
jsonTransformDescription: 'Transform JSON data using expressions',
dataAggregator: 'Data Aggregator',
@@ -1555,13 +1567,15 @@ const thTH = {
merge: 'Merge',
mergeDescription: 'Merge multiple branches',
variableAggregator: 'Variable Aggregator',
variableAggregatorDescription: 'Aggregate variable outputs from multiple branches',
variableAggregatorDescription:
'Aggregate variable outputs from multiple branches',
action: 'Actions',
actionDescription: 'Action execution nodes',
sendMessage: 'Send Message',
sendMessageDescription: 'Send message to platform',
replyMessage: 'Reply Message',
replyMessageDescription: 'Reply to the message that triggered the workflow',
replyMessageDescription:
'Reply to the message that triggered the workflow',
storeData: 'Store Data',
storeDataDescription: 'Store data to database',
callPipeline: 'Call Pipeline',
@@ -1569,7 +1583,8 @@ const thTH = {
setVariable: 'Set Variable',
setVariableDescription: 'Set context variable',
openingStatement: 'Opening Statement',
openingStatementDescription: 'Provide conversation opener and suggested questions',
openingStatementDescription:
'Provide conversation opener and suggested questions',
end: 'End',
endDescription: 'Mark the end of workflow execution',
log: 'Log',
@@ -1615,7 +1630,8 @@ const thTH = {
title: 'Version History',
current: 'Current 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',
rollbackError: 'Failed to rollback: ',
},

View File

@@ -1335,7 +1335,8 @@ const viVN = {
},
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',
selectFromSidebar: 'Select a workflow from the sidebar',
editWorkflow: 'Edit Workflow',
@@ -1375,10 +1376,12 @@ const viVN = {
dangerZoneDesc: 'Irreversible operations',
dangerZoneDescription: 'Irreversible operations',
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',
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
name: 'Name',
namePlaceholder: 'Enter workflow name',
@@ -1441,13 +1444,16 @@ const viVN = {
dragToAdd: 'Drag nodes to add to canvas',
// Property panel
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',
nodeProperties: 'Node Properties',
condition: 'Condition',
hasCondition: 'Set',
conditionPlaceholder: '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.',
conditionPlaceholder:
'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',
deleteEdgeConfirm: 'Confirm Delete Edge',
deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
@@ -1468,7 +1474,8 @@ const viVN = {
noConfigOptions: 'No configuration options for this node type',
deleteNode: '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)
nodeInputs: {
// Common inputs
@@ -1531,7 +1538,8 @@ const viVN = {
aiProcess: 'AI Processing',
aiProcessDescription: 'Process messages using AI models',
llmCall: 'LLM Call',
llmCallDescription: 'Invoke large language model for conversation or generation',
llmCallDescription:
'Invoke large language model for conversation or generation',
codeProcess: 'Code Processing',
codeProcessDescription: 'Execute custom code',
codeExecutor: 'Code Executor',
@@ -1543,13 +1551,17 @@ const viVN = {
dataTransform: 'Data Transform',
dataTransformDescription: 'Transform data format',
questionClassifier: 'Question Classifier',
questionClassifierDescription: 'Classify user questions into predefined categories using LLM',
questionClassifierDescription:
'Classify user questions into predefined categories using LLM',
parameterExtractor: 'Parameter Extractor',
parameterExtractorDescription: 'Extract structured parameters from text using LLM',
parameterExtractorDescription:
'Extract structured parameters from text using LLM',
knowledgeRetrieval: 'Knowledge Retrieval',
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base',
knowledgeRetrievalDescription:
'Retrieve relevant content from knowledge base',
textTemplate: 'Text Template',
textTemplateDescription: 'Generate text using templates with variable interpolation',
textTemplateDescription:
'Generate text using templates with variable interpolation',
jsonTransform: 'JSON Transform',
jsonTransformDescription: 'Transform JSON data using expressions',
dataAggregator: 'Data Aggregator',
@@ -1577,13 +1589,15 @@ const viVN = {
merge: 'Merge',
mergeDescription: 'Merge multiple branches',
variableAggregator: 'Variable Aggregator',
variableAggregatorDescription: 'Aggregate variable outputs from multiple branches',
variableAggregatorDescription:
'Aggregate variable outputs from multiple branches',
action: 'Actions',
actionDescription: 'Action execution nodes',
sendMessage: 'Send Message',
sendMessageDescription: 'Send message to platform',
replyMessage: 'Reply Message',
replyMessageDescription: 'Reply to the message that triggered the workflow',
replyMessageDescription:
'Reply to the message that triggered the workflow',
storeData: 'Store Data',
storeDataDescription: 'Store data to database',
callPipeline: 'Call Pipeline',
@@ -1591,7 +1605,8 @@ const viVN = {
setVariable: 'Set Variable',
setVariableDescription: 'Set context variable',
openingStatement: 'Opening Statement',
openingStatementDescription: 'Provide conversation opener and suggested questions',
openingStatementDescription:
'Provide conversation opener and suggested questions',
end: 'End',
endDescription: 'Mark the end of workflow execution',
log: 'Log',
@@ -1637,7 +1652,8 @@ const viVN = {
title: 'Version History',
current: 'Current 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',
rollbackError: 'Failed to rollback: ',
},

View File

@@ -1276,7 +1276,8 @@ const zhHant = {
},
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',
selectFromSidebar: 'Select a workflow from the sidebar',
editWorkflow: 'Edit Workflow',
@@ -1316,10 +1317,12 @@ const zhHant = {
dangerZoneDesc: 'Irreversible operations',
dangerZoneDescription: 'Irreversible operations',
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',
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
name: 'Name',
namePlaceholder: 'Enter workflow name',
@@ -1382,13 +1385,16 @@ const zhHant = {
dragToAdd: 'Drag nodes to add to canvas',
// Property panel
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',
nodeProperties: 'Node Properties',
condition: 'Condition',
hasCondition: 'Set',
conditionPlaceholder: '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.',
conditionPlaceholder:
'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',
deleteEdgeConfirm: 'Confirm Delete Edge',
deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
@@ -1409,7 +1415,8 @@ const zhHant = {
noConfigOptions: 'No configuration options for this node type',
deleteNode: '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)
nodeInputs: {
// Common inputs
@@ -1472,7 +1479,8 @@ const zhHant = {
aiProcess: 'AI Processing',
aiProcessDescription: 'Process messages using AI models',
llmCall: 'LLM Call',
llmCallDescription: 'Invoke large language model for conversation or generation',
llmCallDescription:
'Invoke large language model for conversation or generation',
codeProcess: 'Code Processing',
codeProcessDescription: 'Execute custom code',
codeExecutor: 'Code Executor',
@@ -1484,13 +1492,17 @@ const zhHant = {
dataTransform: 'Data Transform',
dataTransformDescription: 'Transform data format',
questionClassifier: 'Question Classifier',
questionClassifierDescription: 'Classify user questions into predefined categories using LLM',
questionClassifierDescription:
'Classify user questions into predefined categories using LLM',
parameterExtractor: 'Parameter Extractor',
parameterExtractorDescription: 'Extract structured parameters from text using LLM',
parameterExtractorDescription:
'Extract structured parameters from text using LLM',
knowledgeRetrieval: 'Knowledge Retrieval',
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base',
knowledgeRetrievalDescription:
'Retrieve relevant content from knowledge base',
textTemplate: 'Text Template',
textTemplateDescription: 'Generate text using templates with variable interpolation',
textTemplateDescription:
'Generate text using templates with variable interpolation',
jsonTransform: 'JSON Transform',
jsonTransformDescription: 'Transform JSON data using expressions',
dataAggregator: 'Data Aggregator',
@@ -1518,13 +1530,15 @@ const zhHant = {
merge: 'Merge',
mergeDescription: 'Merge multiple branches',
variableAggregator: 'Variable Aggregator',
variableAggregatorDescription: 'Aggregate variable outputs from multiple branches',
variableAggregatorDescription:
'Aggregate variable outputs from multiple branches',
action: 'Actions',
actionDescription: 'Action execution nodes',
sendMessage: 'Send Message',
sendMessageDescription: 'Send message to platform',
replyMessage: 'Reply Message',
replyMessageDescription: 'Reply to the message that triggered the workflow',
replyMessageDescription:
'Reply to the message that triggered the workflow',
storeData: 'Store Data',
storeDataDescription: 'Store data to database',
callPipeline: 'Call Pipeline',
@@ -1532,7 +1546,8 @@ const zhHant = {
setVariable: 'Set Variable',
setVariableDescription: 'Set context variable',
openingStatement: 'Opening Statement',
openingStatementDescription: 'Provide conversation opener and suggested questions',
openingStatementDescription:
'Provide conversation opener and suggested questions',
end: 'End',
endDescription: 'Mark the end of workflow execution',
log: 'Log',
@@ -1578,7 +1593,8 @@ const zhHant = {
title: 'Version History',
current: 'Current 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',
rollbackError: 'Failed to rollback: ',
},