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 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. 'use client'
  2. import { useRouter } from 'next/navigation'
  3. import { useTranslation } from 'react-i18next'
  4. import type { DataSet } from '@/models/datasets'
  5. import { useSelector as useAppContextWithSelector } from '@/context/app-context'
  6. import { useKnowledge } from '@/hooks/use-knowledge'
  7. import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
  8. import type { Tag } from '@/app/components/base/tag-management/constant'
  9. import TagSelector from '@/app/components/base/tag-management/selector'
  10. import cn from '@/utils/classnames'
  11. import { useHover } from 'ahooks'
  12. import { RiFileTextFill, RiMoreFill, RiRobot2Fill } from '@remixicon/react'
  13. import Tooltip from '@/app/components/base/tooltip'
  14. import { checkIsUsedInApp, deleteDataset } from '@/service/datasets'
  15. import RenameDatasetModal from '../../rename-modal'
  16. import Confirm from '@/app/components/base/confirm'
  17. import Toast from '@/app/components/base/toast'
  18. import CustomPopover from '@/app/components/base/popover'
  19. import Operations from './operations'
  20. import AppIcon from '@/app/components/base/app-icon'
  21. import CornerLabel from '@/app/components/base/corner-label'
  22. import { DOC_FORM_ICON_WITH_BG, DOC_FORM_TEXT } from '@/models/datasets'
  23. import { useExportPipelineDSL } from '@/service/use-pipeline'
  24. import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
  25. const EXTERNAL_PROVIDER = 'external'
  26. type DatasetCardProps = {
  27. dataset: DataSet
  28. onSuccess?: () => void
  29. }
  30. const DatasetCard = ({
  31. dataset,
  32. onSuccess,
  33. }: DatasetCardProps) => {
  34. const { t } = useTranslation()
  35. const { push } = useRouter()
  36. const isCurrentWorkspaceDatasetOperator = useAppContextWithSelector(state => state.isCurrentWorkspaceDatasetOperator)
  37. const [tags, setTags] = useState<Tag[]>(dataset.tags)
  38. const tagSelectorRef = useRef<HTMLDivElement>(null)
  39. const isHoveringTagSelector = useHover(tagSelectorRef)
  40. const [showRenameModal, setShowRenameModal] = useState(false)
  41. const [showConfirmDelete, setShowConfirmDelete] = useState(false)
  42. const [confirmMessage, setConfirmMessage] = useState<string>('')
  43. const [exporting, setExporting] = useState(false)
  44. const isExternalProvider = useMemo(() => {
  45. return dataset.provider === EXTERNAL_PROVIDER
  46. }, [dataset.provider])
  47. const isPipelineUnpublished = useMemo(() => {
  48. return dataset.runtime_mode === 'rag_pipeline' && !dataset.is_published
  49. }, [dataset.runtime_mode, dataset.is_published])
  50. const isShowChunkingModeIcon = useMemo(() => {
  51. return dataset.doc_form && (dataset.runtime_mode !== 'rag_pipeline' || dataset.is_published)
  52. }, [dataset.doc_form, dataset.runtime_mode, dataset.is_published])
  53. const isShowDocModeInfo = useMemo(() => {
  54. return dataset.doc_form && dataset.indexing_technique && dataset.retrieval_model_dict?.search_method && (dataset.runtime_mode !== 'rag_pipeline' || dataset.is_published)
  55. }, [dataset.doc_form, dataset.indexing_technique, dataset.retrieval_model_dict?.search_method, dataset.runtime_mode, dataset.is_published])
  56. const chunkingModeIcon = dataset.doc_form ? DOC_FORM_ICON_WITH_BG[dataset.doc_form] : React.Fragment
  57. const Icon = isExternalProvider ? DOC_FORM_ICON_WITH_BG.external : chunkingModeIcon
  58. const iconInfo = dataset.icon_info || {
  59. icon: '📙',
  60. icon_type: 'emoji',
  61. icon_background: '#FFF4ED',
  62. icon_url: '',
  63. }
  64. const { formatIndexingTechniqueAndMethod } = useKnowledge()
  65. const documentCount = useMemo(() => {
  66. const availableDocCount = dataset.total_available_documents ?? 0
  67. if (availableDocCount === dataset.document_count)
  68. return `${dataset.document_count}`
  69. if (availableDocCount < dataset.document_count)
  70. return `${availableDocCount} / ${dataset.document_count}`
  71. }, [dataset.document_count, dataset.total_available_documents])
  72. const documentCountTooltip = useMemo(() => {
  73. const availableDocCount = dataset.total_available_documents ?? 0
  74. if (availableDocCount === dataset.document_count)
  75. return t('dataset.docAllEnabled', { count: availableDocCount })
  76. if (availableDocCount < dataset.document_count)
  77. return t('dataset.partialEnabled', { count: dataset.document_count, num: availableDocCount })
  78. }, [t, dataset.document_count, dataset.total_available_documents])
  79. const { formatTimeFromNow } = useFormatTimeFromNow()
  80. const openRenameModal = useCallback(() => {
  81. setShowRenameModal(true)
  82. }, [])
  83. const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
  84. const handleExportPipeline = useCallback(async (include = false) => {
  85. const { pipeline_id, name } = dataset
  86. if (!pipeline_id)
  87. return
  88. if (exporting)
  89. return
  90. try {
  91. setExporting(true)
  92. const { data } = await exportPipelineConfig({
  93. pipelineId: pipeline_id,
  94. include,
  95. })
  96. const a = document.createElement('a')
  97. const file = new Blob([data], { type: 'application/yaml' })
  98. const url = URL.createObjectURL(file)
  99. a.href = url
  100. a.download = `${name}.pipeline`
  101. a.click()
  102. URL.revokeObjectURL(url)
  103. }
  104. catch {
  105. Toast.notify({ type: 'error', message: t('app.exportFailed') })
  106. }
  107. finally {
  108. setExporting(false)
  109. }
  110. }, [dataset, exportPipelineConfig, exporting, t])
  111. const detectIsUsedByApp = useCallback(async () => {
  112. try {
  113. const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
  114. setConfirmMessage(isUsedByApp ? t('dataset.datasetUsedByApp')! : t('dataset.deleteDatasetConfirmContent')!)
  115. setShowConfirmDelete(true)
  116. }
  117. catch (e: any) {
  118. const res = await e.json()
  119. Toast.notify({ type: 'error', message: res?.message || 'Unknown error' })
  120. }
  121. }, [dataset.id, t])
  122. const onConfirmDelete = useCallback(async () => {
  123. try {
  124. await deleteDataset(dataset.id)
  125. Toast.notify({ type: 'success', message: t('dataset.datasetDeleted') })
  126. if (onSuccess)
  127. onSuccess()
  128. }
  129. finally {
  130. setShowConfirmDelete(false)
  131. }
  132. }, [dataset.id, onSuccess, t])
  133. useEffect(() => {
  134. setTags(dataset.tags)
  135. }, [dataset])
  136. return (
  137. <>
  138. <div
  139. className='group relative col-span-1 flex h-[166px] cursor-pointer flex-col rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-xs shadow-shadow-shadow-3 transition-all duration-200 ease-in-out hover:bg-components-card-bg-alt hover:shadow-md hover:shadow-shadow-shadow-5'
  140. data-disable-nprogress={true}
  141. onClick={(e) => {
  142. e.preventDefault()
  143. isExternalProvider
  144. ? push(`/datasets/${dataset.id}/hitTesting`)
  145. // eslint-disable-next-line sonarjs/no-nested-conditional
  146. : isPipelineUnpublished
  147. ? push(`/datasets/${dataset.id}/pipeline`)
  148. : push(`/datasets/${dataset.id}/documents`)
  149. }}
  150. >
  151. {!dataset.embedding_available && (
  152. <CornerLabel
  153. label='Unavailable'
  154. className='absolute right-0 top-0 z-10'
  155. labelClassName='rounded-tr-xl' />
  156. )}
  157. <div className={cn('flex items-center gap-x-3 px-4 pb-2 pt-4', !dataset.embedding_available && 'opacity-30')}>
  158. <div className='relative shrink-0'>
  159. <AppIcon
  160. size='large'
  161. iconType={iconInfo.icon_type}
  162. icon={iconInfo.icon}
  163. background={iconInfo.icon_type === 'image' ? undefined : iconInfo.icon_background}
  164. imageUrl={iconInfo.icon_type === 'image' ? iconInfo.icon_url : undefined}
  165. />
  166. {(isShowChunkingModeIcon || isExternalProvider) && (
  167. <div className='absolute -bottom-1 -right-1 z-[5]'>
  168. <Icon className='size-4' />
  169. </div>
  170. )}
  171. </div>
  172. <div className='flex grow flex-col gap-y-1 overflow-hidden py-px'>
  173. <div
  174. className='system-md-semibold truncate text-text-secondary'
  175. title={dataset.name}
  176. >
  177. {dataset.name}
  178. </div>
  179. <div className='system-2xs-medium-uppercase flex items-center gap-x-3 text-text-tertiary'>
  180. {isExternalProvider && <span>{t('dataset.externalKnowledgeBase')}</span>}
  181. {!isExternalProvider && isShowDocModeInfo && (
  182. <>
  183. {dataset.doc_form && <span>{t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`)}</span>}
  184. {dataset.indexing_technique && <span>{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}</span>}
  185. </>
  186. )}
  187. </div>
  188. </div>
  189. </div>
  190. <div
  191. className={cn('system-xs-regular line-clamp-2 h-10 px-4 py-1 text-text-tertiary', !dataset.embedding_available && 'opacity-30')}
  192. title={dataset.description}
  193. >
  194. {dataset.description}
  195. </div>
  196. <div
  197. className={cn('relative w-full px-3', !dataset.embedding_available && 'opacity-30')}
  198. onClick={(e) => {
  199. e.stopPropagation()
  200. e.preventDefault()
  201. }}
  202. >
  203. <div
  204. ref={tagSelectorRef}
  205. className={cn(
  206. 'invisible w-full group-hover:visible',
  207. tags.length > 0 && 'visible',
  208. )}
  209. >
  210. <TagSelector
  211. position='bl'
  212. type='knowledge'
  213. targetID={dataset.id}
  214. value={tags.map(tag => tag.id)}
  215. selectedTags={tags}
  216. onCacheUpdate={setTags}
  217. onChange={onSuccess}
  218. />
  219. </div>
  220. {/* Tag Mask */}
  221. <div
  222. className={cn(
  223. 'absolute right-0 top-0 z-[5] h-full w-20 bg-tag-selector-mask-bg group-hover:bg-tag-selector-mask-hover-bg',
  224. isHoveringTagSelector && 'hidden',
  225. )}
  226. />
  227. </div>
  228. <div
  229. className={cn(
  230. 'flex items-center gap-x-3 px-4 pb-3 pt-2 text-text-tertiary',
  231. !dataset.embedding_available && 'opacity-30',
  232. )}
  233. >
  234. <Tooltip popupContent={documentCountTooltip} >
  235. <div className='flex items-center gap-x-1'>
  236. <RiFileTextFill className='size-3 text-text-quaternary' />
  237. <span className='system-xs-medium'>{documentCount}</span>
  238. </div>
  239. </Tooltip>
  240. {!isExternalProvider && (
  241. <Tooltip popupContent={`${dataset.app_count} ${t('dataset.appCount')}`}>
  242. <div className='flex items-center gap-x-1'>
  243. <RiRobot2Fill className='size-3 text-text-quaternary' />
  244. <span className='system-xs-medium'>{dataset.app_count}</span>
  245. </div>
  246. </Tooltip>
  247. )}
  248. <span className='system-xs-regular text-divider-deep'>/</span>
  249. <span className='system-xs-regular'>{`${t('dataset.updated')} ${formatTimeFromNow(dataset.updated_at * 1000)}`}</span>
  250. </div>
  251. <div className='absolute right-2 top-2 z-[5] hidden group-hover:block'>
  252. <CustomPopover
  253. htmlContent={
  254. <Operations
  255. showDelete={!isCurrentWorkspaceDatasetOperator}
  256. showExportPipeline={dataset.runtime_mode === 'rag_pipeline'}
  257. openRenameModal={openRenameModal}
  258. handleExportPipeline={handleExportPipeline}
  259. detectIsUsedByApp={detectIsUsedByApp}
  260. />
  261. }
  262. className={'z-20 min-w-[186px]'}
  263. popupClassName={'rounded-xl bg-none shadow-none ring-0 min-w-[186px]'}
  264. position='br'
  265. trigger='click'
  266. btnElement={
  267. <div className='flex size-8 items-center justify-center rounded-[10px] hover:bg-state-base-hover'>
  268. <RiMoreFill className='h-5 w-5 text-text-tertiary' />
  269. </div>
  270. }
  271. btnClassName={open =>
  272. cn(
  273. 'size-9 cursor-pointer justify-center rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0 shadow-lg shadow-shadow-shadow-5 ring-[2px] ring-inset ring-components-actionbar-bg hover:border-components-actionbar-border',
  274. open ? 'border-components-actionbar-border bg-state-base-hover' : '',
  275. )
  276. }
  277. />
  278. </div>
  279. </div>
  280. {showRenameModal && (
  281. <RenameDatasetModal
  282. show={showRenameModal}
  283. dataset={dataset}
  284. onClose={() => setShowRenameModal(false)}
  285. onSuccess={onSuccess}
  286. />
  287. )}
  288. {showConfirmDelete && (
  289. <Confirm
  290. title={t('dataset.deleteDatasetConfirmTitle')}
  291. content={confirmMessage}
  292. isShow={showConfirmDelete}
  293. onConfirm={onConfirmDelete}
  294. onCancel={() => setShowConfirmDelete(false)}
  295. />
  296. )}
  297. </>
  298. )
  299. }
  300. export default DatasetCard