child-segment-list.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import { type FC, useMemo, useState } from 'react'
  2. import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
  3. import { useTranslation } from 'react-i18next'
  4. import { EditSlice } from '../../../formatted-text/flavours/edit-slice'
  5. import { useDocumentContext } from '../index'
  6. import { FormattedText } from '../../../formatted-text/formatted'
  7. import Empty from './common/empty'
  8. import FullDocListSkeleton from './skeleton/full-doc-list-skeleton'
  9. import { useSegmentListContext } from './index'
  10. import type { ChildChunkDetail } from '@/models/datasets'
  11. import Input from '@/app/components/base/input'
  12. import classNames from '@/utils/classnames'
  13. import Divider from '@/app/components/base/divider'
  14. import { formatNumber } from '@/utils/format'
  15. type IChildSegmentCardProps = {
  16. childChunks: ChildChunkDetail[]
  17. parentChunkId: string
  18. handleInputChange?: (value: string) => void
  19. handleAddNewChildChunk?: (parentChunkId: string) => void
  20. enabled: boolean
  21. onDelete?: (segId: string, childChunkId: string) => Promise<void>
  22. onClickSlice?: (childChunk: ChildChunkDetail) => void
  23. total?: number
  24. inputValue?: string
  25. onClearFilter?: () => void
  26. isLoading?: boolean
  27. focused?: boolean
  28. }
  29. const ChildSegmentList: FC<IChildSegmentCardProps> = ({
  30. childChunks,
  31. parentChunkId,
  32. handleInputChange,
  33. handleAddNewChildChunk,
  34. enabled,
  35. onDelete,
  36. onClickSlice,
  37. total,
  38. inputValue,
  39. onClearFilter,
  40. isLoading,
  41. focused = false,
  42. }) => {
  43. const { t } = useTranslation()
  44. const parentMode = useDocumentContext(s => s.parentMode)
  45. const currChildChunk = useSegmentListContext(s => s.currChildChunk)
  46. const [collapsed, setCollapsed] = useState(true)
  47. const toggleCollapse = () => {
  48. setCollapsed(!collapsed)
  49. }
  50. const isParagraphMode = useMemo(() => {
  51. return parentMode === 'paragraph'
  52. }, [parentMode])
  53. const isFullDocMode = useMemo(() => {
  54. return parentMode === 'full-doc'
  55. }, [parentMode])
  56. const contentOpacity = useMemo(() => {
  57. return (enabled || focused) ? '' : 'opacity-50 group-hover/card:opacity-100'
  58. }, [enabled, focused])
  59. const totalText = useMemo(() => {
  60. const isSearch = inputValue !== '' && isFullDocMode
  61. if (!isSearch) {
  62. const text = isFullDocMode
  63. ? !total
  64. ? '--'
  65. : formatNumber(total)
  66. : formatNumber(childChunks.length)
  67. const count = isFullDocMode
  68. ? text === '--'
  69. ? 0
  70. : total
  71. : childChunks.length
  72. return `${text} ${t('datasetDocuments.segment.childChunks', { count })}`
  73. }
  74. else {
  75. const text = !total ? '--' : formatNumber(total)
  76. const count = text === '--' ? 0 : total
  77. return `${count} ${t('datasetDocuments.segment.searchResults', { count })}`
  78. }
  79. // eslint-disable-next-line react-hooks/exhaustive-deps
  80. }, [isFullDocMode, total, childChunks.length, inputValue])
  81. return (
  82. <div className={classNames(
  83. 'flex flex-col',
  84. contentOpacity,
  85. isParagraphMode ? 'pt-1 pb-2' : 'px-3 grow',
  86. (isFullDocMode && isLoading) && 'overflow-y-hidden',
  87. )}>
  88. {isFullDocMode ? <Divider type='horizontal' className='h-[1px] bg-divider-subtle my-1' /> : null}
  89. <div className={classNames('flex items-center justify-between', isFullDocMode ? 'pt-2 pb-3 sticky -top-2 left-0 bg-background-default' : '')}>
  90. <div className={classNames(
  91. 'h-7 flex items-center pl-1 pr-3 rounded-lg',
  92. isParagraphMode && 'cursor-pointer',
  93. (isParagraphMode && collapsed) && 'bg-dataset-child-chunk-expand-btn-bg',
  94. isFullDocMode && 'pl-0',
  95. )}
  96. onClick={(event) => {
  97. event.stopPropagation()
  98. toggleCollapse()
  99. }}
  100. >
  101. {
  102. isParagraphMode
  103. ? collapsed
  104. ? (
  105. <RiArrowRightSLine className='w-4 h-4 text-text-secondary opacity-50 mr-0.5' />
  106. )
  107. : (<RiArrowDownSLine className='w-4 h-4 text-text-secondary mr-0.5' />)
  108. : null
  109. }
  110. <span className='text-text-secondary system-sm-semibold-uppercase'>{totalText}</span>
  111. <span className={classNames('text-text-quaternary text-xs font-medium pl-1.5', isParagraphMode ? 'hidden group-hover/card:inline-block' : '')}>·</span>
  112. <button
  113. type='button'
  114. className={classNames(
  115. 'px-1.5 py-1 text-components-button-secondary-accent-text system-xs-semibold-uppercase',
  116. isParagraphMode ? 'hidden group-hover/card:inline-block' : '',
  117. (isFullDocMode && isLoading) ? 'text-components-button-secondary-accent-text-disabled' : '',
  118. )}
  119. onClick={(event) => {
  120. event.stopPropagation()
  121. handleAddNewChildChunk?.(parentChunkId)
  122. }}
  123. disabled={isLoading}
  124. >
  125. {t('common.operation.add')}
  126. </button>
  127. </div>
  128. {isFullDocMode
  129. ? <Input
  130. showLeftIcon
  131. showClearIcon
  132. wrapperClassName='!w-52'
  133. value={inputValue}
  134. onChange={e => handleInputChange?.(e.target.value)}
  135. onClear={() => handleInputChange?.('')}
  136. />
  137. : null}
  138. </div>
  139. {isLoading ? <FullDocListSkeleton /> : null}
  140. {((isFullDocMode && !isLoading) || !collapsed)
  141. ? <div className={classNames('flex gap-x-0.5', isFullDocMode ? 'grow mb-6' : 'items-center')}>
  142. {isParagraphMode && (
  143. <div className='self-stretch'>
  144. <Divider type='vertical' className='w-[2px] mx-[7px] bg-text-accent-secondary' />
  145. </div>
  146. )}
  147. {childChunks.length > 0
  148. ? <FormattedText className={classNames('w-full !leading-6 flex flex-col', isParagraphMode ? 'gap-y-2' : 'gap-y-3')}>
  149. {childChunks.map((childChunk) => {
  150. const edited = childChunk.updated_at !== childChunk.created_at
  151. const focused = currChildChunk?.childChunkInfo?.id === childChunk.id
  152. return <EditSlice
  153. key={childChunk.id}
  154. label={`C-${childChunk.position}${edited ? ` · ${t('datasetDocuments.segment.edited')}` : ''}`}
  155. text={childChunk.content}
  156. onDelete={() => onDelete?.(childChunk.segment_id, childChunk.id)}
  157. labelClassName={focused ? 'bg-state-accent-solid text-text-primary-on-surface' : ''}
  158. labelInnerClassName={'text-[10px] font-semibold align-bottom leading-6'}
  159. contentClassName={classNames('!leading-6', focused ? 'bg-state-accent-hover-alt text-text-primary' : '')}
  160. showDivider={false}
  161. onClick={(e) => {
  162. e.stopPropagation()
  163. onClickSlice?.(childChunk)
  164. }}
  165. offsetOptions={({ rects }) => {
  166. return {
  167. mainAxis: isFullDocMode ? -rects.floating.width : 12 - rects.floating.width,
  168. crossAxis: (20 - rects.floating.height) / 2,
  169. }
  170. }}
  171. />
  172. })}
  173. </FormattedText>
  174. : inputValue !== ''
  175. ? <div className='h-full w-full'>
  176. <Empty onClearFilter={onClearFilter!} />
  177. </div>
  178. : null
  179. }
  180. </div>
  181. : null}
  182. </div>
  183. )
  184. }
  185. export default ChildSegmentList