mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
feat(skill): add three-state upload progress tooltip
Replace simple uploading/success indicator with a full three-state tooltip (uploading, success, partial_error) that overlays the DropTip position. Add upload slice to skill editor store and wire progress tracking into file/folder upload operations.
This commit is contained in:
@ -28,6 +28,7 @@ import { isDescendantOf } from '../utils/tree-utils'
|
||||
import DragActionTooltip from './drag-action-tooltip'
|
||||
import TreeContextMenu from './tree-context-menu'
|
||||
import TreeNode from './tree-node'
|
||||
import UploadStatusTooltip from './upload-status-tooltip'
|
||||
|
||||
type FileTreeProps = {
|
||||
className?: string
|
||||
@ -240,7 +241,7 @@ const FileTree: React.FC<FileTreeProps> = ({ className }) => {
|
||||
{t('skillSidebar.empty')}
|
||||
</span>
|
||||
</div>
|
||||
<DropTip />
|
||||
<UploadStatusTooltip fallback={<DropTip />} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -271,7 +272,7 @@ const FileTree: React.FC<FileTreeProps> = ({ className }) => {
|
||||
data-skill-tree-container
|
||||
className={cn(
|
||||
'flex min-h-[150px] flex-1 flex-col overflow-y-auto',
|
||||
isMutating && 'pointer-events-none opacity-50',
|
||||
isMutating && 'pointer-events-none',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@ -314,7 +315,7 @@ const FileTree: React.FC<FileTreeProps> = ({ className }) => {
|
||||
</div>
|
||||
{dragOverFolderId
|
||||
? <DragActionTooltip action={currentDragType ?? 'upload'} />
|
||||
: <DropTip />}
|
||||
: <UploadStatusTooltip fallback={<DropTip />} />}
|
||||
</div>
|
||||
<TreeContextMenu treeRef={treeRef} />
|
||||
</>
|
||||
|
||||
@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import {
|
||||
RiAlertFill,
|
||||
RiCheckboxCircleFill,
|
||||
RiCloseLine,
|
||||
RiUploadCloud2Line,
|
||||
} from '@remixicon/react'
|
||||
import { memo, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type UploadStatusTooltipProps = {
|
||||
fallback?: ReactNode
|
||||
}
|
||||
|
||||
const SUCCESS_DISPLAY_MS = 2000
|
||||
|
||||
const UploadStatusTooltip: FC<UploadStatusTooltipProps> = ({ fallback }) => {
|
||||
const { t } = useTranslation('workflow')
|
||||
const storeApi = useWorkflowStore()
|
||||
const uploadStatus = useStore(s => s.uploadStatus)
|
||||
const uploadProgress = useStore(s => s.uploadProgress)
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (timerRef.current)
|
||||
clearTimeout(timerRef.current)
|
||||
|
||||
if (uploadStatus === 'success') {
|
||||
timerRef.current = setTimeout(() => {
|
||||
storeApi.getState().resetUpload()
|
||||
}, SUCCESS_DISPLAY_MS)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timerRef.current)
|
||||
clearTimeout(timerRef.current)
|
||||
}
|
||||
}, [storeApi, uploadStatus])
|
||||
|
||||
if (uploadStatus === 'idle')
|
||||
return <>{fallback}</>
|
||||
|
||||
const handleClose = () => {
|
||||
storeApi.getState().resetUpload()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex shrink-0 items-center justify-center px-2 py-3">
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex w-full items-center gap-2 overflow-hidden rounded-lg py-1.5 pl-3 pr-2.5 shadow-lg backdrop-blur-[5px]',
|
||||
'border-[0.5px] border-components-panel-border bg-components-tooltip-bg',
|
||||
)}
|
||||
>
|
||||
{uploadStatus === 'uploading' && (
|
||||
<div className="absolute inset-[-0.5px] animate-pulse bg-state-accent-hover-alt opacity-60" />
|
||||
)}
|
||||
{uploadStatus === 'success' && (
|
||||
<div className="absolute inset-[-0.5px] bg-toast-success-bg opacity-40" />
|
||||
)}
|
||||
{uploadStatus === 'partial_error' && (
|
||||
<div className="absolute inset-[-0.5px] bg-state-warning-hover opacity-40" />
|
||||
)}
|
||||
|
||||
<div className="relative z-10 shrink-0">
|
||||
{uploadStatus === 'uploading' && (
|
||||
<RiUploadCloud2Line className="size-5 text-text-accent" />
|
||||
)}
|
||||
{uploadStatus === 'success' && (
|
||||
<RiCheckboxCircleFill className="size-5 text-text-success" />
|
||||
)}
|
||||
{uploadStatus === 'partial_error' && (
|
||||
<RiAlertFill className="size-5 text-text-warning" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex min-w-0 flex-1 flex-col">
|
||||
<span className="system-xs-semibold truncate text-text-primary">
|
||||
{uploadStatus === 'uploading' && t('skillSidebar.uploadingItems', {
|
||||
uploaded: uploadProgress.uploaded,
|
||||
total: uploadProgress.total,
|
||||
})}
|
||||
{uploadStatus === 'success' && t('skillSidebar.uploadSuccess')}
|
||||
{uploadStatus === 'partial_error' && t('skillSidebar.uploadPartialError')}
|
||||
</span>
|
||||
<span className="system-2xs-regular truncate text-text-tertiary">
|
||||
{uploadStatus === 'success' && t('skillSidebar.uploadSuccessDetail', {
|
||||
uploaded: uploadProgress.uploaded,
|
||||
total: uploadProgress.total,
|
||||
})}
|
||||
{uploadStatus === 'partial_error' && t('skillSidebar.uploadPartialErrorDetail', {
|
||||
failed: uploadProgress.failed,
|
||||
total: uploadProgress.total,
|
||||
})}
|
||||
{uploadStatus === 'uploading' && '\u00A0'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
className="relative z-10 shrink-0 rounded p-0.5 text-text-tertiary hover:text-text-secondary focus-visible:outline focus-visible:outline-2 focus-visible:outline-state-accent-solid"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<RiCloseLine className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(UploadStatusTooltip)
|
||||
Reference in New Issue
Block a user