From 36778a4ebe579936f655ec939b8c87c866bfc7a1 Mon Sep 17 00:00:00 2001 From: twwu Date: Thu, 12 Dec 2024 13:39:45 +0800 Subject: [PATCH 1/3] feat: add and delete child chunks --- .../detail/completed/child-segment-list.tsx | 29 ++-- .../detail/completed/common/chunk-content.tsx | 6 +- .../detail/completed/display-toggle.tsx | 12 +- .../documents/detail/completed/index.tsx | 112 +++++++++++-- .../detail/completed/new-child-segment.tsx | 151 ++++++++++++++++++ .../detail/completed/segment-card.tsx | 13 +- .../detail/completed/segment-list.tsx | 11 +- .../formatted-text/flavours/edit-slice.tsx | 8 +- web/service/knowledge/use-segment.ts | 16 +- 9 files changed, 321 insertions(+), 37 deletions(-) create mode 100644 web/app/components/datasets/documents/detail/completed/new-child-segment.tsx 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 }) }, }) } From c1d1960215fd45d55bbb1dc15dc52b623a3ebd2d Mon Sep 17 00:00:00 2001 From: twwu Date: Thu, 12 Dec 2024 14:45:00 +0800 Subject: [PATCH 2/3] feat: enhance time formatting and add child segment detail component --- .../detail/completed/child-segment-detail.tsx | 118 ++++++++++++++++++ .../completed/common/action-buttons.tsx | 4 +- .../documents/detail/completed/common/dot.tsx | 2 +- .../documents/detail/completed/index.tsx | 83 +++++++++++- .../detail/completed/new-child-segment.tsx | 5 +- .../detail/completed/segment-detail.tsx | 3 +- .../datasets/documents/detail/new-segment.tsx | 3 +- web/utils/time.ts | 7 ++ 8 files changed, 215 insertions(+), 10 deletions(-) create mode 100644 web/app/components/datasets/documents/detail/completed/child-segment-detail.tsx diff --git a/web/app/components/datasets/documents/detail/completed/child-segment-detail.tsx b/web/app/components/datasets/documents/detail/completed/child-segment-detail.tsx new file mode 100644 index 0000000000..a305c48feb --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/child-segment-detail.tsx @@ -0,0 +1,118 @@ +import React, { type FC, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiCloseLine, + RiExpandDiagonalLine, +} from '@remixicon/react' +import ActionButtons from './common/action-buttons' +import ChunkContent from './common/chunk-content' +import Dot from './common/dot' +import { SegmentIndexTag } from './common/segment-index-tag' +import { useSegmentListContext } from './index' +import type { ChildChunkDetail } from '@/models/datasets' +import { useEventEmitterContextContext } from '@/context/event-emitter' +import { formatNumber } from '@/utils/format' +import classNames from '@/utils/classnames' +import Divider from '@/app/components/base/divider' +import { formatTime } from '@/utils/time' + +type IChildSegmentDetailProps = { + chunkId: string + childChunkInfo?: Partial & { id: string } + onUpdate: (segmentId: string, childChunkId: string, content: string) => void + onCancel: () => void + docForm: string +} + +/** + * Show all the contents of the segment + */ +const ChildSegmentDetail: FC = ({ + chunkId, + childChunkInfo, + onUpdate, + onCancel, + docForm, +}) => { + const { t } = useTranslation() + const [content, setContent] = useState(childChunkInfo?.content || '') + const { eventEmitter } = useEventEmitterContextContext() + const [loading, setLoading] = useState(false) + const [fullScreen, toggleFullScreen] = useSegmentListContext(s => [s.fullScreen, s.toggleFullScreen]) + + eventEmitter?.useSubscription((v) => { + if (v === 'update-child-segment') + setLoading(true) + if (v === 'update-child-segment-done') + setLoading(false) + }) + + const handleCancel = () => { + onCancel() + setContent(childChunkInfo?.content || '') + } + + const handleSave = () => { + onUpdate(chunkId, childChunkInfo?.id || '', content) + } + + return ( +
+
+
+
{'Edit Child Chunk'}
+
+ + + {formatNumber(content.length)} {t('datasetDocuments.segment.characters')} + + + {`Edited at ${formatTime({ date: (childChunkInfo?.created_at ?? 0) * 1000, dateFormat: 'MM/DD/YYYY h:mm:ss' })}`} + +
+
+
+ {fullScreen && ( + <> + + + + )} +
+ +
+
+ +
+
+
+
+
+ setContent(content)} + isEditMode={true} + /> +
+
+ {!fullScreen && ( +
+ +
+ )} +
+ ) +} + +export default React.memo(ChildSegmentDetail) diff --git a/web/app/components/datasets/documents/detail/completed/common/action-buttons.tsx b/web/app/components/datasets/documents/detail/completed/common/action-buttons.tsx index 04c829dfda..5ade8267c2 100644 --- a/web/app/components/datasets/documents/detail/completed/common/action-buttons.tsx +++ b/web/app/components/datasets/documents/detail/completed/common/action-buttons.tsx @@ -11,6 +11,7 @@ type IActionButtonsProps = { loading: boolean actionType?: 'edit' | 'add' handleRegeneration?: () => void + isChildChunk?: boolean } const ActionButtons: FC = ({ @@ -19,6 +20,7 @@ const ActionButtons: FC = ({ loading, actionType = 'edit', handleRegeneration, + isChildChunk = false, }) => { const { t } = useTranslation() const [mode, parentMode] = useDocumentContext(s => [s.mode, s.parentMode]) @@ -50,7 +52,7 @@ const ActionButtons: FC = ({ ESC
- {(isParentChildParagraphMode && actionType === 'edit') + {(isParentChildParagraphMode && actionType === 'edit' && !isChildChunk) ?
- · + {formatNumber(content.length)} {t('datasetDocuments.segment.characters')}
@@ -111,6 +112,7 @@ const NewChildSegmentModal: FC = ({ handleSave={handleSave} loading={loading} actionType='add' + isChildChunk={true} /> @@ -141,6 +143,7 @@ const NewChildSegmentModal: FC = ({ handleSave={handleSave} loading={loading} actionType='add' + isChildChunk={true} /> )} diff --git a/web/app/components/datasets/documents/detail/completed/segment-detail.tsx b/web/app/components/datasets/documents/detail/completed/segment-detail.tsx index a55f57851a..e4d526a3cf 100644 --- a/web/app/components/datasets/documents/detail/completed/segment-detail.tsx +++ b/web/app/components/datasets/documents/detail/completed/segment-detail.tsx @@ -10,6 +10,7 @@ import ChunkContent from './common/chunk-content' import Keywords from './common/keywords' import RegenerationModal from './common/regeneration-modal' import { SegmentIndexTag } from './common/segment-index-tag' +import Dot from './common/dot' import { useSegmentListContext } from './index' import type { SegmentDetailModel } from '@/models/datasets' import { useEventEmitterContextContext } from '@/context/event-emitter' @@ -86,7 +87,7 @@ const SegmentDetail: FC = ({
{isEditMode ? 'Edit Chunk' : 'Chunk Detail'}
- · + {formatNumber(isEditMode ? question.length : segInfo?.word_count as number)} {t('datasetDocuments.segment.characters')}
diff --git a/web/app/components/datasets/documents/detail/new-segment.tsx b/web/app/components/datasets/documents/detail/new-segment.tsx index 2806ffbcb1..43a76cb08d 100644 --- a/web/app/components/datasets/documents/detail/new-segment.tsx +++ b/web/app/components/datasets/documents/detail/new-segment.tsx @@ -11,6 +11,7 @@ import ActionButtons from './completed/common/action-buttons' import Keywords from './completed/common/keywords' import ChunkContent from './completed/common/chunk-content' import AddAnother from './completed/common/add-another' +import Dot from './completed/common/dot' import { useDocumentContext } from './index' import { useStore as useAppStore } from '@/app/components/app/store' import { ToastContext } from '@/app/components/base/toast' @@ -118,7 +119,7 @@ const NewSegmentModal: FC = ({ }
- · + {formatNumber(question.length)} {t('datasetDocuments.segment.characters')}
diff --git a/web/utils/time.ts b/web/utils/time.ts index 732a91047a..ff2e38321f 100644 --- a/web/utils/time.ts +++ b/web/utils/time.ts @@ -1,5 +1,12 @@ import dayjs, { type ConfigType } from 'dayjs' +import utc from 'dayjs/plugin/utc' + +dayjs.extend(utc) export const isAfter = (date: ConfigType, compare: ConfigType) => { return dayjs(date).isAfter(dayjs(compare)) } + +export const formatTime = ({ date, dateFormat }: { date: ConfigType; dateFormat: string }) => { + return dayjs(date).format(dateFormat) +} From 5a1159f9ab9a91143be76705746f210437fc6b0e Mon Sep 17 00:00:00 2001 From: twwu Date: Thu, 12 Dec 2024 15:33:54 +0800 Subject: [PATCH 3/3] feat: update child chunk handling and improve UI interactions --- .../detail/completed/child-segment-list.tsx | 2 +- .../documents/detail/completed/index.tsx | 35 +++++++++++++------ .../detail/completed/new-child-segment.tsx | 32 +++++++++++------ .../datasets/documents/detail/new-segment.tsx | 2 +- web/i18n/en-US/common.ts | 1 + web/i18n/en-US/dataset-documents.ts | 2 +- web/i18n/zh-Hans/common.ts | 1 + web/i18n/zh-Hans/dataset-documents.ts | 2 +- web/models/datasets.ts | 1 + 9 files changed, 53 insertions(+), 25 deletions(-) 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 f4e14b9b76..34cbd1c96e 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 @@ -91,7 +91,7 @@ const ChildSegmentList: FC = ({ {isParagraphMode && }
{childChunks.map((childChunk) => { - const edited = childChunk.type === 'customized' + const edited = childChunk.updated_at !== childChunk.created_at return = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [datasetId, documentId, selectedSegmentIds]) - const handleUpdateSegment = async ( + const handleUpdateSegment = useCallback(async ( segmentId: string, question: string, answer: string, @@ -283,7 +283,8 @@ const Completed: FC = ({ finally { eventEmitter?.emit('update-segment-done') } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [segments, datasetId, documentId]) useEffect(() => { if (importStatus === ProcessStatus.COMPLETED) @@ -360,13 +361,27 @@ const Completed: FC = ({ }, ) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [datasetId, documentId]) + }, [datasetId, documentId, parentMode]) const handleAddNewChildChunk = useCallback((parentChunkId: string) => { setShowNewChildSegmentModal(true) setCurrChunkId(parentChunkId) }, []) + const onSaveNewChildChunk = useCallback((newChildChunk?: ChildChunkDetail) => { + if (parentMode === 'paragraph') { + for (const seg of segments) { + if (seg.id === currChunkId) + seg.child_chunks?.push(newChildChunk!) + } + setSegments([...segments]) + } + else { + resetChildList() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [parentMode, currChunkId, segments]) + const viewNewlyAddedChildChunk = useCallback(() => { const totalPages = childChunkListData?.total_pages || 0 const total = childChunkListData?.total || 0 @@ -394,7 +409,7 @@ const Completed: FC = ({ const { mutateAsync: updateChildSegment } = useUpdateChildSegment() - const handleUpdateChildChunk = async ( + const handleUpdateChildChunk = useCallback(async ( segmentId: string, childChunkId: string, content: string, @@ -418,6 +433,7 @@ const Completed: FC = ({ childSeg.content = res.data.content childSeg.type = res.data.type childSeg.word_count = res.data.word_count + childSeg.updated_at = res.data.updated_at } } } @@ -430,6 +446,7 @@ const Completed: FC = ({ childSeg.content = res.data.content childSeg.type = res.data.type childSeg.word_count = res.data.word_count + childSeg.updated_at = res.data.updated_at } } setChildSegments([...childSegments]) @@ -438,7 +455,8 @@ const Completed: FC = ({ finally { eventEmitter?.emit('update-child-segment-done') } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [segments, childSegments, datasetId, documentId, parentMode]) return ( = ({ setShowNewChildSegmentModal(false) setFullScreen(false) }} - onSave={() => { - if (parentMode === 'paragraph') - resetList() - else - resetChildList() - }} + onSave={onSaveNewChildChunk} viewNewlyAddedChildChunk={viewNewlyAddedChildChunk} /> diff --git a/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx b/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx index 1f8133fe5c..98d498560e 100644 --- a/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx +++ b/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx @@ -1,10 +1,11 @@ -import { memo, useRef, useState } from 'react' +import { memo, useMemo, useRef, useState } from 'react' import type { FC } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import { useParams } from 'next/navigation' import { RiCloseLine, RiExpandDiagonalLine } from '@remixicon/react' import { useShallow } from 'zustand/react/shallow' +import { useDocumentContext } from '../index' import { SegmentIndexTag } from './common/segment-index-tag' import ActionButtons from './common/action-buttons' import ChunkContent from './common/chunk-content' @@ -13,7 +14,7 @@ import Dot from './common/dot' import { useSegmentListContext } from './index' import { useStore as useAppStore } from '@/app/components/app/store' import { ToastContext } from '@/app/components/base/toast' -import type { SegmentUpdater } from '@/models/datasets' +import type { ChildChunkDetail, SegmentUpdater } from '@/models/datasets' import classNames from '@/utils/classnames' import { formatNumber } from '@/utils/format' import Divider from '@/app/components/base/divider' @@ -22,7 +23,7 @@ import { useAddChildSegment } from '@/service/knowledge/use-segment' type NewChildSegmentModalProps = { chunkId: string onCancel: () => void - onSave: () => void + onSave: (ChildChunk?: ChildChunkDetail) => void viewNewlyAddedChildChunk?: () => void } @@ -42,15 +43,21 @@ const NewChildSegmentModal: FC = ({ const { appSidebarExpand } = useAppStore(useShallow(state => ({ appSidebarExpand: state.appSidebarExpand, }))) + const parentMode = useDocumentContext(s => s.parentMode) + const refreshTimer = useRef(null) + const isFullDocMode = useMemo(() => { + return parentMode === 'full-doc' + }, [parentMode]) + const CustomButton = <> @@ -72,18 +79,23 @@ const NewChildSegmentModal: FC = ({ setLoading(true) try { - await addChildSegment({ datasetId, documentId, segmentId: chunkId, body: params }) + const res = await addChildSegment({ datasetId, documentId, segmentId: chunkId, body: params }) notify({ type: 'success', - message: t('datasetDocuments.segment.chunkAdded'), + message: t('datasetDocuments.segment.childChunkAdded'), className: `!w-[296px] !bottom-0 ${appSidebarExpand === 'expand' ? '!left-[216px]' : '!left-14'} !top-auto !right-auto !mb-[52px] !ml-11`, - customComponent: CustomButton, + customComponent: isFullDocMode && CustomButton, }) handleCancel('add') - refreshTimer.current = setTimeout(() => { - onSave() - }, 3000) + if (isFullDocMode) { + refreshTimer.current = setTimeout(() => { + onSave() + }, 3000) + } + else { + onSave(res.data) + } } finally { setLoading(false) diff --git a/web/app/components/datasets/documents/detail/new-segment.tsx b/web/app/components/datasets/documents/detail/new-segment.tsx index 43a76cb08d..cadab9ce84 100644 --- a/web/app/components/datasets/documents/detail/new-segment.tsx +++ b/web/app/components/datasets/documents/detail/new-segment.tsx @@ -55,7 +55,7 @@ const NewSegmentModal: FC = ({ clearTimeout(refreshTimer.current) viewNewlyAddedChunk() }}> - {t('datasetDocuments.segment.viewAddedChunk')} + {t('common.operation.view')} diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index 82fa6ce01e..ebffdbe3a9 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -44,6 +44,7 @@ const translation = { openInNewTab: 'Open in new tab', saveAndRegenerate: 'Save & Regenerate Child Chunks', close: 'Close', + view: 'View', viewMore: 'VIEW MORE', regenerate: 'Regenerate', }, diff --git a/web/i18n/en-US/dataset-documents.ts b/web/i18n/en-US/dataset-documents.ts index e074054815..d9e0bb77e3 100644 --- a/web/i18n/en-US/dataset-documents.ts +++ b/web/i18n/en-US/dataset-documents.ts @@ -353,7 +353,7 @@ const translation = { addAnother: 'Add another', delete: 'Delete this chunk ?', chunkAdded: '1 chunk added', - viewAddedChunk: 'View', + childChunkAdded: '1 child chunk added', regenerationConfirmTitle: 'Do you want to regenerate child chunks?', regenerationConfirmMessage: 'Regenerating child chunks will overwrite the current child chunks, including edited chunks and newly added chunks. The regeneration cannot be undone.', regeneratingTitle: 'Regenerating child chunks', diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 1af56d0cfe..9fdeaa703c 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -44,6 +44,7 @@ const translation = { openInNewTab: '在新标签页打开', saveAndRegenerate: '保存并重新生成子分段', close: '关闭', + view: '查看', viewMore: '查看更多', regenerate: '重新生成', }, diff --git a/web/i18n/zh-Hans/dataset-documents.ts b/web/i18n/zh-Hans/dataset-documents.ts index e06ae3f628..9299ee8acb 100644 --- a/web/i18n/zh-Hans/dataset-documents.ts +++ b/web/i18n/zh-Hans/dataset-documents.ts @@ -351,7 +351,7 @@ const translation = { addAnother: '连续新增', delete: '删除这个分段?', chunkAdded: '新增一个分段', - viewAddedChunk: '查看', + childChunkAdded: '新增一个子分段', regenerationConfirmTitle: '是否需要重新生成子分段?', regenerationConfirmMessage: '重新生成的子分段将会覆盖当前的子分段,包括编辑过的分段和新添加的分段。重新生成操作无法撤销。', regeneratingTitle: '正在生成子分段', diff --git a/web/models/datasets.ts b/web/models/datasets.ts index ac2484ef79..0dbc2938a2 100644 --- a/web/models/datasets.ts +++ b/web/models/datasets.ts @@ -627,6 +627,7 @@ export type ChildChunkDetail = { content: string word_count: number created_at: number + updated_at: number type: ChildChunkType }