Merge branch 'feat/parent-child-retrieval' of https://github.com/langgenius/dify into feat/parent-child-retrieval
commit
9dce37bb24
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const Dot = () => {
|
||||||
|
return (
|
||||||
|
<div className='text-text-quaternary text-xs font-medium'>·</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Dot.displayName = 'Dot'
|
||||||
|
|
||||||
|
export default React.memo(Dot)
|
||||||
@ -0,0 +1,129 @@
|
|||||||
|
import React, { type FC, useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { RiLoader2Line } from '@remixicon/react'
|
||||||
|
import Modal from '@/app/components/base/modal'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
|
|
||||||
|
type IDefaultContentProps = {
|
||||||
|
onCancel: () => void
|
||||||
|
onConfirm: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultContent: FC<IDefaultContentProps> = React.memo(({
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='p-6 pb-4'>
|
||||||
|
<span className='text-text-primary title-2xl-semi-bold'>{t('datasetDocuments.segment.regenerationConfirm')}</span>
|
||||||
|
<p className='text-text-secondary system-md-regular'>{t('datasetDocuments.segment.regenerationWarning')}</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-end gap-x-2 p-6'>
|
||||||
|
<Button onClick={onCancel}>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button destructive onClick={onConfirm}>
|
||||||
|
{t('common.operation.regenerate')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const RegeneratingContent: FC = React.memo(() => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='p-6 pb-4'>
|
||||||
|
<span className='text-text-primary title-2xl-semi-bold'>{t('datasetDocuments.segment.regeneratingTitle')}</span>
|
||||||
|
<p className='text-text-secondary system-md-regular'>{t('datasetDocuments.segment.regeneratingMessage')}</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-end p-6'>
|
||||||
|
<Button destructive disabled className='inline-flex items-center gap-x-0.5'>
|
||||||
|
<RiLoader2Line className='w-4 h-4 text-components-button-destructive-primary-text-disabled' />
|
||||||
|
<span>{t('common.operation.regenerate')}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
type IRegenerationCompletedContentProps = {
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RegenerationCompletedContent: FC<IRegenerationCompletedContentProps> = React.memo(({
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [countDown, setCountDown] = useState(5)
|
||||||
|
const timerRef = useRef<any>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
timerRef.current = setInterval(() => {
|
||||||
|
if (countDown > 0)
|
||||||
|
setCountDown(countDown - 1)
|
||||||
|
else
|
||||||
|
clearInterval(timerRef.current)
|
||||||
|
}, 1000)
|
||||||
|
return () => {
|
||||||
|
clearInterval(timerRef.current)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='p-6 pb-4'>
|
||||||
|
<span className='text-text-primary title-2xl-semi-bold'>{t('datasetDocuments.segment.regenerationSuccessTitle')}</span>
|
||||||
|
<p className='text-text-secondary system-md-regular'>{t('datasetDocuments.segment.regenerationSuccessMessage')}</p>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-end p-6'>
|
||||||
|
<Button variant='primary' onClick={onClose}>
|
||||||
|
{`${t('common.operation.close')}(${countDown})`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
type IRegenerationModalProps = {
|
||||||
|
isShow: boolean
|
||||||
|
onConfirm: () => void
|
||||||
|
onCancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RegenerationModal: FC<IRegenerationModalProps> = ({
|
||||||
|
isShow,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [updateSuccess, setUpdateSuccess] = useState(false)
|
||||||
|
const { eventEmitter } = useEventEmitterContextContext()
|
||||||
|
|
||||||
|
eventEmitter?.useSubscription((v) => {
|
||||||
|
if (v === 'update-segment') {
|
||||||
|
setLoading(true)
|
||||||
|
setUpdateSuccess(false)
|
||||||
|
}
|
||||||
|
if (v === 'update-segment-success')
|
||||||
|
setUpdateSuccess(true)
|
||||||
|
if (v === 'update-segment-done')
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isShow={isShow} onClose={() => {}} className='!max-w-[480px] !rounded-2xl'>
|
||||||
|
{(!loading && !updateSuccess) && <DefaultContent onCancel={onCancel} onConfirm={onConfirm} />}
|
||||||
|
{(loading && !updateSuccess) && <RegeneratingContent />}
|
||||||
|
{!loading && updateSuccess && <RegenerationCompletedContent onClose={onCancel} />}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegenerationModal
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
const Tag = ({ text, className }: { text: string; className?: string }) => {
|
||||||
|
return (
|
||||||
|
<div className={cn('inline-flex items-center gap-x-0.5', className)}>
|
||||||
|
<span className='text-text-quaternary text-xs font-medium'>#</span>
|
||||||
|
<span className='text-text-tertiary text-xs'>{text}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Tag.displayName = 'Tag'
|
||||||
|
|
||||||
|
export default React.memo(Tag)
|
||||||
@ -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,87 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { SegmentIndexTag } from '../../documents/detail/completed'
|
||||||
|
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'
|
||||||
|
|
||||||
|
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')}
|
||||||
|
/>
|
||||||
|
<Dot />
|
||||||
|
<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>
|
||||||
|
<div className='flex flex-wrap'>
|
||||||
|
{keywords.map(keyword => (
|
||||||
|
<Tag key={keyword} text={keyword} className='mr-2' />
|
||||||
|
))}
|
||||||
|
</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)
|
||||||
@ -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)
|
||||||
Loading…
Reference in New Issue