feat: add new translations and enhance segment management features

This commit is contained in:
twwu 2024-12-10 11:31:56 +08:00
parent aa0d587516
commit 65a9cac099
8 changed files with 93 additions and 23 deletions

View File

@ -21,6 +21,7 @@ export type IToastProps = {
children?: ReactNode
onClose?: () => void
className?: string
customComponent?: ReactNode
}
type IToastContext = {
notify: (props: IToastProps) => void
@ -35,6 +36,7 @@ const Toast = ({
message,
children,
className,
customComponent,
}: IToastProps) => {
const { close } = useToastContext()
// sometimes message is react node array. Not handle it.
@ -49,7 +51,7 @@ const Toast = ({
'top-0',
'right-0',
)}>
<div className={`absolute inset-0 opacity-40 ${
<div className={`absolute inset-0 opacity-40 -z-10 ${
(type === 'success' && 'bg-[linear-gradient(92deg,rgba(23,178,106,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|| (type === 'warning' && 'bg-[linear-gradient(92deg,rgba(247,144,9,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|| (type === 'error' && 'bg-[linear-gradient(92deg,rgba(240,68,56,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
@ -63,14 +65,17 @@ const Toast = ({
{type === 'warning' && <RiAlertFill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-warning-secondary`} aria-hidden="true" />}
{type === 'info' && <RiInformation2Fill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-accent`} aria-hidden="true" />}
</div>
<div className={`flex py-1 ${size === 'md' ? 'px-1' : 'px-0.5'} flex-col items-start gap-1 flex-grow`}>
<div className='text-text-primary system-sm-semibold'>{message}</div>
<div className={`flex py-1 ${size === 'md' ? 'px-1' : 'px-0.5'} flex-col items-start gap-1 flex-grow z-10`}>
<div className='flex items-center gap-1'>
<div className='text-text-primary system-sm-semibold'>{message}</div>
{customComponent}
</div>
{children && <div className='text-text-secondary system-xs-regular'>
{children}
</div>
}
</div>
<ActionButton className='z-[1000]' onClick={close}>
<ActionButton onClick={close}>
<RiCloseLine className='w-4 h-4 flex-shrink-0 text-text-tertiary' />
</ActionButton>
</div>
@ -117,13 +122,14 @@ Toast.notify = ({
message,
duration,
className,
}: Pick<IToastProps, 'type' | 'size' | 'message' | 'duration' | 'className'>) => {
customComponent,
}: Pick<IToastProps, 'type' | 'size' | 'message' | 'duration' | 'className' | 'customComponent'>) => {
const defaultDuring = (type === 'success' || type === 'info') ? 3000 : 6000
if (typeof window === 'object') {
const holder = document.createElement('div')
const root = createRoot(holder)
root.render(<Toast type={type} size={size} message={message} duration={duration} className={className} />)
root.render(<Toast type={type} size={size} message={message} duration={duration} className={className} customComponent={customComponent} />)
document.body.appendChild(holder)
setTimeout(() => {
if (holder)

View File

@ -10,7 +10,6 @@ const DisplayToggle: FC = () => {
<Tooltip
popupContent={isCollapsed ? 'Expand chunks' : 'Collapse chunks'}
popupClassName='text-text-secondary system-xs-medium border-[0.5px] border-components-panel-border'
needsDelay
>
<button
className='flex items-center justify-center p-2 rounded-lg bg-components-button-secondary-bg cursor-pointer

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useDebounceFn } from 'ahooks'
import { useTranslation } from 'react-i18next'
import { createContext, useContext, useContextSelector } from 'use-context-selector'
@ -27,8 +27,9 @@ import type { ChildChunkDetail, SegmentDetailModel, SegmentUpdater } from '@/mod
import NewSegment from '@/app/components/datasets/documents/detail/new-segment'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import Checkbox from '@/app/components/base/checkbox'
import { useChildSegmentList, useDeleteSegment, useDisableSegment, useEnableSegment, useSegmentList } from '@/service/knowledge/use-segment'
import { useChildSegmentList, useDeleteSegment, useDisableSegment, useEnableSegment, useSegmentList, useSegmentListKey } from '@/service/knowledge/use-segment'
import { Chunk } from '@/app/components/base/icons/src/public/knowledge'
import { useInvalid } from '@/service/use-base'
const DEFAULT_LIMIT = 10
@ -104,6 +105,8 @@ const Completed: FC<ICompletedProps> = ({
const [currentPage, setCurrentPage] = useState(1) // start from 1
const [limit, setLimit] = useState(DEFAULT_LIMIT)
const [fullScreen, setFullScreen] = useState(false)
const segmentListRef = useRef<HTMLDivElement>(null)
const needScrollToBottom = useRef(false)
const { run: handleSearch } = useDebounceFn(() => {
setSearchValue(inputValue)
@ -122,7 +125,7 @@ const Completed: FC<ICompletedProps> = ({
return mode === 'hierarchical' && parentMode === 'full-doc'
}, [mode, parentMode])
const { isLoading: isLoadingSegmentList, data: segmentListData, refetch: refreshSegmentList } = useSegmentList(
const { isFetching: isLoadingSegmentList, data: segmentListData } = useSegmentList(
{
datasetId,
documentId,
@ -134,12 +137,24 @@ const Completed: FC<ICompletedProps> = ({
},
},
)
const invalidSegmentList = useInvalid(useSegmentListKey)
useEffect(() => {
if (segmentListData)
if (segmentListData) {
setSegments(segmentListData.data || [])
if (segmentListData.total_pages < currentPage)
setCurrentPage(segmentListData.total_pages)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [segmentListData])
useEffect(() => {
if (segmentListRef.current && needScrollToBottom.current) {
segmentListRef.current.scrollTo({ top: segmentListRef.current.scrollHeight, behavior: 'smooth' })
needScrollToBottom.current = false
}
}, [segments])
const { data: childChunkListData, refetch: refreshChildSegmentList } = useChildSegmentList(
{
datasetId,
@ -162,7 +177,7 @@ const Completed: FC<ICompletedProps> = ({
const resetList = useCallback(() => {
setSegments([])
setSelectedSegmentIds([])
refreshSegmentList()
invalidSegmentList()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@ -189,7 +204,6 @@ const Completed: FC<ICompletedProps> = ({
seg.enabled = enable
}
setSegments([...segments])
!segId && setSelectedSegmentIds([])
},
onError: () => {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
@ -205,6 +219,7 @@ const Completed: FC<ICompletedProps> = ({
onSuccess: () => {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
resetList()
!segId && setSelectedSegmentIds([])
},
onError: () => {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
@ -298,6 +313,20 @@ const Completed: FC<ICompletedProps> = ({
setFullScreen(!fullScreen)
}, [fullScreen])
const viewNewlyAddedChunk = useCallback(async () => {
const totalPages = segmentListData?.total_pages || 0
const total = segmentListData?.total || 0
const newPage = Math.ceil((total + 1) / limit)
needScrollToBottom.current = true
if (newPage > totalPages)
setCurrentPage(totalPages + 1)
else if (currentPage === totalPages)
resetList()
else
setCurrentPage(totalPages)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [segmentListData, limit, currentPage])
return (
<SegmentListContext.Provider value={{
isCollapsed,
@ -351,6 +380,7 @@ const Completed: FC<ICompletedProps> = ({
/>
</div>
: <SegmentList
ref={segmentListRef}
embeddingAvailable={embeddingAvailable}
isLoading={isLoadingSegmentList}
items={segments}
@ -391,6 +421,7 @@ const Completed: FC<ICompletedProps> = ({
docForm={docForm}
onCancel={() => onNewSegmentModalChange(false)}
onSave={resetList}
viewNewlyAddedChunk={viewNewlyAddedChunk}
/>
</FullScreenDrawer>
{/* Batch Action Buttons */}

View File

@ -1,5 +1,4 @@
import type { FC } from 'react'
import React from 'react'
import React, { type ForwardedRef } from 'react'
import SegmentCard from './segment-card'
import type { SegmentDetailModel } from '@/models/datasets'
import Checkbox from '@/app/components/base/checkbox'
@ -19,7 +18,7 @@ type ISegmentListProps = {
embeddingAvailable: boolean
}
const SegmentList: FC<ISegmentListProps> = ({
const SegmentList = React.forwardRef(({
isLoading,
items,
selectedSegmentIds,
@ -29,11 +28,13 @@ const SegmentList: FC<ISegmentListProps> = ({
onDelete,
archived,
embeddingAvailable,
}) => {
}: ISegmentListProps,
ref: ForwardedRef<HTMLDivElement>,
) => {
if (isLoading)
return <Loading type='app' />
return (
<div className={classNames('flex flex-col h-full overflow-y-auto')}>
<div ref={ref} className={classNames('flex flex-col h-full overflow-y-auto')}>
{
items.map((segItem) => {
const isLast = items[items.length - 1].id === segItem.id
@ -67,6 +68,8 @@ const SegmentList: FC<ISegmentListProps> = ({
}
</div>
)
}
})
SegmentList.displayName = 'SegmentList'
export default SegmentList

View File

@ -1,11 +1,13 @@
import { memo, useState } from 'react'
import { memo, 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 { useKeyPress } from 'ahooks'
import { useShallow } from 'zustand/react/shallow'
import { SegmentIndexTag, useSegmentListContext } from './completed'
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'
@ -21,12 +23,14 @@ type NewSegmentModalProps = {
onCancel: () => void
docForm: string
onSave: () => void
viewNewlyAddedChunk: () => void
}
const NewSegmentModal: FC<NewSegmentModalProps> = ({
onCancel,
docForm,
onSave,
viewNewlyAddedChunk,
}) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
@ -36,6 +40,20 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
const [keywords, setKeywords] = useState<string[]>([])
const [loading, setLoading] = useState(false)
const [fullScreen, toggleFullScreen] = useSegmentListContext(s => [s.fullScreen, s.toggleFullScreen])
const { appSidebarExpand } = useAppStore(useShallow(state => ({
appSidebarExpand: state.appSidebarExpand,
})))
const refreshTimer = useRef<any>(null)
const CustomButton = <>
<Divider type='vertical' className='h-3 mx-1 bg-divider-regular' />
<button className='text-text-accent system-xs-semibold' onClick={() => {
clearTimeout(refreshTimer.current)
viewNewlyAddedChunk()
}}>
{t('datasetDocuments.segment.viewAddedChunk')}
</button>
</>
const handleCancel = () => {
onCancel()
@ -68,9 +86,18 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
setLoading(true)
try {
await addSegment({ datasetId, documentId, body: params })
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
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`,
duration: 6000,
customComponent: CustomButton,
})
handleCancel()
onSave()
refreshTimer.current = setTimeout(() => {
onSave()
}, 6000)
}
finally {
setLoading(false)

View File

@ -350,6 +350,8 @@ const translation = {
newQaSegment: 'New Q&A Segment',
addChunk: 'Add Chunk',
delete: 'Delete this chunk ?',
chunkAdded: '1 chunk added',
viewAddedChunk: 'View',
},
}

View File

@ -348,6 +348,8 @@ const translation = {
newQaSegment: '新问答分段',
addChunk: '新增分段',
delete: '删除这个分段?',
chunkAdded: '新增一个分段',
viewAddedChunk: '查看',
},
}

View File

@ -5,7 +5,7 @@ import type { ChildSegmentResponse, SegmentsResponse } from '@/models/datasets'
const NAME_SPACE = 'segment'
const useSegmentListKey = [NAME_SPACE, 'chunkList']
export const useSegmentListKey = [NAME_SPACE, 'chunkList']
export const useSegmentList = (
payload: {