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.

popup.tsx 5.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import type { FC } from 'react'
  2. import { useEffect, useMemo, useRef, useState } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import {
  5. RiArrowRightUpLine,
  6. RiSearchLine,
  7. } from '@remixicon/react'
  8. import type {
  9. DefaultModel,
  10. Model,
  11. ModelItem,
  12. } from '../declarations'
  13. import { ModelFeatureEnum } from '../declarations'
  14. import { useLanguage } from '../hooks'
  15. import PopupItem from './popup-item'
  16. import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
  17. import { useModalContext } from '@/context/modal-context'
  18. import { supportFunctionCall } from '@/utils/tool-call'
  19. import { tooltipManager } from '@/app/components/base/tooltip/TooltipManager'
  20. type PopupProps = {
  21. defaultModel?: DefaultModel
  22. modelList: Model[]
  23. onSelect: (provider: string, model: ModelItem) => void
  24. scopeFeatures?: string[]
  25. onHide: () => void
  26. }
  27. const Popup: FC<PopupProps> = ({
  28. defaultModel,
  29. modelList,
  30. onSelect,
  31. scopeFeatures = [],
  32. onHide,
  33. }) => {
  34. const { t } = useTranslation()
  35. const language = useLanguage()
  36. const [searchText, setSearchText] = useState('')
  37. const { setShowAccountSettingModal } = useModalContext()
  38. const scrollRef = useRef<HTMLDivElement>(null)
  39. // Close any open tooltips when the user scrolls to prevent them from appearing
  40. // in incorrect positions or becoming detached from their trigger elements
  41. useEffect(() => {
  42. const handleTooltipCloseOnScroll = () => {
  43. tooltipManager.closeActiveTooltip()
  44. }
  45. const scrollContainer = scrollRef.current
  46. if (!scrollContainer) return
  47. // Use passive listener for better performance since we don't prevent default
  48. scrollContainer.addEventListener('scroll', handleTooltipCloseOnScroll, { passive: true })
  49. return () => {
  50. scrollContainer.removeEventListener('scroll', handleTooltipCloseOnScroll)
  51. }
  52. }, [])
  53. const filteredModelList = useMemo(() => {
  54. return modelList.map((model) => {
  55. const filteredModels = model.models
  56. .filter((modelItem) => {
  57. if (modelItem.label[language] !== undefined)
  58. return modelItem.label[language].toLowerCase().includes(searchText.toLowerCase())
  59. return Object.values(modelItem.label).some(label =>
  60. label.toLowerCase().includes(searchText.toLowerCase()),
  61. )
  62. })
  63. .filter((modelItem) => {
  64. if (scopeFeatures.length === 0)
  65. return true
  66. return scopeFeatures.every((feature) => {
  67. if (feature === ModelFeatureEnum.toolCall)
  68. return supportFunctionCall(modelItem.features)
  69. return modelItem.features?.some(featureItem => featureItem === feature)
  70. })
  71. })
  72. return { ...model, models: filteredModels }
  73. }).filter(model => model.models.length > 0)
  74. }, [language, modelList, scopeFeatures, searchText])
  75. return (
  76. <div ref={scrollRef} className='max-h-[480px] w-[320px] overflow-y-auto rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg'>
  77. <div className='sticky top-0 z-10 bg-components-panel-bg pb-1 pl-3 pr-2 pt-3'>
  78. <div className={`
  79. flex h-8 items-center rounded-lg border pl-[9px] pr-[10px]
  80. ${searchText ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-transparent bg-components-input-bg-normal'}
  81. `}>
  82. <RiSearchLine
  83. className={`
  84. mr-[7px] h-[14px] w-[14px] shrink-0
  85. ${searchText ? 'text-text-tertiary' : 'text-text-quaternary'}
  86. `}
  87. />
  88. <input
  89. className='block h-[18px] grow appearance-none bg-transparent text-[13px] text-text-primary outline-none'
  90. placeholder={t('datasetSettings.form.searchModel') || ''}
  91. value={searchText}
  92. onChange={e => setSearchText(e.target.value)}
  93. />
  94. {
  95. searchText && (
  96. <XCircle
  97. className='ml-1.5 h-[14px] w-[14px] shrink-0 cursor-pointer text-text-quaternary'
  98. onClick={() => setSearchText('')}
  99. />
  100. )
  101. }
  102. </div>
  103. </div>
  104. <div className='p-1'>
  105. {
  106. filteredModelList.map(model => (
  107. <PopupItem
  108. key={model.provider}
  109. defaultModel={defaultModel}
  110. model={model}
  111. onSelect={onSelect}
  112. />
  113. ))
  114. }
  115. {
  116. !filteredModelList.length && (
  117. <div className='break-all px-3 py-1.5 text-center text-xs leading-[18px] text-text-tertiary'>
  118. {`No model found for “${searchText}”`}
  119. </div>
  120. )
  121. }
  122. </div>
  123. <div className='sticky bottom-0 flex cursor-pointer items-center rounded-b-lg border-t border-divider-subtle bg-components-panel-bg px-4 py-2 text-text-accent-light-mode-only' onClick={() => {
  124. onHide()
  125. setShowAccountSettingModal({ payload: 'provider' })
  126. }}>
  127. <span className='system-xs-medium'>{t('common.model.settingsLink')}</span>
  128. <RiArrowRightUpLine className='ml-0.5 h-3 w-3' />
  129. </div>
  130. </div>
  131. )
  132. }
  133. export default Popup