Skip to content

feat(upload): add progress indicator and disable send during upload#1705

Merged
kaizhou-lab merged 5 commits intoiOfficeAI:mainfrom
JerryLiu369:feat/upload-progress
Mar 26, 2026
Merged

feat(upload): add progress indicator and disable send during upload#1705
kaizhou-lab merged 5 commits intoiOfficeAI:mainfrom
JerryLiu369:feat/upload-progress

Conversation

@JerryLiu369
Copy link
Copy Markdown
Member

Summary

  • Replace fetch() with XMLHttpRequest in uploadFileViaHttp to support upload progress events
  • Add a module-level upload state store (useUploadState via useSyncExternalStore) — no Context Provider needed
  • Wire all 4 upload entry points (sendbox drag, sendbox attach, workspace drag, workspace paste) to report progress via trackUpload()
  • Show a thin progress bar in both sendbox and workspace toolbar while uploads are active
  • Disable the send button while any upload is in flight

Changes

File Change
src/renderer/hooks/file/useUploadState.ts New — global upload state store with trackUpload() and useUploadState() hook
src/renderer/components/media/UploadProgressBar.tsx New — thin progress bar component (renders nothing when idle)
src/renderer/services/FileService.ts Replace fetch with XHR for progress; add onProgress param; integrate trackUpload in processDroppedFiles
src/renderer/components/chat/sendbox.tsx Import useUploadState, disable send button during upload, render UploadProgressBar
src/renderer/pages/conversation/Workspace/hooks/useWorkspacePaste.ts Wire trackUpload into workspace paste upload loop
src/renderer/pages/conversation/Workspace/components/WorkspaceToolbar.tsx Render UploadProgressBar in toolbar

Test plan

  • Drag files into sendbox → progress bar appears, send button disabled until complete
  • Click attach button in sendbox → same behavior
  • Drag files into workspace → progress bar appears in workspace toolbar
  • Paste files in workspace → progress bar appears in workspace toolbar
  • Upload multiple files simultaneously → weighted average progress shown
  • Upload completes → progress bar disappears, send button re-enabled
  • Verify npm run build and electron-vite build pass cleanly

🤖 Generated with Claude Code

@sentry
Copy link
Copy Markdown

sentry bot commented Mar 25, 2026

@JerryLiu369 JerryLiu369 force-pushed the feat/upload-progress branch 3 times, most recently from 376a2a7 to 6eebf5a Compare March 25, 2026 14:07
@kaizhou-lab
Copy link
Copy Markdown
Collaborator

Hey @JerryLiu369 👋 整体方案很不错!useSyncExternalStore + 模块级 store 的思路很干净,按 source 分区避免了 sendbox/workspace 互相干扰,trackUpload + finish() 的 try/finally 也保证了状态清理。下面有几点想讨论一下:


1. XHR 缺少 abort 事件处理 🟠

uploadFileViaHttp 里只监听了 loaderror,没有处理 abort。如果请求被中断(比如用户导航离开,或者未来要加取消功能),Promise 会永远 pending,tracker.finish() 永远不会被调用 → 上传状态卡死 → 发送按钮永久禁用。

建议加上:

xhr.addEventListener('abort', () => {
  reject(new Error('Upload aborted'));
});

这样 finally 里的 tracker.finish() 就能正常执行了。


2. getSnapshot 闭包稳定性 🟠

export function useUploadState(source?: UploadSource): UploadStateSnapshot {
  const getSnapshot = source ? () => sourceSnapshots[source] : () => globalSnapshot;
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}

每次渲染都会创建新闭包,useSyncExternalStore 检测到引用变化后会重新订阅(unsubscribe → subscribe)。功能没问题,但可以提到模块顶层来避免这个开销:

const getGlobalSnapshot = () => globalSnapshot;
const snapshotGetters: Record<UploadSource, () => UploadStateSnapshot> = {
  sendbox: () => sourceSnapshots.sendbox,
  workspace: () => sourceSnapshots.workspace,
};

export function useUploadState(source?: UploadSource): UploadStateSnapshot {
  const getSnapshot = source ? snapshotGetters[source] : getGlobalSnapshot;
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}

3. UploadProgressBar 的内联 style 🟡

组件里有不少内联 style(padding, fontSize, borderRadius 等),项目约定优先用 UnoCSS utility classes。动态的 width 保留 style 没问题,但静态属性建议迁移成 class,比如:

<div className='px-12px py-4px text-12px color-text-3'>
  <div className='h-3px rd-2px bg-fill-3 overflow-hidden'>
    <div
      className='h-full rd-2px bg-primary-6 transition-width duration-200 ease'
      style={{ width: `${overallPercent}%` }}
    />
  </div>
</div>

4. defaultValue 里的模板字符串 🟡

defaultValue: `Uploading ${activeCount} file(s)...`

这里用了 JS 模板字符串,fallback 时不会走 i18next 的插值/复数管线。建议改成:

defaultValue: 'Uploading {{count}} file(s)...'

虽然实际翻译文件都已经有值了,但 defaultValue 作为最后防线还是用 i18next 语法更保险。


5. 测试文件的导入路径 🔵

import { trackUpload, useUploadState } from '../../src/renderer/hooks/file/useUploadState';

项目里一般用 @renderer/* 别名,这里用了相对路径,小问题,顺手改了就好。


总结:方案本身没问题,主要是 #1 的 abort 处理建议加上,不然状态可能卡住。其他几点优先级不高,看你的节奏处理就行。

@kaizhou-lab kaizhou-lab self-assigned this Mar 26, 2026
JerryLiu369 and others added 4 commits March 26, 2026 11:28
…pload

Replace fetch() with XMLHttpRequest in uploadFileViaHttp to support
upload progress events. Add a module-level upload state store
(useUploadState) and wire all 4 upload entry points (sendbox drag,
sendbox attach, workspace drag, workspace paste) to report progress.
Show a thin progress bar in both sendbox and workspace toolbar, and
disable the send button while uploads are in flight.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Each upload is now tagged with a source ('sendbox' | 'workspace') so
progress bars only show in the area where the upload originated, and
the send button is only disabled during sendbox uploads.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@JerryLiu369 JerryLiu369 force-pushed the feat/upload-progress branch from 6eebf5a to 8b7bada Compare March 26, 2026 03:28
- Add XHR abort event handler to prevent Promise leaks on cancelled uploads
- Move useSyncExternalStore snapshot getters to module level for stability
- Replace inline styles with UnoCSS utility classes in UploadProgressBar
- Fix i18next defaultValue syntax to use interpolation ({{count}})
- Use @Renderer alias for test import path

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@JerryLiu369
Copy link
Copy Markdown
Member Author

Thanks for the thorough review, @kaizhou-lab!

All 5 points have been addressed:

  1. XHR abort handler — Added xhr.addEventListener('abort', () => reject(new Error('Upload aborted'))) so the Promise rejects and tracker.finish() is always called via the finally block.

  2. Snapshot getter stability — Moved snapshot getters to module level as suggested: getGlobalSnapshot and sourceSnapshotGetters are now stable references, avoiding the re-subscription churn on each render.

  3. UnoCSS classes — Replaced all static inline styles in UploadProgressBar with UnoCSS utility classes. The dynamic width percentage is kept as an inline style since it can't be expressed statically.

  4. i18next defaultValue syntax — Changed to use {{count}} interpolation syntax so the fallback goes through the i18next pipeline correctly.

  5. Test import alias — Updated to use the @renderer/* alias instead of the relative path.

@kaizhou-lab kaizhou-lab merged commit 47a9b36 into iOfficeAI:main Mar 26, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants