From f56ddbf0e275cd17874d4c4c9ffd7396d2b82ac9 Mon Sep 17 00:00:00 2001 From: Ida Date: Wed, 27 May 2026 11:18:08 +0200 Subject: [PATCH] feat: visible progress during file upload --- .../UnifiedDataBar/FilesTab.module.css | 54 ++++++++++++ src/components/UnifiedDataBar/FilesTab.tsx | 50 ++++++++++- src/hooks/useFiles.ts | 14 ++- src/pages/basedata/FilesPage.tsx | 88 ++++++++++++++----- 4 files changed, 180 insertions(+), 26 deletions(-) diff --git a/src/components/UnifiedDataBar/FilesTab.module.css b/src/components/UnifiedDataBar/FilesTab.module.css index d992368..e8889ae 100644 --- a/src/components/UnifiedDataBar/FilesTab.module.css +++ b/src/components/UnifiedDataBar/FilesTab.module.css @@ -81,6 +81,60 @@ flex-wrap: wrap; } +.uploadCircleButton { + background: none; + border: none; + cursor: pointer; + font-size: 12px; + color: #f25843; + width: 26px; + height: 26px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; +} + +.uploadCircleButton:disabled { + cursor: not-allowed; +} + +.uploadCircleWrap { + position: relative; + width: 22px; + height: 22px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.uploadCircleSvg { + position: absolute; + inset: 0; + transform: rotate(-90deg); +} + +.uploadCircleTrack { + fill: none; + stroke: rgba(242, 88, 67, 0.25); + stroke-width: 2; +} + +.uploadCircleProgress { + fill: none; + stroke: #f25843; + stroke-width: 2; + stroke-linecap: round; + transition: stroke-dashoffset 120ms linear; +} + +.uploadCircleText { + font-size: 8px; + font-weight: 700; + line-height: 1; + color: #f25843; +} + @media (prefers-color-scheme: dark) { .fileRow:hover { background: rgba(255, 255, 255, 0.05); diff --git a/src/components/UnifiedDataBar/FilesTab.tsx b/src/components/UnifiedDataBar/FilesTab.tsx index 99697b7..d44373f 100644 --- a/src/components/UnifiedDataBar/FilesTab.tsx +++ b/src/components/UnifiedDataBar/FilesTab.tsx @@ -28,6 +28,8 @@ const FilesTab: React.FC = ({ context, onFileSelect, onSendToChat const { showSuccess, showError } = useToast(); const [isDragOver, setIsDragOver] = useState(false); const [uploading, setUploading] = useState(false); + const [uploadProgressPercent, setUploadProgressPercent] = useState(0); + const uploadRunIdRef = useRef(0); const fileInputRef = useRef(null); const provider = useMemo(() => createFolderFileProvider(), []); @@ -54,21 +56,41 @@ const FilesTab: React.FC = ({ context, onFileSelect, onSendToChat const _uploadFiles = useCallback(async (fileList: FileList | File[]) => { if (!context.instanceId || uploading) return; + uploadRunIdRef.current += 1; + const runId = uploadRunIdRef.current; setUploading(true); + setUploadProgressPercent(0); try { - for (const file of Array.from(fileList)) { + const files = Array.from(fileList); + const totalFiles = files.length || 1; + for (const [index, file] of files.entries()) { const formData = new FormData(); formData.append('file', file); formData.append('featureInstanceId', context.instanceId); await api.post('/api/files/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, + onUploadProgress: progressEvent => { + if (uploadRunIdRef.current !== runId) return; + if (!progressEvent.total) return; + const fileProgress = Math.min(100, Math.round((progressEvent.loaded * 100) / progressEvent.total)); + const baseProgress = (index / totalFiles) * 100; + const scaledFileProgress = fileProgress / totalFiles; + setUploadProgressPercent(Math.min(100, Math.round(baseProgress + scaledFileProgress))); + }, }); } + if (uploadRunIdRef.current === runId) setUploadProgressPercent(100); _handleRefresh(); } catch (err) { console.error('File upload failed:', err); } finally { - setUploading(false); + if (uploadRunIdRef.current === runId) { + setUploading(false); + // Let 100% render briefly, then reset. + window.setTimeout(() => { + if (uploadRunIdRef.current === runId) setUploadProgressPercent(0); + }, 250); + } } }, [context.instanceId, uploading, _handleRefresh]); @@ -135,6 +157,10 @@ const FilesTab: React.FC = ({ context, onFileSelect, onSendToChat onSendToChat?.([{ id: node.id, type: node.type === 'folder' ? 'group' : 'file', name: node.name }]); }, [onSendToChat]); + const circleRadius = 11; + const circleCircumference = 2 * Math.PI * circleRadius; + const circleOffset = circleCircumference * (1 - uploadProgressPercent / 100); + return (
= ({ context, onFileSelect, onSendToChat )}