Feat/parent child retrieval (#12086)

Signed-off-by: yihong0618 <zouzou0208@gmail.com>
Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: AkaraChen <akarachen@outlook.com>
Co-authored-by: nite-knite <nkCoding@gmail.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Warren Chen <warren.chen830@gmail.com>
Co-authored-by: crazywoola <427733928@qq.com>
Co-authored-by: Yi Xiao <54782454+YIXIAO0@users.noreply.github.com>
Co-authored-by: yihong <zouzou0208@gmail.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: KVOJJJin <jzongcode@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: JzoNgKVO <27049666+JzoNgKVO@users.noreply.github.com>
Co-authored-by: Charlie.Wei <luowei@cvte.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: huayaoyue6 <huayaoyue@163.com>
Co-authored-by: kurokobo <kuro664@gmail.com>
Co-authored-by: Matsuda <yiyth.fcb6@gmail.com>
Co-authored-by: shirochan <s.yusuke0711@gmail.com>
Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com>
Co-authored-by: Huỳnh Gia Bôi <boihuynh147@gmail.com>
Co-authored-by: Julian Huynh <julian.huynh@immersio.io>
Co-authored-by: Hash Brown <hi@xzd.me>
Co-authored-by: 非法操作 <hjlarry@163.com>
Co-authored-by: Kazuki Takamatsu <kazuki.takamatsu@chowagiken.co.jp>
Co-authored-by: Trey Dong <1346650911@qq.com>
Co-authored-by: VoidIsVoid <343750470@qq.com>
Co-authored-by: Gimling <huangjl@ruyi.ai>
Co-authored-by: xiandan-erizo <xiandan.erizo@gmail.com>
Co-authored-by: Muneyuki Noguchi <nogu.dev@gmail.com>
Co-authored-by: zhaobingshuang <1475195565@qq.com>
Co-authored-by: zhaobs <zhaobs@cailian.net>
Co-authored-by: suzuki.sh <s2terminal@users.noreply.github.com>
Co-authored-by: Yingchun Lai <laiyingchun@apache.org>
Co-authored-by: huanshare <huanshare@live.com>
Co-authored-by: huanshare <liuhuan101@longfor.com>
Co-authored-by: orangeclk <orangeclk@users.noreply.github.com>
Co-authored-by: 문정현 <120004247+JungHyunMoon@users.noreply.github.com>
Co-authored-by: barabicu <kztk533@gmail.com>
Co-authored-by: Wei Mingzhi <whistler_wmz@users.sf.net>
Co-authored-by: Paul van Oorschot <20116814+pvoo@users.noreply.github.com>
Co-authored-by: zkyTech <zhangkunyuan@hotmail.com>
Co-authored-by: zhangkunyuan <zhangkunyuan@cmhi.chinamobile.com>
Co-authored-by: Tommy <34446820+Asterovim@users.noreply.github.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: Novice <857526207@qq.com>
Co-authored-by: Novice Lee <novicelee@NovicedeMacBook-Pro.local>
Co-authored-by: Novice Lee <novicelee@NoviPro.local>
Co-authored-by: zxhlyh <16177003+zxhlyh@users.noreply.github.com>
Co-authored-by: liuzhenghua <1090179900@qq.com>
Co-authored-by: Jiang <65766008+AlwaysBluer@users.noreply.github.com>
Co-authored-by: jiangzhijie <jiangzhijie.jzj@alibaba-inc.com>
Co-authored-by: Joe <79627742+ZhouhaoJiang@users.noreply.github.com>
Co-authored-by: Alok Shrivastwa <alok.shrivastwa@gmail.com>
Co-authored-by: Alok Shrivastwa <Alok.Shrivastwa@microland.com>
Co-authored-by: JasonVV <jasonwangiii@outlook.com>
Co-authored-by: Hiroshi Fujita <fujita-h@users.noreply.github.com>
Co-authored-by: Kevin9703 <51311316+Kevin9703@users.noreply.github.com>
Co-authored-by: NFish <douxc512@gmail.com>
Co-authored-by: Junyan Qin <1010553892@qq.com>
Co-authored-by: IWAI, Masaharu <iwaim.sub@gmail.com>
Co-authored-by: IWAI, Masaharu <iwai_masaharu@funkit.co.jp>
Co-authored-by: Bowen Liang <liangbowen@gf.com.cn>
Co-authored-by: luckylhb90 <luckylhb90@gmail.com>
Co-authored-by: hobo.l <hobo.l@binance.com>
Co-authored-by: douxc <7553076+douxc@users.noreply.github.com>
This commit is contained in:
Wu Tianwei
2024-12-25 18:17:15 +08:00
committed by GitHub
parent 39ace9bdee
commit 2b2263a349
216 changed files with 9066 additions and 3116 deletions

View File

@@ -0,0 +1,30 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { SliceContent } from '../../formatted-text/flavours/shared'
import Score from './score'
import type { HitTestingChildChunk } from '@/models/datasets'
type Props = {
payload: HitTestingChildChunk
isShowAll: boolean
}
const ChildChunks: FC<Props> = ({
payload,
isShowAll,
}) => {
const { id, score, content, position } = payload
return (
<div
className={!isShowAll ? 'line-clamp-2' : ''}
>
<div className='inline-flex items-center relative top-[-2px]'>
<div className='flex items-center h-[20.5px] bg-state-accent-solid system-2xs-semibold-uppercase text-text-primary-on-surface px-1'>C-{position}</div>
<Score value={score} besideChunkName />
</div>
<SliceContent className='py-0.5 bg-state-accent-hover group-hover:bg-state-accent-hover text-sm text-text-secondary font-normal'>{content}</SliceContent>
</div>
)
}
export default React.memo(ChildChunks)

View File

@@ -0,0 +1,89 @@
'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 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'
import Tag from '@/app/components/datasets/documents/detail/completed/common/tag'
const i18nPrefix = 'datasetHitTesting'
type Props = {
payload: HitTesting
onHide: () => void
}
const ChunkDetailModal: FC<Props> = ({
payload,
onHide,
}) => {
const { t } = useTranslation()
const { segment, score, child_chunks } = payload
const { position, content, keywords, document } = segment
const isParentChildRetrieval = !!(child_chunks && child_chunks.length > 0)
const extension = document.name.split('.').slice(-1)[0] as FileAppearanceTypeEnum
const heighClassName = isParentChildRetrieval ? 'h-[min(627px,_80vh)] overflow-y-auto' : 'h-[min(539px,_80vh)] overflow-y-auto'
return (
<Modal
title={t(`${i18nPrefix}.chunkDetail`)}
isShow
closable
onClose={onHide}
className={cn(isParentChildRetrieval ? '!min-w-[1200px]' : '!min-w-[800px]')}
>
<div className='mt-4 flex'>
<div className={cn('flex-1', isParentChildRetrieval && 'pr-6')}>
{/* Meta info */}
<div className='flex justify-between items-center'>
<div className='grow flex items-center space-x-2'>
<SegmentIndexTag
labelPrefix={`${isParentChildRetrieval ? 'Parent-' : ''}Chunk`}
positionId={position}
className={cn('w-fit group-hover:opacity-100')}
/>
<Dot />
<div className='grow 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={cn('mt-2 body-md-regular text-text-secondary', heighClassName)}>
{content}
</div>
{!isParentChildRetrieval && keywords && keywords.length > 0 && (
<div className='mt-6'>
<div className='font-medium text-xs text-text-tertiary uppercase'>{t(`${i18nPrefix}.keyword`)}</div>
<div className='mt-1 flex flex-wrap'>
{keywords.map(keyword => (
<Tag key={keyword} text={keyword} className='mr-2' />
))}
</div>
</div>
)}
</div>
{isParentChildRetrieval && (
<div className='flex-1 pl-6 pb-6'>
<div className='system-xs-semibold-uppercase text-text-secondary'>{t(`${i18nPrefix}.hitChunks`, { num: child_chunks.length })}</div>
<div className={cn('mt-1 space-y-2', heighClassName)}>
{child_chunks.map(item => (
<ChildChunksItem key={item.id} payload={item} isShowAll />
))}
</div>
</div>
)}
</div>
</Modal>
)
}
export default React.memo(ChunkDetailModal)

View File

@@ -0,0 +1,121 @@
'use client'
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/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 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 { 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
const fileType = extensionToFileType(extension)
const [isFold, {
toggle: toggleFold,
}] = useBoolean(false)
const Icon = isFold ? RiArrowRightSLine : RiArrowDownSLine
const [isShowDetailModal, {
setTrue: showDetailModal,
setFalse: hideDetailModal,
}] = useBoolean(false)
const handleClickCard = () => {
if (!isParentChildRetrieval)
showDetailModal()
}
return (
<div className={cn('pt-3 bg-chat-bubble-bg rounded-xl hover:shadow-lg', !isParentChildRetrieval && 'cursor-pointer')} onClick={handleClickCard}>
{/* 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>
{/* Main */}
<div className='mt-1 px-3'>
<div className='line-clamp-2 body-md-regular'>{content}</div>
{isParentChildRetrieval && (
<div className='mt-1'>
<div className={cn('inline-flex items-center h-6 space-x-0.5 text-text-secondary select-none rounded-lg cursor-pointer', isFold && 'pl-1 bg-[linear-gradient(90deg,_rgba(200,_206,_218,_0.20)_0%,_rgba(200,_206,_218,_0.04)_100%)]')} onClick={toggleFold}>
<Icon className={cn('w-4 h-4', isFold && 'opacity-50')} />
<div className='text-xs font-semibold uppercase'>{t(`${i18nPrefix}.hitChunks`, { num: child_chunks.length })}</div>
</div>
{!isFold && (
<div className='space-y-2'>
{child_chunks.map(item => (
<div key={item.id} className='ml-[7px] pl-[7px] border-l-[2px] border-text-accent-secondary'>
<ChildChunkItem payload={item} isShowAll={false} />
</div>
))}
</div>
)}
</div>
)}
{!isParentChildRetrieval && keywords && keywords.length > 0 && (
<div className='mt-2 flex flex-wrap'>
{keywords.map(keyword => (
<Tag key={keyword} text={keyword} className='mr-2' />
))}
</div>
)}
</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>
{
isShowDetailModal && (
<ChunkDetailModal
payload={payload}
onHide={hideDetailModal}
/>
)
}
</div >
)
}
export default React.memo(ResultItem)

View File

@@ -0,0 +1,25 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
type Props = {
value: number
besideChunkName?: boolean
}
const Score: FC<Props> = ({
value,
besideChunkName,
}) => {
return (
<div className={cn('relative items-center px-[5px] border border-components-progress-bar-border overflow-hidden', besideChunkName ? 'border-l-0 h-[20.5px]' : 'h-[20px] rounded-md')}>
<div className={cn('absolute top-0 left-0 h-full bg-util-colors-blue-brand-blue-brand-100 border-r-[1.5px] border-components-progress-brand-progress', value === 1 && 'border-r-0')} style={{ width: `${value * 100}%` }} />
<div className={cn('relative flex items-center h-full 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)

View File

@@ -1,68 +0,0 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { SegmentIndexTag } from '../documents/detail/completed'
import s from '../documents/detail/completed/style.module.css'
import cn from '@/utils/classnames'
import type { SegmentDetailModel } from '@/models/datasets'
import Divider from '@/app/components/base/divider'
type IHitDetailProps = {
segInfo?: Partial<SegmentDetailModel> & { id: string }
}
const HitDetail: FC<IHitDetailProps> = ({ segInfo }) => {
const { t } = useTranslation()
const renderContent = () => {
if (segInfo?.answer) {
return (
<>
<div className='mt-2 mb-1 text-xs font-medium text-gray-500'>QUESTION</div>
<div className='mb-4 text-md text-gray-800'>{segInfo.content}</div>
<div className='mb-1 text-xs font-medium text-gray-500'>ANSWER</div>
<div className='text-md text-gray-800'>{segInfo.answer}</div>
</>
)
}
return <div className='mb-4 text-md text-gray-800 h-full'>{segInfo?.content}</div>
}
return (
segInfo?.id === 'external'
? <div className='w-full overflow-x-auto px-2'>
<div className={s.segModalContent}>{renderContent()}</div>
</div>
: <div className='overflow-x-auto'>
<div className="flex items-center">
<SegmentIndexTag
positionId={segInfo?.position || ''}
className="w-fit mr-6"
/>
<div className={cn(s.commonIcon, s.typeSquareIcon)} />
<span className={cn('mr-6', s.numberInfo)}>
{segInfo?.word_count} {t('datasetDocuments.segment.characters')}
</span>
<div className={cn(s.commonIcon, s.targetIcon)} />
<span className={s.numberInfo}>
{segInfo?.hit_count} {t('datasetDocuments.segment.hitCount')}
</span>
</div>
<Divider />
<div className={s.segModalContent}>{renderContent()}</div>
<div className={s.keywordTitle}>
{t('datasetDocuments.segment.keywords')}
</div>
<div className={s.keywordWrapper}>
{!segInfo?.keywords?.length
? '-'
: segInfo?.keywords?.map((word, index) => {
return <div key={index} className={s.keyword}>{word}</div>
})}
</div>
</div>
)
}
export default HitDetail

View File

@@ -6,16 +6,15 @@ import useSWR from 'swr'
import { omit } from 'lodash-es'
import { useBoolean } from 'ahooks'
import { useContext } from 'use-context-selector'
import { RiApps2Line, RiFocus2Line } from '@remixicon/react'
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 ResultItem from './components/result-item'
import cn from '@/utils/classnames'
import type { ExternalKnowledgeBaseHitTestingResponse, ExternalKnowledgeBaseHitTesting as ExternalKnowledgeBaseHitTestingType, HitTestingResponse, HitTesting as HitTestingType } from '@/models/datasets'
import type { ExternalKnowledgeBaseHitTestingResponse, HitTestingResponse } 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'
@@ -24,6 +23,7 @@ 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'
import docStyle from '@/app/components/datasets/documents/detail/completed/style.module.css'
const limit = 10
@@ -51,8 +51,6 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
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)
@@ -64,51 +62,33 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
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>
const renderHitResults = (results: any[]) => (
<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}
/>
))}
</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'>
<div className='h-full flex flex-col justify-center items-center py-3 px-4 rounded-t-2xl bg-background-body'>
<div className={cn(docStyle.commonIcon, docStyle.targetIcon, '!bg-text-quaternary !h-14 !w-14')} />
<div className='text-text-quaternary text-[13px] mt-3'>
{t('datasetHitTesting.hit.emptyTip')}
</div>
</div>
@@ -120,10 +100,10 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
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 className='px-6 py-3 flex flex-col'>
<div className='flex flex-col justify-center mb-4'>
<h1 className='text-base font-semibold text-text-primary'>{t('datasetHitTesting.title')}</h1>
<p className='mt-0.5 text-[13px] leading-4 font-normal text-text-tertiary'>{t('datasetHitTesting.desc')}</p>
</div>
<Textarea
datasetId={datasetId}
@@ -140,7 +120,7 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
retrievalConfig={retrievalConfig}
isEconomy={currentDataset?.indexing_technique === 'economy'}
/>
<div className={cn(s.title, 'mt-8 mb-2')}>{t('datasetHitTesting.recents')}</div>
<div className='text-base font-semibold text-text-primary mt-6 mb-3'>{t('datasetHitTesting.records')}</div>
{(!recordsRes && !error)
? (
<div className='flex-1'><Loading type='app' /></div>
@@ -149,29 +129,30 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
? (
<>
<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">
<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">
<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>
<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>
<td className='pl-2 w-48 rounded-r-lg bg-background-section-burn'>{t('datasetHitTesting.table.header.time')}</td>
</tr>
</thead>
<tbody className="text-gray-500">
<tbody>
{recordsRes?.data?.map((record) => {
const SourceIcon = record.source === 'app' ? RiApps2Line : RiFocus2Line
return <tr
key={record.id}
className='group border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer'
className='group border-b border-divider-subtle h-10 hover:bg-background-default-hover cursor-pointer'
onClick={() => setText(record.content)}
>
<td className='w-24'>
<td className='pl-3 w-[128px]'>
<div className='flex items-center'>
<div className={cn(s[`${record.source}_icon`], s.commonIcon, 'mr-1')} />
<span className='capitalize'>{record.source.replace('_', ' ')}</span>
<SourceIcon className='mr-1 size-4 text-text-tertiary' />
<span className='capitalize'>{record.source.replace('_', ' ').replace('hit testing', 'retrieval test')}</span>
</div>
</td>
<td className='max-w-xs group-hover:text-primary-600'>{record.content}</td>
<td className='w-36'>
<td className='max-w-xs py-2'>{record.content}</td>
<td className='pl-2 w-36'>
{formatTime(record.created_at, t('datasetHitTesting.dateTimeFormat') as string)}
</td>
</tr>
@@ -189,57 +170,28 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
)}
</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]')}>
<div className='flex flex-col pt-3'>
{/* {renderHitResults(generalResultData)} */}
{submitLoading
? <div className={s.cardWrapper}>
<SegmentCard
loading={true}
scene='hitTesting'
className='h-[216px]'
/>
<SegmentCard
loading={true}
scene='hitTesting'
className='h-[216px]'
/>
</div>
? <SegmentCard
loading={true}
scene='hitTesting'
className='h-[216px]'
/>
: (
(() => {
if (!hitResult?.records.length && !externalHitResult?.records.length)
return renderEmptyState()
if (hitResult?.records.length)
return renderHitResults(hitResult.records, onClickCard)
return renderHitResults(hitResult.records)
return renderHitResults(externalHitResult?.records || [], onClickExternalCard)
return renderHitResults(externalHitResult?.records || [])
})()
)
}
</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 || ''}

View File

@@ -11,6 +11,7 @@ import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/ec
import Button from '@/app/components/base/button'
import { ensureRerankModelSelected, isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { RerankingModeEnum } from '@/models/datasets'
type Props = {
indexMethod: string
@@ -57,7 +58,10 @@ const ModifyRetrievalModal: FC<Props> = ({
}
onSave(ensureRerankModelSelected({
rerankDefaultModel: rerankDefaultModel!,
retrievalConfig,
retrievalConfig: {
...retrievalConfig,
reranking_enable: retrievalConfig.reranking_mode === RerankingModeEnum.RerankingModel,
},
indexMethod,
}))
}
@@ -77,7 +81,7 @@ const ModifyRetrievalModal: FC<Props> = ({
<div className='text-base font-semibold text-gray-900'>
<div>{t('datasetSettings.form.retrievalSetting.title')}</div>
<div className='leading-[18px] text-xs font-normal text-gray-500'>
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-[#155eef]'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-text-accent'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
{t('datasetSettings.form.retrievalSetting.description')}
</div>
</div>

View File

@@ -1,61 +1,43 @@
.container {
@apply flex h-full w-full relative overflow-y-auto;
}
.container > div {
.container>div {
@apply flex-1 h-full;
}
.leftDiv {
@apply border-r border-gray-100 px-6 py-3 flex flex-col;
}
.rightDiv {
@apply flex flex-col;
}
.titleWrapper {
@apply flex flex-col justify-center gap-1 mb-5;
}
.title {
@apply text-xl font-medium text-gray-900;
}
.desc {
@apply text-sm font-normal text-gray-500;
}
.table {
@apply text-[13px] text-gray-500;
}
.table td {
@apply whitespace-nowrap overflow-hidden text-ellipsis;
}
.commonIcon {
@apply w-3.5 h-3.5 inline-block align-middle;
background-repeat: no-repeat;
background-position: center center;
background-size: contain;
}
.app_icon {
background-image: url(./assets/grid.svg);
}
.hit_testing_icon {
background-image: url(../documents/assets/target.svg);
}
.plugin_icon {
background-image: url(./assets/plugin.svg);
}
.wrapper {
@apply relative border border-primary-600 rounded-xl;
}
.cardWrapper {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(284px, auto));
grid-gap: 16px;
grid-auto-rows: 216px;
}
.clockWrapper {
border: 0.5px solid #eaecf5;
@apply rounded-lg w-11 h-11 flex justify-center items-center;
}
.clockIcon {
mask-image: url(./assets/clock.svg);
@apply bg-gray-500;
}
}

View File

@@ -3,10 +3,9 @@ import { useTranslation } from 'react-i18next'
import {
RiEqualizer2Line,
} from '@remixicon/react'
import Image from 'next/image'
import Button from '../../base/button'
import Tag from '../../base/tag'
import { getIcon } from '../common/retrieval-method-info'
import s from './style.module.css'
import ModifyExternalRetrievalModal from './modify-external-retrieval-modal'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
@@ -14,6 +13,7 @@ import type { ExternalKnowledgeBaseHitTestingResponse, HitTestingResponse } from
import { externalKnowledgeBaseHitTesting, hitTesting } from '@/service/datasets'
import { asyncRunSafe } from '@/utils'
import { RETRIEVE_METHOD, type RetrievalConfig } from '@/types/app'
import promptS from '@/app/components/app/configuration/config-prompt/style.module.css'
type TextAreaWithButtonIProps = {
datasetId: string
@@ -103,13 +103,13 @@ const TextAreaWithButton = ({
}
const retrievalMethod = isEconomy ? RETRIEVE_METHOD.invertedIndex : retrievalConfig.search_method
const Icon = getIcon(retrievalMethod)
const icon = <Image className='size-3.5 text-util-colors-purple-purple-600' src={getIcon(retrievalMethod)} alt='' />
return (
<>
<div className={s.wrapper}>
<div className='relative pt-2 rounded-tl-xl rounded-tr-xl bg-[#EEF4FF]'>
<div className="px-4 pb-2 flex justify-between h-8 items-center">
<span className="text-gray-800 font-semibold text-sm">
<div className={cn('relative rounded-xl', promptS.gradientBorder)}>
<div className='relative pt-1.5 rounded-tl-xl rounded-tr-xl bg-background-section-burn'>
<div className="pl-4 pr-1.5 pb-1 flex justify-between h-8 items-center">
<span className="text-text-secondary font-semibold text-[13px] leading-4 uppercase">
{t('datasetHitTesting.input.title')}
</span>
{isExternal
@@ -123,17 +123,14 @@ const TextAreaWithButton = ({
<span className='text-components-button-secondary-text system-xs-medium'>{t('datasetHitTesting.settingTitle')}</span>
</div>
</Button>
: <Tooltip
popupContent={t('dataset.retrieval.changeRetrievalMethod')}
: <div
onClick={onClickRetrievalMethod}
className='flex px-1.5 h-7 items-center bg-components-button-secondary-bg hover:bg-components-button-secondary-bg-hover rounded-lg border-[0.5px] border-components-button-secondary-bg shadow-xs backdrop-blur-[5px] cursor-pointer space-x-0.5'
>
<div
onClick={onClickRetrievalMethod}
className='flex px-2 h-7 items-center space-x-1 bg-white hover:bg-[#ECE9FE] rounded-md shadow-sm cursor-pointer text-[#6927DA]'
>
<Icon className='w-3.5 h-3.5'></Icon>
<div className='text-xs font-medium'>{t(`dataset.retrieval.${retrievalMethod}.title`)}</div>
</div>
</Tooltip>
{icon}
<div className='text-text-secondary text-xs font-medium uppercase'>{t(`dataset.retrieval.${retrievalMethod}.title`)}</div>
<RiEqualizer2Line className='size-4 text-components-menu-item-text'></RiEqualizer2Line>
</div>
}
</div>
{
@@ -147,11 +144,11 @@ const TextAreaWithButton = ({
/>
)
}
<div className='h-2 rounded-tl-xl rounded-tr-xl bg-white'></div>
<div className='h-2 rounded-tl-xl rounded-tr-xl bg-background-default'></div>
</div>
<div className='px-4 pb-11'>
<div className='px-4 pb-11 bg-background-default rounded-b-xl'>
<textarea
className='h-[220px] border-none resize-none font-normal caret-primary-600 text-gray-700 text-sm w-full focus-visible:outline-none placeholder:text-gray-300 placeholder:text-sm placeholder:font-normal'
className='h-[220px] border-none resize-none font-normal caret-primary-600 text-text-secondary text-sm w-full focus-visible:outline-none placeholder:text-gray-300 placeholder:text-sm placeholder:font-normal'
value={text}
onChange={handleTextChange}
placeholder={t('datasetHitTesting.input.placeholder') as string}
@@ -162,24 +159,23 @@ const TextAreaWithButton = ({
<Tooltip
popupContent={t('datasetHitTesting.input.countWarning')}
>
<div>
<Tag color="red" className="!text-red-600">
{text?.length}
<span className="text-red-300 mx-0.5">/</span>
200
</Tag>
<div
className={cn('flex items-center h-5 px-1 rounded-md bg-background-section-burn text-red-600 text-xs font-medium', !text?.length && 'opacity-50')}
>
{text?.length}
<span className="text-red-300 mx-0.5">/</span>
200
</div>
</Tooltip>
)
: (
<Tag
color="gray"
className={cn('!text-gray-500', text?.length ? '' : 'opacity-50')}
<div
className={cn('flex items-center h-5 px-1 rounded-md bg-background-section-burn text-text-tertiary text-xs font-medium', !text?.length && 'opacity-50')}
>
{text?.length}
<span className="text-gray-300 mx-0.5">/</span>
<span className="text-divider-deep mx-0.5">/</span>
200
</Tag>
</div>
)}
<div>
@@ -188,6 +184,7 @@ const TextAreaWithButton = ({
variant="primary"
loading={loading}
disabled={(!text?.length || text?.length > 200)}
className='w-[88px]'
>
{t('datasetHitTesting.input.testing')}
</Button>

View File

@@ -0,0 +1,31 @@
import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
export const extensionToFileType = (extension: string): FileAppearanceTypeEnum => {
switch (extension) {
case 'pdf':
return FileAppearanceTypeEnum.pdf
case 'doc':
case 'docx':
return FileAppearanceTypeEnum.word
case 'txt':
case 'epub':
return FileAppearanceTypeEnum.document
case 'md':
case 'mdx':
case 'markdown':
return FileAppearanceTypeEnum.markdown
case 'csv':
case 'xls':
case 'xlsx':
return FileAppearanceTypeEnum.excel
case 'html':
case 'htm':
case 'xml':
return FileAppearanceTypeEnum.document
case 'ppt':
case 'pptx':
return FileAppearanceTypeEnum.ppt
default:
return FileAppearanceTypeEnum.custom
}
}