mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-18 17:51:24 +00:00
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:
@@ -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 &&
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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} />;
|
||||
}
|
@@ -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
|
||||
];
|
@@ -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;
|
||||
}
|
@@ -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');
|
||||
}
|
||||
|
Reference in New Issue
Block a user