Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

index.tsx 8.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import {
  2. Fragment,
  3. memo,
  4. useCallback,
  5. useState,
  6. } from 'react'
  7. import ReactDOM from 'react-dom'
  8. import {
  9. flip,
  10. offset,
  11. shift,
  12. useFloating,
  13. } from '@floating-ui/react'
  14. import type { TextNode } from 'lexical'
  15. import type { MenuRenderFn } from '@lexical/react/LexicalTypeaheadMenuPlugin'
  16. import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
  17. import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'
  18. import type {
  19. ContextBlockType,
  20. ExternalToolBlockType,
  21. HistoryBlockType,
  22. QueryBlockType,
  23. VariableBlockType,
  24. WorkflowVariableBlockType,
  25. } from '../../types'
  26. import { useBasicTypeaheadTriggerMatch } from '../../hooks'
  27. import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from '../workflow-variable-block'
  28. import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block'
  29. import { $splitNodeContainingQuery } from '../../utils'
  30. import { useOptions } from './hooks'
  31. import type { PickerBlockMenuOption } from './menu'
  32. import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
  33. import { useEventEmitterContextContext } from '@/context/event-emitter'
  34. import { KEY_ESCAPE_COMMAND } from 'lexical'
  35. type ComponentPickerProps = {
  36. triggerString: string
  37. contextBlock?: ContextBlockType
  38. queryBlock?: QueryBlockType
  39. historyBlock?: HistoryBlockType
  40. variableBlock?: VariableBlockType
  41. externalToolBlock?: ExternalToolBlockType
  42. workflowVariableBlock?: WorkflowVariableBlockType
  43. isSupportFileVar?: boolean
  44. }
  45. const ComponentPicker = ({
  46. triggerString,
  47. contextBlock,
  48. queryBlock,
  49. historyBlock,
  50. variableBlock,
  51. externalToolBlock,
  52. workflowVariableBlock,
  53. isSupportFileVar,
  54. }: ComponentPickerProps) => {
  55. const { eventEmitter } = useEventEmitterContextContext()
  56. const { refs, floatingStyles, isPositioned } = useFloating({
  57. placement: 'bottom-start',
  58. middleware: [
  59. offset(0), // fix hide cursor
  60. shift({
  61. padding: 8,
  62. }),
  63. flip(),
  64. ],
  65. })
  66. const [editor] = useLexicalComposerContext()
  67. const checkForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, {
  68. minLength: 0,
  69. maxLength: 0,
  70. })
  71. const [queryString, setQueryString] = useState<string | null>(null)
  72. eventEmitter?.useSubscription((v: any) => {
  73. if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND)
  74. editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`)
  75. })
  76. const {
  77. allFlattenOptions,
  78. workflowVariableOptions,
  79. } = useOptions(
  80. contextBlock,
  81. queryBlock,
  82. historyBlock,
  83. variableBlock,
  84. externalToolBlock,
  85. workflowVariableBlock,
  86. )
  87. const onSelectOption = useCallback(
  88. (
  89. selectedOption: PickerBlockMenuOption,
  90. nodeToRemove: TextNode | null,
  91. closeMenu: () => void,
  92. ) => {
  93. editor.update(() => {
  94. if (nodeToRemove && selectedOption?.key)
  95. nodeToRemove.remove()
  96. selectedOption.onSelectMenuOption()
  97. closeMenu()
  98. })
  99. },
  100. [editor],
  101. )
  102. const handleSelectWorkflowVariable = useCallback((variables: string[]) => {
  103. editor.update(() => {
  104. const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!)
  105. if (needRemove)
  106. needRemove.remove()
  107. })
  108. if (variables[1] === 'sys.query' || variables[1] === 'sys.files')
  109. editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]])
  110. else
  111. editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
  112. }, [editor, checkForTriggerMatch, triggerString])
  113. const handleClose = useCallback(() => {
  114. const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' })
  115. editor.dispatchCommand(KEY_ESCAPE_COMMAND, escapeEvent)
  116. }, [editor])
  117. const renderMenu = useCallback<MenuRenderFn<PickerBlockMenuOption>>((
  118. anchorElementRef,
  119. { options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
  120. ) => {
  121. if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show)))
  122. return null
  123. setTimeout(() => {
  124. if (anchorElementRef.current)
  125. refs.setReference(anchorElementRef.current)
  126. }, 0)
  127. return (
  128. <>
  129. {
  130. ReactDOM.createPortal(
  131. // The `LexicalMenu` will try to calculate the position of the floating menu based on the first child.
  132. // Since we use floating ui, we need to wrap it with a div to prevent the position calculation being affected.
  133. // See https://github.com/facebook/lexical/blob/ac97dfa9e14a73ea2d6934ff566282d7f758e8bb/packages/lexical-react/src/shared/LexicalMenu.ts#L493
  134. <div className='h-0 w-0'>
  135. <div
  136. className='w-[260px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'
  137. style={{
  138. ...floatingStyles,
  139. visibility: isPositioned ? 'visible' : 'hidden',
  140. }}
  141. ref={refs.setFloating}
  142. >
  143. {
  144. workflowVariableBlock?.show && (
  145. <div className='p-1'>
  146. <VarReferenceVars
  147. searchBoxClassName='mt-1'
  148. vars={workflowVariableOptions}
  149. onChange={(variables: string[]) => {
  150. handleSelectWorkflowVariable(variables)
  151. }}
  152. maxHeightClass='max-h-[34vh]'
  153. isSupportFileVar={isSupportFileVar}
  154. onClose={handleClose}
  155. onBlur={handleClose}
  156. autoFocus={false}
  157. />
  158. </div>
  159. )
  160. }
  161. {
  162. workflowVariableBlock?.show && !!options.length && (
  163. <div className='my-1 h-px w-full -translate-x-1 bg-divider-subtle'></div>
  164. )
  165. }
  166. <div>
  167. {
  168. options.map((option, index) => (
  169. <Fragment key={option.key}>
  170. {
  171. // Divider
  172. index !== 0 && options.at(index - 1)?.group !== option.group && (
  173. <div className='my-1 h-px w-full -translate-x-1 bg-divider-subtle'></div>
  174. )
  175. }
  176. {option.renderMenuOption({
  177. queryString,
  178. isSelected: selectedIndex === index,
  179. onSelect: () => {
  180. selectOptionAndCleanUp(option)
  181. },
  182. onSetHighlight: () => {
  183. setHighlightedIndex(index)
  184. },
  185. })}
  186. </Fragment>
  187. ))
  188. }
  189. </div>
  190. </div>
  191. </div>,
  192. anchorElementRef.current,
  193. )
  194. }
  195. </>
  196. )
  197. }, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable, handleClose, isSupportFileVar])
  198. return (
  199. <LexicalTypeaheadMenuPlugin
  200. options={allFlattenOptions}
  201. onQueryChange={setQueryString}
  202. onSelectOption={onSelectOption}
  203. // The `translate` class is used to workaround the issue that the `typeahead-menu` menu is not positioned as expected.
  204. // See also https://github.com/facebook/lexical/blob/772520509308e8ba7e4a82b6cd1996a78b3298d0/packages/lexical-react/src/shared/LexicalMenu.ts#L498
  205. //
  206. // We no need the position function of the `LexicalTypeaheadMenuPlugin`,
  207. // so the reference anchor should be positioned based on the range of the trigger string, and the menu will be positioned by the floating ui.
  208. anchorClassName='z-[999999] translate-y-[calc(-100%-3px)]'
  209. menuRenderFn={renderMenu}
  210. triggerFn={checkForTriggerMatch}
  211. />
  212. )
  213. }
  214. export default memo(ComponentPicker)