feat: add new translations and enhance segment management features
This commit is contained in:
parent
aa0d587516
commit
65a9cac099
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 */}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -350,6 +350,8 @@ const translation = {
|
||||
newQaSegment: 'New Q&A Segment',
|
||||
addChunk: 'Add Chunk',
|
||||
delete: 'Delete this chunk ?',
|
||||
chunkAdded: '1 chunk added',
|
||||
viewAddedChunk: 'View',
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -348,6 +348,8 @@ const translation = {
|
||||
newQaSegment: '新问答分段',
|
||||
addChunk: '新增分段',
|
||||
delete: '删除这个分段?',
|
||||
chunkAdded: '新增一个分段',
|
||||
viewAddedChunk: '查看',
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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: {
|
||||
|
Loading…
Reference in New Issue
Block a user