mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 21:13:50 +00:00
4.6.7-alpha commit (#743)
Co-authored-by: Archer <545436317@qq.com> Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
This commit is contained in:
135
packages/web/components/common/Textarea/PromptEditor/Editor.tsx
Normal file
135
packages/web/components/common/Textarea/PromptEditor/Editor.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
|
||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
||||
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
||||
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
|
||||
import VariablePickerPlugin from './plugins/VariablePickerPlugin';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import styles from './index.module.scss';
|
||||
import VariablePlugin from './plugins/VariablePlugin';
|
||||
import { VariableNode } from './plugins/VariablePlugin/node';
|
||||
import { EditorState, LexicalEditor } from 'lexical';
|
||||
import { textToEditorState } from './utils';
|
||||
import OnBlurPlugin from './plugins/OnBlurPlugin';
|
||||
import MyIcon from '../../Icon';
|
||||
import { PickerMenuItemType } from './type.d';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
|
||||
export default function Editor({
|
||||
h = 200,
|
||||
showResize = true,
|
||||
showOpenModal = true,
|
||||
onOpenModal,
|
||||
variables,
|
||||
onChange,
|
||||
onBlur,
|
||||
defaultValue,
|
||||
placeholder = ''
|
||||
}: {
|
||||
h?: number;
|
||||
showResize?: boolean;
|
||||
showOpenModal?: boolean;
|
||||
onOpenModal?: () => void;
|
||||
variables: PickerMenuItemType[];
|
||||
onChange?: (editorState: EditorState) => void;
|
||||
onBlur?: (editor: LexicalEditor) => void;
|
||||
defaultValue?: string;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const key = useRef(getNanoid(6));
|
||||
const [height, setHeight] = useState(h);
|
||||
const [initialConfig, setInitialConfig] = useState({
|
||||
namespace: 'promptEditor',
|
||||
nodes: [VariableNode],
|
||||
editorState: textToEditorState(defaultValue),
|
||||
onError: (error: Error) => {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
const initialY = useRef(0);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
initialY.current = e.clientY;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const deltaY = e.clientY - initialY.current;
|
||||
setHeight((prevHeight) => (prevHeight + deltaY < h * 0.5 ? h * 0.5 : prevHeight + deltaY));
|
||||
initialY.current = e.clientY;
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position={'relative'} width={'full'} h={`${height}px`} cursor={'text'}>
|
||||
<LexicalComposer initialConfig={initialConfig} key={key.current}>
|
||||
<PlainTextPlugin
|
||||
contentEditable={<ContentEditable className={styles.contentEditable} />}
|
||||
placeholder={
|
||||
<Box
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
py={3}
|
||||
px={4}
|
||||
pointerEvents={'none'}
|
||||
overflow={'overlay'}
|
||||
>
|
||||
<Box
|
||||
color={'myGray.500'}
|
||||
fontSize={'xs'}
|
||||
userSelect={'none'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-all'}
|
||||
h={'100%'}
|
||||
>
|
||||
{placeholder}
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<HistoryPlugin />
|
||||
<OnChangePlugin onChange={(e) => onChange?.(e)} />
|
||||
<VariablePickerPlugin variables={variables} />
|
||||
<VariablePlugin variables={variables} />
|
||||
<OnBlurPlugin onBlur={onBlur} />
|
||||
</LexicalComposer>
|
||||
{showResize && (
|
||||
<Box
|
||||
position={'absolute'}
|
||||
right={'0'}
|
||||
bottom={'-1'}
|
||||
zIndex={9}
|
||||
cursor={'ns-resize'}
|
||||
px={'2px'}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
<MyIcon name={'common/editor/resizer'} width={'14px'} height={'14px'} />
|
||||
</Box>
|
||||
)}
|
||||
{showOpenModal && (
|
||||
<Box
|
||||
zIndex={10}
|
||||
position={'absolute'}
|
||||
bottom={1}
|
||||
right={2}
|
||||
cursor={'pointer'}
|
||||
onClick={onOpenModal}
|
||||
>
|
||||
<MyIcon name={'common/fullScreenLight'} w={'14px'} color={'myGray.600'} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
.contentEditable {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 1px solid var(--chakra-colors-borderColor-base);
|
||||
border-radius: var(--chakra-radii-md);
|
||||
padding: 8px 12px;
|
||||
background: var(--chakra-colors-gray-50);
|
||||
font-size: 13px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.contentEditable:focus {
|
||||
outline: none;
|
||||
border: 1px solid;
|
||||
border-color: var(--chakra-colors-primary-600);
|
||||
background: #fff;
|
||||
box-shadow: 0px 0px 0px 2.4px rgba(51, 112, 255, 0.15);
|
||||
}
|
||||
|
||||
.variable {
|
||||
color: var(--chakra-colors-primary-600);
|
||||
padding: 0 2px;
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
import { Button, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { editorStateToText } from './utils';
|
||||
import Editor from './Editor';
|
||||
import MyModal from '../../MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { $getRoot, EditorState, type LexicalEditor } from 'lexical';
|
||||
import { PickerMenuItemType } from './type.d';
|
||||
import { useCallback, useTransition } from 'react';
|
||||
|
||||
const PromptEditor = ({
|
||||
showOpenModal = true,
|
||||
variables = [],
|
||||
defaultValue,
|
||||
onChange,
|
||||
onBlur,
|
||||
h,
|
||||
placeholder,
|
||||
title
|
||||
}: {
|
||||
showOpenModal?: boolean;
|
||||
variables?: PickerMenuItemType[];
|
||||
defaultValue?: string;
|
||||
onChange?: (text: string) => void;
|
||||
onBlur?: (text: string) => void;
|
||||
h?: number;
|
||||
placeholder?: string;
|
||||
title?: string;
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const [, startSts] = useTransition();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onChangeInput = useCallback((editorState: EditorState) => {
|
||||
const text = editorState.read(() => $getRoot().getTextContent());
|
||||
const formatValue = text.replaceAll('\n\n', '\n').replaceAll('}}{{', '}} {{');
|
||||
onChange?.(formatValue);
|
||||
}, []);
|
||||
const onBlurInput = useCallback((editor: LexicalEditor) => {
|
||||
startSts(() => {
|
||||
const text = editorStateToText(editor).replaceAll('\n\n', '\n').replaceAll('}}{{', '}} {{');
|
||||
onBlur?.(text);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Editor
|
||||
showResize
|
||||
showOpenModal={showOpenModal}
|
||||
onOpenModal={onOpen}
|
||||
variables={variables}
|
||||
h={h}
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChangeInput}
|
||||
onBlur={onBlurInput}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<MyModal isOpen={isOpen} onClose={onClose} iconSrc="modal/edit" title={title} w={'full'}>
|
||||
<ModalBody>
|
||||
<Editor
|
||||
h={400}
|
||||
showResize
|
||||
showOpenModal={false}
|
||||
variables={variables}
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChangeInput}
|
||||
onBlur={onBlurInput}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button mr={2} onClick={onClose}>
|
||||
{t('common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default React.memo(PromptEditor);
|
@@ -0,0 +1,96 @@
|
||||
import { Box, Button, Image } from '@chakra-ui/react';
|
||||
|
||||
export default function ComfirmVar({
|
||||
newVariables,
|
||||
onCancel,
|
||||
onConfirm
|
||||
}: {
|
||||
newVariables: string[];
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
background={'rgba(35, 56, 118, 0.2)'}
|
||||
rounded={'sm'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
/>
|
||||
<Box
|
||||
position={'absolute'}
|
||||
top={'50%'}
|
||||
left={'50%'}
|
||||
transform={'translate(-50%, -50%)'}
|
||||
w={'70%'}
|
||||
h={'70%'}
|
||||
bg={'white'}
|
||||
rounded={'lg'}
|
||||
boxShadow={'0 2px 4px rgba(0, 0, 0, 0.1)'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
justifyContent={'space-between'}
|
||||
pb={4}
|
||||
>
|
||||
<Box display={'flex'} mt={4} mr={4}>
|
||||
<Box
|
||||
w={'36px'}
|
||||
h={'36px'}
|
||||
minW={'36px'}
|
||||
boxShadow={'0 4px 8px rgba(0, 0, 0, 0.1)'}
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
rounded={'md'}
|
||||
border={'1px solid rgba(0, 0, 0, 0.1)'}
|
||||
mx={4}
|
||||
>
|
||||
<Image alt={''} src={'/imgs/module/variable.png'} objectFit={'contain'} w={'20px'} />
|
||||
</Box>
|
||||
<Box>引用了未定义的变量,是否自动添加?</Box>
|
||||
</Box>
|
||||
<Box
|
||||
ml={16}
|
||||
mt={4}
|
||||
fontSize={'sm'}
|
||||
color={'rgb(28,100,242)'}
|
||||
display={'flex'}
|
||||
whiteSpace={'wrap'}
|
||||
>
|
||||
{newVariables.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
bg={'rgb(237,242,250)'}
|
||||
px={1}
|
||||
h={6}
|
||||
rounded={'md'}
|
||||
mr={2}
|
||||
>
|
||||
<span>
|
||||
<span style={{ opacity: '60%' }}>{`{{`}</span>
|
||||
<span>{item}</span>
|
||||
<span style={{ opacity: '60%' }}>{`}}`}</span>
|
||||
</span>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<Box>
|
||||
<Box display={'flex'} justifyContent={'flex-end'} mt={4} mr={4}>
|
||||
<Button size={'sm'} variant={'ghost'} onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button size={'sm'} variant={'primary'} ml={4} onClick={onConfirm}>
|
||||
确定
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
import { useEffect } from 'react';
|
||||
import { BLUR_COMMAND, COMMAND_PRIORITY_EDITOR, LexicalEditor } from 'lexical';
|
||||
import { mergeRegister } from '@lexical/utils';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
|
||||
export default function OnBlurPlugin({ onBlur }: { onBlur?: (editor: LexicalEditor) => void }) {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
BLUR_COMMAND,
|
||||
() => {
|
||||
if (onBlur) onBlur(editor);
|
||||
|
||||
return true;
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR
|
||||
)
|
||||
);
|
||||
}, [editor, onBlur]);
|
||||
|
||||
return null;
|
||||
}
|
@@ -0,0 +1,143 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin';
|
||||
import { $createTextNode, $getSelection, $isRangeSelection, TextNode } from 'lexical';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { VariableInputEnum } from '@fastgpt/global/core/module/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '../../../../Icon';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useBasicTypeaheadTriggerMatch } from '../../utils';
|
||||
import { PickerMenuItemType } from '../../type.d';
|
||||
|
||||
export default function VariablePickerPlugin({ variables }: { variables: PickerMenuItemType[] }) {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const [queryString, setQueryString] = useState<string | null>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('{', {
|
||||
minLength: 0
|
||||
});
|
||||
|
||||
const VariableTypeList = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: t('core.module.variable.input type'),
|
||||
icon: 'core/app/variable/input',
|
||||
value: VariableInputEnum.input
|
||||
},
|
||||
{
|
||||
title: t('core.module.variable.textarea type'),
|
||||
icon: 'core/app/variable/textarea',
|
||||
value: VariableInputEnum.textarea
|
||||
},
|
||||
{
|
||||
title: t('core.module.variable.select type'),
|
||||
icon: 'core/app/variable/select',
|
||||
value: VariableInputEnum.select
|
||||
}
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
// const options: Array<any> = useMemo(() => {
|
||||
// // const newVariableOption = {
|
||||
// // label: t('common.Add New') + "变量",
|
||||
// // key: 'new_variable',
|
||||
// // icon: 'core/modules/variable'
|
||||
// // };
|
||||
// return [
|
||||
// ...variables.map((item) => ({
|
||||
// ...item,
|
||||
// icon: VariableTypeList.find((type) => type.value === item.type)?.icon
|
||||
// }))
|
||||
// // newVariableOption
|
||||
// ];
|
||||
// }, [VariableTypeList, t, variables]);
|
||||
|
||||
const onSelectOption = useCallback(
|
||||
(selectedOption: any, nodeToRemove: TextNode | null, closeMenu: () => void) => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if (!$isRangeSelection(selection) || selectedOption == null) {
|
||||
return;
|
||||
}
|
||||
if (nodeToRemove) {
|
||||
nodeToRemove.remove();
|
||||
}
|
||||
selection.insertNodes([$createTextNode(`{{${selectedOption.key}}}`)]);
|
||||
closeMenu();
|
||||
});
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
onQueryChange={setQueryString}
|
||||
onSelectOption={onSelectOption}
|
||||
triggerFn={checkForTriggerMatch}
|
||||
options={variables as any[]}
|
||||
menuRenderFn={(
|
||||
anchorElementRef,
|
||||
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }
|
||||
) => {
|
||||
if (anchorElementRef.current == null) {
|
||||
return null;
|
||||
}
|
||||
return anchorElementRef.current && variables.length
|
||||
? ReactDOM.createPortal(
|
||||
<Box
|
||||
bg={'white'}
|
||||
boxShadow={'lg'}
|
||||
borderWidth={'1px'}
|
||||
borderColor={'borderColor.base'}
|
||||
p={2}
|
||||
borderRadius={'md'}
|
||||
position={'fixed'}
|
||||
w={'200px'}
|
||||
overflow={'hidden'}
|
||||
zIndex={99999}
|
||||
>
|
||||
{variables.map((item, index) => (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
as={'li'}
|
||||
key={item.key}
|
||||
px={4}
|
||||
py={2}
|
||||
borderRadius={'sm'}
|
||||
cursor={'pointer'}
|
||||
_notLast={{
|
||||
mb: 2
|
||||
}}
|
||||
{...(selectedIndex === index
|
||||
? {
|
||||
bg: 'primary.50',
|
||||
color: 'primary.600'
|
||||
}
|
||||
: {
|
||||
bg: 'white',
|
||||
color: 'myGray.600'
|
||||
})}
|
||||
onClick={() => {
|
||||
setHighlightedIndex(index);
|
||||
selectOptionAndCleanUp(item);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHighlightedIndex(index);
|
||||
}}
|
||||
>
|
||||
<MyIcon name={item.icon as any} w={'14px'} />
|
||||
<Box ml={2} fontSize={'sm'}>{`${item.key}(${item.label})`}</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>,
|
||||
anchorElementRef.current
|
||||
)
|
||||
: null;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
import { TextNode } from 'lexical';
|
||||
import { mergeRegister } from '@lexical/utils';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { getHashtagRegexString } from './utils';
|
||||
import { registerLexicalTextEntity } from '../../utils';
|
||||
import { $createVariableNode, VariableNode } from './node';
|
||||
import { PickerMenuItemType } from '../../type';
|
||||
|
||||
const REGEX = new RegExp(getHashtagRegexString(), 'i');
|
||||
|
||||
export default function VariablePlugin({ variables }: { variables: PickerMenuItemType[] }) {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([VariableNode]))
|
||||
throw new Error('VariablePlugin: VariableNode not registered on editor');
|
||||
}, [editor]);
|
||||
|
||||
const variableKeys: Array<string> = useMemo(() => {
|
||||
return variables.map((item) => item.key);
|
||||
}, [variables]);
|
||||
|
||||
const createVariableNode = useCallback((textNode: TextNode): VariableNode => {
|
||||
return $createVariableNode(textNode.getTextContent());
|
||||
}, []);
|
||||
|
||||
const getVariableMatch = useCallback((text: string) => {
|
||||
const matches = REGEX.exec(text);
|
||||
if (!matches) return null;
|
||||
|
||||
if (variableKeys.indexOf(matches[3]) === -1) return null;
|
||||
const hashtagLength = matches[3].length + 4;
|
||||
const startOffset = matches.index;
|
||||
const endOffset = startOffset + hashtagLength;
|
||||
return {
|
||||
end: endOffset,
|
||||
start: startOffset
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
mergeRegister(
|
||||
...registerLexicalTextEntity(editor, getVariableMatch, VariableNode, createVariableNode)
|
||||
);
|
||||
}, [createVariableNode, editor, getVariableMatch]);
|
||||
|
||||
return null;
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
import type { NodeKey, EditorConfig, LexicalNode, SerializedTextNode } from 'lexical';
|
||||
|
||||
import { TextNode, $applyNodeReplacement } from 'lexical';
|
||||
import { addClassNamesToElement } from '@lexical/utils';
|
||||
import styles from '../../index.module.scss';
|
||||
|
||||
export class VariableNode extends TextNode {
|
||||
static getType(): string {
|
||||
return 'variable';
|
||||
}
|
||||
|
||||
static clone(node: VariableNode): VariableNode {
|
||||
return new VariableNode(node.__text, node.__key);
|
||||
}
|
||||
|
||||
constructor(text: string, key?: NodeKey) {
|
||||
super(text, key);
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const element = super.createDOM(config);
|
||||
addClassNamesToElement(element, styles.variable);
|
||||
return element;
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedTextNode): TextNode {
|
||||
const node = $createVariableNode(serializedNode.text);
|
||||
node.setFormat(serializedNode.format);
|
||||
node.setDetail(serializedNode.detail);
|
||||
node.setMode(serializedNode.mode);
|
||||
node.setStyle(serializedNode.style);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedTextNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'variable'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function $createVariableNode(text: string): VariableNode {
|
||||
return $applyNodeReplacement(new VariableNode(text));
|
||||
}
|
||||
|
||||
export function $isVariableNode(node: LexicalNode | null | undefined): node is VariableNode {
|
||||
return node instanceof VariableNode;
|
||||
}
|
@@ -0,0 +1,223 @@
|
||||
function getHashtagRegexStringChars(): Readonly<{
|
||||
alpha: string;
|
||||
alphanumeric: string;
|
||||
leftChars: string;
|
||||
rightChars: string;
|
||||
}> {
|
||||
// Latin accented characters
|
||||
// Excludes 0xd7 from the range
|
||||
// (the multiplication sign, confusable with "x").
|
||||
// Also excludes 0xf7, the division sign
|
||||
const latinAccents =
|
||||
'\xC0-\xD6' +
|
||||
'\xD8-\xF6' +
|
||||
'\xF8-\xFF' +
|
||||
'\u0100-\u024F' +
|
||||
'\u0253-\u0254' +
|
||||
'\u0256-\u0257' +
|
||||
'\u0259' +
|
||||
'\u025B' +
|
||||
'\u0263' +
|
||||
'\u0268' +
|
||||
'\u026F' +
|
||||
'\u0272' +
|
||||
'\u0289' +
|
||||
'\u028B' +
|
||||
'\u02BB' +
|
||||
'\u0300-\u036F' +
|
||||
'\u1E00-\u1EFF';
|
||||
|
||||
// Cyrillic (Russian, Ukrainian, etc.)
|
||||
const nonLatinChars =
|
||||
'\u0400-\u04FF' + // Cyrillic
|
||||
'\u0500-\u0527' + // Cyrillic Supplement
|
||||
'\u2DE0-\u2DFF' + // Cyrillic Extended A
|
||||
'\uA640-\uA69F' + // Cyrillic Extended B
|
||||
'\u0591-\u05BF' + // Hebrew
|
||||
'\u05C1-\u05C2' +
|
||||
'\u05C4-\u05C5' +
|
||||
'\u05C7' +
|
||||
'\u05D0-\u05EA' +
|
||||
'\u05F0-\u05F4' +
|
||||
'\uFB12-\uFB28' + // Hebrew Presentation Forms
|
||||
'\uFB2A-\uFB36' +
|
||||
'\uFB38-\uFB3C' +
|
||||
'\uFB3E' +
|
||||
'\uFB40-\uFB41' +
|
||||
'\uFB43-\uFB44' +
|
||||
'\uFB46-\uFB4F' +
|
||||
'\u0610-\u061A' + // Arabic
|
||||
'\u0620-\u065F' +
|
||||
'\u066E-\u06D3' +
|
||||
'\u06D5-\u06DC' +
|
||||
'\u06DE-\u06E8' +
|
||||
'\u06EA-\u06EF' +
|
||||
'\u06FA-\u06FC' +
|
||||
'\u06FF' +
|
||||
'\u0750-\u077F' + // Arabic Supplement
|
||||
'\u08A0' + // Arabic Extended A
|
||||
'\u08A2-\u08AC' +
|
||||
'\u08E4-\u08FE' +
|
||||
'\uFB50-\uFBB1' + // Arabic Pres. Forms A
|
||||
'\uFBD3-\uFD3D' +
|
||||
'\uFD50-\uFD8F' +
|
||||
'\uFD92-\uFDC7' +
|
||||
'\uFDF0-\uFDFB' +
|
||||
'\uFE70-\uFE74' + // Arabic Pres. Forms B
|
||||
'\uFE76-\uFEFC' +
|
||||
'\u200C-\u200C' + // Zero-Width Non-Joiner
|
||||
'\u0E01-\u0E3A' + // Thai
|
||||
'\u0E40-\u0E4E' + // Hangul (Korean)
|
||||
'\u1100-\u11FF' + // Hangul Jamo
|
||||
'\u3130-\u3185' + // Hangul Compatibility Jamo
|
||||
'\uA960-\uA97F' + // Hangul Jamo Extended-A
|
||||
'\uAC00-\uD7AF' + // Hangul Syllables
|
||||
'\uD7B0-\uD7FF' + // Hangul Jamo Extended-B
|
||||
'\uFFA1-\uFFDC'; // Half-width Hangul
|
||||
|
||||
const charCode = String.fromCharCode;
|
||||
|
||||
const cjkChars =
|
||||
'\u30A1-\u30FA\u30FC-\u30FE' + // Katakana (full-width)
|
||||
'\uFF66-\uFF9F' + // Katakana (half-width)
|
||||
'\uFF10-\uFF19\uFF21-\uFF3A' +
|
||||
'\uFF41-\uFF5A' + // Latin (full-width)
|
||||
'\u3041-\u3096\u3099-\u309E' + // Hiragana
|
||||
'\u3400-\u4DBF' + // Kanji (CJK Extension A)
|
||||
`\u4E00-\u9FFF${
|
||||
// Kanji (Unified)
|
||||
// Disabled as it breaks the Regex.
|
||||
// charCode(0x20000) + '-' + charCode(0x2A6DF) + // Kanji (CJK Extension B)
|
||||
charCode(0x2a700)
|
||||
}-${
|
||||
charCode(0x2b73f) // Kanji (CJK Extension C)
|
||||
}${charCode(0x2b740)}-${
|
||||
charCode(0x2b81f) // Kanji (CJK Extension D)
|
||||
}${charCode(0x2f800)}-${charCode(0x2fa1f)}\u3003\u3005\u303B`; // Kanji (CJK supplement)
|
||||
|
||||
const otherChars = latinAccents + nonLatinChars + cjkChars;
|
||||
// equivalent of \p{L}
|
||||
|
||||
const unicodeLetters =
|
||||
'\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6' +
|
||||
'\u00F8-\u0241\u0250-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EE\u037A\u0386' +
|
||||
'\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03CE\u03D0-\u03F5\u03F7-\u0481' +
|
||||
'\u048A-\u04CE\u04D0-\u04F9\u0500-\u050F\u0531-\u0556\u0559\u0561-\u0587' +
|
||||
'\u05D0-\u05EA\u05F0-\u05F2\u0621-\u063A\u0640-\u064A\u066E-\u066F' +
|
||||
'\u0671-\u06D3\u06D5\u06E5-\u06E6\u06EE-\u06EF\u06FA-\u06FC\u06FF\u0710' +
|
||||
'\u0712-\u072F\u074D-\u076D\u0780-\u07A5\u07B1\u0904-\u0939\u093D\u0950' +
|
||||
'\u0958-\u0961\u097D\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0' +
|
||||
'\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC-\u09DD\u09DF-\u09E1\u09F0-\u09F1' +
|
||||
'\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33' +
|
||||
'\u0A35-\u0A36\u0A38-\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D' +
|
||||
'\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD' +
|
||||
'\u0AD0\u0AE0-\u0AE1\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30' +
|
||||
'\u0B32-\u0B33\u0B35-\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B71\u0B83' +
|
||||
'\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F' +
|
||||
'\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0C05-\u0C0C\u0C0E-\u0C10' +
|
||||
'\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C60-\u0C61\u0C85-\u0C8C' +
|
||||
'\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE' +
|
||||
'\u0CE0-\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39' +
|
||||
'\u0D60-\u0D61\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6' +
|
||||
'\u0E01-\u0E30\u0E32-\u0E33\u0E40-\u0E46\u0E81-\u0E82\u0E84\u0E87-\u0E88' +
|
||||
'\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7' +
|
||||
'\u0EAA-\u0EAB\u0EAD-\u0EB0\u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6' +
|
||||
'\u0EDC-\u0EDD\u0F00\u0F40-\u0F47\u0F49-\u0F6A\u0F88-\u0F8B\u1000-\u1021' +
|
||||
'\u1023-\u1027\u1029-\u102A\u1050-\u1055\u10A0-\u10C5\u10D0-\u10FA\u10FC' +
|
||||
'\u1100-\u1159\u115F-\u11A2\u11A8-\u11F9\u1200-\u1248\u124A-\u124D' +
|
||||
'\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0' +
|
||||
'\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310' +
|
||||
'\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C' +
|
||||
'\u166F-\u1676\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711' +
|
||||
'\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7' +
|
||||
'\u17DC\u1820-\u1877\u1880-\u18A8\u1900-\u191C\u1950-\u196D\u1970-\u1974' +
|
||||
'\u1980-\u19A9\u19C1-\u19C7\u1A00-\u1A16\u1D00-\u1DBF\u1E00-\u1E9B' +
|
||||
'\u1EA0-\u1EF9\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D' +
|
||||
'\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC' +
|
||||
'\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC' +
|
||||
'\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u2094\u2102\u2107' +
|
||||
'\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D' +
|
||||
'\u212F-\u2131\u2133-\u2139\u213C-\u213F\u2145-\u2149\u2C00-\u2C2E' +
|
||||
'\u2C30-\u2C5E\u2C80-\u2CE4\u2D00-\u2D25\u2D30-\u2D65\u2D6F\u2D80-\u2D96' +
|
||||
'\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6' +
|
||||
'\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3006\u3031-\u3035' +
|
||||
'\u303B-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF' +
|
||||
'\u3105-\u312C\u3131-\u318E\u31A0-\u31B7\u31F0-\u31FF\u3400-\u4DB5' +
|
||||
'\u4E00-\u9FBB\uA000-\uA48C\uA800-\uA801\uA803-\uA805\uA807-\uA80A' +
|
||||
'\uA80C-\uA822\uAC00-\uD7A3\uF900-\uFA2D\uFA30-\uFA6A\uFA70-\uFAD9' +
|
||||
'\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C' +
|
||||
'\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F' +
|
||||
'\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A' +
|
||||
'\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7' +
|
||||
'\uFFDA-\uFFDC';
|
||||
|
||||
// equivalent of \p{Mn}\p{Mc}
|
||||
const unicodeAccents =
|
||||
'\u0300-\u036F\u0483-\u0486\u0591-\u05B9\u05BB-\u05BD\u05BF' +
|
||||
'\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u0615\u064B-\u065E\u0670' +
|
||||
'\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A' +
|
||||
'\u07A6-\u07B0\u0901-\u0903\u093C\u093E-\u094D\u0951-\u0954\u0962-\u0963' +
|
||||
'\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7-\u09C8\u09CB-\u09CD\u09D7' +
|
||||
'\u09E2-\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D' +
|
||||
'\u0A70-\u0A71\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD' +
|
||||
'\u0AE2-\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B43\u0B47-\u0B48\u0B4B-\u0B4D' +
|
||||
'\u0B56-\u0B57\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7' +
|
||||
'\u0C01-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56' +
|
||||
'\u0C82-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5-\u0CD6' +
|
||||
'\u0D02-\u0D03\u0D3E-\u0D43\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D82-\u0D83' +
|
||||
'\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2-\u0DF3\u0E31\u0E34-\u0E3A' +
|
||||
'\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19' +
|
||||
'\u0F35\u0F37\u0F39\u0F3E-\u0F3F\u0F71-\u0F84\u0F86-\u0F87\u0F90-\u0F97' +
|
||||
'\u0F99-\u0FBC\u0FC6\u102C-\u1032\u1036-\u1039\u1056-\u1059\u135F' +
|
||||
'\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B6-\u17D3\u17DD' +
|
||||
'\u180B-\u180D\u18A9\u1920-\u192B\u1930-\u193B\u19B0-\u19C0\u19C8-\u19C9' +
|
||||
'\u1A17-\u1A1B\u1DC0-\u1DC3\u20D0-\u20DC\u20E1\u20E5-\u20EB\u302A-\u302F' +
|
||||
'\u3099-\u309A\uA802\uA806\uA80B\uA823-\uA827\uFB1E\uFE00-\uFE0F' +
|
||||
'\uFE20-\uFE23';
|
||||
|
||||
// equivalent of \p{Dn}
|
||||
const unicodeDigits =
|
||||
'\u0030-\u0039\u0660-\u0669\u06F0-\u06F9\u0966-\u096F\u09E6-\u09EF' +
|
||||
'\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0BE6-\u0BEF\u0C66-\u0C6F' +
|
||||
'\u0CE6-\u0CEF\u0D66-\u0D6F\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29' +
|
||||
'\u1040-\u1049\u17E0-\u17E9\u1810-\u1819\u1946-\u194F\u19D0-\u19D9' +
|
||||
'\uFF10-\uFF19';
|
||||
|
||||
// An alpha char is a unicode chars including unicode marks or
|
||||
// letter or char in otherChars range
|
||||
const alpha = unicodeLetters;
|
||||
|
||||
// A numeric character is any with the number digit property, or
|
||||
// underscore. These characters can be included in hashtags, but a hashtag
|
||||
// cannot have only these characters.
|
||||
const numeric = `${unicodeDigits}_`;
|
||||
|
||||
// Alphanumeric char is any alpha char or a unicode char with decimal
|
||||
// number property \p{Nd}
|
||||
const alphanumeric = alpha + numeric;
|
||||
const leftChars = '{';
|
||||
const rightChars = '}';
|
||||
|
||||
return {
|
||||
alpha,
|
||||
alphanumeric,
|
||||
leftChars,
|
||||
rightChars
|
||||
};
|
||||
}
|
||||
|
||||
export function getHashtagRegexString(): string {
|
||||
const { leftChars, rightChars } = getHashtagRegexStringChars();
|
||||
|
||||
const hashLeftCharList = `[${leftChars}]`;
|
||||
const hashRightCharList = `[${rightChars}]`;
|
||||
|
||||
// A hashtag contains characters, numbers and underscores,
|
||||
// but not all numbers.
|
||||
const hashtag =
|
||||
`(${hashLeftCharList})` +
|
||||
`(${hashLeftCharList})([a-zA-Z0-9_]{0,29}` +
|
||||
`)(${hashRightCharList})(${hashRightCharList})`;
|
||||
|
||||
return hashtag;
|
||||
}
|
5
packages/web/components/common/Textarea/PromptEditor/type.d.ts
vendored
Normal file
5
packages/web/components/common/Textarea/PromptEditor/type.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export type PickerMenuItemType = {
|
||||
key: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
};
|
275
packages/web/components/common/Textarea/PromptEditor/utils.ts
Normal file
275
packages/web/components/common/Textarea/PromptEditor/utils.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
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: string = '') {
|
||||
const paragraph = 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 stringifiedEditorState = JSON.stringify(editor.getEditorState().toJSON());
|
||||
const parsedEditorState = editor.parseEditorState(stringifiedEditorState);
|
||||
const editorStateTextString = parsedEditorState.read(() => $getRoot().getTextContent());
|
||||
const compressedText = editorStateTextString.replace(/\n+/g, '\n\n');
|
||||
|
||||
return compressedText;
|
||||
}
|
||||
|
||||
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]
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user