import type { FC } from 'react' import { useCallback, useMemo, useState } from 'react' import ReactDOM from 'react-dom' import { useTranslation } from 'react-i18next' import { $insertNodes, type TextNode } from 'lexical' import { LexicalTypeaheadMenuPlugin, MenuOption, } from '@lexical/react/LexicalTypeaheadMenuPlugin' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useBasicTypeaheadTriggerMatch } from '../hooks' import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from './variable-block' import { $createCustomTextNode } from './custom-text/node' import { BracketsX } from '@/app/components/base/icons/src/vender/line/development' class VariablePickerOption extends MenuOption { title: string icon?: JSX.Element keywords: Array keyboardShortcut?: string onSelect: (queryString: string) => void constructor( title: string, options: { icon?: JSX.Element keywords?: Array keyboardShortcut?: string onSelect: (queryString: string) => void }, ) { super(title) this.title = title this.keywords = options.keywords || [] this.icon = options.icon this.keyboardShortcut = options.keyboardShortcut this.onSelect = options.onSelect.bind(this) } } type VariablePickerMenuItemProps = { isSelected: boolean onClick: () => void onMouseEnter: () => void option: VariablePickerOption queryString: string | null } const VariablePickerMenuItem: FC = ({ isSelected, onClick, onMouseEnter, option, queryString, }) => { const title = option.title let before = title let middle = '' let after = '' if (queryString) { const regex = new RegExp(queryString, 'i') const match = regex.exec(option.title) if (match) { before = title.substring(0, match.index) middle = match[0] after = title.substring(match.index + match[0].length) } } return (
{option.icon}
{before} {middle} {after}
) } export type Option = { value: string name: string } type VariablePickerProps = { items?: Option[] } const VariablePicker: FC = ({ items = [], }) => { const { t } = useTranslation() const [editor] = useLexicalComposerContext() const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('{', { minLength: 0, maxLength: 6, }) const [queryString, setQueryString] = useState(null) const options = useMemo(() => { const baseOptions = items.map((item) => { return new VariablePickerOption(item.value, { icon: , onSelect: () => { editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`) }, }) }) if (!queryString) return baseOptions const regex = new RegExp(queryString, 'i') return baseOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword))) }, [editor, queryString, items]) const newOption = new VariablePickerOption(t('common.promptEditor.variable.modal.add'), { icon: , onSelect: () => { editor.update(() => { const prefixNode = $createCustomTextNode('{{') const suffixNode = $createCustomTextNode('}}') $insertNodes([prefixNode, suffixNode]) prefixNode.select() }) }, }) const onSelectOption = useCallback( ( selectedOption: VariablePickerOption, nodeToRemove: TextNode | null, closeMenu: () => void, matchingString: string, ) => { editor.update(() => { if (nodeToRemove) nodeToRemove.remove() selectedOption.onSelect(matchingString) closeMenu() }) }, [editor], ) const mergedOptions = [...options, newOption] return ( (anchorElementRef.current && mergedOptions.length) ? ReactDOM.createPortal(
{ !!options.length && ( <>
{options.map((option, i: number) => ( { setHighlightedIndex(i) selectOptionAndCleanUp(option) }} onMouseEnter={() => { setHighlightedIndex(i) }} key={option.key} option={option} queryString={queryString} /> ))}
) }
{ setHighlightedIndex(options.length) selectOptionAndCleanUp(newOption) }} onMouseEnter={() => { setHighlightedIndex(options.length) }} key={newOption.key} > {newOption.icon}
{newOption.title}
, anchorElementRef.current, ) : null} triggerFn={checkForTriggerMatch} /> ) } export default VariablePicker