diff --git a/packages/global/core/workflow/utils.ts b/packages/global/core/workflow/utils.ts index fb7da9a2bbb..dfbcab8def6 100644 --- a/packages/global/core/workflow/utils.ts +++ b/packages/global/core/workflow/utils.ts @@ -3,9 +3,10 @@ import { WorkflowIOValueTypeEnum, NodeInputKeyEnum, VariableInputEnum, - variableMap + variableMap, + VARIABLE_NODE_ID } from './constants'; -import { FlowNodeInputItemType, FlowNodeOutputItemType } from './type/io.d'; +import { FlowNodeInputItemType, FlowNodeOutputItemType, ReferenceValueProps } from './type/io.d'; import { StoreNodeItemType } from './type/node'; import type { VariableItemType, @@ -23,6 +24,7 @@ import { } from '../app/constants'; import { IfElseResultEnum } from './template/system/ifElse/constant'; import { RuntimeNodeItemType } from './runtime/type'; +import { getReferenceVariableValue } from './runtime/utils'; export const getHandleId = (nodeId: string, type: 'source' | 'target', key: string) => { return `${nodeId}-${type}-${key}`; @@ -226,3 +228,69 @@ export const updatePluginInputByVariables = ( : node ); }; + +export function replaceVariableLabel({ + text, + nodes, + variables, + runningNode +}: { + text: any; + nodes: RuntimeNodeItemType[]; + variables: Record; + runningNode: RuntimeNodeItemType; +}) { + if (!(typeof text === 'string')) return text; + + const globalVariables = Object.keys(variables).map((key) => { + return { + nodeId: VARIABLE_NODE_ID, + id: key, + value: variables[key] + }; + }); + + const nodeVariables = nodes + .map((node) => { + return node.outputs.map((output) => { + return { + nodeId: node.nodeId, + id: output.id, + value: output.value + }; + }); + }) + .flat(); + + const customInputs = runningNode.inputs.flatMap((item) => { + if (Array.isArray(item.value)) { + return [ + { + id: item.key, + value: getReferenceVariableValue({ + value: item.value as ReferenceValueProps, + nodes, + variables + }), + nodeId: runningNode.nodeId + } + ]; + } + return []; + }); + + const allVariables = [...globalVariables, ...nodeVariables, ...customInputs]; + + for (const key in allVariables) { + const val = allVariables[key]; + const regex = new RegExp(`\\{\\{\\$(${val.nodeId}\\.${val.id})\\$\\}\\}`, 'g'); + if (['string', 'number'].includes(typeof val.value)) { + text = text.replace(regex, String(val.value)); + } else if (['object'].includes(typeof val.value)) { + text = text.replace(regex, JSON.stringify(val.value)); + } else { + continue; + } + } + return text || ''; +} diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index fd39539aaf1..c8af42c0811 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -19,6 +19,7 @@ import { import { replaceVariable } from '@fastgpt/global/common/string/tools'; import { responseWriteNodeStatus } from '../../../common/response'; import { getSystemTime } from '@fastgpt/global/common/time/timezone'; +import { replaceVariableLabel } from '@fastgpt/global/core/workflow/utils'; import { dispatchWorkflowStart } from './init/workflowStart'; import { dispatchChatCompletion } from './chat/oneapi'; @@ -54,6 +55,7 @@ import { surrenderProcess } from '../../../common/system/tools'; import { dispatchRunCode } from './code/run'; import { dispatchTextEditor } from './tools/textEditor'; import { dispatchCustomFeedback } from './tools/customFeedback'; +import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io'; const callbackMap: Record = { [FlowNodeTypeEnum.workflowStart]: dispatchWorkflowStart, @@ -291,6 +293,14 @@ export async function dispatchWorkFlow(data: Props): Promise { const isIcon = !!iconPaths[src as any]; return isIcon ? ( - + + + ) : ( import('./icons/core/workflow/template/textConcat.svg'), 'core/workflow/template/toolCall': () => import('./icons/core/workflow/template/toolCall.svg'), + 'core/workflow/template/variable': () => import('./icons/core/workflow/template/variable.svg'), 'core/workflow/template/variableUpdate': () => import('./icons/core/workflow/template/variableUpdate.svg'), 'core/workflow/template/workflowStart': () => diff --git a/packages/web/components/common/Icon/icons/core/workflow/template/variable.svg b/packages/web/components/common/Icon/icons/core/workflow/template/variable.svg new file mode 100644 index 00000000000..37e80d7e555 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/workflow/template/variable.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/web/components/common/Textarea/PromptEditor/Editor.tsx b/packages/web/components/common/Textarea/PromptEditor/Editor.tsx index 6203c3fea37..e90d14ae5a0 100644 --- a/packages/web/components/common/Textarea/PromptEditor/Editor.tsx +++ b/packages/web/components/common/Textarea/PromptEditor/Editor.tsx @@ -5,7 +5,7 @@ import { ContentEditable } from '@lexical/react/LexicalContentEditable'; import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'; import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'; -import VariablePickerPlugin from './plugins/VariablePickerPlugin'; +import VariableLabelPickerPlugin from './plugins/VariableLabelPickerPlugin'; import { Box } from '@chakra-ui/react'; import styles from './index.module.scss'; import VariablePlugin from './plugins/VariablePlugin'; @@ -18,6 +18,8 @@ import { getNanoid } from '@fastgpt/global/common/string/tools'; import FocusPlugin from './plugins/FocusPlugin'; import { textToEditorState } from './utils'; import { MaxLengthPlugin } from './plugins/MaxLengthPlugin'; +import { VariableLabelNode } from './plugins/VariableLabelPlugin/node'; +import VariableLabelPlugin from './plugins/VariableLabelPlugin'; export default function Editor({ h = 200, @@ -51,7 +53,7 @@ export default function Editor({ const initialConfig = { namespace: 'promptEditor', - nodes: [VariableNode], + nodes: [VariableNode, VariableLabelNode], editorState: textToEditorState(value), onError: (error: Error) => { throw error; @@ -127,8 +129,9 @@ export default function Editor({ }); }} /> - + + {showResize && ( diff --git a/packages/web/components/common/Textarea/PromptEditor/index.module.scss b/packages/web/components/common/Textarea/PromptEditor/index.module.scss index 8a24228b4c6..21fe7cc2b81 100644 --- a/packages/web/components/common/Textarea/PromptEditor/index.module.scss +++ b/packages/web/components/common/Textarea/PromptEditor/index.module.scss @@ -2,10 +2,11 @@ position: relative; height: 100%; width: 100%; - border: 1px solid var(--chakra-colors-borderColor-base); + border: 1px solid rgb(232, 235, 240); border-radius: var(--chakra-radii-md); padding: 8px 12px; - background: var(--chakra-colors-gray-50); + background: #fff; + font-size: var(--chakra-fontSizes-sm); overflow-y: auto; } diff --git a/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPickerPlugin/index.tsx b/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPickerPlugin/index.tsx new file mode 100644 index 00000000000..0e704e4fa3e --- /dev/null +++ b/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPickerPlugin/index.tsx @@ -0,0 +1,228 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'; +import { $createTextNode, $getSelection, $isRangeSelection, TextNode } from 'lexical'; +import * as React from 'react'; +import { useCallback, useState } from 'react'; +import * as ReactDOM from 'react-dom'; +import { Box, Flex } from '@chakra-ui/react'; +import { useBasicTypeaheadTriggerMatch } from '../../utils'; +import { EditorVariablePickerType } from '../../type'; +import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; +import { useTranslation } from 'react-i18next'; +import Avatar from '../../../../Avatar'; + +type EditorVariablePickerType1 = { + key: string; + label: string; + required?: boolean; + icon?: string; + valueType?: WorkflowIOValueTypeEnum; + index: number; +}; +interface TransformedParent { + id: string; + label: string; + avatar: string; + children: EditorVariablePickerType1[]; +} + +export default function VariableLabelPickerPlugin({ + variables +}: { + variables: EditorVariablePickerType[]; +}) { + const { t } = useTranslation(); + const [editor] = useLexicalComposerContext(); + const [queryString, setQueryString] = useState(null); + + const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', { + minLength: 0 + }); + + const onSelectOption = useCallback( + (selectedOption: any, nodeToRemove: TextNode | null, closeMenu: () => void) => { + editor.update(() => { + const selection = $getSelection(); + if (!$isRangeSelection(selection) || selectedOption == null) { + return; + } + if (nodeToRemove) { + nodeToRemove.remove(); + } + selection.insertNodes([ + $createTextNode(`{{$${selectedOption.parent?.id}.${selectedOption.key}$}}`) + ]); + closeMenu(); + }); + }, + [editor] + ); + + return ( + { + if (anchorElementRef.current == null) { + return null; + } + return anchorElementRef.current && variables.length + ? ReactDOM.createPortal( + + {variableFilter(variables, queryString || '').length === variables.length && ( + + {t('workflow:variable_picker_tips')} + + )} + {variableFilter(variables, queryString || '').length > 0 ? ( + transformData(variableFilter(variables, queryString || '')).map((item) => { + return ( + + + + + {item.label} + + + {item.children?.map((child, index) => ( + { + setHighlightedIndex(child.index); + selectOptionAndCleanUp({ ...child, parent: item }); + }} + onMouseEnter={() => { + setHighlightedIndex(child.index); + }} + > + + {child.label} + + + ))} + + ); + }) + ) : ( + + {t('common:unusable_variable')} + + )} + , + anchorElementRef.current + ) + : null; + }} + /> + ); +} + +function transformData(data: EditorVariablePickerType[]): TransformedParent[] { + const transformedData: TransformedParent[] = []; + const parentMap: { [key: string]: TransformedParent } = {}; + + data.forEach((item, index) => { + const parentId = item.parent!.id; + const parentLabel = item.parent!.label; + const parentAvatar = item.parent!.avatar; + + if (!parentMap[parentId]) { + parentMap[parentId] = { + id: parentId, + label: parentLabel, + avatar: parentAvatar || '', + children: [] + }; + } + parentMap[parentId].children.push({ + label: item.label, + key: item.key, + icon: item.icon, + index + }); + }); + + const addedParents = new Set(); + data.forEach((item) => { + const parentId = item.parent!.id; + if (!addedParents.has(parentId)) { + transformedData.push(parentMap[parentId]); + addedParents.add(parentId); + } + }); + + return transformedData; +} + +function variableFilter( + data: EditorVariablePickerType[], + queryString: string +): EditorVariablePickerType[] { + const lowerCaseQuery = queryString.toLowerCase(); + + return data.filter((item) => { + const labelMatch = item.label.toLowerCase().includes(lowerCaseQuery); + const keyMatch = item.key.toLowerCase().includes(lowerCaseQuery); + const parentLabelMatch = item.parent!.label.toLowerCase().includes(lowerCaseQuery); + + return labelMatch || keyMatch || parentLabelMatch; + }); +} diff --git a/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/components/VariableLabel.tsx b/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/components/VariableLabel.tsx new file mode 100644 index 00000000000..8d7565f0b7f --- /dev/null +++ b/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/components/VariableLabel.tsx @@ -0,0 +1,51 @@ +import { ChevronRightIcon } from '@chakra-ui/icons'; +import { Box } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import Avatar from '../../../../../../../components/common/Avatar'; + +export default function VariableLabel({ + variableLabel, + nodeAvatar +}: { + variableLabel: string; + nodeAvatar: string; +}) { + const { t } = useTranslation(); + const [parentLabel, childLabel] = variableLabel.split('.'); + + return ( + <> + + {parentLabel !== 'undefined' ? ( + + + {parentLabel} + + {childLabel} + + ) : ( + <> + {t('common:invalid_variable')} + + )} + + + ); +} diff --git a/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/index.tsx b/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/index.tsx new file mode 100644 index 00000000000..1588c2ee4f7 --- /dev/null +++ b/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/index.tsx @@ -0,0 +1,58 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { EditorVariablePickerType } from '../../type'; +import { useCallback, useEffect, useMemo } from 'react'; +import { $createVariableLabelNode, VariableLabelNode } from './node'; +import { TextNode } from 'lexical'; +import { getHashtagRegexString } from './utils'; +import { mergeRegister } from '@lexical/utils'; +import { registerLexicalTextEntity } from '../../utils'; + +const REGEX = new RegExp(getHashtagRegexString(), 'i'); + +export default function VariableLabelPlugin({ + variables +}: { + variables: EditorVariablePickerType[]; +}) { + const [editor] = useLexicalComposerContext(); + useEffect(() => { + if (!editor.hasNodes([VariableLabelNode])) + throw new Error('VariableLabelPlugin: VariableLabelPlugin not registered on editor'); + }, [editor]); + + const createVariableLabelPlugin = useCallback((textNode: TextNode): VariableLabelNode => { + const [parentKey, childrenKey] = textNode.getTextContent().slice(3, -3).split('.'); + const currentVariable = variables.find( + (item) => item.parent?.id === parentKey && item.key === childrenKey + ); + const variableLabel = `${currentVariable && currentVariable.parent?.label}.${currentVariable?.label}`; + const nodeAvatar = currentVariable?.parent?.avatar || ''; + return $createVariableLabelNode(textNode.getTextContent(), variableLabel, nodeAvatar); + }, []); + + const getVariableMatch = useCallback((text: string) => { + const matches = REGEX.exec(text); + if (!matches) return null; + // if (variableKeys.indexOf(matches[4]) === -1) return null; + const hashtagLength = matches[4].length + 6; + const startOffset = matches.index; + const endOffset = startOffset + hashtagLength; + return { + end: endOffset, + start: startOffset + }; + }, []); + + useEffect(() => { + mergeRegister( + ...registerLexicalTextEntity( + editor, + getVariableMatch, + VariableLabelNode, + createVariableLabelPlugin + ) + ); + }, [createVariableLabelPlugin, editor, getVariableMatch]); + + return null; +} diff --git a/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/node.tsx b/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/node.tsx new file mode 100644 index 00000000000..d90368a449d --- /dev/null +++ b/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/node.tsx @@ -0,0 +1,124 @@ +import { + DecoratorNode, + DOMConversionMap, + DOMExportOutput, + EditorConfig, + LexicalEditor, + LexicalNode, + NodeKey, + SerializedLexicalNode, + Spread, + TextFormatType +} from 'lexical'; +import VariableLabel from './components/VariableLabel'; + +export type SerializedVariableLabelNode = Spread< + { + variableKey: string; + variableLabel: string; + nodeAvatar: string; + format: number | TextFormatType; + }, + SerializedLexicalNode +>; + +export class VariableLabelNode extends DecoratorNode { + __format: number | TextFormatType; + __variableKey: string; + __variableLabel: string; + __nodeAvatar: string; + static getType(): string { + return 'variableLabel'; + } + static clone(node: VariableLabelNode): VariableLabelNode { + return new VariableLabelNode( + node.__variableKey, + node.__variableLabel, + node.__nodeAvatar, + node.__format, + node.__key + ); + } + constructor( + variableKey: string, + variableLabel: string, + nodeAvatar: string, + format?: number | TextFormatType, + key?: NodeKey + ) { + super(key); + this.__variableKey = variableKey; + this.__format = format || 0; + this.__variableLabel = variableLabel; + this.__nodeAvatar = nodeAvatar; + } + + static importJSON(serializedNode: SerializedVariableLabelNode): VariableLabelNode { + const node = $createVariableLabelNode( + serializedNode.variableKey, + serializedNode.variableLabel, + serializedNode.nodeAvatar + ); + node.setFormat(serializedNode.format); + return node; + } + + setFormat(format: number | TextFormatType): void { + const self = this.getWritable(); + self.__format = format; + } + getFormat(): number | TextFormatType { + return this.__format; + } + + exportJSON(): SerializedVariableLabelNode { + return { + format: this.__format || 0, + type: 'variableLabel', + version: 1, + variableKey: this.getVariableKey(), + variableLabel: this.__variableLabel, + nodeAvatar: this.__nodeAvatar + }; + } + createDOM(): HTMLElement { + const element = document.createElement('span'); + return element; + } + exportDOM(): DOMExportOutput { + const element = document.createElement('span'); + return { element }; + } + static importDOM(): DOMConversionMap | null { + return {}; + } + updateDOM(): false { + return false; + } + getVariableKey(): string { + return this.__variableKey; + } + getTextContent( + _includeInert?: boolean | undefined, + _includeDirectionless?: false | undefined + ): string { + return `${this.__variableKey}`; + } + decorate(_editor: LexicalEditor, config: EditorConfig): JSX.Element { + return ; + } +} + +export function $createVariableLabelNode( + variableKey: string, + variableLabel: string, + nodeAvatar: string +): VariableLabelNode { + return new VariableLabelNode(variableKey, variableLabel, nodeAvatar); +} + +export function $isVariableLabelNode( + node: VariableLabelNode | LexicalNode | null | undefined +): node is VariableLabelNode { + return node instanceof VariableLabelNode; +} diff --git a/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/utils.ts b/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/utils.ts new file mode 100644 index 00000000000..cc74e6298c8 --- /dev/null +++ b/packages/web/components/common/Textarea/PromptEditor/plugins/VariableLabelPlugin/utils.ts @@ -0,0 +1,228 @@ +function getHashtagRegexVariableLabels(): Readonly<{ + alpha: string; + alphanumeric: string; + leftChars: string; + rightChars: string; + middleChars: string; +}> { + // Latin accented characters + // Excludes 0xd7 from the range + // (the multiplication sign, confusable with "x"). + // Also excludes 0xf7, the division sign + const latinAccents = + '\xC0-\xD6' + + '\xD8-\xF6' + + '\xF8-\xFF' + + '\u0100-\u024F' + + '\u0253-\u0254' + + '\u0256-\u0257' + + '\u0259' + + '\u025B' + + '\u0263' + + '\u0268' + + '\u026F' + + '\u0272' + + '\u0289' + + '\u028B' + + '\u02BB' + + '\u0300-\u036F' + + '\u1E00-\u1EFF'; + + // Cyrillic (Russian, Ukrainian, etc.) + const nonLatinChars = + '\u0400-\u04FF' + // Cyrillic + '\u0500-\u0527' + // Cyrillic Supplement + '\u2DE0-\u2DFF' + // Cyrillic Extended A + '\uA640-\uA69F' + // Cyrillic Extended B + '\u0591-\u05BF' + // Hebrew + '\u05C1-\u05C2' + + '\u05C4-\u05C5' + + '\u05C7' + + '\u05D0-\u05EA' + + '\u05F0-\u05F4' + + '\uFB12-\uFB28' + // Hebrew Presentation Forms + '\uFB2A-\uFB36' + + '\uFB38-\uFB3C' + + '\uFB3E' + + '\uFB40-\uFB41' + + '\uFB43-\uFB44' + + '\uFB46-\uFB4F' + + '\u0610-\u061A' + // Arabic + '\u0620-\u065F' + + '\u066E-\u06D3' + + '\u06D5-\u06DC' + + '\u06DE-\u06E8' + + '\u06EA-\u06EF' + + '\u06FA-\u06FC' + + '\u06FF' + + '\u0750-\u077F' + // Arabic Supplement + '\u08A0' + // Arabic Extended A + '\u08A2-\u08AC' + + '\u08E4-\u08FE' + + '\uFB50-\uFBB1' + // Arabic Pres. Forms A + '\uFBD3-\uFD3D' + + '\uFD50-\uFD8F' + + '\uFD92-\uFDC7' + + '\uFDF0-\uFDFB' + + '\uFE70-\uFE74' + // Arabic Pres. Forms B + '\uFE76-\uFEFC' + + '\u200C-\u200C' + // Zero-Width Non-Joiner + '\u0E01-\u0E3A' + // Thai + '\u0E40-\u0E4E' + // Hangul (Korean) + '\u1100-\u11FF' + // Hangul Jamo + '\u3130-\u3185' + // Hangul Compatibility Jamo + '\uA960-\uA97F' + // Hangul Jamo Extended-A + '\uAC00-\uD7AF' + // Hangul Syllables + '\uD7B0-\uD7FF' + // Hangul Jamo Extended-B + '\uFFA1-\uFFDC'; // Half-width Hangul + + const charCode = String.fromCharCode; + + const cjkChars = + '\u30A1-\u30FA\u30FC-\u30FE' + // Katakana (full-width) + '\uFF66-\uFF9F' + // Katakana (half-width) + '\uFF10-\uFF19\uFF21-\uFF3A' + + '\uFF41-\uFF5A' + // Latin (full-width) + '\u3041-\u3096\u3099-\u309E' + // Hiragana + '\u3400-\u4DBF' + // Kanji (CJK Extension A) + `\u4E00-\u9FFF${ + // Kanji (Unified) + // Disabled as it breaks the Regex. + // charCode(0x20000) + '-' + charCode(0x2A6DF) + // Kanji (CJK Extension B) + charCode(0x2a700) + }-${ + charCode(0x2b73f) // Kanji (CJK Extension C) + }${charCode(0x2b740)}-${ + charCode(0x2b81f) // Kanji (CJK Extension D) + }${charCode(0x2f800)}-${charCode(0x2fa1f)}\u3003\u3005\u303B`; // Kanji (CJK supplement) + + const otherChars = latinAccents + nonLatinChars + cjkChars; + // equivalent of \p{L} + + const unicodeLetters = + '\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6' + + '\u00F8-\u0241\u0250-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EE\u037A\u0386' + + '\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03CE\u03D0-\u03F5\u03F7-\u0481' + + '\u048A-\u04CE\u04D0-\u04F9\u0500-\u050F\u0531-\u0556\u0559\u0561-\u0587' + + '\u05D0-\u05EA\u05F0-\u05F2\u0621-\u063A\u0640-\u064A\u066E-\u066F' + + '\u0671-\u06D3\u06D5\u06E5-\u06E6\u06EE-\u06EF\u06FA-\u06FC\u06FF\u0710' + + '\u0712-\u072F\u074D-\u076D\u0780-\u07A5\u07B1\u0904-\u0939\u093D\u0950' + + '\u0958-\u0961\u097D\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0' + + '\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC-\u09DD\u09DF-\u09E1\u09F0-\u09F1' + + '\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33' + + '\u0A35-\u0A36\u0A38-\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D' + + '\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD' + + '\u0AD0\u0AE0-\u0AE1\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30' + + '\u0B32-\u0B33\u0B35-\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B71\u0B83' + + '\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F' + + '\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0C05-\u0C0C\u0C0E-\u0C10' + + '\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C60-\u0C61\u0C85-\u0C8C' + + '\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE' + + '\u0CE0-\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39' + + '\u0D60-\u0D61\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6' + + '\u0E01-\u0E30\u0E32-\u0E33\u0E40-\u0E46\u0E81-\u0E82\u0E84\u0E87-\u0E88' + + '\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7' + + '\u0EAA-\u0EAB\u0EAD-\u0EB0\u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6' + + '\u0EDC-\u0EDD\u0F00\u0F40-\u0F47\u0F49-\u0F6A\u0F88-\u0F8B\u1000-\u1021' + + '\u1023-\u1027\u1029-\u102A\u1050-\u1055\u10A0-\u10C5\u10D0-\u10FA\u10FC' + + '\u1100-\u1159\u115F-\u11A2\u11A8-\u11F9\u1200-\u1248\u124A-\u124D' + + '\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0' + + '\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310' + + '\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C' + + '\u166F-\u1676\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711' + + '\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7' + + '\u17DC\u1820-\u1877\u1880-\u18A8\u1900-\u191C\u1950-\u196D\u1970-\u1974' + + '\u1980-\u19A9\u19C1-\u19C7\u1A00-\u1A16\u1D00-\u1DBF\u1E00-\u1E9B' + + '\u1EA0-\u1EF9\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D' + + '\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC' + + '\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC' + + '\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u2094\u2102\u2107' + + '\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D' + + '\u212F-\u2131\u2133-\u2139\u213C-\u213F\u2145-\u2149\u2C00-\u2C2E' + + '\u2C30-\u2C5E\u2C80-\u2CE4\u2D00-\u2D25\u2D30-\u2D65\u2D6F\u2D80-\u2D96' + + '\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6' + + '\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3006\u3031-\u3035' + + '\u303B-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF' + + '\u3105-\u312C\u3131-\u318E\u31A0-\u31B7\u31F0-\u31FF\u3400-\u4DB5' + + '\u4E00-\u9FBB\uA000-\uA48C\uA800-\uA801\uA803-\uA805\uA807-\uA80A' + + '\uA80C-\uA822\uAC00-\uD7A3\uF900-\uFA2D\uFA30-\uFA6A\uFA70-\uFAD9' + + '\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C' + + '\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F' + + '\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A' + + '\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7' + + '\uFFDA-\uFFDC'; + + // equivalent of \p{Mn}\p{Mc} + const unicodeAccents = + '\u0300-\u036F\u0483-\u0486\u0591-\u05B9\u05BB-\u05BD\u05BF' + + '\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u0615\u064B-\u065E\u0670' + + '\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A' + + '\u07A6-\u07B0\u0901-\u0903\u093C\u093E-\u094D\u0951-\u0954\u0962-\u0963' + + '\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7-\u09C8\u09CB-\u09CD\u09D7' + + '\u09E2-\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D' + + '\u0A70-\u0A71\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD' + + '\u0AE2-\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B43\u0B47-\u0B48\u0B4B-\u0B4D' + + '\u0B56-\u0B57\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7' + + '\u0C01-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56' + + '\u0C82-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5-\u0CD6' + + '\u0D02-\u0D03\u0D3E-\u0D43\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D82-\u0D83' + + '\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2-\u0DF3\u0E31\u0E34-\u0E3A' + + '\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19' + + '\u0F35\u0F37\u0F39\u0F3E-\u0F3F\u0F71-\u0F84\u0F86-\u0F87\u0F90-\u0F97' + + '\u0F99-\u0FBC\u0FC6\u102C-\u1032\u1036-\u1039\u1056-\u1059\u135F' + + '\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B6-\u17D3\u17DD' + + '\u180B-\u180D\u18A9\u1920-\u192B\u1930-\u193B\u19B0-\u19C0\u19C8-\u19C9' + + '\u1A17-\u1A1B\u1DC0-\u1DC3\u20D0-\u20DC\u20E1\u20E5-\u20EB\u302A-\u302F' + + '\u3099-\u309A\uA802\uA806\uA80B\uA823-\uA827\uFB1E\uFE00-\uFE0F' + + '\uFE20-\uFE23'; + + // equivalent of \p{Dn} + const unicodeDigits = + '\u0030-\u0039\u0660-\u0669\u06F0-\u06F9\u0966-\u096F\u09E6-\u09EF' + + '\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F' + + '\u0CE6-\u0CEF\u0D66-\u0D6F\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29' + + '\u1040-\u1049\u17E0-\u17E9\u1810-\u1819\u1946-\u194F\u19D0-\u19D9' + + '\uFF10-\uFF19'; + + // An alpha char is a unicode chars including unicode marks or + // letter or char in otherChars range + const alpha = unicodeLetters; + + // A numeric character is any with the number digit property, or + // underscore. These characters can be included in hashtags, but a hashtag + // cannot have only these characters. + const numeric = `${unicodeDigits}_`; + + // Alphanumeric char is any alpha char or a unicode char with decimal + // number property \p{Nd} + const alphanumeric = alpha + numeric; + const leftChars = '{'; + const rightChars = '}'; + const middleChars = '$'; + + return { + alpha, + alphanumeric, + leftChars, + rightChars, + middleChars + }; +} + +export function getHashtagRegexString(): string { + const { leftChars, rightChars, middleChars } = getHashtagRegexVariableLabels(); + + const hashLeftCharList = `[${leftChars}]`; + const hashRightCharList = `[${rightChars}]`; + const hashMiddleCharList = `[${middleChars}]`; + + // A hashtag contains characters, numbers and underscores, + // but not all numbers. + const hashtag = + `(${hashLeftCharList})` + + `(${hashLeftCharList})` + + `(${hashMiddleCharList})([a-zA-Z0-9_\\.]{0,29})(${hashMiddleCharList})` + + `(${hashRightCharList})(${hashRightCharList})`; + + return hashtag; +} diff --git a/packages/web/components/common/Textarea/PromptEditor/type.d.ts b/packages/web/components/common/Textarea/PromptEditor/type.d.ts index cbf35e6d6b3..a502907dd8e 100644 --- a/packages/web/components/common/Textarea/PromptEditor/type.d.ts +++ b/packages/web/components/common/Textarea/PromptEditor/type.d.ts @@ -6,4 +6,9 @@ export type EditorVariablePickerType = { required?: boolean; icon?: string; valueType?: WorkflowIOValueTypeEnum; + parent?: { + id: string; + label: string; + avatar?: string; + }; }; diff --git a/packages/web/components/common/Textarea/PromptEditor/utils.ts b/packages/web/components/common/Textarea/PromptEditor/utils.ts index 8784087ba0e..b63cb40902c 100644 --- a/packages/web/components/common/Textarea/PromptEditor/utils.ts +++ b/packages/web/components/common/Textarea/PromptEditor/utils.ts @@ -1,9 +1,10 @@ -import type { Klass, LexicalEditor, LexicalNode } from 'lexical'; +import type { DecoratorNode, Klass, LexicalEditor, LexicalNode } from 'lexical'; import type { EntityMatch } from '@lexical/text'; import { $createTextNode, $getRoot, $isTextNode, TextNode } from 'lexical'; import { useCallback } from 'react'; +import { VariableLabelNode } from './plugins/VariableLabelPlugin/node'; -export function registerLexicalTextEntity( +export function registerLexicalTextEntity( editor: LexicalEditor, getMatch: (text: string) => null | EntityMatch, targetNode: Klass, @@ -13,7 +14,7 @@ export function registerLexicalTextEntity( return node instanceof targetNode; }; - const replaceWithSimpleText = (node: TextNode): void => { + const replaceWithSimpleText = (node: TextNode | VariableLabelNode): void => { const textNode = $createTextNode(node.getTextContent()); textNode.setFormat(node.getFormat()); node.replace(textNode); @@ -136,7 +137,7 @@ export function registerLexicalTextEntity( return; } - if (text.length > match.end) { + if (text.length > match.end && $isTextNode(node)) { // This will split out the rest of the text as simple text node.splitText(match.end); @@ -163,7 +164,7 @@ export function registerLexicalTextEntity( }; const removePlainTextTransform = editor.registerNodeTransform(TextNode, textNodeTransform); - const removeReverseNodeTransform = editor.registerNodeTransform( + const removeReverseNodeTransform = editor.registerNodeTransform( targetNode, reverseNodeTransform ); @@ -217,6 +218,8 @@ export function editorStateToText(editor: LexicalEditor) { `); } else if (child.text) { paragraphText.push(child.text); + } else if (child.type === 'variableLabel') { + paragraphText.push(child.variableKey); } }); editorStateTextString.push(paragraphText.join('')); diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 15e6b777b5c..b2521add1a0 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -901,6 +901,7 @@ "overSize": "Team members exceed the limit" } }, + "invalid_variable": "Invalid variable", "navbar": { "Account": "Account", "Chat": "Chat", @@ -1146,6 +1147,7 @@ "Quote Content Tip": "You can customize the structure of the quote content to better adapt to different scenarios. You can use some variables for template configuration:\n{{q}} - search content, {{a}} - expected content, {{source}} - source, {{sourceId}} - source file name, {{index}} - the nth quote, they are all optional, here are the default values:\n{{default}}", "Quote Prompt Tip": "You can use {{quote}} to insert the quote content template, and use {{question}} to insert the question. Here are the default values:\n{{default}}" }, + "unusable_variable": "no usable variable", "user": { "Account": "Account", "Amount of earnings": "Earnings (¥)", @@ -1232,4 +1234,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/web/i18n/en/workflow.json b/packages/web/i18n/en/workflow.json index a388772a492..869f4e5afad 100644 --- a/packages/web/i18n/en/workflow.json +++ b/packages/web/i18n/en/workflow.json @@ -1,31 +1,32 @@ { - "add_new_input": "Add input", "Code": "Code", + "add_new_input": "Add input", + "code": { + "Reset template": "Reset template", + "Reset template confirm": "Are you sure to restore the code template? All input and output to template values will be reset, please be careful to save the current code." + }, "confirm_delete_field_tip": "Confirm to delete the field?", "custom_input": "Custom input", "edit_input": "Edit input", - "field_name_already_exists": "Field name already exists", "field_description": "Field description", "field_description_placeholder": "Describes the functionality of this input field, which affects the quality of model generation if the parameter is called for a tool", + "field_name_already_exists": "Field name already exists", "field_required": "Required", "field_used_as_tool_input": "As tool input", - "input_description": "Input descriotion", - "only_the_reference_type_is_supported": "Only the Reference type is supported", - "optional_value_type": "Optional value type", - "optional_value_type_tip": "One or more data types can be specified, and users can only select the configured type when adding fields in winter", - "tool_input": "Tool", - "code": { - "Reset template": "Reset template", - "Reset template confirm": "Are you sure to restore the code template? All input and output to template values will be reset, please be careful to save the current code." - }, "ifelse": { "Input value": "Input", "Select value": "Select" }, + "input_description": "Input descriotion", + "only_the_reference_type_is_supported": "Only the Reference type is supported", + "optional_value_type": "Optional value type", + "optional_value_type_tip": "One or more data types can be specified, and users can only select the configured type when adding fields in winter", "response": { "Code log": "Log", "Custom inputs": "Custom inputs", "Custom outputs": "Custom outputs", "Error": "Error" - } -} \ No newline at end of file + }, + "tool_input": "Tool", + "variable_picker_tips": "tips: enter node name or variable name to search" +} diff --git a/packages/web/i18n/zh/common.json b/packages/web/i18n/zh/common.json index f07a957b3d3..a1669e88f3a 100644 --- a/packages/web/i18n/zh/common.json +++ b/packages/web/i18n/zh/common.json @@ -901,6 +901,7 @@ "overSize": "团队成员超出上限" } }, + "invalid_variable": "无效变量", "navbar": { "Account": "账号", "Chat": "聊天", @@ -1146,6 +1147,7 @@ "Quote Content Tip": "可以自定义引用内容的结构,以更好的适配不同场景。可以使用一些变量来进行模板配置:\n{{q}} - 检索内容,{{a}} - 预期内容,{{source}} - 来源,{{sourceId}} - 来源文件名,{{index}} - 第 n 个引用,他们都是可选的,下面是默认值:\n{{default}}", "Quote Prompt Tip": "可以用 {{quote}} 来插入引用内容模板,使用 {{question}} 来插入问题。下面是默认值:\n{{default}}" }, + "unusable_variable": "无可用变量", "user": { "Account": "账号", "Amount of earnings": "收益(¥)", @@ -1232,4 +1234,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/web/i18n/zh/workflow.json b/packages/web/i18n/zh/workflow.json index abfd6ca051d..10a01bc9bc9 100644 --- a/packages/web/i18n/zh/workflow.json +++ b/packages/web/i18n/zh/workflow.json @@ -1,31 +1,32 @@ { - "add_new_input": "新增输入", "Code": "代码", + "add_new_input": "新增输入", + "code": { + "Reset template": "还原模板", + "Reset template confirm": "确认还原代码模板?将会重置所有输入和输出至模板值,请注意保存当前代码。" + }, "confirm_delete_field_tip": "确认删除该字段?", "custom_input": "自定义输入", "edit_input": "编辑输入", - "field_name_already_exists": "字段名已经存在", "field_description": "字段描述", "field_description_placeholder": "描述该输入字段的功能,如果为工具调用参数,则该描述会影响模型生成的质量", + "field_name_already_exists": "字段名已经存在", "field_required": "必填", "field_used_as_tool_input": "作为工具调用参数", - "input_description": "字段描述", - "only_the_reference_type_is_supported": "仅支持引用类型", - "optional_value_type": "可选的数据类型", - "optional_value_type_tip": "可以指定 1 个或多个数据类型,用户在冬天添加字段时,仅可选择配置的类型", - "tool_input": "工具参数", - "code": { - "Reset template": "还原模板", - "Reset template confirm": "确认还原代码模板?将会重置所有输入和输出至模板值,请注意保存当前代码。" - }, "ifelse": { "Input value": "输入值", "Select value": "选择值" }, + "input_description": "字段描述", + "only_the_reference_type_is_supported": "仅支持引用类型", + "optional_value_type": "可选的数据类型", + "optional_value_type_tip": "可以指定 1 个或多个数据类型,用户在冬天添加字段时,仅可选择配置的类型", "response": { "Code log": "Log 日志", "Custom inputs": "自定义输入", "Custom outputs": "自定义输出", "Error": "错误信息" - } -} \ No newline at end of file + }, + "tool_input": "工具参数", + "variable_picker_tips": "tips: 可输入节点名或变量名搜索" +} diff --git a/projects/app/public/imgs/workflow/variable.png b/projects/app/public/imgs/workflow/variable.png index 15a9f01d749..9d942611505 100644 Binary files a/projects/app/public/imgs/workflow/variable.png and b/projects/app/public/imgs/workflow/variable.png differ diff --git a/projects/app/src/components/core/app/DatasetParamsModal.tsx b/projects/app/src/components/core/app/DatasetParamsModal.tsx index 4172df1c497..58221c3803f 100644 --- a/projects/app/src/components/core/app/DatasetParamsModal.tsx +++ b/projects/app/src/components/core/app/DatasetParamsModal.tsx @@ -13,7 +13,6 @@ import { } from '@chakra-ui/react'; import { useForm } from 'react-hook-form'; import MySlider from '@/components/Slider'; -import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyModal from '@fastgpt/web/components/common/MyModal'; import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants'; import { useTranslation } from 'next-i18next'; diff --git a/projects/app/src/components/core/app/WelcomeTextConfig.tsx b/projects/app/src/components/core/app/WelcomeTextConfig.tsx index dba4f443785..ffe4c3cdba5 100644 --- a/projects/app/src/components/core/app/WelcomeTextConfig.tsx +++ b/projects/app/src/components/core/app/WelcomeTextConfig.tsx @@ -18,7 +18,6 @@ const WelcomeTextConfig = (props: TextareaProps) => { ({ + ...item, + parent: { + id: 'VARIABLE_NODE_ID', + label: '全局变量', + avatar: '/imgs/workflow/variable.png' + } + })), [appForm.chatConfig.variables, t] ); diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Textarea.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Textarea.tsx index f643124e5a1..dc4554aa263 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Textarea.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/render/RenderInput/templates/Textarea.tsx @@ -5,13 +5,15 @@ import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor'; import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context'; -import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils'; +import { computedNodeInputReference } from '@/web/core/workflow/utils'; import { useCreation } from 'ahooks'; import { AppContext } from '@/pages/app/detail/components/context'; +import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => { const { t } = useTranslation(); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); + const edges = useContextSelector(WorkflowContext, (v) => v.edges); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const getNodeDynamicInputs = useContextSelector(WorkflowContext, (v) => v.getNodeDynamicInputs); @@ -19,20 +21,50 @@ const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => { // get variable const variables = useCreation(() => { - const globalVariables = getWorkflowGlobalVariables({ - nodes: nodeList, - chatConfig: appDetail.chatConfig, - t - }); - + const currentNode = nodeList.find((node) => node.nodeId === nodeId); const nodeVariables = formatEditorVariablePickerIcon( getNodeDynamicInputs(nodeId).map((item) => ({ key: item.key, - label: item.label + label: item.label, + parent: { + id: currentNode?.nodeId, + label: currentNode?.name, + avatar: currentNode?.avatar + } })) ); - return [...globalVariables, ...nodeVariables]; + const sourceNodes = computedNodeInputReference({ + nodeId, + nodes: nodeList, + edges: edges, + chatConfig: appDetail.chatConfig, + t + }); + + const sourceNodeVariables = !sourceNodes + ? [] + : sourceNodes + .map((node) => { + return node.outputs + .filter((output) => !!output.label) + .map((output) => { + return { + label: t((output.label as any) || ''), + key: output.id, + parent: { + id: node.nodeId, + label: node.name, + avatar: node.avatar + } + }; + }); + }) + .flat(); + + const formatSourceNodeVariables = formatEditorVariablePickerIcon(sourceNodeVariables); + + return [...nodeVariables, ...formatSourceNodeVariables]; }, [nodeList, inputs, t]); const onChange = useCallback(