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.

index.tsx 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import type { FC } from 'react'
  2. import { Fragment, useState } from 'react'
  3. import { Popover, Transition } from '@headlessui/react'
  4. import { useTranslation } from 'react-i18next'
  5. import _ from 'lodash-es'
  6. import cn from 'classnames'
  7. import s from './style.module.css'
  8. import type { BackendModel, ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
  9. import { ModelType } from '@/app/components/header/account-setting/model-page/declarations'
  10. import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
  11. import { Check, SearchLg } from '@/app/components/base/icons/src/vender/line/general'
  12. import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
  13. import { AlertCircle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
  14. import Tooltip from '@/app/components/base/tooltip'
  15. import ModelIcon from '@/app/components/app/configuration/config-model/model-icon'
  16. import ModelName from '@/app/components/app/configuration/config-model/model-name'
  17. import ProviderName from '@/app/components/app/configuration/config-model/provider-name'
  18. import { useProviderContext } from '@/context/provider-context'
  19. import ModelModeTypeLabel from '@/app/components/app/configuration/config-model/model-mode-type-label'
  20. import type { ModelModeType } from '@/types/app'
  21. import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
  22. import { useModalContext } from '@/context/modal-context'
  23. type Props = {
  24. value: {
  25. providerName: ProviderEnum
  26. modelName: string
  27. } | undefined
  28. modelType: ModelType
  29. isShowModelModeType?: boolean
  30. isShowAddModel?: boolean
  31. supportAgentThought?: boolean
  32. onChange: (value: BackendModel) => void
  33. popClassName?: string
  34. readonly?: boolean
  35. triggerIconSmall?: boolean
  36. whenEmptyGoToSetting?: boolean
  37. }
  38. type ModelOption = {
  39. type: 'model'
  40. value: string
  41. providerName: ProviderEnum
  42. modelDisplayName: string
  43. model_mode: ModelModeType
  44. } | {
  45. type: 'provider'
  46. value: ProviderEnum
  47. }
  48. const ModelSelector: FC<Props> = ({
  49. value,
  50. modelType,
  51. isShowModelModeType,
  52. isShowAddModel,
  53. supportAgentThought,
  54. onChange,
  55. popClassName,
  56. readonly,
  57. triggerIconSmall,
  58. whenEmptyGoToSetting,
  59. }) => {
  60. const { t } = useTranslation()
  61. const { setShowAccountSettingModal } = useModalContext()
  62. const {
  63. textGenerationModelList,
  64. embeddingsModelList,
  65. speech2textModelList,
  66. rerankModelList,
  67. agentThoughtModelList,
  68. } = useProviderContext()
  69. const [search, setSearch] = useState('')
  70. const modelList = supportAgentThought
  71. ? agentThoughtModelList
  72. : ({
  73. [ModelType.textGeneration]: textGenerationModelList,
  74. [ModelType.embeddings]: embeddingsModelList,
  75. [ModelType.speech2text]: speech2textModelList,
  76. [ModelType.reranking]: rerankModelList,
  77. })[modelType]
  78. const currModel = modelList.find(item => item.model_name === value?.modelName && item.model_provider.provider_name === value.providerName)
  79. const allModelNames = (() => {
  80. if (!search)
  81. return {}
  82. const res: Record<string, string> = {}
  83. modelList.forEach(({ model_name, model_display_name }) => {
  84. res[model_name] = model_display_name
  85. })
  86. return res
  87. })()
  88. const filteredModelList = search
  89. ? modelList.filter(({ model_name }) => {
  90. if (allModelNames[model_name].includes(search))
  91. return true
  92. return false
  93. })
  94. : modelList
  95. const hasRemoved = value && !modelList.find(({ model_name, model_provider }) => model_name === value.modelName && model_provider.provider_name === value.providerName)
  96. const modelOptions: ModelOption[] = (() => {
  97. const providers = _.uniq(filteredModelList.map(item => item.model_provider.provider_name))
  98. const res: ModelOption[] = []
  99. providers.forEach((providerName) => {
  100. res.push({
  101. type: 'provider',
  102. value: providerName,
  103. })
  104. const models = filteredModelList.filter(m => m.model_provider.provider_name === providerName)
  105. models.forEach(({ model_name, model_display_name, model_mode }) => {
  106. res.push({
  107. type: 'model',
  108. providerName,
  109. value: model_name,
  110. modelDisplayName: model_display_name,
  111. model_mode,
  112. })
  113. })
  114. })
  115. return res
  116. })()
  117. return (
  118. <div className=''>
  119. <Popover className='relative'>
  120. <Popover.Button className={cn('flex items-center px-2.5 w-full h-9 rounded-lg', readonly ? '!cursor-auto bg-gray-100 opacity-50' : 'bg-gray-100', hasRemoved && '!bg-[#FEF3F2]')}>
  121. {
  122. ({ open }) => (
  123. <>
  124. {
  125. value
  126. ? (
  127. <>
  128. <ModelIcon
  129. className={cn('mr-1.5', !triggerIconSmall && 'w-5 h-5')}
  130. modelId={value.modelName}
  131. providerName={value.providerName}
  132. />
  133. <div className='mr-1.5 grow flex items-center text-left text-sm text-gray-900 truncate'>
  134. <ModelName modelId={value.modelName} modelDisplayName={currModel?.model_display_name || value.modelName} />
  135. {isShowModelModeType && (
  136. <ModelModeTypeLabel className='ml-2' type={currModel?.model_mode as ModelModeType} />
  137. )}
  138. </div>
  139. </>
  140. )
  141. : (
  142. <div className='grow text-left text-sm text-gray-800 opacity-60'>{t('common.modelProvider.selectModel')}</div>
  143. )
  144. }
  145. {
  146. hasRemoved && (
  147. <Tooltip
  148. selector='model-selector-remove-tip'
  149. htmlContent={
  150. <div className='w-[261px] text-gray-500'>{t('common.modelProvider.selector.tip')}</div>
  151. }
  152. >
  153. <AlertCircle className='mr-1 w-4 h-4 text-[#F04438]' />
  154. </Tooltip>
  155. )
  156. }
  157. {!readonly && <ChevronDown className={`w-4 h-4 text-gray-700 ${open ? 'opacity-100' : 'opacity-60'}`} />}
  158. </>
  159. )
  160. }
  161. </Popover.Button>
  162. {!readonly && (
  163. <Transition
  164. as={Fragment}
  165. leave='transition ease-in duration-100'
  166. leaveFrom='opacity-100'
  167. leaveTo='opacity-0'
  168. >
  169. <Popover.Panel className={cn(popClassName, isShowModelModeType ? 'max-w-[312px]' : 'max-w-[260px]', 'absolute top-10 p-1 min-w-[232px] max-h-[366px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg overflow-auto z-10')}>
  170. <div className='px-2 pt-2 pb-1'>
  171. <div className='flex items-center px-2 h-8 bg-gray-100 rounded-lg'>
  172. <div className='mr-1.5 p-[1px]'><SearchLg className='w-[14px] h-[14px] text-gray-400' /></div>
  173. <div className='grow px-0.5'>
  174. <input
  175. value={search}
  176. onChange={e => setSearch(e.target.value)}
  177. className={`
  178. block w-full h-8 bg-transparent text-[13px] text-gray-700
  179. outline-none appearance-none border-none
  180. `}
  181. placeholder={t('common.modelProvider.searchModel') || ''}
  182. />
  183. </div>
  184. {
  185. search && (
  186. <div className='ml-1 p-0.5 cursor-pointer' onClick={() => setSearch('')}>
  187. <XCircle className='w-3 h-3 text-gray-400' />
  188. </div>
  189. )
  190. }
  191. </div>
  192. </div>
  193. {
  194. modelOptions.map((model) => {
  195. if (model.type === 'provider') {
  196. return (
  197. <div
  198. className='px-3 pt-2 pb-1 text-xs font-medium text-gray-500'
  199. key={`${model.type}-${model.value}`}
  200. >
  201. <ProviderName provideName={model.value} />
  202. </div>
  203. )
  204. }
  205. if (model.type === 'model') {
  206. return (
  207. <Popover.Button
  208. key={`${model.providerName}-${model.value}`}
  209. className={`${s.optionItem}
  210. flex items-center px-3 w-full h-8 rounded-lg hover:bg-gray-50
  211. ${!readonly ? 'cursor-pointer' : 'cursor-auto'}
  212. ${(value?.providerName === model.providerName && value?.modelName === model.value) && 'bg-gray-50'}
  213. `}
  214. onClick={() => {
  215. const selectedModel = modelList.find((item) => {
  216. return item.model_name === model.value && item.model_provider.provider_name === model.providerName
  217. })
  218. onChange(selectedModel as BackendModel)
  219. }}
  220. >
  221. <ModelIcon
  222. className='mr-2 shrink-0'
  223. modelId={model.value}
  224. providerName={model.providerName}
  225. />
  226. <div className='mr-2 grow flex items-center text-left text-sm text-gray-900 truncate'>
  227. <ModelName modelId={model.value} modelDisplayName={model.modelDisplayName} />
  228. {isShowModelModeType && (
  229. <ModelModeTypeLabel className={`${s.modelModeLabel} ml-2`} type={model.model_mode} />
  230. )}
  231. </div>
  232. { (value?.providerName === model.providerName && value?.modelName === model.value) && <Check className='shrink-0 w-4 h-4 text-primary-600' /> }
  233. </Popover.Button>
  234. )
  235. }
  236. return null
  237. })
  238. }
  239. {
  240. whenEmptyGoToSetting && modelList.length === 0 && (
  241. <div className='pt-6'>
  242. <div className='flex items-center justify-center mx-auto mb-2 w-12 h-12 rounded-[10px] border border-[#EAECF5]'>
  243. <CubeOutline className='w-6 h-6 text-gray-500' />
  244. </div>
  245. <div className='mb-1 text-center text-[13px] font-medium text-gray-500'>
  246. {t('common.modelProvider.selector.emptyTip')}
  247. </div>
  248. <div className='mb-6 text-center text-xs text-primary-500'>
  249. <span onClick={() => setShowAccountSettingModal({ payload: 'provider' })}>{t('common.modelProvider.selector.emptySetting')}</span>
  250. </div>
  251. </div>
  252. )
  253. }
  254. {modelList.length !== 0 && (search && filteredModelList.length === 0) && (
  255. <div className='px-3 pt-1.5 h-[30px] text-center text-xs text-gray-500'>{t('common.modelProvider.noModelFound', { model: search })}</div>
  256. )}
  257. {isShowAddModel && (
  258. <div
  259. className='border-t flex items-center h-9 pl-3 text-xs text-[#155EEF] cursor-pointer'
  260. style={{
  261. borderColor: 'rgba(0, 0, 0, 0.05)',
  262. }}
  263. onClick={() => setShowAccountSettingModal({ payload: 'provider' })}
  264. >
  265. <CubeOutline className='w-4 h-4 mr-2' />
  266. <div>{t('common.model.addMoreModel')}</div>
  267. </div>
  268. )}
  269. </Popover.Panel>
  270. </Transition>
  271. )}
  272. </Popover>
  273. </div>
  274. )
  275. }
  276. export default ModelSelector