Revert "Feat/parent child retrieval" (#12095)
@@ -1,54 +0,0 @@
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import { SelectionMod } from '../base/icons/src/public/knowledge'
|
||||
import type { QA } from '@/models/datasets'
|
||||
|
||||
export type ChunkLabelProps = {
|
||||
label: string
|
||||
characterCount: number
|
||||
}
|
||||
|
||||
export const ChunkLabel: FC<ChunkLabelProps> = (props) => {
|
||||
const { label, characterCount } = props
|
||||
return <div className='flex items-center text-text-tertiary text-xs font-medium'>
|
||||
<SelectionMod className='size-[10px]' />
|
||||
<p className='flex gap-2 ml-0.5'><span>
|
||||
{label}
|
||||
</span>
|
||||
<span>
|
||||
·
|
||||
</span>
|
||||
<span>
|
||||
{`${characterCount} characters`}
|
||||
</span></p>
|
||||
</div>
|
||||
}
|
||||
|
||||
export type ChunkContainerProps = ChunkLabelProps & PropsWithChildren
|
||||
|
||||
export const ChunkContainer: FC<ChunkContainerProps> = (props) => {
|
||||
const { label, characterCount, children } = props
|
||||
return <div className='space-y-2'>
|
||||
<ChunkLabel label={label} characterCount={characterCount} />
|
||||
<div className='text-text-secondary body-md-regular'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export type QAPreviewProps = {
|
||||
qa: QA
|
||||
}
|
||||
|
||||
export const QAPreview: FC<QAPreviewProps> = (props) => {
|
||||
const { qa } = props
|
||||
return <div className='flex flex-col gap-y-2'>
|
||||
<div className='flex gap-x-1'>
|
||||
<label className='text-text-tertiary text-[13px] font-medium leading-[20px] shrink-0'>Q</label>
|
||||
<p className='text-text-secondary body-md-regular'>{qa.question}</p>
|
||||
</div>
|
||||
<div className='flex gap-x-1'>
|
||||
<label className='text-text-tertiary text-[13px] font-medium leading-[20px] shrink-0'>A</label>
|
||||
<p className='text-text-secondary body-md-regular'>{qa.answer}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { GeneralType, ParentChildType } from '@/app/components/base/icons/src/public/knowledge'
|
||||
|
||||
type Props = {
|
||||
isGeneralMode: boolean
|
||||
isQAMode: boolean
|
||||
}
|
||||
|
||||
const ChunkingModeLabel: FC<Props> = ({
|
||||
isGeneralMode,
|
||||
isQAMode,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const TypeIcon = isGeneralMode ? GeneralType : ParentChildType
|
||||
|
||||
return (
|
||||
<Badge>
|
||||
<div className='flex items-center h-full space-x-0.5 text-text-tertiary'>
|
||||
<TypeIcon className='w-3 h-3' />
|
||||
<span className='system-2xs-medium-uppercase'>{isGeneralMode ? `${t('dataset.chunkingMode.general')}${isQAMode ? ' · QA' : ''}` : t('dataset.chunkingMode.parentChild')}</span>
|
||||
</div>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
export default React.memo(ChunkingModeLabel)
|
||||
@@ -1,40 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import FileTypeIcon from '../../base/file-uploader/file-type-icon'
|
||||
import type { FileAppearanceType } from '@/app/components/base/file-uploader/types'
|
||||
import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
|
||||
|
||||
const extendToFileTypeMap: { [key: string]: FileAppearanceType } = {
|
||||
pdf: FileAppearanceTypeEnum.pdf,
|
||||
json: FileAppearanceTypeEnum.document,
|
||||
html: FileAppearanceTypeEnum.document,
|
||||
txt: FileAppearanceTypeEnum.document,
|
||||
markdown: FileAppearanceTypeEnum.markdown,
|
||||
md: FileAppearanceTypeEnum.markdown,
|
||||
xlsx: FileAppearanceTypeEnum.excel,
|
||||
xls: FileAppearanceTypeEnum.excel,
|
||||
csv: FileAppearanceTypeEnum.excel,
|
||||
doc: FileAppearanceTypeEnum.word,
|
||||
docx: FileAppearanceTypeEnum.word,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
extension?: string
|
||||
name?: string
|
||||
size?: 'sm' | 'lg' | 'md'
|
||||
className?: string
|
||||
}
|
||||
|
||||
const DocumentFileIcon: FC<Props> = ({
|
||||
extension,
|
||||
name,
|
||||
size = 'md',
|
||||
className,
|
||||
}) => {
|
||||
const localExtension = extension?.toLowerCase() || name?.split('.')?.pop()?.toLowerCase()
|
||||
return (
|
||||
<FileTypeIcon type={extendToFileTypeMap[localExtension!] || FileAppearanceTypeEnum.document} size={size} className={className} />
|
||||
)
|
||||
}
|
||||
export default React.memo(DocumentFileIcon)
|
||||
@@ -1,42 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import FileIcon from '../document-file-icon'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { DocumentItem } from '@/models/datasets'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
list: DocumentItem[]
|
||||
onChange: (value: DocumentItem) => void
|
||||
}
|
||||
|
||||
const DocumentList: FC<Props> = ({
|
||||
className,
|
||||
list,
|
||||
onChange,
|
||||
}) => {
|
||||
const handleChange = useCallback((item: DocumentItem) => {
|
||||
return () => onChange(item)
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
{list.map((item) => {
|
||||
const { id, name, extension } = item
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className='flex items-center h-8 px-2 hover:bg-state-base-hover rounded-lg space-x-2 cursor-pointer'
|
||||
onClick={handleChange(item)}
|
||||
>
|
||||
<FileIcon name={item.name} extension={extension} size='md' />
|
||||
<div className='truncate text-text-secondary text-sm'>{name}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(DocumentList)
|
||||
@@ -1,118 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FileIcon from '../document-file-icon'
|
||||
import DocumentList from './document-list'
|
||||
import type { DocumentItem, ParentMode, SimpleDocumentDetail } from '@/models/datasets'
|
||||
import { ProcessMode } from '@/models/datasets'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import cn from '@/utils/classnames'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import { GeneralType, ParentChildType } from '@/app/components/base/icons/src/public/knowledge'
|
||||
import { useDocumentList } from '@/service/knowledge/use-document'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
|
||||
type Props = {
|
||||
datasetId: string
|
||||
value: {
|
||||
name?: string
|
||||
extension?: string
|
||||
processMode?: ProcessMode
|
||||
parentMode?: ParentMode
|
||||
}
|
||||
onChange: (value: SimpleDocumentDetail) => void
|
||||
}
|
||||
|
||||
const DocumentPicker: FC<Props> = ({
|
||||
datasetId,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
name,
|
||||
extension,
|
||||
processMode,
|
||||
parentMode,
|
||||
} = value
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
const { data } = useDocumentList({
|
||||
datasetId,
|
||||
query: {
|
||||
keyword: query,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
},
|
||||
})
|
||||
const documentsList = data?.data
|
||||
const isParentChild = processMode === ProcessMode.parentChild
|
||||
const TypeIcon = isParentChild ? ParentChildType : GeneralType
|
||||
|
||||
const [open, {
|
||||
set: setOpen,
|
||||
toggle: togglePopup,
|
||||
}] = useBoolean(false)
|
||||
const ArrowIcon = RiArrowDownSLine
|
||||
|
||||
const handleChange = useCallback(({ id }: DocumentItem) => {
|
||||
onChange(documentsList?.find(item => item.id === id) as SimpleDocumentDetail)
|
||||
setOpen(false)
|
||||
}, [documentsList, onChange, setOpen])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={togglePopup}>
|
||||
<div className={cn('flex items-center ml-1 px-2 py-0.5 rounded-lg hover:bg-state-base-hover select-none cursor-pointer', open && 'bg-state-base-hover')}>
|
||||
<FileIcon name={name} extension={extension} size='lg' />
|
||||
<div className='flex flex-col items-start ml-1 mr-0.5'>
|
||||
<div className='flex items-center space-x-0.5'>
|
||||
<span className={cn('system-md-semibold')}> {name || '--'}</span>
|
||||
<ArrowIcon className={'h-4 w-4 text-text-primary'} />
|
||||
</div>
|
||||
<div className='flex items-center h-3 text-text-tertiary space-x-0.5'>
|
||||
<TypeIcon className='w-3 h-3' />
|
||||
<span className={cn('system-2xs-medium-uppercase', isParentChild && 'mt-0.5' /* to icon problem cause not ver align */)}>
|
||||
{isParentChild ? t('dataset.chunkingMode.parentChild') : t('dataset.chunkingMode.general')}
|
||||
{isParentChild && ` · ${!parentMode ? '--' : parentMode === 'paragraph' ? t('dataset.parentMode.paragraph') : t('dataset.parentMode.fullDoc')}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[11]'>
|
||||
<div className='w-[360px] p-1 pt-2 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]'>
|
||||
<SearchInput value={query} onChange={setQuery} className='mx-1' />
|
||||
{documentsList
|
||||
? (
|
||||
<DocumentList
|
||||
className='mt-2'
|
||||
list={documentsList.map(d => ({
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
extension: d.data_source_detail_dict?.upload_file?.extension || '',
|
||||
}))}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
: (<div className='mt-2 flex items-center justify-center w-[360px] h-[100px]'>
|
||||
<Loading />
|
||||
</div>)}
|
||||
</div>
|
||||
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
export default React.memo(DocumentPicker)
|
||||
@@ -1,82 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FileIcon from '../document-file-icon'
|
||||
import DocumentList from './document-list'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import cn from '@/utils/classnames'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import type { DocumentItem } from '@/models/datasets'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
value: DocumentItem
|
||||
files: DocumentItem[]
|
||||
onChange: (value: DocumentItem) => void
|
||||
}
|
||||
|
||||
const PreviewDocumentPicker: FC<Props> = ({
|
||||
className,
|
||||
value,
|
||||
files,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { name, extension } = value
|
||||
|
||||
const [open, {
|
||||
set: setOpen,
|
||||
toggle: togglePopup,
|
||||
}] = useBoolean(false)
|
||||
const ArrowIcon = RiArrowDownSLine
|
||||
|
||||
const handleChange = useCallback((item: DocumentItem) => {
|
||||
onChange(item)
|
||||
setOpen(false)
|
||||
}, [onChange, setOpen])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={4}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={togglePopup}>
|
||||
<div className={cn('flex items-center h-6 px-1 rounded-md hover:bg-state-base-hover select-none', open && 'bg-state-base-hover', className)}>
|
||||
<FileIcon name={name} extension={extension} size='md' />
|
||||
<div className='flex flex-col items-start ml-1'>
|
||||
<div className='flex items-center space-x-0.5'>
|
||||
<span className={cn('system-md-semibold max-w-[200px] truncate text-text-primary')}> {name || '--'}</span>
|
||||
<ArrowIcon className={'h-[18px] w-[18px] text-text-primary'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[11]'>
|
||||
<div className='w-[392px] p-1 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]'>
|
||||
{files?.length > 1 && <div className='pl-2 flex items-center h-8 system-xs-medium-uppercase text-text-tertiary'>{t('dataset.preprocessDocument', { num: files.length })}</div>}
|
||||
{files?.length > 0
|
||||
? (
|
||||
<DocumentList
|
||||
list={files}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
: (<div className='mt-2 flex items-center justify-center w-[360px] h-[100px]'>
|
||||
<Loading />
|
||||
</div>)}
|
||||
</div>
|
||||
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
export default React.memo(PreviewDocumentPicker)
|
||||
@@ -1,38 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import StatusWithAction from './status-with-action'
|
||||
import { useAutoDisabledDocuments, useDocumentEnable, useInvalidDisabledDocument } from '@/service/knowledge/use-document'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
type Props = {
|
||||
datasetId: string
|
||||
}
|
||||
|
||||
const AutoDisabledDocument: FC<Props> = ({
|
||||
datasetId,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { data, isLoading } = useAutoDisabledDocuments(datasetId)
|
||||
const invalidDisabledDocument = useInvalidDisabledDocument()
|
||||
const documentIds = data?.document_ids
|
||||
const hasDisabledDocument = documentIds && documentIds.length > 0
|
||||
const { mutateAsync: enableDocument } = useDocumentEnable()
|
||||
const handleEnableDocuments = useCallback(async () => {
|
||||
await enableDocument({ datasetId, documentIds })
|
||||
invalidDisabledDocument()
|
||||
Toast.notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
}, [])
|
||||
if (!hasDisabledDocument || isLoading)
|
||||
return null
|
||||
|
||||
return (
|
||||
<StatusWithAction
|
||||
type='info'
|
||||
description={t('dataset.documentsDisabled', { num: documentIds?.length })}
|
||||
actionText={t('dataset.enable')}
|
||||
onAction={handleEnableDocuments}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(AutoDisabledDocument)
|
||||
@@ -1,69 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useReducer } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import StatusWithAction from './status-with-action'
|
||||
import { getErrorDocs, retryErrorDocs } from '@/service/datasets'
|
||||
import type { IndexingStatusResponse } from '@/models/datasets'
|
||||
|
||||
type Props = {
|
||||
datasetId: string
|
||||
}
|
||||
type IIndexState = {
|
||||
value: string
|
||||
}
|
||||
type ActionType = 'retry' | 'success' | 'error'
|
||||
|
||||
type IAction = {
|
||||
type: ActionType
|
||||
}
|
||||
const indexStateReducer = (state: IIndexState, action: IAction) => {
|
||||
const actionMap = {
|
||||
retry: 'retry',
|
||||
success: 'success',
|
||||
error: 'error',
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
value: actionMap[action.type] || state.value,
|
||||
}
|
||||
}
|
||||
|
||||
const RetryButton: FC<Props> = ({ datasetId }) => {
|
||||
const { t } = useTranslation()
|
||||
const [indexState, dispatch] = useReducer(indexStateReducer, { value: 'success' })
|
||||
const { data: errorDocs, isLoading } = useSWR({ datasetId }, getErrorDocs)
|
||||
|
||||
const onRetryErrorDocs = async () => {
|
||||
dispatch({ type: 'retry' })
|
||||
const document_ids = errorDocs?.data.map((doc: IndexingStatusResponse) => doc.id) || []
|
||||
const res = await retryErrorDocs({ datasetId, document_ids })
|
||||
if (res.result === 'success')
|
||||
dispatch({ type: 'success' })
|
||||
else
|
||||
dispatch({ type: 'error' })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (errorDocs?.total === 0)
|
||||
dispatch({ type: 'success' })
|
||||
else
|
||||
dispatch({ type: 'error' })
|
||||
}, [errorDocs?.total])
|
||||
|
||||
if (isLoading || indexState.value === 'success')
|
||||
return null
|
||||
|
||||
return (
|
||||
<StatusWithAction
|
||||
type='warning'
|
||||
description={`${errorDocs?.total} ${t('dataset.docsFailedNotice')}`}
|
||||
actionText={t('dataset.retry')}
|
||||
disabled={indexState.value === 'retry'}
|
||||
onAction={indexState.value === 'error' ? onRetryErrorDocs : () => { }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default RetryButton
|
||||
@@ -1,65 +0,0 @@
|
||||
'use client'
|
||||
import { RiAlertFill, RiCheckboxCircleFill, RiErrorWarningFill, RiInformation2Fill } from '@remixicon/react'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
|
||||
type Status = 'success' | 'error' | 'warning' | 'info'
|
||||
type Props = {
|
||||
type?: Status
|
||||
description: string
|
||||
actionText: string
|
||||
onAction: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const IconMap = {
|
||||
success: {
|
||||
Icon: RiCheckboxCircleFill,
|
||||
color: 'text-text-success',
|
||||
},
|
||||
error: {
|
||||
Icon: RiErrorWarningFill,
|
||||
color: 'text-text-destructive',
|
||||
},
|
||||
warning: {
|
||||
Icon: RiAlertFill,
|
||||
color: 'text-text-warning-secondary',
|
||||
},
|
||||
info: {
|
||||
Icon: RiInformation2Fill,
|
||||
color: 'text-text-accent',
|
||||
},
|
||||
}
|
||||
|
||||
const getIcon = (type: Status) => {
|
||||
return IconMap[type]
|
||||
}
|
||||
|
||||
const StatusAction: FC<Props> = ({
|
||||
type = 'info',
|
||||
description,
|
||||
actionText,
|
||||
onAction,
|
||||
disabled,
|
||||
}) => {
|
||||
const { Icon, color } = getIcon(type)
|
||||
return (
|
||||
<div className='relative flex items-center h-[34px] rounded-lg pl-2 pr-3 border border-components-panel-border bg-components-panel-bg-blur shadow-xs'>
|
||||
<div className={`absolute inset-0 opacity-40 rounded-lg ${(type === 'success' && 'bg-[linear-gradient(92deg,rgba(23,178,106,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||
|| (type === 'warning' && 'bg-[linear-gradient(92deg,rgba(247,144,9,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||
|| (type === 'error' && 'bg-[linear-gradient(92deg,rgba(240,68,56,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||
|| (type === 'info' && 'bg-[linear-gradient(92deg,rgba(11,165,236,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|
||||
}`}
|
||||
/>
|
||||
<div className='relative z-10 flex h-full items-center space-x-2'>
|
||||
<Icon className={cn('w-4 h-4', color)} />
|
||||
<div className='text-[13px] font-normal text-text-secondary'>{description}</div>
|
||||
<Divider type='vertical' className='!h-4' />
|
||||
<div onClick={onAction} className={cn('text-text-accent font-semibold text-[13px] cursor-pointer', disabled && 'text-text-disabled cursor-not-allowed')}>{actionText}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(StatusAction)
|
||||
@@ -2,11 +2,10 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Image from 'next/image'
|
||||
import RetrievalParamConfig from '../retrieval-param-config'
|
||||
import { OptionCard } from '../../create/step-two/option-card'
|
||||
import { retrievalIcon } from '../../create/icons'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import RadioCard from '@/app/components/base/radio-card'
|
||||
import { HighPriority } from '@/app/components/base/icons/src/vender/solid/arrows'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
|
||||
type Props = {
|
||||
@@ -22,17 +21,19 @@ const EconomicalRetrievalMethodConfig: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
<OptionCard icon={<Image className='w-4 h-4' src={retrievalIcon.vector} alt='' />}
|
||||
<RadioCard
|
||||
icon={<HighPriority className='w-4 h-4 text-[#7839EE]' />}
|
||||
title={t('dataset.retrieval.invertedIndex.title')}
|
||||
description={t('dataset.retrieval.invertedIndex.description')} isActive
|
||||
activeHeaderClassName='bg-dataset-option-card-purple-gradient'
|
||||
>
|
||||
<RetrievalParamConfig
|
||||
type={RETRIEVE_METHOD.invertedIndex}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</OptionCard>
|
||||
description={t('dataset.retrieval.invertedIndex.description')}
|
||||
noRadio
|
||||
chosenConfig={
|
||||
<RetrievalParamConfig
|
||||
type={RETRIEVE_METHOD.invertedIndex}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Image from 'next/image'
|
||||
import RetrievalParamConfig from '../retrieval-param-config'
|
||||
import { OptionCard } from '../../create/step-two/option-card'
|
||||
import Effect from '../../create/assets/option-card-effect-purple.svg'
|
||||
import { retrievalIcon } from '../../create/icons'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import RadioCard from '@/app/components/base/radio-card'
|
||||
import { PatternRecognition, Semantic } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
@@ -17,7 +16,6 @@ import {
|
||||
RerankingModeEnum,
|
||||
WeightedScoreEnum,
|
||||
} from '@/models/datasets'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
||||
type Props = {
|
||||
value: RetrievalConfig
|
||||
@@ -58,72 +56,67 @@ const RetrievalMethodConfig: FC<Props> = ({
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
{supportRetrievalMethods.includes(RETRIEVE_METHOD.semantic) && (
|
||||
<OptionCard icon={<Image className='w-4 h-4' src={retrievalIcon.vector} alt='' />}
|
||||
<RadioCard
|
||||
icon={<Semantic className='w-4 h-4 text-[#7839EE]' />}
|
||||
title={t('dataset.retrieval.semantic_search.title')}
|
||||
description={t('dataset.retrieval.semantic_search.description')}
|
||||
isActive={
|
||||
value.search_method === RETRIEVE_METHOD.semantic
|
||||
}
|
||||
onSwitched={() => onChange({
|
||||
isChosen={value.search_method === RETRIEVE_METHOD.semantic}
|
||||
onChosen={() => onChange({
|
||||
...value,
|
||||
search_method: RETRIEVE_METHOD.semantic,
|
||||
})}
|
||||
effectImg={Effect.src}
|
||||
activeHeaderClassName='bg-dataset-option-card-purple-gradient'
|
||||
>
|
||||
<RetrievalParamConfig
|
||||
type={RETRIEVE_METHOD.semantic}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</OptionCard>
|
||||
chosenConfig={
|
||||
<RetrievalParamConfig
|
||||
type={RETRIEVE_METHOD.semantic}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{supportRetrievalMethods.includes(RETRIEVE_METHOD.semantic) && (
|
||||
<OptionCard icon={<Image className='w-4 h-4' src={retrievalIcon.fullText} alt='' />}
|
||||
<RadioCard
|
||||
icon={<FileSearch02 className='w-4 h-4 text-[#7839EE]' />}
|
||||
title={t('dataset.retrieval.full_text_search.title')}
|
||||
description={t('dataset.retrieval.full_text_search.description')}
|
||||
isActive={
|
||||
value.search_method === RETRIEVE_METHOD.fullText
|
||||
}
|
||||
onSwitched={() => onChange({
|
||||
isChosen={value.search_method === RETRIEVE_METHOD.fullText}
|
||||
onChosen={() => onChange({
|
||||
...value,
|
||||
search_method: RETRIEVE_METHOD.fullText,
|
||||
})}
|
||||
effectImg={Effect.src}
|
||||
activeHeaderClassName='bg-dataset-option-card-purple-gradient'
|
||||
>
|
||||
<RetrievalParamConfig
|
||||
type={RETRIEVE_METHOD.fullText}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</OptionCard>
|
||||
chosenConfig={
|
||||
<RetrievalParamConfig
|
||||
type={RETRIEVE_METHOD.fullText}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{supportRetrievalMethods.includes(RETRIEVE_METHOD.semantic) && (
|
||||
<OptionCard icon={<Image className='w-4 h-4' src={retrievalIcon.hybrid} alt='' />}
|
||||
<RadioCard
|
||||
icon={<PatternRecognition className='w-4 h-4 text-[#7839EE]' />}
|
||||
title={
|
||||
<div className='flex items-center space-x-1'>
|
||||
<div>{t('dataset.retrieval.hybrid_search.title')}</div>
|
||||
<Badge text={t('dataset.retrieval.hybrid_search.recommend')!} className='border-text-accent-secondary text-text-accent-secondary ml-1 h-[18px]' uppercase />
|
||||
<div className='flex h-full items-center px-1.5 rounded-md border border-[#E0EAFF] text-xs font-medium text-[#444CE7]'>{t('dataset.retrieval.hybrid_search.recommend')}</div>
|
||||
</div>
|
||||
}
|
||||
description={t('dataset.retrieval.hybrid_search.description')} isActive={
|
||||
value.search_method === RETRIEVE_METHOD.hybrid
|
||||
}
|
||||
onSwitched={() => onChange({
|
||||
description={t('dataset.retrieval.hybrid_search.description')}
|
||||
isChosen={value.search_method === RETRIEVE_METHOD.hybrid}
|
||||
onChosen={() => onChange({
|
||||
...value,
|
||||
search_method: RETRIEVE_METHOD.hybrid,
|
||||
reranking_enable: true,
|
||||
})}
|
||||
effectImg={Effect.src}
|
||||
activeHeaderClassName='bg-dataset-option-card-purple-gradient'
|
||||
>
|
||||
<RetrievalParamConfig
|
||||
type={RETRIEVE_METHOD.hybrid}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</OptionCard>
|
||||
chosenConfig={
|
||||
<RetrievalParamConfig
|
||||
type={RETRIEVE_METHOD.hybrid}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Image from 'next/image'
|
||||
import { retrievalIcon } from '../../create/icons'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import RadioCard from '@/app/components/base/radio-card'
|
||||
import { HighPriority } from '@/app/components/base/icons/src/vender/solid/arrows'
|
||||
import { PatternRecognition, Semantic } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
|
||||
|
||||
type Props = {
|
||||
value: RetrievalConfig
|
||||
@@ -14,12 +15,11 @@ type Props = {
|
||||
|
||||
export const getIcon = (type: RETRIEVE_METHOD) => {
|
||||
return ({
|
||||
[RETRIEVE_METHOD.semantic]: retrievalIcon.vector,
|
||||
[RETRIEVE_METHOD.fullText]: retrievalIcon.fullText,
|
||||
[RETRIEVE_METHOD.hybrid]: retrievalIcon.hybrid,
|
||||
[RETRIEVE_METHOD.invertedIndex]: retrievalIcon.vector,
|
||||
[RETRIEVE_METHOD.keywordSearch]: retrievalIcon.vector,
|
||||
})[type] || retrievalIcon.vector
|
||||
[RETRIEVE_METHOD.semantic]: Semantic,
|
||||
[RETRIEVE_METHOD.fullText]: FileSearch02,
|
||||
[RETRIEVE_METHOD.hybrid]: PatternRecognition,
|
||||
[RETRIEVE_METHOD.invertedIndex]: HighPriority,
|
||||
})[type] || FileSearch02
|
||||
}
|
||||
|
||||
const EconomicalRetrievalMethodConfig: FC<Props> = ({
|
||||
@@ -28,11 +28,11 @@ const EconomicalRetrievalMethodConfig: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const type = value.search_method
|
||||
const icon = <Image className='size-3.5 text-util-colors-purple-purple-600' src={getIcon(type)} alt='' />
|
||||
const Icon = getIcon(type)
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
<RadioCard
|
||||
icon={icon}
|
||||
icon={<Icon className='w-4 h-4 text-[#7839EE]' />}
|
||||
title={t(`dataset.retrieval.${type}.title`)}
|
||||
description={t(`dataset.retrieval.${type}.description`)}
|
||||
noRadio
|
||||
|
||||
@@ -3,9 +3,6 @@ import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import Image from 'next/image'
|
||||
import ProgressIndicator from '../../create/assets/progress-indicator.svg'
|
||||
import Reranking from '../../create/assets/rerank.svg'
|
||||
import cn from '@/utils/classnames'
|
||||
import TopKItem from '@/app/components/base/param-item/top-k-item'
|
||||
import ScoreThresholdItem from '@/app/components/base/param-item/score-threshold-item'
|
||||
@@ -23,7 +20,6 @@ import {
|
||||
} from '@/models/datasets'
|
||||
import WeightedScore from '@/app/components/app/configuration/dataset-config/params-config/weighted-score'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import RadioCard from '@/app/components/base/radio-card'
|
||||
|
||||
type Props = {
|
||||
type: RETRIEVE_METHOD
|
||||
@@ -120,7 +116,7 @@ const RetrievalParamConfig: FC<Props> = ({
|
||||
<div>
|
||||
{!isEconomical && !isHybridSearch && (
|
||||
<div>
|
||||
<div className='flex items-center space-x-2 mb-2'>
|
||||
<div className='flex h-8 items-center text-[13px] font-medium text-gray-900 space-x-2'>
|
||||
{canToggleRerankModalEnable && (
|
||||
<div
|
||||
className='flex items-center'
|
||||
@@ -140,7 +136,7 @@ const RetrievalParamConfig: FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center'>
|
||||
<span className='mr-0.5 system-sm-semibold text-text-secondary'>{t('common.modelProvider.rerankModel.key')}</span>
|
||||
<span className='mr-0.5'>{t('common.modelProvider.rerankModel.key')}</span>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className="w-[200px]">{t('common.modelProvider.rerankModel.tip')}</div>
|
||||
@@ -167,7 +163,7 @@ const RetrievalParamConfig: FC<Props> = ({
|
||||
)}
|
||||
{
|
||||
!isHybridSearch && (
|
||||
<div className={cn(!isEconomical && 'mt-4', 'flex space-between space-x-4')}>
|
||||
<div className={cn(!isEconomical && 'mt-4', 'flex space-between space-x-6')}>
|
||||
<TopKItem
|
||||
className='grow'
|
||||
value={value.top_k}
|
||||
@@ -205,22 +201,24 @@ const RetrievalParamConfig: FC<Props> = ({
|
||||
{
|
||||
isHybridSearch && (
|
||||
<>
|
||||
<div className='flex gap-2 mb-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
{
|
||||
rerankingModeOptions.map(option => (
|
||||
<RadioCard
|
||||
<div
|
||||
key={option.value}
|
||||
isChosen={value.reranking_mode === option.value}
|
||||
onChosen={() => handleChangeRerankMode(option.value)}
|
||||
icon={<Image src={
|
||||
option.value === RerankingModeEnum.WeightedScore
|
||||
? ProgressIndicator
|
||||
: Reranking
|
||||
} alt=''/>}
|
||||
title={option.label}
|
||||
description={option.tips}
|
||||
className='flex-1'
|
||||
/>
|
||||
className={cn(
|
||||
'flex items-center justify-center mb-4 w-[calc((100%-8px)/2)] h-8 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg cursor-pointer system-sm-medium text-text-secondary',
|
||||
value.reranking_mode === RerankingModeEnum.WeightedScore && option.value === RerankingModeEnum.WeightedScore && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
|
||||
value.reranking_mode !== RerankingModeEnum.WeightedScore && option.value !== RerankingModeEnum.WeightedScore && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
|
||||
)}
|
||||
onClick={() => handleChangeRerankMode(option.value)}
|
||||
>
|
||||
<div className='truncate'>{option.label}</div>
|
||||
<Tooltip
|
||||
popupContent={<div className='w-[200px]'>{option.tips}</div>}
|
||||
triggerClassName='ml-0.5 w-3.5 h-3.5'
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.18055 6.45828C7.52291 6.45828 8.61111 5.37008 8.61111 4.02772C8.61111 2.68536 7.52291 1.59717 6.18055 1.59717C4.8382 1.59717 3.75 2.68536 3.75 4.02772C3.75 5.37008 4.8382 6.45828 6.18055 6.45828Z" fill="#EF6820"/>
|
||||
<path d="M13.8192 6.45828C15.1616 6.45828 16.2498 5.37008 16.2498 4.02772C16.2498 2.68536 15.1616 1.59717 13.8192 1.59717C12.4769 1.59717 11.3887 2.68536 11.3887 4.02772C11.3887 5.37008 12.4769 6.45828 13.8192 6.45828Z" fill="#EF6820"/>
|
||||
<path d="M13.8193 7.84719C13.0627 7.84805 12.3185 8.03933 11.6552 8.40341C10.992 8.7675 10.4311 9.29267 10.0241 9.93053C10.5745 9.93695 11.1 10.1609 11.4858 10.5535C11.8716 10.9461 12.0864 11.4755 12.0831 12.0259C12.0799 12.5763 11.859 13.1031 11.4687 13.4911C11.0783 13.8792 10.5503 14.097 9.99984 14.097C9.44942 14.097 8.92135 13.8792 8.53101 13.4911C8.14066 13.1031 7.91976 12.5763 7.91655 12.0259C7.91334 11.4755 8.12808 10.9461 8.51387 10.5535C8.89966 10.1609 9.42515 9.93695 9.97554 9.93053C9.45127 9.10686 8.67371 8.47572 7.75983 8.13205C6.84596 7.78839 5.84519 7.75078 4.9081 8.0249C3.97101 8.29902 3.14828 8.87003 2.56368 9.65203C1.97908 10.434 1.66424 11.3847 1.66652 12.3611V16.875C1.66652 17.0591 1.73968 17.2358 1.86991 17.366C2.00015 17.4962 2.17678 17.5694 2.36096 17.5694H7.22207V15.8333L4.72207 13.9583C4.64911 13.9036 4.58765 13.835 4.54118 13.7566C4.49472 13.6781 4.46417 13.5912 4.45127 13.501C4.42522 13.3186 4.47267 13.1334 4.58318 12.9861C4.69369 12.8387 4.8582 12.7413 5.04053 12.7153C5.22285 12.6892 5.40806 12.7367 5.5554 12.8472L8.14776 14.7916H11.8519L14.4443 12.8472C14.5916 12.7367 14.7768 12.6892 14.9592 12.7153C15.1415 12.7413 15.306 12.8387 15.4165 12.9861C15.527 13.1334 15.5745 13.3186 15.5484 13.501C15.5224 13.6833 15.425 13.8478 15.2776 13.9583L12.7776 15.8333V17.5694H17.6387C17.8229 17.5694 17.9995 17.4962 18.1298 17.366C18.26 17.2358 18.3332 17.0591 18.3332 16.875V12.3611C18.3317 11.1644 17.8557 10.0171 17.0095 9.17091C16.1633 8.32471 15.016 7.84867 13.8193 7.84719Z" fill="#EF6820"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="file-list-3-fill">
|
||||
<path id="Vector" d="M15.8332 18.3333H4.1665C2.7858 18.3333 1.6665 17.2141 1.6665 15.8333V2.50001C1.6665 2.03977 2.0396 1.66667 2.49984 1.66667H14.1665C14.6268 1.66667 14.9998 2.03977 14.9998 2.50001V12.5H18.3332V15.8333C18.3332 17.2141 17.2139 18.3333 15.8332 18.3333ZM14.9998 14.1667V15.8333C14.9998 16.2936 15.3729 16.6667 15.8332 16.6667C16.2934 16.6667 16.6665 16.2936 16.6665 15.8333V14.1667H14.9998ZM4.99984 5.83334V7.50001H11.6665V5.83334H4.99984ZM4.99984 9.16667V10.8333H11.6665V9.16667H4.99984ZM4.99984 12.5V14.1667H9.1665V12.5H4.99984Z" fill="#1570EF"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 699 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.99984 1.66663C8.35166 1.66663 6.7405 2.15537 5.37009 3.07105C3.99968 3.98672 2.93157 5.28821 2.30084 6.81093C1.67011 8.33365 1.50509 10.0092 1.82663 11.6257C2.14817 13.2422 2.94185 14.7271 4.10728 15.8925C5.27272 17.058 6.75758 17.8516 8.37409 18.1732C9.9906 18.4947 11.6662 18.3297 13.1889 17.699C14.7116 17.0682 16.0131 16.0001 16.9288 14.6297C17.8444 13.2593 18.3332 11.6481 18.3332 9.99996C18.3332 7.78982 17.4552 5.67021 15.8924 4.1074C14.3296 2.5446 12.21 1.66663 9.99984 1.66663ZM12.295 5.65899L13.1116 4.53538C13.1653 4.46155 13.2329 4.39901 13.3107 4.35133C13.3885 4.30365 13.4749 4.27175 13.565 4.25747C13.6551 4.24319 13.7472 4.24679 13.8359 4.26809C13.9246 4.28938 14.0083 4.32793 14.0821 4.38156C14.156 4.43518 14.2185 4.50282 14.2662 4.58061C14.3139 4.6584 14.3458 4.74482 14.36 4.83494C14.3743 4.92506 14.3707 5.01711 14.3494 5.10583C14.3281 5.19456 14.2896 5.27822 14.236 5.35204L13.4193 6.47565C13.311 6.62474 13.1479 6.72471 12.9659 6.75356C12.7839 6.7824 12.5979 6.73777 12.4488 6.62947C12.2997 6.52118 12.1997 6.35809 12.1709 6.17609C12.142 5.99408 12.1867 5.80808 12.295 5.65899ZM5.9179 4.3819C5.99174 4.32795 6.07551 4.28911 6.1644 4.26761C6.25329 4.24612 6.34556 4.2424 6.43589 4.25666C6.52623 4.27092 6.61286 4.30288 6.69081 4.35071C6.76875 4.39854 6.83649 4.4613 6.89012 4.53538L7.70817 5.65899C7.81647 5.80854 7.86092 5.99499 7.83175 6.17731C7.80258 6.35964 7.70217 6.52291 7.55262 6.63121C7.40307 6.73951 7.21662 6.78396 7.03429 6.75478C6.85196 6.72561 6.68869 6.62521 6.5804 6.47565L5.76373 5.35204C5.71013 5.27823 5.6716 5.19457 5.65034 5.10586C5.62908 5.01715 5.62551 4.92512 5.63983 4.83503C5.65414 4.74494 5.68607 4.65855 5.73378 4.5808C5.78149 4.50306 5.84406 4.43547 5.9179 4.3819ZM5.59151 12.1597L4.27206 12.5888C4.18433 12.6215 4.0909 12.6361 3.99739 12.6317C3.90388 12.6273 3.81222 12.6041 3.72791 12.5634C3.64361 12.5227 3.56841 12.4654 3.50682 12.3949C3.44524 12.3244 3.39854 12.2421 3.36954 12.1531C3.34055 12.0641 3.32984 11.9702 3.33808 11.8769C3.34631 11.7837 3.37332 11.693 3.41747 11.6105C3.46162 11.528 3.522 11.4552 3.59499 11.3966C3.66798 11.3379 3.75207 11.2947 3.8422 11.2694L5.16165 10.8402C5.24947 10.8072 5.34308 10.7924 5.43681 10.7965C5.53054 10.8007 5.62245 10.8238 5.707 10.8645C5.79154 10.9052 5.86697 10.9626 5.92872 11.0332C5.99047 11.1039 6.03727 11.1863 6.06629 11.2755C6.09531 11.3647 6.10595 11.4589 6.09757 11.5524C6.08919 11.6458 6.06195 11.7366 6.01752 11.8192C5.97308 11.9018 5.91236 11.9746 5.83902 12.0331C5.76568 12.0916 5.68194 12.1347 5.59151 12.1597ZM10.6943 16.25C10.6943 16.4341 10.6211 16.6108 10.4909 16.741C10.3607 16.8712 10.184 16.9444 9.99984 16.9444C9.81566 16.9444 9.63903 16.8712 9.50879 16.741C9.37856 16.6108 9.3054 16.4341 9.3054 16.25V14.8611C9.3054 14.6769 9.37856 14.5003 9.50879 14.37C9.63903 14.2398 9.81566 14.1666 9.99984 14.1666C10.184 14.1666 10.3607 14.2398 10.4909 14.37C10.6211 14.5003 10.6943 14.6769 10.6943 14.8611V16.25ZM9.99984 12.2222L7.38595 13.5972L7.88526 10.6868L5.77067 8.62565L8.6929 8.20135L9.99984 5.55551L11.3068 8.20135L14.229 8.62565L12.1144 10.6868L12.6137 13.5972L9.99984 12.2222ZM15.729 12.5902L14.4096 12.1611C14.3191 12.1361 14.2347 12.093 14.1614 12.0345C14.088 11.976 14.0273 11.9032 13.9829 11.8206C13.9384 11.738 13.9112 11.6472 13.9028 11.5537C13.8944 11.4603 13.9051 11.3661 13.9341 11.2769C13.9631 11.1877 14.0099 11.1053 14.0717 11.0346C14.1334 10.964 14.2088 10.9066 14.2934 10.8659C14.3779 10.8252 14.4698 10.8021 14.5636 10.7979C14.6573 10.7938 14.7509 10.8086 14.8387 10.8416L16.1582 11.2708C16.2483 11.2961 16.3324 11.3393 16.4054 11.398C16.4784 11.4566 16.5388 11.5293 16.5829 11.6119C16.6271 11.6944 16.6541 11.7851 16.6623 11.8783C16.6705 11.9716 16.6598 12.0655 16.6308 12.1545C16.6018 12.2435 16.5551 12.3258 16.4936 12.3963C16.432 12.4668 16.3568 12.5241 16.2725 12.5648C16.1882 12.6055 16.0965 12.6287 16.003 12.6331C15.9095 12.6375 15.8167 12.6229 15.729 12.5902Z" fill="#EF6820"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="note-mod">
|
||||
<path id="Vector" d="M17.6387 3.05555H2.36095C1.97762 3.05555 1.6665 3.36666 1.6665 3.74999V16.25C1.6665 16.6333 1.97762 16.9444 2.36095 16.9444H17.6387C18.0221 16.9444 18.3332 16.6333 18.3332 16.25V3.74999C18.3332 3.36666 18.0221 3.05555 17.6387 3.05555ZM9.30539 14.1667H5.13873C4.75539 14.1667 4.44428 13.8555 4.44428 13.4722C4.44428 13.0889 4.75539 12.7778 5.13873 12.7778H9.30539C9.68873 12.7778 9.99984 13.0889 9.99984 13.4722C9.99984 13.8555 9.68873 14.1667 9.30539 14.1667ZM14.8609 10.6944H5.13873C4.75539 10.6944 4.44428 10.3833 4.44428 9.99999C4.44428 9.61666 4.75539 9.30555 5.13873 9.30555H14.8609C15.2443 9.30555 15.5554 9.61666 15.5554 9.99999C15.5554 10.3833 15.2443 10.6944 14.8609 10.6944ZM14.8609 7.22221H5.13873C4.75539 7.22221 4.44428 6.9111 4.44428 6.52777C4.44428 6.14443 4.75539 5.83332 5.13873 5.83332H14.8609C15.2443 5.83332 15.5554 6.14443 15.5554 6.52777C15.5554 6.9111 15.2443 7.22221 14.8609 7.22221Z" fill="#1570EF"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,12 +0,0 @@
|
||||
<svg width="220" height="220" viewBox="0 0 220 220" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Effect" opacity="0.8" filter="url(#filter0_f_1328_28605)">
|
||||
<circle cx="32" cy="32" r="28" fill="#444CE7"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_1328_28605" x="-156" y="-156" width="376" height="376" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="80" result="effect1_foregroundBlur_1328_28605"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 613 B |
@@ -1,12 +0,0 @@
|
||||
<svg width="220" height="220" viewBox="0 0 220 220" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Effect" opacity="0.8" filter="url(#filter0_f_481_16338)">
|
||||
<circle cx="32" cy="32" r="28" fill="#EF6820"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_481_16338" x="-156" y="-156" width="376" height="376" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="80" result="effect1_foregroundBlur_481_16338"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 610 B |
@@ -1,12 +0,0 @@
|
||||
<svg width="220" height="220" viewBox="0 0 220 220" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Effect" opacity="0.8" filter="url(#filter0_f_481_16453)">
|
||||
<circle cx="32" cy="32" r="28" fill="#6938EF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_481_16453" x="-156" y="-156" width="376" height="376" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="80" result="effect1_foregroundBlur_481_16453"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 610 B |
@@ -1,12 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.93923 18.3333C3.48973 18.3333 3.05032 18.2 2.67657 17.9503C2.30282 17.7006 2.01152 17.3456 1.83951 16.9303C1.66749 16.515 1.62248 16.0581 1.71017 15.6172C1.79787 15.1763 2.01432 14.7714 2.33217 14.4535C2.65002 14.1357 3.05498 13.9192 3.49584 13.8315C3.93671 13.7438 4.39368 13.7889 4.80897 13.9609C5.22425 14.1329 5.5792 14.4242 5.82894 14.7979C6.07867 15.1717 6.21196 15.6111 6.21196 16.0606C6.21196 16.6634 5.97251 17.2414 5.54629 17.6677C5.12007 18.0939 4.542 18.3333 3.93923 18.3333Z" fill="#6938EF"/>
|
||||
<path d="M9.99978 7.72726C9.55028 7.72726 9.11087 7.86056 8.73712 8.11029C8.36337 8.36002 8.07207 8.71497 7.90005 9.13026C7.72804 9.54554 7.68303 10.0025 7.77072 10.4434C7.85842 10.8842 8.07487 11.2892 8.39272 11.6071C8.71056 11.9249 9.11553 12.1414 9.55639 12.229C9.99726 12.3167 10.4542 12.2717 10.8695 12.0997C11.2848 11.9277 11.6398 11.6364 11.8895 11.2627C12.1392 10.8889 12.2725 10.4495 12.2725 9.99999C12.2725 9.39723 12.0331 8.81915 11.6068 8.39293C11.1806 7.96671 10.6025 7.72726 9.99978 7.72726Z" fill="#6938EF"/>
|
||||
<path d="M3.93923 1.66666C3.48973 1.66666 3.05032 1.79995 2.67657 2.04968C2.30282 2.29941 2.01152 2.65436 1.83951 3.06965C1.66749 3.48494 1.62248 3.9419 1.71017 4.38277C1.79787 4.82364 2.01432 5.2286 2.33217 5.54644C2.65002 5.86429 3.05498 6.08075 3.49585 6.16844C3.93671 6.25613 4.39368 6.21113 4.80897 6.03911C5.22425 5.86709 5.57921 5.57579 5.82894 5.20204C6.07867 4.8283 6.21196 4.38889 6.21196 3.93938C6.21196 3.33662 5.97251 2.75854 5.54629 2.33232C5.12007 1.9061 4.542 1.66666 3.93923 1.66666Z" fill="#6938EF"/>
|
||||
<path d="M16.0603 1.66666C15.6108 1.66666 15.1714 1.79995 14.7977 2.04968C14.4239 2.29941 14.1326 2.65436 13.9606 3.06965C13.7886 3.48494 13.7436 3.9419 13.8313 4.38277C13.919 4.82364 14.1354 5.2286 14.4533 5.54644C14.7711 5.86429 15.1761 6.08075 15.6169 6.16844C16.0578 6.25613 16.5148 6.21113 16.9301 6.03911C17.3453 5.86709 17.7003 5.57579 17.95 5.20204C18.1998 4.8283 18.3331 4.38889 18.3331 3.93938C18.3331 3.33662 18.0936 2.75854 17.6674 2.33232C17.2412 1.9061 16.6631 1.66666 16.0603 1.66666Z" fill="#6938EF"/>
|
||||
<path d="M16.0603 13.7879C15.6108 13.7879 15.1714 13.9212 14.7977 14.1709C14.4239 14.4206 14.1326 14.7756 13.9606 15.1909C13.7886 15.6062 13.7436 16.0631 13.8313 16.504C13.919 16.9449 14.1354 17.3498 14.4533 17.6677C14.7711 17.9855 15.1761 18.202 15.6169 18.2897C16.0578 18.3774 16.5148 18.3323 16.9301 18.1603C17.3453 17.9883 17.7003 17.697 17.95 17.3233C18.1998 16.9495 18.3331 16.5101 18.3331 16.0606C18.3331 15.4578 18.0936 14.8798 17.6674 14.4535C17.2412 14.0273 16.6631 13.7879 16.0603 13.7879Z" fill="#6938EF"/>
|
||||
<path d="M6.21196 7.72726H1.6665V12.2727H6.21196V7.72726Z" fill="#6938EF"/>
|
||||
<path d="M18.3331 7.72726H13.7876V12.2727H18.3331V7.72726Z" fill="#6938EF"/>
|
||||
<path d="M12.2725 1.66666H7.72705V6.21211H12.2725V1.66666Z" fill="#6938EF"/>
|
||||
<path d="M12.2725 13.7879H7.72705V18.3333H12.2725V13.7879Z" fill="#6938EF"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.91672 15.2028V17.9805H6.52783V15.2028H7.91672Z" fill="#444CE7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1667 15.2028V17.9805H12.7778V15.2028H14.1667Z" fill="#444CE7"/>
|
||||
<path d="M14.1666 2.0083C14.1666 3.54243 12.923 4.78608 11.3889 4.78608C9.85476 4.78608 8.61108 3.54243 8.61108 2.0083L14.1666 2.0083Z" fill="#444CE7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.8864 5.23918C6.9718 4.92907 8.12598 5.30791 8.81883 6.17498H13.1251C16.0015 6.17498 18.3334 8.50683 18.3334 11.3833C18.3334 14.2598 16.0015 16.5916 13.1251 16.5916H7.39252C6.14908 16.5916 4.97062 16.0363 4.1791 15.0773L3.32342 14.0407L1.66675 13.3448V9.93061L3.65692 9.40957L4.44453 8.40703V5.65114L5.8864 5.23918ZM8.61119 8.25831H14.1667V9.64721H8.61119V8.25831ZM6.52786 9.99443C6.52786 10.5697 6.06149 11.0361 5.48619 11.0361C4.91089 11.0361 4.44453 10.5697 4.44453 9.99443C4.44453 9.41915 4.91089 8.95276 5.48619 8.95276C6.06149 8.95276 6.52786 9.41915 6.52786 9.99443Z" fill="#444CE7"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="progress-indicator">
|
||||
<g id="Vector">
|
||||
<path d="M18.4029 10.7639H1.59738C1.17572 10.7639 0.833496 11.1061 0.833496 11.5278V16.1111C0.833496 16.5328 1.17572 16.875 1.59738 16.875H18.4029C18.8246 16.875 19.1668 16.5328 19.1668 16.1111V11.5278C19.1668 11.1061 18.8246 10.7639 18.4029 10.7639ZM17.6391 15.3472H10.0002V12.2917H17.6391V15.3472Z" fill="#1570EF"/>
|
||||
<path d="M9.716 7.58153C9.78933 7.66174 9.89169 7.70833 10.0002 7.70833C10.1086 7.70833 10.211 7.6625 10.2843 7.58153L13.7218 3.76208C13.8227 3.64979 13.8479 3.48937 13.7868 3.35111C13.7249 3.21361 13.5881 3.125 13.4377 3.125H6.56266C6.41218 3.125 6.27544 3.21361 6.21356 3.35111C6.15245 3.48937 6.17766 3.64979 6.2785 3.76208L9.716 7.58153Z" fill="#1570EF"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 835 B |
@@ -1,13 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="rerank">
|
||||
<g id="Vector">
|
||||
<path d="M18.3333 4.58329C18.3333 5.73389 17.4005 6.66663 16.2499 6.66663C15.0993 6.66663 14.1666 5.73389 14.1666 4.58329C14.1666 3.4327 15.0993 2.49996 16.2499 2.49996C17.4005 2.49996 18.3333 3.4327 18.3333 4.58329Z" fill="#0E9384"/>
|
||||
<path d="M13.3333 15.4166C13.3333 16.5672 12.4005 17.5 11.2499 17.5C10.0993 17.5 9.16658 16.5672 9.16658 15.4166C9.16658 14.266 10.0993 13.3333 11.2499 13.3333C12.4005 13.3333 13.3333 14.266 13.3333 15.4166Z" fill="#0E9384"/>
|
||||
<path d="M12.0833 4.58329C12.0833 5.27365 11.5236 5.83329 10.8333 5.83329C10.1429 5.83329 9.58325 5.27365 9.58325 4.58329C9.58325 3.89294 10.1429 3.33329 10.8333 3.33329C11.5236 3.33329 12.0833 3.89294 12.0833 4.58329Z" fill="#0E9384"/>
|
||||
<path d="M17.4999 15.4166C17.4999 16.107 16.9403 16.6666 16.2499 16.6666C15.5596 16.6666 14.9999 16.107 14.9999 15.4166C14.9999 14.7263 15.5596 14.1666 16.2499 14.1666C16.9403 14.1666 17.4999 14.7263 17.4999 15.4166Z" fill="#0E9384"/>
|
||||
<path d="M7.49992 15.4166C7.49992 17.0275 6.19408 18.3333 4.58325 18.3333C2.97242 18.3333 1.66659 17.0275 1.66659 15.4166C1.66659 13.8058 2.97242 12.5 4.58325 12.5C6.19408 12.5 7.49992 13.8058 7.49992 15.4166Z" fill="#0E9384"/>
|
||||
<path d="M7.49992 4.58329C7.49992 6.19412 6.19408 7.49996 4.58325 7.49996C2.97242 7.49996 1.66659 6.19412 1.66659 4.58329C1.66659 2.97246 2.97242 1.66663 4.58325 1.66663C6.19408 1.66663 7.49992 2.97246 7.49992 4.58329Z" fill="#0E9384"/>
|
||||
<path d="M0.833252 9.99996C0.833252 9.53972 1.20635 9.16663 1.66659 9.16663H18.3333C18.7935 9.16663 19.1666 9.53972 19.1666 9.99996C19.1666 10.4602 18.7935 10.8333 18.3333 10.8333H1.66659C1.20635 10.8333 0.833252 10.4602 0.833252 9.99996Z" fill="#0E9384"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.6752 4.83333H9.91553V1.07361L13.6752 4.83333Z" fill="#6938EF"/>
|
||||
<path d="M7.2003 13.8611H2.62391C2.53183 13.8611 2.44351 13.8245 2.37839 13.7594C2.31327 13.6943 2.27669 13.606 2.27669 13.5139V12.8195C2.27669 12.7274 2.31327 12.6391 2.37839 12.5739C2.44351 12.5088 2.53183 12.4722 2.62391 12.4722H7.2003C7.33176 11.6964 7.68097 10.9739 8.20725 10.3889H2.62391C2.53183 10.3889 2.44351 10.3523 2.37839 10.2872C2.31327 10.2221 2.27669 10.1338 2.27669 10.0417V9.34724C2.27669 9.25515 2.31327 9.16684 2.37839 9.10172C2.44351 9.0366 2.53183 9.00002 2.62391 9.00002H11.3045C12.3309 9.0003 13.3207 9.38137 14.0822 10.0695V6.22224H9.22114C9.03696 6.22224 8.86032 6.14908 8.73009 6.01884C8.59986 5.88861 8.52669 5.71198 8.52669 5.5278V0.666687H0.887804C0.703626 0.666687 0.526991 0.739851 0.396757 0.870085C0.266524 1.00032 0.193359 1.17695 0.193359 1.36113V16.6389C0.193359 16.8231 0.266524 16.9997 0.396757 17.13C0.526991 17.2602 0.703626 17.3334 0.887804 17.3334H10.61C9.73337 17.224 8.91945 16.8214 8.30046 16.1911C7.68146 15.5607 7.29375 14.7396 7.2003 13.8611ZM2.62391 5.5278H6.09614C6.18823 5.5278 6.27654 5.56438 6.34166 5.6295C6.40678 5.69461 6.44336 5.78293 6.44336 5.87502V6.56947C6.44336 6.66155 6.40678 6.74987 6.34166 6.81499C6.27654 6.88011 6.18823 6.91669 6.09614 6.91669H2.62391C2.53183 6.91669 2.44351 6.88011 2.37839 6.81499C2.31327 6.74987 2.27669 6.66155 2.27669 6.56947V5.87502C2.27669 5.78293 2.31327 5.69461 2.37839 5.6295C2.44351 5.56438 2.53183 5.5278 2.62391 5.5278Z" fill="#6938EF"/>
|
||||
<path d="M15.2678 16.1479L13.6887 14.5688C13.9439 14.1455 14.08 13.661 14.0824 13.1667C14.0824 12.6173 13.9195 12.0802 13.6143 11.6234C13.309 11.1666 12.8752 10.8106 12.3676 10.6004C11.8601 10.3901 11.3016 10.3351 10.7627 10.4423C10.2239 10.5495 9.72893 10.814 9.34045 11.2025C8.95197 11.591 8.68741 12.0859 8.58023 12.6248C8.47305 13.1636 8.52806 13.7221 8.7383 14.2297C8.94855 14.7373 9.30458 15.1711 9.76138 15.4763C10.2182 15.7816 10.7552 15.9445 11.3046 15.9445C11.799 15.9421 12.2834 15.806 12.7067 15.5507L14.2859 17.1299C14.4169 17.2564 14.5923 17.3264 14.7744 17.3248C14.9564 17.3232 15.1306 17.2502 15.2594 17.1214C15.3881 16.9927 15.4612 16.8185 15.4627 16.6364C15.4643 16.4543 15.3943 16.2789 15.2678 16.1479ZM9.91575 13.1667C9.91575 12.892 9.9972 12.6235 10.1498 12.3951C10.3024 12.1667 10.5193 11.9887 10.7731 11.8835C11.0269 11.7784 11.3062 11.7509 11.5756 11.8045C11.845 11.8581 12.0925 11.9904 12.2867 12.1846C12.481 12.3788 12.6132 12.6263 12.6668 12.8957C12.7204 13.1652 12.6929 13.4444 12.5878 13.6982C12.4827 13.952 12.3047 14.1689 12.0763 14.3215C11.8479 14.4741 11.5793 14.5556 11.3046 14.5556C10.9363 14.5556 10.583 14.4093 10.3225 14.1488C10.0621 13.8883 9.91575 13.5351 9.91575 13.1667Z" fill="#6938EF"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1,12 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.83317 18.3334H1.6665V14.1667H5.83317V18.3334Z" fill="#6938EF"/>
|
||||
<path d="M12.0832 12.0834H7.9165V7.91669H12.0832V12.0834Z" fill="#6938EF"/>
|
||||
<path d="M5.83317 12.0834H1.6665V7.91669H5.83317V12.0834Z" fill="#6938EF"/>
|
||||
<path d="M12.0832 5.83335H7.9165V1.66669H12.0832V5.83335Z" fill="#6938EF"/>
|
||||
<path d="M5.83317 5.83335H1.6665V1.66669H5.83317V5.83335Z" fill="#6938EF"/>
|
||||
<path d="M18.3332 5.83335H14.1665V1.66669H18.3332V5.83335Z" fill="#6938EF"/>
|
||||
<path d="M17.6386 14.8611H14.8608V17.6389H17.6386V14.8611Z" fill="#6938EF"/>
|
||||
<path d="M17.6386 8.61115H14.8608V11.3889H17.6386V8.61115Z" fill="#6938EF"/>
|
||||
<path d="M11.3886 14.8611H8.61084V17.6389H11.3886V14.8611Z" fill="#6938EF"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 835 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.0002 0.833374C10.4604 0.833374 10.8335 1.20647 10.8335 1.66671V2.54597C11.5977 2.63056 12.328 2.8301 13.0061 3.12703L13.4452 2.36666C13.6752 1.96808 14.1849 1.83152 14.5835 2.06164C14.9821 2.29176 15.1186 2.80142 14.8885 3.2L14.4488 3.96146C15.0552 4.40877 15.5915 4.94516 16.0388 5.55143L16.8003 5.11177C17.1989 4.88165 17.7086 5.01821 17.9387 5.41679C18.1688 5.81537 18.0322 6.32502 17.6337 6.55514L16.8732 6.99418C17.1702 7.67226 17.3697 8.40254 17.4543 9.16679H18.3335C18.7937 9.16679 19.1668 9.53987 19.1668 10.0001C19.1668 10.4604 18.7937 10.8335 18.3335 10.8335H17.4543C17.3697 11.5977 17.1702 12.328 16.8732 13.0061L17.6337 13.4452C18.0322 13.6753 18.1688 14.185 17.9387 14.5835C17.7086 14.9821 17.1989 15.1187 16.8003 14.8885L16.0388 14.4489C15.5915 15.0551 15.0551 15.5915 14.4488 16.0388L14.8885 16.8004C15.1186 17.1989 14.9821 17.7085 14.5835 17.9387C14.1849 18.1688 13.6752 18.0322 13.4452 17.6337L13.0061 16.8732C12.328 17.1701 11.5977 17.3697 10.8335 17.4543V18.3334C10.8335 18.7936 10.4604 19.1667 10.0002 19.1667C9.53991 19.1667 9.16683 18.7936 9.16683 18.3334V17.4543C8.40258 17.3697 7.6723 17.1701 6.99424 16.8732L6.55516 17.6337C6.32505 18.0323 5.81539 18.1689 5.41681 17.9388C5.01824 17.7086 4.88167 17.199 5.11179 16.8005L5.55149 16.0388C4.94519 15.5915 4.40878 15.0551 3.96145 14.4488L3.19993 14.8885C2.80135 15.1186 2.2917 14.982 2.06158 14.5835C1.83145 14.1849 1.96802 13.6752 2.3666 13.4451L3.12704 13.006C2.83011 12.328 2.63056 11.5977 2.54598 10.8335L1.66679 10.8334C1.20655 10.8334 0.833474 10.4602 0.833496 10C0.833521 9.53979 1.20663 9.16671 1.66687 9.16671L2.54599 9.16679C2.63058 8.40254 2.8301 7.67229 3.12701 6.99424L2.3666 6.55523C1.96802 6.32512 1.83145 5.81546 2.06157 5.41687C2.29169 5.0183 2.80135 4.88173 3.19992 5.11185L3.96142 5.55148C4.40874 4.94518 4.94515 4.40877 5.55145 3.96144L5.11179 3.19991C4.88167 2.80133 5.01823 2.29167 5.41681 2.06156C5.81539 1.83144 6.32505 1.968 6.55516 2.36657L6.9942 3.12702C7.67228 2.83009 8.40258 2.63055 9.16683 2.54597V1.66671C9.16683 1.20647 9.53991 0.833374 10.0002 0.833374ZM6.39156 5.41655C5.81089 5.87442 5.31917 6.44029 4.94695 7.08361C4.45095 7.94087 4.16681 8.93604 4.16681 10.0001C4.16681 11.0642 4.45096 12.0594 4.94698 12.9167C5.3192 13.56 5.81091 14.1259 6.39159 14.5837L8.1 11.6246C7.72651 11.1881 7.50015 10.6208 7.50015 10.0001C7.50015 9.37946 7.72651 8.81212 8.09999 8.37562L6.39156 5.41655ZM9.54316 7.54194L7.83418 4.5819C8.50325 4.31416 9.23383 4.16679 10.0002 4.16679C11.0642 4.16679 12.0594 4.45095 12.9167 4.94697C13.8022 5.45932 14.541 6.19807 15.0533 7.08357C15.4173 7.71277 15.6673 8.41629 15.7745 9.16679H12.3579C12.0147 8.19579 11.0887 7.50012 10.0002 7.50012C9.84433 7.50012 9.69149 7.51446 9.54316 7.54194ZM12.3579 10.8335C12.0147 11.8045 11.0887 12.5001 10.0002 12.5001C9.84433 12.5001 9.69149 12.4858 9.54316 12.4583L7.8342 15.4184C8.50325 15.6861 9.23383 15.8335 10.0002 15.8335C11.0642 15.8335 12.0594 15.5493 12.9167 15.0533C13.8022 14.541 14.5409 13.8022 15.0532 12.9167C15.4173 12.2875 15.6673 11.584 15.7745 10.8335H12.3579Z" fill="#444CE7"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -14,7 +14,24 @@
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sourceItem.error {
|
||||
background: #FEE4E2;
|
||||
}
|
||||
.sourceItem.success {
|
||||
background: #D1FADF;
|
||||
}
|
||||
.progressbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background-color: #B2CCFF;
|
||||
}
|
||||
.sourceItem .info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
}
|
||||
.sourceItem .info .name {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
@@ -38,6 +55,13 @@
|
||||
color: #05603A;
|
||||
}
|
||||
|
||||
|
||||
.cost {
|
||||
@apply flex justify-between items-center text-xs text-gray-700;
|
||||
}
|
||||
.embeddingStatus {
|
||||
@apply flex items-center justify-between text-gray-900 font-medium text-sm mr-2;
|
||||
}
|
||||
.commonIcon {
|
||||
@apply w-3 h-3 mr-1 inline-block align-middle;
|
||||
}
|
||||
@@ -57,33 +81,35 @@
|
||||
@apply text-xs font-medium;
|
||||
}
|
||||
|
||||
.unknownFileIcon {
|
||||
.fileIcon {
|
||||
@apply w-4 h-4 mr-1 bg-center bg-no-repeat;
|
||||
background-image: url(../assets/unknown.svg);
|
||||
background-size: 16px;
|
||||
}
|
||||
.csv {
|
||||
.fileIcon.csv {
|
||||
background-image: url(../assets/csv.svg);
|
||||
}
|
||||
.docx {
|
||||
.fileIcon.docx {
|
||||
background-image: url(../assets/docx.svg);
|
||||
}
|
||||
.xlsx,
|
||||
.xls {
|
||||
.fileIcon.xlsx,
|
||||
.fileIcon.xls {
|
||||
background-image: url(../assets/xlsx.svg);
|
||||
}
|
||||
.pdf {
|
||||
.fileIcon.pdf {
|
||||
background-image: url(../assets/pdf.svg);
|
||||
}
|
||||
.html,
|
||||
.htm {
|
||||
.fileIcon.html,
|
||||
.fileIcon.htm {
|
||||
background-image: url(../assets/html.svg);
|
||||
}
|
||||
.md,
|
||||
.markdown {
|
||||
.fileIcon.md,
|
||||
.fileIcon.markdown {
|
||||
background-image: url(../assets/md.svg);
|
||||
}
|
||||
.txt {
|
||||
.fileIcon.txt {
|
||||
background-image: url(../assets/txt.svg);
|
||||
}
|
||||
.json {
|
||||
.fileIcon.json {
|
||||
background-image: url(../assets/json.svg);
|
||||
}
|
||||
|
||||
@@ -6,44 +6,32 @@ import { useTranslation } from 'react-i18next'
|
||||
import { omit } from 'lodash-es'
|
||||
import { ArrowRightIcon } from '@heroicons/react/24/solid'
|
||||
import {
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningFill,
|
||||
RiLoader2Fill,
|
||||
RiTerminalBoxLine,
|
||||
} from '@remixicon/react'
|
||||
import Image from 'next/image'
|
||||
import { indexMethodIcon, retrievalIcon } from '../icons'
|
||||
import { IndexingType } from '../step-two'
|
||||
import DocumentFileIcon from '../../common/document-file-icon'
|
||||
import s from './index.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import { FieldInfo } from '@/app/components/datasets/documents/detail/metadata'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { FullDocumentDetail, IndexingStatusResponse, ProcessRuleResponse } from '@/models/datasets'
|
||||
import { fetchIndexingStatusBatch as doFetchIndexingStatus, fetchProcessRule } from '@/service/datasets'
|
||||
import { DataSourceType, ProcessMode } from '@/models/datasets'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import NotionIcon from '@/app/components/base/notion-icon'
|
||||
import PriorityLabel from '@/app/components/billing/priority-label'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { ZapFast } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { sleep } from '@/utils'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { sleep } from '@/utils'
|
||||
|
||||
type Props = {
|
||||
datasetId: string
|
||||
batchId: string
|
||||
documents?: FullDocumentDetail[]
|
||||
indexingType?: string
|
||||
retrievalMethod?: string
|
||||
}
|
||||
|
||||
const RuleDetail: FC<{
|
||||
sourceData?: ProcessRuleResponse
|
||||
indexingType?: string
|
||||
retrievalMethod?: string
|
||||
}> = ({ sourceData, indexingType, retrievalMethod }) => {
|
||||
const RuleDetail: FC<{ sourceData?: ProcessRuleResponse }> = ({ sourceData }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const segmentationRuleMap = {
|
||||
@@ -63,47 +51,29 @@ const RuleDetail: FC<{
|
||||
return t('datasetCreation.stepTwo.removeStopwords')
|
||||
}
|
||||
|
||||
const isNumber = (value: unknown) => {
|
||||
return typeof value === 'number'
|
||||
}
|
||||
|
||||
const getValue = useCallback((field: string) => {
|
||||
let value: string | number | undefined = '-'
|
||||
const maxTokens = isNumber(sourceData?.rules?.segmentation?.max_tokens)
|
||||
? sourceData.rules.segmentation.max_tokens
|
||||
: value
|
||||
const childMaxTokens = isNumber(sourceData?.rules?.subchunk_segmentation?.max_tokens)
|
||||
? sourceData.rules.subchunk_segmentation.max_tokens
|
||||
: value
|
||||
switch (field) {
|
||||
case 'mode':
|
||||
value = !sourceData?.mode
|
||||
? value
|
||||
: sourceData.mode === ProcessMode.general
|
||||
? (t('datasetDocuments.embedding.custom') as string)
|
||||
: `${t('datasetDocuments.embedding.hierarchical')} · ${sourceData?.rules?.parent_mode === 'paragraph'
|
||||
? t('dataset.parentMode.paragraph')
|
||||
: t('dataset.parentMode.fullDoc')}`
|
||||
value = sourceData?.mode === 'automatic' ? (t('datasetDocuments.embedding.automatic') as string) : (t('datasetDocuments.embedding.custom') as string)
|
||||
break
|
||||
case 'segmentLength':
|
||||
value = !sourceData?.mode
|
||||
? value
|
||||
: sourceData.mode === ProcessMode.general
|
||||
? maxTokens
|
||||
: `${t('datasetDocuments.embedding.parentMaxTokens')} ${maxTokens}; ${t('datasetDocuments.embedding.childMaxTokens')} ${childMaxTokens}`
|
||||
value = sourceData?.rules?.segmentation?.max_tokens
|
||||
break
|
||||
default:
|
||||
value = !sourceData?.mode
|
||||
? value
|
||||
: sourceData?.rules?.pre_processing_rules?.filter(rule =>
|
||||
rule.enabled).map(rule => getRuleName(rule.id)).join(',')
|
||||
value = sourceData?.mode === 'automatic'
|
||||
? (t('datasetDocuments.embedding.automatic') as string)
|
||||
// eslint-disable-next-line array-callback-return
|
||||
: sourceData?.rules?.pre_processing_rules?.map((rule) => {
|
||||
if (rule.enabled)
|
||||
return getRuleName(rule.id)
|
||||
}).filter(Boolean).join(';')
|
||||
break
|
||||
}
|
||||
return value
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sourceData])
|
||||
|
||||
return <div className='flex flex-col gap-1'>
|
||||
return <div className='flex flex-col pt-8 pb-10 first:mt-0'>
|
||||
{Object.keys(segmentationRuleMap).map((field) => {
|
||||
return <FieldInfo
|
||||
key={field}
|
||||
@@ -111,43 +81,10 @@ const RuleDetail: FC<{
|
||||
displayedValue={String(getValue(field))}
|
||||
/>
|
||||
})}
|
||||
<FieldInfo
|
||||
label={t('datasetCreation.stepTwo.indexMode')}
|
||||
displayedValue={t(`datasetCreation.stepTwo.${indexingType === IndexingType.ECONOMICAL ? 'economical' : 'qualified'}`) as string}
|
||||
valueIcon={
|
||||
<Image
|
||||
className='size-4'
|
||||
src={
|
||||
indexingType === IndexingType.ECONOMICAL
|
||||
? indexMethodIcon.economical
|
||||
: indexMethodIcon.high_quality
|
||||
}
|
||||
alt=''
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FieldInfo
|
||||
label={t('datasetSettings.form.retrievalSetting.title')}
|
||||
// displayedValue={t(`datasetSettings.form.retrievalSetting.${retrievalMethod}`) as string}
|
||||
displayedValue={t(`dataset.retrieval.${indexingType === IndexingType.ECONOMICAL ? 'invertedIndex' : retrievalMethod}.title`) as string}
|
||||
valueIcon={
|
||||
<Image
|
||||
className='size-4'
|
||||
src={
|
||||
retrievalMethod === RETRIEVE_METHOD.fullText
|
||||
? retrievalIcon.fullText
|
||||
: retrievalMethod === RETRIEVE_METHOD.hybrid
|
||||
? retrievalIcon.hybrid
|
||||
: retrievalIcon.vector
|
||||
}
|
||||
alt=''
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], indexingType, retrievalMethod }) => {
|
||||
const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], indexingType }) => {
|
||||
const { t } = useTranslation()
|
||||
const { enableBilling, plan } = useProviderContext()
|
||||
|
||||
@@ -190,7 +127,6 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsStopQuery(false)
|
||||
startQueryStatus()
|
||||
return () => {
|
||||
stopQueryStatus()
|
||||
@@ -210,9 +146,6 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
|
||||
const navToDocumentList = () => {
|
||||
router.push(`/datasets/${datasetId}/documents`)
|
||||
}
|
||||
const navToApiDocs = () => {
|
||||
router.push('/datasets?category=api')
|
||||
}
|
||||
|
||||
const isEmbedding = useMemo(() => {
|
||||
return indexingStatusBatchDetail.some(indexingStatusDetail => ['indexing', 'splitting', 'parsing', 'cleaning'].includes(indexingStatusDetail?.indexing_status || ''))
|
||||
@@ -244,17 +177,13 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
|
||||
|
||||
return doc?.data_source_info.notion_page_icon
|
||||
}
|
||||
const isSourceEmbedding = (detail: IndexingStatusResponse) =>
|
||||
['indexing', 'splitting', 'parsing', 'cleaning', 'waiting'].includes(detail.indexing_status || '')
|
||||
const isSourceEmbedding = (detail: IndexingStatusResponse) => ['indexing', 'splitting', 'parsing', 'cleaning', 'waiting'].includes(detail.indexing_status || '')
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-5 flex items-center mb-3">
|
||||
<div className="flex items-center justify-between text-gray-900 font-medium text-sm mr-2">
|
||||
{isEmbedding && <div className='flex items-center'>
|
||||
<RiLoader2Fill className='size-4 mr-1 animate-spin' />
|
||||
{t('datasetDocuments.embedding.processing')}
|
||||
</div>}
|
||||
<div className='h-5 flex items-center mb-5'>
|
||||
<div className={s.embeddingStatus}>
|
||||
{isEmbedding && t('datasetDocuments.embedding.processing')}
|
||||
{isEmbeddingCompleted && t('datasetDocuments.embedding.completed')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,80 +200,69 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="flex flex-col gap-0.5 pb-2">
|
||||
<div className={s.progressContainer}>
|
||||
{indexingStatusBatchDetail.map(indexingStatusDetail => (
|
||||
<div key={indexingStatusDetail.id} className={cn(
|
||||
'relative h-[26px] bg-components-progress-bar-bg rounded-md overflow-hidden',
|
||||
indexingStatusDetail.indexing_status === 'error' && 'bg-state-destructive-hover-alt',
|
||||
// indexingStatusDetail.indexing_status === 'completed' && 's.success',
|
||||
s.sourceItem,
|
||||
indexingStatusDetail.indexing_status === 'error' && s.error,
|
||||
indexingStatusDetail.indexing_status === 'completed' && s.success,
|
||||
)}>
|
||||
{isSourceEmbedding(indexingStatusDetail) && (
|
||||
<div className="absolute top-0 left-0 h-full min-w-0.5 bg-components-progress-bar-progress border-r-[2px] border-r-components-progress-bar-progress-highlight" style={{ width: `${getSourcePercent(indexingStatusDetail)}%` }} />
|
||||
<div className={s.progressbar} style={{ width: `${getSourcePercent(indexingStatusDetail)}%` }} />
|
||||
)}
|
||||
<div className="flex gap-1 pl-[6px] pr-2 h-full items-center z-[1]">
|
||||
<div className={`${s.info} grow`}>
|
||||
{getSourceType(indexingStatusDetail.id) === DataSourceType.FILE && (
|
||||
// <div className={cn(
|
||||
// 'shrink-0 marker:size-4 bg-center bg-no-repeat bg-contain',
|
||||
// s[getFileType(getSourceName(indexingStatusDetail.id))] || s.unknownFileIcon,
|
||||
// )} />
|
||||
<DocumentFileIcon
|
||||
className="shrink-0 size-4"
|
||||
name={getSourceName(indexingStatusDetail.id)}
|
||||
extension={getFileType(getSourceName(indexingStatusDetail.id))}
|
||||
/>
|
||||
<div className={cn(s.fileIcon, s[getFileType(getSourceName(indexingStatusDetail.id))])} />
|
||||
)}
|
||||
{getSourceType(indexingStatusDetail.id) === DataSourceType.NOTION && (
|
||||
<NotionIcon
|
||||
className='shrink-0'
|
||||
className='shrink-0 mr-1'
|
||||
type='page'
|
||||
src={getIcon(indexingStatusDetail.id)}
|
||||
/>
|
||||
)}
|
||||
<div className="grow flex items-center gap-1 w-0" title={getSourceName(indexingStatusDetail.id)}>
|
||||
<div className="text-xs truncate">
|
||||
{getSourceName(indexingStatusDetail.id)}
|
||||
</div>
|
||||
{
|
||||
enableBilling && (
|
||||
<PriorityLabel className='ml-0' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className={`${s.name} truncate`} title={getSourceName(indexingStatusDetail.id)}>{getSourceName(indexingStatusDetail.id)}</div>
|
||||
{
|
||||
enableBilling && (
|
||||
<PriorityLabel />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='shrink-0'>
|
||||
{isSourceEmbedding(indexingStatusDetail) && (
|
||||
<div className="shrink-0 text-xs">{`${getSourcePercent(indexingStatusDetail)}%`}</div>
|
||||
<div className={s.percent}>{`${getSourcePercent(indexingStatusDetail)}%`}</div>
|
||||
)}
|
||||
{indexingStatusDetail.indexing_status === 'error' && (
|
||||
{indexingStatusDetail.indexing_status === 'error' && indexingStatusDetail.error && (
|
||||
<Tooltip
|
||||
popupClassName='px-4 py-[14px] max-w-60 text-sm leading-4 text-text-secondary border-[0.5px] border-components-panel-border rounded-xl'
|
||||
offset={4}
|
||||
popupContent={indexingStatusDetail.error}
|
||||
popupContent={(
|
||||
<div className='max-w-[400px]'>
|
||||
{indexingStatusDetail.error}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
<RiErrorWarningFill className='shrink-0 size-4 text-text-destructive' />
|
||||
</span>
|
||||
<div className={cn(s.percent, s.error, 'flex items-center')}>
|
||||
Error
|
||||
<RiErrorWarningFill className='ml-1 w-4 h-4' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{indexingStatusDetail.indexing_status === 'error' && !indexingStatusDetail.error && (
|
||||
<div className={cn(s.percent, s.error, 'flex items-center')}>
|
||||
Error
|
||||
</div>
|
||||
)}
|
||||
{indexingStatusDetail.indexing_status === 'completed' && (
|
||||
<RiCheckboxCircleFill className='shrink-0 size-4 text-text-success' />
|
||||
<div className={cn(s.percent, s.success)}>100%</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<hr className="my-3 h-[1px] bg-divider-subtle border-0" />
|
||||
<RuleDetail
|
||||
sourceData={ruleDetail}
|
||||
indexingType={indexingType}
|
||||
retrievalMethod={retrievalMethod}
|
||||
/>
|
||||
<div className='flex items-center gap-2 my-10'>
|
||||
<Button className='w-fit' onClick={navToApiDocs}>
|
||||
<RiTerminalBoxLine className='size-4 mr-2' />
|
||||
<span>Access the API</span>
|
||||
</Button>
|
||||
<RuleDetail sourceData={ruleDetail} />
|
||||
<div className='flex items-center gap-2 mt-10'>
|
||||
<Button className='w-fit' variant='primary' onClick={navToDocumentList}>
|
||||
<span>{t('datasetCreation.stepThree.navTo')}</span>
|
||||
<ArrowRightIcon className='size-4 ml-2 stroke-current stroke-1' />
|
||||
<ArrowRightIcon className='h-4 w-4 ml-2 stroke-current stroke-1' />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.filePreview {
|
||||
@apply flex flex-col border-l border-gray-200 shrink-0;
|
||||
width: 100%;
|
||||
width: 528px;
|
||||
background-color: #fcfcfd;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,5 @@
|
||||
}
|
||||
.fileContent {
|
||||
white-space: pre-line;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ const FilePreview = ({
|
||||
}, [file])
|
||||
|
||||
return (
|
||||
<div className={cn(s.filePreview, 'h-full')}>
|
||||
<div className={cn(s.filePreview)}>
|
||||
<div className={cn(s.previewHeader)}>
|
||||
<div className={cn(s.title)}>
|
||||
<span>{t('datasetCreation.stepOne.filePreview')}</span>
|
||||
@@ -59,7 +59,7 @@ const FilePreview = ({
|
||||
<div className={cn(s.previewContent)}>
|
||||
{loading && <div className={cn(s.loading)} />}
|
||||
{!loading && (
|
||||
<div className={cn(s.fileContent, 'body-md-regular')}>{previewContent}</div>
|
||||
<div className={cn(s.fileContent)}>{previewContent}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,68 @@
|
||||
.fileUploader {
|
||||
@apply mb-6;
|
||||
}
|
||||
|
||||
.fileUploader .title {
|
||||
@apply mb-2;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #344054;
|
||||
}
|
||||
|
||||
.fileUploader .tip {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.uploader {
|
||||
@apply relative box-border flex justify-center items-center mb-2 p-3;
|
||||
flex-direction: column;
|
||||
max-width: 640px;
|
||||
min-height: 80px;
|
||||
background: #F9FAFB;
|
||||
border: 1px dashed #EAECF0;
|
||||
border-radius: 12px;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.uploader.dragging {
|
||||
background: #F5F8FF;
|
||||
border: 1px dashed #B2CCFF;
|
||||
}
|
||||
|
||||
.uploader .draggingCover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.uploader .uploadIcon {
|
||||
content: '';
|
||||
display: block;
|
||||
margin-right: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: center no-repeat url(../assets/upload-cloud-01.svg);
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.uploader .browse {
|
||||
@apply pl-1 cursor-pointer;
|
||||
color: #155eef;
|
||||
}
|
||||
|
||||
.fileList {
|
||||
@apply space-y-2;
|
||||
}
|
||||
|
||||
.file {
|
||||
@apply box-border relative flex items-center justify-between;
|
||||
padding: 8px 12px 8px 8px;
|
||||
@@ -128,4 +193,4 @@
|
||||
|
||||
.file:hover .actionWrapper .remove {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,10 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import useSWR from 'swr'
|
||||
import { RiDeleteBinLine, RiUploadCloud2Line } from '@remixicon/react'
|
||||
import DocumentFileIcon from '../../common/document-file-icon'
|
||||
import s from './index.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { CustomFile as File, FileItem } from '@/models/datasets'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import SimplePieChart from '@/app/components/base/simple-pie-chart'
|
||||
|
||||
import { upload } from '@/service/base'
|
||||
import { fetchFileUploadConfig } from '@/service/common'
|
||||
@@ -16,8 +14,6 @@ import { fetchSupportFileTypes } from '@/service/datasets'
|
||||
import I18n from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n/language'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { Theme } from '@/types/app'
|
||||
|
||||
const FILES_NUMBER_LIMIT = 20
|
||||
|
||||
@@ -226,9 +222,6 @@ const FileUploader = ({
|
||||
initialUpload(files.filter(isValid))
|
||||
}, [isValid, initialUpload])
|
||||
|
||||
const { theme } = useAppContext()
|
||||
const chartColor = useMemo(() => theme === Theme.dark ? '#5289ff' : '#296dff', [theme])
|
||||
|
||||
useEffect(() => {
|
||||
dropRef.current?.addEventListener('dragenter', handleDragEnter)
|
||||
dropRef.current?.addEventListener('dragover', handleDragOver)
|
||||
@@ -243,12 +236,12 @@ const FileUploader = ({
|
||||
}, [handleDrop])
|
||||
|
||||
return (
|
||||
<div className="mb-5 w-[640px]">
|
||||
<div className={s.fileUploader}>
|
||||
{!hideUpload && (
|
||||
<input
|
||||
ref={fileUploader}
|
||||
id="fileUploader"
|
||||
className="hidden"
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
multiple={!notSupportBatchUpload}
|
||||
accept={ACCEPTS.join(',')}
|
||||
@@ -256,71 +249,52 @@ const FileUploader = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={cn('text-text-tertiary text-sm font-semibold leading-6 mb-1', titleClassName)}>{t('datasetCreation.stepOne.uploader.title')}</div>
|
||||
|
||||
<div className={cn(s.title, titleClassName)}>{t('datasetCreation.stepOne.uploader.title')}</div>
|
||||
{!hideUpload && (
|
||||
<div ref={dropRef} className={cn('relative box-border flex flex-col justify-center items-center gap-1 mb-2 px-4 py-3 max-w-[640px] min-h-20 leading-4 text-xs text-text-tertiary bg-components-dropzone-bg border border-dashed border-components-dropzone-border rounded-xl', dragging && 'bg-components-dropzone-bg-accent border-components-dropzone-border-accent')}>
|
||||
<div className="flex justify-center items-center min-h-5 text-sm leading-4 text-text-secondary">
|
||||
<RiUploadCloud2Line className='mr-2 size-5' />
|
||||
|
||||
<div ref={dropRef} className={cn(s.uploader, dragging && s.dragging)}>
|
||||
<div className='flex justify-center items-center min-h-6 mb-2'>
|
||||
<span className={s.uploadIcon} />
|
||||
<span>
|
||||
{t('datasetCreation.stepOne.uploader.button')}
|
||||
{supportTypes.length > 0 && (
|
||||
<label className="ml-1 text-text-accent cursor-pointer" onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.browse')}</label>
|
||||
)}
|
||||
<label className={s.browse} onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.browse')}</label>
|
||||
</span>
|
||||
</div>
|
||||
<div>{t('datasetCreation.stepOne.uploader.tip', {
|
||||
<div className={s.tip}>{t('datasetCreation.stepOne.uploader.tip', {
|
||||
size: fileUploadConfig.file_size_limit,
|
||||
supportTypes: supportTypesShowNames,
|
||||
})}</div>
|
||||
{dragging && <div ref={dragRef} className='absolute top-0 left-0 w-full h-full' />}
|
||||
{dragging && <div ref={dragRef} className={s.draggingCover} />}
|
||||
</div>
|
||||
)}
|
||||
<div className='space-y-1 max-w-[640px] cursor-default'>
|
||||
|
||||
<div className={s.fileList}>
|
||||
{fileList.map((fileItem, index) => (
|
||||
<div
|
||||
key={`${fileItem.fileID}-${index}`}
|
||||
onClick={() => fileItem.file?.id && onPreview(fileItem.file)}
|
||||
className={cn(
|
||||
'flex items-center h-12 max-w-[640px] bg-components-panel-on-panel-item-bg text-xs leading-3 text-text-tertiary border border-components-panel-border rounded-lg shadow-xs',
|
||||
// 'border-state-destructive-border bg-state-destructive-hover',
|
||||
s.file,
|
||||
fileItem.progress < 100 && s.uploading,
|
||||
)}
|
||||
>
|
||||
<div className="shrink-0 flex justify-center items-center w-12">
|
||||
<DocumentFileIcon
|
||||
className="shrink-0 size-6"
|
||||
name={fileItem.file.name}
|
||||
extension={getFileType(fileItem.file)}
|
||||
/>
|
||||
{fileItem.progress < 100 && (
|
||||
<div className={s.progressbar} style={{ width: `${fileItem.progress}%` }} />
|
||||
)}
|
||||
<div className={s.fileInfo}>
|
||||
<div className={cn(s.fileIcon, s[getFileType(fileItem.file)])} />
|
||||
<div className={s.filename}>{fileItem.file.name}</div>
|
||||
<div className={s.size}>{getFileSize(fileItem.file.size)}</div>
|
||||
</div>
|
||||
<div className="grow shrink flex flex-col gap-0.5">
|
||||
<div className='flex w-full'>
|
||||
<div className="text-sm leading-4 text-text-secondary w-0 grow truncate">{fileItem.file.name}</div>
|
||||
</div>
|
||||
<div className="w-full leading-3 truncate text-text-tertiary">
|
||||
<span className='uppercase'>{getFileType(fileItem.file)}</span>
|
||||
<span className='px-1 text-text-quaternary'>·</span>
|
||||
<span>{getFileSize(fileItem.file.size)}</span>
|
||||
{/* <span className='px-1 text-text-quaternary'>·</span>
|
||||
<span>10k characters</span> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 flex items-center justify-end gap-1 pr-3 w-16">
|
||||
{/* <span className="flex justify-center items-center w-6 h-6 cursor-pointer">
|
||||
<RiErrorWarningFill className='size-4 text-text-warning' />
|
||||
</span> */}
|
||||
<div className={s.actionWrapper}>
|
||||
{(fileItem.progress < 100 && fileItem.progress >= 0) && (
|
||||
// <div className={s.percent}>{`${fileItem.progress}%`}</div>
|
||||
<SimplePieChart percentage={fileItem.progress} stroke={chartColor} fill={chartColor} animationDuration={0} />
|
||||
<div className={s.percent}>{`${fileItem.progress}%`}</div>
|
||||
)}
|
||||
{fileItem.progress === 100 && (
|
||||
<div className={s.remove} onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
removeFile(fileItem.fileID)
|
||||
}} />
|
||||
)}
|
||||
<span className="flex justify-center items-center w-6 h-6 cursor-pointer" onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
removeFile(fileItem.fileID)
|
||||
}}>
|
||||
<RiDeleteBinLine className='size-4 text-text-tertiary' />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import GoldIcon from './assets/gold.svg'
|
||||
import Piggybank from './assets/piggy-bank-mod.svg'
|
||||
import Selection from './assets/selection-mod.svg'
|
||||
import Research from './assets/research-mod.svg'
|
||||
import PatternRecognition from './assets/pattern-recognition-mod.svg'
|
||||
|
||||
export const indexMethodIcon = {
|
||||
high_quality: GoldIcon,
|
||||
economical: Piggybank,
|
||||
}
|
||||
|
||||
export const retrievalIcon = {
|
||||
vector: Selection,
|
||||
fullText: Research,
|
||||
hybrid: PatternRecognition,
|
||||
}
|
||||
@@ -3,10 +3,10 @@ import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppUnavailable from '../../base/app-unavailable'
|
||||
import { ModelTypeEnum } from '../../header/account-setting/model-provider-page/declarations'
|
||||
import StepsNavBar from './steps-nav-bar'
|
||||
import StepOne from './step-one'
|
||||
import StepTwo from './step-two'
|
||||
import StepThree from './step-three'
|
||||
import { Topbar } from './top-bar'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import type { CrawlOptions, CrawlResultItem, DataSet, FileItem, createDocumentResponse } from '@/models/datasets'
|
||||
import { fetchDataSource } from '@/service/common'
|
||||
@@ -36,7 +36,6 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
|
||||
const [dataSourceType, setDataSourceType] = useState<DataSourceType>(DataSourceType.FILE)
|
||||
const [step, setStep] = useState(1)
|
||||
const [indexingTypeCache, setIndexTypeCache] = useState('')
|
||||
const [retrievalMethodCache, setRetrievalMethodCache] = useState('')
|
||||
const [fileList, setFiles] = useState<FileItem[]>([])
|
||||
const [result, setResult] = useState<createDocumentResponse | undefined>()
|
||||
const [hasError, setHasError] = useState(false)
|
||||
@@ -81,9 +80,6 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
|
||||
const updateResultCache = (res?: createDocumentResponse) => {
|
||||
setResult(res)
|
||||
}
|
||||
const updateRetrievalMethodCache = (method: string) => {
|
||||
setRetrievalMethodCache(method)
|
||||
}
|
||||
|
||||
const nextStep = useCallback(() => {
|
||||
setStep(step + 1)
|
||||
@@ -122,29 +118,33 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
|
||||
return <AppUnavailable code={500} unknownReason={t('datasetCreation.error.unavailable') as string} />
|
||||
|
||||
return (
|
||||
<div className='flex flex-col bg-components-panel-bg' style={{ height: 'calc(100vh - 56px)' }}>
|
||||
<Topbar activeIndex={step - 1} />
|
||||
<div style={{ height: 'calc(100% - 52px)' }}>
|
||||
{step === 1 && <StepOne
|
||||
hasConnection={hasConnection}
|
||||
onSetting={() => setShowAccountSettingModal({ payload: 'data-source' })}
|
||||
datasetId={datasetId}
|
||||
dataSourceType={dataSourceType}
|
||||
dataSourceTypeDisable={!!detail?.data_source_type}
|
||||
changeType={setDataSourceType}
|
||||
files={fileList}
|
||||
updateFile={updateFile}
|
||||
updateFileList={updateFileList}
|
||||
notionPages={notionPages}
|
||||
updateNotionPages={updateNotionPages}
|
||||
onStepChange={nextStep}
|
||||
websitePages={websitePages}
|
||||
updateWebsitePages={setWebsitePages}
|
||||
onWebsiteCrawlProviderChange={setWebsiteCrawlProvider}
|
||||
onWebsiteCrawlJobIdChange={setWebsiteCrawlJobId}
|
||||
crawlOptions={crawlOptions}
|
||||
onCrawlOptionsChange={setCrawlOptions}
|
||||
/>}
|
||||
<div className='flex' style={{ height: 'calc(100vh - 56px)' }}>
|
||||
<div className="flex flex-col w-11 sm:w-56 overflow-y-auto bg-white border-r border-gray-200 shrink-0">
|
||||
<StepsNavBar step={step} datasetId={datasetId} />
|
||||
</div>
|
||||
<div className="grow bg-white">
|
||||
<div className={step === 1 ? 'block h-full' : 'hidden'}>
|
||||
<StepOne
|
||||
hasConnection={hasConnection}
|
||||
onSetting={() => setShowAccountSettingModal({ payload: 'data-source' })}
|
||||
datasetId={datasetId}
|
||||
dataSourceType={dataSourceType}
|
||||
dataSourceTypeDisable={!!detail?.data_source_type}
|
||||
changeType={setDataSourceType}
|
||||
files={fileList}
|
||||
updateFile={updateFile}
|
||||
updateFileList={updateFileList}
|
||||
notionPages={notionPages}
|
||||
updateNotionPages={updateNotionPages}
|
||||
onStepChange={nextStep}
|
||||
websitePages={websitePages}
|
||||
updateWebsitePages={setWebsitePages}
|
||||
onWebsiteCrawlProviderChange={setWebsiteCrawlProvider}
|
||||
onWebsiteCrawlJobIdChange={setWebsiteCrawlJobId}
|
||||
crawlOptions={crawlOptions}
|
||||
onCrawlOptionsChange={setCrawlOptions}
|
||||
/>
|
||||
</div>
|
||||
{(step === 2 && (!datasetId || (datasetId && !!detail))) && <StepTwo
|
||||
isAPIKeySet={!!embeddingsDefaultModel}
|
||||
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
|
||||
@@ -158,7 +158,6 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
|
||||
websiteCrawlJobId={websiteCrawlJobId}
|
||||
onStepChange={changeStep}
|
||||
updateIndexingTypeCache={updateIndexingTypeCache}
|
||||
updateRetrievalMethodCache={updateRetrievalMethodCache}
|
||||
updateResultCache={updateResultCache}
|
||||
crawlOptions={crawlOptions}
|
||||
/>}
|
||||
@@ -166,7 +165,6 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
|
||||
datasetId={datasetId}
|
||||
datasetName={detail?.name}
|
||||
indexingType={detail?.indexing_technique || indexingTypeCache}
|
||||
retrievalMethod={detail?.retrieval_model_dict?.search_method || retrievalMethodCache}
|
||||
creationCache={result}
|
||||
/>}
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@ const NotionPagePreview = ({
|
||||
}, [currentPage])
|
||||
|
||||
return (
|
||||
<div className={cn(s.filePreview, 'h-full')}>
|
||||
<div className={cn(s.filePreview)}>
|
||||
<div className={cn(s.previewHeader)}>
|
||||
<div className={cn(s.title)}>
|
||||
<span>{t('datasetCreation.stepOne.pagePreview')}</span>
|
||||
@@ -64,7 +64,7 @@ const NotionPagePreview = ({
|
||||
<div className={cn(s.previewContent)}>
|
||||
{loading && <div className={cn(s.loading)} />}
|
||||
{!loading && (
|
||||
<div className={cn(s.fileContent, 'body-md-regular')}>{previewContent}</div>
|
||||
<div className={cn(s.fileContent)}>{previewContent}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 42px 64px 12px 0;
|
||||
padding: 42px 64px 12px;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.form {
|
||||
position: relative;
|
||||
padding: 12px 64px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.dataSourceItem {
|
||||
@apply box-border relative grow shrink-0 flex items-center p-3 h-14 bg-white rounded-xl cursor-pointer;
|
||||
@apply box-border relative shrink-0 flex items-center mr-3 p-3 h-14 bg-white rounded-xl cursor-pointer;
|
||||
border: 0.5px solid #EAECF0;
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
font-weight: 500;
|
||||
@@ -22,32 +24,27 @@
|
||||
line-height: 20px;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.dataSourceItem:hover {
|
||||
background-color: #f5f8ff;
|
||||
border: 0.5px solid #B2CCFF;
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
}
|
||||
|
||||
.dataSourceItem.active {
|
||||
background-color: #f5f8ff;
|
||||
border: 1.5px solid #528BFF;
|
||||
box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
|
||||
}
|
||||
|
||||
.dataSourceItem.disabled {
|
||||
background-color: #f9fafb;
|
||||
border: 0.5px solid #EAECF0;
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dataSourceItem.disabled:hover {
|
||||
background-color: #f9fafb;
|
||||
border: 0.5px solid #EAECF0;
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
}
|
||||
|
||||
.comingTag {
|
||||
@apply flex justify-center items-center bg-white;
|
||||
position: absolute;
|
||||
@@ -62,7 +59,6 @@
|
||||
line-height: 18px;
|
||||
color: #444CE7;
|
||||
}
|
||||
|
||||
.datasetIcon {
|
||||
@apply flex mr-2 w-8 h-8 rounded-lg bg-center bg-no-repeat;
|
||||
background-color: #F5FAFF;
|
||||
@@ -70,18 +66,15 @@
|
||||
background-size: 16px;
|
||||
border: 0.5px solid #D1E9FF;
|
||||
}
|
||||
|
||||
.dataSourceItem:active .datasetIcon,
|
||||
.dataSourceItem:hover .datasetIcon {
|
||||
background-color: #F5F8FF;
|
||||
border: 0.5px solid #E0EAFF;
|
||||
}
|
||||
|
||||
.datasetIcon.notion {
|
||||
background-image: url(../assets/notion.svg);
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.datasetIcon.web {
|
||||
background-image: url(../assets/web.svg);
|
||||
}
|
||||
@@ -97,12 +90,29 @@
|
||||
background-color: #eaecf0;
|
||||
}
|
||||
|
||||
.OtherCreationOption {
|
||||
@apply flex items-center cursor-pointer;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
color: #155EEF;
|
||||
}
|
||||
.OtherCreationOption::before {
|
||||
content: '';
|
||||
display: block;
|
||||
margin-right: 4px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: center no-repeat url(../assets/folder-plus.svg);
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.notionConnectionTip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 24px;
|
||||
width: 640px;
|
||||
max-width: 640px;
|
||||
background: #F9FAFB;
|
||||
border-radius: 16px;
|
||||
}
|
||||
@@ -128,7 +138,6 @@
|
||||
line-height: 24px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.notionConnectionTip .title::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -139,7 +148,6 @@
|
||||
background: center no-repeat url(../assets/Icon-3-dots.svg);
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.notionConnectionTip .tip {
|
||||
margin-bottom: 20px;
|
||||
font-style: normal;
|
||||
@@ -147,4 +155,4 @@
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
color: #6B7280;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowRightLine, RiFolder6Line } from '@remixicon/react'
|
||||
import FilePreview from '../file-preview'
|
||||
import FileUploader from '../file-uploader'
|
||||
import NotionPagePreview from '../notion-page-preview'
|
||||
@@ -18,7 +17,6 @@ import { NotionPageSelector } from '@/app/components/base/notion-page-selector'
|
||||
import { useDatasetDetailContext } from '@/context/dataset-detail'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import VectorSpaceFull from '@/app/components/billing/vector-space-full'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
type IStepOneProps = {
|
||||
datasetId?: string
|
||||
@@ -122,174 +120,143 @@ const StepOne = ({
|
||||
return true
|
||||
if (isShowVectorSpaceFull)
|
||||
return true
|
||||
return false
|
||||
}, [files, isShowVectorSpaceFull])
|
||||
|
||||
return false
|
||||
}, [files])
|
||||
return (
|
||||
<div className='flex w-full h-full'>
|
||||
<div className='w-1/2 h-full overflow-y-auto relative'>
|
||||
<div className='flex justify-end'>
|
||||
<div className={classNames(s.form)}>
|
||||
{
|
||||
shouldShowDataSourceTypeList && (
|
||||
<div className={classNames(s.stepHeader, 'z-10 text-text-secondary bg-components-panel-bg-blur')}>{t('datasetCreation.steps.one')}</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
shouldShowDataSourceTypeList && (
|
||||
<div className='flex items-center mb-8 flex-wrap gap-4'>
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
dataSourceType === DataSourceType.FILE && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.FILE && s.disabled,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (dataSourceTypeDisable)
|
||||
return
|
||||
changeType(DataSourceType.FILE)
|
||||
hideFilePreview()
|
||||
hideNotionPagePreview()
|
||||
}}
|
||||
>
|
||||
<span className={cn(s.datasetIcon)} />
|
||||
{t('datasetCreation.stepOne.dataSourceType.file')}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
dataSourceType === DataSourceType.NOTION && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.NOTION && s.disabled,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (dataSourceTypeDisable)
|
||||
return
|
||||
changeType(DataSourceType.NOTION)
|
||||
hideFilePreview()
|
||||
hideNotionPagePreview()
|
||||
}}
|
||||
>
|
||||
<span className={cn(s.datasetIcon, s.notion)} />
|
||||
{t('datasetCreation.stepOne.dataSourceType.notion')}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
dataSourceType === DataSourceType.WEB && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled,
|
||||
)}
|
||||
onClick={() => changeType(DataSourceType.WEB)}
|
||||
>
|
||||
<span className={cn(s.datasetIcon, s.web)} />
|
||||
{t('datasetCreation.stepOne.dataSourceType.web')}
|
||||
</div>
|
||||
<div className='grow overflow-y-auto relative'>
|
||||
{
|
||||
shouldShowDataSourceTypeList && (
|
||||
<div className={s.stepHeader}>{t('datasetCreation.steps.one')}</div>
|
||||
)
|
||||
}
|
||||
<div className={s.form}>
|
||||
{
|
||||
shouldShowDataSourceTypeList && (
|
||||
<div className='flex items-center mb-8 flex-wrap gap-y-4'>
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
dataSourceType === DataSourceType.FILE && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.FILE && s.disabled,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (dataSourceTypeDisable)
|
||||
return
|
||||
changeType(DataSourceType.FILE)
|
||||
hideFilePreview()
|
||||
hideNotionPagePreview()
|
||||
}}
|
||||
>
|
||||
<span className={cn(s.datasetIcon)} />
|
||||
{t('datasetCreation.stepOne.dataSourceType.file')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{dataSourceType === DataSourceType.FILE && (
|
||||
<>
|
||||
<FileUploader
|
||||
fileList={files}
|
||||
titleClassName={!shouldShowDataSourceTypeList ? 'mt-[30px] !mb-[44px] !text-lg !font-semibold !text-gray-900' : undefined}
|
||||
prepareFileList={updateFileList}
|
||||
onFileListUpdate={updateFileList}
|
||||
onFileUpdate={updateFile}
|
||||
onPreview={updateCurrentFile}
|
||||
notSupportBatchUpload={notSupportBatchUpload}
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
dataSourceType === DataSourceType.NOTION && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.NOTION && s.disabled,
|
||||
)}
|
||||
onClick={() => {
|
||||
if (dataSourceTypeDisable)
|
||||
return
|
||||
changeType(DataSourceType.NOTION)
|
||||
hideFilePreview()
|
||||
hideNotionPagePreview()
|
||||
}}
|
||||
>
|
||||
<span className={cn(s.datasetIcon, s.notion)} />
|
||||
{t('datasetCreation.stepOne.dataSourceType.notion')}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
s.dataSourceItem,
|
||||
dataSourceType === DataSourceType.WEB && s.active,
|
||||
dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled,
|
||||
)}
|
||||
onClick={() => changeType(DataSourceType.WEB)}
|
||||
>
|
||||
<span className={cn(s.datasetIcon, s.web)} />
|
||||
{t('datasetCreation.stepOne.dataSourceType.web')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{dataSourceType === DataSourceType.FILE && (
|
||||
<>
|
||||
<FileUploader
|
||||
fileList={files}
|
||||
titleClassName={!shouldShowDataSourceTypeList ? 'mt-[30px] !mb-[44px] !text-lg !font-semibold !text-gray-900' : undefined}
|
||||
prepareFileList={updateFileList}
|
||||
onFileListUpdate={updateFileList}
|
||||
onFileUpdate={updateFile}
|
||||
onPreview={updateCurrentFile}
|
||||
notSupportBatchUpload={notSupportBatchUpload}
|
||||
/>
|
||||
{isShowVectorSpaceFull && (
|
||||
<div className='max-w-[640px] mb-4'>
|
||||
<VectorSpaceFull />
|
||||
</div>
|
||||
)}
|
||||
<Button disabled={nextDisabled} className={s.submitButton} variant='primary' onClick={onStepChange}>{t('datasetCreation.stepOne.button')}</Button>
|
||||
</>
|
||||
)}
|
||||
{dataSourceType === DataSourceType.NOTION && (
|
||||
<>
|
||||
{!hasConnection && <NotionConnector onSetting={onSetting} />}
|
||||
{hasConnection && (
|
||||
<>
|
||||
<div className='mb-8 w-[640px]'>
|
||||
<NotionPageSelector
|
||||
value={notionPages.map(page => page.page_id)}
|
||||
onSelect={updateNotionPages}
|
||||
onPreview={updateCurrentPage}
|
||||
/>
|
||||
</div>
|
||||
{isShowVectorSpaceFull && (
|
||||
<div className='max-w-[640px] mb-4'>
|
||||
<VectorSpaceFull />
|
||||
</div>
|
||||
)}
|
||||
<Button disabled={isShowVectorSpaceFull || !notionPages.length} className={s.submitButton} variant='primary' onClick={onStepChange}>{t('datasetCreation.stepOne.button')}</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{dataSourceType === DataSourceType.WEB && (
|
||||
<>
|
||||
<div className={cn('mb-8 w-[640px]', !shouldShowDataSourceTypeList && 'mt-12')}>
|
||||
<Website
|
||||
onPreview={setCurrentWebsite}
|
||||
checkedCrawlResult={websitePages}
|
||||
onCheckedCrawlResultChange={updateWebsitePages}
|
||||
onCrawlProviderChange={onWebsiteCrawlProviderChange}
|
||||
onJobIdChange={onWebsiteCrawlJobIdChange}
|
||||
crawlOptions={crawlOptions}
|
||||
onCrawlOptionsChange={onCrawlOptionsChange}
|
||||
/>
|
||||
{isShowVectorSpaceFull && (
|
||||
<div className='max-w-[640px] mb-4'>
|
||||
<VectorSpaceFull />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2 max-w-[640px]">
|
||||
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
|
||||
<Button disabled={nextDisabled} variant='primary' onClick={onStepChange}>
|
||||
<span className="flex gap-0.5 px-[10px]">
|
||||
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
|
||||
<RiArrowRightLine className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
{isShowVectorSpaceFull && (
|
||||
<div className='max-w-[640px] mb-4'>
|
||||
<VectorSpaceFull />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{dataSourceType === DataSourceType.NOTION && (
|
||||
<>
|
||||
{!hasConnection && <NotionConnector onSetting={onSetting} />}
|
||||
{hasConnection && (
|
||||
<>
|
||||
<div className='mb-8 w-[640px]'>
|
||||
<NotionPageSelector
|
||||
value={notionPages.map(page => page.page_id)}
|
||||
onSelect={updateNotionPages}
|
||||
onPreview={updateCurrentPage}
|
||||
/>
|
||||
</div>
|
||||
{isShowVectorSpaceFull && (
|
||||
<div className='max-w-[640px] mb-4'>
|
||||
<VectorSpaceFull />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2 max-w-[640px]">
|
||||
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
|
||||
<Button disabled={isShowVectorSpaceFull || !notionPages.length} variant='primary' onClick={onStepChange}>
|
||||
<span className="flex gap-0.5 px-[10px]">
|
||||
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
|
||||
<RiArrowRightLine className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{dataSourceType === DataSourceType.WEB && (
|
||||
<>
|
||||
<div className={cn('mb-8 w-[640px]', !shouldShowDataSourceTypeList && 'mt-12')}>
|
||||
<Website
|
||||
onPreview={setCurrentWebsite}
|
||||
checkedCrawlResult={websitePages}
|
||||
onCheckedCrawlResultChange={updateWebsitePages}
|
||||
onCrawlProviderChange={onWebsiteCrawlProviderChange}
|
||||
onJobIdChange={onWebsiteCrawlJobIdChange}
|
||||
crawlOptions={crawlOptions}
|
||||
onCrawlOptionsChange={onCrawlOptionsChange}
|
||||
/>
|
||||
</div>
|
||||
{isShowVectorSpaceFull && (
|
||||
<div className='max-w-[640px] mb-4'>
|
||||
<VectorSpaceFull />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2 max-w-[640px]">
|
||||
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
|
||||
<Button disabled={isShowVectorSpaceFull || !websitePages.length} variant='primary' onClick={onStepChange}>
|
||||
<span className="flex gap-0.5 px-[10px]">
|
||||
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
|
||||
<RiArrowRightLine className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!datasetId && (
|
||||
<>
|
||||
<div className={s.dividerLine} />
|
||||
<span className="inline-flex items-center cursor-pointer text-[13px] leading-4 text-text-accent" onClick={modalShowHandle}>
|
||||
<RiFolder6Line className="size-4 mr-1" />
|
||||
{t('datasetCreation.stepOne.emptyDatasetCreation')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<EmptyDatasetCreationModal show={showModal} onHide={modalCloseHandle} />
|
||||
)}
|
||||
<Button disabled={isShowVectorSpaceFull || !websitePages.length} className={s.submitButton} variant='primary' onClick={onStepChange}>{t('datasetCreation.stepOne.button')}</Button>
|
||||
</>
|
||||
)}
|
||||
{!datasetId && (
|
||||
<>
|
||||
<div className={s.dividerLine} />
|
||||
<div onClick={modalShowHandle} className={s.OtherCreationOption}>{t('datasetCreation.stepOne.emptyDatasetCreation')}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<EmptyDatasetCreationModal show={showModal} onHide={modalCloseHandle} />
|
||||
</div>
|
||||
<div className='w-1/2 h-full overflow-y-auto'>
|
||||
{currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />}
|
||||
{currentNotionPage && <NotionPagePreview currentPage={currentNotionPage} hidePreview={hideNotionPagePreview} />}
|
||||
{currentWebsite && <WebsitePreview payload={currentWebsite} hidePreview={hideWebsitePreview} />}
|
||||
</div>
|
||||
{currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />}
|
||||
{currentNotionPage && <NotionPagePreview currentPage={currentNotionPage} hidePreview={hideNotionPagePreview} />}
|
||||
{currentWebsite && <WebsitePreview payload={currentWebsite} hidePreview={hideWebsitePreview} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,51 +1,45 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiBookOpenLine } from '@remixicon/react'
|
||||
import EmbeddingProcess from '../embedding-process'
|
||||
|
||||
import s from './index.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import type { FullDocumentDetail, createDocumentResponse } from '@/models/datasets'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
|
||||
type StepThreeProps = {
|
||||
datasetId?: string
|
||||
datasetName?: string
|
||||
indexingType?: string
|
||||
retrievalMethod?: string
|
||||
creationCache?: createDocumentResponse
|
||||
}
|
||||
|
||||
const StepThree = ({ datasetId, datasetName, indexingType, creationCache, retrievalMethod }: StepThreeProps) => {
|
||||
const StepThree = ({ datasetId, datasetName, indexingType, creationCache }: StepThreeProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
return (
|
||||
<div className="flex justify-center w-full max-h-full h-full overflow-y-auto">
|
||||
<div className="grow shrink-0 h-full max-w-[960px] overflow-y-auto px-14 sm:px-16">
|
||||
<div className="mx-auto max-w-[640px]">
|
||||
<div className='flex w-full h-full'>
|
||||
<div className={'h-full w-full overflow-y-scroll px-6 sm:px-16'}>
|
||||
<div className='max-w-[636px]'>
|
||||
{!datasetId && (
|
||||
<>
|
||||
<div className="pt-10">
|
||||
<div className="mb-1 text-xl leading-[22px] font-semibold text-text-primary">{t('datasetCreation.stepThree.creationTitle')}</div>
|
||||
<div className="mb-7 text-[13px] leading-4 text-text-tertiary">{t('datasetCreation.stepThree.creationContent')}</div>
|
||||
<div className="flex gap-4">
|
||||
<AppIcon {...creationCache?.dataset} className="size-14 text-2xl self-center" />
|
||||
<div className="grow flex flex-col gap-1">
|
||||
<div className="text-[13px] leading-6 font-semibold">{t('datasetCreation.stepThree.label')}</div>
|
||||
<div className="w-full px-3 py-2 text-[13px] leading-4 bg-components-input-bg-normal rounded-lg truncate">{datasetName || creationCache?.dataset?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.creationInfo}>
|
||||
<div className={s.title}>{t('datasetCreation.stepThree.creationTitle')}</div>
|
||||
<div className={s.content}>{t('datasetCreation.stepThree.creationContent')}</div>
|
||||
<div className={s.label}>{t('datasetCreation.stepThree.label')}</div>
|
||||
<div className={s.datasetName}>{datasetName || creationCache?.dataset?.name}</div>
|
||||
</div>
|
||||
<hr className="my-6 h-[1px] bg-divider-subtle border-0" />
|
||||
<div className={s.dividerLine} />
|
||||
</>
|
||||
)}
|
||||
{datasetId && (
|
||||
<div className="pt-10">
|
||||
<div className="mb-1 text-xl leading-[22px] font-semibold text-text-primary">{t('datasetCreation.stepThree.additionTitle')}</div>
|
||||
<div className="mb-7 text-[13px] leading-4 text-text-tertiary">{`${t('datasetCreation.stepThree.additionP1')} ${datasetName || creationCache?.dataset?.name} ${t('datasetCreation.stepThree.additionP2')}`}</div>
|
||||
<div className={s.creationInfo}>
|
||||
<div className={s.title}>{t('datasetCreation.stepThree.additionTitle')}</div>
|
||||
<div className={s.content}>{`${t('datasetCreation.stepThree.additionP1')} ${datasetName || creationCache?.dataset?.name} ${t('datasetCreation.stepThree.additionP2')}`}</div>
|
||||
</div>
|
||||
)}
|
||||
<EmbeddingProcess
|
||||
@@ -53,21 +47,16 @@ const StepThree = ({ datasetId, datasetName, indexingType, creationCache, retrie
|
||||
batchId={creationCache?.batch || ''}
|
||||
documents={creationCache?.documents as FullDocumentDetail[]}
|
||||
indexingType={indexingType || creationCache?.dataset?.indexing_technique}
|
||||
retrievalMethod={retrievalMethod || creationCache?.dataset?.retrieval_model?.search_method}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className="shrink-0 pt-[88px] pr-8 text-xs">
|
||||
<div className="flex flex-col gap-3 w-[328px] p-6 text-text-tertiary bg-background-section rounded-xl">
|
||||
<div className="flex justify-center items-center size-10 bg-components-card-bg rounded-[10px] shadow-lg">
|
||||
<RiBookOpenLine className="size-5 text-text-accent" />
|
||||
</div>
|
||||
<div className="text-base font-semibold text-text-secondary">{t('datasetCreation.stepThree.sideTipTitle')}</div>
|
||||
<div className="text-text-tertiary">{t('datasetCreation.stepThree.sideTipContent')}</div>
|
||||
</div>
|
||||
{!isMobile && <div className={cn(s.sideTip)}>
|
||||
<div className={s.tipCard}>
|
||||
<span className={s.icon} />
|
||||
<div className={s.title}>{t('datasetCreation.stepThree.sideTipTitle')}</div>
|
||||
<div className={s.content}>{t('datasetCreation.stepThree.sideTipContent')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,18 @@
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.form {
|
||||
@apply px-16 pb-8;
|
||||
}
|
||||
|
||||
.form .label {
|
||||
@apply pt-6 pb-2 flex items-center;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #344054;
|
||||
}
|
||||
|
||||
.segmentationItem {
|
||||
min-height: 68px;
|
||||
}
|
||||
@@ -63,10 +75,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.indexItem.disabled:hover {
|
||||
background-color: #fcfcfd;
|
||||
border-color: #f2f4f7;
|
||||
@@ -79,7 +87,8 @@
|
||||
}
|
||||
|
||||
.radioItem {
|
||||
@apply relative mb-2 rounded-xl border border-components-option-card-option-border cursor-pointer bg-components-option-card-option-bg;
|
||||
@apply relative mb-2 rounded-xl border border-gray-100 cursor-pointer;
|
||||
background-color: #fcfcfd;
|
||||
}
|
||||
|
||||
.radioItem.segmentationItem.custom {
|
||||
@@ -137,7 +146,7 @@
|
||||
}
|
||||
|
||||
.typeIcon.economical {
|
||||
background-image: url(../assets/piggy-bank-mod.svg);
|
||||
background-image: url(../assets/piggy-bank-01.svg);
|
||||
}
|
||||
|
||||
.radioItem .radio {
|
||||
@@ -238,7 +247,7 @@
|
||||
}
|
||||
|
||||
.ruleItem {
|
||||
@apply flex items-center py-1.5;
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
.formFooter {
|
||||
@@ -385,6 +394,19 @@
|
||||
max-width: 524px;
|
||||
}
|
||||
|
||||
.previewHeader {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding-top: 42px;
|
||||
background-color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
color: #101828;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/*
|
||||
* `fixed` must under `previewHeader` because of style override would not work
|
||||
*/
|
||||
@@ -410,4 +432,4 @@
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import type { FC, PropsWithChildren, ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { InputProps } from '@/app/components/base/input'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import type { InputNumberProps } from '@/app/components/base/input-number'
|
||||
import { InputNumber } from '@/app/components/base/input-number'
|
||||
|
||||
const TextLabel: FC<PropsWithChildren> = (props) => {
|
||||
return <label className='text-text-secondary text-xs font-semibold leading-none'>{props.children}</label>
|
||||
}
|
||||
|
||||
const FormField: FC<PropsWithChildren<{ label: ReactNode }>> = (props) => {
|
||||
return <div className='space-y-2 flex-1'>
|
||||
<TextLabel>{props.label}</TextLabel>
|
||||
{props.children}
|
||||
</div>
|
||||
}
|
||||
|
||||
export const DelimiterInput: FC<InputProps & { tooltip?: string }> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
return <FormField label={<div className='flex items-center mb-1'>
|
||||
<span className='system-sm-semibold mr-0.5'>{t('datasetCreation.stepTwo.separator')}</span>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='max-w-[200px]'>
|
||||
{props.tooltip || t('datasetCreation.stepTwo.separatorTip')}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>}>
|
||||
<Input
|
||||
type="text"
|
||||
className='h-9'
|
||||
placeholder={t('datasetCreation.stepTwo.separatorPlaceholder')!}
|
||||
{...props}
|
||||
/>
|
||||
</FormField>
|
||||
}
|
||||
|
||||
export const MaxLengthInput: FC<InputNumberProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
return <FormField label={<div className='system-sm-semibold mb-1'>
|
||||
{t('datasetCreation.stepTwo.maxLength')}
|
||||
</div>}>
|
||||
<InputNumber
|
||||
type="number"
|
||||
className='h-9'
|
||||
placeholder={'≤ 4000'}
|
||||
max={4000}
|
||||
min={1}
|
||||
{...props}
|
||||
/>
|
||||
</FormField>
|
||||
}
|
||||
|
||||
export const OverlapInput: FC<InputNumberProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
return <FormField label={<div className='flex items-center mb-1'>
|
||||
<span className='system-sm-semibold'>{t('datasetCreation.stepTwo.overlap')}</span>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='max-w-[200px]'>
|
||||
{t('datasetCreation.stepTwo.overlapTip')}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>}>
|
||||
<InputNumber
|
||||
type="number"
|
||||
className='h-9'
|
||||
placeholder={t('datasetCreation.stepTwo.overlap') || ''}
|
||||
min={1}
|
||||
{...props}
|
||||
/>
|
||||
</FormField>
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Popover from '@/app/components/base/popover'
|
||||
import { languages } from '@/i18n/language'
|
||||
@@ -22,40 +22,25 @@ const LanguageSelect: FC<ILanguageSelectProps> = ({
|
||||
manualClose
|
||||
trigger='click'
|
||||
disabled={disabled}
|
||||
popupClassName='z-20'
|
||||
htmlContent={
|
||||
<div className='w-full p-1'>
|
||||
<div className='w-full py-1'>
|
||||
{languages.filter(language => language.supported).map(({ prompt_name }) => (
|
||||
<div
|
||||
key={prompt_name}
|
||||
className='w-full py-2 px-3 inline-flex items-center justify-between hover:bg-state-base-hover rounded-lg cursor-pointer'
|
||||
onClick={() => onSelect(prompt_name)}
|
||||
>
|
||||
<span className='text-text-secondary system-sm-medium'>{prompt_name}</span>
|
||||
{(currentLanguage === prompt_name) && <RiCheckLine className='size-4 text-text-accent' />}
|
||||
className='py-2 px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer text-gray-700 text-sm'
|
||||
onClick={() => onSelect(prompt_name)}>{prompt_name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
btnElement={
|
||||
<div className={cn('inline-flex items-center gap-x-[1px]', disabled && 'cursor-not-allowed')}>
|
||||
<span className={cn(
|
||||
'px-[3px] system-xs-semibold text-components-button-tertiary-text',
|
||||
disabled ? 'text-components-button-tertiary-text-disabled' : '',
|
||||
)}>
|
||||
{currentLanguage}
|
||||
</span>
|
||||
<RiArrowDownSLine className={cn(
|
||||
'size-3.5 text-components-button-tertiary-text',
|
||||
disabled ? 'text-components-button-tertiary-text-disabled' : '',
|
||||
)} />
|
||||
<div className='inline-flex items-center'>
|
||||
<span className='pr-[2px] text-xs leading-[18px] font-medium'>{currentLanguage}</span>
|
||||
<RiArrowDownSLine className='w-3 h-3 opacity-60' />
|
||||
</div>
|
||||
}
|
||||
btnClassName={() => cn(
|
||||
'!border-0 rounded-md !px-1.5 !py-1 !mx-1 !bg-components-button-tertiary-bg !hover:bg-components-button-tertiary-bg',
|
||||
disabled ? 'bg-components-button-tertiary-bg-disabled' : '',
|
||||
)}
|
||||
className='!w-[140px] h-fit !z-20 !translate-x-0 !left-1'
|
||||
btnClassName={open => cn('!border-0 !px-0 !py-0 !bg-inherit !hover:bg-inherit', open ? 'text-blue-600' : 'text-gray-500')}
|
||||
className='!w-[120px] h-fit !z-20 !translate-x-0 !left-[-16px]'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { type ComponentProps, type FC, type ReactNode, forwardRef } from 'react'
|
||||
import Image from 'next/image'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
const TriangleArrow: FC<ComponentProps<'svg'>> = props => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="11" viewBox="0 0 24 11" fill="none" {...props}>
|
||||
<path d="M9.87868 1.12132C11.0503 -0.0502525 12.9497 -0.0502525 14.1213 1.12132L23.3137 10.3137H0.686292L9.87868 1.12132Z" fill="currentColor"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
type OptionCardHeaderProps = {
|
||||
icon: ReactNode
|
||||
title: ReactNode
|
||||
description: string
|
||||
isActive?: boolean
|
||||
activeClassName?: string
|
||||
effectImg?: string
|
||||
}
|
||||
|
||||
export const OptionCardHeader: FC<OptionCardHeaderProps> = (props) => {
|
||||
const { icon, title, description, isActive, activeClassName, effectImg } = props
|
||||
return <div className={classNames(
|
||||
'flex h-full overflow-hidden rounded-t-xl relative',
|
||||
isActive && activeClassName,
|
||||
)}>
|
||||
<div className='size-14 flex items-center justify-center relative overflow-hidden'>
|
||||
{isActive && effectImg && <Image src={effectImg} className='absolute top-0 left-0 w-full h-full' alt='' width={56} height={56} />}
|
||||
<div className='p-1'>
|
||||
<div className='size-8 rounded-lg border p-1.5 shadow-md border-components-panel-border-subtle justify-center flex bg-background-default-dodge'>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TriangleArrow
|
||||
className='absolute left-4 -bottom-1.5 text-components-panel-bg'
|
||||
/>
|
||||
<div className='flex-1 space-y-0.5 py-3 pr-4'>
|
||||
<div className='text-text-secondary system-md-semibold'>{title}</div>
|
||||
<div className='text-text-tertiary system-xs-regular'>{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
type OptionCardProps = {
|
||||
icon: ReactNode
|
||||
className?: string
|
||||
activeHeaderClassName?: string
|
||||
title: ReactNode
|
||||
description: string
|
||||
isActive?: boolean
|
||||
actions?: ReactNode
|
||||
effectImg?: string
|
||||
onSwitched?: () => void
|
||||
noHighlight?: boolean
|
||||
disabled?: boolean
|
||||
} & Omit<ComponentProps<'div'>, 'title' | 'onClick'>
|
||||
|
||||
export const OptionCard: FC<OptionCardProps> = forwardRef((props, ref) => {
|
||||
const { icon, className, title, description, isActive, children, actions, activeHeaderClassName, style, effectImg, onSwitched, noHighlight, disabled, ...rest } = props
|
||||
return <div
|
||||
className={classNames(
|
||||
'rounded-xl bg-components-option-card-option-bg shadow-xs',
|
||||
(isActive && !noHighlight)
|
||||
? 'border-[1.5px] border-components-option-card-option-selected-border'
|
||||
: 'border border-components-option-card-option-border',
|
||||
disabled && 'opacity-50',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
...style,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!isActive && !disabled)
|
||||
onSwitched?.()
|
||||
}}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
>
|
||||
<OptionCardHeader
|
||||
icon={icon}
|
||||
title={title}
|
||||
description={description}
|
||||
isActive={isActive && !noHighlight}
|
||||
activeClassName={activeHeaderClassName}
|
||||
effectImg={effectImg}
|
||||
/>
|
||||
{/** Body */}
|
||||
{isActive && (children || actions) && <div className='py-3 px-4 bg-components-panel-bg rounded-b-xl'>
|
||||
{children}
|
||||
{actions && <div className='flex gap-2 mt-4'>
|
||||
{actions}
|
||||
</div>
|
||||
}
|
||||
</div>}
|
||||
</div>
|
||||
})
|
||||
|
||||
OptionCard.displayName = 'OptionCard'
|
||||
@@ -1,27 +0,0 @@
|
||||
import { type FC, Fragment } from 'react'
|
||||
import type { Step } from './step'
|
||||
import { StepperStep } from './step'
|
||||
|
||||
export type StepperProps = {
|
||||
steps: Step[]
|
||||
activeIndex: number
|
||||
}
|
||||
|
||||
export const Stepper: FC<StepperProps> = (props) => {
|
||||
const { steps, activeIndex } = props
|
||||
return <div className='flex items-center gap-3'>
|
||||
{steps.map((step, index) => {
|
||||
const isLast = index === steps.length - 1
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<StepperStep
|
||||
{...step}
|
||||
activeIndex={activeIndex}
|
||||
index={index}
|
||||
/>
|
||||
{!isLast && <div className='w-4 h-px bg-divider-deep' />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
export type Step = {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type StepperStepProps = Step & {
|
||||
index: number
|
||||
activeIndex: number
|
||||
}
|
||||
|
||||
export const StepperStep: FC<StepperStepProps> = (props) => {
|
||||
const { name, activeIndex, index } = props
|
||||
const isActive = index === activeIndex
|
||||
const isDisabled = activeIndex < index
|
||||
const label = isActive ? `STEP ${index + 1}` : `${index + 1}`
|
||||
return <div className='flex items-center gap-2'>
|
||||
<div className={classNames(
|
||||
'h-5 px-2 py-1 rounded-3xl flex-col justify-center items-center gap-2 inline-flex',
|
||||
isActive
|
||||
? 'bg-state-accent-solid'
|
||||
: !isDisabled
|
||||
? 'border border-text-quaternary'
|
||||
: 'border border-divider-deep',
|
||||
)}>
|
||||
<div className={classNames(
|
||||
'text-center system-2xs-semibold-uppercase',
|
||||
isActive
|
||||
? 'text-text-primary-on-surface'
|
||||
: !isDisabled
|
||||
? 'text-text-tertiary'
|
||||
: 'text-text-quaternary',
|
||||
)}>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames('system-xs-medium-uppercase',
|
||||
isActive
|
||||
? 'text-text-accent system-xs-semibold-uppercase'
|
||||
: !isDisabled
|
||||
? 'text-text-tertiary'
|
||||
: 'text-text-quaternary',
|
||||
)}>{name}</div>
|
||||
</div>
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import { RiArrowLeftLine } from '@remixicon/react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Stepper, type StepperProps } from '../stepper'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
export type TopbarProps = Pick<StepperProps, 'activeIndex'> & {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const STEP_T_MAP: Record<number, string> = {
|
||||
1: 'datasetCreation.steps.one',
|
||||
2: 'datasetCreation.steps.two',
|
||||
3: 'datasetCreation.steps.three',
|
||||
}
|
||||
|
||||
export const Topbar: FC<TopbarProps> = (props) => {
|
||||
const { className, ...rest } = props
|
||||
const { t } = useTranslation()
|
||||
return <div className={classNames('flex shrink-0 h-[52px] items-center justify-between relative border-b border-b-divider-subtle', className)}>
|
||||
<Link href={'/datasets'} className="h-12 pl-2 pr-6 py-2 justify-start items-center gap-1 inline-flex">
|
||||
<div className='p-2'>
|
||||
<RiArrowLeftLine className='size-4 text-text-primary' />
|
||||
</div>
|
||||
<p className="text-text-primary system-sm-semibold-uppercase">
|
||||
{t('datasetCreation.steps.header.creation')}
|
||||
</p>
|
||||
</Link>
|
||||
<div className={
|
||||
'top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 absolute'
|
||||
}>
|
||||
<Stepper
|
||||
steps={Array.from({ length: 3 }, (_, i) => ({
|
||||
name: t(STEP_T_MAP[i + 1]),
|
||||
}))}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -18,7 +18,7 @@ const ErrorMessage: FC<Props> = ({
|
||||
return (
|
||||
<div className={cn(className, 'py-2 px-4 border-t border-gray-200 bg-[#FFFAEB]')}>
|
||||
<div className='flex items-center h-5'>
|
||||
<AlertTriangle className='mr-2 w-4 h-4 text-text-warning-secondary' />
|
||||
<AlertTriangle className='mr-2 w-4 h-4 text-[#F79009]' />
|
||||
<div className='text-sm font-medium text-[#DC6803]'>{title}</div>
|
||||
</div>
|
||||
{errorMsg && (
|
||||
|
||||
@@ -94,6 +94,7 @@ const JinaReader: FC<Props> = ({
|
||||
const waitForCrawlFinished = useCallback(async (jobId: string) => {
|
||||
try {
|
||||
const res = await checkJinaReaderTaskStatus(jobId) as any
|
||||
console.log('res', res)
|
||||
if (res.status === 'completed') {
|
||||
return {
|
||||
isError: false,
|
||||
|
||||
@@ -18,7 +18,7 @@ const WebsitePreview = ({
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={cn(s.filePreview, 'h-full')}>
|
||||
<div className={cn(s.filePreview)}>
|
||||
<div className={cn(s.previewHeader)}>
|
||||
<div className={cn(s.title)}>
|
||||
<span>{t('datasetCreation.stepOne.pagePreview')}</span>
|
||||
@@ -32,7 +32,7 @@ const WebsitePreview = ({
|
||||
<div className='truncate leading-[18px] text-xs font-normal text-gray-500' title={payload.source_url}>{payload.source_url}</div>
|
||||
</div>
|
||||
<div className={cn(s.previewContent)}>
|
||||
<div className={cn(s.fileContent, 'body-md-regular')}>{payload.markdown}</div>
|
||||
<div className={cn(s.fileContent)}>{payload.markdown}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import { DocForm } from '@/models/datasets'
|
||||
import I18n from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n/language'
|
||||
|
||||
@@ -32,18 +32,18 @@ const CSV_TEMPLATE_CN = [
|
||||
['内容 2'],
|
||||
]
|
||||
|
||||
const CSVDownload: FC<{ docForm: ChunkingMode }> = ({ docForm }) => {
|
||||
const CSVDownload: FC<{ docForm: DocForm }> = ({ docForm }) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const { CSVDownloader, Type } = useCSVDownloader()
|
||||
|
||||
const getTemplate = () => {
|
||||
if (locale === LanguagesSupported[1]) {
|
||||
if (docForm === ChunkingMode.qa)
|
||||
if (docForm === DocForm.QA)
|
||||
return CSV_TEMPLATE_QA_CN
|
||||
return CSV_TEMPLATE_CN
|
||||
}
|
||||
if (docForm === ChunkingMode.qa)
|
||||
if (docForm === DocForm.QA)
|
||||
return CSV_TEMPLATE_QA_EN
|
||||
return CSV_TEMPLATE_EN
|
||||
}
|
||||
@@ -52,7 +52,7 @@ const CSVDownload: FC<{ docForm: ChunkingMode }> = ({ docForm }) => {
|
||||
<div className='mt-6'>
|
||||
<div className='text-sm text-gray-900 font-medium'>{t('share.generation.csvStructureTitle')}</div>
|
||||
<div className='mt-2 max-h-[500px] overflow-auto'>
|
||||
{docForm === ChunkingMode.qa && (
|
||||
{docForm === DocForm.QA && (
|
||||
<table className='table-fixed w-full border-separate border-spacing-0 border border-gray-200 rounded-lg text-xs'>
|
||||
<thead className='text-gray-500'>
|
||||
<tr>
|
||||
@@ -72,7 +72,7 @@ const CSVDownload: FC<{ docForm: ChunkingMode }> = ({ docForm }) => {
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
{docForm === ChunkingMode.text && (
|
||||
{docForm === DocForm.TEXT && (
|
||||
<table className='table-fixed w-full border-separate border-spacing-0 border border-gray-200 rounded-lg text-xs'>
|
||||
<thead className='text-gray-500'>
|
||||
<tr>
|
||||
@@ -97,7 +97,7 @@ const CSVDownload: FC<{ docForm: ChunkingMode }> = ({ docForm }) => {
|
||||
bom={true}
|
||||
data={getTemplate()}
|
||||
>
|
||||
<div className='flex items-center h-[18px] space-x-1 text-text-accent text-xs font-medium'>
|
||||
<div className='flex items-center h-[18px] space-x-1 text-[#155EEF] text-xs font-medium'>
|
||||
<DownloadIcon className='w-3 h-3 mr-1' />
|
||||
{t('datasetDocuments.list.batchModal.template')}
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ import CSVUploader from './csv-uploader'
|
||||
import CSVDownloader from './csv-downloader'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import type { ChunkingMode } from '@/models/datasets'
|
||||
import type { DocForm } from '@/models/datasets'
|
||||
|
||||
export type IBatchModalProps = {
|
||||
isShow: boolean
|
||||
docForm: ChunkingMode
|
||||
docForm: DocForm
|
||||
onCancel: () => void
|
||||
onConfirm: (file: File) => void
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { CSSProperties, FC } from 'react'
|
||||
import React from 'react'
|
||||
import { FixedSizeList as List } from 'react-window'
|
||||
import InfiniteLoader from 'react-window-infinite-loader'
|
||||
import SegmentCard from './SegmentCard'
|
||||
import s from './style.module.css'
|
||||
import type { SegmentDetailModel } from '@/models/datasets'
|
||||
|
||||
type IInfiniteVirtualListProps = {
|
||||
hasNextPage?: boolean // Are there more items to load? (This information comes from the most recent API request.)
|
||||
isNextPageLoading: boolean // Are we currently loading a page of items? (This may be an in-flight flag in your Redux store for example.)
|
||||
items: Array<SegmentDetailModel[]> // Array of items loaded so far.
|
||||
loadNextPage: () => Promise<void> // Callback function responsible for loading the next page of items.
|
||||
onClick: (detail: SegmentDetailModel) => void
|
||||
onChangeSwitch: (segId: string, enabled: boolean) => Promise<void>
|
||||
onDelete: (segId: string) => Promise<void>
|
||||
archived?: boolean
|
||||
embeddingAvailable: boolean
|
||||
}
|
||||
|
||||
const InfiniteVirtualList: FC<IInfiniteVirtualListProps> = ({
|
||||
hasNextPage,
|
||||
isNextPageLoading,
|
||||
items,
|
||||
loadNextPage,
|
||||
onClick: onClickCard,
|
||||
onChangeSwitch,
|
||||
onDelete,
|
||||
archived,
|
||||
embeddingAvailable,
|
||||
}) => {
|
||||
// If there are more items to be loaded then add an extra row to hold a loading indicator.
|
||||
const itemCount = hasNextPage ? items.length + 1 : items.length
|
||||
|
||||
// Only load 1 page of items at a time.
|
||||
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
|
||||
const loadMoreItems = isNextPageLoading ? () => { } : loadNextPage
|
||||
|
||||
// Every row is loaded except for our loading indicator row.
|
||||
const isItemLoaded = (index: number) => !hasNextPage || index < items.length
|
||||
|
||||
// Render an item or a loading indicator.
|
||||
const Item = ({ index, style }: { index: number; style: CSSProperties }) => {
|
||||
let content
|
||||
if (!isItemLoaded(index)) {
|
||||
content = (
|
||||
<>
|
||||
{[1, 2, 3].map(v => (
|
||||
<SegmentCard key={v} loading={true} detail={{ position: v } as any} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
else {
|
||||
content = items[index].map(segItem => (
|
||||
<SegmentCard
|
||||
key={segItem.id}
|
||||
detail={segItem}
|
||||
onClick={() => onClickCard(segItem)}
|
||||
onChangeSwitch={onChangeSwitch}
|
||||
onDelete={onDelete}
|
||||
loading={false}
|
||||
archived={archived}
|
||||
embeddingAvailable={embeddingAvailable}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={style} className={s.cardWrapper}>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<InfiniteLoader
|
||||
itemCount={itemCount}
|
||||
isItemLoaded={isItemLoaded}
|
||||
loadMoreItems={loadMoreItems}
|
||||
>
|
||||
{({ onItemsRendered, ref }) => (
|
||||
<List
|
||||
ref={ref}
|
||||
className="List"
|
||||
height={800}
|
||||
width={'100%'}
|
||||
itemSize={200}
|
||||
itemCount={itemCount}
|
||||
onItemsRendered={onItemsRendered}
|
||||
>
|
||||
{Item}
|
||||
</List>
|
||||
)}
|
||||
</InfiniteLoader>
|
||||
)
|
||||
}
|
||||
export default InfiniteVirtualList
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
RiDeleteBinLine,
|
||||
} from '@remixicon/react'
|
||||
import { StatusItem } from '../../list'
|
||||
import style from '../../style.module.css'
|
||||
import { DocumentTitle } from '../index'
|
||||
import s from './style.module.css'
|
||||
import { SegmentIndexTag } from './common/segment-index-tag'
|
||||
import { SegmentIndexTag } from './index'
|
||||
import cn from '@/utils/classnames'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
@@ -31,22 +31,6 @@ const ProgressBar: FC<{ percent: number; loading: boolean }> = ({ percent, loadi
|
||||
)
|
||||
}
|
||||
|
||||
type DocumentTitleProps = {
|
||||
extension?: string
|
||||
name?: string
|
||||
iconCls?: string
|
||||
textCls?: string
|
||||
wrapperCls?: string
|
||||
}
|
||||
|
||||
const DocumentTitle: FC<DocumentTitleProps> = ({ extension, name, iconCls, textCls, wrapperCls }) => {
|
||||
const localExtension = extension?.toLowerCase() || name?.split('.')?.pop()?.toLowerCase()
|
||||
return <div className={cn('flex items-center justify-start flex-1', wrapperCls)}>
|
||||
<div className={cn(s[`${localExtension || 'txt'}Icon`], style.titleIcon, iconCls)}></div>
|
||||
<span className={cn('font-semibold text-lg text-gray-900 ml-1', textCls)}> {name || '--'}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
export type UsageScene = 'doc' | 'hitTesting'
|
||||
|
||||
type ISegmentCardProps = {
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import React, { type FC, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiExpandDiagonalLine,
|
||||
} from '@remixicon/react'
|
||||
import ActionButtons from './common/action-buttons'
|
||||
import ChunkContent from './common/chunk-content'
|
||||
import Dot from './common/dot'
|
||||
import { SegmentIndexTag } from './common/segment-index-tag'
|
||||
import { useSegmentListContext } from './index'
|
||||
import type { ChildChunkDetail, ChunkingMode } from '@/models/datasets'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { formatTime } from '@/utils/time'
|
||||
|
||||
type IChildSegmentDetailProps = {
|
||||
chunkId: string
|
||||
childChunkInfo?: Partial<ChildChunkDetail> & { id: string }
|
||||
onUpdate: (segmentId: string, childChunkId: string, content: string) => void
|
||||
onCancel: () => void
|
||||
docForm: ChunkingMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Show all the contents of the segment
|
||||
*/
|
||||
const ChildSegmentDetail: FC<IChildSegmentDetailProps> = ({
|
||||
chunkId,
|
||||
childChunkInfo,
|
||||
onUpdate,
|
||||
onCancel,
|
||||
docForm,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [content, setContent] = useState(childChunkInfo?.content || '')
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const fullScreen = useSegmentListContext(s => s.fullScreen)
|
||||
const toggleFullScreen = useSegmentListContext(s => s.toggleFullScreen)
|
||||
|
||||
eventEmitter?.useSubscription((v) => {
|
||||
if (v === 'update-child-segment')
|
||||
setLoading(true)
|
||||
if (v === 'update-child-segment-done')
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
const handleCancel = () => {
|
||||
onCancel()
|
||||
setContent(childChunkInfo?.content || '')
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onUpdate(chunkId, childChunkInfo?.id || '', content)
|
||||
}
|
||||
|
||||
const wordCountText = useMemo(() => {
|
||||
const count = content.length
|
||||
return `${formatNumber(count)} ${t('datasetDocuments.segment.characters', { count })}`
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [content.length])
|
||||
|
||||
const EditTimeText = useMemo(() => {
|
||||
const timeText = formatTime({
|
||||
date: (childChunkInfo?.updated_at ?? 0) * 1000,
|
||||
dateFormat: 'MM/DD/YYYY h:mm:ss',
|
||||
})
|
||||
return `${t('datasetDocuments.segment.editedAt')} ${timeText}`
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [childChunkInfo?.updated_at])
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col h-full'}>
|
||||
<div className={classNames('flex items-center justify-between', fullScreen ? 'py-3 pr-4 pl-6 border border-divider-subtle' : 'pt-3 pr-3 pl-4')}>
|
||||
<div className='flex flex-col'>
|
||||
<div className='text-text-primary system-xl-semibold'>{t('datasetDocuments.segment.editChildChunk')}</div>
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<SegmentIndexTag positionId={childChunkInfo?.position || ''} labelPrefix={t('datasetDocuments.segment.childChunk') as string} />
|
||||
<Dot />
|
||||
<span className='text-text-tertiary system-xs-medium'>{wordCountText}</span>
|
||||
<Dot />
|
||||
<span className='text-text-tertiary system-xs-medium'>
|
||||
{EditTimeText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
{fullScreen && (
|
||||
<>
|
||||
<ActionButtons
|
||||
handleCancel={handleCancel}
|
||||
handleSave={handleSave}
|
||||
loading={loading}
|
||||
isChildChunk={true}
|
||||
/>
|
||||
<Divider type='vertical' className='h-3.5 bg-divider-regular ml-4 mr-2' />
|
||||
</>
|
||||
)}
|
||||
<div className='w-8 h-8 flex justify-center items-center p-1.5 cursor-pointer mr-1' onClick={toggleFullScreen}>
|
||||
<RiExpandDiagonalLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='w-8 h-8 flex justify-center items-center p-1.5 cursor-pointer' onClick={onCancel}>
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames('flex grow w-full', fullScreen ? 'flex-row justify-center px-6 pt-6' : 'py-3 px-4')}>
|
||||
<div className={classNames('break-all overflow-hidden whitespace-pre-line h-full', fullScreen ? 'w-1/2' : 'w-full')}>
|
||||
<ChunkContent
|
||||
docForm={docForm}
|
||||
question={content}
|
||||
onQuestionChange={content => setContent(content)}
|
||||
isEditMode={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!fullScreen && (
|
||||
<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}
|
||||
isChildChunk={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ChildSegmentDetail)
|
||||
@@ -1,195 +0,0 @@
|
||||
import { type FC, useMemo, useState } from 'react'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { EditSlice } from '../../../formatted-text/flavours/edit-slice'
|
||||
import { useDocumentContext } from '../index'
|
||||
import { FormattedText } from '../../../formatted-text/formatted'
|
||||
import Empty from './common/empty'
|
||||
import FullDocListSkeleton from './skeleton/full-doc-list-skeleton'
|
||||
import { useSegmentListContext } from './index'
|
||||
import type { ChildChunkDetail } from '@/models/datasets'
|
||||
import Input from '@/app/components/base/input'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
|
||||
type IChildSegmentCardProps = {
|
||||
childChunks: ChildChunkDetail[]
|
||||
parentChunkId: string
|
||||
handleInputChange?: (value: string) => void
|
||||
handleAddNewChildChunk?: (parentChunkId: string) => void
|
||||
enabled: boolean
|
||||
onDelete?: (segId: string, childChunkId: string) => Promise<void>
|
||||
onClickSlice?: (childChunk: ChildChunkDetail) => void
|
||||
total?: number
|
||||
inputValue?: string
|
||||
onClearFilter?: () => void
|
||||
isLoading?: boolean
|
||||
focused?: boolean
|
||||
}
|
||||
|
||||
const ChildSegmentList: FC<IChildSegmentCardProps> = ({
|
||||
childChunks,
|
||||
parentChunkId,
|
||||
handleInputChange,
|
||||
handleAddNewChildChunk,
|
||||
enabled,
|
||||
onDelete,
|
||||
onClickSlice,
|
||||
total,
|
||||
inputValue,
|
||||
onClearFilter,
|
||||
isLoading,
|
||||
focused = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const parentMode = useDocumentContext(s => s.parentMode)
|
||||
const currChildChunk = useSegmentListContext(s => s.currChildChunk)
|
||||
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
|
||||
const toggleCollapse = () => {
|
||||
setCollapsed(!collapsed)
|
||||
}
|
||||
|
||||
const isParagraphMode = useMemo(() => {
|
||||
return parentMode === 'paragraph'
|
||||
}, [parentMode])
|
||||
|
||||
const isFullDocMode = useMemo(() => {
|
||||
return parentMode === 'full-doc'
|
||||
}, [parentMode])
|
||||
|
||||
const contentOpacity = useMemo(() => {
|
||||
return (enabled || focused) ? '' : 'opacity-50 group-hover/card:opacity-100'
|
||||
}, [enabled, focused])
|
||||
|
||||
const totalText = useMemo(() => {
|
||||
const isSearch = inputValue !== '' && isFullDocMode
|
||||
if (!isSearch) {
|
||||
const text = isFullDocMode
|
||||
? !total
|
||||
? '--'
|
||||
: formatNumber(total)
|
||||
: formatNumber(childChunks.length)
|
||||
const count = isFullDocMode
|
||||
? text === '--'
|
||||
? 0
|
||||
: total
|
||||
: childChunks.length
|
||||
return `${text} ${t('datasetDocuments.segment.childChunks', { count })}`
|
||||
}
|
||||
else {
|
||||
const text = !total ? '--' : formatNumber(total)
|
||||
const count = text === '--' ? 0 : total
|
||||
return `${count} ${t('datasetDocuments.segment.searchResults', { count })}`
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isFullDocMode, total, childChunks.length, inputValue])
|
||||
|
||||
return (
|
||||
<div className={classNames(
|
||||
'flex flex-col',
|
||||
contentOpacity,
|
||||
isParagraphMode ? 'pt-1 pb-2' : 'px-3 grow',
|
||||
(isFullDocMode && isLoading) && 'overflow-y-hidden',
|
||||
)}>
|
||||
{isFullDocMode ? <Divider type='horizontal' className='h-[1px] bg-divider-subtle my-1' /> : null}
|
||||
<div className={classNames('flex items-center justify-between', isFullDocMode ? 'pt-2 pb-3 sticky -top-2 left-0 bg-background-default' : '')}>
|
||||
<div className={classNames(
|
||||
'h-7 flex items-center pl-1 pr-3 rounded-lg',
|
||||
isParagraphMode && 'cursor-pointer',
|
||||
(isParagraphMode && collapsed) && 'bg-dataset-child-chunk-expand-btn-bg',
|
||||
isFullDocMode && 'pl-0',
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
toggleCollapse()
|
||||
}}
|
||||
>
|
||||
{
|
||||
isParagraphMode
|
||||
? collapsed
|
||||
? (
|
||||
<RiArrowRightSLine className='w-4 h-4 text-text-secondary opacity-50 mr-0.5' />
|
||||
)
|
||||
: (<RiArrowDownSLine className='w-4 h-4 text-text-secondary mr-0.5' />)
|
||||
: null
|
||||
}
|
||||
<span className='text-text-secondary system-sm-semibold-uppercase'>{totalText}</span>
|
||||
<span className={classNames('text-text-quaternary text-xs font-medium pl-1.5', isParagraphMode ? 'hidden group-hover/card:inline-block' : '')}>·</span>
|
||||
<button
|
||||
type='button'
|
||||
className={classNames(
|
||||
'px-1.5 py-1 text-components-button-secondary-accent-text system-xs-semibold-uppercase',
|
||||
isParagraphMode ? 'hidden group-hover/card:inline-block' : '',
|
||||
(isFullDocMode && isLoading) ? 'text-components-button-secondary-accent-text-disabled' : '',
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleAddNewChildChunk?.(parentChunkId)
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t('common.operation.add')}
|
||||
</button>
|
||||
</div>
|
||||
{isFullDocMode
|
||||
? <Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
wrapperClassName='!w-52'
|
||||
value={inputValue}
|
||||
onChange={e => handleInputChange?.(e.target.value)}
|
||||
onClear={() => handleInputChange?.('')}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
{isLoading ? <FullDocListSkeleton /> : null}
|
||||
{((isFullDocMode && !isLoading) || !collapsed)
|
||||
? <div className={classNames('flex gap-x-0.5', isFullDocMode ? 'grow mb-6' : 'items-center')}>
|
||||
{isParagraphMode && (
|
||||
<div className='self-stretch'>
|
||||
<Divider type='vertical' className='w-[2px] mx-[7px] bg-text-accent-secondary' />
|
||||
</div>
|
||||
)}
|
||||
{childChunks.length > 0
|
||||
? <FormattedText className={classNames('w-full !leading-6 flex flex-col', isParagraphMode ? 'gap-y-2' : 'gap-y-3')}>
|
||||
{childChunks.map((childChunk) => {
|
||||
const edited = childChunk.updated_at !== childChunk.created_at
|
||||
const focused = currChildChunk?.childChunkInfo?.id === childChunk.id
|
||||
return <EditSlice
|
||||
key={childChunk.id}
|
||||
label={`C-${childChunk.position}${edited ? ` · ${t('datasetDocuments.segment.edited')}` : ''}`}
|
||||
text={childChunk.content}
|
||||
onDelete={() => onDelete?.(childChunk.segment_id, childChunk.id)}
|
||||
labelClassName={focused ? 'bg-state-accent-solid text-text-primary-on-surface' : ''}
|
||||
labelInnerClassName={'text-[10px] font-semibold align-bottom leading-6'}
|
||||
contentClassName={classNames('!leading-6', focused ? 'bg-state-accent-hover-alt text-text-primary' : '')}
|
||||
showDivider={false}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClickSlice?.(childChunk)
|
||||
}}
|
||||
offsetOptions={({ rects }) => {
|
||||
return {
|
||||
mainAxis: isFullDocMode ? -rects.floating.width : 12 - rects.floating.width,
|
||||
crossAxis: (20 - rects.floating.height) / 2,
|
||||
}
|
||||
}}
|
||||
/>
|
||||
})}
|
||||
</FormattedText>
|
||||
: inputValue !== ''
|
||||
? <div className='h-full w-full'>
|
||||
<Empty onClearFilter={onClearFilter!} />
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChildSegmentList
|
||||
@@ -1,86 +0,0 @@
|
||||
import React, { type FC, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import { useDocumentContext } from '../../index'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
|
||||
type IActionButtonsProps = {
|
||||
handleCancel: () => void
|
||||
handleSave: () => void
|
||||
loading: boolean
|
||||
actionType?: 'edit' | 'add'
|
||||
handleRegeneration?: () => void
|
||||
isChildChunk?: boolean
|
||||
}
|
||||
|
||||
const ActionButtons: FC<IActionButtonsProps> = ({
|
||||
handleCancel,
|
||||
handleSave,
|
||||
loading,
|
||||
actionType = 'edit',
|
||||
handleRegeneration,
|
||||
isChildChunk = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const mode = useDocumentContext(s => s.mode)
|
||||
const parentMode = useDocumentContext(s => s.parentMode)
|
||||
|
||||
useKeyPress(['esc'], (e) => {
|
||||
e.preventDefault()
|
||||
handleCancel()
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.s`, (e) => {
|
||||
e.preventDefault()
|
||||
if (loading)
|
||||
return
|
||||
handleSave()
|
||||
}
|
||||
, { exactMatch: true, useCapture: true })
|
||||
|
||||
const isParentChildParagraphMode = useMemo(() => {
|
||||
return mode === 'hierarchical' && parentMode === 'paragraph'
|
||||
}, [mode, parentMode])
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<div className='flex items-center gap-x-1'>
|
||||
<span className='text-components-button-secondary-text system-sm-medium'>{t('common.operation.cancel')}</span>
|
||||
<span className='px-[1px] bg-components-kbd-bg-gray rounded-[4px] text-text-tertiary system-kbd'>ESC</span>
|
||||
</div>
|
||||
</Button>
|
||||
{(isParentChildParagraphMode && actionType === 'edit' && !isChildChunk)
|
||||
? <Button
|
||||
onClick={handleRegeneration}
|
||||
disabled={loading}
|
||||
>
|
||||
<span className='text-components-button-secondary-text system-sm-medium'>
|
||||
{t('common.operation.saveAndRegenerate')}
|
||||
</span>
|
||||
</Button>
|
||||
: null
|
||||
}
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
>
|
||||
<div className='flex items-center gap-x-1'>
|
||||
<span className='text-components-button-primary-text'>{t('common.operation.save')}</span>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
<span className='w-4 h-4 bg-components-kbd-bg-white rounded-[4px] text-text-primary-on-surface system-kbd capitalize'>{getKeyboardKeyNameBySystem('ctrl')}</span>
|
||||
<span className='w-4 h-4 bg-components-kbd-bg-white rounded-[4px] text-text-primary-on-surface system-kbd'>S</span>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ActionButtons.displayName = 'ActionButtons'
|
||||
|
||||
export default React.memo(ActionButtons)
|
||||
@@ -1,32 +0,0 @@
|
||||
import React, { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
|
||||
type AddAnotherProps = {
|
||||
className?: string
|
||||
isChecked: boolean
|
||||
onCheck: () => void
|
||||
}
|
||||
|
||||
const AddAnother: FC<AddAnotherProps> = ({
|
||||
className,
|
||||
isChecked,
|
||||
onCheck,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={classNames('flex items-center gap-x-1 pl-1', className)}>
|
||||
<Checkbox
|
||||
key='add-another-checkbox'
|
||||
className='shrink-0'
|
||||
checked={isChecked}
|
||||
onCheck={onCheck}
|
||||
/>
|
||||
<span className='text-text-tertiary system-xs-medium'>{t('datasetDocuments.segment.addAnother')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AddAnother)
|
||||
@@ -1,103 +0,0 @@
|
||||
import React, { type FC } from 'react'
|
||||
import { RiArchive2Line, RiCheckboxCircleLine, RiCloseCircleLine, RiDeleteBinLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
|
||||
const i18nPrefix = 'dataset.batchAction'
|
||||
type IBatchActionProps = {
|
||||
className?: string
|
||||
selectedIds: string[]
|
||||
onBatchEnable: () => void
|
||||
onBatchDisable: () => void
|
||||
onBatchDelete: () => Promise<void>
|
||||
onArchive?: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const BatchAction: FC<IBatchActionProps> = ({
|
||||
className,
|
||||
selectedIds,
|
||||
onBatchEnable,
|
||||
onBatchDisable,
|
||||
onArchive,
|
||||
onBatchDelete,
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isShowDeleteConfirm, {
|
||||
setTrue: showDeleteConfirm,
|
||||
setFalse: hideDeleteConfirm,
|
||||
}] = useBoolean(false)
|
||||
const [isDeleting, {
|
||||
setTrue: setIsDeleting,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
setIsDeleting()
|
||||
await onBatchDelete()
|
||||
hideDeleteConfirm()
|
||||
}
|
||||
return (
|
||||
<div className={classNames('w-full flex justify-center gap-x-2', className)}>
|
||||
<div className='flex items-center gap-x-1 p-1 rounded-[10px] bg-components-actionbar-bg-accent border border-components-actionbar-border-accent shadow-xl shadow-shadow-shadow-5 backdrop-blur-[5px]'>
|
||||
<div className='inline-flex items-center gap-x-2 pl-2 pr-3 py-1'>
|
||||
<span className='w-5 h-5 flex items-center justify-center px-1 py-0.5 bg-text-accent rounded-md text-text-primary-on-surface text-xs font-medium'>
|
||||
{selectedIds.length}
|
||||
</span>
|
||||
<span className='text-text-accent text-[13px] font-semibold leading-[16px]'>{t(`${i18nPrefix}.selected`)}</span>
|
||||
</div>
|
||||
<Divider type='vertical' className='mx-0.5 h-3.5 bg-divider-regular' />
|
||||
<div className='flex items-center gap-x-0.5 px-3 py-2'>
|
||||
<RiCheckboxCircleLine className='w-4 h-4 text-components-button-ghost-text' />
|
||||
<button type='button' className='px-0.5 text-components-button-ghost-text text-[13px] font-medium leading-[16px]' onClick={onBatchEnable}>
|
||||
{t(`${i18nPrefix}.enable`)}
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex items-center gap-x-0.5 px-3 py-2'>
|
||||
<RiCloseCircleLine className='w-4 h-4 text-components-button-ghost-text' />
|
||||
<button type='button' className='px-0.5 text-components-button-ghost-text text-[13px] font-medium leading-[16px]' onClick={onBatchDisable}>
|
||||
{t(`${i18nPrefix}.disable`)}
|
||||
</button>
|
||||
</div>
|
||||
{onArchive && (
|
||||
<div className='flex items-center gap-x-0.5 px-3 py-2'>
|
||||
<RiArchive2Line className='w-4 h-4 text-components-button-ghost-text' />
|
||||
<button type='button' className='px-0.5 text-components-button-ghost-text text-[13px] font-medium leading-[16px]' onClick={onArchive}>
|
||||
{t(`${i18nPrefix}.archive`)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center gap-x-0.5 px-3 py-2'>
|
||||
<RiDeleteBinLine className='w-4 h-4 text-components-button-destructive-ghost-text' />
|
||||
<button type='button' className='px-0.5 text-components-button-destructive-ghost-text text-[13px] font-medium leading-[16px]' onClick={showDeleteConfirm}>
|
||||
{t(`${i18nPrefix}.delete`)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Divider type='vertical' className='mx-0.5 h-3.5 bg-divider-regular' />
|
||||
<button type='button' className='px-3.5 py-2 text-components-button-ghost-text text-[13px] font-medium leading-[16px]' onClick={onCancel}>
|
||||
{t(`${i18nPrefix}.cancel`)}
|
||||
</button>
|
||||
</div>
|
||||
{
|
||||
isShowDeleteConfirm && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('datasetDocuments.list.delete.title')}
|
||||
content={t('datasetDocuments.list.delete.content')}
|
||||
confirmText={t('common.operation.sure')}
|
||||
onConfirm={handleBatchDelete}
|
||||
onCancel={hideDeleteConfirm}
|
||||
isLoading={isDeleting}
|
||||
isDisabled={isDeleting}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(BatchAction)
|
||||
@@ -1,192 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import type { ComponentProps, FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
type IContentProps = ComponentProps<'textarea'>
|
||||
|
||||
const Textarea: FC<IContentProps> = React.memo(({
|
||||
value,
|
||||
placeholder,
|
||||
className,
|
||||
disabled,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<textarea
|
||||
className={classNames(
|
||||
'disabled:bg-transparent inset-0 outline-none border-none appearance-none resize-none w-full overflow-y-auto',
|
||||
className,
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
Textarea.displayName = 'Textarea'
|
||||
|
||||
type IAutoResizeTextAreaProps = ComponentProps<'textarea'> & {
|
||||
containerRef: React.RefObject<HTMLDivElement>
|
||||
labelRef: React.RefObject<HTMLDivElement>
|
||||
}
|
||||
|
||||
const AutoResizeTextArea: FC<IAutoResizeTextAreaProps> = React.memo(({
|
||||
className,
|
||||
placeholder,
|
||||
value,
|
||||
disabled,
|
||||
containerRef,
|
||||
labelRef,
|
||||
...rest
|
||||
}) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const observerRef = useRef<ResizeObserver>()
|
||||
const [maxHeight, setMaxHeight] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current
|
||||
if (!textarea)
|
||||
return
|
||||
textarea.style.height = 'auto'
|
||||
const lineHeight = parseInt(getComputedStyle(textarea).lineHeight)
|
||||
const textareaHeight = Math.max(textarea.scrollHeight, lineHeight)
|
||||
textarea.style.height = `${textareaHeight}px`
|
||||
}, [value])
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
const label = labelRef.current
|
||||
if (!container || !label)
|
||||
return
|
||||
const updateMaxHeight = () => {
|
||||
const containerHeight = container.clientHeight
|
||||
const labelHeight = label.clientHeight
|
||||
const padding = 32
|
||||
const space = 12
|
||||
const maxHeight = Math.floor((containerHeight - 2 * labelHeight - padding - space) / 2)
|
||||
setMaxHeight(maxHeight)
|
||||
}
|
||||
updateMaxHeight()
|
||||
observerRef.current = new ResizeObserver(updateMaxHeight)
|
||||
observerRef.current.observe(container)
|
||||
return () => {
|
||||
observerRef.current?.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={classNames(
|
||||
'disabled:bg-transparent inset-0 outline-none border-none appearance-none resize-none w-full',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
maxHeight,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
AutoResizeTextArea.displayName = 'AutoResizeTextArea'
|
||||
|
||||
type IQATextAreaProps = {
|
||||
question: string
|
||||
answer?: string
|
||||
onQuestionChange: (question: string) => void
|
||||
onAnswerChange?: (answer: string) => void
|
||||
isEditMode?: boolean
|
||||
}
|
||||
|
||||
const QATextArea: FC<IQATextAreaProps> = React.memo(({
|
||||
question,
|
||||
answer,
|
||||
onQuestionChange,
|
||||
onAnswerChange,
|
||||
isEditMode = true,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const labelRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className='h-full overflow-hidden'>
|
||||
<div ref={labelRef} className='text-text-tertiary text-xs font-medium mb-1'>QUESTION</div>
|
||||
<AutoResizeTextArea
|
||||
className='text-text-secondary text-sm tracking-[-0.07px] caret-[#295EFF]'
|
||||
value={question}
|
||||
placeholder={t('datasetDocuments.segment.questionPlaceholder') || ''}
|
||||
onChange={e => onQuestionChange(e.target.value)}
|
||||
disabled={!isEditMode}
|
||||
containerRef={containerRef}
|
||||
labelRef={labelRef}
|
||||
/>
|
||||
<div className='text-text-tertiary text-xs font-medium mb-1 mt-6'>ANSWER</div>
|
||||
<AutoResizeTextArea
|
||||
className='text-text-secondary text-sm tracking-[-0.07px] caret-[#295EFF]'
|
||||
value={answer}
|
||||
placeholder={t('datasetDocuments.segment.answerPlaceholder') || ''}
|
||||
onChange={e => onAnswerChange?.(e.target.value)}
|
||||
disabled={!isEditMode}
|
||||
autoFocus
|
||||
containerRef={containerRef}
|
||||
labelRef={labelRef}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
QATextArea.displayName = 'QATextArea'
|
||||
|
||||
type IChunkContentProps = {
|
||||
question: string
|
||||
answer?: string
|
||||
onQuestionChange: (question: string) => void
|
||||
onAnswerChange?: (answer: string) => void
|
||||
isEditMode?: boolean
|
||||
docForm: ChunkingMode
|
||||
}
|
||||
|
||||
const ChunkContent: FC<IChunkContentProps> = ({
|
||||
question,
|
||||
answer,
|
||||
onQuestionChange,
|
||||
onAnswerChange,
|
||||
isEditMode,
|
||||
docForm,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (docForm === ChunkingMode.qa) {
|
||||
return <QATextArea
|
||||
question={question}
|
||||
answer={answer}
|
||||
onQuestionChange={onQuestionChange}
|
||||
onAnswerChange={onAnswerChange}
|
||||
isEditMode={isEditMode}
|
||||
/>
|
||||
}
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
className='h-full w-full pb-6 body-md-regular text-text-secondary tracking-[-0.07px] caret-[#295EFF]'
|
||||
value={question}
|
||||
placeholder={t('datasetDocuments.segment.contentPlaceholder') || ''}
|
||||
onChange={e => onQuestionChange(e.target.value)}
|
||||
disabled={!isEditMode}
|
||||
autoFocus
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
ChunkContent.displayName = 'ChunkContent'
|
||||
|
||||
export default React.memo(ChunkContent)
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
const Dot = () => {
|
||||
return (
|
||||
<div className='text-text-quaternary system-xs-medium'>·</div>
|
||||
)
|
||||
}
|
||||
|
||||
Dot.displayName = 'Dot'
|
||||
|
||||
export default React.memo(Dot)
|
||||
@@ -1,78 +0,0 @@
|
||||
import React, { type FC } from 'react'
|
||||
import { RiFileList2Line } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type IEmptyProps = {
|
||||
onClearFilter: () => void
|
||||
}
|
||||
|
||||
const EmptyCard = React.memo(() => {
|
||||
return (
|
||||
<div className='w-full h-32 rounded-xl opacity-30 bg-background-section-burn shrink-0' />
|
||||
)
|
||||
})
|
||||
|
||||
EmptyCard.displayName = 'EmptyCard'
|
||||
|
||||
type LineProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Line = React.memo(({
|
||||
className,
|
||||
}: LineProps) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="2" height="241" viewBox="0 0 2 241" fill="none" className={className}>
|
||||
<path d="M1 0.5L1 240.5" stroke="url(#paint0_linear_1989_74474)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1989_74474" x1="-7.99584" y1="240.5" x2="-7.88094" y2="0.50004" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white" stopOpacity="0.01"/>
|
||||
<stop offset="0.503965" stopColor="#101828" stopOpacity="0.08"/>
|
||||
<stop offset="1" stopColor="white" stopOpacity="0.01"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
})
|
||||
|
||||
Line.displayName = 'Line'
|
||||
|
||||
const Empty: FC<IEmptyProps> = ({
|
||||
onClearFilter,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={'h-full relative flex items-center justify-center z-0'}>
|
||||
<div className='flex flex-col items-center'>
|
||||
<div className='relative z-10 flex items-center justify-center w-14 h-14 border border-divider-subtle bg-components-card-bg rounded-xl shadow-lg shadow-shadow-shadow-5'>
|
||||
<RiFileList2Line className='w-6 h-6 text-text-secondary' />
|
||||
<Line className='absolute -right-[1px] top-1/2 -translate-y-1/2' />
|
||||
<Line className='absolute -left-[1px] top-1/2 -translate-y-1/2' />
|
||||
<Line className='absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-90' />
|
||||
<Line className='absolute top-full left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-90' />
|
||||
</div>
|
||||
<div className='text-text-tertiary system-md-regular mt-3'>
|
||||
{t('datasetDocuments.segment.empty')}
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
className='text-text-accent system-sm-medium mt-1'
|
||||
onClick={onClearFilter}
|
||||
>
|
||||
{t('datasetDocuments.segment.clearFilter')}
|
||||
</button>
|
||||
</div>
|
||||
<div className='h-full w-full absolute top-0 left-0 flex flex-col gap-y-3 -z-20 overflow-hidden'>
|
||||
{
|
||||
Array.from({ length: 10 }).map((_, i) => (
|
||||
<EmptyCard key={i} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className='h-full w-full absolute top-0 left-0 bg-dataset-chunk-list-mask-bg -z-10' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Empty)
|
||||
@@ -1,35 +0,0 @@
|
||||
import React, { type FC } from 'react'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
type IFullScreenDrawerProps = {
|
||||
isOpen: boolean
|
||||
onClose?: () => void
|
||||
fullScreen: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const FullScreenDrawer: FC<IFullScreenDrawerProps> = ({
|
||||
isOpen,
|
||||
onClose = () => {},
|
||||
fullScreen,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<Drawer
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
panelClassname={classNames('!p-0 bg-components-panel-bg',
|
||||
fullScreen
|
||||
? '!max-w-full !w-full'
|
||||
: 'mt-16 mr-2 mb-2 !max-w-[560px] !w-[560px] border-[0.5px] border-components-panel-border rounded-xl',
|
||||
)}
|
||||
mask={false}
|
||||
unmount
|
||||
footer={null}
|
||||
>
|
||||
{children}
|
||||
</Drawer>)
|
||||
}
|
||||
|
||||
export default FullScreenDrawer
|
||||
@@ -1,47 +0,0 @@
|
||||
import React, { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import classNames from '@/utils/classnames'
|
||||
import type { SegmentDetailModel } from '@/models/datasets'
|
||||
import TagInput from '@/app/components/base/tag-input'
|
||||
|
||||
type IKeywordsProps = {
|
||||
segInfo?: Partial<SegmentDetailModel> & { id: string }
|
||||
className?: string
|
||||
keywords: string[]
|
||||
onKeywordsChange: (keywords: string[]) => void
|
||||
isEditMode?: boolean
|
||||
actionType?: 'edit' | 'add' | 'view'
|
||||
}
|
||||
|
||||
const Keywords: FC<IKeywordsProps> = ({
|
||||
segInfo,
|
||||
className,
|
||||
keywords,
|
||||
onKeywordsChange,
|
||||
isEditMode,
|
||||
actionType = 'view',
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className={classNames('flex flex-col', className)}>
|
||||
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('datasetDocuments.segment.keywords')}</div>
|
||||
<div className='text-text-tertiary w-full max-h-[200px] overflow-auto flex flex-wrap gap-1'>
|
||||
{(!segInfo?.keywords?.length && actionType === 'view')
|
||||
? '-'
|
||||
: (
|
||||
<TagInput
|
||||
items={keywords}
|
||||
onChange={newKeywords => onKeywordsChange(newKeywords)}
|
||||
disableAdd={!isEditMode}
|
||||
disableRemove={!isEditMode || (keywords.length === 1)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Keywords.displayName = 'Keywords'
|
||||
|
||||
export default React.memo(Keywords)
|
||||
@@ -1,131 +0,0 @@
|
||||
import React, { type FC, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { useCountDown } from 'ahooks'
|
||||
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='pb-4'>
|
||||
<span className='text-text-primary title-2xl-semi-bold'>{t('datasetDocuments.segment.regenerationConfirmTitle')}</span>
|
||||
<p className='text-text-secondary system-md-regular'>{t('datasetDocuments.segment.regenerationConfirmMessage')}</p>
|
||||
</div>
|
||||
<div className='flex justify-end gap-x-2 pt-6'>
|
||||
<Button onClick={onCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button variant='warning' destructive onClick={onConfirm}>
|
||||
{t('common.operation.regenerate')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
DefaultContent.displayName = 'DefaultContent'
|
||||
|
||||
const RegeneratingContent: FC = React.memo(() => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='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 pt-6'>
|
||||
<Button variant='warning' destructive disabled className='inline-flex items-center gap-x-0.5'>
|
||||
<RiLoader2Line className='w-4 h-4 text-components-button-destructive-primary-text-disabled animate-spin' />
|
||||
<span>{t('common.operation.regenerate')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
RegeneratingContent.displayName = 'RegeneratingContent'
|
||||
|
||||
type IRegenerationCompletedContentProps = {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const RegenerationCompletedContent: FC<IRegenerationCompletedContentProps> = React.memo(({
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const targetTime = useRef(Date.now() + 5000)
|
||||
const [countdown] = useCountDown({
|
||||
targetDate: targetTime.current,
|
||||
onEnd: () => {
|
||||
onClose()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='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 pt-6'>
|
||||
<Button variant='primary' onClick={onClose}>
|
||||
{`${t('common.operation.close')}${countdown === 0 ? '' : `(${Math.round(countdown / 1000)})`}`}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
RegenerationCompletedContent.displayName = 'RegenerationCompletedContent'
|
||||
|
||||
type IRegenerationModalProps = {
|
||||
isShow: boolean
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const RegenerationModal: FC<IRegenerationModalProps> = ({
|
||||
isShow,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
onClose,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [updateSucceeded, setUpdateSucceeded] = useState(false)
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
eventEmitter?.useSubscription((v) => {
|
||||
if (v === 'update-segment') {
|
||||
setLoading(true)
|
||||
setUpdateSucceeded(false)
|
||||
}
|
||||
if (v === 'update-segment-success')
|
||||
setUpdateSucceeded(true)
|
||||
if (v === 'update-segment-done')
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal isShow={isShow} onClose={() => {}} className='!max-w-[480px] !rounded-2xl'>
|
||||
{!loading && !updateSucceeded && <DefaultContent onCancel={onCancel} onConfirm={onConfirm} />}
|
||||
{loading && !updateSucceeded && <RegeneratingContent />}
|
||||
{!loading && updateSucceeded && <RegenerationCompletedContent onClose={onClose} />}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default RegenerationModal
|
||||
@@ -1,40 +0,0 @@
|
||||
import React, { type FC, useMemo } from 'react'
|
||||
import { Chunk } from '@/app/components/base/icons/src/public/knowledge'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ISegmentIndexTagProps = {
|
||||
positionId?: string | number
|
||||
label?: string
|
||||
className?: string
|
||||
labelPrefix?: string
|
||||
iconClassName?: string
|
||||
labelClassName?: string
|
||||
}
|
||||
|
||||
export const SegmentIndexTag: FC<ISegmentIndexTagProps> = ({
|
||||
positionId,
|
||||
label,
|
||||
className,
|
||||
labelPrefix = 'Chunk',
|
||||
iconClassName,
|
||||
labelClassName,
|
||||
}) => {
|
||||
const localPositionId = useMemo(() => {
|
||||
const positionIdStr = String(positionId)
|
||||
if (positionIdStr.length >= 2)
|
||||
return `${labelPrefix}-${positionId}`
|
||||
return `${labelPrefix}-${positionIdStr.padStart(2, '0')}`
|
||||
}, [positionId, labelPrefix])
|
||||
return (
|
||||
<div className={cn('flex items-center', className)}>
|
||||
<Chunk className={cn('w-3 h-3 p-[1px] text-text-tertiary mr-0.5', iconClassName)} />
|
||||
<div className={cn('text-text-tertiary system-xs-medium', labelClassName)}>
|
||||
{label || localPositionId}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
SegmentIndexTag.displayName = 'SegmentIndexTag'
|
||||
|
||||
export default React.memo(SegmentIndexTag)
|
||||
@@ -1,15 +0,0 @@
|
||||
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 max-w-12 line-clamp-1 shrink-0'>{text}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Tag.displayName = 'Tag'
|
||||
|
||||
export default React.memo(Tag)
|
||||
@@ -1,40 +0,0 @@
|
||||
import React, { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiLineHeight } from '@remixicon/react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Collapse } from '@/app/components/base/icons/src/public/knowledge'
|
||||
|
||||
type DisplayToggleProps = {
|
||||
isCollapsed: boolean
|
||||
toggleCollapsed: () => void
|
||||
}
|
||||
|
||||
const DisplayToggle: FC<DisplayToggleProps> = ({
|
||||
isCollapsed,
|
||||
toggleCollapsed,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={isCollapsed ? t('datasetDocuments.segment.expandChunks') : t('datasetDocuments.segment.collapseChunks')}
|
||||
popupClassName='text-text-secondary system-xs-medium border-[0.5px] border-components-panel-border'
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='flex items-center justify-center p-2 rounded-lg bg-components-button-secondary-bg
|
||||
border-[0.5px] border-components-button-secondary-border shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px]'
|
||||
onClick={toggleCollapsed}
|
||||
>
|
||||
{
|
||||
isCollapsed
|
||||
? <RiLineHeight className='w-4 h-4 text-components-button-secondary-text' />
|
||||
: <Collapse className='w-4 h-4 text-components-button-secondary-text' />
|
||||
}
|
||||
</button>
|
||||
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(DisplayToggle)
|
||||
@@ -1,79 +1,220 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { memo, useEffect, useMemo, useState } from 'react'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import { HashtagIcon } from '@heroicons/react/24/solid'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { createContext, useContext, useContextSelector } from 'use-context-selector'
|
||||
import { useDocumentContext } from '../index'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { isNil, omitBy } from 'lodash-es'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiEditLine,
|
||||
} from '@remixicon/react'
|
||||
import { StatusItem } from '../../list'
|
||||
import { DocumentContext } from '../index'
|
||||
import { ProcessStatus } from '../segment-add'
|
||||
import s from './style.module.css'
|
||||
import SegmentList from './segment-list'
|
||||
import DisplayToggle from './display-toggle'
|
||||
import BatchAction from './common/batch-action'
|
||||
import SegmentDetail from './segment-detail'
|
||||
import SegmentCard from './segment-card'
|
||||
import ChildSegmentList from './child-segment-list'
|
||||
import NewChildSegment from './new-child-segment'
|
||||
import FullScreenDrawer from './common/full-screen-drawer'
|
||||
import ChildSegmentDetail from './child-segment-detail'
|
||||
import StatusItem from './status-item'
|
||||
import Pagination from '@/app/components/base/pagination'
|
||||
import InfiniteVirtualList from './InfiniteVirtualList'
|
||||
import cn from '@/utils/classnames'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import { type ChildChunkDetail, ChunkingMode, type SegmentDetailModel, type SegmentUpdater } from '@/models/datasets'
|
||||
import NewSegment from '@/app/components/datasets/documents/detail/new-segment'
|
||||
import { deleteSegment, disableSegment, enableSegment, fetchSegments, updateSegment } from '@/service/datasets'
|
||||
import type { SegmentDetailModel, SegmentUpdater, SegmentsQuery, SegmentsResponse } from '@/models/datasets'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import type { CommonResponse } from '@/models/common'
|
||||
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea/common'
|
||||
import Button from '@/app/components/base/button'
|
||||
import NewSegmentModal from '@/app/components/datasets/documents/detail/new-segment-modal'
|
||||
import TagInput from '@/app/components/base/tag-input'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import {
|
||||
useChildSegmentList,
|
||||
useChildSegmentListKey,
|
||||
useDeleteChildSegment,
|
||||
useDeleteSegment,
|
||||
useDisableSegment,
|
||||
useEnableSegment,
|
||||
useSegmentList,
|
||||
useSegmentListKey,
|
||||
useUpdateChildSegment,
|
||||
useUpdateSegment,
|
||||
} from '@/service/knowledge/use-segment'
|
||||
import { useInvalid } from '@/service/use-base'
|
||||
|
||||
const DEFAULT_LIMIT = 10
|
||||
|
||||
type CurrSegmentType = {
|
||||
segInfo?: SegmentDetailModel
|
||||
showModal: boolean
|
||||
isEditMode?: boolean
|
||||
export const SegmentIndexTag: FC<{ positionId: string | number; className?: string }> = ({ positionId, className }) => {
|
||||
const localPositionId = useMemo(() => {
|
||||
const positionIdStr = String(positionId)
|
||||
if (positionIdStr.length >= 3)
|
||||
return positionId
|
||||
return positionIdStr.padStart(3, '0')
|
||||
}, [positionId])
|
||||
return (
|
||||
<div className={`text-gray-500 border border-gray-200 box-border flex items-center rounded-md italic text-[11px] pl-1 pr-1.5 font-medium ${className ?? ''}`}>
|
||||
<HashtagIcon className='w-3 h-3 text-gray-400 fill-current mr-1 stroke-current stroke-1' />
|
||||
{localPositionId}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type CurrChildChunkType = {
|
||||
childChunkInfo?: ChildChunkDetail
|
||||
showModal: boolean
|
||||
type ISegmentDetailProps = {
|
||||
embeddingAvailable: boolean
|
||||
segInfo?: Partial<SegmentDetailModel> & { id: string }
|
||||
onChangeSwitch?: (segId: string, enabled: boolean) => Promise<void>
|
||||
onUpdate: (segmentId: string, q: string, a: string, k: string[]) => void
|
||||
onCancel: () => void
|
||||
archived?: boolean
|
||||
}
|
||||
/**
|
||||
* Show all the contents of the segment
|
||||
*/
|
||||
const SegmentDetailComponent: FC<ISegmentDetailProps> = ({
|
||||
embeddingAvailable,
|
||||
segInfo,
|
||||
archived,
|
||||
onChangeSwitch,
|
||||
onUpdate,
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [question, setQuestion] = useState(segInfo?.content || '')
|
||||
const [answer, setAnswer] = useState(segInfo?.answer || '')
|
||||
const [keywords, setKeywords] = useState<string[]>(segInfo?.keywords || [])
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
type SegmentListContextValue = {
|
||||
isCollapsed: boolean
|
||||
fullScreen: boolean
|
||||
toggleFullScreen: (fullscreen?: boolean) => void
|
||||
currSegment: CurrSegmentType
|
||||
currChildChunk: CurrChildChunkType
|
||||
eventEmitter?.useSubscription((v) => {
|
||||
if (v === 'update-segment')
|
||||
setLoading(true)
|
||||
else
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsEditing(false)
|
||||
setQuestion(segInfo?.content || '')
|
||||
setAnswer(segInfo?.answer || '')
|
||||
setKeywords(segInfo?.keywords || [])
|
||||
}
|
||||
const handleSave = () => {
|
||||
onUpdate(segInfo?.id || '', question, answer, keywords)
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (segInfo?.answer) {
|
||||
return (
|
||||
<>
|
||||
<div className='mb-1 text-xs font-medium text-gray-500'>QUESTION</div>
|
||||
<AutoHeightTextarea
|
||||
outerClassName='mb-4'
|
||||
className='leading-6 text-md text-gray-800'
|
||||
value={question}
|
||||
placeholder={t('datasetDocuments.segment.questionPlaceholder') || ''}
|
||||
onChange={e => setQuestion(e.target.value)}
|
||||
disabled={!isEditing}
|
||||
/>
|
||||
<div className='mb-1 text-xs font-medium text-gray-500'>ANSWER</div>
|
||||
<AutoHeightTextarea
|
||||
outerClassName='mb-4'
|
||||
className='leading-6 text-md text-gray-800'
|
||||
value={answer}
|
||||
placeholder={t('datasetDocuments.segment.answerPlaceholder') || ''}
|
||||
onChange={e => setAnswer(e.target.value)}
|
||||
disabled={!isEditing}
|
||||
autoFocus
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AutoHeightTextarea
|
||||
className='leading-6 text-md text-gray-800'
|
||||
value={question}
|
||||
placeholder={t('datasetDocuments.segment.contentPlaceholder') || ''}
|
||||
onChange={e => setQuestion(e.target.value)}
|
||||
disabled={!isEditing}
|
||||
autoFocus
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col relative'}>
|
||||
<div className='absolute right-0 top-0 flex items-center h-7'>
|
||||
{isEditing && (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='ml-3'
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!isEditing && !archived && embeddingAvailable && (
|
||||
<>
|
||||
<div className='group relative flex justify-center items-center w-6 h-6 hover:bg-gray-100 rounded-md cursor-pointer'>
|
||||
<div className={cn(s.editTip, 'hidden items-center absolute -top-10 px-3 h-[34px] bg-white rounded-lg whitespace-nowrap text-xs font-semibold text-gray-700 group-hover:flex')}>{t('common.operation.edit')}</div>
|
||||
<RiEditLine className='w-4 h-4 text-gray-500' onClick={() => setIsEditing(true)} />
|
||||
</div>
|
||||
<div className='mx-3 w-[1px] h-3 bg-gray-200' />
|
||||
</>
|
||||
)}
|
||||
<div className='flex justify-center items-center w-6 h-6 cursor-pointer' onClick={onCancel}>
|
||||
<RiCloseLine className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
<SegmentIndexTag positionId={segInfo?.position || ''} className='w-fit mt-[2px] mb-6' />
|
||||
<div className={s.segModalContent}>{renderContent()}</div>
|
||||
<div className={s.keywordTitle}>{t('datasetDocuments.segment.keywords')}</div>
|
||||
<div className={s.keywordWrapper}>
|
||||
{!segInfo?.keywords?.length
|
||||
? '-'
|
||||
: (
|
||||
<TagInput
|
||||
items={keywords}
|
||||
onChange={newKeywords => setKeywords(newKeywords)}
|
||||
disableAdd={!isEditing}
|
||||
disableRemove={!isEditing || (keywords.length === 1)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className={cn(s.footer, s.numberInfo)}>
|
||||
<div className='flex items-center flex-wrap gap-y-2'>
|
||||
<div className={cn(s.commonIcon, s.typeSquareIcon)} /><span className='mr-8'>{formatNumber(segInfo?.word_count as number)} {t('datasetDocuments.segment.characters')}</span>
|
||||
<div className={cn(s.commonIcon, s.targetIcon)} /><span className='mr-8'>{formatNumber(segInfo?.hit_count as number)} {t('datasetDocuments.segment.hitCount')}</span>
|
||||
<div className={cn(s.commonIcon, s.bezierCurveIcon)} /><span className={s.hashText}>{t('datasetDocuments.segment.vectorHash')}{segInfo?.index_node_hash}</span>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<StatusItem status={segInfo?.enabled ? 'enabled' : 'disabled'} reverse textCls='text-gray-500 text-xs' />
|
||||
{embeddingAvailable && (
|
||||
<>
|
||||
<Divider type='vertical' className='!h-2' />
|
||||
<Switch
|
||||
size='md'
|
||||
defaultValue={segInfo?.enabled}
|
||||
onChange={async (val) => {
|
||||
await onChangeSwitch?.(segInfo?.id || '', val)
|
||||
}}
|
||||
disabled={archived}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export const SegmentDetail = memo(SegmentDetailComponent)
|
||||
|
||||
const SegmentListContext = createContext<SegmentListContextValue>({
|
||||
isCollapsed: true,
|
||||
fullScreen: false,
|
||||
toggleFullScreen: () => {},
|
||||
currSegment: { showModal: false },
|
||||
currChildChunk: { showModal: false },
|
||||
})
|
||||
|
||||
export const useSegmentListContext = (selector: (value: SegmentListContextValue) => any) => {
|
||||
return useContextSelector(SegmentListContext, selector)
|
||||
export const splitArray = (arr: any[], size = 3) => {
|
||||
if (!arr || !arr.length)
|
||||
return []
|
||||
const result = []
|
||||
for (let i = 0; i < arr.length; i += size)
|
||||
result.push(arr.slice(i, i + size))
|
||||
return result
|
||||
}
|
||||
|
||||
type ICompletedProps = {
|
||||
@@ -82,6 +223,7 @@ type ICompletedProps = {
|
||||
onNewSegmentModalChange: (state: boolean) => void
|
||||
importStatus: ProcessStatus | string | undefined
|
||||
archived?: boolean
|
||||
// data: Array<{}> // all/part segments
|
||||
}
|
||||
/**
|
||||
* Embedding done, show list of all segments
|
||||
@@ -96,42 +238,22 @@ const Completed: FC<ICompletedProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const datasetId = useDocumentContext(s => s.datasetId) || ''
|
||||
const documentId = useDocumentContext(s => s.documentId) || ''
|
||||
const docForm = useDocumentContext(s => s.docForm)
|
||||
const mode = useDocumentContext(s => s.mode)
|
||||
const parentMode = useDocumentContext(s => s.parentMode)
|
||||
const { datasetId = '', documentId = '', docForm } = useContext(DocumentContext)
|
||||
// the current segment id and whether to show the modal
|
||||
const [currSegment, setCurrSegment] = useState<CurrSegmentType>({ showModal: false })
|
||||
const [currChildChunk, setCurrChildChunk] = useState<CurrChildChunkType>({ showModal: false })
|
||||
const [currChunkId, setCurrChunkId] = useState('')
|
||||
const [currSegment, setCurrSegment] = useState<{ segInfo?: SegmentDetailModel; showModal: boolean }>({ showModal: false })
|
||||
|
||||
const [inputValue, setInputValue] = useState<string>('') // the input value
|
||||
const [searchValue, setSearchValue] = useState<string>('') // the search value
|
||||
const [selectedStatus, setSelectedStatus] = useState<boolean | 'all'>('all') // the selected status, enabled/disabled/undefined
|
||||
|
||||
const [segments, setSegments] = useState<SegmentDetailModel[]>([]) // all segments data
|
||||
const [childSegments, setChildSegments] = useState<ChildChunkDetail[]>([]) // all child segments data
|
||||
const [selectedSegmentIds, setSelectedSegmentIds] = useState<string[]>([])
|
||||
const [lastSegmentsRes, setLastSegmentsRes] = useState<SegmentsResponse | undefined>(undefined)
|
||||
const [allSegments, setAllSegments] = useState<Array<SegmentDetailModel[]>>([]) // all segments data
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [total, setTotal] = useState<number | undefined>()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [isCollapsed, setIsCollapsed] = useState(true)
|
||||
const [currentPage, setCurrentPage] = useState(1) // start from 1
|
||||
const [limit, setLimit] = useState(DEFAULT_LIMIT)
|
||||
const [fullScreen, setFullScreen] = useState(false)
|
||||
const [showNewChildSegmentModal, setShowNewChildSegmentModal] = useState(false)
|
||||
|
||||
const segmentListRef = useRef<HTMLDivElement>(null)
|
||||
const childSegmentListRef = useRef<HTMLDivElement>(null)
|
||||
const needScrollToBottom = useRef(false)
|
||||
const statusList = useRef<Item[]>([
|
||||
{ value: 'all', name: t('datasetDocuments.list.index.all') },
|
||||
{ value: 0, name: t('datasetDocuments.list.status.disabled') },
|
||||
{ value: 1, name: t('datasetDocuments.list.status.enabled') },
|
||||
])
|
||||
|
||||
const { run: handleSearch } = useDebounceFn(() => {
|
||||
setSearchValue(inputValue)
|
||||
setCurrentPage(1)
|
||||
}, { wait: 500 })
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
@@ -141,145 +263,78 @@ const Completed: FC<ICompletedProps> = ({
|
||||
|
||||
const onChangeStatus = ({ value }: Item) => {
|
||||
setSelectedStatus(value === 'all' ? 'all' : !!value)
|
||||
setCurrentPage(1)
|
||||
}
|
||||
|
||||
const isFullDocMode = useMemo(() => {
|
||||
return mode === 'hierarchical' && parentMode === 'full-doc'
|
||||
}, [mode, parentMode])
|
||||
|
||||
const { isFetching: isLoadingSegmentList, data: segmentListData } = useSegmentList(
|
||||
{
|
||||
const getSegments = async (needLastId?: boolean) => {
|
||||
const finalLastId = lastSegmentsRes?.data?.[lastSegmentsRes.data.length - 1]?.id || ''
|
||||
setLoading(true)
|
||||
const [e, res] = await asyncRunSafe<SegmentsResponse>(fetchSegments({
|
||||
datasetId,
|
||||
documentId,
|
||||
params: {
|
||||
page: isFullDocMode ? 1 : currentPage,
|
||||
limit: isFullDocMode ? 10 : limit,
|
||||
keyword: isFullDocMode ? '' : searchValue,
|
||||
enabled: selectedStatus === 'all' ? 'all' : !!selectedStatus,
|
||||
},
|
||||
},
|
||||
currentPage === 0,
|
||||
)
|
||||
const invalidSegmentList = useInvalid(useSegmentListKey)
|
||||
|
||||
useEffect(() => {
|
||||
if (segmentListData) {
|
||||
setSegments(segmentListData.data || [])
|
||||
if (segmentListData.total_pages < currentPage)
|
||||
setCurrentPage(segmentListData.total_pages)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [segmentListData])
|
||||
|
||||
useEffect(() => {
|
||||
if (segmentListRef.current && needScrollToBottom.current) {
|
||||
segmentListRef.current.scrollTo({ top: segmentListRef.current.scrollHeight, behavior: 'smooth' })
|
||||
needScrollToBottom.current = false
|
||||
}
|
||||
}, [segments])
|
||||
|
||||
const { isFetching: isLoadingChildSegmentList, data: childChunkListData } = useChildSegmentList(
|
||||
{
|
||||
datasetId,
|
||||
documentId,
|
||||
segmentId: segments[0]?.id || '',
|
||||
params: {
|
||||
page: currentPage,
|
||||
limit,
|
||||
params: omitBy({
|
||||
last_id: !needLastId ? undefined : finalLastId,
|
||||
limit: 12,
|
||||
keyword: searchValue,
|
||||
},
|
||||
},
|
||||
!isFullDocMode || segments.length === 0 || currentPage === 0,
|
||||
)
|
||||
const invalidChildSegmentList = useInvalid(useChildSegmentListKey)
|
||||
|
||||
useEffect(() => {
|
||||
if (childSegmentListRef.current && needScrollToBottom.current) {
|
||||
childSegmentListRef.current.scrollTo({ top: childSegmentListRef.current.scrollHeight, behavior: 'smooth' })
|
||||
needScrollToBottom.current = false
|
||||
enabled: selectedStatus === 'all' ? 'all' : !!selectedStatus,
|
||||
}, isNil) as SegmentsQuery,
|
||||
}) as Promise<SegmentsResponse>)
|
||||
if (!e) {
|
||||
setAllSegments([...(!needLastId ? [] : allSegments), ...splitArray(res.data || [])])
|
||||
setLastSegmentsRes(res)
|
||||
if (!lastSegmentsRes || !needLastId)
|
||||
setTotal(res?.total || 0)
|
||||
}
|
||||
}, [childSegments])
|
||||
|
||||
useEffect(() => {
|
||||
if (childChunkListData) {
|
||||
setChildSegments(childChunkListData.data || [])
|
||||
if (childChunkListData.total_pages < currentPage)
|
||||
setCurrentPage(childChunkListData.total_pages)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [childChunkListData])
|
||||
|
||||
const resetList = useCallback(() => {
|
||||
setSegments([])
|
||||
setSelectedSegmentIds([])
|
||||
invalidSegmentList()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const resetChildList = useCallback(() => {
|
||||
setChildSegments([])
|
||||
invalidChildSegmentList()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const onClickCard = (detail: SegmentDetailModel, isEditMode = false) => {
|
||||
setCurrSegment({ segInfo: detail, showModal: true, isEditMode })
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const onCloseSegmentDetail = useCallback(() => {
|
||||
setCurrSegment({ showModal: false })
|
||||
setFullScreen(false)
|
||||
}, [])
|
||||
const resetList = () => {
|
||||
setLastSegmentsRes(undefined)
|
||||
setAllSegments([])
|
||||
setLoading(false)
|
||||
setTotal(undefined)
|
||||
getSegments(false)
|
||||
}
|
||||
|
||||
const { mutateAsync: enableSegment } = useEnableSegment()
|
||||
const { mutateAsync: disableSegment } = useDisableSegment()
|
||||
const onClickCard = (detail: SegmentDetailModel) => {
|
||||
setCurrSegment({ segInfo: detail, showModal: true })
|
||||
}
|
||||
|
||||
const onChangeSwitch = useCallback(async (enable: boolean, segId?: string) => {
|
||||
const operationApi = enable ? enableSegment : disableSegment
|
||||
await operationApi({ datasetId, documentId, segmentIds: segId ? [segId] : selectedSegmentIds }, {
|
||||
onSuccess: () => {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
for (const seg of segments) {
|
||||
if (segId ? seg.id === segId : selectedSegmentIds.includes(seg.id))
|
||||
seg.enabled = enable
|
||||
const onCloseModal = () => {
|
||||
setCurrSegment({ ...currSegment, showModal: false })
|
||||
}
|
||||
|
||||
const onChangeSwitch = async (segId: string, enabled: boolean) => {
|
||||
const opApi = enabled ? enableSegment : disableSegment
|
||||
const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId, segmentId: segId }) as Promise<CommonResponse>)
|
||||
if (!e) {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
for (const item of allSegments) {
|
||||
for (const seg of item) {
|
||||
if (seg.id === segId)
|
||||
seg.enabled = enabled
|
||||
}
|
||||
setSegments([...segments])
|
||||
},
|
||||
onError: () => {
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [datasetId, documentId, selectedSegmentIds, segments])
|
||||
}
|
||||
setAllSegments([...allSegments])
|
||||
}
|
||||
else {
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
}
|
||||
}
|
||||
|
||||
const { mutateAsync: deleteSegment } = useDeleteSegment()
|
||||
const onDelete = async (segId: string) => {
|
||||
const [e] = await asyncRunSafe<CommonResponse>(deleteSegment({ datasetId, documentId, segmentId: segId }) as Promise<CommonResponse>)
|
||||
if (!e) {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
resetList()
|
||||
}
|
||||
else {
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
}
|
||||
}
|
||||
|
||||
const onDelete = useCallback(async (segId?: string) => {
|
||||
await deleteSegment({ datasetId, documentId, segmentIds: segId ? [segId] : selectedSegmentIds }, {
|
||||
onSuccess: () => {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
resetList()
|
||||
!segId && setSelectedSegmentIds([])
|
||||
},
|
||||
onError: () => {
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [datasetId, documentId, selectedSegmentIds])
|
||||
|
||||
const { mutateAsync: updateSegment } = useUpdateSegment()
|
||||
|
||||
const handleUpdateSegment = useCallback(async (
|
||||
segmentId: string,
|
||||
question: string,
|
||||
answer: string,
|
||||
keywords: string[],
|
||||
needRegenerate = false,
|
||||
) => {
|
||||
const handleUpdateSegment = async (segmentId: string, question: string, answer: string, keywords: string[]) => {
|
||||
const params: SegmentUpdater = { content: '' }
|
||||
if (docForm === ChunkingMode.qa) {
|
||||
if (docForm === 'qa_model') {
|
||||
if (!question.trim())
|
||||
return notify({ type: 'error', message: t('datasetDocuments.segment.questionEmpty') })
|
||||
if (!answer.trim())
|
||||
@@ -298,259 +353,55 @@ const Completed: FC<ICompletedProps> = ({
|
||||
if (keywords.length)
|
||||
params.keywords = keywords
|
||||
|
||||
if (needRegenerate)
|
||||
params.regenerate_child_chunks = needRegenerate
|
||||
|
||||
eventEmitter?.emit('update-segment')
|
||||
await updateSegment({ datasetId, documentId, segmentId, body: params }, {
|
||||
onSuccess(res) {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
if (!needRegenerate)
|
||||
onCloseSegmentDetail()
|
||||
for (const seg of segments) {
|
||||
try {
|
||||
eventEmitter?.emit('update-segment')
|
||||
const res = await updateSegment({ datasetId, documentId, segmentId, body: params })
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
onCloseModal()
|
||||
for (const item of allSegments) {
|
||||
for (const seg of item) {
|
||||
if (seg.id === segmentId) {
|
||||
seg.answer = res.data.answer
|
||||
seg.content = res.data.content
|
||||
seg.keywords = res.data.keywords
|
||||
seg.word_count = res.data.word_count
|
||||
seg.hit_count = res.data.hit_count
|
||||
seg.index_node_hash = res.data.index_node_hash
|
||||
seg.enabled = res.data.enabled
|
||||
seg.updated_at = res.data.updated_at
|
||||
seg.child_chunks = res.data.child_chunks
|
||||
}
|
||||
}
|
||||
setSegments([...segments])
|
||||
eventEmitter?.emit('update-segment-success')
|
||||
},
|
||||
onSettled() {
|
||||
eventEmitter?.emit('update-segment-done')
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [segments, datasetId, documentId])
|
||||
}
|
||||
setAllSegments([...allSegments])
|
||||
}
|
||||
finally {
|
||||
eventEmitter?.emit('')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (lastSegmentsRes !== undefined)
|
||||
getSegments(false)
|
||||
}, [selectedStatus, searchValue])
|
||||
|
||||
useEffect(() => {
|
||||
if (importStatus === ProcessStatus.COMPLETED)
|
||||
resetList()
|
||||
}, [importStatus, resetList])
|
||||
|
||||
const onCancelBatchOperation = useCallback(() => {
|
||||
setSelectedSegmentIds([])
|
||||
}, [])
|
||||
|
||||
const onSelected = useCallback((segId: string) => {
|
||||
setSelectedSegmentIds(prev =>
|
||||
prev.includes(segId)
|
||||
? prev.filter(id => id !== segId)
|
||||
: [...prev, segId],
|
||||
)
|
||||
}, [])
|
||||
|
||||
const isAllSelected = useMemo(() => {
|
||||
return segments.length > 0 && segments.every(seg => selectedSegmentIds.includes(seg.id))
|
||||
}, [segments, selectedSegmentIds])
|
||||
|
||||
const isSomeSelected = useMemo(() => {
|
||||
return segments.some(seg => selectedSegmentIds.includes(seg.id))
|
||||
}, [segments, selectedSegmentIds])
|
||||
|
||||
const onSelectedAll = useCallback(() => {
|
||||
setSelectedSegmentIds((prev) => {
|
||||
const currentAllSegIds = segments.map(seg => seg.id)
|
||||
const prevSelectedIds = prev.filter(item => !currentAllSegIds.includes(item))
|
||||
return [...prevSelectedIds, ...((isAllSelected || selectedSegmentIds.length > 0) ? [] : currentAllSegIds)]
|
||||
})
|
||||
}, [segments, isAllSelected, selectedSegmentIds])
|
||||
|
||||
const totalText = useMemo(() => {
|
||||
const isSearch = searchValue !== '' || selectedStatus !== 'all'
|
||||
if (!isSearch) {
|
||||
const total = segmentListData?.total ? formatNumber(segmentListData.total) : '--'
|
||||
const count = total === '--' ? 0 : segmentListData!.total
|
||||
const translationKey = (mode === 'hierarchical' && parentMode === 'paragraph')
|
||||
? 'datasetDocuments.segment.parentChunks'
|
||||
: 'datasetDocuments.segment.chunks'
|
||||
return `${total} ${t(translationKey, { count })}`
|
||||
}
|
||||
else {
|
||||
const total = typeof segmentListData?.total === 'number' ? formatNumber(segmentListData.total) : 0
|
||||
const count = segmentListData?.total || 0
|
||||
return `${total} ${t('datasetDocuments.segment.searchResults', { count })}`
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [segmentListData?.total, mode, parentMode, searchValue, selectedStatus])
|
||||
|
||||
const toggleFullScreen = useCallback(() => {
|
||||
setFullScreen(!fullScreen)
|
||||
}, [fullScreen])
|
||||
|
||||
const viewNewlyAddedChunk = useCallback(async () => {
|
||||
const totalPages = segmentListData?.total_pages || 0
|
||||
const total = segmentListData?.total || 0
|
||||
const newPage = Math.ceil((total + 1) / limit)
|
||||
needScrollToBottom.current = true
|
||||
if (newPage > totalPages) {
|
||||
setCurrentPage(totalPages + 1)
|
||||
}
|
||||
else {
|
||||
resetList()
|
||||
currentPage !== totalPages && setCurrentPage(totalPages)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [segmentListData, limit, currentPage])
|
||||
|
||||
const { mutateAsync: deleteChildSegment } = useDeleteChildSegment()
|
||||
|
||||
const onDeleteChildChunk = useCallback(async (segmentId: string, childChunkId: string) => {
|
||||
await deleteChildSegment(
|
||||
{ datasetId, documentId, segmentId, childChunkId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
if (parentMode === 'paragraph')
|
||||
resetList()
|
||||
else
|
||||
resetChildList()
|
||||
},
|
||||
onError: () => {
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
},
|
||||
},
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [datasetId, documentId, parentMode])
|
||||
|
||||
const handleAddNewChildChunk = useCallback((parentChunkId: string) => {
|
||||
setShowNewChildSegmentModal(true)
|
||||
setCurrChunkId(parentChunkId)
|
||||
}, [])
|
||||
|
||||
const onSaveNewChildChunk = useCallback((newChildChunk?: ChildChunkDetail) => {
|
||||
if (parentMode === 'paragraph') {
|
||||
for (const seg of segments) {
|
||||
if (seg.id === currChunkId)
|
||||
seg.child_chunks?.push(newChildChunk!)
|
||||
}
|
||||
setSegments([...segments])
|
||||
}
|
||||
else {
|
||||
resetChildList()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [parentMode, currChunkId, segments])
|
||||
|
||||
const viewNewlyAddedChildChunk = useCallback(() => {
|
||||
const totalPages = childChunkListData?.total_pages || 0
|
||||
const total = childChunkListData?.total || 0
|
||||
const newPage = Math.ceil((total + 1) / limit)
|
||||
needScrollToBottom.current = true
|
||||
if (newPage > totalPages) {
|
||||
setCurrentPage(totalPages + 1)
|
||||
}
|
||||
else {
|
||||
resetChildList()
|
||||
currentPage !== totalPages && setCurrentPage(totalPages)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [childChunkListData, limit, currentPage])
|
||||
|
||||
const onClickSlice = useCallback((detail: ChildChunkDetail) => {
|
||||
setCurrChildChunk({ childChunkInfo: detail, showModal: true })
|
||||
setCurrChunkId(detail.segment_id)
|
||||
}, [])
|
||||
|
||||
const onCloseChildSegmentDetail = useCallback(() => {
|
||||
setCurrChildChunk({ showModal: false })
|
||||
setFullScreen(false)
|
||||
}, [])
|
||||
|
||||
const { mutateAsync: updateChildSegment } = useUpdateChildSegment()
|
||||
|
||||
const handleUpdateChildChunk = useCallback(async (
|
||||
segmentId: string,
|
||||
childChunkId: string,
|
||||
content: string,
|
||||
) => {
|
||||
const params: SegmentUpdater = { content: '' }
|
||||
if (!content.trim())
|
||||
return notify({ type: 'error', message: t('datasetDocuments.segment.contentEmpty') })
|
||||
|
||||
params.content = content
|
||||
|
||||
eventEmitter?.emit('update-child-segment')
|
||||
await updateChildSegment({ datasetId, documentId, segmentId, childChunkId, body: params }, {
|
||||
onSuccess: (res) => {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
onCloseChildSegmentDetail()
|
||||
if (parentMode === 'paragraph') {
|
||||
for (const seg of segments) {
|
||||
if (seg.id === segmentId) {
|
||||
for (const childSeg of seg.child_chunks!) {
|
||||
if (childSeg.id === childChunkId) {
|
||||
childSeg.content = res.data.content
|
||||
childSeg.type = res.data.type
|
||||
childSeg.word_count = res.data.word_count
|
||||
childSeg.updated_at = res.data.updated_at
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setSegments([...segments])
|
||||
}
|
||||
else {
|
||||
for (const childSeg of childSegments) {
|
||||
if (childSeg.id === childChunkId) {
|
||||
childSeg.content = res.data.content
|
||||
childSeg.type = res.data.type
|
||||
childSeg.word_count = res.data.word_count
|
||||
childSeg.updated_at = res.data.updated_at
|
||||
}
|
||||
}
|
||||
setChildSegments([...childSegments])
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
eventEmitter?.emit('update-child-segment-done')
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [segments, childSegments, datasetId, documentId, parentMode])
|
||||
|
||||
const onClearFilter = useCallback(() => {
|
||||
setInputValue('')
|
||||
setSearchValue('')
|
||||
setSelectedStatus('all')
|
||||
setCurrentPage(1)
|
||||
}, [])
|
||||
}, [importStatus])
|
||||
|
||||
return (
|
||||
<SegmentListContext.Provider value={{
|
||||
isCollapsed,
|
||||
fullScreen,
|
||||
toggleFullScreen,
|
||||
currSegment,
|
||||
currChildChunk,
|
||||
}}>
|
||||
{/* Menu Bar */}
|
||||
{!isFullDocMode && <div className={s.docSearchWrapper}>
|
||||
<Checkbox
|
||||
className='shrink-0'
|
||||
checked={isAllSelected}
|
||||
mixed={!isAllSelected && isSomeSelected}
|
||||
onCheck={onSelectedAll}
|
||||
disabled={isLoadingSegmentList}
|
||||
/>
|
||||
<div className={'system-sm-semibold-uppercase pl-5 text-text-secondary flex-1'}>{totalText}</div>
|
||||
<>
|
||||
<div className={s.docSearchWrapper}>
|
||||
<div className={s.totalText}>{total ? formatNumber(total) : '--'} {t('datasetDocuments.segment.paragraphs')}</div>
|
||||
<SimpleSelect
|
||||
onSelect={onChangeStatus}
|
||||
items={statusList.current}
|
||||
items={[
|
||||
{ value: 'all', name: t('datasetDocuments.list.index.all') },
|
||||
{ value: 0, name: t('datasetDocuments.list.status.disabled') },
|
||||
{ value: 1, name: t('datasetDocuments.list.status.enabled') },
|
||||
]}
|
||||
defaultValue={'all'}
|
||||
className={s.select}
|
||||
wrapperClassName='h-fit mr-2'
|
||||
optionWrapClassName='w-[160px]'
|
||||
optionClassName='p-0'
|
||||
renderOption={({ item, selected }) => <StatusItem item={item} selected={selected} />}
|
||||
/>
|
||||
wrapperClassName='h-fit w-[120px] mr-2' />
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
@@ -559,133 +410,35 @@ const Completed: FC<ICompletedProps> = ({
|
||||
onChange={e => handleInputChange(e.target.value)}
|
||||
onClear={() => handleInputChange('')}
|
||||
/>
|
||||
<Divider type='vertical' className='h-3.5 mx-3' />
|
||||
<DisplayToggle isCollapsed={isCollapsed} toggleCollapsed={() => setIsCollapsed(!isCollapsed)} />
|
||||
</div>}
|
||||
{/* Segment list */}
|
||||
{
|
||||
isFullDocMode
|
||||
? <div className={cn(
|
||||
'flex flex-col grow overflow-x-hidden',
|
||||
(isLoadingSegmentList || isLoadingChildSegmentList) ? 'overflow-y-hidden' : 'overflow-y-auto',
|
||||
)}>
|
||||
<SegmentCard
|
||||
detail={segments[0]}
|
||||
onClick={() => onClickCard(segments[0])}
|
||||
loading={isLoadingSegmentList}
|
||||
focused={{
|
||||
segmentIndex: currSegment?.segInfo?.id === segments[0]?.id,
|
||||
segmentContent: currSegment?.segInfo?.id === segments[0]?.id,
|
||||
}}
|
||||
/>
|
||||
<ChildSegmentList
|
||||
parentChunkId={segments[0]?.id}
|
||||
onDelete={onDeleteChildChunk}
|
||||
childChunks={childSegments}
|
||||
handleInputChange={handleInputChange}
|
||||
handleAddNewChildChunk={handleAddNewChildChunk}
|
||||
onClickSlice={onClickSlice}
|
||||
enabled={!archived}
|
||||
total={childChunkListData?.total || 0}
|
||||
inputValue={inputValue}
|
||||
onClearFilter={onClearFilter}
|
||||
isLoading={isLoadingSegmentList || isLoadingChildSegmentList}
|
||||
/>
|
||||
</div>
|
||||
: <SegmentList
|
||||
ref={segmentListRef}
|
||||
embeddingAvailable={embeddingAvailable}
|
||||
isLoading={isLoadingSegmentList}
|
||||
items={segments}
|
||||
selectedSegmentIds={selectedSegmentIds}
|
||||
onSelected={onSelected}
|
||||
onChangeSwitch={onChangeSwitch}
|
||||
onDelete={onDelete}
|
||||
onClick={onClickCard}
|
||||
archived={archived}
|
||||
onDeleteChildChunk={onDeleteChildChunk}
|
||||
handleAddNewChildChunk={handleAddNewChildChunk}
|
||||
onClickSlice={onClickSlice}
|
||||
onClearFilter={onClearFilter}
|
||||
/>
|
||||
}
|
||||
{/* Pagination */}
|
||||
<Divider type='horizontal' className='w-auto h-[1px] my-0 mx-6 bg-divider-subtle' />
|
||||
<Pagination
|
||||
current={currentPage - 1}
|
||||
onChange={cur => setCurrentPage(cur + 1)}
|
||||
total={(isFullDocMode ? childChunkListData?.total : segmentListData?.total) || 0}
|
||||
limit={limit}
|
||||
onLimitChange={limit => setLimit(limit)}
|
||||
className={isFullDocMode ? 'px-3' : ''}
|
||||
</div>
|
||||
<InfiniteVirtualList
|
||||
embeddingAvailable={embeddingAvailable}
|
||||
hasNextPage={lastSegmentsRes?.has_more ?? true}
|
||||
isNextPageLoading={loading}
|
||||
items={allSegments}
|
||||
loadNextPage={getSegments}
|
||||
onChangeSwitch={onChangeSwitch}
|
||||
onDelete={onDelete}
|
||||
onClick={onClickCard}
|
||||
archived={archived}
|
||||
/>
|
||||
{/* Edit or view segment detail */}
|
||||
<FullScreenDrawer
|
||||
isOpen={currSegment.showModal}
|
||||
fullScreen={fullScreen}
|
||||
>
|
||||
<Modal isShow={currSegment.showModal} onClose={() => { }} className='!max-w-[640px] !overflow-visible'>
|
||||
<SegmentDetail
|
||||
embeddingAvailable={embeddingAvailable}
|
||||
segInfo={currSegment.segInfo ?? { id: '' }}
|
||||
docForm={docForm}
|
||||
isEditMode={currSegment.isEditMode}
|
||||
onChangeSwitch={onChangeSwitch}
|
||||
onUpdate={handleUpdateSegment}
|
||||
onCancel={onCloseSegmentDetail}
|
||||
onCancel={onCloseModal}
|
||||
archived={archived}
|
||||
/>
|
||||
</FullScreenDrawer>
|
||||
{/* Create New Segment */}
|
||||
<FullScreenDrawer
|
||||
isOpen={showNewSegmentModal}
|
||||
fullScreen={fullScreen}
|
||||
>
|
||||
<NewSegment
|
||||
docForm={docForm}
|
||||
onCancel={() => {
|
||||
onNewSegmentModalChange(false)
|
||||
setFullScreen(false)
|
||||
}}
|
||||
onSave={resetList}
|
||||
viewNewlyAddedChunk={viewNewlyAddedChunk}
|
||||
/>
|
||||
</FullScreenDrawer>
|
||||
{/* Edit or view child segment detail */}
|
||||
<FullScreenDrawer
|
||||
isOpen={currChildChunk.showModal}
|
||||
fullScreen={fullScreen}
|
||||
>
|
||||
<ChildSegmentDetail
|
||||
chunkId={currChunkId}
|
||||
childChunkInfo={currChildChunk.childChunkInfo ?? { id: '' }}
|
||||
docForm={docForm}
|
||||
onUpdate={handleUpdateChildChunk}
|
||||
onCancel={onCloseChildSegmentDetail}
|
||||
/>
|
||||
</FullScreenDrawer>
|
||||
{/* Create New Child Segment */}
|
||||
<FullScreenDrawer
|
||||
isOpen={showNewChildSegmentModal}
|
||||
fullScreen={fullScreen}
|
||||
>
|
||||
<NewChildSegment
|
||||
chunkId={currChunkId}
|
||||
onCancel={() => {
|
||||
setShowNewChildSegmentModal(false)
|
||||
setFullScreen(false)
|
||||
}}
|
||||
onSave={onSaveNewChildChunk}
|
||||
viewNewlyAddedChildChunk={viewNewlyAddedChildChunk}
|
||||
/>
|
||||
</FullScreenDrawer>
|
||||
{/* Batch Action Buttons */}
|
||||
{selectedSegmentIds.length > 0
|
||||
&& <BatchAction
|
||||
className='absolute left-0 bottom-16 z-20'
|
||||
selectedIds={selectedSegmentIds}
|
||||
onBatchEnable={onChangeSwitch.bind(null, true, '')}
|
||||
onBatchDisable={onChangeSwitch.bind(null, false, '')}
|
||||
onBatchDelete={onDelete.bind(null, '')}
|
||||
onCancel={onCancelBatchOperation}
|
||||
/>}
|
||||
</SegmentListContext.Provider>
|
||||
</Modal>
|
||||
<NewSegmentModal
|
||||
isShow={showNewSegmentModal}
|
||||
docForm={docForm}
|
||||
onCancel={() => onNewSegmentModalChange(false)}
|
||||
onSave={resetList}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
import { memo, useMemo, useRef, useState } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { RiCloseLine, RiExpandDiagonalLine } from '@remixicon/react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useDocumentContext } from '../index'
|
||||
import { SegmentIndexTag } from './common/segment-index-tag'
|
||||
import ActionButtons from './common/action-buttons'
|
||||
import ChunkContent from './common/chunk-content'
|
||||
import AddAnother from './common/add-another'
|
||||
import Dot from './common/dot'
|
||||
import { useSegmentListContext } from './index'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { type ChildChunkDetail, ChunkingMode, type SegmentUpdater } from '@/models/datasets'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { useAddChildSegment } from '@/service/knowledge/use-segment'
|
||||
|
||||
type NewChildSegmentModalProps = {
|
||||
chunkId: string
|
||||
onCancel: () => void
|
||||
onSave: (ChildChunk?: ChildChunkDetail) => void
|
||||
viewNewlyAddedChildChunk?: () => void
|
||||
}
|
||||
|
||||
const NewChildSegmentModal: FC<NewChildSegmentModalProps> = ({
|
||||
chunkId,
|
||||
onCancel,
|
||||
onSave,
|
||||
viewNewlyAddedChildChunk,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [content, setContent] = useState('')
|
||||
const { datasetId, documentId } = useParams<{ datasetId: string; documentId: string }>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [addAnother, setAddAnother] = useState(true)
|
||||
const fullScreen = useSegmentListContext(s => s.fullScreen)
|
||||
const toggleFullScreen = useSegmentListContext(s => s.toggleFullScreen)
|
||||
const { appSidebarExpand } = useAppStore(useShallow(state => ({
|
||||
appSidebarExpand: state.appSidebarExpand,
|
||||
})))
|
||||
const parentMode = useDocumentContext(s => s.parentMode)
|
||||
|
||||
const refreshTimer = useRef<any>(null)
|
||||
|
||||
const isFullDocMode = useMemo(() => {
|
||||
return parentMode === 'full-doc'
|
||||
}, [parentMode])
|
||||
|
||||
const CustomButton = <>
|
||||
<Divider type='vertical' className='h-3 mx-1 bg-divider-regular' />
|
||||
<button
|
||||
type='button'
|
||||
className='text-text-accent system-xs-semibold'
|
||||
onClick={() => {
|
||||
clearTimeout(refreshTimer.current)
|
||||
viewNewlyAddedChildChunk?.()
|
||||
}}>
|
||||
{t('common.operation.view')}
|
||||
</button>
|
||||
</>
|
||||
|
||||
const handleCancel = (actionType: 'esc' | 'add' = 'esc') => {
|
||||
if (actionType === 'esc' || !addAnother)
|
||||
onCancel()
|
||||
setContent('')
|
||||
}
|
||||
|
||||
const { mutateAsync: addChildSegment } = useAddChildSegment()
|
||||
|
||||
const handleSave = async () => {
|
||||
const params: SegmentUpdater = { content: '' }
|
||||
|
||||
if (!content.trim())
|
||||
return notify({ type: 'error', message: t('datasetDocuments.segment.contentEmpty') })
|
||||
|
||||
params.content = content
|
||||
|
||||
setLoading(true)
|
||||
await addChildSegment({ datasetId, documentId, segmentId: chunkId, body: params }, {
|
||||
onSuccess(res) {
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('datasetDocuments.segment.childChunkAdded'),
|
||||
className: `!w-[296px] !bottom-0 ${appSidebarExpand === 'expand' ? '!left-[216px]' : '!left-14'}
|
||||
!top-auto !right-auto !mb-[52px] !ml-11`,
|
||||
customComponent: isFullDocMode && CustomButton,
|
||||
})
|
||||
handleCancel('add')
|
||||
if (isFullDocMode) {
|
||||
refreshTimer.current = setTimeout(() => {
|
||||
onSave()
|
||||
}, 3000)
|
||||
}
|
||||
else {
|
||||
onSave(res.data)
|
||||
}
|
||||
},
|
||||
onSettled() {
|
||||
setLoading(false)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const wordCountText = useMemo(() => {
|
||||
const count = content.length
|
||||
return `${formatNumber(count)} ${t('datasetDocuments.segment.characters', { count })}`
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [content.length])
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col h-full'}>
|
||||
<div className={classNames('flex items-center justify-between', fullScreen ? 'py-3 pr-4 pl-6 border border-divider-subtle' : 'pt-3 pr-3 pl-4')}>
|
||||
<div className='flex flex-col'>
|
||||
<div className='text-text-primary system-xl-semibold'>{t('datasetDocuments.segment.addChildChunk')}</div>
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<SegmentIndexTag label={t('datasetDocuments.segment.newChildChunk') as string} />
|
||||
<Dot />
|
||||
<span className='text-text-tertiary system-xs-medium'>{wordCountText}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
{fullScreen && (
|
||||
<>
|
||||
<AddAnother className='mr-3' isChecked={addAnother} onCheck={() => setAddAnother(!addAnother)} />
|
||||
<ActionButtons
|
||||
handleCancel={handleCancel.bind(null, 'esc')}
|
||||
handleSave={handleSave}
|
||||
loading={loading}
|
||||
actionType='add'
|
||||
isChildChunk={true}
|
||||
/>
|
||||
<Divider type='vertical' className='h-3.5 bg-divider-regular ml-4 mr-2' />
|
||||
</>
|
||||
)}
|
||||
<div className='w-8 h-8 flex justify-center items-center p-1.5 cursor-pointer mr-1' onClick={toggleFullScreen}>
|
||||
<RiExpandDiagonalLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='w-8 h-8 flex justify-center items-center p-1.5 cursor-pointer' onClick={handleCancel.bind(null, 'esc')}>
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames('flex grow w-full', fullScreen ? 'flex-row justify-center px-6 pt-6' : 'py-3 px-4')}>
|
||||
<div className={classNames('break-all overflow-hidden whitespace-pre-line h-full', fullScreen ? 'w-1/2' : 'w-full')}>
|
||||
<ChunkContent
|
||||
docForm={ChunkingMode.parentChild}
|
||||
question={content}
|
||||
onQuestionChange={content => setContent(content)}
|
||||
isEditMode={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!fullScreen && (
|
||||
<div className='flex items-center justify-between p-4 pt-3 border-t-[1px] border-t-divider-subtle'>
|
||||
<AddAnother isChecked={addAnother} onCheck={() => setAddAnother(!addAnother)} />
|
||||
<ActionButtons
|
||||
handleCancel={handleCancel.bind(null, 'esc')}
|
||||
handleSave={handleSave}
|
||||
loading={loading}
|
||||
actionType='add'
|
||||
isChildChunk={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NewChildSegmentModal)
|
||||
@@ -1,280 +0,0 @@
|
||||
import React, { type FC, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
|
||||
import { StatusItem } from '../../list'
|
||||
import { useDocumentContext } from '../index'
|
||||
import ChildSegmentList from './child-segment-list'
|
||||
import Tag from './common/tag'
|
||||
import Dot from './common/dot'
|
||||
import { SegmentIndexTag } from './common/segment-index-tag'
|
||||
import ParentChunkCardSkeleton from './skeleton/parent-chunk-card-skeleton'
|
||||
import { useSegmentListContext } from './index'
|
||||
import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import cn from '@/utils/classnames'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { isAfter } from '@/utils/time'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type ISegmentCardProps = {
|
||||
loading: boolean
|
||||
detail?: SegmentDetailModel & { document?: { name: string } }
|
||||
onClick?: () => void
|
||||
onChangeSwitch?: (enabled: boolean, segId?: string) => Promise<void>
|
||||
onDelete?: (segId: string) => Promise<void>
|
||||
onDeleteChildChunk?: (segId: string, childChunkId: string) => Promise<void>
|
||||
handleAddNewChildChunk?: (parentChunkId: string) => void
|
||||
onClickSlice?: (childChunk: ChildChunkDetail) => void
|
||||
onClickEdit?: () => void
|
||||
className?: string
|
||||
archived?: boolean
|
||||
embeddingAvailable?: boolean
|
||||
focused: {
|
||||
segmentIndex: boolean
|
||||
segmentContent: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const SegmentCard: FC<ISegmentCardProps> = ({
|
||||
detail = {},
|
||||
onClick,
|
||||
onChangeSwitch,
|
||||
onDelete,
|
||||
onDeleteChildChunk,
|
||||
handleAddNewChildChunk,
|
||||
onClickSlice,
|
||||
onClickEdit,
|
||||
loading = true,
|
||||
className = '',
|
||||
archived,
|
||||
embeddingAvailable,
|
||||
focused,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
id,
|
||||
position,
|
||||
enabled,
|
||||
content,
|
||||
word_count,
|
||||
hit_count,
|
||||
answer,
|
||||
keywords,
|
||||
child_chunks = [],
|
||||
created_at,
|
||||
updated_at,
|
||||
} = detail as Required<ISegmentCardProps>['detail']
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const isCollapsed = useSegmentListContext(s => s.isCollapsed)
|
||||
const mode = useDocumentContext(s => s.mode)
|
||||
const parentMode = useDocumentContext(s => s.parentMode)
|
||||
|
||||
const isGeneralMode = useMemo(() => {
|
||||
return mode === 'custom'
|
||||
}, [mode])
|
||||
|
||||
const isParentChildMode = useMemo(() => {
|
||||
return mode === 'hierarchical'
|
||||
}, [mode])
|
||||
|
||||
const isParagraphMode = useMemo(() => {
|
||||
return mode === 'hierarchical' && parentMode === 'paragraph'
|
||||
}, [mode, parentMode])
|
||||
|
||||
const isFullDocMode = useMemo(() => {
|
||||
return mode === 'hierarchical' && parentMode === 'full-doc'
|
||||
}, [mode, parentMode])
|
||||
|
||||
const chunkEdited = useMemo(() => {
|
||||
if (mode === 'hierarchical' && parentMode === 'full-doc')
|
||||
return false
|
||||
return isAfter(updated_at * 1000, created_at * 1000)
|
||||
}, [mode, parentMode, updated_at, created_at])
|
||||
|
||||
const contentOpacity = useMemo(() => {
|
||||
return (enabled || focused.segmentContent) ? '' : 'opacity-50 group-hover/card:opacity-100'
|
||||
}, [enabled, focused.segmentContent])
|
||||
|
||||
const handleClickCard = useCallback(() => {
|
||||
if (mode !== 'hierarchical' || parentMode !== 'full-doc')
|
||||
onClick?.()
|
||||
}, [mode, parentMode, onClick])
|
||||
|
||||
const renderContent = () => {
|
||||
if (answer) {
|
||||
return (
|
||||
<>
|
||||
<div className='flex gap-x-1'>
|
||||
<div className='w-4 text-[13px] font-medium leading-[20px] text-text-tertiary shrink-0'>Q</div>
|
||||
<div
|
||||
className={cn('text-text-secondary body-md-regular',
|
||||
isCollapsed ? 'line-clamp-2' : 'line-clamp-20',
|
||||
)}>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex gap-x-1'>
|
||||
<div className='w-4 text-[13px] font-medium leading-[20px] text-text-tertiary shrink-0'>A</div>
|
||||
<div className={cn('text-text-secondary body-md-regular',
|
||||
isCollapsed ? 'line-clamp-2' : 'line-clamp-20',
|
||||
)}>
|
||||
{answer}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
const wordCountText = useMemo(() => {
|
||||
const total = formatNumber(word_count)
|
||||
return `${total} ${t('datasetDocuments.segment.characters', { count: word_count })}`
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [word_count])
|
||||
|
||||
const labelPrefix = useMemo(() => {
|
||||
return isParentChildMode ? t('datasetDocuments.segment.parentChunk') : t('datasetDocuments.segment.chunk')
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isParentChildMode])
|
||||
|
||||
if (loading)
|
||||
return <ParentChunkCardSkeleton />
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-full px-3 rounded-xl group/card',
|
||||
isFullDocMode ? '' : 'pt-2.5 pb-2 hover:bg-dataset-chunk-detail-card-hover-bg',
|
||||
focused.segmentContent ? 'bg-dataset-chunk-detail-card-hover-bg' : '',
|
||||
className,
|
||||
)}
|
||||
onClick={handleClickCard}
|
||||
>
|
||||
<div className='h-5 relative flex items-center justify-between'>
|
||||
<>
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<SegmentIndexTag
|
||||
className={cn(contentOpacity)}
|
||||
iconClassName={focused.segmentIndex ? 'text-text-accent' : ''}
|
||||
labelClassName={focused.segmentIndex ? 'text-text-accent' : ''}
|
||||
positionId={position}
|
||||
label={isFullDocMode ? labelPrefix : ''}
|
||||
labelPrefix={labelPrefix}
|
||||
/>
|
||||
<Dot />
|
||||
<div className={cn('text-text-tertiary system-xs-medium', contentOpacity)}>{wordCountText}</div>
|
||||
<Dot />
|
||||
<div className={cn('text-text-tertiary system-xs-medium', contentOpacity)}>{`${formatNumber(hit_count)} ${t('datasetDocuments.segment.hitCount')}`}</div>
|
||||
{chunkEdited && (
|
||||
<>
|
||||
<Dot />
|
||||
<Badge text={t('datasetDocuments.segment.edited') as string} uppercase className={contentOpacity} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isFullDocMode
|
||||
? <div className='flex items-center'>
|
||||
<StatusItem status={enabled ? 'enabled' : 'disabled'} reverse textCls="text-text-tertiary system-xs-regular" />
|
||||
{embeddingAvailable && (
|
||||
<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]">
|
||||
{!archived && (
|
||||
<>
|
||||
<Tooltip
|
||||
popupContent='Edit'
|
||||
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-base-hover cursor-pointer'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClickEdit?.()
|
||||
}}>
|
||||
<RiEditLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent='Delete'
|
||||
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'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowModal(true)
|
||||
}
|
||||
}>
|
||||
<RiDeleteBinLine className='w-4 h-4 text-text-tertiary group-hover/delete:text-text-destructive' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Divider type="vertical" className="h-3.5 bg-divider-regular" />
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
onClick={(e: React.MouseEvent<HTMLDivElement, MouseEvent>) =>
|
||||
e.stopPropagation()
|
||||
}
|
||||
className="flex items-center"
|
||||
>
|
||||
<Switch
|
||||
size='md'
|
||||
disabled={archived || detail?.status !== 'completed'}
|
||||
defaultValue={enabled}
|
||||
onChange={async (val) => {
|
||||
await onChangeSwitch?.(val, id)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
: null}
|
||||
</>
|
||||
</div>
|
||||
<div className={cn('text-text-secondary body-md-regular -tracking-[0.07px] mt-0.5',
|
||||
contentOpacity,
|
||||
isFullDocMode ? 'line-clamp-3' : isCollapsed ? 'line-clamp-2' : 'line-clamp-20',
|
||||
)}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
{isGeneralMode && <div className={cn('flex flex-wrap items-center gap-2 py-1.5', contentOpacity)}>
|
||||
{keywords?.map(keyword => <Tag key={keyword} text={keyword} />)}
|
||||
</div>}
|
||||
{
|
||||
isFullDocMode
|
||||
? <button
|
||||
type='button'
|
||||
className='mt-0.5 mb-2 text-text-accent system-xs-semibold-uppercase'
|
||||
onClick={() => onClick?.()}
|
||||
>{t('common.operation.viewMore')}</button>
|
||||
: null
|
||||
}
|
||||
{
|
||||
isParagraphMode && child_chunks.length > 0
|
||||
&& <ChildSegmentList
|
||||
parentChunkId={id}
|
||||
childChunks={child_chunks}
|
||||
enabled={enabled}
|
||||
onDelete={onDeleteChildChunk!}
|
||||
handleAddNewChildChunk={handleAddNewChildChunk}
|
||||
onClickSlice={onClickSlice}
|
||||
focused={focused.segmentContent}
|
||||
/>
|
||||
}
|
||||
{showModal
|
||||
&& <Confirm
|
||||
isShow={showModal}
|
||||
title={t('datasetDocuments.segment.delete')}
|
||||
confirmText={t('common.operation.sure')}
|
||||
onConfirm={async () => { await onDelete?.(id) }}
|
||||
onCancel={() => setShowModal(false)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(SegmentCard)
|
||||
@@ -1,190 +0,0 @@
|
||||
import React, { type FC, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiExpandDiagonalLine,
|
||||
} from '@remixicon/react'
|
||||
import { useDocumentContext } from '../index'
|
||||
import ActionButtons from './common/action-buttons'
|
||||
import ChunkContent from './common/chunk-content'
|
||||
import Keywords from './common/keywords'
|
||||
import RegenerationModal from './common/regeneration-modal'
|
||||
import { SegmentIndexTag } from './common/segment-index-tag'
|
||||
import Dot from './common/dot'
|
||||
import { useSegmentListContext } from './index'
|
||||
import { ChunkingMode, type SegmentDetailModel } from '@/models/datasets'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
|
||||
type ISegmentDetailProps = {
|
||||
segInfo?: Partial<SegmentDetailModel> & { id: string }
|
||||
onUpdate: (segmentId: string, q: string, a: string, k: string[], needRegenerate?: boolean) => void
|
||||
onCancel: () => void
|
||||
isEditMode?: boolean
|
||||
docForm: ChunkingMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Show all the contents of the segment
|
||||
*/
|
||||
const SegmentDetail: FC<ISegmentDetailProps> = ({
|
||||
segInfo,
|
||||
onUpdate,
|
||||
onCancel,
|
||||
isEditMode,
|
||||
docForm,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [question, setQuestion] = useState(segInfo?.content || '')
|
||||
const [answer, setAnswer] = useState(segInfo?.answer || '')
|
||||
const [keywords, setKeywords] = useState<string[]>(segInfo?.keywords || [])
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showRegenerationModal, setShowRegenerationModal] = useState(false)
|
||||
const fullScreen = useSegmentListContext(s => s.fullScreen)
|
||||
const toggleFullScreen = useSegmentListContext(s => s.toggleFullScreen)
|
||||
const mode = useDocumentContext(s => s.mode)
|
||||
const parentMode = useDocumentContext(s => s.parentMode)
|
||||
|
||||
eventEmitter?.useSubscription((v) => {
|
||||
if (v === 'update-segment')
|
||||
setLoading(true)
|
||||
if (v === 'update-segment-done')
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
const handleCancel = () => {
|
||||
onCancel()
|
||||
setQuestion(segInfo?.content || '')
|
||||
setAnswer(segInfo?.answer || '')
|
||||
setKeywords(segInfo?.keywords || [])
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
onUpdate(segInfo?.id || '', question, answer, keywords)
|
||||
}
|
||||
|
||||
const handleRegeneration = () => {
|
||||
setShowRegenerationModal(true)
|
||||
}
|
||||
|
||||
const onCancelRegeneration = () => {
|
||||
setShowRegenerationModal(false)
|
||||
}
|
||||
|
||||
const onConfirmRegeneration = () => {
|
||||
onUpdate(segInfo?.id || '', question, answer, keywords, true)
|
||||
}
|
||||
|
||||
const isParentChildMode = useMemo(() => {
|
||||
return mode === 'hierarchical'
|
||||
}, [mode])
|
||||
|
||||
const isFullDocMode = useMemo(() => {
|
||||
return mode === 'hierarchical' && parentMode === 'full-doc'
|
||||
}, [mode, parentMode])
|
||||
|
||||
const titleText = useMemo(() => {
|
||||
return isEditMode ? t('datasetDocuments.segment.editChunk') : t('datasetDocuments.segment.chunkDetail')
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isEditMode])
|
||||
|
||||
const isQAModel = useMemo(() => {
|
||||
return docForm === ChunkingMode.qa
|
||||
}, [docForm])
|
||||
|
||||
const wordCountText = useMemo(() => {
|
||||
const contentLength = isQAModel ? (question.length + answer.length) : question.length
|
||||
const total = formatNumber(isEditMode ? contentLength : segInfo!.word_count as number)
|
||||
const count = isEditMode ? contentLength : segInfo!.word_count as number
|
||||
return `${total} ${t('datasetDocuments.segment.characters', { count })}`
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isEditMode, question.length, answer.length, segInfo?.word_count, isQAModel])
|
||||
|
||||
const labelPrefix = useMemo(() => {
|
||||
return isParentChildMode ? t('datasetDocuments.segment.parentChunk') : t('datasetDocuments.segment.chunk')
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isParentChildMode])
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col h-full'}>
|
||||
<div className={classNames('flex items-center justify-between', fullScreen ? 'py-3 pr-4 pl-6 border border-divider-subtle' : 'pt-3 pr-3 pl-4')}>
|
||||
<div className='flex flex-col'>
|
||||
<div className='text-text-primary system-xl-semibold'>{titleText}</div>
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<SegmentIndexTag positionId={segInfo?.position || ''} label={isFullDocMode ? labelPrefix : ''} labelPrefix={labelPrefix} />
|
||||
<Dot />
|
||||
<span className='text-text-tertiary system-xs-medium'>{wordCountText}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
{isEditMode && fullScreen && (
|
||||
<>
|
||||
<ActionButtons
|
||||
handleCancel={handleCancel}
|
||||
handleRegeneration={handleRegeneration}
|
||||
handleSave={handleSave}
|
||||
loading={loading}
|
||||
/>
|
||||
<Divider type='vertical' className='h-3.5 bg-divider-regular ml-4 mr-2' />
|
||||
</>
|
||||
)}
|
||||
<div className='w-8 h-8 flex justify-center items-center p-1.5 cursor-pointer mr-1' onClick={toggleFullScreen}>
|
||||
<RiExpandDiagonalLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='w-8 h-8 flex justify-center items-center p-1.5 cursor-pointer' onClick={onCancel}>
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames(
|
||||
'flex grow',
|
||||
fullScreen ? 'w-full flex-row justify-center px-6 pt-6 gap-x-8' : 'flex-col gap-y-1 py-3 px-4',
|
||||
!isEditMode && 'pb-0',
|
||||
)}>
|
||||
<div className={classNames('break-all overflow-hidden whitespace-pre-line', fullScreen ? 'w-1/2' : 'grow')}>
|
||||
<ChunkContent
|
||||
docForm={docForm}
|
||||
question={question}
|
||||
answer={answer}
|
||||
onQuestionChange={question => setQuestion(question)}
|
||||
onAnswerChange={answer => setAnswer(answer)}
|
||||
isEditMode={isEditMode}
|
||||
/>
|
||||
</div>
|
||||
{mode === 'custom' && <Keywords
|
||||
className={fullScreen ? 'w-1/5' : ''}
|
||||
actionType={isEditMode ? 'edit' : 'view'}
|
||||
segInfo={segInfo}
|
||||
keywords={keywords}
|
||||
isEditMode={isEditMode}
|
||||
onKeywordsChange={keywords => setKeywords(keywords)}
|
||||
/>}
|
||||
</div>
|
||||
{isEditMode && !fullScreen && (
|
||||
<div className='flex items-center justify-end p-4 pt-3 border-t-[1px] border-t-divider-subtle'>
|
||||
<ActionButtons
|
||||
handleCancel={handleCancel}
|
||||
handleRegeneration={handleRegeneration}
|
||||
handleSave={handleSave}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
showRegenerationModal && (
|
||||
<RegenerationModal
|
||||
isShow={showRegenerationModal}
|
||||
onConfirm={onConfirmRegeneration}
|
||||
onCancel={onCancelRegeneration}
|
||||
onClose={onCancelRegeneration}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(SegmentDetail)
|
||||
@@ -1,116 +0,0 @@
|
||||
import React, { type ForwardedRef, useMemo } from 'react'
|
||||
import { useDocumentContext } from '../index'
|
||||
import SegmentCard from './segment-card'
|
||||
import Empty from './common/empty'
|
||||
import GeneralListSkeleton from './skeleton/general-list-skeleton'
|
||||
import ParagraphListSkeleton from './skeleton/paragraph-list-skeleton'
|
||||
import { useSegmentListContext } from './index'
|
||||
import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
|
||||
type ISegmentListProps = {
|
||||
isLoading: boolean
|
||||
items: SegmentDetailModel[]
|
||||
selectedSegmentIds: string[]
|
||||
onSelected: (segId: string) => void
|
||||
onClick: (detail: SegmentDetailModel, isEditMode?: boolean) => void
|
||||
onChangeSwitch: (enabled: boolean, segId?: string,) => Promise<void>
|
||||
onDelete: (segId: string) => Promise<void>
|
||||
onDeleteChildChunk: (sgId: string, childChunkId: string) => Promise<void>
|
||||
handleAddNewChildChunk: (parentChunkId: string) => void
|
||||
onClickSlice: (childChunk: ChildChunkDetail) => void
|
||||
archived?: boolean
|
||||
embeddingAvailable: boolean
|
||||
onClearFilter: () => void
|
||||
}
|
||||
|
||||
const SegmentList = React.forwardRef(({
|
||||
isLoading,
|
||||
items,
|
||||
selectedSegmentIds,
|
||||
onSelected,
|
||||
onClick: onClickCard,
|
||||
onChangeSwitch,
|
||||
onDelete,
|
||||
onDeleteChildChunk,
|
||||
handleAddNewChildChunk,
|
||||
onClickSlice,
|
||||
archived,
|
||||
embeddingAvailable,
|
||||
onClearFilter,
|
||||
}: ISegmentListProps,
|
||||
ref: ForwardedRef<HTMLDivElement>,
|
||||
) => {
|
||||
const mode = useDocumentContext(s => s.mode)
|
||||
const parentMode = useDocumentContext(s => s.parentMode)
|
||||
const currSegment = useSegmentListContext(s => s.currSegment)
|
||||
const currChildChunk = useSegmentListContext(s => s.currChildChunk)
|
||||
|
||||
const Skeleton = useMemo(() => {
|
||||
return (mode === 'hierarchical' && parentMode === 'paragraph') ? ParagraphListSkeleton : GeneralListSkeleton
|
||||
}, [mode, parentMode])
|
||||
|
||||
// Loading skeleton
|
||||
if (isLoading)
|
||||
return <Skeleton />
|
||||
// Search result is empty
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className='h-full pl-6'>
|
||||
<Empty onClearFilter={onClearFilter} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div ref={ref} className={'flex flex-col grow overflow-y-auto'}>
|
||||
{
|
||||
items.map((segItem) => {
|
||||
const isLast = items[items.length - 1].id === segItem.id
|
||||
const segmentIndexFocused
|
||||
= currSegment?.segInfo?.id === segItem.id
|
||||
|| (!currSegment && currChildChunk?.childChunkInfo?.segment_id === segItem.id)
|
||||
const segmentContentFocused = currSegment?.segInfo?.id === segItem.id
|
||||
|| currChildChunk?.childChunkInfo?.segment_id === segItem.id
|
||||
return (
|
||||
<div key={segItem.id} className='flex items-start gap-x-2'>
|
||||
<Checkbox
|
||||
key={`${segItem.id}-checkbox`}
|
||||
className='shrink-0 mt-3.5'
|
||||
checked={selectedSegmentIds.includes(segItem.id)}
|
||||
onCheck={() => onSelected(segItem.id)}
|
||||
/>
|
||||
<div className='grow'>
|
||||
<SegmentCard
|
||||
key={`${segItem.id}-card`}
|
||||
detail={segItem}
|
||||
onClick={() => onClickCard(segItem, true)}
|
||||
onChangeSwitch={onChangeSwitch}
|
||||
onClickEdit={() => onClickCard(segItem, true)}
|
||||
onDelete={onDelete}
|
||||
onDeleteChildChunk={onDeleteChildChunk}
|
||||
handleAddNewChildChunk={handleAddNewChildChunk}
|
||||
onClickSlice={onClickSlice}
|
||||
loading={false}
|
||||
archived={archived}
|
||||
embeddingAvailable={embeddingAvailable}
|
||||
focused={{
|
||||
segmentIndex: segmentIndexFocused,
|
||||
segmentContent: segmentContentFocused,
|
||||
}}
|
||||
/>
|
||||
{!isLast && <div className='w-full px-3'>
|
||||
<Divider type='horizontal' className='bg-divider-subtle my-1' />
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
SegmentList.displayName = 'SegmentList'
|
||||
|
||||
export default SegmentList
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
const Slice = React.memo(() => {
|
||||
return (
|
||||
<div className='flex flex-col gap-y-1'>
|
||||
<div className='w-full h-5 bg-state-base-hover flex items-center'>
|
||||
<span className='w-[30px] h-5 bg-state-base-hover-alt' />
|
||||
</div>
|
||||
<div className='w-2/3 h-5 bg-state-base-hover' />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
Slice.displayName = 'Slice'
|
||||
|
||||
const FullDocListSkeleton = () => {
|
||||
return (
|
||||
<div className='w-full grow flex flex-col gap-y-3 relative z-10 overflow-y-hidden'>
|
||||
<div className='absolute top-0 left-0 bottom-14 w-full h-full bg-dataset-chunk-list-mask-bg z-20' />
|
||||
{[...Array(15)].map((_, index) => <Slice key={index} />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(FullDocListSkeleton)
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
SkeletonContainer,
|
||||
SkeletonPoint,
|
||||
SkeletonRectangle,
|
||||
SkeletonRow,
|
||||
} from '@/app/components/base/skeleton'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
|
||||
const CardSkelton = React.memo(() => {
|
||||
return (
|
||||
<SkeletonContainer className='p-1 pb-2 gap-y-0'>
|
||||
<SkeletonContainer className='px-2 pt-1.5 gap-y-0.5'>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-[72px] bg-text-quaternary' />
|
||||
<SkeletonPoint className='opacity-20' />
|
||||
<SkeletonRectangle className='w-24 bg-text-quaternary' />
|
||||
<SkeletonPoint className='opacity-20' />
|
||||
<SkeletonRectangle className='w-24 bg-text-quaternary' />
|
||||
<SkeletonRow className='grow justify-end gap-1'>
|
||||
<SkeletonRectangle className='w-12 bg-text-quaternary' />
|
||||
<SkeletonRectangle className='w-2 bg-text-quaternary mx-1' />
|
||||
</SkeletonRow>
|
||||
</SkeletonRow>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-full bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-full bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-2/3 bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
</SkeletonContainer>
|
||||
<SkeletonContainer className='px-2 py-1.5'>
|
||||
<SkeletonRow>
|
||||
<SkeletonRectangle className='w-14 bg-text-quaternary' />
|
||||
<SkeletonRectangle className='w-[88px] bg-text-quaternary' />
|
||||
<SkeletonRectangle className='w-14 bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
</SkeletonContainer>
|
||||
</SkeletonContainer>
|
||||
)
|
||||
})
|
||||
|
||||
CardSkelton.displayName = 'CardSkelton'
|
||||
|
||||
const GeneralListSkeleton = () => {
|
||||
return (
|
||||
<div className='relative flex flex-col grow overflow-y-hidden z-10'>
|
||||
<div className='absolute top-0 left-0 w-full h-full bg-dataset-chunk-list-mask-bg z-20' />
|
||||
{[...Array(10)].map((_, index) => {
|
||||
return (
|
||||
<div key={index} className='flex items-start gap-x-2'>
|
||||
<Checkbox
|
||||
key={`${index}-checkbox`}
|
||||
className='shrink-0 mt-3.5'
|
||||
disabled
|
||||
/>
|
||||
<div className='grow'>
|
||||
<CardSkelton />
|
||||
{index !== 9 && <div className='w-full px-3'>
|
||||
<Divider type='horizontal' className='bg-divider-subtle my-1' />
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(GeneralListSkeleton)
|
||||
@@ -1,76 +0,0 @@
|
||||
import React from 'react'
|
||||
import { RiArrowRightSLine } from '@remixicon/react'
|
||||
import {
|
||||
SkeletonContainer,
|
||||
SkeletonPoint,
|
||||
SkeletonRectangle,
|
||||
SkeletonRow,
|
||||
} from '@/app/components/base/skeleton'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
|
||||
const CardSkelton = React.memo(() => {
|
||||
return (
|
||||
<SkeletonContainer className='p-1 pb-2 gap-y-0'>
|
||||
<SkeletonContainer className='px-2 pt-1.5 gap-y-0.5'>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-[72px] bg-text-quaternary' />
|
||||
<SkeletonPoint className='opacity-20' />
|
||||
<SkeletonRectangle className='w-24 bg-text-quaternary' />
|
||||
<SkeletonPoint className='opacity-20' />
|
||||
<SkeletonRectangle className='w-24 bg-text-quaternary' />
|
||||
<SkeletonRow className='grow justify-end gap-1'>
|
||||
<SkeletonRectangle className='w-12 bg-text-quaternary' />
|
||||
<SkeletonRectangle className='w-2 bg-text-quaternary mx-1' />
|
||||
</SkeletonRow>
|
||||
</SkeletonRow>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-full bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-full bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-2/3 bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
</SkeletonContainer>
|
||||
<SkeletonContainer className='p-1 pb-2'>
|
||||
<SkeletonRow>
|
||||
<SkeletonRow className='h-7 pl-1 pr-3 gap-x-0.5 rounded-lg bg-dataset-child-chunk-expand-btn-bg'>
|
||||
<RiArrowRightSLine className='w-4 h-4 text-text-secondary opacity-20' />
|
||||
<SkeletonRectangle className='w-32 bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
</SkeletonRow>
|
||||
</SkeletonContainer>
|
||||
</SkeletonContainer>
|
||||
)
|
||||
})
|
||||
|
||||
CardSkelton.displayName = 'CardSkelton'
|
||||
|
||||
const ParagraphListSkeleton = () => {
|
||||
return (
|
||||
<div className='relative flex flex-col h-full overflow-y-hidden z-10'>
|
||||
<div className='absolute top-0 left-0 w-full h-full bg-dataset-chunk-list-mask-bg z-20' />
|
||||
{[...Array(10)].map((_, index) => {
|
||||
return (
|
||||
<div key={index} className='flex items-start gap-x-2'>
|
||||
<Checkbox
|
||||
key={`${index}-checkbox`}
|
||||
className='shrink-0 mt-3.5'
|
||||
disabled
|
||||
/>
|
||||
<div className='grow'>
|
||||
<CardSkelton />
|
||||
{index !== 9 && <div className='w-full px-3'>
|
||||
<Divider type='horizontal' className='bg-divider-subtle my-1' />
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ParagraphListSkeleton)
|
||||
@@ -1,45 +0,0 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
SkeletonContainer,
|
||||
SkeletonPoint,
|
||||
SkeletonRectangle,
|
||||
SkeletonRow,
|
||||
} from '@/app/components/base/skeleton'
|
||||
|
||||
const ParentChunkCardSkelton = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='flex flex-col pb-2'>
|
||||
<SkeletonContainer className='p-1 pb-0 gap-y-0'>
|
||||
<SkeletonContainer className='px-2 pt-1.5 gap-y-0.5'>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-[72px] bg-text-quaternary' />
|
||||
<SkeletonPoint className='opacity-20' />
|
||||
<SkeletonRectangle className='w-24 bg-text-quaternary' />
|
||||
<SkeletonPoint className='opacity-20' />
|
||||
<SkeletonRectangle className='w-24 bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-full bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-full bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-2/3 bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
</SkeletonContainer>
|
||||
</SkeletonContainer>
|
||||
<div className='flex items-center px-3 mt-0.5'>
|
||||
<button type='button' className='pt-0.5 text-components-button-secondary-accent-text-disabled system-xs-semibold-uppercase' disabled>
|
||||
{t('common.operation.viewMore')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ParentChunkCardSkelton.displayName = 'ParentChunkCardSkelton'
|
||||
|
||||
export default React.memo(ParentChunkCardSkelton)
|
||||
@@ -1,22 +0,0 @@
|
||||
import React, { type FC } from 'react'
|
||||
import { RiCheckLine } from '@remixicon/react'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
|
||||
type IStatusItemProps = {
|
||||
item: Item
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
const StatusItem: FC<IStatusItemProps> = ({
|
||||
item,
|
||||
selected,
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex items-center justify-between py-1.5 px-2'>
|
||||
<span className='system-md-regular'>{item.name}</span>
|
||||
{selected && <RiCheckLine className='w-4 h-4 text-text-accent' />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(StatusItem)
|
||||
@@ -1,5 +1,14 @@
|
||||
/* .cardWrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(290px, auto));
|
||||
grid-gap: 16px;
|
||||
grid-auto-rows: 180px;
|
||||
} */
|
||||
.totalText {
|
||||
@apply text-gray-900 font-medium text-base flex-1;
|
||||
}
|
||||
.docSearchWrapper {
|
||||
@apply sticky w-full -top-3 flex items-center mb-3 justify-between z-[11] flex-wrap gap-y-1 pr-3;
|
||||
@apply sticky w-full py-1 -top-3 bg-white flex items-center mb-3 justify-between z-10 flex-wrap gap-y-1;
|
||||
}
|
||||
.listContainer {
|
||||
height: calc(100% - 3.25rem);
|
||||
@@ -32,7 +41,7 @@
|
||||
@apply text-primary-600 font-semibold text-xs absolute right-0 hidden h-12 pl-12 items-center;
|
||||
}
|
||||
.select {
|
||||
@apply h-8 py-0 pr-5 w-[100px] shadow-none !important;
|
||||
@apply h-8 py-0 bg-gray-50 hover:bg-gray-100 rounded-lg shadow-none !important;
|
||||
}
|
||||
.segModalContent {
|
||||
@apply h-96 text-gray-800 text-base break-all overflow-y-scroll;
|
||||
|
||||
@@ -1,52 +1,59 @@
|
||||
import type { FC } from 'react'
|
||||
import type { FC, SVGProps } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { omit } from 'lodash-es'
|
||||
import { RiLoader2Line, RiPauseCircleLine, RiPlayCircleLine } from '@remixicon/react'
|
||||
import Image from 'next/image'
|
||||
import { ArrowRightIcon } from '@heroicons/react/24/solid'
|
||||
import SegmentCard from '../completed/SegmentCard'
|
||||
import { FieldInfo } from '../metadata'
|
||||
import { useDocumentContext } from '../index'
|
||||
import { IndexingType } from '../../../create/step-two'
|
||||
import { indexMethodIcon, retrievalIcon } from '../../../create/icons'
|
||||
import EmbeddingSkeleton from './skeleton'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import style from '../completed/style.module.css'
|
||||
import { DocumentContext } from '../index'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { ProcessMode, type ProcessRuleResponse } from '@/models/datasets'
|
||||
import type { FullDocumentDetail, ProcessRuleResponse } from '@/models/datasets'
|
||||
import type { CommonResponse } from '@/models/common'
|
||||
import { asyncRunSafe, sleep } from '@/utils'
|
||||
import {
|
||||
fetchIndexingStatus as doFetchIndexingStatus,
|
||||
fetchProcessRule,
|
||||
pauseDocIndexing,
|
||||
resumeDocIndexing,
|
||||
} from '@/service/datasets'
|
||||
import { fetchIndexingStatus as doFetchIndexingStatus, fetchProcessRule, pauseDocIndexing, resumeDocIndexing } from '@/service/datasets'
|
||||
import StopEmbeddingModal from '@/app/components/datasets/create/stop-embedding-modal'
|
||||
|
||||
type IEmbeddingDetailProps = {
|
||||
type Props = {
|
||||
detail?: FullDocumentDetail
|
||||
stopPosition?: 'top' | 'bottom'
|
||||
datasetId?: string
|
||||
documentId?: string
|
||||
indexingType?: IndexingType
|
||||
retrievalMethod?: RETRIEVE_METHOD
|
||||
indexingType?: string
|
||||
detailUpdate: VoidFunction
|
||||
}
|
||||
|
||||
type IRuleDetailProps = {
|
||||
sourceData?: ProcessRuleResponse
|
||||
indexingType?: IndexingType
|
||||
retrievalMethod?: RETRIEVE_METHOD
|
||||
const StopIcon = ({ className }: SVGProps<SVGElement>) => {
|
||||
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||
<g clipPath="url(#clip0_2328_2798)">
|
||||
<path d="M1.5 3.9C1.5 3.05992 1.5 2.63988 1.66349 2.31901C1.8073 2.03677 2.03677 1.8073 2.31901 1.66349C2.63988 1.5 3.05992 1.5 3.9 1.5H8.1C8.94008 1.5 9.36012 1.5 9.68099 1.66349C9.96323 1.8073 10.1927 2.03677 10.3365 2.31901C10.5 2.63988 10.5 3.05992 10.5 3.9V8.1C10.5 8.94008 10.5 9.36012 10.3365 9.68099C10.1927 9.96323 9.96323 10.1927 9.68099 10.3365C9.36012 10.5 8.94008 10.5 8.1 10.5H3.9C3.05992 10.5 2.63988 10.5 2.31901 10.3365C2.03677 10.1927 1.8073 9.96323 1.66349 9.68099C1.5 9.36012 1.5 8.94008 1.5 8.1V3.9Z" stroke="#344054" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2328_2798">
|
||||
<rect width="12" height="12" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
|
||||
const RuleDetail: FC<IRuleDetailProps> = React.memo(({
|
||||
sourceData,
|
||||
indexingType,
|
||||
retrievalMethod,
|
||||
}) => {
|
||||
const ResumeIcon = ({ className }: SVGProps<SVGElement>) => {
|
||||
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||
<path d="M10 3.5H5C3.34315 3.5 2 4.84315 2 6.5C2 8.15685 3.34315 9.5 5 9.5H10M10 3.5L8 1.5M10 3.5L8 5.5" stroke="#344054" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
const RuleDetail: FC<{ sourceData?: ProcessRuleResponse; docName?: string }> = ({ sourceData, docName }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const segmentationRuleMap = {
|
||||
docName: t('datasetDocuments.embedding.docName'),
|
||||
mode: t('datasetDocuments.embedding.mode'),
|
||||
segmentLength: t('datasetDocuments.embedding.segmentLength'),
|
||||
textCleaning: t('datasetDocuments.embedding.textCleaning'),
|
||||
@@ -63,106 +70,48 @@ const RuleDetail: FC<IRuleDetailProps> = React.memo(({
|
||||
return t('datasetCreation.stepTwo.removeStopwords')
|
||||
}
|
||||
|
||||
const isNumber = (value: unknown) => {
|
||||
return typeof value === 'number'
|
||||
}
|
||||
|
||||
const getValue = useCallback((field: string) => {
|
||||
let value: string | number | undefined = '-'
|
||||
const maxTokens = isNumber(sourceData?.rules?.segmentation?.max_tokens)
|
||||
? sourceData.rules.segmentation.max_tokens
|
||||
: value
|
||||
const childMaxTokens = isNumber(sourceData?.rules?.subchunk_segmentation?.max_tokens)
|
||||
? sourceData.rules.subchunk_segmentation.max_tokens
|
||||
: value
|
||||
switch (field) {
|
||||
case 'docName':
|
||||
value = docName
|
||||
break
|
||||
case 'mode':
|
||||
value = !sourceData?.mode
|
||||
? value
|
||||
: sourceData.mode === ProcessMode.general
|
||||
? (t('datasetDocuments.embedding.custom') as string)
|
||||
: `${t('datasetDocuments.embedding.hierarchical')} · ${sourceData?.rules?.parent_mode === 'paragraph'
|
||||
? t('dataset.parentMode.paragraph')
|
||||
: t('dataset.parentMode.fullDoc')}`
|
||||
value = sourceData?.mode === 'automatic' ? (t('datasetDocuments.embedding.automatic') as string) : (t('datasetDocuments.embedding.custom') as string)
|
||||
break
|
||||
case 'segmentLength':
|
||||
value = !sourceData?.mode
|
||||
? value
|
||||
: sourceData.mode === ProcessMode.general
|
||||
? maxTokens
|
||||
: `${t('datasetDocuments.embedding.parentMaxTokens')} ${maxTokens}; ${t('datasetDocuments.embedding.childMaxTokens')} ${childMaxTokens}`
|
||||
value = sourceData?.rules?.segmentation?.max_tokens
|
||||
break
|
||||
default:
|
||||
value = !sourceData?.mode
|
||||
? value
|
||||
: sourceData?.rules?.pre_processing_rules?.filter(rule =>
|
||||
rule.enabled).map(rule => getRuleName(rule.id)).join(',')
|
||||
value = sourceData?.mode === 'automatic'
|
||||
? (t('datasetDocuments.embedding.automatic') as string)
|
||||
// eslint-disable-next-line array-callback-return
|
||||
: sourceData?.rules?.pre_processing_rules?.map((rule) => {
|
||||
if (rule.enabled)
|
||||
return getRuleName(rule.id)
|
||||
}).filter(Boolean).join(';')
|
||||
break
|
||||
}
|
||||
return value
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sourceData])
|
||||
}, [sourceData, docName])
|
||||
|
||||
return <div className='py-3'>
|
||||
<div className='flex flex-col gap-y-1'>
|
||||
{Object.keys(segmentationRuleMap).map((field) => {
|
||||
return <FieldInfo
|
||||
key={field}
|
||||
label={segmentationRuleMap[field as keyof typeof segmentationRuleMap]}
|
||||
displayedValue={String(getValue(field))}
|
||||
/>
|
||||
})}
|
||||
</div>
|
||||
<Divider type='horizontal' className='bg-divider-subtle' />
|
||||
<FieldInfo
|
||||
label={t('datasetCreation.stepTwo.indexMode')}
|
||||
displayedValue={t(`datasetCreation.stepTwo.${indexingType === IndexingType.ECONOMICAL ? 'economical' : 'qualified'}`) as string}
|
||||
valueIcon={
|
||||
<Image
|
||||
className='size-4'
|
||||
src={
|
||||
indexingType === IndexingType.ECONOMICAL
|
||||
? indexMethodIcon.economical
|
||||
: indexMethodIcon.high_quality
|
||||
}
|
||||
alt=''
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FieldInfo
|
||||
label={t('datasetSettings.form.retrievalSetting.title')}
|
||||
displayedValue={t(`dataset.retrieval.${indexingType === IndexingType.ECONOMICAL ? 'invertedIndex' : retrievalMethod}.title`) as string}
|
||||
valueIcon={
|
||||
<Image
|
||||
className='size-4'
|
||||
src={
|
||||
retrievalMethod === RETRIEVE_METHOD.fullText
|
||||
? retrievalIcon.fullText
|
||||
: retrievalMethod === RETRIEVE_METHOD.hybrid
|
||||
? retrievalIcon.hybrid
|
||||
: retrievalIcon.vector
|
||||
}
|
||||
alt=''
|
||||
/>
|
||||
}
|
||||
/>
|
||||
return <div className='flex flex-col pt-8 pb-10 first:mt-0'>
|
||||
{Object.keys(segmentationRuleMap).map((field) => {
|
||||
return <FieldInfo
|
||||
key={field}
|
||||
label={segmentationRuleMap[field as keyof typeof segmentationRuleMap]}
|
||||
displayedValue={String(getValue(field))}
|
||||
/>
|
||||
})}
|
||||
</div>
|
||||
})
|
||||
}
|
||||
|
||||
RuleDetail.displayName = 'RuleDetail'
|
||||
|
||||
const EmbeddingDetail: FC<IEmbeddingDetailProps> = ({
|
||||
datasetId: dstId,
|
||||
documentId: docId,
|
||||
detailUpdate,
|
||||
indexingType,
|
||||
retrievalMethod,
|
||||
}) => {
|
||||
const EmbeddingDetail: FC<Props> = ({ detail, stopPosition = 'top', datasetId: dstId, documentId: docId, detailUpdate }) => {
|
||||
const onTop = stopPosition === 'top'
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
|
||||
const datasetId = useDocumentContext(s => s.datasetId)
|
||||
const documentId = useDocumentContext(s => s.documentId)
|
||||
const { datasetId = '', documentId = '' } = useContext(DocumentContext)
|
||||
const localDatasetId = dstId ?? datasetId
|
||||
const localDocumentId = docId ?? documentId
|
||||
|
||||
@@ -197,7 +146,6 @@ const EmbeddingDetail: FC<IEmbeddingDetailProps> = ({
|
||||
await sleep(2500)
|
||||
await startQueryStatus()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [stopQueryStatus])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -208,13 +156,21 @@ const EmbeddingDetail: FC<IEmbeddingDetailProps> = ({
|
||||
}
|
||||
}, [startQueryStatus, stopQueryStatus])
|
||||
|
||||
const { data: ruleDetail } = useSWR({
|
||||
const { data: ruleDetail, error: ruleError } = useSWR({
|
||||
action: 'fetchProcessRule',
|
||||
params: { documentId: localDocumentId },
|
||||
}, apiParams => fetchProcessRule(omit(apiParams, 'action')), {
|
||||
revalidateOnFocus: false,
|
||||
})
|
||||
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const modalShowHandle = () => setShowModal(true)
|
||||
const modalCloseHandle = () => setShowModal(false)
|
||||
const router = useRouter()
|
||||
const navToDocument = () => {
|
||||
router.push(`/datasets/${localDatasetId}/documents/${localDocumentId}`)
|
||||
}
|
||||
|
||||
const isEmbedding = useMemo(() => ['indexing', 'splitting', 'parsing', 'cleaning'].includes(indexingStatusDetail?.indexing_status || ''), [indexingStatusDetail])
|
||||
const isEmbeddingCompleted = useMemo(() => ['completed'].includes(indexingStatusDetail?.indexing_status || ''), [indexingStatusDetail])
|
||||
const isEmbeddingPaused = useMemo(() => ['paused'].includes(indexingStatusDetail?.indexing_status || ''), [indexingStatusDetail])
|
||||
@@ -233,12 +189,6 @@ const EmbeddingDetail: FC<IEmbeddingDetailProps> = ({
|
||||
const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId: localDatasetId, documentId: localDocumentId }) as Promise<CommonResponse>)
|
||||
if (!e) {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
// if the embedding is resumed from paused, we need to start the query status
|
||||
if (isEmbeddingPaused) {
|
||||
isStopQuery.current = false
|
||||
startQueryStatus()
|
||||
detailUpdate()
|
||||
}
|
||||
setIndexingStatusDetail(null)
|
||||
}
|
||||
else {
|
||||
@@ -246,66 +196,78 @@ const EmbeddingDetail: FC<IEmbeddingDetailProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// if (!ruleDetail && !error)
|
||||
// return <Loading type='app' />
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='py-12 px-16 flex flex-col gap-y-2'>
|
||||
<div className='flex items-center gap-x-1 h-6'>
|
||||
{isEmbedding && <RiLoader2Line className='h-4 w-4 text-text-secondary animate-spin' />}
|
||||
<span className='grow text-text-secondary system-md-semibold-uppercase'>
|
||||
{isEmbedding && t('datasetDocuments.embedding.processing')}
|
||||
{isEmbeddingCompleted && t('datasetDocuments.embedding.completed')}
|
||||
{isEmbeddingPaused && t('datasetDocuments.embedding.paused')}
|
||||
{isEmbeddingError && t('datasetDocuments.embedding.error')}
|
||||
</span>
|
||||
<div className={s.embeddingStatus}>
|
||||
{isEmbedding && t('datasetDocuments.embedding.processing')}
|
||||
{isEmbeddingCompleted && t('datasetDocuments.embedding.completed')}
|
||||
{isEmbeddingPaused && t('datasetDocuments.embedding.paused')}
|
||||
{isEmbeddingError && t('datasetDocuments.embedding.error')}
|
||||
{onTop && isEmbedding && (
|
||||
<Button onClick={handleSwitch} className={s.opBtn}>
|
||||
<StopIcon className={s.opIcon} />
|
||||
{t('datasetDocuments.embedding.stop')}
|
||||
</Button>
|
||||
)}
|
||||
{onTop && isEmbeddingPaused && (
|
||||
<Button onClick={handleSwitch} className={s.opBtn}>
|
||||
<ResumeIcon className={s.opIcon} />
|
||||
{t('datasetDocuments.embedding.resume')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{/* progress bar */}
|
||||
<div className={s.progressContainer}>
|
||||
{new Array(10).fill('').map((_, idx) => <div
|
||||
key={idx}
|
||||
className={cn(s.progressBgItem, isEmbedding ? 'bg-primary-50' : 'bg-gray-100')}
|
||||
/>)}
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-l-md',
|
||||
s.progressBar,
|
||||
(isEmbedding || isEmbeddingCompleted) && s.barProcessing,
|
||||
(isEmbeddingPaused || isEmbeddingError) && s.barPaused,
|
||||
indexingStatusDetail?.indexing_status === 'completed' && 'rounded-r-md',
|
||||
)}
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className={s.progressData}>
|
||||
<div>{t('datasetDocuments.embedding.segments')} {indexingStatusDetail?.completed_segments}/{indexingStatusDetail?.total_segments} · {percent}%</div>
|
||||
</div>
|
||||
<RuleDetail sourceData={ruleDetail} docName={detail?.name} />
|
||||
{!onTop && (
|
||||
<div className='flex items-center gap-2 mt-10'>
|
||||
{isEmbedding && (
|
||||
<button
|
||||
type='button'
|
||||
className={`px-1.5 py-1 border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg
|
||||
shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px] flex items-center gap-x-1 rounded-md`}
|
||||
onClick={handleSwitch}
|
||||
>
|
||||
<RiPauseCircleLine className='w-3.5 h-3.5 text-components-button-secondary-text' />
|
||||
<span className='pr-[3px] text-components-button-secondary-text system-xs-medium'>
|
||||
{t('datasetDocuments.embedding.pause')}
|
||||
</span>
|
||||
</button>
|
||||
<Button onClick={modalShowHandle} className='w-fit'>
|
||||
{t('datasetCreation.stepThree.stop')}
|
||||
</Button>
|
||||
)}
|
||||
{isEmbeddingPaused && (
|
||||
<button
|
||||
type='button'
|
||||
className={`px-1.5 py-1 border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg
|
||||
shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px] flex items-center gap-x-1 rounded-md`}
|
||||
onClick={handleSwitch}
|
||||
>
|
||||
<RiPlayCircleLine className='w-3.5 h-3.5 text-components-button-secondary-text' />
|
||||
<span className='pr-[3px] text-components-button-secondary-text system-xs-medium'>
|
||||
{t('datasetDocuments.embedding.resume')}
|
||||
</span>
|
||||
</button>
|
||||
<Button onClick={handleSwitch} className='w-fit'>
|
||||
{t('datasetCreation.stepThree.resume')}
|
||||
</Button>
|
||||
)}
|
||||
<Button className='w-fit' variant='primary' onClick={navToDocument}>
|
||||
<span>{t('datasetCreation.stepThree.navTo')}</span>
|
||||
<ArrowRightIcon className='h-4 w-4 ml-2 stroke-current stroke-1' />
|
||||
</Button>
|
||||
</div>
|
||||
{/* progress bar */}
|
||||
<div className={cn(
|
||||
'flex items-center w-full h-2 rounded-md border border-components-progress-bar-border overflow-hidden',
|
||||
isEmbedding ? 'bg-components-progress-bar-bg bg-opacity-50' : 'bg-components-progress-bar-bg',
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
'h-full',
|
||||
(isEmbedding || isEmbeddingCompleted) && 'bg-components-progress-bar-progress-solid',
|
||||
(isEmbeddingPaused || isEmbeddingError) && 'bg-components-progress-bar-progress-highlight',
|
||||
)}
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
)}
|
||||
{onTop && <>
|
||||
<Divider />
|
||||
<div className={s.previewTip}>{t('datasetDocuments.embedding.previewTip')}</div>
|
||||
<div className={style.cardWrapper}>
|
||||
{[1, 2, 3].map((v, index) => (
|
||||
<SegmentCard key={index} loading={true} detail={{ position: v } as any} />
|
||||
))}
|
||||
</div>
|
||||
<div className={'w-full flex items-center'}>
|
||||
<span className='text-text-secondary system-xs-medium'>
|
||||
{`${t('datasetDocuments.embedding.segments')} ${indexingStatusDetail?.completed_segments || '--'}/${indexingStatusDetail?.total_segments || '--'} · ${percent}%`}
|
||||
</span>
|
||||
</div>
|
||||
<RuleDetail sourceData={ruleDetail} indexingType={indexingType} retrievalMethod={retrievalMethod} />
|
||||
</div>
|
||||
<EmbeddingSkeleton />
|
||||
</>}
|
||||
<StopEmbeddingModal show={showModal} onConfirm={handleSwitch} onHide={modalCloseHandle} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
SkeletonContainer,
|
||||
SkeletonPoint,
|
||||
SkeletonRectangle,
|
||||
SkeletonRow,
|
||||
} from '@/app/components/base/skeleton'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
|
||||
const CardSkelton = React.memo(() => {
|
||||
return (
|
||||
<SkeletonContainer className='p-1 pb-2 gap-y-0'>
|
||||
<SkeletonContainer className='px-2 pt-1.5 gap-y-0.5'>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-[72px] bg-text-quaternary' />
|
||||
<SkeletonPoint className='opacity-20' />
|
||||
<SkeletonRectangle className='w-24 bg-text-quaternary' />
|
||||
<SkeletonPoint className='opacity-20' />
|
||||
<SkeletonRectangle className='w-24 bg-text-quaternary' />
|
||||
<SkeletonRow className='grow justify-end gap-1'>
|
||||
<SkeletonRectangle className='w-12 bg-text-quaternary' />
|
||||
<SkeletonRectangle className='w-2 bg-text-quaternary mx-1' />
|
||||
</SkeletonRow>
|
||||
</SkeletonRow>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-full bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-full bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
<SkeletonRow className='py-0.5'>
|
||||
<SkeletonRectangle className='w-2/3 bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
</SkeletonContainer>
|
||||
<SkeletonContainer className='px-2 py-1.5'>
|
||||
<SkeletonRow>
|
||||
<SkeletonRectangle className='w-14 bg-text-quaternary' />
|
||||
<SkeletonRectangle className='w-[88px] bg-text-quaternary' />
|
||||
<SkeletonRectangle className='w-14 bg-text-quaternary' />
|
||||
</SkeletonRow>
|
||||
</SkeletonContainer>
|
||||
</SkeletonContainer>
|
||||
)
|
||||
})
|
||||
|
||||
CardSkelton.displayName = 'CardSkelton'
|
||||
|
||||
const EmbeddingSkeleton = () => {
|
||||
return (
|
||||
<div className='relative flex flex-col grow overflow-y-hidden z-10'>
|
||||
<div className='absolute top-0 left-0 w-full h-full bg-dataset-chunk-list-mask-bg z-20' />
|
||||
{[...Array(5)].map((_, index) => {
|
||||
return (
|
||||
<div key={index} className='w-full px-11'>
|
||||
<CardSkelton />
|
||||
{index !== 9 && <div className='w-full px-3'>
|
||||
<Divider type='horizontal' className='bg-divider-subtle my-1' />
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(EmbeddingSkeleton)
|
||||
@@ -1,12 +1,14 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { createContext, useContext, useContextSelector } from 'use-context-selector'
|
||||
import React, { useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { ArrowLeftIcon } from '@heroicons/react/24/solid'
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { RiArrowLeftLine, RiLayoutRight2Line } from '@remixicon/react'
|
||||
import { omit } from 'lodash-es'
|
||||
import { OperationAction, StatusItem } from '../list'
|
||||
import DocumentPicker from '../../common/document-picker'
|
||||
import s from '../style.module.css'
|
||||
import Completed from './completed'
|
||||
import Embedding from './embedding'
|
||||
import Metadata from './metadata'
|
||||
@@ -16,58 +18,30 @@ import style from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import type { MetadataType } from '@/service/datasets'
|
||||
import { checkSegmentBatchImportProgress, fetchDocumentDetail, segmentBatchImport } from '@/service/datasets'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import type { ChunkingMode, ParentMode, ProcessMode } from '@/models/datasets'
|
||||
import type { DocForm } from '@/models/datasets'
|
||||
import { useDatasetDetailContext } from '@/context/dataset-detail'
|
||||
import FloatRightContainer from '@/app/components/base/float-right-container'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { LayoutRight2LineMod } from '@/app/components/base/icons/src/public/knowledge'
|
||||
import { useCheckSegmentBatchImportProgress, useSegmentBatchImport } from '@/service/knowledge/use-segment'
|
||||
import { useDocumentDetail, useDocumentMetadata } from '@/service/knowledge/use-document'
|
||||
|
||||
type DocumentContextValue = {
|
||||
datasetId?: string
|
||||
documentId?: string
|
||||
docForm: string
|
||||
mode?: ProcessMode
|
||||
parentMode?: ParentMode
|
||||
}
|
||||
|
||||
export const DocumentContext = createContext<DocumentContextValue>({ docForm: '' })
|
||||
|
||||
export const useDocumentContext = (selector: (value: DocumentContextValue) => any) => {
|
||||
return useContextSelector(DocumentContext, selector)
|
||||
}
|
||||
export const DocumentContext = createContext<{ datasetId?: string; documentId?: string; docForm: string }>({ docForm: '' })
|
||||
|
||||
type DocumentTitleProps = {
|
||||
datasetId: string
|
||||
extension?: string
|
||||
name?: string
|
||||
processMode?: ProcessMode
|
||||
parent_mode?: ParentMode
|
||||
iconCls?: string
|
||||
textCls?: string
|
||||
wrapperCls?: string
|
||||
}
|
||||
|
||||
export const DocumentTitle: FC<DocumentTitleProps> = ({ datasetId, extension, name, processMode, parent_mode, wrapperCls }) => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<div className={cn('flex items-center justify-start flex-1', wrapperCls)}>
|
||||
<DocumentPicker
|
||||
datasetId={datasetId}
|
||||
value={{
|
||||
name,
|
||||
extension,
|
||||
processMode,
|
||||
parentMode: parent_mode,
|
||||
}}
|
||||
onChange={(doc) => {
|
||||
router.push(`/datasets/${datasetId}/documents/${doc.id}`)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
export const DocumentTitle: FC<DocumentTitleProps> = ({ extension, name, iconCls, textCls, wrapperCls }) => {
|
||||
const localExtension = extension?.toLowerCase() || name?.split('.')?.pop()?.toLowerCase()
|
||||
return <div className={cn('flex items-center justify-start flex-1', wrapperCls)}>
|
||||
<div className={cn(s[`${localExtension || 'txt'}Icon`], style.titleIcon, iconCls)}></div>
|
||||
<span className={cn('font-semibold text-lg text-gray-900 ml-1', textCls)}> {name || '--'}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
type Props = {
|
||||
@@ -93,52 +67,49 @@ const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
|
||||
const showBatchModal = () => setBatchModalVisible(true)
|
||||
const hideBatchModal = () => setBatchModalVisible(false)
|
||||
const resetProcessStatus = () => setImportStatus('')
|
||||
|
||||
const { mutateAsync: checkSegmentBatchImportProgress } = useCheckSegmentBatchImportProgress()
|
||||
const checkProcess = async (jobID: string) => {
|
||||
await checkSegmentBatchImportProgress({ jobID }, {
|
||||
onSuccess: (res) => {
|
||||
setImportStatus(res.job_status)
|
||||
if (res.job_status === ProcessStatus.WAITING || res.job_status === ProcessStatus.PROCESSING)
|
||||
setTimeout(() => checkProcess(res.job_id), 2500)
|
||||
if (res.job_status === ProcessStatus.ERROR)
|
||||
notify({ type: 'error', message: `${t('datasetDocuments.list.batchModal.runError')}` })
|
||||
},
|
||||
onError: (e) => {
|
||||
notify({ type: 'error', message: `${t('datasetDocuments.list.batchModal.runError')}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
},
|
||||
})
|
||||
try {
|
||||
const res = await checkSegmentBatchImportProgress({ jobID })
|
||||
setImportStatus(res.job_status)
|
||||
if (res.job_status === ProcessStatus.WAITING || res.job_status === ProcessStatus.PROCESSING)
|
||||
setTimeout(() => checkProcess(res.job_id), 2500)
|
||||
if (res.job_status === ProcessStatus.ERROR)
|
||||
notify({ type: 'error', message: `${t('datasetDocuments.list.batchModal.runError')}` })
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: `${t('datasetDocuments.list.batchModal.runError')}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
}
|
||||
}
|
||||
|
||||
const { mutateAsync: segmentBatchImport } = useSegmentBatchImport()
|
||||
const runBatch = async (csv: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', csv)
|
||||
await segmentBatchImport({
|
||||
url: `/datasets/${datasetId}/documents/${documentId}/segments/batch_import`,
|
||||
body: formData,
|
||||
}, {
|
||||
onSuccess: (res) => {
|
||||
setImportStatus(res.job_status)
|
||||
checkProcess(res.job_id)
|
||||
},
|
||||
onError: (e) => {
|
||||
notify({ type: 'error', message: `${t('datasetDocuments.list.batchModal.runError')}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
},
|
||||
})
|
||||
try {
|
||||
const res = await segmentBatchImport({
|
||||
url: `/datasets/${datasetId}/documents/${documentId}/segments/batch_import`,
|
||||
body: formData,
|
||||
})
|
||||
setImportStatus(res.job_status)
|
||||
checkProcess(res.job_id)
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: `${t('datasetDocuments.list.batchModal.runError')}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
}
|
||||
}
|
||||
|
||||
const { data: documentDetail, error, refetch: detailMutate } = useDocumentDetail({
|
||||
const { data: documentDetail, error, mutate: detailMutate } = useSWR({
|
||||
action: 'fetchDocumentDetail',
|
||||
datasetId,
|
||||
documentId,
|
||||
params: { metadata: 'without' },
|
||||
})
|
||||
params: { metadata: 'without' as MetadataType },
|
||||
}, apiParams => fetchDocumentDetail(omit(apiParams, 'action')))
|
||||
|
||||
const { data: documentMetadata, error: metadataErr, refetch: metadataMutate } = useDocumentMetadata({
|
||||
const { data: documentMetadata, error: metadataErr, mutate: metadataMutate } = useSWR({
|
||||
action: 'fetchDocumentDetail',
|
||||
datasetId,
|
||||
documentId,
|
||||
params: { metadata: 'only' },
|
||||
})
|
||||
params: { metadata: 'only' as MetadataType },
|
||||
}, apiParams => fetchDocumentDetail(omit(apiParams, 'action')),
|
||||
)
|
||||
|
||||
const backToPrev = () => {
|
||||
router.push(`/datasets/${datasetId}/documents`)
|
||||
@@ -156,65 +127,25 @@ const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
|
||||
detailMutate()
|
||||
}
|
||||
|
||||
const mode = useMemo(() => {
|
||||
return documentDetail?.document_process_rule?.mode
|
||||
}, [documentDetail?.document_process_rule])
|
||||
|
||||
const parentMode = useMemo(() => {
|
||||
return documentDetail?.document_process_rule?.rules?.parent_mode
|
||||
}, [documentDetail?.document_process_rule])
|
||||
|
||||
const isFullDocMode = useMemo(() => {
|
||||
return mode === 'hierarchical' && parentMode === 'full-doc'
|
||||
}, [mode, parentMode])
|
||||
|
||||
return (
|
||||
<DocumentContext.Provider value={{
|
||||
datasetId,
|
||||
documentId,
|
||||
docForm: documentDetail?.doc_form || '',
|
||||
mode,
|
||||
parentMode,
|
||||
}}>
|
||||
<div className='flex flex-col h-full bg-background-default'>
|
||||
<div className='flex items-center justify-between flex-wrap min-h-16 pl-3 pr-4 py-2.5 border-b border-b-divider-subtle'>
|
||||
<div onClick={backToPrev} className={'shrink-0 rounded-full w-8 h-8 flex justify-center items-center cursor-pointer hover:bg-components-button-tertiary-bg'}>
|
||||
<RiArrowLeftLine className='text-components-button-ghost-text hover:text-text-tertiary w-4 h-4' />
|
||||
<DocumentContext.Provider value={{ datasetId, documentId, docForm: documentDetail?.doc_form || '' }}>
|
||||
<div className='flex flex-col h-full'>
|
||||
<div className='flex min-h-16 border-b-gray-100 border-b items-center p-4 justify-between flex-wrap gap-y-2'>
|
||||
<div onClick={backToPrev} className={'shrink-0 rounded-full w-8 h-8 flex justify-center items-center border-gray-100 cursor-pointer border hover:border-gray-300 shadow-[0px_12px_16px_-4px_rgba(16,24,40,0.08),0px_4px_6px_-2px_rgba(16,24,40,0.03)]'}>
|
||||
<ArrowLeftIcon className='text-primary-600 fill-current stroke-current h-4 w-4' />
|
||||
</div>
|
||||
<DocumentTitle
|
||||
datasetId={datasetId}
|
||||
extension={documentDetail?.data_source_info?.upload_file?.extension}
|
||||
name={documentDetail?.name}
|
||||
wrapperCls='mr-2'
|
||||
parent_mode={parentMode}
|
||||
processMode={mode}
|
||||
/>
|
||||
<div className='flex items-center flex-wrap'>
|
||||
{embeddingAvailable && documentDetail && !documentDetail.archived && !isFullDocMode && (
|
||||
<>
|
||||
<SegmentAdd
|
||||
importStatus={importStatus}
|
||||
clearProcessStatus={resetProcessStatus}
|
||||
showNewSegmentModal={showNewSegmentModal}
|
||||
showBatchModal={showBatchModal}
|
||||
embedding={embedding}
|
||||
/>
|
||||
<Divider type='vertical' className='!bg-divider-regular !h-[14px] !mx-3' />
|
||||
</>
|
||||
<Divider className='!h-4' type='vertical' />
|
||||
<DocumentTitle extension={documentDetail?.data_source_info?.upload_file?.extension} name={documentDetail?.name} />
|
||||
<div className='flex items-center flex-wrap gap-y-2'>
|
||||
<StatusItem status={documentDetail?.display_status || 'available'} scene='detail' errorMessage={documentDetail?.error || ''} />
|
||||
{embeddingAvailable && documentDetail && !documentDetail.archived && (
|
||||
<SegmentAdd
|
||||
importStatus={importStatus}
|
||||
clearProcessStatus={resetProcessStatus}
|
||||
showNewSegmentModal={showNewSegmentModal}
|
||||
showBatchModal={showBatchModal}
|
||||
/>
|
||||
)}
|
||||
<StatusItem
|
||||
status={documentDetail?.display_status || 'available'}
|
||||
scene='detail'
|
||||
errorMessage={documentDetail?.error || ''}
|
||||
textCls='font-semibold text-xs uppercase'
|
||||
detail={{
|
||||
enabled: documentDetail?.enabled || false,
|
||||
archived: documentDetail?.archived || false,
|
||||
id: documentId,
|
||||
}}
|
||||
datasetId={datasetId}
|
||||
onUpdate={handleOperate}
|
||||
/>
|
||||
<OperationAction
|
||||
scene='detail'
|
||||
embeddingAvailable={embeddingAvailable}
|
||||
@@ -228,32 +159,20 @@ const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
|
||||
}}
|
||||
datasetId={datasetId}
|
||||
onUpdate={handleOperate}
|
||||
className='!w-[200px]'
|
||||
className='!w-[216px]'
|
||||
/>
|
||||
<button
|
||||
className={style.layoutRightIcon}
|
||||
className={cn(style.layoutRightIcon, showMetadata ? style.iconShow : style.iconClose)}
|
||||
onClick={() => setShowMetadata(!showMetadata)}
|
||||
>
|
||||
{
|
||||
showMetadata
|
||||
? <LayoutRight2LineMod className='w-4 h-4 text-components-button-secondary-text' />
|
||||
: <RiLayoutRight2Line className='w-4 h-4 text-components-button-secondary-text' />
|
||||
}
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row flex-1' style={{ height: 'calc(100% - 4rem)' }}>
|
||||
{isDetailLoading
|
||||
? <Loading type='app' />
|
||||
: <div className={cn('h-full w-full flex flex-col',
|
||||
embedding ? '' : isFullDocMode ? 'relative pt-4 pr-11 pl-11' : 'relative pt-3 pr-11 pl-5',
|
||||
)}>
|
||||
: <div className={`h-full w-full flex flex-col ${embedding ? 'px-6 py-3 sm:py-12 sm:px-16' : 'pb-[30px] pt-3 px-6'}`}>
|
||||
{embedding
|
||||
? <Embedding
|
||||
detailUpdate={detailMutate}
|
||||
indexingType={dataset?.indexing_technique}
|
||||
retrievalMethod={dataset?.retrieval_model_dict?.search_method}
|
||||
/>
|
||||
? <Embedding detail={documentDetail} detailUpdate={detailMutate} />
|
||||
: <Completed
|
||||
embeddingAvailable={embeddingAvailable}
|
||||
showNewSegmentModal={newSegmentModalVisible}
|
||||
@@ -276,7 +195,7 @@ const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
|
||||
isShow={batchModalVisible}
|
||||
onCancel={hideBatchModal}
|
||||
onConfirm={runBatch}
|
||||
docForm={documentDetail?.doc_form as ChunkingMode}
|
||||
docForm={documentDetail?.doc_form as DocForm}
|
||||
/>
|
||||
</div>
|
||||
</DocumentContext.Provider>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { PencilIcon } from '@heroicons/react/24/outline'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { get } from 'lodash-es'
|
||||
import { useDocumentContext } from '../index'
|
||||
import { DocumentContext } from '../index'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import Input from '@/app/components/base/input'
|
||||
@@ -32,7 +32,6 @@ const map2Options = (map: { [key: string]: string }) => {
|
||||
type IFieldInfoProps = {
|
||||
label: string
|
||||
value?: string
|
||||
valueIcon?: ReactNode
|
||||
displayedValue?: string
|
||||
defaultValue?: string
|
||||
showEdit?: boolean
|
||||
@@ -44,7 +43,6 @@ type IFieldInfoProps = {
|
||||
export const FieldInfo: FC<IFieldInfoProps> = ({
|
||||
label,
|
||||
value = '',
|
||||
valueIcon,
|
||||
displayedValue = '',
|
||||
defaultValue,
|
||||
showEdit = false,
|
||||
@@ -58,10 +56,9 @@ export const FieldInfo: FC<IFieldInfoProps> = ({
|
||||
const readAlignTop = !showEdit && textNeedWrap
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-1 py-0.5 min-h-5 text-xs', editAlignTop && '!items-start', readAlignTop && '!items-start pt-1')}>
|
||||
<div className={cn('w-[200px] text-text-tertiary overflow-hidden text-ellipsis whitespace-nowrap shrink-0', editAlignTop && 'pt-1')}>{label}</div>
|
||||
<div className="grow flex items-center gap-1 text-text-secondary">
|
||||
{valueIcon}
|
||||
<div className={cn(s.fieldInfo, editAlignTop && '!items-start', readAlignTop && '!items-start pt-1')}>
|
||||
<div className={cn(s.label, editAlignTop && 'pt-1')}>{label}</div>
|
||||
<div className={s.value}>
|
||||
{!showEdit
|
||||
? displayedValue
|
||||
: inputType === 'select'
|
||||
@@ -150,8 +147,7 @@ const Metadata: FC<IMetadataProps> = ({ docDetail, loading, onUpdate }) => {
|
||||
const [saveLoading, setSaveLoading] = useState(false)
|
||||
|
||||
const { notify } = useContext(ToastContext)
|
||||
const datasetId = useDocumentContext(s => s.datasetId)
|
||||
const documentId = useDocumentContext(s => s.documentId)
|
||||
const { datasetId = '', documentId = '' } = useContext(DocumentContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (docDetail?.doc_type) {
|
||||
@@ -352,7 +348,7 @@ const Metadata: FC<IMetadataProps> = ({ docDetail, loading, onUpdate }) => {
|
||||
·
|
||||
<div
|
||||
onClick={() => { setShowDocTypes(true) }}
|
||||
className='cursor-pointer hover:text-text-accent'
|
||||
className='cursor-pointer hover:text-[#155EEF]'
|
||||
>
|
||||
{t('common.operation.change')}
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,18 @@
|
||||
.desc {
|
||||
@apply text-gray-500 text-xs;
|
||||
}
|
||||
|
||||
.fieldInfo {
|
||||
/* height: 1.75rem; */
|
||||
min-height: 1.75rem;
|
||||
@apply flex flex-row items-center gap-4;
|
||||
}
|
||||
.fieldInfo > .label {
|
||||
@apply w-2/5 max-w-[128px] text-gray-500 text-xs font-medium overflow-hidden text-ellipsis whitespace-nowrap;
|
||||
}
|
||||
.fieldInfo > .value {
|
||||
overflow-wrap: anywhere;
|
||||
@apply w-3/5 text-gray-700 font-normal text-xs;
|
||||
}
|
||||
.changeTip {
|
||||
@apply text-[#D92D20] text-xs text-center;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { memo, useState } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea/common'
|
||||
import { Hash02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import type { SegmentUpdater } from '@/models/datasets'
|
||||
import { addSegment } from '@/service/datasets'
|
||||
import TagInput from '@/app/components/base/tag-input'
|
||||
|
||||
type NewSegmentModalProps = {
|
||||
isShow: boolean
|
||||
onCancel: () => void
|
||||
docForm: string
|
||||
onSave: () => void
|
||||
}
|
||||
|
||||
const NewSegmentModal: FC<NewSegmentModalProps> = ({
|
||||
isShow,
|
||||
onCancel,
|
||||
docForm,
|
||||
onSave,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [question, setQuestion] = useState('')
|
||||
const [answer, setAnswer] = useState('')
|
||||
const { datasetId, documentId } = useParams()
|
||||
const [keywords, setKeywords] = useState<string[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleCancel = () => {
|
||||
setQuestion('')
|
||||
setAnswer('')
|
||||
onCancel()
|
||||
setKeywords([])
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
const params: SegmentUpdater = { content: '' }
|
||||
if (docForm === 'qa_model') {
|
||||
if (!question.trim())
|
||||
return notify({ type: 'error', message: t('datasetDocuments.segment.questionEmpty') })
|
||||
if (!answer.trim())
|
||||
return notify({ type: 'error', message: t('datasetDocuments.segment.answerEmpty') })
|
||||
|
||||
params.content = question
|
||||
params.answer = answer
|
||||
}
|
||||
else {
|
||||
if (!question.trim())
|
||||
return notify({ type: 'error', message: t('datasetDocuments.segment.contentEmpty') })
|
||||
|
||||
params.content = question
|
||||
}
|
||||
|
||||
if (keywords?.length)
|
||||
params.keywords = keywords
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await addSegment({ datasetId, documentId, body: params })
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
handleCancel()
|
||||
onSave()
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (docForm === 'qa_model') {
|
||||
return (
|
||||
<>
|
||||
<div className='mb-1 text-xs font-medium text-gray-500'>QUESTION</div>
|
||||
<AutoHeightTextarea
|
||||
outerClassName='mb-4'
|
||||
className='leading-6 text-md text-gray-800'
|
||||
value={question}
|
||||
placeholder={t('datasetDocuments.segment.questionPlaceholder') || ''}
|
||||
onChange={e => setQuestion(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<div className='mb-1 text-xs font-medium text-gray-500'>ANSWER</div>
|
||||
<AutoHeightTextarea
|
||||
outerClassName='mb-4'
|
||||
className='leading-6 text-md text-gray-800'
|
||||
value={answer}
|
||||
placeholder={t('datasetDocuments.segment.answerPlaceholder') || ''}
|
||||
onChange={e => setAnswer(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AutoHeightTextarea
|
||||
className='leading-6 text-md text-gray-800'
|
||||
value={question}
|
||||
placeholder={t('datasetDocuments.segment.contentPlaceholder') || ''}
|
||||
onChange={e => setQuestion(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isShow={isShow} onClose={() => { }} className='pt-8 px-8 pb-6 !max-w-[640px] !rounded-xl'>
|
||||
<div className={'flex flex-col relative'}>
|
||||
<div className='absolute right-0 -top-0.5 flex items-center h-6'>
|
||||
<div className='flex justify-center items-center w-6 h-6 cursor-pointer' onClick={handleCancel}>
|
||||
<RiCloseLine className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mb-[14px]'>
|
||||
<span className='inline-flex items-center px-1.5 h-5 border border-gray-200 rounded-md'>
|
||||
<Hash02 className='mr-0.5 w-3 h-3 text-gray-400' />
|
||||
<span className='text-[11px] font-medium text-gray-500 italic'>
|
||||
{
|
||||
docForm === 'qa_model'
|
||||
? t('datasetDocuments.segment.newQaSegment')
|
||||
: t('datasetDocuments.segment.newTextSegment')
|
||||
}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className='mb-4 py-1.5 h-[420px] overflow-auto'>{renderContent()}</div>
|
||||
<div className='text-xs font-medium text-gray-500'>{t('datasetDocuments.segment.keywords')}</div>
|
||||
<div className='mb-8'>
|
||||
<TagInput items={keywords} onChange={newKeywords => setKeywords(newKeywords)} />
|
||||
</div>
|
||||
<div className='flex justify-end'>
|
||||
<Button
|
||||
onClick={handleCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NewSegmentModal)
|
||||
@@ -1,208 +0,0 @@
|
||||
import { memo, useMemo, useRef, useState } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { RiCloseLine, RiExpandDiagonalLine } from '@remixicon/react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useSegmentListContext } from './completed'
|
||||
import { SegmentIndexTag } from './completed/common/segment-index-tag'
|
||||
import ActionButtons from './completed/common/action-buttons'
|
||||
import Keywords from './completed/common/keywords'
|
||||
import ChunkContent from './completed/common/chunk-content'
|
||||
import AddAnother from './completed/common/add-another'
|
||||
import Dot from './completed/common/dot'
|
||||
import { useDocumentContext } from './index'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { ChunkingMode, type SegmentUpdater } from '@/models/datasets'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { useAddSegment } from '@/service/knowledge/use-segment'
|
||||
|
||||
type NewSegmentModalProps = {
|
||||
onCancel: () => void
|
||||
docForm: ChunkingMode
|
||||
onSave: () => void
|
||||
viewNewlyAddedChunk: () => void
|
||||
}
|
||||
|
||||
const NewSegmentModal: FC<NewSegmentModalProps> = ({
|
||||
onCancel,
|
||||
docForm,
|
||||
onSave,
|
||||
viewNewlyAddedChunk,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [question, setQuestion] = useState('')
|
||||
const [answer, setAnswer] = useState('')
|
||||
const { datasetId, documentId } = useParams<{ datasetId: string; documentId: string }>()
|
||||
const [keywords, setKeywords] = useState<string[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [addAnother, setAddAnother] = useState(true)
|
||||
const fullScreen = useSegmentListContext(s => s.fullScreen)
|
||||
const toggleFullScreen = useSegmentListContext(s => s.toggleFullScreen)
|
||||
const mode = useDocumentContext(s => s.mode)
|
||||
const { appSidebarExpand } = useAppStore(useShallow(state => ({
|
||||
appSidebarExpand: state.appSidebarExpand,
|
||||
})))
|
||||
const refreshTimer = useRef<any>(null)
|
||||
|
||||
const CustomButton = <>
|
||||
<Divider type='vertical' className='h-3 mx-1 bg-divider-regular' />
|
||||
<button
|
||||
type='button'
|
||||
className='text-text-accent system-xs-semibold'
|
||||
onClick={() => {
|
||||
clearTimeout(refreshTimer.current)
|
||||
viewNewlyAddedChunk()
|
||||
}}>
|
||||
{t('common.operation.view')}
|
||||
</button>
|
||||
</>
|
||||
|
||||
const isQAModel = useMemo(() => {
|
||||
return docForm === ChunkingMode.qa
|
||||
}, [docForm])
|
||||
|
||||
const handleCancel = (actionType: 'esc' | 'add' = 'esc') => {
|
||||
if (actionType === 'esc' || !addAnother)
|
||||
onCancel()
|
||||
setQuestion('')
|
||||
setAnswer('')
|
||||
setKeywords([])
|
||||
}
|
||||
|
||||
const { mutateAsync: addSegment } = useAddSegment()
|
||||
|
||||
const handleSave = async () => {
|
||||
const params: SegmentUpdater = { content: '' }
|
||||
if (isQAModel) {
|
||||
if (!question.trim()) {
|
||||
return notify({
|
||||
type: 'error',
|
||||
message: t('datasetDocuments.segment.questionEmpty'),
|
||||
})
|
||||
}
|
||||
if (!answer.trim()) {
|
||||
return notify({
|
||||
type: 'error',
|
||||
message: t('datasetDocuments.segment.answerEmpty'),
|
||||
})
|
||||
}
|
||||
|
||||
params.content = question
|
||||
params.answer = answer
|
||||
}
|
||||
else {
|
||||
if (!question.trim()) {
|
||||
return notify({
|
||||
type: 'error',
|
||||
message: t('datasetDocuments.segment.contentEmpty'),
|
||||
})
|
||||
}
|
||||
|
||||
params.content = question
|
||||
}
|
||||
|
||||
if (keywords?.length)
|
||||
params.keywords = keywords
|
||||
|
||||
setLoading(true)
|
||||
await addSegment({ datasetId, documentId, body: params }, {
|
||||
onSuccess() {
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('datasetDocuments.segment.chunkAdded'),
|
||||
className: `!w-[296px] !bottom-0 ${appSidebarExpand === 'expand' ? '!left-[216px]' : '!left-14'}
|
||||
!top-auto !right-auto !mb-[52px] !ml-11`,
|
||||
customComponent: CustomButton,
|
||||
})
|
||||
handleCancel('add')
|
||||
refreshTimer.current = setTimeout(() => {
|
||||
onSave()
|
||||
}, 3000)
|
||||
},
|
||||
onSettled() {
|
||||
setLoading(false)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const wordCountText = useMemo(() => {
|
||||
const count = isQAModel ? (question.length + answer.length) : question.length
|
||||
return `${formatNumber(count)} ${t('datasetDocuments.segment.characters', { count })}`
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [question.length, answer.length, isQAModel])
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col h-full'}>
|
||||
<div className={classNames('flex items-center justify-between', fullScreen ? 'py-3 pr-4 pl-6 border border-divider-subtle' : 'pt-3 pr-3 pl-4')}>
|
||||
<div className='flex flex-col'>
|
||||
<div className='text-text-primary system-xl-semibold'>{
|
||||
t('datasetDocuments.segment.addChunk')
|
||||
}</div>
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<SegmentIndexTag label={t('datasetDocuments.segment.newChunk')!} />
|
||||
<Dot />
|
||||
<span className='text-text-tertiary system-xs-medium'>{wordCountText}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
{fullScreen && (
|
||||
<>
|
||||
<AddAnother className='mr-3' isChecked={addAnother} onCheck={() => setAddAnother(!addAnother)} />
|
||||
<ActionButtons
|
||||
handleCancel={handleCancel.bind(null, 'esc')}
|
||||
handleSave={handleSave}
|
||||
loading={loading}
|
||||
actionType='add'
|
||||
/>
|
||||
<Divider type='vertical' className='h-3.5 bg-divider-regular ml-4 mr-2' />
|
||||
</>
|
||||
)}
|
||||
<div className='w-8 h-8 flex justify-center items-center p-1.5 cursor-pointer mr-1' onClick={toggleFullScreen}>
|
||||
<RiExpandDiagonalLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='w-8 h-8 flex justify-center items-center p-1.5 cursor-pointer' onClick={handleCancel.bind(null, 'esc')}>
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames('flex grow', fullScreen ? 'w-full flex-row justify-center px-6 pt-6 gap-x-8' : 'flex-col gap-y-1 py-3 px-4')}>
|
||||
<div className={classNames('break-all overflow-hidden whitespace-pre-line', fullScreen ? 'w-1/2' : 'grow')}>
|
||||
<ChunkContent
|
||||
docForm={docForm}
|
||||
question={question}
|
||||
answer={answer}
|
||||
onQuestionChange={question => setQuestion(question)}
|
||||
onAnswerChange={answer => setAnswer(answer)}
|
||||
isEditMode={true}
|
||||
/>
|
||||
</div>
|
||||
{mode === 'custom' && <Keywords
|
||||
className={fullScreen ? 'w-1/5' : ''}
|
||||
actionType='add'
|
||||
keywords={keywords}
|
||||
isEditMode={true}
|
||||
onKeywordsChange={keywords => setKeywords(keywords)}
|
||||
/>}
|
||||
</div>
|
||||
{!fullScreen && (
|
||||
<div className='flex items-center justify-between p-4 pt-3 border-t-[1px] border-t-divider-subtle'>
|
||||
<AddAnother isChecked={addAnother} onCheck={() => setAddAnother(!addAnother)} />
|
||||
<ActionButtons
|
||||
handleCancel={handleCancel.bind(null, 'esc')}
|
||||
handleSave={handleSave}
|
||||
loading={loading}
|
||||
actionType='add'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(NewSegmentModal)
|
||||
@@ -1,14 +1,13 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiArrowDownSLine,
|
||||
RiErrorWarningFill,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import Popover from '@/app/components/base/popover'
|
||||
|
||||
@@ -17,7 +16,6 @@ export type ISegmentAddProps = {
|
||||
clearProcessStatus: () => void
|
||||
showNewSegmentModal: () => void
|
||||
showBatchModal: () => void
|
||||
embedding: boolean
|
||||
}
|
||||
|
||||
export enum ProcessStatus {
|
||||
@@ -32,49 +30,32 @@ const SegmentAdd: FC<ISegmentAddProps> = ({
|
||||
clearProcessStatus,
|
||||
showNewSegmentModal,
|
||||
showBatchModal,
|
||||
embedding,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const textColor = useMemo(() => {
|
||||
return embedding
|
||||
? 'text-components-button-secondary-accent-text-disabled'
|
||||
: 'text-components-button-secondary-accent-text'
|
||||
}, [embedding])
|
||||
|
||||
if (importStatus) {
|
||||
return (
|
||||
<>
|
||||
{(importStatus === ProcessStatus.WAITING || importStatus === ProcessStatus.PROCESSING) && (
|
||||
<div className='relative overflow-hidden inline-flex items-center mr-2 px-2.5 py-2 text-components-button-secondary-accent-text
|
||||
bg-components-progress-bar-border rounded-lg border-[0.5px] border-components-progress-bar-border
|
||||
shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px]'>
|
||||
<div className={cn('absolute left-0 top-0 h-full bg-components-progress-bar-progress border-r-[1.5px] border-r-components-progress-bar-progress-highlight z-0', importStatus === ProcessStatus.WAITING ? 'w-3/12' : 'w-2/3')} />
|
||||
<RiLoader2Line className='animate-spin mr-1 w-4 h-4' />
|
||||
<span className='system-sm-medium z-10 pr-0.5'>{t('datasetDocuments.list.batchModal.processing')}</span>
|
||||
<div className='relative overflow-hidden inline-flex items-center mr-2 px-3 py-[6px] text-blue-700 bg-[#F5F8FF] rounded-lg border border-black/5'>
|
||||
{importStatus === ProcessStatus.WAITING && <div className='absolute left-0 top-0 w-3/12 h-full bg-[#D1E0FF] z-0' />}
|
||||
{importStatus === ProcessStatus.PROCESSING && <div className='absolute left-0 top-0 w-2/3 h-full bg-[#D1E0FF] z-0' />}
|
||||
<RiLoader2Line className='animate-spin mr-2 w-4 h-4' />
|
||||
<span className='font-medium text-[13px] leading-[18px] z-10'>{t('datasetDocuments.list.batchModal.processing')}</span>
|
||||
</div>
|
||||
)}
|
||||
{importStatus === ProcessStatus.COMPLETED && (
|
||||
<div className='relative inline-flex items-center mr-2 bg-components-panel-bg rounded-lg border-[0.5px] border-components-panel-border shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px] overflow-hidden'>
|
||||
<div className='inline-flex items-center px-2.5 py-2 text-text-success border-r border-r-divider-subtle'>
|
||||
<CheckCircle className='mr-1 w-4 h-4' />
|
||||
<span className='system-sm-medium pr-0.5'>{t('datasetDocuments.list.batchModal.completed')}</span>
|
||||
</div>
|
||||
<div className='m-1 inline-flex items-center'>
|
||||
<span className='system-xs-medium text-components-button-ghost-text hover:bg-components-button-ghost-bg-hover px-1.5 py-1 rounded-md cursor-pointer' onClick={clearProcessStatus}>{t('datasetDocuments.list.batchModal.ok')}</span>
|
||||
</div>
|
||||
<div className='absolute top-0 left-0 w-full h-full bg-dataset-chunk-process-success-bg opacity-40 -z-10' />
|
||||
<div className='inline-flex items-center mr-2 px-3 py-[6px] text-gray-700 bg-[#F6FEF9] rounded-lg border border-black/5'>
|
||||
<CheckCircle className='mr-2 w-4 h-4 text-[#039855]' />
|
||||
<span className='font-medium text-[13px] leading-[18px]'>{t('datasetDocuments.list.batchModal.completed')}</span>
|
||||
<span className='pl-2 font-medium text-[13px] leading-[18px] text-[#155EEF] cursor-pointer' onClick={clearProcessStatus}>{t('datasetDocuments.list.batchModal.ok')}</span>
|
||||
</div>
|
||||
)}
|
||||
{importStatus === ProcessStatus.ERROR && (
|
||||
<div className='relative inline-flex items-center mr-2 bg-components-panel-bg rounded-lg border-[0.5px] border-components-panel-border shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px] overflow-hidden'>
|
||||
<div className='inline-flex items-center px-2.5 py-2 text-text-destructive border-r border-r-divider-subtle'>
|
||||
<RiErrorWarningFill className='mr-1 w-4 h-4' />
|
||||
<span className='system-sm-medium pr-0.5'>{t('datasetDocuments.list.batchModal.error')}</span>
|
||||
</div>
|
||||
<div className='m-1 inline-flex items-center'>
|
||||
<span className='system-xs-medium text-components-button-ghost-text hover:bg-components-button-ghost-bg-hover px-1.5 py-1 rounded-md cursor-pointer' onClick={clearProcessStatus}>{t('datasetDocuments.list.batchModal.ok')}</span>
|
||||
</div>
|
||||
<div className='absolute top-0 left-0 w-full h-full bg-dataset-chunk-process-error-bg opacity-40 -z-10' />
|
||||
<div className='inline-flex items-center mr-2 px-3 py-[6px] text-red-600 bg-red-100 rounded-lg border border-black/5'>
|
||||
<RiErrorWarningFill className='mr-2 w-4 h-4 text-[#D92D20]' />
|
||||
<span className='font-medium text-[13px] leading-[18px]'>{t('datasetDocuments.list.batchModal.error')}</span>
|
||||
<span className='pl-2 font-medium text-[13px] leading-[18px] text-[#155EEF] cursor-pointer' onClick={clearProcessStatus}>{t('datasetDocuments.list.batchModal.ok')}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -82,53 +63,24 @@ const SegmentAdd: FC<ISegmentAddProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex items-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px] relative z-20',
|
||||
embedding && 'border-components-button-secondary-border-disabled bg-components-button-secondary-bg-disabled',
|
||||
)}>
|
||||
<button
|
||||
type='button'
|
||||
className={`inline-flex items-center px-2.5 py-2 rounded-l-lg border-r-[1px] border-r-divider-subtle
|
||||
hover:bg-state-base-hover disabled:cursor-not-allowed disabled:hover:bg-transparent`}
|
||||
onClick={showNewSegmentModal}
|
||||
disabled={embedding}
|
||||
>
|
||||
<RiAddLine className={cn('w-4 h-4', textColor)} />
|
||||
<span className={cn('text-[13px] leading-[16px] font-medium capitalize px-0.5 ml-0.5', textColor)}>
|
||||
{t('datasetDocuments.list.action.addButton')}
|
||||
</span>
|
||||
</button>
|
||||
<Popover
|
||||
position='br'
|
||||
manualClose
|
||||
trigger='click'
|
||||
htmlContent={
|
||||
<div className='w-full p-1'>
|
||||
<button
|
||||
type='button'
|
||||
className='w-full py-1.5 px-2 flex items-center hover:bg-state-base-hover rounded-lg text-text-secondary system-md-regular'
|
||||
onClick={showBatchModal}
|
||||
>
|
||||
{t('datasetDocuments.list.action.batchAdd')}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
btnElement={
|
||||
<div className='flex justify-center items-center' >
|
||||
<RiArrowDownSLine className={cn('w-4 h-4', textColor)}/>
|
||||
</div>
|
||||
}
|
||||
btnClassName={open => cn(
|
||||
`!p-2 !border-0 !rounded-l-none !rounded-r-lg !hover:bg-state-base-hover backdrop-blur-[5px]
|
||||
disabled:cursor-not-allowed disabled:bg-transparent disabled:hover:bg-transparent`,
|
||||
open ? '!bg-state-base-hover' : '',
|
||||
)}
|
||||
popupClassName='!min-w-[128px] !bg-components-panel-bg-blur !rounded-xl border-[0.5px] !ring-0
|
||||
border-components-panel-border !shadow-xl !shadow-shadow-shadow-5 backdrop-blur-[5px]'
|
||||
className='min-w-[128px] h-fit'
|
||||
disabled={embedding}
|
||||
/>
|
||||
</div>
|
||||
<Popover
|
||||
manualClose
|
||||
trigger='click'
|
||||
htmlContent={
|
||||
<div className='w-full py-1'>
|
||||
<div className='py-2 px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer text-gray-700 text-sm' onClick={showNewSegmentModal}>{t('datasetDocuments.list.action.add')}</div>
|
||||
<div className='py-2 px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer text-gray-700 text-sm' onClick={showBatchModal}>{t('datasetDocuments.list.action.batchAdd')}</div>
|
||||
</div>
|
||||
}
|
||||
btnElement={
|
||||
<div className='inline-flex items-center'>
|
||||
<FilePlus02 className='w-4 h-4 text-gray-700' />
|
||||
<span className='pl-1'>{t('datasetDocuments.list.action.addButton')}</span>
|
||||
</div>
|
||||
}
|
||||
btnClassName={open => cn('mr-2 !py-[6px] !text-[13px] !leading-[18px] hover:bg-gray-50 border border-gray-200 hover:border-gray-300 hover:shadow-[0_1px_2px_rgba(16,24,40,0.05)]', open ? '!bg-gray-100 !shadow-none' : '!bg-transparent')}
|
||||
className='!w-[132px] h-fit !z-20 !translate-x-0 !left-0'
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(SegmentAdd)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
'use client'
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import DatasetDetailContext from '@/context/dataset-detail'
|
||||
import type { CrawlOptions, CustomFile } from '@/models/datasets'
|
||||
import type { CrawlOptions, CustomFile, FullDocumentDetail } from '@/models/datasets'
|
||||
import type { MetadataType } from '@/service/datasets'
|
||||
import { fetchDocumentDetail } from '@/service/datasets'
|
||||
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import StepTwo from '@/app/components/datasets/create/step-two'
|
||||
@@ -14,7 +16,6 @@ import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import { useDocumentDetail, useInvalidDocumentDetailKey } from '@/service/knowledge/use-document'
|
||||
|
||||
type DocumentSettingsProps = {
|
||||
datasetId: string
|
||||
@@ -25,23 +26,15 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const [isShowSetAPIKey, { setTrue: showSetAPIKey, setFalse: hideSetAPIkey }] = useBoolean()
|
||||
const [hasError, setHasError] = useState(false)
|
||||
const { indexingTechnique, dataset } = useContext(DatasetDetailContext)
|
||||
const { data: embeddingsDefaultModel } = useDefaultModel(ModelTypeEnum.textEmbedding)
|
||||
|
||||
const invalidDocumentDetail = useInvalidDocumentDetailKey()
|
||||
const saveHandler = () => {
|
||||
invalidDocumentDetail()
|
||||
router.push(`/datasets/${datasetId}/documents/${documentId}`)
|
||||
}
|
||||
const saveHandler = () => router.push(`/datasets/${datasetId}/documents/${documentId}`)
|
||||
|
||||
const cancelHandler = () => router.back()
|
||||
|
||||
const { data: documentDetail, error } = useDocumentDetail({
|
||||
datasetId,
|
||||
documentId,
|
||||
params: { metadata: 'without' },
|
||||
})
|
||||
|
||||
const [documentDetail, setDocumentDetail] = useState<FullDocumentDetail | null>(null)
|
||||
const currentPage = useMemo(() => {
|
||||
return {
|
||||
workspace_id: documentDetail?.data_source_info.notion_workspace_id,
|
||||
@@ -51,8 +44,23 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => {
|
||||
type: documentDetail?.data_source_type,
|
||||
}
|
||||
}, [documentDetail])
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const detail = await fetchDocumentDetail({
|
||||
datasetId,
|
||||
documentId,
|
||||
params: { metadata: 'without' as MetadataType },
|
||||
})
|
||||
setDocumentDetail(detail)
|
||||
}
|
||||
catch (e) {
|
||||
setHasError(true)
|
||||
}
|
||||
})()
|
||||
}, [datasetId, documentId])
|
||||
|
||||
if (error)
|
||||
if (hasError)
|
||||
return <AppUnavailable code={500} unknownReason={t('datasetCreation.error.unavailable') as string} />
|
||||
|
||||
return (
|
||||
@@ -77,7 +85,7 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => {
|
||||
websiteCrawlProvider={documentDetail.data_source_info?.provider}
|
||||
websiteCrawlJobId={documentDetail.data_source_info?.job_id}
|
||||
crawlOptions={documentDetail.data_source_info as unknown as CrawlOptions}
|
||||
indexingType={indexingTechnique}
|
||||
indexingType={indexingTechnique || ''}
|
||||
isSetting
|
||||
documentDetail={documentDetail}
|
||||
files={[documentDetail.data_source_info.upload_file as CustomFile]}
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
@apply h-6 w-6 !important;
|
||||
}
|
||||
.layoutRightIcon {
|
||||
@apply p-2 ml-2 border-[0.5px] border-components-button-secondary-border hover:border-components-button-secondary-border-hover
|
||||
rounded-lg bg-components-button-secondary-bg hover:bg-components-button-secondary-bg-hover cursor-pointer
|
||||
shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px];
|
||||
@apply w-8 h-8 ml-2 box-border border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer hover:shadow-[0_1px_2px_rgba(16,24,40,0.05)];
|
||||
}
|
||||
.iconShow {
|
||||
background: center center url(../assets/layoutRightShow.svg) no-repeat;
|
||||
}
|
||||
.iconClose {
|
||||
background: center center url(../assets/layoutRightClose.svg) no-repeat;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useDebounce, useDebounceFn } from 'ahooks'
|
||||
import { groupBy, omit } from 'lodash-es'
|
||||
import { PlusIcon } from '@heroicons/react/24/solid'
|
||||
import { RiExternalLinkLine } from '@remixicon/react'
|
||||
import AutoDisabledDocument from '../common/document-status-with-action/auto-disabled-document'
|
||||
import List from './list'
|
||||
import s from './style.module.css'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Pagination from '@/app/components/base/pagination'
|
||||
import { get } from '@/service/base'
|
||||
import { createDocument, fetchDocuments } from '@/service/datasets'
|
||||
import { useDatasetDetailContext } from '@/context/dataset-detail'
|
||||
@@ -21,9 +20,10 @@ import { NotionPageSelectorModal } from '@/app/components/base/notion-page-selec
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import type { CreateDocumentReq } from '@/models/datasets'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import IndexFailed from '@/app/components/datasets/common/document-status-with-action/index-failed'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import cn from '@/utils/classnames'
|
||||
import RetryButton from '@/app/components/base/retry-button'
|
||||
// Custom page count is not currently supported.
|
||||
const limit = 15
|
||||
|
||||
const FolderPlusIcon = ({ className }: React.SVGProps<SVGElement>) => {
|
||||
return <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||
<path d="M10.8332 5.83333L9.90355 3.9741C9.63601 3.439 9.50222 3.17144 9.30265 2.97597C9.12615 2.80311 8.91344 2.67164 8.6799 2.59109C8.41581 2.5 8.11668 2.5 7.51841 2.5H4.33317C3.39975 2.5 2.93304 2.5 2.57652 2.68166C2.26292 2.84144 2.00795 3.09641 1.84816 3.41002C1.6665 3.76654 1.6665 4.23325 1.6665 5.16667V5.83333M1.6665 5.83333H14.3332C15.7333 5.83333 16.4334 5.83333 16.9681 6.10582C17.4386 6.3455 17.821 6.72795 18.0607 7.19836C18.3332 7.73314 18.3332 8.4332 18.3332 9.83333V13.5C18.3332 14.9001 18.3332 15.6002 18.0607 16.135C17.821 16.6054 17.4386 16.9878 16.9681 17.2275C16.4334 17.5 15.7333 17.5 14.3332 17.5H5.6665C4.26637 17.5 3.56631 17.5 3.03153 17.2275C2.56112 16.9878 2.17867 16.6054 1.93899 16.135C1.6665 15.6002 1.6665 14.9001 1.6665 13.5V5.83333ZM9.99984 14.1667V9.16667M7.49984 11.6667H12.4998" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
@@ -74,16 +74,12 @@ type IDocumentsProps = {
|
||||
}
|
||||
|
||||
export const fetcher = (url: string) => get(url, {}, {})
|
||||
const DEFAULT_LIMIT = 15
|
||||
|
||||
const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
|
||||
const { t } = useTranslation()
|
||||
const { plan } = useProviderContext()
|
||||
const isFreePlan = plan.type === 'sandbox'
|
||||
const [inputValue, setInputValue] = useState<string>('') // the input value
|
||||
const [searchValue, setSearchValue] = useState<string>('')
|
||||
const [currPage, setCurrPage] = React.useState<number>(0)
|
||||
const [limit, setLimit] = useState<number>(DEFAULT_LIMIT)
|
||||
const router = useRouter()
|
||||
const { dataset } = useDatasetDetailContext()
|
||||
const [notionPageSelectorModalVisible, setNotionPageSelectorModalVisible] = useState(false)
|
||||
@@ -97,9 +93,9 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
|
||||
|
||||
const query = useMemo(() => {
|
||||
return { page: currPage + 1, limit, keyword: debouncedSearchValue, fetch: isDataSourceNotion ? true : '' }
|
||||
}, [currPage, debouncedSearchValue, isDataSourceNotion, limit])
|
||||
}, [currPage, debouncedSearchValue, isDataSourceNotion])
|
||||
|
||||
const { data: documentsRes, error, mutate, isLoading: isListLoading } = useSWR(
|
||||
const { data: documentsRes, error, mutate } = useSWR(
|
||||
{
|
||||
action: 'fetchDocuments',
|
||||
datasetId,
|
||||
@@ -109,17 +105,6 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
|
||||
{ refreshInterval: (isDataSourceNotion && timerCanRun) ? 2500 : 0 },
|
||||
)
|
||||
|
||||
const [isMuting, setIsMuting] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!isListLoading && isMuting)
|
||||
setIsMuting(false)
|
||||
}, [isListLoading, isMuting])
|
||||
|
||||
const handleUpdate = useCallback(() => {
|
||||
setIsMuting(true)
|
||||
mutate()
|
||||
}, [mutate])
|
||||
|
||||
const documentsWithProgress = useMemo(() => {
|
||||
let completedNum = 0
|
||||
let percent = 0
|
||||
@@ -161,7 +146,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
|
||||
router.push(`/datasets/${datasetId}/documents/create`)
|
||||
}
|
||||
|
||||
const isLoading = isListLoading // !documentsRes && !error
|
||||
const isLoading = !documentsRes && !error
|
||||
|
||||
const handleSaveNotionPageSelected = async (selectedPages: NotionPage[]) => {
|
||||
const workspacesMap = groupBy(selectedPages, 'workspace_id')
|
||||
@@ -210,7 +195,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
|
||||
}
|
||||
|
||||
const documentsList = isDataSourceNotion ? documentsWithProgress?.data : documentsRes?.data
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||
|
||||
const { run: handleSearch } = useDebounceFn(() => {
|
||||
setSearchValue(inputValue)
|
||||
}, { wait: 500 })
|
||||
@@ -223,17 +208,8 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
|
||||
return (
|
||||
<div className='flex flex-col h-full overflow-y-auto'>
|
||||
<div className='flex flex-col justify-center gap-1 px-6 pt-4'>
|
||||
<h1 className='text-base font-semibold text-text-primary'>{t('datasetDocuments.list.title')}</h1>
|
||||
<div className='flex items-center text-sm font-normal text-text-tertiary space-x-0.5'>
|
||||
<span>{t('datasetDocuments.list.desc')}</span>
|
||||
<a
|
||||
className='flex items-center text-text-accent'
|
||||
target='_blank'
|
||||
href='https://docs.dify.ai/guides/knowledge-base/integrate-knowledge-within-application'>
|
||||
<span>{t('datasetDocuments.list.learnMore')}</span>
|
||||
<RiExternalLinkLine className='w-3 h-3' />
|
||||
</a>
|
||||
</div>
|
||||
<h1 className={s.title}>{t('datasetDocuments.list.title')}</h1>
|
||||
<p className={s.desc}>{t('datasetDocuments.list.desc')}</p>
|
||||
</div>
|
||||
<div className='flex flex-col px-6 py-4 flex-1'>
|
||||
<div className='flex items-center justify-between flex-wrap'>
|
||||
@@ -246,38 +222,27 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
|
||||
onClear={() => handleInputChange('')}
|
||||
/>
|
||||
<div className='flex gap-2 justify-center items-center !h-8'>
|
||||
{!isFreePlan && <AutoDisabledDocument datasetId={datasetId} />}
|
||||
<IndexFailed datasetId={datasetId} />
|
||||
<RetryButton datasetId={datasetId} />
|
||||
{embeddingAvailable && (
|
||||
<Button variant='primary' onClick={routeToDocCreate} className='shrink-0'>
|
||||
<PlusIcon className={cn('h-4 w-4 mr-2 stroke-current')} />
|
||||
<PlusIcon className='h-4 w-4 mr-2 stroke-current' />
|
||||
{isDataSourceNotion && t('datasetDocuments.list.addPages')}
|
||||
{isDataSourceWeb && t('datasetDocuments.list.addUrl')}
|
||||
{(!dataset?.data_source_type || isDataSourceFile) && t('datasetDocuments.list.addFile')}
|
||||
{isDataSourceFile && t('datasetDocuments.list.addFile')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{(isLoading && !isMuting)
|
||||
{isLoading
|
||||
? <Loading type='app' />
|
||||
: total > 0
|
||||
? <List
|
||||
embeddingAvailable={embeddingAvailable}
|
||||
documents={documentsList || []}
|
||||
datasetId={datasetId}
|
||||
onUpdate={handleUpdate}
|
||||
selectedIds={selectedIds}
|
||||
onSelectedIdChange={setSelectedIds}
|
||||
pagination={{
|
||||
total,
|
||||
limit,
|
||||
onLimitChange: setLimit,
|
||||
current: currPage,
|
||||
onChange: setCurrPage,
|
||||
}}
|
||||
/>
|
||||
? <List embeddingAvailable={embeddingAvailable} documents={documentsList || []} datasetId={datasetId} onUpdate={mutate} />
|
||||
: <EmptyElement canAdd={embeddingAvailable} onClick={routeToDocCreate} type={isDataSourceNotion ? 'sync' : 'upload'} />
|
||||
}
|
||||
{/* Show Pagination only if the total is more than the limit */}
|
||||
{(total && total > limit)
|
||||
? <Pagination current={currPage} onChange={setCurrPage} total={total} limit={limit} />
|
||||
: null}
|
||||
<NotionPageSelectorModal
|
||||
isShow={notionPageSelectorModalVisible}
|
||||
onClose={() => setNotionPageSelectorModalVisible(false)}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
/* eslint-disable no-mixed-operators */
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import type { FC, SVGProps } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useBoolean, useDebounceFn } from 'ahooks'
|
||||
import { ArrowDownIcon } from '@heroicons/react/24/outline'
|
||||
import { pick, uniq } from 'lodash-es'
|
||||
import { ArrowDownIcon, TrashIcon } from '@heroicons/react/24/outline'
|
||||
import { pick } from 'lodash-es'
|
||||
import {
|
||||
RiArchive2Line,
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
RiEqualizer2Line,
|
||||
RiLoopLeftLine,
|
||||
RiMoreFill,
|
||||
} from '@remixicon/react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
@@ -18,33 +14,49 @@ import { useTranslation } from 'react-i18next'
|
||||
import dayjs from 'dayjs'
|
||||
import { Edit03 } from '../../base/icons/src/vender/solid/general'
|
||||
import { Globe01 } from '../../base/icons/src/vender/line/mapsAndTravel'
|
||||
import ChunkingModeLabel from '../common/chunking-mode-label'
|
||||
import FileTypeIcon from '../../base/file-uploader/file-type-icon'
|
||||
import s from './style.module.css'
|
||||
import RenameModal from './rename-modal'
|
||||
import BatchAction from './detail/completed/common/batch-action'
|
||||
import cn from '@/utils/classnames'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Popover from '@/app/components/base/popover'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Toast, { ToastContext } from '@/app/components/base/toast'
|
||||
import type { ColorMap, IndicatorProps } from '@/app/components/header/indicator'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import type { IndicatorProps } from '@/app/components/header/indicator'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { archiveDocument, deleteDocument, disableDocument, enableDocument, syncDocument, syncWebsite, unArchiveDocument } from '@/service/datasets'
|
||||
import NotionIcon from '@/app/components/base/notion-icon'
|
||||
import ProgressBar from '@/app/components/base/progress-bar'
|
||||
import { ChunkingMode, DataSourceType, DocumentActionType, type DocumentDisplayStatus, type SimpleDocumentDetail } from '@/models/datasets'
|
||||
import { DataSourceType, type DocumentDisplayStatus, type SimpleDocumentDetail } from '@/models/datasets'
|
||||
import type { CommonResponse } from '@/models/common'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import { useDatasetDetailContextWithSelector as useDatasetDetailContext } from '@/context/dataset-detail'
|
||||
import type { Props as PaginationProps } from '@/app/components/base/pagination'
|
||||
import Pagination from '@/app/components/base/pagination'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { useDocumentArchive, useDocumentDelete, useDocumentDisable, useDocumentEnable, useDocumentUnArchive, useSyncDocument, useSyncWebsite } from '@/service/knowledge/use-document'
|
||||
import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils/extension-to-file-type'
|
||||
|
||||
export const SettingsIcon = ({ className }: SVGProps<SVGElement>) => {
|
||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||
<path d="M2 5.33325L10 5.33325M10 5.33325C10 6.43782 10.8954 7.33325 12 7.33325C13.1046 7.33325 14 6.43782 14 5.33325C14 4.22868 13.1046 3.33325 12 3.33325C10.8954 3.33325 10 4.22868 10 5.33325ZM6 10.6666L14 10.6666M6 10.6666C6 11.7712 5.10457 12.6666 4 12.6666C2.89543 12.6666 2 11.7712 2 10.6666C2 9.56202 2.89543 8.66659 4 8.66659C5.10457 8.66659 6 9.56202 6 10.6666Z" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const SyncIcon = () => {
|
||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.69773 13.1783C7.29715 13.8879 9.20212 13.8494 10.8334 12.9075C13.5438 11.3427 14.4724 7.87704 12.9076 5.16672L12.7409 4.87804M3.09233 10.8335C1.52752 8.12314 2.45615 4.65746 5.16647 3.09265C6.7978 2.15081 8.70277 2.11227 10.3022 2.82185M1.66226 10.8892L3.48363 11.3773L3.97166 9.5559M12.0284 6.44393L12.5164 4.62256L14.3378 5.1106" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const FilePlusIcon = ({ className }: SVGProps<SVGElement>) => {
|
||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||
<path d="M13.3332 6.99992V4.53325C13.3332 3.41315 13.3332 2.85309 13.1152 2.42527C12.9234 2.04895 12.6175 1.74299 12.2412 1.55124C11.8133 1.33325 11.2533 1.33325 10.1332 1.33325H5.8665C4.7464 1.33325 4.18635 1.33325 3.75852 1.55124C3.3822 1.74299 3.07624 2.04895 2.88449 2.42527C2.6665 2.85309 2.6665 3.41315 2.6665 4.53325V11.4666C2.6665 12.5867 2.6665 13.1467 2.88449 13.5746C3.07624 13.9509 3.3822 14.2569 3.75852 14.4486C4.18635 14.6666 4.7464 14.6666 5.8665 14.6666H7.99984M9.33317 7.33325H5.33317M6.6665 9.99992H5.33317M10.6665 4.66659H5.33317M11.9998 13.9999V9.99992M9.99984 11.9999H13.9998" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ArchiveIcon = ({ className }: SVGProps<SVGElement>) => {
|
||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||
<path d="M2.66683 5.33106C2.55749 5.32824 2.47809 5.32191 2.40671 5.30771C1.87779 5.2025 1.46432 4.78904 1.35912 4.26012C1.3335 4.13132 1.3335 3.97644 1.3335 3.66667C1.3335 3.3569 1.3335 3.20201 1.35912 3.07321C1.46432 2.54429 1.87779 2.13083 2.40671 2.02562C2.53551 2 2.69039 2 3.00016 2H13.0002C13.3099 2 13.4648 2 13.5936 2.02562C14.1225 2.13083 14.536 2.54429 14.6412 3.07321C14.6668 3.20201 14.6668 3.3569 14.6668 3.66667C14.6668 3.97644 14.6668 4.13132 14.6412 4.26012C14.536 4.78904 14.1225 5.2025 13.5936 5.30771C13.5222 5.32191 13.4428 5.32824 13.3335 5.33106M6.66683 8.66667H9.3335M2.66683 5.33333H13.3335V10.8C13.3335 11.9201 13.3335 12.4802 13.1155 12.908C12.9238 13.2843 12.6178 13.5903 12.2415 13.782C11.8137 14 11.2536 14 10.1335 14H5.86683C4.74672 14 4.18667 14 3.75885 13.782C3.38252 13.5903 3.07656 13.2843 2.88482 12.908C2.66683 12.4802 2.66683 11.9201 2.66683 10.8V5.33333Z" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const useIndexStatus = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -60,15 +72,6 @@ export const useIndexStatus = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const STATUS_TEXT_COLOR_MAP: ColorMap = {
|
||||
green: 'text-util-colors-green-green-600',
|
||||
orange: 'text-util-colors-warning-warning-600',
|
||||
red: 'text-util-colors-red-red-600',
|
||||
blue: 'text-util-colors-blue-light-blue-light-600',
|
||||
yellow: 'text-util-colors-warning-warning-600',
|
||||
gray: 'text-text-tertiary',
|
||||
}
|
||||
|
||||
// status item for list
|
||||
export const StatusItem: FC<{
|
||||
status: DocumentDisplayStatus
|
||||
@@ -76,82 +79,16 @@ export const StatusItem: FC<{
|
||||
scene?: 'list' | 'detail'
|
||||
textCls?: string
|
||||
errorMessage?: string
|
||||
detail?: {
|
||||
enabled: boolean
|
||||
archived: boolean
|
||||
id: string
|
||||
}
|
||||
datasetId?: string
|
||||
onUpdate?: (operationName?: string) => void
|
||||
|
||||
}> = ({ status, reverse = false, scene = 'list', textCls = '', errorMessage, datasetId = '', detail, onUpdate }) => {
|
||||
}> = ({ status, reverse = false, scene = 'list', textCls = '', errorMessage }) => {
|
||||
const DOC_INDEX_STATUS_MAP = useIndexStatus()
|
||||
const localStatus = status.toLowerCase() as keyof typeof DOC_INDEX_STATUS_MAP
|
||||
const { enabled = false, archived = false, id = '' } = detail || {}
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { t } = useTranslation()
|
||||
const { mutateAsync: enableDocument } = useDocumentEnable()
|
||||
const { mutateAsync: disableDocument } = useDocumentDisable()
|
||||
const { mutateAsync: deleteDocument } = useDocumentDelete()
|
||||
|
||||
const onOperate = async (operationName: OperationName) => {
|
||||
let opApi = deleteDocument
|
||||
switch (operationName) {
|
||||
case 'enable':
|
||||
opApi = enableDocument
|
||||
break
|
||||
case 'disable':
|
||||
opApi = disableDocument
|
||||
break
|
||||
}
|
||||
const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId, documentId: id }) as Promise<CommonResponse>)
|
||||
if (!e) {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
onUpdate?.(operationName)
|
||||
}
|
||||
else { notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) }
|
||||
}
|
||||
|
||||
const { run: handleSwitch } = useDebounceFn((operationName: OperationName) => {
|
||||
if (operationName === 'enable' && enabled)
|
||||
return
|
||||
if (operationName === 'disable' && !enabled)
|
||||
return
|
||||
onOperate(operationName)
|
||||
}, { wait: 500 })
|
||||
|
||||
const embedding = useMemo(() => {
|
||||
return ['queuing', 'indexing', 'paused'].includes(localStatus)
|
||||
}, [localStatus])
|
||||
|
||||
return <div className={
|
||||
cn('flex items-center',
|
||||
reverse ? 'flex-row-reverse' : '',
|
||||
scene === 'detail' ? s.statusItemDetail : '')
|
||||
}>
|
||||
<Indicator color={DOC_INDEX_STATUS_MAP[localStatus]?.color as IndicatorProps['color']} className={reverse ? 'ml-2' : 'mr-2'} />
|
||||
<span className={cn(`${STATUS_TEXT_COLOR_MAP[DOC_INDEX_STATUS_MAP[localStatus].color as keyof typeof STATUS_TEXT_COLOR_MAP]} text-sm`, textCls)}>
|
||||
{DOC_INDEX_STATUS_MAP[localStatus]?.text}
|
||||
</span>
|
||||
{
|
||||
scene === 'detail' && (
|
||||
<div className='flex justify-between items-center ml-1.5'>
|
||||
<Tooltip
|
||||
popupContent={t('datasetDocuments.list.action.enableWarning')}
|
||||
popupClassName='text-text-secondary system-xs-medium'
|
||||
needsDelay
|
||||
disabled={!archived}
|
||||
>
|
||||
<Switch
|
||||
defaultValue={archived ? false : enabled}
|
||||
onChange={v => !archived && handleSwitch(v ? 'enable' : 'disable')}
|
||||
disabled={embedding || archived}
|
||||
size='md'
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<span className={cn('text-gray-700 text-sm', textCls)}>{DOC_INDEX_STATUS_MAP[localStatus]?.text}</span>
|
||||
{
|
||||
errorMessage && (
|
||||
<Tooltip
|
||||
@@ -189,13 +126,7 @@ export const OperationAction: FC<{
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const { mutateAsync: archiveDocument } = useDocumentArchive()
|
||||
const { mutateAsync: unArchiveDocument } = useDocumentUnArchive()
|
||||
const { mutateAsync: enableDocument } = useDocumentEnable()
|
||||
const { mutateAsync: disableDocument } = useDocumentDisable()
|
||||
const { mutateAsync: deleteDocument } = useDocumentDelete()
|
||||
const { mutateAsync: syncDocument } = useSyncDocument()
|
||||
const { mutateAsync: syncWebsite } = useSyncWebsite()
|
||||
|
||||
const isListScene = scene === 'list'
|
||||
|
||||
const onOperate = async (operationName: OperationName) => {
|
||||
@@ -216,8 +147,10 @@ export const OperationAction: FC<{
|
||||
case 'sync':
|
||||
if (data_source_type === 'notion_import')
|
||||
opApi = syncDocument
|
||||
|
||||
else
|
||||
opApi = syncWebsite
|
||||
|
||||
break
|
||||
default:
|
||||
opApi = deleteDocument
|
||||
@@ -225,13 +158,13 @@ export const OperationAction: FC<{
|
||||
break
|
||||
}
|
||||
const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId, documentId: id }) as Promise<CommonResponse>)
|
||||
if (!e) {
|
||||
if (!e)
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
onUpdate(operationName)
|
||||
}
|
||||
else { notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) }
|
||||
else
|
||||
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
|
||||
if (operationName === 'delete')
|
||||
setDeleting(false)
|
||||
onUpdate(operationName)
|
||||
}
|
||||
|
||||
const { run: handleSwitch } = useDebounceFn((operationName: OperationName) => {
|
||||
@@ -283,71 +216,85 @@ export const OperationAction: FC<{
|
||||
</>
|
||||
)}
|
||||
{embeddingAvailable && (
|
||||
<>
|
||||
<Tooltip
|
||||
popupContent={t('datasetDocuments.list.action.settings')}
|
||||
popupClassName='text-text-secondary system-xs-medium'
|
||||
>
|
||||
<button
|
||||
className={cn('rounded-lg mr-2 cursor-pointer',
|
||||
!isListScene
|
||||
? 'p-2 bg-components-button-secondary-bg hover:bg-components-button-secondary-bg-hover border-[0.5px] border-components-button-secondary-border hover:border-components-button-secondary-border-hover shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px]'
|
||||
: 'p-0.5 hover:bg-state-base-hover')}
|
||||
onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}>
|
||||
<RiEqualizer2Line className='w-4 h-4 text-components-button-secondary-text' />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
htmlContent={
|
||||
<div className='w-full py-1'>
|
||||
{!archived && (
|
||||
<>
|
||||
<div className={s.actionItem} onClick={() => {
|
||||
handleShowRenameModal({
|
||||
id: detail.id,
|
||||
name: detail.name,
|
||||
})
|
||||
}}>
|
||||
<RiEditLine className='w-4 h-4 text-text-tertiary' />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.table.rename')}</span>
|
||||
<Popover
|
||||
htmlContent={
|
||||
<div className='w-full py-1'>
|
||||
{!isListScene && <>
|
||||
<div className='flex justify-between items-center mx-4 pt-2'>
|
||||
<span className={cn(s.actionName, 'font-medium')}>
|
||||
{!archived && enabled ? t('datasetDocuments.list.index.enable') : t('datasetDocuments.list.index.disable')}
|
||||
</span>
|
||||
<Tooltip
|
||||
popupContent={t('datasetDocuments.list.action.enableWarning')}
|
||||
popupClassName='!font-semibold'
|
||||
needsDelay
|
||||
disabled={!archived}
|
||||
>
|
||||
<div>
|
||||
<Switch
|
||||
defaultValue={archived ? false : enabled}
|
||||
onChange={v => !archived && handleSwitch(v ? 'enable' : 'disable')}
|
||||
disabled={archived}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
{['notion_import', DataSourceType.WEB].includes(data_source_type) && (
|
||||
<div className={s.actionItem} onClick={() => onOperate('sync')}>
|
||||
<RiLoopLeftLine className='w-4 h-4 text-text-tertiary' />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.sync')}</span>
|
||||
</div>
|
||||
)}
|
||||
<Divider className='my-1' />
|
||||
</>
|
||||
)}
|
||||
{!archived && <div className={s.actionItem} onClick={() => onOperate('archive')}>
|
||||
<RiArchive2Line className='w-4 h-4 text-text-tertiary' />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span>
|
||||
</div>}
|
||||
{archived && (
|
||||
<div className={s.actionItem} onClick={() => onOperate('un_archive')}>
|
||||
<RiArchive2Line className='w-4 h-4 text-text-tertiary' />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.unarchive')}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={cn(s.actionItem, s.deleteActionItem, 'group')} onClick={() => setShowModal(true)}>
|
||||
<RiDeleteBinLine className={'w-4 h-4 text-text-tertiary group-hover:text-text-destructive'} />
|
||||
<span className={cn(s.actionName, 'group-hover:text-text-destructive')}>{t('datasetDocuments.list.action.delete')}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='mx-4 pb-1 pt-0.5 text-xs text-gray-500'>
|
||||
{!archived && enabled ? t('datasetDocuments.list.index.enableTip') : t('datasetDocuments.list.index.disableTip')}
|
||||
</div>
|
||||
<Divider />
|
||||
</>}
|
||||
{!archived && (
|
||||
<>
|
||||
<div className={s.actionItem} onClick={() => {
|
||||
handleShowRenameModal({
|
||||
id: detail.id,
|
||||
name: detail.name,
|
||||
})
|
||||
}}>
|
||||
<Edit03 className='w-4 h-4 text-gray-500' />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.table.rename')}</span>
|
||||
</div>
|
||||
<div className={s.actionItem} onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}>
|
||||
<SettingsIcon />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.settings')}</span>
|
||||
</div>
|
||||
{['notion_import', DataSourceType.WEB].includes(data_source_type) && (
|
||||
<div className={s.actionItem} onClick={() => onOperate('sync')}>
|
||||
<SyncIcon />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.sync')}</span>
|
||||
</div>
|
||||
)}
|
||||
<Divider className='my-1' />
|
||||
</>
|
||||
)}
|
||||
{!archived && <div className={s.actionItem} onClick={() => onOperate('archive')}>
|
||||
<ArchiveIcon />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span>
|
||||
</div>}
|
||||
{archived && (
|
||||
<div className={s.actionItem} onClick={() => onOperate('un_archive')}>
|
||||
<ArchiveIcon />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.unarchive')}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={cn(s.actionItem, s.deleteActionItem, 'group')} onClick={() => setShowModal(true)}>
|
||||
<TrashIcon className={'w-4 h-4 stroke-current text-gray-500 stroke-2 group-hover:text-red-500'} />
|
||||
<span className={cn(s.actionName, 'group-hover:text-red-500')}>{t('datasetDocuments.list.action.delete')}</span>
|
||||
</div>
|
||||
}
|
||||
trigger='click'
|
||||
position='br'
|
||||
btnElement={
|
||||
<div className={cn(s.commonIcon)}>
|
||||
<RiMoreFill className='w-4 h-4 text-text-components-button-secondary-text' />
|
||||
</div>
|
||||
}
|
||||
btnClassName={open => cn(isListScene ? s.actionIconWrapperList : s.actionIconWrapperDetail, open ? '!hover:bg-state-base-hover !shadow-none' : '!bg-transparent')}
|
||||
popupClassName='!w-full'
|
||||
className={`flex justify-end !w-[200px] h-fit !z-20 ${className}`}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
trigger='click'
|
||||
position='br'
|
||||
btnElement={
|
||||
<div className={cn(s.commonIcon)}>
|
||||
<RiMoreFill className='w-4 h-4 text-gray-700' />
|
||||
</div>
|
||||
}
|
||||
btnClassName={open => cn(isListScene ? s.actionIconWrapperList : s.actionIconWrapperDetail, open ? '!bg-gray-100 !shadow-none' : '!bg-transparent')}
|
||||
className={`flex justify-end !w-[200px] h-fit !z-20 ${className}`}
|
||||
/>
|
||||
)}
|
||||
{showModal
|
||||
&& <Confirm
|
||||
@@ -376,7 +323,7 @@ export const OperationAction: FC<{
|
||||
|
||||
export const renderTdValue = (value: string | number | null, isEmptyStyle = false) => {
|
||||
return (
|
||||
<div className={cn(isEmptyStyle ? 'text-text-tertiary' : 'text-text-secondary', s.tdValue)}>
|
||||
<div className={cn(isEmptyStyle ? 'text-gray-400' : 'text-gray-700', s.tdValue)}>
|
||||
{value ?? '-'}
|
||||
</div>
|
||||
)
|
||||
@@ -396,34 +343,19 @@ type LocalDoc = SimpleDocumentDetail & { percent?: number }
|
||||
type IDocumentListProps = {
|
||||
embeddingAvailable: boolean
|
||||
documents: LocalDoc[]
|
||||
selectedIds: string[]
|
||||
onSelectedIdChange: (selectedIds: string[]) => void
|
||||
datasetId: string
|
||||
pagination: PaginationProps
|
||||
onUpdate: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Document list component including basic information
|
||||
*/
|
||||
const DocumentList: FC<IDocumentListProps> = ({
|
||||
embeddingAvailable,
|
||||
documents = [],
|
||||
selectedIds,
|
||||
onSelectedIdChange,
|
||||
datasetId,
|
||||
pagination,
|
||||
onUpdate,
|
||||
}) => {
|
||||
const DocumentList: FC<IDocumentListProps> = ({ embeddingAvailable, documents = [], datasetId, onUpdate }) => {
|
||||
const { t } = useTranslation()
|
||||
const { formatTime } = useTimestamp()
|
||||
const router = useRouter()
|
||||
const [datasetConfig] = useDatasetDetailContext(s => [s.dataset])
|
||||
const chunkingMode = datasetConfig?.doc_form
|
||||
const isGeneralMode = chunkingMode !== ChunkingMode.parentChild
|
||||
const isQAMode = chunkingMode === ChunkingMode.qa
|
||||
const [localDocs, setLocalDocs] = useState<LocalDoc[]>(documents)
|
||||
const [enableSort, setEnableSort] = useState(true)
|
||||
const [enableSort, setEnableSort] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLocalDocs(documents)
|
||||
@@ -431,7 +363,7 @@ const DocumentList: FC<IDocumentListProps> = ({
|
||||
|
||||
const onClickSort = () => {
|
||||
setEnableSort(!enableSort)
|
||||
if (enableSort) {
|
||||
if (!enableSort) {
|
||||
const sortedDocs = [...localDocs].sort((a, b) => dayjs(a.created_at).isBefore(dayjs(b.created_at)) ? -1 : 1)
|
||||
setLocalDocs(sortedDocs)
|
||||
}
|
||||
@@ -453,119 +385,46 @@ const DocumentList: FC<IDocumentListProps> = ({
|
||||
onUpdate()
|
||||
}, [onUpdate])
|
||||
|
||||
const isAllSelected = useMemo(() => {
|
||||
return localDocs.length > 0 && localDocs.every(doc => selectedIds.includes(doc.id))
|
||||
}, [localDocs, selectedIds])
|
||||
|
||||
const isSomeSelected = useMemo(() => {
|
||||
return localDocs.some(doc => selectedIds.includes(doc.id))
|
||||
}, [localDocs, selectedIds])
|
||||
|
||||
const onSelectedAll = useCallback(() => {
|
||||
if (isAllSelected)
|
||||
onSelectedIdChange([])
|
||||
else
|
||||
onSelectedIdChange(uniq([...selectedIds, ...localDocs.map(doc => doc.id)]))
|
||||
}, [isAllSelected, localDocs, onSelectedIdChange, selectedIds])
|
||||
const { mutateAsync: archiveDocument } = useDocumentArchive()
|
||||
const { mutateAsync: enableDocument } = useDocumentEnable()
|
||||
const { mutateAsync: disableDocument } = useDocumentDisable()
|
||||
const { mutateAsync: deleteDocument } = useDocumentDelete()
|
||||
|
||||
const handleAction = (actionName: DocumentActionType) => {
|
||||
return async () => {
|
||||
let opApi = deleteDocument
|
||||
switch (actionName) {
|
||||
case DocumentActionType.archive:
|
||||
opApi = archiveDocument
|
||||
break
|
||||
case DocumentActionType.enable:
|
||||
opApi = enableDocument
|
||||
break
|
||||
case DocumentActionType.disable:
|
||||
opApi = disableDocument
|
||||
break
|
||||
default:
|
||||
opApi = deleteDocument
|
||||
break
|
||||
}
|
||||
const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId, documentIds: selectedIds }) as Promise<CommonResponse>)
|
||||
|
||||
if (!e) {
|
||||
Toast.notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
onUpdate()
|
||||
}
|
||||
else { Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) }
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative w-full h-full overflow-x-auto'>
|
||||
<div className='w-full h-full overflow-x-auto'>
|
||||
<table className={`min-w-[700px] max-w-full w-full border-collapse border-0 text-sm mt-3 ${s.documentTable}`}>
|
||||
<thead className="h-8 leading-8 border-b border-divider-subtle text-text-tertiary font-medium text-xs uppercase">
|
||||
<thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-medium text-xs uppercase">
|
||||
<tr>
|
||||
<td className='w-12'>
|
||||
<div className='flex items-center' onClick={e => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
className='shrink-0 mr-2'
|
||||
checked={isAllSelected}
|
||||
mixed={!isAllSelected && isSomeSelected}
|
||||
onCheck={onSelectedAll}
|
||||
/>
|
||||
#
|
||||
</div>
|
||||
</td>
|
||||
<td className='w-12'>#</td>
|
||||
<td>
|
||||
<div className='flex'>
|
||||
{t('datasetDocuments.list.table.header.fileName')}
|
||||
</div>
|
||||
</td>
|
||||
<td className='w-[130px]'>{t('datasetDocuments.list.table.header.chunkingMode')}</td>
|
||||
<td className='w-24'>{t('datasetDocuments.list.table.header.words')}</td>
|
||||
<td className='w-44'>{t('datasetDocuments.list.table.header.hitCount')}</td>
|
||||
<td className='w-44'>
|
||||
<div className='flex items-center' onClick={onClickSort}>
|
||||
<div className='flex justify-between items-center'>
|
||||
{t('datasetDocuments.list.table.header.uploadTime')}
|
||||
<ArrowDownIcon className={cn('ml-0.5 h-3 w-3 stroke-current stroke-2 cursor-pointer', enableSort ? 'text-text-tertiary' : 'text-text-disabled')} />
|
||||
<ArrowDownIcon className={cn('h-3 w-3 stroke-current stroke-2 cursor-pointer', enableSort ? 'text-gray-500' : 'text-gray-300')} onClick={onClickSort} />
|
||||
</div>
|
||||
</td>
|
||||
<td className='w-40'>{t('datasetDocuments.list.table.header.status')}</td>
|
||||
<td className='w-20'>{t('datasetDocuments.list.table.header.action')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-text-secondary">
|
||||
{localDocs.map((doc, index) => {
|
||||
<tbody className="text-gray-700">
|
||||
{localDocs.map((doc) => {
|
||||
const isFile = doc.data_source_type === DataSourceType.FILE
|
||||
const fileType = isFile ? doc.data_source_detail_dict?.upload_file?.extension : ''
|
||||
return <tr
|
||||
key={doc.id}
|
||||
className={'border-b border-divider-subtle h-8 hover:bg-background-default-hover cursor-pointer'}
|
||||
className={'border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer'}
|
||||
onClick={() => {
|
||||
router.push(`/datasets/${datasetId}/documents/${doc.id}`)
|
||||
}}>
|
||||
<td className='text-left align-middle text-text-tertiary text-xs'>
|
||||
<div className='flex items-center' onClick={e => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
className='shrink-0 mr-2'
|
||||
checked={selectedIds.includes(doc.id)}
|
||||
onCheck={() => {
|
||||
onSelectedIdChange(
|
||||
selectedIds.includes(doc.id)
|
||||
? selectedIds.filter(id => id !== doc.id)
|
||||
: [...selectedIds, doc.id],
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{/* {doc.position} */}
|
||||
{index + 1}
|
||||
</div>
|
||||
</td>
|
||||
<td className='text-left align-middle text-gray-500 text-xs'>{doc.position}</td>
|
||||
<td>
|
||||
<div className={'group flex items-center justify-between mr-6 hover:mr-0'}>
|
||||
<span className={cn(s.tdValue, 'flex items-center')}>
|
||||
<div className='group flex items-center justify-between'>
|
||||
<span className={s.tdValue}>
|
||||
{doc?.data_source_type === DataSourceType.NOTION && <NotionIcon className='inline-flex -mt-[3px] mr-1.5 align-middle' type='page' src={doc.data_source_info.notion_page_icon} />
|
||||
}
|
||||
{doc?.data_source_type === DataSourceType.FILE && <FileTypeIcon type={extensionToFileType(doc?.data_source_info?.upload_file?.extension ?? fileType)} className='mr-1.5' />}
|
||||
{doc?.data_source_type === DataSourceType.FILE && <div className={cn(s[`${doc?.data_source_info?.upload_file?.extension ?? fileType}Icon`], s.commonIcon, 'mr-1.5')}></div>}
|
||||
{doc?.data_source_type === DataSourceType.WEB && <Globe01 className='inline-flex -mt-[3px] mr-1.5 align-middle' />
|
||||
}
|
||||
{
|
||||
@@ -577,27 +436,22 @@ const DocumentList: FC<IDocumentListProps> = ({
|
||||
popupContent={t('datasetDocuments.list.table.rename')}
|
||||
>
|
||||
<div
|
||||
className='p-1 rounded-md cursor-pointer hover:bg-state-base-hover'
|
||||
className='p-1 rounded-md cursor-pointer hover:bg-black/5'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleShowRenameModal(doc)
|
||||
}}
|
||||
>
|
||||
<Edit03 className='w-4 h-4 text-text-tertiary' />
|
||||
<Edit03 className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<ChunkingModeLabel
|
||||
isGeneralMode={isGeneralMode}
|
||||
isQAMode={isQAMode}
|
||||
/>
|
||||
|
||||
</td>
|
||||
<td>{renderCount(doc.word_count)}</td>
|
||||
<td>{renderCount(doc.hit_count)}</td>
|
||||
<td className='text-text-secondary text-[13px]'>
|
||||
<td className='text-gray-500 text-[13px]'>
|
||||
{formatTime(doc.created_at, t('datasetHitTesting.dateTimeFormat') as string)}
|
||||
</td>
|
||||
<td>
|
||||
@@ -619,26 +473,6 @@ const DocumentList: FC<IDocumentListProps> = ({
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{(selectedIds.length > 0) && (
|
||||
<BatchAction
|
||||
className='absolute left-0 bottom-16 z-20'
|
||||
selectedIds={selectedIds}
|
||||
onArchive={handleAction(DocumentActionType.archive)}
|
||||
onBatchEnable={handleAction(DocumentActionType.enable)}
|
||||
onBatchDisable={handleAction(DocumentActionType.disable)}
|
||||
onBatchDelete={handleAction(DocumentActionType.delete)}
|
||||
onCancel={() => {
|
||||
onSelectedIdChange([])
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* Show Pagination only if the total is more than the limit */}
|
||||
{pagination.total && pagination.total > (pagination.limit || 10) && (
|
||||
<Pagination
|
||||
{...pagination}
|
||||
className='absolute bottom-0 left-0 w-full px-0 pb-0'
|
||||
/>
|
||||
)}
|
||||
|
||||
{isShowRenameModal && currDocument && (
|
||||
<RenameModal
|
||||
|
||||
@@ -8,22 +8,26 @@
|
||||
box-sizing: border-box;
|
||||
max-width: 200px;
|
||||
}
|
||||
.title {
|
||||
@apply text-xl font-medium text-gray-900;
|
||||
}
|
||||
.desc {
|
||||
@apply text-sm font-normal text-gray-500;
|
||||
}
|
||||
.actionIconWrapperList {
|
||||
@apply h-6 w-6 rounded-md border-none p-1 hover:bg-gray-100 !important;
|
||||
}
|
||||
.actionIconWrapperDetail {
|
||||
@apply p-2 bg-components-button-secondary-bg hover:bg-components-button-secondary-bg-hover
|
||||
border-[0.5px] border-components-button-secondary-border hover:border-components-button-secondary-border-hover
|
||||
shadow-xs shadow-shadow-shadow-3 !important;
|
||||
@apply h-8 w-8 p-2 hover:bg-gray-50 border border-gray-200 hover:border-gray-300 hover:shadow-[0_1px_2px_rgba(16,24,40,0.05)] !important;
|
||||
}
|
||||
.actionItem {
|
||||
@apply h-9 py-2 px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer;
|
||||
}
|
||||
.deleteActionItem {
|
||||
@apply hover:bg-state-destructive-hover !important;
|
||||
@apply hover:bg-red-50 !important;
|
||||
}
|
||||
.actionName {
|
||||
@apply text-text-secondary text-sm;
|
||||
@apply text-gray-700 text-sm;
|
||||
}
|
||||
.addFileBtn {
|
||||
@apply mt-4 w-fit !text-[13px] text-primary-600 font-medium bg-white border-[0.5px];
|
||||
@@ -90,8 +94,7 @@
|
||||
background-image: url(~@/assets/docx.svg);
|
||||
}
|
||||
.statusItemDetail {
|
||||
@apply border-[0.5px] border-components-button-secondary-border inline-flex items-center
|
||||
rounded-lg pl-2.5 pr-2 py-2 mr-2 shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px];
|
||||
@apply h-8 font-medium border border-gray-200 inline-flex items-center rounded-lg pl-3 pr-4 mr-2;
|
||||
}
|
||||
.tdValue {
|
||||
@apply text-sm overflow-hidden text-ellipsis whitespace-nowrap;
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { FloatingFocusManager, type OffsetOptions, autoUpdate, flip, offset, shift, useDismiss, useFloating, useHover, useInteractions, useRole } from '@floating-ui/react'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
// @ts-expect-error no types available
|
||||
import lineClamp from 'line-clamp'
|
||||
import type { SliceProps } from './type'
|
||||
import { SliceContainer, SliceContent, SliceDivider, SliceLabel } from './shared'
|
||||
import classNames from '@/utils/classnames'
|
||||
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
|
||||
|
||||
type EditSliceProps = SliceProps<{
|
||||
label: ReactNode
|
||||
onDelete: () => void
|
||||
labelClassName?: string
|
||||
labelInnerClassName?: string
|
||||
contentClassName?: string
|
||||
showDivider?: boolean
|
||||
offsetOptions?: OffsetOptions
|
||||
}>
|
||||
|
||||
export const EditSlice: FC<EditSliceProps> = (props) => {
|
||||
const {
|
||||
label,
|
||||
className,
|
||||
text,
|
||||
onDelete,
|
||||
labelClassName,
|
||||
labelInnerClassName,
|
||||
contentClassName,
|
||||
showDivider = true,
|
||||
offsetOptions,
|
||||
...rest
|
||||
} = props
|
||||
const [delBtnShow, setDelBtnShow] = useState(false)
|
||||
const [isDelBtnHover, setDelBtnHover] = useState(false)
|
||||
|
||||
const { refs, floatingStyles, context } = useFloating({
|
||||
open: delBtnShow,
|
||||
onOpenChange: setDelBtnShow,
|
||||
placement: 'right-start',
|
||||
whileElementsMounted: autoUpdate,
|
||||
middleware: [
|
||||
flip(),
|
||||
shift(),
|
||||
offset(offsetOptions),
|
||||
],
|
||||
})
|
||||
const hover = useHover(context, {})
|
||||
const dismiss = useDismiss(context)
|
||||
const role = useRole(context)
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([hover, dismiss, role])
|
||||
|
||||
const isDestructive = delBtnShow && isDelBtnHover
|
||||
|
||||
return (
|
||||
<>
|
||||
<SliceContainer {...rest}
|
||||
className={classNames('block mr-0', className)}
|
||||
ref={(ref) => {
|
||||
refs.setReference(ref)
|
||||
if (ref)
|
||||
lineClamp(ref, 4)
|
||||
}}
|
||||
{...getReferenceProps()}
|
||||
>
|
||||
<SliceLabel
|
||||
className={classNames(
|
||||
isDestructive && '!bg-state-destructive-solid !text-text-primary-on-surface',
|
||||
labelClassName,
|
||||
)}
|
||||
labelInnerClassName={labelInnerClassName}
|
||||
>
|
||||
{label}
|
||||
</SliceLabel>
|
||||
<SliceContent
|
||||
className={classNames(
|
||||
isDestructive && '!bg-state-destructive-hover-alt',
|
||||
contentClassName,
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</SliceContent>
|
||||
{showDivider && <SliceDivider
|
||||
className={classNames(
|
||||
isDestructive && '!bg-state-destructive-hover-alt',
|
||||
)}
|
||||
/>}
|
||||
{delBtnShow && <FloatingFocusManager
|
||||
context={context}
|
||||
>
|
||||
<span
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
{...getFloatingProps()}
|
||||
className='p-1 rounded-lg bg-components-actionbar-bg shadow inline-flex items-center justify-center'
|
||||
onMouseEnter={() => setDelBtnHover(true)}
|
||||
onMouseLeave={() => setDelBtnHover(false)}
|
||||
>
|
||||
<ActionButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete()
|
||||
setDelBtnShow(false)
|
||||
}}
|
||||
state={ActionButtonState.Destructive}
|
||||
>
|
||||
<RiDeleteBinLine className='w-4 h-4' />
|
||||
</ActionButton>
|
||||
</span>
|
||||
</FloatingFocusManager>}
|
||||
</SliceContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { autoUpdate, flip, inline, shift, useDismiss, useFloating, useHover, useInteractions, useRole } from '@floating-ui/react'
|
||||
import type { SliceProps } from './type'
|
||||
import { SliceContainer, SliceContent, SliceDivider, SliceLabel } from './shared'
|
||||
|
||||
type PreviewSliceProps = SliceProps<{
|
||||
label: ReactNode
|
||||
tooltip: ReactNode
|
||||
labelInnerClassName?: string
|
||||
dividerClassName?: string
|
||||
}>
|
||||
|
||||
export const PreviewSlice: FC<PreviewSliceProps> = (props) => {
|
||||
const { label, className, text, tooltip, labelInnerClassName, dividerClassName, ...rest } = props
|
||||
const [tooltipOpen, setTooltipOpen] = useState(false)
|
||||
const { refs, floatingStyles, context } = useFloating({
|
||||
open: tooltipOpen,
|
||||
onOpenChange: setTooltipOpen,
|
||||
whileElementsMounted: autoUpdate,
|
||||
placement: 'top',
|
||||
middleware: [
|
||||
inline(),
|
||||
flip(),
|
||||
shift(),
|
||||
],
|
||||
})
|
||||
const hover = useHover(context, {
|
||||
delay: { open: 500 },
|
||||
move: true,
|
||||
})
|
||||
const dismiss = useDismiss(context)
|
||||
const role = useRole(context, { role: 'tooltip' })
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([hover, dismiss, role])
|
||||
return (
|
||||
<>
|
||||
<SliceContainer {...rest}
|
||||
className={className}
|
||||
ref={refs.setReference}
|
||||
{...getReferenceProps()}
|
||||
>
|
||||
<SliceLabel labelInnerClassName={labelInnerClassName}>{label}</SliceLabel>
|
||||
<SliceContent>{text}</SliceContent>
|
||||
<SliceDivider className={dividerClassName} />
|
||||
</SliceContainer>
|
||||
{tooltipOpen && <span
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
{...getFloatingProps()}
|
||||
className='p-2 rounded-md bg-components-tooltip-bg shadow shadow-shadow-shadow-5 backdrop-blur-[5px] text-text-secondary leading-4 border-[0.5px] border-components-panel-border text-xs'
|
||||
>
|
||||
{tooltip}
|
||||
</span>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { type ComponentProps, type FC, forwardRef } from 'react'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
const baseStyle = 'py-[3px]'
|
||||
|
||||
export type SliceContainerProps = ComponentProps<'span'>
|
||||
|
||||
export const SliceContainer: FC<SliceContainerProps> = forwardRef((props, ref) => {
|
||||
const { className, ...rest } = props
|
||||
return <span {...rest} ref={ref} className={classNames(
|
||||
'group align-bottom mr-1 select-none text-sm',
|
||||
className,
|
||||
)} />
|
||||
})
|
||||
SliceContainer.displayName = 'SliceContainer'
|
||||
|
||||
export type SliceLabelProps = ComponentProps<'span'> & { labelInnerClassName?: string }
|
||||
|
||||
export const SliceLabel: FC<SliceLabelProps> = forwardRef((props, ref) => {
|
||||
const { className, children, labelInnerClassName, ...rest } = props
|
||||
return <span {...rest} ref={ref} className={classNames(
|
||||
baseStyle,
|
||||
'px-1 bg-state-base-hover-alt group-hover:bg-state-accent-solid group-hover:text-text-primary-on-surface uppercase text-text-tertiary',
|
||||
className,
|
||||
)}>
|
||||
<span className={classNames('text-nowrap', labelInnerClassName)}>
|
||||
{children}
|
||||
</span>
|
||||
</span>
|
||||
})
|
||||
SliceLabel.displayName = 'SliceLabel'
|
||||
|
||||
export type SliceContentProps = ComponentProps<'span'>
|
||||
|
||||
export const SliceContent: FC<SliceContentProps> = forwardRef((props, ref) => {
|
||||
const { className, children, ...rest } = props
|
||||
return <span {...rest} ref={ref} className={classNames(
|
||||
baseStyle,
|
||||
'px-1 bg-state-base-hover group-hover:bg-state-accent-hover-alt group-hover:text-text-primary leading-7 whitespace-pre-line break-all',
|
||||
className,
|
||||
)}>
|
||||
{children}
|
||||
</span>
|
||||
})
|
||||
SliceContent.displayName = 'SliceContent'
|
||||
|
||||
export type SliceDividerProps = ComponentProps<'span'>
|
||||
|
||||
export const SliceDivider: FC<SliceDividerProps> = forwardRef((props, ref) => {
|
||||
const { className, ...rest } = props
|
||||
return <span {...rest} ref={ref} className={classNames(
|
||||
baseStyle,
|
||||
'bg-state-base-active group-hover:bg-state-accent-solid text-sm px-[1px]',
|
||||
className,
|
||||
)}>
|
||||
{/* use a zero-width space to make the hover area bigger */}
|
||||
​
|
||||
</span>
|
||||
})
|
||||
SliceDivider.displayName = 'SliceDivider'
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
|
||||
export type SliceProps<T = {}> = T & {
|
||||
text: string
|
||||
} & ComponentProps<'span'>
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { ComponentProps, FC } from 'react'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
export type FormattedTextProps = ComponentProps<'p'>
|
||||
|
||||
export const FormattedText: FC<FormattedTextProps> = (props) => {
|
||||
const { className, ...rest } = props
|
||||
return <p
|
||||
{...rest}
|
||||
className={classNames('leading-7', className)}
|
||||
>{props.children}</p>
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { SliceContent } from '../../formatted-text/flavours/shared'
|
||||
import Score from './score'
|
||||
import type { HitTestingChildChunk } from '@/models/datasets'
|
||||
|
||||
type Props = {
|
||||
payload: HitTestingChildChunk
|
||||
isShowAll: boolean
|
||||
}
|
||||
|
||||
const ChildChunks: FC<Props> = ({
|
||||
payload,
|
||||
isShowAll,
|
||||
}) => {
|
||||
const { id, score, content, position } = payload
|
||||
return (
|
||||
<div
|
||||
className={!isShowAll ? 'line-clamp-2' : ''}
|
||||
>
|
||||
<div className='inline-flex items-center relative top-[-2px]'>
|
||||
<div className='flex items-center h-[20.5px] bg-state-accent-solid system-2xs-semibold-uppercase text-text-primary-on-surface px-1'>C-{position}</div>
|
||||
<Score value={score} besideChunkName />
|
||||
</div>
|
||||
<SliceContent className='py-0.5 bg-state-accent-hover group-hover:bg-state-accent-hover text-sm text-text-secondary font-normal'>{content}</SliceContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ChildChunks)
|
||||
@@ -1,89 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SegmentIndexTag } from '../../documents/detail/completed/common/segment-index-tag'
|
||||
import Dot from '../../documents/detail/completed/common/dot'
|
||||
import Score from './score'
|
||||
import ChildChunksItem from './child-chunks-item'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import type { HitTesting } from '@/models/datasets'
|
||||
import FileIcon from '@/app/components/base/file-uploader/file-type-icon'
|
||||
import type { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import Tag from '@/app/components/datasets/documents/detail/completed/common/tag'
|
||||
|
||||
const i18nPrefix = 'datasetHitTesting'
|
||||
|
||||
type Props = {
|
||||
payload: HitTesting
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
const ChunkDetailModal: FC<Props> = ({
|
||||
payload,
|
||||
onHide,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { segment, score, child_chunks } = payload
|
||||
const { position, content, keywords, document } = segment
|
||||
const isParentChildRetrieval = !!(child_chunks && child_chunks.length > 0)
|
||||
const extension = document.name.split('.').slice(-1)[0] as FileAppearanceTypeEnum
|
||||
const heighClassName = isParentChildRetrieval ? 'h-[min(627px,_80vh)] overflow-y-auto' : 'h-[min(539px,_80vh)] overflow-y-auto'
|
||||
return (
|
||||
<Modal
|
||||
title={t(`${i18nPrefix}.chunkDetail`)}
|
||||
isShow
|
||||
closable
|
||||
onClose={onHide}
|
||||
className={cn(isParentChildRetrieval ? '!min-w-[1200px]' : '!min-w-[800px]')}
|
||||
>
|
||||
<div className='mt-4 flex'>
|
||||
<div className={cn('flex-1', isParentChildRetrieval && 'pr-6')}>
|
||||
{/* Meta info */}
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='grow flex items-center space-x-2'>
|
||||
<SegmentIndexTag
|
||||
labelPrefix={`${isParentChildRetrieval ? 'Parent-' : ''}Chunk`}
|
||||
positionId={position}
|
||||
className={cn('w-fit group-hover:opacity-100')}
|
||||
/>
|
||||
<Dot />
|
||||
<div className='grow flex items-center space-x-1'>
|
||||
<FileIcon type={extension} size='sm' />
|
||||
<span className='grow w-0 truncate text-text-secondary text-[13px] font-normal'>{document.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Score value={score} />
|
||||
</div>
|
||||
<div className={cn('mt-2 body-md-regular text-text-secondary', heighClassName)}>
|
||||
{content}
|
||||
</div>
|
||||
{!isParentChildRetrieval && keywords && keywords.length > 0 && (
|
||||
<div className='mt-6'>
|
||||
<div className='font-medium text-xs text-text-tertiary uppercase'>{t(`${i18nPrefix}.keyword`)}</div>
|
||||
<div className='mt-1 flex flex-wrap'>
|
||||
{keywords.map(keyword => (
|
||||
<Tag key={keyword} text={keyword} className='mr-2' />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isParentChildRetrieval && (
|
||||
<div className='flex-1 pl-6 pb-6'>
|
||||
<div className='system-xs-semibold-uppercase text-text-secondary'>{t(`${i18nPrefix}.hitChunks`, { num: child_chunks.length })}</div>
|
||||
<div className={cn('mt-1 space-y-2', heighClassName)}>
|
||||
{child_chunks.map(item => (
|
||||
<ChildChunksItem key={item.id} payload={item} isShowAll />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ChunkDetailModal)
|
||||
@@ -1,121 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine, RiArrowRightUpLine } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { SegmentIndexTag } from '../../documents/detail/completed/common/segment-index-tag'
|
||||
import Dot from '../../documents/detail/completed/common/dot'
|
||||
import Score from './score'
|
||||
import ChildChunkItem from './child-chunks-item'
|
||||
import ChunkDetailModal from './chunk-detail-modal'
|
||||
import type { HitTesting } from '@/models/datasets'
|
||||
import cn from '@/utils/classnames'
|
||||
import FileIcon from '@/app/components/base/file-uploader/file-type-icon'
|
||||
import type { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
|
||||
import Tag from '@/app/components/datasets/documents/detail/completed/common/tag'
|
||||
import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils/extension-to-file-type'
|
||||
|
||||
const i18nPrefix = 'datasetHitTesting'
|
||||
type Props = {
|
||||
isExternal: boolean
|
||||
payload: HitTesting
|
||||
}
|
||||
|
||||
const ResultItem: FC<Props> = ({
|
||||
isExternal,
|
||||
payload,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { segment, content: externalContent, score, child_chunks } = payload
|
||||
const data = isExternal ? externalContent : segment
|
||||
const { position, word_count, content, keywords, document } = data
|
||||
const isParentChildRetrieval = !!(child_chunks && child_chunks.length > 0)
|
||||
const extension = document.name.split('.').slice(-1)[0] as FileAppearanceTypeEnum
|
||||
const fileType = extensionToFileType(extension)
|
||||
const [isFold, {
|
||||
toggle: toggleFold,
|
||||
}] = useBoolean(false)
|
||||
const Icon = isFold ? RiArrowRightSLine : RiArrowDownSLine
|
||||
|
||||
const [isShowDetailModal, {
|
||||
setTrue: showDetailModal,
|
||||
setFalse: hideDetailModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleClickCard = () => {
|
||||
if (!isParentChildRetrieval)
|
||||
showDetailModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('pt-3 bg-chat-bubble-bg rounded-xl hover:shadow-lg', !isParentChildRetrieval && 'cursor-pointer')} onClick={handleClickCard}>
|
||||
{/* Meta info */}
|
||||
<div className='flex justify-between items-center px-3'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<SegmentIndexTag
|
||||
labelPrefix={`${isParentChildRetrieval ? 'Parent-' : ''}Chunk`}
|
||||
positionId={position}
|
||||
className={cn('w-fit group-hover:opacity-100')}
|
||||
/>
|
||||
<Dot />
|
||||
<div className='system-xs-medium text-text-tertiary'>{word_count} {t('datasetDocuments.segment.characters', { count: word_count })}</div>
|
||||
</div>
|
||||
<Score value={score} />
|
||||
</div>
|
||||
|
||||
{/* Main */}
|
||||
<div className='mt-1 px-3'>
|
||||
<div className='line-clamp-2 body-md-regular'>{content}</div>
|
||||
{isParentChildRetrieval && (
|
||||
<div className='mt-1'>
|
||||
<div className={cn('inline-flex items-center h-6 space-x-0.5 text-text-secondary select-none rounded-lg cursor-pointer', isFold && 'pl-1 bg-[linear-gradient(90deg,_rgba(200,_206,_218,_0.20)_0%,_rgba(200,_206,_218,_0.04)_100%)]')} onClick={toggleFold}>
|
||||
<Icon className={cn('w-4 h-4', isFold && 'opacity-50')} />
|
||||
<div className='text-xs font-semibold uppercase'>{t(`${i18nPrefix}.hitChunks`, { num: child_chunks.length })}</div>
|
||||
</div>
|
||||
{!isFold && (
|
||||
<div className='space-y-2'>
|
||||
{child_chunks.map(item => (
|
||||
<div key={item.id} className='ml-[7px] pl-[7px] border-l-[2px] border-text-accent-secondary'>
|
||||
<ChildChunkItem payload={item} isShowAll={false} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isParentChildRetrieval && keywords && keywords.length > 0 && (
|
||||
<div className='mt-2 flex flex-wrap'>
|
||||
{keywords.map(keyword => (
|
||||
<Tag key={keyword} text={keyword} className='mr-2' />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Foot */}
|
||||
<div className='mt-3 flex justify-between items-center h-10 pl-3 pr-2 border-t border-divider-subtle'>
|
||||
<div className='grow flex items-center space-x-1'>
|
||||
<FileIcon type={fileType} size='sm' />
|
||||
<span className='grow w-0 truncate text-text-secondary text-[13px] font-normal'>{document.name}</span>
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center space-x-1 cursor-pointer text-text-tertiary'
|
||||
onClick={showDetailModal}
|
||||
>
|
||||
<div className='text-xs uppercase'>{t(`${i18nPrefix}.open`)}</div>
|
||||
<RiArrowRightUpLine className='size-3.5' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
isShowDetailModal && (
|
||||
<ChunkDetailModal
|
||||
payload={payload}
|
||||
onHide={hideDetailModal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default React.memo(ResultItem)
|
||||