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:
yyh
2026-01-28 15:51:52 +08:00
parent 156b779a1d
commit 543802cc65
8 changed files with 203 additions and 31 deletions

View File

@ -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} />
</>

View File

@ -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)