index.tsx 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171
  1. 'use client'
  2. import type { FC, PropsWithChildren } from 'react'
  3. import React, { useCallback, useEffect, useRef, useState } from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import { useContext } from 'use-context-selector'
  6. import {
  7. RiAlertFill,
  8. RiArrowLeftLine,
  9. RiSearchEyeLine,
  10. } from '@remixicon/react'
  11. import Link from 'next/link'
  12. import Image from 'next/image'
  13. import { useHover } from 'ahooks'
  14. import SettingCog from '../assets/setting-gear-mod.svg'
  15. import OrangeEffect from '../assets/option-card-effect-orange.svg'
  16. import FamilyMod from '../assets/family-mod.svg'
  17. import Note from '../assets/note-mod.svg'
  18. import FileList from '../assets/file-list-3-fill.svg'
  19. import { indexMethodIcon } from '../icons'
  20. import { PreviewContainer } from '../../preview/container'
  21. import { ChunkContainer, QAPreview } from '../../chunk'
  22. import { PreviewHeader } from '../../preview/header'
  23. import { FormattedText } from '../../formatted-text/formatted'
  24. import { PreviewSlice } from '../../formatted-text/flavours/preview-slice'
  25. import PreviewDocumentPicker from '../../common/document-picker/preview-document-picker'
  26. import s from './index.module.css'
  27. import unescape from './unescape'
  28. import escape from './escape'
  29. import { OptionCard } from './option-card'
  30. import LanguageSelect from './language-select'
  31. import { DelimiterInput, MaxLengthInput, OverlapInput } from './inputs'
  32. import cn from '@/utils/classnames'
  33. import type { CrawlOptions, CrawlResultItem, CreateDocumentReq, CustomFile, DocumentItem, FullDocumentDetail, ParentMode, PreProcessingRule, ProcessRule, Rules, createDocumentResponse } from '@/models/datasets'
  34. import { ChunkingMode, DataSourceType, ProcessMode } from '@/models/datasets'
  35. import Button from '@/app/components/base/button'
  36. import FloatRightContainer from '@/app/components/base/float-right-container'
  37. import RetrievalMethodConfig from '@/app/components/datasets/common/retrieval-method-config'
  38. import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/economical-retrieval-method-config'
  39. import { type RetrievalConfig } from '@/types/app'
  40. import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
  41. import Toast from '@/app/components/base/toast'
  42. import type { NotionPage } from '@/models/common'
  43. import { DataSourceProvider } from '@/models/common'
  44. import { useDatasetDetailContext } from '@/context/dataset-detail'
  45. import I18n from '@/context/i18n'
  46. import { RETRIEVE_METHOD } from '@/types/app'
  47. import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
  48. import { useDefaultModel, useModelList, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
  49. import { LanguagesSupported } from '@/i18n/language'
  50. import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
  51. import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
  52. import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
  53. import Checkbox from '@/app/components/base/checkbox'
  54. import RadioCard from '@/app/components/base/radio-card'
  55. import { FULL_DOC_PREVIEW_LENGTH, IS_CE_EDITION } from '@/config'
  56. import Divider from '@/app/components/base/divider'
  57. import { getNotionInfo, getWebsiteInfo, useCreateDocument, useCreateFirstDocument, useFetchDefaultProcessRule, useFetchFileIndexingEstimateForFile, useFetchFileIndexingEstimateForNotion, useFetchFileIndexingEstimateForWeb } from '@/service/knowledge/use-create-dataset'
  58. import Badge from '@/app/components/base/badge'
  59. import { SkeletonContainer, SkeletonPoint, SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
  60. import Tooltip from '@/app/components/base/tooltip'
  61. import CustomDialog from '@/app/components/base/dialog'
  62. import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
  63. import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
  64. const TextLabel: FC<PropsWithChildren> = (props) => {
  65. return <label className='text-text-secondary system-sm-semibold'>{props.children}</label>
  66. }
  67. type StepTwoProps = {
  68. isSetting?: boolean
  69. documentDetail?: FullDocumentDetail
  70. isAPIKeySet: boolean
  71. onSetting: () => void
  72. datasetId?: string
  73. indexingType?: IndexingType
  74. retrievalMethod?: string
  75. dataSourceType: DataSourceType
  76. files: CustomFile[]
  77. notionPages?: NotionPage[]
  78. websitePages?: CrawlResultItem[]
  79. crawlOptions?: CrawlOptions
  80. websiteCrawlProvider?: DataSourceProvider
  81. websiteCrawlJobId?: string
  82. onStepChange?: (delta: number) => void
  83. updateIndexingTypeCache?: (type: string) => void
  84. updateRetrievalMethodCache?: (method: string) => void
  85. updateResultCache?: (res: createDocumentResponse) => void
  86. onSave?: () => void
  87. onCancel?: () => void
  88. }
  89. export enum IndexingType {
  90. QUALIFIED = 'high_quality',
  91. ECONOMICAL = 'economy',
  92. }
  93. const DEFAULT_SEGMENT_IDENTIFIER = '\\n\\n'
  94. const DEFAULT_MAXIMUM_CHUNK_LENGTH = 500
  95. const DEFAULT_OVERLAP = 50
  96. type ParentChildConfig = {
  97. chunkForContext: ParentMode
  98. parent: {
  99. delimiter: string
  100. maxLength: number
  101. }
  102. child: {
  103. delimiter: string
  104. maxLength: number
  105. }
  106. }
  107. const defaultParentChildConfig: ParentChildConfig = {
  108. chunkForContext: 'paragraph',
  109. parent: {
  110. delimiter: '\\n\\n',
  111. maxLength: 500,
  112. },
  113. child: {
  114. delimiter: '\\n',
  115. maxLength: 200,
  116. },
  117. }
  118. const StepTwo = ({
  119. isSetting,
  120. documentDetail,
  121. isAPIKeySet,
  122. datasetId,
  123. indexingType,
  124. dataSourceType: inCreatePageDataSourceType,
  125. files,
  126. notionPages = [],
  127. websitePages = [],
  128. crawlOptions,
  129. websiteCrawlProvider = DataSourceProvider.fireCrawl,
  130. websiteCrawlJobId = '',
  131. onStepChange,
  132. updateIndexingTypeCache,
  133. updateResultCache,
  134. onSave,
  135. onCancel,
  136. updateRetrievalMethodCache,
  137. }: StepTwoProps) => {
  138. const { t } = useTranslation()
  139. const { locale } = useContext(I18n)
  140. const media = useBreakpoints()
  141. const isMobile = media === MediaType.mobile
  142. const { dataset: currentDataset, mutateDatasetRes } = useDatasetDetailContext()
  143. const isInUpload = Boolean(currentDataset)
  144. const isUploadInEmptyDataset = isInUpload && !currentDataset?.doc_form
  145. const isNotUploadInEmptyDataset = !isUploadInEmptyDataset
  146. const isInInit = !isInUpload && !isSetting
  147. const isInCreatePage = !datasetId || (datasetId && !currentDataset?.data_source_type)
  148. const dataSourceType = isInCreatePage ? inCreatePageDataSourceType : currentDataset?.data_source_type
  149. const [segmentationType, setSegmentationType] = useState<ProcessMode>(ProcessMode.general)
  150. const [segmentIdentifier, doSetSegmentIdentifier] = useState(DEFAULT_SEGMENT_IDENTIFIER)
  151. const setSegmentIdentifier = useCallback((value: string, canEmpty?: boolean) => {
  152. doSetSegmentIdentifier(value ? escape(value) : (canEmpty ? '' : DEFAULT_SEGMENT_IDENTIFIER))
  153. }, [])
  154. const [maxChunkLength, setMaxChunkLength] = useState(DEFAULT_MAXIMUM_CHUNK_LENGTH) // default chunk length
  155. const [limitMaxChunkLength, setLimitMaxChunkLength] = useState(4000)
  156. const [overlap, setOverlap] = useState(DEFAULT_OVERLAP)
  157. const [rules, setRules] = useState<PreProcessingRule[]>([])
  158. const [defaultConfig, setDefaultConfig] = useState<Rules>()
  159. const hasSetIndexType = !!indexingType
  160. const [indexType, setIndexType] = useState<IndexingType>(
  161. (indexingType
  162. || isAPIKeySet)
  163. ? IndexingType.QUALIFIED
  164. : IndexingType.ECONOMICAL,
  165. )
  166. const [previewFile, setPreviewFile] = useState<DocumentItem>(
  167. (datasetId && documentDetail)
  168. ? documentDetail.file
  169. : files[0],
  170. )
  171. const [previewNotionPage, setPreviewNotionPage] = useState<NotionPage>(
  172. (datasetId && documentDetail)
  173. ? documentDetail.notion_page
  174. : notionPages[0],
  175. )
  176. const [previewWebsitePage, setPreviewWebsitePage] = useState<CrawlResultItem>(
  177. (datasetId && documentDetail)
  178. ? documentDetail.website_page
  179. : websitePages[0],
  180. )
  181. // QA Related
  182. const [isQAConfirmDialogOpen, setIsQAConfirmDialogOpen] = useState(false)
  183. const [docForm, setDocForm] = useState<ChunkingMode>(
  184. (datasetId && documentDetail) ? documentDetail.doc_form as ChunkingMode : ChunkingMode.text,
  185. )
  186. const handleChangeDocform = (value: ChunkingMode) => {
  187. if (value === ChunkingMode.qa && indexType === IndexingType.ECONOMICAL) {
  188. setIsQAConfirmDialogOpen(true)
  189. return
  190. }
  191. if (value === ChunkingMode.parentChild && indexType === IndexingType.ECONOMICAL)
  192. setIndexType(IndexingType.QUALIFIED)
  193. setDocForm(value)
  194. // eslint-disable-next-line @typescript-eslint/no-use-before-define
  195. currentEstimateMutation.reset()
  196. }
  197. const [docLanguage, setDocLanguage] = useState<string>(
  198. (datasetId && documentDetail) ? documentDetail.doc_language : (locale !== LanguagesSupported[1] ? 'English' : 'Chinese'),
  199. )
  200. const [parentChildConfig, setParentChildConfig] = useState<ParentChildConfig>(defaultParentChildConfig)
  201. const getIndexing_technique = () => indexingType || indexType
  202. const currentDocForm = currentDataset?.doc_form || docForm
  203. const getProcessRule = (): ProcessRule => {
  204. if (currentDocForm === ChunkingMode.parentChild) {
  205. return {
  206. rules: {
  207. pre_processing_rules: rules,
  208. segmentation: {
  209. separator: unescape(
  210. parentChildConfig.parent.delimiter,
  211. ),
  212. max_tokens: parentChildConfig.parent.maxLength,
  213. },
  214. parent_mode: parentChildConfig.chunkForContext,
  215. subchunk_segmentation: {
  216. separator: unescape(parentChildConfig.child.delimiter),
  217. max_tokens: parentChildConfig.child.maxLength,
  218. },
  219. },
  220. mode: 'hierarchical',
  221. } as ProcessRule
  222. }
  223. return {
  224. rules: {
  225. pre_processing_rules: rules,
  226. segmentation: {
  227. separator: unescape(segmentIdentifier),
  228. max_tokens: maxChunkLength,
  229. chunk_overlap: overlap,
  230. },
  231. }, // api will check this. It will be removed after api refactored.
  232. mode: segmentationType,
  233. } as ProcessRule
  234. }
  235. const fileIndexingEstimateQuery = useFetchFileIndexingEstimateForFile({
  236. docForm: currentDocForm,
  237. docLanguage,
  238. dataSourceType: DataSourceType.FILE,
  239. files: previewFile
  240. ? [files.find(file => file.name === previewFile.name)!]
  241. : files,
  242. indexingTechnique: getIndexing_technique() as any,
  243. processRule: getProcessRule(),
  244. dataset_id: datasetId!,
  245. })
  246. const notionIndexingEstimateQuery = useFetchFileIndexingEstimateForNotion({
  247. docForm: currentDocForm,
  248. docLanguage,
  249. dataSourceType: DataSourceType.NOTION,
  250. notionPages: [previewNotionPage],
  251. indexingTechnique: getIndexing_technique() as any,
  252. processRule: getProcessRule(),
  253. dataset_id: datasetId || '',
  254. })
  255. const websiteIndexingEstimateQuery = useFetchFileIndexingEstimateForWeb({
  256. docForm: currentDocForm,
  257. docLanguage,
  258. dataSourceType: DataSourceType.WEB,
  259. websitePages: [previewWebsitePage],
  260. crawlOptions,
  261. websiteCrawlProvider,
  262. websiteCrawlJobId,
  263. indexingTechnique: getIndexing_technique() as any,
  264. processRule: getProcessRule(),
  265. dataset_id: datasetId || '',
  266. })
  267. const currentEstimateMutation = dataSourceType === DataSourceType.FILE
  268. ? fileIndexingEstimateQuery
  269. : dataSourceType === DataSourceType.NOTION
  270. ? notionIndexingEstimateQuery
  271. : websiteIndexingEstimateQuery
  272. const fetchEstimate = useCallback(() => {
  273. if (dataSourceType === DataSourceType.FILE)
  274. fileIndexingEstimateQuery.mutate()
  275. if (dataSourceType === DataSourceType.NOTION)
  276. notionIndexingEstimateQuery.mutate()
  277. if (dataSourceType === DataSourceType.WEB)
  278. websiteIndexingEstimateQuery.mutate()
  279. }, [dataSourceType, fileIndexingEstimateQuery, notionIndexingEstimateQuery, websiteIndexingEstimateQuery])
  280. const estimate
  281. = dataSourceType === DataSourceType.FILE
  282. ? fileIndexingEstimateQuery.data
  283. : dataSourceType === DataSourceType.NOTION
  284. ? notionIndexingEstimateQuery.data
  285. : websiteIndexingEstimateQuery.data
  286. const getRuleName = (key: string) => {
  287. if (key === 'remove_extra_spaces')
  288. return t('datasetCreation.stepTwo.removeExtraSpaces')
  289. if (key === 'remove_urls_emails')
  290. return t('datasetCreation.stepTwo.removeUrlEmails')
  291. if (key === 'remove_stopwords')
  292. return t('datasetCreation.stepTwo.removeStopwords')
  293. }
  294. const ruleChangeHandle = (id: string) => {
  295. const newRules = rules.map((rule) => {
  296. if (rule.id === id) {
  297. return {
  298. id: rule.id,
  299. enabled: !rule.enabled,
  300. }
  301. }
  302. return rule
  303. })
  304. setRules(newRules)
  305. }
  306. const resetRules = () => {
  307. if (defaultConfig) {
  308. setSegmentIdentifier(defaultConfig.segmentation.separator)
  309. setMaxChunkLength(defaultConfig.segmentation.max_tokens)
  310. setOverlap(defaultConfig.segmentation.chunk_overlap!)
  311. setRules(defaultConfig.pre_processing_rules)
  312. }
  313. setParentChildConfig(defaultParentChildConfig)
  314. }
  315. const updatePreview = () => {
  316. if (segmentationType === ProcessMode.general && maxChunkLength > 4000) {
  317. Toast.notify({ type: 'error', message: t('datasetCreation.stepTwo.maxLengthCheck') })
  318. return
  319. }
  320. fetchEstimate()
  321. }
  322. const {
  323. modelList: rerankModelList,
  324. defaultModel: rerankDefaultModel,
  325. currentModel: isRerankDefaultModelValid,
  326. } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
  327. const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
  328. const { data: defaultEmbeddingModel } = useDefaultModel(ModelTypeEnum.textEmbedding)
  329. const [embeddingModel, setEmbeddingModel] = useState<DefaultModel>(
  330. currentDataset?.embedding_model
  331. ? {
  332. provider: currentDataset.embedding_model_provider,
  333. model: currentDataset.embedding_model,
  334. }
  335. : {
  336. provider: defaultEmbeddingModel?.provider.provider || '',
  337. model: defaultEmbeddingModel?.model || '',
  338. },
  339. )
  340. const [retrievalConfig, setRetrievalConfig] = useState(currentDataset?.retrieval_model_dict || {
  341. search_method: RETRIEVE_METHOD.semantic,
  342. reranking_enable: false,
  343. reranking_model: {
  344. reranking_provider_name: '',
  345. reranking_model_name: '',
  346. },
  347. top_k: 3,
  348. score_threshold_enabled: false,
  349. score_threshold: 0.5,
  350. } as RetrievalConfig)
  351. useEffect(() => {
  352. if (currentDataset?.retrieval_model_dict)
  353. return
  354. setRetrievalConfig({
  355. search_method: RETRIEVE_METHOD.semantic,
  356. reranking_enable: !!isRerankDefaultModelValid,
  357. reranking_model: {
  358. reranking_provider_name: isRerankDefaultModelValid ? rerankDefaultModel?.provider.provider ?? '' : '',
  359. reranking_model_name: isRerankDefaultModelValid ? rerankDefaultModel?.model ?? '' : '',
  360. },
  361. top_k: 3,
  362. score_threshold_enabled: false,
  363. score_threshold: 0.5,
  364. })
  365. // eslint-disable-next-line react-hooks/exhaustive-deps
  366. }, [rerankDefaultModel, isRerankDefaultModelValid])
  367. const getCreationParams = () => {
  368. let params
  369. if (segmentationType === ProcessMode.general && overlap > maxChunkLength) {
  370. Toast.notify({ type: 'error', message: t('datasetCreation.stepTwo.overlapCheck') })
  371. return
  372. }
  373. if (segmentationType === ProcessMode.general && maxChunkLength > limitMaxChunkLength) {
  374. Toast.notify({ type: 'error', message: t('datasetCreation.stepTwo.maxLengthCheck', { limit: limitMaxChunkLength }) })
  375. return
  376. }
  377. if (isSetting) {
  378. params = {
  379. original_document_id: documentDetail?.id,
  380. doc_form: currentDocForm,
  381. doc_language: docLanguage,
  382. process_rule: getProcessRule(),
  383. retrieval_model: retrievalConfig, // Readonly. If want to changed, just go to settings page.
  384. embedding_model: embeddingModel.model, // Readonly
  385. embedding_model_provider: embeddingModel.provider, // Readonly
  386. indexing_technique: getIndexing_technique(),
  387. } as CreateDocumentReq
  388. }
  389. else { // create
  390. const indexMethod = getIndexing_technique()
  391. if (
  392. !isReRankModelSelected({
  393. rerankModelList,
  394. retrievalConfig,
  395. indexMethod: indexMethod as string,
  396. })
  397. ) {
  398. Toast.notify({ type: 'error', message: t('appDebug.datasetConfig.rerankModelRequired') })
  399. return
  400. }
  401. params = {
  402. data_source: {
  403. type: dataSourceType,
  404. info_list: {
  405. data_source_type: dataSourceType,
  406. },
  407. },
  408. indexing_technique: getIndexing_technique(),
  409. process_rule: getProcessRule(),
  410. doc_form: currentDocForm,
  411. doc_language: docLanguage,
  412. retrieval_model: retrievalConfig,
  413. embedding_model: embeddingModel.model,
  414. embedding_model_provider: embeddingModel.provider,
  415. } as CreateDocumentReq
  416. if (dataSourceType === DataSourceType.FILE) {
  417. params.data_source.info_list.file_info_list = {
  418. file_ids: files.map(file => file.id || '').filter(Boolean),
  419. }
  420. }
  421. if (dataSourceType === DataSourceType.NOTION)
  422. params.data_source.info_list.notion_info_list = getNotionInfo(notionPages)
  423. if (dataSourceType === DataSourceType.WEB) {
  424. params.data_source.info_list.website_info_list = getWebsiteInfo({
  425. websiteCrawlProvider,
  426. websiteCrawlJobId,
  427. websitePages,
  428. })
  429. }
  430. }
  431. return params
  432. }
  433. const fetchDefaultProcessRuleMutation = useFetchDefaultProcessRule({
  434. onSuccess(data) {
  435. const separator = data.rules.segmentation.separator
  436. setSegmentIdentifier(separator)
  437. setMaxChunkLength(data.rules.segmentation.max_tokens)
  438. setOverlap(data.rules.segmentation.chunk_overlap!)
  439. setRules(data.rules.pre_processing_rules)
  440. setDefaultConfig(data.rules)
  441. setLimitMaxChunkLength(data.limits.indexing_max_segmentation_tokens_length)
  442. },
  443. onError(error) {
  444. Toast.notify({
  445. type: 'error',
  446. message: `${error}`,
  447. })
  448. },
  449. })
  450. const getRulesFromDetail = () => {
  451. if (documentDetail) {
  452. const rules = documentDetail.dataset_process_rule.rules
  453. const separator = rules.segmentation.separator
  454. const max = rules.segmentation.max_tokens
  455. const overlap = rules.segmentation.chunk_overlap
  456. setSegmentIdentifier(separator)
  457. setMaxChunkLength(max)
  458. setOverlap(overlap!)
  459. setRules(rules.pre_processing_rules)
  460. setDefaultConfig(rules)
  461. }
  462. }
  463. const getDefaultMode = () => {
  464. if (documentDetail)
  465. setSegmentationType(documentDetail.dataset_process_rule.mode)
  466. }
  467. const createFirstDocumentMutation = useCreateFirstDocument({
  468. onError(error) {
  469. Toast.notify({
  470. type: 'error',
  471. message: `${error}`,
  472. })
  473. },
  474. })
  475. const createDocumentMutation = useCreateDocument(datasetId!, {
  476. onError(error) {
  477. Toast.notify({
  478. type: 'error',
  479. message: `${error}`,
  480. })
  481. },
  482. })
  483. const isCreating = createFirstDocumentMutation.isPending || createDocumentMutation.isPending
  484. const createHandle = async () => {
  485. const params = getCreationParams()
  486. if (!params)
  487. return false
  488. if (!datasetId) {
  489. await createFirstDocumentMutation.mutateAsync(
  490. params,
  491. {
  492. onSuccess(data) {
  493. updateIndexingTypeCache && updateIndexingTypeCache(indexType as string)
  494. updateResultCache && updateResultCache(data)
  495. updateRetrievalMethodCache && updateRetrievalMethodCache(retrievalConfig.search_method as string)
  496. },
  497. },
  498. )
  499. }
  500. else {
  501. await createDocumentMutation.mutateAsync(params, {
  502. onSuccess(data) {
  503. updateIndexingTypeCache && updateIndexingTypeCache(indexType as string)
  504. updateResultCache && updateResultCache(data)
  505. },
  506. })
  507. }
  508. if (mutateDatasetRes)
  509. mutateDatasetRes()
  510. onStepChange && onStepChange(+1)
  511. isSetting && onSave && onSave()
  512. }
  513. useEffect(() => {
  514. // fetch rules
  515. if (!isSetting) {
  516. fetchDefaultProcessRuleMutation.mutate('/datasets/process-rule')
  517. }
  518. else {
  519. getRulesFromDetail()
  520. getDefaultMode()
  521. }
  522. // eslint-disable-next-line react-hooks/exhaustive-deps
  523. }, [])
  524. useEffect(() => {
  525. // get indexing type by props
  526. if (indexingType)
  527. setIndexType(indexingType as IndexingType)
  528. else
  529. setIndexType(isAPIKeySet ? IndexingType.QUALIFIED : IndexingType.ECONOMICAL)
  530. }, [isAPIKeySet, indexingType, datasetId])
  531. const economyDomRef = useRef<HTMLDivElement>(null)
  532. const isHoveringEconomy = useHover(economyDomRef)
  533. const isModelAndRetrievalConfigDisabled = !!datasetId && !!currentDataset?.data_source_type
  534. return (
  535. <div className='flex w-full h-full'>
  536. <div className={cn('relative h-full w-1/2 py-6 overflow-y-auto', isMobile ? 'px-4' : 'px-12')}>
  537. <div className={'system-md-semibold mb-1'}>{t('datasetCreation.stepTwo.segmentation')}</div>
  538. {((isInUpload && [ChunkingMode.text, ChunkingMode.qa].includes(currentDataset!.doc_form))
  539. || isUploadInEmptyDataset
  540. || isInInit)
  541. && <OptionCard
  542. className='bg-background-section mb-2'
  543. title={t('datasetCreation.stepTwo.general')}
  544. icon={<Image width={20} height={20} src={SettingCog} alt={t('datasetCreation.stepTwo.general')} />}
  545. activeHeaderClassName='bg-dataset-option-card-blue-gradient'
  546. description={t('datasetCreation.stepTwo.generalTip')}
  547. isActive={
  548. [ChunkingMode.text, ChunkingMode.qa].includes(currentDocForm)
  549. }
  550. onSwitched={() =>
  551. handleChangeDocform(ChunkingMode.text)
  552. }
  553. actions={
  554. <>
  555. <Button variant={'secondary-accent'} onClick={() => updatePreview()}>
  556. <RiSearchEyeLine className='h-4 w-4 mr-0.5' />
  557. {t('datasetCreation.stepTwo.previewChunk')}
  558. </Button>
  559. <Button variant={'ghost'} onClick={resetRules}>
  560. {t('datasetCreation.stepTwo.reset')}
  561. </Button>
  562. </>
  563. }
  564. noHighlight={isInUpload && isNotUploadInEmptyDataset}
  565. >
  566. <div className='flex flex-col gap-y-4'>
  567. <div className='flex gap-3'>
  568. <DelimiterInput
  569. value={segmentIdentifier}
  570. onChange={e => setSegmentIdentifier(e.target.value, true)}
  571. />
  572. <MaxLengthInput
  573. unit='tokens'
  574. value={maxChunkLength}
  575. onChange={setMaxChunkLength}
  576. />
  577. <OverlapInput
  578. unit='tokens'
  579. value={overlap}
  580. min={1}
  581. onChange={setOverlap}
  582. />
  583. </div>
  584. <div className='w-full flex flex-col'>
  585. <div className='flex items-center gap-x-2'>
  586. <div className='inline-flex shrink-0'>
  587. <TextLabel>{t('datasetCreation.stepTwo.rules')}</TextLabel>
  588. </div>
  589. <Divider className='grow' bgStyle='gradient' />
  590. </div>
  591. <div className='mt-1'>
  592. {rules.map(rule => (
  593. <div key={rule.id} className={s.ruleItem} onClick={() => {
  594. ruleChangeHandle(rule.id)
  595. }}>
  596. <Checkbox
  597. checked={rule.enabled}
  598. />
  599. <label className="ml-2 system-sm-regular cursor-pointer text-text-secondary">{getRuleName(rule.id)}</label>
  600. </div>
  601. ))}
  602. {IS_CE_EDITION && <>
  603. <Divider type='horizontal' className='my-4 bg-divider-subtle' />
  604. <div className='flex items-center py-0.5'>
  605. <div className='flex items-center' onClick={() => {
  606. if (currentDataset?.doc_form)
  607. return
  608. if (docForm === ChunkingMode.qa)
  609. handleChangeDocform(ChunkingMode.text)
  610. else
  611. handleChangeDocform(ChunkingMode.qa)
  612. }}>
  613. <Checkbox
  614. checked={currentDocForm === ChunkingMode.qa}
  615. disabled={!!currentDataset?.doc_form}
  616. />
  617. <label className="ml-2 system-sm-regular cursor-pointer text-text-secondary">
  618. {t('datasetCreation.stepTwo.useQALanguage')}
  619. </label>
  620. </div>
  621. <LanguageSelect
  622. currentLanguage={docLanguage || locale}
  623. onSelect={setDocLanguage}
  624. disabled={currentDocForm !== ChunkingMode.qa}
  625. />
  626. <Tooltip popupContent={t('datasetCreation.stepTwo.QATip')} />
  627. </div>
  628. {currentDocForm === ChunkingMode.qa && (
  629. <div
  630. style={{
  631. background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.1) 0%, rgba(255, 255, 255, 0.00) 100%)',
  632. }}
  633. className='h-10 mt-2 flex items-center gap-2 rounded-xl backdrop-blur-[5px] border-components-panel-border border shadow-xs px-3 text-xs'
  634. >
  635. <RiAlertFill className='size-4 text-text-warning-secondary' />
  636. <span className='system-xs-medium text-text-primary'>
  637. {t('datasetCreation.stepTwo.QATip')}
  638. </span>
  639. </div>
  640. )}
  641. </>}
  642. </div>
  643. </div>
  644. </div>
  645. </OptionCard>}
  646. {
  647. (
  648. (isInUpload && currentDataset!.doc_form === ChunkingMode.parentChild)
  649. || isUploadInEmptyDataset
  650. || isInInit
  651. )
  652. && <OptionCard
  653. title={t('datasetCreation.stepTwo.parentChild')}
  654. icon={<Image width={20} height={20} src={FamilyMod} alt={t('datasetCreation.stepTwo.parentChild')} />}
  655. effectImg={OrangeEffect.src}
  656. activeHeaderClassName='bg-dataset-option-card-orange-gradient'
  657. description={t('datasetCreation.stepTwo.parentChildTip')}
  658. isActive={currentDocForm === ChunkingMode.parentChild}
  659. onSwitched={() => handleChangeDocform(ChunkingMode.parentChild)}
  660. actions={
  661. <>
  662. <Button variant={'secondary-accent'} onClick={() => updatePreview()}>
  663. <RiSearchEyeLine className='h-4 w-4 mr-0.5' />
  664. {t('datasetCreation.stepTwo.previewChunk')}
  665. </Button>
  666. <Button variant={'ghost'} onClick={resetRules}>
  667. {t('datasetCreation.stepTwo.reset')}
  668. </Button>
  669. </>
  670. }
  671. noHighlight={isInUpload && isNotUploadInEmptyDataset}
  672. >
  673. <div className='flex flex-col gap-4'>
  674. <div>
  675. <div className='flex items-center gap-x-2'>
  676. <div className='inline-flex shrink-0'>
  677. <TextLabel>{t('datasetCreation.stepTwo.parentChunkForContext')}</TextLabel>
  678. </div>
  679. <Divider className='grow' bgStyle='gradient' />
  680. </div>
  681. <RadioCard className='mt-1'
  682. icon={<Image src={Note} alt='' />}
  683. title={t('datasetCreation.stepTwo.paragraph')}
  684. description={t('datasetCreation.stepTwo.paragraphTip')}
  685. isChosen={parentChildConfig.chunkForContext === 'paragraph'}
  686. onChosen={() => setParentChildConfig(
  687. {
  688. ...parentChildConfig,
  689. chunkForContext: 'paragraph',
  690. },
  691. )}
  692. chosenConfig={
  693. <div className='flex gap-3'>
  694. <DelimiterInput
  695. value={parentChildConfig.parent.delimiter}
  696. tooltip={t('datasetCreation.stepTwo.parentChildDelimiterTip')!}
  697. onChange={e => setParentChildConfig({
  698. ...parentChildConfig,
  699. parent: {
  700. ...parentChildConfig.parent,
  701. delimiter: e.target.value ? escape(e.target.value) : '',
  702. },
  703. })}
  704. />
  705. <MaxLengthInput
  706. unit='tokens'
  707. value={parentChildConfig.parent.maxLength}
  708. onChange={value => setParentChildConfig({
  709. ...parentChildConfig,
  710. parent: {
  711. ...parentChildConfig.parent,
  712. maxLength: value,
  713. },
  714. })}
  715. />
  716. </div>
  717. }
  718. />
  719. <RadioCard className='mt-2'
  720. icon={<Image src={FileList} alt='' />}
  721. title={t('datasetCreation.stepTwo.fullDoc')}
  722. description={t('datasetCreation.stepTwo.fullDocTip')}
  723. onChosen={() => setParentChildConfig(
  724. {
  725. ...parentChildConfig,
  726. chunkForContext: 'full-doc',
  727. },
  728. )}
  729. isChosen={parentChildConfig.chunkForContext === 'full-doc'}
  730. />
  731. </div>
  732. <div>
  733. <div className='flex items-center gap-x-2'>
  734. <div className='inline-flex shrink-0'>
  735. <TextLabel>{t('datasetCreation.stepTwo.childChunkForRetrieval')}</TextLabel>
  736. </div>
  737. <Divider className='grow' bgStyle='gradient' />
  738. </div>
  739. <div className='flex gap-3 mt-1'>
  740. <DelimiterInput
  741. value={parentChildConfig.child.delimiter}
  742. tooltip={t('datasetCreation.stepTwo.parentChildChunkDelimiterTip')!}
  743. onChange={e => setParentChildConfig({
  744. ...parentChildConfig,
  745. child: {
  746. ...parentChildConfig.child,
  747. delimiter: e.target.value ? escape(e.target.value) : '',
  748. },
  749. })}
  750. />
  751. <MaxLengthInput
  752. unit='tokens'
  753. value={parentChildConfig.child.maxLength}
  754. onChange={value => setParentChildConfig({
  755. ...parentChildConfig,
  756. child: {
  757. ...parentChildConfig.child,
  758. maxLength: value,
  759. },
  760. })}
  761. />
  762. </div>
  763. </div>
  764. <div>
  765. <div className='flex items-center gap-x-2'>
  766. <div className='inline-flex shrink-0'>
  767. <TextLabel>{t('datasetCreation.stepTwo.rules')}</TextLabel>
  768. </div>
  769. <Divider className='grow' bgStyle='gradient' />
  770. </div>
  771. <div className='mt-1'>
  772. {rules.map(rule => (
  773. <div key={rule.id} className={s.ruleItem} onClick={() => {
  774. ruleChangeHandle(rule.id)
  775. }}>
  776. <Checkbox
  777. checked={rule.enabled}
  778. />
  779. <label className="ml-2 system-sm-regular cursor-pointer text-text-secondary">{getRuleName(rule.id)}</label>
  780. </div>
  781. ))}
  782. </div>
  783. </div>
  784. </div>
  785. </OptionCard>}
  786. <Divider className='my-5' />
  787. <div className={'system-md-semibold mb-1'}>{t('datasetCreation.stepTwo.indexMode')}</div>
  788. <div className='flex items-center gap-2'>
  789. {(!hasSetIndexType || (hasSetIndexType && indexingType === IndexingType.QUALIFIED)) && (
  790. <OptionCard className='flex-1'
  791. title={<div className='flex items-center'>
  792. {t('datasetCreation.stepTwo.qualified')}
  793. <Badge className={cn('ml-1 h-[18px]', (!hasSetIndexType && indexType === IndexingType.QUALIFIED) ? 'border-text-accent-secondary text-text-accent-secondary' : '')} uppercase>
  794. {t('datasetCreation.stepTwo.recommend')}
  795. </Badge>
  796. <span className='ml-auto'>
  797. {!hasSetIndexType && <span className={cn(s.radio)} />}
  798. </span>
  799. </div>}
  800. description={t('datasetCreation.stepTwo.qualifiedTip')}
  801. icon={<Image src={indexMethodIcon.high_quality} alt='' />}
  802. isActive={!hasSetIndexType && indexType === IndexingType.QUALIFIED}
  803. disabled={!isAPIKeySet || hasSetIndexType}
  804. onSwitched={() => {
  805. if (isAPIKeySet)
  806. setIndexType(IndexingType.QUALIFIED)
  807. }}
  808. />
  809. )}
  810. {(!hasSetIndexType || (hasSetIndexType && indexingType === IndexingType.ECONOMICAL)) && (
  811. <>
  812. <CustomDialog show={isQAConfirmDialogOpen} onClose={() => setIsQAConfirmDialogOpen(false)} className='w-[432px]'>
  813. <header className='pt-6 mb-4'>
  814. <h2 className='text-lg font-semibold'>
  815. {t('datasetCreation.stepTwo.qaSwitchHighQualityTipTitle')}
  816. </h2>
  817. <p className='font-normal text-sm mt-2'>
  818. {t('datasetCreation.stepTwo.qaSwitchHighQualityTipContent')}
  819. </p>
  820. </header>
  821. <div className='flex gap-2 pb-6'>
  822. <Button className='ml-auto' onClick={() => {
  823. setIsQAConfirmDialogOpen(false)
  824. }}>
  825. {t('datasetCreation.stepTwo.cancel')}
  826. </Button>
  827. <Button variant={'primary'} onClick={() => {
  828. setIsQAConfirmDialogOpen(false)
  829. setIndexType(IndexingType.QUALIFIED)
  830. setDocForm(ChunkingMode.qa)
  831. }}>
  832. {t('datasetCreation.stepTwo.switch')}
  833. </Button>
  834. </div>
  835. </CustomDialog>
  836. <PortalToFollowElem
  837. open={
  838. isHoveringEconomy && docForm !== ChunkingMode.text
  839. }
  840. placement={'top'}
  841. >
  842. <PortalToFollowElemTrigger asChild>
  843. <OptionCard className='flex-1'
  844. title={t('datasetCreation.stepTwo.economical')}
  845. description={t('datasetCreation.stepTwo.economicalTip')}
  846. icon={<Image src={indexMethodIcon.economical} alt='' />}
  847. isActive={!hasSetIndexType && indexType === IndexingType.ECONOMICAL}
  848. disabled={!isAPIKeySet || hasSetIndexType || docForm !== ChunkingMode.text}
  849. ref={economyDomRef}
  850. onSwitched={() => {
  851. if (isAPIKeySet && docForm === ChunkingMode.text)
  852. setIndexType(IndexingType.ECONOMICAL)
  853. }}
  854. />
  855. </PortalToFollowElemTrigger>
  856. <PortalToFollowElemContent>
  857. <div className='p-3 bg-components-tooltip-bg border-components-panel-border text-xs font-medium text-text-secondary rounded-lg shadow-lg'>
  858. {
  859. docForm === ChunkingMode.qa
  860. ? t('datasetCreation.stepTwo.notAvailableForQA')
  861. : t('datasetCreation.stepTwo.notAvailableForParentChild')
  862. }
  863. </div>
  864. </PortalToFollowElemContent>
  865. </PortalToFollowElem>
  866. </>)}
  867. </div>
  868. {!hasSetIndexType && indexType === IndexingType.QUALIFIED && (
  869. <div className='mt-2 h-10 p-2 flex items-center gap-x-0.5 rounded-xl border-[0.5px] border-components-panel-border overflow-hidden bg-components-panel-bg-blur backdrop-blur-[5px] shadow-xs'>
  870. <div className='absolute top-0 left-0 right-0 bottom-0 bg-[linear-gradient(92deg,rgba(247,144,9,0.25)_0%,rgba(255,255,255,0.00)_100%)] opacity-40'></div>
  871. <div className='p-1'>
  872. <AlertTriangle className='size-4 text-text-warning-secondary' />
  873. </div>
  874. <span className='system-xs-medium'>{t('datasetCreation.stepTwo.highQualityTip')}</span>
  875. </div>
  876. )}
  877. {hasSetIndexType && indexType === IndexingType.ECONOMICAL && (
  878. <div className='mt-2 system-xs-medium'>
  879. {t('datasetCreation.stepTwo.indexSettingTip')}
  880. <Link className='text-text-accent' href={`/datasets/${datasetId}/settings`}>{t('datasetCreation.stepTwo.datasetSettingLink')}</Link>
  881. </div>
  882. )}
  883. {/* Embedding model */}
  884. {indexType === IndexingType.QUALIFIED && (
  885. <div className='mt-5'>
  886. <div className={cn('system-md-semibold mb-1', datasetId && 'flex justify-between items-center')}>{t('datasetSettings.form.embeddingModel')}</div>
  887. <ModelSelector
  888. readonly={isModelAndRetrievalConfigDisabled}
  889. triggerClassName={isModelAndRetrievalConfigDisabled ? 'opacity-50' : ''}
  890. defaultModel={embeddingModel}
  891. modelList={embeddingModelList}
  892. onSelect={(model: DefaultModel) => {
  893. setEmbeddingModel(model)
  894. }}
  895. />
  896. {isModelAndRetrievalConfigDisabled && (
  897. <div className='mt-2 system-xs-medium'>
  898. {t('datasetCreation.stepTwo.indexSettingTip')}
  899. <Link className='text-text-accent' href={`/datasets/${datasetId}/settings`}>{t('datasetCreation.stepTwo.datasetSettingLink')}</Link>
  900. </div>
  901. )}
  902. </div>
  903. )}
  904. <Divider className='my-5' />
  905. {/* Retrieval Method Config */}
  906. <div>
  907. {!isModelAndRetrievalConfigDisabled
  908. ? (
  909. <div className={'mb-1'}>
  910. <div className='system-md-semibold mb-0.5'>{t('datasetSettings.form.retrievalSetting.title')}</div>
  911. <div className='body-xs-regular text-text-tertiary'>
  912. <a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-text-accent'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
  913. {t('datasetSettings.form.retrievalSetting.longDescription')}
  914. </div>
  915. </div>
  916. )
  917. : (
  918. <div className={cn('system-md-semibold mb-0.5', 'flex justify-between items-center')}>
  919. <div>{t('datasetSettings.form.retrievalSetting.title')}</div>
  920. </div>
  921. )}
  922. <div className=''>
  923. {
  924. getIndexing_technique() === IndexingType.QUALIFIED
  925. ? (
  926. <RetrievalMethodConfig
  927. disabled={isModelAndRetrievalConfigDisabled}
  928. value={retrievalConfig}
  929. onChange={setRetrievalConfig}
  930. />
  931. )
  932. : (
  933. <EconomicalRetrievalMethodConfig
  934. disabled={isModelAndRetrievalConfigDisabled}
  935. value={retrievalConfig}
  936. onChange={setRetrievalConfig}
  937. />
  938. )
  939. }
  940. </div>
  941. </div>
  942. {!isSetting
  943. ? (
  944. <div className='flex items-center mt-8 py-2'>
  945. <Button onClick={() => onStepChange && onStepChange(-1)}>
  946. <RiArrowLeftLine className='w-4 h-4 mr-1' />
  947. {t('datasetCreation.stepTwo.previousStep')}
  948. </Button>
  949. <Button className='ml-auto' loading={isCreating} variant='primary' onClick={createHandle}>{t('datasetCreation.stepTwo.nextStep')}</Button>
  950. </div>
  951. )
  952. : (
  953. <div className='flex items-center mt-8 py-2'>
  954. <Button loading={isCreating} variant='primary' onClick={createHandle}>{t('datasetCreation.stepTwo.save')}</Button>
  955. <Button className='ml-2' onClick={onCancel}>{t('datasetCreation.stepTwo.cancel')}</Button>
  956. </div>
  957. )}
  958. </div>
  959. <FloatRightContainer isMobile={isMobile} isOpen={true} onClose={() => { }} footer={null}>
  960. <PreviewContainer
  961. header={<PreviewHeader
  962. title={t('datasetCreation.stepTwo.preview')}
  963. >
  964. <div className='flex items-center gap-1'>
  965. {dataSourceType === DataSourceType.FILE
  966. && <PreviewDocumentPicker
  967. files={files as Array<Required<CustomFile>>}
  968. onChange={(selected) => {
  969. currentEstimateMutation.reset()
  970. setPreviewFile(selected)
  971. currentEstimateMutation.mutate()
  972. }}
  973. // when it is from setting, it just has one file
  974. value={isSetting ? (files[0]! as Required<CustomFile>) : previewFile}
  975. />
  976. }
  977. {dataSourceType === DataSourceType.NOTION
  978. && <PreviewDocumentPicker
  979. files={
  980. notionPages.map(page => ({
  981. id: page.page_id,
  982. name: page.page_name,
  983. extension: 'md',
  984. }))
  985. }
  986. onChange={(selected) => {
  987. currentEstimateMutation.reset()
  988. const selectedPage = notionPages.find(page => page.page_id === selected.id)
  989. setPreviewNotionPage(selectedPage!)
  990. currentEstimateMutation.mutate()
  991. }}
  992. value={{
  993. id: previewNotionPage?.page_id || '',
  994. name: previewNotionPage?.page_name || '',
  995. extension: 'md',
  996. }}
  997. />
  998. }
  999. {dataSourceType === DataSourceType.WEB
  1000. && <PreviewDocumentPicker
  1001. files={
  1002. websitePages.map(page => ({
  1003. id: page.source_url,
  1004. name: page.title,
  1005. extension: 'md',
  1006. }))
  1007. }
  1008. onChange={(selected) => {
  1009. currentEstimateMutation.reset()
  1010. const selectedPage = websitePages.find(page => page.source_url === selected.id)
  1011. setPreviewWebsitePage(selectedPage!)
  1012. currentEstimateMutation.mutate()
  1013. }}
  1014. value={
  1015. {
  1016. id: previewWebsitePage?.source_url || '',
  1017. name: previewWebsitePage?.title || '',
  1018. extension: 'md',
  1019. }
  1020. }
  1021. />
  1022. }
  1023. {
  1024. currentDocForm !== ChunkingMode.qa
  1025. && <Badge text={t(
  1026. 'datasetCreation.stepTwo.previewChunkCount', {
  1027. count: estimate?.total_segments || 0,
  1028. }) as string}
  1029. />
  1030. }
  1031. </div>
  1032. </PreviewHeader>}
  1033. className={cn('flex shrink-0 w-1/2 p-4 pr-0 relative h-full', isMobile && 'w-full max-w-[524px]')}
  1034. mainClassName='space-y-6'
  1035. >
  1036. {currentDocForm === ChunkingMode.qa && estimate?.qa_preview && (
  1037. estimate?.qa_preview.map((item, index) => (
  1038. <ChunkContainer
  1039. key={item.question}
  1040. label={`Chunk-${index + 1}`}
  1041. characterCount={item.question.length + item.answer.length}
  1042. >
  1043. <QAPreview qa={item} />
  1044. </ChunkContainer>
  1045. ))
  1046. )}
  1047. {currentDocForm === ChunkingMode.text && estimate?.preview && (
  1048. estimate?.preview.map((item, index) => (
  1049. <ChunkContainer
  1050. key={item.content}
  1051. label={`Chunk-${index + 1}`}
  1052. characterCount={item.content.length}
  1053. >
  1054. {item.content}
  1055. </ChunkContainer>
  1056. ))
  1057. )}
  1058. {currentDocForm === ChunkingMode.parentChild && currentEstimateMutation.data?.preview && (
  1059. estimate?.preview?.map((item, index) => {
  1060. const indexForLabel = index + 1
  1061. const childChunks = parentChildConfig.chunkForContext === 'full-doc'
  1062. ? item.child_chunks.slice(0, FULL_DOC_PREVIEW_LENGTH)
  1063. : item.child_chunks
  1064. return (
  1065. <ChunkContainer
  1066. key={item.content}
  1067. label={`Chunk-${indexForLabel}`}
  1068. characterCount={item.content.length}
  1069. >
  1070. <FormattedText>
  1071. {childChunks.map((child, index) => {
  1072. const indexForLabel = index + 1
  1073. return (
  1074. <PreviewSlice
  1075. key={child}
  1076. label={`C-${indexForLabel}`}
  1077. text={child}
  1078. tooltip={`Child-chunk-${indexForLabel} · ${child.length} Characters`}
  1079. labelInnerClassName='text-[10px] font-semibold align-bottom leading-7'
  1080. dividerClassName='leading-7'
  1081. />
  1082. )
  1083. })}
  1084. </FormattedText>
  1085. </ChunkContainer>
  1086. )
  1087. })
  1088. )}
  1089. {currentEstimateMutation.isIdle && (
  1090. <div className='h-full w-full flex items-center justify-center'>
  1091. <div className='flex flex-col items-center justify-center gap-3'>
  1092. <RiSearchEyeLine className='size-10 text-text-empty-state-icon' />
  1093. <p className='text-sm text-text-tertiary'>
  1094. {t('datasetCreation.stepTwo.previewChunkTip')}
  1095. </p>
  1096. </div>
  1097. </div>
  1098. )}
  1099. {currentEstimateMutation.isPending && (
  1100. <div className='space-y-6'>
  1101. {Array.from({ length: 10 }, (_, i) => (
  1102. <SkeletonContainer key={i}>
  1103. <SkeletonRow>
  1104. <SkeletonRectangle className="w-20" />
  1105. <SkeletonPoint />
  1106. <SkeletonRectangle className="w-24" />
  1107. </SkeletonRow>
  1108. <SkeletonRectangle className="w-full" />
  1109. <SkeletonRectangle className="w-full" />
  1110. <SkeletonRectangle className="w-[422px]" />
  1111. </SkeletonContainer>
  1112. ))}
  1113. </div>
  1114. )}
  1115. </PreviewContainer>
  1116. </FloatRightContainer>
  1117. </div>
  1118. )
  1119. }
  1120. export default StepTwo