You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

main.tsx 6.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import type {
  2. FC,
  3. MouseEventHandler,
  4. } from 'react'
  5. import {
  6. memo,
  7. useCallback,
  8. useMemo,
  9. useState,
  10. } from 'react'
  11. import { useTranslation } from 'react-i18next'
  12. import type {
  13. OffsetOptions,
  14. Placement,
  15. } from '@floating-ui/react'
  16. import type {
  17. BlockEnum,
  18. NodeDefault,
  19. OnSelectBlock,
  20. ToolWithProvider,
  21. } from '../types'
  22. import Tabs from './tabs'
  23. import { TabsEnum } from './types'
  24. import { useTabs } from './hooks'
  25. import {
  26. PortalToFollowElem,
  27. PortalToFollowElemContent,
  28. PortalToFollowElemTrigger,
  29. } from '@/app/components/base/portal-to-follow-elem'
  30. import Input from '@/app/components/base/input'
  31. import {
  32. Plus02,
  33. } from '@/app/components/base/icons/src/vender/line/general'
  34. import SearchBox from '@/app/components/plugins/marketplace/search-box'
  35. export type NodeSelectorProps = {
  36. open?: boolean
  37. onOpenChange?: (open: boolean) => void
  38. onSelect: OnSelectBlock
  39. trigger?: (open: boolean) => React.ReactNode
  40. placement?: Placement
  41. offset?: OffsetOptions
  42. triggerStyle?: React.CSSProperties
  43. triggerClassName?: (open: boolean) => string
  44. triggerInnerClassName?: string
  45. popupClassName?: string
  46. asChild?: boolean
  47. availableBlocksTypes?: BlockEnum[]
  48. disabled?: boolean
  49. blocks?: NodeDefault[]
  50. dataSources?: ToolWithProvider[]
  51. noBlocks?: boolean
  52. noTools?: boolean
  53. }
  54. const NodeSelector: FC<NodeSelectorProps> = ({
  55. open: openFromProps,
  56. onOpenChange,
  57. onSelect,
  58. trigger,
  59. placement = 'right',
  60. offset = 6,
  61. triggerClassName,
  62. triggerInnerClassName,
  63. triggerStyle,
  64. popupClassName,
  65. asChild,
  66. availableBlocksTypes,
  67. disabled,
  68. blocks = [],
  69. dataSources = [],
  70. noBlocks = false,
  71. noTools = false,
  72. }) => {
  73. const { t } = useTranslation()
  74. const [searchText, setSearchText] = useState('')
  75. const [tags, setTags] = useState<string[]>([])
  76. const [localOpen, setLocalOpen] = useState(false)
  77. const open = openFromProps === undefined ? localOpen : openFromProps
  78. const handleOpenChange = useCallback((newOpen: boolean) => {
  79. setLocalOpen(newOpen)
  80. if (!newOpen)
  81. setSearchText('')
  82. if (onOpenChange)
  83. onOpenChange(newOpen)
  84. }, [onOpenChange])
  85. const handleTrigger = useCallback<MouseEventHandler<HTMLDivElement>>((e) => {
  86. if (disabled)
  87. return
  88. e.stopPropagation()
  89. handleOpenChange(!open)
  90. }, [handleOpenChange, open, disabled])
  91. const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
  92. handleOpenChange(false)
  93. onSelect(type, toolDefaultValue)
  94. }, [handleOpenChange, onSelect])
  95. const {
  96. activeTab,
  97. setActiveTab,
  98. tabs,
  99. } = useTabs(noBlocks, !dataSources.length, noTools)
  100. const handleActiveTabChange = useCallback((newActiveTab: TabsEnum) => {
  101. setActiveTab(newActiveTab)
  102. }, [setActiveTab])
  103. const searchPlaceholder = useMemo(() => {
  104. if (activeTab === TabsEnum.Blocks)
  105. return t('workflow.tabs.searchBlock')
  106. if (activeTab === TabsEnum.Tools)
  107. return t('workflow.tabs.searchTool')
  108. if (activeTab === TabsEnum.Sources)
  109. return t('workflow.tabs.searchDataSource')
  110. return ''
  111. }, [activeTab, t])
  112. return (
  113. <PortalToFollowElem
  114. placement={placement}
  115. offset={offset}
  116. open={open}
  117. onOpenChange={handleOpenChange}
  118. >
  119. <PortalToFollowElemTrigger
  120. asChild={asChild}
  121. onClick={handleTrigger}
  122. className={triggerInnerClassName}
  123. >
  124. {
  125. trigger
  126. ? trigger(open)
  127. : (
  128. <div
  129. className={`
  130. z-10 flex h-4
  131. w-4 cursor-pointer items-center justify-center rounded-full bg-components-button-primary-bg text-text-primary-on-surface hover:bg-components-button-primary-bg-hover
  132. ${triggerClassName?.(open)}
  133. `}
  134. style={triggerStyle}
  135. >
  136. <Plus02 className='h-2.5 w-2.5' />
  137. </div>
  138. )
  139. }
  140. </PortalToFollowElemTrigger>
  141. <PortalToFollowElemContent className='z-[1000]'>
  142. <div className={`rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg ${popupClassName}`}>
  143. <Tabs
  144. tabs={tabs}
  145. activeTab={activeTab}
  146. blocks={blocks}
  147. onActiveTabChange={handleActiveTabChange}
  148. filterElem={
  149. <div className='relative m-2' onClick={e => e.stopPropagation()}>
  150. {activeTab === TabsEnum.Blocks && (
  151. <Input
  152. showLeftIcon
  153. showClearIcon
  154. autoFocus
  155. value={searchText}
  156. placeholder={searchPlaceholder}
  157. onChange={e => setSearchText(e.target.value)}
  158. onClear={() => setSearchText('')}
  159. />
  160. )}
  161. {activeTab === TabsEnum.Sources && (
  162. <Input
  163. showLeftIcon
  164. showClearIcon
  165. autoFocus
  166. value={searchText}
  167. placeholder={searchPlaceholder}
  168. onChange={e => setSearchText(e.target.value)}
  169. onClear={() => setSearchText('')}
  170. />
  171. )}
  172. {activeTab === TabsEnum.Tools && (
  173. <SearchBox
  174. search={searchText}
  175. onSearchChange={setSearchText}
  176. tags={tags}
  177. onTagsChange={setTags}
  178. placeholder={t('plugin.searchTools')!}
  179. inputClassName='grow'
  180. />
  181. )}
  182. </div>
  183. }
  184. onSelect={handleSelect}
  185. searchText={searchText}
  186. tags={tags}
  187. availableBlocksTypes={availableBlocksTypes}
  188. noBlocks={noBlocks}
  189. dataSources={dataSources}
  190. noTools={noTools}
  191. onTagsChange={setTags}
  192. />
  193. </div>
  194. </PortalToFollowElemContent>
  195. </PortalToFollowElem>
  196. )
  197. }
  198. export default memo(NodeSelector)