V4.12.3 features (#5595)

* refactor: remove ModelProviderIdType and update related types (#5549)

* perf: model provider

* fix eval create split (#5570)

* git rebase --continuedoc

* add more variable types (#5540)

* variable types

* password

* time picker

* internal var

* file

* fix-test

* time select default value & range

* password & type render

* fix

* fix build

* fix

* move method

* split date select

* icon

* perf: variable code

* prompt editor add markdown plugin (#5556)

* editor markdown

* fix build

* pnpm lock

* add props

* update code

* fix list

* editor ui

* fix variable reset (#5586)

* perf: variables type code

* customize lexical indent (#5588)

* perf: multiple selector

* perf: tab plugin

* doc

* refactor: update workflow constants to use ToolTypeEnum (#5491)

* refactor: replace FlowNodeTemplateTypeEnum with string literals in workflow templates

* perf: tool type

---------

Co-authored-by: archer <545436317@qq.com>

* update doc

* fix: make table's row more natural while dragging it (#5596)

* feat: add APIGetTemplate function and refactor template fetching logic (#5498)

* feat: add APIGetTemplate function and refactor template fetching logic

* chore: adjust the code

* chore: update sdk

---------

Co-authored-by: FinleyGe <m13203533462@163.com>

* perf init system

* doc

* remove log

* remove i18n

* perf: variables render

---------

Co-authored-by: Ctrlz <143257420+ctrlz526@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>
Co-authored-by: FinleyGe <m13203533462@163.com>
This commit is contained in:
Archer
2025-09-07 14:41:48 +08:00
committed by GitHub
parent c747fc03ad
commit 3f9b0fa1d4
166 changed files with 3407 additions and 11604 deletions

View File

@@ -9,20 +9,26 @@
import { useMemo, useState, useTransition } from 'react';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { ListItemNode, ListNode } from '@lexical/list';
import { CodeHighlightNode, CodeNode } from '@lexical/code';
import VariableLabelPickerPlugin from './plugins/VariableLabelPickerPlugin';
import ListDisplayFixPlugin from './plugins/ListDisplayFixPlugin';
import { Box, Flex } from '@chakra-ui/react';
import styles from './index.module.scss';
import VariablePlugin from './plugins/VariablePlugin';
import { VariableNode } from './plugins/VariablePlugin/node';
import type { EditorState, LexicalEditor } from 'lexical';
import OnBlurPlugin from './plugins/OnBlurPlugin';
import MyIcon from '../../Icon';
import type { FormPropsType } from './type.d';
import { type EditorVariableLabelPickerType, type EditorVariablePickerType } from './type.d';
import type { FormPropsType } from './type';
import { type EditorVariableLabelPickerType, type EditorVariablePickerType } from './type';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import FocusPlugin from './plugins/FocusPlugin';
import { textToEditorState } from './utils';
@@ -31,8 +37,38 @@ import { VariableLabelNode } from './plugins/VariableLabelPlugin/node';
import VariableLabelPlugin from './plugins/VariableLabelPlugin';
import { useDeepCompareEffect } from 'ahooks';
import VariablePickerPlugin from './plugins/VariablePickerPlugin';
import MarkdownPlugin from './plugins/MarkdownPlugin';
import MyIcon from '../../Icon';
import TabToSpacesPlugin from './plugins/TabToSpacesPlugin';
import ListExitPlugin from './plugins/ListExitPlugin';
const Placeholder = ({ children }: { children: React.ReactNode }) => (
<Box
position={'absolute'}
top={0}
left={0}
right={0}
bottom={0}
py={3}
px={3.5}
pointerEvents={'none'}
overflow={'hidden'}
>
<Box
color={'myGray.400'}
fontSize={'mini'}
userSelect={'none'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
h={'100%'}
>
{children}
</Box>
</Box>
);
export type EditorProps = {
isRichText?: boolean;
variables?: EditorVariablePickerType[];
variableLabels?: EditorVariableLabelPickerType[];
value?: string;
@@ -50,6 +86,7 @@ export type EditorProps = {
};
export default function Editor({
isRichText = false,
minH = 200,
maxH = 400,
maxLength,
@@ -71,7 +108,7 @@ export default function Editor({
onOpenModal?: () => void;
onChange: (editorState: EditorState, editor: LexicalEditor) => void;
onChangeText?: ((text: string) => void) | undefined;
onBlur: (editor: LexicalEditor) => void;
onBlur?: (editor: LexicalEditor) => void;
}) {
const [key, setKey] = useState(getNanoid(6));
const [_, startSts] = useTransition();
@@ -79,8 +116,17 @@ export default function Editor({
const [scrollHeight, setScrollHeight] = useState(0);
const initialConfig = {
namespace: 'promptEditor',
nodes: [VariableNode, VariableLabelNode],
namespace: isRichText ? 'richPromptEditor' : 'promptEditor',
nodes: [
VariableNode,
VariableLabelNode,
HeadingNode,
ListNode,
ListItemNode,
QuoteNode,
CodeNode,
CodeHighlightNode
],
editorState: textToEditorState(value),
onError: (error: Error) => {
throw error;
@@ -125,59 +171,75 @@ export default function Editor({
borderRadius={'md'}
>
<LexicalComposer initialConfig={initialConfig} key={key}>
<PlainTextPlugin
contentEditable={
<ContentEditable
className={isInvalid ? styles.contentEditable_invalid : styles.contentEditable}
style={{
minHeight: `${minH}px`,
maxHeight: `${maxH}px`
}}
/>
}
placeholder={
<Box
position={'absolute'}
top={0}
left={0}
right={0}
bottom={0}
py={3}
px={3.5}
pointerEvents={'none'}
overflow={'hidden'}
>
<Box
color={'myGray.400'}
fontSize={'mini'}
userSelect={'none'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
h={'100%'}
>
{placeholder}
</Box>
</Box>
}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin />
<MaxLengthPlugin maxLength={maxLength || 999999} />
<FocusPlugin focus={focus} setFocus={setFocus} />
<OnChangePlugin
onChange={(editorState, editor) => {
const rootElement = editor.getRootElement();
setScrollHeight(rootElement?.scrollHeight || 0);
startSts(() => {
onChange?.(editorState, editor);
});
}}
/>
<VariableLabelPlugin variables={variableLabels} />
<VariablePlugin variables={variables} />
<VariableLabelPickerPlugin variables={variableLabels} isFocus={focus} />
<VariablePickerPlugin variables={variableLabels.length > 0 ? [] : variables} />
<OnBlurPlugin onBlur={onBlur} />
{/* Text type */}
{isRichText ? (
<RichTextPlugin
contentEditable={
<ContentEditable
className={`${isInvalid ? styles.contentEditable_invalid : styles.contentEditable} ${styles.richText}`}
style={{
minHeight: `${minH}px`,
maxHeight: `${maxH}px`
}}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
/>
}
placeholder={<Placeholder>{placeholder}</Placeholder>}
ErrorBoundary={LexicalErrorBoundary}
/>
) : (
<PlainTextPlugin
contentEditable={
<ContentEditable
className={isInvalid ? styles.contentEditable_invalid : styles.contentEditable}
style={{
minHeight: `${minH}px`,
maxHeight: `${maxH}px`
}}
/>
}
placeholder={<Placeholder>{placeholder}</Placeholder>}
ErrorBoundary={LexicalErrorBoundary}
/>
)}
{/* Basic Plugin */}
<>
<HistoryPlugin />
<MaxLengthPlugin maxLength={maxLength || 999999} />
<FocusPlugin focus={focus} setFocus={setFocus} />
<VariablePlugin variables={variables} />
{variableLabels.length > 0 && (
<>
<VariableLabelPlugin variables={variableLabels} />
<VariableLabelPickerPlugin variables={variableLabels} isFocus={focus} />
</>
)}
{variableLabels.length > 0 && <VariablePickerPlugin variables={variables} />}
<OnBlurPlugin onBlur={onBlur} />
<ListDisplayFixPlugin />
<OnChangePlugin
onChange={(editorState, editor) => {
const rootElement = editor.getRootElement();
setScrollHeight(rootElement?.scrollHeight || 0);
startSts(() => {
onChange?.(editorState, editor);
});
}}
/>
{isRichText && (
<>
{/* <ListPlugin />
<CheckListPlugin />
<ListExitPlugin /> */}
<TabToSpacesPlugin />
{/* <MarkdownPlugin /> */}
</>
)}
</>
</LexicalComposer>
{onChangeText &&

View File

@@ -76,3 +76,14 @@
color: var(--chakra-colors-primary-600);
padding: 0 2px;
}
.richText {
ul,
ol {
padding-left: 16px;
li::marker {
color: var(--chakra-colors-primary-600);
}
}
}

View File

@@ -1,13 +1,12 @@
import { Box, Button, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react';
import React, { useMemo } from 'react';
import React, { useMemo, useCallback } from 'react';
import { editorStateToText } from './utils';
import type { EditorProps } from './Editor';
import Editor from './Editor';
import MyModal from '../../MyModal';
import { useTranslation } from 'next-i18next';
import type { EditorState, LexicalEditor } from 'lexical';
import type { FormPropsType } from './type.d';
import { useCallback } from 'react';
import type { FormPropsType } from './type';
const PromptEditor = ({
showOpenModal = true,
@@ -34,18 +33,27 @@ const PromptEditor = ({
},
[onChange]
);
const onBlurInput = useCallback(
(editor: LexicalEditor) => {
const text = editorStateToText(editor);
onBlur?.(text);
if (onBlur) {
const text = editorStateToText(editor);
onBlur(text);
}
},
[onBlur]
);
const formattedValue = useMemo(() => {
if (typeof value === 'object') {
return JSON.stringify(value);
}
return value;
if (value === undefined || value === null) {
return '';
}
return String(value || '');
}, [value]);
return (
@@ -74,6 +82,7 @@ const PromptEditor = ({
/>
)}
</Box>
<MyModal
isOpen={isOpen}
onClose={onClose}
@@ -102,4 +111,5 @@ const PromptEditor = ({
</>
);
};
export default React.memo(PromptEditor);

View File

@@ -1,93 +0,0 @@
import { Box, Flex } from '@chakra-ui/react';
import { type EditorVariablePickerType } from '../../type';
import MyIcon from '../../../../Icon';
import React, { useCallback, useEffect } from 'react';
export default function DropDownMenu({
variables,
setDropdownValue
}: {
variables: EditorVariablePickerType[];
setDropdownValue?: (value: string) => void;
}) {
const [highlightedIndex, setHighlightedIndex] = React.useState(0);
const handleKeyDown = useCallback(
(event: any) => {
if (event.keyCode === 38) {
setHighlightedIndex((prevIndex) => Math.max(prevIndex - 1, 0));
} else if (event.keyCode === 40) {
setHighlightedIndex((prevIndex) => Math.min(prevIndex + 1, variables.length - 1));
} else if (event.keyCode === 13 && variables[highlightedIndex]?.key) {
setDropdownValue?.(variables[highlightedIndex].key);
}
},
[highlightedIndex, variables]
);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);
return variables.length ? (
<Box
bg={'white'}
boxShadow={'lg'}
borderWidth={'1px'}
borderColor={'borderColor.base'}
p={2}
borderRadius={'md'}
position={'absolute'}
top={'100%'}
w={'auto'}
zIndex={99999}
maxH={'300px'}
overflow={'auto'}
className="nowheel"
>
{variables.map((item, index) => (
<Flex
alignItems={'center'}
as={'li'}
key={item.key}
px={4}
py={2}
borderRadius={'sm'}
cursor={'pointer'}
maxH={'300px'}
overflow={'auto'}
_notLast={{
mb: 2
}}
{...(highlightedIndex === index
? {
bg: 'primary.50',
color: 'primary.600'
}
: {
bg: 'white',
color: 'myGray.600'
})}
onMouseDown={(e) => {
e.preventDefault();
setDropdownValue?.(item.key);
}}
onMouseEnter={() => {
setHighlightedIndex(index);
}}
>
<MyIcon name={(item.icon as any) || 'core/modules/variable'} w={'14px'} />
<Box ml={2} fontSize={'sm'}>
{item.key}
{item.key !== item.label && `(${item.label})`}
</Box>
</Flex>
))}
</Box>
) : null;
}

View File

@@ -0,0 +1,66 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect } from 'react';
export default function ListDisplayFixPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
const fixListDisplay = () => {
const rootElement = editor.getRootElement();
if (!rootElement) return;
const allListItems = rootElement.querySelectorAll('li');
allListItems.forEach((li) => {
const htmlLi = li as HTMLLIElement;
// Check if this li only contains a sublist without direct text content
const hasDirectText = Array.from(htmlLi.childNodes).some((node) => {
return node.nodeType === Node.TEXT_NODE && node.textContent?.trim();
});
const hasSpan = htmlLi.querySelector(':scope > span');
const hasOnlySublist =
htmlLi.children.length === 1 &&
(htmlLi.children[0].tagName === 'UL' || htmlLi.children[0].tagName === 'OL');
// If this li only contains a sublist without text content, hide its marker
if (!hasDirectText && !hasSpan && hasOnlySublist) {
// Only hide the marker, don't adjust position, let CSS handle indentation
htmlLi.style.listStyle = 'none';
htmlLi.style.paddingLeft = '0';
htmlLi.style.marginLeft = '0';
// Keep normal indentation for sublists
const sublist = htmlLi.children[0] as HTMLElement;
sublist.style.marginTop = '0';
sublist.style.marginBottom = '0';
// Don't modify marginLeft and paddingLeft, let CSS handle it
} else {
htmlLi.style.listStyle = '';
htmlLi.style.paddingLeft = '';
htmlLi.style.marginLeft = '';
if (
htmlLi.children[0] &&
(htmlLi.children[0].tagName === 'UL' || htmlLi.children[0].tagName === 'OL')
) {
const sublist = htmlLi.children[0] as HTMLElement;
sublist.style.marginTop = '';
sublist.style.marginBottom = '';
}
}
});
};
const removeListener = editor.registerUpdateListener(() => {
setTimeout(fixListDisplay, 10);
});
setTimeout(fixListDisplay, 10);
return removeListener;
}, [editor]);
return null;
}

View File

@@ -0,0 +1,135 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect } from 'react';
import {
$getSelection,
$isRangeSelection,
COMMAND_PRIORITY_HIGH,
KEY_ENTER_COMMAND,
KEY_BACKSPACE_COMMAND,
$createParagraphNode
} from 'lexical';
import { $isListItemNode, $isListNode } from '@lexical/list';
export default function ListExitPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
const handleEnterKey = () => {
let handled = false;
editor.update(() => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return;
}
const anchorNode = selection.anchor.getNode();
const listItemNode = anchorNode.getParent();
if ($isListItemNode(listItemNode)) {
// Check if the list item is empty
const textContent = listItemNode.getTextContent().trim();
if (textContent === '') {
// Remove the empty list item and exit list mode
const listNode = listItemNode.getParent();
if ($isListNode(listNode)) {
// If this is the only item in the list, remove the entire list
if (listNode.getChildrenSize() === 1) {
listNode.remove();
} else {
// Remove just this list item
listItemNode.remove();
}
// Insert a paragraph after the list to exit list mode
const paragraph = $createParagraphNode();
if (listNode && !listNode.isAttached()) {
// If we removed the entire list, replace it with a paragraph
listNode.getParent()?.append(paragraph);
} else {
// Insert paragraph after the list
listNode?.insertAfter(paragraph);
}
paragraph.select();
handled = true;
}
}
}
});
return handled;
};
const handleBackspaceKey = (event: KeyboardEvent) => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
const anchorNode = selection.anchor.getNode();
const listItemNode = anchorNode.getParent();
if ($isListItemNode(listItemNode)) {
// Check if cursor is at the beginning of an empty list item
const textContent = listItemNode.getTextContent().trim();
const cursorOffset = selection.anchor.offset;
// Only handle empty list items with cursor at the beginning
if (textContent === '' && cursorOffset === 0) {
// Prevent default backspace behavior
event.preventDefault();
event.stopPropagation();
editor.update(() => {
const listNode = listItemNode.getParent();
if ($isListNode(listNode)) {
// Create a new paragraph
const paragraph = $createParagraphNode();
// Always insert after the current list item and remove it
// This ensures the paragraph appears at the current position
listItemNode.insertAfter(paragraph);
listItemNode.remove();
// If the list is now empty, remove it
if (listNode.getChildrenSize() === 0) {
listNode.remove();
}
// Focus the new paragraph
paragraph.select();
}
});
return true;
}
}
return false;
};
// Register the keyboard event handlers
const removeEnterListener = editor.registerCommand(
KEY_ENTER_COMMAND,
handleEnterKey,
COMMAND_PRIORITY_HIGH
);
const removeBackspaceListener = editor.registerCommand(
KEY_BACKSPACE_COMMAND,
handleBackspaceKey,
COMMAND_PRIORITY_HIGH
);
return () => {
removeEnterListener();
removeBackspaceListener();
};
}, [editor]);
return null;
}

View File

@@ -0,0 +1,8 @@
import type { JSX } from 'react';
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin';
import * as React from 'react';
import { RICH_PROMPT_TRANSFORMERS } from '../MarkdownTransformers';
export default function MarkdownPlugin(): JSX.Element {
return <MarkdownShortcutPlugin transformers={RICH_PROMPT_TRANSFORMERS} />;
}

View File

@@ -0,0 +1,12 @@
import {
CHECK_LIST,
ELEMENT_TRANSFORMERS,
TEXT_FORMAT_TRANSFORMERS,
type Transformer
} from '@lexical/markdown';
export const RICH_PROMPT_TRANSFORMERS: Array<Transformer> = [
CHECK_LIST,
...ELEMENT_TRANSFORMERS,
...TEXT_FORMAT_TRANSFORMERS
];

View File

@@ -0,0 +1,216 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
KEY_TAB_COMMAND,
COMMAND_PRIORITY_EDITOR,
$getSelection,
$isRangeSelection,
$isTextNode
} from 'lexical';
import { $createTextNode } from 'lexical';
import { $isListNode, $isListItemNode } from '@lexical/list';
import { useEffect } from 'react';
export default function TabToSpacesPlugin(): null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
return editor.registerCommand(
KEY_TAB_COMMAND,
(event) => {
try {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
// Check if we're in a list context
let isInList = false;
try {
const nodes = selection.getNodes();
isInList = nodes.some((node) => {
// Check if current node or any of its ancestors is a list or list item
let currentNode = node;
while (currentNode) {
try {
if ($isListNode(currentNode) || $isListItemNode(currentNode)) {
return true;
}
// @ts-ignore
currentNode = currentNode.getParent();
} catch (e) {
// If node is no longer valid, break the loop
break;
}
}
return false;
});
} catch (e) {
// If we can't get nodes safely, assume we're not in a list
isInList = false;
}
// If we're in a list, let the built-in list indentation handle it
if (isInList) {
return false;
}
// Only handle tab for non-list contexts
event.preventDefault();
const isShiftTab = event.shiftKey;
// Handle Shift+Tab (outdent)
if (isShiftTab) {
if (!selection.isCollapsed()) {
// For selected text, remove 4 spaces from the beginning of each line
try {
const selectedText = selection.getTextContent();
const lines = selectedText.split('\n');
const outdentedText = lines
.map((line) => {
// Remove up to 4 spaces from the beginning of the line
if (line.startsWith(' ')) {
return line.slice(4);
} else if (line.startsWith(' ')) {
return line.slice(3);
} else if (line.startsWith(' ')) {
return line.slice(2);
} else if (line.startsWith(' ')) {
return line.slice(1);
}
return line;
})
.join('\n');
// Insert the outdented text and let Lexical handle cursor positioning
selection.insertText(outdentedText);
// Schedule selection restoration in the next update cycle
setTimeout(() => {
editor.update(() => {
const currentSelection = $getSelection();
if ($isRangeSelection(currentSelection) && !currentSelection.isCollapsed()) {
// Selection is already maintained, do nothing
return;
}
// If selection was lost, try to select the inserted text
if ($isRangeSelection(currentSelection)) {
const currentOffset = currentSelection.anchor.offset;
const selectionStart = Math.max(0, currentOffset - outdentedText.length);
currentSelection.anchor.set(
currentSelection.anchor.key,
selectionStart,
'text'
);
currentSelection.focus.set(currentSelection.focus.key, currentOffset, 'text');
}
});
}, 0);
return true;
} catch (e) {
// If operation fails, do nothing
return true;
}
} else {
// For cursor position, try to remove spaces before cursor
try {
const anchorNode = selection.anchor.getNode();
const anchorOffset = selection.anchor.offset;
if ($isTextNode(anchorNode)) {
const textContent = anchorNode.getTextContent();
const beforeCursor = textContent.slice(0, anchorOffset);
const afterCursor = textContent.slice(anchorOffset);
// Check if there are spaces before cursor to remove
let spacesToRemove = 0;
for (let i = beforeCursor.length - 1; i >= 0 && spacesToRemove < 4; i--) {
if (beforeCursor[i] === ' ') {
spacesToRemove++;
} else {
break;
}
}
if (spacesToRemove > 0) {
const newTextContent =
beforeCursor.slice(0, beforeCursor.length - spacesToRemove) + afterCursor;
anchorNode.setTextContent(newTextContent);
selection.anchor.set(
anchorNode.getKey(),
anchorOffset - spacesToRemove,
'text'
);
selection.focus.set(anchorNode.getKey(), anchorOffset - spacesToRemove, 'text');
}
}
return true;
} catch (e) {
return true;
}
}
} else {
// Handle regular Tab (indent)
if (!selection.isCollapsed()) {
try {
const selectedText = selection.getTextContent();
const lines = selectedText.split('\n');
const indentedText = lines.map((line) => ' ' + line).join('\n');
// Insert the indented text and let Lexical handle cursor positioning
selection.insertText(indentedText);
// Schedule selection restoration in the next update cycle
setTimeout(() => {
editor.update(() => {
const currentSelection = $getSelection();
if ($isRangeSelection(currentSelection) && !currentSelection.isCollapsed()) {
// Selection is already maintained, do nothing
return;
}
// If selection was lost, try to select the inserted text
if ($isRangeSelection(currentSelection)) {
const currentOffset = currentSelection.anchor.offset;
const selectionStart = Math.max(0, currentOffset - indentedText.length);
currentSelection.anchor.set(
currentSelection.anchor.key,
selectionStart,
'text'
);
currentSelection.focus.set(currentSelection.focus.key, currentOffset, 'text');
}
});
}, 0);
return true;
} catch (e) {
// If selection operation fails, fall back to simple space insertion
const textNode = $createTextNode(' ');
selection.insertNodes([textNode]);
return true;
}
} else {
// For cursor position (no selection), insert 4 spaces
const textNode = $createTextNode(' '); // 4 spaces
selection.insertNodes([textNode]);
return true;
}
}
} catch (e) {
// If anything fails, just let the default behavior handle it
console.warn('TabToSpacesPlugin error:', e);
return false;
}
},
COMMAND_PRIORITY_EDITOR
);
}, [editor]);
return null;
}

View File

@@ -8,7 +8,7 @@
import type { DecoratorNode, Klass, LexicalEditor, LexicalNode } from 'lexical';
import type { EntityMatch } from '@lexical/text';
import { $createTextNode, $getRoot, $isTextNode, TextNode } from 'lexical';
import { $createTextNode, $isTextNode, TextNode } from 'lexical';
import { useCallback } from 'react';
import type { VariableLabelNode } from './plugins/VariableLabelPlugin/node';
import type { VariableNode } from './plugins/VariablePlugin/node';
@@ -209,36 +209,9 @@ export function textToEditorState(text = '') {
});
}
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);
} else if (child.type === 'variableLabel') {
paragraphText.push(child.variableKey);
} else if (child.type === 'Variable') {
paragraphText.push(child.variableKey);
}
});
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)
@@ -292,3 +265,23 @@ export function useBasicTypeaheadTriggerMatch(
[maxLength, minLength, trigger]
);
}
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('\n');
} else if (child.text) {
paragraphText.push(child.text);
} else if (child.type === 'variableLabel' || child.type === 'Variable') {
paragraphText.push(child.variableKey);
}
});
editorStateTextString.push(paragraphText.join(''));
});
return editorStateTextString.join('\n');
}