Merge pull request #206 from SuanmoSuanyangTechnology/feature/codeNode_zy
feat(web): workflow add code node
This commit is contained in:
@@ -866,7 +866,7 @@ export const en = {
|
|||||||
|
|
||||||
minimumRetention: 'Minimum retention (λ_time)',
|
minimumRetention: 'Minimum retention (λ_time)',
|
||||||
minimumRetentionDesc: 'Controls the minimum retention threshold of memory retention',
|
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',
|
forgettingRateDesc: 'Control the speed of memory forgetting, the higher the value, the faster the forgetting',
|
||||||
offset: 'Offset (offset)',
|
offset: 'Offset (offset)',
|
||||||
offsetDesc: 'The offset of the minimum preservation degree',
|
offsetDesc: 'The offset of the minimum preservation degree',
|
||||||
@@ -934,7 +934,7 @@ export const en = {
|
|||||||
number: 'Number',
|
number: 'Number',
|
||||||
checkbox: 'Checkbox',
|
checkbox: 'Checkbox',
|
||||||
apiVariable: 'API Variable',
|
apiVariable: 'API Variable',
|
||||||
|
|
||||||
displayName: 'Display Name',
|
displayName: 'Display Name',
|
||||||
maxLength: 'Max Length',
|
maxLength: 'Max Length',
|
||||||
required: 'Required',
|
required: 'Required',
|
||||||
@@ -1765,7 +1765,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
externalInteraction: 'External Interaction',
|
externalInteraction: 'External Interaction',
|
||||||
"http-request": 'HTTP Request',
|
"http-request": 'HTTP Request',
|
||||||
tool: 'Tools',
|
tool: 'Tools',
|
||||||
code_execution: 'Code Execution',
|
code: 'Code Execution',
|
||||||
"jinja-render": 'Template Rendering',
|
"jinja-render": 'Template Rendering',
|
||||||
cognitiveUpgrading: 'Cognitive Upgrading (Innovation)',
|
cognitiveUpgrading: 'Cognitive Upgrading (Innovation)',
|
||||||
'memory-read': 'Memory Retrieval',
|
'memory-read': 'Memory Retrieval',
|
||||||
@@ -1858,6 +1858,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
|||||||
'array[number]': 'Array[Number]',
|
'array[number]': 'Array[Number]',
|
||||||
'array[boolean]': 'Array[Boolean]',
|
'array[boolean]': 'Array[Boolean]',
|
||||||
'array[object]': 'Array[Object]',
|
'array[object]': 'Array[Object]',
|
||||||
|
'object': 'Object',
|
||||||
addParams: 'Add Extract Variable',
|
addParams: 'Add Extract Variable',
|
||||||
promptPlaceholder: 'Write prompts here, type "{" to insert variables, type "insert" to insert',
|
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',
|
config_id: 'Memory Configuration',
|
||||||
search_switch: 'Search Mode',
|
search_switch: 'Search Mode',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'code': {
|
||||||
|
input_variables: 'Input Variables',
|
||||||
|
output_variables: 'Output Variables',
|
||||||
|
refreshTip: '同步函数签名至代码',
|
||||||
|
},
|
||||||
name: 'Key',
|
name: 'Key',
|
||||||
type: 'Type',
|
type: 'Type',
|
||||||
value: 'Value',
|
value: 'Value',
|
||||||
|
|||||||
@@ -1609,11 +1609,6 @@ export const zh = {
|
|||||||
loadingEmpty: '内容正在加载中…',
|
loadingEmpty: '内容正在加载中…',
|
||||||
loadingEmptyDesc: '您的内容正在火箭运输中!很快就会降落在您的屏幕上'
|
loadingEmptyDesc: '您的内容正在火箭运输中!很快就会降落在您的屏幕上'
|
||||||
},
|
},
|
||||||
count: '计数: {{count}}',
|
|
||||||
increment: '增加',
|
|
||||||
decrement: '减少',
|
|
||||||
reset: '重置',
|
|
||||||
switchLanguage: '切换语言',
|
|
||||||
|
|
||||||
home: {
|
home: {
|
||||||
title: '首页',
|
title: '首页',
|
||||||
@@ -1858,7 +1853,7 @@ export const zh = {
|
|||||||
externalInteraction: '外部交互',
|
externalInteraction: '外部交互',
|
||||||
"http-request": 'HTTP请求',
|
"http-request": 'HTTP请求',
|
||||||
tool: '工具 (Tool)',
|
tool: '工具 (Tool)',
|
||||||
code_execution: '代码执行',
|
code: '代码执行',
|
||||||
"jinja-render": '模板渲染',
|
"jinja-render": '模板渲染',
|
||||||
cognitiveUpgrading: '认知升级(创新)',
|
cognitiveUpgrading: '认知升级(创新)',
|
||||||
'memory-read': '记忆提取',
|
'memory-read': '记忆提取',
|
||||||
@@ -1952,6 +1947,7 @@ export const zh = {
|
|||||||
'array[number]': 'Array[Number]',
|
'array[number]': 'Array[Number]',
|
||||||
'array[boolean]': 'Array[Boolean]',
|
'array[boolean]': 'Array[Boolean]',
|
||||||
'array[object]': 'Array[Object]',
|
'array[object]': 'Array[Object]',
|
||||||
|
'object': 'Object',
|
||||||
addParams: '添加提取变量',
|
addParams: '添加提取变量',
|
||||||
promptPlaceholder: '在此处编写提示,输入“{”插入变量,输入“insert”插入',
|
promptPlaceholder: '在此处编写提示,输入“{”插入变量,输入“insert”插入',
|
||||||
},
|
},
|
||||||
@@ -2056,6 +2052,12 @@ export const zh = {
|
|||||||
config_id: '记忆配置',
|
config_id: '记忆配置',
|
||||||
search_switch: '检索模式',
|
search_switch: '检索模式',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'code': {
|
||||||
|
input_variables: '输入变量',
|
||||||
|
output_variables: '输出变量',
|
||||||
|
refreshTip: '同步函数签名至代码',
|
||||||
|
},
|
||||||
name: '键',
|
name: '键',
|
||||||
type: '类型',
|
type: '类型',
|
||||||
value: '值',
|
value: '值',
|
||||||
|
|||||||
@@ -15,22 +15,24 @@ import CharacterCountPlugin from './plugin/CharacterCountPlugin'
|
|||||||
import InitialValuePlugin from './plugin/InitialValuePlugin';
|
import InitialValuePlugin from './plugin/InitialValuePlugin';
|
||||||
import CommandPlugin from './plugin/CommandPlugin';
|
import CommandPlugin from './plugin/CommandPlugin';
|
||||||
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
|
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
|
||||||
|
import Python3HighlightPlugin from './plugin/Python3HighlightPlugin';
|
||||||
|
import JavaScriptHighlightPlugin from './plugin/JavaScriptHighlightPlugin';
|
||||||
import LineNumberPlugin from './plugin/LineNumberPlugin';
|
import LineNumberPlugin from './plugin/LineNumberPlugin';
|
||||||
import BlurPlugin from './plugin/BlurPlugin';
|
import BlurPlugin from './plugin/BlurPlugin';
|
||||||
import { VariableNode } from './nodes/VariableNode'
|
import { VariableNode } from './nodes/VariableNode'
|
||||||
|
|
||||||
interface LexicalEditorProps {
|
export interface LexicalEditorProps {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
options: Suggestion[];
|
options?: Suggestion[];
|
||||||
variant?: 'outlined' | 'borderless';
|
variant?: 'outlined' | 'borderless';
|
||||||
height?: number;
|
height?: number;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
lineHeight?: number;
|
lineHeight?: number;
|
||||||
enableJinja2?: boolean;
|
|
||||||
size?: 'default' | 'small';
|
size?: 'default' | 'small';
|
||||||
type?: 'input' | 'textarea'
|
type?: 'input' | 'textarea',
|
||||||
|
language?: 'string' | 'jinja2' | 'python3' | 'javascript'
|
||||||
}
|
}
|
||||||
|
|
||||||
const theme = {
|
const theme = {
|
||||||
@@ -54,20 +56,25 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
placeholder = "请输入内容...",
|
placeholder = "请输入内容...",
|
||||||
value = "",
|
value = "",
|
||||||
onChange,
|
onChange,
|
||||||
options,
|
options = [],
|
||||||
variant = 'borderless',
|
variant = 'borderless',
|
||||||
enableJinja2 = false,
|
|
||||||
size = 'default',
|
size = 'default',
|
||||||
type = 'textarea'
|
type = 'textarea',
|
||||||
|
language = 'string'
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const [_count, setCount] = useState(0);
|
const [_count, setCount] = useState(0);
|
||||||
|
const [enableJinja2, setEnableJinja2] = useState(false)
|
||||||
|
const [enableLineNumbers, setEnableLineNumbers] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (enableJinja2) {
|
const needsLineNumbers = language === 'jinja2' || language === 'python3' || language === 'javascript';
|
||||||
const styleId = 'jinja2-styles';
|
setEnableJinja2(language === 'jinja2');
|
||||||
|
setEnableLineNumbers(needsLineNumbers);
|
||||||
|
|
||||||
|
if (needsLineNumbers) {
|
||||||
|
const styleId = 'code-editor-styles';
|
||||||
let existingStyle = document.getElementById(styleId);
|
let existingStyle = document.getElementById(styleId);
|
||||||
|
|
||||||
if (!existingStyle) {
|
if (!existingStyle) {
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.id = styleId;
|
style.id = styleId;
|
||||||
@@ -119,6 +126,7 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
}
|
}
|
||||||
.editor-content-with-numbers {
|
.editor-content-with-numbers {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||||
}
|
}
|
||||||
.editor-content-with-numbers p {
|
.editor-content-with-numbers p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -128,7 +136,8 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [enableJinja2]);
|
}, [language])
|
||||||
|
|
||||||
const initialConfig = {
|
const initialConfig = {
|
||||||
namespace: 'AutocompleteEditor',
|
namespace: 'AutocompleteEditor',
|
||||||
theme: enableJinja2 ? jinja2Theme : theme,
|
theme: enableJinja2 ? jinja2Theme : theme,
|
||||||
@@ -168,7 +177,7 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<RichTextPlugin
|
<RichTextPlugin
|
||||||
contentEditable={
|
contentEditable={
|
||||||
enableJinja2 ? (
|
enableLineNumbers ? (
|
||||||
<div className="editor-with-line-numbers" style={{
|
<div className="editor-with-line-numbers" style={{
|
||||||
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
@@ -212,8 +221,8 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
style={{
|
style={{
|
||||||
minHeight: placeHolderMinheight,
|
minHeight: placeHolderMinheight,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: enableJinja2 ? '4px' : variant === 'borderless' ? '0' : '6px',
|
top: enableLineNumbers ? '4px' : variant === 'borderless' ? '0' : '6px',
|
||||||
left: enableJinja2 ? '16px' : (variant === 'borderless' ? '0' : '11px'),
|
left: enableLineNumbers ? '16px' : (variant === 'borderless' ? '0' : '11px'),
|
||||||
color: '#A8A9AA',
|
color: '#A8A9AA',
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
lineHeight: placeHolderMinheight,
|
lineHeight: placeHolderMinheight,
|
||||||
@@ -227,12 +236,14 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
/>
|
/>
|
||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<CommandPlugin />
|
<CommandPlugin />
|
||||||
{enableJinja2 && <Jinja2HighlightPlugin />}
|
{language === 'jinja2' && <Jinja2HighlightPlugin />}
|
||||||
{enableJinja2 && <LineNumberPlugin />}
|
{language === 'python3' && <Python3HighlightPlugin />}
|
||||||
|
{language === 'javascript' && <JavaScriptHighlightPlugin />}
|
||||||
|
{enableLineNumbers && <LineNumberPlugin />}
|
||||||
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
||||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||||
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
|
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
|
||||||
{enableJinja2 && <BlurPlugin />}
|
{enableLineNumbers && <BlurPlugin />}
|
||||||
</div>
|
</div>
|
||||||
</LexicalComposer>
|
</LexicalComposer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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<OutputListProps> = ({ label, name, extra }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.List name={name}>
|
||||||
|
{(fields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||||
|
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space size={8}>
|
||||||
|
{extra}
|
||||||
|
<Button
|
||||||
|
onClick={() => add({ type: 'string' })}
|
||||||
|
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
+ {t('workflow.config.addVariable')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
|
<div key={key} className="rb:flex rb:items-center rb:gap-1 rb:mb-2">
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 'name']}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder={t('common.pleaseEnter')}
|
||||||
|
size="small"
|
||||||
|
className="rb:w-45!"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
{...restField}
|
||||||
|
name={[name, 'type']}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder={t('common.pleaseSelect')}
|
||||||
|
options={types.map(key => ({
|
||||||
|
value: key,
|
||||||
|
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||||
|
}))}
|
||||||
|
size="small"
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
className="rb:w-22!"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<div
|
||||||
|
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||||
|
onClick={() => remove(name)}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OutputList;
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Form, Select, Space, Row, Col, Divider, Button, Tooltip } from 'antd'
|
||||||
|
import { Node } from '@antv/x6'
|
||||||
|
|
||||||
|
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||||
|
import MappingList from '../MappingList'
|
||||||
|
import Editor from '../../Editor'
|
||||||
|
import OutputList from './OutputList'
|
||||||
|
|
||||||
|
interface MappingItem {
|
||||||
|
name?: string
|
||||||
|
value?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CodeExecutionProps {
|
||||||
|
options: Suggestion[]
|
||||||
|
selectedNode: Node
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeTemplate = {
|
||||||
|
python3: `def main(arg1: str, arg2: str):
|
||||||
|
return {
|
||||||
|
"result": arg1 + arg2,
|
||||||
|
}`,
|
||||||
|
javascript: `function main({arg1, arg2}) {
|
||||||
|
return {
|
||||||
|
result: arg1 + arg2
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const form = Form.useFormInstance()
|
||||||
|
const values = Form.useWatch([], form) || {}
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
const code = form.getFieldValue('code') || ''
|
||||||
|
const language = form.getFieldValue('language') || 'javascript'
|
||||||
|
const currentInput = form.getFieldValue('input_variables') || []
|
||||||
|
|
||||||
|
// Get input_variables names to replace in code
|
||||||
|
const inputNames = currentInput.map((item: MappingItem) => item.name).filter(Boolean).join(', ')
|
||||||
|
|
||||||
|
let newTemplate = code
|
||||||
|
|
||||||
|
if (language === 'javascript') {
|
||||||
|
// Replace function parameters: function name({arg1, arg2}) or function name(arg1, arg2)
|
||||||
|
newTemplate = code.replace(
|
||||||
|
/function(\s+\w+\s*\(\s*)(\{?)([^})]*)\}?(\s*\))/,
|
||||||
|
(_match: string, prefix: string, brace: string, _params: string, suffix: string) => {
|
||||||
|
return `function${prefix}${brace}${inputNames}${brace ? '}' : ''}${suffix}`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else if (language === 'python3') {
|
||||||
|
// Replace Python function parameters: def name(arg1, arg2):
|
||||||
|
newTemplate = code.replace(
|
||||||
|
/def(\s+\w+\s*\()([^)]*)(\))/,
|
||||||
|
(_match: string, prefix: string, _params: string, suffix: string) => {
|
||||||
|
return `def${prefix}${inputNames}${suffix}`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
form.setFieldValue('code', newTemplate)
|
||||||
|
}
|
||||||
|
const handleChangeLanguage = (value: string) => {
|
||||||
|
form.setFieldValue('code', codeTemplate[value as keyof typeof codeTemplate])
|
||||||
|
form.setFieldsValue({
|
||||||
|
input_variables: [{ name: 'arg1' }, { name: 'arg2' }],
|
||||||
|
code: codeTemplate[value as keyof typeof codeTemplate]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item name="input_variables" noStyle>
|
||||||
|
<MappingList
|
||||||
|
label={t('workflow.config.code.input_variables')}
|
||||||
|
name="input_variables"
|
||||||
|
options={options}
|
||||||
|
valueKey="variable"
|
||||||
|
extra={<Tooltip title={t('workflow.config.code.refreshTip')}>
|
||||||
|
<Button
|
||||||
|
onClick={handleRefresh}
|
||||||
|
className="rb:py-0! rb:px-1.5! rb:text-[12px]! rb:group"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<div onClick={handleRefresh} className="rb:size-3 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/refresh.svg')] rb:group-hover:bg-[url('@/assets/images/refresh_hover.svg')]"></div>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Space size={8} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5">
|
||||||
|
<Row>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item name="language" noStyle>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ label: 'PYTHON3', value: 'python3' },
|
||||||
|
{ label: 'JAVASCRIPT', value: 'javascript' }
|
||||||
|
]}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
className="rb:font-medium!"
|
||||||
|
onChange={handleChangeLanguage}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Form.Item name="code" noStyle>
|
||||||
|
<Editor size="small" language={values.language} />
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<Form.Item name="output_variables" noStyle>
|
||||||
|
<OutputList
|
||||||
|
label={t('workflow.config.code.output_variables')}
|
||||||
|
name="output_variables"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CodeExecution
|
||||||
@@ -144,6 +144,7 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
|||||||
icon={block ? undefined : <PlusOutlined />}
|
icon={block ? undefined : <PlusOutlined />}
|
||||||
onClick={() => add(createNewRow())}
|
onClick={() => add(createNewRow())}
|
||||||
size="small"
|
size="small"
|
||||||
|
block={block}
|
||||||
className={block ? "rb:mt-1 rb:text-[12px]! rb:bg-transparent!" : "rb:text-[12px]!"}
|
className={block ? "rb:mt-1 rb:text-[12px]! rb:bg-transparent!" : "rb:text-[12px]!"}
|
||||||
>
|
>
|
||||||
{block && `+${t('common.add')}`}
|
{block && `+${t('common.add')}`}
|
||||||
@@ -155,7 +156,7 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
|||||||
{title && (
|
{title && (
|
||||||
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
|
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
|
||||||
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">{title}</div>
|
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">{title}</div>
|
||||||
<AddButton block={true} />
|
<AddButton block={false} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
|||||||
placeholder={t('common.pleaseSelect')}
|
placeholder={t('common.pleaseSelect')}
|
||||||
options={options.filter(vo => vo.dataType.includes('file'))}
|
options={options.filter(vo => vo.dataType.includes('file'))}
|
||||||
filterBooleanType={true}
|
filterBooleanType={true}
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item name="mapping" noStyle>
|
<Form.Item name="mapping" noStyle>
|
||||||
<MappingList name="mapping" options={options} />
|
<MappingList label={t('workflow.config.jinja-render.mapping')} name="mapping" options={options} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="template">
|
<Form.Item name="template">
|
||||||
@@ -184,7 +184,7 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
|
|||||||
title={t('workflow.config.jinja-render.template')}
|
title={t('workflow.config.jinja-render.template')}
|
||||||
isArray={false}
|
isArray={false}
|
||||||
parentName="template"
|
parentName="template"
|
||||||
enableJinja2={true}
|
language="jinja2"
|
||||||
options={templateOptions}
|
options={templateOptions}
|
||||||
titleVariant="borderless"
|
titleVariant="borderless"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import React from 'react';
|
import { type FC, type ReactNode } from 'react';
|
||||||
import { useTranslation } from 'react-i18next'
|
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 type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||||
import VariableSelect from '../VariableSelect'
|
import VariableSelect from '../VariableSelect'
|
||||||
|
|
||||||
interface MappingListProps {
|
interface MappingListProps {
|
||||||
|
label: string;
|
||||||
name: string;
|
name: string;
|
||||||
options: Suggestion[];
|
options: Suggestion[];
|
||||||
|
extra?: ReactNode;
|
||||||
|
valueKey?: string;
|
||||||
}
|
}
|
||||||
const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
const MappingList: FC<MappingListProps> = ({ label, name, options, extra, valueKey = 'value' }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -17,16 +20,19 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
|||||||
<>
|
<>
|
||||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||||
{t('workflow.config.jinja-render.mapping')}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Space size={8}>
|
||||||
onClick={() => add()}
|
{extra}
|
||||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
<Button
|
||||||
size="small"
|
onClick={() => add()}
|
||||||
>
|
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||||
+ {t('workflow.config.addVariable')}
|
size="small"
|
||||||
</Button>
|
>
|
||||||
|
+ {t('workflow.config.addVariable')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
{fields.map(({ key, name, ...restField }) => (
|
{fields.map(({ key, name, ...restField }) => (
|
||||||
<div key={key} className="rb:flex rb:items-center rb:gap-1 rb:mb-2">
|
<div key={key} className="rb:flex rb:items-center rb:gap-1 rb:mb-2">
|
||||||
@@ -43,7 +49,7 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
{...restField}
|
{...restField}
|
||||||
name={[name, 'value']}
|
name={[name, valueKey]}
|
||||||
noStyle
|
noStyle
|
||||||
>
|
>
|
||||||
<VariableSelect
|
<VariableSelect
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { type FC, useMemo } from 'react';
|
import { type FC, type ReactNode, useMemo } from 'react';
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd';
|
import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd';
|
||||||
import Editor from '../Editor'
|
import Editor, { type LexicalEditorProps } from '../Editor'
|
||||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||||
|
|
||||||
interface MessageEditor {
|
interface MessageEditor {
|
||||||
options: Suggestion[];
|
options?: Suggestion[];
|
||||||
title?: string;
|
title?: string | ReactNode;
|
||||||
titleVariant?: 'outlined' | 'borderless';
|
titleVariant?: 'outlined' | 'borderless';
|
||||||
isArray?: boolean;
|
isArray?: boolean;
|
||||||
parentName?: string | string[];
|
parentName?: string | string[];
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
enableJinja2?: boolean;
|
language?: LexicalEditorProps['language'];
|
||||||
onChange?: (value?: string) => void;
|
onChange?: (value?: string) => void;
|
||||||
size?: 'small' | 'default'
|
size?: 'small' | 'default'
|
||||||
}
|
}
|
||||||
@@ -29,8 +29,8 @@ const MessageEditor: FC<MessageEditor> = ({
|
|||||||
isArray = true,
|
isArray = true,
|
||||||
parentName = 'messages',
|
parentName = 'messages',
|
||||||
placeholder,
|
placeholder,
|
||||||
options,
|
options = [],
|
||||||
enableJinja2 = false,
|
language,
|
||||||
size = 'default'
|
size = 'default'
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -81,13 +81,15 @@ const MessageEditor: FC<MessageEditor> = ({
|
|||||||
<Space size={8} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5" data-editor-type={parentName === 'template' ? 'template' : undefined}>
|
<Space size={8} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5" data-editor-type={parentName === 'template' ? 'template' : undefined}>
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<div className={clsx("rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2", {
|
{typeof title === 'string'
|
||||||
|
? <div className={clsx("rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2", {
|
||||||
'rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-sm rb:px-2': titleVariant === 'outlined'
|
'rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-sm rb:px-2': titleVariant === 'outlined'
|
||||||
})}>{title ?? t('workflow.answerDesc')}</div>
|
})}>{title ?? t('workflow.answerDesc')}</div>
|
||||||
|
: title}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Form.Item name={parentName} noStyle>
|
<Form.Item name={parentName} noStyle>
|
||||||
<Editor size={size} enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
<Editor size={size} language={language} placeholder={placeholder} options={processedOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
@@ -132,7 +134,7 @@ const MessageEditor: FC<MessageEditor> = ({
|
|||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
<Form.Item {...restField} name={[name, 'content']} noStyle>
|
<Form.Item {...restField} name={[name, 'content']} noStyle>
|
||||||
<Editor size={size} enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
<Editor size={size} language={language} placeholder={placeholder} options={processedOptions} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const processNodeVariables = (
|
|||||||
if (p?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${p.name}`, p.name, p.type || 'string', `${dataNodeId}.${p.name}`, nodeData);
|
if (p?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${p.name}`, p.name, p.type || 'string', `${dataNodeId}.${p.name}`, nodeData);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'var-aggregator':
|
case 'var-aggregator':
|
||||||
if (config.group.defaultValue) {
|
if (config.group.defaultValue) {
|
||||||
(config.group_variables.defaultValue || []).forEach((gv: any) => {
|
(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);
|
if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData);
|
||||||
});
|
});
|
||||||
break;
|
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ import MemoryConfig from './MemoryConfig'
|
|||||||
import VariableList from './VariableList'
|
import VariableList from './VariableList'
|
||||||
import { useVariableList, getCurrentNodeVariables, getChildNodeVariables } from './hooks/useVariableList'
|
import { useVariableList, getCurrentNodeVariables, getChildNodeVariables } from './hooks/useVariableList'
|
||||||
import styles from './properties.module.css'
|
import styles from './properties.module.css'
|
||||||
import Editor from "../Editor";
|
import Editor, { type LexicalEditorProps } from "../Editor";
|
||||||
import RbSlider from './RbSlider'
|
import RbSlider from './RbSlider'
|
||||||
import JinjaRender from './JinjaRender'
|
import JinjaRender from './JinjaRender'
|
||||||
|
import CodeExecution from './CodeExecution'
|
||||||
|
|
||||||
interface PropertiesProps {
|
interface PropertiesProps {
|
||||||
selectedNode?: Node | null;
|
selectedNode?: Node | null;
|
||||||
@@ -364,6 +365,11 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
|
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
|
||||||
templateOptions={getFilteredVariableList(selectedNode?.data?.type, 'template')}
|
templateOptions={getFilteredVariableList(selectedNode?.data?.type, 'template')}
|
||||||
/>
|
/>
|
||||||
|
: selectedNode?.data?.type === 'code'
|
||||||
|
? <CodeExecution
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
|
||||||
|
/>
|
||||||
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
|
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
|
||||||
const config = configs[key] || {}
|
const config = configs[key] || {}
|
||||||
|
|
||||||
@@ -438,7 +444,7 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||||
isArray={!!config.isArray}
|
isArray={!!config.isArray}
|
||||||
parentName={key}
|
parentName={key}
|
||||||
enableJinja2={config.enableJinja2 as boolean}
|
language={config.language as LexicalEditorProps['language']}
|
||||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||||
titleVariant={config.titleVariant}
|
titleVariant={config.titleVariant}
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -87,4 +87,7 @@
|
|||||||
.properties :global(.ant-select .ant-select-arrow) {
|
.properties :global(.ant-select .ant-select-arrow) {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
inset-inline-end: 6px;
|
inset-inline-end: 6px;
|
||||||
|
}
|
||||||
|
.properties :global(.ant-input-sm) {
|
||||||
|
padding: 3.6px 7px;
|
||||||
}
|
}
|
||||||
@@ -284,7 +284,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
config: {
|
config: {
|
||||||
input: {
|
input: {
|
||||||
type: 'variableList',
|
type: 'variableList',
|
||||||
filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop', 'parameter-extractor'],
|
filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop', 'parameter-extractor', 'code'],
|
||||||
filterVariableNames: ['message']
|
filterVariableNames: ['message']
|
||||||
},
|
},
|
||||||
parallel: {
|
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,
|
{ type: "jinja-render", icon: templateRenderingIcon,
|
||||||
config: {
|
config: {
|
||||||
mapping: {
|
mapping: {
|
||||||
@@ -441,12 +466,12 @@ export const nodeLibrary: NodeLibrary[] = [
|
|||||||
template: {
|
template: {
|
||||||
type: 'messageEditor',
|
type: 'messageEditor',
|
||||||
isArray: false,
|
isArray: false,
|
||||||
enableJinja2: true,
|
language: 'jinja2',
|
||||||
titleVariant: 'borderless',
|
titleVariant: 'borderless',
|
||||||
defaultValue: "{{arg1}}"
|
defaultValue: "{{arg1}}"
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ export const useWorkflowGraph = ({
|
|||||||
: group_variables
|
: 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]) {
|
} 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 }))
|
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]) {
|
} else if (nodeLibraryConfig.config && nodeLibraryConfig.config[key] && config[key]) {
|
||||||
nodeLibraryConfig.config[key].defaultValue = config[key]
|
nodeLibraryConfig.config[key].defaultValue = config[key]
|
||||||
}
|
}
|
||||||
@@ -588,77 +594,6 @@ export const useWorkflowGraph = ({
|
|||||||
graphRef.current.resize(containerRef.current.offsetWidth, containerRef.current.offsetHeight);
|
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 = () => {
|
const init = () => {
|
||||||
@@ -912,7 +847,13 @@ export const useWorkflowGraph = ({
|
|||||||
|
|
||||||
if (data.config) {
|
if (data.config) {
|
||||||
Object.keys(data.config).forEach(key => {
|
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
|
const { messages, ...rest } = data.config[key].defaultValue
|
||||||
let memoryMessage = { role: 'USER', content: data.config[key].defaultValue.messages }
|
let memoryMessage = { role: 'USER', content: data.config[key].defaultValue.messages }
|
||||||
itemConfig = {
|
itemConfig = {
|
||||||
|
|||||||
Reference in New Issue
Block a user