feat: add new translations and enhance segment management features

pull/12097/head
twwu 1 year ago
parent aa0d587516
commit 65a9cac099

@ -21,6 +21,7 @@ export type IToastProps = {
children?: ReactNode children?: ReactNode
onClose?: () => void onClose?: () => void
className?: string className?: string
customComponent?: ReactNode
} }
type IToastContext = { type IToastContext = {
notify: (props: IToastProps) => void notify: (props: IToastProps) => void
@ -35,6 +36,7 @@ const Toast = ({
message, message,
children, children,
className, className,
customComponent,
}: IToastProps) => { }: IToastProps) => {
const { close } = useToastContext() const { close } = useToastContext()
// sometimes message is react node array. Not handle it. // sometimes message is react node array. Not handle it.
@ -49,7 +51,7 @@ const Toast = ({
'top-0', 'top-0',
'right-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 === '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 === '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%)]') || (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 === '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" />} {type === 'info' && <RiInformation2Fill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-accent`} aria-hidden="true" />}
</div> </div>
<div className={`flex py-1 ${size === 'md' ? 'px-1' : 'px-0.5'} flex-col items-start gap-1 flex-grow`}> <div className={`flex py-1 ${size === 'md' ? 'px-1' : 'px-0.5'} flex-col items-start gap-1 flex-grow z-10`}>
<div className='text-text-primary system-sm-semibold'>{message}</div> <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 className='text-text-secondary system-xs-regular'>
{children} {children}
</div> </div>
} }
</div> </div>
<ActionButton className='z-[1000]' onClick={close}> <ActionButton onClick={close}>
<RiCloseLine className='w-4 h-4 flex-shrink-0 text-text-tertiary' /> <RiCloseLine className='w-4 h-4 flex-shrink-0 text-text-tertiary' />
</ActionButton> </ActionButton>
</div> </div>
@ -117,13 +122,14 @@ Toast.notify = ({
message, message,
duration, duration,
className, 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 const defaultDuring = (type === 'success' || type === 'info') ? 3000 : 6000
if (typeof window === 'object') { if (typeof window === 'object') {
const holder = document.createElement('div') const holder = document.createElement('div')
const root = createRoot(holder) 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) document.body.appendChild(holder)
setTimeout(() => { setTimeout(() => {
if (holder) if (holder)

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

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

@ -1,5 +1,4 @@
import type { FC } from 'react' import React, { type ForwardedRef } from 'react'
import React from 'react'
import SegmentCard from './segment-card' import SegmentCard from './segment-card'
import type { SegmentDetailModel } from '@/models/datasets' import type { SegmentDetailModel } from '@/models/datasets'
import Checkbox from '@/app/components/base/checkbox' import Checkbox from '@/app/components/base/checkbox'
@ -19,7 +18,7 @@ type ISegmentListProps = {
embeddingAvailable: boolean embeddingAvailable: boolean
} }
const SegmentList: FC<ISegmentListProps> = ({ const SegmentList = React.forwardRef(({
isLoading, isLoading,
items, items,
selectedSegmentIds, selectedSegmentIds,
@ -29,11 +28,13 @@ const SegmentList: FC<ISegmentListProps> = ({
onDelete, onDelete,
archived, archived,
embeddingAvailable, embeddingAvailable,
}) => { }: ISegmentListProps,
ref: ForwardedRef<HTMLDivElement>,
) => {
if (isLoading) if (isLoading)
return <Loading type='app' /> return <Loading type='app' />
return ( 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) => { items.map((segItem) => {
const isLast = items[items.length - 1].id === segItem.id const isLast = items[items.length - 1].id === segItem.id
@ -67,6 +68,8 @@ const SegmentList: FC<ISegmentListProps> = ({
} }
</div> </div>
) )
} })
SegmentList.displayName = 'SegmentList'
export default 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 type { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { useParams } from 'next/navigation' import { useParams } from 'next/navigation'
import { RiCloseLine, RiExpandDiagonalLine } from '@remixicon/react' import { RiCloseLine, RiExpandDiagonalLine } from '@remixicon/react'
import { useKeyPress } from 'ahooks' import { useKeyPress } from 'ahooks'
import { useShallow } from 'zustand/react/shallow'
import { SegmentIndexTag, useSegmentListContext } from './completed' import { SegmentIndexTag, useSegmentListContext } from './completed'
import { useStore as useAppStore } from '@/app/components/app/store'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea/common' import AutoHeightTextarea from '@/app/components/base/auto-height-textarea/common'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
@ -21,12 +23,14 @@ type NewSegmentModalProps = {
onCancel: () => void onCancel: () => void
docForm: string docForm: string
onSave: () => void onSave: () => void
viewNewlyAddedChunk: () => void
} }
const NewSegmentModal: FC<NewSegmentModalProps> = ({ const NewSegmentModal: FC<NewSegmentModalProps> = ({
onCancel, onCancel,
docForm, docForm,
onSave, onSave,
viewNewlyAddedChunk,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
@ -36,6 +40,20 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
const [keywords, setKeywords] = useState<string[]>([]) const [keywords, setKeywords] = useState<string[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [fullScreen, toggleFullScreen] = useSegmentListContext(s => [s.fullScreen, s.toggleFullScreen]) 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 = () => { const handleCancel = () => {
onCancel() onCancel()
@ -68,9 +86,18 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
setLoading(true) setLoading(true)
try { try {
await addSegment({ datasetId, documentId, body: params }) 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() handleCancel()
onSave() refreshTimer.current = setTimeout(() => {
onSave()
}, 6000)
} }
finally { finally {
setLoading(false) setLoading(false)

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

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

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

Loading…
Cancel
Save