Merge pull request #51 from SuanmoSuanyangTechnology/feature/workflow_zy
Feature/workflow zy
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.',
|
||||
|
||||
master: 'Supervisor Mode',
|
||||
master_agent: '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.',
|
||||
handoffs: 'Collaboration Mode',
|
||||
handoffsDesc: 'Multiple Agents collaborate equally, autonomously coordinating according to task requirements, suitable for complex scenarios requiring flexible interaction.',
|
||||
supervisor: 'Supervisor Mode',
|
||||
supervisorDesc: 'Unified scheduling and management by the main Agent, with sub-Agents executing tasks assigned by the supervisor, suitable for scenarios requiring centralized control.',
|
||||
collaboration: 'Collaboration Mode',
|
||||
collaborationDesc: 'Multiple Agents collaborate equally, autonomously coordinating according to task requirements, suitable for complex scenarios requiring flexible interaction.',
|
||||
masterConfig: 'Supervisor Configuration',
|
||||
orchestrationMode: 'Task Assignment Strategy',
|
||||
conditional: 'Intelligent Assignment',
|
||||
@@ -1150,6 +1150,8 @@ export const en = {
|
||||
merge: 'Complete Aggregation',
|
||||
vote: 'Key Information Extraction',
|
||||
priority: 'Structured Integration',
|
||||
addTool: 'Add Tool',
|
||||
tool: 'Tool',
|
||||
},
|
||||
userMemory: {
|
||||
userMemory: 'User Memory',
|
||||
@@ -1207,6 +1209,7 @@ export const en = {
|
||||
IMPLICIT_MEMORY: 'Implicit Memory',
|
||||
EMOTIONAL_MEMORY: 'Emotional Memory',
|
||||
EPISODIC_MEMORY: 'Episodic Memory',
|
||||
FORGETTING_MANAGEMENT: 'Forgetting Management',
|
||||
|
||||
endUserProfile: 'Core Profile',
|
||||
editEndUserProfile: 'Edit',
|
||||
@@ -1839,6 +1842,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
status_code: 'Status Code',
|
||||
max_attempts: 'Max Retry Attempts',
|
||||
retry_interval: 'Retry Interval',
|
||||
errorBranch: 'Error Branch',
|
||||
},
|
||||
'jinja-render': {
|
||||
template: 'Code',
|
||||
|
||||
@@ -626,10 +626,10 @@ export const zh = {
|
||||
promptEmpty: '在左侧描述您的用例,编排预览将在此处显示。',
|
||||
|
||||
master: '主管模式',
|
||||
master_agent: '主管模式',
|
||||
master_agentDesc: '由主 Agent 统一调度和管理,子 Agent 按照主管分配的任务执行,适合需要集中控制的场景。',
|
||||
handoffs: '协作模式',
|
||||
handoffsDesc: '多个 Agent 平等协作,根据任务需求自主协调配合,适合需要灵活互动的复杂场景。',
|
||||
supervisor: '主管模式',
|
||||
supervisorDesc: '由主 Agent 统一调度和管理,子 Agent 按照主管分配的任务执行,适合需要集中控制的场景。',
|
||||
collaboration: '协作模式',
|
||||
collaborationDesc: '多个 Agent 平等协作,根据任务需求自主协调配合,适合需要灵活互动的复杂场景。',
|
||||
masterConfig: '主管配置',
|
||||
orchestrationMode: '任务分配策略',
|
||||
conditional: '智能分配',
|
||||
@@ -639,6 +639,8 @@ export const zh = {
|
||||
merge: '完整汇总',
|
||||
vote: '关键信息提取',
|
||||
priority: '结构化整合',
|
||||
addTool: '添加工具',
|
||||
tool: '工具',
|
||||
},
|
||||
// 角色管理相关翻译
|
||||
role: {
|
||||
@@ -1286,6 +1288,7 @@ export const zh = {
|
||||
IMPLICIT_MEMORY: '隐性记忆',
|
||||
EMOTIONAL_MEMORY: '情绪记忆',
|
||||
EPISODIC_MEMORY: '情景记忆',
|
||||
FORGETTING_MANAGEMENT: '遗忘',
|
||||
|
||||
endUserProfile: '核心档案',
|
||||
editEndUserProfile: '编辑',
|
||||
@@ -1939,6 +1942,7 @@ export const zh = {
|
||||
status_code: '状态码',
|
||||
max_attempts: '最大重试次数',
|
||||
retry_interval: '重试间隔',
|
||||
errorBranch: '异常分支',
|
||||
},
|
||||
'jinja-render': {
|
||||
template: '代码',
|
||||
@@ -2252,5 +2256,12 @@ export const zh = {
|
||||
orderPayInfo: '支付信息',
|
||||
create_time: '创建时间',
|
||||
},
|
||||
forgetDetail: {
|
||||
title: '遗忘管理系统帮助AI智能管理记忆生命周期,通过自动识别低价值记忆、设置遗忘策略和执行定期清理,优化记忆库存储空间,提升检索效率。',
|
||||
overviewTitle: '核心指标概览',
|
||||
totalMemory: '记忆总量',
|
||||
MemoryHealth: '记忆健康度',
|
||||
riskOfForgetting: '遗忘风险',
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import type {
|
||||
MemoryConfig,
|
||||
AiPromptModalRef,
|
||||
Source,
|
||||
ToolModalRef,
|
||||
ToolOption
|
||||
} from './types'
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
@@ -33,7 +32,6 @@ import { memoryConfigListUrl } from '@/api/memory'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import aiPrompt from '@/assets/images/application/aiPrompt.png'
|
||||
import AiPromptModal from './components/AiPromptModal'
|
||||
import ToolModal from './components/ToolModal'
|
||||
import ToolList from './components/ToolList'
|
||||
|
||||
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 [isSave, setIsSave] = useState(false)
|
||||
const initialized = useRef(false)
|
||||
const [toolList, setToolList] = useState<ToolOption[]>([])
|
||||
|
||||
// 初始化完成标记
|
||||
useEffect(() => {
|
||||
@@ -143,6 +142,11 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
if (isSave) return
|
||||
setIsSave(true)
|
||||
}, [values])
|
||||
useEffect(() => {
|
||||
if (!initialized.current) return
|
||||
if (isSave) return
|
||||
setIsSave(true)
|
||||
}, [toolList])
|
||||
|
||||
useEffect(() => {
|
||||
getModels()
|
||||
@@ -294,7 +298,11 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
...(item.config || {})
|
||||
}))
|
||||
} 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)
|
||||
@@ -347,18 +355,6 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
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 (
|
||||
<>
|
||||
{loading && <Spin fullscreen></Spin>}
|
||||
@@ -469,10 +465,6 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
defaultModel={defaultModel}
|
||||
refresh={updatePrompt}
|
||||
/>
|
||||
<ToolModal
|
||||
ref={toolModalRef}
|
||||
refresh={updateTools}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
|
||||
const handleSave = (flag = true) => {
|
||||
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') }))
|
||||
return Promise.resolve()
|
||||
}
|
||||
@@ -140,15 +140,14 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
<Space size={20} direction="vertical" style={{width: '100%'}}>
|
||||
<Card title={t('application.handoffs')}>
|
||||
<Form.Item
|
||||
name={['execution_config', 'routing_mode']}
|
||||
name="orchestration_mode"
|
||||
noStyle
|
||||
>
|
||||
<RadioGroupCard
|
||||
options={['master_agent', 'handoffs'].map((type) => ({
|
||||
options={['supervisor', 'collaboration'].map((type) => ({
|
||||
value: type,
|
||||
label: t(`application.${type}`),
|
||||
labelDesc: t(`application.${type}Desc`),
|
||||
disabled: type === 'handoffs'
|
||||
}))}
|
||||
allowClear={false}
|
||||
/>
|
||||
@@ -192,7 +191,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
))}
|
||||
</Card>
|
||||
|
||||
<Card title={t('application.masterConfig')}>
|
||||
{values?.orchestration_mode !== 'collaboration' && <Card title={t('application.masterConfig')}>
|
||||
<Form.Item
|
||||
label={t('application.model')}
|
||||
required={true}
|
||||
@@ -218,11 +217,11 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
</Row>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="orchestration_mode"
|
||||
name={['execution_config',"sub_agent_execution_mode"]}
|
||||
label={t('application.orchestrationMode')}
|
||||
>
|
||||
<Select
|
||||
options={['conditional', 'sequential', 'parallel'].map((type) => ({
|
||||
options={['sequential', 'parallel'].map((type) => ({
|
||||
value: type,
|
||||
label: t(`application.${type}`),
|
||||
}))}
|
||||
@@ -239,7 +238,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</Card>}
|
||||
</Space>
|
||||
</Form>
|
||||
</Col>
|
||||
|
||||
@@ -90,11 +90,19 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
switch (item.event) {
|
||||
case 'start':
|
||||
currentPromptValueRef.current = ''
|
||||
if (editorRef.current?.clear) {
|
||||
editorRef.current.clear();
|
||||
}
|
||||
break;
|
||||
case 'message':
|
||||
if (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) {
|
||||
setChatList(prev => {
|
||||
@@ -107,6 +115,8 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
|
||||
break;
|
||||
case 'end':
|
||||
setLoading(false)
|
||||
// 流结束时同步表单值
|
||||
form.setFieldsValue({ current_prompt: currentPromptValueRef.current })
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||
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 InitialValuePlugin from './plugin/InitialValuePlugin'
|
||||
import LineBreakPlugin from './plugin/LineBreakPlugin';
|
||||
@@ -12,6 +12,9 @@ import InsertTextPlugin from './plugin/InsertTextPlugin';
|
||||
|
||||
export interface EditorRef {
|
||||
insertText: (text: string) => void;
|
||||
appendText: (text: string) => void;
|
||||
clear: () => void;
|
||||
scrollToBottom: () => void;
|
||||
}
|
||||
|
||||
interface LexicalEditorProps {
|
||||
@@ -46,6 +49,41 @@ const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
|
||||
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]);
|
||||
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
import { type FC, useEffect } from 'react';
|
||||
import { type FC, useEffect, useRef } from 'react';
|
||||
import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
|
||||
// 设置初始值的插件
|
||||
const InitialValuePlugin: FC<{ value?: string }> = ({ value }) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const lastValueRef = useRef<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
// 只有当value真正发生变化时才更新
|
||||
if (lastValueRef.current !== value) {
|
||||
editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const currentText = root.getTextContent();
|
||||
|
||||
// 如果当前内容和新值相同,则不更新
|
||||
if (currentText === (value || '')) {
|
||||
return;
|
||||
}
|
||||
|
||||
root.clear();
|
||||
const paragraph = $createParagraphNode();
|
||||
const textNode = $createTextNode(value);
|
||||
paragraph.append(textNode);
|
||||
root.append(paragraph);
|
||||
if (value) {
|
||||
const paragraph = $createParagraphNode();
|
||||
const textNode = $createTextNode(value);
|
||||
paragraph.append(textNode);
|
||||
root.append(paragraph);
|
||||
} else {
|
||||
// 当value为undefined或空时,创建一个空段落
|
||||
const paragraph = $createParagraphNode();
|
||||
root.append(paragraph);
|
||||
}
|
||||
});
|
||||
lastValueRef.current = 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 { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||
@@ -23,6 +23,7 @@ interface LexicalEditorProps {
|
||||
options: Suggestion[];
|
||||
variant?: 'outlined' | 'borderless';
|
||||
height?: number;
|
||||
enableJinja2?: boolean;
|
||||
}
|
||||
|
||||
const theme = {
|
||||
@@ -33,6 +34,15 @@ const theme = {
|
||||
},
|
||||
};
|
||||
|
||||
const jinja2Theme = {
|
||||
...theme,
|
||||
code: 'jinja2-expression',
|
||||
text: {
|
||||
...theme.text,
|
||||
code: 'jinja2-inline',
|
||||
},
|
||||
};
|
||||
|
||||
const Editor: FC<LexicalEditorProps> =({
|
||||
placeholder = "请输入内容...",
|
||||
value = "",
|
||||
@@ -40,19 +50,62 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
options,
|
||||
variant = 'borderless',
|
||||
height = 60,
|
||||
enableJinja2 = false,
|
||||
}) => {
|
||||
|
||||
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 = {
|
||||
namespace: 'AutocompleteEditor',
|
||||
theme,
|
||||
nodes: [
|
||||
theme: enableJinja2 ? jinja2Theme : theme,
|
||||
nodes: enableJinja2 ? [
|
||||
// 当启用jinja2时,不使用VariableNode,使用普通文本
|
||||
] : [
|
||||
// HeadingNode,
|
||||
// QuoteNode,
|
||||
// ListItemNode,
|
||||
// ListNode,
|
||||
// LinkNode,
|
||||
// CodeNode,
|
||||
VariableNode
|
||||
VariableNode,
|
||||
],
|
||||
onError: (error: Error) => {
|
||||
console.error(error);
|
||||
@@ -96,9 +149,9 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
/>
|
||||
<HistoryPlugin />
|
||||
<CommandPlugin />
|
||||
<AutocompletePlugin options={options} />
|
||||
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||
<InitialValuePlugin value={value} options={options} />
|
||||
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface Suggestion {
|
||||
disabled?: boolean; // 标记是否禁用
|
||||
}
|
||||
|
||||
const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> = ({ options, enableJinja2 = false }) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
@@ -82,7 +82,32 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
||||
}, [editor]);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,14 +8,31 @@ import { type Suggestion } from '../plugin/AutocompletePlugin'
|
||||
interface InitialValuePluginProps {
|
||||
value: string;
|
||||
options?: Suggestion[];
|
||||
enableJinja2?: boolean;
|
||||
}
|
||||
|
||||
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [] }) => {
|
||||
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [], enableJinja2 = false }) => {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const initializedRef = useRef(false);
|
||||
const prevValueRef = useRef<string>('');
|
||||
const isUserInputRef = useRef(false);
|
||||
|
||||
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(() => {
|
||||
const root = $getRoot();
|
||||
root.clear();
|
||||
@@ -28,7 +45,11 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
||||
const contextMatch = part.match(/^\{\{context\}\}$/);
|
||||
const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/);
|
||||
|
||||
// 匹配{{context}}格式
|
||||
if (enableJinja2) {
|
||||
paragraph.append($createTextNode(part));
|
||||
return;
|
||||
}
|
||||
|
||||
if (contextMatch) {
|
||||
const contextSuggestion = options.find(s => s.isContext && s.label === 'context');
|
||||
if (contextSuggestion) {
|
||||
@@ -39,7 +60,6 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
||||
return
|
||||
}
|
||||
|
||||
// 匹配{{conv.xx}}格式
|
||||
if (conversationMatch) {
|
||||
const [_, variableName] = conversationMatch;
|
||||
const conversationSuggestion = options.find(s =>
|
||||
@@ -53,7 +73,6 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
||||
return
|
||||
}
|
||||
|
||||
// 匹配普通变量{{nodeId.label}}格式
|
||||
if (match) {
|
||||
const [_, nodeId, label] = match;
|
||||
|
||||
@@ -75,11 +94,12 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
||||
});
|
||||
|
||||
root.append(paragraph);
|
||||
});
|
||||
|
||||
initializedRef.current = true;
|
||||
}, { discrete: true });
|
||||
}
|
||||
}, [options]);
|
||||
|
||||
prevValueRef.current = value;
|
||||
isUserInputRef.current = false;
|
||||
}, [value, options, editor, enableJinja2]);
|
||||
|
||||
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']) => {
|
||||
if (data) {
|
||||
form.setFieldsValue({
|
||||
const initialValues = {
|
||||
auth: !data.auth_type || data.auth_type === 'none' ? 'none' : 'api_key',
|
||||
auth_type: !data.auth_type || data.auth_type === 'none' ? undefined : data.auth_type,
|
||||
header: data.header,
|
||||
api_key: data.api_key
|
||||
})
|
||||
}
|
||||
form.setFieldValue('auth', initialValues.auth)
|
||||
if (initialValues.auth !== 'none') {
|
||||
setTimeout(() => {
|
||||
form.setFieldsValue(initialValues)
|
||||
}, 1)
|
||||
}
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
@@ -91,6 +97,9 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
<FormItem
|
||||
name="auth"
|
||||
label={t('workflow.config.http-request.authType')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseSelect') }
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
@@ -103,6 +112,9 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
<FormItem
|
||||
name="auth_type"
|
||||
label={t('workflow.config.http-request.authType')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseSelect') }
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Select, Table, Form, type TableProps } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
@@ -6,46 +6,8 @@ import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin';
|
||||
import Empty from '@/components/Empty';
|
||||
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 {
|
||||
key: string;
|
||||
key?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
type?: string;
|
||||
@@ -56,100 +18,158 @@ interface EditableTableProps {
|
||||
title?: string;
|
||||
options?: Suggestion[];
|
||||
typeOptions?: { value: string, label: string }[]
|
||||
filterBooleanType?: boolean;
|
||||
}
|
||||
|
||||
const EditableTable: React.FC<EditableTableProps> = ({
|
||||
parentName,
|
||||
title,
|
||||
options = [],
|
||||
typeOptions = []
|
||||
typeOptions = [],
|
||||
filterBooleanType = false
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
const values = Form.useWatch(typeof parentName === 'string' ? [parentName] : parentName, form);
|
||||
|
||||
const createNewRow = (): TableRow => ({
|
||||
key: Date.now().toString(),
|
||||
name: undefined,
|
||||
value: undefined,
|
||||
...(typeOptions.length > 0 && { type: typeOptions[0].value })
|
||||
});
|
||||
|
||||
const handleAdd = useCallback(() => {
|
||||
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 getColumns = (remove: (index: number) => void): TableProps<TableRow>['columns'] => {
|
||||
const hasType = typeOptions.length > 0;
|
||||
const baseWidth = hasType ? '35%' : '45%';
|
||||
|
||||
return [
|
||||
createColumn('name', 'variableSelect', baseWidth, options),
|
||||
...(hasType ? [createColumn('type', 'select', '20%', typeOptions)] : []),
|
||||
createColumn('value', 'variableSelect', baseWidth, options),
|
||||
{
|
||||
title: t('workflow.config.name'),
|
||||
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: '',
|
||||
dataIndex: 'actions',
|
||||
width: '10%',
|
||||
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 (
|
||||
<div className="rb:mb-4">
|
||||
{title && (
|
||||
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
|
||||
<div className="rb:font-medium">{title}</div>
|
||||
<AddButton />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form.Item name={parentName}>
|
||||
<Table<TableRow>
|
||||
components={{ body: { cell: EditableCell } }}
|
||||
bordered
|
||||
dataSource={values}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
size="small"
|
||||
locale={{ emptyText: <Empty size={88} /> }}
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{!title && <AddButton block />}
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => {
|
||||
const AddButton = ({ block = false }: { block?: boolean }) => (
|
||||
<Button
|
||||
type={block ? "dashed" : "text"}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => add(createNewRow())}
|
||||
size="small"
|
||||
block={block}
|
||||
className={block ? "rb:mt-1" : ""}
|
||||
>
|
||||
{block && `+${t('common.add')}`}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{title && (
|
||||
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
|
||||
<div className="rb:font-medium">{title}</div>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,8 +9,10 @@ import VariableSelect from "../VariableSelect";
|
||||
import MessageEditor from '../MessageEditor'
|
||||
import EditableTable from './EditableTable'
|
||||
|
||||
const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: any; }> = ({
|
||||
options,
|
||||
selectedNode,
|
||||
graphRef
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance();
|
||||
@@ -22,18 +24,45 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
}
|
||||
const handleRefresh = (auth: HttpRequestConfigForm['auth']) => {
|
||||
console.log('handleRefresh', auth)
|
||||
form.setFieldsValue({ auth: {...auth} })
|
||||
form.setFieldsValue({ auth })
|
||||
}
|
||||
|
||||
const handleChangeBodyContentType = (contentType: string) => {
|
||||
const currentValues = form.getFieldsValue()
|
||||
const handleChangeBodyContentType = () => {
|
||||
form.setFieldValue(['body', 'data'], undefined)
|
||||
}
|
||||
|
||||
const handleChangeErrorHandleMethod = (method: string) => {
|
||||
form.setFieldsValue({
|
||||
body: {
|
||||
...currentValues?.body,
|
||||
content_type: contentType,
|
||||
data: undefined
|
||||
error_handle: {
|
||||
method,
|
||||
body: 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)
|
||||
@@ -73,6 +102,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
parentName="headers"
|
||||
title="HEADERS"
|
||||
options={options}
|
||||
filterBooleanType={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -81,6 +111,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
parentName="params"
|
||||
title="PARAMS"
|
||||
options={options}
|
||||
filterBooleanType={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -104,6 +135,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
<EditableTable
|
||||
parentName={['body', 'data']}
|
||||
options={options}
|
||||
filterBooleanType={true}
|
||||
typeOptions={[
|
||||
{ label: 'text', value: 'text' },
|
||||
{ label: 'file', value: 'file' }
|
||||
@@ -116,12 +148,15 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
<EditableTable
|
||||
parentName={['body', 'data']}
|
||||
options={options}
|
||||
filterBooleanType={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
{values?.body?.content_type === 'json' &&
|
||||
<Form.Item name={['body', 'data']}>
|
||||
<MessageEditor
|
||||
key="json"
|
||||
parentName={['body', 'data']}
|
||||
options={options}
|
||||
isArray={false}
|
||||
title="JSON"
|
||||
@@ -131,6 +166,8 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
{values?.body?.content_type === 'raw' &&
|
||||
<Form.Item name={['body', 'data']}>
|
||||
<MessageEditor
|
||||
key="raw"
|
||||
parentName={['body', 'data']}
|
||||
options={options}
|
||||
isArray={false}
|
||||
title="RAW TEXT"
|
||||
@@ -141,6 +178,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
<Form.Item name={['body', 'data']}>
|
||||
<VariableSelect
|
||||
options={options}
|
||||
filterBooleanType={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
@@ -185,7 +223,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
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!" />
|
||||
</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')}>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
onChange={handleChangeErrorHandleMethod}
|
||||
options={[
|
||||
{ value: 'none', label: t('workflow.config.http-request.none') },
|
||||
{ value: 'default', label: t('workflow.config.http-request.default') },
|
||||
@@ -207,32 +246,19 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({
|
||||
<>
|
||||
<Form.Item
|
||||
name={['error_handle', 'body']}
|
||||
label="body"
|
||||
label={<>body <span className="rb:text-[#5B6167] rb:ml-1">string</span></>}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
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!" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['error_handle', 'headers']}
|
||||
label="headers"
|
||||
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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
label={<>headers <span className="rb:text-[#5B6167] rb:ml-1">object</span></>}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -17,14 +17,14 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row gutter={12} className="rb:mb-2">
|
||||
<Row key={key} gutter={12} className="rb:mb-2">
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'name']}
|
||||
noStyle
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
<Input placeholder={t('common.pleaseEnter')} data-field-type="mapping-name" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
|
||||
@@ -5,14 +5,15 @@ import { MinusCircleOutlined } from '@ant-design/icons';
|
||||
import Editor from '../Editor'
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
interface TextareaProps {
|
||||
interface MessageEditor {
|
||||
options: Suggestion[];
|
||||
title?: string
|
||||
isArray?: boolean;
|
||||
parentName?: string;
|
||||
parentName?: string | string[];
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
enableJinja2?: boolean;
|
||||
onChange?: (value?: string) => void;
|
||||
}
|
||||
const roleOptions = [
|
||||
@@ -20,12 +21,13 @@ const roleOptions = [
|
||||
{ label: 'USER', value: 'USER' },
|
||||
{ label: 'ASSISTANT', value: 'ASSISTANT' },
|
||||
]
|
||||
const MessageEditor: FC<TextareaProps> = ({
|
||||
const MessageEditor: FC<MessageEditor> = ({
|
||||
title,
|
||||
isArray = true,
|
||||
parentName = 'messages',
|
||||
placeholder,
|
||||
options,
|
||||
enableJinja2 = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance();
|
||||
@@ -33,10 +35,17 @@ const MessageEditor: FC<TextareaProps> = ({
|
||||
|
||||
// 检查是否已经使用了context变量,将已使用的context设置为disabled
|
||||
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 || '')
|
||||
.join(' ');
|
||||
|
||||
@@ -50,7 +59,11 @@ const MessageEditor: FC<TextareaProps> = ({
|
||||
}, [options, values, parentName, isArray]);
|
||||
|
||||
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';
|
||||
|
||||
add({
|
||||
@@ -61,14 +74,14 @@ const MessageEditor: FC<TextareaProps> = ({
|
||||
|
||||
if (!isArray) {
|
||||
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>
|
||||
<Col span={12}>
|
||||
{title ?? t('workflow.answerDesc')}
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name={parentName} noStyle>
|
||||
<Editor placeholder={placeholder} options={processedOptions} />
|
||||
<Editor enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
);
|
||||
@@ -79,7 +92,11 @@ const MessageEditor: FC<TextareaProps> = ({
|
||||
{(fields, { add, remove }) => (
|
||||
<Space size={12} direction="vertical" className="rb:w-full">
|
||||
{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 (
|
||||
<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>
|
||||
<Form.Item {...restField} name={[name, 'content']} noStyle>
|
||||
<Editor placeholder={placeholder} options={processedOptions} />
|
||||
<Editor enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ interface VariableSelectProps extends SelectProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
allowClear?: boolean;
|
||||
filterBooleanType?: boolean;
|
||||
}
|
||||
|
||||
const VariableSelect: FC<VariableSelectProps> = ({
|
||||
@@ -18,6 +19,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
allowClear = true,
|
||||
onChange,
|
||||
size,
|
||||
filterBooleanType = false,
|
||||
...resetPorps
|
||||
}) => {
|
||||
|
||||
@@ -26,7 +28,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
}
|
||||
const labelRender: LabelRender = (props) => {
|
||||
const { value } = props
|
||||
const filterOption = options.find(vo => `{{${vo.value}}}` === value)
|
||||
const filterOption = filteredOptions.find(vo => `{{${vo.value}}}` === value)
|
||||
|
||||
if (filterOption) {
|
||||
return (
|
||||
@@ -54,7 +56,11 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
}
|
||||
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 nodeId = nodeData.id as string;
|
||||
if (!groups[nodeId]) {
|
||||
@@ -64,12 +70,10 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
return groups;
|
||||
}, {});
|
||||
|
||||
const groupedOptions = Object.entries(groupedSuggestions).map(([nodeId, suggestions]) => ({
|
||||
const groupedOptions = Object.entries(groupedSuggestions).map(([_nodeId, suggestions]) => ({
|
||||
label: suggestions[0].nodeData.name,
|
||||
options: suggestions.map(s => ({ label: s.label, value: `{{${s.value}}}` }))
|
||||
}));
|
||||
|
||||
console.log('groupedOptions', groupedOptions)
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
||||
@@ -45,13 +45,134 @@ const Properties: FC<PropertiesProps> = ({
|
||||
const values = Form.useWatch([], form);
|
||||
const variableModalRef = useRef<VariableEditModalRef>(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(() => {
|
||||
if (selectedNode?.getData()?.id) {
|
||||
form.resetFields()
|
||||
prevMappingNamesRef.current = []
|
||||
prevTemplateVarsRef.current = []
|
||||
lastSyncSourceRef.current = null
|
||||
}
|
||||
}, [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(() => {
|
||||
if (selectedNode && form) {
|
||||
const { type = 'default', name = '', config } = selectedNode.getData() || {}
|
||||
@@ -96,6 +217,8 @@ const Properties: FC<PropertiesProps> = ({
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
|
||||
Object.keys(values).forEach(key => {
|
||||
if (selectedNode.data?.config?.[key]) {
|
||||
// Create a deep copy to avoid reference sharing between nodes
|
||||
@@ -114,7 +237,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
...allRest,
|
||||
})
|
||||
}
|
||||
}, [values, selectedNode])
|
||||
}, [values, selectedNode, form])
|
||||
|
||||
const handleAddVariable = () => {
|
||||
variableModalRef.current?.handleOpen()
|
||||
@@ -190,11 +313,88 @@ const Properties: FC<PropertiesProps> = ({
|
||||
.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 childNodeIds = getChildNodes(selectedNode.id);
|
||||
const parentLoopNode = getParentLoopNode(selectedNode.id);
|
||||
|
||||
console.log('childNodeIds', selectedNode, 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 => {
|
||||
const node = nodes.find(n => n.id === nodeId);
|
||||
if (!node) return;
|
||||
@@ -363,6 +563,87 @@ const Properties: FC<PropertiesProps> = ({
|
||||
});
|
||||
}
|
||||
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;
|
||||
}, [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('variableList', variableList, selectedNode?.data)
|
||||
|
||||
@@ -412,6 +701,8 @@ const Properties: FC<PropertiesProps> = ({
|
||||
{selectedNode?.data?.type === 'http-request'
|
||||
? <HttpRequest
|
||||
options={variableList}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
/>
|
||||
: selectedNode?.data?.type === 'tool'
|
||||
? <ToolConfig options={variableList} />
|
||||
@@ -469,7 +760,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
if (selectedNode?.data?.type === 'llm' && key === 'messages' && config.type === 'define') {
|
||||
// 为llm节点且isArray=true时添加context变量支持
|
||||
let contextVariableList = [...variableList];
|
||||
let contextVariableList = [...getFilteredVariableList('llm')];
|
||||
const isArrayMode = config.isArray !== false; // 默认为true
|
||||
|
||||
if (isArrayMode) {
|
||||
@@ -491,14 +782,14 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<MessageEditor options={contextVariableList} parentName={key} />
|
||||
<MessageEditor key={key} options={contextVariableList} parentName={key} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
if (selectedNode?.data?.type === 'end' && key === 'output') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<MessageEditor isArray={false} parentName={key} options={variableList} />
|
||||
<MessageEditor key={key} isArray={false} parentName={key} options={variableList} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
@@ -525,7 +816,8 @@ const Properties: FC<PropertiesProps> = ({
|
||||
title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
isArray={!!config.isArray}
|
||||
parentName={key}
|
||||
options={variableList}
|
||||
enableJinja2={config.enableJinja2 as boolean}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
@@ -546,7 +838,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
<Form.Item key={key} name={key}>
|
||||
<GroupVariableList
|
||||
name={key}
|
||||
options={variableList}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||
isCanAdd={!!(values as any)?.group}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -558,7 +850,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
<Form.Item key={key} name={key}>
|
||||
<CaseList
|
||||
name={key}
|
||||
options={variableList}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
/>
|
||||
@@ -571,7 +863,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
<Form.Item key={key} name={key}
|
||||
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
>
|
||||
<MappingList name={key} options={variableList} />
|
||||
<MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type)} />
|
||||
</Form.Item>
|
||||
|
||||
)
|
||||
@@ -581,7 +873,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
<Form.Item key={key} name={key}>
|
||||
<CycleVarsList
|
||||
parentName={key}
|
||||
options={variableList}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
@@ -655,9 +947,9 @@ const Properties: FC<PropertiesProps> = ({
|
||||
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')} />
|
||||
: config.type === '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')}
|
||||
/>
|
||||
: config.type === 'inputNumber'
|
||||
@@ -698,9 +990,10 @@ const Properties: FC<PropertiesProps> = ({
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={(() => {
|
||||
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type);
|
||||
// Apply filtering if specified in config
|
||||
if (config.filterNodeTypes || config.filterVariableNames) {
|
||||
return variableList.filter(variable => {
|
||||
return baseVariableList.filter(variable => {
|
||||
const nodeTypeMatch = !config.filterNodeTypes ||
|
||||
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
|
||||
const variableNameMatch = !config.filterVariableNames ||
|
||||
@@ -721,22 +1014,38 @@ const Properties: FC<PropertiesProps> = ({
|
||||
return nodeData?.cycle === selectedNode.id;
|
||||
});
|
||||
|
||||
return variableList.filter(variable =>
|
||||
return baseVariableList.filter(variable =>
|
||||
childNodes.some(node => node.id === variable.nodeData?.id)
|
||||
);
|
||||
}
|
||||
return variableList;
|
||||
return baseVariableList;
|
||||
})()
|
||||
}
|
||||
/>
|
||||
: config.type === 'switch'
|
||||
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
|
||||
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
|
||||
: config.type === 'categoryList'
|
||||
? <CategoryList parentName={key} selectedNode={selectedNode} graphRef={graphRef} />
|
||||
: config.type === 'conditionList'
|
||||
? <ConditionList
|
||||
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}
|
||||
graphRef={graphRef}
|
||||
addBtnText={t('workflow.config.addCase')}
|
||||
|
||||
@@ -300,6 +300,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
config: {
|
||||
cycle_vars: {
|
||||
type: 'cycleVarsList',
|
||||
defaultValue: []
|
||||
},
|
||||
condition: {
|
||||
type: 'conditionList',
|
||||
@@ -395,12 +396,14 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
retry: {
|
||||
type: 'switch',
|
||||
defaultValue: false
|
||||
defaultValue: {
|
||||
enable: false
|
||||
}
|
||||
},
|
||||
error_handle: {
|
||||
type: 'define',
|
||||
defaultValue: {
|
||||
method: 'default'
|
||||
method: 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,11 +423,13 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
config: {
|
||||
mapping: {
|
||||
type: 'mappingList',
|
||||
defaultValue: []
|
||||
defaultValue: [{name: 'arg1'}]
|
||||
},
|
||||
template: {
|
||||
type: 'messageEditor',
|
||||
isArray: false,
|
||||
enableJinja2: true,
|
||||
defaultValue: "{{arg1}}"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +193,27 @@ export const useWorkflowGraph = ({
|
||||
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
|
||||
})
|
||||
|
||||
@@ -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 = {
|
||||
source: {
|
||||
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 {
|
||||
source: sourceCell?.getData().id,
|
||||
target: targetCell?.getData().id,
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface NodeConfig {
|
||||
|
||||
group_variables?: Array<{ key: string, value: string[] }>
|
||||
cycle?: string;
|
||||
cycle_vars?: Array<{ name: string; type: string; value: string; input_type: string; }>
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user