mirror of
https://github.com/langgenius/dify.git
synced 2026-04-27 19:27:23 +08:00
Merge branch 'feat/parent-child-retrieval' of https://github.com/langgenius/dify into feat/parent-child-retrieval
This commit is contained in:
commit
9dce37bb24
@ -82,8 +82,8 @@ const FileTypeIcon = ({
|
|||||||
size = 'sm',
|
size = 'sm',
|
||||||
className,
|
className,
|
||||||
}: FileTypeIconProps) => {
|
}: FileTypeIconProps) => {
|
||||||
const Icon = FILE_TYPE_ICON_MAP[type].component || FileAppearanceTypeEnum.custom
|
const Icon = FILE_TYPE_ICON_MAP[type]?.component || FileAppearanceTypeEnum.custom
|
||||||
const color = FILE_TYPE_ICON_MAP[type].color
|
const color = FILE_TYPE_ICON_MAP[type]?.color || FILE_TYPE_ICON_MAP.custom.color
|
||||||
|
|
||||||
return <Icon className={cn('shrink-0', SizeMap[size], color, className)} />
|
return <Icon className={cn('shrink-0', SizeMap[size], color, className)} />
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,9 +7,10 @@ import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/co
|
|||||||
|
|
||||||
type IActionButtonsProps = {
|
type IActionButtonsProps = {
|
||||||
handleCancel: () => void
|
handleCancel: () => void
|
||||||
handleSave: (needRegenerate: boolean) => void
|
handleSave: () => void
|
||||||
loading: boolean
|
loading: boolean
|
||||||
actionType?: 'edit' | 'add'
|
actionType?: 'edit' | 'add'
|
||||||
|
handleRegeneration?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActionButtons: FC<IActionButtonsProps> = ({
|
const ActionButtons: FC<IActionButtonsProps> = ({
|
||||||
@ -17,6 +18,7 @@ const ActionButtons: FC<IActionButtonsProps> = ({
|
|||||||
handleSave,
|
handleSave,
|
||||||
loading,
|
loading,
|
||||||
actionType = 'edit',
|
actionType = 'edit',
|
||||||
|
handleRegeneration,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [mode, parentMode] = useDocumentContext(s => [s.mode, s.parentMode])
|
const [mode, parentMode] = useDocumentContext(s => [s.mode, s.parentMode])
|
||||||
@ -30,7 +32,7 @@ const ActionButtons: FC<IActionButtonsProps> = ({
|
|||||||
if (loading)
|
if (loading)
|
||||||
return
|
return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
handleSave(false)
|
handleSave()
|
||||||
}
|
}
|
||||||
, { exactMatch: true, useCapture: true })
|
, { exactMatch: true, useCapture: true })
|
||||||
|
|
||||||
@ -50,18 +52,18 @@ const ActionButtons: FC<IActionButtonsProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
{(isParentChildParagraphMode && actionType === 'edit')
|
{(isParentChildParagraphMode && actionType === 'edit')
|
||||||
? <Button
|
? <Button
|
||||||
onClick={handleSave.bind(null, true)}
|
onClick={handleRegeneration}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<span className='text-components-button-secondary-text system-sm-medium'>
|
<span className='text-components-button-secondary-text system-sm-medium'>
|
||||||
{t('datasetDocuments.segment.saveAndRegenerate')}
|
{t('common.operation.saveAndRegenerate')}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
<Button
|
<Button
|
||||||
variant='primary'
|
variant='primary'
|
||||||
onClick={handleSave.bind(null, false)}
|
onClick={handleSave}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<div className='flex items-center gap-x-1'>
|
<div className='flex items-center gap-x-1'>
|
||||||
|
|||||||
@ -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)
|
||||||
@ -42,21 +42,22 @@ type SegmentListContextValue = {
|
|||||||
|
|
||||||
const SegmentListContext = createContext({
|
const SegmentListContext = createContext({
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
toggleCollapsed: () => {},
|
toggleCollapsed: () => { },
|
||||||
fullScreen: false,
|
fullScreen: false,
|
||||||
toggleFullScreen: () => {},
|
toggleFullScreen: () => { },
|
||||||
})
|
})
|
||||||
|
|
||||||
export const useSegmentListContext = (selector: (value: SegmentListContextValue) => any) => {
|
export const useSegmentListContext = (selector: (value: SegmentListContextValue) => any) => {
|
||||||
return useContextSelector(SegmentListContext, selector)
|
return useContextSelector(SegmentListContext, selector)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SegmentIndexTag: FC<{ positionId?: string | number; label?: string; className?: string }> = React.memo(({ positionId, label, className }) => {
|
export const SegmentIndexTag: FC<{ positionId?: string | number; label?: string; className?: string; isParentChildRetrieval?: boolean }> = React.memo(({ positionId, label, className, isParentChildRetrieval }) => {
|
||||||
|
const prefix = `${isParentChildRetrieval ? 'Parent-' : ''}Chunk`
|
||||||
const localPositionId = useMemo(() => {
|
const localPositionId = useMemo(() => {
|
||||||
const positionIdStr = String(positionId)
|
const positionIdStr = String(positionId)
|
||||||
if (positionIdStr.length >= 3)
|
if (positionIdStr.length >= 3)
|
||||||
return `Chunk-${positionId}`
|
return `${prefix}-${positionId}`
|
||||||
return `Chunk-${positionIdStr.padStart(2, '0')}`
|
return `${prefix}-${positionIdStr.padStart(2, '0')}`
|
||||||
}, [positionId])
|
}, [positionId])
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center', className)}>
|
<div className={cn('flex items-center', className)}>
|
||||||
@ -76,7 +77,6 @@ type ICompletedProps = {
|
|||||||
onNewSegmentModalChange: (state: boolean) => void
|
onNewSegmentModalChange: (state: boolean) => void
|
||||||
importStatus: ProcessStatus | string | undefined
|
importStatus: ProcessStatus | string | undefined
|
||||||
archived?: boolean
|
archived?: boolean
|
||||||
// data: Array<{}> // all/part segments
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Embedding done, show list of all segments
|
* Embedding done, show list of all segments
|
||||||
@ -180,7 +180,7 @@ const Completed: FC<ICompletedProps> = ({
|
|||||||
setSegments([])
|
setSegments([])
|
||||||
setSelectedSegmentIds([])
|
setSelectedSegmentIds([])
|
||||||
invalidSegmentList()
|
invalidSegmentList()
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onClickCard = (detail: SegmentDetailModel, isEditMode = false) => {
|
const onClickCard = (detail: SegmentDetailModel, isEditMode = false) => {
|
||||||
@ -211,7 +211,7 @@ const Completed: FC<ICompletedProps> = ({
|
|||||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [datasetId, documentId, selectedSegmentIds, segments])
|
}, [datasetId, documentId, selectedSegmentIds, segments])
|
||||||
|
|
||||||
const { mutateAsync: deleteSegment } = useDeleteSegment()
|
const { mutateAsync: deleteSegment } = useDeleteSegment()
|
||||||
@ -227,7 +227,7 @@ const Completed: FC<ICompletedProps> = ({
|
|||||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [datasetId, documentId, selectedSegmentIds])
|
}, [datasetId, documentId, selectedSegmentIds])
|
||||||
|
|
||||||
const onCancelBatchOperation = useCallback(() => {
|
const onCancelBatchOperation = useCallback(() => {
|
||||||
@ -247,7 +247,7 @@ const Completed: FC<ICompletedProps> = ({
|
|||||||
question: string,
|
question: string,
|
||||||
answer: string,
|
answer: string,
|
||||||
keywords: string[],
|
keywords: string[],
|
||||||
needRegenerate: boolean,
|
needRegenerate = false,
|
||||||
) => {
|
) => {
|
||||||
const params: SegmentUpdater = { content: '' }
|
const params: SegmentUpdater = { content: '' }
|
||||||
if (docForm === 'qa_model') {
|
if (docForm === 'qa_model') {
|
||||||
@ -290,9 +290,10 @@ const Completed: FC<ICompletedProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setSegments([...segments])
|
setSegments([...segments])
|
||||||
|
eventEmitter?.emit('update-segment-success')
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
eventEmitter?.emit('')
|
eventEmitter?.emit('update-segment-done')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,7 +338,7 @@ const Completed: FC<ICompletedProps> = ({
|
|||||||
resetList()
|
resetList()
|
||||||
currentPage !== totalPages && setCurrentPage(totalPages)
|
currentPage !== totalPages && setCurrentPage(totalPages)
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [segmentListData, limit, currentPage])
|
}, [segmentListData, limit, currentPage])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -388,7 +389,7 @@ const Completed: FC<ICompletedProps> = ({
|
|||||||
/>
|
/>
|
||||||
<ChildSegmentList
|
<ChildSegmentList
|
||||||
childChunks={childSegments}
|
childChunks={childSegments}
|
||||||
handleInputChange={() => {}}
|
handleInputChange={() => { }}
|
||||||
enabled={!archived}
|
enabled={!archived}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -443,14 +444,14 @@ const Completed: FC<ICompletedProps> = ({
|
|||||||
</FullScreenDrawer>
|
</FullScreenDrawer>
|
||||||
{/* Batch Action Buttons */}
|
{/* Batch Action Buttons */}
|
||||||
{selectedSegmentIds.length > 0
|
{selectedSegmentIds.length > 0
|
||||||
&& <BatchAction
|
&& <BatchAction
|
||||||
className='absolute left-0 bottom-16 z-20'
|
className='absolute left-0 bottom-16 z-20'
|
||||||
selectedIds={selectedSegmentIds}
|
selectedIds={selectedSegmentIds}
|
||||||
onBatchEnable={onChangeSwitch.bind(null, true, '')}
|
onBatchEnable={onChangeSwitch.bind(null, true, '')}
|
||||||
onBatchDisable={onChangeSwitch.bind(null, false, '')}
|
onBatchDisable={onChangeSwitch.bind(null, false, '')}
|
||||||
onBatchDelete={onDelete.bind(null, '')}
|
onBatchDelete={onDelete.bind(null, '')}
|
||||||
onCancel={onCancelBatchOperation}
|
onCancel={onCancelBatchOperation}
|
||||||
/>}
|
/>}
|
||||||
</SegmentListContext.Provider>
|
</SegmentListContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import React, { type FC, useCallback, useMemo, useState } from 'react'
|
import React, { type FC, useCallback, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RiArrowRightUpLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react'
|
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
|
||||||
import { StatusItem } from '../../list'
|
import { StatusItem } from '../../list'
|
||||||
import DocumentFileIcon from '../../../common/document-file-icon'
|
|
||||||
import { useDocumentContext } from '../index'
|
import { useDocumentContext } from '../index'
|
||||||
import ChildSegmentList from './child-segment-list'
|
import ChildSegmentList from './child-segment-list'
|
||||||
|
import Tag from './common/tag'
|
||||||
|
import Dot from './common/dot'
|
||||||
import { SegmentIndexTag, useSegmentListContext } from '.'
|
import { SegmentIndexTag, useSegmentListContext } from '.'
|
||||||
import type { SegmentDetailModel } from '@/models/datasets'
|
import type { SegmentDetailModel } from '@/models/datasets'
|
||||||
import Indicator from '@/app/components/header/indicator'
|
import Indicator from '@/app/components/header/indicator'
|
||||||
@ -17,74 +18,13 @@ import Badge from '@/app/components/base/badge'
|
|||||||
import { isAfter } from '@/utils/time'
|
import { isAfter } from '@/utils/time'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
|
||||||
const Dot = React.memo(() => {
|
|
||||||
return (
|
|
||||||
<div className='text-text-quaternary text-xs font-medium'>·</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
Dot.displayName = 'Dot'
|
|
||||||
|
|
||||||
const ProgressBar: FC<{ percent: number; loading: boolean }> = React.memo(({ percent, loading }) => {
|
|
||||||
return (
|
|
||||||
<div className=''>
|
|
||||||
<div className=''>
|
|
||||||
<div
|
|
||||||
className=''
|
|
||||||
style={{ width: `${loading ? 0 : (Math.min(percent, 1) * 100).toFixed(2)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className=''>{loading ? null : percent.toFixed(2)}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
ProgressBar.displayName = 'ProgressBar'
|
|
||||||
|
|
||||||
type DocumentTitleProps = {
|
|
||||||
name: string
|
|
||||||
extension?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const DocumentTitle: FC<DocumentTitleProps> = React.memo(({ extension, name }) => {
|
|
||||||
return (
|
|
||||||
<div className=''>
|
|
||||||
<DocumentFileIcon name={name} extension={extension} size={'sm'} />
|
|
||||||
<span className=''>{name || '--'}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
DocumentTitle.displayName = 'DocumentTitle'
|
|
||||||
|
|
||||||
const Tag = React.memo(({ text }: { text: string }) => {
|
|
||||||
return (
|
|
||||||
<div className='inline-flex items-center gap-x-0.5'>
|
|
||||||
<span className='text-text-quaternary text-xs font-medium'>#</span>
|
|
||||||
<span className='text-text-tertiary text-xs'>{text}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
Tag.displayName = 'Tag'
|
|
||||||
|
|
||||||
export type UsageScene = 'doc' | 'hitTesting'
|
|
||||||
|
|
||||||
type ISegmentCardProps = {
|
type ISegmentCardProps = {
|
||||||
loading: boolean
|
loading: boolean
|
||||||
detail?: SegmentDetailModel & { document?: { name: string } }
|
detail?: SegmentDetailModel & { document?: { name: string } }
|
||||||
contentExternal?: string
|
|
||||||
refSource?: {
|
|
||||||
title: string
|
|
||||||
uri: string
|
|
||||||
}
|
|
||||||
isExternal?: boolean
|
|
||||||
score?: number
|
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
onChangeSwitch?: (enabled: boolean, segId?: string) => Promise<void>
|
onChangeSwitch?: (enabled: boolean, segId?: string) => Promise<void>
|
||||||
onDelete?: (segId: string) => Promise<void>
|
onDelete?: (segId: string) => Promise<void>
|
||||||
onClickEdit?: () => void
|
onClickEdit?: () => void
|
||||||
scene?: UsageScene
|
|
||||||
className?: string
|
className?: string
|
||||||
archived?: boolean
|
archived?: boolean
|
||||||
embeddingAvailable?: boolean
|
embeddingAvailable?: boolean
|
||||||
@ -92,16 +32,11 @@ type ISegmentCardProps = {
|
|||||||
|
|
||||||
const SegmentCard: FC<ISegmentCardProps> = ({
|
const SegmentCard: FC<ISegmentCardProps> = ({
|
||||||
detail = {},
|
detail = {},
|
||||||
contentExternal,
|
|
||||||
isExternal,
|
|
||||||
refSource,
|
|
||||||
score,
|
|
||||||
onClick,
|
onClick,
|
||||||
onChangeSwitch,
|
onChangeSwitch,
|
||||||
onDelete,
|
onDelete,
|
||||||
onClickEdit,
|
onClickEdit,
|
||||||
loading = true,
|
loading = true,
|
||||||
scene = 'doc',
|
|
||||||
className = '',
|
className = '',
|
||||||
archived,
|
archived,
|
||||||
embeddingAvailable,
|
embeddingAvailable,
|
||||||
@ -124,10 +59,6 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
|||||||
const isCollapsed = useSegmentListContext(s => s.isCollapsed)
|
const isCollapsed = useSegmentListContext(s => s.isCollapsed)
|
||||||
const [mode, parentMode] = useDocumentContext(s => [s.mode, s.parentMode])
|
const [mode, parentMode] = useDocumentContext(s => [s.mode, s.parentMode])
|
||||||
|
|
||||||
const isDocScene = useMemo(() => {
|
|
||||||
return scene === 'doc'
|
|
||||||
}, [scene])
|
|
||||||
|
|
||||||
const isGeneralMode = useMemo(() => {
|
const isGeneralMode = useMemo(() => {
|
||||||
return mode === 'custom'
|
return mode === 'custom'
|
||||||
}, [mode])
|
}, [mode])
|
||||||
@ -147,9 +78,9 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
|||||||
}, [enabled])
|
}, [enabled])
|
||||||
|
|
||||||
const handleClickCard = useCallback(() => {
|
const handleClickCard = useCallback(() => {
|
||||||
if (!isFullDocMode)
|
if (mode !== 'hierarchical' || parentMode !== 'full-doc')
|
||||||
onClick?.()
|
onClick?.()
|
||||||
}, [isFullDocMode, onClick])
|
}, [mode, parentMode, onClick])
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (answer) {
|
if (answer) {
|
||||||
@ -166,10 +97,6 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentExternal)
|
|
||||||
return contentExternal
|
|
||||||
|
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,96 +106,85 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
|||||||
onClick={handleClickCard}
|
onClick={handleClickCard}
|
||||||
>
|
>
|
||||||
<div className='h-5 relative flex items-center justify-between'>
|
<div className='h-5 relative flex items-center justify-between'>
|
||||||
{isDocScene
|
<>
|
||||||
? <>
|
<div className='flex items-center gap-x-2'>
|
||||||
<div className='flex items-center gap-x-2'>
|
<SegmentIndexTag positionId={position} className={textOpacity} />
|
||||||
<SegmentIndexTag positionId={position} className={textOpacity} />
|
<Dot />
|
||||||
<Dot />
|
<div className={cn('text-text-tertiary system-xs-medium', textOpacity)}>{`${formatNumber(word_count)} Characters`}</div>
|
||||||
<div className={cn('text-text-tertiary system-xs-medium', textOpacity)}>{`${formatNumber(word_count)} Characters`}</div>
|
<Dot />
|
||||||
<Dot />
|
<div className={cn('text-text-tertiary system-xs-medium', textOpacity)}>{`${formatNumber(hit_count)} Retrieval Count`}</div>
|
||||||
<div className={cn('text-text-tertiary system-xs-medium', textOpacity)}>{`${formatNumber(hit_count)} Retrieval Count`}</div>
|
{chunkEdited && (
|
||||||
{chunkEdited && (
|
<>
|
||||||
<>
|
<Dot />
|
||||||
<Dot />
|
<Badge text='edited' uppercase className={textOpacity} />
|
||||||
<Badge text='edited' uppercase className={textOpacity} />
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
{!isFullDocMode
|
||||||
{!isFullDocMode
|
? <div className='flex items-center'>
|
||||||
? <div className='flex items-center'>
|
{loading
|
||||||
{loading
|
? (
|
||||||
? (
|
<Indicator color="gray" />
|
||||||
<Indicator color="gray" />
|
)
|
||||||
)
|
: (
|
||||||
: (
|
<>
|
||||||
<>
|
<StatusItem status={enabled ? 'enabled' : 'disabled'} reverse textCls="text-text-tertiary system-xs-regular" />
|
||||||
<StatusItem status={enabled ? 'enabled' : 'disabled'} reverse textCls="text-text-tertiary system-xs-regular" />
|
{embeddingAvailable && (
|
||||||
{embeddingAvailable && (
|
<div className="absolute -top-2 -right-2.5 z-20 hidden group-hover/card:flex items-center gap-x-0.5 p-1
|
||||||
<div className="absolute -top-2 -right-2.5 z-20 hidden group-hover/card:flex items-center gap-x-0.5 p-1
|
rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-[5px]">
|
||||||
rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-[5px]">
|
{!archived && (
|
||||||
{!archived && (
|
<>
|
||||||
<>
|
<Tooltip
|
||||||
<Tooltip
|
popupContent='Edit'
|
||||||
popupContent='Edit'
|
popupClassName='text-text-secondary system-xs-medium'
|
||||||
popupClassName='text-text-secondary system-xs-medium'
|
>
|
||||||
>
|
<div
|
||||||
<div
|
className='shrink-0 w-6 h-6 flex items-center justify-center rounded-lg hover:bg-state-base-hover cursor-pointer'
|
||||||
className='shrink-0 w-6 h-6 flex items-center justify-center rounded-lg hover:bg-state-base-hover cursor-pointer'
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
e.stopPropagation()
|
||||||
e.stopPropagation()
|
onClickEdit?.()
|
||||||
onClickEdit?.()
|
}}>
|
||||||
}}>
|
<RiEditLine className='w-4 h-4 text-text-tertiary' />
|
||||||
<RiEditLine className='w-4 h-4 text-text-tertiary' />
|
</div>
|
||||||
</div>
|
</Tooltip>
|
||||||
</Tooltip>
|
<Tooltip
|
||||||
<Tooltip
|
popupContent='Delete'
|
||||||
popupContent='Delete'
|
popupClassName='text-text-secondary system-xs-medium'
|
||||||
popupClassName='text-text-secondary system-xs-medium'
|
>
|
||||||
>
|
<div className='shrink-0 w-6 h-6 flex items-center justify-center rounded-lg hover:bg-state-destructive-hover cursor-pointer group/delete'
|
||||||
<div className='shrink-0 w-6 h-6 flex items-center justify-center rounded-lg hover:bg-state-destructive-hover cursor-pointer group/delete'
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
e.stopPropagation()
|
||||||
e.stopPropagation()
|
setShowModal(true)
|
||||||
setShowModal(true)
|
}
|
||||||
}
|
}>
|
||||||
}>
|
<RiDeleteBinLine className='w-4 h-4 text-text-tertiary group-hover/delete:text-text-destructive' />
|
||||||
<RiDeleteBinLine className='w-4 h-4 text-text-tertiary group-hover/delete:text-text-destructive' />
|
</div>
|
||||||
</div>
|
</Tooltip>
|
||||||
</Tooltip>
|
<Divider type="vertical" className="h-3.5 bg-divider-regular" />
|
||||||
<Divider type="vertical" className="h-3.5 bg-divider-regular" />
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
<div
|
||||||
<div
|
onClick={(e: React.MouseEvent<HTMLDivElement, MouseEvent>) =>
|
||||||
onClick={(e: React.MouseEvent<HTMLDivElement, MouseEvent>) =>
|
e.stopPropagation()
|
||||||
e.stopPropagation()
|
}
|
||||||
}
|
className="flex items-center"
|
||||||
className="flex items-center"
|
>
|
||||||
>
|
<Switch
|
||||||
<Switch
|
size='md'
|
||||||
size='md'
|
disabled={archived || detail.status !== 'completed'}
|
||||||
disabled={archived || detail.status !== 'completed'}
|
defaultValue={enabled}
|
||||||
defaultValue={enabled}
|
onChange={async (val) => {
|
||||||
onChange={async (val) => {
|
await onChangeSwitch?.(val, id)
|
||||||
await onChangeSwitch?.(val, id)
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
</>
|
||||||
</div>
|
)}
|
||||||
: null}
|
</div>
|
||||||
</>
|
: null}
|
||||||
: (
|
</>
|
||||||
score !== null
|
|
||||||
? (
|
|
||||||
<div className=''>
|
|
||||||
<div className='' />
|
|
||||||
<ProgressBar percent={score ?? 0} loading={loading} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{loading
|
{loading
|
||||||
? (
|
? (
|
||||||
@ -277,50 +193,32 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
isDocScene
|
<>
|
||||||
? <>
|
<div className={cn('text-text-secondary body-md-regular -tracking-[0.07px] mt-0.5',
|
||||||
<div className={cn('text-text-secondary body-md-regular -tracking-[0.07px] mt-0.5',
|
textOpacity,
|
||||||
textOpacity,
|
isCollapsed ? 'line-clamp-2' : 'line-clamp-20',
|
||||||
isCollapsed ? 'line-clamp-2' : 'line-clamp-20',
|
)}>
|
||||||
)}>
|
{renderContent()}
|
||||||
{renderContent()}
|
</div>
|
||||||
</div>
|
{isGeneralMode && <div className={cn('flex items-center gap-x-2 py-1.5', textOpacity)}>
|
||||||
{isGeneralMode && <div className={cn('flex items-center gap-x-2 py-1.5', textOpacity)}>
|
{keywords?.map(keyword => <Tag key={keyword} text={keyword} />)}
|
||||||
{keywords?.map(keyword => <Tag key={keyword} text={keyword} />)}
|
</div>}
|
||||||
</div>}
|
{
|
||||||
{
|
isFullDocMode
|
||||||
isFullDocMode
|
? <button className='mt-0.5 mb-2 text-text-accent system-xs-semibold-uppercase' onClick={() => onClick?.()}>VIEW MORE</button>
|
||||||
? <button className='mt-0.5 mb-2 text-text-accent system-xs-semibold-uppercase' onClick={() => onClick?.()}>VIEW MORE</button>
|
: null
|
||||||
: null
|
}
|
||||||
}
|
{
|
||||||
{
|
child_chunks.length > 0
|
||||||
child_chunks.length > 0
|
&& <ChildSegmentList
|
||||||
&& <ChildSegmentList
|
childChunks={child_chunks}
|
||||||
childChunks={child_chunks}
|
handleInputChange={() => {}}
|
||||||
handleInputChange={() => {}}
|
enabled={enabled}
|
||||||
enabled={enabled}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
</>
|
||||||
</>
|
)
|
||||||
: <>
|
}
|
||||||
<div className='text-text-secondary body-md-regular -tracking-[0.07px]'>
|
|
||||||
{renderContent()}
|
|
||||||
</div>
|
|
||||||
<div className=''>
|
|
||||||
<Divider />
|
|
||||||
<div className="relative flex items-center w-full pb-1">
|
|
||||||
<DocumentTitle
|
|
||||||
name={detail?.document?.name || refSource?.title || ''}
|
|
||||||
extension={(detail?.document?.name || refSource?.title || '').split('.').pop() || 'txt'}
|
|
||||||
/>
|
|
||||||
<div className=''>
|
|
||||||
{isExternal ? t('datasetHitTesting.viewDetail') : t('datasetHitTesting.viewChart')}
|
|
||||||
<RiArrowRightUpLine className="w-3.5 h-3.5 ml-1" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{showModal
|
{showModal
|
||||||
&& <Confirm
|
&& <Confirm
|
||||||
isShow={showModal}
|
isShow={showModal}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { useDocumentContext } from '../index'
|
|||||||
import ActionButtons from './common/action-buttons'
|
import ActionButtons from './common/action-buttons'
|
||||||
import ChunkContent from './common/chunk-content'
|
import ChunkContent from './common/chunk-content'
|
||||||
import Keywords from './common/keywords'
|
import Keywords from './common/keywords'
|
||||||
|
import RegenerationModal from './common/regeneration-modal'
|
||||||
import { SegmentIndexTag, useSegmentListContext } from './index'
|
import { SegmentIndexTag, useSegmentListContext } from './index'
|
||||||
import type { SegmentDetailModel } from '@/models/datasets'
|
import type { SegmentDetailModel } from '@/models/datasets'
|
||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
@ -17,7 +18,7 @@ import Divider from '@/app/components/base/divider'
|
|||||||
|
|
||||||
type ISegmentDetailProps = {
|
type ISegmentDetailProps = {
|
||||||
segInfo?: Partial<SegmentDetailModel> & { id: string }
|
segInfo?: Partial<SegmentDetailModel> & { id: string }
|
||||||
onUpdate: (segmentId: string, q: string, a: string, k: string[], needRegenerate: boolean) => void
|
onUpdate: (segmentId: string, q: string, a: string, k: string[], needRegenerate?: boolean) => void
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
isEditMode?: boolean
|
isEditMode?: boolean
|
||||||
docForm: string
|
docForm: string
|
||||||
@ -39,13 +40,14 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
|
|||||||
const [keywords, setKeywords] = useState<string[]>(segInfo?.keywords || [])
|
const [keywords, setKeywords] = useState<string[]>(segInfo?.keywords || [])
|
||||||
const { eventEmitter } = useEventEmitterContextContext()
|
const { eventEmitter } = useEventEmitterContextContext()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [showRegenerationModal, setShowRegenerationModal] = useState(false)
|
||||||
const [fullScreen, toggleFullScreen] = useSegmentListContext(s => [s.fullScreen, s.toggleFullScreen])
|
const [fullScreen, toggleFullScreen] = useSegmentListContext(s => [s.fullScreen, s.toggleFullScreen])
|
||||||
const [mode] = useDocumentContext(s => s.mode)
|
const mode = useDocumentContext(s => s.mode)
|
||||||
|
|
||||||
eventEmitter?.useSubscription((v) => {
|
eventEmitter?.useSubscription((v) => {
|
||||||
if (v === 'update-segment')
|
if (v === 'update-segment')
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
else
|
if (v === 'update-segment-done')
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -56,8 +58,20 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
|
|||||||
setKeywords(segInfo?.keywords || [])
|
setKeywords(segInfo?.keywords || [])
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = (needRegenerate = false) => {
|
const handleSave = () => {
|
||||||
onUpdate(segInfo?.id || '', question, answer, keywords, needRegenerate)
|
onUpdate(segInfo?.id || '', question, answer, keywords)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRegeneration = () => {
|
||||||
|
setShowRegenerationModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancelRegeneration = () => {
|
||||||
|
setShowRegenerationModal(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onConfirmRegeneration = () => {
|
||||||
|
onUpdate(segInfo?.id || '', question, answer, keywords, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -74,7 +88,12 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
|
|||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
{isEditMode && fullScreen && (
|
{isEditMode && fullScreen && (
|
||||||
<>
|
<>
|
||||||
<ActionButtons handleCancel={handleCancel} handleSave={handleSave} loading={loading} />
|
<ActionButtons
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
handleRegeneration={handleRegeneration}
|
||||||
|
handleSave={handleSave}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
<Divider type='vertical' className='h-3.5 bg-divider-regular ml-4 mr-2' />
|
<Divider type='vertical' className='h-3.5 bg-divider-regular ml-4 mr-2' />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -108,9 +127,19 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
{isEditMode && !fullScreen && (
|
{isEditMode && !fullScreen && (
|
||||||
<div className='flex items-center justify-end p-4 pt-3 border-t-[1px] border-t-divider-subtle'>
|
<div className='flex items-center justify-end p-4 pt-3 border-t-[1px] border-t-divider-subtle'>
|
||||||
<ActionButtons handleCancel={handleCancel} handleSave={handleSave} loading={loading} />
|
<ActionButtons
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
handleRegeneration={handleRegeneration}
|
||||||
|
handleSave={handleSave}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<RegenerationModal
|
||||||
|
isShow={showRegenerationModal}
|
||||||
|
onConfirm={onConfirmRegeneration}
|
||||||
|
onCancel={onCancelRegeneration}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
export const generalResultData = [
|
import type { HitTesting } from '@/models/datasets'
|
||||||
|
|
||||||
|
export const generalResultData: HitTesting[] = [
|
||||||
{
|
{
|
||||||
segment: {
|
segment: {
|
||||||
id: 'b621b153-f8a7-4e85-bd3d-07feaf61bd9e',
|
id: 'b621b153-f8a7-4e85-bd3d-07feaf61bd9e',
|
||||||
@ -40,7 +42,13 @@ export const generalResultData = [
|
|||||||
doc_type: null,
|
doc_type: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
child_chunks: null,
|
child_chunks: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
score: 0.8771945,
|
||||||
|
content: 'It is quite natural for academics who are continuously told to “publish or perish” to want to always create something from scratch that is their own fresh creation.',
|
||||||
|
},
|
||||||
|
],
|
||||||
score: 0.8771945,
|
score: 0.8771945,
|
||||||
tsne_position: null,
|
tsne_position: null,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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)
|
||||||
@ -2,9 +2,19 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { RiArrowDownSLine, RiArrowRightSLine, RiArrowRightUpLine } from '@remixicon/react'
|
||||||
|
import { useBoolean } from 'ahooks'
|
||||||
import { SegmentIndexTag } from '../../documents/detail/completed'
|
import { SegmentIndexTag } from '../../documents/detail/completed'
|
||||||
|
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 type { HitTesting } from '@/models/datasets'
|
||||||
import cn from '@/utils/classnames'
|
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'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
payload: HitTesting
|
payload: HitTesting
|
||||||
}
|
}
|
||||||
@ -13,20 +23,81 @@ const ResultItem: FC<Props> = ({
|
|||||||
payload,
|
payload,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { segment } = payload
|
const { segment, score, child_chunks } = payload
|
||||||
const { position, word_count } = segment
|
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
|
||||||
|
const [isFold, {
|
||||||
|
toggle: toggleFold,
|
||||||
|
}] = useBoolean(false)
|
||||||
|
const Icon = isFold ? RiArrowRightSLine : RiArrowDownSLine
|
||||||
|
|
||||||
|
const [isShowDetailModal, {
|
||||||
|
setTrue: showDetailModal,
|
||||||
|
setFalse: hideDetailModal,
|
||||||
|
}] = useBoolean(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='pt-3 bg-chat-bubble-bg rounded-xl hover:shadow-lg'>
|
||||||
<div className='flex justify-between items-center'>
|
{/* Meta info */}
|
||||||
|
<div className='flex justify-between items-center px-3'>
|
||||||
<div className='flex items-center space-x-2'>
|
<div className='flex items-center space-x-2'>
|
||||||
<SegmentIndexTag positionId={position} className={cn('w-fit group-hover:opacity-100')} />
|
<SegmentIndexTag
|
||||||
<div className='text-xs font-medium text-text-quaternary'>·</div>
|
isParentChildRetrieval={isParentChildRetrieval}
|
||||||
|
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')}</div>
|
<div className='system-xs-medium text-text-tertiary'>{word_count} {t('datasetDocuments.segment.characters')}</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Score */}
|
<Score value={score} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Main */}
|
||||||
|
<div className='px-3'>
|
||||||
|
<div className='line-clamp-2'>{content}</div>
|
||||||
|
{isParentChildRetrieval && (
|
||||||
|
<div>
|
||||||
|
<div className='flex items-center space-x-0.5 text-text-secondary' onClick={toggleFold}>
|
||||||
|
<Icon className={cn('w-4 h-4', isFold && 'opacity-50')} />
|
||||||
|
<div>{t('dataset.hitChunks', { num: child_chunks.length })}</div>
|
||||||
|
</div>
|
||||||
|
{child_chunks.map(item => (
|
||||||
|
<ChildChunkItem key={item.id} payload={item} isShowAll={false} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isParentChildRetrieval && keywords && keywords.length > 0 && (
|
||||||
|
<div className='flex flex-wrap'>
|
||||||
|
{keywords.map(keyword => (
|
||||||
|
<Tag key={keyword} text={keyword} className='mr-2' />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Foot */}
|
||||||
|
<div className='flex justify-between items-center h-10 pl-3 pr-2 border-t border-divider-subtle'>
|
||||||
|
<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
|
||||||
|
className='flex items-center space-x-1 cursor-pointer text-text-tertiary'
|
||||||
|
onClick={showDetailModal}
|
||||||
|
>
|
||||||
|
<div className='text-xs uppercase'>{t('dataset.open')}</div>
|
||||||
|
<RiArrowRightUpLine className='size-3.5' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isShowDetailModal && (
|
||||||
|
<ChunkDetailModal
|
||||||
|
payload={payload}
|
||||||
|
onHide={hideDetailModal}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
22
web/app/components/datasets/hit-testing/components/score.tsx
Normal file
22
web/app/components/datasets/hit-testing/components/score.tsx
Normal file
@ -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)
|
||||||
@ -12,6 +12,7 @@ import s from './style.module.css'
|
|||||||
import HitDetail from './hit-detail'
|
import HitDetail from './hit-detail'
|
||||||
import ModifyRetrievalModal from './modify-retrieval-modal'
|
import ModifyRetrievalModal from './modify-retrieval-modal'
|
||||||
import { generalResultData } from './assets/test-data'
|
import { generalResultData } from './assets/test-data'
|
||||||
|
import ResultItem from './components/result-item'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import type { ExternalKnowledgeBaseHitTestingResponse, ExternalKnowledgeBaseHitTesting as ExternalKnowledgeBaseHitTestingType, HitTestingResponse, HitTesting as HitTestingType } from '@/models/datasets'
|
import type { ExternalKnowledgeBaseHitTestingResponse, ExternalKnowledgeBaseHitTesting as ExternalKnowledgeBaseHitTestingType, HitTestingResponse, HitTesting as HitTestingType } from '@/models/datasets'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
@ -64,44 +65,26 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
|
|||||||
|
|
||||||
const total = recordsRes?.total || 0
|
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 { dataset: currentDataset } = useContext(DatasetDetailContext)
|
||||||
const isExternal = currentDataset?.provider === 'external'
|
const isExternal = currentDataset?.provider === 'external'
|
||||||
|
|
||||||
const [retrievalConfig, setRetrievalConfig] = useState(currentDataset?.retrieval_model_dict as RetrievalConfig)
|
const [retrievalConfig, setRetrievalConfig] = useState(currentDataset?.retrieval_model_dict as RetrievalConfig)
|
||||||
const [isShowModifyRetrievalModal, setIsShowModifyRetrievalModal] = useState(false)
|
const [isShowModifyRetrievalModal, setIsShowModifyRetrievalModal] = useState(false)
|
||||||
const [isShowRightPanel, { setTrue: showRightPanel, setFalse: hideRightPanel, set: setShowRightPanel }] = useBoolean(!isMobile)
|
const [isShowRightPanel, { setTrue: showRightPanel, setFalse: hideRightPanel, set: setShowRightPanel }] = useBoolean(!isMobile)
|
||||||
const renderHitResults = (results: any[], onClickCard: (record: any) => void) => (
|
const renderHitResults = (results: any[]) => (
|
||||||
<>
|
<div className='h-full flex flex-col py-3 px-4 rounded-t-2xl bg-background-body'>
|
||||||
<div className='text-gray-600 font-semibold mb-4'>{t('datasetHitTesting.hit.title')}</div>
|
<div className='shrink-0 pl-2 text-text-primary font-semibold leading-6 mb-2'>
|
||||||
<div className='overflow-auto flex-1'>
|
{t('datasetHitTesting.hit.title', { num: results.length })}
|
||||||
<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>
|
</div>
|
||||||
</>
|
<div className='grow overflow-y-auto space-y-2'>
|
||||||
|
{results.map((record, idx) => (
|
||||||
|
<ResultItem
|
||||||
|
key={idx}
|
||||||
|
payload={record}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderEmptyState = () => (
|
const renderEmptyState = () => (
|
||||||
@ -190,8 +173,8 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<FloatRightContainer panelClassname='!justify-start !overflow-y-auto' showClose isMobile={isMobile} isOpen={isShowRightPanel} onClose={hideRightPanel} footer={null}>
|
<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={cn(s.rightDiv, 'pt-3')}>
|
||||||
{renderHitResults(generalResultData, onClickCard)}
|
{renderHitResults(generalResultData)}
|
||||||
{submitLoading
|
{submitLoading
|
||||||
? <div className={s.cardWrapper}>
|
? <div className={s.cardWrapper}>
|
||||||
<SegmentCard
|
<SegmentCard
|
||||||
@ -211,9 +194,9 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
|
|||||||
return renderEmptyState()
|
return renderEmptyState()
|
||||||
|
|
||||||
if (hitResult?.records.length)
|
if (hitResult?.records.length)
|
||||||
return renderHitResults(hitResult.records, onClickCard)
|
return renderHitResults(hitResult.records)
|
||||||
|
|
||||||
return renderHitResults(externalHitResult?.records || [], onClickExternalCard)
|
return renderHitResults(externalHitResult?.records || [])
|
||||||
})()
|
})()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
@apply flex-1 h-full;
|
@apply flex-1 h-full;
|
||||||
}
|
}
|
||||||
.leftDiv {
|
.leftDiv {
|
||||||
@apply border-r border-gray-100 px-6 py-3 flex flex-col;
|
@apply px-6 py-3 flex flex-col;
|
||||||
}
|
}
|
||||||
.rightDiv {
|
.rightDiv {
|
||||||
@apply flex flex-col;
|
@apply flex flex-col;
|
||||||
|
|||||||
@ -42,6 +42,8 @@ const translation = {
|
|||||||
zoomOut: 'Zoom Out',
|
zoomOut: 'Zoom Out',
|
||||||
zoomIn: 'Zoom In',
|
zoomIn: 'Zoom In',
|
||||||
openInNewTab: 'Open in new tab',
|
openInNewTab: 'Open in new tab',
|
||||||
|
saveAndRegenerate: 'Save & Regenerate Child Chunks',
|
||||||
|
close: 'Close',
|
||||||
},
|
},
|
||||||
errorMsg: {
|
errorMsg: {
|
||||||
fieldRequired: '{{field}} is required',
|
fieldRequired: '{{field}} is required',
|
||||||
|
|||||||
@ -353,7 +353,12 @@ const translation = {
|
|||||||
delete: 'Delete this chunk ?',
|
delete: 'Delete this chunk ?',
|
||||||
chunkAdded: '1 chunk added',
|
chunkAdded: '1 chunk added',
|
||||||
viewAddedChunk: 'View',
|
viewAddedChunk: 'View',
|
||||||
saveAndRegenerate: 'Save & Regenerate Child Chunks',
|
regenerationConfirmTitle: 'Do you want to regenerate child chunks?',
|
||||||
|
regenerationConfirmMessage: 'Regenerating child chunks will overwrite the current child chunks, including edited chunks and newly added chunks. The regeneration cannot be undone.',
|
||||||
|
regeneratingTitle: 'Regenerating child chunks',
|
||||||
|
regeneratingMessage: 'This may take a moment, please wait...',
|
||||||
|
regenerationSuccessTitle: 'Regeneration completed',
|
||||||
|
regenerationSuccessMessage: 'You can close this window.',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const translation = {
|
|||||||
testing: 'Testing',
|
testing: 'Testing',
|
||||||
},
|
},
|
||||||
hit: {
|
hit: {
|
||||||
title: 'RETRIEVAL PARAGRAPHS',
|
title: '{{num}} Retrieved Chunks',
|
||||||
emptyTip: 'Retrieval Testing results will show here',
|
emptyTip: 'Retrieval Testing results will show here',
|
||||||
},
|
},
|
||||||
noRecentTip: 'No recent query results here',
|
noRecentTip: 'No recent query results here',
|
||||||
|
|||||||
@ -42,6 +42,8 @@ const translation = {
|
|||||||
zoomOut: '缩小',
|
zoomOut: '缩小',
|
||||||
zoomIn: '放大',
|
zoomIn: '放大',
|
||||||
openInNewTab: '在新标签页打开',
|
openInNewTab: '在新标签页打开',
|
||||||
|
saveAndRegenerate: '保存并重新生成子分段',
|
||||||
|
close: '关闭',
|
||||||
},
|
},
|
||||||
errorMsg: {
|
errorMsg: {
|
||||||
fieldRequired: '{{field}} 为必填项',
|
fieldRequired: '{{field}} 为必填项',
|
||||||
|
|||||||
@ -351,7 +351,12 @@ const translation = {
|
|||||||
delete: '删除这个分段?',
|
delete: '删除这个分段?',
|
||||||
chunkAdded: '新增一个分段',
|
chunkAdded: '新增一个分段',
|
||||||
viewAddedChunk: '查看',
|
viewAddedChunk: '查看',
|
||||||
saveAndRegenerate: '保存并重新生成子分段',
|
regenerationConfirmTitle: '是否需要重新生成子分段?',
|
||||||
|
regenerationConfirmMessage: '重新生成的子分段将会覆盖当前的子分段,包括编辑过的分段和新添加的分段。重新生成操作无法撤销。',
|
||||||
|
regeneratingTitle: '正在生成子分段',
|
||||||
|
regeneratingMessage: '生成子分段需要一些时间,请耐心等待...',
|
||||||
|
regenerationSuccessTitle: '子分段已重新生成',
|
||||||
|
regenerationSuccessMessage: '可以关闭窗口',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const translation = {
|
|||||||
testing: '测试',
|
testing: '测试',
|
||||||
},
|
},
|
||||||
hit: {
|
hit: {
|
||||||
title: '召回段落',
|
title: '{{num}} 个召回段落',
|
||||||
emptyTip: '召回测试结果将展示在这里',
|
emptyTip: '召回测试结果将展示在这里',
|
||||||
},
|
},
|
||||||
noRecentTip: '最近无查询结果',
|
noRecentTip: '最近无查询结果',
|
||||||
|
|||||||
@ -479,10 +479,16 @@ export type HitTestingRecord = {
|
|||||||
created_at: number
|
created_at: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HitTestingChildChunk = {
|
||||||
|
id: string
|
||||||
|
content: string
|
||||||
|
score: number
|
||||||
|
}
|
||||||
export type HitTesting = {
|
export type HitTesting = {
|
||||||
segment: Segment
|
segment: Segment
|
||||||
score: number
|
score: number
|
||||||
tsne_position: TsnePosition
|
tsne_position: TsnePosition
|
||||||
|
child_chunks?: HitTestingChildChunk[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExternalKnowledgeBaseHitTesting = {
|
export type ExternalKnowledgeBaseHitTesting = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user