index.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import {
  2. memo,
  3. useCallback,
  4. useState,
  5. } from 'react'
  6. import ReactDOM from 'react-dom'
  7. import {
  8. FloatingPortal,
  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 type { PromptOption } from './prompt-option'
  31. import PromptMenu from './prompt-menu'
  32. import VariableMenu from './variable-menu'
  33. import type { VariableOption } from './variable-option'
  34. import { useOptions } from './hooks'
  35. import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
  36. import { useEventEmitterContextContext } from '@/context/event-emitter'
  37. type ComponentPickerProps = {
  38. triggerString: string
  39. contextBlock?: ContextBlockType
  40. queryBlock?: QueryBlockType
  41. historyBlock?: HistoryBlockType
  42. variableBlock?: VariableBlockType
  43. externalToolBlock?: ExternalToolBlockType
  44. workflowVariableBlock?: WorkflowVariableBlockType
  45. }
  46. const ComponentPicker = ({
  47. triggerString,
  48. contextBlock,
  49. queryBlock,
  50. historyBlock,
  51. variableBlock,
  52. externalToolBlock,
  53. workflowVariableBlock,
  54. }: ComponentPickerProps) => {
  55. const { eventEmitter } = useEventEmitterContextContext()
  56. const { refs, floatingStyles, elements } = useFloating({
  57. placement: 'bottom-start',
  58. middleware: [
  59. offset(0), // fix hide cursor
  60. shift(),
  61. flip(),
  62. ],
  63. })
  64. const [editor] = useLexicalComposerContext()
  65. const checkForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, {
  66. minLength: 0,
  67. maxLength: 0,
  68. })
  69. const [queryString, setQueryString] = useState<string | null>(null)
  70. eventEmitter?.useSubscription((v: any) => {
  71. if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND)
  72. editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`)
  73. })
  74. const {
  75. allOptions,
  76. promptOptions,
  77. variableOptions,
  78. externalToolOptions,
  79. workflowVariableOptions,
  80. } = useOptions(
  81. contextBlock,
  82. queryBlock,
  83. historyBlock,
  84. variableBlock,
  85. externalToolBlock,
  86. workflowVariableBlock,
  87. )
  88. const onSelectOption = useCallback(
  89. (
  90. selectedOption: PromptOption | VariableOption,
  91. nodeToRemove: TextNode | null,
  92. closeMenu: () => void,
  93. matchingString: string,
  94. ) => {
  95. editor.update(() => {
  96. if (nodeToRemove && selectedOption?.key)
  97. nodeToRemove.remove()
  98. if (selectedOption?.onSelect)
  99. selectedOption.onSelect(matchingString)
  100. closeMenu()
  101. })
  102. },
  103. [editor],
  104. )
  105. const handleSelectWorkflowVariable = useCallback((variables: string[]) => {
  106. editor.update(() => {
  107. const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!)
  108. if (needRemove)
  109. needRemove.remove()
  110. })
  111. if (variables[1] === 'sys.query' || variables[1] === 'sys.files')
  112. editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]])
  113. else
  114. editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
  115. }, [editor, checkForTriggerMatch, triggerString])
  116. const renderMenu = useCallback<MenuRenderFn<PromptOption | VariableOption>>((
  117. anchorElementRef,
  118. { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
  119. ) => {
  120. if (anchorElementRef.current && (allOptions.length || workflowVariableBlock?.show)) {
  121. return (
  122. <>
  123. {
  124. ReactDOM.createPortal(
  125. <div ref={refs.setReference}></div>,
  126. anchorElementRef.current,
  127. )
  128. }
  129. {
  130. elements.reference && (
  131. <FloatingPortal id='typeahead-menu'>
  132. <div
  133. className='w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg overflow-y-auto'
  134. style={{
  135. ...floatingStyles,
  136. maxHeight: 'calc(1 / 3 * 100vh)',
  137. }}
  138. ref={refs.setFloating}
  139. >
  140. {
  141. !!promptOptions.length && (
  142. <>
  143. <PromptMenu
  144. startIndex={0}
  145. selectedIndex={selectedIndex}
  146. options={promptOptions}
  147. onClick={(index, option) => {
  148. if (option.disabled)
  149. return
  150. setHighlightedIndex(index)
  151. selectOptionAndCleanUp(option)
  152. }}
  153. onMouseEnter={(index, option) => {
  154. if (option.disabled)
  155. return
  156. setHighlightedIndex(index)
  157. }}
  158. />
  159. </>
  160. )
  161. }
  162. {
  163. !!variableOptions.length && (
  164. <>
  165. {
  166. !!promptOptions.length && (
  167. <div className='h-[1px] bg-gray-100'></div>
  168. )
  169. }
  170. <VariableMenu
  171. startIndex={promptOptions.length}
  172. selectedIndex={selectedIndex}
  173. options={variableOptions}
  174. onClick={(index, option) => {
  175. if (option.disabled)
  176. return
  177. setHighlightedIndex(index)
  178. selectOptionAndCleanUp(option)
  179. }}
  180. onMouseEnter={(index, option) => {
  181. if (option.disabled)
  182. return
  183. setHighlightedIndex(index)
  184. }}
  185. queryString={queryString}
  186. />
  187. </>
  188. )
  189. }
  190. {
  191. !!externalToolOptions.length && (
  192. <>
  193. {
  194. (!!promptOptions.length || !!variableOptions.length) && (
  195. <div className='h-[1px] bg-gray-100'></div>
  196. )
  197. }
  198. <VariableMenu
  199. startIndex={promptOptions.length + variableOptions.length}
  200. selectedIndex={selectedIndex}
  201. options={externalToolOptions}
  202. onClick={(index, option) => {
  203. if (option.disabled)
  204. return
  205. setHighlightedIndex(index)
  206. selectOptionAndCleanUp(option)
  207. }}
  208. onMouseEnter={(index, option) => {
  209. if (option.disabled)
  210. return
  211. setHighlightedIndex(index)
  212. }}
  213. queryString={queryString}
  214. />
  215. </>
  216. )
  217. }
  218. {
  219. workflowVariableBlock?.show && (
  220. <>
  221. {
  222. (!!promptOptions.length || !!variableOptions.length || !!externalToolOptions.length) && (
  223. <div className='h-[1px] bg-gray-100'></div>
  224. )
  225. }
  226. <div className='p-1'>
  227. <VarReferenceVars
  228. hideSearch
  229. vars={workflowVariableOptions}
  230. onChange={(variables: string[]) => {
  231. handleSelectWorkflowVariable(variables)
  232. }}
  233. />
  234. </div>
  235. </>
  236. )
  237. }
  238. </div>
  239. </FloatingPortal>
  240. )
  241. }
  242. </>
  243. )
  244. }
  245. return null
  246. }, [
  247. allOptions,
  248. promptOptions,
  249. variableOptions,
  250. externalToolOptions,
  251. queryString,
  252. workflowVariableBlock?.show,
  253. workflowVariableOptions,
  254. handleSelectWorkflowVariable,
  255. elements,
  256. floatingStyles,
  257. refs,
  258. ])
  259. return (
  260. <LexicalTypeaheadMenuPlugin
  261. options={allOptions as any}
  262. onQueryChange={setQueryString}
  263. onSelectOption={onSelectOption}
  264. anchorClassName='z-[999999]'
  265. menuRenderFn={renderMenu}
  266. triggerFn={checkForTriggerMatch}
  267. />
  268. )
  269. }
  270. export default memo(ComponentPicker)