mirror of
https://github.com/langgenius/dify.git
synced 2026-03-25 16:28:04 +08:00
refactor: restructure Completed component and introduce new hooks for improved state management
- Refactored the Completed component to enhance readability and maintainability. - Introduced new hooks for managing modal states, search filters, and segment selection. - Created a context for segment list management to streamline state sharing. - Added new components for better organization of UI elements, including MenuBar and DrawerGroup. - Improved pagination and data fetching logic for segment and child segment lists. - Enhanced overall user experience with better state handling and UI responsiveness.
This commit is contained in:
@ -0,0 +1,151 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { FileEntity } from '@/app/components/datasets/common/image-uploader/types'
|
||||
import type { ChildChunkDetail, ChunkingMode, SegmentDetailModel } from '@/models/datasets'
|
||||
import NewSegment from '@/app/components/datasets/documents/detail/new-segment'
|
||||
import ChildSegmentDetail from '../child-segment-detail'
|
||||
import FullScreenDrawer from '../common/full-screen-drawer'
|
||||
import NewChildSegment from '../new-child-segment'
|
||||
import SegmentDetail from '../segment-detail'
|
||||
|
||||
type DrawerGroupProps = {
|
||||
// Segment detail drawer
|
||||
currSegment: {
|
||||
segInfo?: SegmentDetailModel
|
||||
showModal: boolean
|
||||
isEditMode?: boolean
|
||||
}
|
||||
onCloseSegmentDetail: () => void
|
||||
onUpdateSegment: (
|
||||
segmentId: string,
|
||||
question: string,
|
||||
answer: string,
|
||||
keywords: string[],
|
||||
attachments: FileEntity[],
|
||||
needRegenerate?: boolean,
|
||||
) => Promise<void>
|
||||
isRegenerationModalOpen: boolean
|
||||
setIsRegenerationModalOpen: (open: boolean) => void
|
||||
// New segment drawer
|
||||
showNewSegmentModal: boolean
|
||||
onCloseNewSegmentModal: () => void
|
||||
onSaveNewSegment: () => void
|
||||
viewNewlyAddedChunk: () => void
|
||||
// Child segment detail drawer
|
||||
currChildChunk: {
|
||||
childChunkInfo?: ChildChunkDetail
|
||||
showModal: boolean
|
||||
}
|
||||
currChunkId: string
|
||||
onCloseChildSegmentDetail: () => void
|
||||
onUpdateChildChunk: (segmentId: string, childChunkId: string, content: string) => Promise<void>
|
||||
// New child segment drawer
|
||||
showNewChildSegmentModal: boolean
|
||||
onCloseNewChildChunkModal: () => void
|
||||
onSaveNewChildChunk: (newChildChunk?: ChildChunkDetail) => void
|
||||
viewNewlyAddedChildChunk: () => void
|
||||
// Common props
|
||||
fullScreen: boolean
|
||||
docForm: ChunkingMode
|
||||
}
|
||||
|
||||
const DrawerGroup: FC<DrawerGroupProps> = ({
|
||||
// Segment detail drawer
|
||||
currSegment,
|
||||
onCloseSegmentDetail,
|
||||
onUpdateSegment,
|
||||
isRegenerationModalOpen,
|
||||
setIsRegenerationModalOpen,
|
||||
// New segment drawer
|
||||
showNewSegmentModal,
|
||||
onCloseNewSegmentModal,
|
||||
onSaveNewSegment,
|
||||
viewNewlyAddedChunk,
|
||||
// Child segment detail drawer
|
||||
currChildChunk,
|
||||
currChunkId,
|
||||
onCloseChildSegmentDetail,
|
||||
onUpdateChildChunk,
|
||||
// New child segment drawer
|
||||
showNewChildSegmentModal,
|
||||
onCloseNewChildChunkModal,
|
||||
onSaveNewChildChunk,
|
||||
viewNewlyAddedChildChunk,
|
||||
// Common props
|
||||
fullScreen,
|
||||
docForm,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{/* Edit or view segment detail */}
|
||||
<FullScreenDrawer
|
||||
isOpen={currSegment.showModal}
|
||||
fullScreen={fullScreen}
|
||||
onClose={onCloseSegmentDetail}
|
||||
showOverlay={false}
|
||||
needCheckChunks
|
||||
modal={isRegenerationModalOpen}
|
||||
>
|
||||
<SegmentDetail
|
||||
key={currSegment.segInfo?.id}
|
||||
segInfo={currSegment.segInfo ?? { id: '' }}
|
||||
docForm={docForm}
|
||||
isEditMode={currSegment.isEditMode}
|
||||
onUpdate={onUpdateSegment}
|
||||
onCancel={onCloseSegmentDetail}
|
||||
onModalStateChange={setIsRegenerationModalOpen}
|
||||
/>
|
||||
</FullScreenDrawer>
|
||||
|
||||
{/* Create New Segment */}
|
||||
<FullScreenDrawer
|
||||
isOpen={showNewSegmentModal}
|
||||
fullScreen={fullScreen}
|
||||
onClose={onCloseNewSegmentModal}
|
||||
modal
|
||||
>
|
||||
<NewSegment
|
||||
docForm={docForm}
|
||||
onCancel={onCloseNewSegmentModal}
|
||||
onSave={onSaveNewSegment}
|
||||
viewNewlyAddedChunk={viewNewlyAddedChunk}
|
||||
/>
|
||||
</FullScreenDrawer>
|
||||
|
||||
{/* Edit or view child segment detail */}
|
||||
<FullScreenDrawer
|
||||
isOpen={currChildChunk.showModal}
|
||||
fullScreen={fullScreen}
|
||||
onClose={onCloseChildSegmentDetail}
|
||||
showOverlay={false}
|
||||
needCheckChunks
|
||||
>
|
||||
<ChildSegmentDetail
|
||||
key={currChildChunk.childChunkInfo?.id}
|
||||
chunkId={currChunkId}
|
||||
childChunkInfo={currChildChunk.childChunkInfo ?? { id: '' }}
|
||||
docForm={docForm}
|
||||
onUpdate={onUpdateChildChunk}
|
||||
onCancel={onCloseChildSegmentDetail}
|
||||
/>
|
||||
</FullScreenDrawer>
|
||||
|
||||
{/* Create New Child Segment */}
|
||||
<FullScreenDrawer
|
||||
isOpen={showNewChildSegmentModal}
|
||||
fullScreen={fullScreen}
|
||||
onClose={onCloseNewChildChunkModal}
|
||||
modal
|
||||
>
|
||||
<NewChildSegment
|
||||
chunkId={currChunkId}
|
||||
onCancel={onCloseNewChildChunkModal}
|
||||
onSave={onSaveNewChildChunk}
|
||||
viewNewlyAddedChildChunk={viewNewlyAddedChildChunk}
|
||||
/>
|
||||
</FullScreenDrawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DrawerGroup
|
||||
@ -0,0 +1,3 @@
|
||||
export { default as DrawerGroup } from './drawer-group'
|
||||
export { default as MenuBar } from './menu-bar'
|
||||
export { FullDocModeContent, GeneralModeContent } from './segment-list-content'
|
||||
@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import DisplayToggle from '../display-toggle'
|
||||
import StatusItem from '../status-item'
|
||||
import s from '../style.module.css'
|
||||
|
||||
type MenuBarProps = {
|
||||
isAllSelected: boolean
|
||||
isSomeSelected: boolean
|
||||
onSelectedAll: () => void
|
||||
isLoading: boolean
|
||||
totalText: string
|
||||
statusList: Item[]
|
||||
selectDefaultValue: 'all' | 0 | 1
|
||||
onChangeStatus: (item: Item) => void
|
||||
inputValue: string
|
||||
onInputChange: (value: string) => void
|
||||
isCollapsed: boolean
|
||||
toggleCollapsed: () => void
|
||||
}
|
||||
|
||||
const MenuBar: FC<MenuBarProps> = ({
|
||||
isAllSelected,
|
||||
isSomeSelected,
|
||||
onSelectedAll,
|
||||
isLoading,
|
||||
totalText,
|
||||
statusList,
|
||||
selectDefaultValue,
|
||||
onChangeStatus,
|
||||
inputValue,
|
||||
onInputChange,
|
||||
isCollapsed,
|
||||
toggleCollapsed,
|
||||
}) => {
|
||||
return (
|
||||
<div className={s.docSearchWrapper}>
|
||||
<Checkbox
|
||||
className="shrink-0"
|
||||
checked={isAllSelected}
|
||||
indeterminate={!isAllSelected && isSomeSelected}
|
||||
onCheck={onSelectedAll}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<div className="system-sm-semibold-uppercase flex-1 pl-5 text-text-secondary">{totalText}</div>
|
||||
<SimpleSelect
|
||||
onSelect={onChangeStatus}
|
||||
items={statusList}
|
||||
defaultValue={selectDefaultValue}
|
||||
className={s.select}
|
||||
wrapperClassName="h-fit mr-2"
|
||||
optionWrapClassName="w-[160px]"
|
||||
optionClassName="p-0"
|
||||
renderOption={({ item, selected }) => <StatusItem item={item} selected={selected} />}
|
||||
notClearable
|
||||
/>
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
wrapperClassName="!w-52"
|
||||
value={inputValue}
|
||||
onChange={e => onInputChange(e.target.value)}
|
||||
onClear={() => onInputChange('')}
|
||||
/>
|
||||
<Divider type="vertical" className="mx-3 h-3.5" />
|
||||
<DisplayToggle isCollapsed={isCollapsed} toggleCollapsed={toggleCollapsed} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MenuBar
|
||||
@ -0,0 +1,127 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ChildSegmentList from '../child-segment-list'
|
||||
import SegmentCard from '../segment-card'
|
||||
import SegmentList from '../segment-list'
|
||||
|
||||
type FullDocModeContentProps = {
|
||||
segments: SegmentDetailModel[]
|
||||
childSegments: ChildChunkDetail[]
|
||||
isLoadingSegmentList: boolean
|
||||
isLoadingChildSegmentList: boolean
|
||||
currSegmentId?: string
|
||||
onClickCard: (detail: SegmentDetailModel, isEditMode?: boolean) => void
|
||||
onDeleteChildChunk: (segmentId: string, childChunkId: string) => Promise<void>
|
||||
handleInputChange: (value: string) => void
|
||||
handleAddNewChildChunk: (parentChunkId: string) => void
|
||||
onClickSlice: (detail: ChildChunkDetail) => void
|
||||
archived?: boolean
|
||||
childChunkTotal: number
|
||||
inputValue: string
|
||||
onClearFilter: () => void
|
||||
}
|
||||
|
||||
export const FullDocModeContent: FC<FullDocModeContentProps> = ({
|
||||
segments,
|
||||
childSegments,
|
||||
isLoadingSegmentList,
|
||||
isLoadingChildSegmentList,
|
||||
currSegmentId,
|
||||
onClickCard,
|
||||
onDeleteChildChunk,
|
||||
handleInputChange,
|
||||
handleAddNewChildChunk,
|
||||
onClickSlice,
|
||||
archived,
|
||||
childChunkTotal,
|
||||
inputValue,
|
||||
onClearFilter,
|
||||
}) => {
|
||||
const firstSegment = segments[0]
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex grow flex-col overflow-x-hidden',
|
||||
(isLoadingSegmentList || isLoadingChildSegmentList) ? 'overflow-y-hidden' : 'overflow-y-auto',
|
||||
)}
|
||||
>
|
||||
<SegmentCard
|
||||
detail={firstSegment}
|
||||
onClick={() => onClickCard(firstSegment)}
|
||||
loading={isLoadingSegmentList}
|
||||
focused={{
|
||||
segmentIndex: currSegmentId === firstSegment?.id,
|
||||
segmentContent: currSegmentId === firstSegment?.id,
|
||||
}}
|
||||
/>
|
||||
<ChildSegmentList
|
||||
parentChunkId={firstSegment?.id}
|
||||
onDelete={onDeleteChildChunk}
|
||||
childChunks={childSegments}
|
||||
handleInputChange={handleInputChange}
|
||||
handleAddNewChildChunk={handleAddNewChildChunk}
|
||||
onClickSlice={onClickSlice}
|
||||
enabled={!archived}
|
||||
total={childChunkTotal}
|
||||
inputValue={inputValue}
|
||||
onClearFilter={onClearFilter}
|
||||
isLoading={isLoadingSegmentList || isLoadingChildSegmentList}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type GeneralModeContentProps = {
|
||||
segmentListRef: React.RefObject<HTMLDivElement | null>
|
||||
embeddingAvailable: boolean
|
||||
isLoadingSegmentList: boolean
|
||||
segments: SegmentDetailModel[]
|
||||
selectedSegmentIds: string[]
|
||||
onSelected: (segId: string) => void
|
||||
onChangeSwitch: (enable: boolean, segId?: string) => Promise<void>
|
||||
onDelete: (segId?: string) => Promise<void>
|
||||
onClickCard: (detail: SegmentDetailModel, isEditMode?: boolean) => void
|
||||
archived?: boolean
|
||||
onDeleteChildChunk: (segmentId: string, childChunkId: string) => Promise<void>
|
||||
handleAddNewChildChunk: (parentChunkId: string) => void
|
||||
onClickSlice: (detail: ChildChunkDetail) => void
|
||||
onClearFilter: () => void
|
||||
}
|
||||
|
||||
export const GeneralModeContent: FC<GeneralModeContentProps> = ({
|
||||
segmentListRef,
|
||||
embeddingAvailable,
|
||||
isLoadingSegmentList,
|
||||
segments,
|
||||
selectedSegmentIds,
|
||||
onSelected,
|
||||
onChangeSwitch,
|
||||
onDelete,
|
||||
onClickCard,
|
||||
archived,
|
||||
onDeleteChildChunk,
|
||||
handleAddNewChildChunk,
|
||||
onClickSlice,
|
||||
onClearFilter,
|
||||
}) => {
|
||||
return (
|
||||
<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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
export { useChildSegmentData } from './use-child-segment-data'
|
||||
export type { UseChildSegmentDataReturn } from './use-child-segment-data'
|
||||
|
||||
export { useModalState } from './use-modal-state'
|
||||
export type { CurrChildChunkType, CurrSegmentType, UseModalStateReturn } from './use-modal-state'
|
||||
|
||||
export { useSearchFilter } from './use-search-filter'
|
||||
export type { UseSearchFilterReturn } from './use-search-filter'
|
||||
|
||||
export { useSegmentListData } from './use-segment-list-data'
|
||||
export type { UseSegmentListDataReturn } from './use-segment-list-data'
|
||||
|
||||
export { useSegmentSelection } from './use-segment-selection'
|
||||
export type { UseSegmentSelectionReturn } from './use-segment-selection'
|
||||
@ -0,0 +1,241 @@
|
||||
import type { ChildChunkDetail, ChildSegmentsResponse, SegmentDetailModel, SegmentUpdater } from '@/models/datasets'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import {
|
||||
useChildSegmentList,
|
||||
useChildSegmentListKey,
|
||||
useDeleteChildSegment,
|
||||
useUpdateChildSegment,
|
||||
} from '@/service/knowledge/use-segment'
|
||||
import { useInvalid } from '@/service/use-base'
|
||||
import { useDocumentContext } from '../../context'
|
||||
|
||||
export type UseChildSegmentDataOptions = {
|
||||
searchValue: string
|
||||
currentPage: number
|
||||
limit: number
|
||||
segments: SegmentDetailModel[]
|
||||
currChunkId: string
|
||||
isFullDocMode: boolean
|
||||
onCloseChildSegmentDetail: () => void
|
||||
refreshChunkListDataWithDetailChanged: () => void
|
||||
updateSegmentInCache: (segmentId: string, updater: (seg: SegmentDetailModel) => SegmentDetailModel) => void
|
||||
}
|
||||
|
||||
export type UseChildSegmentDataReturn = {
|
||||
childSegments: ChildChunkDetail[]
|
||||
isLoadingChildSegmentList: boolean
|
||||
childChunkListData: ReturnType<typeof useChildSegmentList>['data']
|
||||
childSegmentListRef: React.RefObject<HTMLDivElement | null>
|
||||
needScrollToBottom: React.RefObject<boolean>
|
||||
// Operations
|
||||
onDeleteChildChunk: (segmentId: string, childChunkId: string) => Promise<void>
|
||||
handleUpdateChildChunk: (segmentId: string, childChunkId: string, content: string) => Promise<void>
|
||||
onSaveNewChildChunk: (newChildChunk?: ChildChunkDetail) => void
|
||||
resetChildList: () => void
|
||||
viewNewlyAddedChildChunk: () => void
|
||||
}
|
||||
|
||||
export const useChildSegmentData = (options: UseChildSegmentDataOptions): UseChildSegmentDataReturn => {
|
||||
const {
|
||||
searchValue,
|
||||
currentPage,
|
||||
limit,
|
||||
segments,
|
||||
currChunkId,
|
||||
isFullDocMode,
|
||||
onCloseChildSegmentDetail,
|
||||
refreshChunkListDataWithDetailChanged,
|
||||
updateSegmentInCache,
|
||||
} = options
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const datasetId = useDocumentContext(s => s.datasetId) || ''
|
||||
const documentId = useDocumentContext(s => s.documentId) || ''
|
||||
const parentMode = useDocumentContext(s => s.parentMode)
|
||||
|
||||
const childSegmentListRef = useRef<HTMLDivElement>(null)
|
||||
const needScrollToBottom = useRef(false)
|
||||
|
||||
// Build query params
|
||||
const queryParams = useMemo(() => ({
|
||||
page: currentPage === 0 ? 1 : currentPage,
|
||||
limit,
|
||||
keyword: searchValue,
|
||||
}), [currentPage, limit, searchValue])
|
||||
|
||||
const segmentId = segments[0]?.id || ''
|
||||
|
||||
// Build query key for optimistic updates
|
||||
const currentQueryKey = useMemo(() =>
|
||||
[...useChildSegmentListKey, datasetId, documentId, segmentId, queryParams], [datasetId, documentId, segmentId, queryParams])
|
||||
|
||||
// Fetch child segment list
|
||||
const { isLoading: isLoadingChildSegmentList, data: childChunkListData } = useChildSegmentList(
|
||||
{
|
||||
datasetId,
|
||||
documentId,
|
||||
segmentId,
|
||||
params: queryParams,
|
||||
},
|
||||
!isFullDocMode || segments.length === 0,
|
||||
)
|
||||
|
||||
// Derive child segments from query data
|
||||
const childSegments = useMemo(() => childChunkListData?.data || [], [childChunkListData])
|
||||
|
||||
const invalidChildSegmentList = useInvalid(useChildSegmentListKey)
|
||||
|
||||
// Scroll to bottom when child segments change
|
||||
useEffect(() => {
|
||||
if (childSegmentListRef.current && needScrollToBottom.current) {
|
||||
childSegmentListRef.current.scrollTo({ top: childSegmentListRef.current.scrollHeight, behavior: 'smooth' })
|
||||
needScrollToBottom.current = false
|
||||
}
|
||||
}, [childSegments])
|
||||
|
||||
const resetChildList = useCallback(() => {
|
||||
invalidChildSegmentList()
|
||||
}, [invalidChildSegmentList])
|
||||
|
||||
// Optimistic update helper for child segments
|
||||
const updateChildSegmentInCache = useCallback((
|
||||
childChunkId: string,
|
||||
updater: (chunk: ChildChunkDetail) => ChildChunkDetail,
|
||||
) => {
|
||||
queryClient.setQueryData<ChildSegmentsResponse>(currentQueryKey, (old) => {
|
||||
if (!old)
|
||||
return old
|
||||
return {
|
||||
...old,
|
||||
data: old.data.map(chunk => chunk.id === childChunkId ? updater(chunk) : chunk),
|
||||
}
|
||||
})
|
||||
}, [queryClient, currentQueryKey])
|
||||
|
||||
// Mutations
|
||||
const { mutateAsync: deleteChildSegment } = useDeleteChildSegment()
|
||||
const { mutateAsync: updateChildSegment } = useUpdateChildSegment()
|
||||
|
||||
const onDeleteChildChunk = useCallback(async (segmentIdParam: string, childChunkId: string) => {
|
||||
await deleteChildSegment(
|
||||
{ datasetId, documentId, segmentId: segmentIdParam, childChunkId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
if (parentMode === 'paragraph') {
|
||||
// Update parent segment's child_chunks in cache
|
||||
updateSegmentInCache(segmentIdParam, seg => ({
|
||||
...seg,
|
||||
child_chunks: seg.child_chunks?.filter(chunk => chunk.id !== childChunkId),
|
||||
}))
|
||||
}
|
||||
else {
|
||||
resetChildList()
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
},
|
||||
},
|
||||
)
|
||||
}, [datasetId, documentId, parentMode, deleteChildSegment, updateSegmentInCache, resetChildList, t, notify])
|
||||
|
||||
const handleUpdateChildChunk = useCallback(async (
|
||||
segmentIdParam: string,
|
||||
childChunkId: string,
|
||||
content: string,
|
||||
) => {
|
||||
const params: SegmentUpdater = { content: '' }
|
||||
if (!content.trim()) {
|
||||
notify({ type: 'error', message: t('segment.contentEmpty', { ns: 'datasetDocuments' }) })
|
||||
return
|
||||
}
|
||||
|
||||
params.content = content
|
||||
|
||||
eventEmitter?.emit('update-child-segment')
|
||||
await updateChildSegment({ datasetId, documentId, segmentId: segmentIdParam, childChunkId, body: params }, {
|
||||
onSuccess: (res) => {
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
onCloseChildSegmentDetail()
|
||||
|
||||
if (parentMode === 'paragraph') {
|
||||
// Update parent segment's child_chunks in cache
|
||||
updateSegmentInCache(segmentIdParam, seg => ({
|
||||
...seg,
|
||||
child_chunks: seg.child_chunks?.map(childSeg =>
|
||||
childSeg.id === childChunkId
|
||||
? {
|
||||
...childSeg,
|
||||
content: res.data.content,
|
||||
type: res.data.type,
|
||||
word_count: res.data.word_count,
|
||||
updated_at: res.data.updated_at,
|
||||
}
|
||||
: childSeg,
|
||||
),
|
||||
}))
|
||||
refreshChunkListDataWithDetailChanged()
|
||||
}
|
||||
else {
|
||||
updateChildSegmentInCache(childChunkId, chunk => ({
|
||||
...chunk,
|
||||
content: res.data.content,
|
||||
type: res.data.type,
|
||||
word_count: res.data.word_count,
|
||||
updated_at: res.data.updated_at,
|
||||
}))
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
eventEmitter?.emit('update-child-segment-done')
|
||||
},
|
||||
})
|
||||
}, [datasetId, documentId, parentMode, updateChildSegment, notify, eventEmitter, onCloseChildSegmentDetail, updateSegmentInCache, updateChildSegmentInCache, refreshChunkListDataWithDetailChanged, t])
|
||||
|
||||
const onSaveNewChildChunk = useCallback((newChildChunk?: ChildChunkDetail) => {
|
||||
if (parentMode === 'paragraph') {
|
||||
// Update parent segment's child_chunks in cache
|
||||
updateSegmentInCache(currChunkId, seg => ({
|
||||
...seg,
|
||||
child_chunks: [...(seg.child_chunks || []), newChildChunk!],
|
||||
}))
|
||||
refreshChunkListDataWithDetailChanged()
|
||||
}
|
||||
else {
|
||||
resetChildList()
|
||||
}
|
||||
}, [parentMode, currChunkId, updateSegmentInCache, refreshChunkListDataWithDetailChanged, resetChildList])
|
||||
|
||||
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)
|
||||
return
|
||||
resetChildList()
|
||||
}, [childChunkListData, limit, resetChildList])
|
||||
|
||||
return {
|
||||
childSegments,
|
||||
isLoadingChildSegmentList,
|
||||
childChunkListData,
|
||||
childSegmentListRef,
|
||||
needScrollToBottom,
|
||||
onDeleteChildChunk,
|
||||
handleUpdateChildChunk,
|
||||
onSaveNewChildChunk,
|
||||
resetChildList,
|
||||
viewNewlyAddedChildChunk,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,141 @@
|
||||
import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
export type CurrSegmentType = {
|
||||
segInfo?: SegmentDetailModel
|
||||
showModal: boolean
|
||||
isEditMode?: boolean
|
||||
}
|
||||
|
||||
export type CurrChildChunkType = {
|
||||
childChunkInfo?: ChildChunkDetail
|
||||
showModal: boolean
|
||||
}
|
||||
|
||||
export type UseModalStateReturn = {
|
||||
// Segment detail modal
|
||||
currSegment: CurrSegmentType
|
||||
onClickCard: (detail: SegmentDetailModel, isEditMode?: boolean) => void
|
||||
onCloseSegmentDetail: () => void
|
||||
// Child segment detail modal
|
||||
currChildChunk: CurrChildChunkType
|
||||
currChunkId: string
|
||||
onClickSlice: (detail: ChildChunkDetail) => void
|
||||
onCloseChildSegmentDetail: () => void
|
||||
// New segment modal
|
||||
onCloseNewSegmentModal: () => void
|
||||
// New child segment modal
|
||||
showNewChildSegmentModal: boolean
|
||||
handleAddNewChildChunk: (parentChunkId: string) => void
|
||||
onCloseNewChildChunkModal: () => void
|
||||
// Regeneration modal
|
||||
isRegenerationModalOpen: boolean
|
||||
setIsRegenerationModalOpen: (open: boolean) => void
|
||||
// Full screen
|
||||
fullScreen: boolean
|
||||
toggleFullScreen: () => void
|
||||
setFullScreen: (fullScreen: boolean) => void
|
||||
// Collapsed state
|
||||
isCollapsed: boolean
|
||||
toggleCollapsed: () => void
|
||||
}
|
||||
|
||||
type UseModalStateOptions = {
|
||||
onNewSegmentModalChange: (state: boolean) => void
|
||||
}
|
||||
|
||||
export const useModalState = (options: UseModalStateOptions): UseModalStateReturn => {
|
||||
const { onNewSegmentModalChange } = options
|
||||
|
||||
// Segment detail modal state
|
||||
const [currSegment, setCurrSegment] = useState<CurrSegmentType>({ showModal: false })
|
||||
|
||||
// Child segment detail modal state
|
||||
const [currChildChunk, setCurrChildChunk] = useState<CurrChildChunkType>({ showModal: false })
|
||||
const [currChunkId, setCurrChunkId] = useState('')
|
||||
|
||||
// New child segment modal state
|
||||
const [showNewChildSegmentModal, setShowNewChildSegmentModal] = useState(false)
|
||||
|
||||
// Regeneration modal state
|
||||
const [isRegenerationModalOpen, setIsRegenerationModalOpen] = useState(false)
|
||||
|
||||
// Display state
|
||||
const [fullScreen, setFullScreen] = useState(false)
|
||||
const [isCollapsed, setIsCollapsed] = useState(true)
|
||||
|
||||
// Segment detail handlers
|
||||
const onClickCard = useCallback((detail: SegmentDetailModel, isEditMode = false) => {
|
||||
setCurrSegment({ segInfo: detail, showModal: true, isEditMode })
|
||||
}, [])
|
||||
|
||||
const onCloseSegmentDetail = useCallback(() => {
|
||||
setCurrSegment({ showModal: false })
|
||||
setFullScreen(false)
|
||||
}, [])
|
||||
|
||||
// Child segment detail handlers
|
||||
const onClickSlice = useCallback((detail: ChildChunkDetail) => {
|
||||
setCurrChildChunk({ childChunkInfo: detail, showModal: true })
|
||||
setCurrChunkId(detail.segment_id)
|
||||
}, [])
|
||||
|
||||
const onCloseChildSegmentDetail = useCallback(() => {
|
||||
setCurrChildChunk({ showModal: false })
|
||||
setFullScreen(false)
|
||||
}, [])
|
||||
|
||||
// New segment modal handlers
|
||||
const onCloseNewSegmentModal = useCallback(() => {
|
||||
onNewSegmentModalChange(false)
|
||||
setFullScreen(false)
|
||||
}, [onNewSegmentModalChange])
|
||||
|
||||
// New child segment modal handlers
|
||||
const handleAddNewChildChunk = useCallback((parentChunkId: string) => {
|
||||
setShowNewChildSegmentModal(true)
|
||||
setCurrChunkId(parentChunkId)
|
||||
}, [])
|
||||
|
||||
const onCloseNewChildChunkModal = useCallback(() => {
|
||||
setShowNewChildSegmentModal(false)
|
||||
setFullScreen(false)
|
||||
}, [])
|
||||
|
||||
// Display handlers - handles both direct calls and click events
|
||||
const toggleFullScreen = useCallback(() => {
|
||||
setFullScreen(prev => !prev)
|
||||
}, [])
|
||||
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
setIsCollapsed(prev => !prev)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
// Segment detail modal
|
||||
currSegment,
|
||||
onClickCard,
|
||||
onCloseSegmentDetail,
|
||||
// Child segment detail modal
|
||||
currChildChunk,
|
||||
currChunkId,
|
||||
onClickSlice,
|
||||
onCloseChildSegmentDetail,
|
||||
// New segment modal
|
||||
onCloseNewSegmentModal,
|
||||
// New child segment modal
|
||||
showNewChildSegmentModal,
|
||||
handleAddNewChildChunk,
|
||||
onCloseNewChildChunkModal,
|
||||
// Regeneration modal
|
||||
isRegenerationModalOpen,
|
||||
setIsRegenerationModalOpen,
|
||||
// Full screen
|
||||
fullScreen,
|
||||
toggleFullScreen,
|
||||
setFullScreen,
|
||||
// Collapsed state
|
||||
isCollapsed,
|
||||
toggleCollapsed,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export type SearchFilterState = {
|
||||
inputValue: string
|
||||
searchValue: string
|
||||
selectedStatus: boolean | 'all'
|
||||
}
|
||||
|
||||
export type UseSearchFilterReturn = {
|
||||
inputValue: string
|
||||
searchValue: string
|
||||
selectedStatus: boolean | 'all'
|
||||
statusList: Item[]
|
||||
selectDefaultValue: 'all' | 0 | 1
|
||||
handleInputChange: (value: string) => void
|
||||
onChangeStatus: (item: Item) => void
|
||||
onClearFilter: () => void
|
||||
resetPage: () => void
|
||||
}
|
||||
|
||||
type UseSearchFilterOptions = {
|
||||
onPageChange: (page: number) => void
|
||||
}
|
||||
|
||||
export const useSearchFilter = (options: UseSearchFilterOptions): UseSearchFilterReturn => {
|
||||
const { t } = useTranslation()
|
||||
const { onPageChange } = options
|
||||
|
||||
const [inputValue, setInputValue] = useState<string>('')
|
||||
const [searchValue, setSearchValue] = useState<string>('')
|
||||
const [selectedStatus, setSelectedStatus] = useState<boolean | 'all'>('all')
|
||||
|
||||
const statusList = useRef<Item[]>([
|
||||
{ value: 'all', name: t('list.index.all', { ns: 'datasetDocuments' }) },
|
||||
{ value: 0, name: t('list.status.disabled', { ns: 'datasetDocuments' }) },
|
||||
{ value: 1, name: t('list.status.enabled', { ns: 'datasetDocuments' }) },
|
||||
])
|
||||
|
||||
const { run: handleSearch } = useDebounceFn(() => {
|
||||
setSearchValue(inputValue)
|
||||
onPageChange(1)
|
||||
}, { wait: 500 })
|
||||
|
||||
const handleInputChange = useCallback((value: string) => {
|
||||
setInputValue(value)
|
||||
handleSearch()
|
||||
}, [handleSearch])
|
||||
|
||||
const onChangeStatus = useCallback(({ value }: Item) => {
|
||||
setSelectedStatus(value === 'all' ? 'all' : !!value)
|
||||
onPageChange(1)
|
||||
}, [onPageChange])
|
||||
|
||||
const onClearFilter = useCallback(() => {
|
||||
setInputValue('')
|
||||
setSearchValue('')
|
||||
setSelectedStatus('all')
|
||||
onPageChange(1)
|
||||
}, [onPageChange])
|
||||
|
||||
const resetPage = useCallback(() => {
|
||||
onPageChange(1)
|
||||
}, [onPageChange])
|
||||
|
||||
const selectDefaultValue = useMemo(() => {
|
||||
if (selectedStatus === 'all')
|
||||
return 'all'
|
||||
return selectedStatus ? 1 : 0
|
||||
}, [selectedStatus])
|
||||
|
||||
return {
|
||||
inputValue,
|
||||
searchValue,
|
||||
selectedStatus,
|
||||
statusList: statusList.current,
|
||||
selectDefaultValue,
|
||||
handleInputChange,
|
||||
onChangeStatus,
|
||||
onClearFilter,
|
||||
resetPage,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,363 @@
|
||||
import type { FileEntity } from '@/app/components/datasets/common/image-uploader/types'
|
||||
import type { SegmentDetailModel, SegmentsResponse, SegmentUpdater } from '@/models/datasets'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import {
|
||||
useChunkListAllKey,
|
||||
useChunkListDisabledKey,
|
||||
useChunkListEnabledKey,
|
||||
useDeleteSegment,
|
||||
useDisableSegment,
|
||||
useEnableSegment,
|
||||
useSegmentList,
|
||||
useSegmentListKey,
|
||||
useUpdateSegment,
|
||||
} from '@/service/knowledge/use-segment'
|
||||
import { useInvalid } from '@/service/use-base'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { useDocumentContext } from '../../context'
|
||||
import { ProcessStatus } from '../../segment-add'
|
||||
|
||||
const DEFAULT_LIMIT = 10
|
||||
|
||||
export type UseSegmentListDataOptions = {
|
||||
searchValue: string
|
||||
selectedStatus: boolean | 'all'
|
||||
selectedSegmentIds: string[]
|
||||
importStatus: ProcessStatus | string | undefined
|
||||
currentPage: number
|
||||
limit: number
|
||||
onCloseSegmentDetail: () => void
|
||||
clearSelection: () => void
|
||||
}
|
||||
|
||||
export type UseSegmentListDataReturn = {
|
||||
segments: SegmentDetailModel[]
|
||||
isLoadingSegmentList: boolean
|
||||
segmentListData: ReturnType<typeof useSegmentList>['data']
|
||||
totalText: string
|
||||
isFullDocMode: boolean
|
||||
segmentListRef: React.RefObject<HTMLDivElement | null>
|
||||
needScrollToBottom: React.RefObject<boolean>
|
||||
// Operations
|
||||
onChangeSwitch: (enable: boolean, segId?: string) => Promise<void>
|
||||
onDelete: (segId?: string) => Promise<void>
|
||||
handleUpdateSegment: (
|
||||
segmentId: string,
|
||||
question: string,
|
||||
answer: string,
|
||||
keywords: string[],
|
||||
attachments: FileEntity[],
|
||||
needRegenerate?: boolean,
|
||||
) => Promise<void>
|
||||
resetList: () => void
|
||||
viewNewlyAddedChunk: () => void
|
||||
invalidSegmentList: () => void
|
||||
updateSegmentInCache: (segmentId: string, updater: (seg: SegmentDetailModel) => SegmentDetailModel) => void
|
||||
}
|
||||
|
||||
export const useSegmentListData = (options: UseSegmentListDataOptions): UseSegmentListDataReturn => {
|
||||
const {
|
||||
searchValue,
|
||||
selectedStatus,
|
||||
selectedSegmentIds,
|
||||
importStatus,
|
||||
currentPage,
|
||||
limit,
|
||||
onCloseSegmentDetail,
|
||||
clearSelection,
|
||||
} = options
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const pathname = usePathname()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const datasetId = useDocumentContext(s => s.datasetId) || ''
|
||||
const documentId = useDocumentContext(s => s.documentId) || ''
|
||||
const docForm = useDocumentContext(s => s.docForm)
|
||||
const parentMode = useDocumentContext(s => s.parentMode)
|
||||
|
||||
const segmentListRef = useRef<HTMLDivElement>(null)
|
||||
const needScrollToBottom = useRef(false)
|
||||
|
||||
const isFullDocMode = useMemo(() => {
|
||||
return docForm === ChunkingMode.parentChild && parentMode === 'full-doc'
|
||||
}, [docForm, parentMode])
|
||||
|
||||
// Build query params
|
||||
const queryParams = useMemo(() => ({
|
||||
page: isFullDocMode ? 1 : currentPage,
|
||||
limit: isFullDocMode ? DEFAULT_LIMIT : limit,
|
||||
keyword: isFullDocMode ? '' : searchValue,
|
||||
enabled: selectedStatus,
|
||||
}), [isFullDocMode, currentPage, limit, searchValue, selectedStatus])
|
||||
|
||||
// Build query key for optimistic updates
|
||||
const currentQueryKey = useMemo(() =>
|
||||
[...useSegmentListKey, datasetId, documentId, queryParams], [datasetId, documentId, queryParams])
|
||||
|
||||
// Fetch segment list
|
||||
const { isLoading: isLoadingSegmentList, data: segmentListData } = useSegmentList({
|
||||
datasetId,
|
||||
documentId,
|
||||
params: queryParams,
|
||||
})
|
||||
|
||||
// Derive segments from query data
|
||||
const segments = useMemo(() => segmentListData?.data || [], [segmentListData])
|
||||
|
||||
// Invalidation hooks
|
||||
const invalidSegmentList = useInvalid(useSegmentListKey)
|
||||
const invalidChunkListAll = useInvalid(useChunkListAllKey)
|
||||
const invalidChunkListEnabled = useInvalid(useChunkListEnabledKey)
|
||||
const invalidChunkListDisabled = useInvalid(useChunkListDisabledKey)
|
||||
|
||||
// Scroll to bottom when needed
|
||||
useEffect(() => {
|
||||
if (segmentListRef.current && needScrollToBottom.current) {
|
||||
segmentListRef.current.scrollTo({ top: segmentListRef.current.scrollHeight, behavior: 'smooth' })
|
||||
needScrollToBottom.current = false
|
||||
}
|
||||
}, [segments])
|
||||
|
||||
// Reset list on pathname change
|
||||
useEffect(() => {
|
||||
clearSelection()
|
||||
invalidSegmentList()
|
||||
}, [pathname])
|
||||
|
||||
// Reset list on import completion
|
||||
useEffect(() => {
|
||||
if (importStatus === ProcessStatus.COMPLETED) {
|
||||
clearSelection()
|
||||
invalidSegmentList()
|
||||
}
|
||||
}, [importStatus])
|
||||
|
||||
const resetList = useCallback(() => {
|
||||
clearSelection()
|
||||
invalidSegmentList()
|
||||
}, [clearSelection, invalidSegmentList])
|
||||
|
||||
const refreshChunkListWithStatusChanged = useCallback(() => {
|
||||
if (selectedStatus === 'all') {
|
||||
invalidChunkListDisabled()
|
||||
invalidChunkListEnabled()
|
||||
}
|
||||
else {
|
||||
invalidSegmentList()
|
||||
}
|
||||
}, [selectedStatus, invalidChunkListDisabled, invalidChunkListEnabled, invalidSegmentList])
|
||||
|
||||
const refreshChunkListDataWithDetailChanged = useCallback(() => {
|
||||
const refreshMap: Record<string, () => void> = {
|
||||
all: () => {
|
||||
invalidChunkListDisabled()
|
||||
invalidChunkListEnabled()
|
||||
},
|
||||
true: () => {
|
||||
invalidChunkListAll()
|
||||
invalidChunkListDisabled()
|
||||
},
|
||||
false: () => {
|
||||
invalidChunkListAll()
|
||||
invalidChunkListEnabled()
|
||||
},
|
||||
}
|
||||
refreshMap[String(selectedStatus)]?.()
|
||||
}, [selectedStatus, invalidChunkListDisabled, invalidChunkListEnabled, invalidChunkListAll])
|
||||
|
||||
// Optimistic update helper using React Query's setQueryData
|
||||
const updateSegmentInCache = useCallback((
|
||||
segmentId: string,
|
||||
updater: (seg: SegmentDetailModel) => SegmentDetailModel,
|
||||
) => {
|
||||
queryClient.setQueryData<SegmentsResponse>(currentQueryKey, (old) => {
|
||||
if (!old)
|
||||
return old
|
||||
return {
|
||||
...old,
|
||||
data: old.data.map(seg => seg.id === segmentId ? updater(seg) : seg),
|
||||
}
|
||||
})
|
||||
}, [queryClient, currentQueryKey])
|
||||
|
||||
// Batch update helper
|
||||
const updateSegmentsInCache = useCallback((
|
||||
segmentIds: string[],
|
||||
updater: (seg: SegmentDetailModel) => SegmentDetailModel,
|
||||
) => {
|
||||
queryClient.setQueryData<SegmentsResponse>(currentQueryKey, (old) => {
|
||||
if (!old)
|
||||
return old
|
||||
return {
|
||||
...old,
|
||||
data: old.data.map(seg => segmentIds.includes(seg.id) ? updater(seg) : seg),
|
||||
}
|
||||
})
|
||||
}, [queryClient, currentQueryKey])
|
||||
|
||||
// Mutations
|
||||
const { mutateAsync: enableSegment } = useEnableSegment()
|
||||
const { mutateAsync: disableSegment } = useDisableSegment()
|
||||
const { mutateAsync: deleteSegment } = useDeleteSegment()
|
||||
const { mutateAsync: updateSegment } = useUpdateSegment()
|
||||
|
||||
const onChangeSwitch = useCallback(async (enable: boolean, segId?: string) => {
|
||||
const operationApi = enable ? enableSegment : disableSegment
|
||||
const targetIds = segId ? [segId] : selectedSegmentIds
|
||||
|
||||
await operationApi({ datasetId, documentId, segmentIds: targetIds }, {
|
||||
onSuccess: () => {
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
updateSegmentsInCache(targetIds, seg => ({ ...seg, enabled: enable }))
|
||||
refreshChunkListWithStatusChanged()
|
||||
},
|
||||
onError: () => {
|
||||
notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
},
|
||||
})
|
||||
}, [datasetId, documentId, selectedSegmentIds, disableSegment, enableSegment, t, notify, updateSegmentsInCache, refreshChunkListWithStatusChanged])
|
||||
|
||||
const onDelete = useCallback(async (segId?: string) => {
|
||||
const targetIds = segId ? [segId] : selectedSegmentIds
|
||||
|
||||
await deleteSegment({ datasetId, documentId, segmentIds: targetIds }, {
|
||||
onSuccess: () => {
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
resetList()
|
||||
if (!segId)
|
||||
clearSelection()
|
||||
},
|
||||
onError: () => {
|
||||
notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
},
|
||||
})
|
||||
}, [datasetId, documentId, selectedSegmentIds, deleteSegment, resetList, clearSelection, t, notify])
|
||||
|
||||
const handleUpdateSegment = useCallback(async (
|
||||
segmentId: string,
|
||||
question: string,
|
||||
answer: string,
|
||||
keywords: string[],
|
||||
attachments: FileEntity[],
|
||||
needRegenerate = false,
|
||||
) => {
|
||||
const params: SegmentUpdater = { content: '', attachment_ids: [] }
|
||||
|
||||
// Validate and build params based on doc form
|
||||
if (docForm === ChunkingMode.qa) {
|
||||
if (!question.trim()) {
|
||||
notify({ type: 'error', message: t('segment.questionEmpty', { ns: 'datasetDocuments' }) })
|
||||
return
|
||||
}
|
||||
if (!answer.trim()) {
|
||||
notify({ type: 'error', message: t('segment.answerEmpty', { ns: 'datasetDocuments' }) })
|
||||
return
|
||||
}
|
||||
params.content = question
|
||||
params.answer = answer
|
||||
}
|
||||
else {
|
||||
if (!question.trim()) {
|
||||
notify({ type: 'error', message: t('segment.contentEmpty', { ns: 'datasetDocuments' }) })
|
||||
return
|
||||
}
|
||||
params.content = question
|
||||
}
|
||||
|
||||
if (keywords.length)
|
||||
params.keywords = keywords
|
||||
|
||||
if (attachments.length) {
|
||||
const notAllUploaded = attachments.some(item => !item.uploadedId)
|
||||
if (notAllUploaded) {
|
||||
notify({ type: 'error', message: t('segment.allFilesUploaded', { ns: 'datasetDocuments' }) })
|
||||
return
|
||||
}
|
||||
params.attachment_ids = attachments.map(item => item.uploadedId!)
|
||||
}
|
||||
|
||||
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('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
if (!needRegenerate)
|
||||
onCloseSegmentDetail()
|
||||
|
||||
updateSegmentInCache(segmentId, seg => ({
|
||||
...seg,
|
||||
answer: res.data.answer,
|
||||
content: res.data.content,
|
||||
sign_content: res.data.sign_content,
|
||||
keywords: res.data.keywords,
|
||||
attachments: res.data.attachments,
|
||||
word_count: res.data.word_count,
|
||||
hit_count: res.data.hit_count,
|
||||
enabled: res.data.enabled,
|
||||
updated_at: res.data.updated_at,
|
||||
child_chunks: res.data.child_chunks,
|
||||
}))
|
||||
refreshChunkListDataWithDetailChanged()
|
||||
eventEmitter?.emit('update-segment-success')
|
||||
},
|
||||
onSettled() {
|
||||
eventEmitter?.emit('update-segment-done')
|
||||
},
|
||||
})
|
||||
}, [datasetId, documentId, docForm, updateSegment, notify, eventEmitter, onCloseSegmentDetail, updateSegmentInCache, refreshChunkListDataWithDetailChanged, t])
|
||||
|
||||
const viewNewlyAddedChunk = useCallback(() => {
|
||||
const totalPages = segmentListData?.total_pages || 0
|
||||
const total = segmentListData?.total || 0
|
||||
const newPage = Math.ceil((total + 1) / limit)
|
||||
needScrollToBottom.current = true
|
||||
|
||||
if (newPage > totalPages)
|
||||
return
|
||||
resetList()
|
||||
}, [segmentListData, limit, resetList])
|
||||
|
||||
// Compute total text for display
|
||||
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 = (docForm === ChunkingMode.parentChild && parentMode === 'paragraph')
|
||||
? 'segment.parentChunks' as const
|
||||
: 'segment.chunks' as const
|
||||
return `${total} ${t(translationKey, { ns: 'datasetDocuments', count })}`
|
||||
}
|
||||
const total = typeof segmentListData?.total === 'number' ? formatNumber(segmentListData.total) : 0
|
||||
const count = segmentListData?.total || 0
|
||||
return `${total} ${t('segment.searchResults', { ns: 'datasetDocuments', count })}`
|
||||
}, [segmentListData, docForm, parentMode, searchValue, selectedStatus, t])
|
||||
|
||||
return {
|
||||
segments,
|
||||
isLoadingSegmentList,
|
||||
segmentListData,
|
||||
totalText,
|
||||
isFullDocMode,
|
||||
segmentListRef,
|
||||
needScrollToBottom,
|
||||
onChangeSwitch,
|
||||
onDelete,
|
||||
handleUpdateSegment,
|
||||
resetList,
|
||||
viewNewlyAddedChunk,
|
||||
invalidSegmentList,
|
||||
updateSegmentInCache,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import type { SegmentDetailModel } from '@/models/datasets'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
export type UseSegmentSelectionReturn = {
|
||||
selectedSegmentIds: string[]
|
||||
isAllSelected: boolean
|
||||
isSomeSelected: boolean
|
||||
onSelected: (segId: string) => void
|
||||
onSelectedAll: () => void
|
||||
onCancelBatchOperation: () => void
|
||||
clearSelection: () => void
|
||||
}
|
||||
|
||||
export const useSegmentSelection = (segments: SegmentDetailModel[]): UseSegmentSelectionReturn => {
|
||||
const [selectedSegmentIds, setSelectedSegmentIds] = useState<string[]>([])
|
||||
|
||||
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 ? [] : currentAllSegIds)]
|
||||
})
|
||||
}, [segments, isAllSelected])
|
||||
|
||||
const onCancelBatchOperation = useCallback(() => {
|
||||
setSelectedSegmentIds([])
|
||||
}, [])
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
setSelectedSegmentIds([])
|
||||
}, [])
|
||||
|
||||
return {
|
||||
selectedSegmentIds,
|
||||
isAllSelected,
|
||||
isSomeSelected,
|
||||
onSelected,
|
||||
onSelectedAll,
|
||||
onCancelBatchOperation,
|
||||
clearSelection,
|
||||
}
|
||||
}
|
||||
@ -1,89 +1,33 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import type { FileEntity } from '@/app/components/datasets/common/image-uploader/types'
|
||||
import type { ChildChunkDetail, SegmentDetailModel, SegmentUpdater } from '@/models/datasets'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { createContext, useContext, useContextSelector } from 'use-context-selector'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import type { ProcessStatus } from '../segment-add'
|
||||
import type { SegmentListContextValue } from './segment-list-context'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Pagination from '@/app/components/base/pagination'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import NewSegment from '@/app/components/datasets/documents/detail/new-segment'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import {
|
||||
useChildSegmentList,
|
||||
useChildSegmentListKey,
|
||||
useChunkListAllKey,
|
||||
useChunkListDisabledKey,
|
||||
useChunkListEnabledKey,
|
||||
useDeleteChildSegment,
|
||||
useDeleteSegment,
|
||||
useDisableSegment,
|
||||
useEnableSegment,
|
||||
useSegmentList,
|
||||
useSegmentListKey,
|
||||
useUpdateChildSegment,
|
||||
useUpdateSegment,
|
||||
} from '@/service/knowledge/use-segment'
|
||||
import { useInvalid } from '@/service/use-base'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { useDocumentContext } from '../context'
|
||||
import { ProcessStatus } from '../segment-add'
|
||||
import ChildSegmentDetail from './child-segment-detail'
|
||||
import ChildSegmentList from './child-segment-list'
|
||||
import BatchAction from './common/batch-action'
|
||||
import FullScreenDrawer from './common/full-screen-drawer'
|
||||
import DisplayToggle from './display-toggle'
|
||||
import NewChildSegment from './new-child-segment'
|
||||
import SegmentCard from './segment-card'
|
||||
import SegmentDetail from './segment-detail'
|
||||
import SegmentList from './segment-list'
|
||||
import StatusItem from './status-item'
|
||||
import s from './style.module.css'
|
||||
import { DrawerGroup, FullDocModeContent, GeneralModeContent, MenuBar } from './components'
|
||||
import {
|
||||
useChildSegmentData,
|
||||
useModalState,
|
||||
useSearchFilter,
|
||||
useSegmentListData,
|
||||
useSegmentSelection,
|
||||
} from './hooks'
|
||||
import {
|
||||
SegmentListContext,
|
||||
useSegmentListContext,
|
||||
} from './segment-list-context'
|
||||
|
||||
const DEFAULT_LIMIT = 10
|
||||
|
||||
type CurrSegmentType = {
|
||||
segInfo?: SegmentDetailModel
|
||||
showModal: boolean
|
||||
isEditMode?: boolean
|
||||
}
|
||||
|
||||
type CurrChildChunkType = {
|
||||
childChunkInfo?: ChildChunkDetail
|
||||
showModal: boolean
|
||||
}
|
||||
|
||||
export type SegmentListContextValue = {
|
||||
isCollapsed: boolean
|
||||
fullScreen: boolean
|
||||
toggleFullScreen: (fullscreen?: boolean) => void
|
||||
currSegment: CurrSegmentType
|
||||
currChildChunk: CurrChildChunkType
|
||||
}
|
||||
|
||||
const SegmentListContext = createContext<SegmentListContextValue>({
|
||||
isCollapsed: true,
|
||||
fullScreen: false,
|
||||
toggleFullScreen: noop,
|
||||
currSegment: { showModal: false },
|
||||
currChildChunk: { showModal: false },
|
||||
})
|
||||
|
||||
export const useSegmentListContext = (selector: (value: SegmentListContextValue) => any) => {
|
||||
return useContextSelector(SegmentListContext, selector)
|
||||
}
|
||||
|
||||
type ICompletedProps = {
|
||||
embeddingAvailable: boolean
|
||||
showNewSegmentModal: boolean
|
||||
@ -91,6 +35,7 @@ type ICompletedProps = {
|
||||
importStatus: ProcessStatus | string | undefined
|
||||
archived?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Embedding done, show list of all segments
|
||||
* Support search and filter
|
||||
@ -102,669 +47,219 @@ const Completed: FC<ICompletedProps> = ({
|
||||
importStatus,
|
||||
archived,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const pathname = usePathname()
|
||||
const datasetId = useDocumentContext(s => s.datasetId) || ''
|
||||
const documentId = useDocumentContext(s => s.documentId) || ''
|
||||
const docForm = useDocumentContext(s => s.docForm)
|
||||
const parentMode = useDocumentContext(s => s.parentMode)
|
||||
// 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 [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 { eventEmitter } = useEventEmitterContextContext()
|
||||
const [isCollapsed, setIsCollapsed] = useState(true)
|
||||
const [currentPage, setCurrentPage] = useState(1) // start from 1
|
||||
// Pagination state
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [limit, setLimit] = useState(DEFAULT_LIMIT)
|
||||
const [fullScreen, setFullScreen] = useState(false)
|
||||
const [showNewChildSegmentModal, setShowNewChildSegmentModal] = useState(false)
|
||||
const [isRegenerationModalOpen, setIsRegenerationModalOpen] = useState(false)
|
||||
|
||||
const segmentListRef = useRef<HTMLDivElement>(null)
|
||||
const childSegmentListRef = useRef<HTMLDivElement>(null)
|
||||
const needScrollToBottom = useRef(false)
|
||||
const statusList = useRef<Item[]>([
|
||||
{ value: 'all', name: t('list.index.all', { ns: 'datasetDocuments' }) },
|
||||
{ value: 0, name: t('list.status.disabled', { ns: 'datasetDocuments' }) },
|
||||
{ value: 1, name: t('list.status.enabled', { ns: 'datasetDocuments' }) },
|
||||
])
|
||||
// Search and filter state
|
||||
const searchFilter = useSearchFilter({
|
||||
onPageChange: setCurrentPage,
|
||||
})
|
||||
|
||||
const { run: handleSearch } = useDebounceFn(() => {
|
||||
setSearchValue(inputValue)
|
||||
setCurrentPage(1)
|
||||
}, { wait: 500 })
|
||||
// Modal state
|
||||
const modalState = useModalState({
|
||||
onNewSegmentModalChange,
|
||||
})
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
setInputValue(value)
|
||||
handleSearch()
|
||||
}
|
||||
// Selection state (need segments first, so we use a placeholder initially)
|
||||
const [segmentsForSelection, setSegmentsForSelection] = useState<string[]>([])
|
||||
|
||||
const onChangeStatus = ({ value }: Item) => {
|
||||
setSelectedStatus(value === 'all' ? 'all' : !!value)
|
||||
setCurrentPage(1)
|
||||
}
|
||||
|
||||
const isFullDocMode = useMemo(() => {
|
||||
return docForm === ChunkingMode.parentChild && parentMode === 'full-doc'
|
||||
}, [docForm, parentMode])
|
||||
|
||||
const { isLoading: isLoadingSegmentList, data: segmentListData } = useSegmentList(
|
||||
{
|
||||
datasetId,
|
||||
documentId,
|
||||
params: {
|
||||
page: isFullDocMode ? 1 : currentPage,
|
||||
limit: isFullDocMode ? 10 : limit,
|
||||
keyword: isFullDocMode ? '' : searchValue,
|
||||
enabled: selectedStatus,
|
||||
},
|
||||
},
|
||||
)
|
||||
const invalidSegmentList = useInvalid(useSegmentListKey)
|
||||
|
||||
useEffect(() => {
|
||||
if (segmentListData) {
|
||||
setSegments(segmentListData.data || [])
|
||||
const totalPages = segmentListData.total_pages
|
||||
if (totalPages < currentPage)
|
||||
setCurrentPage(totalPages === 0 ? 1 : totalPages)
|
||||
}
|
||||
}, [segmentListData])
|
||||
|
||||
useEffect(() => {
|
||||
if (segmentListRef.current && needScrollToBottom.current) {
|
||||
segmentListRef.current.scrollTo({ top: segmentListRef.current.scrollHeight, behavior: 'smooth' })
|
||||
needScrollToBottom.current = false
|
||||
}
|
||||
}, [segments])
|
||||
|
||||
const { isLoading: isLoadingChildSegmentList, data: childChunkListData } = useChildSegmentList(
|
||||
{
|
||||
datasetId,
|
||||
documentId,
|
||||
segmentId: segments[0]?.id || '',
|
||||
params: {
|
||||
page: currentPage === 0 ? 1 : currentPage,
|
||||
limit,
|
||||
keyword: searchValue,
|
||||
},
|
||||
},
|
||||
!isFullDocMode || segments.length === 0,
|
||||
)
|
||||
const invalidChildSegmentList = useInvalid(useChildSegmentListKey)
|
||||
|
||||
useEffect(() => {
|
||||
if (childSegmentListRef.current && needScrollToBottom.current) {
|
||||
childSegmentListRef.current.scrollTo({ top: childSegmentListRef.current.scrollHeight, behavior: 'smooth' })
|
||||
needScrollToBottom.current = false
|
||||
}
|
||||
}, [childSegments])
|
||||
|
||||
useEffect(() => {
|
||||
if (childChunkListData) {
|
||||
setChildSegments(childChunkListData.data || [])
|
||||
const totalPages = childChunkListData.total_pages
|
||||
if (totalPages < currentPage)
|
||||
setCurrentPage(totalPages === 0 ? 1 : totalPages)
|
||||
}
|
||||
}, [childChunkListData])
|
||||
|
||||
const resetList = useCallback(() => {
|
||||
setSelectedSegmentIds([])
|
||||
invalidSegmentList()
|
||||
}, [invalidSegmentList])
|
||||
|
||||
const resetChildList = useCallback(() => {
|
||||
invalidChildSegmentList()
|
||||
}, [invalidChildSegmentList])
|
||||
|
||||
const onClickCard = (detail: SegmentDetailModel, isEditMode = false) => {
|
||||
setCurrSegment({ segInfo: detail, showModal: true, isEditMode })
|
||||
}
|
||||
|
||||
const onCloseSegmentDetail = useCallback(() => {
|
||||
setCurrSegment({ showModal: false })
|
||||
setFullScreen(false)
|
||||
}, [])
|
||||
|
||||
const onCloseNewSegmentModal = useCallback(() => {
|
||||
onNewSegmentModalChange(false)
|
||||
setFullScreen(false)
|
||||
}, [onNewSegmentModalChange])
|
||||
|
||||
const onCloseNewChildChunkModal = useCallback(() => {
|
||||
setShowNewChildSegmentModal(false)
|
||||
setFullScreen(false)
|
||||
}, [])
|
||||
|
||||
const { mutateAsync: enableSegment } = useEnableSegment()
|
||||
const { mutateAsync: disableSegment } = useDisableSegment()
|
||||
// Invalidation hooks for child segment data
|
||||
const invalidChunkListAll = useInvalid(useChunkListAllKey)
|
||||
const invalidChunkListEnabled = useInvalid(useChunkListEnabledKey)
|
||||
const invalidChunkListDisabled = useInvalid(useChunkListDisabledKey)
|
||||
|
||||
const refreshChunkListWithStatusChanged = useCallback(() => {
|
||||
switch (selectedStatus) {
|
||||
case 'all':
|
||||
invalidChunkListDisabled()
|
||||
invalidChunkListEnabled()
|
||||
break
|
||||
default:
|
||||
invalidSegmentList()
|
||||
}
|
||||
}, [selectedStatus, invalidChunkListDisabled, invalidChunkListEnabled, invalidSegmentList])
|
||||
|
||||
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('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
for (const seg of segments) {
|
||||
if (segId ? seg.id === segId : selectedSegmentIds.includes(seg.id))
|
||||
seg.enabled = enable
|
||||
}
|
||||
setSegments([...segments])
|
||||
refreshChunkListWithStatusChanged()
|
||||
},
|
||||
onError: () => {
|
||||
notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
},
|
||||
})
|
||||
}, [datasetId, documentId, selectedSegmentIds, segments, disableSegment, enableSegment, t, notify, refreshChunkListWithStatusChanged])
|
||||
|
||||
const { mutateAsync: deleteSegment } = useDeleteSegment()
|
||||
|
||||
const onDelete = useCallback(async (segId?: string) => {
|
||||
await deleteSegment({ datasetId, documentId, segmentIds: segId ? [segId] : selectedSegmentIds }, {
|
||||
onSuccess: () => {
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
resetList()
|
||||
if (!segId)
|
||||
setSelectedSegmentIds([])
|
||||
},
|
||||
onError: () => {
|
||||
notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
},
|
||||
})
|
||||
}, [datasetId, documentId, selectedSegmentIds, deleteSegment, resetList, t, notify])
|
||||
|
||||
const { mutateAsync: updateSegment } = useUpdateSegment()
|
||||
|
||||
const refreshChunkListDataWithDetailChanged = useCallback(() => {
|
||||
switch (selectedStatus) {
|
||||
case 'all':
|
||||
const refreshMap: Record<string, () => void> = {
|
||||
all: () => {
|
||||
invalidChunkListDisabled()
|
||||
invalidChunkListEnabled()
|
||||
break
|
||||
case true:
|
||||
},
|
||||
true: () => {
|
||||
invalidChunkListAll()
|
||||
invalidChunkListDisabled()
|
||||
break
|
||||
case false:
|
||||
},
|
||||
false: () => {
|
||||
invalidChunkListAll()
|
||||
invalidChunkListEnabled()
|
||||
break
|
||||
}
|
||||
}, [selectedStatus, invalidChunkListDisabled, invalidChunkListEnabled, invalidChunkListAll])
|
||||
|
||||
const handleUpdateSegment = useCallback(async (
|
||||
segmentId: string,
|
||||
question: string,
|
||||
answer: string,
|
||||
keywords: string[],
|
||||
attachments: FileEntity[],
|
||||
needRegenerate = false,
|
||||
) => {
|
||||
const params: SegmentUpdater = { content: '', attachment_ids: [] }
|
||||
if (docForm === ChunkingMode.qa) {
|
||||
if (!question.trim())
|
||||
return notify({ type: 'error', message: t('segment.questionEmpty', { ns: 'datasetDocuments' }) })
|
||||
if (!answer.trim())
|
||||
return notify({ type: 'error', message: t('segment.answerEmpty', { ns: 'datasetDocuments' }) })
|
||||
|
||||
params.content = question
|
||||
params.answer = answer
|
||||
}
|
||||
else {
|
||||
if (!question.trim())
|
||||
return notify({ type: 'error', message: t('segment.contentEmpty', { ns: 'datasetDocuments' }) })
|
||||
|
||||
params.content = question
|
||||
}
|
||||
|
||||
if (keywords.length)
|
||||
params.keywords = keywords
|
||||
|
||||
if (attachments.length) {
|
||||
const notAllUploaded = attachments.some(item => !item.uploadedId)
|
||||
if (notAllUploaded)
|
||||
return notify({ type: 'error', message: t('segment.allFilesUploaded', { ns: 'datasetDocuments' }) })
|
||||
params.attachment_ids = attachments.map(item => item.uploadedId!)
|
||||
}
|
||||
|
||||
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('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
if (!needRegenerate)
|
||||
onCloseSegmentDetail()
|
||||
for (const seg of segments) {
|
||||
if (seg.id === segmentId) {
|
||||
seg.answer = res.data.answer
|
||||
seg.content = res.data.content
|
||||
seg.sign_content = res.data.sign_content
|
||||
seg.keywords = res.data.keywords
|
||||
seg.attachments = res.data.attachments
|
||||
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
|
||||
}
|
||||
}
|
||||
setSegments([...segments])
|
||||
refreshChunkListDataWithDetailChanged()
|
||||
eventEmitter?.emit('update-segment-success')
|
||||
},
|
||||
onSettled() {
|
||||
eventEmitter?.emit('update-segment-done')
|
||||
},
|
||||
})
|
||||
}, [segments, datasetId, documentId, updateSegment, docForm, notify, eventEmitter, onCloseSegmentDetail, refreshChunkListDataWithDetailChanged, t])
|
||||
}
|
||||
refreshMap[String(searchFilter.selectedStatus)]?.()
|
||||
}, [searchFilter.selectedStatus, invalidChunkListDisabled, invalidChunkListEnabled, invalidChunkListAll])
|
||||
|
||||
useEffect(() => {
|
||||
resetList()
|
||||
}, [pathname])
|
||||
// Segment list data
|
||||
const segmentListDataHook = useSegmentListData({
|
||||
searchValue: searchFilter.searchValue,
|
||||
selectedStatus: searchFilter.selectedStatus,
|
||||
selectedSegmentIds: segmentsForSelection,
|
||||
importStatus,
|
||||
currentPage,
|
||||
limit,
|
||||
onCloseSegmentDetail: modalState.onCloseSegmentDetail,
|
||||
clearSelection: () => setSegmentsForSelection([]),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (importStatus === ProcessStatus.COMPLETED)
|
||||
resetList()
|
||||
}, [importStatus])
|
||||
// Selection state (with actual segments)
|
||||
const selectionState = useSegmentSelection(segmentListDataHook.segments)
|
||||
|
||||
const onCancelBatchOperation = useCallback(() => {
|
||||
setSelectedSegmentIds([])
|
||||
// Sync selection state for segment list data hook
|
||||
useMemo(() => {
|
||||
setSegmentsForSelection(selectionState.selectedSegmentIds)
|
||||
}, [selectionState.selectedSegmentIds])
|
||||
|
||||
// Child segment data
|
||||
const childSegmentDataHook = useChildSegmentData({
|
||||
searchValue: searchFilter.searchValue,
|
||||
currentPage,
|
||||
limit,
|
||||
segments: segmentListDataHook.segments,
|
||||
currChunkId: modalState.currChunkId,
|
||||
isFullDocMode: segmentListDataHook.isFullDocMode,
|
||||
onCloseChildSegmentDetail: modalState.onCloseChildSegmentDetail,
|
||||
refreshChunkListDataWithDetailChanged,
|
||||
updateSegmentInCache: segmentListDataHook.updateSegmentInCache,
|
||||
})
|
||||
|
||||
// Compute total for pagination
|
||||
const paginationTotal = useMemo(() => {
|
||||
if (segmentListDataHook.isFullDocMode)
|
||||
return childSegmentDataHook.childChunkListData?.total || 0
|
||||
return segmentListDataHook.segmentListData?.total || 0
|
||||
}, [segmentListDataHook.isFullDocMode, childSegmentDataHook.childChunkListData, segmentListDataHook.segmentListData])
|
||||
|
||||
// Handle page change
|
||||
const handlePageChange = useCallback((page: number) => {
|
||||
setCurrentPage(page + 1)
|
||||
}, [])
|
||||
|
||||
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 ? [] : currentAllSegIds)]
|
||||
})
|
||||
}, [segments, isAllSelected])
|
||||
|
||||
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 = (docForm === ChunkingMode.parentChild && parentMode === 'paragraph')
|
||||
? 'segment.parentChunks' as const
|
||||
: 'segment.chunks' as const
|
||||
return `${total} ${t(translationKey, { ns: 'datasetDocuments', count })}`
|
||||
}
|
||||
else {
|
||||
const total = typeof segmentListData?.total === 'number' ? formatNumber(segmentListData.total) : 0
|
||||
const count = segmentListData?.total || 0
|
||||
return `${total} ${t('segment.searchResults', { ns: 'datasetDocuments', count })}`
|
||||
}
|
||||
}, [segmentListData, docForm, parentMode, searchValue, selectedStatus, t])
|
||||
|
||||
const toggleFullScreen = useCallback(() => {
|
||||
setFullScreen(!fullScreen)
|
||||
}, [fullScreen])
|
||||
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
setIsCollapsed(prev => !prev)
|
||||
}, [])
|
||||
|
||||
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()
|
||||
if (currentPage !== totalPages)
|
||||
setCurrentPage(totalPages)
|
||||
}
|
||||
}, [segmentListData, limit, currentPage, resetList])
|
||||
|
||||
const { mutateAsync: deleteChildSegment } = useDeleteChildSegment()
|
||||
|
||||
const onDeleteChildChunk = useCallback(async (segmentId: string, childChunkId: string) => {
|
||||
await deleteChildSegment(
|
||||
{ datasetId, documentId, segmentId, childChunkId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
if (parentMode === 'paragraph')
|
||||
resetList()
|
||||
else
|
||||
resetChildList()
|
||||
},
|
||||
onError: () => {
|
||||
notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) })
|
||||
},
|
||||
},
|
||||
)
|
||||
}, [datasetId, documentId, parentMode, deleteChildSegment, resetList, resetChildList, t, notify])
|
||||
|
||||
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])
|
||||
refreshChunkListDataWithDetailChanged()
|
||||
}
|
||||
else {
|
||||
resetChildList()
|
||||
}
|
||||
}, [parentMode, currChunkId, segments, refreshChunkListDataWithDetailChanged, resetChildList])
|
||||
|
||||
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()
|
||||
if (currentPage !== totalPages)
|
||||
setCurrentPage(totalPages)
|
||||
}
|
||||
}, [childChunkListData, limit, currentPage, resetChildList])
|
||||
|
||||
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('segment.contentEmpty', { ns: 'datasetDocuments' }) })
|
||||
|
||||
params.content = content
|
||||
|
||||
eventEmitter?.emit('update-child-segment')
|
||||
await updateChildSegment({ datasetId, documentId, segmentId, childChunkId, body: params }, {
|
||||
onSuccess: (res) => {
|
||||
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
|
||||
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])
|
||||
refreshChunkListDataWithDetailChanged()
|
||||
}
|
||||
else {
|
||||
resetChildList()
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
eventEmitter?.emit('update-child-segment-done')
|
||||
},
|
||||
})
|
||||
}, [segments, datasetId, documentId, parentMode, updateChildSegment, notify, eventEmitter, onCloseChildSegmentDetail, refreshChunkListDataWithDetailChanged, resetChildList, t])
|
||||
|
||||
const onClearFilter = useCallback(() => {
|
||||
setInputValue('')
|
||||
setSearchValue('')
|
||||
setSelectedStatus('all')
|
||||
setCurrentPage(1)
|
||||
}, [])
|
||||
|
||||
const selectDefaultValue = useMemo(() => {
|
||||
if (selectedStatus === 'all')
|
||||
return 'all'
|
||||
return selectedStatus ? 1 : 0
|
||||
}, [selectedStatus])
|
||||
|
||||
// Context value
|
||||
const contextValue = useMemo<SegmentListContextValue>(() => ({
|
||||
isCollapsed,
|
||||
fullScreen,
|
||||
toggleFullScreen,
|
||||
currSegment,
|
||||
currChildChunk,
|
||||
}), [isCollapsed, fullScreen, toggleFullScreen, currSegment, currChildChunk])
|
||||
isCollapsed: modalState.isCollapsed,
|
||||
fullScreen: modalState.fullScreen,
|
||||
toggleFullScreen: modalState.toggleFullScreen,
|
||||
currSegment: modalState.currSegment,
|
||||
currChildChunk: modalState.currChildChunk,
|
||||
}), [
|
||||
modalState.isCollapsed,
|
||||
modalState.fullScreen,
|
||||
modalState.toggleFullScreen,
|
||||
modalState.currSegment,
|
||||
modalState.currChildChunk,
|
||||
])
|
||||
|
||||
return (
|
||||
<SegmentListContext.Provider value={contextValue}>
|
||||
{/* Menu Bar */}
|
||||
{!isFullDocMode && (
|
||||
<div className={s.docSearchWrapper}>
|
||||
<Checkbox
|
||||
className="shrink-0"
|
||||
checked={isAllSelected}
|
||||
indeterminate={!isAllSelected && isSomeSelected}
|
||||
onCheck={onSelectedAll}
|
||||
disabled={isLoadingSegmentList}
|
||||
/>
|
||||
<div className="system-sm-semibold-uppercase flex-1 pl-5 text-text-secondary">{totalText}</div>
|
||||
<SimpleSelect
|
||||
onSelect={onChangeStatus}
|
||||
items={statusList.current}
|
||||
defaultValue={selectDefaultValue}
|
||||
className={s.select}
|
||||
wrapperClassName="h-fit mr-2"
|
||||
optionWrapClassName="w-[160px]"
|
||||
optionClassName="p-0"
|
||||
renderOption={({ item, selected }) => <StatusItem item={item} selected={selected} />}
|
||||
notClearable
|
||||
/>
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
wrapperClassName="!w-52"
|
||||
value={inputValue}
|
||||
onChange={e => handleInputChange(e.target.value)}
|
||||
onClear={() => handleInputChange('')}
|
||||
/>
|
||||
<Divider type="vertical" className="mx-3 h-3.5" />
|
||||
<DisplayToggle isCollapsed={isCollapsed} toggleCollapsed={toggleCollapsed} />
|
||||
</div>
|
||||
{!segmentListDataHook.isFullDocMode && (
|
||||
<MenuBar
|
||||
isAllSelected={selectionState.isAllSelected}
|
||||
isSomeSelected={selectionState.isSomeSelected}
|
||||
onSelectedAll={selectionState.onSelectedAll}
|
||||
isLoading={segmentListDataHook.isLoadingSegmentList}
|
||||
totalText={segmentListDataHook.totalText}
|
||||
statusList={searchFilter.statusList}
|
||||
selectDefaultValue={searchFilter.selectDefaultValue}
|
||||
onChangeStatus={searchFilter.onChangeStatus}
|
||||
inputValue={searchFilter.inputValue}
|
||||
onInputChange={searchFilter.handleInputChange}
|
||||
isCollapsed={modalState.isCollapsed}
|
||||
toggleCollapsed={modalState.toggleCollapsed}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Segment list */}
|
||||
{
|
||||
isFullDocMode
|
||||
? (
|
||||
<div className={cn(
|
||||
'flex grow flex-col 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{segmentListDataHook.isFullDocMode
|
||||
? (
|
||||
<FullDocModeContent
|
||||
segments={segmentListDataHook.segments}
|
||||
childSegments={childSegmentDataHook.childSegments}
|
||||
isLoadingSegmentList={segmentListDataHook.isLoadingSegmentList}
|
||||
isLoadingChildSegmentList={childSegmentDataHook.isLoadingChildSegmentList}
|
||||
currSegmentId={modalState.currSegment?.segInfo?.id}
|
||||
onClickCard={modalState.onClickCard}
|
||||
onDeleteChildChunk={childSegmentDataHook.onDeleteChildChunk}
|
||||
handleInputChange={searchFilter.handleInputChange}
|
||||
handleAddNewChildChunk={modalState.handleAddNewChildChunk}
|
||||
onClickSlice={modalState.onClickSlice}
|
||||
archived={archived}
|
||||
childChunkTotal={childSegmentDataHook.childChunkListData?.total || 0}
|
||||
inputValue={searchFilter.inputValue}
|
||||
onClearFilter={searchFilter.onClearFilter}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<GeneralModeContent
|
||||
segmentListRef={segmentListDataHook.segmentListRef}
|
||||
embeddingAvailable={embeddingAvailable}
|
||||
isLoadingSegmentList={segmentListDataHook.isLoadingSegmentList}
|
||||
segments={segmentListDataHook.segments}
|
||||
selectedSegmentIds={selectionState.selectedSegmentIds}
|
||||
onSelected={selectionState.onSelected}
|
||||
onChangeSwitch={segmentListDataHook.onChangeSwitch}
|
||||
onDelete={segmentListDataHook.onDelete}
|
||||
onClickCard={modalState.onClickCard}
|
||||
archived={archived}
|
||||
onDeleteChildChunk={childSegmentDataHook.onDeleteChildChunk}
|
||||
handleAddNewChildChunk={modalState.handleAddNewChildChunk}
|
||||
onClickSlice={modalState.onClickSlice}
|
||||
onClearFilter={searchFilter.onClearFilter}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
<Divider type="horizontal" className="mx-6 my-0 h-px w-auto bg-divider-subtle" />
|
||||
<Pagination
|
||||
current={currentPage - 1}
|
||||
onChange={cur => setCurrentPage(cur + 1)}
|
||||
total={(isFullDocMode ? childChunkListData?.total : segmentListData?.total) || 0}
|
||||
onChange={handlePageChange}
|
||||
total={paginationTotal}
|
||||
limit={limit}
|
||||
onLimitChange={limit => setLimit(limit)}
|
||||
className={isFullDocMode ? 'px-3' : ''}
|
||||
onLimitChange={setLimit}
|
||||
className={segmentListDataHook.isFullDocMode ? 'px-3' : ''}
|
||||
/>
|
||||
{/* Edit or view segment detail */}
|
||||
<FullScreenDrawer
|
||||
isOpen={currSegment.showModal}
|
||||
fullScreen={fullScreen}
|
||||
onClose={onCloseSegmentDetail}
|
||||
showOverlay={false}
|
||||
needCheckChunks
|
||||
modal={isRegenerationModalOpen}
|
||||
>
|
||||
<SegmentDetail
|
||||
key={currSegment.segInfo?.id}
|
||||
segInfo={currSegment.segInfo ?? { id: '' }}
|
||||
|
||||
{/* Drawer Group - only render when docForm is available */}
|
||||
{docForm && (
|
||||
<DrawerGroup
|
||||
currSegment={modalState.currSegment}
|
||||
onCloseSegmentDetail={modalState.onCloseSegmentDetail}
|
||||
onUpdateSegment={segmentListDataHook.handleUpdateSegment}
|
||||
isRegenerationModalOpen={modalState.isRegenerationModalOpen}
|
||||
setIsRegenerationModalOpen={modalState.setIsRegenerationModalOpen}
|
||||
showNewSegmentModal={showNewSegmentModal}
|
||||
onCloseNewSegmentModal={modalState.onCloseNewSegmentModal}
|
||||
onSaveNewSegment={segmentListDataHook.resetList}
|
||||
viewNewlyAddedChunk={segmentListDataHook.viewNewlyAddedChunk}
|
||||
currChildChunk={modalState.currChildChunk}
|
||||
currChunkId={modalState.currChunkId}
|
||||
onCloseChildSegmentDetail={modalState.onCloseChildSegmentDetail}
|
||||
onUpdateChildChunk={childSegmentDataHook.handleUpdateChildChunk}
|
||||
showNewChildSegmentModal={modalState.showNewChildSegmentModal}
|
||||
onCloseNewChildChunkModal={modalState.onCloseNewChildChunkModal}
|
||||
onSaveNewChildChunk={childSegmentDataHook.onSaveNewChildChunk}
|
||||
viewNewlyAddedChildChunk={childSegmentDataHook.viewNewlyAddedChildChunk}
|
||||
fullScreen={modalState.fullScreen}
|
||||
docForm={docForm}
|
||||
isEditMode={currSegment.isEditMode}
|
||||
onUpdate={handleUpdateSegment}
|
||||
onCancel={onCloseSegmentDetail}
|
||||
onModalStateChange={setIsRegenerationModalOpen}
|
||||
/>
|
||||
</FullScreenDrawer>
|
||||
{/* Create New Segment */}
|
||||
<FullScreenDrawer
|
||||
isOpen={showNewSegmentModal}
|
||||
fullScreen={fullScreen}
|
||||
onClose={onCloseNewSegmentModal}
|
||||
modal
|
||||
>
|
||||
<NewSegment
|
||||
docForm={docForm}
|
||||
onCancel={onCloseNewSegmentModal}
|
||||
onSave={resetList}
|
||||
viewNewlyAddedChunk={viewNewlyAddedChunk}
|
||||
/>
|
||||
</FullScreenDrawer>
|
||||
{/* Edit or view child segment detail */}
|
||||
<FullScreenDrawer
|
||||
isOpen={currChildChunk.showModal}
|
||||
fullScreen={fullScreen}
|
||||
onClose={onCloseChildSegmentDetail}
|
||||
showOverlay={false}
|
||||
needCheckChunks
|
||||
>
|
||||
<ChildSegmentDetail
|
||||
key={currChildChunk.childChunkInfo?.id}
|
||||
chunkId={currChunkId}
|
||||
childChunkInfo={currChildChunk.childChunkInfo ?? { id: '' }}
|
||||
docForm={docForm}
|
||||
onUpdate={handleUpdateChildChunk}
|
||||
onCancel={onCloseChildSegmentDetail}
|
||||
/>
|
||||
</FullScreenDrawer>
|
||||
{/* Create New Child Segment */}
|
||||
<FullScreenDrawer
|
||||
isOpen={showNewChildSegmentModal}
|
||||
fullScreen={fullScreen}
|
||||
onClose={onCloseNewChildChunkModal}
|
||||
modal
|
||||
>
|
||||
<NewChildSegment
|
||||
chunkId={currChunkId}
|
||||
onCancel={onCloseNewChildChunkModal}
|
||||
onSave={onSaveNewChildChunk}
|
||||
viewNewlyAddedChildChunk={viewNewlyAddedChildChunk}
|
||||
/>
|
||||
</FullScreenDrawer>
|
||||
)}
|
||||
|
||||
{/* Batch Action Buttons */}
|
||||
{selectedSegmentIds.length > 0 && (
|
||||
{selectionState.selectedSegmentIds.length > 0 && (
|
||||
<BatchAction
|
||||
className="absolute bottom-16 left-0 z-20"
|
||||
selectedIds={selectedSegmentIds}
|
||||
onBatchEnable={onChangeSwitch.bind(null, true, '')}
|
||||
onBatchDisable={onChangeSwitch.bind(null, false, '')}
|
||||
onBatchDelete={onDelete.bind(null, '')}
|
||||
onCancel={onCancelBatchOperation}
|
||||
selectedIds={selectionState.selectedSegmentIds}
|
||||
onBatchEnable={() => segmentListDataHook.onChangeSwitch(true, '')}
|
||||
onBatchDisable={() => segmentListDataHook.onChangeSwitch(false, '')}
|
||||
onBatchDelete={() => segmentListDataHook.onDelete('')}
|
||||
onCancel={selectionState.onCancelBatchOperation}
|
||||
/>
|
||||
)}
|
||||
</SegmentListContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export { useSegmentListContext }
|
||||
export type { SegmentListContextValue }
|
||||
|
||||
export default Completed
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { createContext, useContextSelector } from 'use-context-selector'
|
||||
|
||||
export type CurrSegmentType = {
|
||||
segInfo?: SegmentDetailModel
|
||||
showModal: boolean
|
||||
isEditMode?: boolean
|
||||
}
|
||||
|
||||
export type CurrChildChunkType = {
|
||||
childChunkInfo?: ChildChunkDetail
|
||||
showModal: boolean
|
||||
}
|
||||
|
||||
export type SegmentListContextValue = {
|
||||
isCollapsed: boolean
|
||||
fullScreen: boolean
|
||||
toggleFullScreen: () => void
|
||||
currSegment: CurrSegmentType
|
||||
currChildChunk: CurrChildChunkType
|
||||
}
|
||||
|
||||
export const SegmentListContext = createContext<SegmentListContextValue>({
|
||||
isCollapsed: true,
|
||||
fullScreen: false,
|
||||
toggleFullScreen: noop,
|
||||
currSegment: { showModal: false },
|
||||
currChildChunk: { showModal: false },
|
||||
})
|
||||
|
||||
export const useSegmentListContext = <T>(selector: (value: SegmentListContextValue) => T): T => {
|
||||
return useContextSelector(SegmentListContext, selector)
|
||||
}
|
||||
@ -1787,14 +1787,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/datasets/documents/detail/completed/index.tsx": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 6
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/datasets/documents/detail/completed/new-child-segment.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
|
||||
Reference in New Issue
Block a user