您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

tool-picker.tsx 5.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useCallback, useMemo, useState } from 'react'
  4. import {
  5. PortalToFollowElem,
  6. PortalToFollowElemContent,
  7. PortalToFollowElemTrigger,
  8. } from '@/app/components/base/portal-to-follow-elem'
  9. import { useInstalledPluginList } from '@/service/use-plugins'
  10. import { PLUGIN_TYPE_SEARCH_MAP } from '../../marketplace/plugin-type-switch'
  11. import SearchBox from '@/app/components/plugins/marketplace/search-box'
  12. import { useTranslation } from 'react-i18next'
  13. import cn from '@/utils/classnames'
  14. import ToolItem from './tool-item'
  15. import Loading from '@/app/components/base/loading'
  16. import NoDataPlaceholder from './no-data-placeholder'
  17. import { PluginSource } from '../../types'
  18. type Props = {
  19. trigger: React.ReactNode
  20. value: string[]
  21. onChange: (value: string[]) => void
  22. isShow: boolean
  23. onShowChange: (isShow: boolean) => void
  24. }
  25. const ToolPicker: FC<Props> = ({
  26. trigger,
  27. value,
  28. onChange,
  29. isShow,
  30. onShowChange,
  31. }) => {
  32. const { t } = useTranslation()
  33. const toggleShowPopup = useCallback(() => {
  34. onShowChange(!isShow)
  35. }, [onShowChange, isShow])
  36. const tabs = [
  37. {
  38. key: PLUGIN_TYPE_SEARCH_MAP.all,
  39. name: t('plugin.category.all'),
  40. },
  41. {
  42. key: PLUGIN_TYPE_SEARCH_MAP.model,
  43. name: t('plugin.category.models'),
  44. },
  45. {
  46. key: PLUGIN_TYPE_SEARCH_MAP.tool,
  47. name: t('plugin.category.tools'),
  48. },
  49. {
  50. key: PLUGIN_TYPE_SEARCH_MAP.agent,
  51. name: t('plugin.category.agents'),
  52. },
  53. {
  54. key: PLUGIN_TYPE_SEARCH_MAP.extension,
  55. name: t('plugin.category.extensions'),
  56. },
  57. {
  58. key: PLUGIN_TYPE_SEARCH_MAP.bundle,
  59. name: t('plugin.category.bundles'),
  60. },
  61. ]
  62. const [pluginType, setPluginType] = useState(PLUGIN_TYPE_SEARCH_MAP.all)
  63. const [query, setQuery] = useState('')
  64. const [tags, setTags] = useState<string[]>([])
  65. const { data, isLoading } = useInstalledPluginList()
  66. const filteredList = useMemo(() => {
  67. const list = data ? data.plugins : []
  68. return list.filter((plugin) => {
  69. const isFromMarketPlace = plugin.source === PluginSource.marketplace
  70. return (
  71. isFromMarketPlace && (pluginType === PLUGIN_TYPE_SEARCH_MAP.all || plugin.declaration.category === pluginType)
  72. && (tags.length === 0 || tags.some(tag => plugin.declaration.tags.includes(tag)))
  73. && (query === '' || plugin.plugin_id.toLowerCase().includes(query.toLowerCase()))
  74. )
  75. })
  76. }, [data, pluginType, query, tags])
  77. const handleCheckChange = useCallback((pluginId: string) => {
  78. return () => {
  79. const newValue = value.includes(pluginId)
  80. ? value.filter(id => id !== pluginId)
  81. : [...value, pluginId]
  82. onChange(newValue)
  83. }
  84. }, [onChange, value])
  85. const listContent = (
  86. <div className='max-h-[396px] overflow-y-auto'>
  87. {filteredList.map(item => (
  88. <ToolItem
  89. key={item.plugin_id}
  90. payload={item}
  91. isChecked={value.includes(item.plugin_id)}
  92. onCheckChange={handleCheckChange(item.plugin_id)}
  93. />
  94. ))}
  95. </div>
  96. )
  97. const loadingContent = (
  98. <div className='flex h-[396px] items-center justify-center'>
  99. <Loading />
  100. </div>
  101. )
  102. const noData = (
  103. <NoDataPlaceholder className='h-[396px]' noPlugins={!query} />
  104. )
  105. return (
  106. <PortalToFollowElem
  107. placement='top'
  108. offset={0}
  109. open={isShow}
  110. onOpenChange={onShowChange}
  111. >
  112. <PortalToFollowElemTrigger
  113. onClick={toggleShowPopup}
  114. >
  115. {trigger}
  116. </PortalToFollowElemTrigger>
  117. <PortalToFollowElemContent className='z-[1000]'>
  118. <div className={cn('relative min-h-20 w-[436px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-2 shadow-lg backdrop-blur-sm')}>
  119. <div className='p-2 pb-1'>
  120. <SearchBox
  121. search={query}
  122. onSearchChange={setQuery}
  123. tags={tags}
  124. onTagsChange={setTags}
  125. placeholder={t('plugin.searchTools')!}
  126. inputClassName='w-full'
  127. />
  128. </div>
  129. <div className='flex items-center justify-between border-b-[0.5px] border-divider-subtle bg-background-default-hover px-3 shadow-xs'>
  130. <div className='flex h-8 items-center space-x-1'>
  131. {
  132. tabs.map(tab => (
  133. <div
  134. className={cn(
  135. 'flex h-6 cursor-pointer items-center rounded-md px-2 hover:bg-state-base-hover',
  136. 'text-xs font-medium text-text-secondary',
  137. pluginType === tab.key && 'bg-state-base-hover-alt',
  138. )}
  139. key={tab.key}
  140. onClick={() => setPluginType(tab.key)}
  141. >
  142. {tab.name}
  143. </div>
  144. ))
  145. }
  146. </div>
  147. </div>
  148. {!isLoading && filteredList.length > 0 && listContent}
  149. {!isLoading && filteredList.length === 0 && noData}
  150. {isLoading && loadingContent}
  151. </div>
  152. </PortalToFollowElemContent>
  153. </PortalToFollowElem>
  154. )
  155. }
  156. export default React.memo(ToolPicker)