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/child-segment-list.tsx b/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx index b0d8bc7f02..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 @@ -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.updated_at !== childChunk.created_at return {}} - className='' + onDelete={() => onDelete?.(childChunk.segment_id, childChunk.id)} + onClick={() => onClickSlice?.(childChunk)} /> })} - +
: null} 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) ? + + + 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 { + const res = await addChildSegment({ datasetId, documentId, segmentId: chunkId, body: params }) + notify({ + type: 'success', + message: t('datasetDocuments.segment.childChunkAdded'), + className: `!w-[296px] !bottom-0 ${appSidebarExpand === 'expand' ? '!left-[216px]' : '!left-14'} + !top-auto !right-auto !mb-[52px] !ml-11`, + customComponent: isFullDocMode && CustomButton, + }) + handleCancel('add') + if (isFullDocMode) { + refreshTimer.current = setTimeout(() => { + onSave() + }, 3000) + } + else { + onSave(res.data) + } + } + 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-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/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/documents/detail/new-segment.tsx b/web/app/components/datasets/documents/detail/new-segment.tsx index 2806ffbcb1..cadab9ce84 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' @@ -54,7 +55,7 @@ const NewSegmentModal: FC = ({ clearTimeout(refreshTimer.current) viewNewlyAddedChunk() }}> - {t('datasetDocuments.segment.viewAddedChunk')} + {t('common.operation.view')} @@ -118,7 +119,7 @@ const NewSegmentModal: FC = ({ }
- · + {formatNumber(question.length)} {t('datasetDocuments.segment.characters')}
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 }) }, }) } 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) +}