feat: enhance segment management by adding new segment mutation and improving UI layout

This commit is contained in:
twwu 2024-12-17 10:13:53 +08:00
parent 493ec06e95
commit 9006a744b9
6 changed files with 69 additions and 154 deletions

View File

@ -1,98 +0,0 @@
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

View File

@ -50,6 +50,7 @@ const ChunkContent: FC<IChunkContentProps> = ({
return (
<AutoHeightTextarea
outerClassName='mb-6'
className='body-md-regular text-text-secondary tracking-[-0.07px] caret-[#295EFF]'
value={question}
placeholder={t('datasetDocuments.segment.contentPlaceholder') || ''}

View File

@ -24,7 +24,6 @@ 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 { updateSegment } from '@/service/datasets'
import { type ChildChunkDetail, ChuckingMode, type SegmentDetailModel, type SegmentUpdater } from '@/models/datasets'
import NewSegment from '@/app/components/datasets/documents/detail/new-segment'
import { useEventEmitterContextContext } from '@/context/event-emitter'
@ -39,6 +38,7 @@ import {
useSegmentList,
useSegmentListKey,
useUpdateChildSegment,
useUpdateSegment,
} from '@/service/knowledge/use-segment'
import { useInvalid } from '@/service/use-base'
@ -244,6 +244,8 @@ const Completed: FC<ICompletedProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [datasetId, documentId, selectedSegmentIds])
const { mutateAsync: updateSegment } = useUpdateSegment()
const handleUpdateSegment = useCallback(async (
segmentId: string,
question: string,
@ -274,30 +276,31 @@ const Completed: FC<ICompletedProps> = ({
if (needRegenerate)
params.regenerate_child_chunks = needRegenerate
try {
eventEmitter?.emit('update-segment')
const res = await updateSegment({ datasetId, documentId, segmentId, body: params })
await updateSegment({ datasetId, documentId, segmentId, body: params }, {
onSuccess(data) {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
if (!needRegenerate)
onCloseSegmentDetail()
for (const seg of segments) {
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.enabled = res.data.enabled
seg.updated_at = res.data.updated_at
seg.child_chunks = res.data.child_chunks
seg.answer = data.data.answer
seg.content = data.data.content
seg.keywords = data.data.keywords
seg.word_count = data.data.word_count
seg.hit_count = data.data.hit_count
seg.enabled = data.data.enabled
seg.updated_at = data.data.updated_at
seg.child_chunks = data.data.child_chunks
}
}
setSegments([...segments])
eventEmitter?.emit('update-segment-success')
}
finally {
},
onSettled() {
eventEmitter?.emit('update-segment-done')
}
},
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [segments, datasetId, documentId])

View File

@ -16,10 +16,10 @@ import { useDocumentContext } from './index'
import { useStore as useAppStore } from '@/app/components/app/store'
import { ToastContext } from '@/app/components/base/toast'
import { ChuckingMode, type SegmentUpdater } from '@/models/datasets'
import { addSegment } from '@/service/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
@ -71,6 +71,8 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
setKeywords([])
}
const { mutateAsync: addSegment } = useAddSegment()
const handleSave = async () => {
const params: SegmentUpdater = { content: '' }
if (isQAModel) {
@ -105,8 +107,8 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
params.keywords = keywords
setLoading(true)
try {
await addSegment({ datasetId, documentId, body: params })
await addSegment({ datasetId, documentId, body: params }, {
onSuccess() {
notify({
type: 'success',
message: t('datasetDocuments.segment.chunkAdded'),
@ -118,10 +120,11 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
refreshTimer.current = setTimeout(() => {
onSave()
}, 3000)
}
finally {
},
onSettled() {
setLoading(false)
}
},
})
}
const wordCountText = useMemo(() => {

View File

@ -23,8 +23,6 @@ import type {
IndexingStatusResponse,
ProcessRuleResponse,
RelatedAppResponse,
SegmentDetailModel,
SegmentUpdater,
createDocumentResponse,
} from '@/models/datasets'
import type { CreateKnowledgeBaseReq } from '@/app/components/datasets/external-knowledge-base/create/declarations'
@ -178,18 +176,6 @@ export const modifyDocMetadata: Fetcher<CommonResponse, CommonDocReq & { body: {
}
// apis for segments in a document
export const updateSegment: Fetcher<{ data: SegmentDetailModel; doc_form: string }, { datasetId: string; documentId: string; segmentId: string; body: SegmentUpdater }> = ({ datasetId, documentId, segmentId, body }) => {
return patch<{ data: SegmentDetailModel; doc_form: string }>(`/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`, { body })
}
export const addSegment: Fetcher<{ data: SegmentDetailModel; doc_form: string }, { datasetId: string; documentId: string; body: SegmentUpdater }> = ({ datasetId, documentId, body }) => {
return post<{ data: SegmentDetailModel; doc_form: string }>(`/datasets/${datasetId}/documents/${documentId}/segment`, { body })
}
export const deleteSegment: Fetcher<CommonResponse, { datasetId: string; documentId: string; segmentId: string }> = ({ datasetId, documentId, segmentId }) => {
return del<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`)
}
export const segmentBatchImport: Fetcher<{ job_id: string; job_status: string }, { url: string; body: FormData }> = ({ url, body }) => {
return post<{ job_id: string; job_status: string }>(url, { body }, { bodyStringify: false, deleteContentType: true })
}

View File

@ -1,7 +1,7 @@
import { useMutation, useQuery } from '@tanstack/react-query'
import { del, get, patch, post } from '../base'
import type { CommonResponse } from '@/models/common'
import type { ChildChunkDetail, ChildSegmentsResponse, SegmentsResponse } from '@/models/datasets'
import type { ChildChunkDetail, ChildSegmentsResponse, ChuckingMode, SegmentDetailModel, SegmentUpdater, SegmentsResponse } from '@/models/datasets'
const NAME_SPACE = 'segment'
@ -31,6 +31,26 @@ export const useSegmentList = (
})
}
export const useUpdateSegment = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'update'],
mutationFn: (payload: { datasetId: string; documentId: string; segmentId: string; body: SegmentUpdater }) => {
const { datasetId, documentId, segmentId, body } = payload
return patch<{ data: SegmentDetailModel; doc_form: ChuckingMode }>(`/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`, { body })
},
})
}
export const useAddSegment = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'add'],
mutationFn: (payload: { datasetId: string; documentId: string; body: SegmentUpdater }) => {
const { datasetId, documentId, body } = payload
return post<{ data: SegmentDetailModel; doc_form: ChuckingMode }>(`/datasets/${datasetId}/documents/${documentId}/segment`, { body })
},
})
}
export const useEnableSegment = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'enable'],