Files
FastGPT/packages/web/components/common/Textarea/PromptEditor/utils.ts
Archer 439c819ff1 4.8 preview (#1288)
* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* perf: workflow ux

* system config

* Newflow (#89)

* docs: Add doc for Xinference (#1266)

Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* perf: workflow ux

* system config

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* rename code

* move code

* update flow

* input type selector

* perf: workflow runtime

* feat: node adapt newflow

* feat: adapt plugin

* feat: 360 connection

* check workflow

* perf: flow 性能

* change plugin input type (#81)

* change plugin input type

* plugin label mode

* perf: nodecard

* debug

* perf: debug ui

* connection ui

* change workflow ui (#82)

* feat: workflow debug

* adapt openAPI for new workflow (#83)

* adapt openAPI for new workflow

* i18n

* perf: plugin debug

* plugin input ui

* delete

* perf: global variable select

* fix rebase

* perf: workflow performance

* feat: input render type icon

* input icon

* adapt flow (#84)

* adapt newflow

* temp

* temp

* fix

* feat: app schedule trigger

* feat: app schedule trigger

* perf: schedule ui

* feat: ioslatevm run js code

* perf: workflow varialbe table ui

* feat: adapt simple mode

* feat: adapt input params

* output

* feat: adapt tamplate

* fix: ts

* add if-else module (#86)

* perf: worker

* if else node

* perf: tiktoken worker

* fix: ts

* perf: tiktoken

* fix if-else node (#87)

* fix if-else node

* type

* fix

* perf: audio render

* perf: Parallel worker

* log

* perf: if else node

* adapt plugin

* prompt

* perf: reference ui

* reference ui

* handle ux

* template ui and plugin tool

* adapt v1 workflow

* adapt v1 workflow completions

* perf: time variables

* feat: workflow keyboard shortcuts

* adapt v1 workflow

* update workflow example doc (#88)

* fix: simple mode select tool

---------

Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
Co-authored-by: Carson Yang <yangchuansheng33@gmail.com>
Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>

* doc

* perf: extract node

* extra node field

* update plugin version

* doc

* variable

* change doc & fix prompt editor (#90)

* fold workflow code

* value type label

---------

Signed-off-by: Carson Yang <yangchuansheng33@gmail.com>
Co-authored-by: Carson Yang <yangchuansheng33@gmail.com>
Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
2024-04-25 17:51:20 +08:00

287 lines
7.9 KiB
TypeScript

import type { Klass, LexicalEditor, LexicalNode } from 'lexical';
import type { EntityMatch } from '@lexical/text';
import { $createTextNode, $getRoot, $isTextNode, TextNode } from 'lexical';
import { useCallback } from 'react';
export function registerLexicalTextEntity<T extends TextNode>(
editor: LexicalEditor,
getMatch: (text: string) => null | EntityMatch,
targetNode: Klass<T>,
createNode: (textNode: TextNode) => T
): Array<() => void> {
const isTargetNode = (node: LexicalNode | null | undefined): node is T => {
return node instanceof targetNode;
};
const replaceWithSimpleText = (node: TextNode): void => {
const textNode = $createTextNode(node.getTextContent());
textNode.setFormat(node.getFormat());
node.replace(textNode);
};
const getMode = (node: TextNode): number => {
return node.getLatest().__mode;
};
const textNodeTransform = (node: TextNode) => {
if (!node.isSimpleText()) {
return;
}
const prevSibling = node.getPreviousSibling();
let text = node.getTextContent();
let currentNode = node;
let match;
if ($isTextNode(prevSibling)) {
const previousText = prevSibling.getTextContent();
const combinedText = previousText + text;
const prevMatch = getMatch(combinedText);
if (isTargetNode(prevSibling)) {
if (prevMatch === null || getMode(prevSibling) !== 0) {
replaceWithSimpleText(prevSibling);
return;
} else {
const diff = prevMatch.end - previousText.length;
if (diff > 0) {
const concatText = text.slice(0, diff);
const newTextContent = previousText + concatText;
prevSibling.select();
prevSibling.setTextContent(newTextContent);
if (diff === text.length) {
node.remove();
} else {
const remainingText = text.slice(diff);
node.setTextContent(remainingText);
}
return;
}
}
} else if (prevMatch === null || prevMatch.start < previousText.length) {
return;
}
}
// eslint-disable-next-line no-constant-condition
while (true) {
match = getMatch(text);
let nextText = match === null ? '' : text.slice(match.end);
text = nextText;
if (nextText === '') {
const nextSibling = currentNode.getNextSibling();
if ($isTextNode(nextSibling)) {
nextText = currentNode.getTextContent() + nextSibling.getTextContent();
const nextMatch = getMatch(nextText);
if (nextMatch === null) {
if (isTargetNode(nextSibling)) {
replaceWithSimpleText(nextSibling);
} else {
nextSibling.markDirty();
}
return;
} else if (nextMatch.start !== 0) {
return;
}
}
} else {
const nextMatch = getMatch(nextText);
if (nextMatch !== null && nextMatch.start === 0) {
return;
}
}
if (match === null) {
return;
}
if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity()) {
continue;
}
let nodeToReplace;
if (match.start === 0) {
[nodeToReplace, currentNode] = currentNode.splitText(match.end);
} else {
[, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end);
}
const replacementNode = createNode(nodeToReplace);
replacementNode.setFormat(nodeToReplace.getFormat());
nodeToReplace.replace(replacementNode);
if (currentNode == null) {
return;
}
}
};
const reverseNodeTransform = (node: T) => {
const text = node.getTextContent();
const match = getMatch(text);
if (match === null || match.start !== 0) {
replaceWithSimpleText(node);
return;
}
if (text.length > match.end) {
// This will split out the rest of the text as simple text
node.splitText(match.end);
return;
}
const prevSibling = node.getPreviousSibling();
if ($isTextNode(prevSibling) && prevSibling.isTextEntity()) {
replaceWithSimpleText(prevSibling);
replaceWithSimpleText(node);
}
const nextSibling = node.getNextSibling();
if ($isTextNode(nextSibling) && nextSibling.isTextEntity()) {
replaceWithSimpleText(nextSibling);
// This may have already been converted in the previous block
if (isTargetNode(node)) {
replaceWithSimpleText(node);
}
}
};
const removePlainTextTransform = editor.registerNodeTransform(TextNode, textNodeTransform);
const removeReverseNodeTransform = editor.registerNodeTransform<T>(
targetNode,
reverseNodeTransform
);
return [removePlainTextTransform, removeReverseNodeTransform];
}
export function textToEditorState(text = '') {
const paragraph = typeof text === 'string' ? text?.split('\n') : [''];
return JSON.stringify({
root: {
children: paragraph.map((p) => {
return {
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: p,
type: 'text',
version: 1
}
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1
};
}),
direction: 'ltr',
format: '',
indent: 0,
type: 'root',
version: 1
}
});
}
export function editorStateToText(editor: LexicalEditor) {
const editorStateTextString: string[] = [];
const paragraphs = editor.getEditorState().toJSON().root.children;
paragraphs.forEach((paragraph: any) => {
const children = paragraph.children;
const paragraphText: string[] = [];
children.forEach((child: any) => {
if (child.type === 'linebreak') {
paragraphText.push(`
`);
} else if (child.text) {
paragraphText.push(child.text);
}
});
editorStateTextString.push(paragraphText.join(''));
});
return editorStateTextString.join(`
`);
}
const varRegex = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
export const getVars = (value: string) => {
if (!value) return [];
// .filter((item) => {
// return ![CONTEXT_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT, QUERY_PLACEHOLDER_TEXT, PRE_PROMPT_PLACEHOLDER_TEXT].includes(item)
// })
const keys =
value
.match(varRegex)
?.map((item) => {
return item.replace('{{', '').replace('}}', '');
})
.filter((key) => key.length <= 10) || [];
const keyObj: Record<string, boolean> = {};
// remove duplicate keys
const res: string[] = [];
keys.forEach((key) => {
if (keyObj[key]) return;
keyObj[key] = true;
res.push(key);
});
return res;
};
export type MenuTextMatch = {
leadOffset: number;
matchingString: string;
replaceableString: string;
};
export type TriggerFn = (text: string, editor: LexicalEditor) => MenuTextMatch | null;
export const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;';
export function useBasicTypeaheadTriggerMatch(
trigger: string,
{ minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number }
): TriggerFn {
return useCallback(
(text: string) => {
const validChars = `[^${trigger}${PUNCTUATION}\\s]`;
const TypeaheadTriggerRegex = new RegExp(
`([^${trigger}]|^)(` + `[${trigger}]` + `((?:${validChars}){0,${maxLength}})` + ')$'
);
const match = TypeaheadTriggerRegex.exec(text);
if (match !== null) {
const maybeLeadingWhitespace = match[1];
const matchingString = match[3];
if (matchingString.length >= minLength) {
return {
leadOffset: match.index + maybeLeadingWhitespace.length,
matchingString,
replaceableString: match[2]
};
}
}
return null;
},
[maxLength, minLength, trigger]
);
}