feat: new retrieval result ui
This commit is contained in:
parent
4017c65c1f
commit
b8ced5102c
@ -82,8 +82,8 @@ const FileTypeIcon = ({
|
||||
size = 'sm',
|
||||
className,
|
||||
}: FileTypeIconProps) => {
|
||||
const Icon = FILE_TYPE_ICON_MAP[type].component || FileAppearanceTypeEnum.custom
|
||||
const color = FILE_TYPE_ICON_MAP[type].color
|
||||
const Icon = FILE_TYPE_ICON_MAP[type]?.component || FileAppearanceTypeEnum.custom
|
||||
const color = FILE_TYPE_ICON_MAP[type]?.color || FILE_TYPE_ICON_MAP.custom.color
|
||||
|
||||
return <Icon className={cn('shrink-0', SizeMap[size], color, className)} />
|
||||
}
|
||||
|
@ -42,21 +42,22 @@ type SegmentListContextValue = {
|
||||
|
||||
const SegmentListContext = createContext({
|
||||
isCollapsed: true,
|
||||
toggleCollapsed: () => {},
|
||||
toggleCollapsed: () => { },
|
||||
fullScreen: false,
|
||||
toggleFullScreen: () => {},
|
||||
toggleFullScreen: () => { },
|
||||
})
|
||||
|
||||
export const useSegmentListContext = (selector: (value: SegmentListContextValue) => any) => {
|
||||
return useContextSelector(SegmentListContext, selector)
|
||||
}
|
||||
|
||||
export const SegmentIndexTag: FC<{ positionId?: string | number; label?: string; className?: string }> = React.memo(({ positionId, label, className }) => {
|
||||
export const SegmentIndexTag: FC<{ positionId?: string | number; label?: string; className?: string; isParentChildRetrieval?: boolean }> = React.memo(({ positionId, label, className, isParentChildRetrieval }) => {
|
||||
const prefix = `${isParentChildRetrieval ? 'Parent-' : ''}Chunk`
|
||||
const localPositionId = useMemo(() => {
|
||||
const positionIdStr = String(positionId)
|
||||
if (positionIdStr.length >= 3)
|
||||
return `Chunk-${positionId}`
|
||||
return `Chunk-${positionIdStr.padStart(2, '0')}`
|
||||
return `${prefix}-${positionId}`
|
||||
return `${prefix}-${positionIdStr.padStart(2, '0')}`
|
||||
}, [positionId])
|
||||
return (
|
||||
<div className={cn('flex items-center', className)}>
|
||||
@ -179,7 +180,7 @@ const Completed: FC<ICompletedProps> = ({
|
||||
setSegments([])
|
||||
setSelectedSegmentIds([])
|
||||
invalidSegmentList()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const onClickCard = (detail: SegmentDetailModel, isEditMode = false) => {
|
||||
@ -210,7 +211,7 @@ const Completed: FC<ICompletedProps> = ({
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [datasetId, documentId, selectedSegmentIds, segments])
|
||||
|
||||
const { mutateAsync: deleteSegment } = useDeleteSegment()
|
||||
@ -226,7 +227,7 @@ const Completed: FC<ICompletedProps> = ({
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [datasetId, documentId, selectedSegmentIds])
|
||||
|
||||
const onCancelBatchOperation = useCallback(() => {
|
||||
@ -337,7 +338,7 @@ const Completed: FC<ICompletedProps> = ({
|
||||
resetList()
|
||||
currentPage !== totalPages && setCurrentPage(totalPages)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [segmentListData, limit, currentPage])
|
||||
|
||||
return (
|
||||
@ -388,7 +389,7 @@ const Completed: FC<ICompletedProps> = ({
|
||||
/>
|
||||
<ChildSegmentList
|
||||
childChunks={childSegments}
|
||||
handleInputChange={() => {}}
|
||||
handleInputChange={() => { }}
|
||||
enabled={!archived}
|
||||
/>
|
||||
</div>
|
||||
@ -443,14 +444,14 @@ const Completed: FC<ICompletedProps> = ({
|
||||
</FullScreenDrawer>
|
||||
{/* Batch Action Buttons */}
|
||||
{selectedSegmentIds.length > 0
|
||||
&& <BatchAction
|
||||
className='absolute left-0 bottom-16 z-20'
|
||||
selectedIds={selectedSegmentIds}
|
||||
onBatchEnable={onChangeSwitch.bind(null, true, '')}
|
||||
onBatchDisable={onChangeSwitch.bind(null, false, '')}
|
||||
onBatchDelete={onDelete.bind(null, '')}
|
||||
onCancel={onCancelBatchOperation}
|
||||
/>}
|
||||
&& <BatchAction
|
||||
className='absolute left-0 bottom-16 z-20'
|
||||
selectedIds={selectedSegmentIds}
|
||||
onBatchEnable={onChangeSwitch.bind(null, true, '')}
|
||||
onBatchDisable={onChangeSwitch.bind(null, false, '')}
|
||||
onBatchDelete={onDelete.bind(null, '')}
|
||||
onCancel={onCancelBatchOperation}
|
||||
/>}
|
||||
</SegmentListContext.Provider>
|
||||
)
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
export const generalResultData = [
|
||||
import type { HitTesting } from '@/models/datasets'
|
||||
|
||||
export const generalResultData: HitTesting[] = [
|
||||
{
|
||||
segment: {
|
||||
id: 'b621b153-f8a7-4e85-bd3d-07feaf61bd9e',
|
||||
@ -40,7 +42,13 @@ export const generalResultData = [
|
||||
doc_type: null,
|
||||
},
|
||||
},
|
||||
child_chunks: null,
|
||||
child_chunks: [
|
||||
{
|
||||
id: '1',
|
||||
score: 0.8771945,
|
||||
content: 'It is quite natural for academics who are continuously told to “publish or perish” to want to always create something from scratch that is their own fresh creation.',
|
||||
},
|
||||
],
|
||||
score: 0.8771945,
|
||||
tsne_position: null,
|
||||
},
|
||||
|
@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SliceContent, SliceLabel } from '../../formatted-text/flavours/shared'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { HitTestingChildChunk } from '@/models/datasets'
|
||||
|
||||
type Props = {
|
||||
payload: HitTestingChildChunk
|
||||
isShowAll: boolean
|
||||
}
|
||||
|
||||
const ChildChunks: FC<Props> = ({
|
||||
payload,
|
||||
isShowAll,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { id, score, content } = payload
|
||||
return (
|
||||
<div className='flex items-center space-x-2'>
|
||||
<SliceLabel>
|
||||
{id} {score}
|
||||
</SliceLabel>
|
||||
<SliceContent className={cn(!isShowAll && 'line-clamp-2')}>
|
||||
{content}
|
||||
</SliceContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ChildChunks)
|
@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SegmentIndexTag } from '../../documents/detail/completed'
|
||||
import Score from './score'
|
||||
import ChildChunksItem from './child-chunks-item'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import type { HitTesting } from '@/models/datasets'
|
||||
import FileIcon from '@/app/components/base/file-uploader/file-type-icon'
|
||||
import type { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
payload: HitTesting
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
const ChunkDetailModal: FC<Props> = ({
|
||||
payload,
|
||||
onHide,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { segment, score, child_chunks } = payload
|
||||
const { position, word_count, content, keywords, document } = segment
|
||||
const isParentChildRetrieval = !!(child_chunks && child_chunks.length > 0)
|
||||
const extension = document.name.split('.').slice(0, -1)[0] as FileAppearanceTypeEnum
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('dataset.chunkDetail')}
|
||||
isShow
|
||||
closable
|
||||
onClose={onHide}
|
||||
className={cn(isParentChildRetrieval ? '!min-w-[1200px]' : '!min-w-[720px]')}
|
||||
>
|
||||
<div className='flex h-'>
|
||||
<div>
|
||||
{/* Meta info */}
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='grow flex items-center space-x-2'>
|
||||
<SegmentIndexTag
|
||||
isParentChildRetrieval={isParentChildRetrieval}
|
||||
positionId={position}
|
||||
className={cn('w-fit group-hover:opacity-100')}
|
||||
/>
|
||||
<div className='text-xs font-medium text-text-quaternary'>·</div>
|
||||
<div className='flex items-center space-x-1'>
|
||||
<FileIcon type={extension} size='sm' />
|
||||
<span className='grow w-0 truncate text-text-secondary text-[13px] font-normal'>{document.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Score value={score} />
|
||||
</div>
|
||||
<div className=' max-h-[752px] overflow-y-auto'>
|
||||
{content}
|
||||
</div>
|
||||
{!isParentChildRetrieval && keywords && keywords.length > 0 && (
|
||||
<div>
|
||||
<div>{t('dataset.keywords')}</div>
|
||||
{keywords.map(keyword => (
|
||||
<div key={keyword} className='inline-block px-1 py-0.5 bg-components-tag-bg text-components-tag-text text-xs rounded-md mr-1'>{keyword}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isParentChildRetrieval && (
|
||||
<div className='shrink-0 w-[552px] px-6'>
|
||||
<div>{t('dataset.hitChunks', { num: child_chunks.length })}</div>
|
||||
<div className='space-y-2'>
|
||||
{child_chunks.map(item => (
|
||||
<ChildChunksItem key={item.id} payload={item} isShowAll />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ChunkDetailModal)
|
@ -2,9 +2,17 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine, RiArrowRightUpLine } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { SegmentIndexTag } from '../../documents/detail/completed'
|
||||
import Score from './score'
|
||||
import ChildChunkItem from './child-chunks-item'
|
||||
import ChunkDetailModal from './chunk-detail-modal'
|
||||
import type { HitTesting } from '@/models/datasets'
|
||||
import cn from '@/utils/classnames'
|
||||
import FileIcon from '@/app/components/base/file-uploader/file-type-icon'
|
||||
import type { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
|
||||
|
||||
type Props = {
|
||||
payload: HitTesting
|
||||
}
|
||||
@ -13,20 +21,81 @@ const ResultItem: FC<Props> = ({
|
||||
payload,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { segment } = payload
|
||||
const { position, word_count } = segment
|
||||
const { segment, score, child_chunks } = payload
|
||||
const { position, word_count, content, keywords, document } = segment
|
||||
const isParentChildRetrieval = !!(child_chunks && child_chunks.length > 0)
|
||||
const extension = document.name.split('.').slice(0, -1)[0] as FileAppearanceTypeEnum
|
||||
const [isFold, {
|
||||
toggle: toggleFold,
|
||||
}] = useBoolean(false)
|
||||
const Icon = isFold ? RiArrowRightSLine : RiArrowDownSLine
|
||||
|
||||
const [isShowDetailModal, {
|
||||
setTrue: showDetailModal,
|
||||
setFalse: hideDetailModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Meta info */}
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<SegmentIndexTag positionId={position} className={cn('w-fit group-hover:opacity-100')} />
|
||||
<SegmentIndexTag
|
||||
isParentChildRetrieval={isParentChildRetrieval}
|
||||
positionId={position}
|
||||
className={cn('w-fit group-hover:opacity-100')}
|
||||
/>
|
||||
<div className='text-xs font-medium text-text-quaternary'>·</div>
|
||||
<div className='system-xs-medium text-text-tertiary'>{word_count} {t('datasetDocuments.segment.characters')}</div>
|
||||
</div>
|
||||
{/* Score */}
|
||||
<Score value={score} />
|
||||
</div>
|
||||
|
||||
{/* Main */}
|
||||
<div>
|
||||
<div className='line-clamp-2'>{content}</div>
|
||||
{isParentChildRetrieval && (
|
||||
<div>
|
||||
<div className='flex items-center space-x-0.5 text-text-secondary' onClick={toggleFold}>
|
||||
<Icon className={cn('w-4 h-4', isFold && 'opacity-50')} />
|
||||
<div>{t('dataset.hitChunks', { num: child_chunks.length })}</div>
|
||||
</div>
|
||||
{child_chunks.map(item => (
|
||||
<ChildChunkItem key={item.id} payload={item} isShowAll={false} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!isParentChildRetrieval && keywords && keywords.length > 0 && (
|
||||
<div>
|
||||
{keywords.map(keyword => (
|
||||
<div key={keyword} className='inline-block px-1 py-0.5 bg-components-tag-bg text-components-tag-text text-xs rounded-md mr-1'>{keyword}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Foot */}
|
||||
<div className='flex justify-between items-center h-10 pl-3 pr-2 border-t border-divider-subtle'>
|
||||
<div className='flex items-center space-x-1'>
|
||||
<FileIcon type={extension} size='sm' />
|
||||
<span className='grow w-0 truncate text-text-secondary text-[13px] font-normal'>{document.name}</span>
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center space-x-1 cursor-pointer text-text-tertiary'
|
||||
onClick={showDetailModal}
|
||||
>
|
||||
<div className='text-xs uppercase'>{t('dataset.open')}</div>
|
||||
<RiArrowRightUpLine className='size-3.5' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
isShowDetailModal && (
|
||||
<ChunkDetailModal
|
||||
payload={payload}
|
||||
onHide={hideDetailModal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
22
web/app/components/datasets/hit-testing/components/score.tsx
Normal file
22
web/app/components/datasets/hit-testing/components/score.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
value: number
|
||||
}
|
||||
|
||||
const Score: FC<Props> = ({
|
||||
value,
|
||||
}) => {
|
||||
return (
|
||||
<div className='relative items-center px-[5px] rounded-md border border-components-progress-bar-border overflow-hidden'>
|
||||
<div className='absolute top-0 left-0 h-full bg-util-colors-blue-brand-blue-brand-100 border-r-[1.5px] border-components-progress-brand-progress ' style={{ width: `${value * 100}%` }} />
|
||||
<div className='relative flex items-center h-4 space-x-0.5 text-util-colors-blue-brand-blue-brand-700'>
|
||||
<div className='system-2xs-medium-uppercase'>score</div>
|
||||
<div className='system-xs-semibold'>{value.toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Score)
|
@ -12,6 +12,7 @@ import s from './style.module.css'
|
||||
import HitDetail from './hit-detail'
|
||||
import ModifyRetrievalModal from './modify-retrieval-modal'
|
||||
import { generalResultData } from './assets/test-data'
|
||||
import ResultItem from './components/result-item'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { ExternalKnowledgeBaseHitTestingResponse, ExternalKnowledgeBaseHitTesting as ExternalKnowledgeBaseHitTestingType, HitTestingResponse, HitTesting as HitTestingType } from '@/models/datasets'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
@ -83,20 +84,9 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
|
||||
<div className='overflow-auto flex-1'>
|
||||
<div className={s.cardWrapper}>
|
||||
{results.map((record, idx) => (
|
||||
<SegmentCard
|
||||
<ResultItem
|
||||
key={idx}
|
||||
loading={false}
|
||||
refSource={{
|
||||
title: record.title,
|
||||
uri: record.metadata ? record.metadata['x-amz-bedrock-kb-source-uri'] : '',
|
||||
}}
|
||||
isExternal={isExternal}
|
||||
detail={record.segment}
|
||||
contentExternal={record.content}
|
||||
score={record.score}
|
||||
scene='hitTesting'
|
||||
className='h-[216px] mb-4'
|
||||
onClick={() => onClickCard(record)}
|
||||
payload={record}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -479,10 +479,16 @@ export type HitTestingRecord = {
|
||||
created_at: number
|
||||
}
|
||||
|
||||
export type HitTestingChildChunk = {
|
||||
id: string
|
||||
content: string
|
||||
score: number
|
||||
}
|
||||
export type HitTesting = {
|
||||
segment: Segment
|
||||
score: number
|
||||
tsne_position: TsnePosition
|
||||
child_chunks?: HitTestingChildChunk[] | null
|
||||
}
|
||||
|
||||
export type ExternalKnowledgeBaseHitTesting = {
|
||||
|
Loading…
Reference in New Issue
Block a user