feat: add empty state handling and translations for segment list

This commit is contained in:
twwu 2024-12-13 17:38:26 +08:00
parent f7d6dbe90b
commit f1782664b6
12 changed files with 135 additions and 14 deletions

View File

@ -74,7 +74,7 @@ const DocumentPicker: FC<Props> = ({
placement='bottom-start'
>
<PortalToFollowElemTrigger onClick={togglePopup}>
<div className={cn('flex items-center ml-1 px-2 py-0.5 rounded-lg hover:bg-state-base-hover select-none', open && 'bg-state-base-hover')}>
<div className={cn('flex items-center ml-1 px-2 py-0.5 rounded-lg hover:bg-state-base-hover select-none cursor-pointer', open && 'bg-state-base-hover')}>
<FileIcon name={name} extension={extension} size='lg' />
<div className='flex flex-col items-start ml-1 mr-0.5'>
<div className='flex items-center space-x-0.5'>

View File

@ -0,0 +1,77 @@
import React, { type FC } from 'react'
import { RiFileList2Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
type IEmptyProps = {
onClearFilter: () => void
}
const EmptyCard = React.memo(() => {
return (
<div className='w-full h-32 rounded-xl opacity-30 bg-background-section-burn shrink-0' />
)
})
EmptyCard.displayName = 'EmptyCard'
type LineProps = {
className?: string
}
const Line = React.memo(({
className,
}: LineProps) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="2" height="241" viewBox="0 0 2 241" fill="none" className={className}>
<path d="M1 0.5L1 240.5" stroke="url(#paint0_linear_1989_74474)"/>
<defs>
<linearGradient id="paint0_linear_1989_74474" x1="-7.99584" y1="240.5" x2="-7.88094" y2="0.50004" gradientUnits="userSpaceOnUse">
<stop stopColor="white" stopOpacity="0.01"/>
<stop offset="0.503965" stopColor="#101828" stopOpacity="0.08"/>
<stop offset="1" stopColor="white" stopOpacity="0.01"/>
</linearGradient>
</defs>
</svg>
)
})
Line.displayName = 'Line'
const Empty: FC<IEmptyProps> = ({
onClearFilter,
}) => {
const { t } = useTranslation()
return (
<div className={'h-full relative flex items-center justify-center z-0'}>
<div className='flex flex-col items-center'>
<div className='relative z-10 flex items-center justify-center w-14 h-14 border border-divider-subtle bg-components-card-bg rounded-xl shadow-lg shadow-shadow-shadow-5'>
<RiFileList2Line className='w-6 h-6 text-text-secondary' />
<Line className='absolute -right-[1px] top-1/2 -translate-y-1/2' />
<Line className='absolute -left-[1px] top-1/2 -translate-y-1/2' />
<Line className='absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-90' />
<Line className='absolute top-full left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-90' />
</div>
<div className='text-text-tertiary system-md-regular mt-3'>
{t('datasetDocuments.segment.empty')}
</div>
<button
className='text-text-accent system-sm-medium mt-1'
onClick={onClearFilter}
>
{t('datasetDocuments.segment.clearFilter')}
</button>
</div>
<div className='h-full w-full absolute top-0 left-0 flex flex-col gap-y-3 -z-20 overflow-hidden'>
{
Array.from({ length: 10 }).map((_, i) => (
<EmptyCard key={i} />
))
}
</div>
<div className='h-full w-full absolute top-0 left-0 bg-dataset-chunk-list-empty-bg -z-10' />
</div>
)
}
export default React.memo(Empty)

View File

@ -106,6 +106,7 @@ const Completed: FC<ICompletedProps> = ({
const { run: handleSearch } = useDebounceFn(() => {
setSearchValue(inputValue)
setCurrentPage(1)
}, { wait: 500 })
const handleInputChange = (value: string) => {
@ -115,6 +116,7 @@ const Completed: FC<ICompletedProps> = ({
const onChangeStatus = ({ value }: Item) => {
setSelectedStatus(value === 'all' ? 'all' : !!value)
setCurrentPage(1)
}
const isFullDocMode = useMemo(() => {
@ -132,6 +134,7 @@ const Completed: FC<ICompletedProps> = ({
enabled: selectedStatus === 'all' ? 'all' : !!selectedStatus,
},
},
currentPage === 0,
)
const invalidSegmentList = useInvalid(useSegmentListKey)
@ -162,7 +165,7 @@ const Completed: FC<ICompletedProps> = ({
keyword: searchValue,
},
},
!isFullDocMode || segments.length === 0,
!isFullDocMode || segments.length === 0 || currentPage === 0,
)
const invalidChildSegmentList = useInvalid(useChildSegmentListKey)
@ -174,8 +177,12 @@ const Completed: FC<ICompletedProps> = ({
}, [childSegments])
useEffect(() => {
if (childChunkListData)
if (childChunkListData) {
setChildSegments(childChunkListData.data || [])
if (childChunkListData.total_pages < currentPage)
setCurrentPage(childChunkListData.total_pages)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [childChunkListData])
const resetList = useCallback(() => {
@ -328,12 +335,20 @@ const Completed: FC<ICompletedProps> = ({
}, [segments, isAllSelected, selectedSegmentIds])
const totalText = useMemo(() => {
const total = segmentListData?.total ? formatNumber(segmentListData.total) : '--'
const count = total === '--' ? 0 : segmentListData!.total
const translationKey = (mode === 'hierarchical' && parentMode === 'paragraph')
? 'datasetDocuments.segment.parentChunks'
: 'datasetDocuments.segment.chunks'
return `${total} ${t(translationKey, { count })}`
const isSearch = searchValue !== '' || selectedStatus !== 'all'
if (!isSearch) {
const total = segmentListData?.total ? formatNumber(segmentListData.total) : '--'
const count = total === '--' ? 0 : segmentListData!.total
const translationKey = (mode === 'hierarchical' && parentMode === 'paragraph')
? 'datasetDocuments.segment.parentChunks'
: 'datasetDocuments.segment.chunks'
return `${total} ${t(translationKey, { count })}`
}
else {
const total = typeof segmentListData?.total === 'number' ? formatNumber(segmentListData.total) : 0
const count = segmentListData?.total || 0
return `${total} ${t('datasetDocuments.segment.searchResults', { count })}`
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [segmentListData?.total, mode, parentMode])
@ -472,6 +487,13 @@ const Completed: FC<ICompletedProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [segments, childSegments, datasetId, documentId, parentMode])
const onClearFilter = useCallback(() => {
setInputValue('')
setSearchValue('')
setSelectedStatus('all')
setCurrentPage(1)
}, [])
return (
<SegmentListContext.Provider value={{
isCollapsed,
@ -543,6 +565,7 @@ const Completed: FC<ICompletedProps> = ({
onDeleteChildChunk={onDeleteChildChunk}
handleAddNewChildChunk={handleAddNewChildChunk}
onClickSlice={onClickSlice}
onClearFilter={onClearFilter}
/>
}
{/* Pagination */}

View File

@ -1,5 +1,6 @@
import React, { type ForwardedRef } from 'react'
import SegmentCard from './segment-card'
import Empty from './common/empty'
import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets'
import Checkbox from '@/app/components/base/checkbox'
import Loading from '@/app/components/base/loading'
@ -19,6 +20,7 @@ type ISegmentListProps = {
onClickSlice: (childChunk: ChildChunkDetail) => void
archived?: boolean
embeddingAvailable: boolean
onClearFilter: () => void
}
const SegmentList = React.forwardRef(({
@ -34,11 +36,19 @@ const SegmentList = React.forwardRef(({
onClickSlice,
archived,
embeddingAvailable,
onClearFilter,
}: ISegmentListProps,
ref: ForwardedRef<HTMLDivElement>,
) => {
if (isLoading)
return <Loading type='app' />
if (items.length === 0) {
return (
<div className='h-full pl-6'>
<Empty onClearFilter={onClearFilter} />
</div>
)
}
return (
<div ref={ref} className={classNames('flex flex-col h-full overflow-y-auto')}>
{

View File

@ -8,7 +8,7 @@
@apply text-text-secondary flex-1;
}
.docSearchWrapper {
@apply sticky w-full -top-3 bg-white flex items-center mb-3 justify-between z-10 flex-wrap gap-y-1;
@apply sticky w-full -top-3 bg-white flex items-center mb-3 justify-between z-10 flex-wrap gap-y-1 pr-3;
}
.listContainer {
height: calc(100% - 3.25rem);

View File

@ -55,7 +55,7 @@ type DocumentTitleProps = {
export const DocumentTitle: FC<DocumentTitleProps> = ({ datasetId, extension, name, processMode, parent_mode, wrapperCls }) => {
const router = useRouter()
return (
<div className={cn('flex items-center justify-start flex-1 cursor-pointer', wrapperCls)}>
<div className={cn('flex items-center justify-start flex-1', wrapperCls)}>
<DocumentPicker
datasetId={datasetId}
value={{

View File

@ -339,6 +339,11 @@ const translation = {
parentChunks_other: 'PARENT CHUNKS',
childChunks_one: 'CHILD CHUNK',
childChunks_other: 'CHILD CHUNKS',
searchResults_zero: 'RESULT',
searchResults_one: 'RESULT',
searchResults_other: 'RESULTS',
empty: 'No Chunk found',
clearFilter: 'Clear filter',
chunk: 'Chunk',
parentChunk: 'Parent-Chunk',
childChunk: 'Child-Chunk',

View File

@ -337,6 +337,11 @@ const translation = {
parentChunks_other: '父分段',
childChunks_one: '子分段',
childChunks_other: '子分段',
searchResults_zero: '搜索结果',
searchResults_one: '搜索结果',
searchResults_other: '搜索结果',
empty: '未找到分段',
clearFilter: '清空搜索条件',
chunk: '分段',
parentChunk: '父分段',
childChunk: '子分段',
@ -358,7 +363,7 @@ const translation = {
newQaSegment: '新问答分段',
addChunk: '新增分段',
addChildChunk: '新增子分段',
addAnother: '续新增',
addAnother: '续新增',
delete: '删除这个分段?',
chunkAdded: '新增一个分段',
childChunkAdded: '新增一个子分段',

View File

@ -28,7 +28,6 @@ export const useSegmentList = (
return get<SegmentsResponse>(`/datasets/${datasetId}/documents/${documentId}/segments`, { params })
},
enabled: !disable,
initialData: disable ? { data: [], has_more: false, page: 1, total: 0, total_pages: 0, limit: 10 } : undefined,
})
}
@ -88,7 +87,6 @@ export const useChildSegmentList = (
return get<ChildSegmentsResponse>(`/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`, { params })
},
enabled: !disable,
initialData: disable ? { data: [], total: 0, page: 1, total_pages: 0, limit: 10 } : undefined,
})
}

View File

@ -102,6 +102,7 @@ const config = {
'dataset-chunk-process-error-bg': 'var(--color-dataset-chunk-process-error-bg)',
'dataset-chunk-detail-card-hover-bg': 'var(--color-dataset-chunk-detail-card-hover-bg)',
'dataset-child-chunk-expand-btn-bg': 'var(--color-dataset-child-chunk-expand-btn-bg)',
'dataset-chunk-list-empty-bg': 'var(--color-dataset-chunk-list-empty-bg)',
},
lineClamp: {
20: '20',

View File

@ -9,4 +9,5 @@ html[data-theme="dark"] {
--color-dataset-option-card-blue-gradient: linear-gradient(180deg, #24252E 0%, #1E1E21 100%);
--color-dataset-option-card-purple-gradient: linear-gradient(180deg, #25242E 0%, #1E1E21 100%);
--color-dataset-option-card-orange-gradient: linear-gradient(180deg, #2B2322 0%, #1E1E21 100%);
--color-dataset-chunk-list-empty-bg: linear-gradient(180deg, rgba(34, 34, 37, 0.00) 0%, #222225 100%);
}

View File

@ -9,4 +9,5 @@ html[data-theme="light"] {
--color-dataset-option-card-blue-gradient: linear-gradient(180deg, #F2F4F7 0%, #F9FAFB 100%);
--color-dataset-option-card-purple-gradient: linear-gradient(180deg, #F0EEFA 0%, #F9FAFB 100%);
--color-dataset-option-card-orange-gradient: linear-gradient(180deg, #F8F2EE 0%, #F9FAFB 100%);
--color-dataset-chunk-list-empty-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FCFCFD 100%);
}