Revert "Feat/parent child retrieval" (#12095)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user