Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: get node variables in prompt editor #2087

Merged
merged 10 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/global/common/string/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEFAULT_PARENT_ID = 'DEFAULT';
45 changes: 45 additions & 0 deletions packages/global/common/string/tools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import crypto from 'crypto';
import { customAlphabet } from 'nanoid';
import { DEFAULT_PARENT_ID } from './constant';
import { RuntimeNodeItemType } from '../../core/workflow/runtime/type';

/* check string is a web link */
export function strIsLink(str?: string) {
Expand Down Expand Up @@ -36,6 +38,49 @@ export function replaceVariable(text: any, obj: Record<string, string | number>)
if (!['string', 'number'].includes(typeof val)) continue;

text = text.replace(new RegExp(`{{(${key})}}`, 'g'), String(val));
text = text.replace(
new RegExp(`\\{\\{\\$(${DEFAULT_PARENT_ID}\\.${key})\\$\\}\\}`, 'g'),
String(val)
);
}
return text || '';
}

export function replaceVariableLabel(
text: any,
nodes: RuntimeNodeItemType[],
obj: Record<string, string | number>
) {
if (!(typeof text === 'string')) return text;

const globalVariables = Object.keys(obj).map((key) => {
return {
nodeId: 'VARIABLE_NODE_ID',
id: key,
value: obj[key]
};
});

const nodeVariables = nodes
.map((node) => {
return node.outputs.map((output) => {
return {
nodeId: node.nodeId,
id: output.id,
value: output.value
};
});
})
.flat();

const allVariables = [...globalVariables, ...nodeVariables];

for (const key in allVariables) {
const val = allVariables[key];
if (!['string', 'number'].includes(typeof val.value)) continue;
const regex = new RegExp(`\\{\\{\\$(${val.nodeId}\\.${val.id})\\$\\}\\}`, 'g');

text = text.replace(regex, String(val.value));
}
return text || '';
}
Expand Down
7 changes: 5 additions & 2 deletions packages/service/core/workflow/dispatch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { replaceVariable } from '@fastgpt/global/common/string/tools';
import { replaceVariable, replaceVariableLabel } from '@fastgpt/global/common/string/tools';
import { responseWriteNodeStatus } from '../../../common/response';
import { getSystemTime } from '@fastgpt/global/common/time/timezone';

Expand Down Expand Up @@ -288,9 +288,12 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
node.inputs.forEach((input) => {
if (input.key === dynamicInput?.key) return;

// replace {{}} variables
// replace {{}} variables & {{$DEFAULT.xx$}} variables
let value = replaceVariable(input.value, variables);

// replace {{$$}} variables
value = replaceVariableLabel(value, runtimeNodes, variables);

// replace reference variables
value = getReferenceVariableValue({
value,
Expand Down
1 change: 1 addition & 0 deletions packages/web/components/common/Icon/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ export const iconPaths = {
'core/workflow/template/textConcat': () =>
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': () =>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -129,6 +131,7 @@ export default function Editor({
/>
<VariablePickerPlugin variables={variables} />
<VariablePlugin variables={variables} />
<VariableLabelPlugin variables={variables} />
<OnBlurPlugin onBlur={onBlur} />
</LexicalComposer>
{showResize && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ChevronRightIcon } from '@chakra-ui/icons';
import { Box, Flex } from '@chakra-ui/react';
import { DEFAULT_PARENT_ID } from '@fastgpt/global/common/string/constant';
import MyIcon from '../../../../../../../components/common/Icon';

export default function VariableLabel({
variableKey,
variableLabel,
nodeAvatar
}: {
variableKey: string;
variableLabel: string;
nodeAvatar: string;
}) {
const [parentLabel, childLabel] = variableLabel.split('.');
return (
<>
<Box
display="inline-flex"
alignItems="center"
m={'2px'}
rounded={'4px'}
px={1.5}
py={'1px'}
bg={parentLabel !== 'undefined' ? 'primary.50' : 'red.50'}
color={parentLabel !== 'undefined' ? 'myGray.900' : 'red.600'}
cursor={'pointer'}
>
{parentLabel !== 'undefined' ? (
<>
<Flex hidden={parentLabel === DEFAULT_PARENT_ID} alignItems={'center'}>
<Box mr={1}>
<MyIcon name={nodeAvatar as any} w={'16px'} rounded={'2.8px'} mt={'2.5px'} />
</Box>
{parentLabel}
<ChevronRightIcon />
</Flex>
<Box>{childLabel}</Box>
</>
) : (
<>
<Box>{'无效变量'}</Box>
</>
)}
</Box>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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';
import { DEFAULT_PARENT_ID } from '@fastgpt/global/common/string/constant';

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 || parentKey === DEFAULT_PARENT_ID) &&
item.key === childrenKey
);
const variableLabel = `${currentVariable && (currentVariable.parent?.label || DEFAULT_PARENT_ID)}.${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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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<JSX.Element> {
__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 (
<VariableLabel
variableKey={this.__variableKey}
variableLabel={this.__variableLabel}
nodeAvatar={this.__nodeAvatar}
/>
);
}
}

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;
}
Loading
Loading