textarea.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import React, { useState } from 'react'
  2. import { useTranslation } from 'react-i18next'
  3. import {
  4. RiEqualizer2Line,
  5. } from '@remixicon/react'
  6. import Image from 'next/image'
  7. import Button from '../../base/button'
  8. import { getIcon } from '../common/retrieval-method-info'
  9. import ModifyExternalRetrievalModal from './modify-external-retrieval-modal'
  10. import Tooltip from '@/app/components/base/tooltip'
  11. import cn from '@/utils/classnames'
  12. import type { ExternalKnowledgeBaseHitTestingResponse, HitTestingResponse } from '@/models/datasets'
  13. import { externalKnowledgeBaseHitTesting, hitTesting } from '@/service/datasets'
  14. import { asyncRunSafe } from '@/utils'
  15. import { RETRIEVE_METHOD, type RetrievalConfig } from '@/types/app'
  16. import promptS from '@/app/components/app/configuration/config-prompt/style.module.css'
  17. type TextAreaWithButtonIProps = {
  18. datasetId: string
  19. onUpdateList: () => void
  20. setHitResult: (res: HitTestingResponse) => void
  21. setExternalHitResult: (res: ExternalKnowledgeBaseHitTestingResponse) => void
  22. loading: boolean
  23. setLoading: (v: boolean) => void
  24. text: string
  25. setText: (v: string) => void
  26. isExternal?: boolean
  27. onClickRetrievalMethod: () => void
  28. retrievalConfig: RetrievalConfig
  29. isEconomy: boolean
  30. onSubmit?: () => void
  31. }
  32. const TextAreaWithButton = ({
  33. datasetId,
  34. onUpdateList,
  35. setHitResult,
  36. setExternalHitResult,
  37. setLoading,
  38. loading,
  39. text,
  40. setText,
  41. isExternal = false,
  42. onClickRetrievalMethod,
  43. retrievalConfig,
  44. isEconomy,
  45. onSubmit: _onSubmit,
  46. }: TextAreaWithButtonIProps) => {
  47. const { t } = useTranslation()
  48. const [isSettingsOpen, setIsSettingsOpen] = useState(false)
  49. const [externalRetrievalSettings, setExternalRetrievalSettings] = useState({
  50. top_k: 2,
  51. score_threshold: 0.5,
  52. score_threshold_enabled: false,
  53. })
  54. const handleSaveExternalRetrievalSettings = (data: { top_k: number; score_threshold: number; score_threshold_enabled: boolean }) => {
  55. setExternalRetrievalSettings(data)
  56. setIsSettingsOpen(false)
  57. }
  58. function handleTextChange(event: any) {
  59. setText(event.target.value)
  60. }
  61. const onSubmit = async () => {
  62. setLoading(true)
  63. const [e, res] = await asyncRunSafe<HitTestingResponse>(
  64. hitTesting({
  65. datasetId,
  66. queryText: text,
  67. retrieval_model: {
  68. ...retrievalConfig,
  69. search_method: isEconomy ? RETRIEVE_METHOD.keywordSearch : retrievalConfig.search_method,
  70. },
  71. }) as Promise<HitTestingResponse>,
  72. )
  73. if (!e) {
  74. setHitResult(res)
  75. onUpdateList?.()
  76. }
  77. setLoading(false)
  78. _onSubmit && _onSubmit()
  79. }
  80. const externalRetrievalTestingOnSubmit = async () => {
  81. setLoading(true)
  82. const [e, res] = await asyncRunSafe<ExternalKnowledgeBaseHitTestingResponse>(
  83. externalKnowledgeBaseHitTesting({
  84. datasetId,
  85. query: text,
  86. external_retrieval_model: {
  87. top_k: externalRetrievalSettings.top_k,
  88. score_threshold: externalRetrievalSettings.score_threshold,
  89. score_threshold_enabled: externalRetrievalSettings.score_threshold_enabled,
  90. },
  91. }) as Promise<ExternalKnowledgeBaseHitTestingResponse>,
  92. )
  93. if (!e) {
  94. setExternalHitResult(res)
  95. onUpdateList?.()
  96. }
  97. setLoading(false)
  98. }
  99. const retrievalMethod = isEconomy ? RETRIEVE_METHOD.invertedIndex : retrievalConfig.search_method
  100. const icon = <Image className='size-3.5 text-util-colors-purple-purple-600' src={getIcon(retrievalMethod)} alt='' />
  101. return (
  102. <>
  103. <div className={cn('relative rounded-xl', promptS.gradientBorder)}>
  104. <div className='relative pt-1.5 rounded-tl-xl rounded-tr-xl bg-background-section-burn'>
  105. <div className="pl-4 pr-1.5 pb-1 flex justify-between h-8 items-center">
  106. <span className="text-text-secondary font-semibold text-[13px] leading-4 uppercase">
  107. {t('datasetHitTesting.input.title')}
  108. </span>
  109. {isExternal
  110. ? <Button
  111. variant='secondary'
  112. size='small'
  113. onClick={() => setIsSettingsOpen(!isSettingsOpen)}
  114. >
  115. <RiEqualizer2Line className='text-components-button-secondary-text w-3.5 h-3.5' />
  116. <div className='flex px-[3px] justify-center items-center gap-1'>
  117. <span className='text-components-button-secondary-text system-xs-medium'>{t('datasetHitTesting.settingTitle')}</span>
  118. </div>
  119. </Button>
  120. : <div
  121. onClick={onClickRetrievalMethod}
  122. className='flex px-1.5 h-7 items-center bg-components-button-secondary-bg hover:bg-components-button-secondary-bg-hover rounded-lg border-[0.5px] border-components-button-secondary-bg shadow-xs backdrop-blur-[5px] cursor-pointer space-x-0.5'
  123. >
  124. {icon}
  125. <div className='text-text-secondary text-xs font-medium uppercase'>{t(`dataset.retrieval.${retrievalMethod}.title`)}</div>
  126. <RiEqualizer2Line className='size-4 text-components-menu-item-text'></RiEqualizer2Line>
  127. </div>
  128. }
  129. </div>
  130. {
  131. isSettingsOpen && (
  132. <ModifyExternalRetrievalModal
  133. onClose={() => setIsSettingsOpen(false)}
  134. onSave={handleSaveExternalRetrievalSettings}
  135. initialTopK={externalRetrievalSettings.top_k}
  136. initialScoreThreshold={externalRetrievalSettings.score_threshold}
  137. initialScoreThresholdEnabled={externalRetrievalSettings.score_threshold_enabled}
  138. />
  139. )
  140. }
  141. <div className='h-2 rounded-tl-xl rounded-tr-xl bg-background-default'></div>
  142. </div>
  143. <div className='px-4 pb-11 bg-background-default rounded-b-xl'>
  144. <textarea
  145. className='h-[220px] border-none resize-none font-normal caret-primary-600 text-text-secondary text-sm w-full focus-visible:outline-none placeholder:text-gray-300 placeholder:text-sm placeholder:font-normal'
  146. value={text}
  147. onChange={handleTextChange}
  148. placeholder={t('datasetHitTesting.input.placeholder') as string}
  149. />
  150. <div className="absolute inset-x-0 bottom-0 flex items-center justify-between mx-4 mt-2 mb-2">
  151. {text?.length > 200
  152. ? (
  153. <Tooltip
  154. popupContent={t('datasetHitTesting.input.countWarning')}
  155. >
  156. <div
  157. className={cn('flex items-center h-5 px-1 rounded-md bg-background-section-burn text-red-600 text-xs font-medium', !text?.length && 'opacity-50')}
  158. >
  159. {text?.length}
  160. <span className="text-red-300 mx-0.5">/</span>
  161. 200
  162. </div>
  163. </Tooltip>
  164. )
  165. : (
  166. <div
  167. className={cn('flex items-center h-5 px-1 rounded-md bg-background-section-burn text-text-tertiary text-xs font-medium', !text?.length && 'opacity-50')}
  168. >
  169. {text?.length}
  170. <span className="text-divider-deep mx-0.5">/</span>
  171. 200
  172. </div>
  173. )}
  174. <div>
  175. <Button
  176. onClick={isExternal ? externalRetrievalTestingOnSubmit : onSubmit}
  177. variant="primary"
  178. loading={loading}
  179. disabled={(!text?.length || text?.length > 200)}
  180. className='w-[88px]'
  181. >
  182. {t('datasetHitTesting.input.testing')}
  183. </Button>
  184. </div>
  185. </div>
  186. </div>
  187. </div>
  188. </>
  189. )
  190. }
  191. export default TextAreaWithButton