mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
refactor: improve ChildSegmentList component and enhance Drawer functionality
- Simplified state management and rendering logic in ChildSegmentList. - Introduced computeTotalInfo function for better total information handling. - Enhanced Drawer component with improved click handling and overlay behavior. - Refactored event handling to streamline user interactions.
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import type { ChildChunkDetail } from '@/models/datasets'
|
import type { ChildChunkDetail } from '@/models/datasets'
|
||||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||||
import { useMemo, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
@ -29,6 +29,37 @@ type IChildSegmentCardProps = {
|
|||||||
focused?: boolean
|
focused?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function computeTotalInfo(
|
||||||
|
isFullDocMode: boolean,
|
||||||
|
isSearching: boolean,
|
||||||
|
total: number | undefined,
|
||||||
|
childChunksLength: number,
|
||||||
|
): { displayText: string, count: number, translationKey: 'segment.searchResults' | 'segment.childChunks' } {
|
||||||
|
if (isSearching) {
|
||||||
|
const count = total ?? 0
|
||||||
|
return {
|
||||||
|
displayText: count === 0 ? '--' : String(formatNumber(count)),
|
||||||
|
count,
|
||||||
|
translationKey: 'segment.searchResults',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFullDocMode) {
|
||||||
|
const count = total ?? 0
|
||||||
|
return {
|
||||||
|
displayText: count === 0 ? '--' : String(formatNumber(count)),
|
||||||
|
count,
|
||||||
|
translationKey: 'segment.childChunks',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayText: String(formatNumber(childChunksLength)),
|
||||||
|
count: childChunksLength,
|
||||||
|
translationKey: 'segment.childChunks',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const ChildSegmentList: FC<IChildSegmentCardProps> = ({
|
const ChildSegmentList: FC<IChildSegmentCardProps> = ({
|
||||||
childChunks,
|
childChunks,
|
||||||
parentChunkId,
|
parentChunkId,
|
||||||
@ -49,59 +80,87 @@ const ChildSegmentList: FC<IChildSegmentCardProps> = ({
|
|||||||
|
|
||||||
const [collapsed, setCollapsed] = useState(true)
|
const [collapsed, setCollapsed] = useState(true)
|
||||||
|
|
||||||
const toggleCollapse = () => {
|
const isParagraphMode = parentMode === 'paragraph'
|
||||||
setCollapsed(!collapsed)
|
const isFullDocMode = parentMode === 'full-doc'
|
||||||
|
const isSearching = inputValue !== '' && isFullDocMode
|
||||||
|
const contentOpacity = (enabled || focused) ? '' : 'opacity-50 group-hover/card:opacity-100'
|
||||||
|
const { displayText, count, translationKey } = computeTotalInfo(isFullDocMode, isSearching, total, childChunks.length)
|
||||||
|
const totalText = `${displayText} ${t(translationKey, { ns: 'datasetDocuments', count })}`
|
||||||
|
|
||||||
|
const toggleCollapse = () => setCollapsed(prev => !prev)
|
||||||
|
const showContent = (isFullDocMode && !isLoading) || !collapsed
|
||||||
|
const hoverVisibleClass = isParagraphMode ? 'hidden group-hover/card:inline-block' : ''
|
||||||
|
|
||||||
|
const renderCollapseIcon = () => {
|
||||||
|
if (!isParagraphMode)
|
||||||
|
return null
|
||||||
|
const Icon = collapsed ? RiArrowRightSLine : RiArrowDownSLine
|
||||||
|
return <Icon className={cn('mr-0.5 h-4 w-4 text-text-secondary', collapsed && 'opacity-50')} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const isParagraphMode = useMemo(() => {
|
const renderChildChunkItem = (childChunk: ChildChunkDetail) => {
|
||||||
return parentMode === 'paragraph'
|
const isEdited = childChunk.updated_at !== childChunk.created_at
|
||||||
}, [parentMode])
|
const isFocused = currChildChunk?.childChunkInfo?.id === childChunk.id
|
||||||
|
const label = isEdited
|
||||||
|
? `C-${childChunk.position} · ${t('segment.edited', { ns: 'datasetDocuments' })}`
|
||||||
|
: `C-${childChunk.position}`
|
||||||
|
|
||||||
const isFullDocMode = useMemo(() => {
|
return (
|
||||||
return parentMode === 'full-doc'
|
<EditSlice
|
||||||
}, [parentMode])
|
key={childChunk.id}
|
||||||
|
label={label}
|
||||||
|
text={childChunk.content}
|
||||||
|
onDelete={() => onDelete?.(childChunk.segment_id, childChunk.id)}
|
||||||
|
className="child-chunk"
|
||||||
|
labelClassName={isFocused ? 'bg-state-accent-solid text-text-primary-on-surface' : ''}
|
||||||
|
labelInnerClassName="text-[10px] font-semibold align-bottom leading-6"
|
||||||
|
contentClassName={cn('!leading-6', isFocused ? 'bg-state-accent-hover-alt text-text-primary' : 'text-text-secondary')}
|
||||||
|
showDivider={false}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onClickSlice?.(childChunk)
|
||||||
|
}}
|
||||||
|
offsetOptions={({ rects }) => ({
|
||||||
|
mainAxis: isFullDocMode ? -rects.floating.width : 12 - rects.floating.width,
|
||||||
|
crossAxis: (20 - rects.floating.height) / 2,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const contentOpacity = useMemo(() => {
|
const renderContent = () => {
|
||||||
return (enabled || focused) ? '' : 'opacity-50 group-hover/card:opacity-100'
|
if (childChunks.length > 0) {
|
||||||
}, [enabled, focused])
|
return (
|
||||||
|
<FormattedText className={cn('flex w-full flex-col !leading-6', isParagraphMode ? 'gap-y-2' : 'gap-y-3')}>
|
||||||
const totalText = useMemo(() => {
|
{childChunks.map(renderChildChunkItem)}
|
||||||
const isSearch = inputValue !== '' && isFullDocMode
|
</FormattedText>
|
||||||
if (!isSearch) {
|
)
|
||||||
const text = isFullDocMode
|
|
||||||
? !total
|
|
||||||
? '--'
|
|
||||||
: formatNumber(total)
|
|
||||||
: formatNumber(childChunks.length)
|
|
||||||
const count = isFullDocMode
|
|
||||||
? text === '--'
|
|
||||||
? 0
|
|
||||||
: total
|
|
||||||
: childChunks.length
|
|
||||||
return `${text} ${t('segment.childChunks', { ns: 'datasetDocuments', count })}`
|
|
||||||
}
|
}
|
||||||
else {
|
if (inputValue !== '') {
|
||||||
const text = !total ? '--' : formatNumber(total)
|
return (
|
||||||
const count = text === '--' ? 0 : total
|
<div className="h-full w-full">
|
||||||
return `${count} ${t('segment.searchResults', { ns: 'datasetDocuments', count })}`
|
<Empty onClearFilter={onClearFilter!} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [isFullDocMode, total, childChunks.length, inputValue])
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'flex flex-col',
|
'flex flex-col',
|
||||||
contentOpacity,
|
contentOpacity,
|
||||||
isParagraphMode ? 'pb-2 pt-1' : 'grow px-3',
|
isParagraphMode ? 'pb-2 pt-1' : 'grow px-3',
|
||||||
(isFullDocMode && isLoading) && 'overflow-y-hidden',
|
isFullDocMode && isLoading && 'overflow-y-hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isFullDocMode ? <Divider type="horizontal" className="my-1 h-px bg-divider-subtle" /> : null}
|
{isFullDocMode && <Divider type="horizontal" className="my-1 h-px bg-divider-subtle" />}
|
||||||
<div className={cn('flex items-center justify-between', isFullDocMode ? 'sticky -top-2 left-0 bg-background-default pb-3 pt-2' : '')}>
|
<div className={cn('flex items-center justify-between', isFullDocMode && 'sticky -top-2 left-0 bg-background-default pb-3 pt-2')}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-7 items-center rounded-lg pl-1 pr-3',
|
'flex h-7 items-center rounded-lg pl-1 pr-3',
|
||||||
isParagraphMode && 'cursor-pointer',
|
isParagraphMode && 'cursor-pointer',
|
||||||
(isParagraphMode && collapsed) && 'bg-dataset-child-chunk-expand-btn-bg',
|
isParagraphMode && collapsed && 'bg-dataset-child-chunk-expand-btn-bg',
|
||||||
isFullDocMode && 'pl-0',
|
isFullDocMode && 'pl-0',
|
||||||
)}
|
)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
@ -109,23 +168,15 @@ const ChildSegmentList: FC<IChildSegmentCardProps> = ({
|
|||||||
toggleCollapse()
|
toggleCollapse()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{
|
{renderCollapseIcon()}
|
||||||
isParagraphMode
|
|
||||||
? collapsed
|
|
||||||
? (
|
|
||||||
<RiArrowRightSLine className="mr-0.5 h-4 w-4 text-text-secondary opacity-50" />
|
|
||||||
)
|
|
||||||
: (<RiArrowDownSLine className="mr-0.5 h-4 w-4 text-text-secondary" />)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
<span className="system-sm-semibold-uppercase text-text-secondary">{totalText}</span>
|
<span className="system-sm-semibold-uppercase text-text-secondary">{totalText}</span>
|
||||||
<span className={cn('pl-1.5 text-xs font-medium text-text-quaternary', isParagraphMode ? 'hidden group-hover/card:inline-block' : '')}>·</span>
|
<span className={cn('pl-1.5 text-xs font-medium text-text-quaternary', hoverVisibleClass)}>·</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
'system-xs-semibold-uppercase px-1.5 py-1 text-components-button-secondary-accent-text',
|
'system-xs-semibold-uppercase px-1.5 py-1 text-components-button-secondary-accent-text',
|
||||||
isParagraphMode ? 'hidden group-hover/card:inline-block' : '',
|
hoverVisibleClass,
|
||||||
(isFullDocMode && isLoading) ? 'text-components-button-secondary-accent-text-disabled' : '',
|
isFullDocMode && isLoading && 'text-components-button-secondary-accent-text-disabled',
|
||||||
)}
|
)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@ -136,70 +187,28 @@ const ChildSegmentList: FC<IChildSegmentCardProps> = ({
|
|||||||
{t('operation.add', { ns: 'common' })}
|
{t('operation.add', { ns: 'common' })}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{isFullDocMode
|
{isFullDocMode && (
|
||||||
? (
|
<Input
|
||||||
<Input
|
showLeftIcon
|
||||||
showLeftIcon
|
showClearIcon
|
||||||
showClearIcon
|
wrapperClassName="!w-52"
|
||||||
wrapperClassName="!w-52"
|
value={inputValue}
|
||||||
value={inputValue}
|
onChange={e => handleInputChange?.(e.target.value)}
|
||||||
onChange={e => handleInputChange?.(e.target.value)}
|
onClear={() => handleInputChange?.('')}
|
||||||
onClear={() => handleInputChange?.('')}
|
/>
|
||||||
/>
|
)}
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</div>
|
</div>
|
||||||
{isLoading ? <FullDocListSkeleton /> : null}
|
{isLoading && <FullDocListSkeleton />}
|
||||||
{((isFullDocMode && !isLoading) || !collapsed)
|
{showContent && (
|
||||||
? (
|
<div className={cn('flex gap-x-0.5', isFullDocMode ? 'mb-6 grow' : 'items-center')}>
|
||||||
<div className={cn('flex gap-x-0.5', isFullDocMode ? 'mb-6 grow' : 'items-center')}>
|
{isParagraphMode && (
|
||||||
{isParagraphMode && (
|
<div className="self-stretch">
|
||||||
<div className="self-stretch">
|
<Divider type="vertical" className="mx-[7px] w-[2px] bg-text-accent-secondary" />
|
||||||
<Divider type="vertical" className="mx-[7px] w-[2px] bg-text-accent-secondary" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{childChunks.length > 0
|
|
||||||
? (
|
|
||||||
<FormattedText className={cn('flex w-full flex-col !leading-6', isParagraphMode ? 'gap-y-2' : 'gap-y-3')}>
|
|
||||||
{childChunks.map((childChunk) => {
|
|
||||||
const edited = childChunk.updated_at !== childChunk.created_at
|
|
||||||
const focused = currChildChunk?.childChunkInfo?.id === childChunk.id
|
|
||||||
return (
|
|
||||||
<EditSlice
|
|
||||||
key={childChunk.id}
|
|
||||||
label={`C-${childChunk.position}${edited ? ` · ${t('segment.edited', { ns: 'datasetDocuments' })}` : ''}`}
|
|
||||||
text={childChunk.content}
|
|
||||||
onDelete={() => onDelete?.(childChunk.segment_id, childChunk.id)}
|
|
||||||
className="child-chunk"
|
|
||||||
labelClassName={focused ? 'bg-state-accent-solid text-text-primary-on-surface' : ''}
|
|
||||||
labelInnerClassName="text-[10px] font-semibold align-bottom leading-6"
|
|
||||||
contentClassName={cn('!leading-6', focused ? 'bg-state-accent-hover-alt text-text-primary' : 'text-text-secondary')}
|
|
||||||
showDivider={false}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
onClickSlice?.(childChunk)
|
|
||||||
}}
|
|
||||||
offsetOptions={({ rects }) => {
|
|
||||||
return {
|
|
||||||
mainAxis: isFullDocMode ? -rects.floating.width : 12 - rects.floating.width,
|
|
||||||
crossAxis: (20 - rects.floating.height) / 2,
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</FormattedText>
|
|
||||||
)
|
|
||||||
: inputValue !== ''
|
|
||||||
? (
|
|
||||||
<div className="h-full w-full">
|
|
||||||
<Empty onClearFilter={onClearFilter!} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
: null}
|
{renderContent()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,31 @@ type DrawerProps = {
|
|||||||
needCheckChunks?: boolean
|
needCheckChunks?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SIDE_POSITION_CLASS = {
|
||||||
|
right: 'right-0',
|
||||||
|
left: 'left-0',
|
||||||
|
bottom: 'bottom-0',
|
||||||
|
top: 'top-0',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
function containsTarget(selector: string, target: Node | null): boolean {
|
||||||
|
const elements = document.querySelectorAll(selector)
|
||||||
|
return Array.from(elements).some(el => el?.contains(target))
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldReopenChunkDetail(
|
||||||
|
isClickOnChunk: boolean,
|
||||||
|
isClickOnChildChunk: boolean,
|
||||||
|
segmentModalOpen: boolean,
|
||||||
|
childChunkModalOpen: boolean,
|
||||||
|
): boolean {
|
||||||
|
if (segmentModalOpen && isClickOnChildChunk)
|
||||||
|
return true
|
||||||
|
if (childChunkModalOpen && isClickOnChunk && !isClickOnChildChunk)
|
||||||
|
return true
|
||||||
|
return !isClickOnChunk && !isClickOnChildChunk
|
||||||
|
}
|
||||||
|
|
||||||
const Drawer = ({
|
const Drawer = ({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
@ -41,22 +66,22 @@ const Drawer = ({
|
|||||||
|
|
||||||
const shouldCloseDrawer = useCallback((target: Node | null) => {
|
const shouldCloseDrawer = useCallback((target: Node | null) => {
|
||||||
const panelContent = panelContentRef.current
|
const panelContent = panelContentRef.current
|
||||||
if (!panelContent)
|
if (!panelContent || !target)
|
||||||
return false
|
return false
|
||||||
const chunks = document.querySelectorAll('.chunk-card')
|
|
||||||
const childChunks = document.querySelectorAll('.child-chunk')
|
if (panelContent.contains(target))
|
||||||
const imagePreviewer = document.querySelector('.image-previewer')
|
return false
|
||||||
const isClickOnChunk = Array.from(chunks).some((chunk) => {
|
|
||||||
return chunk && chunk.contains(target)
|
if (containsTarget('.image-previewer', target))
|
||||||
})
|
return false
|
||||||
const isClickOnChildChunk = Array.from(childChunks).some((chunk) => {
|
|
||||||
return chunk && chunk.contains(target)
|
if (!needCheckChunks)
|
||||||
})
|
return true
|
||||||
const reopenChunkDetail = (currSegment.showModal && isClickOnChildChunk)
|
|
||||||
|| (currChildChunk.showModal && isClickOnChunk && !isClickOnChildChunk) || (!isClickOnChunk && !isClickOnChildChunk)
|
const isClickOnChunk = containsTarget('.chunk-card', target)
|
||||||
const isClickOnImagePreviewer = imagePreviewer && imagePreviewer.contains(target)
|
const isClickOnChildChunk = containsTarget('.child-chunk', target)
|
||||||
return target && !panelContent.contains(target) && (!needCheckChunks || reopenChunkDetail) && !isClickOnImagePreviewer
|
return shouldReopenChunkDetail(isClickOnChunk, isClickOnChildChunk, currSegment.showModal, currChildChunk.showModal)
|
||||||
}, [currSegment, currChildChunk, needCheckChunks])
|
}, [currSegment.showModal, currChildChunk.showModal, needCheckChunks])
|
||||||
|
|
||||||
const onDownCapture = useCallback((e: PointerEvent) => {
|
const onDownCapture = useCallback((e: PointerEvent) => {
|
||||||
if (!open || modal)
|
if (!open || modal)
|
||||||
@ -77,32 +102,27 @@ const Drawer = ({
|
|||||||
|
|
||||||
const isHorizontal = side === 'left' || side === 'right'
|
const isHorizontal = side === 'left' || side === 'right'
|
||||||
|
|
||||||
|
const overlayPointerEvents = modal && open ? 'pointer-events-auto' : 'pointer-events-none'
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div className="pointer-events-none fixed inset-0 z-[9999]">
|
<div className="pointer-events-none fixed inset-0 z-[9999]">
|
||||||
{showOverlay
|
{showOverlay && (
|
||||||
? (
|
<div
|
||||||
<div
|
onClick={modal ? onClose : undefined}
|
||||||
onClick={modal ? onClose : undefined}
|
aria-hidden="true"
|
||||||
aria-hidden="true"
|
className={cn(
|
||||||
className={cn(
|
'fixed inset-0 bg-black/30 opacity-0 transition-opacity duration-200 ease-in',
|
||||||
'fixed inset-0 bg-black/30 opacity-0 transition-opacity duration-200 ease-in',
|
open && 'opacity-100',
|
||||||
open && 'opacity-100',
|
overlayPointerEvents,
|
||||||
modal && open ? 'pointer-events-auto' : 'pointer-events-none',
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
)}
|
||||||
)
|
|
||||||
: null}
|
|
||||||
|
|
||||||
{/* Drawer panel */}
|
|
||||||
<div
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal={modal ? 'true' : 'false'}
|
aria-modal={modal ? 'true' : 'false'}
|
||||||
className={cn(
|
className={cn(
|
||||||
'pointer-events-auto fixed flex flex-col',
|
'pointer-events-auto fixed flex flex-col',
|
||||||
side === 'right' && 'right-0',
|
SIDE_POSITION_CLASS[side],
|
||||||
side === 'left' && 'left-0',
|
|
||||||
side === 'bottom' && 'bottom-0',
|
|
||||||
side === 'top' && 'top-0',
|
|
||||||
isHorizontal ? 'h-screen' : 'w-screen',
|
isHorizontal ? 'h-screen' : 'w-screen',
|
||||||
panelClassName,
|
panelClassName,
|
||||||
)}
|
)}
|
||||||
@ -114,7 +134,10 @@ const Drawer = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
return open && createPortal(content, document.body)
|
if (!open)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return createPortal(content, document.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Drawer
|
export default Drawer
|
||||||
|
|||||||
Reference in New Issue
Block a user