diff --git a/web/src/views/Workflow/components/Editor/index.tsx b/web/src/views/Workflow/components/Editor/index.tsx index c487f2f4..9a387eec 100644 --- a/web/src/views/Workflow/components/Editor/index.tsx +++ b/web/src/views/Workflow/components/Editor/index.tsx @@ -14,6 +14,8 @@ import AutocompletePlugin, { type Suggestion } from './plugin/AutocompletePlugin import CharacterCountPlugin from './plugin/CharacterCountPlugin' import InitialValuePlugin from './plugin/InitialValuePlugin'; import CommandPlugin from './plugin/CommandPlugin'; +import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin'; +import LineNumberPlugin from './plugin/LineNumberPlugin'; import { VariableNode } from './nodes/VariableNode' interface LexicalEditorProps { @@ -88,6 +90,35 @@ const Editor: FC =({ .editor-paragraph:has-text('[') .editor-text { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important; } + .editor-with-line-numbers { + display: flex; + } + .line-numbers { + background-color: #f8f9fa; + border-right: 1px solid #e1e4e8; + color: #656d76; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-size: 12px; + line-height: 20px; + padding: 4px 8px; + text-align: right; + user-select: none; + display: flex; + flex-direction: column; + } + .line-numbers > div { + min-height: 20px; + display: flex; + align-items: flex-start; + } + .editor-content-with-numbers { + flex: 1; + white-space: pre-wrap; + } + .editor-content-with-numbers p { + margin: 0; + min-height: 20px; + } `; document.head.appendChild(style); } @@ -117,25 +148,49 @@ const Editor: FC =({
+ minHeight: `${height}px`, + }}> +
+
1
+
+ +
+ ) : ( + + ) } placeholder={
=({ /> + {enableJinja2 && } + {enableJinja2 && } { setCount(count) }} onChange={onChange} /> diff --git a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx index 33e31199..5ad18dcd 100644 --- a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx @@ -36,64 +36,68 @@ const InitialValuePlugin: React.FC = ({ value, options editor.update(() => { const root = $getRoot(); root.clear(); - const paragraph = $createParagraphNode(); const parts = value.split(/(\{\{[^}]+\}\})/); - parts.forEach(part => { - const match = part.match(/^\{\{([^.]+)\.([^}]+)\}\}$/); - const contextMatch = part.match(/^\{\{context\}\}$/); - const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/); + if (enableJinja2) { + // Handle newlines properly in Jinja2 mode + const lines = value.split('\n'); + lines.forEach((line) => { + const paragraph = $createParagraphNode(); + paragraph.append($createTextNode(line)); + root.append(paragraph); + }); + } else { + const paragraph = $createParagraphNode(); + parts.forEach(part => { + const match = part.match(/^\{\{([^.]+)\.([^}]+)\}\}$/); + const contextMatch = part.match(/^\{\{context\}\}$/); + const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/); - if (enableJinja2) { - paragraph.append($createTextNode(part)); - return; - } - - if (contextMatch) { - const contextSuggestion = options.find(s => s.isContext && s.label === 'context'); - if (contextSuggestion) { - paragraph.append($createVariableNode(contextSuggestion)); - } else { - paragraph.append($createTextNode(part)); - } - return - } - - if (conversationMatch) { - const [_, variableName] = conversationMatch; - const conversationSuggestion = options.find(s => - s.group === 'CONVERSATION' && s.label === variableName - ); - if (conversationSuggestion) { - paragraph.append($createVariableNode(conversationSuggestion)); - } else { - paragraph.append($createTextNode(part)); - } - return - } - - if (match) { - const [_, nodeId, label] = match; - - const suggestion = options.find(s => { - if (nodeId === 'sys') { - return s.nodeData.type === 'start' && s.label === `sys.${label}` + if (contextMatch) { + const contextSuggestion = options.find(s => s.isContext && s.label === 'context'); + if (contextSuggestion) { + paragraph.append($createVariableNode(contextSuggestion)); + } else { + paragraph.append($createTextNode(part)); } - return s.nodeData.id === nodeId && s.label === label - }); + return + } + + if (conversationMatch) { + const [_, variableName] = conversationMatch; + const conversationSuggestion = options.find(s => + s.group === 'CONVERSATION' && s.label === variableName + ); + if (conversationSuggestion) { + paragraph.append($createVariableNode(conversationSuggestion)); + } else { + paragraph.append($createTextNode(part)); + } + return + } + + if (match) { + const [_, nodeId, label] = match; - if (suggestion) { - paragraph.append($createVariableNode(suggestion)); - } else { + const suggestion = options.find(s => { + if (nodeId === 'sys') { + return s.nodeData.type === 'start' && s.label === `sys.${label}` + } + return s.nodeData.id === nodeId && s.label === label + }); + + if (suggestion) { + paragraph.append($createVariableNode(suggestion)); + } else { + paragraph.append($createTextNode(part)); + } + } else if (part) { paragraph.append($createTextNode(part)); } - } else if (part) { - paragraph.append($createTextNode(part)); - } - }); - - root.append(paragraph); + }); + root.append(paragraph); + } }, { discrete: true }); } diff --git a/web/src/views/Workflow/components/Editor/plugin/Jinja2HighlightPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/Jinja2HighlightPlugin.tsx new file mode 100644 index 00000000..498f78d7 --- /dev/null +++ b/web/src/views/Workflow/components/Editor/plugin/Jinja2HighlightPlugin.tsx @@ -0,0 +1,151 @@ +import { useEffect } from 'react'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { TextNode, $createTextNode } from 'lexical'; + +const Jinja2HighlightPlugin = () => { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + return editor.registerNodeTransform(TextNode, (textNode: TextNode) => { + const text = textNode.getTextContent(); + + if (containsJinja2Patterns(text)) { + const parent = textNode.getParent(); + if (!parent) return; + + const tokens = tokenizeJinja2(text); + const newNodes = tokens.map(token => { + const newNode = $createTextNode(token.text); + + switch (token.type) { + case 'number': + newNode.setStyle('color: #005cc5; font-weight: 500;'); + break; + case 'header-0': + case 'header-1': + case 'header-2': + case 'header-3': + case 'header-4': + case 'header-5': + newNode.setStyle('color: #008000'); + break; + case 'brace-0': + newNode.setStyle('color: #d73a49; font-family: monospace; font-weight: bold;'); + break; + case 'brace-1': + newNode.setStyle('color: #0366d6; font-family: monospace; font-weight: bold;'); + break; + case 'brace-2': + newNode.setStyle('color: #28a745; font-family: monospace; font-weight: bold;'); + break; + case 'brace-3': + newNode.setStyle('color: #6f42c1; font-family: monospace; font-weight: bold;'); + break; + case 'expression-0': + case 'expression-1': + case 'expression-2': + case 'expression-3': + case 'statement-0': + case 'statement-1': + case 'statement-2': + case 'statement-3': + // Jinja2 delimiters use same color as braces + break; + case 'comment-0': + case 'comment-1': + case 'comment-2': + case 'comment-3': + newNode.setStyle('color: #721c24; font-family: monospace;'); + break; + case 'variable': + newNode.setStyle('color: #0969da; font-weight: 500;'); + break; + case 'filter': + newNode.setStyle('color: #8250df; font-weight: 500;'); + break; + case 'keyword': + newNode.setStyle('color: #cf222e; font-weight: 600;'); + 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]); + } + } + } + }); + }, [editor]); + + return null; +}; + +function containsJinja2Patterns(text: string): boolean { + return /[{}#\d]/.test(text); +} + +function tokenizeJinja2(text: string): Array<{text: string, type: string}> { + const tokens: Array<{text: string, type: string}> = []; + let i = 0; + let braceLevel = 0; + + while (i < text.length) { + // Check for markdown headers (at start or after whitespace) + if (text[i] === '#' && (i === 0 || /\s/.test(text[i - 1]))) { + let headerLevel = 0; + let start = i; + while (i < text.length && text[i] === '#') { + headerLevel++; + i++; + } + // Skip space after # + if (i < text.length && text[i] === ' ') { + i++; + } + // Get the rest of the header text + while (i < text.length && text[i] !== '\n' && !/[{}]/.test(text[i])) { + i++; + } + tokens.push({ text: text.slice(start, i), type: `header-${Math.min(headerLevel - 1, 5)}` }); + continue; + } + + // Check for 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; + } + + if (text[i] === '{') { + tokens.push({ text: '{', type: `brace-${braceLevel % 4}` }); + braceLevel++; + i++; + } else if (text[i] === '}') { + braceLevel = Math.max(0, braceLevel - 1); + tokens.push({ text: '}', type: `brace-${braceLevel % 4}` }); + i++; + } else { + let start = i; + while (i < text.length && text[i] !== '{' && text[i] !== '}' && + !(text[i] === '#' && (i === 0 || /\s/.test(text[i - 1]))) && + !/\d/.test(text[i])) { + i++; + } + if (start < i) { + tokens.push({ text: text.slice(start, i), type: 'text' }); + } + } + } + + return tokens; +} + +export default Jinja2HighlightPlugin; \ No newline at end of file diff --git a/web/src/views/Workflow/components/Editor/plugin/JsonHighlightPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/JsonHighlightPlugin.tsx deleted file mode 100644 index 93180f79..00000000 --- a/web/src/views/Workflow/components/Editor/plugin/JsonHighlightPlugin.tsx +++ /dev/null @@ -1,109 +0,0 @@ -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; \ No newline at end of file diff --git a/web/src/views/Workflow/components/Editor/plugin/LineNumberPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/LineNumberPlugin.tsx new file mode 100644 index 00000000..d84625ac --- /dev/null +++ b/web/src/views/Workflow/components/Editor/plugin/LineNumberPlugin.tsx @@ -0,0 +1,63 @@ +import { useEffect, useState } from 'react'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { $getRoot } from 'lexical'; + +const LineNumberPlugin = () => { + const [editor] = useLexicalComposerContext(); + const [lineCount, setLineCount] = useState(1); + + useEffect(() => { + return editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + const root = $getRoot(); + const paragraphCount = root.getChildren().length; + const lines = Math.max(1, paragraphCount); + setLineCount(lines); + }); + }); + }, [editor]); + + useEffect(() => { + const updateLineNumbers = () => { + const lineNumbersElement = document.querySelector('.line-numbers'); + const editorElement = document.querySelector('.editor-content-with-numbers'); + + if (lineNumbersElement && editorElement) { + const paragraphs = editorElement.querySelectorAll('p'); + + // Clear existing line numbers + lineNumbersElement.innerHTML = ''; + + // Create line numbers positioned at each paragraph + paragraphs.forEach((paragraph, index) => { + const lineNumber = document.createElement('div'); + lineNumber.textContent = (index + 1).toString(); + lineNumber.style.position = 'absolute'; + lineNumber.style.top = paragraph.offsetTop + 'px'; + lineNumber.style.right = '8px'; + lineNumber.style.height = '20px'; + lineNumber.style.lineHeight = '20px'; + lineNumbersElement.appendChild(lineNumber); + }); + + // Set line numbers container to relative positioning + (lineNumbersElement as HTMLElement).style.position = 'relative'; + } + }; + + // Update line numbers after content changes + const timer = setTimeout(updateLineNumbers, 100); + + // Also update on window resize + window.addEventListener('resize', updateLineNumbers); + + return () => { + clearTimeout(timer); + window.removeEventListener('resize', updateLineNumbers); + }; + }, [lineCount]); + + return null; +}; + +export default LineNumberPlugin; \ No newline at end of file diff --git a/web/src/views/Workflow/components/Nodes/AddNode.tsx b/web/src/views/Workflow/components/Nodes/AddNode.tsx index 973a503c..52b8da4f 100644 --- a/web/src/views/Workflow/components/Nodes/AddNode.tsx +++ b/web/src/views/Workflow/components/Nodes/AddNode.tsx @@ -56,7 +56,8 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { graph.addEdge({ source: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'right')?.id || 'right' }, target: { cell: edge.getTargetCellId(), port: targetPortId }, - attrs: edge.getAttrs() + attrs: edge.getAttrs(), + zIndex: 3 }); }); diff --git a/web/src/views/Workflow/components/Nodes/LoopNode.tsx b/web/src/views/Workflow/components/Nodes/LoopNode.tsx index 37feb2dc..dac91b68 100644 --- a/web/src/views/Workflow/components/Nodes/LoopNode.tsx +++ b/web/src/views/Workflow/components/Nodes/LoopNode.tsx @@ -61,6 +61,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { }, }, }, + zIndex: 3 }); } } @@ -127,6 +128,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { }, }, }, + zIndex: 3 } graph.addEdge(edgeConfig) diff --git a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx index 80584220..bbb3238d 100644 --- a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx +++ b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx @@ -101,8 +101,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an vo.dataType === 'string' || vo.dataType === 'number')} /> @@ -110,8 +109,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an vo.dataType === 'string' || vo.dataType === 'number')} /> @@ -134,8 +132,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an vo.dataType === 'string' || vo.dataType === 'number')} typeOptions={[ { label: 'text', value: 'text' }, { label: 'file', value: 'file' } @@ -168,7 +165,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an vo.dataType === 'string' || vo.dataType === 'number')} isArray={false} title="RAW TEXT" /> @@ -177,7 +174,8 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an {values?.body?.content_type === 'binary' && vo.dataType.includes('file'))} filterBooleanType={true} /> diff --git a/web/src/views/Workflow/components/Properties/index.tsx b/web/src/views/Workflow/components/Properties/index.tsx index d92f5855..765fd207 100644 --- a/web/src/views/Workflow/components/Properties/index.tsx +++ b/web/src/views/Workflow/components/Properties/index.tsx @@ -64,7 +64,7 @@ const Properties: FC = ({ useEffect(() => { if (isSyncingRef.current || lastSyncSourceRef.current === 'mapping' || selectedNode?.data?.type !== 'jinja-render' || !values?.mapping || !values?.template) return - const currentMappingNames = Array.isArray(values.mapping) ? values.mapping.map((item: any) => item.name).filter(Boolean) : [] + const currentMappingNames = Array.isArray(values.mapping) ? values.mapping.filter(item => item && item.name).map((item: any) => item.name) : [] const prevNames = prevMappingNamesRef.current if (prevNames.length === 0) { @@ -121,8 +121,8 @@ const Properties: FC = ({ return } - const updatedMapping = Array.isArray(values.mapping) ? [...values.mapping] : [] - const existingNames = updatedMapping.map(item => item.name) + const updatedMapping = Array.isArray(values.mapping) ? [...values.mapping.filter(item => item)] : [] + const existingNames = updatedMapping.filter(item => item && item.name).map(item => item.name) let updatedTemplate = String(values.template) if (prevTemplateVarsRef.current.length > 0) { @@ -157,7 +157,7 @@ const Properties: FC = ({ isSyncingRef.current = true lastSyncSourceRef.current = 'template' - prevMappingNamesRef.current = finalMapping.map((item: any) => item.name).filter(Boolean) + prevMappingNamesRef.current = finalMapping.filter(item => item && item.name).map((item: any) => item.name) prevTemplateVarsRef.current = templateVars if (JSON.stringify(finalMapping) !== JSON.stringify(values.mapping)) { @@ -391,6 +391,54 @@ const Properties: FC = ({ } } + // Check if parent loop/iteration is connected to http-request via ERROR connection + if (parentData.type === 'loop' || parentData.type === 'iteration') { + const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id); + parentPreviousNodeIds.forEach(prevNodeId => { + const prevNode = nodes.find(n => n.id === prevNodeId); + if (!prevNode) return; + + const prevNodeData = prevNode.getData(); + if (prevNodeData.type === 'http-request') { + // Check if connected via ERROR connection point + const errorEdges = edges.filter(edge => { + return edge.getTargetCellId() === parentLoopNode.id && + edge.getSourceCellId() === prevNodeId && + edge.getSourcePortId() === 'ERROR' + }); + + if (errorEdges.length > 0) { + const errorMessageKey = `${prevNodeData.id}_error_message`; + const errorTypeKey = `${prevNodeData.id}_error_type`; + + if (!addedKeys.has(errorMessageKey)) { + addedKeys.add(errorMessageKey); + variableList.push({ + key: errorMessageKey, + label: 'error_message', + type: 'variable', + dataType: 'string', + value: `${prevNodeData.id}.error_message`, + nodeData: prevNodeData, + }); + } + + if (!addedKeys.has(errorTypeKey)) { + addedKeys.add(errorTypeKey); + variableList.push({ + key: errorTypeKey, + label: 'error_type', + type: 'variable', + dataType: 'string', + value: `${prevNodeData.id}.error_type`, + nodeData: prevNodeData, + }); + } + } + } + }); + } + // Add variables from nodes preceding the parent loop/iteration node const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id); allRelevantNodeIds.push(...parentPreviousNodeIds); @@ -455,15 +503,15 @@ const Properties: FC = ({ } break case 'knowledge-retrieval': - const knowledgeKey = `${dataNodeId}_message`; + const knowledgeKey = `${dataNodeId}_output`; if (!addedKeys.has(knowledgeKey)) { addedKeys.add(knowledgeKey); variableList.push({ key: knowledgeKey, - label: 'message', + label: 'output', type: 'variable', dataType: 'array[object]', - value: `${dataNodeId}.message`, + value: `${dataNodeId}.output`, nodeData: nodeData, }); } @@ -571,6 +619,42 @@ const Properties: FC = ({ nodeData: nodeData, }); } + + // Check if connected via ERROR connection point + const errorEdges = edges.filter(edge => + edge.getTargetCellId() === selectedNode.id && + edge.getSourceCellId() === nodeId && + edge.getSourcePortId() === 'ERROR' + ); + + if (errorEdges.length > 0) { + const errorMessageKey = `${dataNodeId}_error_message`; + const errorTypeKey = `${dataNodeId}_error_type`; + + if (!addedKeys.has(errorMessageKey)) { + addedKeys.add(errorMessageKey); + variableList.push({ + key: errorMessageKey, + label: 'error_message', + type: 'variable', + dataType: 'string', + value: `${dataNodeId}.error_message`, + nodeData: nodeData, + }); + } + + if (!addedKeys.has(errorTypeKey)) { + addedKeys.add(errorTypeKey); + variableList.push({ + key: errorTypeKey, + label: 'error_type', + type: 'variable', + dataType: 'string', + value: `${dataNodeId}.error_type`, + nodeData: nodeData, + }); + } + } break case 'jinja-render': const jinjaOutputKey = `${dataNodeId}_output`; @@ -613,7 +697,6 @@ const Properties: FC = ({ } break case 'iteration': - console.log('iteration addedKeys', addedKeys) const iterationOutputKey = `${dataNodeId}_output`; const iterationItemKey = `${dataNodeId}_item`; if (!addedKeys.has(iterationOutputKey)) { @@ -651,18 +734,21 @@ const Properties: FC = ({ break case 'loop': const cycleVars = nodeData.config.cycle_vars.defaultValue || []; + console.log('cycleVars', cycleVars) cycleVars.forEach((cycleVar: any) => { const cycleVarKey = `${dataNodeId}_cycle_${cycleVar.name}`; if (!addedKeys.has(cycleVarKey)) { addedKeys.add(cycleVarKey); - variableList.push({ - key: cycleVarKey, - label: cycleVar.name, - type: 'variable', - dataType: cycleVar.type || 'string', - value: `${dataNodeId}.${cycleVar.name}`, - nodeData: nodeData, - }); + if (cycleVar.name && cycleVar.name.trim() !== '') { + variableList.push({ + key: cycleVarKey, + label: cycleVar.name, + type: 'variable', + dataType: cycleVar.type || 'string', + value: `${dataNodeId}.${cycleVar.name}`, + nodeData: nodeData, + }); + } } }); break @@ -818,7 +904,11 @@ const Properties: FC = ({ return ( - + variable.nodeData?.type !== 'knowledge-retrieval')} + parentName={key} + /> ) } @@ -1010,39 +1100,25 @@ const Properties: FC = ({ ? { - // 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).filter(variable => { - // Keep conversation variables - if (variable.group === 'CONVERSATION') return true; - // Keep sys variables from start nodes - if (variable.nodeData?.type === 'start' && variable.value?.startsWith('sys.')) return true; - // Keep variables from non-start nodes - if (variable.nodeData?.type !== 'start') return true; - // Filter out custom variables from start nodes - return false; - }), ...cycleVarSuggestions]; - } - // Filter options for condition list: only sys variables from start nodes and conversation variables - return getFilteredVariableList(selectedNode?.data?.type).filter(variable => { + const cycleVars = values?.cycle_vars || []; + const cycleVarSuggestions: Suggestion[] = cycleVars.filter(vo => vo.name && vo.name.trim() !== '').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 [...variableList.filter(variable => { // Keep conversation variables if (variable.group === 'CONVERSATION') return true; // Keep sys variables from start nodes if (variable.nodeData?.type === 'start' && variable.value?.startsWith('sys.')) return true; // Keep variables from non-start nodes - if (variable.nodeData?.type !== 'start') return true; + if (variable.nodeData?.type !== 'start' && variable.nodeData?.type !== 'http-request' && variable.dataType !== 'boolean') return true; // Filter out custom variables from start nodes return false; - }); + }), ...cycleVarSuggestions]; })() } selectedNode={selectedNode} diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index 38e93bab..dfbf2e92 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -342,7 +342,7 @@ export const useWorkflowGraph = ({ }, }, }, - zIndex: 0 + zIndex: targetCell.getData()?.cycle ? 3 : 0 } return edgeConfig @@ -434,13 +434,13 @@ export const useWorkflowGraph = ({ ); }; // 显示/隐藏连接桩 - const showPorts = (show: boolean) => { - const container = containerRef.current!; - const ports = container.querySelectorAll('.x6-port-body') as NodeListOf; - for (let i = 0, len = ports.length; i < len; i += 1) { - ports[i].style.visibility = show ? 'visible' : 'hidden'; - } - }; + // const showPorts = (show: boolean) => { + // const container = containerRef.current!; + // const ports = container.querySelectorAll('.x6-port-body') as NodeListOf; + // for (let i = 0, len = ports.length; i < len; i += 1) { + // ports[i].style.visibility = show ? 'visible' : 'hidden'; + // } + // }; // 节点选择事件 const nodeClick = ({ node }: { node: Node }) => { // 忽略 add-node 类型的节点点击