| '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 |
|
|