Merge branch 'develop' of github.com:SuanmoSuanyangTechnology/MemoryBear into develop
This commit is contained in:
@@ -1137,10 +1137,10 @@ export const en = {
|
|||||||
promptEmpty: 'Describe your use case on the left, and the orchestration preview will be displayed here.',
|
promptEmpty: 'Describe your use case on the left, and the orchestration preview will be displayed here.',
|
||||||
|
|
||||||
master: 'Supervisor Mode',
|
master: 'Supervisor Mode',
|
||||||
master_agent: 'Supervisor Mode',
|
supervisor: 'Supervisor Mode',
|
||||||
master_agentDesc: 'Unified scheduling and management by the main Agent, with sub-Agents executing tasks assigned by the supervisor, suitable for scenarios requiring centralized control.',
|
supervisorDesc: 'Unified scheduling and management by the main Agent, with sub-Agents executing tasks assigned by the supervisor, suitable for scenarios requiring centralized control.',
|
||||||
handoffs: 'Collaboration Mode',
|
collaboration: 'Collaboration Mode',
|
||||||
handoffsDesc: 'Multiple Agents collaborate equally, autonomously coordinating according to task requirements, suitable for complex scenarios requiring flexible interaction.',
|
collaborationDesc: 'Multiple Agents collaborate equally, autonomously coordinating according to task requirements, suitable for complex scenarios requiring flexible interaction.',
|
||||||
masterConfig: 'Supervisor Configuration',
|
masterConfig: 'Supervisor Configuration',
|
||||||
orchestrationMode: 'Task Assignment Strategy',
|
orchestrationMode: 'Task Assignment Strategy',
|
||||||
conditional: 'Intelligent Assignment',
|
conditional: 'Intelligent Assignment',
|
||||||
@@ -1150,6 +1150,8 @@ export const en = {
|
|||||||
merge: 'Complete Aggregation',
|
merge: 'Complete Aggregation',
|
||||||
vote: 'Key Information Extraction',
|
vote: 'Key Information Extraction',
|
||||||
priority: 'Structured Integration',
|
priority: 'Structured Integration',
|
||||||
|
addTool: 'Add Tool',
|
||||||
|
tool: 'Tool',
|
||||||
},
|
},
|
||||||
userMemory: {
|
userMemory: {
|
||||||
userMemory: 'User Memory',
|
userMemory: 'User Memory',
|
||||||
@@ -1207,6 +1209,7 @@ export const en = {
|
|||||||
IMPLICIT_MEMORY: 'Implicit Memory',
|
IMPLICIT_MEMORY: 'Implicit Memory',
|
||||||
EMOTIONAL_MEMORY: 'Emotional Memory',
|
EMOTIONAL_MEMORY: 'Emotional Memory',
|
||||||
EPISODIC_MEMORY: 'Episodic Memory',
|
EPISODIC_MEMORY: 'Episodic Memory',
|
||||||
|
FORGETTING_MANAGEMENT: 'Forgetting Management',
|
||||||
|
|
||||||
endUserProfile: 'Core Profile',
|
endUserProfile: 'Core Profile',
|
||||||
editEndUserProfile: 'Edit',
|
editEndUserProfile: 'Edit',
|
||||||
@@ -1839,6 +1842,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
status_code: 'Status Code',
|
status_code: 'Status Code',
|
||||||
max_attempts: 'Max Retry Attempts',
|
max_attempts: 'Max Retry Attempts',
|
||||||
retry_interval: 'Retry Interval',
|
retry_interval: 'Retry Interval',
|
||||||
|
errorBranch: 'Error Branch',
|
||||||
},
|
},
|
||||||
'jinja-render': {
|
'jinja-render': {
|
||||||
template: 'Code',
|
template: 'Code',
|
||||||
|
|||||||
@@ -626,10 +626,10 @@ export const zh = {
|
|||||||
promptEmpty: '在左侧描述您的用例,编排预览将在此处显示。',
|
promptEmpty: '在左侧描述您的用例,编排预览将在此处显示。',
|
||||||
|
|
||||||
master: '主管模式',
|
master: '主管模式',
|
||||||
master_agent: '主管模式',
|
supervisor: '主管模式',
|
||||||
master_agentDesc: '由主 Agent 统一调度和管理,子 Agent 按照主管分配的任务执行,适合需要集中控制的场景。',
|
supervisorDesc: '由主 Agent 统一调度和管理,子 Agent 按照主管分配的任务执行,适合需要集中控制的场景。',
|
||||||
handoffs: '协作模式',
|
collaboration: '协作模式',
|
||||||
handoffsDesc: '多个 Agent 平等协作,根据任务需求自主协调配合,适合需要灵活互动的复杂场景。',
|
collaborationDesc: '多个 Agent 平等协作,根据任务需求自主协调配合,适合需要灵活互动的复杂场景。',
|
||||||
masterConfig: '主管配置',
|
masterConfig: '主管配置',
|
||||||
orchestrationMode: '任务分配策略',
|
orchestrationMode: '任务分配策略',
|
||||||
conditional: '智能分配',
|
conditional: '智能分配',
|
||||||
@@ -639,6 +639,8 @@ export const zh = {
|
|||||||
merge: '完整汇总',
|
merge: '完整汇总',
|
||||||
vote: '关键信息提取',
|
vote: '关键信息提取',
|
||||||
priority: '结构化整合',
|
priority: '结构化整合',
|
||||||
|
addTool: '添加工具',
|
||||||
|
tool: '工具',
|
||||||
},
|
},
|
||||||
// 角色管理相关翻译
|
// 角色管理相关翻译
|
||||||
role: {
|
role: {
|
||||||
@@ -1286,6 +1288,7 @@ export const zh = {
|
|||||||
IMPLICIT_MEMORY: '隐性记忆',
|
IMPLICIT_MEMORY: '隐性记忆',
|
||||||
EMOTIONAL_MEMORY: '情绪记忆',
|
EMOTIONAL_MEMORY: '情绪记忆',
|
||||||
EPISODIC_MEMORY: '情景记忆',
|
EPISODIC_MEMORY: '情景记忆',
|
||||||
|
FORGETTING_MANAGEMENT: '遗忘',
|
||||||
|
|
||||||
endUserProfile: '核心档案',
|
endUserProfile: '核心档案',
|
||||||
editEndUserProfile: '编辑',
|
editEndUserProfile: '编辑',
|
||||||
@@ -1939,6 +1942,7 @@ export const zh = {
|
|||||||
status_code: '状态码',
|
status_code: '状态码',
|
||||||
max_attempts: '最大重试次数',
|
max_attempts: '最大重试次数',
|
||||||
retry_interval: '重试间隔',
|
retry_interval: '重试间隔',
|
||||||
|
errorBranch: '异常分支',
|
||||||
},
|
},
|
||||||
'jinja-render': {
|
'jinja-render': {
|
||||||
template: '代码',
|
template: '代码',
|
||||||
@@ -2252,5 +2256,12 @@ export const zh = {
|
|||||||
orderPayInfo: '支付信息',
|
orderPayInfo: '支付信息',
|
||||||
create_time: '创建时间',
|
create_time: '创建时间',
|
||||||
},
|
},
|
||||||
|
forgetDetail: {
|
||||||
|
title: '遗忘管理系统帮助AI智能管理记忆生命周期,通过自动识别低价值记忆、设置遗忘策略和执行定期清理,优化记忆库存储空间,提升检索效率。',
|
||||||
|
overviewTitle: '核心指标概览',
|
||||||
|
totalMemory: '记忆总量',
|
||||||
|
MemoryHealth: '记忆健康度',
|
||||||
|
riskOfForgetting: '遗忘风险',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,6 @@ import type {
|
|||||||
MemoryConfig,
|
MemoryConfig,
|
||||||
AiPromptModalRef,
|
AiPromptModalRef,
|
||||||
Source,
|
Source,
|
||||||
ToolModalRef,
|
|
||||||
ToolOption
|
ToolOption
|
||||||
} from './types'
|
} from './types'
|
||||||
import type { Model } from '@/views/ModelManagement/types'
|
import type { Model } from '@/views/ModelManagement/types'
|
||||||
@@ -33,7 +32,6 @@ import { memoryConfigListUrl } from '@/api/memory'
|
|||||||
import CustomSelect from '@/components/CustomSelect'
|
import CustomSelect from '@/components/CustomSelect'
|
||||||
import aiPrompt from '@/assets/images/application/aiPrompt.png'
|
import aiPrompt from '@/assets/images/application/aiPrompt.png'
|
||||||
import AiPromptModal from './components/AiPromptModal'
|
import AiPromptModal from './components/AiPromptModal'
|
||||||
import ToolModal from './components/ToolModal'
|
|
||||||
import ToolList from './components/ToolList'
|
import ToolList from './components/ToolList'
|
||||||
|
|
||||||
const DescWrapper: FC<{desc: string, className?: string}> = ({desc, className}) => {
|
const DescWrapper: FC<{desc: string, className?: string}> = ({desc, className}) => {
|
||||||
@@ -115,6 +113,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
|||||||
const [variableList, setVariableList] = useState<Variable[]>([])
|
const [variableList, setVariableList] = useState<Variable[]>([])
|
||||||
const [isSave, setIsSave] = useState(false)
|
const [isSave, setIsSave] = useState(false)
|
||||||
const initialized = useRef(false)
|
const initialized = useRef(false)
|
||||||
|
const [toolList, setToolList] = useState<ToolOption[]>([])
|
||||||
|
|
||||||
// 初始化完成标记
|
// 初始化完成标记
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -143,6 +142,11 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
|||||||
if (isSave) return
|
if (isSave) return
|
||||||
setIsSave(true)
|
setIsSave(true)
|
||||||
}, [values])
|
}, [values])
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initialized.current) return
|
||||||
|
if (isSave) return
|
||||||
|
setIsSave(true)
|
||||||
|
}, [toolList])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getModels()
|
getModels()
|
||||||
@@ -294,7 +298,11 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
|||||||
...(item.config || {})
|
...(item.config || {})
|
||||||
}))
|
}))
|
||||||
} as KnowledgeConfig : null,
|
} as KnowledgeConfig : null,
|
||||||
tools: toolList
|
tools: toolList.map(vo => ({
|
||||||
|
tool_id: vo.tool_id,
|
||||||
|
operation: vo.operation,
|
||||||
|
enabled: vo.enabled
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('params', rest, params)
|
console.log('params', rest, params)
|
||||||
@@ -347,18 +355,6 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
|||||||
form.setFieldValue('system_prompt', value)
|
form.setFieldValue('system_prompt', value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolModalRef = useRef<ToolModalRef>(null)
|
|
||||||
const [toolList, setToolList] = useState<ToolOption[]>([])
|
|
||||||
const handleAddTool = () => {
|
|
||||||
toolModalRef.current?.handleOpen()
|
|
||||||
}
|
|
||||||
const updateTools = (tool: ToolOption) => {
|
|
||||||
const tools = [...toolList, tool]
|
|
||||||
setToolList(tools)
|
|
||||||
form.setFieldValue('tools', tools)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('toolList', toolList)
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{loading && <Spin fullscreen></Spin>}
|
{loading && <Spin fullscreen></Spin>}
|
||||||
@@ -469,10 +465,6 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
|||||||
defaultModel={defaultModel}
|
defaultModel={defaultModel}
|
||||||
refresh={updatePrompt}
|
refresh={updatePrompt}
|
||||||
/>
|
/>
|
||||||
<ToolModal
|
|
||||||
ref={toolModalRef}
|
|
||||||
refresh={updateTools}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
|||||||
|
|
||||||
const handleSave = (flag = true) => {
|
const handleSave = (flag = true) => {
|
||||||
if (!data) return Promise.resolve()
|
if (!data) return Promise.resolve()
|
||||||
if (!values.default_model_config_id) {
|
if (!values.default_model_config_id && values.orchestration_mode === 'supervisor') {
|
||||||
message.warning(t('common.selectPlaceholder', { title: t('application.model') }))
|
message.warning(t('common.selectPlaceholder', { title: t('application.model') }))
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
@@ -140,15 +140,14 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
|||||||
<Space size={20} direction="vertical" style={{width: '100%'}}>
|
<Space size={20} direction="vertical" style={{width: '100%'}}>
|
||||||
<Card title={t('application.handoffs')}>
|
<Card title={t('application.handoffs')}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={['execution_config', 'routing_mode']}
|
name="orchestration_mode"
|
||||||
noStyle
|
noStyle
|
||||||
>
|
>
|
||||||
<RadioGroupCard
|
<RadioGroupCard
|
||||||
options={['master_agent', 'handoffs'].map((type) => ({
|
options={['supervisor', 'collaboration'].map((type) => ({
|
||||||
value: type,
|
value: type,
|
||||||
label: t(`application.${type}`),
|
label: t(`application.${type}`),
|
||||||
labelDesc: t(`application.${type}Desc`),
|
labelDesc: t(`application.${type}Desc`),
|
||||||
disabled: type === 'handoffs'
|
|
||||||
}))}
|
}))}
|
||||||
allowClear={false}
|
allowClear={false}
|
||||||
/>
|
/>
|
||||||
@@ -192,7 +191,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
|||||||
))}
|
))}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card title={t('application.masterConfig')}>
|
{values?.orchestration_mode !== 'collaboration' && <Card title={t('application.masterConfig')}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('application.model')}
|
label={t('application.model')}
|
||||||
required={true}
|
required={true}
|
||||||
@@ -218,11 +217,11 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
|||||||
</Row>
|
</Row>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="orchestration_mode"
|
name={['execution_config',"sub_agent_execution_mode"]}
|
||||||
label={t('application.orchestrationMode')}
|
label={t('application.orchestrationMode')}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
options={['conditional', 'sequential', 'parallel'].map((type) => ({
|
options={['sequential', 'parallel'].map((type) => ({
|
||||||
value: type,
|
value: type,
|
||||||
label: t(`application.${type}`),
|
label: t(`application.${type}`),
|
||||||
}))}
|
}))}
|
||||||
@@ -239,7 +238,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
|||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Card>
|
</Card>}
|
||||||
</Space>
|
</Space>
|
||||||
</Form>
|
</Form>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -90,11 +90,19 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
switch (item.event) {
|
switch (item.event) {
|
||||||
case 'start':
|
case 'start':
|
||||||
currentPromptValueRef.current = ''
|
currentPromptValueRef.current = ''
|
||||||
|
if (editorRef.current?.clear) {
|
||||||
|
editorRef.current.clear();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'message':
|
case 'message':
|
||||||
if (content) {
|
if (content) {
|
||||||
currentPromptValueRef.current += content;
|
currentPromptValueRef.current += content;
|
||||||
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
if (editorRef.current?.appendText) {
|
||||||
|
editorRef.current.appendText(content);
|
||||||
|
editorRef.current.scrollToBottom();
|
||||||
|
} else {
|
||||||
|
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (desc) {
|
if (desc) {
|
||||||
setChatList(prev => {
|
setChatList(prev => {
|
||||||
@@ -107,6 +115,8 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
|||||||
break;
|
break;
|
||||||
case 'end':
|
case 'end':
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
// 流结束时同步表单值
|
||||||
|
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|||||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
||||||
import { $getSelection } from 'lexical';
|
import { $getSelection, $getRoot, $createParagraphNode, $createTextNode, $isParagraphNode, $isTextNode } from 'lexical';
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
import InitialValuePlugin from './plugin/InitialValuePlugin'
|
import InitialValuePlugin from './plugin/InitialValuePlugin'
|
||||||
import LineBreakPlugin from './plugin/LineBreakPlugin';
|
import LineBreakPlugin from './plugin/LineBreakPlugin';
|
||||||
@@ -12,6 +12,9 @@ import InsertTextPlugin from './plugin/InsertTextPlugin';
|
|||||||
|
|
||||||
export interface EditorRef {
|
export interface EditorRef {
|
||||||
insertText: (text: string) => void;
|
insertText: (text: string) => void;
|
||||||
|
appendText: (text: string) => void;
|
||||||
|
clear: () => void;
|
||||||
|
scrollToBottom: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LexicalEditorProps {
|
interface LexicalEditorProps {
|
||||||
@@ -46,6 +49,41 @@ const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
|
|||||||
selection.insertText(text);
|
selection.insertText(text);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
appendText: (text: string) => {
|
||||||
|
editor.update(() => {
|
||||||
|
const root = $getRoot();
|
||||||
|
const lastChild = root.getLastChild();
|
||||||
|
if (lastChild && $isParagraphNode(lastChild)) {
|
||||||
|
const lastTextNode = lastChild.getLastChild();
|
||||||
|
if (lastTextNode && $isTextNode(lastTextNode)) {
|
||||||
|
const currentText = lastTextNode.getTextContent();
|
||||||
|
lastTextNode.setTextContent(currentText + text);
|
||||||
|
} else {
|
||||||
|
const textNode = $createTextNode(text);
|
||||||
|
lastChild.append(textNode);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const paragraph = $createParagraphNode();
|
||||||
|
const textNode = $createTextNode(text);
|
||||||
|
paragraph.append(textNode);
|
||||||
|
root.append(paragraph);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clear: () => {
|
||||||
|
editor.update(() => {
|
||||||
|
const root = $getRoot();
|
||||||
|
root.clear();
|
||||||
|
const paragraph = $createParagraphNode();
|
||||||
|
root.append(paragraph);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
scrollToBottom: () => {
|
||||||
|
const editorElement = editor.getRootElement();
|
||||||
|
if (editorElement) {
|
||||||
|
editorElement.scrollTop = editorElement.scrollHeight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}), [editor]);
|
}), [editor]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,37 @@
|
|||||||
import { type FC, useEffect } from 'react';
|
import { type FC, useEffect, useRef } from 'react';
|
||||||
import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical';
|
import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical';
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
|
||||||
// 设置初始值的插件
|
// 设置初始值的插件
|
||||||
const InitialValuePlugin: FC<{ value?: string }> = ({ value }) => {
|
const InitialValuePlugin: FC<{ value?: string }> = ({ value }) => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
|
const lastValueRef = useRef<string | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value) {
|
// 只有当value真正发生变化时才更新
|
||||||
|
if (lastValueRef.current !== value) {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const root = $getRoot();
|
const root = $getRoot();
|
||||||
|
const currentText = root.getTextContent();
|
||||||
|
|
||||||
|
// 如果当前内容和新值相同,则不更新
|
||||||
|
if (currentText === (value || '')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
root.clear();
|
root.clear();
|
||||||
const paragraph = $createParagraphNode();
|
if (value) {
|
||||||
const textNode = $createTextNode(value);
|
const paragraph = $createParagraphNode();
|
||||||
paragraph.append(textNode);
|
const textNode = $createTextNode(value);
|
||||||
root.append(paragraph);
|
paragraph.append(textNode);
|
||||||
|
root.append(paragraph);
|
||||||
|
} else {
|
||||||
|
// 当value为undefined或空时,创建一个空段落
|
||||||
|
const paragraph = $createParagraphNode();
|
||||||
|
root.append(paragraph);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
lastValueRef.current = value;
|
||||||
}
|
}
|
||||||
}, [editor, value]);
|
}, [editor, value]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FC, useState } from 'react';
|
import { type FC, useState, useEffect } from 'react';
|
||||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||||
@@ -23,6 +23,7 @@ interface LexicalEditorProps {
|
|||||||
options: Suggestion[];
|
options: Suggestion[];
|
||||||
variant?: 'outlined' | 'borderless';
|
variant?: 'outlined' | 'borderless';
|
||||||
height?: number;
|
height?: number;
|
||||||
|
enableJinja2?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const theme = {
|
const theme = {
|
||||||
@@ -33,6 +34,15 @@ const theme = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const jinja2Theme = {
|
||||||
|
...theme,
|
||||||
|
code: 'jinja2-expression',
|
||||||
|
text: {
|
||||||
|
...theme.text,
|
||||||
|
code: 'jinja2-inline',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const Editor: FC<LexicalEditorProps> =({
|
const Editor: FC<LexicalEditorProps> =({
|
||||||
placeholder = "请输入内容...",
|
placeholder = "请输入内容...",
|
||||||
value = "",
|
value = "",
|
||||||
@@ -40,19 +50,62 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
options,
|
options,
|
||||||
variant = 'borderless',
|
variant = 'borderless',
|
||||||
height = 60,
|
height = 60,
|
||||||
|
enableJinja2 = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const [_count, setCount] = useState(0);
|
const [_count, setCount] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (enableJinja2) {
|
||||||
|
const styleId = 'jinja2-styles';
|
||||||
|
let existingStyle = document.getElementById(styleId);
|
||||||
|
|
||||||
|
if (!existingStyle) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = styleId;
|
||||||
|
style.textContent = `
|
||||||
|
.jinja2-expression {
|
||||||
|
background-color: #f6f8fa !important;
|
||||||
|
border: 1px solid #d1d9e0 !important;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
padding: 2px 4px !important;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
color: #0969da !important;
|
||||||
|
}
|
||||||
|
.jinja2-inline {
|
||||||
|
background-color: #f6f8fa !important;
|
||||||
|
padding: 1px 3px !important;
|
||||||
|
border-radius: 2px !important;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
color: #0969da !important;
|
||||||
|
}
|
||||||
|
.editor-paragraph {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.editor-paragraph:has-text('{') .editor-text,
|
||||||
|
.editor-paragraph:has-text('[') .editor-text {
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [enableJinja2]);
|
||||||
const initialConfig = {
|
const initialConfig = {
|
||||||
namespace: 'AutocompleteEditor',
|
namespace: 'AutocompleteEditor',
|
||||||
theme,
|
theme: enableJinja2 ? jinja2Theme : theme,
|
||||||
nodes: [
|
nodes: enableJinja2 ? [
|
||||||
|
// 当启用jinja2时,不使用VariableNode,使用普通文本
|
||||||
|
] : [
|
||||||
// HeadingNode,
|
// HeadingNode,
|
||||||
// QuoteNode,
|
// QuoteNode,
|
||||||
// ListItemNode,
|
// ListItemNode,
|
||||||
// ListNode,
|
// ListNode,
|
||||||
// LinkNode,
|
// LinkNode,
|
||||||
// CodeNode,
|
// CodeNode,
|
||||||
VariableNode
|
VariableNode,
|
||||||
],
|
],
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -96,9 +149,9 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
/>
|
/>
|
||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<CommandPlugin />
|
<CommandPlugin />
|
||||||
<AutocompletePlugin options={options} />
|
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
||||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||||
<InitialValuePlugin value={value} options={options} />
|
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
|
||||||
</div>
|
</div>
|
||||||
</LexicalComposer>
|
</LexicalComposer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface Suggestion {
|
|||||||
disabled?: boolean; // 标记是否禁用
|
disabled?: boolean; // 标记是否禁用
|
||||||
}
|
}
|
||||||
|
|
||||||
const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> = ({ options, enableJinja2 = false }) => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
@@ -82,7 +82,32 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
|||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
const insertMention = (suggestion: Suggestion) => {
|
const insertMention = (suggestion: Suggestion) => {
|
||||||
editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion });
|
if (enableJinja2) {
|
||||||
|
// 在jinja2模式下,插入{{variable}}格式的文本
|
||||||
|
editor.update(() => {
|
||||||
|
const selection = $getSelection();
|
||||||
|
if ($isRangeSelection(selection)) {
|
||||||
|
const anchorNode = selection.anchor.getNode();
|
||||||
|
const anchorOffset = selection.anchor.offset;
|
||||||
|
const nodeText = anchorNode.getTextContent();
|
||||||
|
|
||||||
|
// 移除触发字符'/'
|
||||||
|
const textBefore = nodeText.substring(0, anchorOffset - 1);
|
||||||
|
const textAfter = nodeText.substring(anchorOffset);
|
||||||
|
const newText = textBefore + `{{${suggestion.value}}}` + textAfter;
|
||||||
|
|
||||||
|
anchorNode.setTextContent(newText);
|
||||||
|
|
||||||
|
// 设置光标位置到插入文本之后
|
||||||
|
const newOffset = textBefore.length + `{{${suggestion.value}}}`.length;
|
||||||
|
selection.anchor.offset = newOffset;
|
||||||
|
selection.focus.offset = newOffset;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 普通模式下使用VariableNode
|
||||||
|
editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion });
|
||||||
|
}
|
||||||
setShowSuggestions(false);
|
setShowSuggestions(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,31 @@ import { type Suggestion } from '../plugin/AutocompletePlugin'
|
|||||||
interface InitialValuePluginProps {
|
interface InitialValuePluginProps {
|
||||||
value: string;
|
value: string;
|
||||||
options?: Suggestion[];
|
options?: Suggestion[];
|
||||||
|
enableJinja2?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [] }) => {
|
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [], enableJinja2 = false }) => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
const initializedRef = useRef(false);
|
const prevValueRef = useRef<string>('');
|
||||||
|
const isUserInputRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initializedRef.current && value) {
|
// 监听编辑器变化,标记是否为用户输入
|
||||||
|
const removeListener = editor.registerUpdateListener(({ editorState }) => {
|
||||||
|
editorState.read(() => {
|
||||||
|
const root = $getRoot();
|
||||||
|
const textContent = root.getTextContent();
|
||||||
|
if (textContent !== prevValueRef.current) {
|
||||||
|
isUserInputRef.current = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return removeListener;
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value !== prevValueRef.current && !isUserInputRef.current) {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const root = $getRoot();
|
const root = $getRoot();
|
||||||
root.clear();
|
root.clear();
|
||||||
@@ -28,7 +45,11 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
|||||||
const contextMatch = part.match(/^\{\{context\}\}$/);
|
const contextMatch = part.match(/^\{\{context\}\}$/);
|
||||||
const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/);
|
const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/);
|
||||||
|
|
||||||
// 匹配{{context}}格式
|
if (enableJinja2) {
|
||||||
|
paragraph.append($createTextNode(part));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (contextMatch) {
|
if (contextMatch) {
|
||||||
const contextSuggestion = options.find(s => s.isContext && s.label === 'context');
|
const contextSuggestion = options.find(s => s.isContext && s.label === 'context');
|
||||||
if (contextSuggestion) {
|
if (contextSuggestion) {
|
||||||
@@ -39,7 +60,6 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 匹配{{conv.xx}}格式
|
|
||||||
if (conversationMatch) {
|
if (conversationMatch) {
|
||||||
const [_, variableName] = conversationMatch;
|
const [_, variableName] = conversationMatch;
|
||||||
const conversationSuggestion = options.find(s =>
|
const conversationSuggestion = options.find(s =>
|
||||||
@@ -53,7 +73,6 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 匹配普通变量{{nodeId.label}}格式
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const [_, nodeId, label] = match;
|
const [_, nodeId, label] = match;
|
||||||
|
|
||||||
@@ -75,11 +94,12 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
|||||||
});
|
});
|
||||||
|
|
||||||
root.append(paragraph);
|
root.append(paragraph);
|
||||||
});
|
}, { discrete: true });
|
||||||
|
|
||||||
initializedRef.current = true;
|
|
||||||
}
|
}
|
||||||
}, [options]);
|
|
||||||
|
prevValueRef.current = value;
|
||||||
|
isUserInputRef.current = false;
|
||||||
|
}, [value, options, editor, enableJinja2]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
import { $getRoot, $getSelection, $isRangeSelection, TextNode, $createTextNode } from 'lexical';
|
||||||
|
|
||||||
|
const JsonHighlightPlugin = () => {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
||||||
|
const text = textNode.getTextContent();
|
||||||
|
|
||||||
|
// Check if text contains JSON-like patterns
|
||||||
|
if (containsJsonPatterns(text)) {
|
||||||
|
const parent = textNode.getParent();
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
// Split text into tokens and create new nodes with appropriate classes
|
||||||
|
const tokens = tokenizeJson(text);
|
||||||
|
const newNodes = tokens.map(token => {
|
||||||
|
const newNode = $createTextNode(token.text);
|
||||||
|
|
||||||
|
// Set format based on token type
|
||||||
|
switch (token.type) {
|
||||||
|
case 'string':
|
||||||
|
newNode.setFormat('code');
|
||||||
|
newNode.setStyle('color: #032f62');
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
newNode.setFormat('code');
|
||||||
|
newNode.setStyle('color: #005cc5');
|
||||||
|
break;
|
||||||
|
case 'boolean':
|
||||||
|
newNode.setFormat('code');
|
||||||
|
newNode.setStyle('color: #d73a49');
|
||||||
|
break;
|
||||||
|
case 'null':
|
||||||
|
newNode.setFormat('code');
|
||||||
|
newNode.setStyle('color: #6f42c1');
|
||||||
|
break;
|
||||||
|
case 'key':
|
||||||
|
newNode.setFormat('code');
|
||||||
|
newNode.setStyle('color: #22863a; font-weight: bold');
|
||||||
|
break;
|
||||||
|
case 'punctuation':
|
||||||
|
newNode.setFormat('code');
|
||||||
|
newNode.setStyle('color: #24292e');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNode;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace the original text node with the new highlighted nodes
|
||||||
|
if (newNodes.length > 1) {
|
||||||
|
textNode.replace(newNodes[0]);
|
||||||
|
for (let i = 1; i < newNodes.length; i++) {
|
||||||
|
newNodes[i - 1].insertAfter(newNodes[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function containsJsonPatterns(text: string): boolean {
|
||||||
|
// Check for JSON-like patterns
|
||||||
|
return /[{}\[\]:,]/.test(text) ||
|
||||||
|
/"[^"]*"/.test(text) ||
|
||||||
|
/\b\d+(\.\d+)?\b/.test(text) ||
|
||||||
|
/\b(true|false|null)\b/.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenizeJson(text: string): Array<{text: string, type: string}> {
|
||||||
|
const tokens: Array<{text: string, type: string}> = [];
|
||||||
|
const regex = /("[^"]*")|([{}\[\]:,])|(\b\d+(?:\.\d+)?\b)|(\b(?:true|false|null)\b)|(\s+)|([^\s{}\[\]:,"]+)/g;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
const [fullMatch, string, punctuation, number, boolean, whitespace, other] = match;
|
||||||
|
|
||||||
|
if (string) {
|
||||||
|
// Check if it's a key (followed by colon)
|
||||||
|
const afterMatch = text.slice(match.index + fullMatch.length).trim();
|
||||||
|
if (afterMatch.startsWith(':')) {
|
||||||
|
tokens.push({ text: fullMatch, type: 'key' });
|
||||||
|
} else {
|
||||||
|
tokens.push({ text: fullMatch, type: 'string' });
|
||||||
|
}
|
||||||
|
} else if (punctuation) {
|
||||||
|
tokens.push({ text: fullMatch, type: 'punctuation' });
|
||||||
|
} else if (number) {
|
||||||
|
tokens.push({ text: fullMatch, type: 'number' });
|
||||||
|
} else if (boolean) {
|
||||||
|
if (fullMatch === 'null') {
|
||||||
|
tokens.push({ text: fullMatch, type: 'null' });
|
||||||
|
} else {
|
||||||
|
tokens.push({ text: fullMatch, type: 'boolean' });
|
||||||
|
}
|
||||||
|
} else if (whitespace || other) {
|
||||||
|
tokens.push({ text: fullMatch, type: 'text' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JsonHighlightPlugin;
|
||||||
@@ -28,12 +28,18 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
|||||||
|
|
||||||
const handleOpen = (data?: HttpRequestConfigForm['auth']) => {
|
const handleOpen = (data?: HttpRequestConfigForm['auth']) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.setFieldsValue({
|
const initialValues = {
|
||||||
auth: !data.auth_type || data.auth_type === 'none' ? 'none' : 'api_key',
|
auth: !data.auth_type || data.auth_type === 'none' ? 'none' : 'api_key',
|
||||||
auth_type: !data.auth_type || data.auth_type === 'none' ? undefined : data.auth_type,
|
auth_type: !data.auth_type || data.auth_type === 'none' ? undefined : data.auth_type,
|
||||||
header: data.header,
|
header: data.header,
|
||||||
api_key: data.api_key
|
api_key: data.api_key
|
||||||
})
|
}
|
||||||
|
form.setFieldValue('auth', initialValues.auth)
|
||||||
|
if (initialValues.auth !== 'none') {
|
||||||
|
setTimeout(() => {
|
||||||
|
form.setFieldsValue(initialValues)
|
||||||
|
}, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
};
|
};
|
||||||
@@ -91,6 +97,9 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
|||||||
<FormItem
|
<FormItem
|
||||||
name="auth"
|
name="auth"
|
||||||
label={t('workflow.config.http-request.authType')}
|
label={t('workflow.config.http-request.authType')}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: t('common.pleaseSelect') }
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={[
|
||||||
@@ -103,6 +112,9 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
|||||||
<FormItem
|
<FormItem
|
||||||
name="auth_type"
|
name="auth_type"
|
||||||
label={t('workflow.config.http-request.authType')}
|
label={t('workflow.config.http-request.authType')}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: t('common.pleaseSelect') }
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={[
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useCallback } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button, Select, Table, Form, type TableProps } from 'antd';
|
import { Button, Select, Table, Form, type TableProps } from 'antd';
|
||||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
@@ -6,46 +6,8 @@ import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin';
|
|||||||
import Empty from '@/components/Empty';
|
import Empty from '@/components/Empty';
|
||||||
import VariableSelect from '../VariableSelect';
|
import VariableSelect from '../VariableSelect';
|
||||||
|
|
||||||
interface EditableCellProps extends React.HTMLAttributes<HTMLElement> {
|
|
||||||
name?: string | string[];
|
|
||||||
inputType?: 'select' | 'variableSelect';
|
|
||||||
options?: { value: string, label: string }[] | Suggestion[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditableCell: React.FC<React.PropsWithChildren<EditableCellProps>> = ({
|
|
||||||
name,
|
|
||||||
inputType,
|
|
||||||
options,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
if (!inputType) return <td {...restProps}>{children}</td>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<td {...restProps}>
|
|
||||||
<Form.Item name={name} style={{ margin: 0 }}>
|
|
||||||
{inputType === 'select' ? (
|
|
||||||
<Select
|
|
||||||
placeholder={t('common.pleaseSelect')}
|
|
||||||
size="small"
|
|
||||||
options={options as { value: string, label: string }[]}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<VariableSelect
|
|
||||||
placeholder={t('common.pleaseSelect')}
|
|
||||||
size="small"
|
|
||||||
options={(options as Suggestion[]) || []}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface TableRow {
|
export interface TableRow {
|
||||||
key: string;
|
key?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
@@ -56,100 +18,158 @@ interface EditableTableProps {
|
|||||||
title?: string;
|
title?: string;
|
||||||
options?: Suggestion[];
|
options?: Suggestion[];
|
||||||
typeOptions?: { value: string, label: string }[]
|
typeOptions?: { value: string, label: string }[]
|
||||||
|
filterBooleanType?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditableTable: React.FC<EditableTableProps> = ({
|
const EditableTable: React.FC<EditableTableProps> = ({
|
||||||
parentName,
|
parentName,
|
||||||
title,
|
title,
|
||||||
options = [],
|
options = [],
|
||||||
typeOptions = []
|
typeOptions = [],
|
||||||
|
filterBooleanType = false
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const form = Form.useFormInstance();
|
|
||||||
const values = Form.useWatch(typeof parentName === 'string' ? [parentName] : parentName, form);
|
|
||||||
|
|
||||||
const createNewRow = (): TableRow => ({
|
const createNewRow = (): TableRow => ({
|
||||||
key: Date.now().toString(),
|
|
||||||
name: undefined,
|
name: undefined,
|
||||||
value: undefined,
|
value: undefined,
|
||||||
...(typeOptions.length > 0 && { type: typeOptions[0].value })
|
...(typeOptions.length > 0 && { type: typeOptions[0].value })
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAdd = useCallback(() => {
|
const getColumns = (remove: (index: number) => void): TableProps<TableRow>['columns'] => {
|
||||||
form.setFieldValue(parentName, [...(values ?? []), createNewRow()]);
|
|
||||||
}, [form, parentName, values, typeOptions]);
|
|
||||||
|
|
||||||
const handleDelete = useCallback((index: number) => {
|
|
||||||
const currentValues = form.getFieldValue(parentName) || [];
|
|
||||||
form.setFieldValue(parentName, currentValues.filter((_: TableRow, i: number) => i !== index));
|
|
||||||
}, [form, parentName]);
|
|
||||||
|
|
||||||
const createColumn = (dataIndex: string, inputType: 'select' | 'variableSelect', width: string, columnOptions: any[]) => ({
|
|
||||||
title: t(`workflow.config.${dataIndex}`),
|
|
||||||
dataIndex,
|
|
||||||
width,
|
|
||||||
onCell: (_: TableRow, index?: number) => ({
|
|
||||||
name: typeof parentName === 'string' ? [parentName, index ?? 0, dataIndex] : [...parentName, index ?? 0, dataIndex],
|
|
||||||
inputType,
|
|
||||||
options: columnOptions
|
|
||||||
} as any)
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns: TableProps<TableRow>['columns'] = useMemo(() => {
|
|
||||||
const hasType = typeOptions.length > 0;
|
const hasType = typeOptions.length > 0;
|
||||||
const baseWidth = hasType ? '35%' : '45%';
|
const baseWidth = hasType ? '35%' : '45%';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
createColumn('name', 'variableSelect', baseWidth, options),
|
{
|
||||||
...(hasType ? [createColumn('type', 'select', '20%', typeOptions)] : []),
|
title: t('workflow.config.name'),
|
||||||
createColumn('value', 'variableSelect', baseWidth, options),
|
dataIndex: 'name',
|
||||||
|
width: baseWidth,
|
||||||
|
render: (_: any, __: TableRow, index: number) => (
|
||||||
|
<Form.Item name={[index, 'name']} noStyle>
|
||||||
|
<VariableSelect
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
size="small"
|
||||||
|
options={options}
|
||||||
|
filterBooleanType={filterBooleanType}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
...(hasType ? [{
|
||||||
|
title: t('workflow.config.type'),
|
||||||
|
dataIndex: 'type',
|
||||||
|
width: '20%',
|
||||||
|
render: (_: any, __: TableRow, index: number) => (
|
||||||
|
<Form.Item shouldUpdate noStyle>
|
||||||
|
{(form) => (
|
||||||
|
<Form.Item name={[index, 'type']} noStyle>
|
||||||
|
<Select
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
size="small"
|
||||||
|
options={typeOptions}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
onChange={() => {
|
||||||
|
form.setFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'value'], undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}] : []),
|
||||||
|
{
|
||||||
|
title: t('workflow.config.value'),
|
||||||
|
dataIndex: 'value',
|
||||||
|
width: baseWidth,
|
||||||
|
render: (_: any, __: TableRow, index: number) => (
|
||||||
|
<Form.Item
|
||||||
|
shouldUpdate={(prevValues, currentValues) => {
|
||||||
|
const prevType = prevValues?.[Array.isArray(parentName) ? parentName.join('.') : parentName]?.[index]?.type;
|
||||||
|
const currentType = currentValues?.[Array.isArray(parentName) ? parentName.join('.') : parentName]?.[index]?.type;
|
||||||
|
return prevType !== currentType;
|
||||||
|
}}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
{(form) => {
|
||||||
|
const currentType = form.getFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'type']);
|
||||||
|
const filteredOptions = currentType === 'file'
|
||||||
|
? options.filter(option => option.dataType === 'file')
|
||||||
|
: options;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item name={[index, 'value']} noStyle>
|
||||||
|
<VariableSelect
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
size="small"
|
||||||
|
options={filteredOptions}
|
||||||
|
filterBooleanType={filterBooleanType}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
dataIndex: 'actions',
|
dataIndex: 'actions',
|
||||||
width: '10%',
|
width: '10%',
|
||||||
render: (_: any, __: TableRow, index: number) => (
|
render: (_: any, __: TableRow, index: number) => (
|
||||||
<Button type="text" icon={<DeleteOutlined />} onClick={() => handleDelete(index)} />
|
<Button type="text" icon={<DeleteOutlined />} onClick={() => remove(index)} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [typeOptions, options, t, parentName, handleDelete]);
|
};
|
||||||
|
|
||||||
const AddButton = ({ block = false }: { block?: boolean }) => (
|
|
||||||
<Button
|
|
||||||
type={block ? "dashed" : "text"}
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
onClick={handleAdd}
|
|
||||||
size="small"
|
|
||||||
block={block}
|
|
||||||
className={block ? "rb:mt-1" : ""}
|
|
||||||
>
|
|
||||||
{block && `+${t('common.add')}`}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rb:mb-4">
|
<div className="rb:mb-4">
|
||||||
{title && (
|
<Form.List name={parentName}>
|
||||||
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
|
{(fields, { add, remove }) => {
|
||||||
<div className="rb:font-medium">{title}</div>
|
const AddButton = ({ block = false }: { block?: boolean }) => (
|
||||||
<AddButton />
|
<Button
|
||||||
</div>
|
type={block ? "dashed" : "text"}
|
||||||
)}
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => add(createNewRow())}
|
||||||
<Form.Item name={parentName}>
|
size="small"
|
||||||
<Table<TableRow>
|
block={block}
|
||||||
components={{ body: { cell: EditableCell } }}
|
className={block ? "rb:mt-1" : ""}
|
||||||
bordered
|
>
|
||||||
dataSource={values}
|
{block && `+${t('common.add')}`}
|
||||||
columns={columns}
|
</Button>
|
||||||
pagination={false}
|
);
|
||||||
size="small"
|
|
||||||
locale={{ emptyText: <Empty size={88} /> }}
|
return (
|
||||||
scroll={{ x: 'max-content' }}
|
<>
|
||||||
/>
|
{title && (
|
||||||
</Form.Item>
|
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
|
||||||
|
<div className="rb:font-medium">{title}</div>
|
||||||
{!title && <AddButton block />}
|
<AddButton />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Table<TableRow>
|
||||||
|
bordered
|
||||||
|
dataSource={fields.map((field) => ({
|
||||||
|
key: String(field.key),
|
||||||
|
name: undefined,
|
||||||
|
value: undefined,
|
||||||
|
type: undefined
|
||||||
|
}))}
|
||||||
|
columns={getColumns(remove)}
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
locale={{ emptyText: <Empty size={88} /> }}
|
||||||
|
scroll={{ x: 'max-content' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!title && <AddButton block />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import VariableSelect from "../VariableSelect";
|
|||||||
import MessageEditor from '../MessageEditor'
|
import MessageEditor from '../MessageEditor'
|
||||||
import EditableTable from './EditableTable'
|
import EditableTable from './EditableTable'
|
||||||
|
|
||||||
const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: any; }> = ({
|
||||||
options,
|
options,
|
||||||
|
selectedNode,
|
||||||
|
graphRef
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const form = Form.useFormInstance();
|
const form = Form.useFormInstance();
|
||||||
@@ -22,18 +24,45 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
|||||||
}
|
}
|
||||||
const handleRefresh = (auth: HttpRequestConfigForm['auth']) => {
|
const handleRefresh = (auth: HttpRequestConfigForm['auth']) => {
|
||||||
console.log('handleRefresh', auth)
|
console.log('handleRefresh', auth)
|
||||||
form.setFieldsValue({ auth: {...auth} })
|
form.setFieldsValue({ auth })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChangeBodyContentType = (contentType: string) => {
|
const handleChangeBodyContentType = () => {
|
||||||
const currentValues = form.getFieldsValue()
|
form.setFieldValue(['body', 'data'], undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangeErrorHandleMethod = (method: string) => {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
body: {
|
error_handle: {
|
||||||
...currentValues?.body,
|
method,
|
||||||
content_type: contentType,
|
body: undefined,
|
||||||
data: undefined
|
status_code: undefined,
|
||||||
|
headers: undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 更新节点连接桩
|
||||||
|
console.log('handleChangeErrorHandleMethod', selectedNode, graphRef?.current)
|
||||||
|
if (selectedNode && graphRef?.current) {
|
||||||
|
const existingPorts = selectedNode.getPorts();
|
||||||
|
const errorPort = existingPorts.find((port: any) => port.id === 'ERROR');
|
||||||
|
|
||||||
|
if (method === 'branch' && !errorPort) {
|
||||||
|
// 添加异常节点连接桩
|
||||||
|
selectedNode.addPort({
|
||||||
|
id: 'ERROR',
|
||||||
|
group: 'right',
|
||||||
|
attrs: { text: { text: t('workflow.config.http-request.errorBranch'), fontSize: 12, fill: '#5B6167' }}
|
||||||
|
});
|
||||||
|
} else if (method !== 'branch' && errorPort) {
|
||||||
|
// 移除异常节点连接桩和相关连线
|
||||||
|
const edges = graphRef.current.getEdges().filter((edge: any) =>
|
||||||
|
edge.getSourceCellId() === selectedNode.id && edge.getSourcePortId() === 'ERROR'
|
||||||
|
);
|
||||||
|
edges.forEach((edge: any) => graphRef.current.removeCell(edge));
|
||||||
|
selectedNode.removePort('ERROR');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('HttpRequest', values)
|
console.log('HttpRequest', values)
|
||||||
@@ -73,6 +102,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
|||||||
parentName="headers"
|
parentName="headers"
|
||||||
title="HEADERS"
|
title="HEADERS"
|
||||||
options={options}
|
options={options}
|
||||||
|
filterBooleanType={true}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -81,6 +111,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
|||||||
parentName="params"
|
parentName="params"
|
||||||
title="PARAMS"
|
title="PARAMS"
|
||||||
options={options}
|
options={options}
|
||||||
|
filterBooleanType={true}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -104,6 +135,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
|||||||
<EditableTable
|
<EditableTable
|
||||||
parentName={['body', 'data']}
|
parentName={['body', 'data']}
|
||||||
options={options}
|
options={options}
|
||||||
|
filterBooleanType={true}
|
||||||
typeOptions={[
|
typeOptions={[
|
||||||
{ label: 'text', value: 'text' },
|
{ label: 'text', value: 'text' },
|
||||||
{ label: 'file', value: 'file' }
|
{ label: 'file', value: 'file' }
|
||||||
@@ -116,12 +148,15 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
|||||||
<EditableTable
|
<EditableTable
|
||||||
parentName={['body', 'data']}
|
parentName={['body', 'data']}
|
||||||
options={options}
|
options={options}
|
||||||
|
filterBooleanType={true}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
}
|
}
|
||||||
{values?.body?.content_type === 'json' &&
|
{values?.body?.content_type === 'json' &&
|
||||||
<Form.Item name={['body', 'data']}>
|
<Form.Item name={['body', 'data']}>
|
||||||
<MessageEditor
|
<MessageEditor
|
||||||
|
key="json"
|
||||||
|
parentName={['body', 'data']}
|
||||||
options={options}
|
options={options}
|
||||||
isArray={false}
|
isArray={false}
|
||||||
title="JSON"
|
title="JSON"
|
||||||
@@ -131,6 +166,8 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
|||||||
{values?.body?.content_type === 'raw' &&
|
{values?.body?.content_type === 'raw' &&
|
||||||
<Form.Item name={['body', 'data']}>
|
<Form.Item name={['body', 'data']}>
|
||||||
<MessageEditor
|
<MessageEditor
|
||||||
|
key="raw"
|
||||||
|
parentName={['body', 'data']}
|
||||||
options={options}
|
options={options}
|
||||||
isArray={false}
|
isArray={false}
|
||||||
title="RAW TEXT"
|
title="RAW TEXT"
|
||||||
@@ -141,6 +178,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
|||||||
<Form.Item name={['body', 'data']}>
|
<Form.Item name={['body', 'data']}>
|
||||||
<VariableSelect
|
<VariableSelect
|
||||||
options={options}
|
options={options}
|
||||||
|
filterBooleanType={true}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
}
|
}
|
||||||
@@ -185,7 +223,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={['retry', 'retry_interval']}
|
name={['retry', 'retry_interval']}
|
||||||
label={t('workflow.config.http-request.retry_interval')}
|
label={<>{t('workflow.config.http-request.retry_interval')} <span className="rb:text-[#5B6167]">(ms)</span></>}
|
||||||
>
|
>
|
||||||
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
|
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -196,6 +234,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
|||||||
<Form.Item layout="horizontal" name={['error_handle', 'method']} label={t('workflow.config.http-request.error_handle')}>
|
<Form.Item layout="horizontal" name={['error_handle', 'method']} label={t('workflow.config.http-request.error_handle')}>
|
||||||
<Select
|
<Select
|
||||||
placeholder={t('common.pleaseSelect')}
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
onChange={handleChangeErrorHandleMethod}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'none', label: t('workflow.config.http-request.none') },
|
{ value: 'none', label: t('workflow.config.http-request.none') },
|
||||||
{ value: 'default', label: t('workflow.config.http-request.default') },
|
{ value: 'default', label: t('workflow.config.http-request.default') },
|
||||||
@@ -207,32 +246,19 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
|||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={['error_handle', 'body']}
|
name={['error_handle', 'body']}
|
||||||
label="body"
|
label={<>body <span className="rb:text-[#5B6167] rb:ml-1">string</span></>}
|
||||||
>
|
>
|
||||||
<Input placeholder={t('common.pleaseEnter')} />
|
<Input placeholder={t('common.pleaseEnter')} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={['error_handle', 'status_code']}
|
name={['error_handle', 'status_code']}
|
||||||
label="status_code"
|
label={<>status_code <span className="rb:text-[#5B6167] rb:ml-1">number</span></>}
|
||||||
>
|
>
|
||||||
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
|
<InputNumber placeholder={t('common.pleaseEnter')} className="rb:w-full!" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={['error_handle', 'headers']}
|
name={['error_handle', 'headers']}
|
||||||
label="headers"
|
label={<>headers <span className="rb:text-[#5B6167] rb:ml-1">object</span></>}
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
validator: (_, value) => {
|
|
||||||
if (!value) return Promise.resolve();
|
|
||||||
try {
|
|
||||||
JSON.parse(value);
|
|
||||||
return Promise.resolve();
|
|
||||||
} catch {
|
|
||||||
return Promise.reject(new Error('Please enter valid JSON format'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
|||||||
{(fields, { add, remove }) => (
|
{(fields, { add, remove }) => (
|
||||||
<>
|
<>
|
||||||
{fields.map(({ key, name, ...restField }) => (
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
<Row gutter={12} className="rb:mb-2">
|
<Row key={key} gutter={12} className="rb:mb-2">
|
||||||
<Col span={10}>
|
<Col span={10}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...restField}
|
{...restField}
|
||||||
name={[name, 'name']}
|
name={[name, 'name']}
|
||||||
noStyle
|
noStyle
|
||||||
>
|
>
|
||||||
<Input placeholder={t('common.pleaseEnter')} />
|
<Input placeholder={t('common.pleaseEnter')} data-field-type="mapping-name" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import { MinusCircleOutlined } from '@ant-design/icons';
|
|||||||
import Editor from '../Editor'
|
import Editor from '../Editor'
|
||||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||||
|
|
||||||
interface TextareaProps {
|
interface MessageEditor {
|
||||||
options: Suggestion[];
|
options: Suggestion[];
|
||||||
title?: string
|
title?: string
|
||||||
isArray?: boolean;
|
isArray?: boolean;
|
||||||
parentName?: string;
|
parentName?: string | string[];
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
enableJinja2?: boolean;
|
||||||
onChange?: (value?: string) => void;
|
onChange?: (value?: string) => void;
|
||||||
}
|
}
|
||||||
const roleOptions = [
|
const roleOptions = [
|
||||||
@@ -20,12 +21,13 @@ const roleOptions = [
|
|||||||
{ label: 'USER', value: 'USER' },
|
{ label: 'USER', value: 'USER' },
|
||||||
{ label: 'ASSISTANT', value: 'ASSISTANT' },
|
{ label: 'ASSISTANT', value: 'ASSISTANT' },
|
||||||
]
|
]
|
||||||
const MessageEditor: FC<TextareaProps> = ({
|
const MessageEditor: FC<MessageEditor> = ({
|
||||||
title,
|
title,
|
||||||
isArray = true,
|
isArray = true,
|
||||||
parentName = 'messages',
|
parentName = 'messages',
|
||||||
placeholder,
|
placeholder,
|
||||||
options,
|
options,
|
||||||
|
enableJinja2 = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const form = Form.useFormInstance();
|
const form = Form.useFormInstance();
|
||||||
@@ -33,10 +35,17 @@ const MessageEditor: FC<TextareaProps> = ({
|
|||||||
|
|
||||||
// 检查是否已经使用了context变量,将已使用的context设置为disabled
|
// 检查是否已经使用了context变量,将已使用的context设置为disabled
|
||||||
const processedOptions = useMemo(() => {
|
const processedOptions = useMemo(() => {
|
||||||
if (!isArray || !values?.[parentName]) return options;
|
if (!isArray) return options;
|
||||||
|
|
||||||
|
// 获取表单中对应字段的值
|
||||||
|
const fieldValue = Array.isArray(parentName)
|
||||||
|
? parentName.reduce((obj, key) => obj?.[key], values)
|
||||||
|
: values?.[parentName];
|
||||||
|
|
||||||
|
if (!fieldValue) return options;
|
||||||
|
|
||||||
// 获取所有消息内容
|
// 获取所有消息内容
|
||||||
const allContents = values[parentName]
|
const allContents = fieldValue
|
||||||
.map((msg: any) => msg?.content || '')
|
.map((msg: any) => msg?.content || '')
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
|
||||||
@@ -50,7 +59,11 @@ const MessageEditor: FC<TextareaProps> = ({
|
|||||||
}, [options, values, parentName, isArray]);
|
}, [options, values, parentName, isArray]);
|
||||||
|
|
||||||
const handleAdd = (add: FormListOperation['add']) => {
|
const handleAdd = (add: FormListOperation['add']) => {
|
||||||
const list = values?.[parentName] || [];
|
const fieldValue = Array.isArray(parentName)
|
||||||
|
? parentName.reduce((obj, key) => obj?.[key], values)
|
||||||
|
: values?.[parentName];
|
||||||
|
|
||||||
|
const list = fieldValue || [];
|
||||||
const lastRole = list.length > 0 ? list[list.length - 1]?.role : 'ASSISTANT';
|
const lastRole = list.length > 0 ? list[list.length - 1]?.role : 'ASSISTANT';
|
||||||
|
|
||||||
add({
|
add({
|
||||||
@@ -61,14 +74,14 @@ const MessageEditor: FC<TextareaProps> = ({
|
|||||||
|
|
||||||
if (!isArray) {
|
if (!isArray) {
|
||||||
return (
|
return (
|
||||||
<Space size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
<Space size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white" data-editor-type={parentName === 'template' ? 'template' : undefined}>
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
{title ?? t('workflow.answerDesc')}
|
{title ?? t('workflow.answerDesc')}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Form.Item name={parentName} noStyle>
|
<Form.Item name={parentName} noStyle>
|
||||||
<Editor placeholder={placeholder} options={processedOptions} />
|
<Editor enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
@@ -79,7 +92,11 @@ const MessageEditor: FC<TextareaProps> = ({
|
|||||||
{(fields, { add, remove }) => (
|
{(fields, { add, remove }) => (
|
||||||
<Space size={12} direction="vertical" className="rb:w-full">
|
<Space size={12} direction="vertical" className="rb:w-full">
|
||||||
{fields.map(({ key, name, ...restField }) => {
|
{fields.map(({ key, name, ...restField }) => {
|
||||||
const currentRole = (values?.[parentName]?.[name]?.role || 'USER').toUpperCase();
|
const fieldValue = Array.isArray(parentName)
|
||||||
|
? parentName.reduce((obj, key) => obj?.[key], values)
|
||||||
|
: values?.[parentName];
|
||||||
|
|
||||||
|
const currentRole = (fieldValue?.[name]?.role || 'USER').toUpperCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space key={key} size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
<Space key={key} size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||||
@@ -105,7 +122,7 @@ const MessageEditor: FC<TextareaProps> = ({
|
|||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
<Form.Item {...restField} name={[name, 'content']} noStyle>
|
<Form.Item {...restField} name={[name, 'content']} noStyle>
|
||||||
<Editor placeholder={placeholder} options={processedOptions} />
|
<Editor enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface VariableSelectProps extends SelectProps {
|
|||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
allowClear?: boolean;
|
allowClear?: boolean;
|
||||||
|
filterBooleanType?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VariableSelect: FC<VariableSelectProps> = ({
|
const VariableSelect: FC<VariableSelectProps> = ({
|
||||||
@@ -18,6 +19,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
|||||||
allowClear = true,
|
allowClear = true,
|
||||||
onChange,
|
onChange,
|
||||||
size,
|
size,
|
||||||
|
filterBooleanType = false,
|
||||||
...resetPorps
|
...resetPorps
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
|||||||
}
|
}
|
||||||
const labelRender: LabelRender = (props) => {
|
const labelRender: LabelRender = (props) => {
|
||||||
const { value } = props
|
const { value } = props
|
||||||
const filterOption = options.find(vo => `{{${vo.value}}}` === value)
|
const filterOption = filteredOptions.find(vo => `{{${vo.value}}}` === value)
|
||||||
|
|
||||||
if (filterOption) {
|
if (filterOption) {
|
||||||
return (
|
return (
|
||||||
@@ -54,7 +56,11 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const groupedSuggestions = options.reduce((groups: Record<string, any[]>, suggestion) => {
|
const filteredOptions = filterBooleanType
|
||||||
|
? options.filter(option => option.dataType !== 'boolean')
|
||||||
|
: options;
|
||||||
|
|
||||||
|
const groupedSuggestions = filteredOptions.reduce((groups: Record<string, any[]>, suggestion) => {
|
||||||
const { nodeData } = suggestion
|
const { nodeData } = suggestion
|
||||||
const nodeId = nodeData.id as string;
|
const nodeId = nodeData.id as string;
|
||||||
if (!groups[nodeId]) {
|
if (!groups[nodeId]) {
|
||||||
@@ -64,12 +70,10 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
|||||||
return groups;
|
return groups;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const groupedOptions = Object.entries(groupedSuggestions).map(([nodeId, suggestions]) => ({
|
const groupedOptions = Object.entries(groupedSuggestions).map(([_nodeId, suggestions]) => ({
|
||||||
label: suggestions[0].nodeData.name,
|
label: suggestions[0].nodeData.name,
|
||||||
options: suggestions.map(s => ({ label: s.label, value: `{{${s.value}}}` }))
|
options: suggestions.map(s => ({ label: s.label, value: `{{${s.value}}}` }))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log('groupedOptions', groupedOptions)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -45,13 +45,134 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
const values = Form.useWatch([], form);
|
const values = Form.useWatch([], form);
|
||||||
const variableModalRef = useRef<VariableEditModalRef>(null)
|
const variableModalRef = useRef<VariableEditModalRef>(null)
|
||||||
const [editIndex, setEditIndex] = useState<number | null>(null)
|
const [editIndex, setEditIndex] = useState<number | null>(null)
|
||||||
|
const prevMappingNamesRef = useRef<string[]>([])
|
||||||
|
const prevTemplateVarsRef = useRef<string[]>([])
|
||||||
|
const syncTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const isSyncingRef = useRef(false)
|
||||||
|
const lastSyncSourceRef = useRef<'mapping' | 'template' | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedNode?.getData()?.id) {
|
if (selectedNode?.getData()?.id) {
|
||||||
form.resetFields()
|
form.resetFields()
|
||||||
|
prevMappingNamesRef.current = []
|
||||||
|
prevTemplateVarsRef.current = []
|
||||||
|
lastSyncSourceRef.current = null
|
||||||
}
|
}
|
||||||
}, [selectedNode?.getData()?.id])
|
}, [selectedNode?.getData()?.id])
|
||||||
|
|
||||||
|
// Sync template when mapping names change
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSyncingRef.current || lastSyncSourceRef.current === 'mapping' || selectedNode?.data?.type !== 'jinja-render' || !values?.mapping || !values?.template) return
|
||||||
|
|
||||||
|
const currentMappingNames = values.mapping.map((item: any) => item.name).filter(Boolean)
|
||||||
|
const prevNames = prevMappingNamesRef.current
|
||||||
|
|
||||||
|
if (prevNames.length === 0) {
|
||||||
|
prevMappingNamesRef.current = currentMappingNames
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JSON.stringify(prevNames) === JSON.stringify(currentMappingNames)) return
|
||||||
|
|
||||||
|
if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current)
|
||||||
|
const activeElement = document.activeElement as HTMLElement
|
||||||
|
|
||||||
|
syncTimeoutRef.current = setTimeout(() => {
|
||||||
|
let updatedTemplate = String(form.getFieldValue('template') || '')
|
||||||
|
|
||||||
|
prevNames.forEach((oldName, index) => {
|
||||||
|
const newName = currentMappingNames[index]
|
||||||
|
if (newName && oldName !== newName) {
|
||||||
|
updatedTemplate = updatedTemplate.replace(new RegExp(`{{\\s*${oldName}\\s*}}`, 'g'), `{{${newName}}}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (updatedTemplate !== form.getFieldValue('template')) {
|
||||||
|
isSyncingRef.current = true
|
||||||
|
lastSyncSourceRef.current = 'mapping'
|
||||||
|
const newTemplateVars = (updatedTemplate.match(/{{\s*([\w.]+)\s*}}/g) || []).map(m => m.replace(/{{\s*|\s*}}/g, ''))
|
||||||
|
prevTemplateVarsRef.current = newTemplateVars
|
||||||
|
prevMappingNamesRef.current = currentMappingNames
|
||||||
|
form.setFieldValue('template', updatedTemplate)
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
activeElement?.focus?.()
|
||||||
|
setTimeout(() => {
|
||||||
|
isSyncingRef.current = false
|
||||||
|
lastSyncSourceRef.current = null
|
||||||
|
}, 50)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
prevMappingNamesRef.current = currentMappingNames
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}, [values?.mapping, selectedNode?.data?.type, form])
|
||||||
|
|
||||||
|
// Sync mapping when template variables change
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSyncingRef.current || lastSyncSourceRef.current === 'template' || selectedNode?.data?.type !== 'jinja-render' || !values?.template || !values?.mapping) return
|
||||||
|
|
||||||
|
const templateVars = (String(values.template).match(/{{\s*([\w.]+)\s*}}/g) || []).map(m => m.replace(/{{\s*|\s*}}/g, ''))
|
||||||
|
if (JSON.stringify(prevTemplateVarsRef.current) === JSON.stringify(templateVars)) return
|
||||||
|
|
||||||
|
const isTemplateEditor = document.activeElement?.closest('[data-editor-type="template"]')
|
||||||
|
if (!isTemplateEditor) {
|
||||||
|
prevTemplateVarsRef.current = templateVars
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedMapping = [...values.mapping]
|
||||||
|
const existingNames = updatedMapping.map(item => item.name)
|
||||||
|
let updatedTemplate = String(values.template)
|
||||||
|
|
||||||
|
if (prevTemplateVarsRef.current.length > 0) {
|
||||||
|
prevTemplateVarsRef.current.forEach((oldVar, index) => {
|
||||||
|
const newVar = templateVars[index]
|
||||||
|
if (newVar && oldVar !== newVar && updatedMapping[index]) {
|
||||||
|
updatedMapping[index] = { ...updatedMapping[index], name: newVar }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
templateVars.forEach(varName => {
|
||||||
|
const existingMapping = updatedMapping.find(item => item.value === `{{${varName}}}`)
|
||||||
|
const regex = new RegExp(`{{\\s*${varName.replace(/\./g, '\\.')}\\s*}}`, 'g')
|
||||||
|
|
||||||
|
if (existingMapping) {
|
||||||
|
updatedTemplate = updatedTemplate.replace(regex, `{{${existingMapping.name}}}`)
|
||||||
|
} else if (!existingNames.includes(varName)) {
|
||||||
|
const mappingName = varName.includes('.') ? varName.split('.').pop() || varName : varName
|
||||||
|
updatedMapping.push({ name: mappingName, value: `{{${varName}}}` })
|
||||||
|
updatedTemplate = updatedTemplate.replace(regex, `{{${mappingName}}}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const seenNames = new Set<string>()
|
||||||
|
const finalMapping = updatedMapping.filter(item => {
|
||||||
|
const isUsed = templateVars.some(v => item.name === v || item.value === `{{${v}}}`)
|
||||||
|
if (!isUsed || seenNames.has(item.name)) return false
|
||||||
|
seenNames.add(item.name)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
isSyncingRef.current = true
|
||||||
|
lastSyncSourceRef.current = 'template'
|
||||||
|
prevMappingNamesRef.current = finalMapping.map((item: any) => item.name).filter(Boolean)
|
||||||
|
prevTemplateVarsRef.current = templateVars
|
||||||
|
|
||||||
|
if (JSON.stringify(finalMapping) !== JSON.stringify(values.mapping)) {
|
||||||
|
form.setFieldValue('mapping', finalMapping)
|
||||||
|
}
|
||||||
|
if (updatedTemplate !== String(values.template)) {
|
||||||
|
form.setFieldValue('template', updatedTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isSyncingRef.current = false
|
||||||
|
lastSyncSourceRef.current = null
|
||||||
|
}, 50)
|
||||||
|
}, [values?.template, selectedNode?.data?.type, form])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedNode && form) {
|
if (selectedNode && form) {
|
||||||
const { type = 'default', name = '', config } = selectedNode.getData() || {}
|
const { type = 'default', name = '', config } = selectedNode.getData() || {}
|
||||||
@@ -96,6 +217,8 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Object.keys(values).forEach(key => {
|
Object.keys(values).forEach(key => {
|
||||||
if (selectedNode.data?.config?.[key]) {
|
if (selectedNode.data?.config?.[key]) {
|
||||||
// Create a deep copy to avoid reference sharing between nodes
|
// Create a deep copy to avoid reference sharing between nodes
|
||||||
@@ -114,7 +237,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
...allRest,
|
...allRest,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [values, selectedNode])
|
}, [values, selectedNode, form])
|
||||||
|
|
||||||
const handleAddVariable = () => {
|
const handleAddVariable = () => {
|
||||||
variableModalRef.current?.handleOpen()
|
variableModalRef.current?.handleOpen()
|
||||||
@@ -190,11 +313,88 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
.map(node => node.id);
|
.map(node => node.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Find parent loop/iteration node if current node is a child
|
||||||
|
const getParentLoopNode = (nodeId: string): Node | null => {
|
||||||
|
const node = nodes.find(n => n.id === nodeId);
|
||||||
|
if (!node) return null;
|
||||||
|
|
||||||
|
const nodeData = node.getData();
|
||||||
|
const cycle = nodeData?.cycle;
|
||||||
|
|
||||||
|
if (cycle) {
|
||||||
|
const parentNode = nodes.find(n => n.getData().id === cycle);
|
||||||
|
if (parentNode) {
|
||||||
|
const parentData = parentNode.getData();
|
||||||
|
if (parentData?.type === 'loop' || parentData?.type === 'iteration') {
|
||||||
|
return parentNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const allPreviousNodeIds = getAllPreviousNodes(selectedNode.id);
|
const allPreviousNodeIds = getAllPreviousNodes(selectedNode.id);
|
||||||
const childNodeIds = getChildNodes(selectedNode.id);
|
const childNodeIds = getChildNodes(selectedNode.id);
|
||||||
|
const parentLoopNode = getParentLoopNode(selectedNode.id);
|
||||||
|
|
||||||
console.log('childNodeIds', selectedNode, childNodeIds)
|
console.log('childNodeIds', selectedNode, childNodeIds)
|
||||||
const allRelevantNodeIds = [...allPreviousNodeIds, ...childNodeIds];
|
const allRelevantNodeIds = [...allPreviousNodeIds, ...childNodeIds];
|
||||||
|
|
||||||
|
// Add parent loop/iteration node variables if current node is a child
|
||||||
|
if (parentLoopNode) {
|
||||||
|
const parentData = parentLoopNode.getData();
|
||||||
|
|
||||||
|
if (parentData.type === 'loop') {
|
||||||
|
const cycleVars = parentData.cycle_vars || [];
|
||||||
|
cycleVars.forEach((cycleVar: any) => {
|
||||||
|
const key = `${parentLoopNode.getData().id}_cycle_${cycleVar.name}`;
|
||||||
|
if (!addedKeys.has(key)) {
|
||||||
|
addedKeys.add(key);
|
||||||
|
variableList.push({
|
||||||
|
key,
|
||||||
|
label: cycleVar.name,
|
||||||
|
type: 'variable',
|
||||||
|
dataType: cycleVar.type || 'String',
|
||||||
|
value: `${parentLoopNode.getData().id}.${cycleVar.name}`,
|
||||||
|
nodeData: parentData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (parentData.type === 'iteration') {
|
||||||
|
// Add item and index variables for iteration parent
|
||||||
|
const itemKey = `${parentLoopNode.getData().id}_item`;
|
||||||
|
const indexKey = `${parentLoopNode.getData().id}_index`;
|
||||||
|
|
||||||
|
if (!addedKeys.has(itemKey)) {
|
||||||
|
addedKeys.add(itemKey);
|
||||||
|
variableList.push({
|
||||||
|
key: itemKey,
|
||||||
|
label: 'item',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'Object',
|
||||||
|
value: `${parentLoopNode.getData().id}.item`,
|
||||||
|
nodeData: parentData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!addedKeys.has(indexKey)) {
|
||||||
|
addedKeys.add(indexKey);
|
||||||
|
variableList.push({
|
||||||
|
key: indexKey,
|
||||||
|
label: 'index',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'Number',
|
||||||
|
value: `${parentLoopNode.getData().id}.index`,
|
||||||
|
nodeData: parentData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add variables from nodes preceding the parent loop/iteration node
|
||||||
|
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
|
||||||
|
allRelevantNodeIds.push(...parentPreviousNodeIds);
|
||||||
|
}
|
||||||
|
|
||||||
allRelevantNodeIds.forEach(nodeId => {
|
allRelevantNodeIds.forEach(nodeId => {
|
||||||
const node = nodes.find(n => n.id === nodeId);
|
const node = nodes.find(n => n.id === nodeId);
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
@@ -363,6 +563,87 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case 'question-classifier':
|
||||||
|
const classNameKey = `${nodeId}_class_name`;
|
||||||
|
const outputKey = `${nodeId}_output`;
|
||||||
|
if (!addedKeys.has(classNameKey)) {
|
||||||
|
addedKeys.add(classNameKey);
|
||||||
|
variableList.push({
|
||||||
|
key: classNameKey,
|
||||||
|
label: 'class_name',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'string',
|
||||||
|
value: `${node.getData().id}.class_name`,
|
||||||
|
nodeData: nodeData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!addedKeys.has(outputKey)) {
|
||||||
|
addedKeys.add(outputKey);
|
||||||
|
variableList.push({
|
||||||
|
key: outputKey,
|
||||||
|
label: 'output',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'string',
|
||||||
|
value: `${node.getData().id}.output`,
|
||||||
|
nodeData: nodeData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'iteration':
|
||||||
|
const iterationOutputKey = `${nodeId}_output`;
|
||||||
|
if (!addedKeys.has(iterationOutputKey)) {
|
||||||
|
addedKeys.add(iterationOutputKey);
|
||||||
|
// Get the data type from the output configuration, default to string
|
||||||
|
const outputConfig = nodeData.output;
|
||||||
|
let outputDataType = 'string';
|
||||||
|
if (outputConfig) {
|
||||||
|
// Find the selected variable from variableList to get its type
|
||||||
|
const selectedVariable = variableList.find(v => v.value === outputConfig);
|
||||||
|
if (selectedVariable) {
|
||||||
|
outputDataType = selectedVariable.dataType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
variableList.push({
|
||||||
|
key: iterationOutputKey,
|
||||||
|
label: 'output',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: outputDataType,
|
||||||
|
value: `${node.getData().id}.output`,
|
||||||
|
nodeData: nodeData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'loop':
|
||||||
|
const cycleVars = nodeData.cycle_vars || [];
|
||||||
|
cycleVars.forEach((cycleVar: any) => {
|
||||||
|
const cycleVarKey = `${nodeId}_cycle_${cycleVar.name}`;
|
||||||
|
if (!addedKeys.has(cycleVarKey)) {
|
||||||
|
addedKeys.add(cycleVarKey);
|
||||||
|
variableList.push({
|
||||||
|
key: cycleVarKey,
|
||||||
|
label: cycleVar.name,
|
||||||
|
type: 'variable',
|
||||||
|
dataType: cycleVar.type || 'string',
|
||||||
|
value: `${node.getData().id}.${cycleVar.name}`,
|
||||||
|
nodeData: nodeData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break
|
||||||
|
case 'tool':
|
||||||
|
const toolDataKey = `${nodeId}_data`;
|
||||||
|
if (!addedKeys.has(toolDataKey)) {
|
||||||
|
addedKeys.add(toolDataKey);
|
||||||
|
variableList.push({
|
||||||
|
key: toolDataKey,
|
||||||
|
label: 'data',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'object',
|
||||||
|
value: `${node.getData().id}.data`,
|
||||||
|
nodeData: nodeData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -388,6 +669,14 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
return variableList;
|
return variableList;
|
||||||
}, [selectedNode, graphRef]);
|
}, [selectedNode, graphRef]);
|
||||||
|
|
||||||
|
// Filter out boolean type variables for loop and llm nodes
|
||||||
|
const getFilteredVariableList = (nodeType?: string) => {
|
||||||
|
if (nodeType === 'loop' || nodeType === 'llm') {
|
||||||
|
return variableList.filter(variable => variable.dataType !== 'boolean');
|
||||||
|
}
|
||||||
|
return variableList;
|
||||||
|
};
|
||||||
|
|
||||||
console.log('values', values)
|
console.log('values', values)
|
||||||
console.log('variableList', variableList, selectedNode?.data)
|
console.log('variableList', variableList, selectedNode?.data)
|
||||||
|
|
||||||
@@ -412,6 +701,8 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
{selectedNode?.data?.type === 'http-request'
|
{selectedNode?.data?.type === 'http-request'
|
||||||
? <HttpRequest
|
? <HttpRequest
|
||||||
options={variableList}
|
options={variableList}
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
graphRef={graphRef}
|
||||||
/>
|
/>
|
||||||
: selectedNode?.data?.type === 'tool'
|
: selectedNode?.data?.type === 'tool'
|
||||||
? <ToolConfig options={variableList} />
|
? <ToolConfig options={variableList} />
|
||||||
@@ -469,7 +760,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
|
|
||||||
if (selectedNode?.data?.type === 'llm' && key === 'messages' && config.type === 'define') {
|
if (selectedNode?.data?.type === 'llm' && key === 'messages' && config.type === 'define') {
|
||||||
// 为llm节点且isArray=true时添加context变量支持
|
// 为llm节点且isArray=true时添加context变量支持
|
||||||
let contextVariableList = [...variableList];
|
let contextVariableList = [...getFilteredVariableList('llm')];
|
||||||
const isArrayMode = config.isArray !== false; // 默认为true
|
const isArrayMode = config.isArray !== false; // 默认为true
|
||||||
|
|
||||||
if (isArrayMode) {
|
if (isArrayMode) {
|
||||||
@@ -491,14 +782,14 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Item key={key} name={key}>
|
<Form.Item key={key} name={key}>
|
||||||
<MessageEditor options={contextVariableList} parentName={key} />
|
<MessageEditor key={key} options={contextVariableList} parentName={key} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (selectedNode?.data?.type === 'end' && key === 'output') {
|
if (selectedNode?.data?.type === 'end' && key === 'output') {
|
||||||
return (
|
return (
|
||||||
<Form.Item key={key} name={key}>
|
<Form.Item key={key} name={key}>
|
||||||
<MessageEditor isArray={false} parentName={key} options={variableList} />
|
<MessageEditor key={key} isArray={false} parentName={key} options={variableList} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -525,7 +816,8 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||||
isArray={!!config.isArray}
|
isArray={!!config.isArray}
|
||||||
parentName={key}
|
parentName={key}
|
||||||
options={variableList}
|
enableJinja2={config.enableJinja2 as boolean}
|
||||||
|
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
@@ -546,7 +838,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
<Form.Item key={key} name={key}>
|
<Form.Item key={key} name={key}>
|
||||||
<GroupVariableList
|
<GroupVariableList
|
||||||
name={key}
|
name={key}
|
||||||
options={variableList}
|
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||||
isCanAdd={!!(values as any)?.group}
|
isCanAdd={!!(values as any)?.group}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -558,7 +850,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
<Form.Item key={key} name={key}>
|
<Form.Item key={key} name={key}>
|
||||||
<CaseList
|
<CaseList
|
||||||
name={key}
|
name={key}
|
||||||
options={variableList}
|
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||||
selectedNode={selectedNode}
|
selectedNode={selectedNode}
|
||||||
graphRef={graphRef}
|
graphRef={graphRef}
|
||||||
/>
|
/>
|
||||||
@@ -571,7 +863,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
<Form.Item key={key} name={key}
|
<Form.Item key={key} name={key}
|
||||||
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||||
>
|
>
|
||||||
<MappingList name={key} options={variableList} />
|
<MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type)} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -581,7 +873,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
<Form.Item key={key} name={key}>
|
<Form.Item key={key} name={key}>
|
||||||
<CycleVarsList
|
<CycleVarsList
|
||||||
parentName={key}
|
parentName={key}
|
||||||
options={variableList}
|
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
@@ -655,9 +947,9 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
findParentLoopIteration(selectedNode.id);
|
findParentLoopIteration(selectedNode.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...variableList, ...loopIterationVars];
|
return [...getFilteredVariableList(selectedNode?.data?.type), ...loopIterationVars];
|
||||||
}
|
}
|
||||||
return variableList;
|
return getFilteredVariableList(selectedNode?.data?.type);
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -678,7 +970,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
|
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||||
: config.type === 'select'
|
: config.type === 'select'
|
||||||
? <Select
|
? <Select
|
||||||
options={config.needTranslation ? config.options?.map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
|
options={config.needTranslation ? (config.options || []).map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
|
||||||
placeholder={t('common.pleaseSelect')}
|
placeholder={t('common.pleaseSelect')}
|
||||||
/>
|
/>
|
||||||
: config.type === 'inputNumber'
|
: config.type === 'inputNumber'
|
||||||
@@ -698,9 +990,10 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
? <VariableSelect
|
? <VariableSelect
|
||||||
placeholder={t('common.pleaseSelect')}
|
placeholder={t('common.pleaseSelect')}
|
||||||
options={(() => {
|
options={(() => {
|
||||||
|
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type);
|
||||||
// Apply filtering if specified in config
|
// Apply filtering if specified in config
|
||||||
if (config.filterNodeTypes || config.filterVariableNames) {
|
if (config.filterNodeTypes || config.filterVariableNames) {
|
||||||
return variableList.filter(variable => {
|
return baseVariableList.filter(variable => {
|
||||||
const nodeTypeMatch = !config.filterNodeTypes ||
|
const nodeTypeMatch = !config.filterNodeTypes ||
|
||||||
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
|
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
|
||||||
const variableNameMatch = !config.filterVariableNames ||
|
const variableNameMatch = !config.filterVariableNames ||
|
||||||
@@ -721,22 +1014,38 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
return nodeData?.cycle === selectedNode.id;
|
return nodeData?.cycle === selectedNode.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
return variableList.filter(variable =>
|
return baseVariableList.filter(variable =>
|
||||||
childNodes.some(node => node.id === variable.nodeData?.id)
|
childNodes.some(node => node.id === variable.nodeData?.id)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return variableList;
|
return baseVariableList;
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
: config.type === 'switch'
|
: config.type === 'switch'
|
||||||
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
|
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
|
||||||
: config.type === 'categoryList'
|
: config.type === 'categoryList'
|
||||||
? <CategoryList parentName={key} selectedNode={selectedNode} graphRef={graphRef} />
|
? <CategoryList parentName={key} selectedNode={selectedNode} graphRef={graphRef} />
|
||||||
: config.type === 'conditionList'
|
: config.type === 'conditionList'
|
||||||
? <ConditionList
|
? <ConditionList
|
||||||
parentName={key}
|
parentName={key}
|
||||||
options={variableList}
|
options={(() => {
|
||||||
|
// For loop nodes, add cycle_vars to condition options
|
||||||
|
if (selectedNode?.data?.type === 'loop') {
|
||||||
|
const cycleVars = values?.cycle_vars || [];
|
||||||
|
const cycleVarSuggestions: Suggestion[] = cycleVars.map((cycleVar: any) => ({
|
||||||
|
key: `${selectedNode.id}_cycle_${cycleVar.name}`,
|
||||||
|
label: cycleVar.name,
|
||||||
|
type: 'variable',
|
||||||
|
dataType: cycleVar.type || 'String',
|
||||||
|
value: `${selectedNode.getData().id}.${cycleVar.name}`,
|
||||||
|
nodeData: selectedNode.getData(),
|
||||||
|
}));
|
||||||
|
return [...getFilteredVariableList(selectedNode?.data?.type), ...cycleVarSuggestions];
|
||||||
|
}
|
||||||
|
return getFilteredVariableList(selectedNode?.data?.type);
|
||||||
|
})()
|
||||||
|
}
|
||||||
selectedNode={selectedNode}
|
selectedNode={selectedNode}
|
||||||
graphRef={graphRef}
|
graphRef={graphRef}
|
||||||
addBtnText={t('workflow.config.addCase')}
|
addBtnText={t('workflow.config.addCase')}
|
||||||
|
|||||||
@@ -300,6 +300,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
config: {
|
config: {
|
||||||
cycle_vars: {
|
cycle_vars: {
|
||||||
type: 'cycleVarsList',
|
type: 'cycleVarsList',
|
||||||
|
defaultValue: []
|
||||||
},
|
},
|
||||||
condition: {
|
condition: {
|
||||||
type: 'conditionList',
|
type: 'conditionList',
|
||||||
@@ -395,12 +396,14 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
},
|
},
|
||||||
retry: {
|
retry: {
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
defaultValue: false
|
defaultValue: {
|
||||||
|
enable: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error_handle: {
|
error_handle: {
|
||||||
type: 'define',
|
type: 'define',
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
method: 'default'
|
method: 'none'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,11 +423,13 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
config: {
|
config: {
|
||||||
mapping: {
|
mapping: {
|
||||||
type: 'mappingList',
|
type: 'mappingList',
|
||||||
defaultValue: []
|
defaultValue: [{name: 'arg1'}]
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
type: 'messageEditor',
|
type: 'messageEditor',
|
||||||
isArray: false,
|
isArray: false,
|
||||||
|
enableJinja2: true,
|
||||||
|
defaultValue: "{{arg1}}"
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,6 +193,27 @@ export const useWorkflowGraph = ({
|
|||||||
nodeConfig.height = newHeight;
|
nodeConfig.height = newHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是http-request节点,检查error_handle.method配置
|
||||||
|
if (type === 'http-request' && (config as any).error_handle?.method === 'branch') {
|
||||||
|
const portAttrs = {
|
||||||
|
circle: {
|
||||||
|
r: 4, magnet: true, stroke: '#155EEF', strokeWidth: 2, fill: '#155EEF', position: { top: 22 }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
nodeConfig.ports = {
|
||||||
|
groups: {
|
||||||
|
right: { position: 'right', attrs: portAttrs },
|
||||||
|
left: { position: 'left', attrs: portAttrs },
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{ group: 'left' },
|
||||||
|
{ group: 'right', id: 'right' },
|
||||||
|
{ group: 'right', id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), fontSize: 12, fill: '#5B6167' }}}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return nodeConfig
|
return nodeConfig
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -284,6 +305,14 @@ export const useWorkflowGraph = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是http-request节点且有label,根据label匹配对应的端口
|
||||||
|
if (sourceCell.getData()?.type === 'http-request' && label) {
|
||||||
|
const matchingPort = sourcePorts.find((port: any) => port.id === label);
|
||||||
|
if (matchingPort) {
|
||||||
|
sourcePort = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const edgeConfig = {
|
const edgeConfig = {
|
||||||
source: {
|
source: {
|
||||||
cell: sourceCell.id,
|
cell: sourceCell.id,
|
||||||
@@ -954,6 +983,23 @@ export const useWorkflowGraph = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是http-request节点的右侧端口连线,添加label
|
||||||
|
if (sourceCell?.getData()?.type === 'http-request') {
|
||||||
|
if (sourcePortId === 'ERROR') {
|
||||||
|
return {
|
||||||
|
source: sourceCell.getData().id,
|
||||||
|
target: targetCell?.getData().id,
|
||||||
|
label: 'ERROR',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
source: sourceCell.getData().id,
|
||||||
|
target: targetCell?.getData().id,
|
||||||
|
label: 'SUCCESS',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
source: sourceCell?.getData().id,
|
source: sourceCell?.getData().id,
|
||||||
target: targetCell?.getData().id,
|
target: targetCell?.getData().id,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface NodeConfig {
|
|||||||
|
|
||||||
group_variables?: Array<{ key: string, value: string[] }>
|
group_variables?: Array<{ key: string, value: string[] }>
|
||||||
cycle?: string;
|
cycle?: string;
|
||||||
|
cycle_vars?: Array<{ name: string; type: string; value: string; input_type: string; }>
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user