Co-authored-by: zhuxinliang <zhuxinliang@didiglobal.com>tags/0.15.2
| @@ -0,0 +1,60 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useBoolean } from 'ahooks' | |||
| import ResultItemMeta from './result-item-meta' | |||
| import ResultItemFooter from './result-item-footer' | |||
| import type { ExternalKnowledgeBaseHitTesting } from '@/models/datasets' | |||
| import cn from '@/utils/classnames' | |||
| import Modal from '@/app/components/base/modal' | |||
| import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types' | |||
| const i18nPrefix = 'datasetHitTesting' | |||
| type Props = { | |||
| payload: ExternalKnowledgeBaseHitTesting | |||
| positionId: number | |||
| } | |||
| const ResultItemExternal: FC<Props> = ({ payload, positionId }) => { | |||
| const { t } = useTranslation() | |||
| const { content, title, score } = payload | |||
| const [ | |||
| isShowDetailModal, | |||
| { setTrue: showDetailModal, setFalse: hideDetailModal }, | |||
| ] = useBoolean(false) | |||
| return ( | |||
| <div className={cn('pt-3 bg-chat-bubble-bg rounded-xl hover:shadow-lg cursor-pointer')} onClick={showDetailModal}> | |||
| {/* Meta info */} | |||
| <ResultItemMeta className='px-3' labelPrefix={'Chunk'} positionId={positionId} wordCount={content.length} score={score} /> | |||
| {/* Main */} | |||
| <div className='mt-1 px-3'> | |||
| <div className='line-clamp-2 body-md-regular break-all'>{content}</div> | |||
| </div> | |||
| {/* Foot */} | |||
| <ResultItemFooter docType={FileAppearanceTypeEnum.custom} docTitle={title} showDetailModal={showDetailModal} /> | |||
| {isShowDetailModal && ( | |||
| <Modal | |||
| title={t(`${i18nPrefix}.chunkDetail`)} | |||
| className={'!min-w-[800px]'} | |||
| closable | |||
| onClose={hideDetailModal} | |||
| isShow={isShowDetailModal} | |||
| > | |||
| <div className='mt-4 flex-1'> | |||
| <ResultItemMeta labelPrefix={'Chunk'} positionId={positionId} wordCount={content.length} score={score} /> | |||
| <div className={cn('mt-2 body-md-regular text-text-secondary break-all', 'h-[min(539px,_80vh)] overflow-y-auto')}> | |||
| {content} | |||
| </div> | |||
| </div> | |||
| </Modal> | |||
| )} | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(ResultItemExternal) | |||
| @@ -0,0 +1,42 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { RiArrowRightUpLine } from '@remixicon/react' | |||
| import FileIcon from '@/app/components/base/file-uploader/file-type-icon' | |||
| import type { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types' | |||
| type Props = { | |||
| docType: FileAppearanceTypeEnum | |||
| docTitle: string | |||
| showDetailModal: () => void | |||
| } | |||
| const i18nPrefix = 'datasetHitTesting' | |||
| const ResultItemFooter: FC<Props> = ({ | |||
| docType, | |||
| docTitle, | |||
| showDetailModal, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <div className="mt-3 flex justify-between items-center h-10 pl-3 pr-2 border-t border-divider-subtle"> | |||
| <div className="grow flex items-center space-x-1"> | |||
| <FileIcon type={docType} size="sm" /> | |||
| <span className="grow w-0 truncate text-text-secondary text-[13px] font-normal"> | |||
| {docTitle} | |||
| </span> | |||
| </div> | |||
| <div | |||
| className="flex items-center space-x-1 cursor-pointer text-text-tertiary" | |||
| onClick={showDetailModal} | |||
| > | |||
| <div className="text-xs uppercase">{t(`${i18nPrefix}.open`)}</div> | |||
| <RiArrowRightUpLine className="size-3.5" /> | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(ResultItemFooter) | |||
| @@ -0,0 +1,45 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { SegmentIndexTag } from '../../documents/detail/completed/common/segment-index-tag' | |||
| import Dot from '../../documents/detail/completed/common/dot' | |||
| import Score from './score' | |||
| import cn from '@/utils/classnames' | |||
| type Props = { | |||
| labelPrefix: string | |||
| positionId: number | |||
| wordCount: number | |||
| score: number | |||
| className?: string | |||
| } | |||
| const ResultItemMeta: FC<Props> = ({ | |||
| labelPrefix, | |||
| positionId, | |||
| wordCount, | |||
| score, | |||
| className, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <div className={cn('flex justify-between items-center', className)}> | |||
| <div className="flex items-center space-x-2"> | |||
| <SegmentIndexTag | |||
| labelPrefix={labelPrefix} | |||
| positionId={positionId} | |||
| className={cn('w-fit group-hover:opacity-100')} | |||
| /> | |||
| <Dot /> | |||
| <div className="system-xs-medium text-text-tertiary"> | |||
| {wordCount} {t('datasetDocuments.segment.characters', { count: wordCount })} | |||
| </div> | |||
| </div> | |||
| <Score value={score} /> | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(ResultItemMeta) | |||
| @@ -2,33 +2,29 @@ | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { RiArrowDownSLine, RiArrowRightSLine, RiArrowRightUpLine } from '@remixicon/react' | |||
| import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' | |||
| import { useBoolean } from 'ahooks' | |||
| import { SegmentIndexTag } from '../../documents/detail/completed/common/segment-index-tag' | |||
| import Dot from '../../documents/detail/completed/common/dot' | |||
| import Score from './score' | |||
| import ChildChunkItem from './child-chunks-item' | |||
| import ChunkDetailModal from './chunk-detail-modal' | |||
| import ResultItemMeta from './result-item-meta' | |||
| import ResultItemFooter from './result-item-footer' | |||
| 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' | |||
| import Tag from '@/app/components/datasets/documents/detail/completed/common/tag' | |||
| import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils/extension-to-file-type' | |||
| const i18nPrefix = 'datasetHitTesting' | |||
| type Props = { | |||
| isExternal: boolean | |||
| payload: HitTesting | |||
| } | |||
| const ResultItem: FC<Props> = ({ | |||
| isExternal, | |||
| payload, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { segment, content: externalContent, score, child_chunks } = payload | |||
| const data = isExternal ? externalContent : segment | |||
| const { segment, score, child_chunks } = payload | |||
| const data = segment | |||
| const { position, word_count, content, keywords, document } = data | |||
| const isParentChildRetrieval = !!(child_chunks && child_chunks.length > 0) | |||
| const extension = document.name.split('.').slice(-1)[0] as FileAppearanceTypeEnum | |||
| @@ -46,18 +42,7 @@ const ResultItem: FC<Props> = ({ | |||
| return ( | |||
| <div className={cn('pt-3 bg-chat-bubble-bg rounded-xl hover:shadow-lg cursor-pointer')} onClick={showDetailModal}> | |||
| {/* Meta info */} | |||
| <div className='flex justify-between items-center px-3'> | |||
| <div className='flex items-center space-x-2'> | |||
| <SegmentIndexTag | |||
| labelPrefix={`${isParentChildRetrieval ? 'Parent-' : ''}Chunk`} | |||
| positionId={position} | |||
| className={cn('w-fit group-hover:opacity-100')} | |||
| /> | |||
| <Dot /> | |||
| <div className='system-xs-medium text-text-tertiary'>{word_count} {t('datasetDocuments.segment.characters', { count: word_count })}</div> | |||
| </div> | |||
| <Score value={score} /> | |||
| </div> | |||
| <ResultItemMeta className='px-3' labelPrefix={`${isParentChildRetrieval ? 'Parent-' : ''}Chunk`} positionId={position} wordCount={word_count} score={score} /> | |||
| {/* Main */} | |||
| <div className='mt-1 px-3'> | |||
| @@ -88,19 +73,7 @@ const ResultItem: FC<Props> = ({ | |||
| )} | |||
| </div> | |||
| {/* Foot */} | |||
| <div className='mt-3 flex justify-between items-center h-10 pl-3 pr-2 border-t border-divider-subtle'> | |||
| <div className='grow flex items-center space-x-1'> | |||
| <FileIcon type={fileType} 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(`${i18nPrefix}.open`)}</div> | |||
| <RiArrowRightUpLine className='size-3.5' /> | |||
| </div> | |||
| </div> | |||
| <ResultItemFooter docType={fileType} docTitle={document.name} showDetailModal={showDetailModal} /> | |||
| { | |||
| isShowDetailModal && ( | |||
| @@ -12,8 +12,9 @@ import Textarea from './textarea' | |||
| import s from './style.module.css' | |||
| import ModifyRetrievalModal from './modify-retrieval-modal' | |||
| import ResultItem from './components/result-item' | |||
| import ResultItemExternal from './components/result-item-external' | |||
| import cn from '@/utils/classnames' | |||
| import type { ExternalKnowledgeBaseHitTestingResponse, HitTestingResponse } from '@/models/datasets' | |||
| import type { ExternalKnowledgeBaseHitTesting, ExternalKnowledgeBaseHitTestingResponse, HitTesting, HitTestingResponse } from '@/models/datasets' | |||
| import Loading from '@/app/components/base/loading' | |||
| import Drawer from '@/app/components/base/drawer' | |||
| import Pagination from '@/app/components/base/pagination' | |||
| @@ -41,7 +42,7 @@ const RecordsEmpty: FC = () => { | |||
| </div> | |||
| } | |||
| const HitTesting: FC<Props> = ({ datasetId }: Props) => { | |||
| const HitTestingPage: FC<Props> = ({ datasetId }: Props) => { | |||
| const { t } = useTranslation() | |||
| const { formatTime } = useTimestamp() | |||
| @@ -68,19 +69,25 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => { | |||
| const [retrievalConfig, setRetrievalConfig] = useState(currentDataset?.retrieval_model_dict as RetrievalConfig) | |||
| const [isShowModifyRetrievalModal, setIsShowModifyRetrievalModal] = useState(false) | |||
| const [isShowRightPanel, { setTrue: showRightPanel, setFalse: hideRightPanel, set: setShowRightPanel }] = useBoolean(!isMobile) | |||
| const renderHitResults = (results: any[]) => ( | |||
| const renderHitResults = (results: HitTesting[] | ExternalKnowledgeBaseHitTesting[]) => ( | |||
| <div className='h-full flex flex-col py-3 px-4 rounded-t-2xl bg-background-body'> | |||
| <div className='shrink-0 pl-2 text-text-primary font-semibold leading-6 mb-2'> | |||
| {t('datasetHitTesting.hit.title', { num: results.length })} | |||
| </div> | |||
| <div className='grow overflow-y-auto space-y-2'> | |||
| {results.map((record, idx) => ( | |||
| <ResultItem | |||
| key={idx} | |||
| payload={record} | |||
| isExternal={isExternal} | |||
| /> | |||
| ))} | |||
| {results.map((record, idx) => | |||
| isExternal | |||
| ? ( | |||
| <ResultItemExternal | |||
| key={idx} | |||
| positionId={idx + 1} | |||
| payload={record as ExternalKnowledgeBaseHitTesting} | |||
| /> | |||
| ) | |||
| : ( | |||
| <ResultItem key={idx} payload={record as HitTesting} /> | |||
| ), | |||
| )} | |||
| </div> | |||
| </div> | |||
| ) | |||
| @@ -130,7 +137,7 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => { | |||
| <> | |||
| <div className='grow overflow-y-auto'> | |||
| <table className={'w-full border-collapse border-0 text-[13px] leading-4 text-text-secondary '}> | |||
| <thead className="sticky top-0 h-7 leading-7 text-xs text-text-tertiary font-medium uppercase"> | |||
| <thead className='sticky top-0 h-7 leading-7 text-xs text-text-tertiary font-medium uppercase'> | |||
| <tr> | |||
| <td className='pl-3 w-[128px] rounded-l-lg bg-background-section-burn'>{t('datasetHitTesting.table.header.source')}</td> | |||
| <td className='bg-background-section-burn'>{t('datasetHitTesting.table.header.text')}</td> | |||
| @@ -208,4 +215,4 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => { | |||
| ) | |||
| } | |||
| export default HitTesting | |||
| export default HitTestingPage | |||
| @@ -84,6 +84,7 @@ const TextAreaWithButton = ({ | |||
| } | |||
| const externalRetrievalTestingOnSubmit = async () => { | |||
| setLoading(true) | |||
| const [e, res] = await asyncRunSafe<ExternalKnowledgeBaseHitTestingResponse>( | |||
| externalKnowledgeBaseHitTesting({ | |||
| datasetId, | |||