child-segment-list.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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. }, [isFullDocMode, total, childChunks.length, inputValue])
  80. return (
  81. <div className={classNames(
  82. 'flex flex-col',
  83. contentOpacity,
  84. isParagraphMode ? 'pb-2 pt-1' : 'grow px-3',
  85. (isFullDocMode && isLoading) && 'overflow-y-hidden',
  86. )}>
  87. {isFullDocMode ? <Divider type='horizontal' className='my-1 h-px bg-divider-subtle' /> : null}
  88. <div className={classNames('flex items-center justify-between', isFullDocMode ? 'sticky -top-2 left-0 bg-background-default pb-3 pt-2' : '')}>
  89. <div className={classNames(
  90. 'flex h-7 items-center rounded-lg pl-1 pr-3',
  91. isParagraphMode && 'cursor-pointer',
  92. (isParagraphMode && collapsed) && 'bg-dataset-child-chunk-expand-btn-bg',
  93. isFullDocMode && 'pl-0',
  94. )}
  95. onClick={(event) => {
  96. event.stopPropagation()
  97. toggleCollapse()
  98. }}
  99. >
  100. {
  101. isParagraphMode
  102. ? collapsed
  103. ? (
  104. <RiArrowRightSLine className='mr-0.5 h-4 w-4 text-text-secondary opacity-50' />
  105. )
  106. : (<RiArrowDownSLine className='mr-0.5 h-4 w-4 text-text-secondary' />)
  107. : null
  108. }
  109. <span className='system-sm-semibold-uppercase text-text-secondary'>{totalText}</span>
  110. <span className={classNames('pl-1.5 text-xs font-medium text-text-quaternary', isParagraphMode ? 'hidden group-hover/card:inline-block' : '')}>·</span>
  111. <button
  112. type='button'
  113. className={classNames(
  114. 'system-xs-semibold-uppercase px-1.5 py-1 text-components-button-secondary-accent-text',
  115. isParagraphMode ? 'hidden group-hover/card:inline-block' : '',
  116. (isFullDocMode && isLoading) ? 'text-components-button-secondary-accent-text-disabled' : '',
  117. )}
  118. onClick={(event) => {
  119. event.stopPropagation()
  120. handleAddNewChildChunk?.(parentChunkId)
  121. }}
  122. disabled={isLoading}
  123. >
  124. {t('common.operation.add')}
  125. </button>
  126. </div>
  127. {isFullDocMode
  128. ? <Input
  129. showLeftIcon
  130. showClearIcon
  131. wrapperClassName='!w-52'
  132. value={inputValue}
  133. onChange={e => handleInputChange?.(e.target.value)}
  134. onClear={() => handleInputChange?.('')}
  135. />
  136. : null}
  137. </div>
  138. {isLoading ? <FullDocListSkeleton /> : null}
  139. {((isFullDocMode && !isLoading) || !collapsed)
  140. ? <div className={classNames('flex gap-x-0.5', isFullDocMode ? 'mb-6 grow' : 'items-center')}>
  141. {isParagraphMode && (
  142. <div className='self-stretch'>
  143. <Divider type='vertical' className='mx-[7px] w-[2px] bg-text-accent-secondary' />
  144. </div>
  145. )}
  146. {childChunks.length > 0
  147. ? <FormattedText className={classNames('flex w-full flex-col !leading-6', isParagraphMode ? 'gap-y-2' : 'gap-y-3')}>
  148. {childChunks.map((childChunk) => {
  149. const edited = childChunk.updated_at !== childChunk.created_at
  150. const focused = currChildChunk?.childChunkInfo?.id === childChunk.id
  151. return <EditSlice
  152. key={childChunk.id}
  153. label={`C-${childChunk.position}${edited ? ` · ${t('datasetDocuments.segment.edited')}` : ''}`}
  154. text={childChunk.content}
  155. onDelete={() => onDelete?.(childChunk.segment_id, childChunk.id)}
  156. labelClassName={focused ? 'bg-state-accent-solid text-text-primary-on-surface' : ''}
  157. labelInnerClassName={'text-[10px] font-semibold align-bottom leading-6'}
  158. contentClassName={classNames('!leading-6', focused ? 'bg-state-accent-hover-alt text-text-primary' : 'text-text-secondary')}
  159. showDivider={false}
  160. onClick={(e) => {
  161. e.stopPropagation()
  162. onClickSlice?.(childChunk)
  163. }}
  164. offsetOptions={({ rects }) => {
  165. return {
  166. mainAxis: isFullDocMode ? -rects.floating.width : 12 - rects.floating.width,
  167. crossAxis: (20 - rects.floating.height) / 2,
  168. }
  169. }}
  170. />
  171. })}
  172. </FormattedText>
  173. : inputValue !== ''
  174. ? <div className='h-full w-full'>
  175. <Empty onClearFilter={onClearFilter!} />
  176. </div>
  177. : null
  178. }
  179. </div>
  180. : null}
  181. </div>
  182. )
  183. }
  184. export default ChildSegmentList