Spaces:
Build error
Build error
'use client' | |
import type { FC } from 'react' | |
import React, { useEffect, useState } from 'react' | |
import { useTranslation } from 'react-i18next' | |
import useSWR from 'swr' | |
import { omit } from 'lodash-es' | |
import { useBoolean } from 'ahooks' | |
import { useContext } from 'use-context-selector' | |
import SegmentCard from '../documents/detail/completed/SegmentCard' | |
import docStyle from '../documents/detail/completed/style.module.css' | |
import Textarea from './textarea' | |
import s from './style.module.css' | |
import HitDetail from './hit-detail' | |
import ModifyRetrievalModal from './modify-retrieval-modal' | |
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' | |
import Modal from '@/app/components/base/modal' | |
import Drawer from '@/app/components/base/drawer' | |
import Pagination from '@/app/components/base/pagination' | |
import FloatRightContainer from '@/app/components/base/float-right-container' | |
import { fetchTestingRecords } from '@/service/datasets' | |
import DatasetDetailContext from '@/context/dataset-detail' | |
import type { RetrievalConfig } from '@/types/app' | |
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | |
import useTimestamp from '@/hooks/use-timestamp' | |
const limit = 10 | |
type Props = { | |
datasetId: string | |
} | |
const RecordsEmpty: FC = () => { | |
const { t } = useTranslation() | |
return <div className='bg-gray-50 rounded-2xl p-5'> | |
<div className={s.clockWrapper}> | |
<div className={cn(s.clockIcon, 'w-5 h-5')}></div> | |
</div> | |
<div className='my-2 text-gray-500 text-sm'>{t('datasetHitTesting.noRecentTip')}</div> | |
</div> | |
} | |
const HitTesting: FC<Props> = ({ datasetId }: Props) => { | |
const { t } = useTranslation() | |
const { formatTime } = useTimestamp() | |
const media = useBreakpoints() | |
const isMobile = media === MediaType.mobile | |
const [hitResult, setHitResult] = useState<HitTestingResponse | undefined>() // 初始化记录为空数组 | |
const [externalHitResult, setExternalHitResult] = useState<ExternalKnowledgeBaseHitTestingResponse | undefined>() | |
const [submitLoading, setSubmitLoading] = useState(false) | |
const [currParagraph, setCurrParagraph] = useState<{ paraInfo?: HitTestingType; showModal: boolean }>({ showModal: false }) | |
const [externalCurrParagraph, setExternalCurrParagraph] = useState<{ paraInfo?: ExternalKnowledgeBaseHitTestingType; showModal: boolean }>({ showModal: false }) | |
const [text, setText] = useState('') | |
const [currPage, setCurrPage] = React.useState<number>(0) | |
const { data: recordsRes, error, mutate: recordsMutate } = useSWR({ | |
action: 'fetchTestingRecords', | |
datasetId, | |
params: { limit, page: currPage + 1 }, | |
}, apiParams => fetchTestingRecords(omit(apiParams, 'action'))) | |
const total = recordsRes?.total || 0 | |
const onClickCard = (detail: HitTestingType) => { | |
setCurrParagraph({ paraInfo: detail, showModal: true }) | |
} | |
const onClickExternalCard = (detail: ExternalKnowledgeBaseHitTestingType) => { | |
setExternalCurrParagraph({ paraInfo: detail, showModal: true }) | |
} | |
const { dataset: currentDataset } = useContext(DatasetDetailContext) | |
const isExternal = currentDataset?.provider === 'external' | |
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[], onClickCard: (record: any) => void) => ( | |
<> | |
<div className='text-gray-600 font-semibold mb-4'>{t('datasetHitTesting.hit.title')}</div> | |
<div className='overflow-auto flex-1'> | |
<div className={s.cardWrapper}> | |
{results.map((record, idx) => ( | |
<SegmentCard | |
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)} | |
/> | |
))} | |
</div> | |
</div> | |
</> | |
) | |
const renderEmptyState = () => ( | |
<div className='h-full flex flex-col justify-center items-center'> | |
<div className={cn(docStyle.commonIcon, docStyle.targetIcon, '!bg-gray-200 !h-14 !w-14')} /> | |
<div className='text-gray-300 text-[13px] mt-3'> | |
{t('datasetHitTesting.hit.emptyTip')} | |
</div> | |
</div> | |
) | |
useEffect(() => { | |
setShowRightPanel(!isMobile) | |
}, [isMobile, setShowRightPanel]) | |
return ( | |
<div className={s.container}> | |
<div className={s.leftDiv}> | |
<div className={s.titleWrapper}> | |
<h1 className={s.title}>{t('datasetHitTesting.title')}</h1> | |
<p className={s.desc}>{t('datasetHitTesting.desc')}</p> | |
</div> | |
<Textarea | |
datasetId={datasetId} | |
setHitResult={setHitResult} | |
setExternalHitResult={setExternalHitResult} | |
onSubmit={showRightPanel} | |
onUpdateList={recordsMutate} | |
loading={submitLoading} | |
setLoading={setSubmitLoading} | |
setText={setText} | |
text={text} | |
isExternal={isExternal} | |
onClickRetrievalMethod={() => setIsShowModifyRetrievalModal(true)} | |
retrievalConfig={retrievalConfig} | |
isEconomy={currentDataset?.indexing_technique === 'economy'} | |
/> | |
<div className={cn(s.title, 'mt-8 mb-2')}>{t('datasetHitTesting.recents')}</div> | |
{(!recordsRes && !error) | |
? ( | |
<div className='flex-1'><Loading type='app' /></div> | |
) | |
: recordsRes?.data?.length | |
? ( | |
<> | |
<div className='grow overflow-y-auto'> | |
<table className={`w-full border-collapse border-0 mt-3 ${s.table}`}> | |
<thead className="sticky top-0 h-8 bg-white leading-8 border-b border-gray-200 text-gray-500 font-bold"> | |
<tr> | |
<td className='w-28'>{t('datasetHitTesting.table.header.source')}</td> | |
<td>{t('datasetHitTesting.table.header.text')}</td> | |
<td className='w-48'>{t('datasetHitTesting.table.header.time')}</td> | |
</tr> | |
</thead> | |
<tbody className="text-gray-500"> | |
{recordsRes?.data?.map((record) => { | |
return <tr | |
key={record.id} | |
className='group border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer' | |
onClick={() => setText(record.content)} | |
> | |
<td className='w-24'> | |
<div className='flex items-center'> | |
<div className={cn(s[`${record.source}_icon`], s.commonIcon, 'mr-1')} /> | |
<span className='capitalize'>{record.source.replace('_', ' ')}</span> | |
</div> | |
</td> | |
<td className='max-w-xs group-hover:text-primary-600'>{record.content}</td> | |
<td className='w-36'> | |
{formatTime(record.created_at, t('datasetHitTesting.dateTimeFormat') as string)} | |
</td> | |
</tr> | |
})} | |
</tbody> | |
</table> | |
</div> | |
{(total && total > limit) | |
? <Pagination current={currPage} onChange={setCurrPage} total={total} limit={limit} /> | |
: null} | |
</> | |
) | |
: ( | |
<RecordsEmpty /> | |
)} | |
</div> | |
<FloatRightContainer panelClassname='!justify-start !overflow-y-auto' showClose isMobile={isMobile} isOpen={isShowRightPanel} onClose={hideRightPanel} footer={null}> | |
<div className={cn(s.rightDiv, 'p-0 sm:px-8 sm:pt-[42px] sm:pb-[26px]')}> | |
{submitLoading | |
? <div className={s.cardWrapper}> | |
<SegmentCard | |
loading={true} | |
scene='hitTesting' | |
className='h-[216px]' | |
/> | |
<SegmentCard | |
loading={true} | |
scene='hitTesting' | |
className='h-[216px]' | |
/> | |
</div> | |
: ( | |
(() => { | |
if (!hitResult?.records.length && !externalHitResult?.records.length) | |
return renderEmptyState() | |
if (hitResult?.records.length) | |
return renderHitResults(hitResult.records, onClickCard) | |
return renderHitResults(externalHitResult?.records || [], onClickExternalCard) | |
})() | |
) | |
} | |
</div> | |
</FloatRightContainer> | |
<Modal | |
className={isExternal ? 'py-10 px-8' : 'w-full'} | |
closable | |
onClose={() => { | |
setCurrParagraph({ showModal: false }) | |
setExternalCurrParagraph({ showModal: false }) | |
}} | |
isShow={currParagraph.showModal || externalCurrParagraph.showModal} | |
> | |
{currParagraph.showModal && ( | |
<HitDetail | |
segInfo={currParagraph.paraInfo?.segment} | |
/> | |
)} | |
{externalCurrParagraph.showModal && ( | |
<HitDetail | |
segInfo={{ | |
id: 'external', | |
content: externalCurrParagraph.paraInfo?.content, | |
}} | |
/> | |
)} | |
</Modal> | |
<Drawer isOpen={isShowModifyRetrievalModal} onClose={() => setIsShowModifyRetrievalModal(false)} footer={null} mask={isMobile} panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'> | |
<ModifyRetrievalModal | |
indexMethod={currentDataset?.indexing_technique || ''} | |
value={retrievalConfig} | |
isShow={isShowModifyRetrievalModal} | |
onHide={() => setIsShowModifyRetrievalModal(false)} | |
onSave={(value) => { | |
setRetrievalConfig(value) | |
setIsShowModifyRetrievalModal(false) | |
}} | |
/> | |
</Drawer> | |
</div> | |
) | |
} | |
export default HitTesting | |