mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-07 22:36:02 +00:00
ruff
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}"}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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: '[]',
|
||||
|
||||
@@ -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: '第一个非空' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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: '表达式' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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: '入群请求' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 '';
|
||||
|
||||
@@ -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: ',
|
||||
},
|
||||
|
||||
@@ -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: ',
|
||||
},
|
||||
|
||||
@@ -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: '返信先',
|
||||
|
||||
@@ -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: ',
|
||||
},
|
||||
|
||||
@@ -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: ',
|
||||
},
|
||||
|
||||
@@ -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: ',
|
||||
},
|
||||
|
||||
@@ -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: ',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user