123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- import type { FC } from 'react'
- import { useCallback } from 'react'
- import ReactDOM from 'react-dom'
- import { useTranslation } from 'react-i18next'
- import type { TextNode } from 'lexical'
- import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
- import {
- LexicalTypeaheadMenuPlugin,
- MenuOption,
- } from '@lexical/react/LexicalTypeaheadMenuPlugin'
- import { useBasicTypeaheadTriggerMatch } from '../hooks'
- import { INSERT_CONTEXT_BLOCK_COMMAND } from './context-block'
- import { INSERT_VARIABLE_BLOCK_COMMAND } from './variable-block'
- import { INSERT_HISTORY_BLOCK_COMMAND } from './history-block'
- import { INSERT_QUERY_BLOCK_COMMAND } from './query-block'
- import { File05 } from '@/app/components/base/icons/src/vender/solid/files'
- import { Variable } from '@/app/components/base/icons/src/vender/line/development'
- import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general'
- import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
- class ComponentPickerOption extends MenuOption {
- title: string
- icon?: JSX.Element
- keywords: Array<string>
- keyboardShortcut?: string
- desc: string
- onSelect: (queryString: string) => void
- disabled?: boolean
- constructor(
- title: string,
- options: {
- icon?: JSX.Element
- keywords?: Array<string>
- keyboardShortcut?: string
- desc: string
- onSelect: (queryString: string) => void
- disabled?: boolean
- },
- ) {
- super(title)
- this.title = title
- this.keywords = options.keywords || []
- this.icon = options.icon
- this.keyboardShortcut = options.keyboardShortcut
- this.desc = options.desc
- this.onSelect = options.onSelect.bind(this)
- this.disabled = options.disabled
- }
- }
- type ComponentPickerMenuItemProps = {
- isSelected: boolean
- onClick: () => void
- onMouseEnter: () => void
- option: ComponentPickerOption
- }
- const ComponentPickerMenuItem: FC<ComponentPickerMenuItemProps> = ({
- isSelected,
- onClick,
- onMouseEnter,
- option,
- }) => {
- const { t } = useTranslation()
- return (
- <div
- key={option.key}
- className={`
- flex items-center px-3 py-1.5 rounded-lg
- ${isSelected && !option.disabled && '!bg-gray-50'}
- ${option.disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'}
- `}
- tabIndex={-1}
- ref={option.setRefElement}
- onMouseEnter={onMouseEnter}
- onClick={onClick}>
- <div className='flex items-center justify-center mr-2 w-8 h-8 rounded-lg border border-gray-100'>
- {option.icon}
- </div>
- <div className='grow'>
- <div className='flex items-center justify-between h-5 text-sm text-gray-900'>
- {option.title}
- <span className='text-xs text-gray-400'>{option.disabled && t('common.promptEditor.existed')}</span>
- </div>
- <div className='text-xs text-gray-500'>{option.desc}</div>
- </div>
- </div>
- )
- }
- type ComponentPickerProps = {
- contextDisabled?: boolean
- historyDisabled?: boolean
- queryDisabled?: boolean
- contextShow?: boolean
- historyShow?: boolean
- queryShow?: boolean
- }
- const ComponentPicker: FC<ComponentPickerProps> = ({
- contextDisabled,
- historyDisabled,
- queryDisabled,
- contextShow,
- historyShow,
- queryShow,
- }) => {
- const { t } = useTranslation()
- const [editor] = useLexicalComposerContext()
- const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
- minLength: 0,
- maxLength: 0,
- })
- const options = [
- ...contextShow
- ? [
- new ComponentPickerOption(t('common.promptEditor.context.item.title'), {
- desc: t('common.promptEditor.context.item.desc'),
- icon: <File05 className='w-4 h-4 text-[#6938EF]' />,
- onSelect: () => {
- if (contextDisabled)
- return
- editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
- },
- disabled: contextDisabled,
- }),
- ]
- : [],
- new ComponentPickerOption(t('common.promptEditor.variable.item.title'), {
- desc: t('common.promptEditor.variable.item.desc'),
- icon: <Variable className='w-4 h-4 text-[#2970FF]' />,
- onSelect: () => {
- editor.dispatchCommand(INSERT_VARIABLE_BLOCK_COMMAND, undefined)
- },
- }),
- ...historyShow
- ? [
- new ComponentPickerOption(t('common.promptEditor.history.item.title'), {
- desc: t('common.promptEditor.history.item.desc'),
- icon: <MessageClockCircle className='w-4 h-4 text-[#DD2590]' />,
- onSelect: () => {
- if (historyDisabled)
- return
- editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined)
- },
- disabled: historyDisabled,
- }),
- ]
- : [],
- ...queryShow
- ? [
- new ComponentPickerOption(t('common.promptEditor.query.item.title'), {
- desc: t('common.promptEditor.query.item.desc'),
- icon: <UserEdit02 className='w-4 h-4 text-[#FD853A]' />,
- onSelect: () => {
- if (queryDisabled)
- return
- editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined)
- },
- disabled: queryDisabled,
- }),
- ]
- : [],
- ]
- const onSelectOption = useCallback(
- (
- selectedOption: ComponentPickerOption,
- nodeToRemove: TextNode | null,
- closeMenu: () => void,
- matchingString: string,
- ) => {
- editor.update(() => {
- if (nodeToRemove)
- nodeToRemove.remove()
- selectedOption.onSelect(matchingString)
- closeMenu()
- })
- },
- [editor],
- )
- return (
- <LexicalTypeaheadMenuPlugin
- options={options}
- onQueryChange={() => {}}
- onSelectOption={onSelectOption}
- menuRenderFn={(
- anchorElementRef,
- { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
- ) =>
- (anchorElementRef.current && options.length)
- ? ReactDOM.createPortal(
- <div className='mt-[25px] p-1 w-[400px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
- {options.map((option, i: number) => (
- <ComponentPickerMenuItem
- isSelected={selectedIndex === i}
- onClick={() => {
- if (option.disabled)
- return
- setHighlightedIndex(i)
- selectOptionAndCleanUp(option)
- }}
- onMouseEnter={() => {
- if (option.disabled)
- return
- setHighlightedIndex(i)
- }}
- key={option.key}
- option={option}
- />
- ))}
- </div>,
- anchorElementRef.current,
- )
- : null}
- triggerFn={checkForTriggerMatch}
- />
- )
- }
- export default ComponentPicker
|