mirror of
https://github.com/langgenius/dify.git
synced 2026-01-07 23:04:12 +00:00
feat: add functionality to regenerate child chunks and enhance UI components for segment management
This commit is contained in:
@@ -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<IActionButtonsProps> = ({
|
||||
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 (
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<div className='flex items-center gap-x-1'>
|
||||
<span className='text-components-button-secondary-text system-sm-medium'>{t('common.operation.cancel')}</span>
|
||||
<span className='px-[1px] bg-components-kbd-bg-gray rounded-[4px] text-text-tertiary system-kbd'>ESC</span>
|
||||
</div>
|
||||
</Button>
|
||||
{(isParentChildParagraphMode && actionType === 'edit')
|
||||
? <Button
|
||||
onClick={handleSave.bind(null, true)}
|
||||
disabled={loading}
|
||||
>
|
||||
<span className='text-components-button-secondary-text system-sm-medium'>
|
||||
{t('datasetDocuments.segment.saveAndRegenerate')}
|
||||
</span>
|
||||
</Button>
|
||||
: null
|
||||
}
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSave.bind(null, false)}
|
||||
disabled={loading}
|
||||
>
|
||||
<div className='flex items-center gap-x-1'>
|
||||
<span className='text-components-button-primary-text'>{t('common.operation.save')}</span>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
<span className='w-4 h-4 bg-components-kbd-bg-white rounded-[4px] text-text-primary-on-surface system-kbd capitalize'>{getKeyboardKeyNameBySystem('ctrl')}</span>
|
||||
<span className='w-4 h-4 bg-components-kbd-bg-white rounded-[4px] text-text-primary-on-surface system-kbd'>S</span>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ActionButtons.displayName = 'ActionButtons'
|
||||
|
||||
export default React.memo(ActionButtons)
|
||||
@@ -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<AddAnotherProps> = ({
|
||||
className,
|
||||
isChecked,
|
||||
onCheck,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={classNames('flex items-center gap-x-1 pl-1', className)}>
|
||||
<Checkbox
|
||||
key='add-another-checkbox'
|
||||
className='shrink-0'
|
||||
checked={isChecked}
|
||||
onCheck={onCheck}
|
||||
/>
|
||||
<span className='text-text-tertiary system-xs-medium'>{t('datasetDocuments.segment.addAnother')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AddAnother)
|
||||
@@ -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<IChunkContentProps> = ({
|
||||
question,
|
||||
answer,
|
||||
onQuestionChange,
|
||||
onAnswerChange,
|
||||
isEditMode,
|
||||
docForm,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (docForm === 'qa_model') {
|
||||
return (
|
||||
<>
|
||||
<div className='mb-1 text-xs font-medium text-gray-500'>QUESTION</div>
|
||||
<AutoHeightTextarea
|
||||
outerClassName='mb-4'
|
||||
className='leading-6 text-md text-gray-800'
|
||||
value={question}
|
||||
placeholder={t('datasetDocuments.segment.questionPlaceholder') || ''}
|
||||
onChange={e => onQuestionChange(e.target.value)}
|
||||
disabled={!isEditMode}
|
||||
/>
|
||||
<div className='mb-1 text-xs font-medium text-gray-500'>ANSWER</div>
|
||||
<AutoHeightTextarea
|
||||
outerClassName='mb-4'
|
||||
className='leading-6 text-md text-gray-800'
|
||||
value={answer}
|
||||
placeholder={t('datasetDocuments.segment.answerPlaceholder') || ''}
|
||||
onChange={e => onAnswerChange(e.target.value)}
|
||||
disabled={!isEditMode}
|
||||
autoFocus
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AutoHeightTextarea
|
||||
className='body-md-regular text-text-secondary tracking-[-0.07px] caret-[#295EFF]'
|
||||
value={question}
|
||||
placeholder={t('datasetDocuments.segment.contentPlaceholder') || ''}
|
||||
onChange={e => onQuestionChange(e.target.value)}
|
||||
disabled={!isEditMode}
|
||||
autoFocus
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
ChunkContent.displayName = 'ChunkContent'
|
||||
|
||||
export default React.memo(ChunkContent)
|
||||
@@ -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<SegmentDetailModel> & { id: string }
|
||||
className?: string
|
||||
keywords: string[]
|
||||
onKeywordsChange: (keywords: string[]) => void
|
||||
isEditMode?: boolean
|
||||
actionType?: 'edit' | 'add' | 'view'
|
||||
}
|
||||
|
||||
const Keywords: FC<IKeywordsProps> = ({
|
||||
segInfo,
|
||||
className,
|
||||
keywords,
|
||||
onKeywordsChange,
|
||||
isEditMode,
|
||||
actionType = 'view',
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className={classNames('flex flex-col', className)}>
|
||||
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('datasetDocuments.segment.keywords')}</div>
|
||||
<div className='text-text-tertiary w-full max-h-[200px] overflow-auto flex flex-wrap gap-1'>
|
||||
{(!segInfo?.keywords?.length && actionType === 'view')
|
||||
? '-'
|
||||
: (
|
||||
<TagInput
|
||||
items={keywords}
|
||||
onChange={newKeywords => onKeywordsChange(newKeywords)}
|
||||
disableAdd={!isEditMode}
|
||||
disableRemove={!isEditMode || (keywords.length === 1)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Keywords.displayName = 'Keywords'
|
||||
|
||||
export default React.memo(Keywords)
|
||||
@@ -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;
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
SegmentIndexTag.displayName = 'SegmentIndexTag'
|
||||
|
||||
type ICompletedProps = {
|
||||
embeddingAvailable: boolean
|
||||
@@ -240,7 +242,13 @@ const Completed: FC<ICompletedProps> = ({
|
||||
)
|
||||
}, [])
|
||||
|
||||
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<ICompletedProps> = ({
|
||||
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<ICompletedProps> = ({
|
||||
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<ICompletedProps> = ({
|
||||
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<ICompletedProps> = ({
|
||||
>
|
||||
<SegmentDetail
|
||||
segInfo={currSegment.segInfo ?? { id: '' }}
|
||||
docForm={docForm}
|
||||
isEditMode={currSegment.isEditMode}
|
||||
onUpdate={handleUpdateSegment}
|
||||
onCancel={onCloseDrawer}
|
||||
|
||||
@@ -4,23 +4,23 @@ import {
|
||||
RiCloseLine,
|
||||
RiExpandDiagonalLine,
|
||||
} from '@remixicon/react'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import { useDocumentContext } from '../index'
|
||||
import ActionButtons from './common/action-buttons'
|
||||
import ChunkContent from './common/chunk-content'
|
||||
import Keywords from './common/keywords'
|
||||
import { SegmentIndexTag, useSegmentListContext } from './index'
|
||||
import type { SegmentDetailModel } from '@/models/datasets'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea/common'
|
||||
import Button from '@/app/components/base/button'
|
||||
import TagInput from '@/app/components/base/tag-input'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
|
||||
type ISegmentDetailProps = {
|
||||
segInfo?: Partial<SegmentDetailModel> & { 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<ISegmentDetailProps> = ({
|
||||
onUpdate,
|
||||
onCancel,
|
||||
isEditMode,
|
||||
docForm,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [question, setQuestion] = useState(segInfo?.content || '')
|
||||
@@ -39,6 +40,7 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
|
||||
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<ISegmentDetailProps> = ({
|
||||
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 (
|
||||
<>
|
||||
<div className='mb-1 text-xs font-medium text-gray-500'>QUESTION</div>
|
||||
<AutoHeightTextarea
|
||||
outerClassName='mb-4'
|
||||
className='leading-6 text-md text-gray-800'
|
||||
value={question}
|
||||
placeholder={t('datasetDocuments.segment.questionPlaceholder') || ''}
|
||||
onChange={e => setQuestion(e.target.value)}
|
||||
disabled={!isEditMode}
|
||||
/>
|
||||
<div className='mb-1 text-xs font-medium text-gray-500'>ANSWER</div>
|
||||
<AutoHeightTextarea
|
||||
outerClassName='mb-4'
|
||||
className='leading-6 text-md text-gray-800'
|
||||
value={answer}
|
||||
placeholder={t('datasetDocuments.segment.answerPlaceholder') || ''}
|
||||
onChange={e => setAnswer(e.target.value)}
|
||||
disabled={!isEditMode}
|
||||
autoFocus
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AutoHeightTextarea
|
||||
className='body-md-regular text-text-secondary tracking-[-0.07px] caret-[#295EFF]'
|
||||
value={question}
|
||||
placeholder={t('datasetDocuments.segment.contentPlaceholder') || ''}
|
||||
onChange={e => setQuestion(e.target.value)}
|
||||
disabled={!isEditMode}
|
||||
autoFocus
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const renderActionButtons = () => {
|
||||
return (
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<div className='flex items-center gap-x-1'>
|
||||
<span className='text-components-button-secondary-text system-sm-medium'>{t('common.operation.cancel')}</span>
|
||||
<span className='px-[1px] bg-components-kbd-bg-gray rounded-[4px] text-text-tertiary system-kbd'>ESC</span>
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
>
|
||||
<div className='flex items-center gap-x-1'>
|
||||
<span className='text-components-button-primary-text'>{t('common.operation.save')}</span>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
<span className='w-4 h-4 bg-components-kbd-bg-white rounded-[4px] text-text-primary-on-surface system-kbd capitalize'>{getKeyboardKeyNameBySystem('ctrl')}</span>
|
||||
<span className='w-4 h-4 bg-components-kbd-bg-white rounded-[4px] text-text-primary-on-surface system-kbd'>S</span>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderKeywords = () => {
|
||||
return (
|
||||
<div className={classNames('flex flex-col', fullScreen ? 'w-1/5' : '')}>
|
||||
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('datasetDocuments.segment.keywords')}</div>
|
||||
<div className='text-text-tertiary w-full max-h-[200px] overflow-auto flex flex-wrap gap-1'>
|
||||
{!segInfo?.keywords?.length
|
||||
? '-'
|
||||
: (
|
||||
<TagInput
|
||||
items={keywords}
|
||||
onChange={newKeywords => setKeywords(newKeywords)}
|
||||
disableAdd={!isEditMode}
|
||||
disableRemove={!isEditMode || (keywords.length === 1)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
const handleSave = (needRegenerate = false) => {
|
||||
onUpdate(segInfo?.id || '', question, answer, keywords, needRegenerate)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -173,7 +74,7 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
|
||||
<div className='flex items-center'>
|
||||
{isEditMode && fullScreen && (
|
||||
<>
|
||||
{renderActionButtons()}
|
||||
<ActionButtons handleCancel={handleCancel} handleSave={handleSave} loading={loading} />
|
||||
<Divider type='vertical' className='h-3.5 bg-divider-regular ml-4 mr-2' />
|
||||
</>
|
||||
)}
|
||||
@@ -187,13 +88,27 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
|
||||
</div>
|
||||
<div className={classNames('flex grow overflow-hidden', fullScreen ? 'w-full flex-row justify-center px-6 pt-6 gap-x-8 mx-auto' : 'flex-col gap-y-1 py-3 px-4')}>
|
||||
<div className={classNames('break-all overflow-y-auto whitespace-pre-line', fullScreen ? 'w-1/2' : 'grow')}>
|
||||
{renderContent()}
|
||||
<ChunkContent
|
||||
docForm={docForm}
|
||||
question={question}
|
||||
answer={answer}
|
||||
onQuestionChange={question => setQuestion(question)}
|
||||
onAnswerChange={answer => setAnswer(answer)}
|
||||
isEditMode={isEditMode}
|
||||
/>
|
||||
</div>
|
||||
{renderKeywords()}
|
||||
{mode === 'custom' && <Keywords
|
||||
className={fullScreen ? 'w-1/5' : ''}
|
||||
actionType={isEditMode ? 'edit' : 'view'}
|
||||
segInfo={segInfo}
|
||||
keywords={keywords}
|
||||
isEditMode={isEditMode}
|
||||
onKeywordsChange={keywords => setKeywords(keywords)}
|
||||
/>}
|
||||
</div>
|
||||
{isEditMode && !fullScreen && (
|
||||
<div className='flex items-center justify-end p-4 pt-3 border-t-[1px] border-t-divider-subtle'>
|
||||
{renderActionButtons()}
|
||||
<ActionButtons handleCancel={handleCancel} handleSave={handleSave} loading={loading} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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<NewSegmentModalProps> = ({
|
||||
const { datasetId, documentId } = useParams<{ datasetId: string; documentId: string }>()
|
||||
const [keywords, setKeywords] = useState<string[]>([])
|
||||
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<NewSegmentModalProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className='mb-1 text-xs font-medium text-gray-500'>QUESTION</div>
|
||||
<AutoHeightTextarea
|
||||
outerClassName='mb-4'
|
||||
className='leading-6 text-md text-gray-800'
|
||||
value={question}
|
||||
placeholder={t('datasetDocuments.segment.questionPlaceholder') || ''}
|
||||
onChange={e => setQuestion(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<div className='mb-1 text-xs font-medium text-gray-500'>ANSWER</div>
|
||||
<AutoHeightTextarea
|
||||
outerClassName='mb-4'
|
||||
className='leading-6 text-md text-gray-800'
|
||||
value={answer}
|
||||
placeholder={t('datasetDocuments.segment.answerPlaceholder') || ''}
|
||||
onChange={e => setAnswer(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AutoHeightTextarea
|
||||
className='body-md-regular text-text-secondary tracking-[-0.07px] caret-[#295EFF]'
|
||||
value={question}
|
||||
placeholder={t('datasetDocuments.segment.contentPlaceholder') || ''}
|
||||
onChange={e => setQuestion(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const renderActionButtons = () => {
|
||||
return (
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<Button
|
||||
className='flex items-center gap-x-1'
|
||||
onClick={handleCancel.bind(null, 'esc')}
|
||||
>
|
||||
<span className='text-components-button-secondary-text system-sm-medium'>{t('common.operation.cancel')}</span>
|
||||
<span className='px-[1px] bg-components-kbd-bg-gray rounded-[4px] text-text-tertiary system-kbd'>ESC</span>
|
||||
</Button>
|
||||
<Button
|
||||
className='flex items-center gap-x-1'
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
>
|
||||
<span className='text-components-button-primary-text'>{t('common.operation.save')}</span>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
<span className='w-4 h-4 bg-components-kbd-bg-white rounded-[4px] text-text-primary-on-surface system-kbd capitalize'>{getKeyboardKeyNameBySystem('ctrl')}</span>
|
||||
<span className='w-4 h-4 bg-components-kbd-bg-white rounded-[4px] text-text-primary-on-surface system-kbd'>S</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const AddAnotherCheckBox = () => {
|
||||
return (
|
||||
<div className={classNames('flex items-center gap-x-1 pl-1', fullScreen ? 'mr-3' : '')}>
|
||||
<Checkbox
|
||||
key='add-another-checkbox'
|
||||
className='shrink-0'
|
||||
checked={addAnother}
|
||||
onCheck={() => setAnother(!addAnother)}
|
||||
/>
|
||||
<span className='text-text-tertiary system-xs-medium'>{t('datasetDocuments.segment.addAnother')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderKeywords = () => {
|
||||
return (
|
||||
<div className={classNames('flex flex-col', fullScreen ? 'w-1/5' : '')}>
|
||||
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('datasetDocuments.segment.keywords')}</div>
|
||||
<div className='text-text-tertiary w-full max-h-[200px] overflow-auto flex flex-wrap gap-1'>
|
||||
<TagInput items={keywords} onChange={newKeywords => setKeywords(newKeywords)} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col h-full'}>
|
||||
<div className={classNames('flex items-center justify-between', fullScreen ? 'py-3 pr-4 pl-6 border border-divider-subtle' : 'pt-3 pr-3 pl-4')}>
|
||||
@@ -225,8 +124,13 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
|
||||
<div className='flex items-center'>
|
||||
{fullScreen && (
|
||||
<>
|
||||
{AddAnotherCheckBox()}
|
||||
{renderActionButtons()}
|
||||
<AddAnother className='mr-3' isChecked={addAnother} onCheck={() => setAddAnother(!addAnother)} />
|
||||
<ActionButtons
|
||||
handleCancel={handleCancel.bind(null, 'esc')}
|
||||
handleSave={handleSave}
|
||||
loading={loading}
|
||||
actionType='add'
|
||||
/>
|
||||
<Divider type='vertical' className='h-3.5 bg-divider-regular ml-4 mr-2' />
|
||||
</>
|
||||
)}
|
||||
@@ -240,14 +144,32 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
|
||||
</div>
|
||||
<div className={classNames('flex grow overflow-hidden', fullScreen ? 'w-full flex-row justify-center px-6 pt-6 gap-x-8' : 'flex-col gap-y-1 py-3 px-4')}>
|
||||
<div className={classNames('break-all overflow-y-auto whitespace-pre-line', fullScreen ? 'w-1/2' : 'grow')}>
|
||||
{renderContent()}
|
||||
<ChunkContent
|
||||
docForm={docForm}
|
||||
question={question}
|
||||
answer={answer}
|
||||
onQuestionChange={question => setQuestion(question)}
|
||||
onAnswerChange={answer => setAnswer(answer)}
|
||||
isEditMode={true}
|
||||
/>
|
||||
</div>
|
||||
{renderKeywords()}
|
||||
{mode === 'custom' && <Keywords
|
||||
className={fullScreen ? 'w-1/5' : ''}
|
||||
actionType='add'
|
||||
keywords={keywords}
|
||||
isEditMode={true}
|
||||
onKeywordsChange={keywords => setKeywords(keywords)}
|
||||
/>}
|
||||
</div>
|
||||
{!fullScreen && (
|
||||
<div className='flex items-center justify-between p-4 pt-3 border-t-[1px] border-t-divider-subtle'>
|
||||
{AddAnotherCheckBox()}
|
||||
{renderActionButtons()}
|
||||
<AddAnother isChecked={addAnother} onCheck={() => setAddAnother(!addAnother)} />
|
||||
<ActionButtons
|
||||
handleCancel={handleCancel.bind(null, 'esc')}
|
||||
handleSave={handleSave}
|
||||
loading={loading}
|
||||
actionType='add'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -353,6 +353,7 @@ const translation = {
|
||||
delete: 'Delete this chunk ?',
|
||||
chunkAdded: '1 chunk added',
|
||||
viewAddedChunk: 'View',
|
||||
saveAndRegenerate: 'Save & Regenerate Child Chunks',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -351,6 +351,7 @@ const translation = {
|
||||
delete: '删除这个分段?',
|
||||
chunkAdded: '新增一个分段',
|
||||
viewAddedChunk: '查看',
|
||||
saveAndRegenerate: '保存并重新生成子分段',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -561,6 +561,7 @@ export type SegmentUpdater = {
|
||||
content: string
|
||||
answer?: string
|
||||
keywords?: string[]
|
||||
regenerate_child_chunks?: boolean
|
||||
}
|
||||
|
||||
export enum DocForm {
|
||||
|
||||
Reference in New Issue
Block a user