Skip to content

Commit

Permalink
feat: get node variables in prompt editor
Browse files Browse the repository at this point in the history
  • Loading branch information
newfish-cmyk committed Jul 18, 2024
1 parent 5cb1965 commit 63fd5f5
Show file tree
Hide file tree
Showing 11 changed files with 659 additions and 51 deletions.
40 changes: 40 additions & 0 deletions packages/global/common/string/tools.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RuntimeNodeItemType } from 'core/workflow/runtime/type';
import crypto from 'crypto';
import { customAlphabet } from 'nanoid';

Expand Down Expand Up @@ -40,6 +41,45 @@ export function replaceVariable(text: any, obj: Record<string, string | number>)
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 || '';
}

/* replace sensitive text */
export const replaceSensitiveText = (text: string) => {
// 1. http link
Expand Down
5 changes: 4 additions & 1 deletion 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 @@ -291,6 +291,9 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
// replace {{}} variables
let value = replaceVariable(input.value, variables);

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

// replace reference variables
value = getReferenceVariableValue({
value,
Expand Down
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,34 @@
import { ChevronRightIcon } from '@chakra-ui/icons';

export default function VariableLabel({
variableKey,
variableLabel,
nodeAvatar
}: {
variableKey: string;
variableLabel: string;
nodeAvatar: string;
}) {
const [parentLabel, childLabel] = variableLabel.split('.');

return (
<span
style={{
display: 'inline-block',
margin: '0 2px',
border: '1px solid #E8EBF0',
borderRadius: '4px',
padding: '2px 4px'
}}
>
<span>
<img src={nodeAvatar} style={{ width: '16px', display: 'inline', margin: '0 2px' }} />
</span>
<span>{parentLabel}</span>
<span>
<ChevronRightIcon />
</span>
<span>{childLabel}</span>
</span>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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 variableKeys: Array<string> = useMemo(() => {
return variables.map((item) => `${item.parent?.id}.${item.key}`);
}, [variables]);

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?.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;
}
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

0 comments on commit 63fd5f5

Please sign in to comment.