diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 87a95c40..60c06acf 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -866,7 +866,7 @@ export const en = { minimumRetention: 'Minimum retention (λ_time)', minimumRetentionDesc: 'Controls the minimum retention threshold of memory retention', - forgettingRate: 'Forgetting rate (λ_mem)', + forgettingRate: 'Forgetting rate (λ_mem)', forgettingRateDesc: 'Control the speed of memory forgetting, the higher the value, the faster the forgetting', offset: 'Offset (offset)', offsetDesc: 'The offset of the minimum preservation degree', @@ -934,7 +934,7 @@ export const en = { number: 'Number', checkbox: 'Checkbox', apiVariable: 'API Variable', - + displayName: 'Display Name', maxLength: 'Max Length', required: 'Required', @@ -1765,7 +1765,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re externalInteraction: 'External Interaction', "http-request": 'HTTP Request', tool: 'Tools', - code_execution: 'Code Execution', + code: 'Code Execution', "jinja-render": 'Template Rendering', cognitiveUpgrading: 'Cognitive Upgrading (Innovation)', 'memory-read': 'Memory Retrieval', @@ -1858,6 +1858,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re 'array[number]': 'Array[Number]', 'array[boolean]': 'Array[Boolean]', 'array[object]': 'Array[Object]', + 'object': 'Object', addParams: 'Add Extract Variable', promptPlaceholder: 'Write prompts here, type "{" to insert variables, type "insert" to insert', }, @@ -1962,6 +1963,12 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re config_id: 'Memory Configuration', search_switch: 'Search Mode', }, + + 'code': { + input_variables: 'Input Variables', + output_variables: 'Output Variables', + refreshTip: '同步函数签名至代码', + }, name: 'Key', type: 'Type', value: 'Value', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index fc683a66..76a95da4 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1609,11 +1609,6 @@ export const zh = { loadingEmpty: '内容正在加载中…', loadingEmptyDesc: '您的内容正在火箭运输中!很快就会降落在您的屏幕上' }, - count: '计数: {{count}}', - increment: '增加', - decrement: '减少', - reset: '重置', - switchLanguage: '切换语言', home: { title: '首页', @@ -1858,7 +1853,7 @@ export const zh = { externalInteraction: '外部交互', "http-request": 'HTTP请求', tool: '工具 (Tool)', - code_execution: '代码执行', + code: '代码执行', "jinja-render": '模板渲染', cognitiveUpgrading: '认知升级(创新)', 'memory-read': '记忆提取', @@ -1952,6 +1947,7 @@ export const zh = { 'array[number]': 'Array[Number]', 'array[boolean]': 'Array[Boolean]', 'array[object]': 'Array[Object]', + 'object': 'Object', addParams: '添加提取变量', promptPlaceholder: '在此处编写提示,输入“{”插入变量,输入“insert”插入', }, @@ -2056,6 +2052,12 @@ export const zh = { config_id: '记忆配置', search_switch: '检索模式', }, + + 'code': { + input_variables: '输入变量', + output_variables: '输出变量', + refreshTip: '同步函数签名至代码', + }, name: '键', type: '类型', value: '值', diff --git a/web/src/views/Workflow/components/Editor/index.tsx b/web/src/views/Workflow/components/Editor/index.tsx index fd3e937b..e37c71de 100644 --- a/web/src/views/Workflow/components/Editor/index.tsx +++ b/web/src/views/Workflow/components/Editor/index.tsx @@ -15,22 +15,24 @@ import CharacterCountPlugin from './plugin/CharacterCountPlugin' import InitialValuePlugin from './plugin/InitialValuePlugin'; import CommandPlugin from './plugin/CommandPlugin'; import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin'; +import Python3HighlightPlugin from './plugin/Python3HighlightPlugin'; +import JavaScriptHighlightPlugin from './plugin/JavaScriptHighlightPlugin'; import LineNumberPlugin from './plugin/LineNumberPlugin'; import BlurPlugin from './plugin/BlurPlugin'; import { VariableNode } from './nodes/VariableNode' -interface LexicalEditorProps { +export interface LexicalEditorProps { placeholder?: string; value?: string; onChange?: (value: string) => void; - options: Suggestion[]; + options?: Suggestion[]; variant?: 'outlined' | 'borderless'; height?: number; fontSize?: number; lineHeight?: number; - enableJinja2?: boolean; size?: 'default' | 'small'; - type?: 'input' | 'textarea' + type?: 'input' | 'textarea', + language?: 'string' | 'jinja2' | 'python3' | 'javascript' } const theme = { @@ -54,20 +56,25 @@ const Editor: FC =({ placeholder = "请输入内容...", value = "", onChange, - options, + options = [], variant = 'borderless', - enableJinja2 = false, size = 'default', - type = 'textarea' + type = 'textarea', + language = 'string' }) => { - const [_count, setCount] = useState(0); + const [enableJinja2, setEnableJinja2] = useState(false) + const [enableLineNumbers, setEnableLineNumbers] = useState(false) useEffect(() => { - if (enableJinja2) { - const styleId = 'jinja2-styles'; + const needsLineNumbers = language === 'jinja2' || language === 'python3' || language === 'javascript'; + setEnableJinja2(language === 'jinja2'); + setEnableLineNumbers(needsLineNumbers); + + if (needsLineNumbers) { + const styleId = 'code-editor-styles'; let existingStyle = document.getElementById(styleId); - + if (!existingStyle) { const style = document.createElement('style'); style.id = styleId; @@ -119,6 +126,7 @@ const Editor: FC =({ } .editor-content-with-numbers { white-space: pre-wrap; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; } .editor-content-with-numbers p { margin: 0; @@ -128,7 +136,8 @@ const Editor: FC =({ document.head.appendChild(style); } } - }, [enableJinja2]); + }, [language]) + const initialConfig = { namespace: 'AutocompleteEditor', theme: enableJinja2 ? jinja2Theme : theme, @@ -168,7 +177,7 @@ const Editor: FC =({
=({ style={{ minHeight: placeHolderMinheight, position: 'absolute', - top: enableJinja2 ? '4px' : variant === 'borderless' ? '0' : '6px', - left: enableJinja2 ? '16px' : (variant === 'borderless' ? '0' : '11px'), + top: enableLineNumbers ? '4px' : variant === 'borderless' ? '0' : '6px', + left: enableLineNumbers ? '16px' : (variant === 'borderless' ? '0' : '11px'), color: '#A8A9AA', fontSize: fontSize, lineHeight: placeHolderMinheight, @@ -227,12 +236,14 @@ const Editor: FC =({ /> - {enableJinja2 && } - {enableJinja2 && } + {language === 'jinja2' && } + {language === 'python3' && } + {language === 'javascript' && } + {enableLineNumbers && } { setCount(count) }} onChange={onChange} /> - {enableJinja2 && } + {enableLineNumbers && }
); diff --git a/web/src/views/Workflow/components/Editor/plugin/JavaScriptHighlightPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/JavaScriptHighlightPlugin.tsx new file mode 100644 index 00000000..90053646 --- /dev/null +++ b/web/src/views/Workflow/components/Editor/plugin/JavaScriptHighlightPlugin.tsx @@ -0,0 +1,164 @@ +import { useEffect } from 'react'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { TextNode, $createTextNode, $getSelection, $isRangeSelection } from 'lexical'; + +const JS_KEYWORDS = new Set([ + 'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', + 'delete', 'do', 'else', 'export', 'extends', 'finally', 'for', 'function', 'if', 'import', + 'in', 'instanceof', 'let', 'new', 'return', 'super', 'switch', 'this', 'throw', 'try', + 'typeof', 'var', 'void', 'while', 'with', 'yield', 'true', 'false', 'null', 'undefined' +]); + +const JavaScriptHighlightPlugin = () => { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + return editor.registerNodeTransform(TextNode, (textNode: TextNode) => { + const text = textNode.getTextContent(); + + if (textNode.hasFormat('code')) return; + if (!needsHighlight(text)) return; + + const parent = textNode.getParent(); + if (!parent) return; + + const selection = $getSelection(); + let selectionOffset = null; + if ($isRangeSelection(selection)) { + const anchor = selection.anchor; + if (anchor.getNode() === textNode) { + selectionOffset = anchor.offset; + } + } + + const tokens = tokenizeJavaScript(text); + if (tokens.length <= 1) return; + + const newNodes = tokens.map(token => { + const newNode = $createTextNode(token.text); + newNode.toggleFormat('code'); + + switch (token.type) { + case 'keyword': + newNode.setStyle('color: #d73a49; font-weight: 600;'); + break; + case 'string': + newNode.setStyle('color: #032f62;'); + break; + case 'comment': + newNode.setStyle('color: #6a737d; font-style: italic;'); + break; + case 'number': + newNode.setStyle('color: #005cc5; font-weight: 500;'); + break; + case 'function': + newNode.setStyle('color: #6f42c1; font-weight: 500;'); + break; + } + + return newNode; + }); + + if (newNodes.length > 1) { + textNode.replace(newNodes[0]); + for (let i = 1; i < newNodes.length; i++) { + newNodes[i - 1].insertAfter(newNodes[i]); + } + + if (selectionOffset !== null && $isRangeSelection(selection)) { + let currentOffset = 0; + for (const node of newNodes) { + const nodeLength = node.getTextContent().length; + if (currentOffset + nodeLength >= selectionOffset) { + node.select(selectionOffset - currentOffset, selectionOffset - currentOffset); + break; + } + currentOffset += nodeLength; + } + } + } + }); + }, [editor]); + + return null; +}; + +function needsHighlight(text: string): boolean { + return /[a-zA-Z0-9_/"'`]/.test(text); +} + +function tokenizeJavaScript(text: string): Array<{text: string, type: string}> { + const tokens: Array<{text: string, type: string}> = []; + let i = 0; + + while (i < text.length) { + // Single-line comments + if (text.slice(i, i + 2) === '//') { + let start = i; + while (i < text.length && text[i] !== '\n') i++; + tokens.push({ text: text.slice(start, i), type: 'comment' }); + continue; + } + + // Multi-line comments + if (text.slice(i, i + 2) === '/*') { + let start = i; + i += 2; + while (i < text.length && text.slice(i, i + 2) !== '*/') i++; + if (i < text.length) i += 2; + tokens.push({ text: text.slice(start, i), type: 'comment' }); + continue; + } + + // Strings + if (text[i] === '"' || text[i] === "'" || text[i] === '`') { + const quote = text[i]; + let start = i++; + + while (i < text.length) { + if (text[i] === quote && text[i - 1] !== '\\') { + i++; + break; + } + i++; + } + tokens.push({ text: text.slice(start, i), type: 'string' }); + continue; + } + + // Numbers + if (/\d/.test(text[i])) { + let start = i; + while (i < text.length && /[\d.]/.test(text[i])) i++; + tokens.push({ text: text.slice(start, i), type: 'number' }); + continue; + } + + // Keywords and identifiers + if (/[a-zA-Z_$]/.test(text[i])) { + let start = i; + while (i < text.length && /[a-zA-Z0-9_$]/.test(text[i])) i++; + const word = text.slice(start, i); + + if (JS_KEYWORDS.has(word)) { + tokens.push({ text: word, type: 'keyword' }); + } else if (i < text.length && text[i] === '(') { + tokens.push({ text: word, type: 'function' }); + } else { + tokens.push({ text: word, type: 'text' }); + } + continue; + } + + // Other characters + let start = i; + while (i < text.length && !/[a-zA-Z0-9_$/"'`]/.test(text[i])) i++; + if (start < i) { + tokens.push({ text: text.slice(start, i), type: 'text' }); + } + } + + return tokens; +} + +export default JavaScriptHighlightPlugin; diff --git a/web/src/views/Workflow/components/Editor/plugin/Python3HighlightPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/Python3HighlightPlugin.tsx new file mode 100644 index 00000000..387160ed --- /dev/null +++ b/web/src/views/Workflow/components/Editor/plugin/Python3HighlightPlugin.tsx @@ -0,0 +1,159 @@ +import { useEffect } from 'react'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { TextNode, $createTextNode, $getSelection, $isRangeSelection } from 'lexical'; + +const PYTHON_KEYWORDS = new Set([ + 'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', + 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', + 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', + 'with', 'yield' +]); + +const Python3HighlightPlugin = () => { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + return editor.registerNodeTransform(TextNode, (textNode: TextNode) => { + const text = textNode.getTextContent(); + + if (textNode.hasFormat('code')) return; + if (!needsHighlight(text)) return; + + const parent = textNode.getParent(); + if (!parent) return; + + const selection = $getSelection(); + let selectionOffset = null; + if ($isRangeSelection(selection)) { + const anchor = selection.anchor; + if (anchor.getNode() === textNode) { + selectionOffset = anchor.offset; + } + } + + const tokens = tokenizePython(text); + if (tokens.length <= 1) return; + + const newNodes = tokens.map(token => { + const newNode = $createTextNode(token.text); + newNode.toggleFormat('code'); + + switch (token.type) { + case 'keyword': + newNode.setStyle('color: #d73a49; font-weight: 600;'); + break; + case 'string': + newNode.setStyle('color: #032f62;'); + break; + case 'comment': + newNode.setStyle('color: #6a737d; font-style: italic;'); + break; + case 'number': + newNode.setStyle('color: #005cc5; font-weight: 500;'); + break; + case 'function': + newNode.setStyle('color: #6f42c1; font-weight: 500;'); + break; + } + + return newNode; + }); + + if (newNodes.length > 1) { + textNode.replace(newNodes[0]); + for (let i = 1; i < newNodes.length; i++) { + newNodes[i - 1].insertAfter(newNodes[i]); + } + + if (selectionOffset !== null && $isRangeSelection(selection)) { + let currentOffset = 0; + for (const node of newNodes) { + const nodeLength = node.getTextContent().length; + if (currentOffset + nodeLength >= selectionOffset) { + node.select(selectionOffset - currentOffset, selectionOffset - currentOffset); + break; + } + currentOffset += nodeLength; + } + } + } + }); + }, [editor]); + + return null; +}; + +function needsHighlight(text: string): boolean { + return /[a-zA-Z0-9_#"']/.test(text); +} + +function tokenizePython(text: string): Array<{text: string, type: string}> { + const tokens: Array<{text: string, type: string}> = []; + let i = 0; + + while (i < text.length) { + // Comments + if (text[i] === '#') { + let start = i; + while (i < text.length && text[i] !== '\n') i++; + tokens.push({ text: text.slice(start, i), type: 'comment' }); + continue; + } + + // Strings + if (text[i] === '"' || text[i] === "'") { + const quote = text[i]; + let start = i++; + const isTriple = text.slice(start, start + 3) === quote.repeat(3); + if (isTriple) i += 2; + + while (i < text.length) { + if (isTriple && text.slice(i, i + 3) === quote.repeat(3)) { + i += 3; + break; + } else if (!isTriple && text[i] === quote && text[i - 1] !== '\\') { + i++; + break; + } + i++; + } + tokens.push({ text: text.slice(start, i), type: 'string' }); + continue; + } + + // Numbers + if (/\d/.test(text[i])) { + let start = i; + while (i < text.length && /[\d.]/.test(text[i])) i++; + tokens.push({ text: text.slice(start, i), type: 'number' }); + continue; + } + + // Keywords and identifiers + if (/[a-zA-Z_]/.test(text[i])) { + let start = i; + while (i < text.length && /[a-zA-Z0-9_]/.test(text[i])) i++; + const word = text.slice(start, i); + + if (PYTHON_KEYWORDS.has(word)) { + tokens.push({ text: word, type: 'keyword' }); + } else if (i < text.length && text[i] === '(') { + tokens.push({ text: word, type: 'function' }); + } else { + tokens.push({ text: word, type: 'text' }); + } + continue; + } + + // Other characters + let start = i; + while (i < text.length && !/[a-zA-Z0-9_#"']/.test(text[i])) i++; + if (start < i) { + tokens.push({ text: text.slice(start, i), type: 'text' }); + } + } + + return tokens; +} + +export default Python3HighlightPlugin; diff --git a/web/src/views/Workflow/components/Properties/CodeExecution/OutputList.tsx b/web/src/views/Workflow/components/Properties/CodeExecution/OutputList.tsx new file mode 100644 index 00000000..8be8d97e --- /dev/null +++ b/web/src/views/Workflow/components/Properties/CodeExecution/OutputList.tsx @@ -0,0 +1,86 @@ +import { type FC, type ReactNode } from 'react'; +import { useTranslation } from 'react-i18next' +import { Button, Form, Input, Divider, Space, Select } from 'antd'; + +interface OutputListProps { + label: string; + name: string; + extra?: ReactNode; +} + +const types = [ + 'string', + 'number', + 'boolean', + 'array[string]', + 'array[number]', + 'array[boolean]', + 'array[object]', + 'object' +] +const OutputList: FC = ({ label, name, extra }) => { + const { t } = useTranslation() + return ( + <> + + {(fields, { add, remove }) => ( + <> +
+
+ {label} +
+ + + {extra} + + +
+ {fields.map(({ key, name, ...restField }) => ( +
+ + + + + + + + + + + + + + + + + + + ) +} + +export default CodeExecution diff --git a/web/src/views/Workflow/components/Properties/HttpRequest/EditableTable.tsx b/web/src/views/Workflow/components/Properties/HttpRequest/EditableTable.tsx index 671ae074..d1383f45 100644 --- a/web/src/views/Workflow/components/Properties/HttpRequest/EditableTable.tsx +++ b/web/src/views/Workflow/components/Properties/HttpRequest/EditableTable.tsx @@ -144,6 +144,7 @@ const EditableTable: React.FC = ({ icon={block ? undefined : } onClick={() => add(createNewRow())} size="small" + block={block} className={block ? "rb:mt-1 rb:text-[12px]! rb:bg-transparent!" : "rb:text-[12px]!"} > {block && `+${t('common.add')}`} @@ -155,7 +156,7 @@ const EditableTable: React.FC = ({ {title && (
{title}
- +
)} diff --git a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx index 7fcd333e..a6b50e33 100644 --- a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx +++ b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx @@ -196,6 +196,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an placeholder={t('common.pleaseSelect')} options={options.filter(vo => vo.dataType.includes('file'))} filterBooleanType={true} + size="small" /> } diff --git a/web/src/views/Workflow/components/Properties/JinjaRender/index.tsx b/web/src/views/Workflow/components/Properties/JinjaRender/index.tsx index d1a392ae..7b466310 100644 --- a/web/src/views/Workflow/components/Properties/JinjaRender/index.tsx +++ b/web/src/views/Workflow/components/Properties/JinjaRender/index.tsx @@ -175,7 +175,7 @@ const JinjaRender: FC = ({ selectedNode, options, templateOpti return ( <> - + @@ -184,7 +184,7 @@ const JinjaRender: FC = ({ selectedNode, options, templateOpti title={t('workflow.config.jinja-render.template')} isArray={false} parentName="template" - enableJinja2={true} + language="jinja2" options={templateOptions} titleVariant="borderless" size="small" diff --git a/web/src/views/Workflow/components/Properties/MappingList/index.tsx b/web/src/views/Workflow/components/Properties/MappingList/index.tsx index 4da1f3c3..d0f56e1c 100644 --- a/web/src/views/Workflow/components/Properties/MappingList/index.tsx +++ b/web/src/views/Workflow/components/Properties/MappingList/index.tsx @@ -1,14 +1,17 @@ -import React from 'react'; +import { type FC, type ReactNode } from 'react'; import { useTranslation } from 'react-i18next' -import { Button, Form, Input, Divider } from 'antd'; +import { Button, Form, Input, Divider, Space } from 'antd'; import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import VariableSelect from '../VariableSelect' interface MappingListProps { + label: string; name: string; options: Suggestion[]; + extra?: ReactNode; + valueKey?: string; } -const MappingList: React.FC = ({ name, options }) => { +const MappingList: FC = ({ label, name, options, extra, valueKey = 'value' }) => { const { t } = useTranslation() return ( <> @@ -17,16 +20,19 @@ const MappingList: React.FC = ({ name, options }) => { <>
- {t('workflow.config.jinja-render.mapping')} + {label}
- + + {extra} + +
{fields.map(({ key, name, ...restField }) => (
@@ -43,7 +49,7 @@ const MappingList: React.FC = ({ name, options }) => { void; size?: 'small' | 'default' } @@ -29,8 +29,8 @@ const MessageEditor: FC = ({ isArray = true, parentName = 'messages', placeholder, - options, - enableJinja2 = false, + options = [], + language, size = 'default' }) => { const { t } = useTranslation() @@ -81,13 +81,15 @@ const MessageEditor: FC = ({ -
{title ?? t('workflow.answerDesc')}
+ : title}
- +
); @@ -132,7 +134,7 @@ const MessageEditor: FC = ({ )} - + ); diff --git a/web/src/views/Workflow/components/Properties/hooks/useVariableList.ts b/web/src/views/Workflow/components/Properties/hooks/useVariableList.ts index 37574f75..11d91d98 100644 --- a/web/src/views/Workflow/components/Properties/hooks/useVariableList.ts +++ b/web/src/views/Workflow/components/Properties/hooks/useVariableList.ts @@ -68,7 +68,7 @@ const processNodeVariables = ( if (p?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${p.name}`, p.name, p.type || 'string', `${dataNodeId}.${p.name}`, nodeData); }); break; - + case 'var-aggregator': if (config.group.defaultValue) { (config.group_variables.defaultValue || []).forEach((gv: any) => { @@ -106,6 +106,11 @@ const processNodeVariables = ( if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData); }); break; + case 'code': + (config.output_variables.defaultValue || []).forEach((cv: any) => { + if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData); + }); + break; } }; diff --git a/web/src/views/Workflow/components/Properties/index.tsx b/web/src/views/Workflow/components/Properties/index.tsx index 38fd3005..aa757275 100644 --- a/web/src/views/Workflow/components/Properties/index.tsx +++ b/web/src/views/Workflow/components/Properties/index.tsx @@ -26,9 +26,10 @@ import MemoryConfig from './MemoryConfig' import VariableList from './VariableList' import { useVariableList, getCurrentNodeVariables, getChildNodeVariables } from './hooks/useVariableList' import styles from './properties.module.css' -import Editor from "../Editor"; +import Editor, { type LexicalEditorProps } from "../Editor"; import RbSlider from './RbSlider' import JinjaRender from './JinjaRender' +import CodeExecution from './CodeExecution' interface PropertiesProps { selectedNode?: Node | null; @@ -364,6 +365,11 @@ const Properties: FC = ({ options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')} templateOptions={getFilteredVariableList(selectedNode?.data?.type, 'template')} /> + : selectedNode?.data?.type === 'code' + ? : configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => { const config = configs[key] || {} @@ -438,7 +444,7 @@ const Properties: FC = ({ title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)} isArray={!!config.isArray} parentName={key} - enableJinja2={config.enableJinja2 as boolean} + language={config.language as LexicalEditorProps['language']} options={getFilteredVariableList(selectedNode?.data?.type, key)} titleVariant={config.titleVariant} size="small" diff --git a/web/src/views/Workflow/components/Properties/properties.module.css b/web/src/views/Workflow/components/Properties/properties.module.css index 292a13e4..4820788f 100644 --- a/web/src/views/Workflow/components/Properties/properties.module.css +++ b/web/src/views/Workflow/components/Properties/properties.module.css @@ -87,4 +87,7 @@ .properties :global(.ant-select .ant-select-arrow) { font-size: 10px; inset-inline-end: 6px; +} +.properties :global(.ant-input-sm) { + padding: 3.6px 7px; } \ No newline at end of file diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts index f528b9df..2d09159d 100644 --- a/web/src/views/Workflow/constant.ts +++ b/web/src/views/Workflow/constant.ts @@ -284,7 +284,7 @@ export const nodeLibrary: NodeLibrary[] = [ config: { input: { type: 'variableList', - filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop', 'parameter-extractor'], + filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop', 'parameter-extractor', 'code'], filterVariableNames: ['message'] }, parallel: { @@ -431,7 +431,32 @@ export const nodeLibrary: NodeLibrary[] = [ } } }, - // { type: "code_execution", icon: codeExecutionIcon }, + { type: "code", icon: codeExecutionIcon, + config: { + input_variables: { + type: 'inputList', + defaultValue: [{ name: 'arg1' }, { name: 'arg2' }] + }, + language: { + type: 'select', + defaultValue: 'python3' + }, + code: { + type: 'messageEditor', + isArray: false, + language: ['python3', 'javascript'], + titleVariant: 'borderless', + defaultValue: `def main(arg1: str, arg2: str): + return { + "result": arg1 + arg2, + }` + }, + output_variables: { + type: 'outputList', + defaultValue: [{name: 'result', type: 'string'}] + }, + } + }, { type: "jinja-render", icon: templateRenderingIcon, config: { mapping: { @@ -441,12 +466,12 @@ export const nodeLibrary: NodeLibrary[] = [ template: { type: 'messageEditor', isArray: false, - enableJinja2: true, + language: 'jinja2', titleVariant: 'borderless', defaultValue: "{{arg1}}" }, } - } + }, ] }, // { diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index 0cc69fea..48cd6652 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -109,6 +109,12 @@ export const useWorkflowGraph = ({ : group_variables } else if (type === 'http-request' && (key === 'headers' || key === 'params') && config[key] && typeof config[key] === 'object' && !Array.isArray(config[key]) && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) { nodeLibraryConfig.config[key].defaultValue = Object.entries(config[key]).map(([name, value]) => ({ name, value })) + } else if (type === 'code' && key === 'code' && config[key] && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) { + try { + nodeLibraryConfig.config[key].defaultValue = decodeURIComponent(atob(config[key] as string)) + } catch { + nodeLibraryConfig.config[key].defaultValue = config[key] + } } else if (nodeLibraryConfig.config && nodeLibraryConfig.config[key] && config[key]) { nodeLibraryConfig.config[key].defaultValue = config[key] } @@ -588,77 +594,6 @@ export const useWorkflowGraph = ({ graphRef.current.resize(containerRef.current.offsetWidth, containerRef.current.offsetHeight); } }; - - const nodeChangePosition = ({ node, options }: { node: Node; options: { skipParentHandler?: boolean } }) => { - const embedPadding = 50; // Define the embed padding constant - if (options.skipParentHandler) { - return - } - - const children = node.getChildren() - if (children && children.length) { - node.prop('originPosition', node.getPosition()) - } - - const parent = node.getParent() - if (parent && parent.isNode()) { - let originSize = parent.prop('originSize') - if (originSize == null) { - originSize = parent.getSize() - parent.prop('originSize', originSize) - } - - let originPosition = parent.prop('originPosition') - if (originPosition == null) { - originPosition = parent.getPosition() - parent.prop('originPosition', originPosition) - } - - let x = originPosition.x - let y = originPosition.y - let cornerX = originPosition.x + originSize.width - let cornerY = originPosition.y + originSize.height - let hasChange = false - - const children = parent.getChildren() - if (children) { - children.forEach((child) => { - const bbox = child.getBBox().inflate(embedPadding) - const corner = bbox.getCorner() - - if (bbox.x < x) { - x = bbox.x - hasChange = true - } - - if (bbox.y < y) { - y = bbox.y - hasChange = true - } - - if (corner.x > cornerX) { - cornerX = corner.x - hasChange = true - } - - if (corner.y > cornerY) { - cornerY = corner.y - hasChange = true - } - }) - } - - if (hasChange) { - parent.prop( - { - position: { x, y }, - size: { width: cornerX - x, height: cornerY - y }, - }, - { skipParentHandler: true }, - ) - } - } - } // 初始化 const init = () => { @@ -912,7 +847,13 @@ export const useWorkflowGraph = ({ if (data.config) { Object.keys(data.config).forEach(key => { - if (key === 'memory' && data.config[key] && 'defaultValue' in data.config[key]) { + if (data.type === 'code' && key === 'code' && data.config[key] && 'defaultValue' in data.config[key]) { + const code = data.config[key].defaultValue || '' + itemConfig = { + ...itemConfig, + code: btoa(encodeURIComponent(code || '')) + } + } else if (key === 'memory' && data.config[key] && 'defaultValue' in data.config[key]) { const { messages, ...rest } = data.config[key].defaultValue let memoryMessage = { role: 'USER', content: data.config[key].defaultValue.messages } itemConfig = {