From b4a6ec077fddfe72515a30fa05d1b311dd9a6cbb Mon Sep 17 00:00:00 2001 From: twwu Date: Tue, 10 Dec 2024 18:01:38 +0800 Subject: [PATCH] feat: add functionality to regenerate child chunks and enhance UI components for segment management --- .../completed/common/action-buttons.tsx | 81 ++++++++++ .../detail/completed/common/add-another.tsx | 32 ++++ .../detail/completed/common/chunk-content.tsx | 64 ++++++++ .../detail/completed/common/keywords.tsx | 47 ++++++ .../documents/detail/completed/index.tsx | 28 +++- .../detail/completed/segment-detail.tsx | 141 ++++------------ .../datasets/documents/detail/new-segment.tsx | 150 +++++------------- web/i18n/en-US/dataset-documents.ts | 1 + web/i18n/zh-Hans/dataset-documents.ts | 1 + web/models/datasets.ts | 1 + 10 files changed, 312 insertions(+), 234 deletions(-) create mode 100644 web/app/components/datasets/documents/detail/completed/common/action-buttons.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/common/add-another.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/common/chunk-content.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/common/keywords.tsx 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 new file mode 100644 index 0000000000..aa3336a0ad --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/action-buttons.tsx @@ -0,0 +1,81 @@ +import React, { type FC, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useKeyPress } from 'ahooks' +import { useDocumentContext } from '../../index' +import Button from '@/app/components/base/button' +import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils' + +type IActionButtonsProps = { + handleCancel: () => void + handleSave: (needRegenerate: boolean) => void + loading: boolean + actionType?: 'edit' | 'add' +} + +const ActionButtons: FC = ({ + handleCancel, + handleSave, + loading, + actionType = 'edit', +}) => { + const { t } = useTranslation() + const [mode, parentMode] = useDocumentContext(s => [s.mode, s.parentMode]) + + useKeyPress(['esc'], (e) => { + e.preventDefault() + handleCancel() + }) + + useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.s`, (e) => { + if (loading) + return + e.preventDefault() + handleSave(false) + } + , { exactMatch: true, useCapture: true }) + + const isParentChildParagraphMode = useMemo(() => { + return mode === 'hierarchical' && parentMode === 'paragraph' + }, [mode, parentMode]) + + return ( +
+ + {(isParentChildParagraphMode && actionType === 'edit') + ? + : null + } + +
+ ) +} + +ActionButtons.displayName = 'ActionButtons' + +export default React.memo(ActionButtons) diff --git a/web/app/components/datasets/documents/detail/completed/common/add-another.tsx b/web/app/components/datasets/documents/detail/completed/common/add-another.tsx new file mode 100644 index 0000000000..444560e55f --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/add-another.tsx @@ -0,0 +1,32 @@ +import React, { type FC } from 'react' +import { useTranslation } from 'react-i18next' +import classNames from '@/utils/classnames' +import Checkbox from '@/app/components/base/checkbox' + +type AddAnotherProps = { + className?: string + isChecked: boolean + onCheck: () => void +} + +const AddAnother: FC = ({ + className, + isChecked, + onCheck, +}) => { + const { t } = useTranslation() + + return ( +
+ + {t('datasetDocuments.segment.addAnother')} +
+ ) +} + +export default React.memo(AddAnother) 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 new file mode 100644 index 0000000000..3d55427cd3 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/chunk-content.tsx @@ -0,0 +1,64 @@ +import React, { type FC } from 'react' +import { useTranslation } from 'react-i18next' +import AutoHeightTextarea from '@/app/components/base/auto-height-textarea/common' + +type IChunkContentProps = { + question: string + answer: string + onQuestionChange: (question: string) => void + onAnswerChange: (answer: string) => void + isEditMode?: boolean + docForm: string +} + +const ChunkContent: FC = ({ + question, + answer, + onQuestionChange, + onAnswerChange, + isEditMode, + docForm, +}) => { + const { t } = useTranslation() + + if (docForm === 'qa_model') { + return ( + <> +
QUESTION
+ onQuestionChange(e.target.value)} + disabled={!isEditMode} + /> +
ANSWER
+ onAnswerChange(e.target.value)} + disabled={!isEditMode} + autoFocus + /> + + ) + } + + return ( + onQuestionChange(e.target.value)} + disabled={!isEditMode} + autoFocus + /> + ) +} + +ChunkContent.displayName = 'ChunkContent' + +export default React.memo(ChunkContent) diff --git a/web/app/components/datasets/documents/detail/completed/common/keywords.tsx b/web/app/components/datasets/documents/detail/completed/common/keywords.tsx new file mode 100644 index 0000000000..8c85ec1378 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/common/keywords.tsx @@ -0,0 +1,47 @@ +import React, { type FC } from 'react' +import { useTranslation } from 'react-i18next' +import classNames from '@/utils/classnames' +import type { SegmentDetailModel } from '@/models/datasets' +import TagInput from '@/app/components/base/tag-input' + +type IKeywordsProps = { + segInfo?: Partial & { id: string } + className?: string + keywords: string[] + onKeywordsChange: (keywords: string[]) => void + isEditMode?: boolean + actionType?: 'edit' | 'add' | 'view' +} + +const Keywords: FC = ({ + segInfo, + className, + keywords, + onKeywordsChange, + isEditMode, + actionType = 'view', +}) => { + const { t } = useTranslation() + return ( +
+
{t('datasetDocuments.segment.keywords')}
+
+ {(!segInfo?.keywords?.length && actionType === 'view') + ? '-' + : ( + onKeywordsChange(newKeywords)} + disableAdd={!isEditMode} + disableRemove={!isEditMode || (keywords.length === 1)} + /> + ) + } +
+
+ ) +} + +Keywords.displayName = 'Keywords' + +export default React.memo(Keywords) diff --git a/web/app/components/datasets/documents/detail/completed/index.tsx b/web/app/components/datasets/documents/detail/completed/index.tsx index 48b136c047..c3f513e8aa 100644 --- a/web/app/components/datasets/documents/detail/completed/index.tsx +++ b/web/app/components/datasets/documents/detail/completed/index.tsx @@ -51,7 +51,7 @@ export const useSegmentListContext = (selector: (value: SegmentListContextValue) return useContextSelector(SegmentListContext, selector) } -export const SegmentIndexTag: FC<{ positionId?: string | number; label?: string; className?: string }> = ({ positionId, label, className }) => { +export const SegmentIndexTag: FC<{ positionId?: string | number; label?: string; className?: string }> = React.memo(({ positionId, label, className }) => { const localPositionId = useMemo(() => { const positionIdStr = String(positionId) if (positionIdStr.length >= 3) @@ -66,7 +66,9 @@ export const SegmentIndexTag: FC<{ positionId?: string | number; label?: string; ) -} +}) + +SegmentIndexTag.displayName = 'SegmentIndexTag' type ICompletedProps = { embeddingAvailable: boolean @@ -240,7 +242,13 @@ const Completed: FC = ({ ) }, []) - const handleUpdateSegment = async (segmentId: string, question: string, answer: string, keywords: string[]) => { + const handleUpdateSegment = async ( + segmentId: string, + question: string, + answer: string, + keywords: string[], + needRegenerate: boolean, + ) => { const params: SegmentUpdater = { content: '' } if (docForm === 'qa_model') { if (!question.trim()) @@ -261,6 +269,9 @@ const Completed: FC = ({ if (keywords.length) params.keywords = keywords + if (needRegenerate) + params.regenerate_child_chunks = needRegenerate + try { eventEmitter?.emit('update-segment') const res = await updateSegment({ datasetId, documentId, segmentId, body: params }) @@ -275,6 +286,7 @@ const Completed: FC = ({ 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]) @@ -318,12 +330,13 @@ const Completed: FC = ({ const total = segmentListData?.total || 0 const newPage = Math.ceil((total + 1) / limit) needScrollToBottom.current = true - if (newPage > totalPages) + if (newPage > totalPages) { setCurrentPage(totalPages + 1) - else if (currentPage === totalPages) + } + else { resetList() - else - setCurrentPage(totalPages) + currentPage !== totalPages && setCurrentPage(totalPages) + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [segmentListData, limit, currentPage]) @@ -407,6 +420,7 @@ const Completed: FC = ({ > & { id: string } - onUpdate: (segmentId: string, q: string, a: string, k: string[]) => void + onUpdate: (segmentId: string, q: string, a: string, k: string[], needRegenerate: boolean) => void onCancel: () => void isEditMode?: boolean + docForm: string } /** @@ -31,6 +31,7 @@ const SegmentDetail: FC = ({ onUpdate, onCancel, isEditMode, + docForm, }) => { const { t } = useTranslation() const [question, setQuestion] = useState(segInfo?.content || '') @@ -39,6 +40,7 @@ const SegmentDetail: FC = ({ const { eventEmitter } = useEventEmitterContextContext() const [loading, setLoading] = useState(false) const [fullScreen, toggleFullScreen] = useSegmentListContext(s => [s.fullScreen, s.toggleFullScreen]) + const [mode] = useDocumentContext(s => s.mode) eventEmitter?.useSubscription((v) => { if (v === 'update-segment') @@ -53,110 +55,9 @@ const SegmentDetail: FC = ({ setAnswer(segInfo?.answer || '') setKeywords(segInfo?.keywords || []) } - const handleSave = () => { - onUpdate(segInfo?.id || '', question, answer, keywords) - } - useKeyPress(['esc'], (e) => { - e.preventDefault() - handleCancel() - }) - - useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.s`, (e) => { - if (loading) - return - e.preventDefault() - handleSave() - } - , { exactMatch: true, useCapture: true }) - - const renderContent = () => { - if (segInfo?.answer) { - return ( - <> -
QUESTION
- setQuestion(e.target.value)} - disabled={!isEditMode} - /> -
ANSWER
- setAnswer(e.target.value)} - disabled={!isEditMode} - autoFocus - /> - - ) - } - - return ( - setQuestion(e.target.value)} - disabled={!isEditMode} - autoFocus - /> - ) - } - - const renderActionButtons = () => { - return ( -
- - -
- ) - } - - const renderKeywords = () => { - return ( -
-
{t('datasetDocuments.segment.keywords')}
-
- {!segInfo?.keywords?.length - ? '-' - : ( - setKeywords(newKeywords)} - disableAdd={!isEditMode} - disableRemove={!isEditMode || (keywords.length === 1)} - /> - ) - } -
-
- ) + const handleSave = (needRegenerate = false) => { + onUpdate(segInfo?.id || '', question, answer, keywords, needRegenerate) } return ( @@ -173,7 +74,7 @@ const SegmentDetail: FC = ({
{isEditMode && fullScreen && ( <> - {renderActionButtons()} + )} @@ -187,13 +88,27 @@ const SegmentDetail: FC = ({
- {renderContent()} + setQuestion(question)} + onAnswerChange={answer => setAnswer(answer)} + isEditMode={isEditMode} + />
- {renderKeywords()} + {mode === 'custom' && setKeywords(keywords)} + />}
{isEditMode && !fullScreen && (
- {renderActionButtons()} +
)} diff --git a/web/app/components/datasets/documents/detail/new-segment.tsx b/web/app/components/datasets/documents/detail/new-segment.tsx index a6f334e6e2..ae88900cf3 100644 --- a/web/app/components/datasets/documents/detail/new-segment.tsx +++ b/web/app/components/datasets/documents/detail/new-segment.tsx @@ -4,21 +4,20 @@ import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import { useParams } from 'next/navigation' import { RiCloseLine, RiExpandDiagonalLine } from '@remixicon/react' -import { useKeyPress } from 'ahooks' import { useShallow } from 'zustand/react/shallow' import { SegmentIndexTag, useSegmentListContext } from './completed' +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 { useDocumentContext } from './index' import { useStore as useAppStore } from '@/app/components/app/store' -import Button from '@/app/components/base/button' -import AutoHeightTextarea from '@/app/components/base/auto-height-textarea/common' import { ToastContext } from '@/app/components/base/toast' import type { SegmentUpdater } from '@/models/datasets' import { addSegment } from '@/service/datasets' -import TagInput from '@/app/components/base/tag-input' import classNames from '@/utils/classnames' import { formatNumber } from '@/utils/format' -import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils' import Divider from '@/app/components/base/divider' -import Checkbox from '@/app/components/base/checkbox' type NewSegmentModalProps = { onCancel: () => void @@ -40,8 +39,9 @@ const NewSegmentModal: FC = ({ const { datasetId, documentId } = useParams<{ datasetId: string; documentId: string }>() const [keywords, setKeywords] = useState([]) const [loading, setLoading] = useState(false) - const [addAnother, setAnother] = useState(true) + const [addAnother, setAddAnother] = useState(true) const [fullScreen, toggleFullScreen] = useSegmentListContext(s => [s.fullScreen, s.toggleFullScreen]) + const [mode] = useDocumentContext(s => s.mode) const { appSidebarExpand } = useAppStore(useShallow(state => ({ appSidebarExpand: state.appSidebarExpand, }))) @@ -106,107 +106,6 @@ const NewSegmentModal: FC = ({ } } - useKeyPress(['esc'], (e) => { - e.preventDefault() - handleCancel() - }) - - useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.s`, (e) => { - if (loading) - return - e.preventDefault() - handleSave() - } - , { exactMatch: true, useCapture: true }) - - const renderContent = () => { - if (docForm === 'qa_model') { - return ( - <> -
QUESTION
- setQuestion(e.target.value)} - autoFocus - /> -
ANSWER
- setAnswer(e.target.value)} - /> - - ) - } - - return ( - setQuestion(e.target.value)} - autoFocus - /> - ) - } - - const renderActionButtons = () => { - return ( -
- - -
- ) - } - - const AddAnotherCheckBox = () => { - return ( -
- setAnother(!addAnother)} - /> - {t('datasetDocuments.segment.addAnother')} -
- ) - } - - const renderKeywords = () => { - return ( -
-
{t('datasetDocuments.segment.keywords')}
-
- setKeywords(newKeywords)} /> -
-
- ) - } - return (
@@ -225,8 +124,13 @@ const NewSegmentModal: FC = ({
{fullScreen && ( <> - {AddAnotherCheckBox()} - {renderActionButtons()} + setAddAnother(!addAnother)} /> + )} @@ -240,14 +144,32 @@ const NewSegmentModal: FC = ({
- {renderContent()} + setQuestion(question)} + onAnswerChange={answer => setAnswer(answer)} + isEditMode={true} + />
- {renderKeywords()} + {mode === 'custom' && setKeywords(keywords)} + />}
{!fullScreen && (
- {AddAnotherCheckBox()} - {renderActionButtons()} + setAddAnother(!addAnother)} /> +
)}
diff --git a/web/i18n/en-US/dataset-documents.ts b/web/i18n/en-US/dataset-documents.ts index 63f49667a5..470d62252c 100644 --- a/web/i18n/en-US/dataset-documents.ts +++ b/web/i18n/en-US/dataset-documents.ts @@ -353,6 +353,7 @@ const translation = { delete: 'Delete this chunk ?', chunkAdded: '1 chunk added', viewAddedChunk: 'View', + saveAndRegenerate: 'Save & Regenerate Child Chunks', }, } diff --git a/web/i18n/zh-Hans/dataset-documents.ts b/web/i18n/zh-Hans/dataset-documents.ts index 81cbbd41ad..824da98850 100644 --- a/web/i18n/zh-Hans/dataset-documents.ts +++ b/web/i18n/zh-Hans/dataset-documents.ts @@ -351,6 +351,7 @@ const translation = { delete: '删除这个分段?', chunkAdded: '新增一个分段', viewAddedChunk: '查看', + saveAndRegenerate: '保存并重新生成子分段', }, } diff --git a/web/models/datasets.ts b/web/models/datasets.ts index f71c314995..10495f19e7 100644 --- a/web/models/datasets.ts +++ b/web/models/datasets.ts @@ -561,6 +561,7 @@ export type SegmentUpdater = { content: string answer?: string keywords?: string[] + regenerate_child_chunks?: boolean } export enum DocForm {