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 5a13127..87d7675 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 )}