diff --git a/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx b/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx index b0d8bc7f02..f4e14b9b76 100644 --- a/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx +++ b/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx @@ -1,6 +1,5 @@ import { type FC, useMemo, useState } from 'react' import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' -import { FormattedText } from '../../../formatted-text/formatted' import { EditSlice } from '../../../formatted-text/flavours/edit-slice' import { useDocumentContext } from '../index' import type { ChildChunkDetail } from '@/models/datasets' @@ -10,14 +9,22 @@ import Divider from '@/app/components/base/divider' type IChildSegmentCardProps = { childChunks: ChildChunkDetail[] - handleInputChange: (value: string) => void + parentChunkId: string + handleInputChange?: (value: string) => void + handleAddNewChildChunk?: (parentChunkId: string) => void enabled: boolean + onDelete?: (segId: string, childChunkId: string) => Promise + onClickSlice?: (childChunk: ChildChunkDetail) => void } const ChildSegmentList: FC = ({ childChunks, + parentChunkId, handleInputChange, + handleAddNewChildChunk, enabled, + onDelete, + onClickSlice, }) => { const parentMode = useDocumentContext(s => s.parentMode) @@ -62,6 +69,7 @@ const ChildSegmentList: FC = ({ className={classNames('px-1.5 py-1 text-components-button-secondary-accent-text system-xs-semibold', isParagraphMode ? 'hidden group-hover/card:inline-block' : '')} onClick={(event) => { event.stopPropagation() + handleAddNewChildChunk?.(parentChunkId) }} > ADD @@ -73,25 +81,26 @@ const ChildSegmentList: FC = ({ showClearIcon wrapperClassName='!w-52' value={''} - onChange={e => handleInputChange(e.target.value)} - onClear={() => handleInputChange('')} + onChange={e => handleInputChange?.(e.target.value)} + onClear={() => handleInputChange?.('')} /> : null} {(isFullDocMode || !collapsed) ?
{isParagraphMode && } - +
{childChunks.map((childChunk) => { + const edited = childChunk.type === 'customized' return {}} - className='' + onDelete={() => onDelete?.(childChunk.segment_id, childChunk.id)} + onClick={() => onClickSlice?.(childChunk)} /> })} - +
: null} diff --git a/web/app/components/datasets/documents/detail/completed/common/chunk-content.tsx b/web/app/components/datasets/documents/detail/completed/common/chunk-content.tsx index 3d55427cd3..ba4979f9dd 100644 --- a/web/app/components/datasets/documents/detail/completed/common/chunk-content.tsx +++ b/web/app/components/datasets/documents/detail/completed/common/chunk-content.tsx @@ -4,9 +4,9 @@ import AutoHeightTextarea from '@/app/components/base/auto-height-textarea/commo type IChunkContentProps = { question: string - answer: string + answer?: string onQuestionChange: (question: string) => void - onAnswerChange: (answer: string) => void + onAnswerChange?: (answer: string) => void isEditMode?: boolean docForm: string } @@ -39,7 +39,7 @@ const ChunkContent: FC = ({ className='leading-6 text-md text-gray-800' value={answer} placeholder={t('datasetDocuments.segment.answerPlaceholder') || ''} - onChange={e => onAnswerChange(e.target.value)} + onChange={e => onAnswerChange?.(e.target.value)} disabled={!isEditMode} autoFocus /> diff --git a/web/app/components/datasets/documents/detail/completed/display-toggle.tsx b/web/app/components/datasets/documents/detail/completed/display-toggle.tsx index 1128cca6ed..890545df41 100644 --- a/web/app/components/datasets/documents/detail/completed/display-toggle.tsx +++ b/web/app/components/datasets/documents/detail/completed/display-toggle.tsx @@ -1,11 +1,17 @@ import React, { type FC } from 'react' import { RiLineHeight } from '@remixicon/react' -import { useSegmentListContext } from '.' import Tooltip from '@/app/components/base/tooltip' import { Collapse } from '@/app/components/base/icons/src/public/knowledge' -const DisplayToggle: FC = () => { - const [isCollapsed, toggleCollapsed] = useSegmentListContext(s => [s.isCollapsed, s.toggleCollapsed]) +type DisplayToggleProps = { + isCollapsed: boolean + toggleCollapsed: () => void +} + +const DisplayToggle: FC = ({ + isCollapsed, + toggleCollapsed, +}) => { return ( void fullScreen: boolean - toggleFullScreen: () => void + toggleFullScreen: (fullscreen?: boolean) => void } -const SegmentListContext = createContext({ - isCollapsed: true, - toggleCollapsed: () => { }, - fullScreen: false, - toggleFullScreen: () => { }, -}) +const SegmentListContext = createContext({} as SegmentListContextValue) export const useSegmentListContext = (selector: (value: SegmentListContextValue) => any) => { return useContextSelector(SegmentListContext, selector) @@ -73,6 +77,8 @@ const Completed: FC = ({ const [datasetId = '', documentId = '', docForm, mode, parentMode] = useDocumentContext(s => [s.datasetId, s.documentId, s.docForm, s.mode, s.parentMode]) // the current segment id and whether to show the modal const [currSegment, setCurrSegment] = useState<{ segInfo?: SegmentDetailModel; showModal: boolean; isEditMode?: boolean }>({ showModal: false }) + const [currChildChunk, setCurrChildChunk] = useState<{ childChunkInfo?: ChildChunkDetail; showModal: boolean }>({ showModal: false }) + const [currChunkId, setCurrChunkId] = useState('') const [inputValue, setInputValue] = useState('') // the input value const [searchValue, setSearchValue] = useState('') // the search value @@ -86,6 +92,8 @@ const Completed: FC = ({ const [currentPage, setCurrentPage] = useState(1) // start from 1 const [limit, setLimit] = useState(DEFAULT_LIMIT) const [fullScreen, setFullScreen] = useState(false) + const [showNewChildSegmentModal, setShowNewChildSegmentModal] = useState(false) + const segmentListRef = useRef(null) const needScrollToBottom = useRef(false) @@ -136,7 +144,7 @@ const Completed: FC = ({ } }, [segments]) - const { data: childChunkListData, refetch: refreshChildSegmentList } = useChildSegmentList( + const { data: childChunkListData } = useChildSegmentList( { datasetId, documentId, @@ -149,6 +157,7 @@ const Completed: FC = ({ }, !isFullDocMode || segments.length === 0, ) + const invalidChildSegmentList = useInvalid(useChildSegmentListKey) useEffect(() => { if (childChunkListData) @@ -162,6 +171,12 @@ const Completed: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + const resetChildList = useCallback(() => { + setChildSegments([]) + invalidChildSegmentList() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + const onClickCard = (detail: SegmentDetailModel, isEditMode = false) => { setCurrSegment({ segInfo: detail, showModal: true, isEditMode }) } @@ -320,10 +335,54 @@ const Completed: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [segmentListData, limit, currentPage]) + const { mutateAsync: deleteChildSegment } = useDeleteChildSegment() + + const onDeleteChildChunk = useCallback(async (segmentId: string, childChunkId: string) => { + await deleteChildSegment( + { datasetId, documentId, segmentId, childChunkId }, + { + onSuccess: () => { + notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) + if (parentMode === 'paragraph') + resetList() + else + resetChildList() + }, + onError: () => { + notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) + }, + }, + ) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [datasetId, documentId]) + + const handleAddNewChildChunk = useCallback((parentChunkId: string) => { + setShowNewChildSegmentModal(true) + setCurrChunkId(parentChunkId) + }, []) + + const viewNewlyAddedChildChunk = useCallback(() => { + const totalPages = childChunkListData?.total_pages || 0 + const total = childChunkListData?.total || 0 + const newPage = Math.ceil((total + 1) / limit) + needScrollToBottom.current = true + if (newPage > totalPages) { + setCurrentPage(totalPages + 1) + } + else { + resetChildList() + currentPage !== totalPages && setCurrentPage(totalPages) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [childChunkListData, limit, currentPage]) + + const onClickSlice = useCallback((detail: ChildChunkDetail) => { + setCurrChildChunk({ childChunkInfo: detail, showModal: true }) + }, []) + return ( setIsCollapsed(!isCollapsed), fullScreen, toggleFullScreen, }}> @@ -355,7 +414,7 @@ const Completed: FC = ({ onClear={() => handleInputChange('')} /> - + setIsCollapsed(!isCollapsed)} /> } {/* Segment list */} { @@ -367,8 +426,12 @@ const Completed: FC = ({ loading={false} /> { }} + handleInputChange={handleInputChange} + handleAddNewChildChunk={handleAddNewChildChunk} + onClickSlice={onClickSlice} enabled={!archived} /> @@ -383,6 +446,9 @@ const Completed: FC = ({ onDelete={onDelete} onClick={onClickCard} archived={archived} + onDeleteChildChunk={onDeleteChildChunk} + handleAddNewChildChunk={handleAddNewChildChunk} + onClickSlice={onClickSlice} /> } {/* Pagination */} @@ -421,6 +487,26 @@ const Completed: FC = ({ viewNewlyAddedChunk={viewNewlyAddedChunk} /> + {/* Create New Child Segment */} + + { + setShowNewChildSegmentModal(false) + setFullScreen(false) + }} + onSave={() => { + if (parentMode === 'paragraph') + resetList() + else + resetChildList() + }} + viewNewlyAddedChildChunk={viewNewlyAddedChildChunk} + /> + {/* Batch Action Buttons */} {selectedSegmentIds.length > 0 && void + onSave: () => void + viewNewlyAddedChildChunk?: () => void +} + +const NewChildSegmentModal: FC = ({ + chunkId, + onCancel, + onSave, + viewNewlyAddedChildChunk, +}) => { + const { t } = useTranslation() + const { notify } = useContext(ToastContext) + const [content, setContent] = useState('') + const { datasetId, documentId } = useParams<{ datasetId: string; documentId: string }>() + const [loading, setLoading] = useState(false) + const [addAnother, setAddAnother] = useState(true) + const [fullScreen, toggleFullScreen] = useSegmentListContext(s => [s.fullScreen, s.toggleFullScreen]) + const { appSidebarExpand } = useAppStore(useShallow(state => ({ + appSidebarExpand: state.appSidebarExpand, + }))) + const refreshTimer = useRef(null) + + const CustomButton = <> + + + + + const handleCancel = (actionType: 'esc' | 'add' = 'esc') => { + if (actionType === 'esc' || !addAnother) + onCancel() + setContent('') + } + + const { mutateAsync: addChildSegment } = useAddChildSegment() + + const handleSave = async () => { + const params: SegmentUpdater = { content: '' } + + if (!content.trim()) + return notify({ type: 'error', message: t('datasetDocuments.segment.contentEmpty') }) + + params.content = content + + setLoading(true) + try { + await addChildSegment({ datasetId, documentId, segmentId: chunkId, body: params }) + notify({ + type: 'success', + message: t('datasetDocuments.segment.chunkAdded'), + className: `!w-[296px] !bottom-0 ${appSidebarExpand === 'expand' ? '!left-[216px]' : '!left-14'} + !top-auto !right-auto !mb-[52px] !ml-11`, + customComponent: CustomButton, + }) + handleCancel('add') + refreshTimer.current = setTimeout(() => { + onSave() + }, 3000) + } + finally { + setLoading(false) + } + } + + return ( +
+
+
+
{ + t('datasetDocuments.segment.addChildChunk') + }
+
+ + · + {formatNumber(content.length)} {t('datasetDocuments.segment.characters')} +
+
+
+ {fullScreen && ( + <> + setAddAnother(!addAnother)} /> + + + + )} +
+ +
+
+ +
+
+
+
+
+ setContent(content)} + isEditMode={true} + /> +
+
+ {!fullScreen && ( +
+ setAddAnother(!addAnother)} /> + +
+ )} +
+ ) +} + +export default memo(NewChildSegmentModal) diff --git a/web/app/components/datasets/documents/detail/completed/segment-card.tsx b/web/app/components/datasets/documents/detail/completed/segment-card.tsx index d700889378..a3d25c214d 100644 --- a/web/app/components/datasets/documents/detail/completed/segment-card.tsx +++ b/web/app/components/datasets/documents/detail/completed/segment-card.tsx @@ -8,7 +8,7 @@ import Tag from './common/tag' import Dot from './common/dot' import { SegmentIndexTag } from './common/segment-index-tag' import { useSegmentListContext } from './index' -import type { SegmentDetailModel } from '@/models/datasets' +import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets' import Indicator from '@/app/components/header/indicator' import Switch from '@/app/components/base/switch' import Divider from '@/app/components/base/divider' @@ -25,6 +25,9 @@ type ISegmentCardProps = { onClick?: () => void onChangeSwitch?: (enabled: boolean, segId?: string) => Promise onDelete?: (segId: string) => Promise + onDeleteChildChunk?: (segId: string, childChunkId: string) => Promise + handleAddNewChildChunk?: (parentChunkId: string) => void + onClickSlice?: (childChunk: ChildChunkDetail) => void onClickEdit?: () => void className?: string archived?: boolean @@ -36,6 +39,9 @@ const SegmentCard: FC = ({ onClick, onChangeSwitch, onDelete, + onDeleteChildChunk, + handleAddNewChildChunk, + onClickSlice, onClickEdit, loading = true, className = '', @@ -216,9 +222,12 @@ const SegmentCard: FC = ({ { child_chunks.length > 0 && {}} enabled={enabled} + onDelete={onDeleteChildChunk!} + handleAddNewChildChunk={handleAddNewChildChunk} + onClickSlice={onClickSlice} /> } diff --git a/web/app/components/datasets/documents/detail/completed/segment-list.tsx b/web/app/components/datasets/documents/detail/completed/segment-list.tsx index 35e2ca268d..4f50b3a050 100644 --- a/web/app/components/datasets/documents/detail/completed/segment-list.tsx +++ b/web/app/components/datasets/documents/detail/completed/segment-list.tsx @@ -1,6 +1,6 @@ import React, { type ForwardedRef } from 'react' import SegmentCard from './segment-card' -import type { SegmentDetailModel } from '@/models/datasets' +import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets' import Checkbox from '@/app/components/base/checkbox' import Loading from '@/app/components/base/loading' import Divider from '@/app/components/base/divider' @@ -14,6 +14,9 @@ type ISegmentListProps = { onClick: (detail: SegmentDetailModel, isEditMode?: boolean) => void onChangeSwitch: (enabled: boolean, segId?: string,) => Promise onDelete: (segId: string) => Promise + onDeleteChildChunk: (sgId: string, childChunkId: string) => Promise + handleAddNewChildChunk: (parentChunkId: string) => void + onClickSlice: (childChunk: ChildChunkDetail) => void archived?: boolean embeddingAvailable: boolean } @@ -26,6 +29,9 @@ const SegmentList = React.forwardRef(({ onClick: onClickCard, onChangeSwitch, onDelete, + onDeleteChildChunk, + handleAddNewChildChunk, + onClickSlice, archived, embeddingAvailable, }: ISegmentListProps, @@ -54,6 +60,9 @@ ref: ForwardedRef, onChangeSwitch={onChangeSwitch} onClickEdit={() => onClickCard(segItem, true)} onDelete={onDelete} + onDeleteChildChunk={onDeleteChildChunk} + handleAddNewChildChunk={handleAddNewChildChunk} + onClickSlice={onClickSlice} loading={false} archived={archived} embeddingAvailable={embeddingAvailable} diff --git a/web/app/components/datasets/formatted-text/flavours/edit-slice.tsx b/web/app/components/datasets/formatted-text/flavours/edit-slice.tsx index 1fe81180c4..00d836e517 100644 --- a/web/app/components/datasets/formatted-text/flavours/edit-slice.tsx +++ b/web/app/components/datasets/formatted-text/flavours/edit-slice.tsx @@ -10,10 +10,11 @@ import ActionButton, { ActionButtonState } from '@/app/components/base/action-bu type EditSliceProps = SliceProps<{ label: ReactNode onDelete: () => void + onClick?: () => void }> export const EditSlice: FC = (props) => { - const { label, className, text, onDelete, ...rest } = props + const { label, className, text, onDelete, onClick, ...rest } = props const [delBtnShow, setDelBtnShow] = useState(false) const [isDelBtnHover, setDelBtnHover] = useState(false) @@ -35,7 +36,10 @@ export const EditSlice: FC = (props) => { const isDestructive = delBtnShow && isDelBtnHover return ( -
+
{ + e.stopPropagation() + onClick?.() + }}> { - return get(`/datasets/${datasetId}/documents/${documentId}/segment/${segmentId}/child_chunks`, { params }) + return get(`/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`, { params }) }, enabled: !disable, initialData: disable ? { data: [], total: 0, page: 1, total_pages: 0, limit: 10 } : undefined, @@ -97,7 +97,7 @@ export const useDeleteChildSegment = () => { mutationKey: [NAME_SPACE, 'childChunk', 'delete'], mutationFn: (payload: { datasetId: string; documentId: string; segmentId: string; childChunkId: string }) => { const { datasetId, documentId, segmentId, childChunkId } = payload - return del(`/datasets/${datasetId}/documents/${documentId}/segment/${segmentId}/child_chunks/${childChunkId}`) + return del(`/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`) }, }) } @@ -107,7 +107,17 @@ export const useAddChildSegment = () => { mutationKey: [NAME_SPACE, 'childChunk', 'add'], mutationFn: (payload: { datasetId: string; documentId: string; segmentId: string; body: { content: string } }) => { const { datasetId, documentId, segmentId, body } = payload - return post<{ data: ChildChunkDetail }>(`/datasets/${datasetId}/documents/${documentId}/segment/${segmentId}/child_chunks`, { body }) + return post<{ data: ChildChunkDetail }>(`/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`, { body }) + }, + }) +} + +export const useUpdateChildSegment = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'childChunk', 'update'], + mutationFn: (payload: { datasetId: string; documentId: string; segmentId: string; childChunkId: string; body: { content: string } }) => { + const { datasetId, documentId, segmentId, childChunkId, body } = payload + return patch<{ data: ChildChunkDetail }>(`/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`, { body }) }, }) }