dify
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.9166 9.58333L17.2505 11.25L15.5833 9.58333M17.4542 10.8333C17.4845 10.5597 17.5 10.2817 17.5 10C17.5 5.85786 14.1421 2.5 10 2.5C5.85786 2.5 2.5 5.85786 2.5 10C2.5 14.1421 5.85786 17.5 10 17.5C12.3561 17.5 14.4584 16.4136 15.8333 14.7144M10 5.83333V10L12.5 11.6667" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 466 B |
@@ -0,0 +1,6 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.9 1.75H2.68333C2.35664 1.75 2.19329 1.75 2.06851 1.81358C1.95874 1.86951 1.86951 1.95874 1.81358 2.06851C1.75 2.19329 1.75 2.35664 1.75 2.68333V4.9C1.75 5.2267 1.75 5.39005 1.81358 5.51483C1.86951 5.62459 1.95874 5.71383 2.06851 5.76975C2.19329 5.83333 2.35664 5.83333 2.68333 5.83333H4.9C5.2267 5.83333 5.39005 5.83333 5.51483 5.76975C5.62459 5.71383 5.71383 5.62459 5.76975 5.51483C5.83333 5.39005 5.83333 5.2267 5.83333 4.9V2.68333C5.83333 2.35664 5.83333 2.19329 5.76975 2.06851C5.71383 1.95874 5.62459 1.86951 5.51483 1.81358C5.39005 1.75 5.2267 1.75 4.9 1.75Z" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.3167 1.75H9.1C8.7733 1.75 8.60995 1.75 8.48517 1.81358C8.37541 1.86951 8.28617 1.95874 8.23025 2.06851C8.16667 2.19329 8.16667 2.35664 8.16667 2.68333V4.9C8.16667 5.2267 8.16667 5.39005 8.23025 5.51483C8.28617 5.62459 8.37541 5.71383 8.48517 5.76975C8.60995 5.83333 8.7733 5.83333 9.1 5.83333H11.3167C11.6434 5.83333 11.8067 5.83333 11.9315 5.76975C12.0413 5.71383 12.1305 5.62459 12.1864 5.51483C12.25 5.39005 12.25 5.2267 12.25 4.9V2.68333C12.25 2.35664 12.25 2.19329 12.1864 2.06851C12.1305 1.95874 12.0413 1.86951 11.9315 1.81358C11.8067 1.75 11.6434 1.75 11.3167 1.75Z" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.3167 8.16667H9.1C8.7733 8.16667 8.60995 8.16667 8.48517 8.23025C8.37541 8.28617 8.28617 8.37541 8.23025 8.48517C8.16667 8.60995 8.16667 8.7733 8.16667 9.1V11.3167C8.16667 11.6434 8.16667 11.8067 8.23025 11.9315C8.28617 12.0413 8.37541 12.1305 8.48517 12.1864C8.60995 12.25 8.7733 12.25 9.1 12.25H11.3167C11.6434 12.25 11.8067 12.25 11.9315 12.1864C12.0413 12.1305 12.1305 12.0413 12.1864 11.9315C12.25 11.8067 12.25 11.6434 12.25 11.3167V9.1C12.25 8.7733 12.25 8.60995 12.1864 8.48517C12.1305 8.37541 12.0413 8.28617 11.9315 8.23025C11.8067 8.16667 11.6434 8.16667 11.3167 8.16667Z" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.9 8.16667H2.68333C2.35664 8.16667 2.19329 8.16667 2.06851 8.23025C1.95874 8.28617 1.86951 8.37541 1.81358 8.48517C1.75 8.60995 1.75 8.7733 1.75 9.1V11.3167C1.75 11.6434 1.75 11.8067 1.81358 11.9315C1.86951 12.0413 1.95874 12.1305 2.06851 12.1864C2.19329 12.25 2.35664 12.25 2.68333 12.25H4.9C5.2267 12.25 5.39005 12.25 5.51483 12.1864C5.62459 12.1305 5.71383 12.0413 5.76975 11.9315C5.83333 11.8067 5.83333 11.6434 5.83333 11.3167V9.1C5.83333 8.7733 5.83333 8.60995 5.76975 8.48517C5.71383 8.37541 5.62459 8.28617 5.51483 8.23025C5.39005 8.16667 5.2267 8.16667 4.9 8.16667Z" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4151_6854)">
|
||||
<path d="M4.37484 2.62484C4.37484 1.81942 5.02776 1.1665 5.83317 1.1665C6.63859 1.1665 7.2915 1.81942 7.2915 2.62484V3.49984H7.87484C8.69023 3.49984 9.09793 3.49984 9.41953 3.63305C9.84833 3.81066 10.189 4.15134 10.3666 4.58014C10.4998 4.90174 10.4998 5.30944 10.4998 6.12484H11.3748C12.1803 6.12484 12.8332 6.77776 12.8332 7.58317C12.8332 8.38859 12.1803 9.0415 11.3748 9.0415H10.4998V10.0332C10.4998 11.0133 10.4998 11.5033 10.3091 11.8777C10.1413 12.2069 9.8736 12.4747 9.54432 12.6424C9.16997 12.8332 8.67993 12.8332 7.69984 12.8332H7.2915V11.8123C7.2915 11.0875 6.70388 10.4998 5.979 10.4998C5.25413 10.4998 4.6665 11.0875 4.6665 11.8123V12.8332H3.9665C2.98641 12.8332 2.49637 12.8332 2.12202 12.6424C1.79274 12.4747 1.52502 12.2069 1.35724 11.8777C1.1665 11.5033 1.1665 11.0133 1.1665 10.0332V9.0415H2.0415C2.84692 9.0415 3.49984 8.38859 3.49984 7.58317C3.49984 6.77776 2.84692 6.12484 2.0415 6.12484H1.1665C1.1665 5.30944 1.1665 4.90174 1.29971 4.58014C1.47733 4.15134 1.81801 3.81066 2.24681 3.63305C2.56841 3.49984 2.97611 3.49984 3.7915 3.49984H4.37484V2.62484Z" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4151_6854">
|
||||
<rect width="14" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -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 { score, content, position } = payload
|
||||
return (
|
||||
<div
|
||||
className={!isShowAll ? 'line-clamp-2 break-all' : ''}
|
||||
>
|
||||
<div className='relative top-[-2px] inline-flex items-center'>
|
||||
<div className='system-2xs-semibold-uppercase flex h-[20.5px] items-center bg-state-accent-solid px-1 text-text-primary-on-surface'>C-{position}</div>
|
||||
<Score value={score} besideChunkName />
|
||||
</div>
|
||||
<SliceContent className='bg-state-accent-hover py-0.5 text-sm font-normal text-text-secondary group-hover:bg-state-accent-hover'>{content}</SliceContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ChildChunks)
|
||||
@@ -0,0 +1,111 @@
|
||||
'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'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
|
||||
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, sign_content, keywords, document, answer } = 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'
|
||||
const labelPrefix = isParentChildRetrieval ? t('datasetDocuments.segment.parentChunk') : t('datasetDocuments.segment.chunk')
|
||||
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 items-center justify-between'>
|
||||
<div className='flex grow items-center space-x-2'>
|
||||
<SegmentIndexTag
|
||||
labelPrefix={labelPrefix}
|
||||
positionId={position}
|
||||
className={cn('w-fit group-hover:opacity-100')}
|
||||
/>
|
||||
<Dot />
|
||||
<div className='flex grow items-center space-x-1'>
|
||||
<FileIcon type={extension} size='sm' />
|
||||
<span className='w-0 grow truncate text-[13px] font-normal text-text-secondary'>{document.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Score value={score} />
|
||||
</div>
|
||||
{!answer && (
|
||||
<Markdown
|
||||
className={cn('!mt-2 !text-text-secondary', heighClassName)}
|
||||
content={sign_content || content}
|
||||
customDisallowedElements={['input']}
|
||||
/>
|
||||
)}
|
||||
{answer && (
|
||||
<div className='break-all'>
|
||||
<div className='flex gap-x-1'>
|
||||
<div className='w-4 shrink-0 text-[13px] font-medium leading-[20px] text-text-tertiary'>Q</div>
|
||||
<div className={cn('body-md-regular line-clamp-20 text-text-secondary')}>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-x-1'>
|
||||
<div className='w-4 shrink-0 text-[13px] font-medium leading-[20px] text-text-tertiary'>A</div>
|
||||
<div className={cn('body-md-regular line-clamp-20 text-text-secondary')}>
|
||||
{answer}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isParentChildRetrieval && keywords && keywords.length > 0 && (
|
||||
<div className='mt-6'>
|
||||
<div className='text-xs font-medium uppercase text-text-tertiary'>{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 pb-6 pl-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)
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import ResultItemMeta from './result-item-meta'
|
||||
import ResultItemFooter from './result-item-footer'
|
||||
import type { ExternalKnowledgeBaseHitTesting } from '@/models/datasets'
|
||||
import cn from '@/utils/classnames'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
|
||||
|
||||
const i18nPrefix = 'datasetHitTesting'
|
||||
type Props = {
|
||||
payload: ExternalKnowledgeBaseHitTesting
|
||||
positionId: number
|
||||
}
|
||||
|
||||
const ResultItemExternal: FC<Props> = ({ payload, positionId }) => {
|
||||
const { t } = useTranslation()
|
||||
const { content, title, score } = payload
|
||||
const [
|
||||
isShowDetailModal,
|
||||
{ setTrue: showDetailModal, setFalse: hideDetailModal },
|
||||
] = useBoolean(false)
|
||||
|
||||
return (
|
||||
<div className={cn('cursor-pointer rounded-xl bg-chat-bubble-bg pt-3 hover:shadow-lg')} onClick={showDetailModal}>
|
||||
{/* Meta info */}
|
||||
<ResultItemMeta className='px-3' labelPrefix={'Chunk'} positionId={positionId} wordCount={content.length} score={score} />
|
||||
|
||||
{/* Main */}
|
||||
<div className='mt-1 px-3'>
|
||||
<div className='body-md-regular line-clamp-2 break-all'>{content}</div>
|
||||
</div>
|
||||
|
||||
{/* Foot */}
|
||||
<ResultItemFooter docType={FileAppearanceTypeEnum.custom} docTitle={title} showDetailModal={showDetailModal} />
|
||||
|
||||
{isShowDetailModal && (
|
||||
<Modal
|
||||
title={t(`${i18nPrefix}.chunkDetail`)}
|
||||
className={'!min-w-[800px]'}
|
||||
closable
|
||||
onClose={hideDetailModal}
|
||||
isShow={isShowDetailModal}
|
||||
>
|
||||
<div className='mt-4 flex-1'>
|
||||
<ResultItemMeta labelPrefix={'Chunk'} positionId={positionId} wordCount={content.length} score={score} />
|
||||
<div className={cn('body-md-regular mt-2 break-all text-text-secondary', 'h-[min(539px,_80vh)] overflow-y-auto')}>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ResultItemExternal)
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowRightUpLine } from '@remixicon/react'
|
||||
import FileIcon from '@/app/components/base/file-uploader/file-type-icon'
|
||||
import type { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
|
||||
|
||||
type Props = {
|
||||
docType: FileAppearanceTypeEnum
|
||||
docTitle: string
|
||||
showDetailModal: () => void
|
||||
}
|
||||
const i18nPrefix = 'datasetHitTesting'
|
||||
|
||||
const ResultItemFooter: FC<Props> = ({
|
||||
docType,
|
||||
docTitle,
|
||||
showDetailModal,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="mt-3 flex h-10 items-center justify-between border-t border-divider-subtle pl-3 pr-2">
|
||||
<div className="flex grow items-center space-x-1">
|
||||
<FileIcon type={docType} size="sm" />
|
||||
<span className="w-0 grow truncate text-[13px] font-normal text-text-secondary">
|
||||
{docTitle}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex cursor-pointer items-center space-x-1 text-text-tertiary"
|
||||
onClick={showDetailModal}
|
||||
>
|
||||
<div className="text-xs uppercase">{t(`${i18nPrefix}.open`)}</div>
|
||||
<RiArrowRightUpLine className="size-3.5" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ResultItemFooter)
|
||||
@@ -0,0 +1,45 @@
|
||||
'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 cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
labelPrefix: string
|
||||
positionId: number
|
||||
wordCount: number
|
||||
score: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
const ResultItemMeta: FC<Props> = ({
|
||||
labelPrefix,
|
||||
positionId,
|
||||
wordCount,
|
||||
score,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center justify-between', className)}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<SegmentIndexTag
|
||||
labelPrefix={labelPrefix}
|
||||
positionId={positionId}
|
||||
className={cn('w-fit group-hover:opacity-100')}
|
||||
/>
|
||||
<Dot />
|
||||
<div className="system-xs-medium text-text-tertiary">
|
||||
{wordCount} {t('datasetDocuments.segment.characters', { count: wordCount })}
|
||||
</div>
|
||||
</div>
|
||||
<Score value={score} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ResultItemMeta)
|
||||
@@ -0,0 +1,100 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import ChildChunkItem from './child-chunks-item'
|
||||
import ChunkDetailModal from './chunk-detail-modal'
|
||||
import ResultItemMeta from './result-item-meta'
|
||||
import ResultItemFooter from './result-item-footer'
|
||||
import type { HitTesting } from '@/models/datasets'
|
||||
import cn from '@/utils/classnames'
|
||||
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'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
|
||||
const i18nPrefix = 'datasetHitTesting'
|
||||
type Props = {
|
||||
payload: HitTesting
|
||||
}
|
||||
|
||||
const ResultItem: FC<Props> = ({
|
||||
payload,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { segment, score, child_chunks } = payload
|
||||
const data = segment
|
||||
const { position, word_count, content, sign_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)
|
||||
|
||||
return (
|
||||
<div className={cn('cursor-pointer rounded-xl bg-chat-bubble-bg pt-3 hover:shadow-lg')} onClick={showDetailModal}>
|
||||
{/* Meta info */}
|
||||
<ResultItemMeta className='px-3' labelPrefix={`${isParentChildRetrieval ? 'Parent-' : ''}Chunk`} positionId={position} wordCount={word_count} score={score} />
|
||||
|
||||
{/* Main */}
|
||||
<div className='mt-1 px-3'>
|
||||
<Markdown
|
||||
className='line-clamp-2'
|
||||
content={sign_content || content}
|
||||
customDisallowedElements={['input']}
|
||||
/>
|
||||
{isParentChildRetrieval && (
|
||||
<div className='mt-1'>
|
||||
<div
|
||||
className={cn('inline-flex h-6 cursor-pointer select-none items-center space-x-0.5 rounded-lg text-text-secondary', isFold && 'bg-workflow-process-bg pl-1')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleFold()
|
||||
}}
|
||||
>
|
||||
<Icon className={cn('h-4 w-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] border-l-[2px] border-text-accent-secondary pl-[7px]'>
|
||||
<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 */}
|
||||
<ResultItemFooter docType={fileType} docTitle={document.name} showDetailModal={showDetailModal} />
|
||||
|
||||
{
|
||||
isShowDetailModal && (
|
||||
<ChunkDetailModal
|
||||
payload={payload}
|
||||
onHide={hideDetailModal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default React.memo(ResultItem)
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
value: number | null
|
||||
besideChunkName?: boolean
|
||||
}
|
||||
|
||||
const Score: FC<Props> = ({
|
||||
value,
|
||||
besideChunkName,
|
||||
}) => {
|
||||
if (!value || isNaN(value))
|
||||
return null
|
||||
return (
|
||||
<div className={cn('relative items-center overflow-hidden border border-components-progress-bar-border px-[5px]',
|
||||
besideChunkName ? 'h-[20.5px] border-l-0' : 'h-[20px] rounded-md')}>
|
||||
<div className={cn('absolute left-0 top-0 h-full border-r-[1.5px] border-components-progress-brand-progress bg-util-colors-blue-brand-blue-brand-100', value === 1 && 'border-r-0')} style={{ width: `${value * 100}%` }} />
|
||||
<div className={cn('relative flex h-full items-center 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)
|
||||
216
dify/web/app/components/datasets/hit-testing/index.tsx
Normal file
216
dify/web/app/components/datasets/hit-testing/index.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import { omit } from 'lodash-es'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { RiApps2Line, RiFocus2Line, RiHistoryLine } from '@remixicon/react'
|
||||
import Textarea from './textarea'
|
||||
import s from './style.module.css'
|
||||
import ModifyRetrievalModal from './modify-retrieval-modal'
|
||||
import ResultItem from './components/result-item'
|
||||
import ResultItemExternal from './components/result-item-external'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { ExternalKnowledgeBaseHitTesting, ExternalKnowledgeBaseHitTestingResponse, HitTesting, HitTestingResponse } from '@/models/datasets'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import Pagination from '@/app/components/base/pagination'
|
||||
import FloatRightContainer from '@/app/components/base/float-right-container'
|
||||
import { fetchTestingRecords } from '@/service/datasets'
|
||||
import DatasetDetailContext from '@/context/dataset-detail'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import docStyle from '@/app/components/datasets/documents/detail/completed/style.module.css'
|
||||
import { CardSkelton } from '../documents/detail/completed/skeleton/general-list-skeleton'
|
||||
|
||||
const limit = 10
|
||||
|
||||
type Props = {
|
||||
datasetId: string
|
||||
}
|
||||
|
||||
const RecordsEmpty: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return <div className='rounded-2xl bg-workflow-process-bg p-5'>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg p-1 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]'>
|
||||
<RiHistoryLine className='h-5 w-5 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='my-2 text-[13px] font-medium leading-4 text-text-tertiary'>{t('datasetHitTesting.noRecentTip')}</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { formatTime } = useTimestamp()
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
const [hitResult, setHitResult] = useState<HitTestingResponse | undefined>() // 初始化记录为空数组
|
||||
const [externalHitResult, setExternalHitResult] = useState<ExternalKnowledgeBaseHitTestingResponse | undefined>()
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [text, setText] = useState('')
|
||||
|
||||
const [currPage, setCurrPage] = React.useState<number>(0)
|
||||
const { data: recordsRes, error, mutate: recordsMutate } = useSWR({
|
||||
action: 'fetchTestingRecords',
|
||||
datasetId,
|
||||
params: { limit, page: currPage + 1 },
|
||||
}, apiParams => fetchTestingRecords(omit(apiParams, 'action')))
|
||||
|
||||
const total = recordsRes?.total || 0
|
||||
|
||||
const { 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: HitTesting[] | ExternalKnowledgeBaseHitTesting[]) => (
|
||||
<div className='flex h-full flex-col rounded-tl-2xl bg-background-body px-4 py-3'>
|
||||
<div className='mb-2 shrink-0 pl-2 font-semibold leading-6 text-text-primary'>
|
||||
{t('datasetHitTesting.hit.title', { num: results.length })}
|
||||
</div>
|
||||
<div className='grow space-y-2 overflow-y-auto'>
|
||||
{results.map((record, idx) =>
|
||||
isExternal
|
||||
? (
|
||||
<ResultItemExternal
|
||||
key={idx}
|
||||
positionId={idx + 1}
|
||||
payload={record as ExternalKnowledgeBaseHitTesting}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<ResultItem key={idx} payload={record as HitTesting} />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderEmptyState = () => (
|
||||
<div className='flex h-full flex-col items-center justify-center rounded-tl-2xl bg-background-body px-4 py-3'>
|
||||
<div className={cn(docStyle.commonIcon, docStyle.targetIcon, '!h-14 !w-14 !bg-text-quaternary')} />
|
||||
<div className='mt-3 text-[13px] text-text-quaternary'>
|
||||
{t('datasetHitTesting.hit.emptyTip')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setShowRightPanel(!isMobile)
|
||||
}, [isMobile, setShowRightPanel])
|
||||
|
||||
return (
|
||||
<div className={s.container}>
|
||||
<div className='flex flex-col px-6 py-3'>
|
||||
<div className='mb-4 flex flex-col justify-center'>
|
||||
<h1 className='text-base font-semibold text-text-primary'>{t('datasetHitTesting.title')}</h1>
|
||||
<p className='mt-0.5 text-[13px] font-normal leading-4 text-text-tertiary'>{t('datasetHitTesting.desc')}</p>
|
||||
</div>
|
||||
<Textarea
|
||||
datasetId={datasetId}
|
||||
setHitResult={setHitResult}
|
||||
setExternalHitResult={setExternalHitResult}
|
||||
onSubmit={showRightPanel}
|
||||
onUpdateList={recordsMutate}
|
||||
loading={submitLoading}
|
||||
setLoading={setSubmitLoading}
|
||||
setText={setText}
|
||||
text={text}
|
||||
isExternal={isExternal}
|
||||
onClickRetrievalMethod={() => setIsShowModifyRetrievalModal(true)}
|
||||
retrievalConfig={retrievalConfig}
|
||||
isEconomy={currentDataset?.indexing_technique === 'economy'}
|
||||
/>
|
||||
<div className='mb-3 mt-6 text-base font-semibold text-text-primary'>{t('datasetHitTesting.records')}</div>
|
||||
{(!recordsRes && !error)
|
||||
? (
|
||||
<div className='flex-1'><Loading type='app' /></div>
|
||||
)
|
||||
: recordsRes?.data?.length
|
||||
? (
|
||||
<>
|
||||
<div className='grow overflow-y-auto'>
|
||||
<table className={'w-full border-collapse border-0 text-[13px] leading-4 text-text-secondary '}>
|
||||
<thead className='sticky top-0 h-7 text-xs font-medium uppercase leading-7 text-text-tertiary backdrop-blur-[5px]'>
|
||||
<tr>
|
||||
<td className='w-[128px] rounded-l-lg bg-background-section-burn pl-3'>{t('datasetHitTesting.table.header.source')}</td>
|
||||
<td className='bg-background-section-burn'>{t('datasetHitTesting.table.header.text')}</td>
|
||||
<td className='w-48 rounded-r-lg bg-background-section-burn pl-2'>{t('datasetHitTesting.table.header.time')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recordsRes?.data?.map((record) => {
|
||||
const SourceIcon = record.source === 'app' ? RiApps2Line : RiFocus2Line
|
||||
return <tr
|
||||
key={record.id}
|
||||
className='group h-10 cursor-pointer border-b border-divider-subtle hover:bg-background-default-hover'
|
||||
onClick={() => setText(record.content)}
|
||||
>
|
||||
<td className='w-[128px] pl-3'>
|
||||
<div className='flex items-center'>
|
||||
<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 py-2'>{record.content}</td>
|
||||
<td className='w-36 pl-2'>
|
||||
{formatTime(record.created_at, t('datasetHitTesting.dateTimeFormat') as string)}
|
||||
</td>
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{(total && total > limit)
|
||||
? <Pagination current={currPage} onChange={setCurrPage} total={total} limit={limit} />
|
||||
: null}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<RecordsEmpty />
|
||||
)}
|
||||
</div>
|
||||
<FloatRightContainer panelClassName='!justify-start !overflow-y-auto' showClose isMobile={isMobile} isOpen={isShowRightPanel} onClose={hideRightPanel} footer={null}>
|
||||
<div className='flex flex-col pt-3'>
|
||||
{/* {renderHitResults(generalResultData)} */}
|
||||
{submitLoading
|
||||
? <div className='flex h-full flex-col rounded-tl-2xl bg-background-body px-4 py-3'>
|
||||
<CardSkelton />
|
||||
</div>
|
||||
: (
|
||||
(() => {
|
||||
if (!hitResult?.records.length && !externalHitResult?.records.length)
|
||||
return renderEmptyState()
|
||||
|
||||
if (hitResult?.records.length)
|
||||
return renderHitResults(hitResult.records)
|
||||
|
||||
return renderHitResults(externalHitResult?.records || [])
|
||||
})()
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</FloatRightContainer>
|
||||
<Drawer unmount={true} isOpen={isShowModifyRetrievalModal} onClose={() => setIsShowModifyRetrievalModal(false)} footer={null} mask={isMobile} panelClassName='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'>
|
||||
<ModifyRetrievalModal
|
||||
indexMethod={currentDataset?.indexing_technique || ''}
|
||||
value={retrievalConfig}
|
||||
isShow={isShowModifyRetrievalModal}
|
||||
onHide={() => setIsShowModifyRetrievalModal(false)}
|
||||
onSave={(value) => {
|
||||
setRetrievalConfig(value)
|
||||
setIsShowModifyRetrievalModal(false)
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HitTestingPage
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import RetrievalSettings from '../external-knowledge-base/create/RetrievalSettings'
|
||||
import Button from '@/app/components/base/button'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
||||
type ModifyExternalRetrievalModalProps = {
|
||||
onClose: () => void
|
||||
onSave: (data: { top_k: number; score_threshold: number; score_threshold_enabled: boolean }) => void
|
||||
initialTopK: number
|
||||
initialScoreThreshold: number
|
||||
initialScoreThresholdEnabled: boolean
|
||||
}
|
||||
|
||||
const ModifyExternalRetrievalModal: React.FC<ModifyExternalRetrievalModalProps> = ({
|
||||
onClose,
|
||||
onSave,
|
||||
initialTopK,
|
||||
initialScoreThreshold,
|
||||
initialScoreThresholdEnabled,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [topK, setTopK] = useState(initialTopK)
|
||||
const [scoreThreshold, setScoreThreshold] = useState(initialScoreThreshold)
|
||||
const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState(initialScoreThresholdEnabled)
|
||||
|
||||
const handleSettingsChange = (data: { top_k?: number; score_threshold?: number; score_threshold_enabled?: boolean }) => {
|
||||
if (data.top_k !== undefined)
|
||||
setTopK(data.top_k)
|
||||
if (data.score_threshold !== undefined)
|
||||
setScoreThreshold(data.score_threshold)
|
||||
if (data.score_threshold_enabled !== undefined)
|
||||
setScoreThresholdEnabled(data.score_threshold_enabled)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onSave({ top_k: topK, score_threshold: scoreThreshold, score_threshold_enabled: scoreThresholdEnabled })
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='shadows-shadow-2xl absolute right-[14px] top-[36px] z-10 flex w-[320px] flex-col items-start rounded-2xl
|
||||
border-[0.5px] border-components-panel-border bg-components-panel-bg'
|
||||
>
|
||||
<div className='flex items-center justify-between self-stretch p-4 pb-2'>
|
||||
<div className='system-xl-semibold grow text-text-primary'>{t('datasetHitTesting.settingTitle')}</div>
|
||||
<ActionButton className='ml-auto' onClick={onClose}>
|
||||
<RiCloseLine className='h-4 w-4 shrink-0' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className='flex flex-col items-start justify-center gap-4 self-stretch p-4 pt-2'>
|
||||
<RetrievalSettings
|
||||
topK={topK}
|
||||
scoreThreshold={scoreThreshold}
|
||||
scoreThresholdEnabled={scoreThresholdEnabled}
|
||||
onChange={handleSettingsChange}
|
||||
isInHitTesting={true}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex w-full items-end justify-end gap-1 p-4 pt-2'>
|
||||
<Button className='min-w-[72px] shrink-0' onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant='primary' className='min-w-[72px] shrink-0' onClick={handleSave}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModifyExternalRetrievalModal
|
||||
@@ -0,0 +1,123 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import Toast from '../../base/toast'
|
||||
import { ModelTypeEnum } from '../../header/account-setting/model-provider-page/declarations'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import RetrievalMethodConfig from '@/app/components/datasets/common/retrieval-method-config'
|
||||
import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/economical-retrieval-method-config'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
type Props = {
|
||||
indexMethod: string
|
||||
value: RetrievalConfig
|
||||
isShow: boolean
|
||||
onHide: () => void
|
||||
onSave: (value: RetrievalConfig) => void
|
||||
}
|
||||
|
||||
const ModifyRetrievalModal: FC<Props> = ({
|
||||
indexMethod,
|
||||
value,
|
||||
isShow,
|
||||
onHide,
|
||||
onSave,
|
||||
}) => {
|
||||
const ref = useRef(null)
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const [retrievalConfig, setRetrievalConfig] = useState(value)
|
||||
|
||||
// useClickAway(() => {
|
||||
// if (ref)
|
||||
// onHide()
|
||||
// }, ref)
|
||||
|
||||
const {
|
||||
modelList: rerankModelList,
|
||||
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
|
||||
|
||||
const handleSave = () => {
|
||||
if (
|
||||
!isReRankModelSelected({
|
||||
rerankModelList,
|
||||
retrievalConfig,
|
||||
indexMethod,
|
||||
})
|
||||
) {
|
||||
Toast.notify({ type: 'error', message: t('appDebug.datasetConfig.rerankModelRequired') })
|
||||
return
|
||||
}
|
||||
onSave(retrievalConfig)
|
||||
}
|
||||
|
||||
if (!isShow)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className='flex w-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-2xl shadow-shadow-shadow-9'
|
||||
style={{
|
||||
height: 'calc(100vh - 72px)',
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<div className='h-15 flex shrink-0 justify-between px-3 pb-1 pt-3.5'>
|
||||
<div className='text-base font-semibold text-text-primary'>
|
||||
<div>{t('datasetSettings.form.retrievalSetting.title')}</div>
|
||||
<div className='text-xs font-normal leading-[18px] text-text-tertiary'>
|
||||
<a
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
href={docLink('/guides/knowledge-base/retrieval-test-and-citation#modify-text-retrieval-setting', {
|
||||
'zh-Hans': '/guides/knowledge-base/retrieval-test-and-citation#修改文本检索方式',
|
||||
'ja-JP': '/guides/knowledge-base/retrieval-test-and-citation',
|
||||
})}
|
||||
className='text-text-accent'
|
||||
>
|
||||
{t('datasetSettings.form.retrievalSetting.learnMore')}
|
||||
</a>
|
||||
{t('datasetSettings.form.retrievalSetting.description')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div
|
||||
onClick={onHide}
|
||||
className='flex h-8 w-8 cursor-pointer items-center justify-center'
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='px-4 py-2'>
|
||||
<div className='mb-1 text-[13px] font-semibold leading-6 text-text-secondary'>
|
||||
{t('datasetSettings.form.retrievalSetting.method')}
|
||||
</div>
|
||||
{indexMethod === 'high_quality'
|
||||
? (
|
||||
<RetrievalMethodConfig
|
||||
value={retrievalConfig}
|
||||
onChange={setRetrievalConfig}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<EconomicalRetrievalMethodConfig
|
||||
value={retrievalConfig}
|
||||
onChange={setRetrievalConfig}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex justify-end p-4 pt-2'>
|
||||
<Button className='mr-2 shrink-0' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant='primary' className='shrink-0' onClick={handleSave} >{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ModifyRetrievalModal)
|
||||
@@ -0,0 +1,43 @@
|
||||
.container {
|
||||
@apply flex h-full w-full relative overflow-y-auto;
|
||||
}
|
||||
|
||||
.container>div {
|
||||
@apply flex-1 h-full;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
201
dify/web/app/components/datasets/hit-testing/textarea.tsx
Normal file
201
dify/web/app/components/datasets/hit-testing/textarea.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import type { ChangeEvent } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiEqualizer2Line,
|
||||
} from '@remixicon/react'
|
||||
import Image from 'next/image'
|
||||
import Button from '../../base/button'
|
||||
import { getIcon } from '../common/retrieval-method-info'
|
||||
import ModifyExternalRetrievalModal from './modify-external-retrieval-modal'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { ExternalKnowledgeBaseHitTestingResponse, HitTestingResponse } from '@/models/datasets'
|
||||
import { externalKnowledgeBaseHitTesting, hitTesting } from '@/service/datasets'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import { RETRIEVE_METHOD, type RetrievalConfig } from '@/types/app'
|
||||
|
||||
type TextAreaWithButtonIProps = {
|
||||
datasetId: string
|
||||
onUpdateList: () => void
|
||||
setHitResult: (res: HitTestingResponse) => void
|
||||
setExternalHitResult: (res: ExternalKnowledgeBaseHitTestingResponse) => void
|
||||
loading: boolean
|
||||
setLoading: (v: boolean) => void
|
||||
text: string
|
||||
setText: (v: string) => void
|
||||
isExternal?: boolean
|
||||
onClickRetrievalMethod: () => void
|
||||
retrievalConfig: RetrievalConfig
|
||||
isEconomy: boolean
|
||||
onSubmit?: () => void
|
||||
}
|
||||
|
||||
const TextAreaWithButton = ({
|
||||
datasetId,
|
||||
onUpdateList,
|
||||
setHitResult,
|
||||
setExternalHitResult,
|
||||
setLoading,
|
||||
loading,
|
||||
text,
|
||||
setText,
|
||||
isExternal = false,
|
||||
onClickRetrievalMethod,
|
||||
retrievalConfig,
|
||||
isEconomy,
|
||||
onSubmit: _onSubmit,
|
||||
}: TextAreaWithButtonIProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
||||
const [externalRetrievalSettings, setExternalRetrievalSettings] = useState({
|
||||
top_k: 4,
|
||||
score_threshold: 0.5,
|
||||
score_threshold_enabled: false,
|
||||
})
|
||||
|
||||
const handleSaveExternalRetrievalSettings = (data: { top_k: number; score_threshold: number; score_threshold_enabled: boolean }) => {
|
||||
setExternalRetrievalSettings(data)
|
||||
setIsSettingsOpen(false)
|
||||
}
|
||||
|
||||
function handleTextChange(event: ChangeEvent<HTMLTextAreaElement>) {
|
||||
setText(event.target.value)
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
setLoading(true)
|
||||
const [e, res] = await asyncRunSafe<HitTestingResponse>(
|
||||
hitTesting({
|
||||
datasetId,
|
||||
queryText: text,
|
||||
retrieval_model: {
|
||||
...retrievalConfig,
|
||||
search_method: isEconomy ? RETRIEVE_METHOD.keywordSearch : retrievalConfig.search_method,
|
||||
},
|
||||
}) as Promise<HitTestingResponse>,
|
||||
)
|
||||
if (!e) {
|
||||
setHitResult(res)
|
||||
onUpdateList?.()
|
||||
}
|
||||
setLoading(false)
|
||||
if (_onSubmit)
|
||||
_onSubmit()
|
||||
}
|
||||
|
||||
const externalRetrievalTestingOnSubmit = async () => {
|
||||
setLoading(true)
|
||||
const [e, res] = await asyncRunSafe<ExternalKnowledgeBaseHitTestingResponse>(
|
||||
externalKnowledgeBaseHitTesting({
|
||||
datasetId,
|
||||
query: text,
|
||||
external_retrieval_model: {
|
||||
top_k: externalRetrievalSettings.top_k,
|
||||
score_threshold: externalRetrievalSettings.score_threshold,
|
||||
score_threshold_enabled: externalRetrievalSettings.score_threshold_enabled,
|
||||
},
|
||||
}) as Promise<ExternalKnowledgeBaseHitTestingResponse>,
|
||||
)
|
||||
if (!e) {
|
||||
setExternalHitResult(res)
|
||||
onUpdateList?.()
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const retrievalMethod = isEconomy ? RETRIEVE_METHOD.keywordSearch : retrievalConfig.search_method
|
||||
const icon = <Image className='size-3.5 text-util-colors-purple-purple-600' src={getIcon(retrievalMethod)} alt='' />
|
||||
return (
|
||||
<>
|
||||
<div className={cn('relative rounded-xl bg-gradient-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2 p-0.5 shadow-xs')}>
|
||||
<div className='relative rounded-t-xl bg-background-section-burn pt-1.5'>
|
||||
<div className="flex h-8 items-center justify-between pb-1 pl-4 pr-1.5">
|
||||
<span className="text-[13px] font-semibold uppercase leading-4 text-text-secondary">
|
||||
{t('datasetHitTesting.input.title')}
|
||||
</span>
|
||||
{isExternal
|
||||
? <Button
|
||||
variant='secondary'
|
||||
size='small'
|
||||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||
>
|
||||
<RiEqualizer2Line className='h-3.5 w-3.5 text-components-button-secondary-text' />
|
||||
<div className='flex items-center justify-center gap-1 px-[3px]'>
|
||||
<span className='system-xs-medium text-components-button-secondary-text'>{t('datasetHitTesting.settingTitle')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
: <div
|
||||
onClick={onClickRetrievalMethod}
|
||||
className='flex h-7 cursor-pointer items-center space-x-0.5 rounded-lg border-[0.5px] border-components-button-secondary-bg bg-components-button-secondary-bg px-1.5 shadow-xs backdrop-blur-[5px] hover:bg-components-button-secondary-bg-hover'
|
||||
>
|
||||
{icon}
|
||||
<div className='text-xs font-medium uppercase text-text-secondary'>{t(`dataset.retrieval.${retrievalMethod}.title`)}</div>
|
||||
<RiEqualizer2Line className='size-4 text-components-menu-item-text'></RiEqualizer2Line>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{
|
||||
isSettingsOpen && (
|
||||
<ModifyExternalRetrievalModal
|
||||
onClose={() => setIsSettingsOpen(false)}
|
||||
onSave={handleSaveExternalRetrievalSettings}
|
||||
initialTopK={externalRetrievalSettings.top_k}
|
||||
initialScoreThreshold={externalRetrievalSettings.score_threshold}
|
||||
initialScoreThresholdEnabled={externalRetrievalSettings.score_threshold_enabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className='h-2 rounded-t-xl bg-background-default'></div>
|
||||
</div>
|
||||
<div className='rounded-b-xl bg-background-default px-4 pb-11'>
|
||||
<textarea
|
||||
className='h-[220px] w-full resize-none border-none bg-transparent text-sm font-normal text-text-secondary caret-[#295EFF] placeholder:text-sm placeholder:font-normal placeholder:text-components-input-text-placeholder focus-visible:outline-none'
|
||||
value={text}
|
||||
onChange={handleTextChange}
|
||||
placeholder={t('datasetHitTesting.input.placeholder') as string}
|
||||
/>
|
||||
<div className="absolute inset-x-0 bottom-0 mx-4 mb-2 mt-2 flex items-center justify-between">
|
||||
{text?.length > 200
|
||||
? (
|
||||
<Tooltip
|
||||
popupContent={t('datasetHitTesting.input.countWarning')}
|
||||
>
|
||||
<div
|
||||
className={cn('flex h-5 items-center rounded-md bg-background-section-burn px-1 text-xs font-medium text-red-600', !text?.length && 'opacity-50')}
|
||||
>
|
||||
{text?.length}
|
||||
<span className="mx-0.5 text-red-300">/</span>
|
||||
200
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
: (
|
||||
<div
|
||||
className={cn('flex h-5 items-center rounded-md bg-background-section-burn px-1 text-xs font-medium text-text-tertiary', !text?.length && 'opacity-50')}
|
||||
>
|
||||
{text?.length}
|
||||
<span className="mx-0.5 text-divider-deep">/</span>
|
||||
200
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Button
|
||||
onClick={isExternal ? externalRetrievalTestingOnSubmit : onSubmit}
|
||||
variant="primary"
|
||||
loading={loading}
|
||||
disabled={(!text?.length || text?.length > 200)}
|
||||
className='w-[88px]'
|
||||
>
|
||||
{t('datasetHitTesting.input.testing')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextAreaWithButton
|
||||
@@ -0,0 +1,30 @@
|
||||
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 'md':
|
||||
case 'mdx':
|
||||
case 'markdown':
|
||||
return FileAppearanceTypeEnum.markdown
|
||||
case 'csv':
|
||||
case 'xls':
|
||||
case 'xlsx':
|
||||
return FileAppearanceTypeEnum.excel
|
||||
case 'txt':
|
||||
case 'epub':
|
||||
case 'html':
|
||||
case 'htm':
|
||||
case 'xml':
|
||||
return FileAppearanceTypeEnum.document
|
||||
case 'ppt':
|
||||
case 'pptx':
|
||||
return FileAppearanceTypeEnum.ppt
|
||||
default:
|
||||
return FileAppearanceTypeEnum.custom
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user