mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-08 06:46:02 +00:00
后端没修完版
This commit is contained in:
@@ -46,6 +46,7 @@
|
||||
"@tailwindcss/postcss": "^4.1.5",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"axios": "^1.15.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -77,7 +78,8 @@
|
||||
"tailwindcss": "^4.1.5",
|
||||
"uuidjs": "^5.1.0",
|
||||
"vite": "^8.0.5",
|
||||
"zod": "^3.24.4"
|
||||
"zod": "^3.24.4",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.12",
|
||||
|
||||
148
web/pnpm-lock.yaml
generated
148
web/pnpm-lock.yaml
generated
@@ -89,6 +89,9 @@ dependencies:
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1(vite@8.0.8)
|
||||
'@xyflow/react':
|
||||
specifier: ^12.10.2
|
||||
version: 12.10.2(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1)
|
||||
axios:
|
||||
specifier: ^1.15.0
|
||||
version: 1.15.0
|
||||
@@ -185,6 +188,9 @@ dependencies:
|
||||
zod:
|
||||
specifier: ^3.24.4
|
||||
version: 3.25.76
|
||||
zustand:
|
||||
specifier: ^5.0.12
|
||||
version: 5.0.12(@types/react@19.2.10)(react@19.2.1)
|
||||
|
||||
devDependencies:
|
||||
'@types/debug':
|
||||
@@ -1903,6 +1909,12 @@ packages:
|
||||
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
|
||||
dev: false
|
||||
|
||||
/@types/d3-drag@3.0.7:
|
||||
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
|
||||
dependencies:
|
||||
'@types/d3-selection': 3.0.11
|
||||
dev: false
|
||||
|
||||
/@types/d3-ease@3.0.2:
|
||||
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
|
||||
dev: false
|
||||
@@ -1923,6 +1935,10 @@ packages:
|
||||
'@types/d3-time': 3.0.4
|
||||
dev: false
|
||||
|
||||
/@types/d3-selection@3.0.11:
|
||||
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
|
||||
dev: false
|
||||
|
||||
/@types/d3-shape@3.1.8:
|
||||
resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
|
||||
dependencies:
|
||||
@@ -1937,6 +1953,19 @@ packages:
|
||||
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||
dev: false
|
||||
|
||||
/@types/d3-transition@3.0.9:
|
||||
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
|
||||
dependencies:
|
||||
'@types/d3-selection': 3.0.11
|
||||
dev: false
|
||||
|
||||
/@types/d3-zoom@3.0.8:
|
||||
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
|
||||
dependencies:
|
||||
'@types/d3-interpolate': 3.0.4
|
||||
'@types/d3-selection': 3.0.11
|
||||
dev: false
|
||||
|
||||
/@types/debug@4.1.12:
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
dependencies:
|
||||
@@ -2172,6 +2201,36 @@ packages:
|
||||
vite: 8.0.8(@types/node@20.19.30)
|
||||
dev: false
|
||||
|
||||
/@xyflow/react@12.10.2(@types/react@19.2.10)(react-dom@19.2.1)(react@19.2.1):
|
||||
resolution: {integrity: sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==}
|
||||
peerDependencies:
|
||||
react: '>=17'
|
||||
react-dom: '>=17'
|
||||
dependencies:
|
||||
'@xyflow/system': 0.0.76
|
||||
classcat: 5.0.5
|
||||
react: 19.2.1
|
||||
react-dom: 19.2.1(react@19.2.1)
|
||||
zustand: 4.5.7(@types/react@19.2.10)(react@19.2.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- immer
|
||||
dev: false
|
||||
|
||||
/@xyflow/system@0.0.76:
|
||||
resolution: {integrity: sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==}
|
||||
dependencies:
|
||||
'@types/d3-drag': 3.0.7
|
||||
'@types/d3-interpolate': 3.0.4
|
||||
'@types/d3-selection': 3.0.11
|
||||
'@types/d3-transition': 3.0.9
|
||||
'@types/d3-zoom': 3.0.8
|
||||
d3-drag: 3.0.0
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-zoom: 3.0.0
|
||||
dev: false
|
||||
|
||||
/acorn-jsx@5.3.2(acorn@8.15.0):
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
@@ -2434,6 +2493,10 @@ packages:
|
||||
clsx: 2.1.1
|
||||
dev: false
|
||||
|
||||
/classcat@5.0.5:
|
||||
resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
|
||||
dev: false
|
||||
|
||||
/cli-cursor@5.0.0:
|
||||
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2524,6 +2587,19 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/d3-dispatch@3.0.1:
|
||||
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/d3-drag@3.0.0:
|
||||
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
dev: false
|
||||
|
||||
/d3-ease@3.0.1:
|
||||
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2557,6 +2633,11 @@ packages:
|
||||
d3-time-format: 4.1.0
|
||||
dev: false
|
||||
|
||||
/d3-selection@3.0.0:
|
||||
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/d3-shape@3.2.0:
|
||||
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2583,6 +2664,31 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/d3-transition@3.0.1(d3-selection@3.0.0):
|
||||
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
d3-selection: 2 - 3
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
d3-dispatch: 3.0.1
|
||||
d3-ease: 3.0.1
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-timer: 3.0.1
|
||||
dev: false
|
||||
|
||||
/d3-zoom@3.0.0:
|
||||
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
d3-dispatch: 3.0.1
|
||||
d3-drag: 3.0.0
|
||||
d3-interpolate: 3.0.1
|
||||
d3-selection: 3.0.0
|
||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
dev: false
|
||||
|
||||
/data-view-buffer@1.0.2:
|
||||
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6201,6 +6307,48 @@ packages:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
dev: false
|
||||
|
||||
/zustand@4.5.7(@types/react@19.2.10)(react@19.2.1):
|
||||
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
|
||||
engines: {node: '>=12.7.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '>=16.8'
|
||||
immer: '>=9.0.6'
|
||||
react: '>=16.8'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/react': 19.2.10
|
||||
react: 19.2.1
|
||||
use-sync-external-store: 1.6.0(react@19.2.1)
|
||||
dev: false
|
||||
|
||||
/zustand@5.0.12(@types/react@19.2.10)(react@19.2.1):
|
||||
resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '>=18.0.0'
|
||||
immer: '>=9.0.6'
|
||||
react: '>=18.0.0'
|
||||
use-sync-external-store: '>=1.2.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
use-sync-external-store:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/react': 19.2.10
|
||||
react: 19.2.1
|
||||
dev: false
|
||||
|
||||
/zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
dev: false
|
||||
|
||||
@@ -16,7 +16,7 @@ import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { Bot } from '@/app/infra/entities/api';
|
||||
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
import RoutingRulesEditor from './RoutingRulesEditor';
|
||||
import { UnifiedBindingSelector } from '@/app/home/components/unified-binding-selector';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
// SelectGroup used in adapter selector
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -64,29 +65,11 @@ const getFormSchema = (t: (key: string) => string) =>
|
||||
adapter: z.string().min(1, { message: t('bots.adapterRequired') }),
|
||||
adapter_config: z.record(z.string(), z.any()),
|
||||
enable: z.boolean().optional(),
|
||||
// New unified binding fields
|
||||
binding_type: z.enum(['pipeline', 'workflow']).optional(),
|
||||
binding_uuid: z.string().optional(),
|
||||
// Legacy fields (kept for backward compatibility, but not used in UI)
|
||||
use_pipeline_uuid: z.string().optional(),
|
||||
pipeline_routing_rules: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z.enum([
|
||||
'launcher_type',
|
||||
'launcher_id',
|
||||
'message_content',
|
||||
'message_has_element',
|
||||
]),
|
||||
operator: z.enum([
|
||||
'eq',
|
||||
'neq',
|
||||
'contains',
|
||||
'not_contains',
|
||||
'starts_with',
|
||||
'regex',
|
||||
]),
|
||||
value: z.string(),
|
||||
pipeline_uuid: z.string(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export default function BotForm({
|
||||
@@ -111,8 +94,9 @@ export default function BotForm({
|
||||
adapter: '',
|
||||
adapter_config: {},
|
||||
enable: true,
|
||||
binding_type: 'pipeline',
|
||||
binding_uuid: '',
|
||||
use_pipeline_uuid: '',
|
||||
pipeline_routing_rules: [],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -178,8 +162,9 @@ export default function BotForm({
|
||||
adapter: val.adapter,
|
||||
adapter_config: val.adapter_config,
|
||||
enable: val.enable,
|
||||
binding_type: val.binding_type || 'pipeline',
|
||||
binding_uuid: val.binding_uuid || val.use_pipeline_uuid || '',
|
||||
use_pipeline_uuid: val.use_pipeline_uuid || '',
|
||||
pipeline_routing_rules: val.pipeline_routing_rules || [],
|
||||
});
|
||||
handleAdapterSelect(val.adapter);
|
||||
|
||||
@@ -294,8 +279,9 @@ export default function BotForm({
|
||||
name: bot.name,
|
||||
adapter_config: bot.adapter_config,
|
||||
enable: bot.enable ?? true,
|
||||
binding_type: bot.binding_type ?? 'pipeline',
|
||||
binding_uuid: bot.binding_uuid ?? bot.use_pipeline_uuid ?? '',
|
||||
use_pipeline_uuid: bot.use_pipeline_uuid ?? '',
|
||||
pipeline_routing_rules: bot.pipeline_routing_rules ?? [],
|
||||
webhook_full_url: runtimeValues?.webhook_full_url as
|
||||
| string
|
||||
| undefined,
|
||||
@@ -332,15 +318,21 @@ export default function BotForm({
|
||||
function onDynamicFormSubmit() {
|
||||
setIsLoading(true);
|
||||
if (initBotId) {
|
||||
const formValues = form.getValues();
|
||||
const updateBot: Bot = {
|
||||
uuid: initBotId,
|
||||
name: form.getValues().name,
|
||||
description: form.getValues().description ?? '',
|
||||
adapter: form.getValues().adapter,
|
||||
adapter_config: form.getValues().adapter_config,
|
||||
enable: form.getValues().enable,
|
||||
use_pipeline_uuid: form.getValues().use_pipeline_uuid,
|
||||
pipeline_routing_rules: form.getValues().pipeline_routing_rules ?? [],
|
||||
name: formValues.name,
|
||||
description: formValues.description ?? '',
|
||||
adapter: formValues.adapter,
|
||||
adapter_config: formValues.adapter_config,
|
||||
enable: formValues.enable,
|
||||
binding_type: formValues.binding_type ?? 'pipeline',
|
||||
binding_uuid: formValues.binding_uuid ?? '',
|
||||
// Sync use_pipeline_uuid for backward compatibility when binding_type is 'pipeline'
|
||||
use_pipeline_uuid:
|
||||
formValues.binding_type === 'pipeline'
|
||||
? formValues.binding_uuid
|
||||
: formValues.use_pipeline_uuid,
|
||||
};
|
||||
httpClient
|
||||
.updateBot(initBotId, updateBot)
|
||||
@@ -429,74 +421,40 @@ export default function BotForm({
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Card 2: Pipeline Binding (edit mode only) */}
|
||||
{/* Card 2: Unified Binding (edit mode only) */}
|
||||
{initBotId && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('bots.routingConnection')}</CardTitle>
|
||||
<CardTitle>{t('bots.bindTarget')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('bots.routingConnectionDescription')}
|
||||
{t('bots.bindTargetDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="use_pipeline_uuid"
|
||||
name="binding_uuid"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('bots.bindPipeline')}</FormLabel>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} {...field}>
|
||||
<SelectTrigger>
|
||||
{field.value ? (
|
||||
(() => {
|
||||
const pipeline = pipelineNameList.find(
|
||||
(p) => p.value === field.value,
|
||||
);
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{pipeline?.emoji && (
|
||||
<span className="text-sm shrink-0">
|
||||
{pipeline.emoji}
|
||||
</span>
|
||||
)}
|
||||
<span>{pipeline?.label ?? field.value}</span>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<SelectValue
|
||||
placeholder={t('bots.selectPipeline')}
|
||||
/>
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{pipelineNameList.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.emoji && (
|
||||
<span className="text-sm shrink-0">
|
||||
{item.emoji}
|
||||
</span>
|
||||
)}
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<UnifiedBindingSelector
|
||||
value={{
|
||||
type: form.watch('binding_type') ?? 'pipeline',
|
||||
id: field.value ?? null,
|
||||
}}
|
||||
onChange={(val) => {
|
||||
form.setValue('binding_type', val.type, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
form.setValue('binding_uuid', val.id ?? '', {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Pipeline Routing Rules */}
|
||||
<RoutingRulesEditor
|
||||
form={form}
|
||||
pipelineNameList={pipelineNameList}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -12,8 +12,25 @@ import {
|
||||
} from '@/components/ui/form';
|
||||
import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from 'i18next';
|
||||
import { resolveI18nLabel, maybeTranslateKey } from '@/app/home/workflows/components/workflow-editor/workflow-i18n';
|
||||
|
||||
// Helper function to translate i18n key if the value is an i18n key string
|
||||
const translateIfKey = (value: string | undefined): string | undefined => {
|
||||
if (!value) return value;
|
||||
const translated = maybeTranslateKey(value);
|
||||
return translated || value;
|
||||
};
|
||||
|
||||
// Helper to extract i18n label and translate if it's an i18n key
|
||||
const extractAndTranslateI18n = (label: any): string => {
|
||||
if (!label) return '';
|
||||
if (typeof label === 'string') {
|
||||
return translateIfKey(label) || label;
|
||||
}
|
||||
return resolveI18nLabel(label) || '';
|
||||
};
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -34,6 +51,9 @@ function resolveShowIfValue(
|
||||
externalDependentValues?: Record<string, unknown>,
|
||||
systemContext?: Record<string, unknown>,
|
||||
): unknown {
|
||||
if (!field || typeof field !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
if (field.startsWith('__system.')) {
|
||||
const key = field.slice('__system.'.length);
|
||||
return systemContext?.[key];
|
||||
@@ -200,6 +220,38 @@ export default function DynamicFormComponent({
|
||||
// Default to a single empty system prompt entry
|
||||
return [{ role: 'system', content: '' }];
|
||||
}
|
||||
if (
|
||||
item.type === 'string' ||
|
||||
item.type === 'text' ||
|
||||
item.type === 'secret' ||
|
||||
item.type === 'select' ||
|
||||
item.type === 'llm-model-selector' ||
|
||||
item.type === 'embedding-model-selector' ||
|
||||
item.type === 'rerank-model-selector' ||
|
||||
item.type === 'knowledge-base-selector' ||
|
||||
item.type === 'bot-selector'
|
||||
) {
|
||||
return typeof value === 'string' ? value : '';
|
||||
}
|
||||
if (
|
||||
item.type === 'array[string]' ||
|
||||
item.type === 'knowledge-base-multi-selector' ||
|
||||
item.type === 'tools-selector'
|
||||
) {
|
||||
return Array.isArray(value)
|
||||
? value.filter((item): item is string => typeof item === 'string')
|
||||
: [];
|
||||
}
|
||||
if (item.type === 'boolean') {
|
||||
return typeof value === 'boolean' ? value : Boolean(value);
|
||||
}
|
||||
if (item.type === 'integer' || item.type === 'float') {
|
||||
return typeof value === 'number' && !Number.isNaN(value)
|
||||
? value
|
||||
: typeof item.default === 'number'
|
||||
? item.default
|
||||
: 0;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
@@ -390,7 +442,7 @@ export default function DynamicFormComponent({
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 w-full overflow-x-hidden">
|
||||
{itemConfigList.map((config) => {
|
||||
if (config.show_if) {
|
||||
const dependValue = resolveShowIfValue(
|
||||
@@ -434,11 +486,13 @@ export default function DynamicFormComponent({
|
||||
|
||||
return (
|
||||
<WebhookUrlField
|
||||
key={config.id}
|
||||
label={extractI18nObject(config.label)}
|
||||
key={`${config.id}-${config.name}`}
|
||||
label={extractAndTranslateI18n(config.label)}
|
||||
description={
|
||||
config.description
|
||||
? extractI18nObject(config.description)
|
||||
? (typeof config.description === 'string'
|
||||
? (config.description.startsWith('workflows.') ? String(t(config.description)) : config.description)
|
||||
: extractAndTranslateI18n(config.description))
|
||||
: undefined
|
||||
}
|
||||
url={webhookUrl}
|
||||
@@ -451,24 +505,26 @@ export default function DynamicFormComponent({
|
||||
if (config.type === 'boolean') {
|
||||
return (
|
||||
<FormField
|
||||
key={config.id}
|
||||
key={`${config.id}-${config.name}`}
|
||||
control={form.control}
|
||||
name={config.name as keyof FormValues}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-row items-center justify-between rounded-lg border p-4 max-w-2xl',
|
||||
'flex flex-row items-center justify-between rounded-lg border p-4 w-full max-w-full overflow-hidden',
|
||||
isFieldDisabled && 'pointer-events-none opacity-60',
|
||||
)}
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">
|
||||
{extractI18nObject(config.label)}
|
||||
{extractAndTranslateI18n(config.label)}
|
||||
</FormLabel>
|
||||
{config.description && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{extractI18nObject(config.description)}
|
||||
{typeof config.description === 'string'
|
||||
? (config.description.startsWith('workflows.') ? String(t(config.description)) : translateIfKey(config.description))
|
||||
: extractAndTranslateI18n(config.description)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -489,13 +545,18 @@ export default function DynamicFormComponent({
|
||||
|
||||
return (
|
||||
<FormField
|
||||
key={config.id}
|
||||
key={`${config.id}-${config.name}`}
|
||||
control={form.control}
|
||||
name={config.name as keyof FormValues}
|
||||
render={({ field }) => (
|
||||
render={({ field }) => {
|
||||
// Use the i18n label from config.label (I18nObject), falling back to config.name
|
||||
const i18nLabel = config.label
|
||||
? extractAndTranslateI18n(config.label)
|
||||
: config.name;
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{extractI18nObject(config.label)}{' '}
|
||||
{i18nLabel}{' '}
|
||||
{config.required && <span className="text-red-500">*</span>}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
@@ -511,14 +572,20 @@ export default function DynamicFormComponent({
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
{config.description && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{extractI18nObject(config.description)}
|
||||
</p>
|
||||
)}
|
||||
{config.description && (() => {
|
||||
const desc = config.description;
|
||||
if (typeof desc === 'string') {
|
||||
if (desc.startsWith('workflows.')) {
|
||||
return <p className="text-sm text-muted-foreground">{String(t(desc))}</p>;
|
||||
}
|
||||
return <p className="text-sm text-muted-foreground">{translateIfKey(desc) || desc}</p>;
|
||||
}
|
||||
return <p className="text-sm text-muted-foreground">{extractAndTranslateI18n(desc)}</p>;
|
||||
})()}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
IDynamicFormItemSchema,
|
||||
IFileConfig,
|
||||
} from '@/app/infra/entities/form/dynamic';
|
||||
import type { I18nObject } from '@/app/infra/entities/common';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
@@ -28,7 +29,7 @@ import {
|
||||
} from '@/app/infra/entities/api';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { resolveI18nLabel, maybeTranslateKey } from '@/app/home/workflows/components/workflow-editor/workflow-i18n';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import {
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
Plus,
|
||||
X,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Wrench,
|
||||
Trash2,
|
||||
Sparkles,
|
||||
@@ -63,6 +65,31 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import ModelsDialog from '@/app/home/components/models-dialog/ModelsDialog';
|
||||
|
||||
const resolveOptionLabel = (
|
||||
label: unknown,
|
||||
fallback: string,
|
||||
): string => {
|
||||
if (!label || typeof label !== 'object') return fallback;
|
||||
return resolveI18nLabel(label as Record<string, string> | I18nObject) || fallback;
|
||||
};
|
||||
|
||||
const getSelectedOptionLabel = (
|
||||
options: IDynamicFormItemSchema['options'],
|
||||
value: unknown,
|
||||
): string | null => {
|
||||
if (typeof value !== 'string' || !options?.length) return null;
|
||||
const matched = options.find((option) => option.name === value);
|
||||
if (!matched) return null;
|
||||
return resolveOptionLabel(matched.label, matched.name);
|
||||
};
|
||||
|
||||
const resolveModelLabel = (model: {
|
||||
name: string;
|
||||
display_name?: string;
|
||||
}): string => {
|
||||
return maybeTranslateKey(model.display_name || model.name) || model.display_name || model.name;
|
||||
};
|
||||
|
||||
export default function DynamicFormItemComponent({
|
||||
config,
|
||||
field,
|
||||
@@ -88,6 +115,7 @@ export default function DynamicFormItemComponent({
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const [modelsDialogOpen, setModelsDialogOpen] = useState(false);
|
||||
const [secretVisible, setSecretVisible] = useState(false);
|
||||
|
||||
const fetchLlmModels = () => {
|
||||
httpClient
|
||||
@@ -280,7 +308,7 @@ export default function DynamicFormItemComponent({
|
||||
onClick={() => field.onChange(option.name)}
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span>{extractI18nObject(option.label)}</span>
|
||||
<span>{resolveOptionLabel(option.label, option.name)}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{option.name}
|
||||
</span>
|
||||
@@ -292,24 +320,65 @@ export default function DynamicFormItemComponent({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <Input className="max-w-md" {...field} />;
|
||||
return <Input className="max-w-md" {...field} value={field.value ?? ''} />;
|
||||
|
||||
case DynamicFormItemType.SECRET:
|
||||
const secretValue = typeof field.value === 'string' ? field.value : '';
|
||||
const secretPlaceholder = resolveI18nLabel(config.label) || config.name;
|
||||
return (
|
||||
<div className="max-w-md flex items-center gap-1.5">
|
||||
<Input
|
||||
className="flex-1 transition-colors hover:bg-muted/60"
|
||||
type={secretVisible ? 'text' : 'password'}
|
||||
autoComplete="off"
|
||||
placeholder={secretPlaceholder}
|
||||
value={secretValue}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0 text-muted-foreground hover:bg-muted/60 hover:text-foreground"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => setSecretVisible((prev) => !prev)}
|
||||
>
|
||||
{secretVisible ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.TEXT:
|
||||
return <Textarea {...field} className="min-h-[120px] max-w-2xl" />;
|
||||
// Ensure value is always a string to avoid [object Object] display
|
||||
const textValue = typeof field.value === 'string'
|
||||
? field.value
|
||||
: (field.value != null ? JSON.stringify(field.value, null, 2) : '');
|
||||
return (
|
||||
<Textarea
|
||||
{...field}
|
||||
value={textValue}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
className="min-h-[120px] max-w-2xl"
|
||||
/>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.BOOLEAN:
|
||||
return <Switch checked={field.value} onCheckedChange={field.onChange} />;
|
||||
return <Switch checked={!!field.value} onCheckedChange={field.onChange} />;
|
||||
|
||||
case DynamicFormItemType.STRING_ARRAY:
|
||||
const arrayValue = Array.isArray(field.value) ? field.value : [];
|
||||
return (
|
||||
<div className="space-y-2 max-w-md">
|
||||
{field.value.map((item: string, index: number) => (
|
||||
{arrayValue.map((item: string, index: number) => (
|
||||
<div key={index} className="flex gap-1.5 items-center">
|
||||
<Input
|
||||
className="flex-1"
|
||||
value={item}
|
||||
value={item ?? ''}
|
||||
onChange={(e) => {
|
||||
const newValue = [...field.value];
|
||||
const newValue = [...(Array.isArray(field.value) ? field.value : [])];
|
||||
newValue[index] = e.target.value;
|
||||
field.onChange(newValue);
|
||||
}}
|
||||
@@ -320,7 +389,7 @@ export default function DynamicFormItemComponent({
|
||||
size="icon"
|
||||
className="shrink-0 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => {
|
||||
const newValue = field.value.filter(
|
||||
const newValue = (Array.isArray(field.value) ? field.value : []).filter(
|
||||
(_: string, i: number) => i !== index,
|
||||
);
|
||||
field.onChange(newValue);
|
||||
@@ -335,7 +404,7 @@ export default function DynamicFormItemComponent({
|
||||
variant="outline"
|
||||
className="w-full border-dashed text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
field.onChange([...field.value, '']);
|
||||
field.onChange([...(Array.isArray(field.value) ? field.value : []), '']);
|
||||
}}
|
||||
>
|
||||
<Plus className="size-4 mr-1.5" />
|
||||
@@ -345,20 +414,24 @@ export default function DynamicFormItemComponent({
|
||||
);
|
||||
|
||||
case DynamicFormItemType.SELECT:
|
||||
const selectedOptionLabel = getSelectedOptionLabel(config.options, field.value);
|
||||
return (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="max-w-md bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('common.select')} />
|
||||
<Select
|
||||
value={typeof field.value === 'string' ? field.value : ''}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="max-w-md bg-[#ffffff] dark:bg-[#2a2a2e] hover:bg-muted/60 transition-colors">
|
||||
{selectedOptionLabel ? (
|
||||
<span className="truncate">{selectedOptionLabel}</span>
|
||||
) : (
|
||||
<SelectValue placeholder={t('common.select')} />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{config.options?.map((option) => (
|
||||
<SelectItem
|
||||
key={option.name}
|
||||
value={option.name}
|
||||
description={option.name}
|
||||
>
|
||||
{extractI18nObject(option.label)}
|
||||
<SelectItem key={option.name} value={option.name}>
|
||||
{resolveOptionLabel(option.label, option.name)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@@ -411,18 +484,18 @@ export default function DynamicFormItemComponent({
|
||||
return (
|
||||
<div className="max-w-md flex items-center gap-1.5">
|
||||
<div className="flex-1">
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<Select value={field.value ?? ''} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('models.selectModel')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(groupedModels).map(([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectGroup key={`llm-regular-${providerName}`}>
|
||||
<SelectLabel>{providerName}</SelectLabel>
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{model.name}
|
||||
{resolveModelLabel(model)}
|
||||
{model.abilities?.includes('vision') && (
|
||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
@@ -464,12 +537,12 @@ export default function DynamicFormItemComponent({
|
||||
: previewModelNames
|
||||
)
|
||||
.slice(0, 3)
|
||||
.map((name) => (
|
||||
.map((name, index) => (
|
||||
<div
|
||||
key={name}
|
||||
key={`llm-preview-${name}-${index}`}
|
||||
className="relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm text-muted-foreground/60"
|
||||
>
|
||||
{name}
|
||||
{maybeTranslateKey(name) || name}
|
||||
</div>
|
||||
))}
|
||||
{/* Blurred remaining models with login overlay */}
|
||||
@@ -483,12 +556,12 @@ export default function DynamicFormItemComponent({
|
||||
: previewModelNames
|
||||
)
|
||||
.slice(3)
|
||||
.map((name) => (
|
||||
.map((name, index) => (
|
||||
<div
|
||||
key={name}
|
||||
key={`llm-preview-blur-${name}-${index}`}
|
||||
className="flex w-full items-center py-1.5 pl-8 pr-2 text-sm text-muted-foreground/40 blur-[2px]"
|
||||
>
|
||||
{name}
|
||||
{maybeTranslateKey(name) || name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -516,7 +589,7 @@ export default function DynamicFormItemComponent({
|
||||
// User is logged into Space — show space models normally
|
||||
Object.entries(groupedSpaceModels).map(
|
||||
([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectGroup key={`llm-space-${providerName}`}>
|
||||
<SelectLabel>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Sparkles className="h-3.5 w-3.5 text-purple-500" />
|
||||
@@ -526,7 +599,7 @@ export default function DynamicFormItemComponent({
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{model.name}
|
||||
{resolveModelLabel(model)}
|
||||
{model.abilities?.includes('vision') && (
|
||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
@@ -578,18 +651,18 @@ export default function DynamicFormItemComponent({
|
||||
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<Select value={field.value ?? ''} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('knowledge.selectEmbeddingModel')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(groupedEmbeddingModels).map(
|
||||
([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectGroup key={`embedding-${providerName}`}>
|
||||
<SelectLabel>{providerName}</SelectLabel>
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
{model.name}
|
||||
{resolveModelLabel(model)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@@ -624,11 +697,11 @@ export default function DynamicFormItemComponent({
|
||||
<SelectItem value="__none__">{t('common.none')}</SelectItem>
|
||||
{Object.entries(groupedRerankModels).map(
|
||||
([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectGroup key={`rerank-${providerName}`}>
|
||||
<SelectLabel>{providerName}</SelectLabel>
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
{model.name}
|
||||
{resolveModelLabel(model)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@@ -712,19 +785,19 @@ export default function DynamicFormItemComponent({
|
||||
onChange: (val: string) => void,
|
||||
placeholder: string,
|
||||
) => (
|
||||
<Select value={value} onValueChange={onChange}>
|
||||
<Select value={value ?? ''} onValueChange={onChange}>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(groupedModelsForFallback).map(
|
||||
([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectGroup key={`fallback-regular-${providerName}`}>
|
||||
<SelectLabel>{providerName}</SelectLabel>
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{model.name}
|
||||
{resolveModelLabel(model)}
|
||||
{model.abilities?.includes('vision') && (
|
||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
@@ -767,12 +840,12 @@ export default function DynamicFormItemComponent({
|
||||
: fbPreviewModelNames
|
||||
)
|
||||
.slice(0, 3)
|
||||
.map((name) => (
|
||||
.map((name, index) => (
|
||||
<div
|
||||
key={name}
|
||||
key={`fallback-preview-${name}-${index}`}
|
||||
className="relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm text-muted-foreground/60"
|
||||
>
|
||||
{name}
|
||||
{maybeTranslateKey(name) || name}
|
||||
</div>
|
||||
))}
|
||||
{/* Blurred remaining models with login overlay */}
|
||||
@@ -786,12 +859,12 @@ export default function DynamicFormItemComponent({
|
||||
: fbPreviewModelNames
|
||||
)
|
||||
.slice(3)
|
||||
.map((name) => (
|
||||
.map((name, index) => (
|
||||
<div
|
||||
key={name}
|
||||
key={`fallback-preview-blur-${name}-${index}`}
|
||||
className="flex w-full items-center py-1.5 pl-8 pr-2 text-sm text-muted-foreground/40 blur-[2px]"
|
||||
>
|
||||
{name}
|
||||
{maybeTranslateKey(name) || name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -819,7 +892,7 @@ export default function DynamicFormItemComponent({
|
||||
// User is logged into Space — show space models normally
|
||||
Object.entries(fbGroupedSpaceModels).map(
|
||||
([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectGroup key={`fallback-space-${providerName}`}>
|
||||
<SelectLabel>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Sparkles className="h-3.5 w-3.5 text-purple-500" />
|
||||
@@ -829,7 +902,7 @@ export default function DynamicFormItemComponent({
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{model.name}
|
||||
{resolveModelLabel(model)}
|
||||
{model.abilities?.includes('vision') && (
|
||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
@@ -990,7 +1063,7 @@ export default function DynamicFormItemComponent({
|
||||
const kbsByEngine = knowledgeBases.reduce(
|
||||
(acc, kb) => {
|
||||
const engineName = kb.knowledge_engine?.name
|
||||
? extractI18nObject(kb.knowledge_engine.name)
|
||||
? resolveI18nLabel(kb.knowledge_engine.name) || t('knowledge.unknownEngine')
|
||||
: t('knowledge.unknownEngine');
|
||||
if (!acc[engineName]) {
|
||||
acc[engineName] = [];
|
||||
@@ -1002,7 +1075,7 @@ export default function DynamicFormItemComponent({
|
||||
);
|
||||
|
||||
return (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<Select value={field.value ?? '__none__'} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
{field.value && field.value !== '__none__' ? (
|
||||
(() => {
|
||||
@@ -1053,7 +1126,7 @@ export default function DynamicFormItemComponent({
|
||||
const multiKbsByEngine = knowledgeBases.reduce(
|
||||
(acc, kb) => {
|
||||
const engineName = kb.knowledge_engine?.name
|
||||
? extractI18nObject(kb.knowledge_engine.name)
|
||||
? resolveI18nLabel(kb.knowledge_engine.name) || t('knowledge.unknownEngine')
|
||||
: t('knowledge.unknownEngine');
|
||||
if (!acc[engineName]) {
|
||||
acc[engineName] = [];
|
||||
@@ -1064,12 +1137,15 @@ export default function DynamicFormItemComponent({
|
||||
{} as Record<string, typeof knowledgeBases>,
|
||||
);
|
||||
|
||||
// Ensure field.value is always an array
|
||||
const safeValue = Array.isArray(field.value) ? field.value : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
{field.value && field.value.length > 0 ? (
|
||||
{safeValue.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{field.value.map((kbId: string) => {
|
||||
{safeValue.map((kbId: string) => {
|
||||
const currentKb = knowledgeBases.find(
|
||||
(base) => base.uuid === kbId,
|
||||
);
|
||||
@@ -1091,9 +1167,9 @@ export default function DynamicFormItemComponent({
|
||||
{currentKb.name}
|
||||
{currentKb.knowledge_engine?.name && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300">
|
||||
{extractI18nObject(
|
||||
{resolveI18nLabel(
|
||||
currentKb.knowledge_engine.name,
|
||||
)}
|
||||
) || t('knowledge.unknownEngine')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -1109,7 +1185,7 @@ export default function DynamicFormItemComponent({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
const newValue = field.value.filter(
|
||||
const newValue = safeValue.filter(
|
||||
(id: string) => id !== kbId,
|
||||
);
|
||||
field.onChange(newValue);
|
||||
@@ -1133,7 +1209,7 @@ export default function DynamicFormItemComponent({
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTempSelectedKBIds(field.value || []);
|
||||
setTempSelectedKBIds(safeValue);
|
||||
setKbDialogOpen(true);
|
||||
}}
|
||||
variant="outline"
|
||||
@@ -1220,7 +1296,7 @@ export default function DynamicFormItemComponent({
|
||||
|
||||
case DynamicFormItemType.BOT_SELECTOR:
|
||||
return (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<Select value={field.value ?? ''} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('bots.selectBot')} />
|
||||
</SelectTrigger>
|
||||
@@ -1643,6 +1719,6 @@ export default function DynamicFormItemComponent({
|
||||
);
|
||||
|
||||
default:
|
||||
return <Input {...field} />;
|
||||
return <Input {...field} value={field.value ?? ''} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export class DynamicFormItemConfig implements IDynamicFormItemSchema {
|
||||
label: I18nObject;
|
||||
required: boolean;
|
||||
type: DynamicFormItemType;
|
||||
description?: I18nObject;
|
||||
description?: I18nObject | string;
|
||||
options?: IDynamicFormItemOption[];
|
||||
show_if?: IShowIfCondition;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||
import DynamicFormItemComponent from '@/app/home/components/dynamic-form/DynamicFormItemComponent';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { maybeTranslateKey } from '@/app/home/workflows/components/workflow-editor/workflow-i18n';
|
||||
|
||||
/**
|
||||
* N8n认证表单组件
|
||||
@@ -179,23 +180,34 @@ export default function N8nAuthFormComponent({
|
||||
key={config.id}
|
||||
control={form.control}
|
||||
name={config.name as keyof FormValues}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{extractI18nObject(config.label)}{' '}
|
||||
{config.required && <span className="text-red-500">*</span>}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<DynamicFormItemComponent config={config} field={field} />
|
||||
</FormControl>
|
||||
{config.description && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{extractI18nObject(config.description)}
|
||||
</p>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
const labelText =
|
||||
typeof config.label === 'string'
|
||||
? maybeTranslateKey(config.label) || config.label
|
||||
: extractI18nObject(config.label);
|
||||
const descriptionText =
|
||||
typeof config.description === 'string'
|
||||
? maybeTranslateKey(config.description) || config.description
|
||||
: extractI18nObject(config.description);
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{labelText || config.name}{' '}
|
||||
{config.required && <span className="text-red-500">*</span>}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<DynamicFormItemComponent config={config} field={field} />
|
||||
</FormControl>
|
||||
{descriptionText && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{descriptionText}
|
||||
</p>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -116,6 +116,7 @@ function compareVersions(v1: string, v2: string): boolean {
|
||||
const ENTITY_CATEGORY_IDS = [
|
||||
'bots',
|
||||
'pipelines',
|
||||
'workflows',
|
||||
'knowledge',
|
||||
'plugins',
|
||||
'mcp',
|
||||
@@ -126,6 +127,7 @@ type EntityCategoryId = (typeof ENTITY_CATEGORY_IDS)[number];
|
||||
const DETAIL_PAGE_CATEGORIES: EntityCategoryId[] = [
|
||||
'bots',
|
||||
'pipelines',
|
||||
'workflows',
|
||||
'knowledge',
|
||||
'plugins',
|
||||
'mcp',
|
||||
@@ -135,6 +137,7 @@ const DETAIL_PAGE_CATEGORIES: EntityCategoryId[] = [
|
||||
const CREATABLE_CATEGORIES: EntityCategoryId[] = [
|
||||
'bots',
|
||||
'pipelines',
|
||||
'workflows',
|
||||
'knowledge',
|
||||
'mcp',
|
||||
'plugins',
|
||||
@@ -144,6 +147,7 @@ const CREATABLE_CATEGORIES: EntityCategoryId[] = [
|
||||
const COLLAPSIBLE_ONLY_CATEGORIES: EntityCategoryId[] = [
|
||||
'bots',
|
||||
'pipelines',
|
||||
'workflows',
|
||||
'knowledge',
|
||||
'mcp',
|
||||
];
|
||||
@@ -155,10 +159,11 @@ function isEntityCategory(id: string): id is EntityCategoryId {
|
||||
// Map sidebar config IDs to SidebarDataContext keys
|
||||
const ENTITY_KEY_MAP: Record<
|
||||
EntityCategoryId,
|
||||
'bots' | 'pipelines' | 'knowledgeBases' | 'plugins' | 'mcpServers'
|
||||
'bots' | 'pipelines' | 'workflows' | 'knowledgeBases' | 'plugins' | 'mcpServers'
|
||||
> = {
|
||||
bots: 'bots',
|
||||
pipelines: 'pipelines',
|
||||
workflows: 'workflows',
|
||||
knowledge: 'knowledgeBases',
|
||||
plugins: 'plugins',
|
||||
mcp: 'mcpServers',
|
||||
@@ -168,6 +173,7 @@ const ENTITY_KEY_MAP: Record<
|
||||
const ENTITY_ROUTE_MAP: Record<EntityCategoryId, string> = {
|
||||
bots: '/home/bots',
|
||||
pipelines: '/home/pipelines',
|
||||
workflows: '/home/workflows',
|
||||
knowledge: '/home/knowledge',
|
||||
plugins: '/home/plugins',
|
||||
mcp: '/home/mcp',
|
||||
@@ -717,13 +723,13 @@ function NavItems({
|
||||
(isPlugin ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all"
|
||||
<div
|
||||
role="button"
|
||||
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all cursor-pointer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{systemInfo.enable_marketplace && (
|
||||
@@ -760,25 +766,25 @@ function NavItems({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all"
|
||||
<div
|
||||
role="button"
|
||||
className="p-1 rounded-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [@media(hover:hover)]:opacity-0 group-hover/category-header:opacity-100 transition-all cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`${routePrefix}?id=new`);
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 rounded-sm hover:bg-sidebar-accent"
|
||||
<div
|
||||
role="button"
|
||||
className="p-1 rounded-sm hover:bg-sidebar-accent cursor-pointer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ChevronRight className="size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</button>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
|
||||
@@ -35,11 +35,13 @@ export type PluginInstallAction = 'local' | 'github' | null;
|
||||
export interface SidebarDataContextValue {
|
||||
bots: SidebarEntityItem[];
|
||||
pipelines: SidebarEntityItem[];
|
||||
workflows: SidebarEntityItem[];
|
||||
knowledgeBases: SidebarEntityItem[];
|
||||
plugins: SidebarEntityItem[];
|
||||
mcpServers: SidebarEntityItem[];
|
||||
refreshBots: () => Promise<void>;
|
||||
refreshPipelines: () => Promise<void>;
|
||||
refreshWorkflows: () => Promise<void>;
|
||||
refreshKnowledgeBases: () => Promise<void>;
|
||||
refreshPlugins: () => Promise<void>;
|
||||
refreshMCPServers: () => Promise<void>;
|
||||
@@ -61,6 +63,7 @@ export function SidebarDataProvider({
|
||||
}) {
|
||||
const [bots, setBots] = useState<SidebarEntityItem[]>([]);
|
||||
const [pipelines, setPipelines] = useState<SidebarEntityItem[]>([]);
|
||||
const [workflows, setWorkflows] = useState<SidebarEntityItem[]>([]);
|
||||
const [knowledgeBases, setKnowledgeBases] = useState<SidebarEntityItem[]>([]);
|
||||
const [plugins, setPlugins] = useState<SidebarEntityItem[]>([]);
|
||||
const [mcpServers, setMCPServers] = useState<SidebarEntityItem[]>([]);
|
||||
@@ -103,6 +106,24 @@ export function SidebarDataProvider({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshWorkflows = useCallback(async () => {
|
||||
try {
|
||||
const resp = await httpClient.getWorkflows();
|
||||
setWorkflows(
|
||||
resp.workflows.map((w) => ({
|
||||
id: w.uuid || '',
|
||||
name: w.name,
|
||||
description: w.description,
|
||||
emoji: w.emoji,
|
||||
updatedAt: w.updated_at,
|
||||
enabled: w.is_enabled ?? true,
|
||||
})),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch workflows for sidebar:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshKnowledgeBases = useCallback(async () => {
|
||||
try {
|
||||
const resp = await httpClient.getKnowledgeBases();
|
||||
@@ -189,6 +210,7 @@ export function SidebarDataProvider({
|
||||
await Promise.all([
|
||||
refreshBots(),
|
||||
refreshPipelines(),
|
||||
refreshWorkflows(),
|
||||
refreshKnowledgeBases(),
|
||||
refreshPlugins(),
|
||||
refreshMCPServers(),
|
||||
@@ -196,6 +218,7 @@ export function SidebarDataProvider({
|
||||
}, [
|
||||
refreshBots,
|
||||
refreshPipelines,
|
||||
refreshWorkflows,
|
||||
refreshKnowledgeBases,
|
||||
refreshPlugins,
|
||||
refreshMCPServers,
|
||||
@@ -211,11 +234,13 @@ export function SidebarDataProvider({
|
||||
value={{
|
||||
bots,
|
||||
pipelines,
|
||||
workflows,
|
||||
knowledgeBases,
|
||||
plugins,
|
||||
mcpServers,
|
||||
refreshBots,
|
||||
refreshPipelines,
|
||||
refreshWorkflows,
|
||||
refreshKnowledgeBases,
|
||||
refreshPlugins,
|
||||
refreshMCPServers,
|
||||
|
||||
@@ -95,6 +95,27 @@ export const sidebarConfigList = [
|
||||
},
|
||||
section: 'home',
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: 'workflows',
|
||||
name: t('workflows.title'),
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="text-blue-500"
|
||||
>
|
||||
<path d="M2 3C2 2.44772 2.44772 2 3 2H7C7.55228 2 8 2.44772 8 3V7C8 7.55228 7.55228 8 7 8H5V11H11V9C11 8.44772 11.4477 8 12 8H21C21.5523 8 22 8.44772 22 9V13C22 13.5523 21.5523 14 21 14H12C11.4477 14 11 13.5523 11 13V12H5V17H11V15C11 14.4477 11.4477 14 12 14H21C21.5523 14 22 14.4477 22 15V19C22 19.5523 21.5523 20 21 20H12C11.4477 20 11 19.5523 11 19V18H4C3.44772 18 3 17.5523 3 17V8H3C2.44772 8 2 7.55228 2 7V3ZM4 4V6H6V4H4ZM13 10V12H20V10H13ZM13 16V18H20V16H13Z"></path>
|
||||
</svg>
|
||||
),
|
||||
route: '/home/workflows',
|
||||
description: t('workflows.description'),
|
||||
helpLink: {
|
||||
en_US: '',
|
||||
zh_Hans: '',
|
||||
},
|
||||
section: 'home',
|
||||
}),
|
||||
new SidebarChildVO({
|
||||
id: 'knowledge',
|
||||
name: t('knowledge.title'),
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSidebarData, SidebarEntityItem } from '../home-sidebar/SidebarDataContext';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { Check, ChevronsUpDown, GitBranch, Workflow } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
||||
export type BindingType = 'pipeline' | 'workflow';
|
||||
|
||||
export interface BindingValue {
|
||||
type: BindingType;
|
||||
id: string | null;
|
||||
}
|
||||
|
||||
interface UnifiedBindingSelectorProps {
|
||||
value: BindingValue;
|
||||
onChange: (value: BindingValue) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function UnifiedBindingSelector({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
className,
|
||||
}: UnifiedBindingSelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
const { pipelines, workflows, refreshPipelines, refreshWorkflows } = useSidebarData();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
refreshPipelines();
|
||||
refreshWorkflows();
|
||||
}, [refreshPipelines, refreshWorkflows]);
|
||||
|
||||
// Get current selection display
|
||||
const getSelectionDisplay = () => {
|
||||
if (!value.id) {
|
||||
return t('bots.selectBinding');
|
||||
}
|
||||
|
||||
if (value.type === 'pipeline') {
|
||||
const pipeline = pipelines.find((p) => p.id === value.id);
|
||||
return pipeline ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{pipeline.emoji && <span>{pipeline.emoji}</span>}
|
||||
<span>{pipeline.name}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Pipeline
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
value.id
|
||||
);
|
||||
} else {
|
||||
const workflow = workflows.find((w) => w.id === value.id);
|
||||
return workflow ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{workflow.emoji && <span>{workflow.emoji}</span>}
|
||||
<span>{workflow.name}</span>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Workflow
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
value.id
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle type change
|
||||
const handleTypeChange = (newType: BindingType) => {
|
||||
onChange({
|
||||
type: newType,
|
||||
id: null,
|
||||
});
|
||||
};
|
||||
|
||||
// Handle selection
|
||||
const handleSelect = (id: string, type: BindingType) => {
|
||||
onChange({
|
||||
type,
|
||||
id,
|
||||
});
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-4', className)}>
|
||||
{/* Binding type selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>{t('bots.bindingType')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={value.type === 'pipeline' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => handleTypeChange('pipeline')}
|
||||
disabled={disabled}
|
||||
className="flex-1"
|
||||
>
|
||||
<GitBranch className="size-4 mr-1.5" />
|
||||
Pipeline
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={value.type === 'workflow' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => handleTypeChange('workflow')}
|
||||
disabled={disabled}
|
||||
className="flex-1"
|
||||
>
|
||||
<Workflow className="size-4 mr-1.5" />
|
||||
Workflow
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Entity selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>
|
||||
{value.type === 'pipeline' ? t('bots.selectPipeline') : t('bots.selectWorkflow')}
|
||||
</Label>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between"
|
||||
disabled={disabled}
|
||||
>
|
||||
{getSelectionDisplay()}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0" align="start">
|
||||
<ScrollArea className="h-[300px]">
|
||||
<div className="p-2 space-y-1">
|
||||
{value.type === 'pipeline' ? (
|
||||
pipelines.length === 0 ? (
|
||||
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||
{t('bots.noPipelinesFound')}
|
||||
</div>
|
||||
) : (
|
||||
pipelines.map((pipeline) => (
|
||||
<Button
|
||||
key={pipeline.id}
|
||||
variant="ghost"
|
||||
className="w-full justify-start"
|
||||
onClick={() => handleSelect(pipeline.id, 'pipeline')}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'mr-2 h-4 w-4',
|
||||
value.id === pipeline.id ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center gap-2 flex-1 text-left">
|
||||
{pipeline.emoji && <span>{pipeline.emoji}</span>}
|
||||
<span className="truncate">{pipeline.name}</span>
|
||||
</div>
|
||||
{pipeline.description && (
|
||||
<span className="text-xs text-muted-foreground truncate max-w-[120px]">
|
||||
{pipeline.description}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
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>
|
||||
)}
|
||||
</Button>
|
||||
))
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Helper text */}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{value.type === 'pipeline'
|
||||
? t('bots.pipelineBindingHelp')
|
||||
: t('bots.workflowBindingHelp')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
default as UnifiedBindingSelector,
|
||||
type BindingType,
|
||||
type BindingValue,
|
||||
} from './UnifiedBindingSelector';
|
||||
476
web/src/app/home/workflows/WorkflowDetailContent.tsx
Normal file
476
web/src/app/home/workflows/WorkflowDetailContent.tsx
Normal file
@@ -0,0 +1,476 @@
|
||||
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 { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import WorkflowEditorComponent from './components/workflow-editor/WorkflowEditorComponent';
|
||||
import WorkflowFormComponent from './components/workflow-form/WorkflowFormComponent';
|
||||
import WorkflowExecutionsTab from './components/workflow-executions/WorkflowExecutionsTab';
|
||||
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 { backendClient } from '@/app/infra/http';
|
||||
import { Workflow } from '@/app/infra/entities/api';
|
||||
import { useWorkflowStore } from './store/useWorkflowStore';
|
||||
import { toast } from 'sonner';
|
||||
import EmojiPicker from '@/components/ui/emoji-picker';
|
||||
|
||||
export default function WorkflowDetailContent({ id }: { id: string }) {
|
||||
const isCreateMode = id === 'new';
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { refreshWorkflows, workflows, setDetailEntityName } = useSidebarData();
|
||||
|
||||
const {
|
||||
currentWorkflow,
|
||||
setCurrentWorkflow,
|
||||
fromWorkflowDefinition,
|
||||
toWorkflowDefinition,
|
||||
isDirty,
|
||||
setDirty,
|
||||
isSaving,
|
||||
setSaving,
|
||||
setLoading,
|
||||
reset,
|
||||
nodeTypes,
|
||||
setNodeTypes,
|
||||
} = useWorkflowStore();
|
||||
|
||||
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 }>({
|
||||
name: '',
|
||||
description: '',
|
||||
emoji: '🔄',
|
||||
});
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isWebSocketConnected, setIsWebSocketConnected] = useState(false);
|
||||
|
||||
// Set breadcrumb entity name
|
||||
useEffect(() => {
|
||||
if (isCreateMode) {
|
||||
setDetailEntityName(t('workflows.createWorkflow'));
|
||||
} else {
|
||||
const wf = workflows.find((w) => w.id === id);
|
||||
setDetailEntityName(wf?.name ?? id);
|
||||
}
|
||||
return () => setDetailEntityName(null);
|
||||
}, [id, isCreateMode, workflows, setDetailEntityName, t]);
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
}, [nodeTypes.length, setNodeTypes]);
|
||||
|
||||
// Load workflow data
|
||||
useEffect(() => {
|
||||
if (isCreateMode) {
|
||||
reset();
|
||||
setWorkflow(null);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
return () => {
|
||||
reset();
|
||||
};
|
||||
}, [id, isCreateMode]);
|
||||
|
||||
// Save handler
|
||||
const handleSave = useCallback(async () => {
|
||||
if (isSaving) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const { nodes, edges } = toWorkflowDefinition();
|
||||
|
||||
if (isCreateMode) {
|
||||
const resp = await backendClient.createWorkflow({
|
||||
name: basicInfo.name || t('workflows.newWorkflow'),
|
||||
description: basicInfo.description,
|
||||
emoji: basicInfo.emoji,
|
||||
nodes,
|
||||
edges,
|
||||
});
|
||||
refreshWorkflows();
|
||||
navigate(`/home/workflows?id=${encodeURIComponent(resp.uuid)}`);
|
||||
toast.success(t('workflows.createSuccess'));
|
||||
} else {
|
||||
await backendClient.updateWorkflow(id, {
|
||||
name: workflow?.name,
|
||||
emoji: workflow?.emoji,
|
||||
description: workflow?.description,
|
||||
nodes,
|
||||
edges,
|
||||
variables: workflow?.variables,
|
||||
settings: workflow?.settings,
|
||||
triggers: workflow?.triggers,
|
||||
});
|
||||
setDirty(false);
|
||||
refreshWorkflows();
|
||||
toast.success(t('workflows.saveSuccess'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to save workflow:', err);
|
||||
toast.error(t('workflows.saveError'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [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 || '',
|
||||
emoji: workflow?.emoji || '🔄',
|
||||
nodes,
|
||||
edges,
|
||||
variables: workflow?.variables || {},
|
||||
settings: workflow?.settings || {},
|
||||
version: '1.0',
|
||||
exportedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
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;
|
||||
a.download = `${workflow?.name || 'workflow'}.json`;
|
||||
document.body.appendChild(a);
|
||||
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`);
|
||||
}
|
||||
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'));
|
||||
}
|
||||
};
|
||||
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]);
|
||||
|
||||
// Publish handler
|
||||
const handlePublish = useCallback(async () => {
|
||||
if (!workflow?.uuid) return;
|
||||
|
||||
try {
|
||||
await backendClient.publishWorkflow(workflow.uuid);
|
||||
toast.success(t('workflows.publishSuccess'));
|
||||
refreshWorkflows();
|
||||
} catch (err) {
|
||||
console.error('Failed to publish workflow:', err);
|
||||
toast.error(t('workflows.publishError'));
|
||||
}
|
||||
}, [workflow, refreshWorkflows, t]);
|
||||
|
||||
// Delete handler
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (!workflow?.uuid) return;
|
||||
|
||||
try {
|
||||
await backendClient.deleteWorkflow(workflow.uuid);
|
||||
refreshWorkflows();
|
||||
navigate('/home/workflows');
|
||||
toast.success(t('workflows.deleteSuccess'));
|
||||
} catch (err) {
|
||||
console.error('Failed to delete workflow:', err);
|
||||
toast.error(t('workflows.deleteError'));
|
||||
}
|
||||
}, [workflow, refreshWorkflows, navigate, t]);
|
||||
|
||||
// ==================== Create Mode ====================
|
||||
if (isCreateMode && createStep === 'basic') {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
<h1 className="text-xl font-semibold">
|
||||
{t('workflows.createWorkflow')}
|
||||
</h1>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleFileChange}
|
||||
style={{ display: 'none' }}
|
||||
ref={fileInputRef}
|
||||
/>
|
||||
<Button variant="outline" onClick={() => fileInputRef.current?.click()}>
|
||||
<Upload className="size-4 mr-1" />
|
||||
{t('workflows.import')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setCreateStep('editor')}
|
||||
disabled={!basicInfo.name.trim()}
|
||||
>
|
||||
{t('common.next')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
<div className="mx-auto max-w-2xl space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('workflows.basicInfo')}</CardTitle>
|
||||
<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 })} />
|
||||
<Input
|
||||
id="workflow-name"
|
||||
value={basicInfo.name}
|
||||
onChange={(e) => setBasicInfo({ ...basicInfo, name: e.target.value })}
|
||||
placeholder={t('workflows.namePlaceholder')}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="workflow-description">{t('workflows.description')}</Label>
|
||||
<Textarea
|
||||
id="workflow-description"
|
||||
value={basicInfo.description}
|
||||
onChange={(e) => setBasicInfo({ ...basicInfo, description: e.target.value })}
|
||||
placeholder={t('workflows.descriptionPlaceholder')}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isCreateMode) {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
<h1 className="text-xl font-semibold">
|
||||
{t('workflows.createWorkflow')}
|
||||
</h1>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setCreateStep('basic')}>
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? t('common.saving') : t('common.create')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0">
|
||||
<WorkflowEditorComponent />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== Edit Mode ====================
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Hidden file input for import */}
|
||||
<input
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleFileChange}
|
||||
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()}>
|
||||
<Upload className="size-4 mr-1" />
|
||||
{t('workflows.import')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleExport}>
|
||||
<Download className="size-4 mr-1" />
|
||||
{t('workflows.export')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handlePublish}>
|
||||
<GitBranch className="size-4 mr-1" />
|
||||
{t('workflows.publish')}
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!isDirty || isSaving}>
|
||||
{isSaving ? t('common.saving') : t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Horizontal Tabs */}
|
||||
<Tabs
|
||||
key={id}
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="flex flex-1 flex-col min-h-0"
|
||||
>
|
||||
<TabsList className="shrink-0">
|
||||
<TabsTrigger value="editor" className="gap-1.5">
|
||||
<Play className="size-3.5" />
|
||||
{t('workflows.editor')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="debug" className="gap-1.5">
|
||||
<Bug className="size-3.5" />
|
||||
{t('workflows.debugChat')}
|
||||
{activeTab === 'debug' && (
|
||||
<span
|
||||
className={`inline-block size-2 rounded-full ${
|
||||
isWebSocketConnected ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="config" className="gap-1.5">
|
||||
<Settings className="size-3.5" />
|
||||
{t('workflows.configuration')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="executions" className="gap-1.5">
|
||||
<BarChart3 className="size-3.5" />
|
||||
{t('workflows.executions')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Tab: Editor */}
|
||||
<TabsContent
|
||||
value="editor"
|
||||
className="flex-1 min-h-0 mt-4"
|
||||
>
|
||||
<WorkflowEditorComponent />
|
||||
</TabsContent>
|
||||
|
||||
{/* Tab: Debug Chat */}
|
||||
<TabsContent value="debug" className="flex-1 min-h-0 mt-4">
|
||||
<WorkflowDebugDialog
|
||||
open={activeTab === 'debug'}
|
||||
workflowId={id}
|
||||
isEmbedded={true}
|
||||
onConnectionStatusChange={setIsWebSocketConnected}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Tab: Configuration */}
|
||||
<TabsContent
|
||||
value="config"
|
||||
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
||||
>
|
||||
<WorkflowFormComponent
|
||||
workflow={workflow}
|
||||
onWorkflowChange={setWorkflow}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Tab: Executions */}
|
||||
<TabsContent
|
||||
value="executions"
|
||||
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
||||
>
|
||||
<WorkflowExecutionsTab workflowId={id} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
interface AtBadgeProps {
|
||||
targetName: string;
|
||||
readonly?: boolean;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
export default function AtBadge({
|
||||
targetName,
|
||||
readonly = false,
|
||||
onRemove,
|
||||
}: AtBadgeProps) {
|
||||
return (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1 px-2 py-1 text-sm bg-blue-100 dark:bg-blue-900/40 text-blue-600 dark:text-blue-300 hover:bg-blue-200 dark:hover:bg-blue-900/60"
|
||||
>
|
||||
@{targetName}
|
||||
{!readonly && onRemove && (
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="ml-1 hover:text-blue-800 dark:hover:text-blue-200 focus:outline-none"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ImagePreviewDialogProps {
|
||||
open: boolean;
|
||||
imageUrl: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function ImagePreviewDialog({
|
||||
open,
|
||||
imageUrl,
|
||||
onClose,
|
||||
}: ImagePreviewDialogProps) {
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center p-8 animate-in fade-in duration-200"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/20 " />
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="self-end w-9 h-9 rounded-full bg-white hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-800 dark:text-gray-100 shadow-lg transition-all hover:scale-105 flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Preview"
|
||||
className="max-w-[50vw] max-h-[50vh] object-contain rounded-lg shadow-2xl bg-white"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,959 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { backendClient } from '@/app/infra/http';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Message,
|
||||
MessageChainComponent,
|
||||
Image,
|
||||
Plain,
|
||||
At,
|
||||
Quote,
|
||||
Voice,
|
||||
Source,
|
||||
} from '@/app/infra/entities/message';
|
||||
import { toast } from 'sonner';
|
||||
import AtBadge from './AtBadge';
|
||||
import { WorkflowWebSocketClient } from '@/app/infra/websocket/WorkflowWebSocketClient';
|
||||
import ImagePreviewDialog from './ImagePreviewDialog';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import rehypeSanitize from 'rehype-sanitize';
|
||||
import rehypeSlug from 'rehype-slug';
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||
import '@/styles/github-markdown.css';
|
||||
import {
|
||||
User,
|
||||
Users,
|
||||
ImageIcon,
|
||||
Paperclip,
|
||||
Send,
|
||||
Reply,
|
||||
Music,
|
||||
Code,
|
||||
AlignLeft,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface WorkflowDebugDialogProps {
|
||||
open: boolean;
|
||||
workflowId: string;
|
||||
isEmbedded?: boolean;
|
||||
onConnectionStatusChange?: (isConnected: boolean) => void;
|
||||
}
|
||||
|
||||
export default function WorkflowDebugDialog({
|
||||
open,
|
||||
workflowId,
|
||||
isEmbedded = false,
|
||||
onConnectionStatusChange,
|
||||
}: WorkflowDebugDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedWorkflowId, setSelectedWorkflowId] = useState(workflowId);
|
||||
const [sessionType, setSessionType] = useState<'person' | 'group'>('person');
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [showAtPopover, setShowAtPopover] = useState(false);
|
||||
const [hasAt, setHasAt] = useState(false);
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [selectedImages, setSelectedImages] = useState<
|
||||
Array<{ file: File; preview: string; fileKey?: string }>
|
||||
>([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [previewImageUrl, setPreviewImageUrl] = useState<string>('');
|
||||
const [showImagePreview, setShowImagePreview] = useState(false);
|
||||
const [quotedMessage, setQuotedMessage] = useState<Message | null>(null);
|
||||
const [rawModeMessages, setRawModeMessages] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [streamOutput, setStreamOutput] = useState(true);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const wsClientRef = useRef<WorkflowWebSocketClient | null>(null);
|
||||
const isInitializingRef = useRef<boolean>(false);
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
const scrollArea = document.querySelector('.workflow-scroll-area') as HTMLElement;
|
||||
if (scrollArea) {
|
||||
scrollArea.scrollTo({
|
||||
top: scrollArea.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, 0);
|
||||
}, []);
|
||||
|
||||
const loadMessages = useCallback(
|
||||
async (workflowId: string) => {
|
||||
try {
|
||||
const response = await backendClient.getWorkflowWebSocketHistoryMessages(
|
||||
workflowId,
|
||||
sessionType,
|
||||
);
|
||||
setMessages(response.messages);
|
||||
} catch (error) {
|
||||
console.error('Failed to load messages:', error);
|
||||
}
|
||||
},
|
||||
[sessionType],
|
||||
);
|
||||
|
||||
const initWebSocket = useCallback(
|
||||
async (workflowId: string) => {
|
||||
if (isInitializingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isInitializingRef.current = true;
|
||||
|
||||
if (wsClientRef.current) {
|
||||
wsClientRef.current.disconnect();
|
||||
wsClientRef.current = null;
|
||||
}
|
||||
|
||||
const wsClient = new WorkflowWebSocketClient(workflowId, sessionType);
|
||||
|
||||
wsClient
|
||||
.onConnected(() => {
|
||||
setIsConnected(true);
|
||||
isInitializingRef.current = false;
|
||||
})
|
||||
.onMessage((wsMessage) => {
|
||||
const message: Message = {
|
||||
...wsMessage,
|
||||
message_chain: wsMessage.message_chain as MessageChainComponent[],
|
||||
};
|
||||
|
||||
setMessages((prevMessages) => {
|
||||
const existingIndex = prevMessages.findIndex(
|
||||
(m) => m.id === message.id,
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
const newMessages = [...prevMessages];
|
||||
newMessages[existingIndex] = message;
|
||||
return newMessages;
|
||||
} else {
|
||||
return [...prevMessages, message];
|
||||
}
|
||||
});
|
||||
})
|
||||
.onError((error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
setIsConnected(false);
|
||||
isInitializingRef.current = false;
|
||||
const errorMessage =
|
||||
error instanceof Error && error.message
|
||||
? error.message
|
||||
: t('workflows.debugDialog.connectionError');
|
||||
toast.error(errorMessage);
|
||||
})
|
||||
.onClose(() => {
|
||||
setIsConnected(false);
|
||||
isInitializingRef.current = false;
|
||||
})
|
||||
.onBroadcast((message) => {
|
||||
toast.info(message);
|
||||
});
|
||||
|
||||
await wsClient.connect();
|
||||
wsClientRef.current = wsClient;
|
||||
} catch (error) {
|
||||
console.error('WebSocket connection failed:', error);
|
||||
setIsConnected(false);
|
||||
isInitializingRef.current = false;
|
||||
toast.error(t('workflows.debugDialog.connectionFailed'));
|
||||
}
|
||||
},
|
||||
[sessionType, t],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages, scrollToBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setSelectedWorkflowId(workflowId);
|
||||
} else {
|
||||
if (wsClientRef.current) {
|
||||
wsClientRef.current.disconnect();
|
||||
wsClientRef.current = null;
|
||||
setIsConnected(false);
|
||||
isInitializingRef.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (wsClientRef.current) {
|
||||
wsClientRef.current.disconnect();
|
||||
wsClientRef.current = null;
|
||||
isInitializingRef.current = false;
|
||||
}
|
||||
};
|
||||
}, [open, workflowId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setMessages([]);
|
||||
loadMessages(selectedWorkflowId);
|
||||
initWebSocket(selectedWorkflowId);
|
||||
}
|
||||
}, [sessionType, selectedWorkflowId, open, loadMessages, initWebSocket]);
|
||||
|
||||
useEffect(() => {
|
||||
onConnectionStatusChange?.(isConnected);
|
||||
}, [isConnected, onConnectionStatusChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
popoverRef.current &&
|
||||
!popoverRef.current.contains(event.target as Node) &&
|
||||
!inputRef.current?.contains(event.target as Node)
|
||||
) {
|
||||
setShowAtPopover(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (showAtPopover) {
|
||||
setIsHovering(true);
|
||||
}
|
||||
}, [showAtPopover]);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
if (sessionType === 'group') {
|
||||
if (value.endsWith('@')) {
|
||||
setShowAtPopover(true);
|
||||
} else if (showAtPopover && (!value.includes('@') || value.length > 1)) {
|
||||
setShowAtPopover(false);
|
||||
}
|
||||
}
|
||||
setInputValue(value);
|
||||
};
|
||||
|
||||
const handleAtSelect = () => {
|
||||
setHasAt(true);
|
||||
setShowAtPopover(false);
|
||||
setInputValue(inputValue.slice(0, -1));
|
||||
};
|
||||
|
||||
const handleAtRemove = () => {
|
||||
setHasAt(false);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (showAtPopover) {
|
||||
handleAtSelect();
|
||||
} else {
|
||||
sendMessage();
|
||||
}
|
||||
} else if (e.key === 'Backspace' && hasAt && inputValue === '') {
|
||||
handleAtRemove();
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const newImages: Array<{ file: File; preview: string }> = [];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (file.type.startsWith('image/')) {
|
||||
const preview = URL.createObjectURL(file);
|
||||
newImages.push({ file, preview });
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedImages((prev) => [...prev, ...newImages]);
|
||||
};
|
||||
|
||||
const handleRemoveImage = (index: number) => {
|
||||
setSelectedImages((prev) => {
|
||||
const newImages = [...prev];
|
||||
URL.revokeObjectURL(newImages[index].preview);
|
||||
newImages.splice(index, 1);
|
||||
return newImages;
|
||||
});
|
||||
};
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (
|
||||
!inputValue.trim() &&
|
||||
!hasAt &&
|
||||
selectedImages.length === 0 &&
|
||||
!quotedMessage
|
||||
)
|
||||
return;
|
||||
if (!isConnected || !wsClientRef.current) {
|
||||
toast.error(t('workflows.debugDialog.notConnected'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsUploading(true);
|
||||
|
||||
const messageChain = [];
|
||||
|
||||
if (quotedMessage) {
|
||||
const sourceComponent = quotedMessage.message_chain.find(
|
||||
(c) => c.type === 'Source',
|
||||
) as Source | undefined;
|
||||
const messageId = sourceComponent
|
||||
? sourceComponent.id
|
||||
: quotedMessage.id;
|
||||
|
||||
messageChain.push({
|
||||
type: 'Quote',
|
||||
id: messageId,
|
||||
origin: quotedMessage.message_chain.filter(
|
||||
(c) => c.type !== 'Source',
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let text_content = inputValue.trim();
|
||||
if (hasAt) {
|
||||
text_content = ' ' + text_content;
|
||||
}
|
||||
|
||||
if (hasAt) {
|
||||
messageChain.push({
|
||||
type: 'At',
|
||||
target: 'websocketbot',
|
||||
display: 'websocketbot',
|
||||
});
|
||||
}
|
||||
|
||||
if (text_content) {
|
||||
messageChain.push({
|
||||
type: 'Plain',
|
||||
text: text_content,
|
||||
});
|
||||
}
|
||||
|
||||
for (const image of selectedImages) {
|
||||
try {
|
||||
const result = await backendClient.uploadWorkflowWebSocketImage(
|
||||
selectedWorkflowId,
|
||||
image.file,
|
||||
);
|
||||
messageChain.push({
|
||||
type: 'Image',
|
||||
path: result.file_key,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Image upload failed:', error);
|
||||
toast.error(t('workflows.debugDialog.imageUploadFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
setInputValue('');
|
||||
setHasAt(false);
|
||||
setQuotedMessage(null);
|
||||
selectedImages.forEach((img) => URL.revokeObjectURL(img.preview));
|
||||
setSelectedImages([]);
|
||||
|
||||
wsClientRef.current.sendMessage(messageChain, streamOutput);
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
toast.error(t('workflows.debugDialog.sendFailed'));
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const renderMessageComponent = (
|
||||
component: MessageChainComponent,
|
||||
index: number,
|
||||
) => {
|
||||
switch (component.type) {
|
||||
case 'Plain':
|
||||
return <span key={index}>{(component as Plain).text}</span>;
|
||||
|
||||
case 'At': {
|
||||
const atComponent = component as At;
|
||||
const displayName =
|
||||
atComponent.display || atComponent.target?.toString() || '';
|
||||
return (
|
||||
<span key={index} className="inline-flex align-middle mx-1">
|
||||
<AtBadge targetName={displayName} readonly={true} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
case 'AtAll':
|
||||
return (
|
||||
<span key={index} className="inline-flex align-middle mx-1">
|
||||
<AtBadge
|
||||
targetName={t('workflows.debugDialog.allMembers')}
|
||||
readonly={true}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
case 'Image': {
|
||||
const img = component as Image;
|
||||
const imageUrl = img.url || (img.base64 ? img.base64 : '');
|
||||
|
||||
if (!imageUrl) return null;
|
||||
|
||||
return (
|
||||
<div key={index} className="my-2">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Image"
|
||||
className="max-w-full max-h-96 rounded-lg cursor-pointer hover:opacity-90 transition-opacity"
|
||||
onClick={() => {
|
||||
setPreviewImageUrl(imageUrl);
|
||||
setShowImagePreview(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'File': {
|
||||
const file = component as MessageChainComponent & { name?: string };
|
||||
return (
|
||||
<div key={index} className="my-2 flex items-center gap-2 text-sm">
|
||||
<Paperclip className="size-4" />
|
||||
<span>
|
||||
[{t('workflows.debugDialog.file')}] {file.name || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'Voice': {
|
||||
const voice = component as Voice;
|
||||
const voiceUrl = voice.url || (voice.base64 ? voice.base64 : '');
|
||||
|
||||
if (!voiceUrl) {
|
||||
return <span key={index}>[{t('workflows.debugDialog.voice')}]</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} className="my-2 flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-muted rounded-lg">
|
||||
<Music className="size-5" />
|
||||
<audio
|
||||
controls
|
||||
src={voiceUrl}
|
||||
className="h-8"
|
||||
style={{ maxWidth: '200px' }}
|
||||
>
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
{voice.length && voice.length > 0 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{voice.length}s
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'Quote': {
|
||||
const quote = component as Quote;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-2 pl-3 border-l-2 border-muted-foreground/50"
|
||||
>
|
||||
<div className="text-sm opacity-75">
|
||||
{quote.origin?.map((comp, idx) =>
|
||||
renderMessageComponent(comp as MessageChainComponent, idx),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'Source':
|
||||
return null;
|
||||
|
||||
default:
|
||||
return <span key={index}>[{component.type}]</span>;
|
||||
}
|
||||
};
|
||||
|
||||
const getMessageTimestamp = (message: Message): number => {
|
||||
const sourceComponent = message.message_chain.find(
|
||||
(c) => c.type === 'Source',
|
||||
) as Source | undefined;
|
||||
|
||||
if (sourceComponent && sourceComponent.timestamp) {
|
||||
return sourceComponent.timestamp;
|
||||
}
|
||||
|
||||
if (message.timestamp) {
|
||||
return Math.floor(new Date(message.timestamp).getTime() / 1000);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const formatTimestamp = (timestamp: number): string => {
|
||||
if (!timestamp) return '';
|
||||
|
||||
const date = new Date(timestamp * 1000);
|
||||
const now = new Date();
|
||||
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
const isToday = now.toDateString() === date.toDateString();
|
||||
if (isToday) {
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
const yesterday = new Date(now);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const isYesterday = yesterday.toDateString() === date.toDateString();
|
||||
if (isYesterday) {
|
||||
return `${t('bots.yesterday')} ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
const isThisYear = now.getFullYear() === date.getFullYear();
|
||||
if (isThisYear) {
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
return t('bots.dateFormat', { month, day });
|
||||
}
|
||||
|
||||
return t('bots.earlier');
|
||||
};
|
||||
|
||||
const getMessageKey = (message: Message): string => {
|
||||
return `${message.id}-${message.timestamp}`;
|
||||
};
|
||||
|
||||
const toggleRawMode = (message: Message) => {
|
||||
const key = getMessageKey(message);
|
||||
setRawModeMessages((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(key)) {
|
||||
newSet.delete(key);
|
||||
} else {
|
||||
newSet.add(key);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const hasPlainText = (message: Message): boolean => {
|
||||
return message.message_chain.some((c) => c.type === 'Plain');
|
||||
};
|
||||
|
||||
const getPlainText = (message: Message): string => {
|
||||
return message.message_chain
|
||||
.filter((c) => c.type === 'Plain')
|
||||
.map((c) => (c as Plain).text)
|
||||
.join('');
|
||||
};
|
||||
|
||||
const renderMessageContent = (message: Message) => {
|
||||
const key = getMessageKey(message);
|
||||
const isRawMode = rawModeMessages.has(key);
|
||||
|
||||
if (!isRawMode && hasPlainText(message)) {
|
||||
const plainText = getPlainText(message);
|
||||
const nonPlainComponents = message.message_chain.filter(
|
||||
(c) => c.type !== 'Plain' && c.type !== 'Source',
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="text-base leading-relaxed align-middle">
|
||||
{nonPlainComponents.map((component, index) =>
|
||||
renderMessageComponent(component, index),
|
||||
)}
|
||||
<div className="markdown-body">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[
|
||||
rehypeRaw,
|
||||
rehypeSanitize,
|
||||
rehypeHighlight,
|
||||
rehypeSlug,
|
||||
[
|
||||
rehypeAutolinkHeadings,
|
||||
{
|
||||
behavior: 'wrap',
|
||||
properties: {
|
||||
className: ['anchor'],
|
||||
},
|
||||
},
|
||||
],
|
||||
]}
|
||||
components={{
|
||||
ul: ({ children }) => <ul className="list-disc">{children}</ul>,
|
||||
ol: ({ children }) => (
|
||||
<ol className="list-decimal">{children}</ol>
|
||||
),
|
||||
li: ({ children }) => <li className="ml-4">{children}</li>,
|
||||
img: ({ src, alt, ...props }) => {
|
||||
const imageSrc = src || '';
|
||||
|
||||
if (typeof imageSrc !== 'string') {
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt || ''}
|
||||
className="max-w-full h-auto rounded-lg my-4"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={alt || ''}
|
||||
className="max-w-lg h-auto my-4"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{plainText}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-base leading-relaxed align-middle whitespace-pre-wrap">
|
||||
{message.message_chain.map((component, index) =>
|
||||
renderMessageComponent(component, index),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = () => (
|
||||
<div className="flex flex-1 h-full min-h-0">
|
||||
<div className="w-14 p-2 pl-0 shrink-0 flex flex-col justify-start gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'w-10 h-10 justify-center rounded-md transition-none border-0 shadow-none',
|
||||
sessionType === 'person'
|
||||
? 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
||||
)}
|
||||
onClick={() => setSessionType('person')}
|
||||
>
|
||||
<User className="size-5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'w-10 h-10 justify-center rounded-md transition-none border-0 shadow-none',
|
||||
sessionType === 'group'
|
||||
? 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
||||
)}
|
||||
onClick={() => setSessionType('group')}
|
||||
>
|
||||
<Users className="size-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col w-[10rem] h-full min-h-0">
|
||||
<ScrollArea className="flex-1 p-6 overflow-y-auto min-h-0 workflow-scroll-area">
|
||||
<div className="space-y-6">
|
||||
{messages.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-12 text-lg">
|
||||
{t('workflows.debugDialog.noMessages')}
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<div
|
||||
key={message.id + message.timestamp}
|
||||
className={cn(
|
||||
'flex',
|
||||
message.role === 'user' ? 'justify-end' : 'justify-start',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-3xl px-5 py-3 rounded-2xl',
|
||||
message.role === 'user'
|
||||
? 'user-message-bubble bg-primary/10 text-foreground rounded-br-none'
|
||||
: 'bg-muted text-foreground rounded-bl-none',
|
||||
)}
|
||||
>
|
||||
{renderMessageContent(message)}
|
||||
<div
|
||||
className={cn(
|
||||
'text-xs mt-2 flex items-center justify-between gap-2',
|
||||
'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>
|
||||
{message.role === 'user'
|
||||
? t('workflows.debugDialog.userMessage')
|
||||
: t('workflows.debugDialog.botMessage')}
|
||||
</span>
|
||||
{hasPlainText(message) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleRawMode(message)}
|
||||
className={cn(
|
||||
'px-1.5 py-0.5 rounded text-[10px] transition-colors',
|
||||
'hover:bg-accent',
|
||||
)}
|
||||
title={
|
||||
rawModeMessages.has(getMessageKey(message))
|
||||
? t('workflows.debugDialog.showMarkdown')
|
||||
: t('workflows.debugDialog.showRaw')
|
||||
}
|
||||
>
|
||||
{rawModeMessages.has(getMessageKey(message)) ? (
|
||||
<span className="flex items-center gap-0.5">
|
||||
<Code className="size-3" />
|
||||
MD
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-0.5">
|
||||
<AlignLeft className="size-3" />
|
||||
{t('workflows.debugDialog.showRaw')}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setQuotedMessage(message)}
|
||||
className={cn(
|
||||
'px-1.5 py-0.5 rounded text-[10px] transition-colors flex items-center gap-0.5',
|
||||
'hover:bg-accent',
|
||||
)}
|
||||
title={t('workflows.debugDialog.reply')}
|
||||
>
|
||||
<Reply className="size-3" />
|
||||
{t('workflows.debugDialog.reply')}
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-[10px]">
|
||||
{formatTimestamp(getMessageTimestamp(message))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Quoted message preview */}
|
||||
{quotedMessage && (
|
||||
<div className="px-4 py-2 bg-muted/50 border-t">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1 pl-3 border-l-2 border-primary">
|
||||
<div className="text-xs text-muted-foreground mb-1">
|
||||
{t('workflows.debugDialog.replyTo')}{' '}
|
||||
{quotedMessage.role === 'user'
|
||||
? t('workflows.debugDialog.userMessage')
|
||||
: t('workflows.debugDialog.botMessage')}
|
||||
</div>
|
||||
<div className="text-sm text-foreground/70 line-clamp-2">
|
||||
{quotedMessage.message_chain
|
||||
.filter((c) => c.type === 'Plain')
|
||||
.map((c) => (c as Plain).text)
|
||||
.join('')}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setQuotedMessage(null)}
|
||||
className="w-5 h-5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Image preview area */}
|
||||
{selectedImages.length > 0 && (
|
||||
<div className="px-4 pb-2">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{selectedImages.map((image, index) => (
|
||||
<div key={index} className="relative group">
|
||||
<img
|
||||
src={image.preview}
|
||||
alt={`preview-${index}`}
|
||||
className="w-20 h-20 object-cover rounded-lg border"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveImage(index)}
|
||||
className="absolute -top-2 -right-2 w-5 h-5 bg-destructive text-destructive-foreground rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4 pb-0 flex gap-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('workflows.debugDialog.streamOutput')}
|
||||
</span>
|
||||
<Switch
|
||||
checked={streamOutput}
|
||||
onCheckedChange={setStreamOutput}
|
||||
disabled={!isConnected}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
onChange={handleImageSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={!isConnected || isUploading}
|
||||
className="w-10 h-10 rounded-md hover:bg-accent"
|
||||
title={t('workflows.debugDialog.uploadImage')}
|
||||
>
|
||||
<ImageIcon className="size-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
{hasAt && (
|
||||
<AtBadge targetName="websocketbot" onRemove={handleAtRemove} />
|
||||
)}
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder={t('workflows.debugDialog.inputPlaceholder', {
|
||||
type:
|
||||
sessionType === 'person'
|
||||
? t('workflows.debugDialog.privateChat')
|
||||
: t('workflows.debugDialog.groupChat'),
|
||||
})}
|
||||
disabled={!isConnected || isUploading}
|
||||
className="flex-1 rounded-md px-3 py-2 transition-none text-base disabled:opacity-50"
|
||||
/>
|
||||
{showAtPopover && (
|
||||
<div
|
||||
ref={popoverRef}
|
||||
className="absolute bottom-full left-0 mb-2 w-auto rounded-md border bg-popover text-popover-foreground shadow-lg"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-4 py-1.5 rounded cursor-pointer',
|
||||
isHovering ? 'bg-accent' : '',
|
||||
)}
|
||||
onClick={handleAtSelect}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
>
|
||||
<span>
|
||||
@websocketbot - {t('workflows.debugDialog.atTips')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={sendMessage}
|
||||
disabled={
|
||||
(!inputValue.trim() &&
|
||||
!hasAt &&
|
||||
selectedImages.length === 0 &&
|
||||
!quotedMessage) ||
|
||||
!isConnected ||
|
||||
isUploading
|
||||
}
|
||||
className="rounded-md w-20 px-6 py-2 text-base font-medium transition-none flex items-center gap-2 shadow-none disabled:opacity-50"
|
||||
>
|
||||
{isUploading ? (
|
||||
t('workflows.debugDialog.uploading')
|
||||
) : (
|
||||
<>
|
||||
<Send className="size-4" />
|
||||
{t('workflows.debugDialog.send')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isEmbedded) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
<div className="flex-1 min-h-0 flex flex-col">{renderContent()}</div>
|
||||
</div>
|
||||
<ImagePreviewDialog
|
||||
open={showImagePreview}
|
||||
imageUrl={previewImageUrl}
|
||||
onClose={() => setShowImagePreview(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
<div className="flex-1 min-h-0 flex flex-col">{renderContent()}</div>
|
||||
</div>
|
||||
<ImagePreviewDialog
|
||||
open={showImagePreview}
|
||||
imageUrl={previewImageUrl}
|
||||
onClose={() => setShowImagePreview(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,716 @@
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
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 {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import {
|
||||
Play,
|
||||
Pause,
|
||||
StepForward,
|
||||
Square,
|
||||
Bug,
|
||||
Terminal,
|
||||
Eye,
|
||||
Circle,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Loader2,
|
||||
XCircle,
|
||||
Plus,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface WorkflowDebuggerProps {
|
||||
workflowId: string;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const statusIcons: Record<string, React.ElementType> = {
|
||||
pending: Clock,
|
||||
running: Loader2,
|
||||
completed: CheckCircle2,
|
||||
failed: AlertCircle,
|
||||
skipped: XCircle,
|
||||
};
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
pending: 'text-yellow-500',
|
||||
running: 'text-blue-500 animate-spin',
|
||||
completed: 'text-green-500',
|
||||
failed: 'text-red-500',
|
||||
skipped: 'text-gray-400',
|
||||
};
|
||||
|
||||
const logLevelColors: Record<string, string> = {
|
||||
info: 'text-blue-400',
|
||||
warning: 'text-yellow-400',
|
||||
error: 'text-red-400',
|
||||
debug: 'text-gray-400',
|
||||
};
|
||||
|
||||
export default function WorkflowDebugger({ workflowId, onClose }: WorkflowDebuggerProps) {
|
||||
const { t } = useTranslation();
|
||||
const logsEndRef = useRef<HTMLDivElement>(null);
|
||||
const [activeTab, setActiveTab] = useState<string>('context');
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const [newVariable, setNewVariable] = useState({ key: '', value: '' });
|
||||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
||||
const pollCancelledRef = useRef(false);
|
||||
|
||||
const {
|
||||
debugMode,
|
||||
debugState,
|
||||
debugExecutionId,
|
||||
currentNodeId,
|
||||
nodeExecutionResults,
|
||||
breakpoints,
|
||||
debugLogs,
|
||||
debugContext,
|
||||
watchedVariables,
|
||||
nodes,
|
||||
setDebugMode,
|
||||
setDebugState,
|
||||
setDebugExecutionId,
|
||||
setCurrentNodeId,
|
||||
updateNodeExecutionResult,
|
||||
clearNodeExecutionResults,
|
||||
toggleBreakpoint,
|
||||
clearBreakpoints,
|
||||
addDebugLog,
|
||||
clearDebugLogs,
|
||||
setDebugContext,
|
||||
resetDebugContext,
|
||||
addWatchedVariable,
|
||||
removeWatchedVariable,
|
||||
resetDebugState,
|
||||
} = useWorkflowStore();
|
||||
|
||||
// Cleanup polling on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
pollCancelledRef.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Auto-scroll logs
|
||||
useEffect(() => {
|
||||
if (autoScroll && logsEndRef.current) {
|
||||
logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [debugLogs, autoScroll]);
|
||||
|
||||
// Start debug execution
|
||||
const handleStart = useCallback(async () => {
|
||||
try {
|
||||
setDebugState('running');
|
||||
clearNodeExecutionResults();
|
||||
clearDebugLogs();
|
||||
|
||||
addDebugLog({ level: 'info', message: t('workflows.debug.starting') });
|
||||
|
||||
const response = await backendClient.startWorkflowDebug(workflowId, {
|
||||
context: {
|
||||
message_content: debugContext.messageContent,
|
||||
sender_id: debugContext.senderId,
|
||||
sender_name: debugContext.senderName,
|
||||
platform: debugContext.platform,
|
||||
conversation_id: debugContext.conversationId,
|
||||
is_group: debugContext.isGroup,
|
||||
},
|
||||
variables: debugContext.customVariables,
|
||||
breakpoints: Object.keys(breakpoints).filter(k => breakpoints[k]),
|
||||
});
|
||||
|
||||
setDebugExecutionId(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}` });
|
||||
}
|
||||
}, [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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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]);
|
||||
|
||||
// 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}` });
|
||||
}
|
||||
}, [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}` });
|
||||
}
|
||||
}, [workflowId, debugExecutionId, t, pollDebugState]);
|
||||
|
||||
// Step execution
|
||||
const handleStep = useCallback(async () => {
|
||||
if (!debugExecutionId) return;
|
||||
|
||||
try {
|
||||
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',
|
||||
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}` });
|
||||
}
|
||||
}, [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}` });
|
||||
}
|
||||
}, [workflowId, debugExecutionId, t, resetDebugState]);
|
||||
|
||||
// Toggle node expansion
|
||||
const toggleNodeExpanded = (nodeId: string) => {
|
||||
setExpandedNodes((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(nodeId)) {
|
||||
newSet.delete(nodeId);
|
||||
} else {
|
||||
newSet.add(nodeId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// Add custom variable
|
||||
const handleAddVariable = () => {
|
||||
if (newVariable.key.trim()) {
|
||||
try {
|
||||
const value = JSON.parse(newVariable.value);
|
||||
setDebugContext({
|
||||
customVariables: {
|
||||
...debugContext.customVariables,
|
||||
[newVariable.key]: value,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
setDebugContext({
|
||||
customVariables: {
|
||||
...debugContext.customVariables,
|
||||
[newVariable.key]: newVariable.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
setNewVariable({ key: '', value: '' });
|
||||
}
|
||||
};
|
||||
|
||||
// Remove custom variable
|
||||
const handleRemoveVariable = (key: string) => {
|
||||
const newVars = { ...debugContext.customVariables };
|
||||
delete newVars[key];
|
||||
setDebugContext({ customVariables: newVars });
|
||||
};
|
||||
|
||||
const isRunning = debugState === 'running';
|
||||
const isPaused = debugState === 'paused';
|
||||
const canStart = debugState === 'idle' || debugState === 'completed' || debugState === 'error';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-background border-l">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bug className="size-5 text-primary" />
|
||||
<span className="font-semibold">{t('workflows.debug.title')}</span>
|
||||
{debugState !== 'idle' && (
|
||||
<Badge variant={isRunning ? 'default' : isPaused ? 'secondary' : 'outline'}>
|
||||
{t(`workflows.debug.state.${debugState}`)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{onClose && (
|
||||
<Button variant="ghost" size="icon" onClick={onClose}>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Control Bar */}
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-b bg-muted/30">
|
||||
{canStart ? (
|
||||
<Button size="sm" onClick={handleStart} className="gap-1">
|
||||
<Play className="size-4" />
|
||||
{t('workflows.debug.start')}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
{isRunning ? (
|
||||
<Button size="sm" variant="secondary" onClick={handlePause} className="gap-1">
|
||||
<Pause className="size-4" />
|
||||
{t('workflows.debug.pause')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="sm" onClick={handleResume} className="gap-1">
|
||||
<Play className="size-4" />
|
||||
{t('workflows.debug.resume')}
|
||||
</Button>
|
||||
)}
|
||||
<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">
|
||||
<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')}>
|
||||
<Circle className="size-4" />
|
||||
</Button>
|
||||
<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">
|
||||
<TabsList className="mx-4 mt-2 justify-start">
|
||||
<TabsTrigger value="context" className="gap-1">
|
||||
<Terminal className="size-4" />
|
||||
{t('workflows.debug.context')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="variables" className="gap-1">
|
||||
<Eye className="size-4" />
|
||||
{t('workflows.debug.variables')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="nodes" className="gap-1">
|
||||
<CheckCircle2 className="size-4" />
|
||||
{t('workflows.debug.nodeStates')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="logs" className="gap-1">
|
||||
<Terminal className="size-4" />
|
||||
{t('workflows.debug.logs')}
|
||||
{debugLogs.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 text-xs">
|
||||
{debugLogs.length}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Context Tab */}
|
||||
<TabsContent value="context" className="flex-1 p-4 overflow-auto">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('workflows.debug.messageContent')}</Label>
|
||||
<Textarea
|
||||
value={debugContext.messageContent}
|
||||
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 })}
|
||||
placeholder={t('workflows.debug.senderIdPlaceholder')}
|
||||
disabled={!canStart}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('workflows.debug.senderName')}</Label>
|
||||
<Input
|
||||
value={debugContext.senderName}
|
||||
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 })}
|
||||
placeholder={t('workflows.debug.platformPlaceholder')}
|
||||
disabled={!canStart}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t('workflows.debug.conversationId')}</Label>
|
||||
<Input
|
||||
value={debugContext.conversationId}
|
||||
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 })}
|
||||
disabled={!canStart}
|
||||
/>
|
||||
<Label>{t('workflows.debug.isGroup')}</Label>
|
||||
</div>
|
||||
|
||||
{/* Custom Variables */}
|
||||
<Card>
|
||||
<CardHeader className="py-3">
|
||||
<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>
|
||||
))}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t('workflows.debug.variableKey')}
|
||||
value={newVariable.key}
|
||||
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 })}
|
||||
className="text-xs"
|
||||
disabled={!canStart}
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={handleAddVariable}
|
||||
disabled={!canStart}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button variant="outline" size="sm" onClick={resetDebugContext} disabled={!canStart}>
|
||||
<RefreshCw className="size-4 mr-2" />
|
||||
{t('workflows.debug.resetContext')}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Variables Tab */}
|
||||
<TabsContent value="variables" className="flex-1 p-4 overflow-auto">
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader className="py-3">
|
||||
<CardTitle className="text-sm">{t('workflows.debug.watchedVariables')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="py-2">
|
||||
{watchedVariables.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('workflows.debug.noWatchedVariables')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{watchedVariables.map((variable) => {
|
||||
// Try to find value from node outputs
|
||||
let value: unknown = undefined;
|
||||
const parts = variable.split('.');
|
||||
if (parts[0] === 'nodes' && parts.length >= 3) {
|
||||
const nodeId = parts[1];
|
||||
const outputKey = parts.slice(2).join('.');
|
||||
const nodeResult = nodeExecutionResults[nodeId];
|
||||
if (nodeResult?.outputs) {
|
||||
value = nodeResult.outputs[outputKey];
|
||||
}
|
||||
}
|
||||
|
||||
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'}
|
||||
</code>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-6"
|
||||
onClick={() => removeWatchedVariable(variable)}
|
||||
>
|
||||
<X className="size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Node Outputs */}
|
||||
<Card>
|
||||
<CardHeader className="py-3">
|
||||
<CardTitle className="text-sm">{t('workflows.debug.nodeOutputs')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="py-2">
|
||||
{Object.keys(nodeExecutionResults).length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('workflows.debug.noNodeOutputs')}
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Node States Tab */}
|
||||
<TabsContent value="nodes" className="flex-1 p-4 overflow-auto">
|
||||
<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 isCurrentNode = currentNodeId === node.id;
|
||||
const hasBreakpoint = !!breakpoints[node.id];
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={node.id}
|
||||
className={`${isCurrentNode ? 'border-primary ring-1 ring-primary' : ''}`}
|
||||
>
|
||||
<CardHeader className="py-2 px-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => toggleBreakpoint(node.id)}
|
||||
className={`size-4 rounded-full border-2 ${
|
||||
hasBreakpoint
|
||||
? 'bg-red-500 border-red-500'
|
||||
: 'border-gray-400 hover:border-red-400'
|
||||
}`}
|
||||
title={t('workflows.debug.toggleBreakpoint')}
|
||||
/>
|
||||
<StatusIcon className={`size-4 ${statusColor}`} />
|
||||
<span className="text-sm font-medium">{node.data.label}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{node.data.type}
|
||||
</Badge>
|
||||
</div>
|
||||
{result?.duration !== undefined && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{result.duration}ms
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
{result?.error && (
|
||||
<CardContent className="py-2 px-3">
|
||||
<div className="text-xs text-red-500 bg-red-50 dark:bg-red-950 p-2 rounded">
|
||||
{result.error}
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Logs Tab */}
|
||||
<TabsContent value="logs" className="flex-1 flex flex-col min-h-0">
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={autoScroll}
|
||||
onCheckedChange={setAutoScroll}
|
||||
className="scale-75"
|
||||
/>
|
||||
<Label className="text-xs">{t('workflows.debug.autoScroll')}</Label>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{debugLogs.length} {t('workflows.debug.logEntries')}
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
) : (
|
||||
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]}`}>
|
||||
[{log.level}]
|
||||
</span>
|
||||
{log.nodeId && (
|
||||
<span className="text-purple-400 shrink-0">[{log.nodeId}]</span>
|
||||
)}
|
||||
<span className="text-foreground">{log.message}</span>
|
||||
{log.data && (
|
||||
<span className="text-muted-foreground">
|
||||
{JSON.stringify(log.data)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as WorkflowDebugger } from './WorkflowDebugger';
|
||||
@@ -0,0 +1,334 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useWorkflowStore } from '../../store/useWorkflowStore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Search,
|
||||
Settings,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
ExternalLink,
|
||||
Layers,
|
||||
Cpu,
|
||||
} from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
NODE_ICONS,
|
||||
NODE_TYPE_I18N_KEYS,
|
||||
CATEGORY_I18N_KEYS,
|
||||
PALETTE_CATEGORY_COLORS as categoryColors,
|
||||
PALETTE_CATEGORY_BG as categoryBgColors,
|
||||
PALETTE_CATEGORY_BORDER as categoryBorderColors,
|
||||
CATEGORY_ICONS as categoryIcons,
|
||||
findNodeI18nKeys,
|
||||
} from './workflow-constants';
|
||||
import { resolveI18nLabel } from './workflow-i18n';
|
||||
|
||||
// Use shared icon mapping
|
||||
const nodeIcons = NODE_ICONS;
|
||||
|
||||
// Use shared i18n key mapping
|
||||
const nodeTypeI18nKeys = NODE_TYPE_I18N_KEYS;
|
||||
|
||||
// Use shared category i18n keys
|
||||
const categoryI18nKeys = CATEGORY_I18N_KEYS;
|
||||
|
||||
// Common node type definition for UI purposes
|
||||
interface NodeTypeForUI {
|
||||
type: string;
|
||||
category: string;
|
||||
labelKey?: string;
|
||||
descriptionKey?: string;
|
||||
// Also support raw label dict from backend
|
||||
label?: Record<string, string>;
|
||||
description?: Record<string, string>;
|
||||
}
|
||||
|
||||
// Default node types generated from shared constants
|
||||
const defaultNodeTypes: NodeTypeForUI[] = Object.entries(NODE_TYPE_I18N_KEYS).map(([type, keys]) => ({
|
||||
type,
|
||||
category: type.split('.')[0],
|
||||
labelKey: keys.labelKey,
|
||||
descriptionKey: keys.descriptionKey,
|
||||
}));
|
||||
|
||||
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'])
|
||||
);
|
||||
|
||||
// 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]);
|
||||
|
||||
// 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]);
|
||||
|
||||
// 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,
|
||||
labelKey: i18nKeys?.labelKey,
|
||||
descriptionKey: i18nKeys?.descriptionKey,
|
||||
// Keep raw label dict as fallback for unknown nodes
|
||||
label: i18nKeys ? undefined : node.label,
|
||||
description: i18nKeys ? undefined : node.description,
|
||||
};
|
||||
});
|
||||
}
|
||||
return defaultNodeTypes;
|
||||
}, [backendNodeTypes]);
|
||||
|
||||
// Filter nodes based on search query
|
||||
const filteredNodes = useMemo(() => {
|
||||
if (!searchQuery.trim()) {
|
||||
return nodeTypes;
|
||||
}
|
||||
const query = searchQuery.toLowerCase();
|
||||
return nodeTypes.filter((node) => {
|
||||
const label = getNodeLabel(node);
|
||||
const description = getNodeDescription(node);
|
||||
return (
|
||||
label.toLowerCase().includes(query) ||
|
||||
description.toLowerCase().includes(query) ||
|
||||
node.type.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
}, [nodeTypes, searchQuery, getNodeLabel, getNodeDescription]);
|
||||
|
||||
// Group filtered nodes by category
|
||||
const groupedNodes = useMemo(() => {
|
||||
const groups: Record<string, typeof nodeTypes> = {};
|
||||
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]) {
|
||||
groups[category] = [];
|
||||
}
|
||||
groups[category].push(node);
|
||||
}
|
||||
|
||||
// Remove empty categories
|
||||
Object.keys(groups).forEach((key) => {
|
||||
if (groups[key].length === 0) {
|
||||
delete groups[key];
|
||||
}
|
||||
});
|
||||
|
||||
return groups;
|
||||
}, [filteredNodes]);
|
||||
|
||||
// Toggle category expansion
|
||||
const toggleCategory = useCallback((category: string) => {
|
||||
setExpandedCategories((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(category)) {
|
||||
next.delete(category);
|
||||
} else {
|
||||
next.add(category);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Handle drag start
|
||||
const onDragStart = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>, nodeType: string) => {
|
||||
event.dataTransfer.setData('application/reactflow', nodeType);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', nodeType);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Get category label using i18n
|
||||
const getCategoryLabel = useCallback(
|
||||
(categoryName: string) => {
|
||||
const i18nKey = categoryI18nKeys[categoryName];
|
||||
if (i18nKey) {
|
||||
return t(i18nKey.labelKey, { defaultValue: categoryName });
|
||||
}
|
||||
// Fallback to backend category label dict
|
||||
const category = nodeCategories?.find((c) => c.name === categoryName);
|
||||
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['en'] || category.label['en-US'] || categoryName;
|
||||
}
|
||||
return categoryName;
|
||||
},
|
||||
[nodeCategories, t, i18n.language]
|
||||
);
|
||||
|
||||
// Clear search
|
||||
const clearSearch = useCallback(() => {
|
||||
setSearchQuery('');
|
||||
}, []);
|
||||
|
||||
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 && (
|
||||
<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="text-primary hover:underline mt-2"
|
||||
>
|
||||
{t('workflows.clearSearch')}
|
||||
</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);
|
||||
|
||||
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 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" />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,713 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useWorkflowStore } from '../../store/useWorkflowStore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import {
|
||||
Trash2,
|
||||
Settings,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
Variable,
|
||||
Code,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
Info,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||
import { getNodeConfig } from './node-configs';
|
||||
import i18n from 'i18next';
|
||||
import { I18nObject } from '@/app/infra/entities/common';
|
||||
import { normalizeWorkflowNodeTypeMeta } from './workflow-node-metadata';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { resolveI18nLabel, maybeTranslateKey } from './workflow-i18n';
|
||||
|
||||
// Delegate to shared utility
|
||||
const translateIfKey = (value: string | undefined): string | undefined => {
|
||||
if (!value) return value;
|
||||
return maybeTranslateKey(value);
|
||||
};
|
||||
|
||||
// Delegate to shared utility
|
||||
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;
|
||||
};
|
||||
|
||||
// Get translated type label using i18n
|
||||
const getTypeLabel = (type: string | undefined): string => {
|
||||
if (!type) return '';
|
||||
const i18nKey = `workflows.type.${type.toLowerCase()}`;
|
||||
const translated = i18n.t(i18nKey);
|
||||
// If translation key doesn't exist, return the original type
|
||||
return translated === i18nKey ? type : translated;
|
||||
};
|
||||
|
||||
interface PropertyPanelProps {
|
||||
selectedNodeId: string | null;
|
||||
selectedEdgeId: string | null;
|
||||
}
|
||||
|
||||
// Variable reference component
|
||||
// Format variable reference to show only the short name
|
||||
const formatVariableName = (fullPath: string): string => {
|
||||
// nodes.node_xxx.body -> body
|
||||
// message.content -> content
|
||||
const parts = fullPath.split('.');
|
||||
return parts.length > 2 ? parts.slice(2).join('.') : parts[parts.length - 1];
|
||||
};
|
||||
|
||||
const resolvePortDisplayLabel = (
|
||||
port: { name: string; label?: string | Record<string, string> | I18nObject },
|
||||
prefix: 'workflows.nodeInputs' | 'workflows.nodeOutputs',
|
||||
): string => {
|
||||
if (port.label) {
|
||||
if (typeof port.label === 'object') {
|
||||
const resolved = extractI18nLabel(port.label as Record<string, string>);
|
||||
if (resolved) return resolved;
|
||||
} else {
|
||||
const resolved = translateIfKey(port.label);
|
||||
if (resolved && resolved !== port.label) return resolved;
|
||||
if (resolved && !resolved.startsWith('workflows.')) return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
const translated = translateIfKey(`${prefix}.${port.name}`);
|
||||
return translated && translated !== `${prefix}.${port.name}`
|
||||
? translated
|
||||
: port.name;
|
||||
};
|
||||
|
||||
function VariableReference({
|
||||
variable,
|
||||
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>
|
||||
{variable.type && (
|
||||
<Badge variant="outline" className="text-[10px] px-1 py-0 flex-shrink-0">
|
||||
{getTypeLabel(variable.type)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-6 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
|
||||
onClick={() => onCopy(ref)}
|
||||
>
|
||||
<Copy className="size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" className="max-w-xs">
|
||||
<code className="text-xs break-all">{ref}</code>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// Collapsible section component
|
||||
function CollapsibleSection({
|
||||
title,
|
||||
icon: Icon,
|
||||
children,
|
||||
defaultOpen = true,
|
||||
badge,
|
||||
}: {
|
||||
title: string;
|
||||
icon: React.ElementType;
|
||||
children: React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
badge?: React.ReactNode;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<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 ? (
|
||||
<ChevronDown className="size-4 text-muted-foreground flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="size-4 text-muted-foreground flex-shrink-0" />
|
||||
)}
|
||||
<Icon className="size-4 text-muted-foreground flex-shrink-0" />
|
||||
<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>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PropertyPanel({
|
||||
selectedNodeId,
|
||||
selectedEdgeId,
|
||||
}: PropertyPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
nodeTypes,
|
||||
updateNodeConfig,
|
||||
updateNodeLabel,
|
||||
deleteNode,
|
||||
deleteEdge,
|
||||
updateEdgeCondition,
|
||||
pushHistory,
|
||||
} = useWorkflowStore();
|
||||
|
||||
// Use extractI18nObject for i18n handling (it automatically handles language detection)
|
||||
|
||||
// Get selected node
|
||||
const selectedNode = useMemo(() => {
|
||||
if (!selectedNodeId) return null;
|
||||
return nodes.find((n) => n.id === selectedNodeId) || null;
|
||||
}, [nodes, selectedNodeId]);
|
||||
|
||||
// Get selected edge
|
||||
const selectedEdge = useMemo(() => {
|
||||
if (!selectedEdgeId) return null;
|
||||
return edges.find((e) => e.id === selectedEdgeId) || null;
|
||||
}, [edges, selectedEdgeId]);
|
||||
|
||||
// Get node type metadata for selected node
|
||||
// Priority: API metadata first, local registry as normalized fallback
|
||||
const nodeTypeMeta = useMemo(() => {
|
||||
if (!selectedNode) return null;
|
||||
|
||||
const nodeType = selectedNode.data.type;
|
||||
return normalizeWorkflowNodeTypeMeta(
|
||||
nodeType,
|
||||
nodeTypes.find((t) => t.type === nodeType),
|
||||
);
|
||||
}, [selectedNode, nodeTypes]);
|
||||
|
||||
// Get local node config for additional metadata not carried by backend schema
|
||||
const localNodeConfig = useMemo(() => {
|
||||
if (!selectedNode) return null;
|
||||
const nodeType = selectedNode.data.type;
|
||||
return getNodeConfig(nodeType) || null;
|
||||
}, [selectedNode]);
|
||||
|
||||
// Prefer local registry config schema so workflow editor can reuse the existing
|
||||
// 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;
|
||||
|
||||
return rawConfigSchema.map((item) => {
|
||||
const backendItem = backendConfigSchema.find(
|
||||
(candidate) => candidate.name === item.name || candidate.id === item.id,
|
||||
);
|
||||
|
||||
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: '',
|
||||
},
|
||||
options: item.options || backendItem?.options,
|
||||
show_if: item.show_if || backendItem?.show_if,
|
||||
};
|
||||
});
|
||||
}, [localNodeConfig?.configSchema, nodeTypeMeta?.config_schema]);
|
||||
|
||||
// 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 }[] }[] = [];
|
||||
|
||||
// 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) {
|
||||
const outputs = node.data.outputs || [{ name: 'output', type: 'any' }];
|
||||
variables.push({
|
||||
nodeId: node.id,
|
||||
nodeLabel: node.data.label,
|
||||
outputs: outputs.map((o) => ({
|
||||
name: `nodes.${node.id}.${o.name}`,
|
||||
label: resolvePortDisplayLabel(o, 'workflows.nodeOutputs'),
|
||||
type: o.type,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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' },
|
||||
],
|
||||
});
|
||||
|
||||
return variables;
|
||||
}, [selectedNode, edges, nodes, t]);
|
||||
|
||||
// Handle label change
|
||||
const handleLabelChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (selectedNodeId) {
|
||||
updateNodeLabel(selectedNodeId, e.target.value);
|
||||
}
|
||||
},
|
||||
[selectedNodeId, updateNodeLabel]
|
||||
);
|
||||
|
||||
// Handle config change from dynamic form
|
||||
const handleConfigChange = useCallback(
|
||||
(values: object): unknown => {
|
||||
if (selectedNodeId) {
|
||||
updateNodeConfig(selectedNodeId, values as Record<string, unknown>);
|
||||
pushHistory();
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
[selectedNodeId, updateNodeConfig, pushHistory]
|
||||
);
|
||||
|
||||
// Handle node delete
|
||||
const handleDeleteNode = useCallback(() => {
|
||||
if (selectedNodeId) {
|
||||
deleteNode(selectedNodeId);
|
||||
}
|
||||
}, [selectedNodeId, deleteNode]);
|
||||
|
||||
// Handle edge delete
|
||||
const handleDeleteEdge = useCallback(() => {
|
||||
if (selectedEdgeId) {
|
||||
deleteEdge(selectedEdgeId);
|
||||
}
|
||||
}, [selectedEdgeId, deleteEdge]);
|
||||
|
||||
// Handle edge condition change
|
||||
const handleConditionChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (selectedEdgeId) {
|
||||
updateEdgeCondition(selectedEdgeId, e.target.value);
|
||||
}
|
||||
},
|
||||
[selectedEdgeId, updateEdgeCondition]
|
||||
);
|
||||
|
||||
// Copy variable reference
|
||||
const handleCopyVariable = useCallback((ref: string) => {
|
||||
navigator.clipboard.writeText(ref);
|
||||
toast.success(t('common.copySuccess'));
|
||||
}, [t]);
|
||||
|
||||
// No selection
|
||||
if (!selectedNodeId && !selectedEdgeId) {
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<div className="p-4 border-b w-full overflow-hidden">
|
||||
<h3 className="font-semibold text-sm flex items-center gap-2">
|
||||
<Settings className="size-4" />
|
||||
{t('workflows.properties')}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center w-full overflow-hidden">
|
||||
<div className="text-center p-8 w-full max-w-full overflow-hidden">
|
||||
<Settings className="size-12 mx-auto mb-3 opacity-20" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('workflows.selectNodeOrEdge')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{t('workflows.selectNodeOrEdgeHint')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Edge selected
|
||||
if (selectedEdge) {
|
||||
const sourceNode = nodes.find((n) => n.id === selectedEdge.source);
|
||||
const targetNode = nodes.find((n) => n.id === selectedEdge.target);
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<div className="p-4 border-b w-full overflow-hidden">
|
||||
<h3 className="font-semibold text-sm flex items-center gap-2">
|
||||
<ArrowRight className="size-4" />
|
||||
{t('workflows.edgeProperties')}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 w-full min-h-0">
|
||||
<div className="p-4 space-y-4 w-full overflow-hidden box-border">
|
||||
{/* 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">
|
||||
{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">
|
||||
{targetNode?.data.label || selectedEdge.target}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Condition */}
|
||||
<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}
|
||||
>
|
||||
<div className="space-y-2 w-full overflow-hidden">
|
||||
<Textarea
|
||||
value={selectedEdge.data?.condition || ''}
|
||||
onChange={handleConditionChange}
|
||||
placeholder={t('workflows.conditionPlaceholder')}
|
||||
rows={4}
|
||||
className="font-mono text-sm w-full"
|
||||
/>
|
||||
<div className="flex items-start gap-2 text-xs text-muted-foreground">
|
||||
<Info className="size-3.5 mt-0.5 flex-shrink-0" />
|
||||
<p>{t('workflows.conditionHelp')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Delete edge */}
|
||||
<div className="w-full overflow-hidden">
|
||||
<AlertDialog>
|
||||
<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>
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<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]">
|
||||
{t('common.delete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// 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' }];
|
||||
|
||||
// Extract i18n labels using extractI18nLabel
|
||||
const nodeLabel = nodeTypeMeta?.label
|
||||
? extractI18nLabel(nodeTypeMeta.label)
|
||||
: selectedNode.data.type;
|
||||
const nodeDescription = nodeTypeMeta?.description
|
||||
? extractI18nLabel(nodeTypeMeta.description)
|
||||
: undefined;
|
||||
|
||||
// Get node category color from local config
|
||||
const nodeColor = localNodeConfig?.color || nodeTypeMeta?.color;
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b w-full overflow-hidden">
|
||||
<h3 className="font-semibold text-sm flex items-center gap-2">
|
||||
<Sparkles className="size-4" />
|
||||
{t('workflows.nodeProperties')}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 w-full min-h-0">
|
||||
<div className="p-4 space-y-4 w-full box-border">
|
||||
{/* Node type info */}
|
||||
<div className="bg-muted/50 p-3 rounded-lg space-y-2 w-full overflow-hidden">
|
||||
<div className="flex items-center gap-2 min-w-0 overflow-hidden">
|
||||
<Badge className="font-mono text-xs truncate max-w-full">
|
||||
{selectedNode.data.type}
|
||||
</Badge>
|
||||
</div>
|
||||
{nodeDescription && (
|
||||
<p className="text-xs text-muted-foreground">{nodeDescription}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Basic Info */}
|
||||
<CollapsibleSection
|
||||
title={t('workflows.basicInfo')}
|
||||
icon={Settings}
|
||||
>
|
||||
<div className="space-y-3 w-full overflow-hidden">
|
||||
<div className="space-y-1.5 w-full overflow-hidden">
|
||||
<Label htmlFor="node-label" className="text-xs">
|
||||
{t('workflows.nodeLabel')}
|
||||
</Label>
|
||||
<Input
|
||||
id="node-label"
|
||||
value={selectedNode.data.label}
|
||||
onChange={handleLabelChange}
|
||||
placeholder={t('workflows.nodeLabelPlaceholder')}
|
||||
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')}
|
||||
</Label>
|
||||
<div className="flex items-center gap-2 min-w-0 w-full overflow-hidden">
|
||||
<code className="text-xs bg-muted px-2 py-1 rounded flex-1 truncate min-w-0 overflow-hidden">
|
||||
{selectedNode.id}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-6 flex-shrink-0"
|
||||
onClick={() => handleCopyVariable(selectedNode.id)}
|
||||
>
|
||||
<Copy className="size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Input/Output Variables */}
|
||||
<CollapsibleSection
|
||||
title={t('workflows.inputOutputVariables')}
|
||||
icon={Variable}
|
||||
badge={
|
||||
<Badge variant="secondary" className="text-xs flex-shrink-0">
|
||||
{nodeInputs.length} / {nodeOutputs.length}
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3 w-full overflow-hidden">
|
||||
{/* Inputs */}
|
||||
<div className="space-y-1.5 w-full overflow-hidden">
|
||||
<div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
||||
<ArrowLeft className="size-3" />
|
||||
{t('workflows.inputs')}
|
||||
</div>
|
||||
<div className="space-y-1 w-full overflow-hidden">
|
||||
{nodeInputs.map((input) => (
|
||||
<div
|
||||
key={input.name}
|
||||
className="flex items-center gap-2 py-1 px-2 rounded bg-blue-50 dark:bg-blue-950/30 text-sm overflow-hidden w-full min-w-0"
|
||||
>
|
||||
<Variable className="size-3.5 text-blue-600 dark:text-blue-400 flex-shrink-0" />
|
||||
<span className="font-mono text-xs truncate min-w-0 flex-1">
|
||||
{resolvePortDisplayLabel(input, 'workflows.nodeInputs')}
|
||||
</span>
|
||||
{input.type && (
|
||||
<Badge variant="outline" className="text-[10px] px-1 py-0 flex-shrink-0">
|
||||
{getTypeLabel(input.type)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Outputs */}
|
||||
<div className="space-y-1.5 w-full overflow-hidden">
|
||||
<div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
||||
<ArrowRight className="size-3" />
|
||||
{t('workflows.outputs')}
|
||||
</div>
|
||||
<div className="space-y-1 w-full overflow-hidden">
|
||||
{nodeOutputs.map((output) => (
|
||||
<VariableReference
|
||||
key={output.name}
|
||||
variable={{
|
||||
name: `nodes.${selectedNode.id}.${output.name}`,
|
||||
label: resolvePortDisplayLabel(output, 'workflows.nodeOutputs'),
|
||||
type: output.type,
|
||||
}}
|
||||
onCopy={handleCopyVariable}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Available Variables from upstream nodes */}
|
||||
{availableInputVariables.length > 0 && (
|
||||
<CollapsibleSection
|
||||
title={t('workflows.availableVariables')}
|
||||
icon={Code}
|
||||
defaultOpen={false}
|
||||
>
|
||||
<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 className="text-xs font-medium text-muted-foreground">
|
||||
{group.nodeLabel}
|
||||
</div>
|
||||
<div className="space-y-1 w-full overflow-hidden">
|
||||
{group.outputs.map((variable) => (
|
||||
<VariableReference
|
||||
key={variable.name}
|
||||
variable={variable}
|
||||
onCopy={handleCopyVariable}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Node configuration */}
|
||||
{configSchema.length > 0 && (
|
||||
<CollapsibleSection
|
||||
title={t('workflows.nodeConfig')}
|
||||
icon={Settings}
|
||||
badge={
|
||||
<Badge variant="secondary" className="text-xs flex-shrink-0">
|
||||
{configSchema.length}
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
<div className="space-y-2 w-full overflow-hidden box-border">
|
||||
<DynamicFormComponent
|
||||
itemConfigList={configSchema}
|
||||
initialValues={selectedNode.data.config as Record<string, object>}
|
||||
onSubmit={handleConfigChange}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Show message if no config schema */}
|
||||
{configSchema.length === 0 && (
|
||||
<div className="text-sm text-muted-foreground text-center py-4 bg-muted/30 rounded-lg">
|
||||
<Settings className="size-6 mx-auto mb-2 opacity-50" />
|
||||
<p>{t('workflows.noConfigOptions')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Delete node */}
|
||||
<div className="w-full overflow-hidden">
|
||||
<AlertDialog>
|
||||
<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>
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<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]">
|
||||
{t('common.delete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,691 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
Controls,
|
||||
MiniMap,
|
||||
Panel,
|
||||
ReactFlowProvider,
|
||||
useReactFlow,
|
||||
BackgroundVariant,
|
||||
SelectionMode,
|
||||
} from '@xyflow/react';
|
||||
import type { Node, NodeTypes, OnSelectionChangeParams } from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import {
|
||||
useWorkflowStore,
|
||||
WorkflowNode,
|
||||
WorkflowEdge,
|
||||
} from '../../store/useWorkflowStore';
|
||||
import WorkflowNodeComponent from './WorkflowNodeComponent';
|
||||
import NodePalette from './NodePalette';
|
||||
import PropertyPanel from './PropertyPanel';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Undo2,
|
||||
Redo2,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Maximize2,
|
||||
Copy,
|
||||
ClipboardPaste,
|
||||
Trash2,
|
||||
Keyboard,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
// Custom node types - use type assertion to satisfy NodeTypes
|
||||
const nodeTypes: NodeTypes = {
|
||||
workflowNode: WorkflowNodeComponent,
|
||||
};
|
||||
|
||||
// Clipboard storage for copy/paste
|
||||
interface ClipboardData {
|
||||
nodes: WorkflowNode[];
|
||||
edges: WorkflowEdge[];
|
||||
}
|
||||
|
||||
// Generate unique ID for pasted nodes
|
||||
const generatePastedNodeId = () =>
|
||||
`node_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const generatePastedEdgeId = () =>
|
||||
`edge_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
function WorkflowEditorInner() {
|
||||
const { t } = useTranslation();
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const { screenToFlowPosition, fitView, zoomIn, zoomOut } = useReactFlow();
|
||||
|
||||
// Clipboard state
|
||||
const [clipboard, setClipboard] = useState<ClipboardData | null>(null);
|
||||
// Multi-selection state
|
||||
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
|
||||
const [selectedEdges, setSelectedEdges] = useState<string[]>([]);
|
||||
// Property panel visibility state
|
||||
const [showPropertyPanel, setShowPropertyPanel] = useState(false);
|
||||
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
onNodesChange,
|
||||
onEdgesChange,
|
||||
onConnect,
|
||||
addNode,
|
||||
selectNode,
|
||||
selectEdge,
|
||||
clearSelection,
|
||||
selectedNodeId,
|
||||
selectedEdgeId,
|
||||
deleteNode,
|
||||
deleteEdge,
|
||||
undo,
|
||||
redo,
|
||||
history,
|
||||
historyIndex,
|
||||
isDirty,
|
||||
setNodes,
|
||||
setEdges,
|
||||
pushHistory,
|
||||
nodeExecutionResults,
|
||||
} = useWorkflowStore();
|
||||
|
||||
// Handle node click
|
||||
const handleNodeClick = useCallback(
|
||||
(_: React.MouseEvent, node: WorkflowNode) => {
|
||||
selectNode(node.id);
|
||||
setShowPropertyPanel(true);
|
||||
},
|
||||
[selectNode],
|
||||
);
|
||||
|
||||
// Handle edge click
|
||||
const handleEdgeClick = useCallback(
|
||||
(_: React.MouseEvent, edge: WorkflowEdge) => {
|
||||
selectEdge(edge.id);
|
||||
},
|
||||
[selectEdge],
|
||||
);
|
||||
|
||||
// Handle pane click (deselect)
|
||||
const handlePaneClick = useCallback(() => {
|
||||
clearSelection();
|
||||
setSelectedNodes([]);
|
||||
setSelectedEdges([]);
|
||||
setShowPropertyPanel(false);
|
||||
}, [clearSelection]);
|
||||
|
||||
// Handle selection change for multi-select
|
||||
const handleSelectionChange = useCallback(
|
||||
({
|
||||
nodes: selectedNodesArr,
|
||||
edges: selectedEdgesArr,
|
||||
}: OnSelectionChangeParams) => {
|
||||
const nodeIds = selectedNodesArr.map((n) => n.id);
|
||||
const edgeIds = selectedEdgesArr.map((e) => e.id);
|
||||
setSelectedNodes(nodeIds);
|
||||
setSelectedEdges(edgeIds);
|
||||
|
||||
// Update single selection for property panel
|
||||
if (nodeIds.length === 1) {
|
||||
selectNode(nodeIds[0]);
|
||||
} else if (edgeIds.length === 1 && nodeIds.length === 0) {
|
||||
selectEdge(edgeIds[0]);
|
||||
} else if (nodeIds.length === 0 && edgeIds.length === 0) {
|
||||
clearSelection();
|
||||
}
|
||||
},
|
||||
[selectNode, selectEdge, clearSelection],
|
||||
);
|
||||
|
||||
// Handle drop from palette
|
||||
const onDrop = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const type = event.dataTransfer.getData('application/reactflow');
|
||||
if (!type || !reactFlowWrapper.current) return;
|
||||
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
|
||||
addNode(type, position);
|
||||
},
|
||||
[screenToFlowPosition, addNode],
|
||||
);
|
||||
|
||||
const onDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}, []);
|
||||
|
||||
// Copy selected nodes
|
||||
const handleCopy = useCallback(() => {
|
||||
const nodesToCopy = nodes.filter(
|
||||
(n) => selectedNodes.includes(n.id) || n.id === selectedNodeId,
|
||||
);
|
||||
|
||||
if (nodesToCopy.length === 0) {
|
||||
toast.error(t('workflows.nothingToCopy'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get edges between selected nodes
|
||||
const nodeIds = new Set(nodesToCopy.map((n) => n.id));
|
||||
const edgesToCopy = edges.filter(
|
||||
(e) => nodeIds.has(e.source) && nodeIds.has(e.target),
|
||||
);
|
||||
|
||||
setClipboard({
|
||||
nodes: nodesToCopy,
|
||||
edges: edgesToCopy,
|
||||
});
|
||||
|
||||
toast.success(t('workflows.copied', { count: nodesToCopy.length }));
|
||||
}, [nodes, edges, selectedNodes, selectedNodeId, t]);
|
||||
|
||||
// Paste nodes from clipboard
|
||||
const handlePaste = useCallback(() => {
|
||||
if (!clipboard || clipboard.nodes.length === 0) {
|
||||
toast.error(t('workflows.nothingToPaste'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create ID mapping for pasted nodes
|
||||
const idMapping: Record<string, string> = {};
|
||||
clipboard.nodes.forEach((node) => {
|
||||
idMapping[node.id] = generatePastedNodeId();
|
||||
});
|
||||
|
||||
// Offset position for pasted nodes
|
||||
const offset = { x: 50, y: 50 };
|
||||
|
||||
// Create new nodes with new IDs and offset positions
|
||||
const newNodes: WorkflowNode[] = clipboard.nodes.map((node) => ({
|
||||
...node,
|
||||
id: idMapping[node.id],
|
||||
position: {
|
||||
x: node.position.x + offset.x,
|
||||
y: node.position.y + offset.y,
|
||||
},
|
||||
selected: true,
|
||||
data: {
|
||||
...node.data,
|
||||
label: `${node.data.label} (copy)`,
|
||||
},
|
||||
}));
|
||||
|
||||
// Create new edges with updated source/target IDs
|
||||
const newEdges: WorkflowEdge[] = clipboard.edges.map((edge) => ({
|
||||
...edge,
|
||||
id: generatePastedEdgeId(),
|
||||
source: idMapping[edge.source],
|
||||
target: idMapping[edge.target],
|
||||
}));
|
||||
|
||||
// Add new nodes and edges
|
||||
setNodes([...nodes.map((n) => ({ ...n, selected: false })), ...newNodes]);
|
||||
setEdges([...edges, ...newEdges]);
|
||||
pushHistory();
|
||||
|
||||
// Select pasted nodes
|
||||
setSelectedNodes(newNodes.map((n) => n.id));
|
||||
|
||||
toast.success(t('workflows.pasted', { count: newNodes.length }));
|
||||
}, [clipboard, nodes, edges, setNodes, setEdges, pushHistory, t]);
|
||||
|
||||
// Delete selected nodes/edges
|
||||
const handleDelete = useCallback(() => {
|
||||
const nodesToDelete =
|
||||
selectedNodes.length > 0
|
||||
? selectedNodes
|
||||
: selectedNodeId
|
||||
? [selectedNodeId]
|
||||
: [];
|
||||
const edgesToDelete =
|
||||
selectedEdges.length > 0
|
||||
? selectedEdges
|
||||
: selectedEdgeId
|
||||
? [selectedEdgeId]
|
||||
: [];
|
||||
|
||||
if (nodesToDelete.length === 0 && edgesToDelete.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete nodes (this will also delete connected edges)
|
||||
nodesToDelete.forEach((nodeId) => {
|
||||
deleteNode(nodeId);
|
||||
});
|
||||
|
||||
// Delete edges
|
||||
edgesToDelete.forEach((edgeId) => {
|
||||
deleteEdge(edgeId);
|
||||
});
|
||||
|
||||
setSelectedNodes([]);
|
||||
setSelectedEdges([]);
|
||||
clearSelection();
|
||||
|
||||
toast.success(t('workflows.deleted'));
|
||||
}, [
|
||||
selectedNodes,
|
||||
selectedEdges,
|
||||
selectedNodeId,
|
||||
selectedEdgeId,
|
||||
deleteNode,
|
||||
deleteEdge,
|
||||
clearSelection,
|
||||
t,
|
||||
]);
|
||||
|
||||
// Select all nodes
|
||||
const handleSelectAll = useCallback(() => {
|
||||
const allNodeIds = nodes.map((n) => n.id);
|
||||
setSelectedNodes(allNodeIds);
|
||||
setNodes(nodes.map((n) => ({ ...n, selected: true })));
|
||||
}, [nodes, setNodes]);
|
||||
|
||||
// Keyboard shortcuts
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
// Prevent shortcuts when typing in input fields
|
||||
if (
|
||||
event.target instanceof HTMLInputElement ||
|
||||
event.target instanceof HTMLTextAreaElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCtrlOrCmd = event.ctrlKey || event.metaKey;
|
||||
|
||||
// Ctrl/Cmd + Z: Undo
|
||||
if (isCtrlOrCmd && event.key === 'z' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
undo();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + Shift + Z or Ctrl/Cmd + Y: Redo
|
||||
if (
|
||||
(isCtrlOrCmd && event.shiftKey && event.key === 'z') ||
|
||||
(isCtrlOrCmd && event.key === 'y')
|
||||
) {
|
||||
event.preventDefault();
|
||||
redo();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + C: Copy
|
||||
if (isCtrlOrCmd && event.key === 'c') {
|
||||
event.preventDefault();
|
||||
handleCopy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + V: Paste
|
||||
if (isCtrlOrCmd && event.key === 'v') {
|
||||
event.preventDefault();
|
||||
handlePaste();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + A: Select All
|
||||
if (isCtrlOrCmd && event.key === 'a') {
|
||||
event.preventDefault();
|
||||
handleSelectAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete or Backspace: Delete selected
|
||||
if (event.key === 'Delete' || event.key === 'Backspace') {
|
||||
event.preventDefault();
|
||||
handleDelete();
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape: Clear selection
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
clearSelection();
|
||||
setSelectedNodes([]);
|
||||
setSelectedEdges([]);
|
||||
return;
|
||||
}
|
||||
},
|
||||
[
|
||||
undo,
|
||||
redo,
|
||||
handleCopy,
|
||||
handlePaste,
|
||||
handleSelectAll,
|
||||
handleDelete,
|
||||
clearSelection,
|
||||
],
|
||||
);
|
||||
|
||||
// Memoize mini map node color
|
||||
const minimapNodeColor = useCallback((node: Node) => {
|
||||
const workflowNode = node as WorkflowNode;
|
||||
const categoryColors: Record<string, string> = {
|
||||
trigger: '#22c55e',
|
||||
process: '#3b82f6',
|
||||
control: '#f59e0b',
|
||||
action: '#8b5cf6',
|
||||
integration: '#ec4899',
|
||||
};
|
||||
// Extract category from node type (e.g., 'trigger.message' -> 'trigger')
|
||||
const category = workflowNode.data?.type?.split('.')[0] || 'process';
|
||||
return categoryColors[category] || '#6b7280';
|
||||
}, []);
|
||||
|
||||
const displayNodes = nodes.map((node) => {
|
||||
const executionResult = nodeExecutionResults[node.id];
|
||||
|
||||
if (!executionResult) {
|
||||
return node;
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
executionStatus: executionResult.status,
|
||||
executionError: executionResult.error,
|
||||
executionDuration: executionResult.duration,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const canUndo = historyIndex > 0;
|
||||
const canRedo = historyIndex < history.length - 1;
|
||||
const hasSelection = selectedNodes.length > 0 || selectedNodeId !== null;
|
||||
const hasClipboard = clipboard !== null && clipboard.nodes.length > 0;
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div
|
||||
ref={reactFlowWrapper}
|
||||
className="h-full w-full flex"
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
>
|
||||
{/* Left: Node Palette */}
|
||||
<div className="w-64 border-r bg-muted/30 overflow-y-auto flex-shrink-0">
|
||||
<NodePalette />
|
||||
</div>
|
||||
|
||||
{/* Center: Flow Canvas */}
|
||||
<div className="flex-1 relative">
|
||||
<ReactFlow
|
||||
nodes={displayNodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onNodeClick={handleNodeClick}
|
||||
onEdgeClick={handleEdgeClick}
|
||||
onPaneClick={handlePaneClick}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
snapToGrid
|
||||
snapGrid={[15, 15]}
|
||||
selectionMode={SelectionMode.Partial}
|
||||
selectionOnDrag
|
||||
panOnDrag={[1, 2]} // Middle click and right click to pan
|
||||
selectNodesOnDrag={false}
|
||||
defaultEdgeOptions={{
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
}}
|
||||
deleteKeyCode={null} // We handle delete manually
|
||||
>
|
||||
<Background
|
||||
gap={15}
|
||||
size={1}
|
||||
variant={BackgroundVariant.Dots}
|
||||
color="hsl(var(--muted-foreground) / 0.3)"
|
||||
/>
|
||||
<Controls
|
||||
showInteractive={false}
|
||||
className="!bg-background !border-border !shadow-md [&_button]:!bg-background [&_button]:!border-border [&_button]:!fill-foreground [&_button:hover]:!bg-muted"
|
||||
/>
|
||||
<MiniMap
|
||||
nodeColor={minimapNodeColor}
|
||||
maskColor="rgba(0, 0, 0, 0.1)"
|
||||
className="!bg-background/80"
|
||||
pannable
|
||||
zoomable
|
||||
/>
|
||||
|
||||
{/* Main Toolbar Panel */}
|
||||
<Panel
|
||||
position="top-right"
|
||||
className="flex gap-1 bg-background/80 backdrop-blur-sm rounded-lg p-1 shadow-md border"
|
||||
>
|
||||
{/* Undo/Redo */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => undo()}
|
||||
disabled={!canUndo}
|
||||
className="size-8"
|
||||
>
|
||||
<Undo2 className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{t('common.undo')} (Ctrl+Z)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => redo()}
|
||||
disabled={!canRedo}
|
||||
className="size-8"
|
||||
>
|
||||
<Redo2 className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{t('common.redo')} (Ctrl+Shift+Z)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div className="w-px bg-border mx-0.5" />
|
||||
|
||||
{/* Copy/Paste */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleCopy}
|
||||
disabled={!hasSelection}
|
||||
className="size-8"
|
||||
>
|
||||
<Copy className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{t('common.copy')} (Ctrl+C)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handlePaste}
|
||||
disabled={!hasClipboard}
|
||||
className="size-8"
|
||||
>
|
||||
<ClipboardPaste className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{t('workflows.paste')} (Ctrl+V)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleDelete}
|
||||
disabled={!hasSelection && selectedEdgeId === null}
|
||||
className="size-8 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{t('common.delete')} (Delete)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div className="w-px bg-border mx-0.5" />
|
||||
|
||||
{/* Zoom controls */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => zoomIn()}
|
||||
className="size-8 text-muted-foreground hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground"
|
||||
>
|
||||
<ZoomIn className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{t('workflows.zoomIn')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => zoomOut()}
|
||||
className="size-8 text-muted-foreground hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground"
|
||||
>
|
||||
<ZoomOut className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{t('workflows.zoomOut')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => fitView()}
|
||||
className="size-8 text-muted-foreground hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground"
|
||||
>
|
||||
<Maximize2 className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{t('workflows.fitView')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Panel>
|
||||
|
||||
{/* Keyboard shortcuts hint */}
|
||||
<Panel
|
||||
position="bottom-center"
|
||||
className="text-xs text-muted-foreground bg-background/60 backdrop-blur-sm rounded px-2 py-1"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Keyboard className="size-3" />
|
||||
<span>
|
||||
Ctrl+Z/Y: {t('common.undo')}/{t('common.redo')} | Ctrl+C/V:{' '}
|
||||
{t('common.copy')}/{t('workflows.paste')} | Del:{' '}
|
||||
{t('common.delete')}
|
||||
</span>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
{/* Dirty indicator */}
|
||||
{isDirty && (
|
||||
<Panel position="top-left" className="ml-2">
|
||||
<div className="text-xs text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/50 border border-amber-200 dark:border-amber-800 px-2 py-1 rounded flex items-center gap-1">
|
||||
<div className="size-1.5 rounded-full bg-amber-500 animate-pulse" />
|
||||
{t('workflows.unsavedChanges')}
|
||||
</div>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{/* Selection info */}
|
||||
{(selectedNodes.length > 1 || selectedEdges.length > 0) && (
|
||||
<Panel
|
||||
position="bottom-right"
|
||||
className="text-xs text-muted-foreground bg-background/80 backdrop-blur-sm rounded px-2 py-1 mr-2 mb-2"
|
||||
>
|
||||
{selectedNodes.length > 0 && (
|
||||
<span>
|
||||
{t('workflows.nodesSelected', {
|
||||
count: selectedNodes.length,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
{selectedNodes.length > 0 && selectedEdges.length > 0 && (
|
||||
<span> | </span>
|
||||
)}
|
||||
{selectedEdges.length > 0 && (
|
||||
<span>
|
||||
{t('workflows.edgesSelected', {
|
||||
count: selectedEdges.length,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</Panel>
|
||||
)}
|
||||
</ReactFlow>
|
||||
</div>
|
||||
|
||||
{/* Right: Property Panel (conditionally rendered) */}
|
||||
{showPropertyPanel && (
|
||||
<div className="w-80 border-l bg-muted/30 overflow-hidden flex-shrink-0 h-full">
|
||||
<PropertyPanel
|
||||
selectedNodeId={selectedNodeId}
|
||||
selectedEdgeId={selectedEdgeId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WorkflowEditorComponent() {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<WorkflowEditorInner />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import type { NodeProps } from '@xyflow/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PauseCircle,
|
||||
Settings,
|
||||
Loader2,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Clock,
|
||||
Play,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
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';
|
||||
|
||||
// Use shared icon mapping
|
||||
const nodeIcons = NODE_ICONS;
|
||||
|
||||
// Use shared i18n key mapping
|
||||
const nodeTypeI18nKeys: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(NODE_TYPE_I18N_KEYS).map(([k, v]) => [k, v.labelKey]),
|
||||
);
|
||||
|
||||
// Category colors with improved design
|
||||
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',
|
||||
text: 'text-amber-900 dark:text-amber-100',
|
||||
icon: 'text-amber-600 dark:text-amber-400',
|
||||
gradient: 'from-amber-500/10 to-transparent',
|
||||
handleBg: '#f59e0b',
|
||||
},
|
||||
process: {
|
||||
bg: 'bg-blue-50 dark:bg-blue-950/40',
|
||||
border: 'border-blue-400 dark:border-blue-600',
|
||||
text: 'text-blue-900 dark:text-blue-100',
|
||||
icon: 'text-blue-600 dark:text-blue-400',
|
||||
gradient: 'from-blue-500/10 to-transparent',
|
||||
handleBg: '#3b82f6',
|
||||
},
|
||||
control: {
|
||||
bg: 'bg-purple-50 dark:bg-purple-950/40',
|
||||
border: 'border-purple-400 dark:border-purple-600',
|
||||
text: 'text-purple-900 dark:text-purple-100',
|
||||
icon: 'text-purple-600 dark:text-purple-400',
|
||||
gradient: 'from-purple-500/10 to-transparent',
|
||||
handleBg: '#8b5cf6',
|
||||
},
|
||||
action: {
|
||||
bg: 'bg-green-50 dark:bg-green-950/40',
|
||||
border: 'border-green-400 dark:border-green-600',
|
||||
text: 'text-green-900 dark:text-green-100',
|
||||
icon: 'text-green-600 dark:text-green-400',
|
||||
gradient: 'from-green-500/10 to-transparent',
|
||||
handleBg: '#22c55e',
|
||||
},
|
||||
integration: {
|
||||
bg: 'bg-pink-50 dark:bg-pink-950/40',
|
||||
border: 'border-pink-400 dark:border-pink-600',
|
||||
text: 'text-pink-900 dark:text-pink-100',
|
||||
icon: 'text-pink-600 dark:text-pink-400',
|
||||
gradient: 'from-pink-500/10 to-transparent',
|
||||
handleBg: '#ec4899',
|
||||
},
|
||||
};
|
||||
|
||||
// Node execution status
|
||||
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;
|
||||
}> = {
|
||||
idle: {
|
||||
icon: Play,
|
||||
color: 'text-muted-foreground',
|
||||
bgColor: 'bg-muted',
|
||||
},
|
||||
pending: {
|
||||
icon: Clock,
|
||||
color: 'text-amber-600 dark:text-amber-400',
|
||||
bgColor: 'bg-amber-100 dark:bg-amber-900/50',
|
||||
},
|
||||
running: {
|
||||
icon: Loader2,
|
||||
color: 'text-blue-600 dark:text-blue-400',
|
||||
bgColor: 'bg-blue-100 dark:bg-blue-900/50',
|
||||
animate: true,
|
||||
},
|
||||
completed: {
|
||||
icon: CheckCircle2,
|
||||
color: 'text-green-600 dark:text-green-400',
|
||||
bgColor: 'bg-green-100 dark:bg-green-900/50',
|
||||
},
|
||||
failed: {
|
||||
icon: XCircle,
|
||||
color: 'text-red-600 dark:text-red-400',
|
||||
bgColor: 'bg-red-100 dark:bg-red-900/50',
|
||||
},
|
||||
skipped: {
|
||||
icon: PauseCircle,
|
||||
color: 'text-gray-500 dark:text-gray-400',
|
||||
bgColor: 'bg-gray-100 dark:bg-gray-800',
|
||||
},
|
||||
};
|
||||
|
||||
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 }[];
|
||||
// Execution state
|
||||
executionStatus?: NodeExecutionStatus;
|
||||
executionError?: string;
|
||||
executionDuration?: number; // in milliseconds
|
||||
// Node type metadata from backend
|
||||
nodeTypeLabel?: Record<string, string>; // i18n label dict from backend
|
||||
nodeTypeDescription?: Record<string, string>; // i18n description from backend
|
||||
}
|
||||
|
||||
// Helper function to get port label with i18n support
|
||||
function getPortLabel(
|
||||
label: string | Record<string, string> | I18nObject | undefined,
|
||||
fallbackName: string,
|
||||
prefix: 'workflows.nodeOutputs' | 'workflows.nodeInputs',
|
||||
t: (key: string, options?: { defaultValue: string }) => string,
|
||||
): string {
|
||||
if (label && typeof label === 'object') {
|
||||
const resolved = resolveI18nLabel(label);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
|
||||
if (typeof label === 'string' && label) {
|
||||
if (label.startsWith('workflows.nodeOutputs.') || label.startsWith('workflows.nodeInputs.')) {
|
||||
return t(label, { defaultValue: fallbackName });
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
const key = `${prefix}.${fallbackName}`;
|
||||
const translated = t(key, { defaultValue: fallbackName });
|
||||
return translated === key ? fallbackName : translated;
|
||||
}
|
||||
|
||||
// Helper function to extract i18n value from I18nObject (delegates to shared utility)
|
||||
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,
|
||||
t: (key: string, options?: { defaultValue: string }) => string,
|
||||
nodeTypeLabel?: Record<string, string>
|
||||
): string {
|
||||
return nodeType.includes('.') ? nodeType.split('.').slice(1).join('.') : nodeType;
|
||||
}
|
||||
|
||||
function WorkflowNodeComponent({ data, selected }: NodeProps) {
|
||||
const { t } = useTranslation();
|
||||
const nodeData = data as WorkflowNodeData;
|
||||
const category = nodeData.type.split('.')[0];
|
||||
const colors = categoryColors[category] || categoryColors.process;
|
||||
const Icon = nodeIcons[nodeData.type] || Settings;
|
||||
|
||||
// Get execution status
|
||||
const status = nodeData.executionStatus || 'idle';
|
||||
const statusInfo = statusConfig[status];
|
||||
const StatusIcon = statusInfo.icon;
|
||||
|
||||
// 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' }];
|
||||
}, [nodeData.inputs]);
|
||||
|
||||
const outputs = useMemo(() => {
|
||||
return nodeData.outputs || [{ name: 'output', label: 'workflows.nodeOutputs.output', type: 'any' }];
|
||||
}, [nodeData.outputs]);
|
||||
|
||||
// Determine if this is a trigger node (no inputs)
|
||||
const isTrigger = category === 'trigger';
|
||||
|
||||
// Format execution duration
|
||||
const formattedDuration = useMemo(() => {
|
||||
if (!nodeData.executionDuration) return null;
|
||||
if (nodeData.executionDuration < 1000) {
|
||||
return `${nodeData.executionDuration}ms`;
|
||||
}
|
||||
return `${(nodeData.executionDuration / 1000).toFixed(2)}s`;
|
||||
}, [nodeData.executionDuration]);
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div
|
||||
className={cn(
|
||||
'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]',
|
||||
status === 'running' && 'shadow-blue-200 dark:shadow-blue-900/50',
|
||||
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>
|
||||
))}
|
||||
|
||||
{/* 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'
|
||||
)}>
|
||||
<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)}>
|
||||
{nodeData.label}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground truncate mt-0.5">
|
||||
{getNodeTypeDescription(nodeData.type, t, nodeData.nodeTypeLabel)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status indicator */}
|
||||
{status !== 'idle' && (
|
||||
<div className={cn(
|
||||
'p-1 rounded-full shrink-0',
|
||||
statusInfo.bgColor
|
||||
)}>
|
||||
<StatusIcon
|
||||
className={cn(
|
||||
'size-4',
|
||||
statusInfo.color,
|
||||
statusInfo.animate && 'animate-spin'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 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('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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error message */}
|
||||
{status === 'failed' && nodeData.executionError && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="mt-2 p-2 rounded-md bg-red-100 dark:bg-red-950/50 text-xs text-red-700 dark:text-red-300 truncate cursor-help">
|
||||
{nodeData.executionError}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="max-w-[300px]">
|
||||
<p className="text-sm">{nodeData.executionError}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Output handles */}
|
||||
{outputs.map((output, index) => (
|
||||
<Tooltip key={`output-${output.name}`}>
|
||||
<TooltipTrigger asChild>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id={output.name}
|
||||
style={{
|
||||
top: outputs.length === 1 ? '50%' : `${((index + 1) / (outputs.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="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>}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(WorkflowNodeComponent);
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as WorkflowEditorComponent } from './WorkflowEditorComponent';
|
||||
export { default as WorkflowNodeComponent, type WorkflowNodeData } from './WorkflowNodeComponent';
|
||||
export { default as NodePalette } from './NodePalette';
|
||||
export { default as PropertyPanel } from './PropertyPanel';
|
||||
|
||||
// Export node configurations
|
||||
export * from './node-configs';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,749 @@
|
||||
/**
|
||||
* 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
|
||||
* - parameter_extractor: Extract structured parameters from text
|
||||
* - knowledge_retrieval: Retrieve information from knowledge bases
|
||||
* - text_embedding: Generate text embeddings
|
||||
* - intent_recognition: Recognize user intent
|
||||
*/
|
||||
|
||||
import { DynamicFormItemType } from '@/app/infra/entities/form/dynamic';
|
||||
import { NodeConfigMeta, createInput, createOutput } from './types';
|
||||
|
||||
/**
|
||||
* LLM Call Node
|
||||
* Makes a call to a large language model
|
||||
*/
|
||||
export const llmCallConfig: NodeConfigMeta = {
|
||||
nodeType: 'llm_call',
|
||||
label: {
|
||||
en_US: 'LLM Call',
|
||||
zh_Hans: 'LLM 调用',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Call a large language model to generate responses',
|
||||
zh_Hans: '调用大语言模型生成响应',
|
||||
},
|
||||
icon: 'Brain',
|
||||
category: 'process',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('input', 'string', {
|
||||
description: 'Input text to send to the model',
|
||||
label: { en_US: 'Input', zh_Hans: '输入' },
|
||||
}),
|
||||
createInput('context', 'object', {
|
||||
description: 'Additional context data',
|
||||
label: { en_US: 'Context', zh_Hans: '上下文' },
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('response', 'string', {
|
||||
description: 'Model response text',
|
||||
label: { en_US: 'Response', zh_Hans: '响应' },
|
||||
}),
|
||||
createOutput('usage', 'object', {
|
||||
description: 'Token usage information',
|
||||
label: { en_US: 'Usage', zh_Hans: '使用量' },
|
||||
}),
|
||||
createOutput('parsed', 'object', {
|
||||
description: 'Parsed output (if output format is JSON)',
|
||||
label: { en_US: 'Parsed', zh_Hans: '解析结果' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'model',
|
||||
name: 'model',
|
||||
type: DynamicFormItemType.LLM_MODEL_SELECTOR,
|
||||
label: {
|
||||
en_US: 'Model',
|
||||
zh_Hans: '模型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Select the LLM model to use',
|
||||
zh_Hans: '选择要使用的 LLM 模型',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'system_prompt',
|
||||
name: 'system_prompt',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'System Prompt',
|
||||
zh_Hans: '系统提示词',
|
||||
},
|
||||
description: {
|
||||
en_US: 'System prompt to set the model behavior (supports variable interpolation with {{variable}})',
|
||||
zh_Hans: '设置模型行为的系统提示词(支持使用 {{variable}} 进行变量插值)',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'user_prompt_template',
|
||||
name: 'user_prompt_template',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'User Prompt Template',
|
||||
zh_Hans: '用户提示词模板',
|
||||
},
|
||||
description: {
|
||||
en_US: 'User prompt template with variable placeholders (e.g., {{input}}, {{context.key}})',
|
||||
zh_Hans: '带有变量占位符的用户提示词模板(例如 {{input}}、{{context.key}})',
|
||||
},
|
||||
required: true,
|
||||
default: '{{input}}',
|
||||
},
|
||||
{
|
||||
id: 'temperature',
|
||||
name: 'temperature',
|
||||
type: DynamicFormItemType.FLOAT,
|
||||
label: {
|
||||
en_US: 'Temperature',
|
||||
zh_Hans: '温度',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Controls randomness in responses (0.0 = deterministic, 2.0 = very random)',
|
||||
zh_Hans: '控制响应的随机性(0.0 = 确定性,2.0 = 非常随机)',
|
||||
},
|
||||
required: false,
|
||||
default: 0.7,
|
||||
},
|
||||
{
|
||||
id: 'max_tokens',
|
||||
name: 'max_tokens',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: {
|
||||
en_US: 'Max Tokens',
|
||||
zh_Hans: '最大令牌数',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Maximum number of tokens to generate (leave 0 for model default)',
|
||||
zh_Hans: '生成的最大令牌数(设为 0 使用模型默认值)',
|
||||
},
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
id: 'output_format',
|
||||
name: 'output_format',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Output Format',
|
||||
zh_Hans: '输出格式',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Expected format of the model output',
|
||||
zh_Hans: '模型输出的预期格式',
|
||||
},
|
||||
required: false,
|
||||
default: 'text',
|
||||
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 文本' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'json_schema',
|
||||
name: 'json_schema',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'JSON Schema',
|
||||
zh_Hans: 'JSON Schema',
|
||||
},
|
||||
description: {
|
||||
en_US: 'JSON schema for structured output validation (optional)',
|
||||
zh_Hans: '用于结构化输出验证的 JSON Schema(可选)',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'output_format',
|
||||
operator: 'eq',
|
||||
value: 'json',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'enable_tools',
|
||||
name: 'enable_tools',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
label: {
|
||||
en_US: 'Enable Tools',
|
||||
zh_Hans: '启用工具',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Allow the model to use function calling tools',
|
||||
zh_Hans: '允许模型使用函数调用工具',
|
||||
},
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
id: 'tools',
|
||||
name: 'tools',
|
||||
type: DynamicFormItemType.TOOLS_SELECTOR,
|
||||
label: {
|
||||
en_US: 'Tools',
|
||||
zh_Hans: '工具',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Select tools that the model can use',
|
||||
zh_Hans: '选择模型可以使用的工具',
|
||||
},
|
||||
required: false,
|
||||
default: [],
|
||||
show_if: {
|
||||
field: 'enable_tools',
|
||||
operator: 'eq',
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
model: '',
|
||||
system_prompt: '',
|
||||
user_prompt_template: '{{input}}',
|
||||
temperature: 0.7,
|
||||
max_tokens: 0,
|
||||
output_format: 'text',
|
||||
json_schema: '',
|
||||
enable_tools: false,
|
||||
tools: [],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Question Classifier Node
|
||||
* Classifies user questions into predefined categories
|
||||
*/
|
||||
export const questionClassifierConfig: NodeConfigMeta = {
|
||||
nodeType: 'question_classifier',
|
||||
label: {
|
||||
en_US: 'Question Classifier',
|
||||
zh_Hans: '问题分类器',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Classify user questions into predefined categories using AI',
|
||||
zh_Hans: '使用 AI 将用户问题分类到预定义的类别中',
|
||||
},
|
||||
icon: 'Tags',
|
||||
category: 'process',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('question', 'string', {
|
||||
description: 'The question to classify',
|
||||
label: { en_US: 'Question', zh_Hans: '问题' },
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('category', 'string', {
|
||||
description: 'The classified category',
|
||||
label: { en_US: 'Category', zh_Hans: '分类' },
|
||||
}),
|
||||
createOutput('confidence', 'number', {
|
||||
description: 'Classification confidence score (0-1)',
|
||||
label: { en_US: 'Confidence', zh_Hans: '置信度' },
|
||||
}),
|
||||
createOutput('all_scores', 'object', {
|
||||
description: 'Scores for all categories',
|
||||
label: { en_US: 'All Scores', zh_Hans: '所有分数' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'model',
|
||||
name: 'model',
|
||||
type: DynamicFormItemType.LLM_MODEL_SELECTOR,
|
||||
label: {
|
||||
en_US: 'Classification Model',
|
||||
zh_Hans: '分类模型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Select the model to use for classification',
|
||||
zh_Hans: '选择用于分类的模型',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'categories',
|
||||
name: 'categories',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'Categories Definition',
|
||||
zh_Hans: '分类定义',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Define categories in JSON format: [{"name": "category1", "description": "...", "examples": ["..."]}]',
|
||||
zh_Hans: '使用 JSON 格式定义分类: [{"name": "分类1", "description": "...", "examples": ["..."]}]',
|
||||
},
|
||||
required: true,
|
||||
default: '[]',
|
||||
},
|
||||
{
|
||||
id: 'confidence_threshold',
|
||||
name: 'confidence_threshold',
|
||||
type: DynamicFormItemType.FLOAT,
|
||||
label: {
|
||||
en_US: 'Confidence Threshold',
|
||||
zh_Hans: '置信度阈值',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Minimum confidence score required (0.0-1.0)',
|
||||
zh_Hans: '所需的最小置信度分数(0.0-1.0)',
|
||||
},
|
||||
required: false,
|
||||
default: 0.7,
|
||||
},
|
||||
{
|
||||
id: 'fallback_category',
|
||||
name: 'fallback_category',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Fallback Category',
|
||||
zh_Hans: '默认分类',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Category to use when confidence is below threshold',
|
||||
zh_Hans: '当置信度低于阈值时使用的分类',
|
||||
},
|
||||
required: false,
|
||||
default: 'other',
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
model: '',
|
||||
categories: '[]',
|
||||
confidence_threshold: 0.7,
|
||||
fallback_category: 'other',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Parameter Extractor Node
|
||||
* Extracts structured parameters from natural language
|
||||
*/
|
||||
export const parameterExtractorConfig: NodeConfigMeta = {
|
||||
nodeType: 'parameter_extractor',
|
||||
label: {
|
||||
en_US: 'Parameter Extractor',
|
||||
zh_Hans: '参数提取器',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Extract structured parameters from natural language text using AI',
|
||||
zh_Hans: '使用 AI 从自然语言文本中提取结构化参数',
|
||||
},
|
||||
icon: 'FileSearch',
|
||||
category: 'process',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('text', 'string', {
|
||||
description: 'Text to extract parameters from',
|
||||
label: { en_US: 'Text', zh_Hans: '文本' },
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('parameters', 'object', {
|
||||
description: 'Extracted parameters as key-value pairs',
|
||||
label: { en_US: 'Parameters', zh_Hans: '参数' },
|
||||
}),
|
||||
createOutput('missing', 'array', {
|
||||
description: 'List of required parameters that could not be extracted',
|
||||
label: { en_US: 'Missing', zh_Hans: '缺失项' },
|
||||
}),
|
||||
createOutput('success', 'boolean', {
|
||||
description: 'Whether all required parameters were extracted',
|
||||
label: { en_US: 'Success', zh_Hans: '成功' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'model',
|
||||
name: 'model',
|
||||
type: DynamicFormItemType.LLM_MODEL_SELECTOR,
|
||||
label: {
|
||||
en_US: 'Extraction Model',
|
||||
zh_Hans: '提取模型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Select the model to use for parameter extraction',
|
||||
zh_Hans: '选择用于参数提取的模型',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'parameters',
|
||||
name: 'parameters',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'Parameters Schema',
|
||||
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}]',
|
||||
},
|
||||
required: true,
|
||||
default: '[]',
|
||||
},
|
||||
{
|
||||
id: 'extraction_prompt',
|
||||
name: 'extraction_prompt',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'Extraction Prompt',
|
||||
zh_Hans: '提取提示',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Additional instructions for the extraction model',
|
||||
zh_Hans: '提取模型的额外指令',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'strict_mode',
|
||||
name: 'strict_mode',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
label: {
|
||||
en_US: 'Strict Mode',
|
||||
zh_Hans: '严格模式',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Fail if any required parameter cannot be extracted',
|
||||
zh_Hans: '如果任何必需参数无法提取则失败',
|
||||
},
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
model: '',
|
||||
parameters_definition: '[]',
|
||||
extraction_prompt: '',
|
||||
strict_mode: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Knowledge Retrieval Node
|
||||
* Retrieves relevant information from knowledge bases
|
||||
*/
|
||||
export const knowledgeRetrievalConfig: NodeConfigMeta = {
|
||||
nodeType: 'knowledge_retrieval',
|
||||
label: {
|
||||
en_US: 'Knowledge Retrieval',
|
||||
zh_Hans: '知识检索',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Retrieve relevant information from knowledge bases using semantic search',
|
||||
zh_Hans: '使用语义搜索从知识库中检索相关信息',
|
||||
},
|
||||
icon: 'BookOpen',
|
||||
category: 'process',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('query', 'string', {
|
||||
description: 'Query text to search for',
|
||||
label: { en_US: 'Query', zh_Hans: '查询' },
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('results', 'array', {
|
||||
description: 'Retrieved documents/chunks',
|
||||
label: { en_US: 'Results', zh_Hans: '结果' },
|
||||
}),
|
||||
createOutput('context', 'string', {
|
||||
description: 'Concatenated text from all results',
|
||||
label: { en_US: 'Context', zh_Hans: '上下文' },
|
||||
}),
|
||||
createOutput('scores', 'array', {
|
||||
description: 'Similarity scores for each result',
|
||||
label: { en_US: 'Scores', zh_Hans: '分数' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'knowledge_bases',
|
||||
name: 'knowledge_bases',
|
||||
type: DynamicFormItemType.KNOWLEDGE_BASE_MULTI_SELECTOR,
|
||||
label: {
|
||||
en_US: 'Knowledge Bases',
|
||||
zh_Hans: '知识库',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Select knowledge bases to search',
|
||||
zh_Hans: '选择要搜索的知识库',
|
||||
},
|
||||
required: true,
|
||||
default: [],
|
||||
},
|
||||
{
|
||||
id: 'top_k',
|
||||
name: 'top_k',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: {
|
||||
en_US: 'Top K Results',
|
||||
zh_Hans: '返回数量 (Top K)',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Number of top results to retrieve',
|
||||
zh_Hans: '返回的最相关结果数量',
|
||||
},
|
||||
required: false,
|
||||
default: 5,
|
||||
},
|
||||
{
|
||||
id: 'similarity_threshold',
|
||||
name: 'similarity_threshold',
|
||||
type: DynamicFormItemType.FLOAT,
|
||||
label: {
|
||||
en_US: 'Similarity Threshold',
|
||||
zh_Hans: '相似度阈值',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Minimum similarity score (0.0-1.0) for results to be included',
|
||||
zh_Hans: '结果被包含的最小相似度分数(0.0-1.0)',
|
||||
},
|
||||
required: false,
|
||||
default: 0.5,
|
||||
},
|
||||
{
|
||||
id: 'retrieval_mode',
|
||||
name: 'retrieval_mode',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Retrieval Mode',
|
||||
zh_Hans: '检索模式',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Method used for retrieving documents',
|
||||
zh_Hans: '用于检索文档的方法',
|
||||
},
|
||||
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: '关键词检索' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'rerank_enabled',
|
||||
name: 'rerank_enabled',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
label: {
|
||||
en_US: 'Enable Reranking',
|
||||
zh_Hans: '启用重排序',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Use a reranking model to improve result relevance',
|
||||
zh_Hans: '使用重排序模型提高结果相关性',
|
||||
},
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
id: 'rerank_model',
|
||||
name: 'rerank_model',
|
||||
type: DynamicFormItemType.RERANK_MODEL_SELECTOR,
|
||||
label: {
|
||||
en_US: 'Rerank Model',
|
||||
zh_Hans: '重排序模型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Model to use for reranking results',
|
||||
zh_Hans: '用于结果重排序的模型',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'rerank_enabled',
|
||||
operator: 'eq',
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
knowledge_bases: [],
|
||||
top_k: 5,
|
||||
similarity_threshold: 0.5,
|
||||
retrieval_mode: 'vector',
|
||||
rerank_enabled: false,
|
||||
rerank_model: '',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Text Embedding Node
|
||||
* Generates vector embeddings for text
|
||||
*/
|
||||
export const textEmbeddingConfig: NodeConfigMeta = {
|
||||
nodeType: 'text_embedding',
|
||||
label: {
|
||||
en_US: 'Text Embedding',
|
||||
zh_Hans: '文本嵌入',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Generate vector embeddings for text using an embedding model',
|
||||
zh_Hans: '使用嵌入模型为文本生成向量嵌入',
|
||||
},
|
||||
icon: 'Binary',
|
||||
category: 'process',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('text', 'string', {
|
||||
description: 'Text to embed',
|
||||
label: { en_US: 'Text', zh_Hans: '文本' },
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('embedding', 'array', {
|
||||
description: 'Vector embedding array',
|
||||
label: { en_US: 'Embedding', zh_Hans: '嵌入向量' },
|
||||
}),
|
||||
createOutput('dimensions', 'number', {
|
||||
description: 'Number of dimensions in the embedding',
|
||||
label: { en_US: 'Dimensions', zh_Hans: '维度数' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'model',
|
||||
name: 'model',
|
||||
type: DynamicFormItemType.EMBEDDING_MODEL_SELECTOR,
|
||||
label: {
|
||||
en_US: 'Embedding Model',
|
||||
zh_Hans: '嵌入模型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Select the embedding model to use',
|
||||
zh_Hans: '选择要使用的嵌入模型',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
model: '',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Intent Recognition Node
|
||||
* Recognizes user intent from natural language
|
||||
*/
|
||||
export const intentRecognitionConfig: NodeConfigMeta = {
|
||||
nodeType: 'intent_recognition',
|
||||
label: {
|
||||
en_US: 'Intent Recognition',
|
||||
zh_Hans: '意图识别',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Recognize user intent from natural language using AI',
|
||||
zh_Hans: '使用 AI 从自然语言中识别用户意图',
|
||||
},
|
||||
icon: 'Target',
|
||||
category: 'process',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('text', 'string', {
|
||||
description: 'Text to analyze',
|
||||
label: { en_US: 'Text', zh_Hans: '文本' },
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('intent', 'string', {
|
||||
description: 'Recognized intent',
|
||||
label: { en_US: 'Intent', zh_Hans: '意图' },
|
||||
}),
|
||||
createOutput('confidence', 'number', {
|
||||
description: 'Recognition confidence score',
|
||||
label: { en_US: 'Confidence', zh_Hans: '置信度' },
|
||||
}),
|
||||
createOutput('entities', 'object', {
|
||||
description: 'Extracted entities from the text',
|
||||
label: { en_US: 'Entities', zh_Hans: '实体' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'model',
|
||||
name: 'model',
|
||||
type: DynamicFormItemType.LLM_MODEL_SELECTOR,
|
||||
label: {
|
||||
en_US: 'Recognition Model',
|
||||
zh_Hans: '识别模型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Select the model for intent recognition',
|
||||
zh_Hans: '选择用于意图识别的模型',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'intents_definition',
|
||||
name: 'intents_definition',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'Intents Definition',
|
||||
zh_Hans: '意图定义',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Define intents in JSON format: [{"name": "intent1", "description": "...", "examples": ["..."]}]',
|
||||
zh_Hans: '使用 JSON 格式定义意图: [{"name": "意图1", "description": "...", "examples": ["..."]}]',
|
||||
},
|
||||
required: true,
|
||||
default: '[]',
|
||||
},
|
||||
{
|
||||
id: 'extract_entities',
|
||||
name: 'extract_entities',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
label: {
|
||||
en_US: 'Extract Entities',
|
||||
zh_Hans: '提取实体',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Also extract named entities from the text',
|
||||
zh_Hans: '同时从文本中提取命名实体',
|
||||
},
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
model: '',
|
||||
intents_definition: '[]',
|
||||
extract_entities: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* All AI node configurations
|
||||
*/
|
||||
export const aiConfigs: NodeConfigMeta[] = [
|
||||
llmCallConfig,
|
||||
questionClassifierConfig,
|
||||
parameterExtractorConfig,
|
||||
knowledgeRetrievalConfig,
|
||||
textEmbeddingConfig,
|
||||
intentRecognitionConfig,
|
||||
];
|
||||
|
||||
/**
|
||||
* Get AI config by type
|
||||
*/
|
||||
export function getAIConfig(nodeType: string): NodeConfigMeta | undefined {
|
||||
return aiConfigs.find((config) => config.nodeType === nodeType);
|
||||
}
|
||||
@@ -0,0 +1,926 @@
|
||||
/**
|
||||
* Control Node Configurations
|
||||
*
|
||||
* Defines configurations for flow control node types:
|
||||
* - condition: Conditional branching
|
||||
* - switch_case: Multi-way branching
|
||||
* - loop: Loop/iteration
|
||||
* - parallel: Parallel execution
|
||||
* - wait: Wait/delay
|
||||
* - end: End workflow
|
||||
*/
|
||||
|
||||
import { DynamicFormItemType } from '@/app/infra/entities/form/dynamic';
|
||||
import { NodeConfigMeta, createInput, createOutput } from './types';
|
||||
|
||||
/**
|
||||
* Condition Node
|
||||
* Conditional branching based on expression
|
||||
*/
|
||||
export const conditionConfig: NodeConfigMeta = {
|
||||
nodeType: 'condition',
|
||||
label: {
|
||||
en_US: 'Condition',
|
||||
zh_Hans: '条件分支',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Branch workflow based on a condition',
|
||||
zh_Hans: '根据条件分支工作流',
|
||||
},
|
||||
icon: 'GitBranch',
|
||||
category: 'control',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('input', 'any', {
|
||||
description: 'Input data for condition evaluation',
|
||||
label: { en_US: 'Input', zh_Hans: '输入' },
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('true', 'any', {
|
||||
description: 'Output when condition is true',
|
||||
label: { en_US: 'True', zh_Hans: '真' },
|
||||
}),
|
||||
createOutput('false', 'any', {
|
||||
description: 'Output when condition is false',
|
||||
label: { en_US: 'False', zh_Hans: '假' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'condition_type',
|
||||
name: 'condition_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Condition Type',
|
||||
zh_Hans: '条件类型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Type of condition to evaluate',
|
||||
zh_Hans: '要评估的条件类型',
|
||||
},
|
||||
required: true,
|
||||
default: 'expression',
|
||||
options: [
|
||||
{ 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: '类型检查' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'expression',
|
||||
name: 'expression',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Expression',
|
||||
zh_Hans: '表达式',
|
||||
},
|
||||
description: {
|
||||
en_US: 'JavaScript expression that evaluates to true/false',
|
||||
zh_Hans: '评估为 true/false 的 JavaScript 表达式',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'condition_type',
|
||||
operator: 'eq',
|
||||
value: 'expression',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'left_value',
|
||||
name: 'left_value',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Left Value',
|
||||
zh_Hans: '左值',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Left side of comparison (supports variable references)',
|
||||
zh_Hans: '比较的左侧(支持变量引用)',
|
||||
},
|
||||
required: true,
|
||||
default: '{{input}}',
|
||||
show_if: {
|
||||
field: 'condition_type',
|
||||
operator: 'in',
|
||||
value: ['comparison', 'exists', 'type_check'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'operator',
|
||||
name: 'operator',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Operator',
|
||||
zh_Hans: '运算符',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Comparison operator',
|
||||
zh_Hans: '比较运算符',
|
||||
},
|
||||
required: true,
|
||||
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: 'lt', label: { en_US: 'Less Than (<)', 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: '匹配正则' } },
|
||||
],
|
||||
show_if: {
|
||||
field: 'condition_type',
|
||||
operator: 'eq',
|
||||
value: 'comparison',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'right_value',
|
||||
name: 'right_value',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Right Value',
|
||||
zh_Hans: '右值',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Right side of comparison',
|
||||
zh_Hans: '比较的右侧',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'condition_type',
|
||||
operator: 'eq',
|
||||
value: 'comparison',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'expected_type',
|
||||
name: 'expected_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Expected Type',
|
||||
zh_Hans: '期望类型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'The type to check for',
|
||||
zh_Hans: '要检查的类型',
|
||||
},
|
||||
required: true,
|
||||
default: 'string',
|
||||
options: [
|
||||
{ name: 'string', label: { en_US: 'String', zh_Hans: '字符串' } },
|
||||
{ name: 'number', label: { en_US: 'Number', zh_Hans: '数字' } },
|
||||
{ name: 'boolean', label: { en_US: 'Boolean', zh_Hans: '布尔' } },
|
||||
{ name: 'object', label: { en_US: 'Object', zh_Hans: '对象' } },
|
||||
{ name: 'array', label: { en_US: 'Array', zh_Hans: '数组' } },
|
||||
{ name: 'null', label: { en_US: 'Null', zh_Hans: '空' } },
|
||||
],
|
||||
show_if: {
|
||||
field: 'condition_type',
|
||||
operator: 'eq',
|
||||
value: 'type_check',
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
condition_type: 'expression',
|
||||
expression: '',
|
||||
left_value: '{{input}}',
|
||||
operator: 'eq',
|
||||
right_value: '',
|
||||
expected_type: 'string',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Switch Case Node
|
||||
* Multi-way branching based on value
|
||||
*/
|
||||
export const switchCaseConfig: NodeConfigMeta = {
|
||||
nodeType: 'switch_case',
|
||||
label: {
|
||||
en_US: 'Switch',
|
||||
zh_Hans: '多路分支',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Branch workflow based on multiple cases',
|
||||
zh_Hans: '根据多个条件分支工作流',
|
||||
},
|
||||
icon: 'GitFork',
|
||||
category: 'control',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('input', 'any', {
|
||||
description: 'Value to switch on',
|
||||
label: { en_US: 'Input', zh_Hans: '输入' },
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('case_1', 'any', {
|
||||
description: 'Branch 1 output',
|
||||
label: { en_US: 'Branch 1', zh_Hans: '分支 1' },
|
||||
}),
|
||||
createOutput('case_2', 'any', {
|
||||
description: 'Branch 2 output',
|
||||
label: { en_US: 'Branch 2', zh_Hans: '分支 2' },
|
||||
}),
|
||||
createOutput('default', 'any', {
|
||||
description: 'Default branch output',
|
||||
label: { en_US: 'Default Branch', zh_Hans: '默认分支' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'switch_expression',
|
||||
name: 'switch_expression',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Switch Expression',
|
||||
zh_Hans: '开关表达式',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Expression to evaluate for switching (e.g., {{input.type}})',
|
||||
zh_Hans: '用于切换的表达式(例如 {{input.type}})',
|
||||
},
|
||||
required: true,
|
||||
default: '{{input}}',
|
||||
},
|
||||
{
|
||||
id: 'cases',
|
||||
name: 'cases',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'Cases',
|
||||
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"]}]',
|
||||
},
|
||||
required: true,
|
||||
default: '[{"name": "case_1", "value": ""}, {"name": "case_2", "value": ""}]',
|
||||
},
|
||||
{
|
||||
id: 'case_sensitive',
|
||||
name: 'case_sensitive',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
label: {
|
||||
en_US: 'Case Sensitive',
|
||||
zh_Hans: '区分大小写',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Whether string comparisons are case-sensitive',
|
||||
zh_Hans: '字符串比较是否区分大小写',
|
||||
},
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
switch_expression: '{{input}}',
|
||||
cases: '[{"name": "case_1", "value": ""}, {"name": "case_2", "value": ""}]',
|
||||
case_sensitive: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Loop Node
|
||||
* Iterates over items or until condition
|
||||
*/
|
||||
export const loopConfig: NodeConfigMeta = {
|
||||
nodeType: 'loop',
|
||||
label: {
|
||||
en_US: 'Loop',
|
||||
zh_Hans: '循环',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Iterate over items or repeat until condition',
|
||||
zh_Hans: '遍历项目或重复直到满足条件',
|
||||
},
|
||||
icon: 'Repeat',
|
||||
category: 'control',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('items', 'array', {
|
||||
description: 'Items to iterate over (for each loop)',
|
||||
label: { en_US: 'Items', zh_Hans: '项目' },
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('item', 'any', {
|
||||
description: 'Current item in iteration',
|
||||
label: { en_US: 'Item', zh_Hans: '当前项' },
|
||||
}),
|
||||
createOutput('index', 'number', {
|
||||
description: 'Current iteration index',
|
||||
label: { en_US: 'Index', zh_Hans: '索引' },
|
||||
}),
|
||||
createOutput('completed', 'any', {
|
||||
description: 'Output after loop completes',
|
||||
label: { en_US: 'Completed', zh_Hans: '完成' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'loop_type',
|
||||
name: 'loop_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Loop Type',
|
||||
zh_Hans: '循环类型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Type of loop to execute',
|
||||
zh_Hans: '要执行的循环类型',
|
||||
},
|
||||
required: true,
|
||||
default: 'foreach',
|
||||
options: [
|
||||
{ name: 'foreach', label: { en_US: 'For Each', zh_Hans: '逐项遍历' } },
|
||||
{ name: 'while', label: { en_US: 'While', zh_Hans: '条件循环' } },
|
||||
{ name: 'count', label: { en_US: 'Count', zh_Hans: '计数' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'max_iterations',
|
||||
name: 'max_iterations',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: {
|
||||
en_US: 'Max Iterations',
|
||||
zh_Hans: '最大迭代次数',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Maximum number of iterations (safety limit)',
|
||||
zh_Hans: '最大迭代次数(安全限制)',
|
||||
},
|
||||
required: false,
|
||||
default: 100,
|
||||
},
|
||||
{
|
||||
id: 'count',
|
||||
name: 'count',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: {
|
||||
en_US: 'Count',
|
||||
zh_Hans: '计数',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Number of times to iterate',
|
||||
zh_Hans: '迭代次数',
|
||||
},
|
||||
required: true,
|
||||
default: 10,
|
||||
show_if: {
|
||||
field: 'loop_type',
|
||||
operator: 'eq',
|
||||
value: 'count',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'while_condition',
|
||||
name: 'while_condition',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'While Condition',
|
||||
zh_Hans: 'While 条件',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Condition expression to continue looping',
|
||||
zh_Hans: '继续循环的条件表达式',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'loop_type',
|
||||
operator: 'eq',
|
||||
value: 'while',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'parallel',
|
||||
name: 'parallel',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
label: {
|
||||
en_US: 'Parallel Execution',
|
||||
zh_Hans: '并行执行',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Execute iterations in parallel',
|
||||
zh_Hans: '并行执行迭代',
|
||||
},
|
||||
required: false,
|
||||
default: false,
|
||||
show_if: {
|
||||
field: 'loop_type',
|
||||
operator: 'eq',
|
||||
value: 'foreach',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'parallel_limit',
|
||||
name: 'parallel_limit',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: {
|
||||
en_US: 'Parallel Limit',
|
||||
zh_Hans: '并行限制',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Maximum number of parallel executions',
|
||||
zh_Hans: '最大并行执行数',
|
||||
},
|
||||
required: false,
|
||||
default: 5,
|
||||
show_if: {
|
||||
field: 'parallel',
|
||||
operator: 'eq',
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
loop_type: 'foreach',
|
||||
max_iterations: 100,
|
||||
count: 10,
|
||||
while_condition: '',
|
||||
parallel: false,
|
||||
parallel_limit: 5,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Parallel Node
|
||||
* Execute multiple branches in parallel
|
||||
*/
|
||||
export const parallelConfig: NodeConfigMeta = {
|
||||
nodeType: 'parallel',
|
||||
label: {
|
||||
en_US: 'Parallel',
|
||||
zh_Hans: '并行执行',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Execute multiple branches in parallel',
|
||||
zh_Hans: '并行执行多个分支',
|
||||
},
|
||||
icon: 'GitMerge',
|
||||
category: 'control',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('input', 'any', {
|
||||
description: 'Input data for all branches',
|
||||
label: { en_US: 'Input', zh_Hans: '输入' },
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('branch_1', 'any', {
|
||||
description: 'Branch 1 output',
|
||||
label: { en_US: 'Branch 1', zh_Hans: '分支 1' },
|
||||
}),
|
||||
createOutput('branch_2', 'any', {
|
||||
description: 'Branch 2 output',
|
||||
label: { en_US: 'Branch 2', zh_Hans: '分支 2' },
|
||||
}),
|
||||
createOutput('results', 'object', {
|
||||
description: 'Combined results from all branches',
|
||||
label: { en_US: 'Results', zh_Hans: '结果' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'branches',
|
||||
name: 'branches',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'Branches',
|
||||
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"}]',
|
||||
},
|
||||
required: true,
|
||||
default: '[{"name": "branch_1"}, {"name": "branch_2"}]',
|
||||
},
|
||||
{
|
||||
id: 'wait_for_all',
|
||||
name: 'wait_for_all',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
label: {
|
||||
en_US: 'Wait for All',
|
||||
zh_Hans: '等待全部完成',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Wait for all branches to complete before continuing',
|
||||
zh_Hans: '等待所有分支完成后再继续',
|
||||
},
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: 'fail_fast',
|
||||
name: 'fail_fast',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
label: {
|
||||
en_US: 'Fail Fast',
|
||||
zh_Hans: '快速失败',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Stop all branches if any one fails',
|
||||
zh_Hans: '如果任何一个分支失败则停止所有分支',
|
||||
},
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
branches: '[{"name": "branch_1"}, {"name": "branch_2"}]',
|
||||
wait_for_all: true,
|
||||
fail_fast: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait Node
|
||||
* Pause workflow execution
|
||||
*/
|
||||
export const waitConfig: NodeConfigMeta = {
|
||||
nodeType: 'wait',
|
||||
label: {
|
||||
en_US: 'Wait',
|
||||
zh_Hans: '等待',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Pause workflow execution for a specified duration or condition',
|
||||
zh_Hans: '暂停工作流执行指定的时间或等待条件满足',
|
||||
},
|
||||
icon: 'Clock',
|
||||
category: 'control',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('input', 'any', {
|
||||
description: 'Input to pass through',
|
||||
label: { en_US: 'Input', zh_Hans: '输入' },
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('output', 'any', {
|
||||
description: 'Passed through input',
|
||||
label: { en_US: 'Output', zh_Hans: '输出' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'wait_type',
|
||||
name: 'wait_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Wait Type',
|
||||
zh_Hans: '等待类型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Type of wait operation',
|
||||
zh_Hans: '等待操作的类型',
|
||||
},
|
||||
required: true,
|
||||
default: 'duration',
|
||||
options: [
|
||||
{ name: 'duration', label: { en_US: 'Duration', zh_Hans: '时长' } },
|
||||
{ name: 'until', label: { en_US: 'Until Time', zh_Hans: '直到时间' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'duration',
|
||||
name: 'duration',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: {
|
||||
en_US: 'Duration (seconds)',
|
||||
zh_Hans: '时长(秒)',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Number of seconds to wait',
|
||||
zh_Hans: '等待的秒数',
|
||||
},
|
||||
required: true,
|
||||
default: 5,
|
||||
show_if: {
|
||||
field: 'wait_type',
|
||||
operator: 'eq',
|
||||
value: 'duration',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'until_time',
|
||||
name: 'until_time',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Until Time',
|
||||
zh_Hans: '直到时间',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Wait until this time (ISO 8601 format or expression)',
|
||||
zh_Hans: '等待直到此时间(ISO 8601 格式或表达式)',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'wait_type',
|
||||
operator: 'eq',
|
||||
value: 'until',
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
wait_type: 'duration',
|
||||
duration: 5,
|
||||
until_time: '',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* End Node
|
||||
* Terminates workflow execution
|
||||
*/
|
||||
export const endConfig: NodeConfigMeta = {
|
||||
nodeType: 'end',
|
||||
label: {
|
||||
en_US: 'End',
|
||||
zh_Hans: '结束',
|
||||
},
|
||||
description: {
|
||||
en_US: 'End the workflow execution',
|
||||
zh_Hans: '结束工作流执行',
|
||||
},
|
||||
icon: 'CircleStop',
|
||||
category: 'control',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('input', 'any', {
|
||||
description: 'Final output data',
|
||||
label: { en_US: 'Input', zh_Hans: '输入' },
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
outputs: [],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'status',
|
||||
name: 'status',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'End Status',
|
||||
zh_Hans: '结束状态',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Status to report when workflow ends',
|
||||
zh_Hans: '工作流结束时报告的状态',
|
||||
},
|
||||
required: true,
|
||||
default: 'success',
|
||||
options: [
|
||||
{ name: 'success', label: { en_US: 'Success', zh_Hans: '成功' } },
|
||||
{ name: 'failed', label: { en_US: 'Failed', zh_Hans: '失败' } },
|
||||
{ name: 'cancelled', label: { en_US: 'Cancelled', zh_Hans: '取消' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'message',
|
||||
name: 'message',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Message',
|
||||
zh_Hans: '消息',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Optional message to include with the end status',
|
||||
zh_Hans: '与结束状态一起包含的可选消息',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
status: 'success',
|
||||
message: '',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Iterator Node
|
||||
* Iterates over array items one by one
|
||||
*/
|
||||
export const iteratorConfig: NodeConfigMeta = {
|
||||
nodeType: 'iterator',
|
||||
label: {
|
||||
en_US: 'Iterator',
|
||||
zh_Hans: '迭代器',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Iterate over array elements one by one',
|
||||
zh_Hans: '逐个遍历数组元素',
|
||||
},
|
||||
icon: 'Repeat',
|
||||
category: 'control',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('items', 'array', {
|
||||
description: 'Array to iterate over',
|
||||
label: { en_US: 'Items', zh_Hans: '项目' },
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('item', 'any', {
|
||||
description: 'Current item',
|
||||
label: { en_US: 'Item', zh_Hans: '当前项' },
|
||||
}),
|
||||
createOutput('index', 'number', {
|
||||
description: 'Current index',
|
||||
label: { en_US: 'Index', zh_Hans: '索引' },
|
||||
}),
|
||||
createOutput('is_first', 'boolean', {
|
||||
description: 'Whether this is the first item',
|
||||
label: { en_US: 'Is First', zh_Hans: '是否第一个' },
|
||||
}),
|
||||
createOutput('is_last', 'boolean', {
|
||||
description: 'Whether this is the last item',
|
||||
label: { en_US: 'Is Last', zh_Hans: '是否最后一个' },
|
||||
}),
|
||||
createOutput('completed', 'any', {
|
||||
description: 'Output after iteration completes',
|
||||
label: { en_US: 'Completed', zh_Hans: '完成' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'parallel',
|
||||
name: 'parallel',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
label: { en_US: 'Parallel Processing', zh_Hans: '并行处理' },
|
||||
description: { en_US: 'Process items in parallel', zh_Hans: '并行处理项目' },
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
id: 'max_concurrency',
|
||||
name: 'max_concurrency',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: { en_US: 'Max Concurrency', zh_Hans: '最大并发数' },
|
||||
description: { en_US: 'Maximum number of concurrent iterations', zh_Hans: '最大并发迭代数' },
|
||||
required: false,
|
||||
default: 5,
|
||||
show_if: { field: 'parallel', operator: 'eq', value: true },
|
||||
},
|
||||
{
|
||||
id: 'max_iterations',
|
||||
name: 'max_iterations',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: { en_US: 'Max Iterations', zh_Hans: '最大迭代次数' },
|
||||
description: { en_US: 'Safety limit on iterations', zh_Hans: '迭代次数安全限制' },
|
||||
required: false,
|
||||
default: 1000,
|
||||
},
|
||||
],
|
||||
defaultConfig: { parallel: false, max_concurrency: 5, max_iterations: 1000 },
|
||||
};
|
||||
|
||||
/**
|
||||
* Merge Node
|
||||
* Merges multiple branches back together
|
||||
*/
|
||||
export const mergeConfig: NodeConfigMeta = {
|
||||
nodeType: 'merge',
|
||||
label: {
|
||||
en_US: 'Merge',
|
||||
zh_Hans: '合并',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Merge multiple branches back together',
|
||||
zh_Hans: '将多个分支合并在一起',
|
||||
},
|
||||
icon: 'GitMerge',
|
||||
category: 'control',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('branch_1', 'any', {
|
||||
description: 'Input from branch 1',
|
||||
label: { en_US: 'Branch 1', zh_Hans: '分支 1' },
|
||||
required: false,
|
||||
}),
|
||||
createInput('branch_2', 'any', {
|
||||
description: 'Input from branch 2',
|
||||
label: { en_US: 'Branch 2', zh_Hans: '分支 2' },
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('output', 'any', {
|
||||
description: 'Merged output',
|
||||
label: { en_US: 'Output', zh_Hans: '输出' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'merge_strategy',
|
||||
name: 'merge_strategy',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: { en_US: 'Merge Strategy', 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: '收集为数组' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
defaultConfig: { merge_strategy: 'wait_all' },
|
||||
};
|
||||
|
||||
/**
|
||||
* Variable Aggregator Node
|
||||
* Aggregates variable outputs from multiple branches
|
||||
*/
|
||||
export const variableAggregatorConfig: NodeConfigMeta = {
|
||||
nodeType: 'variable_aggregator',
|
||||
label: {
|
||||
en_US: 'Variable Aggregator',
|
||||
zh_Hans: '变量聚合器',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Aggregate variable outputs from multiple branches',
|
||||
zh_Hans: '聚合多个分支的变量输出',
|
||||
},
|
||||
icon: 'GitMerge',
|
||||
category: 'control',
|
||||
color: '#8b5cf6',
|
||||
inputs: [
|
||||
createInput('input', 'any', {
|
||||
description: 'Input data',
|
||||
label: { en_US: 'Input', zh_Hans: '输入' },
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('output', 'any', {
|
||||
description: 'Aggregated output',
|
||||
label: { en_US: 'Output', zh_Hans: '输出' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'variable_mappings',
|
||||
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}}"}' },
|
||||
required: true,
|
||||
default: '{}',
|
||||
},
|
||||
{
|
||||
id: 'aggregation_mode',
|
||||
name: 'aggregation_mode',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: { en_US: 'Aggregation Mode', 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: '第一个非空' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
defaultConfig: { variable_mappings: '{}', aggregation_mode: 'merge' },
|
||||
};
|
||||
|
||||
/**
|
||||
* All control node configurations
|
||||
*/
|
||||
export const controlConfigs: NodeConfigMeta[] = [
|
||||
conditionConfig,
|
||||
switchCaseConfig,
|
||||
loopConfig,
|
||||
iteratorConfig,
|
||||
parallelConfig,
|
||||
waitConfig,
|
||||
mergeConfig,
|
||||
variableAggregatorConfig,
|
||||
endConfig,
|
||||
];
|
||||
|
||||
/**
|
||||
* Get control config by type
|
||||
*/
|
||||
export function getControlConfig(nodeType: string): NodeConfigMeta | undefined {
|
||||
return controlConfigs.find((config) => config.nodeType === nodeType);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Node Configurations Index
|
||||
*
|
||||
* This module exports all node configuration metadata and provides
|
||||
* utility functions for accessing node configurations.
|
||||
*/
|
||||
|
||||
// Types
|
||||
export * from './types';
|
||||
|
||||
// Trigger Nodes
|
||||
export {
|
||||
triggerConfigs,
|
||||
getTriggerConfig,
|
||||
messageTriggerConfig,
|
||||
cronTriggerConfig,
|
||||
webhookTriggerConfig,
|
||||
eventTriggerConfig,
|
||||
} from './trigger-configs';
|
||||
|
||||
// AI Nodes
|
||||
export {
|
||||
aiConfigs,
|
||||
getAIConfig,
|
||||
llmCallConfig,
|
||||
questionClassifierConfig,
|
||||
parameterExtractorConfig,
|
||||
knowledgeRetrievalConfig,
|
||||
textEmbeddingConfig,
|
||||
intentRecognitionConfig,
|
||||
} from './ai-configs';
|
||||
|
||||
// Process Nodes
|
||||
export {
|
||||
processConfigs,
|
||||
getProcessConfig,
|
||||
textTemplateConfig,
|
||||
jsonTransformConfig,
|
||||
codeExecutorConfig,
|
||||
dataAggregatorConfig,
|
||||
textSplitterConfig,
|
||||
variableAssignmentConfig,
|
||||
dataTransformConfig,
|
||||
} from './process-configs';
|
||||
|
||||
// Control Nodes
|
||||
export {
|
||||
controlConfigs,
|
||||
getControlConfig,
|
||||
conditionConfig,
|
||||
switchCaseConfig,
|
||||
loopConfig,
|
||||
iteratorConfig,
|
||||
parallelConfig,
|
||||
waitConfig,
|
||||
mergeConfig,
|
||||
variableAggregatorConfig,
|
||||
endConfig,
|
||||
} from './control-configs';
|
||||
|
||||
// Action Nodes
|
||||
export {
|
||||
actionConfigs,
|
||||
getActionConfig,
|
||||
sendMessageConfig,
|
||||
replyMessageConfig,
|
||||
httpRequestConfig,
|
||||
storeDataConfig,
|
||||
callPipelineConfig,
|
||||
setVariableConfig,
|
||||
openingStatementConfig,
|
||||
botInvokeConfig,
|
||||
workflowInvokeConfig,
|
||||
notificationConfig,
|
||||
} from './action-configs';
|
||||
|
||||
// Integration Nodes
|
||||
export {
|
||||
integrationConfigs,
|
||||
getIntegrationConfig,
|
||||
difyWorkflowConfig,
|
||||
difyKnowledgeQueryConfig,
|
||||
n8nWorkflowConfig,
|
||||
langflowFlowConfig,
|
||||
cozeBotConfig,
|
||||
databaseQueryConfig,
|
||||
redisOperationConfig,
|
||||
mcpToolConfig,
|
||||
memoryStoreConfig,
|
||||
} from './integration-configs';
|
||||
|
||||
import { NodeConfigMeta, NodeConfigRegistry } from './types';
|
||||
import { triggerConfigs } from './trigger-configs';
|
||||
import { aiConfigs } from './ai-configs';
|
||||
import { processConfigs } from './process-configs';
|
||||
import { controlConfigs } from './control-configs';
|
||||
import { actionConfigs } from './action-configs';
|
||||
import { integrationConfigs } from './integration-configs';
|
||||
import { NodeCategory } from '@/app/infra/entities/workflow';
|
||||
|
||||
/**
|
||||
* All node configurations combined
|
||||
*/
|
||||
export const allNodeConfigs: NodeConfigMeta[] = [
|
||||
...triggerConfigs,
|
||||
...aiConfigs,
|
||||
...processConfigs,
|
||||
...controlConfigs,
|
||||
...actionConfigs,
|
||||
...integrationConfigs,
|
||||
];
|
||||
|
||||
/**
|
||||
* Node configuration registry by type
|
||||
* Registers each config under both its short name (e.g. "message_trigger")
|
||||
* and its full category-prefixed name (e.g. "trigger.message_trigger")
|
||||
* so lookups from PropertyPanel / useWorkflowStore always succeed.
|
||||
*/
|
||||
export const nodeConfigRegistry: NodeConfigRegistry = (() => {
|
||||
const registry: NodeConfigRegistry = {};
|
||||
for (const config of allNodeConfigs) {
|
||||
// Short name
|
||||
registry[config.nodeType] = config;
|
||||
// Full category.name
|
||||
registry[`${config.category}.${config.nodeType}`] = config;
|
||||
}
|
||||
// Aliases for nodes whose palette type differs from config nodeType
|
||||
// control.switch -> switch_case config
|
||||
if (registry['switch_case']) {
|
||||
registry['switch'] = registry['switch_case'];
|
||||
registry['control.switch'] = registry['switch_case'];
|
||||
}
|
||||
// action.end also points to the end config in control
|
||||
if (registry['end']) {
|
||||
registry['action.end'] = registry['end'];
|
||||
}
|
||||
return registry;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Get node configuration by type
|
||||
*/
|
||||
export function getNodeConfig(nodeType: string): NodeConfigMeta | undefined {
|
||||
return nodeConfigRegistry[nodeType];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all node configurations for a category
|
||||
*/
|
||||
export function getNodeConfigsByCategory(category: NodeCategory): NodeConfigMeta[] {
|
||||
return allNodeConfigs.filter((config) => config.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entry point node configurations (trigger nodes)
|
||||
*/
|
||||
export function getEntryPointConfigs(): NodeConfigMeta[] {
|
||||
return allNodeConfigs.filter((config) => config.isEntryPoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node type exists
|
||||
*/
|
||||
export function isValidNodeType(nodeType: string): boolean {
|
||||
return nodeType in nodeConfigRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default configuration for a node type
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate node configuration against schema
|
||||
*/
|
||||
export function validateNodeConfig(
|
||||
nodeType: string,
|
||||
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 === '')) {
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert node config metadata to NodeTypeMetadata format
|
||||
* (for compatibility with existing workflow store)
|
||||
*/
|
||||
export function toNodeTypeMetadata(config: NodeConfigMeta) {
|
||||
return {
|
||||
type: config.nodeType,
|
||||
name: config.label,
|
||||
description: config.description,
|
||||
category: config.category,
|
||||
icon: config.icon,
|
||||
color: config.color,
|
||||
inputs: config.inputs.map((input) => ({
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
description: input.description,
|
||||
required: input.required,
|
||||
})),
|
||||
outputs: config.outputs.map((output) => ({
|
||||
name: output.name,
|
||||
type: output.type,
|
||||
description: output.description,
|
||||
required: output.required,
|
||||
})),
|
||||
config_schema: config.configSchema,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all node configs to NodeTypeMetadata format
|
||||
*/
|
||||
export function getAllNodeTypeMetadata() {
|
||||
return allNodeConfigs.map(toNodeTypeMetadata);
|
||||
}
|
||||
@@ -0,0 +1,685 @@
|
||||
/**
|
||||
* Integration Node Configurations
|
||||
*
|
||||
* Defines configurations for integration node types:
|
||||
* - database_query: Query databases
|
||||
* - redis_operation: Redis operations
|
||||
* - mcp_tool: MCP tool invocation
|
||||
*/
|
||||
|
||||
import { DynamicFormItemType } from '@/app/infra/entities/form/dynamic';
|
||||
import { NodeConfigMeta, createInput, createOutput } from './types';
|
||||
|
||||
/**
|
||||
* Database Query Node
|
||||
* Executes database queries
|
||||
*/
|
||||
export const databaseQueryConfig: NodeConfigMeta = {
|
||||
nodeType: 'database_query',
|
||||
label: {
|
||||
en_US: 'Database Query',
|
||||
zh_Hans: '数据库查询',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Execute database queries',
|
||||
zh_Hans: '执行数据库查询',
|
||||
},
|
||||
icon: 'Database',
|
||||
category: 'integration',
|
||||
color: '#ec4899',
|
||||
inputs: [
|
||||
createInput('parameters', 'object', {
|
||||
description: 'Query parameters',
|
||||
label: { en_US: 'Parameters', zh_Hans: '参数' },
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('results', 'array', {
|
||||
description: 'Query results',
|
||||
label: { en_US: 'Results', zh_Hans: '结果' },
|
||||
}),
|
||||
createOutput('row_count', 'number', {
|
||||
description: 'Number of rows affected/returned',
|
||||
label: { en_US: 'Row Count', zh_Hans: '行数' },
|
||||
}),
|
||||
createOutput('success', 'boolean', {
|
||||
description: 'Whether query was successful',
|
||||
label: { en_US: 'Success', zh_Hans: '成功' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'connection_type',
|
||||
name: 'connection_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Database Type',
|
||||
zh_Hans: '数据库类型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Type of database to connect to',
|
||||
zh_Hans: '要连接的数据库类型',
|
||||
},
|
||||
required: true,
|
||||
default: 'postgresql',
|
||||
options: [
|
||||
{ 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' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'connection_string',
|
||||
name: 'connection_string',
|
||||
type: DynamicFormItemType.SECRET,
|
||||
label: {
|
||||
en_US: 'Connection String',
|
||||
zh_Hans: '连接字符串',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Database connection string',
|
||||
zh_Hans: '数据库连接字符串',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'query',
|
||||
name: 'query',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'SQL Query',
|
||||
zh_Hans: 'SQL 查询',
|
||||
},
|
||||
description: {
|
||||
en_US: 'SQL query to execute (use $1, $2, etc. for parameters)',
|
||||
zh_Hans: '要执行的 SQL 查询(使用 $1、$2 等作为参数占位符)',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'query_type',
|
||||
name: 'query_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Query Type',
|
||||
zh_Hans: '查询类型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Type of query operation',
|
||||
zh_Hans: '查询操作的类型',
|
||||
},
|
||||
required: true,
|
||||
default: 'select',
|
||||
options: [
|
||||
{ name: 'select', label: { en_US: 'SELECT', zh_Hans: 'SELECT' } },
|
||||
{ name: 'insert', label: { en_US: 'INSERT', zh_Hans: 'INSERT' } },
|
||||
{ name: 'update', label: { en_US: 'UPDATE', zh_Hans: 'UPDATE' } },
|
||||
{ name: 'delete', label: { en_US: 'DELETE', zh_Hans: 'DELETE' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'timeout',
|
||||
name: 'timeout',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: {
|
||||
en_US: 'Timeout (seconds)',
|
||||
zh_Hans: '超时时间(秒)',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Query timeout',
|
||||
zh_Hans: '查询超时时间',
|
||||
},
|
||||
required: false,
|
||||
default: 30,
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
connection_type: 'postgresql',
|
||||
connection_string: '',
|
||||
query: '',
|
||||
query_type: 'select',
|
||||
timeout: 30,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Redis Operation Node
|
||||
* Performs Redis operations
|
||||
*/
|
||||
export const redisOperationConfig: NodeConfigMeta = {
|
||||
nodeType: 'redis_operation',
|
||||
label: {
|
||||
en_US: 'Redis Operation',
|
||||
zh_Hans: 'Redis 操作',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Perform Redis cache operations',
|
||||
zh_Hans: '执行 Redis 缓存操作',
|
||||
},
|
||||
icon: 'Server',
|
||||
category: 'integration',
|
||||
color: '#ec4899',
|
||||
inputs: [
|
||||
createInput('key', 'string', {
|
||||
description: 'Redis key',
|
||||
label: { en_US: 'Key', zh_Hans: '键' },
|
||||
required: false,
|
||||
}),
|
||||
createInput('value', 'any', {
|
||||
description: 'Value to store',
|
||||
label: { en_US: 'Value', zh_Hans: '值' },
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('result', 'any', {
|
||||
description: 'Operation result',
|
||||
label: { en_US: 'Result', zh_Hans: '结果' },
|
||||
}),
|
||||
createOutput('success', 'boolean', {
|
||||
description: 'Whether operation was successful',
|
||||
label: { en_US: 'Success', zh_Hans: '成功' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'connection_url',
|
||||
name: 'connection_url',
|
||||
type: DynamicFormItemType.SECRET,
|
||||
label: {
|
||||
en_US: 'Redis URL',
|
||||
zh_Hans: 'Redis URL',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Redis connection URL (e.g., redis://localhost:6379)',
|
||||
zh_Hans: 'Redis 连接 URL(例如 redis://localhost:6379)',
|
||||
},
|
||||
required: true,
|
||||
default: 'redis://localhost:6379',
|
||||
},
|
||||
{
|
||||
id: 'operation',
|
||||
name: 'operation',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Operation',
|
||||
zh_Hans: '操作',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Redis operation to perform',
|
||||
zh_Hans: '要执行的 Redis 操作',
|
||||
},
|
||||
required: true,
|
||||
default: 'get',
|
||||
options: [
|
||||
{ name: 'get', label: { en_US: 'GET', zh_Hans: 'GET' } },
|
||||
{ name: 'set', label: { en_US: 'SET', zh_Hans: 'SET' } },
|
||||
{ name: 'delete', label: { en_US: 'DELETE', zh_Hans: 'DELETE' } },
|
||||
{ name: 'exists', label: { en_US: 'EXISTS', zh_Hans: 'EXISTS' } },
|
||||
{ name: 'incr', label: { en_US: 'INCR', zh_Hans: 'INCR' } },
|
||||
{ name: 'decr', label: { en_US: 'DECR', zh_Hans: 'DECR' } },
|
||||
{ name: 'hget', label: { en_US: 'HGET', zh_Hans: 'HGET' } },
|
||||
{ name: 'hset', label: { en_US: 'HSET', zh_Hans: 'HSET' } },
|
||||
{ name: 'lpush', label: { en_US: 'LPUSH', zh_Hans: 'LPUSH' } },
|
||||
{ name: 'rpush', label: { en_US: 'RPUSH', zh_Hans: 'RPUSH' } },
|
||||
{ name: 'lpop', label: { en_US: 'LPOP', zh_Hans: 'LPOP' } },
|
||||
{ name: 'rpop', label: { en_US: 'RPOP', zh_Hans: 'RPOP' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'key_template',
|
||||
name: 'key_template',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Key Template',
|
||||
zh_Hans: '键模板',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Redis key (supports variable interpolation)',
|
||||
zh_Hans: 'Redis 键(支持变量插值)',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'hash_field',
|
||||
name: 'hash_field',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Hash Field',
|
||||
zh_Hans: '哈希字段',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Field name for hash operations',
|
||||
zh_Hans: '哈希操作的字段名',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'operation',
|
||||
operator: 'in',
|
||||
value: ['hget', 'hset'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ttl',
|
||||
name: 'ttl',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: {
|
||||
en_US: 'TTL (seconds)',
|
||||
zh_Hans: 'TTL(秒)',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Time to live for SET operations (0 = no expiry)',
|
||||
zh_Hans: 'SET 操作的过期时间(0 = 不过期)',
|
||||
},
|
||||
required: false,
|
||||
default: 0,
|
||||
show_if: {
|
||||
field: 'operation',
|
||||
operator: 'eq',
|
||||
value: 'set',
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
connection_url: 'redis://localhost:6379',
|
||||
operation: 'get',
|
||||
key_template: '',
|
||||
hash_field: '',
|
||||
ttl: 0,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* MCP Tool Node
|
||||
* Invokes MCP (Model Context Protocol) tools
|
||||
*/
|
||||
export const mcpToolConfig: NodeConfigMeta = {
|
||||
nodeType: 'mcp_tool',
|
||||
label: {
|
||||
en_US: 'MCP Tool',
|
||||
zh_Hans: 'MCP 工具',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Invoke an MCP (Model Context Protocol) tool',
|
||||
zh_Hans: '调用 MCP(模型上下文协议)工具',
|
||||
},
|
||||
icon: 'Wrench',
|
||||
category: 'integration',
|
||||
color: '#ec4899',
|
||||
inputs: [
|
||||
createInput('arguments', 'object', {
|
||||
description: 'Tool arguments',
|
||||
label: { en_US: 'Arguments', zh_Hans: '参数' },
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('result', 'any', {
|
||||
description: 'Tool execution result',
|
||||
label: { en_US: 'Result', zh_Hans: '结果' },
|
||||
}),
|
||||
createOutput('success', 'boolean', {
|
||||
description: 'Whether tool call was successful',
|
||||
label: { en_US: 'Success', zh_Hans: '成功' },
|
||||
}),
|
||||
createOutput('error', 'string', {
|
||||
description: 'Error message if failed',
|
||||
label: { en_US: 'Error', zh_Hans: '错误' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'server_name',
|
||||
name: 'server_name',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'MCP Server',
|
||||
zh_Hans: 'MCP 服务器',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Name of the MCP server',
|
||||
zh_Hans: 'MCP 服务器名称',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'tool_name',
|
||||
name: 'tool_name',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Tool Name',
|
||||
zh_Hans: '工具名称',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Name of the MCP tool to invoke',
|
||||
zh_Hans: '要调用的 MCP 工具名称',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'arguments_template',
|
||||
name: 'arguments_template',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'Arguments Template',
|
||||
zh_Hans: '参数模板',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Tool arguments as JSON (supports variable interpolation). Leave empty to use input.',
|
||||
zh_Hans: '工具参数(JSON 格式,支持变量插值)。留空则使用输入。',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'timeout',
|
||||
name: 'timeout',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: {
|
||||
en_US: 'Timeout (seconds)',
|
||||
zh_Hans: '超时时间(秒)',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Maximum execution time',
|
||||
zh_Hans: '最大执行时间',
|
||||
},
|
||||
required: false,
|
||||
default: 30,
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
server_name: '',
|
||||
tool_name: '',
|
||||
arguments_template: '',
|
||||
timeout: 30,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Memory Store Node
|
||||
* Store and retrieve from workflow memory
|
||||
*/
|
||||
export const memoryStoreConfig: NodeConfigMeta = {
|
||||
nodeType: 'memory_store',
|
||||
label: {
|
||||
en_US: 'Memory Store',
|
||||
zh_Hans: '记忆存储',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Store and retrieve data from workflow memory',
|
||||
zh_Hans: '从工作流记忆中存储和检索数据',
|
||||
},
|
||||
icon: 'HardDrive',
|
||||
category: 'integration',
|
||||
color: '#ec4899',
|
||||
inputs: [
|
||||
createInput('value', 'any', {
|
||||
description: 'Value to store',
|
||||
label: { en_US: 'Value', zh_Hans: '值' },
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('result', 'any', {
|
||||
description: 'Retrieved or stored value',
|
||||
label: { en_US: 'Result', zh_Hans: '结果' },
|
||||
}),
|
||||
createOutput('success', 'boolean', {
|
||||
description: 'Whether operation was successful',
|
||||
label: { en_US: 'Success', zh_Hans: '成功' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'operation',
|
||||
name: 'operation',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Operation',
|
||||
zh_Hans: '操作',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Memory operation to perform',
|
||||
zh_Hans: '要执行的记忆操作',
|
||||
},
|
||||
required: true,
|
||||
default: 'get',
|
||||
options: [
|
||||
{ name: 'get', label: { en_US: 'Get', zh_Hans: '获取' } },
|
||||
{ name: 'set', label: { en_US: 'Set', zh_Hans: '设置' } },
|
||||
{ name: 'delete', label: { en_US: 'Delete', zh_Hans: '删除' } },
|
||||
{ name: 'append', label: { en_US: 'Append', zh_Hans: '追加' } },
|
||||
{ name: 'list', label: { en_US: 'List All', zh_Hans: '列出全部' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'key',
|
||||
name: 'key',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Key',
|
||||
zh_Hans: '键',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Memory key (supports variable interpolation)',
|
||||
zh_Hans: '记忆键(支持变量插值)',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'operation',
|
||||
operator: 'neq',
|
||||
value: 'list',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'scope',
|
||||
name: 'scope',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Scope',
|
||||
zh_Hans: '作用域',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Scope of the memory storage',
|
||||
zh_Hans: '记忆存储的作用域',
|
||||
},
|
||||
required: true,
|
||||
default: 'execution',
|
||||
options: [
|
||||
{ name: 'execution', label: { en_US: 'Execution', zh_Hans: '执行' } },
|
||||
{ name: 'workflow', label: { en_US: 'Workflow', zh_Hans: '工作流' } },
|
||||
{ name: 'session', label: { en_US: 'Session', zh_Hans: '会话' } },
|
||||
{ name: 'user', label: { en_US: 'User', zh_Hans: '用户' } },
|
||||
{ name: 'global', label: { en_US: 'Global', zh_Hans: '全局' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ttl',
|
||||
name: 'ttl',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: {
|
||||
en_US: 'TTL (seconds)',
|
||||
zh_Hans: 'TTL(秒)',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Time to live (0 = no expiry)',
|
||||
zh_Hans: '过期时间(0 = 不过期)',
|
||||
},
|
||||
required: false,
|
||||
default: 0,
|
||||
show_if: {
|
||||
field: 'operation',
|
||||
operator: 'eq',
|
||||
value: 'set',
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
operation: 'get',
|
||||
key: '',
|
||||
scope: 'execution',
|
||||
ttl: 0,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Dify Workflow Node
|
||||
* Calls Dify platform workflow
|
||||
*/
|
||||
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 平台工作流' },
|
||||
icon: 'Bot',
|
||||
category: 'integration',
|
||||
color: '#ec4899',
|
||||
inputs: [
|
||||
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: '成功' } }),
|
||||
],
|
||||
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 },
|
||||
],
|
||||
defaultConfig: { 'base-url': '', 'api-key': '', 'app-type': 'workflow', timeout: 60 },
|
||||
};
|
||||
|
||||
/**
|
||||
* Dify Knowledge Query Node
|
||||
*/
|
||||
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 知识库' },
|
||||
icon: 'Search',
|
||||
category: 'integration',
|
||||
color: '#ec4899',
|
||||
inputs: [
|
||||
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: '成功' } }),
|
||||
],
|
||||
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 },
|
||||
],
|
||||
defaultConfig: { 'base-url': '', 'api-key': '', dataset_id: '', top_k: 5 },
|
||||
};
|
||||
|
||||
/**
|
||||
* N8n Workflow Node
|
||||
*/
|
||||
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 工作流' },
|
||||
icon: 'Settings',
|
||||
category: 'integration',
|
||||
color: '#ec4899',
|
||||
inputs: [
|
||||
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: '成功' } }),
|
||||
],
|
||||
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 },
|
||||
],
|
||||
defaultConfig: { 'webhook-url': '', timeout: 60 },
|
||||
};
|
||||
|
||||
/**
|
||||
* Langflow Flow Node
|
||||
*/
|
||||
export const langflowFlowConfig: NodeConfigMeta = {
|
||||
nodeType: 'langflow_flow',
|
||||
label: { en_US: 'Langflow Flow', zh_Hans: 'Langflow 流程' },
|
||||
description: { en_US: 'Call a Langflow flow', zh_Hans: '调用 Langflow 流程' },
|
||||
icon: 'Workflow',
|
||||
category: 'integration',
|
||||
color: '#ec4899',
|
||||
inputs: [
|
||||
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: '成功' } }),
|
||||
],
|
||||
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 },
|
||||
],
|
||||
defaultConfig: { 'base-url': '', 'flow-id': '', 'api-key': '', timeout: 60 },
|
||||
};
|
||||
|
||||
/**
|
||||
* Coze Bot Node
|
||||
*/
|
||||
export const cozeBotConfig: NodeConfigMeta = {
|
||||
nodeType: 'coze_bot',
|
||||
label: { en_US: 'Coze Bot', zh_Hans: 'Coze Bot' },
|
||||
description: { en_US: 'Call a Coze Bot', zh_Hans: '调用扣子 Bot' },
|
||||
icon: 'Bot',
|
||||
category: 'integration',
|
||||
color: '#ec4899',
|
||||
inputs: [
|
||||
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: '成功' } }),
|
||||
],
|
||||
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 },
|
||||
],
|
||||
defaultConfig: { 'api-base': 'https://api.coze.com', 'bot-id': '', 'api-key': '', timeout: 60 },
|
||||
};
|
||||
|
||||
/**
|
||||
* All integration node configurations
|
||||
*/
|
||||
export const integrationConfigs: NodeConfigMeta[] = [
|
||||
difyWorkflowConfig,
|
||||
difyKnowledgeQueryConfig,
|
||||
n8nWorkflowConfig,
|
||||
langflowFlowConfig,
|
||||
cozeBotConfig,
|
||||
databaseQueryConfig,
|
||||
redisOperationConfig,
|
||||
mcpToolConfig,
|
||||
memoryStoreConfig,
|
||||
];
|
||||
|
||||
/**
|
||||
* Get integration config by type
|
||||
*/
|
||||
export function getIntegrationConfig(nodeType: string): NodeConfigMeta | undefined {
|
||||
return integrationConfigs.find((config) => config.nodeType === nodeType);
|
||||
}
|
||||
@@ -0,0 +1,786 @@
|
||||
/**
|
||||
* Process Node Configurations
|
||||
*
|
||||
* Defines configurations for general processing node types:
|
||||
* - text_template: Generate text using templates
|
||||
* - json_transform: Transform JSON data
|
||||
* - code_executor: Execute custom code
|
||||
* - data_aggregator: Aggregate data from multiple sources
|
||||
* - text_splitter: Split text into chunks
|
||||
*/
|
||||
|
||||
import { DynamicFormItemType } from '@/app/infra/entities/form/dynamic';
|
||||
import { NodeConfigMeta, createInput, createOutput } from './types';
|
||||
|
||||
/**
|
||||
* Text Template Node
|
||||
* Generates text using variable interpolation
|
||||
*/
|
||||
export const textTemplateConfig: NodeConfigMeta = {
|
||||
nodeType: 'text_template',
|
||||
label: {
|
||||
en_US: 'Text Template',
|
||||
zh_Hans: '文本模板',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Generate text using templates with variable interpolation',
|
||||
zh_Hans: '使用带有变量插值的模板生成文本',
|
||||
},
|
||||
icon: 'FileText',
|
||||
category: 'process',
|
||||
color: '#3b82f6',
|
||||
inputs: [
|
||||
createInput('variables', 'object', {
|
||||
description: 'Variables to use in the template',
|
||||
label: { en_US: 'Variables', zh_Hans: '变量' },
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('text', 'string', {
|
||||
description: 'Generated text',
|
||||
label: { en_US: 'Text', zh_Hans: '文本' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'template',
|
||||
name: 'template',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'Template',
|
||||
zh_Hans: '模板',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Text template with variable placeholders (e.g., {{variable_name}})',
|
||||
zh_Hans: '带有变量占位符的文本模板(例如 {{variable_name}})',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'escape_html',
|
||||
name: 'escape_html',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
label: {
|
||||
en_US: 'Escape HTML',
|
||||
zh_Hans: '转义 HTML',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Escape HTML characters in variable values',
|
||||
zh_Hans: '转义变量值中的 HTML 字符',
|
||||
},
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
id: 'trim_whitespace',
|
||||
name: 'trim_whitespace',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
label: {
|
||||
en_US: 'Trim Whitespace',
|
||||
zh_Hans: '去除空白',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Remove leading and trailing whitespace from output',
|
||||
zh_Hans: '去除输出的前后空白',
|
||||
},
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
template: '',
|
||||
escape_html: false,
|
||||
trim_whitespace: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* JSON Transform Node
|
||||
* Transforms JSON data using JSONPath or JMESPath expressions
|
||||
*/
|
||||
export const jsonTransformConfig: NodeConfigMeta = {
|
||||
nodeType: 'json_transform',
|
||||
label: {
|
||||
en_US: 'JSON Transform',
|
||||
zh_Hans: 'JSON 转换',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Transform JSON data using expressions or mappings',
|
||||
zh_Hans: '使用表达式或映射转换 JSON 数据',
|
||||
},
|
||||
icon: 'Braces',
|
||||
category: 'process',
|
||||
color: '#3b82f6',
|
||||
inputs: [
|
||||
createInput('input', 'object', {
|
||||
description: 'JSON data to transform',
|
||||
label: { en_US: 'Input', zh_Hans: '输入' },
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('output', 'any', {
|
||||
description: 'Transformed data',
|
||||
label: { en_US: 'Output', zh_Hans: '输出' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'transform_type',
|
||||
name: 'transform_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Transform Type',
|
||||
zh_Hans: '转换类型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Method of transformation',
|
||||
zh_Hans: '转换方法',
|
||||
},
|
||||
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: '字段映射' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'expression',
|
||||
name: 'expression',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Expression',
|
||||
zh_Hans: '表达式',
|
||||
},
|
||||
description: {
|
||||
en_US: 'JMESPath or JSONPath expression',
|
||||
zh_Hans: 'JMESPath 或 JSONPath 表达式',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'transform_type',
|
||||
operator: 'in',
|
||||
value: ['jmespath', 'jsonpath'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mapping',
|
||||
name: 'mapping',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'Field Mapping',
|
||||
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"}',
|
||||
},
|
||||
required: true,
|
||||
default: '{}',
|
||||
show_if: {
|
||||
field: 'transform_type',
|
||||
operator: 'eq',
|
||||
value: 'mapping',
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
transform_type: 'jmespath',
|
||||
expression: '',
|
||||
mapping: '{}',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Code Executor Node
|
||||
* Executes custom code (JavaScript/Python)
|
||||
*/
|
||||
export const codeExecutorConfig: NodeConfigMeta = {
|
||||
nodeType: 'code_executor',
|
||||
label: {
|
||||
en_US: 'Code Executor',
|
||||
zh_Hans: '代码执行',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Execute custom code to process data',
|
||||
zh_Hans: '执行自定义代码处理数据',
|
||||
},
|
||||
icon: 'Code',
|
||||
category: 'process',
|
||||
color: '#3b82f6',
|
||||
inputs: [
|
||||
createInput('input', 'any', {
|
||||
description: 'Input data for the code',
|
||||
label: { en_US: 'Input', zh_Hans: '输入' },
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('output', 'any', {
|
||||
description: 'Code execution result',
|
||||
label: { en_US: 'Output', zh_Hans: '输出' },
|
||||
}),
|
||||
createOutput('logs', 'array', {
|
||||
description: 'Console logs from code execution',
|
||||
label: { en_US: 'Logs', zh_Hans: '日志' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'language',
|
||||
name: 'language',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Language',
|
||||
zh_Hans: '语言',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Programming language to use',
|
||||
zh_Hans: '要使用的编程语言',
|
||||
},
|
||||
required: true,
|
||||
default: 'javascript',
|
||||
options: [
|
||||
{ name: 'javascript', label: { en_US: 'JavaScript', zh_Hans: 'JavaScript' } },
|
||||
{ name: 'python', label: { en_US: 'Python', zh_Hans: 'Python' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'code',
|
||||
name: 'code',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'Code',
|
||||
zh_Hans: '代码',
|
||||
},
|
||||
description: {
|
||||
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;',
|
||||
},
|
||||
{
|
||||
id: 'timeout',
|
||||
name: 'timeout',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: {
|
||||
en_US: 'Timeout (ms)',
|
||||
zh_Hans: '超时时间 (毫秒)',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Maximum execution time in milliseconds',
|
||||
zh_Hans: '最大执行时间(毫秒)',
|
||||
},
|
||||
required: false,
|
||||
default: 5000,
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
language: 'javascript',
|
||||
code: '// Access input with: input\n// Return result with: return result;\n\nreturn input;',
|
||||
timeout: 5000,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Data Aggregator Node
|
||||
* Aggregates data from multiple inputs
|
||||
*/
|
||||
export const dataAggregatorConfig: NodeConfigMeta = {
|
||||
nodeType: 'data_aggregator',
|
||||
label: {
|
||||
en_US: 'Data Aggregator',
|
||||
zh_Hans: '数据聚合',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Aggregate and combine data from multiple sources',
|
||||
zh_Hans: '聚合和组合来自多个来源的数据',
|
||||
},
|
||||
icon: 'Layers',
|
||||
category: 'process',
|
||||
color: '#3b82f6',
|
||||
inputs: [
|
||||
createInput('items', 'array', {
|
||||
description: 'Array of items to aggregate',
|
||||
label: { en_US: 'Items', zh_Hans: '项目' },
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('result', 'any', {
|
||||
description: 'Aggregated result',
|
||||
label: { en_US: 'Result', zh_Hans: '结果' },
|
||||
}),
|
||||
createOutput('count', 'number', {
|
||||
description: 'Number of items aggregated',
|
||||
label: { en_US: 'Count', zh_Hans: '数量' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'aggregation_type',
|
||||
name: 'aggregation_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Aggregation Type',
|
||||
zh_Hans: '聚合类型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'How to aggregate the data',
|
||||
zh_Hans: '如何聚合数据',
|
||||
},
|
||||
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: 'sum', label: { en_US: 'Sum 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: 'first', label: { en_US: 'First Item', zh_Hans: '第一项' } },
|
||||
{ name: 'last', label: { en_US: 'Last Item', zh_Hans: '最后一项' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'separator',
|
||||
name: 'separator',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Separator',
|
||||
zh_Hans: '分隔符',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Separator for string concatenation',
|
||||
zh_Hans: '字符串连接的分隔符',
|
||||
},
|
||||
required: false,
|
||||
default: '\n',
|
||||
show_if: {
|
||||
field: 'aggregation_type',
|
||||
operator: 'eq',
|
||||
value: 'concat',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'field_path',
|
||||
name: 'field_path',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Field Path',
|
||||
zh_Hans: '字段路径',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Path to the field to aggregate (for objects)',
|
||||
zh_Hans: '要聚合的字段路径(用于对象)',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
aggregation_type: 'array',
|
||||
separator: '\n',
|
||||
field_path: '',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Text Splitter Node
|
||||
* Splits text into chunks
|
||||
*/
|
||||
export const textSplitterConfig: NodeConfigMeta = {
|
||||
nodeType: 'text_splitter',
|
||||
label: {
|
||||
en_US: 'Text Splitter',
|
||||
zh_Hans: '文本分割',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Split text into smaller chunks',
|
||||
zh_Hans: '将文本分割成较小的块',
|
||||
},
|
||||
icon: 'Scissors',
|
||||
category: 'process',
|
||||
color: '#3b82f6',
|
||||
inputs: [
|
||||
createInput('text', 'string', {
|
||||
description: 'Text to split',
|
||||
label: { en_US: 'Text', zh_Hans: '文本' },
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('chunks', 'array', {
|
||||
description: 'Array of text chunks',
|
||||
label: { en_US: 'Chunks', zh_Hans: '块' },
|
||||
}),
|
||||
createOutput('count', 'number', {
|
||||
description: 'Number of chunks',
|
||||
label: { en_US: 'Count', zh_Hans: '数量' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'split_type',
|
||||
name: 'split_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Split Type',
|
||||
zh_Hans: '分割类型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'How to split the text',
|
||||
zh_Hans: '如何分割文本',
|
||||
},
|
||||
required: true,
|
||||
default: 'separator',
|
||||
options: [
|
||||
{ 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: '按正则表达式' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'separator',
|
||||
name: 'separator',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Separator',
|
||||
zh_Hans: '分隔符',
|
||||
},
|
||||
description: {
|
||||
en_US: 'String to split on',
|
||||
zh_Hans: '用于分割的字符串',
|
||||
},
|
||||
required: false,
|
||||
default: '\n',
|
||||
show_if: {
|
||||
field: 'split_type',
|
||||
operator: 'eq',
|
||||
value: 'separator',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'chunk_size',
|
||||
name: 'chunk_size',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: {
|
||||
en_US: 'Chunk Size',
|
||||
zh_Hans: '块大小',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Maximum characters per chunk',
|
||||
zh_Hans: '每块的最大字符数',
|
||||
},
|
||||
required: false,
|
||||
default: 1000,
|
||||
show_if: {
|
||||
field: 'split_type',
|
||||
operator: 'eq',
|
||||
value: 'length',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'chunk_overlap',
|
||||
name: 'chunk_overlap',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: {
|
||||
en_US: 'Chunk Overlap',
|
||||
zh_Hans: '块重叠',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Number of characters to overlap between chunks',
|
||||
zh_Hans: '块之间重叠的字符数',
|
||||
},
|
||||
required: false,
|
||||
default: 100,
|
||||
show_if: {
|
||||
field: 'split_type',
|
||||
operator: 'eq',
|
||||
value: 'length',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'regex_pattern',
|
||||
name: 'regex_pattern',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Regex Pattern',
|
||||
zh_Hans: '正则表达式模式',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Regular expression pattern to split on',
|
||||
zh_Hans: '用于分割的正则表达式模式',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'split_type',
|
||||
operator: 'eq',
|
||||
value: 'regex',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'remove_empty',
|
||||
name: 'remove_empty',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
label: {
|
||||
en_US: 'Remove Empty',
|
||||
zh_Hans: '移除空块',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Remove empty chunks from result',
|
||||
zh_Hans: '从结果中移除空块',
|
||||
},
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
split_type: 'separator',
|
||||
separator: '\n',
|
||||
chunk_size: 1000,
|
||||
chunk_overlap: 100,
|
||||
regex_pattern: '',
|
||||
remove_empty: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Variable Assignment Node
|
||||
* Assigns values to workflow variables
|
||||
*/
|
||||
export const variableAssignmentConfig: NodeConfigMeta = {
|
||||
nodeType: 'variable_assignment',
|
||||
label: {
|
||||
en_US: 'Variable Assignment',
|
||||
zh_Hans: '变量赋值',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Assign values to workflow variables',
|
||||
zh_Hans: '为工作流变量赋值',
|
||||
},
|
||||
icon: 'Variable',
|
||||
category: 'process',
|
||||
color: '#3b82f6',
|
||||
inputs: [
|
||||
createInput('value', 'any', {
|
||||
description: 'Value to assign',
|
||||
label: { en_US: 'Value', zh_Hans: '值' },
|
||||
required: false,
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('output', 'any', {
|
||||
description: 'The assigned value',
|
||||
label: { en_US: 'Output', zh_Hans: '输出' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'variable_name',
|
||||
name: 'variable_name',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Variable Name',
|
||||
zh_Hans: '变量名',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Name of the variable to assign',
|
||||
zh_Hans: '要赋值的变量名',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'value_type',
|
||||
name: 'value_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Value Type',
|
||||
zh_Hans: '值类型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Type of value to assign',
|
||||
zh_Hans: '要赋的值类型',
|
||||
},
|
||||
required: true,
|
||||
default: 'input',
|
||||
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: '表达式' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'static_value',
|
||||
name: 'static_value',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'Static Value',
|
||||
zh_Hans: '静态值',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Value to assign (as JSON)',
|
||||
zh_Hans: '要赋的值(JSON 格式)',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'value_type',
|
||||
operator: 'eq',
|
||||
value: 'static',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'expression',
|
||||
name: 'expression',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Expression',
|
||||
zh_Hans: '表达式',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Expression to evaluate (e.g., {{input}} + 1)',
|
||||
zh_Hans: '要计算的表达式(例如 {{input}} + 1)',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'value_type',
|
||||
operator: 'eq',
|
||||
value: 'expression',
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
variable_name: '',
|
||||
value_type: 'input',
|
||||
static_value: '',
|
||||
expression: '',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Data Transform Node
|
||||
* Transform and extract data using templates or JSONPath
|
||||
*/
|
||||
export const dataTransformConfig: NodeConfigMeta = {
|
||||
nodeType: 'data_transform',
|
||||
label: {
|
||||
en_US: 'Data Transform',
|
||||
zh_Hans: '数据转换',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Transform and extract data using templates or JSONPath',
|
||||
zh_Hans: '使用模板或 JSONPath 转换和提取数据',
|
||||
},
|
||||
icon: 'RefreshCw',
|
||||
category: 'process',
|
||||
color: '#3b82f6',
|
||||
inputs: [
|
||||
createInput('data', 'any', {
|
||||
description: 'Input data',
|
||||
label: { en_US: 'Data', zh_Hans: '数据' },
|
||||
required: true,
|
||||
}),
|
||||
],
|
||||
outputs: [
|
||||
createOutput('result', 'any', {
|
||||
description: 'Transform result',
|
||||
label: { en_US: 'Result', zh_Hans: '结果' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'transform_type',
|
||||
name: 'transform_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Transform Type',
|
||||
zh_Hans: '转换类型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Type of transformation to perform',
|
||||
zh_Hans: '要执行的转换类型',
|
||||
},
|
||||
required: true,
|
||||
default: 'template',
|
||||
options: [
|
||||
{ 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: '表达式' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'template',
|
||||
name: 'template',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'Template',
|
||||
zh_Hans: '模板',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Template with {{variable}} syntax',
|
||||
zh_Hans: '支持 {{variable}} 语法的模板',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'transform_type',
|
||||
operator: 'eq',
|
||||
value: 'template',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'expression',
|
||||
name: 'expression',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Expression',
|
||||
zh_Hans: '表达式',
|
||||
},
|
||||
description: {
|
||||
en_US: 'JSONPath/JMESPath expression',
|
||||
zh_Hans: 'JSONPath/JMESPath 表达式',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'transform_type',
|
||||
operator: 'in',
|
||||
value: ['jsonpath', 'jmespath', 'expression'],
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
transform_type: 'template',
|
||||
template: '',
|
||||
expression: '',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* All process node configurations
|
||||
*/
|
||||
export const processConfigs: NodeConfigMeta[] = [
|
||||
textTemplateConfig,
|
||||
jsonTransformConfig,
|
||||
codeExecutorConfig,
|
||||
dataAggregatorConfig,
|
||||
textSplitterConfig,
|
||||
variableAssignmentConfig,
|
||||
dataTransformConfig,
|
||||
];
|
||||
|
||||
/**
|
||||
* Get process config by type
|
||||
*/
|
||||
export function getProcessConfig(nodeType: string): NodeConfigMeta | undefined {
|
||||
return processConfigs.find((config) => config.nodeType === nodeType);
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
/**
|
||||
* Trigger Node Configurations
|
||||
*
|
||||
* Defines configurations for all trigger node types:
|
||||
* - message_trigger: Triggered by incoming messages
|
||||
* - cron_trigger: Triggered by scheduled time
|
||||
* - webhook_trigger: Triggered by HTTP webhook calls
|
||||
* - event_trigger: Triggered by system events
|
||||
*/
|
||||
|
||||
import { DynamicFormItemType } from '@/app/infra/entities/form/dynamic';
|
||||
import { NodeConfigMeta, createOutput } from './types';
|
||||
|
||||
/**
|
||||
* Message Trigger Node
|
||||
* Triggers workflow when a message matches specified conditions
|
||||
*/
|
||||
export const messageTriggerConfig: NodeConfigMeta = {
|
||||
nodeType: 'message_trigger',
|
||||
label: {
|
||||
en_US: 'Message Trigger',
|
||||
zh_Hans: '消息触发',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Trigger workflow when a message matches the specified conditions',
|
||||
zh_Hans: '当收到匹配指定条件的消息时触发工作流',
|
||||
},
|
||||
icon: 'MessageSquare',
|
||||
category: 'trigger',
|
||||
color: '#f59e0b',
|
||||
isEntryPoint: true,
|
||||
maxInstances: 1,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
createOutput('message', 'object', {
|
||||
description: 'The received message object',
|
||||
label: { en_US: 'Message', zh_Hans: '消息' },
|
||||
}),
|
||||
createOutput('sender', 'object', {
|
||||
description: 'Message sender information',
|
||||
label: { en_US: 'Sender', zh_Hans: '发送者' },
|
||||
}),
|
||||
createOutput('context', 'object', {
|
||||
description: 'Message context information',
|
||||
label: { en_US: 'Context', zh_Hans: '上下文' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'match_type',
|
||||
name: 'match_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Match Type',
|
||||
zh_Hans: '匹配类型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'How to match the incoming message',
|
||||
zh_Hans: '如何匹配收到的消息',
|
||||
},
|
||||
required: true,
|
||||
default: 'all',
|
||||
options: [
|
||||
{ name: 'all', label: { en_US: 'All Messages', 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: 'exact', label: { en_US: 'Exact Match', zh_Hans: '精确匹配' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'match_pattern',
|
||||
name: 'match_pattern',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Match Pattern',
|
||||
zh_Hans: '匹配模式',
|
||||
},
|
||||
description: {
|
||||
en_US: 'The pattern to match against the message (prefix, regex, keyword, or exact text)',
|
||||
zh_Hans: '用于匹配消息的模式(前缀、正则表达式、关键词或精确文本)',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'match_type',
|
||||
operator: 'neq',
|
||||
value: 'all',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ignore_bot_messages',
|
||||
name: 'ignore_bot_messages',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
label: {
|
||||
en_US: 'Ignore Bot Messages',
|
||||
zh_Hans: '忽略机器人消息',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Do not trigger for messages sent by bots',
|
||||
zh_Hans: '不对机器人发送的消息触发',
|
||||
},
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
match_type: 'all',
|
||||
match_pattern: '',
|
||||
ignore_bot_messages: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Cron Trigger Node
|
||||
* Triggers workflow on a schedule
|
||||
*/
|
||||
export const cronTriggerConfig: NodeConfigMeta = {
|
||||
nodeType: 'cron_trigger',
|
||||
label: {
|
||||
en_US: 'Scheduled Trigger',
|
||||
zh_Hans: '定时触发',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Trigger workflow on a scheduled time using cron expression',
|
||||
zh_Hans: '使用 Cron 表达式按计划时间触发工作流',
|
||||
},
|
||||
icon: 'Clock',
|
||||
category: 'trigger',
|
||||
color: '#f59e0b',
|
||||
isEntryPoint: true,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
createOutput('trigger_time', 'datetime', {
|
||||
description: 'The time when the trigger fired',
|
||||
label: { en_US: 'Trigger Time', zh_Hans: '触发时间' },
|
||||
}),
|
||||
createOutput('context', 'object', {
|
||||
description: 'Trigger context information',
|
||||
label: { en_US: 'Context', zh_Hans: '上下文' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'cron_expression',
|
||||
name: 'cron_expression',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Cron Expression',
|
||||
zh_Hans: 'Cron 表达式',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Standard cron expression (e.g., "0 9 * * *" for 9 AM daily)',
|
||||
zh_Hans: '标准 Cron 表达式(例如 "0 9 * * *" 表示每天上午 9 点)',
|
||||
},
|
||||
required: true,
|
||||
default: '0 9 * * *',
|
||||
},
|
||||
{
|
||||
id: 'timezone',
|
||||
name: 'timezone',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Timezone',
|
||||
zh_Hans: '时区',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Timezone for the cron schedule',
|
||||
zh_Hans: 'Cron 计划的时区',
|
||||
},
|
||||
required: true,
|
||||
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)' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
name: 'description',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Description',
|
||||
zh_Hans: '描述',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Optional description for this scheduled trigger',
|
||||
zh_Hans: '此定时触发器的可选描述',
|
||||
},
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'enabled',
|
||||
name: 'enabled',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
label: {
|
||||
en_US: 'Enabled',
|
||||
zh_Hans: '启用',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Whether this scheduled trigger is active',
|
||||
zh_Hans: '此定时触发器是否激活',
|
||||
},
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
cron_expression: '0 9 * * *',
|
||||
timezone: 'Asia/Shanghai',
|
||||
description: '',
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Webhook Trigger Node
|
||||
* Triggers workflow via HTTP webhook
|
||||
*/
|
||||
export const webhookTriggerConfig: NodeConfigMeta = {
|
||||
nodeType: 'webhook_trigger',
|
||||
label: {
|
||||
en_US: 'Webhook Trigger',
|
||||
zh_Hans: 'Webhook 触发',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Trigger workflow when an HTTP request is received at the webhook URL',
|
||||
zh_Hans: '当在 Webhook URL 收到 HTTP 请求时触发工作流',
|
||||
},
|
||||
icon: 'Webhook',
|
||||
category: 'trigger',
|
||||
color: '#f59e0b',
|
||||
isEntryPoint: true,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
createOutput('body', 'object', {
|
||||
description: 'Request body data',
|
||||
label: { en_US: 'Body', zh_Hans: '请求体' },
|
||||
}),
|
||||
createOutput('headers', 'object', {
|
||||
description: 'Request headers',
|
||||
label: { en_US: 'Headers', zh_Hans: '请求头' },
|
||||
}),
|
||||
createOutput('query', 'object', {
|
||||
description: 'Query parameters',
|
||||
label: { en_US: 'Query', zh_Hans: '查询参数' },
|
||||
}),
|
||||
createOutput('method', 'string', {
|
||||
description: 'HTTP method',
|
||||
label: { en_US: 'Method', zh_Hans: 'HTTP 方法' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'webhook_path',
|
||||
name: 'webhook_path',
|
||||
type: DynamicFormItemType.STRING,
|
||||
label: {
|
||||
en_US: 'Webhook Path',
|
||||
zh_Hans: 'Webhook 路径',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Unique path for this webhook (e.g., "my-workflow")',
|
||||
zh_Hans: '此 Webhook 的唯一路径(例如 "my-workflow")',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
id: 'auth_type',
|
||||
name: 'auth_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Authentication',
|
||||
zh_Hans: '认证方式',
|
||||
},
|
||||
description: {
|
||||
en_US: 'How to authenticate incoming webhook requests',
|
||||
zh_Hans: '如何验证传入的 Webhook 请求',
|
||||
},
|
||||
required: true,
|
||||
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: 'basic', label: { en_US: 'Basic Auth', zh_Hans: '基本认证' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'auth_token',
|
||||
name: 'auth_token',
|
||||
type: DynamicFormItemType.SECRET,
|
||||
label: {
|
||||
en_US: 'Auth Token',
|
||||
zh_Hans: '认证令牌',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Token or secret for authentication',
|
||||
zh_Hans: '用于认证的令牌或密钥',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
show_if: {
|
||||
field: 'auth_type',
|
||||
operator: 'in',
|
||||
value: ['token', 'signature', 'basic'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'content_type',
|
||||
name: 'content_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Content Type',
|
||||
zh_Hans: '内容类型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Expected Content-Type of the request',
|
||||
zh_Hans: '请求预期的 Content-Type',
|
||||
},
|
||||
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: '纯文本' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'validation',
|
||||
name: 'validation',
|
||||
type: DynamicFormItemType.TEXT,
|
||||
label: {
|
||||
en_US: 'Validation Rules',
|
||||
zh_Hans: '验证规则',
|
||||
},
|
||||
description: {
|
||||
en_US: 'JSON validation rules for request body (optional)',
|
||||
zh_Hans: '请求体的 JSON 验证规则(可选)',
|
||||
},
|
||||
required: false,
|
||||
default: '{}',
|
||||
},
|
||||
{
|
||||
id: 'timeout',
|
||||
name: 'timeout',
|
||||
type: DynamicFormItemType.INT,
|
||||
label: {
|
||||
en_US: 'Timeout (seconds)',
|
||||
zh_Hans: '超时时间(秒)',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Request timeout in seconds',
|
||||
zh_Hans: '请求超时时间(秒)',
|
||||
},
|
||||
required: false,
|
||||
default: 30,
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
webhook_path: '',
|
||||
auth_type: 'none',
|
||||
auth_token: '',
|
||||
content_type: 'application/json',
|
||||
validation: '{}',
|
||||
timeout: 30,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Event Trigger Node
|
||||
* Triggers workflow on system events
|
||||
*/
|
||||
export const eventTriggerConfig: NodeConfigMeta = {
|
||||
nodeType: 'event_trigger',
|
||||
label: {
|
||||
en_US: 'Event Trigger',
|
||||
zh_Hans: '事件触发',
|
||||
},
|
||||
description: {
|
||||
en_US: 'Trigger workflow when a system event occurs',
|
||||
zh_Hans: '当系统事件发生时触发工作流',
|
||||
},
|
||||
icon: 'Zap',
|
||||
category: 'trigger',
|
||||
color: '#f59e0b',
|
||||
isEntryPoint: true,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
createOutput('event', 'object', {
|
||||
description: 'The event data',
|
||||
label: { en_US: 'Event', zh_Hans: '事件' },
|
||||
}),
|
||||
createOutput('event_type', 'string', {
|
||||
description: 'Type of the event',
|
||||
label: { en_US: 'Event Type', zh_Hans: '事件类型' },
|
||||
}),
|
||||
createOutput('context', 'object', {
|
||||
description: 'Event context information',
|
||||
label: { en_US: 'Context', zh_Hans: '上下文' },
|
||||
}),
|
||||
],
|
||||
configSchema: [
|
||||
{
|
||||
id: 'event_type',
|
||||
name: 'event_type',
|
||||
type: DynamicFormItemType.SELECT,
|
||||
label: {
|
||||
en_US: 'Event Type',
|
||||
zh_Hans: '事件类型',
|
||||
},
|
||||
description: {
|
||||
en_US: 'The type of system event to listen for',
|
||||
zh_Hans: '要监听的系统事件类型',
|
||||
},
|
||||
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: '入群请求' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
defaultConfig: {
|
||||
event_type: 'member_join',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* All trigger node configurations
|
||||
*/
|
||||
export const triggerConfigs: NodeConfigMeta[] = [
|
||||
messageTriggerConfig,
|
||||
cronTriggerConfig,
|
||||
webhookTriggerConfig,
|
||||
eventTriggerConfig,
|
||||
];
|
||||
|
||||
/**
|
||||
* Get trigger config by type
|
||||
*/
|
||||
export function getTriggerConfig(nodeType: string): NodeConfigMeta | undefined {
|
||||
return triggerConfigs.find((config) => config.nodeType === nodeType);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||
import { I18nObject } from '@/app/infra/entities/common';
|
||||
import { NodeCategory, PortDefinition } from '@/app/infra/entities/workflow';
|
||||
|
||||
/**
|
||||
* Extended port configuration with additional metadata
|
||||
*/
|
||||
export interface ExtendedPortDefinition extends PortDefinition {
|
||||
label?: I18nObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node configuration metadata
|
||||
* Defines all aspects of a node type including its appearance, ports, and configuration options
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of all node configurations by type
|
||||
*/
|
||||
export type NodeConfigRegistry = Record<string, NodeConfigMeta>;
|
||||
|
||||
/**
|
||||
* Helper function to create a consistent port definition
|
||||
*/
|
||||
export function createPort(
|
||||
name: string,
|
||||
type: string,
|
||||
options?: {
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
label?: I18nObject;
|
||||
}
|
||||
): ExtendedPortDefinition {
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
description: options?.description,
|
||||
required: options?.required ?? false,
|
||||
label: options?.label,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create input port
|
||||
*/
|
||||
export function createInput(
|
||||
name: string,
|
||||
type: string,
|
||||
options?: {
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
label?: I18nObject;
|
||||
}
|
||||
): ExtendedPortDefinition {
|
||||
return createPort(name, type, { ...options, required: options?.required ?? true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create output port
|
||||
*/
|
||||
export function createOutput(
|
||||
name: string,
|
||||
type: string,
|
||||
options?: {
|
||||
description?: string;
|
||||
label?: I18nObject;
|
||||
}
|
||||
): ExtendedPortDefinition {
|
||||
return createPort(name, type, { ...options, required: false });
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* Shared constants for the Workflow editor.
|
||||
*
|
||||
* Centralises `nodeTypeI18nKeys`, `nodeIcons`, and palette category
|
||||
* colours that were previously duplicated across WorkflowNodeComponent,
|
||||
* NodePalette, and useWorkflowStore.
|
||||
*/
|
||||
|
||||
import {
|
||||
MessageSquare,
|
||||
Timer,
|
||||
Webhook,
|
||||
Bot,
|
||||
Brain,
|
||||
Search,
|
||||
Code,
|
||||
FileText,
|
||||
GitBranch,
|
||||
Repeat,
|
||||
GitMerge,
|
||||
PauseCircle,
|
||||
AlertCircle,
|
||||
Variable,
|
||||
Send,
|
||||
Database,
|
||||
Zap,
|
||||
Globe,
|
||||
Settings,
|
||||
Bell,
|
||||
ArrowRightLeft,
|
||||
Split,
|
||||
Layers,
|
||||
Clock,
|
||||
ListFilter,
|
||||
Workflow,
|
||||
MessageCircle,
|
||||
Cpu,
|
||||
Play,
|
||||
Plug,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
|
||||
import i18n from 'i18next';
|
||||
import { resolveI18nLabel, maybeTranslateKey } from './workflow-i18n';
|
||||
|
||||
// ─── Node type → i18n key mapping ──────────────────────────────────
|
||||
//
|
||||
// Single source of truth. Used by WorkflowNodeComponent,
|
||||
// NodePalette, and useWorkflowStore.
|
||||
|
||||
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' },
|
||||
// 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' },
|
||||
// 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' },
|
||||
// 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' },
|
||||
// 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 – 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' },
|
||||
};
|
||||
|
||||
// Flat version: type → label key only (convenience for store / node component)
|
||||
export const NODE_TYPE_LABEL_KEYS: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(NODE_TYPE_I18N_KEYS).map(([k, v]) => [k, v.labelKey]),
|
||||
);
|
||||
|
||||
// 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' },
|
||||
integration: { labelKey: 'workflows.nodes.integration' },
|
||||
};
|
||||
|
||||
// ─── Icon mapping ───────────────────────────────────────────────────
|
||||
|
||||
export const NODE_ICONS: Record<string, React.ElementType> = {
|
||||
// Trigger
|
||||
'trigger.message': MessageSquare,
|
||||
'trigger.message_trigger': MessageSquare,
|
||||
'trigger.schedule': Timer,
|
||||
'trigger.cron': Timer,
|
||||
'trigger.cron_trigger': Timer,
|
||||
'trigger.webhook': Webhook,
|
||||
'trigger.webhook_trigger': Webhook,
|
||||
'trigger.manual': Zap,
|
||||
'trigger.event': Bell,
|
||||
'trigger.event_trigger': Bell,
|
||||
// Process / AI
|
||||
'process.llm': Brain,
|
||||
'process.llm_call': Brain,
|
||||
'process.knowledge': Search,
|
||||
'process.knowledge_retrieval': Search,
|
||||
'process.code': Code,
|
||||
'process.code_executor': Code,
|
||||
'process.template': FileText,
|
||||
'process.text_template': FileText,
|
||||
'process.data_transform': ArrowRightLeft,
|
||||
'process.http': Globe,
|
||||
'process.http_request': Globe,
|
||||
'process.question_classifier': ListFilter,
|
||||
'process.parameter_extractor': Variable,
|
||||
'process.json_transform': ArrowRightLeft,
|
||||
'process.data_aggregator': Layers,
|
||||
'process.text_splitter': Code,
|
||||
'process.variable_assignment': Variable,
|
||||
// Control
|
||||
'control.condition': GitBranch,
|
||||
'control.switch': Split,
|
||||
'control.loop': Repeat,
|
||||
'control.iterator': Repeat,
|
||||
'control.parallel': Layers,
|
||||
'control.merge': GitMerge,
|
||||
'control.variable_aggregator': GitMerge,
|
||||
'control.delay': Timer,
|
||||
'control.wait': Clock,
|
||||
'control.error_handler': AlertCircle,
|
||||
// Action
|
||||
'action.reply': Send,
|
||||
'action.reply_message': Send,
|
||||
'action.send_message': MessageCircle,
|
||||
'action.variable': Variable,
|
||||
'action.set_variable': Variable,
|
||||
'action.store_data': Database,
|
||||
'action.database': Database,
|
||||
'action.notify': Bell,
|
||||
'action.external': Globe,
|
||||
'action.call_pipeline': Workflow,
|
||||
'action.opening_statement': MessageSquare,
|
||||
'action.end': PauseCircle,
|
||||
// Integration – external services
|
||||
'integration.dify': Bot,
|
||||
'integration.dify_workflow': Bot,
|
||||
'integration.dify_knowledge_query': Search,
|
||||
'integration.n8n': Settings,
|
||||
'integration.n8n_workflow': Settings,
|
||||
'integration.langflow': Workflow,
|
||||
'integration.langflow_flow': Workflow,
|
||||
'integration.coze': Bot,
|
||||
'integration.coze_bot': Bot,
|
||||
// Integration – data & tools
|
||||
'integration.database_query': Database,
|
||||
'integration.redis_operation': Cpu,
|
||||
'integration.mcp_tool': Settings,
|
||||
'integration.memory_store': Layers,
|
||||
};
|
||||
|
||||
// ─── Category palette colours ───────────────────────────────────────
|
||||
|
||||
export const PALETTE_CATEGORY_COLORS: Record<string, string> = {
|
||||
trigger: 'text-amber-600 dark:text-amber-400',
|
||||
process: 'text-blue-600 dark:text-blue-400',
|
||||
control: 'text-purple-600 dark:text-purple-400',
|
||||
action: 'text-green-600 dark:text-green-400',
|
||||
integration: 'text-pink-600 dark:text-pink-400',
|
||||
};
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
export const PALETTE_CATEGORY_BORDER: Record<string, string> = {
|
||||
trigger: 'border-amber-300 dark:border-amber-700',
|
||||
process: 'border-blue-300 dark:border-blue-700',
|
||||
control: 'border-purple-300 dark:border-purple-700',
|
||||
action: 'border-green-300 dark:border-green-700',
|
||||
integration: 'border-pink-300 dark:border-pink-700',
|
||||
};
|
||||
|
||||
export const CATEGORY_ICONS: Record<string, React.ElementType> = {
|
||||
trigger: Zap,
|
||||
process: Cpu,
|
||||
control: GitBranch,
|
||||
action: Play,
|
||||
integration: Plug,
|
||||
};
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
let keys = NODE_TYPE_I18N_KEYS[type];
|
||||
if (keys) return keys;
|
||||
|
||||
// Try stripping or adding category prefix
|
||||
const parts = type.split('.');
|
||||
const typeName = parts[parts.length - 1];
|
||||
if (parts.length > 1) {
|
||||
keys = NODE_TYPE_I18N_KEYS[type]; // already tried
|
||||
} else {
|
||||
for (const cat of ['trigger', 'process', 'control', 'action', 'integration']) {
|
||||
keys = NODE_TYPE_I18N_KEYS[`${cat}.${typeName}`];
|
||||
if (keys) return keys;
|
||||
}
|
||||
}
|
||||
|
||||
// Scan all entries for a tail match
|
||||
for (const [k, v] of Object.entries(NODE_TYPE_I18N_KEYS)) {
|
||||
if (k.endsWith(`.${typeName}`) || k === typeName) return v;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a human-readable label for a node type.
|
||||
* Priority: i18n key → backend label dict → prettified type string.
|
||||
*/
|
||||
export function getNodeTypeLabel(
|
||||
type: string,
|
||||
labelDict?: Record<string, string>,
|
||||
): string {
|
||||
// 1. i18n key
|
||||
const keys = findNodeI18nKeys(type);
|
||||
if (keys) {
|
||||
const translated = i18n.t(keys.labelKey, { defaultValue: '' });
|
||||
if (translated) return translated;
|
||||
}
|
||||
|
||||
// 2. Backend label dict
|
||||
if (labelDict) {
|
||||
const label = resolveI18nLabel(labelDict);
|
||||
if (label) return label;
|
||||
}
|
||||
|
||||
// 3. Prettify type string
|
||||
const base = type.includes('.') ? type.split('.').slice(1).join('.') : type;
|
||||
return base
|
||||
.split('_')
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Unified i18n utilities for the Workflow module.
|
||||
*
|
||||
* The backend API returns label dicts with keys like `zh-CN`, `en`,
|
||||
* while node-configs use `zh_Hans`, `en_US`, and the i18next system
|
||||
* uses `zh-Hans`, `en-US`. This module normalises **all** variants
|
||||
* into a single lookup so every consumer gets the right value without
|
||||
* maintaining its own fallback chain.
|
||||
*/
|
||||
|
||||
import i18n from 'i18next';
|
||||
import { I18nObject } from '@/app/infra/entities/common';
|
||||
|
||||
// ─── Key normalisation ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* All known aliases for a given canonical locale, ordered by priority.
|
||||
* When the user's language starts with a prefix we try every alias in
|
||||
* order until one hits.
|
||||
*/
|
||||
const ZH_KEYS = ['zh-CN', 'zh_Hans', 'zh-Hans', 'zh_CN', 'zh'] as const;
|
||||
const EN_KEYS = ['en-US', 'en_US', 'en'] as const;
|
||||
|
||||
/**
|
||||
* Resolve a translated string from a label dict that may use **any**
|
||||
* combination of `zh-CN`, `zh_Hans`, `en`, `en-US`, `en_US` etc.
|
||||
*
|
||||
* Works with both `Record<string, string>` (backend) and the typed
|
||||
* `I18nObject` (node-configs).
|
||||
*
|
||||
* Optionally falls through to `i18n.t(value)` when the stored value
|
||||
* itself looks like an i18n key (e.g. `"workflows.nodes.llmCall"`).
|
||||
*/
|
||||
export function resolveI18nLabel(
|
||||
obj: Record<string, string> | I18nObject | undefined | null,
|
||||
): string {
|
||||
if (!obj || typeof obj !== 'object') return '';
|
||||
|
||||
const record = obj as Record<string, string>;
|
||||
const lang = i18n.language; // e.g. "zh-Hans", "en-US"
|
||||
|
||||
// 1. Try exact match with current language
|
||||
if (record[lang]) return maybeTranslateKey(record[lang]);
|
||||
|
||||
// 2. Try aliases for the current language family
|
||||
const primary = lang.startsWith('zh') ? ZH_KEYS : EN_KEYS;
|
||||
const fallback = lang.startsWith('zh') ? EN_KEYS : ZH_KEYS;
|
||||
|
||||
for (const k of primary) {
|
||||
if (record[k]) return maybeTranslateKey(record[k]);
|
||||
}
|
||||
for (const k of fallback) {
|
||||
if (record[k]) return maybeTranslateKey(record[k]);
|
||||
}
|
||||
|
||||
// 3. Last resort – grab the first non-empty value in the dict
|
||||
const first = Object.values(record).find((v) => typeof v === 'string' && v);
|
||||
return first ? maybeTranslateKey(first) : '';
|
||||
}
|
||||
|
||||
// ─── i18n key detection ─────────────────────────────────────────────
|
||||
|
||||
const I18N_KEY_PREFIXES = ['workflows.', 'common.', 'bots.', 'models.'];
|
||||
|
||||
/**
|
||||
* If `value` looks like an i18n key (e.g. `"workflows.nodes.llmCall"`)
|
||||
* translate it via i18next; otherwise return it unchanged.
|
||||
*/
|
||||
export function maybeTranslateKey(value: string): string {
|
||||
if (!value) return value;
|
||||
if (I18N_KEY_PREFIXES.some((p) => value.startsWith(p))) {
|
||||
const translated = i18n.t(value);
|
||||
if (translated !== value) return translated;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import type {
|
||||
WorkflowNodeTypeMetadata,
|
||||
WorkflowPortDefinition,
|
||||
} from '@/app/infra/entities/api';
|
||||
import type { I18nObject } from '@/app/infra/entities/common';
|
||||
import {
|
||||
getNodeConfig,
|
||||
type NodeConfigMeta,
|
||||
} from './node-configs';
|
||||
|
||||
export const WORKFLOW_NODE_CATEGORIES = [
|
||||
'trigger',
|
||||
'process',
|
||||
'control',
|
||||
'action',
|
||||
'integration',
|
||||
] as const;
|
||||
|
||||
const DEFAULT_INPUT_PORT: WorkflowPortDefinition = {
|
||||
name: 'input',
|
||||
type: 'any',
|
||||
label: 'workflows.nodeInputs.input',
|
||||
};
|
||||
|
||||
const DEFAULT_OUTPUT_PORT: WorkflowPortDefinition = {
|
||||
name: 'output',
|
||||
type: 'any',
|
||||
label: 'workflows.nodeOutputs.output',
|
||||
};
|
||||
|
||||
function ensurePortLabelKey(
|
||||
prefix: 'workflows.nodeInputs' | 'workflows.nodeOutputs',
|
||||
portName: string,
|
||||
label?: string | Record<string, string>,
|
||||
): string {
|
||||
const key = `${prefix}.${portName}`;
|
||||
|
||||
if (typeof label === 'string') {
|
||||
return label.startsWith(prefix) ? label : key;
|
||||
}
|
||||
|
||||
if (label && typeof label === 'object') {
|
||||
const existing = Object.values(label).find(
|
||||
(value) => typeof value === 'string' && value.startsWith(prefix),
|
||||
);
|
||||
if (existing) return existing;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
function normalizePort(
|
||||
prefix: 'workflows.nodeInputs' | 'workflows.nodeOutputs',
|
||||
port: WorkflowPortDefinition,
|
||||
): WorkflowPortDefinition {
|
||||
return {
|
||||
...port,
|
||||
label: ensurePortLabelKey(prefix, port.name, port.label),
|
||||
};
|
||||
}
|
||||
|
||||
function toBackendI18nObject(value?: I18nObject): Record<string, string> | undefined {
|
||||
if (!value) return undefined;
|
||||
|
||||
return {
|
||||
'en-US': value.en_US,
|
||||
en: value.en_US,
|
||||
'zh-Hans': value.zh_Hans,
|
||||
'zh-CN': value.zh_Hans,
|
||||
};
|
||||
}
|
||||
|
||||
function toWorkflowPortDefinition(
|
||||
prefix: 'workflows.nodeInputs' | 'workflows.nodeOutputs',
|
||||
port: NodeConfigMeta['inputs'][number] | NodeConfigMeta['outputs'][number],
|
||||
): WorkflowPortDefinition {
|
||||
return normalizePort(prefix, {
|
||||
name: port.name,
|
||||
type: port.type,
|
||||
label: `${prefix}.${port.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveNodeTypeCategory(type: string): string {
|
||||
if (type.includes('.')) {
|
||||
return type.split('.')[0];
|
||||
}
|
||||
return 'process';
|
||||
}
|
||||
|
||||
function getLocalConfigVariants(type: string): string[] {
|
||||
const variants = new Set<string>([type]);
|
||||
|
||||
if (type.includes('.')) {
|
||||
variants.add(type.split('.').slice(1).join('.'));
|
||||
variants.add(type.replace(/\./g, '_'));
|
||||
} else {
|
||||
for (const category of WORKFLOW_NODE_CATEGORIES) {
|
||||
variants.add(`${category}.${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
return [...variants];
|
||||
}
|
||||
|
||||
export function getLocalNodeTypeMeta(type: string): WorkflowNodeTypeMetadata | null {
|
||||
let localConfig: NodeConfigMeta | undefined;
|
||||
|
||||
for (const variant of getLocalConfigVariants(type)) {
|
||||
localConfig = getNodeConfig(variant);
|
||||
if (localConfig) break;
|
||||
}
|
||||
|
||||
if (!localConfig) return null;
|
||||
|
||||
return {
|
||||
type,
|
||||
category: localConfig.category,
|
||||
label: toBackendI18nObject(localConfig.label) ?? {},
|
||||
description: toBackendI18nObject(localConfig.description),
|
||||
icon: localConfig.icon,
|
||||
color: localConfig.color,
|
||||
config_schema: localConfig.configSchema,
|
||||
inputs: localConfig.inputs.map((input) =>
|
||||
toWorkflowPortDefinition('workflows.nodeInputs', input),
|
||||
),
|
||||
outputs: localConfig.outputs.map((output) =>
|
||||
toWorkflowPortDefinition('workflows.nodeOutputs', output),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeWorkflowNodeTypeMeta(
|
||||
type: string,
|
||||
nodeType?: WorkflowNodeTypeMetadata | null,
|
||||
): WorkflowNodeTypeMetadata {
|
||||
const localMeta = getLocalNodeTypeMeta(type);
|
||||
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 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
|
||||
: [];
|
||||
|
||||
return {
|
||||
type,
|
||||
category,
|
||||
label: nodeType?.label || localMeta?.label || {},
|
||||
description: nodeType?.description || localMeta?.description,
|
||||
icon: nodeType?.icon || localMeta?.icon,
|
||||
color: nodeType?.color || localMeta?.color,
|
||||
config_schema: configSchema,
|
||||
config_schema_source: nodeType?.config_schema_source,
|
||||
config_stages: nodeType?.config_stages,
|
||||
inputs,
|
||||
outputs,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,722 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { backendClient } from '@/app/infra/http';
|
||||
import { WorkflowExecution } from '@/app/infra/entities/api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
RefreshCw,
|
||||
Play,
|
||||
XCircle,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
Loader2,
|
||||
FileText,
|
||||
RotateCcw,
|
||||
Filter,
|
||||
TrendingUp,
|
||||
Calendar,
|
||||
ChevronDown,
|
||||
ChevronUp
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@/components/ui/tabs';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface WorkflowExecutionsTabProps {
|
||||
workflowId: string;
|
||||
}
|
||||
|
||||
interface ExecutionLog {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
level: string;
|
||||
node_id?: string;
|
||||
message: string;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface WorkflowStats {
|
||||
total_executions: number;
|
||||
successful_executions: number;
|
||||
failed_executions: number;
|
||||
success_rate: number;
|
||||
average_duration_ms: number;
|
||||
last_execution_time?: string;
|
||||
}
|
||||
|
||||
const statusIcons: Record<string, React.ElementType> = {
|
||||
pending: Clock,
|
||||
running: Loader2,
|
||||
completed: CheckCircle2,
|
||||
failed: AlertCircle,
|
||||
cancelled: XCircle,
|
||||
};
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||||
running: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||
completed: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||
failed: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
cancelled: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
|
||||
};
|
||||
|
||||
const logLevelColors: Record<string, string> = {
|
||||
info: 'text-blue-600 dark:text-blue-400',
|
||||
warning: 'text-yellow-600 dark:text-yellow-400',
|
||||
error: 'text-red-600 dark:text-red-400',
|
||||
debug: 'text-gray-600 dark:text-gray-400',
|
||||
};
|
||||
|
||||
export default function WorkflowExecutionsTab({
|
||||
workflowId,
|
||||
}: WorkflowExecutionsTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const [executions, setExecutions] = useState<WorkflowExecution[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedExecution, setSelectedExecution] = useState<WorkflowExecution | null>(null);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
// Filters
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [dateFilter, setDateFilter] = useState<string>('all');
|
||||
|
||||
// Statistics
|
||||
const [stats, setStats] = useState<WorkflowStats | null>(null);
|
||||
const [statsLoading, setStatsLoading] = useState(false);
|
||||
const [showStats, setShowStats] = useState(true);
|
||||
|
||||
// Logs
|
||||
const [executionLogs, setExecutionLogs] = useState<ExecutionLog[]>([]);
|
||||
const [logsLoading, setLogsLoading] = useState(false);
|
||||
const [selectedTab, setSelectedTab] = useState('details');
|
||||
|
||||
// Rerun
|
||||
const [rerunning, setRerunning] = useState<string | null>(null);
|
||||
|
||||
// Load executions
|
||||
const loadExecutions = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await backendClient.getWorkflowExecutions(workflowId, 50, 0);
|
||||
setExecutions(resp.executions || []);
|
||||
setTotal(resp.total ?? resp.executions?.length ?? 0);
|
||||
} catch (err) {
|
||||
// Silently handle connection errors — don't spam console on backend down
|
||||
setExecutions([]);
|
||||
setTotal(0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [workflowId]);
|
||||
|
||||
// Load statistics
|
||||
const loadStats = useCallback(async () => {
|
||||
setStatsLoading(true);
|
||||
try {
|
||||
const resp = await backendClient.getWorkflowStats(workflowId);
|
||||
setStats(resp ?? null);
|
||||
} catch {
|
||||
// Backend might not support stats endpoint yet — just show empty
|
||||
setStats(null);
|
||||
} finally {
|
||||
setStatsLoading(false);
|
||||
}
|
||||
}, [workflowId]);
|
||||
|
||||
// Load execution logs
|
||||
const loadExecutionLogs = useCallback(async (executionUuid: string) => {
|
||||
setLogsLoading(true);
|
||||
try {
|
||||
const resp = await backendClient.getWorkflowExecutionLogs(workflowId, executionUuid, 200, 0);
|
||||
setExecutionLogs(resp.logs);
|
||||
} catch (err) {
|
||||
console.error('Failed to load execution logs:', err);
|
||||
setExecutionLogs([]);
|
||||
} finally {
|
||||
setLogsLoading(false);
|
||||
}
|
||||
}, [workflowId]);
|
||||
|
||||
useEffect(() => {
|
||||
loadExecutions();
|
||||
loadStats();
|
||||
}, [loadExecutions, loadStats]);
|
||||
|
||||
// Filter executions
|
||||
const filteredExecutions = useMemo(() => {
|
||||
let filtered = executions;
|
||||
|
||||
// Status filter
|
||||
if (statusFilter !== 'all') {
|
||||
filtered = filtered.filter(e => e.status === statusFilter);
|
||||
}
|
||||
|
||||
// Date filter
|
||||
if (dateFilter !== 'all') {
|
||||
const now = new Date();
|
||||
let startDate: Date;
|
||||
|
||||
switch (dateFilter) {
|
||||
case 'today':
|
||||
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
break;
|
||||
case 'week':
|
||||
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
break;
|
||||
case 'month':
|
||||
startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
break;
|
||||
default:
|
||||
startDate = new Date(0);
|
||||
}
|
||||
|
||||
filtered = filtered.filter(e => {
|
||||
if (!e.started_at) return false;
|
||||
return new Date(e.started_at) >= startDate;
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [executions, statusFilter, dateFilter]);
|
||||
|
||||
// Manual trigger
|
||||
const handleManualTrigger = useCallback(async () => {
|
||||
try {
|
||||
await backendClient.executeWorkflow(workflowId, {
|
||||
trigger_type: 'manual',
|
||||
});
|
||||
toast.success(t('workflows.manualTrigger') + ' ✓');
|
||||
loadExecutions();
|
||||
loadStats();
|
||||
} catch (err: unknown) {
|
||||
const msg = (err as { msg?: string })?.msg || String(err);
|
||||
toast.error(`${t('workflows.manualTrigger')}: ${msg}`);
|
||||
}
|
||||
}, [workflowId, loadExecutions, loadStats, t]);
|
||||
|
||||
// View execution details
|
||||
const handleViewDetails = useCallback(async (executionUuid: string) => {
|
||||
try {
|
||||
const resp = await backendClient.getWorkflowExecution(workflowId, executionUuid);
|
||||
setSelectedExecution(resp.execution);
|
||||
setSelectedTab('details');
|
||||
loadExecutionLogs(executionUuid);
|
||||
} catch (err: unknown) {
|
||||
const msg = (err as { msg?: string })?.msg || String(err);
|
||||
toast.error(`${t('workflows.executionDetails')}: ${msg}`);
|
||||
}
|
||||
}, [workflowId, loadExecutionLogs, t]);
|
||||
|
||||
// Cancel execution
|
||||
const handleCancel = useCallback(async (executionUuid: string) => {
|
||||
try {
|
||||
await backendClient.cancelWorkflowExecution(workflowId, executionUuid);
|
||||
toast.success(t('common.cancel') + ' ✓');
|
||||
loadExecutions();
|
||||
} catch (err: unknown) {
|
||||
const msg = (err as { msg?: string })?.msg || String(err);
|
||||
toast.error(`${t('common.cancel')}: ${msg}`);
|
||||
}
|
||||
}, [workflowId, loadExecutions, t]);
|
||||
|
||||
// Rerun execution
|
||||
const handleRerun = useCallback(async (executionUuid: string) => {
|
||||
setRerunning(executionUuid);
|
||||
try {
|
||||
await backendClient.rerunWorkflowExecution(workflowId, executionUuid);
|
||||
toast.success(t('workflows.rerun') + ' ✓');
|
||||
loadExecutions();
|
||||
loadStats();
|
||||
} catch (err: unknown) {
|
||||
const msg = (err as { msg?: string })?.msg || String(err);
|
||||
toast.error(`${t('workflows.rerun')}: ${msg}`);
|
||||
} finally {
|
||||
setRerunning(null);
|
||||
}
|
||||
}, [workflowId, loadExecutions, loadStats, t]);
|
||||
|
||||
// Format duration
|
||||
const formatDuration = (seconds: number): string => {
|
||||
if (seconds === null || seconds === undefined || isNaN(seconds)) return '0.0s';
|
||||
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${minutes}m ${secs.toFixed(0)}s`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Statistics Panel */}
|
||||
<Collapsible open={showStats} onOpenChange={setShowStats}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" className="w-full justify-between p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="size-4" />
|
||||
<span className="font-medium">{t('workflows.statistics')}</span>
|
||||
</div>
|
||||
{showStats ? <ChevronUp className="size-4" /> : <ChevronDown className="size-4" />}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
{statsLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="size-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : stats ? (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 p-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{t('workflows.totalExecutions', { count: stats.total_executions ?? 0 })}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.total_executions ?? 0}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t('workflows.successfulCount', { count: stats.successful_executions ?? 0 })}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{t('workflows.successRate')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||
{((stats.success_rate ?? 0) * 100).toFixed(1)}%
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{stats.successful_executions ?? 0} / {stats.total_executions ?? 0}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{t('workflows.averageDuration')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{formatDuration((stats.average_duration_ms ?? 0) / 1000)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t('workflows.perExecution')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{t('workflows.failedExecutions')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600 dark:text-red-400">
|
||||
{stats.failed_executions ?? 0}
|
||||
</div>
|
||||
{stats.last_execution_time && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t('workflows.lastExecution')}: {new Date(stats.last_execution_time).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4 text-sm text-muted-foreground">
|
||||
{t('workflows.noExecutions')}
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* Toolbar with Filters */}
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="size-4 text-muted-foreground" />
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue placeholder={t('workflows.filterByStatus')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t('workflows.allStatuses')}</SelectItem>
|
||||
<SelectItem value="completed">{t('workflows.status.completed')}</SelectItem>
|
||||
<SelectItem value="running">{t('workflows.status.running')}</SelectItem>
|
||||
<SelectItem value="failed">{t('workflows.status.failed')}</SelectItem>
|
||||
<SelectItem value="cancelled">{t('workflows.status.cancelled')}</SelectItem>
|
||||
<SelectItem value="pending">{t('workflows.status.pending')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="size-4 text-muted-foreground" />
|
||||
<Select value={dateFilter} onValueChange={setDateFilter}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue placeholder={t('workflows.filterByDate')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t('workflows.allTime')}</SelectItem>
|
||||
<SelectItem value="today">{t('workflows.today')}</SelectItem>
|
||||
<SelectItem value="week">{t('workflows.lastWeek')}</SelectItem>
|
||||
<SelectItem value="month">{t('workflows.lastMonth')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('workflows.showingExecutions', {
|
||||
shown: filteredExecutions.length,
|
||||
total: total
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => { loadExecutions(); loadStats(); }} disabled={loading}>
|
||||
<RefreshCw className={`size-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleManualTrigger}>
|
||||
<Play className="size-4 mr-2" />
|
||||
{t('workflows.manualTrigger')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Executions Table */}
|
||||
<div className="border rounded-lg">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t('workflows.executionId')}</TableHead>
|
||||
<TableHead>{t('workflows.status')}</TableHead>
|
||||
<TableHead>{t('workflows.triggerType')}</TableHead>
|
||||
<TableHead>{t('workflows.startedAt')}</TableHead>
|
||||
<TableHead>{t('workflows.duration')}</TableHead>
|
||||
<TableHead>{t('common.actions')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredExecutions.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
|
||||
{loading ? t('common.loading') : t('workflows.noExecutions')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredExecutions.map((execution) => {
|
||||
const StatusIcon = statusIcons[execution.status] || Clock;
|
||||
const duration = execution.completed_at && execution.started_at
|
||||
? Math.round((new Date(execution.completed_at).getTime() - new Date(execution.started_at).getTime()) / 1000)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<TableRow key={execution.uuid}>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{execution.uuid.slice(0, 8)}...
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={statusColors[execution.status]}>
|
||||
<StatusIcon className={`size-3 mr-1 ${execution.status === 'running' ? 'animate-spin' : ''}`} />
|
||||
{t(`workflows.status.${execution.status}`)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{execution.trigger_type || '-'}</TableCell>
|
||||
<TableCell>
|
||||
{execution.started_at
|
||||
? new Date(execution.started_at).toLocaleString()
|
||||
: '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{duration !== null ? `${duration}s` : '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleViewDetails(execution.uuid)}
|
||||
>
|
||||
{t('common.details')}
|
||||
</Button>
|
||||
{execution.status === 'running' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleCancel(execution.uuid)}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
{(execution.status === 'completed' || execution.status === 'failed') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRerun(execution.uuid)}
|
||||
disabled={rerunning === execution.uuid}
|
||||
>
|
||||
{rerunning === execution.uuid ? (
|
||||
<Loader2 className="size-3 animate-spin mr-1" />
|
||||
) : (
|
||||
<RotateCcw className="size-3 mr-1" />
|
||||
)}
|
||||
{t('workflows.rerun')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Execution Details Dialog */}
|
||||
<Dialog open={!!selectedExecution} onOpenChange={() => setSelectedExecution(null)}>
|
||||
<DialogContent className="max-w-3xl max-h-[85vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('workflows.executionDetails')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedExecution?.uuid}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedExecution && (
|
||||
<Tabs value={selectedTab} onValueChange={setSelectedTab} className="flex-1 flex flex-col overflow-hidden">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="details">{t('workflows.details')}</TabsTrigger>
|
||||
<TabsTrigger value="nodes">{t('workflows.nodeExecutions')}</TabsTrigger>
|
||||
<TabsTrigger value="logs">
|
||||
<FileText className="size-3 mr-1" />
|
||||
{t('workflows.logs')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="details" className="flex-1 overflow-auto space-y-4 mt-4">
|
||||
{/* Summary */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">{t('workflows.status')}:</span>
|
||||
<Badge className={`ml-2 ${statusColors[selectedExecution.status]}`}>
|
||||
{t(`workflows.status.${selectedExecution.status}`)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">{t('workflows.triggerType')}:</span>
|
||||
<span className="ml-2">{selectedExecution.trigger_type || '-'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">{t('workflows.startedAt')}:</span>
|
||||
<span className="ml-2">
|
||||
{selectedExecution.started_at
|
||||
? new Date(selectedExecution.started_at).toLocaleString()
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">{t('workflows.completedAt')}:</span>
|
||||
<span className="ml-2">
|
||||
{selectedExecution.completed_at
|
||||
? new Date(selectedExecution.completed_at).toLocaleString()
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{selectedExecution.error && (
|
||||
<div className="bg-destructive/10 border border-destructive/20 rounded p-3">
|
||||
<div className="text-sm font-medium text-destructive mb-1">
|
||||
{t('workflows.error')}
|
||||
</div>
|
||||
<div className="text-sm text-destructive/80 font-mono whitespace-pre-wrap">
|
||||
{selectedExecution.error}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result */}
|
||||
{selectedExecution.result && (
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">{t('workflows.result')}</h4>
|
||||
<pre className="bg-muted p-3 rounded text-xs overflow-x-auto max-h-[200px]">
|
||||
{JSON.stringify(selectedExecution.result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rerun button */}
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleRerun(selectedExecution.uuid);
|
||||
setSelectedExecution(null);
|
||||
}}
|
||||
disabled={selectedExecution.status === 'running'}
|
||||
>
|
||||
<RotateCcw className="size-4 mr-2" />
|
||||
{t('workflows.rerunExecution')}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="nodes" className="flex-1 overflow-auto mt-4">
|
||||
{selectedExecution.node_executions && selectedExecution.node_executions.length > 0 ? (
|
||||
<ScrollArea className="h-[400px]">
|
||||
<div className="space-y-2 pr-4">
|
||||
{selectedExecution.node_executions.map((nodeExec) => {
|
||||
const NodeStatusIcon = statusIcons[nodeExec.status] || Clock;
|
||||
const isFailedNode = nodeExec.status === 'failed';
|
||||
return (
|
||||
<div
|
||||
key={nodeExec.node_id}
|
||||
className={`border rounded p-3 text-sm ${
|
||||
isFailedNode
|
||||
? 'border-red-300 bg-red-50/70 dark:border-red-800 dark:bg-red-950/20'
|
||||
: 'border-border'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className={isFailedNode ? 'font-medium text-red-700 dark:text-red-300' : 'font-medium'}>
|
||||
{nodeExec.node_id}
|
||||
</span>
|
||||
<Badge className={statusColors[nodeExec.status]}>
|
||||
<NodeStatusIcon className="size-3 mr-1" />
|
||||
{nodeExec.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div
|
||||
className={`text-xs mt-1 ${
|
||||
isFailedNode
|
||||
? 'text-red-600/80 dark:text-red-400/80'
|
||||
: 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{nodeExec.node_type}
|
||||
</div>
|
||||
{nodeExec.inputs && Object.keys(nodeExec.inputs).length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-xs text-muted-foreground mb-1">{t('workflows.inputs')}:</div>
|
||||
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto max-h-[100px]">
|
||||
{JSON.stringify(nodeExec.inputs, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{nodeExec.outputs && Object.keys(nodeExec.outputs).length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-xs text-muted-foreground mb-1">{t('workflows.outputs')}:</div>
|
||||
<pre className="bg-muted p-2 rounded text-xs overflow-x-auto max-h-[100px]">
|
||||
{JSON.stringify(nodeExec.outputs, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{nodeExec.error && (
|
||||
<div className="text-destructive text-xs mt-2 bg-destructive/10 p-2 rounded">
|
||||
{nodeExec.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{t('workflows.noNodeExecutions')}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="logs" className="flex-1 overflow-hidden mt-4">
|
||||
{logsLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="size-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : executionLogs.length > 0 ? (
|
||||
<ScrollArea className="h-[400px] border rounded">
|
||||
<div className="p-2 space-y-1 font-mono text-xs">
|
||||
{executionLogs.map((log) => (
|
||||
<div
|
||||
key={log.id}
|
||||
className={`flex gap-2 p-1 hover:bg-muted/50 rounded ${logLevelColors[log.level]}`}
|
||||
>
|
||||
<span className="text-muted-foreground shrink-0">
|
||||
{new Date(log.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
<span className="uppercase w-12 shrink-0 font-semibold">
|
||||
[{log.level}]
|
||||
</span>
|
||||
{log.node_id && (
|
||||
<span className="text-muted-foreground shrink-0">
|
||||
[{log.node_id}]
|
||||
</span>
|
||||
)}
|
||||
<span className="flex-1 break-all">{log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{t('workflows.noLogs')}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
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 { Trash2, AlertTriangle } from 'lucide-react';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import EmojiPicker from '@/components/ui/emoji-picker';
|
||||
|
||||
interface WorkflowFormComponentProps {
|
||||
workflow: Workflow | null;
|
||||
onWorkflowChange: (workflow: Workflow) => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export default function WorkflowFormComponent({
|
||||
workflow,
|
||||
onWorkflowChange,
|
||||
onDelete,
|
||||
}: WorkflowFormComponentProps) {
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState(workflow?.name || '');
|
||||
const [description, setDescription] = useState(workflow?.description || '');
|
||||
const [emoji, setEmoji] = useState(workflow?.emoji || '🔄');
|
||||
const [isEnabled, setIsEnabled] = useState(workflow?.is_enabled ?? true);
|
||||
const isSyncingFromProp = useRef(false);
|
||||
|
||||
// Sync with workflow prop
|
||||
useEffect(() => {
|
||||
if (workflow) {
|
||||
isSyncingFromProp.current = true;
|
||||
setName(workflow.name || '');
|
||||
setDescription(workflow.description || '');
|
||||
setEmoji(workflow.emoji || '🔄');
|
||||
setIsEnabled(workflow.is_enabled ?? true);
|
||||
}
|
||||
}, [workflow?.uuid, workflow?.version]);
|
||||
|
||||
// Update parent when values change (skip if the change came from prop sync)
|
||||
useEffect(() => {
|
||||
if (isSyncingFromProp.current) {
|
||||
isSyncingFromProp.current = false;
|
||||
return;
|
||||
}
|
||||
if (workflow) {
|
||||
onWorkflowChange({
|
||||
...workflow,
|
||||
name,
|
||||
description,
|
||||
emoji,
|
||||
is_enabled: isEnabled,
|
||||
});
|
||||
}
|
||||
}, [name, description, emoji, isEnabled]);
|
||||
|
||||
if (!workflow) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
||||
{t('workflows.loading')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl space-y-6">
|
||||
{/* Basic Info Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('workflows.basicInfo')}</CardTitle>
|
||||
<CardDescription>{t('workflows.basicInfoDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Name with Emoji */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="workflow-name">{t('workflows.name')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<EmojiPicker value={emoji} onChange={setEmoji} />
|
||||
<Input
|
||||
id="workflow-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={t('workflows.namePlaceholder')}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="workflow-description">{t('workflows.description')}</Label>
|
||||
<Textarea
|
||||
id="workflow-description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder={t('workflows.descriptionPlaceholder')}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Enabled toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>{t('workflows.enabled')}</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('workflows.enabledDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
onCheckedChange={setIsEnabled}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Workflow Info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('workflows.info')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<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="ml-2">{workflow.version || 1}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">{t('workflows.createdAt')}:</span>
|
||||
<span className="ml-2">
|
||||
{workflow.created_at
|
||||
? new Date(workflow.created_at).toLocaleString()
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">{t('workflows.updatedAt')}:</span>
|
||||
<span className="ml-2">
|
||||
{workflow.updated_at
|
||||
? new Date(workflow.updated_at).toLocaleString()
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<Card className="border-destructive/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-destructive flex items-center gap-2">
|
||||
<AlertTriangle className="size-5" />
|
||||
{t('workflows.dangerZone')}
|
||||
</CardTitle>
|
||||
<CardDescription>{t('workflows.dangerZoneDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive">
|
||||
<Trash2 className="size-4 mr-2" />
|
||||
{t('workflows.deleteWorkflow')}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t('workflows.deleteConfirm')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t('workflows.deleteConfirmDesc', { name: workflow.name })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onDelete}>
|
||||
{t('common.delete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
19
web/src/app/home/workflows/page.tsx
Normal file
19
web/src/app/home/workflows/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import WorkflowDetailContent from './WorkflowDetailContent';
|
||||
|
||||
export default function WorkflowPage() {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const detailId = searchParams.get('id');
|
||||
|
||||
if (detailId) {
|
||||
return <WorkflowDetailContent id={detailId} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground">
|
||||
<p>{t('workflows.selectFromSidebar')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
567
web/src/app/home/workflows/store/useWorkflowStore.ts
Normal file
567
web/src/app/home/workflows/store/useWorkflowStore.ts
Normal file
@@ -0,0 +1,567 @@
|
||||
import { create } from 'zustand';
|
||||
import { Node, Edge, Connection, addEdge, applyNodeChanges, applyEdgeChanges, NodeChange, EdgeChange } from '@xyflow/react';
|
||||
import {
|
||||
Workflow,
|
||||
WorkflowNodeDefinition,
|
||||
WorkflowEdgeDefinition,
|
||||
WorkflowNodeTypeMetadata,
|
||||
WorkflowNodeCategory,
|
||||
} from '@/app/infra/entities/api';
|
||||
import { getNodeTypeLabel as sharedGetNodeTypeLabel } from '../components/workflow-editor/workflow-constants';
|
||||
import { normalizeWorkflowNodeTypeMeta } from '../components/workflow-editor/workflow-node-metadata';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export interface WorkflowNode extends Node {
|
||||
data: {
|
||||
label: string;
|
||||
type: string;
|
||||
config: Record<string, unknown>;
|
||||
inputs?: { name: string; label?: string; type?: string }[];
|
||||
outputs?: { name: string; label?: string; type?: string }[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkflowEdge extends Edge {
|
||||
data?: {
|
||||
label?: string;
|
||||
condition?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Debug related types
|
||||
export type DebugState = 'idle' | 'running' | 'paused' | 'completed' | 'error';
|
||||
|
||||
export interface NodeExecutionResult {
|
||||
nodeId: string;
|
||||
status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
|
||||
inputs?: Record<string, unknown>;
|
||||
outputs?: Record<string, unknown>;
|
||||
error?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface DebugLog {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
level: 'info' | 'warning' | 'error' | 'debug';
|
||||
nodeId?: string;
|
||||
message: string;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface DebugContext {
|
||||
messageContent: string;
|
||||
senderId: string;
|
||||
senderName: string;
|
||||
platform: string;
|
||||
conversationId: string;
|
||||
isGroup: boolean;
|
||||
customVariables: Record<string, unknown>;
|
||||
}
|
||||
|
||||
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;
|
||||
debugExecutionId: string | null;
|
||||
currentNodeId: string | null;
|
||||
nodeExecutionResults: Record<string, NodeExecutionResult>;
|
||||
breakpoints: Record<string, boolean>;
|
||||
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;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
clearNodeExecutionResults: () => void;
|
||||
toggleBreakpoint: (nodeId: string) => void;
|
||||
clearBreakpoints: () => void;
|
||||
addDebugLog: (log: Omit<DebugLog, 'id' | 'timestamp'>) => void;
|
||||
clearDebugLogs: () => void;
|
||||
setDebugContext: (context: Partial<DebugContext>) => void;
|
||||
resetDebugContext: () => void;
|
||||
addWatchedVariable: (variable: string) => void;
|
||||
removeWatchedVariable: (variable: string) => void;
|
||||
clearWatchedVariables: () => void;
|
||||
resetDebugState: () => void;
|
||||
}
|
||||
|
||||
const generateUuidLikeId = () => {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 11)}`;
|
||||
};
|
||||
|
||||
const generateNodeId = () => `node_${generateUuidLikeId()}`;
|
||||
const generateEdgeId = () => `edge_${generateUuidLikeId()}`;
|
||||
|
||||
const defaultDebugContext: DebugContext = {
|
||||
messageContent: '',
|
||||
senderId: `user_${Date.now().toString(36)}`,
|
||||
senderName: '',
|
||||
platform: '',
|
||||
conversationId: `session_${Date.now().toString(36)}`,
|
||||
isGroup: false,
|
||||
customVariables: {},
|
||||
};
|
||||
|
||||
const generateLogId = () => `log_${generateUuidLikeId()}`;
|
||||
|
||||
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
// Initial state
|
||||
currentWorkflow: null,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
nodeTypes: [],
|
||||
nodeCategories: [],
|
||||
selectedNodeId: null,
|
||||
selectedEdgeId: null,
|
||||
isDirty: false,
|
||||
isSaving: false,
|
||||
isLoading: false,
|
||||
history: [],
|
||||
historyIndex: -1,
|
||||
|
||||
// Debug initial state
|
||||
debugMode: false,
|
||||
debugState: 'idle',
|
||||
debugExecutionId: null,
|
||||
currentNodeId: null,
|
||||
nodeExecutionResults: {},
|
||||
breakpoints: {} as Record<string, boolean>,
|
||||
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 }),
|
||||
|
||||
// Node change handlers
|
||||
onNodesChange: (changes) => {
|
||||
set((state) => ({
|
||||
nodes: applyNodeChanges(changes, state.nodes) as WorkflowNode[],
|
||||
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();
|
||||
const nodeType = normalizeWorkflowNodeTypeMeta(
|
||||
type,
|
||||
nodeTypes.find((t) => t.type === type),
|
||||
);
|
||||
|
||||
const getNodeLabel = (
|
||||
nodeT: WorkflowNodeTypeMetadata | undefined,
|
||||
nodeType_str: string,
|
||||
): string => {
|
||||
return sharedGetNodeTypeLabel(nodeType_str, nodeT?.label);
|
||||
};
|
||||
|
||||
const newNode: WorkflowNode = {
|
||||
id: generateNodeId(),
|
||||
type: 'workflowNode',
|
||||
position,
|
||||
data: {
|
||||
label: getNodeLabel(nodeType, type),
|
||||
type,
|
||||
config: {},
|
||||
inputs: (nodeType.inputs || []).map((input) => ({
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
label: input.label,
|
||||
})),
|
||||
outputs: (nodeType.outputs || []).map((output) => ({
|
||||
name: output.name,
|
||||
type: output.type,
|
||||
label: output.label,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
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
|
||||
),
|
||||
isDirty: true,
|
||||
}));
|
||||
},
|
||||
|
||||
// Update node label
|
||||
updateNodeLabel: (nodeId, label) => {
|
||||
set((state) => ({
|
||||
nodes: state.nodes.map((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
|
||||
),
|
||||
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,
|
||||
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
|
||||
),
|
||||
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();
|
||||
if (historyIndex > 0) {
|
||||
const newIndex = historyIndex - 1;
|
||||
const { nodes, edges } = history[newIndex];
|
||||
set({ nodes, edges, historyIndex: newIndex, isDirty: true });
|
||||
}
|
||||
},
|
||||
|
||||
// Redo
|
||||
redo: () => {
|
||||
const { history, historyIndex } = get();
|
||||
if (historyIndex < history.length - 1) {
|
||||
const newIndex = historyIndex + 1;
|
||||
const { nodes, edges } = history[newIndex];
|
||||
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,
|
||||
position: node.position,
|
||||
config: node.data.config,
|
||||
label: node.data.label,
|
||||
inputs: node.data.inputs,
|
||||
outputs: node.data.outputs,
|
||||
}));
|
||||
|
||||
const workflowEdges: WorkflowEdgeDefinition[] = edges.map((edge) => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
source_port: edge.sourceHandle || undefined,
|
||||
target_port: edge.targetHandle || undefined,
|
||||
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) => ({
|
||||
id: node.id,
|
||||
type: 'workflowNode',
|
||||
position: node.position,
|
||||
data: {
|
||||
label: node.label || node.type,
|
||||
type: node.type,
|
||||
config: node.config,
|
||||
inputs: node.inputs,
|
||||
outputs: node.outputs,
|
||||
},
|
||||
}));
|
||||
|
||||
const edges: WorkflowEdge[] = apiEdges.map((edge) => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
sourceHandle: edge.source_port,
|
||||
targetHandle: edge.target_port,
|
||||
type: 'smoothstep',
|
||||
data: {
|
||||
label: edge.label,
|
||||
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: [],
|
||||
}),
|
||||
|
||||
// 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' }),
|
||||
...result,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
clearNodeExecutionResults: () => set({ nodeExecutionResults: {} }),
|
||||
|
||||
toggleBreakpoint: (nodeId) => {
|
||||
set((state) => {
|
||||
const newBreakpoints = { ...state.breakpoints };
|
||||
if (newBreakpoints[nodeId]) {
|
||||
delete newBreakpoints[nodeId];
|
||||
} else {
|
||||
newBreakpoints[nodeId] = true;
|
||||
}
|
||||
return { breakpoints: newBreakpoints };
|
||||
});
|
||||
},
|
||||
|
||||
clearBreakpoints: () => set({ breakpoints: {} as Record<string, boolean> }),
|
||||
|
||||
addDebugLog: (log) => {
|
||||
set((state) => ({
|
||||
debugLogs: [
|
||||
...state.debugLogs,
|
||||
{
|
||||
...log,
|
||||
id: generateLogId(),
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
].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: {},
|
||||
},
|
||||
}),
|
||||
|
||||
addWatchedVariable: (variable) => {
|
||||
set((state) => ({
|
||||
watchedVariables: state.watchedVariables.includes(variable)
|
||||
? state.watchedVariables
|
||||
: [...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: [],
|
||||
}),
|
||||
}));
|
||||
@@ -186,6 +186,9 @@ export interface Bot {
|
||||
use_pipeline_name?: string;
|
||||
use_pipeline_uuid?: string;
|
||||
pipeline_routing_rules?: PipelineRoutingRule[];
|
||||
// New unified binding fields
|
||||
binding_type?: 'pipeline' | 'workflow';
|
||||
binding_uuid?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
adapter_runtime_values?: object;
|
||||
@@ -545,3 +548,176 @@ export interface ApiRespTools {
|
||||
export interface ApiRespToolDetail {
|
||||
tool: PluginTool;
|
||||
}
|
||||
|
||||
// ============ Workflow Types ============
|
||||
export interface WorkflowPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface WorkflowPortDefinition {
|
||||
name: string;
|
||||
label?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowNodeDefinition {
|
||||
id: string;
|
||||
type: string;
|
||||
position: WorkflowPosition;
|
||||
config: Record<string, unknown>;
|
||||
label?: string;
|
||||
inputs?: WorkflowPortDefinition[];
|
||||
outputs?: WorkflowPortDefinition[];
|
||||
}
|
||||
|
||||
export interface WorkflowEdgeDefinition {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
source_port?: string;
|
||||
target_port?: string;
|
||||
label?: string;
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowTriggerDefinition {
|
||||
type: string;
|
||||
config: Record<string, unknown>;
|
||||
enabled: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowSettings {
|
||||
max_execution_time?: number;
|
||||
retry_policy?: Record<string, unknown>;
|
||||
error_handling?: Record<string, unknown>;
|
||||
logging_level?: string;
|
||||
}
|
||||
|
||||
export interface Workflow {
|
||||
uuid?: string;
|
||||
name: string;
|
||||
emoji?: string;
|
||||
description?: string;
|
||||
version?: number;
|
||||
nodes: WorkflowNodeDefinition[];
|
||||
edges: WorkflowEdgeDefinition[];
|
||||
variables?: Record<string, unknown>;
|
||||
settings?: WorkflowSettings;
|
||||
triggers?: WorkflowTriggerDefinition[];
|
||||
is_enabled?: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ApiRespWorkflows {
|
||||
workflows: Workflow[];
|
||||
}
|
||||
|
||||
export interface ApiRespWorkflow {
|
||||
workflow: Workflow;
|
||||
}
|
||||
|
||||
export interface WorkflowNodeTypeMetadata {
|
||||
type: string;
|
||||
category: string;
|
||||
label: Record<string, string>;
|
||||
description?: Record<string, string>;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
config_schema: unknown[];
|
||||
// Pipeline metadata reuse
|
||||
config_schema_source?: string; // e.g. 'pipeline:ai', 'pipeline:trigger'
|
||||
config_stages?: string[]; // specific stages to include from pipeline config
|
||||
inputs?: WorkflowPortDefinition[];
|
||||
outputs?: WorkflowPortDefinition[];
|
||||
}
|
||||
|
||||
export interface WorkflowNodeCategory {
|
||||
name: string;
|
||||
label: Record<string, string>;
|
||||
icon?: string;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
export interface ApiRespWorkflowNodeTypes {
|
||||
node_types: WorkflowNodeTypeMetadata[];
|
||||
categories: WorkflowNodeCategory[];
|
||||
}
|
||||
|
||||
export interface WorkflowExecutionNodeInfo {
|
||||
node_id: string;
|
||||
node_type: string;
|
||||
status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
|
||||
started_at?: string;
|
||||
completed_at?: string;
|
||||
inputs?: Record<string, unknown>;
|
||||
outputs?: Record<string, unknown>;
|
||||
error?: string;
|
||||
retry_count?: number;
|
||||
}
|
||||
|
||||
export interface WorkflowExecution {
|
||||
uuid: string;
|
||||
workflow_uuid: string;
|
||||
workflow_version: number;
|
||||
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
started_at?: string;
|
||||
completed_at?: string;
|
||||
trigger_type?: string;
|
||||
trigger_data?: Record<string, unknown>;
|
||||
variables?: Record<string, unknown>;
|
||||
result?: Record<string, unknown>;
|
||||
error?: string;
|
||||
node_executions?: WorkflowExecutionNodeInfo[];
|
||||
}
|
||||
|
||||
export interface ApiRespWorkflowExecutions {
|
||||
executions: WorkflowExecution[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ApiRespWorkflowExecution {
|
||||
execution: WorkflowExecution;
|
||||
}
|
||||
|
||||
export interface WorkflowVersion {
|
||||
version: number;
|
||||
name: string;
|
||||
created_at: string;
|
||||
definition_snapshot: Workflow;
|
||||
}
|
||||
|
||||
export interface ApiRespWorkflowVersions {
|
||||
versions: WorkflowVersion[];
|
||||
}
|
||||
|
||||
export interface CreateWorkflowRequest {
|
||||
name: string;
|
||||
emoji?: string;
|
||||
description?: string;
|
||||
nodes?: WorkflowNodeDefinition[];
|
||||
edges?: WorkflowEdgeDefinition[];
|
||||
variables?: Record<string, unknown>;
|
||||
settings?: WorkflowSettings;
|
||||
triggers?: WorkflowTriggerDefinition[];
|
||||
}
|
||||
|
||||
export interface UpdateWorkflowRequest {
|
||||
name?: string;
|
||||
emoji?: string;
|
||||
description?: string;
|
||||
nodes?: WorkflowNodeDefinition[];
|
||||
edges?: WorkflowEdgeDefinition[];
|
||||
variables?: Record<string, unknown>;
|
||||
settings?: WorkflowSettings;
|
||||
triggers?: WorkflowTriggerDefinition[];
|
||||
is_enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ExecuteWorkflowRequest {
|
||||
trigger_type?: string;
|
||||
trigger_data?: Record<string, unknown>;
|
||||
variables?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface IDynamicFormItemSchema {
|
||||
name: string;
|
||||
required: boolean;
|
||||
type: DynamicFormItemType;
|
||||
description?: I18nObject;
|
||||
description?: I18nObject | string;
|
||||
options?: IDynamicFormItemOption[];
|
||||
show_if?: IShowIfCondition;
|
||||
|
||||
@@ -28,6 +28,7 @@ export enum DynamicFormItemType {
|
||||
FLOAT = 'float',
|
||||
BOOLEAN = 'boolean',
|
||||
STRING = 'string',
|
||||
SECRET = 'secret',
|
||||
TEXT = 'text',
|
||||
STRING_ARRAY = 'array[string]',
|
||||
FILE = 'file',
|
||||
|
||||
216
web/src/app/infra/entities/workflow/index.ts
Normal file
216
web/src/app/infra/entities/workflow/index.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { I18nObject } from '@/app/infra/entities/common';
|
||||
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||
|
||||
// 节点位置
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
// 端口定义
|
||||
export interface PortDefinition {
|
||||
name: string;
|
||||
type: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
// 节点定义
|
||||
export interface NodeDefinition {
|
||||
id: string;
|
||||
type: string;
|
||||
name: string;
|
||||
position: Position;
|
||||
config: Record<string, unknown>;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// 边定义
|
||||
export interface EdgeDefinition {
|
||||
id: string;
|
||||
source: string;
|
||||
sourceHandle?: string;
|
||||
target: string;
|
||||
targetHandle?: string;
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
// 触发器定义
|
||||
export interface TriggerDefinition {
|
||||
id: string;
|
||||
type: string;
|
||||
config: Record<string, unknown>;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
// 工作流设置
|
||||
export interface WorkflowSettings {
|
||||
timeout?: number;
|
||||
max_retries?: number;
|
||||
retry_delay?: number;
|
||||
error_handling?: 'stop' | 'continue' | 'retry';
|
||||
}
|
||||
|
||||
// 工作流定义
|
||||
export interface WorkflowDefinition {
|
||||
uuid: string;
|
||||
name: string;
|
||||
emoji?: string;
|
||||
description?: string;
|
||||
version: number;
|
||||
nodes: NodeDefinition[];
|
||||
edges: EdgeDefinition[];
|
||||
variables?: Record<string, unknown>;
|
||||
settings?: WorkflowSettings;
|
||||
triggers?: TriggerDefinition[];
|
||||
is_enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 节点类型元数据
|
||||
export interface NodeTypeMetadata {
|
||||
type: string;
|
||||
name: I18nObject;
|
||||
description?: I18nObject;
|
||||
category: NodeCategory;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
inputs: PortDefinition[];
|
||||
outputs: PortDefinition[];
|
||||
config_schema: IDynamicFormItemSchema[];
|
||||
}
|
||||
|
||||
// 节点类别
|
||||
export type NodeCategory = 'trigger' | 'process' | 'control' | 'action' | 'integration';
|
||||
|
||||
// 节点类别信息
|
||||
export interface NodeCategoryInfo {
|
||||
id: NodeCategory;
|
||||
name: I18nObject;
|
||||
icon: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
// 工作流执行状态
|
||||
export type ExecutionStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
|
||||
// 工作流执行记录
|
||||
export interface WorkflowExecution {
|
||||
id: string;
|
||||
workflow_uuid: string;
|
||||
workflow_version: number;
|
||||
status: ExecutionStatus;
|
||||
trigger_type?: string;
|
||||
trigger_data?: Record<string, unknown>;
|
||||
variables?: Record<string, unknown>;
|
||||
start_time?: string;
|
||||
end_time?: string;
|
||||
error?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 节点执行记录
|
||||
export interface NodeExecution {
|
||||
id: number;
|
||||
execution_id: string;
|
||||
node_id: string;
|
||||
node_type: string;
|
||||
status: ExecutionStatus;
|
||||
inputs?: Record<string, unknown>;
|
||||
outputs?: Record<string, unknown>;
|
||||
start_time?: string;
|
||||
end_time?: string;
|
||||
error?: string;
|
||||
retry_count: number;
|
||||
}
|
||||
|
||||
// API 响应类型
|
||||
export interface ApiRespWorkflows {
|
||||
workflows: WorkflowDefinition[];
|
||||
}
|
||||
|
||||
export interface ApiRespWorkflow {
|
||||
workflow: WorkflowDefinition;
|
||||
}
|
||||
|
||||
export interface ApiRespNodeTypes {
|
||||
node_types: NodeTypeMetadata[];
|
||||
categories: NodeCategoryInfo[];
|
||||
}
|
||||
|
||||
export interface ApiRespWorkflowExecutions {
|
||||
executions: WorkflowExecution[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ApiRespWorkflowExecution {
|
||||
execution: WorkflowExecution;
|
||||
node_executions: NodeExecution[];
|
||||
}
|
||||
|
||||
// 创建工作流请求
|
||||
export interface CreateWorkflowRequest {
|
||||
name: string;
|
||||
emoji?: string;
|
||||
description?: string;
|
||||
nodes?: NodeDefinition[];
|
||||
edges?: EdgeDefinition[];
|
||||
variables?: Record<string, unknown>;
|
||||
settings?: WorkflowSettings;
|
||||
triggers?: TriggerDefinition[];
|
||||
}
|
||||
|
||||
// 更新工作流请求
|
||||
export interface UpdateWorkflowRequest {
|
||||
name?: string;
|
||||
emoji?: string;
|
||||
description?: string;
|
||||
nodes?: NodeDefinition[];
|
||||
edges?: EdgeDefinition[];
|
||||
variables?: Record<string, unknown>;
|
||||
settings?: WorkflowSettings;
|
||||
triggers?: TriggerDefinition[];
|
||||
is_enabled?: boolean;
|
||||
}
|
||||
|
||||
// 执行工作流请求
|
||||
export interface ExecuteWorkflowRequest {
|
||||
trigger_type?: string;
|
||||
trigger_data?: Record<string, unknown>;
|
||||
variables?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// 节点类别配置
|
||||
export const NODE_CATEGORIES: NodeCategoryInfo[] = [
|
||||
{
|
||||
id: 'trigger',
|
||||
name: { en_US: 'Trigger', zh_Hans: '触发' },
|
||||
icon: 'Zap',
|
||||
color: '#f59e0b',
|
||||
},
|
||||
{
|
||||
id: 'process',
|
||||
name: { en_US: 'Process', zh_Hans: '处理' },
|
||||
icon: 'Cpu',
|
||||
color: '#3b82f6',
|
||||
},
|
||||
{
|
||||
id: 'control',
|
||||
name: { en_US: 'Control', zh_Hans: '控制' },
|
||||
icon: 'GitBranch',
|
||||
color: '#8b5cf6',
|
||||
},
|
||||
{
|
||||
id: 'action',
|
||||
name: { en_US: 'Action', zh_Hans: '动作' },
|
||||
icon: 'Play',
|
||||
color: '#10b981',
|
||||
},
|
||||
{
|
||||
id: 'integration',
|
||||
name: { en_US: 'Integration', zh_Hans: '集成' },
|
||||
icon: 'Plug',
|
||||
color: '#ec4899',
|
||||
},
|
||||
];
|
||||
@@ -47,6 +47,16 @@ import {
|
||||
RagMigrationStatusResp,
|
||||
ApiRespTools,
|
||||
ApiRespToolDetail,
|
||||
ApiRespWorkflows,
|
||||
ApiRespWorkflow,
|
||||
ApiRespWorkflowNodeTypes,
|
||||
ApiRespWorkflowExecutions,
|
||||
ApiRespWorkflowExecution,
|
||||
ApiRespWorkflowVersions,
|
||||
Workflow,
|
||||
CreateWorkflowRequest,
|
||||
UpdateWorkflowRequest,
|
||||
ExecuteWorkflowRequest,
|
||||
} from '@/app/infra/entities/api';
|
||||
import { Plugin } from '@/app/infra/entities/plugin';
|
||||
import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest';
|
||||
@@ -1109,6 +1119,258 @@ export class BackendClient extends BaseHttpClient {
|
||||
public dismissSurvey(surveyId: string): Promise<object> {
|
||||
return this.post('/api/v1/survey/dismiss', { survey_id: surveyId });
|
||||
}
|
||||
|
||||
// ============ Workflow API ============
|
||||
public getWorkflows(
|
||||
sortBy?: string,
|
||||
sortOrder?: string,
|
||||
): Promise<ApiRespWorkflows> {
|
||||
const params = new URLSearchParams();
|
||||
if (sortBy) params.append('sort_by', sortBy);
|
||||
if (sortOrder) params.append('sort_order', sortOrder);
|
||||
const queryString = params.toString();
|
||||
return this.get(`/api/v1/workflows${queryString ? `?${queryString}` : ''}`);
|
||||
}
|
||||
|
||||
public getWorkflow(uuid: string): Promise<ApiRespWorkflow> {
|
||||
return this.get(`/api/v1/workflows/${uuid}`);
|
||||
}
|
||||
|
||||
public createWorkflow(
|
||||
workflow: CreateWorkflowRequest,
|
||||
): Promise<{ uuid: string }> {
|
||||
return this.post('/api/v1/workflows', workflow);
|
||||
}
|
||||
|
||||
public updateWorkflow(
|
||||
uuid: string,
|
||||
workflow: UpdateWorkflowRequest,
|
||||
): Promise<object> {
|
||||
return this.put(`/api/v1/workflows/${uuid}`, workflow);
|
||||
}
|
||||
|
||||
public deleteWorkflow(uuid: string): Promise<object> {
|
||||
return this.delete(`/api/v1/workflows/${uuid}`);
|
||||
}
|
||||
|
||||
public copyWorkflow(uuid: string): Promise<{ uuid: string }> {
|
||||
return this.post(`/api/v1/workflows/${uuid}/copy`);
|
||||
}
|
||||
|
||||
public publishWorkflow(uuid: string): Promise<{ version: number }> {
|
||||
return this.post(`/api/v1/workflows/${uuid}/publish`);
|
||||
}
|
||||
|
||||
public getWorkflowVersions(uuid: string): Promise<ApiRespWorkflowVersions> {
|
||||
return this.get(`/api/v1/workflows/${uuid}/versions`);
|
||||
}
|
||||
|
||||
public rollbackWorkflow(
|
||||
uuid: string,
|
||||
version: number,
|
||||
): Promise<{ version: number }> {
|
||||
return this.post(`/api/v1/workflows/${uuid}/rollback`, { version });
|
||||
}
|
||||
|
||||
public executeWorkflow(
|
||||
uuid: string,
|
||||
request: ExecuteWorkflowRequest,
|
||||
): Promise<{ execution_id: string }> {
|
||||
return this.post(`/api/v1/workflows/${uuid}/execute`, request);
|
||||
}
|
||||
|
||||
public getWorkflowExecutions(
|
||||
uuid: string,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
): Promise<ApiRespWorkflowExecutions> {
|
||||
const params = new URLSearchParams();
|
||||
if (limit) params.append('limit', limit.toString());
|
||||
if (offset) params.append('offset', offset.toString());
|
||||
const queryString = params.toString();
|
||||
return this.get(
|
||||
`/api/v1/workflows/${uuid}/executions${queryString ? `?${queryString}` : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
public getWorkflowExecution(
|
||||
workflowUuid: string,
|
||||
executionUuid: string,
|
||||
): Promise<ApiRespWorkflowExecution> {
|
||||
const url = `/api/v1/workflows/${workflowUuid}/executions/${executionUuid}`;
|
||||
console.debug('[WorkflowExecution] requesting execution detail', {
|
||||
url,
|
||||
workflowUuid,
|
||||
executionUuid,
|
||||
baseURL: this.baseURL,
|
||||
stack: new Error().stack,
|
||||
});
|
||||
return this.get(url);
|
||||
}
|
||||
|
||||
public cancelWorkflowExecution(
|
||||
workflowUuid: string,
|
||||
executionUuid: string,
|
||||
): Promise<object> {
|
||||
return this.post(`/api/v1/executions/${executionUuid}/cancel`);
|
||||
}
|
||||
|
||||
public getWorkflowNodeTypes(): Promise<ApiRespWorkflowNodeTypes> {
|
||||
return this.get('/api/v1/workflows/_/node-types');
|
||||
}
|
||||
|
||||
public toggleWorkflow(uuid: string, enabled: boolean): Promise<object> {
|
||||
return this.put(`/api/v1/workflows/${uuid}`, { is_enabled: enabled });
|
||||
}
|
||||
|
||||
// ============ Workflow WebSocket Chat API ============
|
||||
public getWorkflowWebSocketHistoryMessages(
|
||||
workflowId: string,
|
||||
sessionType: string,
|
||||
): Promise<ApiRespWebChatMessages> {
|
||||
return this.get(
|
||||
`/api/v1/workflows/${workflowId}/ws/messages/${sessionType}`,
|
||||
);
|
||||
}
|
||||
|
||||
public async uploadWorkflowWebSocketImage(
|
||||
workflowId: string,
|
||||
imageFile: File,
|
||||
): Promise<{ file_key: string }> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', imageFile);
|
||||
|
||||
return this.postFile(`/api/v1/files/images`, formData);
|
||||
}
|
||||
|
||||
public resetWorkflowWebSocketSession(
|
||||
workflowId: string,
|
||||
sessionType: string,
|
||||
): Promise<{ message: string }> {
|
||||
return this.post(`/api/v1/workflows/${workflowId}/ws/reset/${sessionType}`);
|
||||
}
|
||||
|
||||
// ============ Workflow Debug API ============
|
||||
public startWorkflowDebug(
|
||||
uuid: string,
|
||||
options: {
|
||||
context?: {
|
||||
message_content?: string;
|
||||
sender_id?: string;
|
||||
sender_name?: string;
|
||||
platform?: string;
|
||||
conversation_id?: string;
|
||||
is_group?: boolean;
|
||||
};
|
||||
variables?: Record<string, unknown>;
|
||||
breakpoints?: string[];
|
||||
},
|
||||
): Promise<{ execution_id: string }> {
|
||||
return this.post(`/api/v1/workflows/${uuid}/debug/start`, options);
|
||||
}
|
||||
|
||||
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> {
|
||||
return this.post(`/api/v1/workflows/${uuid}/debug/${executionId}/resume`);
|
||||
}
|
||||
|
||||
public stepWorkflowDebug(
|
||||
uuid: string,
|
||||
executionId: string,
|
||||
): Promise<{
|
||||
node_id?: string;
|
||||
node_state?: {
|
||||
status: string;
|
||||
inputs?: Record<string, unknown>;
|
||||
outputs?: Record<string, unknown>;
|
||||
error?: string;
|
||||
};
|
||||
completed: boolean;
|
||||
}> {
|
||||
return this.post(`/api/v1/workflows/${uuid}/debug/${executionId}/step`);
|
||||
}
|
||||
|
||||
public stopWorkflowDebug(uuid: string, executionId: string): Promise<object> {
|
||||
return this.post(`/api/v1/workflows/${uuid}/debug/${executionId}/stop`);
|
||||
}
|
||||
|
||||
public getWorkflowDebugState(
|
||||
uuid: string,
|
||||
executionId: string,
|
||||
): Promise<{
|
||||
status: string;
|
||||
current_node_id?: string;
|
||||
node_states?: Record<
|
||||
string,
|
||||
{
|
||||
status: string;
|
||||
inputs?: Record<string, unknown>;
|
||||
outputs?: Record<string, unknown>;
|
||||
error?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
duration?: number;
|
||||
}
|
||||
>;
|
||||
new_logs?: Array<{
|
||||
level: 'info' | 'warning' | 'error' | 'debug';
|
||||
nodeId?: string;
|
||||
message: string;
|
||||
data?: Record<string, unknown>;
|
||||
}>;
|
||||
error?: string;
|
||||
}> {
|
||||
return this.get(`/api/v1/workflows/${uuid}/debug/${executionId}/state`);
|
||||
}
|
||||
|
||||
// ============ Workflow Execution Logs API ============
|
||||
public getWorkflowExecutionLogs(
|
||||
workflowUuid: string,
|
||||
executionUuid: string,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
): Promise<{
|
||||
logs: Array<{
|
||||
id: string;
|
||||
timestamp: string;
|
||||
level: string;
|
||||
node_id?: string;
|
||||
message: string;
|
||||
data?: Record<string, unknown>;
|
||||
}>;
|
||||
total: number;
|
||||
}> {
|
||||
const params = new URLSearchParams();
|
||||
if (limit) params.append('limit', limit.toString());
|
||||
if (offset) params.append('offset', offset.toString());
|
||||
const queryString = params.toString();
|
||||
return this.get(
|
||||
`/api/v1/workflows/${workflowUuid}/executions/${executionUuid}/logs${queryString ? `?${queryString}` : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
public getWorkflowStats(uuid: string): Promise<{
|
||||
total_executions: number;
|
||||
successful_executions: number;
|
||||
failed_executions: number;
|
||||
success_rate: number;
|
||||
average_duration_ms: number;
|
||||
last_execution_time?: string;
|
||||
}> {
|
||||
return this.get(`/api/v1/workflows/${uuid}/stats`);
|
||||
}
|
||||
|
||||
public rerunWorkflowExecution(
|
||||
workflowUuid: string,
|
||||
executionUuid: string,
|
||||
): Promise<{ execution_uuid: string }> {
|
||||
return this.post(
|
||||
`/api/v1/workflows/${workflowUuid}/executions/${executionUuid}/rerun`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface SurveyQuestion {
|
||||
|
||||
328
web/src/app/infra/websocket/WorkflowWebSocketClient.ts
Normal file
328
web/src/app/infra/websocket/WorkflowWebSocketClient.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* Workflow WebSocket客户端类
|
||||
* 用于管理工作流调试的WebSocket连接和消息处理
|
||||
*/
|
||||
export interface WorkflowWebSocketMessage {
|
||||
id: number;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
message_chain: Array<{ type: string; text?: string; target?: string }>;
|
||||
timestamp: string;
|
||||
is_final?: boolean;
|
||||
connection_id?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowWebSocketResponse {
|
||||
type:
|
||||
| 'connected'
|
||||
| 'response'
|
||||
| 'user_message'
|
||||
| 'pong'
|
||||
| 'broadcast'
|
||||
| 'error';
|
||||
connection_id?: string;
|
||||
workflow_uuid?: string;
|
||||
session_type?: string;
|
||||
timestamp?: string;
|
||||
data?: WorkflowWebSocketMessage;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export class WorkflowWebSocketClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private connectionId: string | null = null;
|
||||
private reconnectAttempts = 0;
|
||||
private maxReconnectAttempts = 5;
|
||||
private reconnectDelay = 3000;
|
||||
private heartbeatInterval: NodeJS.Timeout | null = null;
|
||||
private heartbeatIntervalMs = 30000;
|
||||
private reconnectTimer: NodeJS.Timeout | null = null;
|
||||
private isConnecting = false;
|
||||
private shouldReconnect = true;
|
||||
|
||||
private onConnectedCallback?: (data: WorkflowWebSocketResponse) => void;
|
||||
private onMessageCallback?: (data: WorkflowWebSocketMessage) => void;
|
||||
private onErrorCallback?: (error: Error) => void;
|
||||
private onCloseCallback?: () => void;
|
||||
private onBroadcastCallback?: (message: string) => void;
|
||||
|
||||
constructor(
|
||||
private workflowId: string,
|
||||
private sessionType: 'person' | 'group' = 'person',
|
||||
private token?: string,
|
||||
) {}
|
||||
|
||||
public connect(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if (
|
||||
this.isConnecting ||
|
||||
(this.ws && this.ws.readyState === WebSocket.CONNECTING)
|
||||
) {
|
||||
console.warn('WebSocket正在连接中,忽略重复连接请求');
|
||||
reject(new Error('Connection already in progress'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
console.warn('WebSocket已连接,忽略重复连接请求');
|
||||
resolve(this.connectionId || '');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isConnecting = true;
|
||||
this.shouldReconnect = true;
|
||||
this.clearReconnectTimer();
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host =
|
||||
import.meta.env.VITE_API_BASE_URL?.split('://')[1] ||
|
||||
window.location.host;
|
||||
const url = `${protocol}//${host}/api/v1/workflows/${this.workflowId}/ws/connect?session_type=${this.sessionType}`;
|
||||
|
||||
console.debug('[WorkflowWebSocket] connect:start', {
|
||||
workflowId: this.workflowId,
|
||||
sessionType: this.sessionType,
|
||||
url,
|
||||
reconnectAttempts: this.reconnectAttempts,
|
||||
maxReconnectAttempts: this.maxReconnectAttempts,
|
||||
readyState: this.ws?.readyState ?? null,
|
||||
locationHost: window.location.host,
|
||||
envBaseUrl: import.meta.env.VITE_API_BASE_URL,
|
||||
});
|
||||
|
||||
this.ws = new WebSocket(url);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.debug('[WorkflowWebSocket] connect:open', {
|
||||
workflowId: this.workflowId,
|
||||
sessionType: this.sessionType,
|
||||
url,
|
||||
reconnectAttempts: this.reconnectAttempts,
|
||||
readyState: this.ws?.readyState ?? null,
|
||||
});
|
||||
this.reconnectAttempts = 0;
|
||||
this.isConnecting = false;
|
||||
this.clearReconnectTimer();
|
||||
this.startHeartbeat();
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data: WorkflowWebSocketResponse = JSON.parse(event.data);
|
||||
this.handleMessage(data);
|
||||
|
||||
if (data.type === 'connected' && data.connection_id) {
|
||||
this.connectionId = data.connection_id;
|
||||
resolve(data.connection_id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析WebSocket消息失败:', error);
|
||||
this.onErrorCallback?.(error as Error);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
console.warn('[WorkflowWebSocket] connect:close', {
|
||||
workflowId: this.workflowId,
|
||||
sessionType: this.sessionType,
|
||||
url,
|
||||
code: event.code,
|
||||
reason: event.reason,
|
||||
wasClean: event.wasClean,
|
||||
reconnectAttempts: this.reconnectAttempts,
|
||||
maxReconnectAttempts: this.maxReconnectAttempts,
|
||||
});
|
||||
this.isConnecting = false;
|
||||
this.stopHeartbeat();
|
||||
this.onCloseCallback?.();
|
||||
|
||||
if (
|
||||
this.shouldReconnect &&
|
||||
this.reconnectAttempts < this.maxReconnectAttempts
|
||||
) {
|
||||
this.reconnectAttempts++;
|
||||
console.debug('[WorkflowWebSocket] connect:retry-scheduled', {
|
||||
workflowId: this.workflowId,
|
||||
sessionType: this.sessionType,
|
||||
url,
|
||||
reconnectAttempts: this.reconnectAttempts,
|
||||
delayMs: this.reconnectDelay * this.reconnectAttempts,
|
||||
});
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.connect().catch(console.error);
|
||||
}, this.reconnectDelay * this.reconnectAttempts);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (event) => {
|
||||
console.error('[WorkflowWebSocket] connect:error', {
|
||||
workflowId: this.workflowId,
|
||||
sessionType: this.sessionType,
|
||||
url,
|
||||
reconnectAttempts: this.reconnectAttempts,
|
||||
readyState: this.ws?.readyState ?? null,
|
||||
event,
|
||||
});
|
||||
this.isConnecting = false;
|
||||
const error = new Error('WebSocket连接失败');
|
||||
this.onErrorCallback?.(error);
|
||||
reject(error);
|
||||
};
|
||||
} catch (error) {
|
||||
this.isConnecting = false;
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleMessage(data: WorkflowWebSocketResponse) {
|
||||
switch (data.type) {
|
||||
case 'connected':
|
||||
this.onConnectedCallback?.(data);
|
||||
break;
|
||||
|
||||
case 'response':
|
||||
if (!data.session_type || data.session_type !== this.sessionType) {
|
||||
console.debug(
|
||||
`忽略不匹配的消息: 当前session=${this.sessionType}, 消息session=${data.session_type}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (data.data) {
|
||||
this.onMessageCallback?.(data.data);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'user_message':
|
||||
if (!data.session_type || data.session_type !== this.sessionType) {
|
||||
console.debug(
|
||||
`忽略不匹配的用户消息: 当前session=${this.sessionType}, 消息session=${data.session_type}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (data.data) {
|
||||
this.onMessageCallback?.(data.data);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'pong':
|
||||
break;
|
||||
|
||||
case 'broadcast':
|
||||
if (data.message) {
|
||||
this.onBroadcastCallback?.(data.message);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
this.shouldReconnect = false;
|
||||
this.clearReconnectTimer();
|
||||
this.stopHeartbeat();
|
||||
const error = new Error(data.message || '未知错误');
|
||||
this.onErrorCallback?.(error);
|
||||
this.ws?.close(1000, 'workflow-error');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('未知消息类型:', data);
|
||||
}
|
||||
}
|
||||
|
||||
public sendMessage(
|
||||
messageChain: Array<{ type: string; text?: string; target?: string }>,
|
||||
stream: boolean = true,
|
||||
) {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
throw new Error('WebSocket未连接');
|
||||
}
|
||||
|
||||
const message = {
|
||||
type: 'message',
|
||||
message: messageChain,
|
||||
stream: stream,
|
||||
};
|
||||
|
||||
this.ws.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
private sendHeartbeat() {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
this.ws.send(JSON.stringify({ type: 'ping' }));
|
||||
}
|
||||
|
||||
private startHeartbeat() {
|
||||
this.stopHeartbeat();
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
this.sendHeartbeat();
|
||||
}, this.heartbeatIntervalMs);
|
||||
}
|
||||
|
||||
private stopHeartbeat() {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
private clearReconnectTimer() {
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
this.shouldReconnect = false;
|
||||
this.clearReconnectTimer();
|
||||
|
||||
if (this.ws) {
|
||||
this.stopHeartbeat();
|
||||
this.reconnectAttempts = this.maxReconnectAttempts;
|
||||
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify({ type: 'disconnect' }));
|
||||
}
|
||||
|
||||
this.ws.close(1000, 'client-disconnect');
|
||||
this.ws = null;
|
||||
this.connectionId = null;
|
||||
this.isConnecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
public getConnectionId(): string | null {
|
||||
return this.connectionId;
|
||||
}
|
||||
|
||||
public isConnected(): boolean {
|
||||
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
public onConnected(callback: (data: WorkflowWebSocketResponse) => void) {
|
||||
this.onConnectedCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public onMessage(callback: (data: WorkflowWebSocketMessage) => void) {
|
||||
this.onMessageCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public onError(callback: (error: Error) => void) {
|
||||
this.onErrorCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public onClose(callback: () => void) {
|
||||
this.onCloseCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public onBroadcast(callback: (message: string) => void) {
|
||||
this.onBroadcastCallback = callback;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ function ScrollArea({
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn('relative', className)}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
|
||||
@@ -18,8 +18,11 @@ export default function I18nProvider({ children }: I18nProviderProps) {
|
||||
// return i18nLabel.en_US;
|
||||
// }
|
||||
|
||||
export const extractI18nObject = (i18nObject: I18nObject): string => {
|
||||
export const extractI18nObject = (i18nObject: I18nObject | undefined | null): string => {
|
||||
// 根据当前语言返回对应的值, fallback优先级:en_US、zh_Hans、zh_Hant、ja_JP
|
||||
if (!i18nObject || typeof i18nObject !== 'object') {
|
||||
return '';
|
||||
}
|
||||
const language = i18n.language.replace('-', '_');
|
||||
if (language === 'en_US' && i18nObject.en_US) return i18nObject.en_US;
|
||||
if (language === 'zh_Hans' && i18nObject.zh_Hans) return i18nObject.zh_Hans;
|
||||
|
||||
@@ -38,6 +38,9 @@ const enUS = {
|
||||
select: 'Select',
|
||||
cancel: 'Cancel',
|
||||
submit: 'Submit',
|
||||
next: 'Next',
|
||||
back: 'Back',
|
||||
refresh: 'Refresh',
|
||||
error: 'Error',
|
||||
success: 'Success',
|
||||
save: 'Save',
|
||||
@@ -57,6 +60,8 @@ const enUS = {
|
||||
deleteError: 'Delete failed: ',
|
||||
addRound: 'Add Round',
|
||||
copy: 'Copy',
|
||||
undo: 'Undo',
|
||||
redo: 'Redo',
|
||||
copySuccess: 'Copy Successfully',
|
||||
copyFailed: 'Copy Failed',
|
||||
test: 'Test',
|
||||
@@ -148,6 +153,12 @@ const enUS = {
|
||||
more: 'More ({{count}})',
|
||||
less: 'Less',
|
||||
noItems: 'No items',
|
||||
data: 'Data',
|
||||
result: 'Result',
|
||||
body: 'Body',
|
||||
headers: 'Headers',
|
||||
query: 'Query',
|
||||
method: 'Method',
|
||||
},
|
||||
notFound: {
|
||||
title: 'Page not found',
|
||||
@@ -362,6 +373,16 @@ const enUS = {
|
||||
pipelineDiscard: 'Discard Message',
|
||||
sessionTypePerson: 'Private Chat',
|
||||
sessionTypeGroup: 'Group Chat',
|
||||
// Unified binding (replacing routing rules)
|
||||
bindTarget: 'Bind Target',
|
||||
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.',
|
||||
adapterConfigDescription: 'Configure the selected platform adapter',
|
||||
dangerZone: 'Danger Zone',
|
||||
dangerZoneDescription: 'Irreversible and destructive actions',
|
||||
@@ -1291,6 +1312,795 @@ const enUS = {
|
||||
backToWorkbench: 'Back to Workbench',
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
title: 'Workflows',
|
||||
description: 'Create and manage visual workflows for complex message processing logic',
|
||||
createWorkflow: 'Create Workflow',
|
||||
selectFromSidebar: 'Select a workflow from the sidebar',
|
||||
editWorkflow: 'Edit Workflow',
|
||||
newWorkflow: 'New Workflow',
|
||||
getWorkflowListError: 'Failed to get workflow list: ',
|
||||
workflowName: 'Workflow Name',
|
||||
workflowDescription: 'Workflow Description',
|
||||
workflowNameRequired: 'Workflow name is required',
|
||||
defaultDescription: 'A workflow',
|
||||
getWorkflowError: 'Failed to get workflow: ',
|
||||
loadError: 'Failed to load workflow',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveError: 'Failed to save: ',
|
||||
createSuccess: 'Workflow created successfully',
|
||||
createError: 'Failed to create: ',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
deleteError: 'Failed to delete: ',
|
||||
deleteConfirmation: 'Are you sure you want to delete this workflow?',
|
||||
copySuccess: 'Copied successfully',
|
||||
copyError: 'Failed to copy: ',
|
||||
export: 'Export',
|
||||
import: 'Import',
|
||||
exportSuccess: 'Workflow exported',
|
||||
importSuccess: 'Workflow imported',
|
||||
importError: 'Import failed: Invalid file format',
|
||||
publish: 'Publish',
|
||||
publishSuccess: 'Published successfully',
|
||||
publishError: 'Failed to publish',
|
||||
configuration: 'Configuration',
|
||||
executions: 'Executions',
|
||||
editor: 'Editor',
|
||||
debugChat: 'Debug Chat',
|
||||
basicInfo: 'Basic Info',
|
||||
basicInfoDesc: 'Set workflow name, icon and description',
|
||||
basicInfoDescription: 'Set workflow name and description',
|
||||
dangerZone: 'Danger Zone',
|
||||
dangerZoneDesc: 'Irreversible operations',
|
||||
dangerZoneDescription: 'Irreversible operations',
|
||||
deleteWorkflowAction: 'Delete this workflow',
|
||||
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.',
|
||||
// Form component
|
||||
name: 'Name',
|
||||
namePlaceholder: 'Enter workflow name',
|
||||
descriptionPlaceholder: 'Enter workflow description (optional)',
|
||||
enabled: 'Enabled',
|
||||
enabledDesc: 'When enabled, the workflow can be triggered for execution',
|
||||
loading: 'Loading...',
|
||||
info: 'Workflow Info',
|
||||
uuid: 'UUID',
|
||||
version: 'Version',
|
||||
createdAt: 'Created At',
|
||||
updatedAt: 'Updated At',
|
||||
// Executions tab
|
||||
totalExecutions: '{{count}} execution(s) total',
|
||||
statistics: 'Statistics',
|
||||
successfulCount: '{{count}} successful',
|
||||
successRate: 'Success Rate',
|
||||
averageDuration: 'Avg. Duration',
|
||||
perExecution: 'per execution',
|
||||
failedExecutions: 'Failed',
|
||||
lastExecution: 'Last run',
|
||||
filterByStatus: 'Filter by status',
|
||||
allStatuses: 'All statuses',
|
||||
manualTrigger: 'Manual Trigger',
|
||||
executionId: 'Execution ID',
|
||||
status: 'Status',
|
||||
triggerType: 'Trigger Type',
|
||||
startedAt: 'Started At',
|
||||
duration: 'Duration',
|
||||
noExecutions: 'No executions yet',
|
||||
executionDetails: 'Execution Details',
|
||||
error: 'Error',
|
||||
nodeExecutions: 'Node Executions',
|
||||
result: 'Result',
|
||||
'status.pending': 'Pending',
|
||||
'status.running': 'Running',
|
||||
'status.completed': 'Completed',
|
||||
'status.failed': 'Failed',
|
||||
'status.cancelled': 'Cancelled',
|
||||
// Editor component translations
|
||||
nodePalette: 'Node Palette',
|
||||
properties: 'Properties',
|
||||
zoomIn: 'Zoom In',
|
||||
zoomOut: 'Zoom Out',
|
||||
fitView: 'Fit View',
|
||||
unsavedChanges: 'Unsaved changes',
|
||||
paste: 'Paste',
|
||||
deleted: 'Deleted',
|
||||
nothingToCopy: 'No nodes selected to copy',
|
||||
nothingToPaste: 'Clipboard is empty',
|
||||
copied: 'Copied {{count}} node(s)',
|
||||
pasted: 'Pasted {{count}} node(s)',
|
||||
nodesSelected: '{{count}} node(s) selected',
|
||||
edgesSelected: '{{count}} edge(s) selected',
|
||||
// Node palette
|
||||
searchNodes: 'Search nodes...',
|
||||
loadingNodeTypes: 'Loading node types...',
|
||||
noNodesFound: 'No matching nodes found',
|
||||
clearSearch: 'Clear search',
|
||||
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',
|
||||
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.',
|
||||
deleteEdge: 'Delete Edge',
|
||||
deleteEdgeConfirm: 'Confirm Delete Edge',
|
||||
deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
|
||||
nodeLabel: 'Node Label',
|
||||
nodeLabelPlaceholder: 'Enter node display name',
|
||||
nodeId: 'Node ID',
|
||||
inputOutputVariables: 'Input/Output Variables',
|
||||
inputs: 'Inputs',
|
||||
outputs: 'Outputs',
|
||||
availableVariables: 'Available Variables',
|
||||
globalVariables: 'Global Variables',
|
||||
messageContent: 'Message Content',
|
||||
messageSender: 'Sender',
|
||||
platform: 'Platform',
|
||||
sessionId: 'Session ID',
|
||||
timestamp: 'Timestamp',
|
||||
nodeConfig: 'Node Configuration',
|
||||
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.',
|
||||
// Node inputs/outputs i18n (for port labels)
|
||||
nodeInputs: {
|
||||
// Common inputs
|
||||
input: 'Input',
|
||||
message: 'Message',
|
||||
text: 'Text',
|
||||
query: 'Query',
|
||||
data: 'Data',
|
||||
condition: 'Condition',
|
||||
value: 'Value',
|
||||
// Trigger inputs
|
||||
content: 'Content',
|
||||
context: 'Context',
|
||||
body: 'Request Body',
|
||||
variables: 'Variables',
|
||||
items: 'Items',
|
||||
arguments: 'Arguments',
|
||||
// AI/Process inputs
|
||||
question: 'Question',
|
||||
parameters: 'Parameters',
|
||||
key: 'Key',
|
||||
// Control inputs
|
||||
case_1: 'Case 1',
|
||||
case_2: 'Case 2',
|
||||
branch_1: 'Branch 1',
|
||||
branch_2: 'Branch 2',
|
||||
// Action inputs
|
||||
notification_id: 'Notification ID',
|
||||
// Integration inputs
|
||||
key_template: 'Key Template',
|
||||
hash_field: 'Hash Field',
|
||||
server_name: 'Server Name',
|
||||
tool_name: 'Tool Name',
|
||||
arguments_template: 'Arguments Template',
|
||||
scope: 'Scope',
|
||||
},
|
||||
// Data type labels
|
||||
'type.string': 'String',
|
||||
'type.object': 'Object',
|
||||
'type.array': 'Array',
|
||||
'type.boolean': 'Boolean',
|
||||
'type.number': 'Number',
|
||||
'type.any': 'Any',
|
||||
'type.datetime': 'DateTime',
|
||||
nodes: {
|
||||
trigger: 'Triggers',
|
||||
triggerDescription: 'Starting nodes for workflows',
|
||||
messageTrigger: 'Message Trigger',
|
||||
messageTriggerDescription: 'Triggered when a message is received',
|
||||
scheduleTrigger: 'Schedule Trigger',
|
||||
scheduleTriggerDescription: 'Triggered on a schedule',
|
||||
cronTrigger: 'Cron Trigger',
|
||||
cronTriggerDescription: 'Trigger workflow on a scheduled time',
|
||||
webhookTrigger: 'Webhook Trigger',
|
||||
webhookTriggerDescription: 'Triggered via HTTP request',
|
||||
eventTrigger: 'Event Trigger',
|
||||
eventTriggerDescription: 'Triggered on system events',
|
||||
process: 'AI/Process',
|
||||
processDescription: 'Data processing nodes',
|
||||
aiProcess: 'AI Processing',
|
||||
aiProcessDescription: 'Process messages using AI models',
|
||||
llmCall: 'LLM Call',
|
||||
llmCallDescription: 'Invoke large language model for conversation or generation',
|
||||
codeProcess: 'Code Processing',
|
||||
codeProcessDescription: 'Execute custom code',
|
||||
codeExecutor: 'Code Executor',
|
||||
codeExecutorDescription: 'Execute Python/JavaScript code',
|
||||
templateProcess: 'Template Processing',
|
||||
templateProcessDescription: 'Format output using templates',
|
||||
httpRequest: 'HTTP Request',
|
||||
httpRequestDescription: 'Send HTTP requests',
|
||||
dataTransform: 'Data Transform',
|
||||
dataTransformDescription: 'Transform data format',
|
||||
questionClassifier: 'Question Classifier',
|
||||
questionClassifierDescription: 'Classify user questions into predefined categories using LLM',
|
||||
parameterExtractor: 'Parameter Extractor',
|
||||
parameterExtractorDescription: 'Extract structured parameters from text using LLM',
|
||||
knowledgeRetrieval: 'Knowledge Retrieval',
|
||||
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base',
|
||||
textTemplate: 'Text Template',
|
||||
textTemplateDescription: 'Generate text using templates with variable interpolation',
|
||||
jsonTransform: 'JSON Transform',
|
||||
jsonTransformDescription: 'Transform JSON data using expressions',
|
||||
dataAggregator: 'Data Aggregator',
|
||||
dataAggregatorDescription: 'Aggregate data from multiple sources',
|
||||
textSplitter: 'Text Splitter',
|
||||
textSplitterDescription: 'Split text into smaller chunks',
|
||||
variableAssignment: 'Variable Assignment',
|
||||
variableAssignmentDescription: 'Assign values to workflow variables',
|
||||
control: 'Control Flow',
|
||||
controlDescription: 'Flow control nodes',
|
||||
condition: 'Condition Branch',
|
||||
conditionDescription: 'Branch based on conditions',
|
||||
switch: 'Switch',
|
||||
switchDescription: 'Multi-way branching',
|
||||
loop: 'Loop',
|
||||
loopDescription: 'Repeat execution',
|
||||
iterator: 'Iterator',
|
||||
iteratorDescription: 'Iterate over array elements',
|
||||
parallel: 'Parallel',
|
||||
parallelDescription: 'Execute multiple branches in parallel',
|
||||
wait: 'Wait',
|
||||
waitDescription: 'Wait for specified time',
|
||||
delay: 'Delay',
|
||||
delayDescription: 'Wait for a specified time',
|
||||
merge: 'Merge',
|
||||
mergeDescription: 'Merge multiple branches',
|
||||
variableAggregator: 'Variable Aggregator',
|
||||
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',
|
||||
storeData: 'Store Data',
|
||||
storeDataDescription: 'Store data to database',
|
||||
callPipeline: 'Call Pipeline',
|
||||
callPipelineDescription: 'Call an existing Pipeline',
|
||||
setVariable: 'Set Variable',
|
||||
setVariableDescription: 'Set context variable',
|
||||
openingStatement: 'Opening Statement',
|
||||
openingStatementDescription: 'Provide conversation opener and suggested questions',
|
||||
end: 'End',
|
||||
endDescription: 'Mark the end of workflow execution',
|
||||
log: 'Log',
|
||||
logDescription: 'Record log information',
|
||||
integration: 'Integration',
|
||||
integrationDescription: 'Third-party platform integration nodes',
|
||||
difyWorkflow: 'Dify Workflow',
|
||||
difyWorkflowDescription: 'Call Dify platform workflow',
|
||||
difyKnowledgeQuery: 'Dify Knowledge Query',
|
||||
difyKnowledgeQueryDescription: 'Query Dify knowledge base',
|
||||
n8nWorkflow: 'N8n Workflow',
|
||||
n8nWorkflowDescription: 'Call n8n workflow',
|
||||
langflowFlow: 'Langflow Flow',
|
||||
langflowFlowDescription: 'Call Langflow flow',
|
||||
cozeBot: 'Coze Bot',
|
||||
cozeBotDescription: 'Call Coze Bot',
|
||||
// Data & Tools integration nodes
|
||||
databaseQuery: 'Database Query',
|
||||
databaseQueryDescription: 'Execute database queries',
|
||||
redisOperation: 'Redis Operation',
|
||||
redisOperationDescription: 'Perform Redis cache operations',
|
||||
mcpTool: 'MCP Tool',
|
||||
mcpToolDescription: 'Invoke an MCP tool',
|
||||
memoryStore: 'Memory Store',
|
||||
memoryStoreDescription: 'Store and retrieve data from workflow memory',
|
||||
},
|
||||
executionHistory: {
|
||||
title: 'Execution History',
|
||||
noExecutions: 'No executions yet',
|
||||
status: 'Status',
|
||||
startTime: 'Start Time',
|
||||
duration: 'Duration',
|
||||
running: 'Running',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled',
|
||||
viewDetails: 'View Details',
|
||||
cancel: 'Cancel Execution',
|
||||
retry: 'Retry',
|
||||
nodeResults: 'Node Execution Results',
|
||||
},
|
||||
versions: {
|
||||
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.',
|
||||
rollbackSuccess: 'Rollback successful',
|
||||
rollbackError: 'Failed to rollback: ',
|
||||
},
|
||||
// Debug and monitoring
|
||||
debug: {
|
||||
title: 'Debug',
|
||||
mode: 'Debug Mode',
|
||||
panel: 'Debug Panel',
|
||||
start: 'Start Debug',
|
||||
pause: 'Pause',
|
||||
resume: 'Resume',
|
||||
step: 'Step',
|
||||
stop: 'Stop',
|
||||
context: 'Debug Context',
|
||||
messageContent: 'Simulated Message',
|
||||
messageContentPlaceholder: 'Enter the message content to simulate',
|
||||
senderId: 'Sender ID',
|
||||
senderIdPlaceholder: 'Sender unique identifier',
|
||||
senderName: 'Sender Name',
|
||||
senderNamePlaceholder: 'Sender display name',
|
||||
platform: 'Platform',
|
||||
platformPlaceholder: 'e.g. qq, wechat, telegram',
|
||||
conversationId: 'Conversation ID',
|
||||
conversationIdPlaceholder: 'Conversation unique identifier',
|
||||
isGroup: 'Group Chat',
|
||||
customVariables: 'Custom Variables',
|
||||
customVariablesDesc: 'Add custom variables for debugging',
|
||||
variableKey: 'Variable Name',
|
||||
variableValue: 'Variable Value',
|
||||
addVariable: 'Add Variable',
|
||||
variables: 'Watched Variables',
|
||||
watchedVariables: 'Watched Variables',
|
||||
noWatchedVariables: 'No watched variables',
|
||||
addWatchVariable: 'Add Watch',
|
||||
nodeStates: 'Node States',
|
||||
nodeOutputs: 'Node Outputs',
|
||||
noNodeOutputs: 'No node outputs yet',
|
||||
toggleBreakpoint: 'Toggle Breakpoint',
|
||||
clearBreakpoints: 'Clear All Breakpoints',
|
||||
breakpointSet: 'Breakpoint set',
|
||||
breakpointRemoved: 'Breakpoint removed',
|
||||
logs: 'Debug Logs',
|
||||
noLogs: 'No logs yet',
|
||||
clearLogs: 'Clear Logs',
|
||||
autoScroll: 'Auto Scroll',
|
||||
logEntries: 'log entries',
|
||||
resetContext: 'Reset Context',
|
||||
// Debug execution messages
|
||||
starting: 'Starting debug execution...',
|
||||
started: 'Debug execution started (ID: {{id}})',
|
||||
startError: 'Failed to start debug',
|
||||
completed: 'Debug execution completed',
|
||||
unknownError: 'Unknown error',
|
||||
paused: 'Execution paused',
|
||||
pauseError: 'Failed to pause',
|
||||
resumed: 'Execution resumed',
|
||||
resumeError: 'Failed to resume',
|
||||
steppedTo: 'Stepped to node: {{node}}',
|
||||
stepError: 'Failed to step',
|
||||
stopped: 'Debug stopped',
|
||||
stopError: 'Failed to stop',
|
||||
},
|
||||
debugMode: 'Debug Mode',
|
||||
debugPanel: 'Debug Panel',
|
||||
startDebug: 'Start Debug',
|
||||
pauseDebug: 'Pause',
|
||||
resumeDebug: 'Resume',
|
||||
stepDebug: 'Step',
|
||||
stopDebug: 'Stop',
|
||||
debugContext: 'Debug Context',
|
||||
simulatedMessage: 'Simulated Message',
|
||||
simulatedMessagePlaceholder: 'Enter the message content to simulate',
|
||||
senderId: 'Sender ID',
|
||||
senderIdPlaceholder: 'Sender unique identifier',
|
||||
senderName: 'Sender Name',
|
||||
senderNamePlaceholder: 'Sender display name',
|
||||
conversationId: 'Conversation ID',
|
||||
conversationIdPlaceholder: 'Conversation unique identifier',
|
||||
isGroup: 'Group Chat',
|
||||
customVariables: 'Custom Variables',
|
||||
addVariable: 'Add Variable',
|
||||
variableName: 'Variable Name',
|
||||
variableValue: 'Variable Value',
|
||||
watchedVariables: 'Watched Variables',
|
||||
addWatchVariable: 'Add Watch',
|
||||
nodeStates: 'Node States',
|
||||
breakpoints: 'Breakpoints',
|
||||
toggleBreakpoint: 'Toggle Breakpoint',
|
||||
breakpointSet: 'Breakpoint set',
|
||||
breakpointRemoved: 'Breakpoint removed',
|
||||
debugLogs: 'Debug Logs',
|
||||
noLogs: 'No logs yet',
|
||||
clearLogs: 'Clear Logs',
|
||||
autoScroll: 'Auto Scroll',
|
||||
debugState: {
|
||||
idle: 'Idle',
|
||||
running: 'Running',
|
||||
paused: 'Paused',
|
||||
completed: 'Completed',
|
||||
error: 'Error',
|
||||
},
|
||||
nodeStatus: {
|
||||
pending: 'Pending',
|
||||
running: 'Running',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
skipped: 'Skipped',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'Workflow Chat',
|
||||
selectWorkflow: 'Select Workflow',
|
||||
sessionType: 'Session Type',
|
||||
privateChat: 'Private Chat',
|
||||
groupChat: 'Group Chat',
|
||||
send: 'Send',
|
||||
reset: 'Reset Conversation',
|
||||
inputPlaceholder: 'Send {{type}} message...',
|
||||
noMessages: 'No messages',
|
||||
userMessage: 'User',
|
||||
botMessage: 'Bot',
|
||||
sendFailed: 'Send failed',
|
||||
resetSuccess: 'Conversation reset successfully',
|
||||
resetFailed: 'Reset failed',
|
||||
loadMessagesFailed: 'Failed to load messages',
|
||||
loadWorkflowsFailed: 'Failed to load workflows',
|
||||
atTips: 'Mention bot',
|
||||
streaming: 'Streaming',
|
||||
streamOutput: 'Stream',
|
||||
connected: 'WebSocket connected',
|
||||
disconnected: 'WebSocket disconnected',
|
||||
connectionError: 'WebSocket connection error',
|
||||
connectionFailed: 'WebSocket connection failed',
|
||||
notConnected: 'WebSocket not connected, please try again later',
|
||||
imageUploadFailed: 'Image upload failed',
|
||||
reply: 'Reply',
|
||||
replyTo: 'Reply to',
|
||||
showMarkdown: 'Render',
|
||||
showRaw: 'Raw',
|
||||
allMembers: 'All Members',
|
||||
file: 'File',
|
||||
voice: 'Voice',
|
||||
uploadImage: 'Upload Image',
|
||||
uploading: 'Uploading...',
|
||||
},
|
||||
// Execution history and monitoring
|
||||
filterByDate: 'Filter by Date',
|
||||
allTime: 'All Time',
|
||||
today: 'Today',
|
||||
lastWeek: 'Last Week',
|
||||
lastMonth: 'Last Month',
|
||||
showingExecutions: 'Showing {{shown}} / {{total}} executions',
|
||||
rerun: 'Rerun',
|
||||
rerunExecution: 'Rerun Execution',
|
||||
logs: 'Logs',
|
||||
details: 'Details',
|
||||
completedAt: 'Completed At',
|
||||
noNodeExecutions: 'No node executions yet',
|
||||
// Node config field labels (used by DynamicFormComponent)
|
||||
nodeConfigFields: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
conditions: 'Trigger Conditions',
|
||||
keyword_filter: 'Keyword Filter',
|
||||
regex_filter: 'Regex Filter',
|
||||
min_length: 'Min Length',
|
||||
max_length: 'Max Length',
|
||||
require_mention: 'Require Mention',
|
||||
respond_rules: 'Group Respond Rules',
|
||||
access_control: 'Access Control',
|
||||
// trigger.py - CronTriggerNode
|
||||
cron: 'Cron Expression',
|
||||
timezone: 'Timezone',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
path: 'Webhook Path',
|
||||
allowed_methods: 'Allowed HTTP Methods',
|
||||
content_type: 'Content-Type',
|
||||
auth_type: 'Authentication Type',
|
||||
auth_key: 'Auth Key',
|
||||
validation: 'Request Validation',
|
||||
timeout: 'Timeout',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_types: 'Event Types',
|
||||
filter: 'Event Filter',
|
||||
debounce_ms: 'Debounce Time',
|
||||
// process.py - LLMCallNode
|
||||
model: 'Model',
|
||||
prompt_template: 'Prompt Template',
|
||||
system_prompt: 'System Prompt',
|
||||
temperature: 'Temperature',
|
||||
top_p: 'Top P',
|
||||
frequency_penalty: 'Frequency Penalty',
|
||||
presence_penalty: 'Presence Penalty',
|
||||
max_tokens: 'Max Tokens',
|
||||
stop_sequences: 'Stop Sequences',
|
||||
seed: 'Random Seed',
|
||||
stream: 'Stream Output',
|
||||
use_conversation_history: 'Use Conversation History',
|
||||
// process.py - CodeExecutorNode
|
||||
language: 'Programming Language',
|
||||
code: 'Code',
|
||||
// process.py - HTTPRequestNode
|
||||
url: 'Request URL',
|
||||
method: 'Request Method',
|
||||
auth_config: 'Auth Configuration',
|
||||
// process.py - DataTransformNode
|
||||
transform_type: 'Transform Type',
|
||||
template: 'Template',
|
||||
expression: 'Expression',
|
||||
output_type: 'Output Type',
|
||||
// process.py - QuestionClassifierNode
|
||||
categories: 'Categories',
|
||||
instruction: 'Instruction',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: 'Parameter Definitions',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
knowledge_bases: 'Knowledge Bases',
|
||||
top_k: 'Top K Results',
|
||||
score_threshold: 'Score Threshold',
|
||||
search_method: 'Search Method',
|
||||
enable_citations: 'Enable Citations',
|
||||
// control.py - ConditionNode
|
||||
condition_type: 'Condition Type',
|
||||
condition_expression: 'Condition Expression',
|
||||
left_value: 'Left Value',
|
||||
operator: 'Comparison Operator',
|
||||
right_value: 'Right Value',
|
||||
// control.py - SwitchNode
|
||||
cases: 'Branch Cases',
|
||||
// control.py - LoopNode
|
||||
max_iterations: 'Max Iterations',
|
||||
break_condition: 'Break Condition',
|
||||
// control.py - IteratorNode
|
||||
parallel: 'Parallel Processing',
|
||||
max_concurrency: 'Max Concurrency',
|
||||
// control.py - ParallelNode
|
||||
branches: 'Branch Configuration',
|
||||
wait_all: 'Wait All',
|
||||
fail_fast: 'Fail Fast',
|
||||
// control.py - WaitNode
|
||||
duration: 'Duration',
|
||||
duration_type: 'Time Unit',
|
||||
// control.py - MergeNode
|
||||
merge_strategy: 'Merge Strategy',
|
||||
// control.py - VariableAggregatorNode
|
||||
variable_mappings: 'Variable Mappings',
|
||||
aggregation_mode: 'Aggregation Mode',
|
||||
// action.py - SendMessageNode
|
||||
target_type: 'Target Type',
|
||||
target_id: 'Target ID',
|
||||
platform: 'Platform',
|
||||
message_type: 'Message Type',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_mode: 'Reply Mode',
|
||||
message_template: 'Message Template',
|
||||
long_text_processing: 'Long Text Processing',
|
||||
force_delay: 'Force Delay',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_uuid: 'Pipeline',
|
||||
inherit_context: 'Inherit Context',
|
||||
// action.py - StoreDataNode
|
||||
storage_type: 'Storage Type',
|
||||
ttl: 'TTL',
|
||||
key_prefix: 'Key Prefix',
|
||||
// action.py - SetVariableNode
|
||||
variable_name: 'Variable Name',
|
||||
variable_scope: 'Variable Scope',
|
||||
set_variable_operation: 'Operation Type',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: 'Opening Statement',
|
||||
suggested_questions: 'Suggested Questions',
|
||||
show_suggestions: 'Show Suggestions',
|
||||
// action.py - EndNode
|
||||
output_format: 'Output Format',
|
||||
success_message: 'Success Message',
|
||||
// integration.py - DatabaseQueryNode
|
||||
connection_type: 'Database Type',
|
||||
connection_string: 'Connection String',
|
||||
query: 'SQL Query',
|
||||
query_type: 'Query Type',
|
||||
// integration.py - RedisOperationNode
|
||||
connection_url: 'Connection URL',
|
||||
operation: 'Operation Type',
|
||||
key_template: 'Key Template',
|
||||
hash_field: 'Hash Field',
|
||||
// integration.py - MCPToolNode
|
||||
server_name: 'Server Name',
|
||||
tool_name: 'Tool Name',
|
||||
arguments_template: 'Arguments Template',
|
||||
// integration.py - MemoryStoreNode
|
||||
scope: 'Scope',
|
||||
// integration.py - DifyWorkflowNode
|
||||
'base-url': 'Base URL',
|
||||
'api-key': 'API Key',
|
||||
'app-type': 'App Type',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
dataset_id: 'Dataset ID',
|
||||
// integration.py - N8nWorkflowNode
|
||||
'webhook-url': 'Webhook URL',
|
||||
// integration.py - LangflowFlowNode
|
||||
'flow-id': 'Flow ID',
|
||||
// integration.py - CozeBotNode
|
||||
'bot-id': 'Bot ID',
|
||||
'api-base': 'API Base URL',
|
||||
},
|
||||
// Node outputs i18n
|
||||
nodeOutputs: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
message: 'Message Content',
|
||||
sender_id: 'Sender ID',
|
||||
sender_name: 'Sender Name',
|
||||
platform: 'Platform',
|
||||
conversation_id: 'Conversation ID',
|
||||
is_group: 'Is Group Chat',
|
||||
context: 'Full Message Context',
|
||||
// trigger.py - CronTriggerNode
|
||||
timestamp: 'Trigger Time',
|
||||
schedule: 'Trigger Schedule',
|
||||
cron_timestamp: 'Cron Timestamp',
|
||||
cron_schedule: 'Cron Schedule',
|
||||
cron_context: 'Cron Context',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
body: 'Request Body',
|
||||
headers: 'Request Headers',
|
||||
query: 'Query Parameters',
|
||||
method: 'Request Method',
|
||||
webhook_body: 'Webhook Body',
|
||||
webhook_headers: 'Webhook Headers',
|
||||
webhook_query: 'Webhook Query',
|
||||
webhook_method: 'Webhook Method',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_type: 'Event Type',
|
||||
event_data: 'Event Data',
|
||||
event_timestamp: 'Event Timestamp',
|
||||
// process.py - LLMCallNode
|
||||
response: 'Model Response',
|
||||
usage: 'Token Usage Statistics',
|
||||
prompt: 'Prompt/Question',
|
||||
context_info: 'Context Information',
|
||||
// process.py - CodeExecutorNode
|
||||
output: 'Output Data',
|
||||
console: 'Console Output',
|
||||
code_input: 'Code Input',
|
||||
code_output: 'Code Output',
|
||||
// process.py - HTTPRequestNode
|
||||
status_code: 'Status Code',
|
||||
http_body: 'HTTP Body',
|
||||
http_headers: 'HTTP Headers',
|
||||
http_response: 'HTTP Response',
|
||||
response_headers: 'Response Headers',
|
||||
// process.py - DataTransformNode
|
||||
result: 'Transform Result',
|
||||
transform_input: 'Transform Input',
|
||||
transform_result: 'Transform Result',
|
||||
// process.py - QuestionClassifierNode
|
||||
category: 'Category Result',
|
||||
confidence: 'Confidence',
|
||||
all_scores: 'All Category Scores',
|
||||
question: 'User Question',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: 'Extracted Parameters',
|
||||
extraction_success: 'Extraction Success',
|
||||
extract_text: 'Input Text',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
documents: 'Retrieved Documents',
|
||||
citations: 'Citation Information',
|
||||
knowledge_context: 'Merged Context',
|
||||
knowledge_query: 'Retrieval Query',
|
||||
// control.py - ConditionNode
|
||||
true: 'True Branch Output',
|
||||
false: 'False Branch Output',
|
||||
condition_input: 'Condition Input',
|
||||
// control.py - SwitchNode
|
||||
matched_case: 'Matched Branch Output',
|
||||
default: 'Default Branch Output',
|
||||
switch_input: 'Switch Input',
|
||||
// control.py - LoopNode
|
||||
item: 'Current Item',
|
||||
index: 'Current Index',
|
||||
results: 'All Iteration Results',
|
||||
completed: 'Is Completed',
|
||||
loop_items: 'Items to Iterate',
|
||||
// control.py - IteratorNode
|
||||
is_first: 'Is First',
|
||||
is_last: 'Is Last',
|
||||
iterator_array: 'Input Array',
|
||||
iterator_item: 'Current Element',
|
||||
iterator_index: 'Current Index',
|
||||
// control.py - ParallelNode
|
||||
errors: 'Error List',
|
||||
parallel_input: 'Parallel Input',
|
||||
parallel_results: 'All Branch Results',
|
||||
// control.py - WaitNode
|
||||
wait_input: 'Passthrough Input',
|
||||
wait_output: 'Passthrough Output',
|
||||
// control.py - MergeNode
|
||||
merged: 'Merged Result',
|
||||
merge_array: 'Array Result',
|
||||
merge_input_1: 'Input 1',
|
||||
merge_input_2: 'Input 2',
|
||||
merge_input_3: 'Input 3',
|
||||
merge_input_4: 'Input 4',
|
||||
// control.py - VariableAggregatorNode
|
||||
aggregated: 'Aggregated Variables',
|
||||
aggregator_variables: 'Variable Input',
|
||||
// action.py - SendMessageNode
|
||||
status: 'Send Status',
|
||||
message_id: 'Message ID',
|
||||
target: 'Target ID',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_message: 'Reply Content',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_response: 'Pipeline Response',
|
||||
pipeline_result: 'Full Result',
|
||||
pipeline_query: 'Query Content',
|
||||
context_data: 'Context Data',
|
||||
// action.py - StoreDataNode
|
||||
store_status: 'Store Status',
|
||||
store_key: 'Store Key',
|
||||
store_value: 'Store Value',
|
||||
// action.py - SetVariableNode
|
||||
variable_value: 'Variable Value',
|
||||
variable_result: 'Set Variable Result',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: 'Opening Statement',
|
||||
suggested_questions: 'Suggested Questions',
|
||||
// action.py - EndNode
|
||||
workflow_output: 'Workflow Output',
|
||||
final_result: 'Final Result',
|
||||
// integration.py - DatabaseQueryNode
|
||||
query_results: 'Query Results',
|
||||
row_count: 'Affected/Returned Rows',
|
||||
query_success: 'Query Success',
|
||||
query_params: 'Query Parameters',
|
||||
// integration.py - RedisOperationNode
|
||||
redis_result: 'Operation Result',
|
||||
redis_success: 'Operation Success',
|
||||
redis_key: 'Redis Key',
|
||||
redis_value: 'Redis Value',
|
||||
error: 'Error Message',
|
||||
plugin_input: 'Plugin Input',
|
||||
// integration.py - MCPToolNode
|
||||
tool_result: 'Tool Execution Result',
|
||||
tool_success: 'Tool Call Success',
|
||||
mcp_arguments: 'Tool Arguments',
|
||||
// integration.py - MemoryStoreNode
|
||||
memory_result: 'Retrieved/Stored Value',
|
||||
memory_success: 'Operation Success',
|
||||
memory_value: 'Value to Store',
|
||||
// integration.py - DifyWorkflowNode
|
||||
answer: 'Dify Answer',
|
||||
dify_success: 'Call Success',
|
||||
dify_query: 'User Input/Query',
|
||||
dify_conversation_id: 'Conversation ID',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
search_results: 'Search Results',
|
||||
knowledge_base_query: 'Query Content',
|
||||
// integration.py - N8nWorkflowNode
|
||||
n8n_result: 'Workflow Execution Result',
|
||||
n8n_success: 'Call Success',
|
||||
n8n_payload: 'Workflow Input Data',
|
||||
// integration.py - LangflowFlowNode
|
||||
flow_result: 'Flow Execution Result',
|
||||
flow_success: 'Call Success',
|
||||
langflow_input: 'Input Content',
|
||||
// integration.py - CozeBotNode
|
||||
bot_answer: 'Bot Reply',
|
||||
bot_success: 'Call Success',
|
||||
coze_query: 'User Input/Query',
|
||||
coze_conversation_id: 'Conversation ID',
|
||||
bot_conversation_id: 'Conversation ID',
|
||||
},
|
||||
},
|
||||
unifiedBinding: {
|
||||
bindingType: 'Binding Type',
|
||||
pipeline: 'Pipeline',
|
||||
workflow: 'Workflow',
|
||||
pipelineDescription: 'Process messages using traditional pipeline',
|
||||
workflowDescription: 'Process messages using visual workflow',
|
||||
selectPipeline: 'Select Pipeline',
|
||||
selectWorkflow: 'Select Workflow',
|
||||
noPipelines: 'No pipelines available',
|
||||
noWorkflows: 'No workflows available',
|
||||
createPipeline: 'Create Pipeline',
|
||||
createWorkflow: 'Create Workflow',
|
||||
},
|
||||
};
|
||||
|
||||
export default enUS;
|
||||
|
||||
@@ -1328,6 +1328,726 @@ const esES = {
|
||||
backToWorkbench: 'Volver al panel de trabajo',
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
title: 'Workflows',
|
||||
description: 'Create and manage visual workflows for complex message processing logic',
|
||||
createWorkflow: 'Create Workflow',
|
||||
selectFromSidebar: 'Select a workflow from the sidebar',
|
||||
editWorkflow: 'Edit Workflow',
|
||||
newWorkflow: 'New Workflow',
|
||||
getWorkflowListError: 'Failed to get workflow list: ',
|
||||
workflowName: 'Workflow Name',
|
||||
workflowDescription: 'Workflow Description',
|
||||
workflowNameRequired: 'Workflow name is required',
|
||||
defaultDescription: 'A workflow',
|
||||
getWorkflowError: 'Failed to get workflow: ',
|
||||
loadError: 'Failed to load workflow',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveError: 'Failed to save: ',
|
||||
createSuccess: 'Workflow created successfully',
|
||||
createError: 'Failed to create: ',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
deleteError: 'Failed to delete: ',
|
||||
deleteConfirmation: 'Are you sure you want to delete this workflow?',
|
||||
copySuccess: 'Copied successfully',
|
||||
copyError: 'Failed to copy: ',
|
||||
export: 'Export',
|
||||
import: 'Import',
|
||||
exportSuccess: 'Workflow exported',
|
||||
importSuccess: 'Workflow imported',
|
||||
importError: 'Import failed: Invalid file format',
|
||||
publish: 'Publish',
|
||||
publishSuccess: 'Published successfully',
|
||||
publishError: 'Failed to publish',
|
||||
configuration: 'Configuration',
|
||||
executions: 'Executions',
|
||||
editor: 'Editor',
|
||||
debugChat: 'Chat de depuración',
|
||||
basicInfo: 'Basic Info',
|
||||
basicInfoDesc: 'Set workflow name, icon and description',
|
||||
basicInfoDescription: 'Set workflow name and description',
|
||||
dangerZone: 'Danger Zone',
|
||||
dangerZoneDesc: 'Irreversible operations',
|
||||
dangerZoneDescription: 'Irreversible operations',
|
||||
deleteWorkflowAction: 'Delete this workflow',
|
||||
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.',
|
||||
// Form component
|
||||
name: 'Name',
|
||||
namePlaceholder: 'Enter workflow name',
|
||||
descriptionPlaceholder: 'Enter workflow description (optional)',
|
||||
enabled: 'Enabled',
|
||||
enabledDesc: 'When enabled, the workflow can be triggered for execution',
|
||||
loading: 'Loading...',
|
||||
info: 'Workflow Info',
|
||||
uuid: 'UUID',
|
||||
version: 'Version',
|
||||
createdAt: 'Created At',
|
||||
updatedAt: 'Updated At',
|
||||
// Executions tab
|
||||
totalExecutions: '{{count}} execution(s) total',
|
||||
statistics: 'Statistics',
|
||||
successfulCount: '{{count}} successful',
|
||||
successRate: 'Success Rate',
|
||||
averageDuration: 'Avg. Duration',
|
||||
perExecution: 'per execution',
|
||||
failedExecutions: 'Failed',
|
||||
lastExecution: 'Last run',
|
||||
filterByStatus: 'Filter by status',
|
||||
allStatuses: 'All statuses',
|
||||
manualTrigger: 'Manual Trigger',
|
||||
executionId: 'Execution ID',
|
||||
status: 'Status',
|
||||
triggerType: 'Trigger Type',
|
||||
startedAt: 'Started At',
|
||||
duration: 'Duration',
|
||||
noExecutions: 'No executions yet',
|
||||
executionDetails: 'Execution Details',
|
||||
error: 'Error',
|
||||
nodeExecutions: 'Node Executions',
|
||||
result: 'Result',
|
||||
'status.pending': 'Pending',
|
||||
'status.running': 'Running',
|
||||
'status.completed': 'Completed',
|
||||
'status.failed': 'Failed',
|
||||
'status.cancelled': 'Cancelled',
|
||||
// Editor component translations
|
||||
nodePalette: 'Node Palette',
|
||||
properties: 'Properties',
|
||||
zoomIn: 'Zoom In',
|
||||
zoomOut: 'Zoom Out',
|
||||
fitView: 'Fit View',
|
||||
unsavedChanges: 'Unsaved changes',
|
||||
paste: 'Paste',
|
||||
deleted: 'Deleted',
|
||||
nothingToCopy: 'No nodes selected to copy',
|
||||
nothingToPaste: 'Clipboard is empty',
|
||||
copied: 'Copied {{count}} node(s)',
|
||||
pasted: 'Pasted {{count}} node(s)',
|
||||
nodesSelected: '{{count}} node(s) selected',
|
||||
edgesSelected: '{{count}} edge(s) selected',
|
||||
// Node palette
|
||||
searchNodes: 'Search nodes...',
|
||||
loadingNodeTypes: 'Loading node types...',
|
||||
noNodesFound: 'No matching nodes found',
|
||||
clearSearch: 'Clear search',
|
||||
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',
|
||||
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.',
|
||||
deleteEdge: 'Delete Edge',
|
||||
deleteEdgeConfirm: 'Confirm Delete Edge',
|
||||
deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
|
||||
nodeLabel: 'Node Label',
|
||||
nodeLabelPlaceholder: 'Enter node display name',
|
||||
nodeId: 'Node ID',
|
||||
inputOutputVariables: 'Input/Output Variables',
|
||||
inputs: 'Inputs',
|
||||
outputs: 'Outputs',
|
||||
availableVariables: 'Available Variables',
|
||||
globalVariables: 'Global Variables',
|
||||
messageContent: 'Message Content',
|
||||
messageSender: 'Sender',
|
||||
platform: 'Platform',
|
||||
sessionId: 'Session ID',
|
||||
timestamp: 'Timestamp',
|
||||
nodeConfig: 'Node Configuration',
|
||||
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.',
|
||||
// Node inputs/outputs i18n (for port labels)
|
||||
nodeInputs: {
|
||||
// Common inputs
|
||||
input: 'Input',
|
||||
message: 'Message',
|
||||
text: 'Text',
|
||||
query: 'Query',
|
||||
data: 'Data',
|
||||
condition: 'Condition',
|
||||
value: 'Value',
|
||||
// Trigger inputs
|
||||
content: 'Content',
|
||||
context: 'Context',
|
||||
body: 'Request Body',
|
||||
variables: 'Variables',
|
||||
items: 'Items',
|
||||
arguments: 'Arguments',
|
||||
// AI/Process inputs
|
||||
question: 'Question',
|
||||
parameters: 'Parameters',
|
||||
key: 'Key',
|
||||
// Control inputs
|
||||
case_1: 'Case 1',
|
||||
case_2: 'Case 2',
|
||||
branch_1: 'Branch 1',
|
||||
branch_2: 'Branch 2',
|
||||
// Action inputs
|
||||
notification_id: 'Notification ID',
|
||||
// Integration inputs
|
||||
key_template: 'Key Template',
|
||||
hash_field: 'Hash Field',
|
||||
server_name: 'Server Name',
|
||||
tool_name: 'Tool Name',
|
||||
arguments_template: 'Arguments Template',
|
||||
scope: 'Scope',
|
||||
},
|
||||
// Data type labels
|
||||
'type.string': 'String',
|
||||
'type.object': 'Object',
|
||||
'type.array': 'Array',
|
||||
'type.boolean': 'Boolean',
|
||||
'type.number': 'Number',
|
||||
'type.any': 'Any',
|
||||
'type.datetime': 'DateTime',
|
||||
nodes: {
|
||||
trigger: 'Triggers',
|
||||
triggerDescription: 'Starting nodes for workflows',
|
||||
messageTrigger: 'Message Trigger',
|
||||
messageTriggerDescription: 'Triggered when a message is received',
|
||||
scheduleTrigger: 'Schedule Trigger',
|
||||
scheduleTriggerDescription: 'Triggered on a schedule',
|
||||
cronTrigger: 'Cron Trigger',
|
||||
cronTriggerDescription: 'Trigger workflow on a scheduled time',
|
||||
webhookTrigger: 'Webhook Trigger',
|
||||
webhookTriggerDescription: 'Triggered via HTTP request',
|
||||
eventTrigger: 'Event Trigger',
|
||||
eventTriggerDescription: 'Triggered on system events',
|
||||
process: 'AI/Process',
|
||||
processDescription: 'Data processing nodes',
|
||||
aiProcess: 'AI Processing',
|
||||
aiProcessDescription: 'Process messages using AI models',
|
||||
llmCall: 'LLM Call',
|
||||
llmCallDescription: 'Invoke large language model for conversation or generation',
|
||||
codeProcess: 'Code Processing',
|
||||
codeProcessDescription: 'Execute custom code',
|
||||
codeExecutor: 'Code Executor',
|
||||
codeExecutorDescription: 'Execute Python/JavaScript code',
|
||||
templateProcess: 'Template Processing',
|
||||
templateProcessDescription: 'Format output using templates',
|
||||
httpRequest: 'HTTP Request',
|
||||
httpRequestDescription: 'Send HTTP requests',
|
||||
dataTransform: 'Data Transform',
|
||||
dataTransformDescription: 'Transform data format',
|
||||
questionClassifier: 'Question Classifier',
|
||||
questionClassifierDescription: 'Classify user questions into predefined categories using LLM',
|
||||
parameterExtractor: 'Parameter Extractor',
|
||||
parameterExtractorDescription: 'Extract structured parameters from text using LLM',
|
||||
knowledgeRetrieval: 'Knowledge Retrieval',
|
||||
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base',
|
||||
textTemplate: 'Text Template',
|
||||
textTemplateDescription: 'Generate text using templates with variable interpolation',
|
||||
jsonTransform: 'JSON Transform',
|
||||
jsonTransformDescription: 'Transform JSON data using expressions',
|
||||
dataAggregator: 'Data Aggregator',
|
||||
dataAggregatorDescription: 'Aggregate data from multiple sources',
|
||||
textSplitter: 'Text Splitter',
|
||||
textSplitterDescription: 'Split text into smaller chunks',
|
||||
variableAssignment: 'Variable Assignment',
|
||||
variableAssignmentDescription: 'Assign values to workflow variables',
|
||||
control: 'Control Flow',
|
||||
controlDescription: 'Flow control nodes',
|
||||
condition: 'Condition Branch',
|
||||
conditionDescription: 'Branch based on conditions',
|
||||
switch: 'Switch',
|
||||
switchDescription: 'Multi-way branching',
|
||||
loop: 'Loop',
|
||||
loopDescription: 'Repeat execution',
|
||||
iterator: 'Iterator',
|
||||
iteratorDescription: 'Iterate over array elements',
|
||||
parallel: 'Parallel',
|
||||
parallelDescription: 'Execute multiple branches in parallel',
|
||||
wait: 'Wait',
|
||||
waitDescription: 'Wait for specified time',
|
||||
delay: 'Delay',
|
||||
delayDescription: 'Wait for a specified time',
|
||||
merge: 'Merge',
|
||||
mergeDescription: 'Merge multiple branches',
|
||||
variableAggregator: 'Variable Aggregator',
|
||||
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',
|
||||
storeData: 'Store Data',
|
||||
storeDataDescription: 'Store data to database',
|
||||
callPipeline: 'Call Pipeline',
|
||||
callPipelineDescription: 'Call an existing Pipeline',
|
||||
setVariable: 'Set Variable',
|
||||
setVariableDescription: 'Set context variable',
|
||||
openingStatement: 'Opening Statement',
|
||||
openingStatementDescription: 'Provide conversation opener and suggested questions',
|
||||
end: 'End',
|
||||
endDescription: 'Mark the end of workflow execution',
|
||||
log: 'Log',
|
||||
logDescription: 'Record log information',
|
||||
integration: 'Integration',
|
||||
integrationDescription: 'Third-party platform integration nodes',
|
||||
difyWorkflow: 'Dify Workflow',
|
||||
difyWorkflowDescription: 'Call Dify platform workflow',
|
||||
difyKnowledgeQuery: 'Dify Knowledge Query',
|
||||
difyKnowledgeQueryDescription: 'Query Dify knowledge base',
|
||||
n8nWorkflow: 'N8n Workflow',
|
||||
n8nWorkflowDescription: 'Call n8n workflow',
|
||||
langflowFlow: 'Langflow Flow',
|
||||
langflowFlowDescription: 'Call Langflow flow',
|
||||
cozeBot: 'Coze Bot',
|
||||
cozeBotDescription: 'Call Coze Bot',
|
||||
// Data & Tools integration nodes
|
||||
databaseQuery: 'Database Query',
|
||||
databaseQueryDescription: 'Execute database queries',
|
||||
redisOperation: 'Redis Operation',
|
||||
redisOperationDescription: 'Perform Redis cache operations',
|
||||
mcpTool: 'MCP Tool',
|
||||
mcpToolDescription: 'Invoke an MCP tool',
|
||||
memoryStore: 'Memory Store',
|
||||
memoryStoreDescription: 'Store and retrieve data from workflow memory',
|
||||
},
|
||||
executionHistory: {
|
||||
title: 'Execution History',
|
||||
noExecutions: 'No executions yet',
|
||||
status: 'Status',
|
||||
startTime: 'Start Time',
|
||||
duration: 'Duration',
|
||||
running: 'Running',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled',
|
||||
viewDetails: 'View Details',
|
||||
cancel: 'Cancel Execution',
|
||||
retry: 'Retry',
|
||||
nodeResults: 'Node Execution Results',
|
||||
},
|
||||
versions: {
|
||||
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.',
|
||||
rollbackSuccess: 'Rollback successful',
|
||||
rollbackError: 'Failed to rollback: ',
|
||||
},
|
||||
// Debug and monitoring
|
||||
debug: 'Debug',
|
||||
debugMode: 'Debug Mode',
|
||||
debugPanel: 'Debug Panel',
|
||||
startDebug: 'Start Debug',
|
||||
pauseDebug: 'Pause',
|
||||
resumeDebug: 'Resume',
|
||||
stepDebug: 'Step',
|
||||
stopDebug: 'Stop',
|
||||
debugContext: 'Debug Context',
|
||||
simulatedMessage: 'Simulated Message',
|
||||
simulatedMessagePlaceholder: 'Enter the message content to simulate',
|
||||
senderId: 'Sender ID',
|
||||
senderIdPlaceholder: 'Sender unique identifier',
|
||||
senderName: 'Sender Name',
|
||||
senderNamePlaceholder: 'Sender display name',
|
||||
conversationId: 'Conversation ID',
|
||||
conversationIdPlaceholder: 'Conversation unique identifier',
|
||||
isGroup: 'Group Chat',
|
||||
customVariables: 'Custom Variables',
|
||||
addVariable: 'Add Variable',
|
||||
variableName: 'Variable Name',
|
||||
variableValue: 'Variable Value',
|
||||
watchedVariables: 'Watched Variables',
|
||||
addWatchVariable: 'Add Watch',
|
||||
nodeStates: 'Node States',
|
||||
breakpoints: 'Breakpoints',
|
||||
toggleBreakpoint: 'Toggle Breakpoint',
|
||||
breakpointSet: 'Breakpoint set',
|
||||
breakpointRemoved: 'Breakpoint removed',
|
||||
debugLogs: 'Debug Logs',
|
||||
noLogs: 'No logs yet',
|
||||
clearLogs: 'Clear Logs',
|
||||
autoScroll: 'Auto Scroll',
|
||||
debugState: {
|
||||
idle: 'Idle',
|
||||
running: 'Running',
|
||||
paused: 'Paused',
|
||||
completed: 'Completed',
|
||||
error: 'Error',
|
||||
},
|
||||
nodeStatus: {
|
||||
pending: 'Pending',
|
||||
running: 'Running',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
skipped: 'Skipped',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'Workflow Chat',
|
||||
selectWorkflow: 'Select Workflow',
|
||||
sessionType: 'Session Type',
|
||||
privateChat: 'Private Chat',
|
||||
groupChat: 'Group Chat',
|
||||
send: 'Send',
|
||||
reset: 'Reset Conversation',
|
||||
inputPlaceholder: 'Send {{type}} message...',
|
||||
noMessages: 'No messages',
|
||||
userMessage: 'User',
|
||||
botMessage: 'Bot',
|
||||
sendFailed: 'Send failed',
|
||||
resetSuccess: 'Conversation reset successfully',
|
||||
resetFailed: 'Reset failed',
|
||||
loadMessagesFailed: 'Failed to load messages',
|
||||
loadWorkflowsFailed: 'Failed to load workflows',
|
||||
atTips: 'Mention bot',
|
||||
streaming: 'Streaming',
|
||||
streamOutput: 'Stream',
|
||||
connected: 'WebSocket connected',
|
||||
disconnected: 'WebSocket disconnected',
|
||||
connectionError: 'WebSocket connection error',
|
||||
connectionFailed: 'WebSocket connection failed',
|
||||
notConnected: 'WebSocket not connected, please try again later',
|
||||
imageUploadFailed: 'Image upload failed',
|
||||
reply: 'Reply',
|
||||
replyTo: 'Reply to',
|
||||
showMarkdown: 'Render',
|
||||
showRaw: 'Raw',
|
||||
allMembers: 'All Members',
|
||||
file: 'File',
|
||||
voice: 'Voice',
|
||||
uploadImage: 'Upload Image',
|
||||
uploading: 'Uploading...',
|
||||
},
|
||||
// Execution history and monitoring
|
||||
filterByDate: 'Filter by Date',
|
||||
allTime: 'All Time',
|
||||
today: 'Today',
|
||||
lastWeek: 'Last Week',
|
||||
lastMonth: 'Last Month',
|
||||
showingExecutions: 'Showing {{shown}} / {{total}} executions',
|
||||
rerun: 'Rerun',
|
||||
rerunExecution: 'Rerun Execution',
|
||||
logs: 'Logs',
|
||||
details: 'Details',
|
||||
completedAt: 'Completed At',
|
||||
noNodeExecutions: 'No node executions yet',
|
||||
// Node config field labels (used by DynamicFormComponent)
|
||||
nodeConfigFields: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
conditions: 'Trigger Conditions',
|
||||
keyword_filter: 'Keyword Filter',
|
||||
regex_filter: 'Regex Filter',
|
||||
min_length: 'Min Length',
|
||||
max_length: 'Max Length',
|
||||
require_mention: 'Require Mention',
|
||||
respond_rules: 'Group Respond Rules',
|
||||
access_control: 'Access Control',
|
||||
// trigger.py - CronTriggerNode
|
||||
cron: 'Cron Expression',
|
||||
timezone: 'Timezone',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
path: 'Webhook Path',
|
||||
allowed_methods: 'Allowed HTTP Methods',
|
||||
content_type: 'Content-Type',
|
||||
auth_type: 'Authentication Type',
|
||||
auth_key: 'Auth Key',
|
||||
validation: 'Request Validation',
|
||||
timeout: 'Timeout',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_types: 'Event Types',
|
||||
filter: 'Event Filter',
|
||||
debounce_ms: 'Debounce Time',
|
||||
// process.py - LLMCallNode
|
||||
model: 'Model',
|
||||
prompt_template: 'Prompt Template',
|
||||
system_prompt: 'System Prompt',
|
||||
temperature: 'Temperature',
|
||||
top_p: 'Top P',
|
||||
frequency_penalty: 'Frequency Penalty',
|
||||
presence_penalty: 'Presence Penalty',
|
||||
max_tokens: 'Max Tokens',
|
||||
stop_sequences: 'Stop Sequences',
|
||||
seed: 'Random Seed',
|
||||
stream: 'Stream Output',
|
||||
use_conversation_history: 'Use Conversation History',
|
||||
// process.py - CodeExecutorNode
|
||||
language: 'Programming Language',
|
||||
code: 'Code',
|
||||
// process.py - HTTPRequestNode
|
||||
url: 'Request URL',
|
||||
method: 'Request Method',
|
||||
auth_config: 'Auth Configuration',
|
||||
// process.py - DataTransformNode
|
||||
transform_type: 'Transform Type',
|
||||
template: 'Template',
|
||||
expression: 'Expression',
|
||||
output_type: 'Output Type',
|
||||
// process.py - QuestionClassifierNode
|
||||
categories: 'Categories',
|
||||
instruction: 'Instruction',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: 'Parameter Definitions',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
knowledge_bases: 'Knowledge Bases',
|
||||
top_k: 'Top K Results',
|
||||
score_threshold: 'Score Threshold',
|
||||
search_method: 'Search Method',
|
||||
enable_citations: 'Enable Citations',
|
||||
// control.py - ConditionNode
|
||||
condition_type: 'Condition Type',
|
||||
condition_expression: 'Condition Expression',
|
||||
left_value: 'Left Value',
|
||||
operator: 'Comparison Operator',
|
||||
right_value: 'Right Value',
|
||||
// control.py - SwitchNode
|
||||
cases: 'Branch Cases',
|
||||
// control.py - LoopNode
|
||||
max_iterations: 'Max Iterations',
|
||||
break_condition: 'Break Condition',
|
||||
// control.py - IteratorNode
|
||||
parallel: 'Parallel Processing',
|
||||
max_concurrency: 'Max Concurrency',
|
||||
// control.py - ParallelNode
|
||||
branches: 'Branch Configuration',
|
||||
wait_all: 'Wait All',
|
||||
fail_fast: 'Fail Fast',
|
||||
// control.py - WaitNode
|
||||
duration: 'Duration',
|
||||
duration_type: 'Time Unit',
|
||||
// control.py - MergeNode
|
||||
merge_strategy: 'Merge Strategy',
|
||||
// control.py - VariableAggregatorNode
|
||||
variable_mappings: 'Variable Mappings',
|
||||
aggregation_mode: 'Aggregation Mode',
|
||||
// action.py - SendMessageNode
|
||||
target_type: 'Target Type',
|
||||
target_id: 'Target ID',
|
||||
platform: 'Platform',
|
||||
message_type: 'Message Type',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_mode: 'Reply Mode',
|
||||
message_template: 'Message Template',
|
||||
long_text_processing: 'Long Text Processing',
|
||||
force_delay: 'Force Delay',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_uuid: 'Pipeline',
|
||||
inherit_context: 'Inherit Context',
|
||||
// action.py - StoreDataNode
|
||||
storage_type: 'Storage Type',
|
||||
ttl: 'TTL',
|
||||
key_prefix: 'Key Prefix',
|
||||
// action.py - SetVariableNode
|
||||
variable_name: 'Variable Name',
|
||||
variable_scope: 'Variable Scope',
|
||||
set_variable_operation: 'Operation Type',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: 'Opening Statement',
|
||||
suggested_questions: 'Suggested Questions',
|
||||
show_suggestions: 'Show Suggestions',
|
||||
// action.py - EndNode
|
||||
output_format: 'Output Format',
|
||||
success_message: 'Success Message',
|
||||
// integration.py - DatabaseQueryNode
|
||||
connection_type: 'Database Type',
|
||||
connection_string: 'Connection String',
|
||||
query: 'SQL Query',
|
||||
query_type: 'Query Type',
|
||||
// integration.py - RedisOperationNode
|
||||
connection_url: 'Connection URL',
|
||||
operation: 'Operation Type',
|
||||
key_template: 'Key Template',
|
||||
hash_field: 'Hash Field',
|
||||
// integration.py - MCPToolNode
|
||||
server_name: 'Server Name',
|
||||
tool_name: 'Tool Name',
|
||||
arguments_template: 'Arguments Template',
|
||||
// integration.py - MemoryStoreNode
|
||||
scope: 'Scope',
|
||||
// integration.py - DifyWorkflowNode
|
||||
'base-url': 'Base URL',
|
||||
'api-key': 'API Key',
|
||||
'app-type': 'App Type',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
dataset_id: 'Dataset ID',
|
||||
// integration.py - N8nWorkflowNode
|
||||
'webhook-url': 'Webhook URL',
|
||||
// integration.py - LangflowFlowNode
|
||||
'flow-id': 'Flow ID',
|
||||
// integration.py - CozeBotNode
|
||||
'bot-id': 'Bot ID',
|
||||
'api-base': 'API Base URL',
|
||||
},
|
||||
// Node outputs i18n
|
||||
nodeOutputs: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
message: 'Message Content',
|
||||
sender_id: 'Sender ID',
|
||||
sender_name: 'Sender Name',
|
||||
platform: 'Platform',
|
||||
conversation_id: 'Conversation ID',
|
||||
is_group: 'Is Group Chat',
|
||||
context: 'Full Message Context',
|
||||
// trigger.py - CronTriggerNode
|
||||
timestamp: 'Trigger Time',
|
||||
schedule: 'Trigger Schedule',
|
||||
cron_timestamp: 'Cron Timestamp',
|
||||
cron_schedule: 'Cron Schedule',
|
||||
cron_context: 'Cron Context',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
body: 'Request Body',
|
||||
headers: 'Request Headers',
|
||||
query: 'Query Parameters',
|
||||
method: 'Request Method',
|
||||
webhook_body: 'Webhook Body',
|
||||
webhook_headers: 'Webhook Headers',
|
||||
webhook_query: 'Webhook Query',
|
||||
webhook_method: 'Webhook Method',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_type: 'Event Type',
|
||||
event_data: 'Event Data',
|
||||
event_timestamp: 'Event Timestamp',
|
||||
// process.py - LLMCallNode
|
||||
response: 'Model Response',
|
||||
usage: 'Token Usage Statistics',
|
||||
prompt: 'Prompt/Question',
|
||||
context_info: 'Context Information',
|
||||
// process.py - CodeExecutorNode
|
||||
output: 'Output Data',
|
||||
console: 'Console Output',
|
||||
code_input: 'Code Input',
|
||||
code_output: 'Code Output',
|
||||
// process.py - HTTPRequestNode
|
||||
status_code: 'Status Code',
|
||||
http_body: 'HTTP Body',
|
||||
http_headers: 'HTTP Headers',
|
||||
http_response: 'HTTP Response',
|
||||
response_headers: 'Response Headers',
|
||||
// process.py - DataTransformNode
|
||||
result: 'Transform Result',
|
||||
transform_input: 'Transform Input',
|
||||
transform_result: 'Transform Result',
|
||||
// process.py - QuestionClassifierNode
|
||||
category: 'Category Result',
|
||||
confidence: 'Confidence',
|
||||
all_scores: 'All Category Scores',
|
||||
question: 'User Question',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: 'Extracted Parameters',
|
||||
extraction_success: 'Extraction Success',
|
||||
extract_text: 'Input Text',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
documents: 'Retrieved Documents',
|
||||
citations: 'Citation Information',
|
||||
knowledge_context: 'Merged Context',
|
||||
knowledge_query: 'Retrieval Query',
|
||||
// control.py - ConditionNode
|
||||
true: 'True Branch Output',
|
||||
false: 'False Branch Output',
|
||||
condition_input: 'Condition Input',
|
||||
// control.py - SwitchNode
|
||||
matched_case: 'Matched Branch Output',
|
||||
default: 'Default Branch Output',
|
||||
switch_input: 'Switch Input',
|
||||
// control.py - LoopNode
|
||||
item: 'Current Item',
|
||||
index: 'Current Index',
|
||||
results: 'All Iteration Results',
|
||||
completed: 'Is Completed',
|
||||
loop_items: 'Items to Iterate',
|
||||
// control.py - IteratorNode
|
||||
is_first: 'Is First',
|
||||
is_last: 'Is Last',
|
||||
iterator_array: 'Input Array',
|
||||
iterator_item: 'Current Element',
|
||||
iterator_index: 'Current Index',
|
||||
// control.py - ParallelNode
|
||||
errors: 'Error List',
|
||||
parallel_input: 'Parallel Input',
|
||||
parallel_results: 'All Branch Results',
|
||||
// control.py - WaitNode
|
||||
wait_input: 'Passthrough Input',
|
||||
wait_output: 'Passthrough Output',
|
||||
// control.py - MergeNode
|
||||
merged: 'Merged Result',
|
||||
merge_array: 'Array Result',
|
||||
merge_input_1: 'Input 1',
|
||||
merge_input_2: 'Input 2',
|
||||
merge_input_3: 'Input 3',
|
||||
merge_input_4: 'Input 4',
|
||||
// control.py - VariableAggregatorNode
|
||||
aggregated: 'Aggregated Variables',
|
||||
aggregator_variables: 'Variable Input',
|
||||
// action.py - SendMessageNode
|
||||
status: 'Send Status',
|
||||
message_id: 'Message ID',
|
||||
target: 'Target ID',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_message: 'Reply Content',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_response: 'Pipeline Response',
|
||||
pipeline_result: 'Full Result',
|
||||
pipeline_query: 'Query Content',
|
||||
context_data: 'Context Data',
|
||||
// action.py - StoreDataNode
|
||||
store_status: 'Store Status',
|
||||
store_key: 'Store Key',
|
||||
store_value: 'Store Value',
|
||||
// action.py - SetVariableNode
|
||||
variable_value: 'Variable Value',
|
||||
variable_result: 'Set Variable Result',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: 'Opening Statement',
|
||||
suggested_questions: 'Suggested Questions',
|
||||
// action.py - EndNode
|
||||
workflow_output: 'Workflow Output',
|
||||
final_result: 'Final Result',
|
||||
// integration.py - DatabaseQueryNode
|
||||
query_results: 'Query Results',
|
||||
row_count: 'Affected/Returned Rows',
|
||||
query_success: 'Query Success',
|
||||
query_params: 'Query Parameters',
|
||||
// integration.py - RedisOperationNode
|
||||
redis_result: 'Operation Result',
|
||||
redis_success: 'Operation Success',
|
||||
redis_key: 'Redis Key',
|
||||
redis_value: 'Redis Value',
|
||||
error: 'Error Message',
|
||||
plugin_input: 'Plugin Input',
|
||||
// integration.py - MCPToolNode
|
||||
tool_result: 'Tool Execution Result',
|
||||
tool_success: 'Tool Call Success',
|
||||
mcp_arguments: 'Tool Arguments',
|
||||
// integration.py - MemoryStoreNode
|
||||
memory_result: 'Retrieved/Stored Value',
|
||||
memory_success: 'Operation Success',
|
||||
memory_value: 'Value to Store',
|
||||
// integration.py - DifyWorkflowNode
|
||||
answer: 'Dify Answer',
|
||||
dify_success: 'Call Success',
|
||||
dify_query: 'User Input/Query',
|
||||
dify_conversation_id: 'Conversation ID',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
search_results: 'Search Results',
|
||||
knowledge_base_query: 'Query Content',
|
||||
// integration.py - N8nWorkflowNode
|
||||
n8n_result: 'Workflow Execution Result',
|
||||
n8n_success: 'Call Success',
|
||||
n8n_payload: 'Workflow Input Data',
|
||||
// integration.py - LangflowFlowNode
|
||||
flow_result: 'Flow Execution Result',
|
||||
flow_success: 'Call Success',
|
||||
langflow_input: 'Input Content',
|
||||
// integration.py - CozeBotNode
|
||||
bot_answer: 'Bot Reply',
|
||||
bot_success: 'Call Success',
|
||||
coze_query: 'User Input/Query',
|
||||
coze_conversation_id: 'Conversation ID',
|
||||
bot_conversation_id: 'Conversation ID',
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default esES;
|
||||
|
||||
@@ -1298,6 +1298,726 @@
|
||||
backToWorkbench: 'ワークベンチに戻る',
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
title: 'Workflows',
|
||||
description: 'Create and manage visual workflows for complex message processing logic',
|
||||
createWorkflow: 'Create Workflow',
|
||||
selectFromSidebar: 'Select a workflow from the sidebar',
|
||||
editWorkflow: 'Edit Workflow',
|
||||
newWorkflow: 'New Workflow',
|
||||
getWorkflowListError: 'Failed to get workflow list: ',
|
||||
workflowName: 'Workflow Name',
|
||||
workflowDescription: 'Workflow Description',
|
||||
workflowNameRequired: 'Workflow name is required',
|
||||
defaultDescription: 'A workflow',
|
||||
getWorkflowError: 'Failed to get workflow: ',
|
||||
loadError: 'Failed to load workflow',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveError: 'Failed to save: ',
|
||||
createSuccess: 'Workflow created successfully',
|
||||
createError: 'Failed to create: ',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
deleteError: 'Failed to delete: ',
|
||||
deleteConfirmation: 'Are you sure you want to delete this workflow?',
|
||||
copySuccess: 'Copied successfully',
|
||||
copyError: 'Failed to copy: ',
|
||||
export: 'Export',
|
||||
import: 'Import',
|
||||
exportSuccess: 'Workflow exported',
|
||||
importSuccess: 'Workflow imported',
|
||||
importError: 'Import failed: Invalid file format',
|
||||
publish: 'Publish',
|
||||
publishSuccess: 'Published successfully',
|
||||
publishError: 'Failed to publish',
|
||||
configuration: 'Configuration',
|
||||
executions: 'Executions',
|
||||
editor: 'Editor',
|
||||
debugChat: 'チャットデバッグ',
|
||||
basicInfo: 'Basic Info',
|
||||
basicInfoDesc: 'Set workflow name, icon and description',
|
||||
basicInfoDescription: 'Set workflow name and description',
|
||||
dangerZone: 'Danger Zone',
|
||||
dangerZoneDesc: 'Irreversible operations',
|
||||
dangerZoneDescription: 'Irreversible operations',
|
||||
deleteWorkflowAction: 'Delete this workflow',
|
||||
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.',
|
||||
// Form component
|
||||
name: 'Name',
|
||||
namePlaceholder: 'Enter workflow name',
|
||||
descriptionPlaceholder: 'Enter workflow description (optional)',
|
||||
enabled: 'Enabled',
|
||||
enabledDesc: 'When enabled, the workflow can be triggered for execution',
|
||||
loading: 'Loading...',
|
||||
info: 'Workflow Info',
|
||||
uuid: 'UUID',
|
||||
version: 'Version',
|
||||
createdAt: 'Created At',
|
||||
updatedAt: 'Updated At',
|
||||
// Executions tab
|
||||
totalExecutions: '{{count}} execution(s) total',
|
||||
statistics: 'Statistics',
|
||||
successfulCount: '{{count}} successful',
|
||||
successRate: 'Success Rate',
|
||||
averageDuration: 'Avg. Duration',
|
||||
perExecution: 'per execution',
|
||||
failedExecutions: 'Failed',
|
||||
lastExecution: 'Last run',
|
||||
filterByStatus: 'Filter by status',
|
||||
allStatuses: 'All statuses',
|
||||
manualTrigger: 'Manual Trigger',
|
||||
executionId: 'Execution ID',
|
||||
status: 'Status',
|
||||
triggerType: 'Trigger Type',
|
||||
startedAt: 'Started At',
|
||||
duration: 'Duration',
|
||||
noExecutions: 'No executions yet',
|
||||
executionDetails: 'Execution Details',
|
||||
error: 'Error',
|
||||
nodeExecutions: 'Node Executions',
|
||||
result: 'Result',
|
||||
'status.pending': 'Pending',
|
||||
'status.running': 'Running',
|
||||
'status.completed': 'Completed',
|
||||
'status.failed': 'Failed',
|
||||
'status.cancelled': 'Cancelled',
|
||||
// Editor component translations
|
||||
nodePalette: 'Node Palette',
|
||||
properties: 'Properties',
|
||||
zoomIn: 'Zoom In',
|
||||
zoomOut: 'Zoom Out',
|
||||
fitView: 'Fit View',
|
||||
unsavedChanges: 'Unsaved changes',
|
||||
paste: 'Paste',
|
||||
deleted: 'Deleted',
|
||||
nothingToCopy: 'No nodes selected to copy',
|
||||
nothingToPaste: 'Clipboard is empty',
|
||||
copied: 'Copied {{count}} node(s)',
|
||||
pasted: 'Pasted {{count}} node(s)',
|
||||
nodesSelected: '{{count}} node(s) selected',
|
||||
edgesSelected: '{{count}} edge(s) selected',
|
||||
// Node palette
|
||||
searchNodes: 'Search nodes...',
|
||||
loadingNodeTypes: 'Loading node types...',
|
||||
noNodesFound: 'No matching nodes found',
|
||||
clearSearch: 'Clear search',
|
||||
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',
|
||||
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.',
|
||||
deleteEdge: 'Delete Edge',
|
||||
deleteEdgeConfirm: 'Confirm Delete Edge',
|
||||
deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
|
||||
nodeLabel: 'Node Label',
|
||||
nodeLabelPlaceholder: 'Enter node display name',
|
||||
nodeId: 'Node ID',
|
||||
inputOutputVariables: 'Input/Output Variables',
|
||||
inputs: 'Inputs',
|
||||
outputs: 'Outputs',
|
||||
availableVariables: 'Available Variables',
|
||||
globalVariables: 'Global Variables',
|
||||
messageContent: 'Message Content',
|
||||
messageSender: 'Sender',
|
||||
platform: 'Platform',
|
||||
sessionId: 'Session ID',
|
||||
timestamp: 'Timestamp',
|
||||
nodeConfig: 'Node Configuration',
|
||||
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.',
|
||||
// Node inputs/outputs i18n (for port labels)
|
||||
nodeInputs: {
|
||||
// Common inputs
|
||||
input: 'Input',
|
||||
message: 'Message',
|
||||
text: 'Text',
|
||||
query: 'Query',
|
||||
data: 'Data',
|
||||
condition: 'Condition',
|
||||
value: 'Value',
|
||||
// Trigger inputs
|
||||
content: 'Content',
|
||||
context: 'Context',
|
||||
body: 'Request Body',
|
||||
variables: 'Variables',
|
||||
items: 'Items',
|
||||
arguments: 'Arguments',
|
||||
// AI/Process inputs
|
||||
question: 'Question',
|
||||
parameters: 'Parameters',
|
||||
key: 'Key',
|
||||
// Control inputs
|
||||
case_1: 'Case 1',
|
||||
case_2: 'Case 2',
|
||||
branch_1: 'Branch 1',
|
||||
branch_2: 'Branch 2',
|
||||
// Action inputs
|
||||
notification_id: 'Notification ID',
|
||||
// Integration inputs
|
||||
key_template: 'Key Template',
|
||||
hash_field: 'Hash Field',
|
||||
server_name: 'Server Name',
|
||||
tool_name: 'Tool Name',
|
||||
arguments_template: 'Arguments Template',
|
||||
scope: 'Scope',
|
||||
},
|
||||
// Data type labels
|
||||
'type.string': 'String',
|
||||
'type.object': 'Object',
|
||||
'type.array': 'Array',
|
||||
'type.boolean': 'Boolean',
|
||||
'type.number': 'Number',
|
||||
'type.any': 'Any',
|
||||
'type.datetime': 'DateTime',
|
||||
nodes: {
|
||||
trigger: 'Triggers',
|
||||
triggerDescription: 'Starting nodes for workflows',
|
||||
messageTrigger: 'Message Trigger',
|
||||
messageTriggerDescription: 'Triggered when a message is received',
|
||||
scheduleTrigger: 'Schedule Trigger',
|
||||
scheduleTriggerDescription: 'Triggered on a schedule',
|
||||
cronTrigger: 'Cron Trigger',
|
||||
cronTriggerDescription: 'Trigger workflow on a scheduled time',
|
||||
webhookTrigger: 'Webhook Trigger',
|
||||
webhookTriggerDescription: 'Triggered via HTTP request',
|
||||
eventTrigger: 'Event Trigger',
|
||||
eventTriggerDescription: 'Triggered on system events',
|
||||
process: 'AI/Process',
|
||||
processDescription: 'Data processing nodes',
|
||||
aiProcess: 'AI Processing',
|
||||
aiProcessDescription: 'Process messages using AI models',
|
||||
llmCall: 'LLM Call',
|
||||
llmCallDescription: 'Invoke large language model for conversation or generation',
|
||||
codeProcess: 'Code Processing',
|
||||
codeProcessDescription: 'Execute custom code',
|
||||
codeExecutor: 'Code Executor',
|
||||
codeExecutorDescription: 'Execute Python/JavaScript code',
|
||||
templateProcess: 'Template Processing',
|
||||
templateProcessDescription: 'Format output using templates',
|
||||
httpRequest: 'HTTP Request',
|
||||
httpRequestDescription: 'Send HTTP requests',
|
||||
dataTransform: 'Data Transform',
|
||||
dataTransformDescription: 'Transform data format',
|
||||
questionClassifier: 'Question Classifier',
|
||||
questionClassifierDescription: 'Classify user questions into predefined categories using LLM',
|
||||
parameterExtractor: 'Parameter Extractor',
|
||||
parameterExtractorDescription: 'Extract structured parameters from text using LLM',
|
||||
knowledgeRetrieval: 'Knowledge Retrieval',
|
||||
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base',
|
||||
textTemplate: 'Text Template',
|
||||
textTemplateDescription: 'Generate text using templates with variable interpolation',
|
||||
jsonTransform: 'JSON Transform',
|
||||
jsonTransformDescription: 'Transform JSON data using expressions',
|
||||
dataAggregator: 'Data Aggregator',
|
||||
dataAggregatorDescription: 'Aggregate data from multiple sources',
|
||||
textSplitter: 'Text Splitter',
|
||||
textSplitterDescription: 'Split text into smaller chunks',
|
||||
variableAssignment: 'Variable Assignment',
|
||||
variableAssignmentDescription: 'Assign values to workflow variables',
|
||||
control: 'Control Flow',
|
||||
controlDescription: 'Flow control nodes',
|
||||
condition: 'Condition Branch',
|
||||
conditionDescription: 'Branch based on conditions',
|
||||
switch: 'Switch',
|
||||
switchDescription: 'Multi-way branching',
|
||||
loop: 'Loop',
|
||||
loopDescription: 'Repeat execution',
|
||||
iterator: 'Iterator',
|
||||
iteratorDescription: 'Iterate over array elements',
|
||||
parallel: 'Parallel',
|
||||
parallelDescription: 'Execute multiple branches in parallel',
|
||||
wait: 'Wait',
|
||||
waitDescription: 'Wait for specified time',
|
||||
delay: 'Delay',
|
||||
delayDescription: 'Wait for a specified time',
|
||||
merge: 'Merge',
|
||||
mergeDescription: 'Merge multiple branches',
|
||||
variableAggregator: 'Variable Aggregator',
|
||||
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',
|
||||
storeData: 'Store Data',
|
||||
storeDataDescription: 'Store data to database',
|
||||
callPipeline: 'Call Pipeline',
|
||||
callPipelineDescription: 'Call an existing Pipeline',
|
||||
setVariable: 'Set Variable',
|
||||
setVariableDescription: 'Set context variable',
|
||||
openingStatement: 'Opening Statement',
|
||||
openingStatementDescription: 'Provide conversation opener and suggested questions',
|
||||
end: 'End',
|
||||
endDescription: 'Mark the end of workflow execution',
|
||||
log: 'Log',
|
||||
logDescription: 'Record log information',
|
||||
integration: 'Integration',
|
||||
integrationDescription: 'Third-party platform integration nodes',
|
||||
difyWorkflow: 'Dify Workflow',
|
||||
difyWorkflowDescription: 'Call Dify platform workflow',
|
||||
difyKnowledgeQuery: 'Dify Knowledge Query',
|
||||
difyKnowledgeQueryDescription: 'Query Dify knowledge base',
|
||||
n8nWorkflow: 'N8n Workflow',
|
||||
n8nWorkflowDescription: 'Call n8n workflow',
|
||||
langflowFlow: 'Langflow Flow',
|
||||
langflowFlowDescription: 'Call Langflow flow',
|
||||
cozeBot: 'Coze Bot',
|
||||
cozeBotDescription: 'Call Coze Bot',
|
||||
// Data & Tools integration nodes
|
||||
databaseQuery: 'Database Query',
|
||||
databaseQueryDescription: 'Execute database queries',
|
||||
redisOperation: 'Redis Operation',
|
||||
redisOperationDescription: 'Perform Redis cache operations',
|
||||
mcpTool: 'MCP Tool',
|
||||
mcpToolDescription: 'Invoke an MCP tool',
|
||||
memoryStore: 'Memory Store',
|
||||
memoryStoreDescription: 'Store and retrieve data from workflow memory',
|
||||
},
|
||||
executionHistory: {
|
||||
title: 'Execution History',
|
||||
noExecutions: 'No executions yet',
|
||||
status: 'Status',
|
||||
startTime: 'Start Time',
|
||||
duration: 'Duration',
|
||||
running: 'Running',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled',
|
||||
viewDetails: 'View Details',
|
||||
cancel: 'Cancel Execution',
|
||||
retry: 'Retry',
|
||||
nodeResults: 'Node Execution Results',
|
||||
},
|
||||
versions: {
|
||||
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.',
|
||||
rollbackSuccess: 'Rollback successful',
|
||||
rollbackError: 'Failed to rollback: ',
|
||||
},
|
||||
// Debug and monitoring
|
||||
debug: 'Debug',
|
||||
debugMode: 'Debug Mode',
|
||||
debugPanel: 'Debug Panel',
|
||||
startDebug: 'Start Debug',
|
||||
pauseDebug: 'Pause',
|
||||
resumeDebug: 'Resume',
|
||||
stepDebug: 'Step',
|
||||
stopDebug: 'Stop',
|
||||
debugContext: 'Debug Context',
|
||||
simulatedMessage: 'Simulated Message',
|
||||
simulatedMessagePlaceholder: 'Enter the message content to simulate',
|
||||
senderId: 'Sender ID',
|
||||
senderIdPlaceholder: 'Sender unique identifier',
|
||||
senderName: 'Sender Name',
|
||||
senderNamePlaceholder: 'Sender display name',
|
||||
conversationId: 'Conversation ID',
|
||||
conversationIdPlaceholder: 'Conversation unique identifier',
|
||||
isGroup: 'Group Chat',
|
||||
customVariables: 'Custom Variables',
|
||||
addVariable: 'Add Variable',
|
||||
variableName: 'Variable Name',
|
||||
variableValue: 'Variable Value',
|
||||
watchedVariables: 'Watched Variables',
|
||||
addWatchVariable: 'Add Watch',
|
||||
nodeStates: 'Node States',
|
||||
breakpoints: 'Breakpoints',
|
||||
toggleBreakpoint: 'Toggle Breakpoint',
|
||||
breakpointSet: 'Breakpoint set',
|
||||
breakpointRemoved: 'Breakpoint removed',
|
||||
debugLogs: 'Debug Logs',
|
||||
noLogs: 'No logs yet',
|
||||
clearLogs: 'Clear Logs',
|
||||
autoScroll: 'Auto Scroll',
|
||||
debugState: {
|
||||
idle: 'Idle',
|
||||
running: 'Running',
|
||||
paused: 'Paused',
|
||||
completed: 'Completed',
|
||||
error: 'Error',
|
||||
},
|
||||
nodeStatus: {
|
||||
pending: 'Pending',
|
||||
running: 'Running',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
skipped: 'Skipped',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'ワークフロー チャット',
|
||||
selectWorkflow: 'ワークフローを選択',
|
||||
sessionType: 'セッションタイプ',
|
||||
privateChat: 'プライベートチャット',
|
||||
groupChat: 'グループチャット',
|
||||
send: '送信',
|
||||
reset: '会話をリセット',
|
||||
inputPlaceholder: '{{type}} メッセージを送信...',
|
||||
noMessages: 'メッセージなし',
|
||||
userMessage: 'ユーザー',
|
||||
botMessage: 'ボット',
|
||||
sendFailed: '送信失敗',
|
||||
resetSuccess: '会話がリセットされました',
|
||||
resetFailed: 'リセット失敗',
|
||||
loadMessagesFailed: 'メッセージの読み込みに失敗しました',
|
||||
loadWorkflowsFailed: 'ワークフローの読み込みに失敗しました',
|
||||
atTips: 'ボットをメンション',
|
||||
streaming: 'ストリーミング',
|
||||
streamOutput: 'ストリーム',
|
||||
connected: 'WebSocket接続済み',
|
||||
disconnected: 'WebSocket未接続',
|
||||
connectionError: 'WebSocket接続エラー',
|
||||
connectionFailed: 'WebSocket接続失敗',
|
||||
notConnected: 'WebSocket未接続です。しばらくしてからもう一度お試しください',
|
||||
imageUploadFailed: '画像アップロード失敗',
|
||||
reply: '返信',
|
||||
replyTo: '返信先',
|
||||
showMarkdown: 'レンダリング',
|
||||
showRaw: '原文',
|
||||
allMembers: '全メンバー',
|
||||
file: 'ファイル',
|
||||
voice: '音声',
|
||||
uploadImage: '画像をアップロード',
|
||||
uploading: 'アップロード中...',
|
||||
},
|
||||
// Execution history and monitoring
|
||||
filterByDate: 'Filter by Date',
|
||||
allTime: 'All Time',
|
||||
today: 'Today',
|
||||
lastWeek: 'Last Week',
|
||||
lastMonth: 'Last Month',
|
||||
showingExecutions: 'Showing {{shown}} / {{total}} executions',
|
||||
rerun: 'Rerun',
|
||||
rerunExecution: 'Rerun Execution',
|
||||
logs: 'Logs',
|
||||
details: 'Details',
|
||||
completedAt: 'Completed At',
|
||||
noNodeExecutions: 'No node executions yet',
|
||||
// Node config field labels (used by DynamicFormComponent)
|
||||
nodeConfigFields: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
conditions: 'Trigger Conditions',
|
||||
keyword_filter: 'Keyword Filter',
|
||||
regex_filter: 'Regex Filter',
|
||||
min_length: 'Min Length',
|
||||
max_length: 'Max Length',
|
||||
require_mention: 'Require Mention',
|
||||
respond_rules: 'Group Respond Rules',
|
||||
access_control: 'Access Control',
|
||||
// trigger.py - CronTriggerNode
|
||||
cron: 'Cron Expression',
|
||||
timezone: 'Timezone',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
path: 'Webhook Path',
|
||||
allowed_methods: 'Allowed HTTP Methods',
|
||||
content_type: 'Content-Type',
|
||||
auth_type: 'Authentication Type',
|
||||
auth_key: 'Auth Key',
|
||||
validation: 'Request Validation',
|
||||
timeout: 'Timeout',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_types: 'Event Types',
|
||||
filter: 'Event Filter',
|
||||
debounce_ms: 'Debounce Time',
|
||||
// process.py - LLMCallNode
|
||||
model: 'Model',
|
||||
prompt_template: 'Prompt Template',
|
||||
system_prompt: 'System Prompt',
|
||||
temperature: 'Temperature',
|
||||
top_p: 'Top P',
|
||||
frequency_penalty: 'Frequency Penalty',
|
||||
presence_penalty: 'Presence Penalty',
|
||||
max_tokens: 'Max Tokens',
|
||||
stop_sequences: 'Stop Sequences',
|
||||
seed: 'Random Seed',
|
||||
stream: 'Stream Output',
|
||||
use_conversation_history: 'Use Conversation History',
|
||||
// process.py - CodeExecutorNode
|
||||
language: 'Programming Language',
|
||||
code: 'Code',
|
||||
// process.py - HTTPRequestNode
|
||||
url: 'Request URL',
|
||||
method: 'Request Method',
|
||||
auth_config: 'Auth Configuration',
|
||||
// process.py - DataTransformNode
|
||||
transform_type: 'Transform Type',
|
||||
template: 'Template',
|
||||
expression: 'Expression',
|
||||
output_type: 'Output Type',
|
||||
// process.py - QuestionClassifierNode
|
||||
categories: 'Categories',
|
||||
instruction: 'Instruction',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: 'Parameter Definitions',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
knowledge_bases: 'Knowledge Bases',
|
||||
top_k: 'Top K Results',
|
||||
score_threshold: 'Score Threshold',
|
||||
search_method: 'Search Method',
|
||||
enable_citations: 'Enable Citations',
|
||||
// control.py - ConditionNode
|
||||
condition_type: 'Condition Type',
|
||||
condition_expression: 'Condition Expression',
|
||||
left_value: 'Left Value',
|
||||
operator: 'Comparison Operator',
|
||||
right_value: 'Right Value',
|
||||
// control.py - SwitchNode
|
||||
cases: 'Branch Cases',
|
||||
// control.py - LoopNode
|
||||
max_iterations: 'Max Iterations',
|
||||
break_condition: 'Break Condition',
|
||||
// control.py - IteratorNode
|
||||
parallel: 'Parallel Processing',
|
||||
max_concurrency: 'Max Concurrency',
|
||||
// control.py - ParallelNode
|
||||
branches: 'Branch Configuration',
|
||||
wait_all: 'Wait All',
|
||||
fail_fast: 'Fail Fast',
|
||||
// control.py - WaitNode
|
||||
duration: 'Duration',
|
||||
duration_type: 'Time Unit',
|
||||
// control.py - MergeNode
|
||||
merge_strategy: 'Merge Strategy',
|
||||
// control.py - VariableAggregatorNode
|
||||
variable_mappings: 'Variable Mappings',
|
||||
aggregation_mode: 'Aggregation Mode',
|
||||
// action.py - SendMessageNode
|
||||
target_type: 'Target Type',
|
||||
target_id: 'Target ID',
|
||||
platform: 'Platform',
|
||||
message_type: 'Message Type',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_mode: 'Reply Mode',
|
||||
message_template: 'Message Template',
|
||||
long_text_processing: 'Long Text Processing',
|
||||
force_delay: 'Force Delay',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_uuid: 'Pipeline',
|
||||
inherit_context: 'Inherit Context',
|
||||
// action.py - StoreDataNode
|
||||
storage_type: 'Storage Type',
|
||||
ttl: 'TTL',
|
||||
key_prefix: 'Key Prefix',
|
||||
// action.py - SetVariableNode
|
||||
variable_name: 'Variable Name',
|
||||
variable_scope: 'Variable Scope',
|
||||
set_variable_operation: 'Operation Type',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: 'Opening Statement',
|
||||
suggested_questions: 'Suggested Questions',
|
||||
show_suggestions: 'Show Suggestions',
|
||||
// action.py - EndNode
|
||||
output_format: 'Output Format',
|
||||
success_message: 'Success Message',
|
||||
// integration.py - DatabaseQueryNode
|
||||
connection_type: 'Database Type',
|
||||
connection_string: 'Connection String',
|
||||
query: 'SQL Query',
|
||||
query_type: 'Query Type',
|
||||
// integration.py - RedisOperationNode
|
||||
connection_url: 'Connection URL',
|
||||
operation: 'Operation Type',
|
||||
key_template: 'Key Template',
|
||||
hash_field: 'Hash Field',
|
||||
// integration.py - MCPToolNode
|
||||
server_name: 'Server Name',
|
||||
tool_name: 'Tool Name',
|
||||
arguments_template: 'Arguments Template',
|
||||
// integration.py - MemoryStoreNode
|
||||
scope: 'Scope',
|
||||
// integration.py - DifyWorkflowNode
|
||||
'base-url': 'Base URL',
|
||||
'api-key': 'API Key',
|
||||
'app-type': 'App Type',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
dataset_id: 'Dataset ID',
|
||||
// integration.py - N8nWorkflowNode
|
||||
'webhook-url': 'Webhook URL',
|
||||
// integration.py - LangflowFlowNode
|
||||
'flow-id': 'Flow ID',
|
||||
// integration.py - CozeBotNode
|
||||
'bot-id': 'Bot ID',
|
||||
'api-base': 'API Base URL',
|
||||
},
|
||||
// Node outputs i18n
|
||||
nodeOutputs: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
message: 'Message Content',
|
||||
sender_id: 'Sender ID',
|
||||
sender_name: 'Sender Name',
|
||||
platform: 'Platform',
|
||||
conversation_id: 'Conversation ID',
|
||||
is_group: 'Is Group Chat',
|
||||
context: 'Full Message Context',
|
||||
// trigger.py - CronTriggerNode
|
||||
timestamp: 'Trigger Time',
|
||||
schedule: 'Trigger Schedule',
|
||||
cron_timestamp: 'Cron Timestamp',
|
||||
cron_schedule: 'Cron Schedule',
|
||||
cron_context: 'Cron Context',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
body: 'Request Body',
|
||||
headers: 'Request Headers',
|
||||
query: 'Query Parameters',
|
||||
method: 'Request Method',
|
||||
webhook_body: 'Webhook Body',
|
||||
webhook_headers: 'Webhook Headers',
|
||||
webhook_query: 'Webhook Query',
|
||||
webhook_method: 'Webhook Method',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_type: 'Event Type',
|
||||
event_data: 'Event Data',
|
||||
event_timestamp: 'Event Timestamp',
|
||||
// process.py - LLMCallNode
|
||||
response: 'Model Response',
|
||||
usage: 'Token Usage Statistics',
|
||||
prompt: 'Prompt/Question',
|
||||
context_info: 'Context Information',
|
||||
// process.py - CodeExecutorNode
|
||||
output: 'Output Data',
|
||||
console: 'Console Output',
|
||||
code_input: 'Code Input',
|
||||
code_output: 'Code Output',
|
||||
// process.py - HTTPRequestNode
|
||||
status_code: 'Status Code',
|
||||
http_body: 'HTTP Body',
|
||||
http_headers: 'HTTP Headers',
|
||||
http_response: 'HTTP Response',
|
||||
response_headers: 'Response Headers',
|
||||
// process.py - DataTransformNode
|
||||
result: 'Transform Result',
|
||||
transform_input: 'Transform Input',
|
||||
transform_result: 'Transform Result',
|
||||
// process.py - QuestionClassifierNode
|
||||
category: 'Category Result',
|
||||
confidence: 'Confidence',
|
||||
all_scores: 'All Category Scores',
|
||||
question: 'User Question',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: 'Extracted Parameters',
|
||||
extraction_success: 'Extraction Success',
|
||||
extract_text: 'Input Text',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
documents: 'Retrieved Documents',
|
||||
citations: 'Citation Information',
|
||||
knowledge_context: 'Merged Context',
|
||||
knowledge_query: 'Retrieval Query',
|
||||
// control.py - ConditionNode
|
||||
true: 'True Branch Output',
|
||||
false: 'False Branch Output',
|
||||
condition_input: 'Condition Input',
|
||||
// control.py - SwitchNode
|
||||
matched_case: 'Matched Branch Output',
|
||||
default: 'Default Branch Output',
|
||||
switch_input: 'Switch Input',
|
||||
// control.py - LoopNode
|
||||
item: 'Current Item',
|
||||
index: 'Current Index',
|
||||
results: 'All Iteration Results',
|
||||
completed: 'Is Completed',
|
||||
loop_items: 'Items to Iterate',
|
||||
// control.py - IteratorNode
|
||||
is_first: 'Is First',
|
||||
is_last: 'Is Last',
|
||||
iterator_array: 'Input Array',
|
||||
iterator_item: 'Current Element',
|
||||
iterator_index: 'Current Index',
|
||||
// control.py - ParallelNode
|
||||
errors: 'Error List',
|
||||
parallel_input: 'Parallel Input',
|
||||
parallel_results: 'All Branch Results',
|
||||
// control.py - WaitNode
|
||||
wait_input: 'Passthrough Input',
|
||||
wait_output: 'Passthrough Output',
|
||||
// control.py - MergeNode
|
||||
merged: 'Merged Result',
|
||||
merge_array: 'Array Result',
|
||||
merge_input_1: 'Input 1',
|
||||
merge_input_2: 'Input 2',
|
||||
merge_input_3: 'Input 3',
|
||||
merge_input_4: 'Input 4',
|
||||
// control.py - VariableAggregatorNode
|
||||
aggregated: 'Aggregated Variables',
|
||||
aggregator_variables: 'Variable Input',
|
||||
// action.py - SendMessageNode
|
||||
status: 'Send Status',
|
||||
message_id: 'Message ID',
|
||||
target: 'Target ID',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_message: 'Reply Content',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_response: 'Pipeline Response',
|
||||
pipeline_result: 'Full Result',
|
||||
pipeline_query: 'Query Content',
|
||||
context_data: 'Context Data',
|
||||
// action.py - StoreDataNode
|
||||
store_status: 'Store Status',
|
||||
store_key: 'Store Key',
|
||||
store_value: 'Store Value',
|
||||
// action.py - SetVariableNode
|
||||
variable_value: 'Variable Value',
|
||||
variable_result: 'Set Variable Result',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: 'Opening Statement',
|
||||
suggested_questions: 'Suggested Questions',
|
||||
// action.py - EndNode
|
||||
workflow_output: 'Workflow Output',
|
||||
final_result: 'Final Result',
|
||||
// integration.py - DatabaseQueryNode
|
||||
query_results: 'Query Results',
|
||||
row_count: 'Affected/Returned Rows',
|
||||
query_success: 'Query Success',
|
||||
query_params: 'Query Parameters',
|
||||
// integration.py - RedisOperationNode
|
||||
redis_result: 'Operation Result',
|
||||
redis_success: 'Operation Success',
|
||||
redis_key: 'Redis Key',
|
||||
redis_value: 'Redis Value',
|
||||
error: 'Error Message',
|
||||
plugin_input: 'Plugin Input',
|
||||
// integration.py - MCPToolNode
|
||||
tool_result: 'Tool Execution Result',
|
||||
tool_success: 'Tool Call Success',
|
||||
mcp_arguments: 'Tool Arguments',
|
||||
// integration.py - MemoryStoreNode
|
||||
memory_result: 'Retrieved/Stored Value',
|
||||
memory_success: 'Operation Success',
|
||||
memory_value: 'Value to Store',
|
||||
// integration.py - DifyWorkflowNode
|
||||
answer: 'Dify Answer',
|
||||
dify_success: 'Call Success',
|
||||
dify_query: 'User Input/Query',
|
||||
dify_conversation_id: 'Conversation ID',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
search_results: 'Search Results',
|
||||
knowledge_base_query: 'Query Content',
|
||||
// integration.py - N8nWorkflowNode
|
||||
n8n_result: 'Workflow Execution Result',
|
||||
n8n_success: 'Call Success',
|
||||
n8n_payload: 'Workflow Input Data',
|
||||
// integration.py - LangflowFlowNode
|
||||
flow_result: 'Flow Execution Result',
|
||||
flow_success: 'Call Success',
|
||||
langflow_input: 'Input Content',
|
||||
// integration.py - CozeBotNode
|
||||
bot_answer: 'Bot Reply',
|
||||
bot_success: 'Call Success',
|
||||
coze_query: 'User Input/Query',
|
||||
coze_conversation_id: 'Conversation ID',
|
||||
bot_conversation_id: 'Conversation ID',
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default jaJP;
|
||||
|
||||
@@ -1300,6 +1300,726 @@ const ruRU = {
|
||||
backToWorkbench: 'Вернуться к рабочей панели',
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
title: 'Workflows',
|
||||
description: 'Create and manage visual workflows for complex message processing logic',
|
||||
createWorkflow: 'Create Workflow',
|
||||
selectFromSidebar: 'Select a workflow from the sidebar',
|
||||
editWorkflow: 'Edit Workflow',
|
||||
newWorkflow: 'New Workflow',
|
||||
getWorkflowListError: 'Failed to get workflow list: ',
|
||||
workflowName: 'Workflow Name',
|
||||
workflowDescription: 'Workflow Description',
|
||||
workflowNameRequired: 'Workflow name is required',
|
||||
defaultDescription: 'A workflow',
|
||||
getWorkflowError: 'Failed to get workflow: ',
|
||||
loadError: 'Failed to load workflow',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveError: 'Failed to save: ',
|
||||
createSuccess: 'Workflow created successfully',
|
||||
createError: 'Failed to create: ',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
deleteError: 'Failed to delete: ',
|
||||
deleteConfirmation: 'Are you sure you want to delete this workflow?',
|
||||
copySuccess: 'Copied successfully',
|
||||
copyError: 'Failed to copy: ',
|
||||
export: 'Export',
|
||||
import: 'Import',
|
||||
exportSuccess: 'Workflow exported',
|
||||
importSuccess: 'Workflow imported',
|
||||
importError: 'Import failed: Invalid file format',
|
||||
publish: 'Publish',
|
||||
publishSuccess: 'Published successfully',
|
||||
publishError: 'Failed to publish',
|
||||
configuration: 'Configuration',
|
||||
executions: 'Executions',
|
||||
editor: 'Editor',
|
||||
debugChat: 'Отладочный чат',
|
||||
basicInfo: 'Basic Info',
|
||||
basicInfoDesc: 'Set workflow name, icon and description',
|
||||
basicInfoDescription: 'Set workflow name and description',
|
||||
dangerZone: 'Danger Zone',
|
||||
dangerZoneDesc: 'Irreversible operations',
|
||||
dangerZoneDescription: 'Irreversible operations',
|
||||
deleteWorkflowAction: 'Delete this workflow',
|
||||
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.',
|
||||
// Form component
|
||||
name: 'Name',
|
||||
namePlaceholder: 'Enter workflow name',
|
||||
descriptionPlaceholder: 'Enter workflow description (optional)',
|
||||
enabled: 'Enabled',
|
||||
enabledDesc: 'When enabled, the workflow can be triggered for execution',
|
||||
loading: 'Loading...',
|
||||
info: 'Workflow Info',
|
||||
uuid: 'UUID',
|
||||
version: 'Version',
|
||||
createdAt: 'Created At',
|
||||
updatedAt: 'Updated At',
|
||||
// Executions tab
|
||||
totalExecutions: '{{count}} execution(s) total',
|
||||
statistics: 'Statistics',
|
||||
successfulCount: '{{count}} successful',
|
||||
successRate: 'Success Rate',
|
||||
averageDuration: 'Avg. Duration',
|
||||
perExecution: 'per execution',
|
||||
failedExecutions: 'Failed',
|
||||
lastExecution: 'Last run',
|
||||
filterByStatus: 'Filter by status',
|
||||
allStatuses: 'All statuses',
|
||||
manualTrigger: 'Manual Trigger',
|
||||
executionId: 'Execution ID',
|
||||
status: 'Status',
|
||||
triggerType: 'Trigger Type',
|
||||
startedAt: 'Started At',
|
||||
duration: 'Duration',
|
||||
noExecutions: 'No executions yet',
|
||||
executionDetails: 'Execution Details',
|
||||
error: 'Error',
|
||||
nodeExecutions: 'Node Executions',
|
||||
result: 'Result',
|
||||
'status.pending': 'Pending',
|
||||
'status.running': 'Running',
|
||||
'status.completed': 'Completed',
|
||||
'status.failed': 'Failed',
|
||||
'status.cancelled': 'Cancelled',
|
||||
// Editor component translations
|
||||
nodePalette: 'Node Palette',
|
||||
properties: 'Properties',
|
||||
zoomIn: 'Zoom In',
|
||||
zoomOut: 'Zoom Out',
|
||||
fitView: 'Fit View',
|
||||
unsavedChanges: 'Unsaved changes',
|
||||
paste: 'Paste',
|
||||
deleted: 'Deleted',
|
||||
nothingToCopy: 'No nodes selected to copy',
|
||||
nothingToPaste: 'Clipboard is empty',
|
||||
copied: 'Copied {{count}} node(s)',
|
||||
pasted: 'Pasted {{count}} node(s)',
|
||||
nodesSelected: '{{count}} node(s) selected',
|
||||
edgesSelected: '{{count}} edge(s) selected',
|
||||
// Node palette
|
||||
searchNodes: 'Search nodes...',
|
||||
loadingNodeTypes: 'Loading node types...',
|
||||
noNodesFound: 'No matching nodes found',
|
||||
clearSearch: 'Clear search',
|
||||
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',
|
||||
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.',
|
||||
deleteEdge: 'Delete Edge',
|
||||
deleteEdgeConfirm: 'Confirm Delete Edge',
|
||||
deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
|
||||
nodeLabel: 'Node Label',
|
||||
nodeLabelPlaceholder: 'Enter node display name',
|
||||
nodeId: 'Node ID',
|
||||
inputOutputVariables: 'Input/Output Variables',
|
||||
inputs: 'Inputs',
|
||||
outputs: 'Outputs',
|
||||
availableVariables: 'Available Variables',
|
||||
globalVariables: 'Global Variables',
|
||||
messageContent: 'Message Content',
|
||||
messageSender: 'Sender',
|
||||
platform: 'Platform',
|
||||
sessionId: 'Session ID',
|
||||
timestamp: 'Timestamp',
|
||||
nodeConfig: 'Node Configuration',
|
||||
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.',
|
||||
// Node inputs/outputs i18n (for port labels)
|
||||
nodeInputs: {
|
||||
// Common inputs
|
||||
input: 'Input',
|
||||
message: 'Message',
|
||||
text: 'Text',
|
||||
query: 'Query',
|
||||
data: 'Data',
|
||||
condition: 'Condition',
|
||||
value: 'Value',
|
||||
// Trigger inputs
|
||||
content: 'Content',
|
||||
context: 'Context',
|
||||
body: 'Request Body',
|
||||
variables: 'Variables',
|
||||
items: 'Items',
|
||||
arguments: 'Arguments',
|
||||
// AI/Process inputs
|
||||
question: 'Question',
|
||||
parameters: 'Parameters',
|
||||
key: 'Key',
|
||||
// Control inputs
|
||||
case_1: 'Case 1',
|
||||
case_2: 'Case 2',
|
||||
branch_1: 'Branch 1',
|
||||
branch_2: 'Branch 2',
|
||||
// Action inputs
|
||||
notification_id: 'Notification ID',
|
||||
// Integration inputs
|
||||
key_template: 'Key Template',
|
||||
hash_field: 'Hash Field',
|
||||
server_name: 'Server Name',
|
||||
tool_name: 'Tool Name',
|
||||
arguments_template: 'Arguments Template',
|
||||
scope: 'Scope',
|
||||
},
|
||||
// Data type labels
|
||||
'type.string': 'String',
|
||||
'type.object': 'Object',
|
||||
'type.array': 'Array',
|
||||
'type.boolean': 'Boolean',
|
||||
'type.number': 'Number',
|
||||
'type.any': 'Any',
|
||||
'type.datetime': 'DateTime',
|
||||
nodes: {
|
||||
trigger: 'Triggers',
|
||||
triggerDescription: 'Starting nodes for workflows',
|
||||
messageTrigger: 'Message Trigger',
|
||||
messageTriggerDescription: 'Triggered when a message is received',
|
||||
scheduleTrigger: 'Schedule Trigger',
|
||||
scheduleTriggerDescription: 'Triggered on a schedule',
|
||||
cronTrigger: 'Cron Trigger',
|
||||
cronTriggerDescription: 'Trigger workflow on a scheduled time',
|
||||
webhookTrigger: 'Webhook Trigger',
|
||||
webhookTriggerDescription: 'Triggered via HTTP request',
|
||||
eventTrigger: 'Event Trigger',
|
||||
eventTriggerDescription: 'Triggered on system events',
|
||||
process: 'AI/Process',
|
||||
processDescription: 'Data processing nodes',
|
||||
aiProcess: 'AI Processing',
|
||||
aiProcessDescription: 'Process messages using AI models',
|
||||
llmCall: 'LLM Call',
|
||||
llmCallDescription: 'Invoke large language model for conversation or generation',
|
||||
codeProcess: 'Code Processing',
|
||||
codeProcessDescription: 'Execute custom code',
|
||||
codeExecutor: 'Code Executor',
|
||||
codeExecutorDescription: 'Execute Python/JavaScript code',
|
||||
templateProcess: 'Template Processing',
|
||||
templateProcessDescription: 'Format output using templates',
|
||||
httpRequest: 'HTTP Request',
|
||||
httpRequestDescription: 'Send HTTP requests',
|
||||
dataTransform: 'Data Transform',
|
||||
dataTransformDescription: 'Transform data format',
|
||||
questionClassifier: 'Question Classifier',
|
||||
questionClassifierDescription: 'Classify user questions into predefined categories using LLM',
|
||||
parameterExtractor: 'Parameter Extractor',
|
||||
parameterExtractorDescription: 'Extract structured parameters from text using LLM',
|
||||
knowledgeRetrieval: 'Knowledge Retrieval',
|
||||
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base',
|
||||
textTemplate: 'Text Template',
|
||||
textTemplateDescription: 'Generate text using templates with variable interpolation',
|
||||
jsonTransform: 'JSON Transform',
|
||||
jsonTransformDescription: 'Transform JSON data using expressions',
|
||||
dataAggregator: 'Data Aggregator',
|
||||
dataAggregatorDescription: 'Aggregate data from multiple sources',
|
||||
textSplitter: 'Text Splitter',
|
||||
textSplitterDescription: 'Split text into smaller chunks',
|
||||
variableAssignment: 'Variable Assignment',
|
||||
variableAssignmentDescription: 'Assign values to workflow variables',
|
||||
control: 'Control Flow',
|
||||
controlDescription: 'Flow control nodes',
|
||||
condition: 'Condition Branch',
|
||||
conditionDescription: 'Branch based on conditions',
|
||||
switch: 'Switch',
|
||||
switchDescription: 'Multi-way branching',
|
||||
loop: 'Loop',
|
||||
loopDescription: 'Repeat execution',
|
||||
iterator: 'Iterator',
|
||||
iteratorDescription: 'Iterate over array elements',
|
||||
parallel: 'Parallel',
|
||||
parallelDescription: 'Execute multiple branches in parallel',
|
||||
wait: 'Wait',
|
||||
waitDescription: 'Wait for specified time',
|
||||
delay: 'Delay',
|
||||
delayDescription: 'Wait for a specified time',
|
||||
merge: 'Merge',
|
||||
mergeDescription: 'Merge multiple branches',
|
||||
variableAggregator: 'Variable Aggregator',
|
||||
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',
|
||||
storeData: 'Store Data',
|
||||
storeDataDescription: 'Store data to database',
|
||||
callPipeline: 'Call Pipeline',
|
||||
callPipelineDescription: 'Call an existing Pipeline',
|
||||
setVariable: 'Set Variable',
|
||||
setVariableDescription: 'Set context variable',
|
||||
openingStatement: 'Opening Statement',
|
||||
openingStatementDescription: 'Provide conversation opener and suggested questions',
|
||||
end: 'End',
|
||||
endDescription: 'Mark the end of workflow execution',
|
||||
log: 'Log',
|
||||
logDescription: 'Record log information',
|
||||
integration: 'Integration',
|
||||
integrationDescription: 'Third-party platform integration nodes',
|
||||
difyWorkflow: 'Dify Workflow',
|
||||
difyWorkflowDescription: 'Call Dify platform workflow',
|
||||
difyKnowledgeQuery: 'Dify Knowledge Query',
|
||||
difyKnowledgeQueryDescription: 'Query Dify knowledge base',
|
||||
n8nWorkflow: 'N8n Workflow',
|
||||
n8nWorkflowDescription: 'Call n8n workflow',
|
||||
langflowFlow: 'Langflow Flow',
|
||||
langflowFlowDescription: 'Call Langflow flow',
|
||||
cozeBot: 'Coze Bot',
|
||||
cozeBotDescription: 'Call Coze Bot',
|
||||
// Data & Tools integration nodes
|
||||
databaseQuery: 'Database Query',
|
||||
databaseQueryDescription: 'Execute database queries',
|
||||
redisOperation: 'Redis Operation',
|
||||
redisOperationDescription: 'Perform Redis cache operations',
|
||||
mcpTool: 'MCP Tool',
|
||||
mcpToolDescription: 'Invoke an MCP tool',
|
||||
memoryStore: 'Memory Store',
|
||||
memoryStoreDescription: 'Store and retrieve data from workflow memory',
|
||||
},
|
||||
executionHistory: {
|
||||
title: 'Execution History',
|
||||
noExecutions: 'No executions yet',
|
||||
status: 'Status',
|
||||
startTime: 'Start Time',
|
||||
duration: 'Duration',
|
||||
running: 'Running',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled',
|
||||
viewDetails: 'View Details',
|
||||
cancel: 'Cancel Execution',
|
||||
retry: 'Retry',
|
||||
nodeResults: 'Node Execution Results',
|
||||
},
|
||||
versions: {
|
||||
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.',
|
||||
rollbackSuccess: 'Rollback successful',
|
||||
rollbackError: 'Failed to rollback: ',
|
||||
},
|
||||
// Debug and monitoring
|
||||
debug: 'Debug',
|
||||
debugMode: 'Debug Mode',
|
||||
debugPanel: 'Debug Panel',
|
||||
startDebug: 'Start Debug',
|
||||
pauseDebug: 'Pause',
|
||||
resumeDebug: 'Resume',
|
||||
stepDebug: 'Step',
|
||||
stopDebug: 'Stop',
|
||||
debugContext: 'Debug Context',
|
||||
simulatedMessage: 'Simulated Message',
|
||||
simulatedMessagePlaceholder: 'Enter the message content to simulate',
|
||||
senderId: 'Sender ID',
|
||||
senderIdPlaceholder: 'Sender unique identifier',
|
||||
senderName: 'Sender Name',
|
||||
senderNamePlaceholder: 'Sender display name',
|
||||
conversationId: 'Conversation ID',
|
||||
conversationIdPlaceholder: 'Conversation unique identifier',
|
||||
isGroup: 'Group Chat',
|
||||
customVariables: 'Custom Variables',
|
||||
addVariable: 'Add Variable',
|
||||
variableName: 'Variable Name',
|
||||
variableValue: 'Variable Value',
|
||||
watchedVariables: 'Watched Variables',
|
||||
addWatchVariable: 'Add Watch',
|
||||
nodeStates: 'Node States',
|
||||
breakpoints: 'Breakpoints',
|
||||
toggleBreakpoint: 'Toggle Breakpoint',
|
||||
breakpointSet: 'Breakpoint set',
|
||||
breakpointRemoved: 'Breakpoint removed',
|
||||
debugLogs: 'Debug Logs',
|
||||
noLogs: 'No logs yet',
|
||||
clearLogs: 'Clear Logs',
|
||||
autoScroll: 'Auto Scroll',
|
||||
debugState: {
|
||||
idle: 'Idle',
|
||||
running: 'Running',
|
||||
paused: 'Paused',
|
||||
completed: 'Completed',
|
||||
error: 'Error',
|
||||
},
|
||||
nodeStatus: {
|
||||
pending: 'Pending',
|
||||
running: 'Running',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
skipped: 'Skipped',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'Workflow Chat',
|
||||
selectWorkflow: 'Select Workflow',
|
||||
sessionType: 'Session Type',
|
||||
privateChat: 'Private Chat',
|
||||
groupChat: 'Group Chat',
|
||||
send: 'Send',
|
||||
reset: 'Reset Conversation',
|
||||
inputPlaceholder: 'Send {{type}} message...',
|
||||
noMessages: 'No messages',
|
||||
userMessage: 'User',
|
||||
botMessage: 'Bot',
|
||||
sendFailed: 'Send failed',
|
||||
resetSuccess: 'Conversation reset successfully',
|
||||
resetFailed: 'Reset failed',
|
||||
loadMessagesFailed: 'Failed to load messages',
|
||||
loadWorkflowsFailed: 'Failed to load workflows',
|
||||
atTips: 'Mention bot',
|
||||
streaming: 'Streaming',
|
||||
streamOutput: 'Stream',
|
||||
connected: 'WebSocket connected',
|
||||
disconnected: 'WebSocket disconnected',
|
||||
connectionError: 'WebSocket connection error',
|
||||
connectionFailed: 'WebSocket connection failed',
|
||||
notConnected: 'WebSocket not connected, please try again later',
|
||||
imageUploadFailed: 'Image upload failed',
|
||||
reply: 'Reply',
|
||||
replyTo: 'Reply to',
|
||||
showMarkdown: 'Render',
|
||||
showRaw: 'Raw',
|
||||
allMembers: 'All Members',
|
||||
file: 'File',
|
||||
voice: 'Voice',
|
||||
uploadImage: 'Upload Image',
|
||||
uploading: 'Uploading...',
|
||||
},
|
||||
// Execution history and monitoring
|
||||
filterByDate: 'Filter by Date',
|
||||
allTime: 'All Time',
|
||||
today: 'Today',
|
||||
lastWeek: 'Last Week',
|
||||
lastMonth: 'Last Month',
|
||||
showingExecutions: 'Showing {{shown}} / {{total}} executions',
|
||||
rerun: 'Rerun',
|
||||
rerunExecution: 'Rerun Execution',
|
||||
logs: 'Logs',
|
||||
details: 'Details',
|
||||
completedAt: 'Completed At',
|
||||
noNodeExecutions: 'No node executions yet',
|
||||
// Node config field labels (used by DynamicFormComponent)
|
||||
nodeConfigFields: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
conditions: 'Trigger Conditions',
|
||||
keyword_filter: 'Keyword Filter',
|
||||
regex_filter: 'Regex Filter',
|
||||
min_length: 'Min Length',
|
||||
max_length: 'Max Length',
|
||||
require_mention: 'Require Mention',
|
||||
respond_rules: 'Group Respond Rules',
|
||||
access_control: 'Access Control',
|
||||
// trigger.py - CronTriggerNode
|
||||
cron: 'Cron Expression',
|
||||
timezone: 'Timezone',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
path: 'Webhook Path',
|
||||
allowed_methods: 'Allowed HTTP Methods',
|
||||
content_type: 'Content-Type',
|
||||
auth_type: 'Authentication Type',
|
||||
auth_key: 'Auth Key',
|
||||
validation: 'Request Validation',
|
||||
timeout: 'Timeout',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_types: 'Event Types',
|
||||
filter: 'Event Filter',
|
||||
debounce_ms: 'Debounce Time',
|
||||
// process.py - LLMCallNode
|
||||
model: 'Model',
|
||||
prompt_template: 'Prompt Template',
|
||||
system_prompt: 'System Prompt',
|
||||
temperature: 'Temperature',
|
||||
top_p: 'Top P',
|
||||
frequency_penalty: 'Frequency Penalty',
|
||||
presence_penalty: 'Presence Penalty',
|
||||
max_tokens: 'Max Tokens',
|
||||
stop_sequences: 'Stop Sequences',
|
||||
seed: 'Random Seed',
|
||||
stream: 'Stream Output',
|
||||
use_conversation_history: 'Use Conversation History',
|
||||
// process.py - CodeExecutorNode
|
||||
language: 'Programming Language',
|
||||
code: 'Code',
|
||||
// process.py - HTTPRequestNode
|
||||
url: 'Request URL',
|
||||
method: 'Request Method',
|
||||
auth_config: 'Auth Configuration',
|
||||
// process.py - DataTransformNode
|
||||
transform_type: 'Transform Type',
|
||||
template: 'Template',
|
||||
expression: 'Expression',
|
||||
output_type: 'Output Type',
|
||||
// process.py - QuestionClassifierNode
|
||||
categories: 'Categories',
|
||||
instruction: 'Instruction',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: 'Parameter Definitions',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
knowledge_bases: 'Knowledge Bases',
|
||||
top_k: 'Top K Results',
|
||||
score_threshold: 'Score Threshold',
|
||||
search_method: 'Search Method',
|
||||
enable_citations: 'Enable Citations',
|
||||
// control.py - ConditionNode
|
||||
condition_type: 'Condition Type',
|
||||
condition_expression: 'Condition Expression',
|
||||
left_value: 'Left Value',
|
||||
operator: 'Comparison Operator',
|
||||
right_value: 'Right Value',
|
||||
// control.py - SwitchNode
|
||||
cases: 'Branch Cases',
|
||||
// control.py - LoopNode
|
||||
max_iterations: 'Max Iterations',
|
||||
break_condition: 'Break Condition',
|
||||
// control.py - IteratorNode
|
||||
parallel: 'Parallel Processing',
|
||||
max_concurrency: 'Max Concurrency',
|
||||
// control.py - ParallelNode
|
||||
branches: 'Branch Configuration',
|
||||
wait_all: 'Wait All',
|
||||
fail_fast: 'Fail Fast',
|
||||
// control.py - WaitNode
|
||||
duration: 'Duration',
|
||||
duration_type: 'Time Unit',
|
||||
// control.py - MergeNode
|
||||
merge_strategy: 'Merge Strategy',
|
||||
// control.py - VariableAggregatorNode
|
||||
variable_mappings: 'Variable Mappings',
|
||||
aggregation_mode: 'Aggregation Mode',
|
||||
// action.py - SendMessageNode
|
||||
target_type: 'Target Type',
|
||||
target_id: 'Target ID',
|
||||
platform: 'Platform',
|
||||
message_type: 'Message Type',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_mode: 'Reply Mode',
|
||||
message_template: 'Message Template',
|
||||
long_text_processing: 'Long Text Processing',
|
||||
force_delay: 'Force Delay',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_uuid: 'Pipeline',
|
||||
inherit_context: 'Inherit Context',
|
||||
// action.py - StoreDataNode
|
||||
storage_type: 'Storage Type',
|
||||
ttl: 'TTL',
|
||||
key_prefix: 'Key Prefix',
|
||||
// action.py - SetVariableNode
|
||||
variable_name: 'Variable Name',
|
||||
variable_scope: 'Variable Scope',
|
||||
set_variable_operation: 'Operation Type',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: 'Opening Statement',
|
||||
suggested_questions: 'Suggested Questions',
|
||||
show_suggestions: 'Show Suggestions',
|
||||
// action.py - EndNode
|
||||
output_format: 'Output Format',
|
||||
success_message: 'Success Message',
|
||||
// integration.py - DatabaseQueryNode
|
||||
connection_type: 'Database Type',
|
||||
connection_string: 'Connection String',
|
||||
query: 'SQL Query',
|
||||
query_type: 'Query Type',
|
||||
// integration.py - RedisOperationNode
|
||||
connection_url: 'Connection URL',
|
||||
operation: 'Operation Type',
|
||||
key_template: 'Key Template',
|
||||
hash_field: 'Hash Field',
|
||||
// integration.py - MCPToolNode
|
||||
server_name: 'Server Name',
|
||||
tool_name: 'Tool Name',
|
||||
arguments_template: 'Arguments Template',
|
||||
// integration.py - MemoryStoreNode
|
||||
scope: 'Scope',
|
||||
// integration.py - DifyWorkflowNode
|
||||
'base-url': 'Base URL',
|
||||
'api-key': 'API Key',
|
||||
'app-type': 'App Type',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
dataset_id: 'Dataset ID',
|
||||
// integration.py - N8nWorkflowNode
|
||||
'webhook-url': 'Webhook URL',
|
||||
// integration.py - LangflowFlowNode
|
||||
'flow-id': 'Flow ID',
|
||||
// integration.py - CozeBotNode
|
||||
'bot-id': 'Bot ID',
|
||||
'api-base': 'API Base URL',
|
||||
},
|
||||
// Node outputs i18n
|
||||
nodeOutputs: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
message: 'Message Content',
|
||||
sender_id: 'Sender ID',
|
||||
sender_name: 'Sender Name',
|
||||
platform: 'Platform',
|
||||
conversation_id: 'Conversation ID',
|
||||
is_group: 'Is Group Chat',
|
||||
context: 'Full Message Context',
|
||||
// trigger.py - CronTriggerNode
|
||||
timestamp: 'Trigger Time',
|
||||
schedule: 'Trigger Schedule',
|
||||
cron_timestamp: 'Cron Timestamp',
|
||||
cron_schedule: 'Cron Schedule',
|
||||
cron_context: 'Cron Context',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
body: 'Request Body',
|
||||
headers: 'Request Headers',
|
||||
query: 'Query Parameters',
|
||||
method: 'Request Method',
|
||||
webhook_body: 'Webhook Body',
|
||||
webhook_headers: 'Webhook Headers',
|
||||
webhook_query: 'Webhook Query',
|
||||
webhook_method: 'Webhook Method',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_type: 'Event Type',
|
||||
event_data: 'Event Data',
|
||||
event_timestamp: 'Event Timestamp',
|
||||
// process.py - LLMCallNode
|
||||
response: 'Model Response',
|
||||
usage: 'Token Usage Statistics',
|
||||
prompt: 'Prompt/Question',
|
||||
context_info: 'Context Information',
|
||||
// process.py - CodeExecutorNode
|
||||
output: 'Output Data',
|
||||
console: 'Console Output',
|
||||
code_input: 'Code Input',
|
||||
code_output: 'Code Output',
|
||||
// process.py - HTTPRequestNode
|
||||
status_code: 'Status Code',
|
||||
http_body: 'HTTP Body',
|
||||
http_headers: 'HTTP Headers',
|
||||
http_response: 'HTTP Response',
|
||||
response_headers: 'Response Headers',
|
||||
// process.py - DataTransformNode
|
||||
result: 'Transform Result',
|
||||
transform_input: 'Transform Input',
|
||||
transform_result: 'Transform Result',
|
||||
// process.py - QuestionClassifierNode
|
||||
category: 'Category Result',
|
||||
confidence: 'Confidence',
|
||||
all_scores: 'All Category Scores',
|
||||
question: 'User Question',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: 'Extracted Parameters',
|
||||
extraction_success: 'Extraction Success',
|
||||
extract_text: 'Input Text',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
documents: 'Retrieved Documents',
|
||||
citations: 'Citation Information',
|
||||
knowledge_context: 'Merged Context',
|
||||
knowledge_query: 'Retrieval Query',
|
||||
// control.py - ConditionNode
|
||||
true: 'True Branch Output',
|
||||
false: 'False Branch Output',
|
||||
condition_input: 'Condition Input',
|
||||
// control.py - SwitchNode
|
||||
matched_case: 'Matched Branch Output',
|
||||
default: 'Default Branch Output',
|
||||
switch_input: 'Switch Input',
|
||||
// control.py - LoopNode
|
||||
item: 'Current Item',
|
||||
index: 'Current Index',
|
||||
results: 'All Iteration Results',
|
||||
completed: 'Is Completed',
|
||||
loop_items: 'Items to Iterate',
|
||||
// control.py - IteratorNode
|
||||
is_first: 'Is First',
|
||||
is_last: 'Is Last',
|
||||
iterator_array: 'Input Array',
|
||||
iterator_item: 'Current Element',
|
||||
iterator_index: 'Current Index',
|
||||
// control.py - ParallelNode
|
||||
errors: 'Error List',
|
||||
parallel_input: 'Parallel Input',
|
||||
parallel_results: 'All Branch Results',
|
||||
// control.py - WaitNode
|
||||
wait_input: 'Passthrough Input',
|
||||
wait_output: 'Passthrough Output',
|
||||
// control.py - MergeNode
|
||||
merged: 'Merged Result',
|
||||
merge_array: 'Array Result',
|
||||
merge_input_1: 'Input 1',
|
||||
merge_input_2: 'Input 2',
|
||||
merge_input_3: 'Input 3',
|
||||
merge_input_4: 'Input 4',
|
||||
// control.py - VariableAggregatorNode
|
||||
aggregated: 'Aggregated Variables',
|
||||
aggregator_variables: 'Variable Input',
|
||||
// action.py - SendMessageNode
|
||||
status: 'Send Status',
|
||||
message_id: 'Message ID',
|
||||
target: 'Target ID',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_message: 'Reply Content',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_response: 'Pipeline Response',
|
||||
pipeline_result: 'Full Result',
|
||||
pipeline_query: 'Query Content',
|
||||
context_data: 'Context Data',
|
||||
// action.py - StoreDataNode
|
||||
store_status: 'Store Status',
|
||||
store_key: 'Store Key',
|
||||
store_value: 'Store Value',
|
||||
// action.py - SetVariableNode
|
||||
variable_value: 'Variable Value',
|
||||
variable_result: 'Set Variable Result',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: 'Opening Statement',
|
||||
suggested_questions: 'Suggested Questions',
|
||||
// action.py - EndNode
|
||||
workflow_output: 'Workflow Output',
|
||||
final_result: 'Final Result',
|
||||
// integration.py - DatabaseQueryNode
|
||||
query_results: 'Query Results',
|
||||
row_count: 'Affected/Returned Rows',
|
||||
query_success: 'Query Success',
|
||||
query_params: 'Query Parameters',
|
||||
// integration.py - RedisOperationNode
|
||||
redis_result: 'Operation Result',
|
||||
redis_success: 'Operation Success',
|
||||
redis_key: 'Redis Key',
|
||||
redis_value: 'Redis Value',
|
||||
error: 'Error Message',
|
||||
plugin_input: 'Plugin Input',
|
||||
// integration.py - MCPToolNode
|
||||
tool_result: 'Tool Execution Result',
|
||||
tool_success: 'Tool Call Success',
|
||||
mcp_arguments: 'Tool Arguments',
|
||||
// integration.py - MemoryStoreNode
|
||||
memory_result: 'Retrieved/Stored Value',
|
||||
memory_success: 'Operation Success',
|
||||
memory_value: 'Value to Store',
|
||||
// integration.py - DifyWorkflowNode
|
||||
answer: 'Dify Answer',
|
||||
dify_success: 'Call Success',
|
||||
dify_query: 'User Input/Query',
|
||||
dify_conversation_id: 'Conversation ID',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
search_results: 'Search Results',
|
||||
knowledge_base_query: 'Query Content',
|
||||
// integration.py - N8nWorkflowNode
|
||||
n8n_result: 'Workflow Execution Result',
|
||||
n8n_success: 'Call Success',
|
||||
n8n_payload: 'Workflow Input Data',
|
||||
// integration.py - LangflowFlowNode
|
||||
flow_result: 'Flow Execution Result',
|
||||
flow_success: 'Call Success',
|
||||
langflow_input: 'Input Content',
|
||||
// integration.py - CozeBotNode
|
||||
bot_answer: 'Bot Reply',
|
||||
bot_success: 'Call Success',
|
||||
coze_query: 'User Input/Query',
|
||||
coze_conversation_id: 'Conversation ID',
|
||||
bot_conversation_id: 'Conversation ID',
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default ruRU;
|
||||
|
||||
@@ -1270,6 +1270,726 @@ const thTH = {
|
||||
backToWorkbench: 'กลับไปหน้าทำงาน',
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
title: 'Workflows',
|
||||
description: 'Create and manage visual workflows for complex message processing logic',
|
||||
createWorkflow: 'Create Workflow',
|
||||
selectFromSidebar: 'Select a workflow from the sidebar',
|
||||
editWorkflow: 'Edit Workflow',
|
||||
newWorkflow: 'New Workflow',
|
||||
getWorkflowListError: 'Failed to get workflow list: ',
|
||||
workflowName: 'Workflow Name',
|
||||
workflowDescription: 'Workflow Description',
|
||||
workflowNameRequired: 'Workflow name is required',
|
||||
defaultDescription: 'A workflow',
|
||||
getWorkflowError: 'Failed to get workflow: ',
|
||||
loadError: 'Failed to load workflow',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveError: 'Failed to save: ',
|
||||
createSuccess: 'Workflow created successfully',
|
||||
createError: 'Failed to create: ',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
deleteError: 'Failed to delete: ',
|
||||
deleteConfirmation: 'Are you sure you want to delete this workflow?',
|
||||
copySuccess: 'Copied successfully',
|
||||
copyError: 'Failed to copy: ',
|
||||
export: 'Export',
|
||||
import: 'Import',
|
||||
exportSuccess: 'Workflow exported',
|
||||
importSuccess: 'Workflow imported',
|
||||
importError: 'Import failed: Invalid file format',
|
||||
publish: 'Publish',
|
||||
publishSuccess: 'Published successfully',
|
||||
publishError: 'Failed to publish',
|
||||
configuration: 'Configuration',
|
||||
executions: 'Executions',
|
||||
editor: 'Editor',
|
||||
debugChat: 'แชทดีบัก',
|
||||
basicInfo: 'Basic Info',
|
||||
basicInfoDesc: 'Set workflow name, icon and description',
|
||||
basicInfoDescription: 'Set workflow name and description',
|
||||
dangerZone: 'Danger Zone',
|
||||
dangerZoneDesc: 'Irreversible operations',
|
||||
dangerZoneDescription: 'Irreversible operations',
|
||||
deleteWorkflowAction: 'Delete this workflow',
|
||||
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.',
|
||||
// Form component
|
||||
name: 'Name',
|
||||
namePlaceholder: 'Enter workflow name',
|
||||
descriptionPlaceholder: 'Enter workflow description (optional)',
|
||||
enabled: 'Enabled',
|
||||
enabledDesc: 'When enabled, the workflow can be triggered for execution',
|
||||
loading: 'Loading...',
|
||||
info: 'Workflow Info',
|
||||
uuid: 'UUID',
|
||||
version: 'Version',
|
||||
createdAt: 'Created At',
|
||||
updatedAt: 'Updated At',
|
||||
// Executions tab
|
||||
totalExecutions: '{{count}} execution(s) total',
|
||||
statistics: 'Statistics',
|
||||
successfulCount: '{{count}} successful',
|
||||
successRate: 'Success Rate',
|
||||
averageDuration: 'Avg. Duration',
|
||||
perExecution: 'per execution',
|
||||
failedExecutions: 'Failed',
|
||||
lastExecution: 'Last run',
|
||||
filterByStatus: 'Filter by status',
|
||||
allStatuses: 'All statuses',
|
||||
manualTrigger: 'Manual Trigger',
|
||||
executionId: 'Execution ID',
|
||||
status: 'Status',
|
||||
triggerType: 'Trigger Type',
|
||||
startedAt: 'Started At',
|
||||
duration: 'Duration',
|
||||
noExecutions: 'No executions yet',
|
||||
executionDetails: 'Execution Details',
|
||||
error: 'Error',
|
||||
nodeExecutions: 'Node Executions',
|
||||
result: 'Result',
|
||||
'status.pending': 'Pending',
|
||||
'status.running': 'Running',
|
||||
'status.completed': 'Completed',
|
||||
'status.failed': 'Failed',
|
||||
'status.cancelled': 'Cancelled',
|
||||
// Editor component translations
|
||||
nodePalette: 'Node Palette',
|
||||
properties: 'Properties',
|
||||
zoomIn: 'Zoom In',
|
||||
zoomOut: 'Zoom Out',
|
||||
fitView: 'Fit View',
|
||||
unsavedChanges: 'Unsaved changes',
|
||||
paste: 'Paste',
|
||||
deleted: 'Deleted',
|
||||
nothingToCopy: 'No nodes selected to copy',
|
||||
nothingToPaste: 'Clipboard is empty',
|
||||
copied: 'Copied {{count}} node(s)',
|
||||
pasted: 'Pasted {{count}} node(s)',
|
||||
nodesSelected: '{{count}} node(s) selected',
|
||||
edgesSelected: '{{count}} edge(s) selected',
|
||||
// Node palette
|
||||
searchNodes: 'Search nodes...',
|
||||
loadingNodeTypes: 'Loading node types...',
|
||||
noNodesFound: 'No matching nodes found',
|
||||
clearSearch: 'Clear search',
|
||||
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',
|
||||
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.',
|
||||
deleteEdge: 'Delete Edge',
|
||||
deleteEdgeConfirm: 'Confirm Delete Edge',
|
||||
deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
|
||||
nodeLabel: 'Node Label',
|
||||
nodeLabelPlaceholder: 'Enter node display name',
|
||||
nodeId: 'Node ID',
|
||||
inputOutputVariables: 'Input/Output Variables',
|
||||
inputs: 'Inputs',
|
||||
outputs: 'Outputs',
|
||||
availableVariables: 'Available Variables',
|
||||
globalVariables: 'Global Variables',
|
||||
messageContent: 'Message Content',
|
||||
messageSender: 'Sender',
|
||||
platform: 'Platform',
|
||||
sessionId: 'Session ID',
|
||||
timestamp: 'Timestamp',
|
||||
nodeConfig: 'Node Configuration',
|
||||
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.',
|
||||
// Node inputs/outputs i18n (for port labels)
|
||||
nodeInputs: {
|
||||
// Common inputs
|
||||
input: 'Input',
|
||||
message: 'Message',
|
||||
text: 'Text',
|
||||
query: 'Query',
|
||||
data: 'Data',
|
||||
condition: 'Condition',
|
||||
value: 'Value',
|
||||
// Trigger inputs
|
||||
content: 'Content',
|
||||
context: 'Context',
|
||||
body: 'Request Body',
|
||||
variables: 'Variables',
|
||||
items: 'Items',
|
||||
arguments: 'Arguments',
|
||||
// AI/Process inputs
|
||||
question: 'Question',
|
||||
parameters: 'Parameters',
|
||||
key: 'Key',
|
||||
// Control inputs
|
||||
case_1: 'Case 1',
|
||||
case_2: 'Case 2',
|
||||
branch_1: 'Branch 1',
|
||||
branch_2: 'Branch 2',
|
||||
// Action inputs
|
||||
notification_id: 'Notification ID',
|
||||
// Integration inputs
|
||||
key_template: 'Key Template',
|
||||
hash_field: 'Hash Field',
|
||||
server_name: 'Server Name',
|
||||
tool_name: 'Tool Name',
|
||||
arguments_template: 'Arguments Template',
|
||||
scope: 'Scope',
|
||||
},
|
||||
// Data type labels
|
||||
'type.string': 'String',
|
||||
'type.object': 'Object',
|
||||
'type.array': 'Array',
|
||||
'type.boolean': 'Boolean',
|
||||
'type.number': 'Number',
|
||||
'type.any': 'Any',
|
||||
'type.datetime': 'DateTime',
|
||||
nodes: {
|
||||
trigger: 'Triggers',
|
||||
triggerDescription: 'Starting nodes for workflows',
|
||||
messageTrigger: 'Message Trigger',
|
||||
messageTriggerDescription: 'Triggered when a message is received',
|
||||
scheduleTrigger: 'Schedule Trigger',
|
||||
scheduleTriggerDescription: 'Triggered on a schedule',
|
||||
cronTrigger: 'Cron Trigger',
|
||||
cronTriggerDescription: 'Trigger workflow on a scheduled time',
|
||||
webhookTrigger: 'Webhook Trigger',
|
||||
webhookTriggerDescription: 'Triggered via HTTP request',
|
||||
eventTrigger: 'Event Trigger',
|
||||
eventTriggerDescription: 'Triggered on system events',
|
||||
process: 'AI/Process',
|
||||
processDescription: 'Data processing nodes',
|
||||
aiProcess: 'AI Processing',
|
||||
aiProcessDescription: 'Process messages using AI models',
|
||||
llmCall: 'LLM Call',
|
||||
llmCallDescription: 'Invoke large language model for conversation or generation',
|
||||
codeProcess: 'Code Processing',
|
||||
codeProcessDescription: 'Execute custom code',
|
||||
codeExecutor: 'Code Executor',
|
||||
codeExecutorDescription: 'Execute Python/JavaScript code',
|
||||
templateProcess: 'Template Processing',
|
||||
templateProcessDescription: 'Format output using templates',
|
||||
httpRequest: 'HTTP Request',
|
||||
httpRequestDescription: 'Send HTTP requests',
|
||||
dataTransform: 'Data Transform',
|
||||
dataTransformDescription: 'Transform data format',
|
||||
questionClassifier: 'Question Classifier',
|
||||
questionClassifierDescription: 'Classify user questions into predefined categories using LLM',
|
||||
parameterExtractor: 'Parameter Extractor',
|
||||
parameterExtractorDescription: 'Extract structured parameters from text using LLM',
|
||||
knowledgeRetrieval: 'Knowledge Retrieval',
|
||||
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base',
|
||||
textTemplate: 'Text Template',
|
||||
textTemplateDescription: 'Generate text using templates with variable interpolation',
|
||||
jsonTransform: 'JSON Transform',
|
||||
jsonTransformDescription: 'Transform JSON data using expressions',
|
||||
dataAggregator: 'Data Aggregator',
|
||||
dataAggregatorDescription: 'Aggregate data from multiple sources',
|
||||
textSplitter: 'Text Splitter',
|
||||
textSplitterDescription: 'Split text into smaller chunks',
|
||||
variableAssignment: 'Variable Assignment',
|
||||
variableAssignmentDescription: 'Assign values to workflow variables',
|
||||
control: 'Control Flow',
|
||||
controlDescription: 'Flow control nodes',
|
||||
condition: 'Condition Branch',
|
||||
conditionDescription: 'Branch based on conditions',
|
||||
switch: 'Switch',
|
||||
switchDescription: 'Multi-way branching',
|
||||
loop: 'Loop',
|
||||
loopDescription: 'Repeat execution',
|
||||
iterator: 'Iterator',
|
||||
iteratorDescription: 'Iterate over array elements',
|
||||
parallel: 'Parallel',
|
||||
parallelDescription: 'Execute multiple branches in parallel',
|
||||
wait: 'Wait',
|
||||
waitDescription: 'Wait for specified time',
|
||||
delay: 'Delay',
|
||||
delayDescription: 'Wait for a specified time',
|
||||
merge: 'Merge',
|
||||
mergeDescription: 'Merge multiple branches',
|
||||
variableAggregator: 'Variable Aggregator',
|
||||
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',
|
||||
storeData: 'Store Data',
|
||||
storeDataDescription: 'Store data to database',
|
||||
callPipeline: 'Call Pipeline',
|
||||
callPipelineDescription: 'Call an existing Pipeline',
|
||||
setVariable: 'Set Variable',
|
||||
setVariableDescription: 'Set context variable',
|
||||
openingStatement: 'Opening Statement',
|
||||
openingStatementDescription: 'Provide conversation opener and suggested questions',
|
||||
end: 'End',
|
||||
endDescription: 'Mark the end of workflow execution',
|
||||
log: 'Log',
|
||||
logDescription: 'Record log information',
|
||||
integration: 'Integration',
|
||||
integrationDescription: 'Third-party platform integration nodes',
|
||||
difyWorkflow: 'Dify Workflow',
|
||||
difyWorkflowDescription: 'Call Dify platform workflow',
|
||||
difyKnowledgeQuery: 'Dify Knowledge Query',
|
||||
difyKnowledgeQueryDescription: 'Query Dify knowledge base',
|
||||
n8nWorkflow: 'N8n Workflow',
|
||||
n8nWorkflowDescription: 'Call n8n workflow',
|
||||
langflowFlow: 'Langflow Flow',
|
||||
langflowFlowDescription: 'Call Langflow flow',
|
||||
cozeBot: 'Coze Bot',
|
||||
cozeBotDescription: 'Call Coze Bot',
|
||||
// Data & Tools integration nodes
|
||||
databaseQuery: 'Database Query',
|
||||
databaseQueryDescription: 'Execute database queries',
|
||||
redisOperation: 'Redis Operation',
|
||||
redisOperationDescription: 'Perform Redis cache operations',
|
||||
mcpTool: 'MCP Tool',
|
||||
mcpToolDescription: 'Invoke an MCP tool',
|
||||
memoryStore: 'Memory Store',
|
||||
memoryStoreDescription: 'Store and retrieve data from workflow memory',
|
||||
},
|
||||
executionHistory: {
|
||||
title: 'Execution History',
|
||||
noExecutions: 'No executions yet',
|
||||
status: 'Status',
|
||||
startTime: 'Start Time',
|
||||
duration: 'Duration',
|
||||
running: 'Running',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled',
|
||||
viewDetails: 'View Details',
|
||||
cancel: 'Cancel Execution',
|
||||
retry: 'Retry',
|
||||
nodeResults: 'Node Execution Results',
|
||||
},
|
||||
versions: {
|
||||
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.',
|
||||
rollbackSuccess: 'Rollback successful',
|
||||
rollbackError: 'Failed to rollback: ',
|
||||
},
|
||||
// Debug and monitoring
|
||||
debug: 'Debug',
|
||||
debugMode: 'Debug Mode',
|
||||
debugPanel: 'Debug Panel',
|
||||
startDebug: 'Start Debug',
|
||||
pauseDebug: 'Pause',
|
||||
resumeDebug: 'Resume',
|
||||
stepDebug: 'Step',
|
||||
stopDebug: 'Stop',
|
||||
debugContext: 'Debug Context',
|
||||
simulatedMessage: 'Simulated Message',
|
||||
simulatedMessagePlaceholder: 'Enter the message content to simulate',
|
||||
senderId: 'Sender ID',
|
||||
senderIdPlaceholder: 'Sender unique identifier',
|
||||
senderName: 'Sender Name',
|
||||
senderNamePlaceholder: 'Sender display name',
|
||||
conversationId: 'Conversation ID',
|
||||
conversationIdPlaceholder: 'Conversation unique identifier',
|
||||
isGroup: 'Group Chat',
|
||||
customVariables: 'Custom Variables',
|
||||
addVariable: 'Add Variable',
|
||||
variableName: 'Variable Name',
|
||||
variableValue: 'Variable Value',
|
||||
watchedVariables: 'Watched Variables',
|
||||
addWatchVariable: 'Add Watch',
|
||||
nodeStates: 'Node States',
|
||||
breakpoints: 'Breakpoints',
|
||||
toggleBreakpoint: 'Toggle Breakpoint',
|
||||
breakpointSet: 'Breakpoint set',
|
||||
breakpointRemoved: 'Breakpoint removed',
|
||||
debugLogs: 'Debug Logs',
|
||||
noLogs: 'No logs yet',
|
||||
clearLogs: 'Clear Logs',
|
||||
autoScroll: 'Auto Scroll',
|
||||
debugState: {
|
||||
idle: 'Idle',
|
||||
running: 'Running',
|
||||
paused: 'Paused',
|
||||
completed: 'Completed',
|
||||
error: 'Error',
|
||||
},
|
||||
nodeStatus: {
|
||||
pending: 'Pending',
|
||||
running: 'Running',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
skipped: 'Skipped',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'Workflow Chat',
|
||||
selectWorkflow: 'Select Workflow',
|
||||
sessionType: 'Session Type',
|
||||
privateChat: 'Private Chat',
|
||||
groupChat: 'Group Chat',
|
||||
send: 'Send',
|
||||
reset: 'Reset Conversation',
|
||||
inputPlaceholder: 'Send {{type}} message...',
|
||||
noMessages: 'No messages',
|
||||
userMessage: 'User',
|
||||
botMessage: 'Bot',
|
||||
sendFailed: 'Send failed',
|
||||
resetSuccess: 'Conversation reset successfully',
|
||||
resetFailed: 'Reset failed',
|
||||
loadMessagesFailed: 'Failed to load messages',
|
||||
loadWorkflowsFailed: 'Failed to load workflows',
|
||||
atTips: 'Mention bot',
|
||||
streaming: 'Streaming',
|
||||
streamOutput: 'Stream',
|
||||
connected: 'WebSocket connected',
|
||||
disconnected: 'WebSocket disconnected',
|
||||
connectionError: 'WebSocket connection error',
|
||||
connectionFailed: 'WebSocket connection failed',
|
||||
notConnected: 'WebSocket not connected, please try again later',
|
||||
imageUploadFailed: 'Image upload failed',
|
||||
reply: 'Reply',
|
||||
replyTo: 'Reply to',
|
||||
showMarkdown: 'Render',
|
||||
showRaw: 'Raw',
|
||||
allMembers: 'All Members',
|
||||
file: 'File',
|
||||
voice: 'Voice',
|
||||
uploadImage: 'Upload Image',
|
||||
uploading: 'Uploading...',
|
||||
},
|
||||
// Execution history and monitoring
|
||||
filterByDate: 'Filter by Date',
|
||||
allTime: 'All Time',
|
||||
today: 'Today',
|
||||
lastWeek: 'Last Week',
|
||||
lastMonth: 'Last Month',
|
||||
showingExecutions: 'Showing {{shown}} / {{total}} executions',
|
||||
rerun: 'Rerun',
|
||||
rerunExecution: 'Rerun Execution',
|
||||
logs: 'Logs',
|
||||
details: 'Details',
|
||||
completedAt: 'Completed At',
|
||||
noNodeExecutions: 'No node executions yet',
|
||||
// Node config field labels (used by DynamicFormComponent)
|
||||
nodeConfigFields: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
conditions: 'Trigger Conditions',
|
||||
keyword_filter: 'Keyword Filter',
|
||||
regex_filter: 'Regex Filter',
|
||||
min_length: 'Min Length',
|
||||
max_length: 'Max Length',
|
||||
require_mention: 'Require Mention',
|
||||
respond_rules: 'Group Respond Rules',
|
||||
access_control: 'Access Control',
|
||||
// trigger.py - CronTriggerNode
|
||||
cron: 'Cron Expression',
|
||||
timezone: 'Timezone',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
path: 'Webhook Path',
|
||||
allowed_methods: 'Allowed HTTP Methods',
|
||||
content_type: 'Content-Type',
|
||||
auth_type: 'Authentication Type',
|
||||
auth_key: 'Auth Key',
|
||||
validation: 'Request Validation',
|
||||
timeout: 'Timeout',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_types: 'Event Types',
|
||||
filter: 'Event Filter',
|
||||
debounce_ms: 'Debounce Time',
|
||||
// process.py - LLMCallNode
|
||||
model: 'Model',
|
||||
prompt_template: 'Prompt Template',
|
||||
system_prompt: 'System Prompt',
|
||||
temperature: 'Temperature',
|
||||
top_p: 'Top P',
|
||||
frequency_penalty: 'Frequency Penalty',
|
||||
presence_penalty: 'Presence Penalty',
|
||||
max_tokens: 'Max Tokens',
|
||||
stop_sequences: 'Stop Sequences',
|
||||
seed: 'Random Seed',
|
||||
stream: 'Stream Output',
|
||||
use_conversation_history: 'Use Conversation History',
|
||||
// process.py - CodeExecutorNode
|
||||
language: 'Programming Language',
|
||||
code: 'Code',
|
||||
// process.py - HTTPRequestNode
|
||||
url: 'Request URL',
|
||||
method: 'Request Method',
|
||||
auth_config: 'Auth Configuration',
|
||||
// process.py - DataTransformNode
|
||||
transform_type: 'Transform Type',
|
||||
template: 'Template',
|
||||
expression: 'Expression',
|
||||
output_type: 'Output Type',
|
||||
// process.py - QuestionClassifierNode
|
||||
categories: 'Categories',
|
||||
instruction: 'Instruction',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: 'Parameter Definitions',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
knowledge_bases: 'Knowledge Bases',
|
||||
top_k: 'Top K Results',
|
||||
score_threshold: 'Score Threshold',
|
||||
search_method: 'Search Method',
|
||||
enable_citations: 'Enable Citations',
|
||||
// control.py - ConditionNode
|
||||
condition_type: 'Condition Type',
|
||||
condition_expression: 'Condition Expression',
|
||||
left_value: 'Left Value',
|
||||
operator: 'Comparison Operator',
|
||||
right_value: 'Right Value',
|
||||
// control.py - SwitchNode
|
||||
cases: 'Branch Cases',
|
||||
// control.py - LoopNode
|
||||
max_iterations: 'Max Iterations',
|
||||
break_condition: 'Break Condition',
|
||||
// control.py - IteratorNode
|
||||
parallel: 'Parallel Processing',
|
||||
max_concurrency: 'Max Concurrency',
|
||||
// control.py - ParallelNode
|
||||
branches: 'Branch Configuration',
|
||||
wait_all: 'Wait All',
|
||||
fail_fast: 'Fail Fast',
|
||||
// control.py - WaitNode
|
||||
duration: 'Duration',
|
||||
duration_type: 'Time Unit',
|
||||
// control.py - MergeNode
|
||||
merge_strategy: 'Merge Strategy',
|
||||
// control.py - VariableAggregatorNode
|
||||
variable_mappings: 'Variable Mappings',
|
||||
aggregation_mode: 'Aggregation Mode',
|
||||
// action.py - SendMessageNode
|
||||
target_type: 'Target Type',
|
||||
target_id: 'Target ID',
|
||||
platform: 'Platform',
|
||||
message_type: 'Message Type',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_mode: 'Reply Mode',
|
||||
message_template: 'Message Template',
|
||||
long_text_processing: 'Long Text Processing',
|
||||
force_delay: 'Force Delay',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_uuid: 'Pipeline',
|
||||
inherit_context: 'Inherit Context',
|
||||
// action.py - StoreDataNode
|
||||
storage_type: 'Storage Type',
|
||||
ttl: 'TTL',
|
||||
key_prefix: 'Key Prefix',
|
||||
// action.py - SetVariableNode
|
||||
variable_name: 'Variable Name',
|
||||
variable_scope: 'Variable Scope',
|
||||
set_variable_operation: 'Operation Type',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: 'Opening Statement',
|
||||
suggested_questions: 'Suggested Questions',
|
||||
show_suggestions: 'Show Suggestions',
|
||||
// action.py - EndNode
|
||||
output_format: 'Output Format',
|
||||
success_message: 'Success Message',
|
||||
// integration.py - DatabaseQueryNode
|
||||
connection_type: 'Database Type',
|
||||
connection_string: 'Connection String',
|
||||
query: 'SQL Query',
|
||||
query_type: 'Query Type',
|
||||
// integration.py - RedisOperationNode
|
||||
connection_url: 'Connection URL',
|
||||
operation: 'Operation Type',
|
||||
key_template: 'Key Template',
|
||||
hash_field: 'Hash Field',
|
||||
// integration.py - MCPToolNode
|
||||
server_name: 'Server Name',
|
||||
tool_name: 'Tool Name',
|
||||
arguments_template: 'Arguments Template',
|
||||
// integration.py - MemoryStoreNode
|
||||
scope: 'Scope',
|
||||
// integration.py - DifyWorkflowNode
|
||||
'base-url': 'Base URL',
|
||||
'api-key': 'API Key',
|
||||
'app-type': 'App Type',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
dataset_id: 'Dataset ID',
|
||||
// integration.py - N8nWorkflowNode
|
||||
'webhook-url': 'Webhook URL',
|
||||
// integration.py - LangflowFlowNode
|
||||
'flow-id': 'Flow ID',
|
||||
// integration.py - CozeBotNode
|
||||
'bot-id': 'Bot ID',
|
||||
'api-base': 'API Base URL',
|
||||
},
|
||||
// Node outputs i18n
|
||||
nodeOutputs: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
message: 'Message Content',
|
||||
sender_id: 'Sender ID',
|
||||
sender_name: 'Sender Name',
|
||||
platform: 'Platform',
|
||||
conversation_id: 'Conversation ID',
|
||||
is_group: 'Is Group Chat',
|
||||
context: 'Full Message Context',
|
||||
// trigger.py - CronTriggerNode
|
||||
timestamp: 'Trigger Time',
|
||||
schedule: 'Trigger Schedule',
|
||||
cron_timestamp: 'Cron Timestamp',
|
||||
cron_schedule: 'Cron Schedule',
|
||||
cron_context: 'Cron Context',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
body: 'Request Body',
|
||||
headers: 'Request Headers',
|
||||
query: 'Query Parameters',
|
||||
method: 'Request Method',
|
||||
webhook_body: 'Webhook Body',
|
||||
webhook_headers: 'Webhook Headers',
|
||||
webhook_query: 'Webhook Query',
|
||||
webhook_method: 'Webhook Method',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_type: 'Event Type',
|
||||
event_data: 'Event Data',
|
||||
event_timestamp: 'Event Timestamp',
|
||||
// process.py - LLMCallNode
|
||||
response: 'Model Response',
|
||||
usage: 'Token Usage Statistics',
|
||||
prompt: 'Prompt/Question',
|
||||
context_info: 'Context Information',
|
||||
// process.py - CodeExecutorNode
|
||||
output: 'Output Data',
|
||||
console: 'Console Output',
|
||||
code_input: 'Code Input',
|
||||
code_output: 'Code Output',
|
||||
// process.py - HTTPRequestNode
|
||||
status_code: 'Status Code',
|
||||
http_body: 'HTTP Body',
|
||||
http_headers: 'HTTP Headers',
|
||||
http_response: 'HTTP Response',
|
||||
response_headers: 'Response Headers',
|
||||
// process.py - DataTransformNode
|
||||
result: 'Transform Result',
|
||||
transform_input: 'Transform Input',
|
||||
transform_result: 'Transform Result',
|
||||
// process.py - QuestionClassifierNode
|
||||
category: 'Category Result',
|
||||
confidence: 'Confidence',
|
||||
all_scores: 'All Category Scores',
|
||||
question: 'User Question',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: 'Extracted Parameters',
|
||||
extraction_success: 'Extraction Success',
|
||||
extract_text: 'Input Text',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
documents: 'Retrieved Documents',
|
||||
citations: 'Citation Information',
|
||||
knowledge_context: 'Merged Context',
|
||||
knowledge_query: 'Retrieval Query',
|
||||
// control.py - ConditionNode
|
||||
true: 'True Branch Output',
|
||||
false: 'False Branch Output',
|
||||
condition_input: 'Condition Input',
|
||||
// control.py - SwitchNode
|
||||
matched_case: 'Matched Branch Output',
|
||||
default: 'Default Branch Output',
|
||||
switch_input: 'Switch Input',
|
||||
// control.py - LoopNode
|
||||
item: 'Current Item',
|
||||
index: 'Current Index',
|
||||
results: 'All Iteration Results',
|
||||
completed: 'Is Completed',
|
||||
loop_items: 'Items to Iterate',
|
||||
// control.py - IteratorNode
|
||||
is_first: 'Is First',
|
||||
is_last: 'Is Last',
|
||||
iterator_array: 'Input Array',
|
||||
iterator_item: 'Current Element',
|
||||
iterator_index: 'Current Index',
|
||||
// control.py - ParallelNode
|
||||
errors: 'Error List',
|
||||
parallel_input: 'Parallel Input',
|
||||
parallel_results: 'All Branch Results',
|
||||
// control.py - WaitNode
|
||||
wait_input: 'Passthrough Input',
|
||||
wait_output: 'Passthrough Output',
|
||||
// control.py - MergeNode
|
||||
merged: 'Merged Result',
|
||||
merge_array: 'Array Result',
|
||||
merge_input_1: 'Input 1',
|
||||
merge_input_2: 'Input 2',
|
||||
merge_input_3: 'Input 3',
|
||||
merge_input_4: 'Input 4',
|
||||
// control.py - VariableAggregatorNode
|
||||
aggregated: 'Aggregated Variables',
|
||||
aggregator_variables: 'Variable Input',
|
||||
// action.py - SendMessageNode
|
||||
status: 'Send Status',
|
||||
message_id: 'Message ID',
|
||||
target: 'Target ID',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_message: 'Reply Content',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_response: 'Pipeline Response',
|
||||
pipeline_result: 'Full Result',
|
||||
pipeline_query: 'Query Content',
|
||||
context_data: 'Context Data',
|
||||
// action.py - StoreDataNode
|
||||
store_status: 'Store Status',
|
||||
store_key: 'Store Key',
|
||||
store_value: 'Store Value',
|
||||
// action.py - SetVariableNode
|
||||
variable_value: 'Variable Value',
|
||||
variable_result: 'Set Variable Result',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: 'Opening Statement',
|
||||
suggested_questions: 'Suggested Questions',
|
||||
// action.py - EndNode
|
||||
workflow_output: 'Workflow Output',
|
||||
final_result: 'Final Result',
|
||||
// integration.py - DatabaseQueryNode
|
||||
query_results: 'Query Results',
|
||||
row_count: 'Affected/Returned Rows',
|
||||
query_success: 'Query Success',
|
||||
query_params: 'Query Parameters',
|
||||
// integration.py - RedisOperationNode
|
||||
redis_result: 'Operation Result',
|
||||
redis_success: 'Operation Success',
|
||||
redis_key: 'Redis Key',
|
||||
redis_value: 'Redis Value',
|
||||
error: 'Error Message',
|
||||
plugin_input: 'Plugin Input',
|
||||
// integration.py - MCPToolNode
|
||||
tool_result: 'Tool Execution Result',
|
||||
tool_success: 'Tool Call Success',
|
||||
mcp_arguments: 'Tool Arguments',
|
||||
// integration.py - MemoryStoreNode
|
||||
memory_result: 'Retrieved/Stored Value',
|
||||
memory_success: 'Operation Success',
|
||||
memory_value: 'Value to Store',
|
||||
// integration.py - DifyWorkflowNode
|
||||
answer: 'Dify Answer',
|
||||
dify_success: 'Call Success',
|
||||
dify_query: 'User Input/Query',
|
||||
dify_conversation_id: 'Conversation ID',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
search_results: 'Search Results',
|
||||
knowledge_base_query: 'Query Content',
|
||||
// integration.py - N8nWorkflowNode
|
||||
n8n_result: 'Workflow Execution Result',
|
||||
n8n_success: 'Call Success',
|
||||
n8n_payload: 'Workflow Input Data',
|
||||
// integration.py - LangflowFlowNode
|
||||
flow_result: 'Flow Execution Result',
|
||||
flow_success: 'Call Success',
|
||||
langflow_input: 'Input Content',
|
||||
// integration.py - CozeBotNode
|
||||
bot_answer: 'Bot Reply',
|
||||
bot_success: 'Call Success',
|
||||
coze_query: 'User Input/Query',
|
||||
coze_conversation_id: 'Conversation ID',
|
||||
bot_conversation_id: 'Conversation ID',
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default thTH;
|
||||
|
||||
@@ -1291,6 +1291,726 @@ const viVN = {
|
||||
backToWorkbench: 'Quay lại bàn làm việc',
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
title: 'Workflows',
|
||||
description: 'Create and manage visual workflows for complex message processing logic',
|
||||
createWorkflow: 'Create Workflow',
|
||||
selectFromSidebar: 'Select a workflow from the sidebar',
|
||||
editWorkflow: 'Edit Workflow',
|
||||
newWorkflow: 'New Workflow',
|
||||
getWorkflowListError: 'Failed to get workflow list: ',
|
||||
workflowName: 'Workflow Name',
|
||||
workflowDescription: 'Workflow Description',
|
||||
workflowNameRequired: 'Workflow name is required',
|
||||
defaultDescription: 'A workflow',
|
||||
getWorkflowError: 'Failed to get workflow: ',
|
||||
loadError: 'Failed to load workflow',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveError: 'Failed to save: ',
|
||||
createSuccess: 'Workflow created successfully',
|
||||
createError: 'Failed to create: ',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
deleteError: 'Failed to delete: ',
|
||||
deleteConfirmation: 'Are you sure you want to delete this workflow?',
|
||||
copySuccess: 'Copied successfully',
|
||||
copyError: 'Failed to copy: ',
|
||||
export: 'Export',
|
||||
import: 'Import',
|
||||
exportSuccess: 'Workflow exported',
|
||||
importSuccess: 'Workflow imported',
|
||||
importError: 'Import failed: Invalid file format',
|
||||
publish: 'Publish',
|
||||
publishSuccess: 'Published successfully',
|
||||
publishError: 'Failed to publish',
|
||||
configuration: 'Configuration',
|
||||
executions: 'Executions',
|
||||
editor: 'Editor',
|
||||
debugChat: 'Trò chuyện gỡ lỗi',
|
||||
basicInfo: 'Basic Info',
|
||||
basicInfoDesc: 'Set workflow name, icon and description',
|
||||
basicInfoDescription: 'Set workflow name and description',
|
||||
dangerZone: 'Danger Zone',
|
||||
dangerZoneDesc: 'Irreversible operations',
|
||||
dangerZoneDescription: 'Irreversible operations',
|
||||
deleteWorkflowAction: 'Delete this workflow',
|
||||
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.',
|
||||
// Form component
|
||||
name: 'Name',
|
||||
namePlaceholder: 'Enter workflow name',
|
||||
descriptionPlaceholder: 'Enter workflow description (optional)',
|
||||
enabled: 'Enabled',
|
||||
enabledDesc: 'When enabled, the workflow can be triggered for execution',
|
||||
loading: 'Loading...',
|
||||
info: 'Workflow Info',
|
||||
uuid: 'UUID',
|
||||
version: 'Version',
|
||||
createdAt: 'Created At',
|
||||
updatedAt: 'Updated At',
|
||||
// Executions tab
|
||||
totalExecutions: '{{count}} execution(s) total',
|
||||
statistics: 'Statistics',
|
||||
successfulCount: '{{count}} successful',
|
||||
successRate: 'Success Rate',
|
||||
averageDuration: 'Avg. Duration',
|
||||
perExecution: 'per execution',
|
||||
failedExecutions: 'Failed',
|
||||
lastExecution: 'Last run',
|
||||
filterByStatus: 'Filter by status',
|
||||
allStatuses: 'All statuses',
|
||||
manualTrigger: 'Manual Trigger',
|
||||
executionId: 'Execution ID',
|
||||
status: 'Status',
|
||||
triggerType: 'Trigger Type',
|
||||
startedAt: 'Started At',
|
||||
duration: 'Duration',
|
||||
noExecutions: 'No executions yet',
|
||||
executionDetails: 'Execution Details',
|
||||
error: 'Error',
|
||||
nodeExecutions: 'Node Executions',
|
||||
result: 'Result',
|
||||
'status.pending': 'Pending',
|
||||
'status.running': 'Running',
|
||||
'status.completed': 'Completed',
|
||||
'status.failed': 'Failed',
|
||||
'status.cancelled': 'Cancelled',
|
||||
// Editor component translations
|
||||
nodePalette: 'Node Palette',
|
||||
properties: 'Properties',
|
||||
zoomIn: 'Zoom In',
|
||||
zoomOut: 'Zoom Out',
|
||||
fitView: 'Fit View',
|
||||
unsavedChanges: 'Unsaved changes',
|
||||
paste: 'Paste',
|
||||
deleted: 'Deleted',
|
||||
nothingToCopy: 'No nodes selected to copy',
|
||||
nothingToPaste: 'Clipboard is empty',
|
||||
copied: 'Copied {{count}} node(s)',
|
||||
pasted: 'Pasted {{count}} node(s)',
|
||||
nodesSelected: '{{count}} node(s) selected',
|
||||
edgesSelected: '{{count}} edge(s) selected',
|
||||
// Node palette
|
||||
searchNodes: 'Search nodes...',
|
||||
loadingNodeTypes: 'Loading node types...',
|
||||
noNodesFound: 'No matching nodes found',
|
||||
clearSearch: 'Clear search',
|
||||
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',
|
||||
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.',
|
||||
deleteEdge: 'Delete Edge',
|
||||
deleteEdgeConfirm: 'Confirm Delete Edge',
|
||||
deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
|
||||
nodeLabel: 'Node Label',
|
||||
nodeLabelPlaceholder: 'Enter node display name',
|
||||
nodeId: 'Node ID',
|
||||
inputOutputVariables: 'Input/Output Variables',
|
||||
inputs: 'Inputs',
|
||||
outputs: 'Outputs',
|
||||
availableVariables: 'Available Variables',
|
||||
globalVariables: 'Global Variables',
|
||||
messageContent: 'Message Content',
|
||||
messageSender: 'Sender',
|
||||
platform: 'Platform',
|
||||
sessionId: 'Session ID',
|
||||
timestamp: 'Timestamp',
|
||||
nodeConfig: 'Node Configuration',
|
||||
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.',
|
||||
// Node inputs/outputs i18n (for port labels)
|
||||
nodeInputs: {
|
||||
// Common inputs
|
||||
input: 'Input',
|
||||
message: 'Message',
|
||||
text: 'Text',
|
||||
query: 'Query',
|
||||
data: 'Data',
|
||||
condition: 'Condition',
|
||||
value: 'Value',
|
||||
// Trigger inputs
|
||||
content: 'Content',
|
||||
context: 'Context',
|
||||
body: 'Request Body',
|
||||
variables: 'Variables',
|
||||
items: 'Items',
|
||||
arguments: 'Arguments',
|
||||
// AI/Process inputs
|
||||
question: 'Question',
|
||||
parameters: 'Parameters',
|
||||
key: 'Key',
|
||||
// Control inputs
|
||||
case_1: 'Case 1',
|
||||
case_2: 'Case 2',
|
||||
branch_1: 'Branch 1',
|
||||
branch_2: 'Branch 2',
|
||||
// Action inputs
|
||||
notification_id: 'Notification ID',
|
||||
// Integration inputs
|
||||
key_template: 'Key Template',
|
||||
hash_field: 'Hash Field',
|
||||
server_name: 'Server Name',
|
||||
tool_name: 'Tool Name',
|
||||
arguments_template: 'Arguments Template',
|
||||
scope: 'Scope',
|
||||
},
|
||||
// Data type labels
|
||||
'type.string': 'String',
|
||||
'type.object': 'Object',
|
||||
'type.array': 'Array',
|
||||
'type.boolean': 'Boolean',
|
||||
'type.number': 'Number',
|
||||
'type.any': 'Any',
|
||||
'type.datetime': 'DateTime',
|
||||
nodes: {
|
||||
trigger: 'Triggers',
|
||||
triggerDescription: 'Starting nodes for workflows',
|
||||
messageTrigger: 'Message Trigger',
|
||||
messageTriggerDescription: 'Triggered when a message is received',
|
||||
scheduleTrigger: 'Schedule Trigger',
|
||||
scheduleTriggerDescription: 'Triggered on a schedule',
|
||||
cronTrigger: 'Cron Trigger',
|
||||
cronTriggerDescription: 'Trigger workflow on a scheduled time',
|
||||
webhookTrigger: 'Webhook Trigger',
|
||||
webhookTriggerDescription: 'Triggered via HTTP request',
|
||||
eventTrigger: 'Event Trigger',
|
||||
eventTriggerDescription: 'Triggered on system events',
|
||||
process: 'AI/Process',
|
||||
processDescription: 'Data processing nodes',
|
||||
aiProcess: 'AI Processing',
|
||||
aiProcessDescription: 'Process messages using AI models',
|
||||
llmCall: 'LLM Call',
|
||||
llmCallDescription: 'Invoke large language model for conversation or generation',
|
||||
codeProcess: 'Code Processing',
|
||||
codeProcessDescription: 'Execute custom code',
|
||||
codeExecutor: 'Code Executor',
|
||||
codeExecutorDescription: 'Execute Python/JavaScript code',
|
||||
templateProcess: 'Template Processing',
|
||||
templateProcessDescription: 'Format output using templates',
|
||||
httpRequest: 'HTTP Request',
|
||||
httpRequestDescription: 'Send HTTP requests',
|
||||
dataTransform: 'Data Transform',
|
||||
dataTransformDescription: 'Transform data format',
|
||||
questionClassifier: 'Question Classifier',
|
||||
questionClassifierDescription: 'Classify user questions into predefined categories using LLM',
|
||||
parameterExtractor: 'Parameter Extractor',
|
||||
parameterExtractorDescription: 'Extract structured parameters from text using LLM',
|
||||
knowledgeRetrieval: 'Knowledge Retrieval',
|
||||
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base',
|
||||
textTemplate: 'Text Template',
|
||||
textTemplateDescription: 'Generate text using templates with variable interpolation',
|
||||
jsonTransform: 'JSON Transform',
|
||||
jsonTransformDescription: 'Transform JSON data using expressions',
|
||||
dataAggregator: 'Data Aggregator',
|
||||
dataAggregatorDescription: 'Aggregate data from multiple sources',
|
||||
textSplitter: 'Text Splitter',
|
||||
textSplitterDescription: 'Split text into smaller chunks',
|
||||
variableAssignment: 'Variable Assignment',
|
||||
variableAssignmentDescription: 'Assign values to workflow variables',
|
||||
control: 'Control Flow',
|
||||
controlDescription: 'Flow control nodes',
|
||||
condition: 'Condition Branch',
|
||||
conditionDescription: 'Branch based on conditions',
|
||||
switch: 'Switch',
|
||||
switchDescription: 'Multi-way branching',
|
||||
loop: 'Loop',
|
||||
loopDescription: 'Repeat execution',
|
||||
iterator: 'Iterator',
|
||||
iteratorDescription: 'Iterate over array elements',
|
||||
parallel: 'Parallel',
|
||||
parallelDescription: 'Execute multiple branches in parallel',
|
||||
wait: 'Wait',
|
||||
waitDescription: 'Wait for specified time',
|
||||
delay: 'Delay',
|
||||
delayDescription: 'Wait for a specified time',
|
||||
merge: 'Merge',
|
||||
mergeDescription: 'Merge multiple branches',
|
||||
variableAggregator: 'Variable Aggregator',
|
||||
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',
|
||||
storeData: 'Store Data',
|
||||
storeDataDescription: 'Store data to database',
|
||||
callPipeline: 'Call Pipeline',
|
||||
callPipelineDescription: 'Call an existing Pipeline',
|
||||
setVariable: 'Set Variable',
|
||||
setVariableDescription: 'Set context variable',
|
||||
openingStatement: 'Opening Statement',
|
||||
openingStatementDescription: 'Provide conversation opener and suggested questions',
|
||||
end: 'End',
|
||||
endDescription: 'Mark the end of workflow execution',
|
||||
log: 'Log',
|
||||
logDescription: 'Record log information',
|
||||
integration: 'Integration',
|
||||
integrationDescription: 'Third-party platform integration nodes',
|
||||
difyWorkflow: 'Dify Workflow',
|
||||
difyWorkflowDescription: 'Call Dify platform workflow',
|
||||
difyKnowledgeQuery: 'Dify Knowledge Query',
|
||||
difyKnowledgeQueryDescription: 'Query Dify knowledge base',
|
||||
n8nWorkflow: 'N8n Workflow',
|
||||
n8nWorkflowDescription: 'Call n8n workflow',
|
||||
langflowFlow: 'Langflow Flow',
|
||||
langflowFlowDescription: 'Call Langflow flow',
|
||||
cozeBot: 'Coze Bot',
|
||||
cozeBotDescription: 'Call Coze Bot',
|
||||
// Data & Tools integration nodes
|
||||
databaseQuery: 'Database Query',
|
||||
databaseQueryDescription: 'Execute database queries',
|
||||
redisOperation: 'Redis Operation',
|
||||
redisOperationDescription: 'Perform Redis cache operations',
|
||||
mcpTool: 'MCP Tool',
|
||||
mcpToolDescription: 'Invoke an MCP tool',
|
||||
memoryStore: 'Memory Store',
|
||||
memoryStoreDescription: 'Store and retrieve data from workflow memory',
|
||||
},
|
||||
executionHistory: {
|
||||
title: 'Execution History',
|
||||
noExecutions: 'No executions yet',
|
||||
status: 'Status',
|
||||
startTime: 'Start Time',
|
||||
duration: 'Duration',
|
||||
running: 'Running',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled',
|
||||
viewDetails: 'View Details',
|
||||
cancel: 'Cancel Execution',
|
||||
retry: 'Retry',
|
||||
nodeResults: 'Node Execution Results',
|
||||
},
|
||||
versions: {
|
||||
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.',
|
||||
rollbackSuccess: 'Rollback successful',
|
||||
rollbackError: 'Failed to rollback: ',
|
||||
},
|
||||
// Debug and monitoring
|
||||
debug: 'Debug',
|
||||
debugMode: 'Debug Mode',
|
||||
debugPanel: 'Debug Panel',
|
||||
startDebug: 'Start Debug',
|
||||
pauseDebug: 'Pause',
|
||||
resumeDebug: 'Resume',
|
||||
stepDebug: 'Step',
|
||||
stopDebug: 'Stop',
|
||||
debugContext: 'Debug Context',
|
||||
simulatedMessage: 'Simulated Message',
|
||||
simulatedMessagePlaceholder: 'Enter the message content to simulate',
|
||||
senderId: 'Sender ID',
|
||||
senderIdPlaceholder: 'Sender unique identifier',
|
||||
senderName: 'Sender Name',
|
||||
senderNamePlaceholder: 'Sender display name',
|
||||
conversationId: 'Conversation ID',
|
||||
conversationIdPlaceholder: 'Conversation unique identifier',
|
||||
isGroup: 'Group Chat',
|
||||
customVariables: 'Custom Variables',
|
||||
addVariable: 'Add Variable',
|
||||
variableName: 'Variable Name',
|
||||
variableValue: 'Variable Value',
|
||||
watchedVariables: 'Watched Variables',
|
||||
addWatchVariable: 'Add Watch',
|
||||
nodeStates: 'Node States',
|
||||
breakpoints: 'Breakpoints',
|
||||
toggleBreakpoint: 'Toggle Breakpoint',
|
||||
breakpointSet: 'Breakpoint set',
|
||||
breakpointRemoved: 'Breakpoint removed',
|
||||
debugLogs: 'Debug Logs',
|
||||
noLogs: 'No logs yet',
|
||||
clearLogs: 'Clear Logs',
|
||||
autoScroll: 'Auto Scroll',
|
||||
debugState: {
|
||||
idle: 'Idle',
|
||||
running: 'Running',
|
||||
paused: 'Paused',
|
||||
completed: 'Completed',
|
||||
error: 'Error',
|
||||
},
|
||||
nodeStatus: {
|
||||
pending: 'Pending',
|
||||
running: 'Running',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
skipped: 'Skipped',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'Workflow Chat',
|
||||
selectWorkflow: 'Select Workflow',
|
||||
sessionType: 'Session Type',
|
||||
privateChat: 'Private Chat',
|
||||
groupChat: 'Group Chat',
|
||||
send: 'Send',
|
||||
reset: 'Reset Conversation',
|
||||
inputPlaceholder: 'Send {{type}} message...',
|
||||
noMessages: 'No messages',
|
||||
userMessage: 'User',
|
||||
botMessage: 'Bot',
|
||||
sendFailed: 'Send failed',
|
||||
resetSuccess: 'Conversation reset successfully',
|
||||
resetFailed: 'Reset failed',
|
||||
loadMessagesFailed: 'Failed to load messages',
|
||||
loadWorkflowsFailed: 'Failed to load workflows',
|
||||
atTips: 'Mention bot',
|
||||
streaming: 'Streaming',
|
||||
streamOutput: 'Stream',
|
||||
connected: 'WebSocket connected',
|
||||
disconnected: 'WebSocket disconnected',
|
||||
connectionError: 'WebSocket connection error',
|
||||
connectionFailed: 'WebSocket connection failed',
|
||||
notConnected: 'WebSocket not connected, please try again later',
|
||||
imageUploadFailed: 'Image upload failed',
|
||||
reply: 'Reply',
|
||||
replyTo: 'Reply to',
|
||||
showMarkdown: 'Render',
|
||||
showRaw: 'Raw',
|
||||
allMembers: 'All Members',
|
||||
file: 'File',
|
||||
voice: 'Voice',
|
||||
uploadImage: 'Upload Image',
|
||||
uploading: 'Uploading...',
|
||||
},
|
||||
// Execution history and monitoring
|
||||
filterByDate: 'Filter by Date',
|
||||
allTime: 'All Time',
|
||||
today: 'Today',
|
||||
lastWeek: 'Last Week',
|
||||
lastMonth: 'Last Month',
|
||||
showingExecutions: 'Showing {{shown}} / {{total}} executions',
|
||||
rerun: 'Rerun',
|
||||
rerunExecution: 'Rerun Execution',
|
||||
logs: 'Logs',
|
||||
details: 'Details',
|
||||
completedAt: 'Completed At',
|
||||
noNodeExecutions: 'No node executions yet',
|
||||
// Node config field labels (used by DynamicFormComponent)
|
||||
nodeConfigFields: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
conditions: 'Trigger Conditions',
|
||||
keyword_filter: 'Keyword Filter',
|
||||
regex_filter: 'Regex Filter',
|
||||
min_length: 'Min Length',
|
||||
max_length: 'Max Length',
|
||||
require_mention: 'Require Mention',
|
||||
respond_rules: 'Group Respond Rules',
|
||||
access_control: 'Access Control',
|
||||
// trigger.py - CronTriggerNode
|
||||
cron: 'Cron Expression',
|
||||
timezone: 'Timezone',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
path: 'Webhook Path',
|
||||
allowed_methods: 'Allowed HTTP Methods',
|
||||
content_type: 'Content-Type',
|
||||
auth_type: 'Authentication Type',
|
||||
auth_key: 'Auth Key',
|
||||
validation: 'Request Validation',
|
||||
timeout: 'Timeout',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_types: 'Event Types',
|
||||
filter: 'Event Filter',
|
||||
debounce_ms: 'Debounce Time',
|
||||
// process.py - LLMCallNode
|
||||
model: 'Model',
|
||||
prompt_template: 'Prompt Template',
|
||||
system_prompt: 'System Prompt',
|
||||
temperature: 'Temperature',
|
||||
top_p: 'Top P',
|
||||
frequency_penalty: 'Frequency Penalty',
|
||||
presence_penalty: 'Presence Penalty',
|
||||
max_tokens: 'Max Tokens',
|
||||
stop_sequences: 'Stop Sequences',
|
||||
seed: 'Random Seed',
|
||||
stream: 'Stream Output',
|
||||
use_conversation_history: 'Use Conversation History',
|
||||
// process.py - CodeExecutorNode
|
||||
language: 'Programming Language',
|
||||
code: 'Code',
|
||||
// process.py - HTTPRequestNode
|
||||
url: 'Request URL',
|
||||
method: 'Request Method',
|
||||
auth_config: 'Auth Configuration',
|
||||
// process.py - DataTransformNode
|
||||
transform_type: 'Transform Type',
|
||||
template: 'Template',
|
||||
expression: 'Expression',
|
||||
output_type: 'Output Type',
|
||||
// process.py - QuestionClassifierNode
|
||||
categories: 'Categories',
|
||||
instruction: 'Instruction',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: 'Parameter Definitions',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
knowledge_bases: 'Knowledge Bases',
|
||||
top_k: 'Top K Results',
|
||||
score_threshold: 'Score Threshold',
|
||||
search_method: 'Search Method',
|
||||
enable_citations: 'Enable Citations',
|
||||
// control.py - ConditionNode
|
||||
condition_type: 'Condition Type',
|
||||
condition_expression: 'Condition Expression',
|
||||
left_value: 'Left Value',
|
||||
operator: 'Comparison Operator',
|
||||
right_value: 'Right Value',
|
||||
// control.py - SwitchNode
|
||||
cases: 'Branch Cases',
|
||||
// control.py - LoopNode
|
||||
max_iterations: 'Max Iterations',
|
||||
break_condition: 'Break Condition',
|
||||
// control.py - IteratorNode
|
||||
parallel: 'Parallel Processing',
|
||||
max_concurrency: 'Max Concurrency',
|
||||
// control.py - ParallelNode
|
||||
branches: 'Branch Configuration',
|
||||
wait_all: 'Wait All',
|
||||
fail_fast: 'Fail Fast',
|
||||
// control.py - WaitNode
|
||||
duration: 'Duration',
|
||||
duration_type: 'Time Unit',
|
||||
// control.py - MergeNode
|
||||
merge_strategy: 'Merge Strategy',
|
||||
// control.py - VariableAggregatorNode
|
||||
variable_mappings: 'Variable Mappings',
|
||||
aggregation_mode: 'Aggregation Mode',
|
||||
// action.py - SendMessageNode
|
||||
target_type: 'Target Type',
|
||||
target_id: 'Target ID',
|
||||
platform: 'Platform',
|
||||
message_type: 'Message Type',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_mode: 'Reply Mode',
|
||||
message_template: 'Message Template',
|
||||
long_text_processing: 'Long Text Processing',
|
||||
force_delay: 'Force Delay',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_uuid: 'Pipeline',
|
||||
inherit_context: 'Inherit Context',
|
||||
// action.py - StoreDataNode
|
||||
storage_type: 'Storage Type',
|
||||
ttl: 'TTL',
|
||||
key_prefix: 'Key Prefix',
|
||||
// action.py - SetVariableNode
|
||||
variable_name: 'Variable Name',
|
||||
variable_scope: 'Variable Scope',
|
||||
set_variable_operation: 'Operation Type',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: 'Opening Statement',
|
||||
suggested_questions: 'Suggested Questions',
|
||||
show_suggestions: 'Show Suggestions',
|
||||
// action.py - EndNode
|
||||
output_format: 'Output Format',
|
||||
success_message: 'Success Message',
|
||||
// integration.py - DatabaseQueryNode
|
||||
connection_type: 'Database Type',
|
||||
connection_string: 'Connection String',
|
||||
query: 'SQL Query',
|
||||
query_type: 'Query Type',
|
||||
// integration.py - RedisOperationNode
|
||||
connection_url: 'Connection URL',
|
||||
operation: 'Operation Type',
|
||||
key_template: 'Key Template',
|
||||
hash_field: 'Hash Field',
|
||||
// integration.py - MCPToolNode
|
||||
server_name: 'Server Name',
|
||||
tool_name: 'Tool Name',
|
||||
arguments_template: 'Arguments Template',
|
||||
// integration.py - MemoryStoreNode
|
||||
scope: 'Scope',
|
||||
// integration.py - DifyWorkflowNode
|
||||
'base-url': 'Base URL',
|
||||
'api-key': 'API Key',
|
||||
'app-type': 'App Type',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
dataset_id: 'Dataset ID',
|
||||
// integration.py - N8nWorkflowNode
|
||||
'webhook-url': 'Webhook URL',
|
||||
// integration.py - LangflowFlowNode
|
||||
'flow-id': 'Flow ID',
|
||||
// integration.py - CozeBotNode
|
||||
'bot-id': 'Bot ID',
|
||||
'api-base': 'API Base URL',
|
||||
},
|
||||
// Node outputs i18n
|
||||
nodeOutputs: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
message: 'Message Content',
|
||||
sender_id: 'Sender ID',
|
||||
sender_name: 'Sender Name',
|
||||
platform: 'Platform',
|
||||
conversation_id: 'Conversation ID',
|
||||
is_group: 'Is Group Chat',
|
||||
context: 'Full Message Context',
|
||||
// trigger.py - CronTriggerNode
|
||||
timestamp: 'Trigger Time',
|
||||
schedule: 'Trigger Schedule',
|
||||
cron_timestamp: 'Cron Timestamp',
|
||||
cron_schedule: 'Cron Schedule',
|
||||
cron_context: 'Cron Context',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
body: 'Request Body',
|
||||
headers: 'Request Headers',
|
||||
query: 'Query Parameters',
|
||||
method: 'Request Method',
|
||||
webhook_body: 'Webhook Body',
|
||||
webhook_headers: 'Webhook Headers',
|
||||
webhook_query: 'Webhook Query',
|
||||
webhook_method: 'Webhook Method',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_type: 'Event Type',
|
||||
event_data: 'Event Data',
|
||||
event_timestamp: 'Event Timestamp',
|
||||
// process.py - LLMCallNode
|
||||
response: 'Model Response',
|
||||
usage: 'Token Usage Statistics',
|
||||
prompt: 'Prompt/Question',
|
||||
context_info: 'Context Information',
|
||||
// process.py - CodeExecutorNode
|
||||
output: 'Output Data',
|
||||
console: 'Console Output',
|
||||
code_input: 'Code Input',
|
||||
code_output: 'Code Output',
|
||||
// process.py - HTTPRequestNode
|
||||
status_code: 'Status Code',
|
||||
http_body: 'HTTP Body',
|
||||
http_headers: 'HTTP Headers',
|
||||
http_response: 'HTTP Response',
|
||||
response_headers: 'Response Headers',
|
||||
// process.py - DataTransformNode
|
||||
result: 'Transform Result',
|
||||
transform_input: 'Transform Input',
|
||||
transform_result: 'Transform Result',
|
||||
// process.py - QuestionClassifierNode
|
||||
category: 'Category Result',
|
||||
confidence: 'Confidence',
|
||||
all_scores: 'All Category Scores',
|
||||
question: 'User Question',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: 'Extracted Parameters',
|
||||
extraction_success: 'Extraction Success',
|
||||
extract_text: 'Input Text',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
documents: 'Retrieved Documents',
|
||||
citations: 'Citation Information',
|
||||
knowledge_context: 'Merged Context',
|
||||
knowledge_query: 'Retrieval Query',
|
||||
// control.py - ConditionNode
|
||||
true: 'True Branch Output',
|
||||
false: 'False Branch Output',
|
||||
condition_input: 'Condition Input',
|
||||
// control.py - SwitchNode
|
||||
matched_case: 'Matched Branch Output',
|
||||
default: 'Default Branch Output',
|
||||
switch_input: 'Switch Input',
|
||||
// control.py - LoopNode
|
||||
item: 'Current Item',
|
||||
index: 'Current Index',
|
||||
results: 'All Iteration Results',
|
||||
completed: 'Is Completed',
|
||||
loop_items: 'Items to Iterate',
|
||||
// control.py - IteratorNode
|
||||
is_first: 'Is First',
|
||||
is_last: 'Is Last',
|
||||
iterator_array: 'Input Array',
|
||||
iterator_item: 'Current Element',
|
||||
iterator_index: 'Current Index',
|
||||
// control.py - ParallelNode
|
||||
errors: 'Error List',
|
||||
parallel_input: 'Parallel Input',
|
||||
parallel_results: 'All Branch Results',
|
||||
// control.py - WaitNode
|
||||
wait_input: 'Passthrough Input',
|
||||
wait_output: 'Passthrough Output',
|
||||
// control.py - MergeNode
|
||||
merged: 'Merged Result',
|
||||
merge_array: 'Array Result',
|
||||
merge_input_1: 'Input 1',
|
||||
merge_input_2: 'Input 2',
|
||||
merge_input_3: 'Input 3',
|
||||
merge_input_4: 'Input 4',
|
||||
// control.py - VariableAggregatorNode
|
||||
aggregated: 'Aggregated Variables',
|
||||
aggregator_variables: 'Variable Input',
|
||||
// action.py - SendMessageNode
|
||||
status: 'Send Status',
|
||||
message_id: 'Message ID',
|
||||
target: 'Target ID',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_message: 'Reply Content',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_response: 'Pipeline Response',
|
||||
pipeline_result: 'Full Result',
|
||||
pipeline_query: 'Query Content',
|
||||
context_data: 'Context Data',
|
||||
// action.py - StoreDataNode
|
||||
store_status: 'Store Status',
|
||||
store_key: 'Store Key',
|
||||
store_value: 'Store Value',
|
||||
// action.py - SetVariableNode
|
||||
variable_value: 'Variable Value',
|
||||
variable_result: 'Set Variable Result',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: 'Opening Statement',
|
||||
suggested_questions: 'Suggested Questions',
|
||||
// action.py - EndNode
|
||||
workflow_output: 'Workflow Output',
|
||||
final_result: 'Final Result',
|
||||
// integration.py - DatabaseQueryNode
|
||||
query_results: 'Query Results',
|
||||
row_count: 'Affected/Returned Rows',
|
||||
query_success: 'Query Success',
|
||||
query_params: 'Query Parameters',
|
||||
// integration.py - RedisOperationNode
|
||||
redis_result: 'Operation Result',
|
||||
redis_success: 'Operation Success',
|
||||
redis_key: 'Redis Key',
|
||||
redis_value: 'Redis Value',
|
||||
error: 'Error Message',
|
||||
plugin_input: 'Plugin Input',
|
||||
// integration.py - MCPToolNode
|
||||
tool_result: 'Tool Execution Result',
|
||||
tool_success: 'Tool Call Success',
|
||||
mcp_arguments: 'Tool Arguments',
|
||||
// integration.py - MemoryStoreNode
|
||||
memory_result: 'Retrieved/Stored Value',
|
||||
memory_success: 'Operation Success',
|
||||
memory_value: 'Value to Store',
|
||||
// integration.py - DifyWorkflowNode
|
||||
answer: 'Dify Answer',
|
||||
dify_success: 'Call Success',
|
||||
dify_query: 'User Input/Query',
|
||||
dify_conversation_id: 'Conversation ID',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
search_results: 'Search Results',
|
||||
knowledge_base_query: 'Query Content',
|
||||
// integration.py - N8nWorkflowNode
|
||||
n8n_result: 'Workflow Execution Result',
|
||||
n8n_success: 'Call Success',
|
||||
n8n_payload: 'Workflow Input Data',
|
||||
// integration.py - LangflowFlowNode
|
||||
flow_result: 'Flow Execution Result',
|
||||
flow_success: 'Call Success',
|
||||
langflow_input: 'Input Content',
|
||||
// integration.py - CozeBotNode
|
||||
bot_answer: 'Bot Reply',
|
||||
bot_success: 'Call Success',
|
||||
coze_query: 'User Input/Query',
|
||||
coze_conversation_id: 'Conversation ID',
|
||||
bot_conversation_id: 'Conversation ID',
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default viVN;
|
||||
|
||||
@@ -37,6 +37,9 @@ const zhHans = {
|
||||
select: '请选择',
|
||||
cancel: '取消',
|
||||
submit: '提交',
|
||||
next: '下一步',
|
||||
back: '返回',
|
||||
refresh: '刷新',
|
||||
error: '错误',
|
||||
success: '成功',
|
||||
save: '保存',
|
||||
@@ -56,6 +59,8 @@ const zhHans = {
|
||||
deleteError: '删除失败:',
|
||||
addRound: '添加回合',
|
||||
copy: '复制',
|
||||
undo: '撤销',
|
||||
redo: '重做',
|
||||
copySuccess: '复制成功',
|
||||
copyFailed: '复制失败',
|
||||
test: '测试',
|
||||
@@ -73,6 +78,7 @@ const zhHans = {
|
||||
loginLocal: '使用本地账号登录',
|
||||
loginWithPassword: '通过密码登录',
|
||||
spaceLoginTitle: '通过 Space 登录',
|
||||
details: '详情',
|
||||
spaceLoginDescription: '扫描二维码或访问下方链接进行授权',
|
||||
spaceLoginUserCode: '您的验证码',
|
||||
spaceLoginExpires: '验证码将在 {{seconds}} 秒后过期',
|
||||
@@ -139,6 +145,12 @@ const zhHans = {
|
||||
more: '更多 ({{count}})',
|
||||
less: '收起',
|
||||
noItems: '暂无内容',
|
||||
data: '数据',
|
||||
result: '结果',
|
||||
body: '请求体',
|
||||
headers: '请求头',
|
||||
query: '查询参数',
|
||||
method: '请求方法',
|
||||
},
|
||||
notFound: {
|
||||
title: '页面不存在',
|
||||
@@ -348,6 +360,16 @@ const zhHans = {
|
||||
pipelineDiscard: '丢弃消息',
|
||||
sessionTypePerson: '私聊',
|
||||
sessionTypeGroup: '群聊',
|
||||
// Unified binding (replacing routing rules)
|
||||
bindTarget: '绑定目标',
|
||||
bindTargetDescription: '选择处理此机器人消息的 Pipeline 或 Workflow',
|
||||
bindingType: '绑定类型',
|
||||
selectBinding: '请选择绑定目标',
|
||||
selectWorkflow: '选择工作流',
|
||||
noPipelinesFound: '暂无可用的流水线',
|
||||
noWorkflowsFound: '暂无可用的工作流',
|
||||
pipelineBindingHelp: '流水线是传统的消息处理方式,通过预定义的阶段处理消息。',
|
||||
workflowBindingHelp: '工作流提供可视化的节点编排,支持更灵活的消息处理逻辑。',
|
||||
adapterConfigDescription: '配置所选平台适配器',
|
||||
dangerZone: '危险区域',
|
||||
dangerZoneDescription: '不可逆的操作',
|
||||
@@ -1233,6 +1255,848 @@ const zhHans = {
|
||||
backToWorkbench: '返回工作台',
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
title: '工作流',
|
||||
description: '创建和管理可视化工作流,实现复杂的消息处理逻辑',
|
||||
createWorkflow: '创建工作流',
|
||||
selectFromSidebar: '从侧边栏选择一个工作流',
|
||||
editWorkflow: '编辑工作流',
|
||||
newWorkflow: '新工作流',
|
||||
getWorkflowListError: '获取工作流列表失败:',
|
||||
workflowName: '工作流名称',
|
||||
workflowDescription: '工作流描述',
|
||||
workflowNameRequired: '工作流名称不能为空',
|
||||
defaultDescription: '一个工作流',
|
||||
getWorkflowError: '获取工作流失败:',
|
||||
loadError: '加载工作流失败',
|
||||
saveSuccess: '保存成功',
|
||||
saveError: '保存失败:',
|
||||
createSuccess: '工作流创建成功',
|
||||
createError: '创建失败:',
|
||||
deleteSuccess: '删除成功',
|
||||
deleteError: '删除失败:',
|
||||
deleteConfirmation: '你确定要删除这个工作流吗?',
|
||||
copySuccess: '复制成功',
|
||||
copyError: '复制失败:',
|
||||
export: '导出',
|
||||
import: '导入',
|
||||
exportSuccess: '工作流已导出',
|
||||
importSuccess: '工作流已导入',
|
||||
importError: '导入失败:文件格式无效',
|
||||
publish: '发布',
|
||||
publishSuccess: '发布成功',
|
||||
publishError: '发布失败',
|
||||
configuration: '配置',
|
||||
executions: '执行记录',
|
||||
editor: '编辑器',
|
||||
debugChat: '对话调试',
|
||||
basicInfo: '基础信息',
|
||||
basicInfoDesc: '设置工作流名称、图标和描述',
|
||||
basicInfoDescription: '设置工作流名称和描述',
|
||||
dangerZone: '危险区域',
|
||||
dangerZoneDesc: '不可逆的操作',
|
||||
dangerZoneDescription: '不可逆的操作',
|
||||
deleteWorkflowAction: '删除此工作流',
|
||||
deleteWorkflowHint: '删除后,所有关联配置将被永久移除,且无法恢复。',
|
||||
deleteWorkflow: '删除工作流',
|
||||
deleteConfirm: '确认删除',
|
||||
deleteConfirmDesc: '您确定要删除工作流 "{{name}}" 吗?此操作无法撤销。',
|
||||
// Form component
|
||||
name: '名称',
|
||||
namePlaceholder: '输入工作流名称',
|
||||
descriptionPlaceholder: '输入工作流描述(可选)',
|
||||
enabled: '启用',
|
||||
enabledDesc: '启用后,工作流将可以被触发执行',
|
||||
loading: '加载中...',
|
||||
info: '工作流信息',
|
||||
uuid: 'UUID',
|
||||
version: '版本',
|
||||
createdAt: '创建时间',
|
||||
updatedAt: '更新时间',
|
||||
// Executions tab
|
||||
totalExecutions: '共 {{count}} 条执行记录',
|
||||
statistics: '统计分析',
|
||||
successfulCount: '成功 {{count}} 次',
|
||||
successRate: '成功率',
|
||||
averageDuration: '平均耗时',
|
||||
perExecution: '每次执行',
|
||||
failedExecutions: '失败次数',
|
||||
lastExecution: '最后执行',
|
||||
filterByStatus: '按状态筛选',
|
||||
allStatuses: '全部状态',
|
||||
manualTrigger: '手动触发',
|
||||
executionId: '执行 ID',
|
||||
status: '状态',
|
||||
triggerType: '触发类型',
|
||||
startedAt: '开始时间',
|
||||
duration: '耗时',
|
||||
noExecutions: '暂无执行记录',
|
||||
executionDetails: '执行详情',
|
||||
error: '错误信息',
|
||||
nodeExecutions: '节点执行记录',
|
||||
result: '执行结果',
|
||||
'status.pending': '等待中',
|
||||
'status.running': '执行中',
|
||||
'status.completed': '已完成',
|
||||
'status.failed': '失败',
|
||||
'status.cancelled': '已取消',
|
||||
// Editor component translations
|
||||
nodePalette: '节点面板',
|
||||
properties: '属性',
|
||||
zoomIn: '放大',
|
||||
zoomOut: '缩小',
|
||||
fitView: '适应视图',
|
||||
unsavedChanges: '有未保存的更改',
|
||||
paste: '粘贴',
|
||||
deleted: '已删除',
|
||||
nothingToCopy: '没有选中的节点可复制',
|
||||
nothingToPaste: '剪贴板为空',
|
||||
copied: '已复制 {{count}} 个节点',
|
||||
pasted: '已粘贴 {{count}} 个节点',
|
||||
nodesSelected: '已选中 {{count}} 个节点',
|
||||
edgesSelected: '已选中 {{count}} 条连线',
|
||||
// Node palette
|
||||
searchNodes: '搜索节点...',
|
||||
loadingNodeTypes: '正在加载节点类型...',
|
||||
noNodesFound: '未找到匹配的节点',
|
||||
clearSearch: '清除搜索',
|
||||
dragToAdd: '拖拽节点到画布添加',
|
||||
// Property panel
|
||||
selectNodeOrEdge: '选择一个节点或连线',
|
||||
selectNodeOrEdgeHint: '点击画布中的节点或连线来查看和编辑其属性',
|
||||
edgeProperties: '连线属性',
|
||||
nodeProperties: '节点属性',
|
||||
condition: '条件',
|
||||
hasCondition: '已设置',
|
||||
conditionPlaceholder: '输入条件表达式,如: output.success == true',
|
||||
conditionHelp: '条件为空时,该连线将始终被执行。支持使用 {{变量名}} 引用上下文变量。',
|
||||
deleteEdge: '删除连线',
|
||||
deleteEdgeConfirm: '确认删除连线',
|
||||
deleteEdgeConfirmDesc: '删除后,该连线将被永久移除。',
|
||||
nodeLabel: '节点名称',
|
||||
nodeLabelPlaceholder: '输入节点显示名称',
|
||||
nodeId: '节点 ID',
|
||||
inputOutputVariables: '输入/输出变量',
|
||||
inputs: '输入',
|
||||
outputs: '输出',
|
||||
availableVariables: '可用变量',
|
||||
globalVariables: '全局变量',
|
||||
messageContent: '消息内容',
|
||||
messageSender: '发送者',
|
||||
platform: '平台',
|
||||
sessionId: '会话 ID',
|
||||
timestamp: '时间戳',
|
||||
nodeConfig: '节点配置',
|
||||
noConfigOptions: '该节点类型暂无配置选项',
|
||||
deleteNode: '删除节点',
|
||||
deleteNodeConfirm: '确认删除节点',
|
||||
deleteNodeConfirmDesc: '删除后,该节点及其所有连线将被永久移除。',
|
||||
// Node inputs/outputs i18n (for port labels)
|
||||
nodeInputs: {
|
||||
// Common inputs
|
||||
input: '输入',
|
||||
message: '消息内容',
|
||||
text: '文本',
|
||||
query: '查询',
|
||||
data: '数据',
|
||||
condition: '条件',
|
||||
value: '值',
|
||||
// Trigger inputs
|
||||
content: '内容',
|
||||
context: '上下文',
|
||||
body: '请求体',
|
||||
variables: '变量',
|
||||
items: '项目列表',
|
||||
arguments: '参数',
|
||||
// AI/Process inputs
|
||||
question: '问题',
|
||||
parameters: '参数定义',
|
||||
key: '键',
|
||||
payload: '载荷',
|
||||
input_value: '输入值',
|
||||
conversation_id: '会话 ID',
|
||||
// Control inputs
|
||||
case_1: '分支1输入',
|
||||
case_2: '分支2输入',
|
||||
branch_1: '分支1',
|
||||
branch_2: '分支2',
|
||||
// Action inputs
|
||||
notification_id: '通知ID',
|
||||
// Integration inputs
|
||||
key_template: '键模板',
|
||||
hash_field: '哈希字段',
|
||||
server_name: '服务器名称',
|
||||
tool_name: '工具名称',
|
||||
arguments_template: '参数模板',
|
||||
scope: '作用域',
|
||||
},
|
||||
nodeOutputs: {
|
||||
// Common outputs
|
||||
output: '输出',
|
||||
result: '结果',
|
||||
response: '响应',
|
||||
message: '消息',
|
||||
data: '数据',
|
||||
error: '错误',
|
||||
// Trigger outputs
|
||||
sender: '发送者',
|
||||
sender_id: '发送者 ID',
|
||||
sender_name: '发送者名称',
|
||||
conversation_id: '会话 ID',
|
||||
context: '上下文',
|
||||
cron_timestamp: 'Cron 时间戳',
|
||||
cron_schedule: 'Cron 调度表达式',
|
||||
cron_context: 'Cron 上下文',
|
||||
trigger_time: '触发时间',
|
||||
schedule: '触发计划',
|
||||
headers: '请求头',
|
||||
query_params: '查询参数',
|
||||
query: '查询参数',
|
||||
method: '请求方法',
|
||||
body: '请求体',
|
||||
is_group: '是否群聊',
|
||||
platform: '平台',
|
||||
event: '事件',
|
||||
event_type: '事件类型',
|
||||
event_data: '事件数据',
|
||||
event_timestamp: '事件时间戳',
|
||||
// AI/Process outputs
|
||||
usage: 'Token使用统计',
|
||||
parsed: '解析结果',
|
||||
category: '分类结果',
|
||||
confidence: '置信度',
|
||||
all_scores: '所有分数',
|
||||
missing: '缺失参数',
|
||||
success: '是否成功',
|
||||
scores: '分数',
|
||||
chunks: '文本块',
|
||||
count: '数量',
|
||||
logs: '日志',
|
||||
embedding: '向量',
|
||||
dimensions: '维度',
|
||||
intent: '意图',
|
||||
entities: '实体',
|
||||
prompt: '提示/问题',
|
||||
context_info: '上下文信息',
|
||||
console: '控制台输出',
|
||||
code_input: '代码输入',
|
||||
code_output: '代码输出',
|
||||
http_body: 'HTTP 请求体',
|
||||
http_headers: 'HTTP 请求头',
|
||||
http_response: 'HTTP 响应',
|
||||
response_headers: '响应头',
|
||||
transform_input: '转换输入',
|
||||
transform_result: '转换结果',
|
||||
question: '用户问题',
|
||||
parameters: '提取的参数',
|
||||
extraction_success: '提取成功',
|
||||
extract_text: '输入文本',
|
||||
documents: '检索的文档',
|
||||
citations: '引用信息',
|
||||
knowledge_context: '合并上下文',
|
||||
knowledge_query: '检索查询',
|
||||
text: '文本',
|
||||
// Control outputs
|
||||
true: '条件为真输出',
|
||||
false: '条件为假输出',
|
||||
matched_case: '匹配分支',
|
||||
case_1: '分支 1',
|
||||
case_2: '分支 2',
|
||||
default: '默认分支',
|
||||
item: '当前项',
|
||||
index: '当前索引',
|
||||
completed: '是否完成',
|
||||
is_first: '是否第一个',
|
||||
is_last: '是否最后一个',
|
||||
results: '结果',
|
||||
branch_1: '分支1输出',
|
||||
branch_2: '分支2输出',
|
||||
condition_input: '条件输入',
|
||||
switch_input: '开关输入',
|
||||
loop_items: '迭代项目',
|
||||
iterator_array: '输入数组',
|
||||
iterator_item: '当前元素',
|
||||
iterator_index: '当前索引',
|
||||
errors: '错误列表',
|
||||
parallel_input: '并行输入',
|
||||
parallel_results: '所有分支结果',
|
||||
wait_input: '透传输入',
|
||||
wait_output: '透传输出',
|
||||
merged: '合并结果',
|
||||
merge_array: '数组结果',
|
||||
merge_input_1: '输入 1',
|
||||
merge_input_2: '输入 2',
|
||||
merge_input_3: '输入 3',
|
||||
merge_input_4: '输入 4',
|
||||
aggregated: '聚合变量',
|
||||
aggregator_variables: '变量输入',
|
||||
// Action outputs
|
||||
message_id: '消息ID',
|
||||
status_code: '状态码',
|
||||
status: '状态',
|
||||
execution_id: '执行ID',
|
||||
notification_id: '通知ID',
|
||||
target: '目标 ID',
|
||||
reply_message: '回复内容',
|
||||
pipeline_response: 'Pipeline 响应',
|
||||
pipeline_result: '完整结果',
|
||||
pipeline_query: '查询内容',
|
||||
context_data: '上下文数据',
|
||||
store_status: '存储状态',
|
||||
store_key: '存储键',
|
||||
store_value: '存储值',
|
||||
variable_value: '变量值',
|
||||
variable_result: '设置变量结果',
|
||||
statement: '开场白',
|
||||
suggested_questions: '建议问题',
|
||||
suggestions: '建议问题',
|
||||
workflow_output: '工作流输出',
|
||||
final_result: '最终结果',
|
||||
// Integration outputs
|
||||
query_results: '查询结果',
|
||||
row_count: '行数',
|
||||
query_success: '查询成功',
|
||||
redis_result: 'Redis结果',
|
||||
redis_success: 'Redis成功',
|
||||
redis_key: 'Redis 键',
|
||||
redis_value: 'Redis 值',
|
||||
plugin_input: '插件输入',
|
||||
tool_result: '工具结果',
|
||||
tool_success: '工具成功',
|
||||
mcp_arguments: '工具参数',
|
||||
memory_result: '记忆结果',
|
||||
memory_success: '记忆成功',
|
||||
memory_value: '存储值',
|
||||
answer: '答案',
|
||||
dify_success: 'Dify成功',
|
||||
dify_query: '用户输入/查询',
|
||||
dify_conversation_id: '会话 ID',
|
||||
search_results: '搜索结果',
|
||||
knowledge_base_query: '查询内容',
|
||||
n8n_result: 'N8n结果',
|
||||
n8n_success: 'N8n成功',
|
||||
n8n_payload: '工作流输入数据',
|
||||
flow_result: '流程结果',
|
||||
flow_success: '流程成功',
|
||||
langflow_input: '输入内容',
|
||||
bot_answer: '机器人回复',
|
||||
bot_success: '机器人成功',
|
||||
coze_query: '用户输入/查询',
|
||||
coze_conversation_id: '会话 ID',
|
||||
bot_conversation_id: '会话 ID',
|
||||
webhook_body: 'Webhook 请求体',
|
||||
webhook_headers: 'Webhook 请求头',
|
||||
webhook_query: 'Webhook 查询参数',
|
||||
webhook_method: 'Webhook 请求方法',
|
||||
timestamp: '时间戳',
|
||||
},
|
||||
// Data type labels
|
||||
'type.string': '字符串',
|
||||
'type.object': '对象',
|
||||
'type.array': '数组',
|
||||
'type.boolean': '布尔值',
|
||||
'type.number': '数字',
|
||||
'type.any': '任意类型',
|
||||
'type.datetime': '日期时间',
|
||||
// Legacy editor keys for compatibility
|
||||
legacyEditor: {
|
||||
title: '可视化编辑器',
|
||||
save: '保存',
|
||||
undo: '撤销',
|
||||
redo: '重做',
|
||||
zoomIn: '放大',
|
||||
zoomOut: '缩小',
|
||||
fitView: '适应视图',
|
||||
unsavedChanges: '有未保存的更改',
|
||||
nodePalette: '节点面板',
|
||||
properties: '属性',
|
||||
nodeLabel: '节点名称',
|
||||
nodeConfig: '节点配置',
|
||||
deleteNode: '删除节点',
|
||||
deleteNodeConfirm: '确定删除此节点?',
|
||||
deleteEdge: '删除连线',
|
||||
deleteEdgeConfirm: '确定删除此连线?',
|
||||
edgeCondition: '条件表达式',
|
||||
edgeConditionPlaceholder: '如: ${output.success} == true',
|
||||
noNodeSelected: '未选中节点',
|
||||
selectNodeToEdit: '点击节点查看和编辑属性',
|
||||
dragNodeHint: '拖拽节点到画布',
|
||||
},
|
||||
nodes: {
|
||||
trigger: '触发器',
|
||||
triggerDescription: '工作流的起始节点',
|
||||
messageTrigger: '消息触发',
|
||||
messageTriggerDescription: '当收到消息时触发',
|
||||
scheduleTrigger: '定时触发',
|
||||
scheduleTriggerDescription: '按计划定时触发',
|
||||
cronTrigger: '定时触发',
|
||||
cronTriggerDescription: '按定时计划触发工作流',
|
||||
webhookTrigger: 'Webhook 触发',
|
||||
webhookTriggerDescription: '通过 HTTP 请求触发',
|
||||
eventTrigger: '事件触发',
|
||||
eventTriggerDescription: '当系统事件发生时触发',
|
||||
process: 'AI/处理',
|
||||
processDescription: '数据处理节点',
|
||||
aiProcess: 'AI 处理',
|
||||
aiProcessDescription: '使用 AI 模型处理消息',
|
||||
llmCall: 'LLM 调用',
|
||||
llmCallDescription: '调用大语言模型进行对话或生成',
|
||||
codeProcess: '代码处理',
|
||||
codeProcessDescription: '执行自定义代码',
|
||||
codeExecutor: '代码执行',
|
||||
codeExecutorDescription: '执行 Python/JavaScript 代码',
|
||||
templateProcess: '模板处理',
|
||||
templateProcessDescription: '使用模板格式化输出',
|
||||
httpRequest: 'HTTP 请求',
|
||||
httpRequestDescription: '发送 HTTP 请求',
|
||||
dataTransform: '数据转换',
|
||||
dataTransformDescription: '转换数据格式',
|
||||
questionClassifier: '问题分类器',
|
||||
questionClassifierDescription: '使用 LLM 将用户问题分类到预定义类别',
|
||||
parameterExtractor: '参数提取器',
|
||||
parameterExtractorDescription: '使用 LLM 从文本中提取结构化参数',
|
||||
knowledgeRetrieval: '知识库检索',
|
||||
knowledgeRetrievalDescription: '从知识库中检索相关内容',
|
||||
textTemplate: '文本模板',
|
||||
textTemplateDescription: '使用模板生成文本',
|
||||
jsonTransform: 'JSON 转换',
|
||||
jsonTransformDescription: '转换 JSON 数据',
|
||||
dataAggregator: '数据聚合',
|
||||
dataAggregatorDescription: '聚合多个数据源',
|
||||
textSplitter: '文本分割',
|
||||
textSplitterDescription: '将文本分割成块',
|
||||
variableAssignment: '变量赋值',
|
||||
variableAssignmentDescription: '为工作流变量赋值',
|
||||
control: '控制流',
|
||||
controlDescription: '流程控制节点',
|
||||
condition: '条件分支',
|
||||
conditionDescription: '根据条件分流',
|
||||
switch: '多路分支',
|
||||
switchDescription: '多条件分支选择',
|
||||
loop: '循环',
|
||||
loopDescription: '重复执行',
|
||||
iterator: '迭代器',
|
||||
iteratorDescription: '遍历数组元素',
|
||||
parallel: '并行执行',
|
||||
parallelDescription: '并行执行多个分支',
|
||||
wait: '等待',
|
||||
waitDescription: '等待指定时间',
|
||||
delay: '延迟',
|
||||
delayDescription: '等待指定时间',
|
||||
merge: '合并',
|
||||
mergeDescription: '合并多个分支',
|
||||
variableAggregator: '变量聚合器',
|
||||
variableAggregatorDescription: '聚合多个分支的变量输出',
|
||||
action: '动作',
|
||||
actionDescription: '执行动作的节点',
|
||||
sendMessage: '发送消息',
|
||||
sendMessageDescription: '发送消息到平台',
|
||||
replyMessage: '回复消息',
|
||||
replyMessageDescription: '回复触发工作流的消息',
|
||||
storeData: '存储数据',
|
||||
storeDataDescription: '存储数据到数据库',
|
||||
callPipeline: '调用 Pipeline',
|
||||
callPipelineDescription: '调用现有的 Pipeline',
|
||||
setVariable: '设置变量',
|
||||
setVariableDescription: '设置上下文变量',
|
||||
openingStatement: '对话开场白',
|
||||
openingStatementDescription: '提供对话开场白和建议问题',
|
||||
end: '结束',
|
||||
endDescription: '标记工作流执行结束',
|
||||
log: '日志',
|
||||
logDescription: '记录日志信息',
|
||||
integration: '集成',
|
||||
integrationDescription: '第三方平台集成节点',
|
||||
difyWorkflow: 'Dify 工作流',
|
||||
difyWorkflowDescription: '调用 Dify 平台工作流',
|
||||
difyKnowledgeQuery: 'Dify 知识库',
|
||||
difyKnowledgeQueryDescription: '查询 Dify 知识库',
|
||||
n8nWorkflow: 'n8n 工作流',
|
||||
n8nWorkflowDescription: '调用 n8n 工作流',
|
||||
langflowFlow: 'Langflow 流程',
|
||||
langflowFlowDescription: '调用 Langflow 流程',
|
||||
cozeBot: 'Coze Bot',
|
||||
cozeBotDescription: '调用扣子 Bot',
|
||||
// Data & Tools integration nodes
|
||||
databaseQuery: '数据库查询',
|
||||
databaseQueryDescription: '执行数据库查询',
|
||||
redisOperation: 'Redis 操作',
|
||||
redisOperationDescription: '执行 Redis 缓存操作',
|
||||
mcpTool: 'MCP 工具',
|
||||
mcpToolDescription: '调用 MCP 工具',
|
||||
memoryStore: '记忆存储',
|
||||
memoryStoreDescription: '从工作流记忆中存储和检索数据',
|
||||
},
|
||||
executionHistory: {
|
||||
title: '执行记录',
|
||||
noExecutions: '暂无执行记录',
|
||||
status: '状态',
|
||||
startTime: '开始时间',
|
||||
duration: '耗时',
|
||||
running: '运行中',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
cancelled: '已取消',
|
||||
viewDetails: '查看详情',
|
||||
cancel: '取消执行',
|
||||
retry: '重试',
|
||||
nodeResults: '节点执行结果',
|
||||
},
|
||||
versions: {
|
||||
title: '版本历史',
|
||||
current: '当前版本',
|
||||
rollback: '回滚到此版本',
|
||||
rollbackConfirm: '确定回滚到此版本?当前更改将丢失。',
|
||||
rollbackSuccess: '回滚成功',
|
||||
rollbackError: '回滚失败:',
|
||||
},
|
||||
// Debug and monitoring
|
||||
debug: {
|
||||
title: '调试',
|
||||
mode: '调试模式',
|
||||
panel: '调试面板',
|
||||
start: '开始调试',
|
||||
pause: '暂停',
|
||||
resume: '继续',
|
||||
step: '单步执行',
|
||||
stop: '停止',
|
||||
context: '调试上下文',
|
||||
messageContent: '模拟消息',
|
||||
messageContentPlaceholder: '输入要模拟的消息内容',
|
||||
senderId: '发送者 ID',
|
||||
senderIdPlaceholder: '发送者唯一标识',
|
||||
senderName: '发送者名称',
|
||||
senderNamePlaceholder: '发送者显示名称',
|
||||
platform: '平台',
|
||||
platformPlaceholder: '例如 qq、wechat、telegram',
|
||||
conversationId: '会话 ID',
|
||||
conversationIdPlaceholder: '会话唯一标识',
|
||||
isGroup: '群聊',
|
||||
customVariables: '自定义变量',
|
||||
customVariablesDesc: '添加自定义变量用于调试',
|
||||
variableKey: '变量名',
|
||||
variableValue: '变量值',
|
||||
addVariable: '添加变量',
|
||||
variables: '监控变量',
|
||||
watchedVariables: '监控变量',
|
||||
noWatchedVariables: '暂无监控变量',
|
||||
addWatchVariable: '添加监控',
|
||||
nodeStates: '节点状态',
|
||||
nodeOutputs: '节点输出',
|
||||
noNodeOutputs: '暂无节点输出',
|
||||
toggleBreakpoint: '切换断点',
|
||||
clearBreakpoints: '清除所有断点',
|
||||
breakpointSet: '断点已设置',
|
||||
breakpointRemoved: '断点已移除',
|
||||
logs: '调试日志',
|
||||
noLogs: '暂无日志',
|
||||
clearLogs: '清空日志',
|
||||
autoScroll: '自动滚动',
|
||||
logEntries: '条日志',
|
||||
resetContext: '重置上下文',
|
||||
// Debug execution messages
|
||||
starting: '正在启动调试执行...',
|
||||
started: '调试执行已启动 (ID: {{id}})',
|
||||
startError: '启动调试失败',
|
||||
completed: '调试执行已完成',
|
||||
unknownError: '未知错误',
|
||||
paused: '执行已暂停',
|
||||
pauseError: '暂停失败',
|
||||
resumed: '执行已恢复',
|
||||
resumeError: '恢复失败',
|
||||
steppedTo: '已执行到节点: {{node}}',
|
||||
stepError: '单步执行失败',
|
||||
stopped: '调试已停止',
|
||||
stopError: '停止失败',
|
||||
},
|
||||
debugMode: '调试模式',
|
||||
debugPanel: '调试面板',
|
||||
startDebug: '开始调试',
|
||||
pauseDebug: '暂停',
|
||||
resumeDebug: '继续',
|
||||
stepDebug: '单步执行',
|
||||
stopDebug: '停止',
|
||||
debugContext: '调试上下文',
|
||||
simulatedMessage: '模拟消息',
|
||||
simulatedMessagePlaceholder: '输入要模拟的消息内容',
|
||||
senderId: '发送者 ID',
|
||||
senderIdPlaceholder: '发送者唯一标识',
|
||||
senderName: '发送者名称',
|
||||
senderNamePlaceholder: '发送者显示名称',
|
||||
conversationId: '会话 ID',
|
||||
conversationIdPlaceholder: '会话唯一标识',
|
||||
isGroup: '群聊',
|
||||
customVariables: '自定义变量',
|
||||
addVariable: '添加变量',
|
||||
variableName: '变量名',
|
||||
variableValue: '变量值',
|
||||
watchedVariables: '监控变量',
|
||||
addWatchVariable: '添加监控',
|
||||
nodeStates: '节点状态',
|
||||
breakpoints: '断点',
|
||||
toggleBreakpoint: '切换断点',
|
||||
breakpointSet: '断点已设置',
|
||||
breakpointRemoved: '断点已移除',
|
||||
debugLogs: '调试日志',
|
||||
noLogs: '暂无日志',
|
||||
clearLogs: '清空日志',
|
||||
autoScroll: '自动滚动',
|
||||
debugState: {
|
||||
idle: '空闲',
|
||||
running: '运行中',
|
||||
paused: '已暂停',
|
||||
completed: '已完成',
|
||||
error: '错误',
|
||||
},
|
||||
nodeStatus: {
|
||||
pending: '等待中',
|
||||
running: '执行中',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
skipped: '已跳过',
|
||||
},
|
||||
debugDialog: {
|
||||
title: '工作流对话',
|
||||
selectWorkflow: '选择工作流',
|
||||
sessionType: '会话类型',
|
||||
privateChat: '私聊',
|
||||
groupChat: '群聊',
|
||||
send: '发送',
|
||||
reset: '重置对话',
|
||||
inputPlaceholder: '发送 {{type}} 消息...',
|
||||
noMessages: '暂无消息',
|
||||
userMessage: '用户',
|
||||
botMessage: '机器人',
|
||||
sendFailed: '发送失败',
|
||||
resetSuccess: '对话已重置',
|
||||
resetFailed: '重置失败',
|
||||
loadMessagesFailed: '加载消息失败',
|
||||
loadWorkflowsFailed: '加载工作流失败',
|
||||
atTips: '提及机器人',
|
||||
streaming: '流式传输',
|
||||
streamOutput: '流式',
|
||||
connected: 'WebSocket已连接',
|
||||
disconnected: 'WebSocket未连接',
|
||||
connectionError: 'WebSocket连接错误',
|
||||
connectionFailed: 'WebSocket连接失败',
|
||||
notConnected: 'WebSocket未连接,请稍后重试',
|
||||
imageUploadFailed: '图片上传失败',
|
||||
reply: '回复',
|
||||
replyTo: '回复给',
|
||||
showMarkdown: '渲染',
|
||||
showRaw: '原文',
|
||||
allMembers: '全体成员',
|
||||
file: '文件',
|
||||
voice: '语音',
|
||||
uploadImage: '上传图片',
|
||||
uploading: '上传中...',
|
||||
},
|
||||
// Execution history and monitoring
|
||||
filterByDate: '按日期筛选',
|
||||
allTime: '全部时间',
|
||||
today: '今天',
|
||||
lastWeek: '最近一周',
|
||||
lastMonth: '最近一个月',
|
||||
showingExecutions: '显示 {{shown}} / {{total}} 条记录',
|
||||
rerun: '重新运行',
|
||||
rerunExecution: '重新执行',
|
||||
logs: '日志',
|
||||
details: '详情',
|
||||
completedAt: '完成时间',
|
||||
noNodeExecutions: '暂无节点执行记录',
|
||||
// Node config field labels (used by DynamicFormComponent)
|
||||
nodeConfigFields: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
conditions: '触发条件',
|
||||
keyword_filter: '关键词过滤',
|
||||
regex_filter: '正则过滤',
|
||||
min_length: '最小长度',
|
||||
max_length: '最大长度',
|
||||
require_mention: '需要@机器人',
|
||||
respond_rules: '群响应规则',
|
||||
access_control: '访问控制',
|
||||
// trigger.py - CronTriggerNode
|
||||
cron: 'Cron表达式',
|
||||
timezone: '时区',
|
||||
description: '描述',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
path: 'Webhook路径',
|
||||
allowed_methods: '允许的HTTP方法',
|
||||
content_type: 'Content-Type',
|
||||
auth_type: '认证方式',
|
||||
auth_key: '认证密钥',
|
||||
validation: '请求验证',
|
||||
timeout: '超时时间',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_types: '事件类型',
|
||||
filter: '事件过滤',
|
||||
debounce_ms: '防抖时间',
|
||||
// process.py - LLMCallNode
|
||||
model: '模型',
|
||||
prompt_template: '提示词模板',
|
||||
system_prompt: '系统提示词',
|
||||
temperature: '温度',
|
||||
top_p: 'Top P',
|
||||
frequency_penalty: '频率惩罚',
|
||||
presence_penalty: '存在惩罚',
|
||||
max_tokens: '最大Token数',
|
||||
stop_sequences: '停止序列',
|
||||
seed: '随机种子',
|
||||
stream: '流式输出',
|
||||
use_conversation_history: '使用对话历史',
|
||||
// process.py - CodeExecutorNode
|
||||
language: '编程语言',
|
||||
code: '代码',
|
||||
// process.py - HTTPRequestNode
|
||||
url: '请求URL',
|
||||
method: '请求方法',
|
||||
auth_config: '认证配置',
|
||||
// process.py - DataTransformNode
|
||||
transform_type: '转换类型',
|
||||
template: '模板',
|
||||
expression: '表达式',
|
||||
output_type: '输出类型',
|
||||
// process.py - QuestionClassifierNode
|
||||
categories: '分类类别',
|
||||
instruction: '指令',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: '参数定义',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
knowledge_bases: '知识库',
|
||||
top_k: '返回数量',
|
||||
score_threshold: '相似度阈值',
|
||||
search_method: '搜索方法',
|
||||
enable_citations: '启用引用',
|
||||
// control.py - ConditionNode
|
||||
condition_type: '条件类型',
|
||||
condition_expression: '条件表达式',
|
||||
left_value: '左值',
|
||||
operator: '比较运算符',
|
||||
right_value: '右值',
|
||||
// control.py - SwitchNode
|
||||
cases: '分支条件',
|
||||
// control.py - LoopNode
|
||||
max_iterations: '最大迭代次数',
|
||||
break_condition: '中断条件',
|
||||
// control.py - IteratorNode
|
||||
parallel: '并行处理',
|
||||
max_concurrency: '最大并发数',
|
||||
// control.py - ParallelNode
|
||||
branches: '分支配置',
|
||||
wait_all: '等待所有',
|
||||
fail_fast: '快速失败',
|
||||
// control.py - WaitNode
|
||||
duration: '等待时间',
|
||||
duration_type: '时间单位',
|
||||
// control.py - MergeNode
|
||||
merge_strategy: '合并策略',
|
||||
// control.py - VariableAggregatorNode
|
||||
variable_mappings: '变量映射',
|
||||
aggregation_mode: '聚合模式',
|
||||
// action.py - SendMessageNode
|
||||
target_type: '目标类型',
|
||||
target_id: '目标ID',
|
||||
platform: '平台',
|
||||
message_type: '消息类型',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_mode: '回复模式',
|
||||
message_template: '消息模板',
|
||||
long_text_processing: '长文本处理',
|
||||
force_delay: '强制延迟',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_uuid: '流水线',
|
||||
inherit_context: '继承上下文',
|
||||
// action.py - StoreDataNode
|
||||
storage_type: '存储类型',
|
||||
ttl: '过期时间',
|
||||
key_prefix: '键前缀',
|
||||
// action.py - SetVariableNode
|
||||
variable_name: '变量名称',
|
||||
variable_scope: '变量作用域',
|
||||
set_variable_operation: '操作类型',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: '开场白',
|
||||
suggested_questions: '建议问题',
|
||||
show_suggestions: '显示建议',
|
||||
// action.py - EndNode
|
||||
output_format: '输出格式',
|
||||
success_message: '成功消息',
|
||||
// integration.py - DatabaseQueryNode
|
||||
connection_type: '数据库类型',
|
||||
connection_string: '连接字符串',
|
||||
query: 'SQL查询',
|
||||
query_type: '查询类型',
|
||||
// integration.py - RedisOperationNode
|
||||
connection_url: '连接URL',
|
||||
operation: '操作类型',
|
||||
key_template: '键模板',
|
||||
hash_field: '哈希字段',
|
||||
// integration.py - MCPToolNode
|
||||
server_name: '服务器名称',
|
||||
tool_name: '工具名称',
|
||||
arguments_template: '参数模板',
|
||||
// integration.py - MemoryStoreNode
|
||||
scope: '作用域',
|
||||
// integration.py - DifyWorkflowNode
|
||||
'base-url': '基础URL',
|
||||
'api-key': 'API密钥',
|
||||
'app-type': '应用类型',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
dataset_id: '知识库ID',
|
||||
// integration.py - N8nWorkflowNode
|
||||
'webhook-url': 'Webhook URL',
|
||||
// integration.py - LangflowFlowNode
|
||||
'flow-id': '流程ID',
|
||||
// integration.py - CozeBotNode
|
||||
'bot-id': '机器人ID',
|
||||
'api-base': 'API基础URL',
|
||||
// process-configs.ts - TextTemplateNode
|
||||
escape_html: '转义HTML',
|
||||
trim_whitespace: '去除空白',
|
||||
// process-configs.ts - JsonTransformNode
|
||||
json_transform_type: 'JSON转换类型',
|
||||
json_expression: 'JSON表达式',
|
||||
mapping: '字段映射',
|
||||
// process-configs.ts - CodeExecutorNode
|
||||
code_language: '编程语言',
|
||||
code_content: '代码内容',
|
||||
// process-configs.ts - DataAggregatorNode
|
||||
aggregation_type: '聚合类型',
|
||||
separator: '分隔符',
|
||||
field_path: '字段路径',
|
||||
// process-configs.ts - TextSplitterNode
|
||||
split_type: '分割类型',
|
||||
chunk_size: '块大小',
|
||||
chunk_overlap: '块重叠',
|
||||
regex_pattern: '正则表达式',
|
||||
remove_empty: '移除空块',
|
||||
// process-configs.ts - VariableAssignmentNode
|
||||
assign_variable_name: '变量名',
|
||||
value_type: '值类型',
|
||||
static_value: '静态值',
|
||||
// integration-configs.ts - N8nWorkflowNode
|
||||
n8n_webhook_url: 'Webhook URL',
|
||||
n8n_auth_type: '认证方式',
|
||||
// integration-configs.ts - LangflowFlowNode
|
||||
langflow_flow_id: '流程ID',
|
||||
// integration-configs.ts - CozeBotNode
|
||||
coze_bot_id: '机器人ID',
|
||||
coze_api_base: 'API基础URL',
|
||||
},
|
||||
},
|
||||
unifiedBinding: {
|
||||
bindingType: '绑定类型',
|
||||
pipeline: '流水线',
|
||||
workflow: '工作流',
|
||||
pipelineDescription: '使用传统流水线处理消息',
|
||||
workflowDescription: '使用可视化工作流处理消息',
|
||||
selectPipeline: '选择流水线',
|
||||
selectWorkflow: '选择工作流',
|
||||
noPipelines: '暂无流水线',
|
||||
noWorkflows: '暂无工作流',
|
||||
createPipeline: '创建流水线',
|
||||
createWorkflow: '创建工作流',
|
||||
},
|
||||
};
|
||||
|
||||
export default zhHans;
|
||||
|
||||
@@ -1233,6 +1233,726 @@ const zhHant = {
|
||||
backToWorkbench: '返回工作台',
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
title: 'Workflows',
|
||||
description: 'Create and manage visual workflows for complex message processing logic',
|
||||
createWorkflow: 'Create Workflow',
|
||||
selectFromSidebar: 'Select a workflow from the sidebar',
|
||||
editWorkflow: 'Edit Workflow',
|
||||
newWorkflow: 'New Workflow',
|
||||
getWorkflowListError: 'Failed to get workflow list: ',
|
||||
workflowName: 'Workflow Name',
|
||||
workflowDescription: 'Workflow Description',
|
||||
workflowNameRequired: 'Workflow name is required',
|
||||
defaultDescription: 'A workflow',
|
||||
getWorkflowError: 'Failed to get workflow: ',
|
||||
loadError: 'Failed to load workflow',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveError: 'Failed to save: ',
|
||||
createSuccess: 'Workflow created successfully',
|
||||
createError: 'Failed to create: ',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
deleteError: 'Failed to delete: ',
|
||||
deleteConfirmation: 'Are you sure you want to delete this workflow?',
|
||||
copySuccess: 'Copied successfully',
|
||||
copyError: 'Failed to copy: ',
|
||||
export: 'Export',
|
||||
import: 'Import',
|
||||
exportSuccess: 'Workflow exported',
|
||||
importSuccess: 'Workflow imported',
|
||||
importError: 'Import failed: Invalid file format',
|
||||
publish: 'Publish',
|
||||
publishSuccess: 'Published successfully',
|
||||
publishError: 'Failed to publish',
|
||||
configuration: 'Configuration',
|
||||
executions: 'Executions',
|
||||
editor: 'Editor',
|
||||
debugChat: '對話除錯',
|
||||
basicInfo: 'Basic Info',
|
||||
basicInfoDesc: 'Set workflow name, icon and description',
|
||||
basicInfoDescription: 'Set workflow name and description',
|
||||
dangerZone: 'Danger Zone',
|
||||
dangerZoneDesc: 'Irreversible operations',
|
||||
dangerZoneDescription: 'Irreversible operations',
|
||||
deleteWorkflowAction: 'Delete this workflow',
|
||||
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.',
|
||||
// Form component
|
||||
name: 'Name',
|
||||
namePlaceholder: 'Enter workflow name',
|
||||
descriptionPlaceholder: 'Enter workflow description (optional)',
|
||||
enabled: 'Enabled',
|
||||
enabledDesc: 'When enabled, the workflow can be triggered for execution',
|
||||
loading: 'Loading...',
|
||||
info: 'Workflow Info',
|
||||
uuid: 'UUID',
|
||||
version: 'Version',
|
||||
createdAt: 'Created At',
|
||||
updatedAt: 'Updated At',
|
||||
// Executions tab
|
||||
totalExecutions: '{{count}} execution(s) total',
|
||||
statistics: 'Statistics',
|
||||
successfulCount: '{{count}} successful',
|
||||
successRate: 'Success Rate',
|
||||
averageDuration: 'Avg. Duration',
|
||||
perExecution: 'per execution',
|
||||
failedExecutions: 'Failed',
|
||||
lastExecution: 'Last run',
|
||||
filterByStatus: 'Filter by status',
|
||||
allStatuses: 'All statuses',
|
||||
manualTrigger: 'Manual Trigger',
|
||||
executionId: 'Execution ID',
|
||||
status: 'Status',
|
||||
triggerType: 'Trigger Type',
|
||||
startedAt: 'Started At',
|
||||
duration: 'Duration',
|
||||
noExecutions: 'No executions yet',
|
||||
executionDetails: 'Execution Details',
|
||||
error: 'Error',
|
||||
nodeExecutions: 'Node Executions',
|
||||
result: 'Result',
|
||||
'status.pending': 'Pending',
|
||||
'status.running': 'Running',
|
||||
'status.completed': 'Completed',
|
||||
'status.failed': 'Failed',
|
||||
'status.cancelled': 'Cancelled',
|
||||
// Editor component translations
|
||||
nodePalette: 'Node Palette',
|
||||
properties: 'Properties',
|
||||
zoomIn: 'Zoom In',
|
||||
zoomOut: 'Zoom Out',
|
||||
fitView: 'Fit View',
|
||||
unsavedChanges: 'Unsaved changes',
|
||||
paste: 'Paste',
|
||||
deleted: 'Deleted',
|
||||
nothingToCopy: 'No nodes selected to copy',
|
||||
nothingToPaste: 'Clipboard is empty',
|
||||
copied: 'Copied {{count}} node(s)',
|
||||
pasted: 'Pasted {{count}} node(s)',
|
||||
nodesSelected: '{{count}} node(s) selected',
|
||||
edgesSelected: '{{count}} edge(s) selected',
|
||||
// Node palette
|
||||
searchNodes: 'Search nodes...',
|
||||
loadingNodeTypes: 'Loading node types...',
|
||||
noNodesFound: 'No matching nodes found',
|
||||
clearSearch: 'Clear search',
|
||||
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',
|
||||
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.',
|
||||
deleteEdge: 'Delete Edge',
|
||||
deleteEdgeConfirm: 'Confirm Delete Edge',
|
||||
deleteEdgeConfirmDesc: 'This edge will be permanently removed.',
|
||||
nodeLabel: 'Node Label',
|
||||
nodeLabelPlaceholder: 'Enter node display name',
|
||||
nodeId: 'Node ID',
|
||||
inputOutputVariables: 'Input/Output Variables',
|
||||
inputs: 'Inputs',
|
||||
outputs: 'Outputs',
|
||||
availableVariables: 'Available Variables',
|
||||
globalVariables: 'Global Variables',
|
||||
messageContent: 'Message Content',
|
||||
messageSender: 'Sender',
|
||||
platform: 'Platform',
|
||||
sessionId: 'Session ID',
|
||||
timestamp: 'Timestamp',
|
||||
nodeConfig: 'Node Configuration',
|
||||
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.',
|
||||
// Node inputs/outputs i18n (for port labels)
|
||||
nodeInputs: {
|
||||
// Common inputs
|
||||
input: 'Input',
|
||||
message: 'Message',
|
||||
text: 'Text',
|
||||
query: 'Query',
|
||||
data: 'Data',
|
||||
condition: 'Condition',
|
||||
value: 'Value',
|
||||
// Trigger inputs
|
||||
content: 'Content',
|
||||
context: 'Context',
|
||||
body: 'Request Body',
|
||||
variables: 'Variables',
|
||||
items: 'Items',
|
||||
arguments: 'Arguments',
|
||||
// AI/Process inputs
|
||||
question: 'Question',
|
||||
parameters: 'Parameters',
|
||||
key: 'Key',
|
||||
// Control inputs
|
||||
case_1: 'Case 1',
|
||||
case_2: 'Case 2',
|
||||
branch_1: 'Branch 1',
|
||||
branch_2: 'Branch 2',
|
||||
// Action inputs
|
||||
notification_id: 'Notification ID',
|
||||
// Integration inputs
|
||||
key_template: 'Key Template',
|
||||
hash_field: 'Hash Field',
|
||||
server_name: 'Server Name',
|
||||
tool_name: 'Tool Name',
|
||||
arguments_template: 'Arguments Template',
|
||||
scope: 'Scope',
|
||||
},
|
||||
// Data type labels
|
||||
'type.string': 'String',
|
||||
'type.object': 'Object',
|
||||
'type.array': 'Array',
|
||||
'type.boolean': 'Boolean',
|
||||
'type.number': 'Number',
|
||||
'type.any': 'Any',
|
||||
'type.datetime': 'DateTime',
|
||||
nodes: {
|
||||
trigger: 'Triggers',
|
||||
triggerDescription: 'Starting nodes for workflows',
|
||||
messageTrigger: 'Message Trigger',
|
||||
messageTriggerDescription: 'Triggered when a message is received',
|
||||
scheduleTrigger: 'Schedule Trigger',
|
||||
scheduleTriggerDescription: 'Triggered on a schedule',
|
||||
cronTrigger: 'Cron Trigger',
|
||||
cronTriggerDescription: 'Trigger workflow on a scheduled time',
|
||||
webhookTrigger: 'Webhook Trigger',
|
||||
webhookTriggerDescription: 'Triggered via HTTP request',
|
||||
eventTrigger: 'Event Trigger',
|
||||
eventTriggerDescription: 'Triggered on system events',
|
||||
process: 'AI/Process',
|
||||
processDescription: 'Data processing nodes',
|
||||
aiProcess: 'AI Processing',
|
||||
aiProcessDescription: 'Process messages using AI models',
|
||||
llmCall: 'LLM Call',
|
||||
llmCallDescription: 'Invoke large language model for conversation or generation',
|
||||
codeProcess: 'Code Processing',
|
||||
codeProcessDescription: 'Execute custom code',
|
||||
codeExecutor: 'Code Executor',
|
||||
codeExecutorDescription: 'Execute Python/JavaScript code',
|
||||
templateProcess: 'Template Processing',
|
||||
templateProcessDescription: 'Format output using templates',
|
||||
httpRequest: 'HTTP Request',
|
||||
httpRequestDescription: 'Send HTTP requests',
|
||||
dataTransform: 'Data Transform',
|
||||
dataTransformDescription: 'Transform data format',
|
||||
questionClassifier: 'Question Classifier',
|
||||
questionClassifierDescription: 'Classify user questions into predefined categories using LLM',
|
||||
parameterExtractor: 'Parameter Extractor',
|
||||
parameterExtractorDescription: 'Extract structured parameters from text using LLM',
|
||||
knowledgeRetrieval: 'Knowledge Retrieval',
|
||||
knowledgeRetrievalDescription: 'Retrieve relevant content from knowledge base',
|
||||
textTemplate: 'Text Template',
|
||||
textTemplateDescription: 'Generate text using templates with variable interpolation',
|
||||
jsonTransform: 'JSON Transform',
|
||||
jsonTransformDescription: 'Transform JSON data using expressions',
|
||||
dataAggregator: 'Data Aggregator',
|
||||
dataAggregatorDescription: 'Aggregate data from multiple sources',
|
||||
textSplitter: 'Text Splitter',
|
||||
textSplitterDescription: 'Split text into smaller chunks',
|
||||
variableAssignment: 'Variable Assignment',
|
||||
variableAssignmentDescription: 'Assign values to workflow variables',
|
||||
control: 'Control Flow',
|
||||
controlDescription: 'Flow control nodes',
|
||||
condition: 'Condition Branch',
|
||||
conditionDescription: 'Branch based on conditions',
|
||||
switch: 'Switch',
|
||||
switchDescription: 'Multi-way branching',
|
||||
loop: 'Loop',
|
||||
loopDescription: 'Repeat execution',
|
||||
iterator: 'Iterator',
|
||||
iteratorDescription: 'Iterate over array elements',
|
||||
parallel: 'Parallel',
|
||||
parallelDescription: 'Execute multiple branches in parallel',
|
||||
wait: 'Wait',
|
||||
waitDescription: 'Wait for specified time',
|
||||
delay: 'Delay',
|
||||
delayDescription: 'Wait for a specified time',
|
||||
merge: 'Merge',
|
||||
mergeDescription: 'Merge multiple branches',
|
||||
variableAggregator: 'Variable Aggregator',
|
||||
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',
|
||||
storeData: 'Store Data',
|
||||
storeDataDescription: 'Store data to database',
|
||||
callPipeline: 'Call Pipeline',
|
||||
callPipelineDescription: 'Call an existing Pipeline',
|
||||
setVariable: 'Set Variable',
|
||||
setVariableDescription: 'Set context variable',
|
||||
openingStatement: 'Opening Statement',
|
||||
openingStatementDescription: 'Provide conversation opener and suggested questions',
|
||||
end: 'End',
|
||||
endDescription: 'Mark the end of workflow execution',
|
||||
log: 'Log',
|
||||
logDescription: 'Record log information',
|
||||
integration: 'Integration',
|
||||
integrationDescription: 'Third-party platform integration nodes',
|
||||
difyWorkflow: 'Dify Workflow',
|
||||
difyWorkflowDescription: 'Call Dify platform workflow',
|
||||
difyKnowledgeQuery: 'Dify Knowledge Query',
|
||||
difyKnowledgeQueryDescription: 'Query Dify knowledge base',
|
||||
n8nWorkflow: 'N8n Workflow',
|
||||
n8nWorkflowDescription: 'Call n8n workflow',
|
||||
langflowFlow: 'Langflow Flow',
|
||||
langflowFlowDescription: 'Call Langflow flow',
|
||||
cozeBot: 'Coze Bot',
|
||||
cozeBotDescription: 'Call Coze Bot',
|
||||
// Data & Tools integration nodes
|
||||
databaseQuery: 'Database Query',
|
||||
databaseQueryDescription: 'Execute database queries',
|
||||
redisOperation: 'Redis Operation',
|
||||
redisOperationDescription: 'Perform Redis cache operations',
|
||||
mcpTool: 'MCP Tool',
|
||||
mcpToolDescription: 'Invoke an MCP tool',
|
||||
memoryStore: 'Memory Store',
|
||||
memoryStoreDescription: 'Store and retrieve data from workflow memory',
|
||||
},
|
||||
executionHistory: {
|
||||
title: 'Execution History',
|
||||
noExecutions: 'No executions yet',
|
||||
status: 'Status',
|
||||
startTime: 'Start Time',
|
||||
duration: 'Duration',
|
||||
running: 'Running',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled',
|
||||
viewDetails: 'View Details',
|
||||
cancel: 'Cancel Execution',
|
||||
retry: 'Retry',
|
||||
nodeResults: 'Node Execution Results',
|
||||
},
|
||||
versions: {
|
||||
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.',
|
||||
rollbackSuccess: 'Rollback successful',
|
||||
rollbackError: 'Failed to rollback: ',
|
||||
},
|
||||
// Debug and monitoring
|
||||
debug: 'Debug',
|
||||
debugMode: 'Debug Mode',
|
||||
debugPanel: 'Debug Panel',
|
||||
startDebug: 'Start Debug',
|
||||
pauseDebug: 'Pause',
|
||||
resumeDebug: 'Resume',
|
||||
stepDebug: 'Step',
|
||||
stopDebug: 'Stop',
|
||||
debugContext: 'Debug Context',
|
||||
simulatedMessage: 'Simulated Message',
|
||||
simulatedMessagePlaceholder: 'Enter the message content to simulate',
|
||||
senderId: 'Sender ID',
|
||||
senderIdPlaceholder: 'Sender unique identifier',
|
||||
senderName: 'Sender Name',
|
||||
senderNamePlaceholder: 'Sender display name',
|
||||
conversationId: 'Conversation ID',
|
||||
conversationIdPlaceholder: 'Conversation unique identifier',
|
||||
isGroup: 'Group Chat',
|
||||
customVariables: 'Custom Variables',
|
||||
addVariable: 'Add Variable',
|
||||
variableName: 'Variable Name',
|
||||
variableValue: 'Variable Value',
|
||||
watchedVariables: 'Watched Variables',
|
||||
addWatchVariable: 'Add Watch',
|
||||
nodeStates: 'Node States',
|
||||
breakpoints: 'Breakpoints',
|
||||
toggleBreakpoint: 'Toggle Breakpoint',
|
||||
breakpointSet: 'Breakpoint set',
|
||||
breakpointRemoved: 'Breakpoint removed',
|
||||
debugLogs: 'Debug Logs',
|
||||
noLogs: 'No logs yet',
|
||||
clearLogs: 'Clear Logs',
|
||||
autoScroll: 'Auto Scroll',
|
||||
debugState: {
|
||||
idle: 'Idle',
|
||||
running: 'Running',
|
||||
paused: 'Paused',
|
||||
completed: 'Completed',
|
||||
error: 'Error',
|
||||
},
|
||||
nodeStatus: {
|
||||
pending: 'Pending',
|
||||
running: 'Running',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
skipped: 'Skipped',
|
||||
},
|
||||
debugDialog: {
|
||||
title: '工作流對話',
|
||||
selectWorkflow: '選擇工作流',
|
||||
sessionType: '會話類型',
|
||||
privateChat: '私聊',
|
||||
groupChat: '群聊',
|
||||
send: '發送',
|
||||
reset: '重置對話',
|
||||
inputPlaceholder: '發送 {{type}} 訊息...',
|
||||
noMessages: '暫無訊息',
|
||||
userMessage: '使用者',
|
||||
botMessage: '機器人',
|
||||
sendFailed: '發送失敗',
|
||||
resetSuccess: '對話已重置',
|
||||
resetFailed: '重置失敗',
|
||||
loadMessagesFailed: '載入訊息失敗',
|
||||
loadWorkflowsFailed: '載入工作流失敗',
|
||||
atTips: '提及機器人',
|
||||
streaming: '串流傳輸',
|
||||
streamOutput: '串流',
|
||||
connected: 'WebSocket已連線',
|
||||
disconnected: 'WebSocket未連線',
|
||||
connectionError: 'WebSocket連線錯誤',
|
||||
connectionFailed: 'WebSocket連線失敗',
|
||||
notConnected: 'WebSocket未連線,請稍後重試',
|
||||
imageUploadFailed: '圖片上傳失敗',
|
||||
reply: '回覆',
|
||||
replyTo: '回覆給',
|
||||
showMarkdown: '渲染',
|
||||
showRaw: '原文',
|
||||
allMembers: '全體成員',
|
||||
file: '檔案',
|
||||
voice: '語音',
|
||||
uploadImage: '上傳圖片',
|
||||
uploading: '上傳中...',
|
||||
},
|
||||
// Execution history and monitoring
|
||||
filterByDate: 'Filter by Date',
|
||||
allTime: 'All Time',
|
||||
today: 'Today',
|
||||
lastWeek: 'Last Week',
|
||||
lastMonth: 'Last Month',
|
||||
showingExecutions: 'Showing {{shown}} / {{total}} executions',
|
||||
rerun: 'Rerun',
|
||||
rerunExecution: 'Rerun Execution',
|
||||
logs: 'Logs',
|
||||
details: 'Details',
|
||||
completedAt: 'Completed At',
|
||||
noNodeExecutions: 'No node executions yet',
|
||||
// Node config field labels (used by DynamicFormComponent)
|
||||
nodeConfigFields: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
conditions: 'Trigger Conditions',
|
||||
keyword_filter: 'Keyword Filter',
|
||||
regex_filter: 'Regex Filter',
|
||||
min_length: 'Min Length',
|
||||
max_length: 'Max Length',
|
||||
require_mention: 'Require Mention',
|
||||
respond_rules: 'Group Respond Rules',
|
||||
access_control: 'Access Control',
|
||||
// trigger.py - CronTriggerNode
|
||||
cron: 'Cron Expression',
|
||||
timezone: 'Timezone',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
path: 'Webhook Path',
|
||||
allowed_methods: 'Allowed HTTP Methods',
|
||||
content_type: 'Content-Type',
|
||||
auth_type: 'Authentication Type',
|
||||
auth_key: 'Auth Key',
|
||||
validation: 'Request Validation',
|
||||
timeout: 'Timeout',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_types: 'Event Types',
|
||||
filter: 'Event Filter',
|
||||
debounce_ms: 'Debounce Time',
|
||||
// process.py - LLMCallNode
|
||||
model: 'Model',
|
||||
prompt_template: 'Prompt Template',
|
||||
system_prompt: 'System Prompt',
|
||||
temperature: 'Temperature',
|
||||
top_p: 'Top P',
|
||||
frequency_penalty: 'Frequency Penalty',
|
||||
presence_penalty: 'Presence Penalty',
|
||||
max_tokens: 'Max Tokens',
|
||||
stop_sequences: 'Stop Sequences',
|
||||
seed: 'Random Seed',
|
||||
stream: 'Stream Output',
|
||||
use_conversation_history: 'Use Conversation History',
|
||||
// process.py - CodeExecutorNode
|
||||
language: 'Programming Language',
|
||||
code: 'Code',
|
||||
// process.py - HTTPRequestNode
|
||||
url: 'Request URL',
|
||||
method: 'Request Method',
|
||||
auth_config: 'Auth Configuration',
|
||||
// process.py - DataTransformNode
|
||||
transform_type: 'Transform Type',
|
||||
template: 'Template',
|
||||
expression: 'Expression',
|
||||
output_type: 'Output Type',
|
||||
// process.py - QuestionClassifierNode
|
||||
categories: 'Categories',
|
||||
instruction: 'Instruction',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: 'Parameter Definitions',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
knowledge_bases: 'Knowledge Bases',
|
||||
top_k: 'Top K Results',
|
||||
score_threshold: 'Score Threshold',
|
||||
search_method: 'Search Method',
|
||||
enable_citations: 'Enable Citations',
|
||||
// control.py - ConditionNode
|
||||
condition_type: 'Condition Type',
|
||||
condition_expression: 'Condition Expression',
|
||||
left_value: 'Left Value',
|
||||
operator: 'Comparison Operator',
|
||||
right_value: 'Right Value',
|
||||
// control.py - SwitchNode
|
||||
cases: 'Branch Cases',
|
||||
// control.py - LoopNode
|
||||
max_iterations: 'Max Iterations',
|
||||
break_condition: 'Break Condition',
|
||||
// control.py - IteratorNode
|
||||
parallel: 'Parallel Processing',
|
||||
max_concurrency: 'Max Concurrency',
|
||||
// control.py - ParallelNode
|
||||
branches: 'Branch Configuration',
|
||||
wait_all: 'Wait All',
|
||||
fail_fast: 'Fail Fast',
|
||||
// control.py - WaitNode
|
||||
duration: 'Duration',
|
||||
duration_type: 'Time Unit',
|
||||
// control.py - MergeNode
|
||||
merge_strategy: 'Merge Strategy',
|
||||
// control.py - VariableAggregatorNode
|
||||
variable_mappings: 'Variable Mappings',
|
||||
aggregation_mode: 'Aggregation Mode',
|
||||
// action.py - SendMessageNode
|
||||
target_type: 'Target Type',
|
||||
target_id: 'Target ID',
|
||||
platform: 'Platform',
|
||||
message_type: 'Message Type',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_mode: 'Reply Mode',
|
||||
message_template: 'Message Template',
|
||||
long_text_processing: 'Long Text Processing',
|
||||
force_delay: 'Force Delay',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_uuid: 'Pipeline',
|
||||
inherit_context: 'Inherit Context',
|
||||
// action.py - StoreDataNode
|
||||
storage_type: 'Storage Type',
|
||||
ttl: 'TTL',
|
||||
key_prefix: 'Key Prefix',
|
||||
// action.py - SetVariableNode
|
||||
variable_name: 'Variable Name',
|
||||
variable_scope: 'Variable Scope',
|
||||
set_variable_operation: 'Operation Type',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: 'Opening Statement',
|
||||
suggested_questions: 'Suggested Questions',
|
||||
show_suggestions: 'Show Suggestions',
|
||||
// action.py - EndNode
|
||||
output_format: 'Output Format',
|
||||
success_message: 'Success Message',
|
||||
// integration.py - DatabaseQueryNode
|
||||
connection_type: 'Database Type',
|
||||
connection_string: 'Connection String',
|
||||
query: 'SQL Query',
|
||||
query_type: 'Query Type',
|
||||
// integration.py - RedisOperationNode
|
||||
connection_url: 'Connection URL',
|
||||
operation: 'Operation Type',
|
||||
key_template: 'Key Template',
|
||||
hash_field: 'Hash Field',
|
||||
// integration.py - MCPToolNode
|
||||
server_name: 'Server Name',
|
||||
tool_name: 'Tool Name',
|
||||
arguments_template: 'Arguments Template',
|
||||
// integration.py - MemoryStoreNode
|
||||
scope: 'Scope',
|
||||
// integration.py - DifyWorkflowNode
|
||||
'base-url': 'Base URL',
|
||||
'api-key': 'API Key',
|
||||
'app-type': 'App Type',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
dataset_id: 'Dataset ID',
|
||||
// integration.py - N8nWorkflowNode
|
||||
'webhook-url': 'Webhook URL',
|
||||
// integration.py - LangflowFlowNode
|
||||
'flow-id': 'Flow ID',
|
||||
// integration.py - CozeBotNode
|
||||
'bot-id': 'Bot ID',
|
||||
'api-base': 'API Base URL',
|
||||
},
|
||||
// Node outputs i18n
|
||||
nodeOutputs: {
|
||||
// trigger.py - MessageTriggerNode
|
||||
message: 'Message Content',
|
||||
sender_id: 'Sender ID',
|
||||
sender_name: 'Sender Name',
|
||||
platform: 'Platform',
|
||||
conversation_id: 'Conversation ID',
|
||||
is_group: 'Is Group Chat',
|
||||
context: 'Full Message Context',
|
||||
// trigger.py - CronTriggerNode
|
||||
timestamp: 'Trigger Time',
|
||||
schedule: 'Trigger Schedule',
|
||||
cron_timestamp: 'Cron Timestamp',
|
||||
cron_schedule: 'Cron Schedule',
|
||||
cron_context: 'Cron Context',
|
||||
// trigger.py - WebhookTriggerNode
|
||||
body: 'Request Body',
|
||||
headers: 'Request Headers',
|
||||
query: 'Query Parameters',
|
||||
method: 'Request Method',
|
||||
webhook_body: 'Webhook Body',
|
||||
webhook_headers: 'Webhook Headers',
|
||||
webhook_query: 'Webhook Query',
|
||||
webhook_method: 'Webhook Method',
|
||||
// trigger.py - EventTriggerNode
|
||||
event_type: 'Event Type',
|
||||
event_data: 'Event Data',
|
||||
event_timestamp: 'Event Timestamp',
|
||||
// process.py - LLMCallNode
|
||||
response: 'Model Response',
|
||||
usage: 'Token Usage Statistics',
|
||||
prompt: 'Prompt/Question',
|
||||
context_info: 'Context Information',
|
||||
// process.py - CodeExecutorNode
|
||||
output: 'Output Data',
|
||||
console: 'Console Output',
|
||||
code_input: 'Code Input',
|
||||
code_output: 'Code Output',
|
||||
// process.py - HTTPRequestNode
|
||||
status_code: 'Status Code',
|
||||
http_body: 'HTTP Body',
|
||||
http_headers: 'HTTP Headers',
|
||||
http_response: 'HTTP Response',
|
||||
response_headers: 'Response Headers',
|
||||
// process.py - DataTransformNode
|
||||
result: 'Transform Result',
|
||||
transform_input: 'Transform Input',
|
||||
transform_result: 'Transform Result',
|
||||
// process.py - QuestionClassifierNode
|
||||
category: 'Category Result',
|
||||
confidence: 'Confidence',
|
||||
all_scores: 'All Category Scores',
|
||||
question: 'User Question',
|
||||
// process.py - ParameterExtractorNode
|
||||
parameters: 'Extracted Parameters',
|
||||
extraction_success: 'Extraction Success',
|
||||
extract_text: 'Input Text',
|
||||
// process.py - KnowledgeRetrievalNode
|
||||
documents: 'Retrieved Documents',
|
||||
citations: 'Citation Information',
|
||||
knowledge_context: 'Merged Context',
|
||||
knowledge_query: 'Retrieval Query',
|
||||
// control.py - ConditionNode
|
||||
true: 'True Branch Output',
|
||||
false: 'False Branch Output',
|
||||
condition_input: 'Condition Input',
|
||||
// control.py - SwitchNode
|
||||
matched_case: 'Matched Branch Output',
|
||||
default: 'Default Branch Output',
|
||||
switch_input: 'Switch Input',
|
||||
// control.py - LoopNode
|
||||
item: 'Current Item',
|
||||
index: 'Current Index',
|
||||
results: 'All Iteration Results',
|
||||
completed: 'Is Completed',
|
||||
loop_items: 'Items to Iterate',
|
||||
// control.py - IteratorNode
|
||||
is_first: 'Is First',
|
||||
is_last: 'Is Last',
|
||||
iterator_array: 'Input Array',
|
||||
iterator_item: 'Current Element',
|
||||
iterator_index: 'Current Index',
|
||||
// control.py - ParallelNode
|
||||
errors: 'Error List',
|
||||
parallel_input: 'Parallel Input',
|
||||
parallel_results: 'All Branch Results',
|
||||
// control.py - WaitNode
|
||||
wait_input: 'Passthrough Input',
|
||||
wait_output: 'Passthrough Output',
|
||||
// control.py - MergeNode
|
||||
merged: 'Merged Result',
|
||||
merge_array: 'Array Result',
|
||||
merge_input_1: 'Input 1',
|
||||
merge_input_2: 'Input 2',
|
||||
merge_input_3: 'Input 3',
|
||||
merge_input_4: 'Input 4',
|
||||
// control.py - VariableAggregatorNode
|
||||
aggregated: 'Aggregated Variables',
|
||||
aggregator_variables: 'Variable Input',
|
||||
// action.py - SendMessageNode
|
||||
status: 'Send Status',
|
||||
message_id: 'Message ID',
|
||||
target: 'Target ID',
|
||||
// action.py - ReplyMessageNode
|
||||
reply_message: 'Reply Content',
|
||||
// action.py - CallPipelineNode
|
||||
pipeline_response: 'Pipeline Response',
|
||||
pipeline_result: 'Full Result',
|
||||
pipeline_query: 'Query Content',
|
||||
context_data: 'Context Data',
|
||||
// action.py - StoreDataNode
|
||||
store_status: 'Store Status',
|
||||
store_key: 'Store Key',
|
||||
store_value: 'Store Value',
|
||||
// action.py - SetVariableNode
|
||||
variable_value: 'Variable Value',
|
||||
variable_result: 'Set Variable Result',
|
||||
// action.py - OpeningStatementNode
|
||||
statement: 'Opening Statement',
|
||||
suggested_questions: 'Suggested Questions',
|
||||
// action.py - EndNode
|
||||
workflow_output: 'Workflow Output',
|
||||
final_result: 'Final Result',
|
||||
// integration.py - DatabaseQueryNode
|
||||
query_results: 'Query Results',
|
||||
row_count: 'Affected/Returned Rows',
|
||||
query_success: 'Query Success',
|
||||
query_params: 'Query Parameters',
|
||||
// integration.py - RedisOperationNode
|
||||
redis_result: 'Operation Result',
|
||||
redis_success: 'Operation Success',
|
||||
redis_key: 'Redis Key',
|
||||
redis_value: 'Redis Value',
|
||||
error: 'Error Message',
|
||||
plugin_input: 'Plugin Input',
|
||||
// integration.py - MCPToolNode
|
||||
tool_result: 'Tool Execution Result',
|
||||
tool_success: 'Tool Call Success',
|
||||
mcp_arguments: 'Tool Arguments',
|
||||
// integration.py - MemoryStoreNode
|
||||
memory_result: 'Retrieved/Stored Value',
|
||||
memory_success: 'Operation Success',
|
||||
memory_value: 'Value to Store',
|
||||
// integration.py - DifyWorkflowNode
|
||||
answer: 'Dify Answer',
|
||||
dify_success: 'Call Success',
|
||||
dify_query: 'User Input/Query',
|
||||
dify_conversation_id: 'Conversation ID',
|
||||
// integration.py - DifyKnowledgeQueryNode
|
||||
search_results: 'Search Results',
|
||||
knowledge_base_query: 'Query Content',
|
||||
// integration.py - N8nWorkflowNode
|
||||
n8n_result: 'Workflow Execution Result',
|
||||
n8n_success: 'Call Success',
|
||||
n8n_payload: 'Workflow Input Data',
|
||||
// integration.py - LangflowFlowNode
|
||||
flow_result: 'Flow Execution Result',
|
||||
flow_success: 'Call Success',
|
||||
langflow_input: 'Input Content',
|
||||
// integration.py - CozeBotNode
|
||||
bot_answer: 'Bot Reply',
|
||||
bot_success: 'Call Success',
|
||||
coze_query: 'User Input/Query',
|
||||
coze_conversation_id: 'Conversation ID',
|
||||
bot_conversation_id: 'Conversation ID',
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default zhHant;
|
||||
|
||||
@@ -17,6 +17,7 @@ import HomePage from '@/app/home/page';
|
||||
import MonitoringPage from '@/app/home/monitoring/page';
|
||||
import BotsPage from '@/app/home/bots/page';
|
||||
import PipelinesPage from '@/app/home/pipelines/page';
|
||||
import WorkflowsPage from '@/app/home/workflows/page';
|
||||
import PluginsPage from '@/app/home/plugins/page';
|
||||
import MarketPage from '@/app/home/market/page';
|
||||
import MCPPage from '@/app/home/mcp/page';
|
||||
@@ -101,6 +102,16 @@ export const router = createBrowserRouter([
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/home/workflows',
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<HomeLayout>
|
||||
<WorkflowsPage />
|
||||
</HomeLayout>
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/home/plugins',
|
||||
element: (
|
||||
|
||||
Reference in New Issue
Block a user