依 autoflow-agent workspace v2 設計把 PRD / 設計 / 架構 / 交付類 共享文件從個人層 .autoflow/(ignored)搬到 docs/autoflow/(進 git), 讓團隊可共享產品與架構文件,個人層只留 progress / review / testing 等 per-branch 筆記。 - 02-prd/ 21 個檔(PRD、features、market-analysis 等) - 03-design/ 18 個檔(design-spec、wireframes、flows 等) - 04-architecture/ 31 個檔(TDD、design-doc、ADR×14、API 規格等) - 07-delivery/ 3 個檔(project-summary、phase-0.6-handover、stage-deployment-setup) 合計 73 檔。原檔已從 .autoflow/ 移除(migration 工具執行 git mv, 但因 .autoflow/ 在 .gitignore 中、git 將此操作視為新增、無 rename history)。
26 KiB
轉檔流程 — visionA Cloud
Phase 0.8 雲端版新增流程。使用者把 ONNX / TFLite 模型轉成
.nef,全程在 visionA Cloud 內完成,不必跳到 converter 站台。對應 wireframe:
wireframes/wireframe-conversion.md對應 Feature spec:02-prd/features/feature-converter-integration.md
1. User Story
作為 一個 Kneron AI 應用開發者, 我想要 把手上的 ONNX / TFLite 模型直接在 visionA Cloud 轉成
.nef, 這樣 我就能立刻把它部署到我配對的 KL 裝置,不用先去 converter 站台再回來,整個動線是一條直線。
成功條件:
- 從上傳到拿到
.nef的 P95 時間 < 10 分鐘(含上傳 + 轉檔 + promote) - 完成後使用者顯式選擇結果如何處理(加到模型庫 / 下載 / 兩個都做)
- 失敗時錯誤訊息可理解(PRD §F5 對照表)
2. 設計決策摘要
對齊 Feature spec §6「整合決策」。設計面承接的關鍵決策:
| # | 決策 | UX 含意 |
|---|---|---|
| D1 | Upload 走 visionA backend streaming proxy(非 presigned PUT) | 使用者只看到一個進度條:browser → visionA。不暴露 converter 端點 |
| D2 | Download 走 server-side 換 token + 302 redirect → browser 直連 FAA | 點下載 → 前端打 GET /api/conversion/{job_id}/download → backend 302 redirect 到 FAA;沒有第二段「下載到瀏覽器」進度;token 不暴露給前端 JS |
| D3 | 結果處理半自動(user 顯式選擇) | 完成後永遠顯示「加到模型庫」「下載」兩個按鈕,不自動執行 |
| D4 | Polling 5–10 秒一次 | 不做 SSE / WebSocket;前端複雜度低;不顯示「即時百分比」(converter 不給) |
| D5 | converter API 不動 | UI 直接綁 visionA backend 的 /api/conversion/* |
| D6 | Sidebar 獨立 tab,不混 /models |
入口單一、心智清楚 |
3. 流程全景圖(Mermaid)
sequenceDiagram
autonumber
participant U as User<br/>(Browser)
participant FE as visionA Frontend<br/>(/conversion)
participant BE as visionA Backend<br/>(/api/conversion/*)
participant CV as kneron_model_converter
participant FAA as File Access Agent
Note over U,FE: ── State A:idle ──
U->>FE: 進 /conversion
FE->>BE: GET /api/conversion/active
alt 已有 active job
BE-->>FE: { active: true, jobId, status }
FE->>FE: 直接切 processing 畫面(§5)
else 無 active job
BE-->>FE: { active: false }
FE->>U: 顯示空狀態 + 「開始轉檔」CTA
end
Note over U,FE: ── State B:選檔 + 設定 ──
U->>FE: 點「開始轉檔」開 Upload Dialog
U->>FE: 選 .onnx / .tflite + 選 chip + (可選) ref images
FE->>FE: 前端驗證(副檔名、大小、必填)
U->>FE: 按「開始上傳」
Note over U,FE: ── State C:uploading(XHR streaming proxy)──
FE->>BE: POST /api/conversion/init<br/>(multipart, XHR upload.onprogress)
BE->>CV: forward stream 到 POST /api/v1/jobs
CV-->>BE: 201 Created { job_id }
BE-->>FE: 200 { job_id, status: queued }
FE->>FE: Dialog 自動關閉,主畫面切 processing
Note over U,FE: ── State D:processing(polling)──
loop 每 5–10 秒(分頁可見時)
FE->>BE: GET /api/conversion/{job_id}
BE->>CV: GET /api/v1/jobs/{job_id}
CV-->>BE: { status: queued/running/succeeded/failed, ... }
BE-->>FE: { status, error_code?, ... }
end
alt status = succeeded
Note over CV,FAA: converter 內部 promote 已上 FAA
FE->>U: 顯示 success 畫面(§7)
else status = failed
FE->>U: 顯示 failed 畫面(§8)
end
Note over U,FE: ── State E:completed.success ──
alt 使用者點「加到模型庫」
U->>FE: 點 + 確認 Dialog(輸入名稱)
FE->>BE: POST /api/conversion/{job_id}/promote-to-models
BE->>FAA: server-to-server pull NEF
FAA-->>BE: NEF binary
BE->>BE: 走既有 /api/models/init + /finalize
BE-->>FE: 200 { model_id }
FE->>U: toast 「已加入模型庫」+ 連結
end
alt 使用者點「下載」
U->>FE: 點按鈕
FE->>U: window.location.href = '/api/conversion/{job_id}/download'
U->>BE: GET /api/conversion/{job_id}/download
BE->>BE: 跟 MC 換 delegated token(server-side)
BE-->>U: HTTP 302 Redirect<br/>Location: <FAA-URL>/files/{key}?access_token=...
U->>FAA: GET /files/{key}?access_token=...(跟著 redirect)
FAA-->>U: NEF binary(瀏覽器下載)
end
4. State Machine(前端)
/conversion 路由內以單一 store 維護狀態(建議:useConversionStore Zustand):
idle
│
│ click「開始轉檔」
▼
upload-form-open (Upload Dialog 顯示中)
│
│ submit
▼
uploading (Dialog 內、XHR onprogress 0–100%)
│
├── XHR 4xx/5xx → idle (toast error)
├── 取消上傳 → idle (toast canceled)
└── XHR 200 + got job_id
▼
processing (主畫面、polling)
│
├── poll status=succeeded → completed.success
├── poll status=failed → completed.failed
├── poll 5 次 fail → 顯示「重試」按鈕,不離開 processing
└── 使用者離開頁面 / 重新整理 → 下次進入 §3.idle 自動恢復
狀態保存策略(D6 補充):
| 狀態 | 存 localStorage? | 為何 |
|---|---|---|
idle |
否 | 預設狀態 |
upload-form-open 內的選檔 |
否 | 檔案物件無法序列化,使用者重新整理就清空 |
uploading 進度 |
否 | XHR 中斷無法續傳 |
processing job_id |
否 | 由 backend GET /jobs/active 提供 source of truth,不靠前端記 |
completed 結果 |
否 | 重新整理後從 backend 重新取(仍需要顯示給使用者) |
→ 核心原則:前端不持久化 jobId,全部由 backend GET /jobs/active 提供。 這樣「換瀏覽器 / 多分頁 / 私密模式」都能正確還原。
5. State 細節
5.1 idle — 進入頁面 / 完成後重置
進入點:
- 直接進
/conversion(從 sidebar) - 完成後點「開始新轉檔」
- 失敗後點「重新開始」
載入流程:
- Mount 時打
GET /api/conversion/active - 若
active=true→ 跳processing或對應 completed 狀態 - 若
active=false→ 顯示空狀態(wireframe §3)
邊界:
- API 失敗 → 顯示「無法載入轉檔狀態,請重試」+ retry button(不假設沒 active job 就讓使用者開新的,避免重複 submit)
5.2 upload-form-open — Dialog 內選檔
操作:
- 拖拽檔案到 dropzone(或點「選擇檔案」開 file picker)
- 輸入任務名稱(自動帶檔名 stem,可改)
- 選 chip(4 個 RadioGroup 必選)
- 加 ref images(多選,選填)
前端驗證 — submit 前:
| 規則 | 違反訊息 | 阻擋送出 |
|---|---|---|
| 必須選檔 | conversion.upload.error.noFile |
✅ |
副檔名 .onnx / .tflite |
conversion.upload.error.unsupported |
✅ |
| 模型 ≤ 500 MB | conversion.upload.error.modelTooLarge |
✅ |
| 必選 chip | conversion.upload.error.noChip |
✅ |
| 每張 ref ≤ 10 MB | conversion.upload.error.refTooLarge |
✅ 該檔 |
| ref images ≤ 100 張 | conversion.upload.error.refTooMany |
✅ |
驗證失敗顯示在對應欄位下方紅字(text-sm text-destructive),同時「開始上傳」按鈕變 disabled。
取消:
- 點
[✕]或[取消]→ 關 Dialog、回 idle、選檔狀態清空
5.3 uploading — Dialog 內顯示進度
送出:
- 構造
FormData(model file + ref images + 任務名稱 + chip) XMLHttpRequestPOST 到/api/conversion/initxhr.upload.onprogress更新 progress(loaded / total)- 計算預估剩餘:取最近 3 秒移動平均速度
進度條顯示文案:
| 狀態 | 顯示 |
|---|---|
progress < 100% |
已上傳 11.9 / 28.4 MB · 預估剩餘 0:24 |
progress = 100% 但 server 還沒回 |
即將完成…(XHR 已送完但 backend 還在 forward 到 converter) |
| 等待時間超過 5 秒 | 伺服器處理中… |
離開警告:
window.addEventListener('beforeunload', e => { e.preventDefault(); e.returnValue = ''; })- 取消 / 完成 / 失敗時 cleanup
Tab 標題更新:
visionA Cloud · 上傳中 (42%) ← 動態插入百分比
完成後還原為 visionA Cloud。
取消:
- 點「取消上傳」→ AlertDialog 確認 →
xhr.abort()→ toast「已取消上傳」→ 回 idle - 重要:visionA backend 收到取消信號後也要對 converter 發 cancel(避免孤立 job)
失敗:
| HTTP | 行為 |
|---|---|
| 4xx 一般(不含 409) | Dialog 內紅字顯示錯誤;保留 「重試」按鈕(重新打 submit) |
409 user_has_active_job |
Dialog 關閉 → 切 processing 畫面 + banner「您已有一個轉檔正在進行中,已切換至該任務」 |
| 5xx / 網路 | Dialog 內顯示「上傳失敗:{訊息}」+ 「重試」 |
5.4 processing — 主畫面、polling
Polling 策略:
const POLL_FAST_INTERVAL = 5_000; // 0–60 秒
const POLL_SLOW_INTERVAL = 10_000; // 60+ 秒
const POLL_MAX_RETRIES = 5;
// 暫停 / 恢復
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
pollNow();
resumePolling();
} else {
pausePolling();
}
});
狀態變化處理:
| 從 → 到 | 觸發 | UI 更新 |
|---|---|---|
queued → queued |
持續 polling | 無變化(保持 stage 1 完成、stage 2 當前) |
queued → running |
第一次拿到 running | stage 2 標完成、stage 3 當前 |
running → running |
持續 polling | indeterminate progress 持續動 |
* → succeeded |
拿到 succeeded | 切 §5.5 success 畫面、停 polling、tab title 更新 |
* → failed |
拿到 failed | 切 §5.6 failed 畫面、停 polling |
| 連 5 次 polling 失敗 | exponential backoff 用完 | 顯示「無法取得轉檔狀態」+ retry 按鈕(不切走 processing) |
Tab 標題更新:
| 狀態 | Tab title |
|---|---|
| processing | visionA Cloud · 轉檔中… |
| 完成(5 秒內持續顯示) | ✓ 轉檔完成 · visionA Cloud |
| 失敗 | ⚠ 轉檔失敗 · visionA Cloud |
完成 / 失敗時用 emoji 是為了在分頁列上一眼可見;reduced-motion / SR 友善(純文字)。
長時間排隊提示:
queued持續 > 5 分鐘 → banner「目前排隊較久,你可以離開此頁稍後再回」running持續 > 15 分鐘 → banner「轉檔耗時較長,仍在進行中」
5.5 completed.success — 顯示結果 + 半自動分支
進入時:
- 停止 polling
- 顯示成功 Card(wireframe §7)
- toast「轉檔完成」+ action「下載 .nef」(使用者直接 toast 點下載也 OK)
「加到模型庫」分支:
1. 點按鈕 → 開 AlertDialog(含名稱輸入欄)
- 預設 name = job.name 或 source filename stem + "_" + chip(lowercased)
- 例:yolov5s.onnx + KL720 → "yolov5s_kl720"
2. 使用者確認名稱 → 按「加到模型庫」
3. 按鈕變 spinner + disabled(避免重複點)
4. POST /api/conversion/{job_id}/promote-to-models
body: { name }
5. 成功(200 + model_id):
- toast「已加入模型庫」+ action 連結 /models/{model_id}
- 結果 Card 內按鈕變綠勾「✓ 已加入(前往查看 →)」
- 按鈕仍可重複點(再點會 409 重複)
6. 409 already imported:
- toast「此任務已加入過模型庫」+ action「查看現有模型 →」
7. 其他錯誤:toast 顯示 + 按鈕回原狀態
「下載」分支:
1. 點按鈕 → 按鈕短暫變 spinner + 「準備下載…」
2. window.location.href = '/api/conversion/{job_id}/download'
(或用 anchor tag <a href="..." download>,效果等價)
3. backend 收到後 server-side 跟 MC 換 delegated token,
直接回 HTTP 302 Redirect → Location 指向 FAA URL
4. 瀏覽器跟著 302 跳轉,內建下載管理器接管下載
- 按鈕回原狀態(可重複點 → 重新換新 token)
- toast「下載已開始」+ 「若沒看到下載提示,請檢查瀏覽器設定」
5. 4xx / 5xx(backend 還沒到 redirect 階段就 fail):
- 因為已經 navigation,瀏覽器會顯示 backend 的錯誤頁
- 為避免此情境,可選用 fetch + manual handling 偵測 status 後再 navigate
- 簡化版:直接 navigate,靠 backend 顯示錯誤頁;遇到 4xx/5xx 使用者按 back 即可
注意:token 不暴露給 frontend JS,整個換 token 流程在 backend 內完成。
為何兩個按鈕互不互斥? PRD §F4 D3 明文:使用者可以兩個都做(先試試下載驗證、再加進模型庫;或反過來)。按鈕不會因為按過就消失,只會在 import 成功後加綠勾標記。
過期提醒:
- 計算
expires_at - now()→ 顯示「6 天 21 小時後自動清除」 - 每分鐘更新一次(不需要每秒)
- 過期當下:頁面切「已過期」狀態(wireframe §8.2),按鈕全部 disabled
「開始新轉檔」:
- 點擊 → 重置 store → 回 idle 狀態
- 不需要清除 backend job(converter 自己 7 天 GC)
5.6 completed.failed — 顯示錯誤 + 重試引導
進入時:
- 停止 polling
- 顯示 failed Card(wireframe §8)
- toast「轉檔失敗:{user-friendly message}」
錯誤翻譯: 對照 PRD §F5 表 + wireframe §8.1。前端從 error_code 查 i18n key:
const message = t(`conversion.error.${errorCode}.message`)
?? t('conversion.error.unknown.message');
const suggestions = t(`conversion.error.${errorCode}.suggestions`)
?? t('conversion.error.unknown.suggestions');
未知 code 一律 fallback 到 unknown。
「重新開始」:
- 重置 store → 回 idle
- 上一個失敗的 job 不會自動重送(converter 根本沒收到,或已 failed);使用者需要重新選檔
「回模型庫」:
router.push('/models')
「複製任務 ID」:
navigator.clipboard.writeText(fullJobId)→ toast「已複製任務 ID」
6. 邊界情境
6.1 同 user 已有 active job(409)
PRD §F3 D1:converter 端 enforce「同 user 同時 1 個 active job」。
| 情境 | UI 行為 |
|---|---|
進入 /conversion、有 active |
直接落 processing(§5.1) |
| 點「開始轉檔」、submit 時拿到 409 | Dialog 自動關閉、切 processing、banner「您已有一個轉檔正在進行中,已切換至該任務」 |
| 點「開始新轉檔」(在 success 畫面)、實際上有別的 active | 同上(理論上 success 表示自己的 job 已結束,不會撞) |
→ 設計上不要在 idle 顯示「您有 active job」就把 CTA 變 disabled,因為 §5.1 已經會直接跳走。
6.2 上傳到一半失敗
| 失敗點 | 已產生 converter job? | UI 行為 |
|---|---|---|
| 網路斷在前段(XHR 還在 forward) | 否 | toast「上傳失敗,請重試」+ Dialog 內可 retry |
| 網路斷在 backend → converter 之間 | 可能(看 backend 實作) | backend 應 cancel converter job,前端 retry 不會撞 409 |
| 取消上傳 | 視 backend 實作 | backend 應 cancel converter job |
→ 給 Architect 的補充:visionA backend 在 forward stream 失敗 / 收到 cancel 時,必須對 converter 發 POST /api/v1/jobs/{id}/cancel,否則使用者下一次 submit 就撞 409 直到 converter idle 為止。
6.3 Job 7 天後過期
converter Phase 1 已實作 7 天 GC。前端體驗:
| 進入點 | UI |
|---|---|
| 使用者重新整理 success 畫面(過期後) | GET /jobs/active 回 404 → 進 idle;如果有 backend cache 顯示「已過期」hint card 更友善 |
| 使用者點「下載」/「加到模型庫」(過期後) | 4xx 失敗 → toast 顯示 + 自動切「已過期」狀態 |
理想做法:success 畫面每分鐘 check 一次 expires_at,到期當下自動切「已過期」(不靠 polling 回 404)。
6.4 多分頁同時開 /conversion
| 情境 | 行為 |
|---|---|
| 兩個分頁都在 idle | 各自獨立、互不影響 |
| 分頁 A submit 開始 upload,分頁 B 進 idle | 分頁 B 會在頁面 mount 時打 /jobs/active,發現 A 已開始 → 直接落 processing 畫面 |
| 兩個分頁同時點「開始轉檔」並各自 submit | 第二個 submit 會收 409 → 切 processing 顯示已存在的 job |
| 一個分頁完成、按「加到模型庫」、另一個分頁仍在 processing | A 已 model imported;B 的 polling 拿到 succeeded 也切到 success 畫面,不會撞(兩個分頁狀態一致) |
→ 不需要跨分頁通訊(BroadcastChannel),靠 backend 是 source of truth 就足夠。
6.5 使用者在 uploading 中重新整理 / 關掉
- XHR 中斷
- backend 偵測到 stream 結束(沒收滿)→ cancel converter job
- 使用者重進頁面 →
/jobs/active回 false → 落 idle
6.6 使用者在 processing 中重新整理 / 關掉 / 切走
- 沒影響:backend / converter 繼續跑
- 重進 →
/jobs/active回 true → 落 processing → 繼續 polling
→ 這是 visionA Cloud 相對 local-tool 的核心優勢:「跑一個轉檔可以離開電腦」。在 idle 空狀態與 processing hint 都會說明這點。
6.7 上傳大檔(500 MB)的 UX
- 進度條 + ETA(基於
loaded / total移動平均) - 不擋 UI:使用者可以離開
/conversion切到別頁(XHR 仍在背景跑、Dialog 關掉但 XHR 不取消)- ⚠️ 雛形範圍不做這個(會把上傳邏輯從 component 拆到 store / context)。Phase 0.8 規格:Dialog 關掉 = 上傳取消。
- 文案上 §11 的
conversion.uploading.warning已聲明「請勿關閉此分頁」
- 分頁標題持續更新百分比(讓使用者切到別的分頁也能看進度)
- 慢網(< 1 MB/s):ETA 顯示「估計 8 分鐘」這種長時間,使用者要意識到要等
6.8 下載失敗 / 取消
window.location.href = url是瀏覽器 navigation,不會回到 visionA 顯示錯誤- 如果 token 已過期(5 分鐘 TTL),瀏覽器會顯示 FAA 的 403 頁面
- 緩解:使用者回
/conversion再點一次「下載」即可重拿 token
7. UX Writing 要點
對齊 design-spec.md §1.1「誠實呈現狀態」+ components.md §12「對開發者語調」:
| 場景 | 寫法 | 不要 |
|---|---|---|
| 空狀態 heading | 「還沒有進行中的轉檔」 | ❌「您尚未建立任何轉換任務」(過度禮貌) |
| 開始按鈕 | 「開始轉檔」 | ❌「立即開始」「執行轉換」(贅字) |
| processing hint | 「你可以離開此頁面,回來時會自動更新進度」 | ❌「請耐心等候」(沒提供資訊) |
| 失敗 | 「模型內含不支援的運算子,無法量化到目標晶片」 | ❌「轉檔失敗,請重試」(沒說原因) |
| 過期 | 「此轉檔結果保留期為 7 天,目前已超過保留期限並自動清除」 | ❌「資源已不存在」(technical 語) |
| 取消上傳確認 | 「上傳尚未完成,確定取消?」 | ❌「您確定要中止此操作嗎?」 |
| 加入模型庫 toast | 「已加入模型庫」 | ❌「您的模型已成功加入至模型庫中」 |
→ 全部走 i18n key(§wireframe §11),不在元件 hardcode。
8. 給其他 Agent 的補充
8.1 給 PM
- 「加到模型庫」確認 Dialog 內欄位:建議只保留模型名稱一個欄位,預設
{job.name}_{chip.toLowerCase()}。描述 / tags 留 Phase 1。如果 PM 認為要做最簡 UX「點下去就 import 不問」,請明確 confirm,我移除 Dialog(直接走/models/{id}後使用者再去改名)。 GET /jobs/active端點:UX 設計依賴「進頁面就知道有沒有 active job」。如果這 API 沒列在 PRD §F 段,請補上;否則建議用 query param?resume=true+ 前端 localStorage jobId 替代(但體驗較差,跨瀏覽器壞掉)。- 「開始新轉檔」 vs 結果保留:success 畫面下方有兩個動作(result card 內的「加模型庫 / 下載」+ 卡片外面的「開始新轉檔」)。我有意把「開始新轉檔」放結果卡片外而不是並列,避免「使用者剛轉完想下載結果,結果一不小心點到開新的」造成困擾。如果 PM 覺得這樣動線不夠順,可以改放結果卡片內,但加 confirm dialog。
- 錯誤訊息 i18n fallback:
conversion.error.unknown.*用於未知 code,未來 converter 加新 code 時,前端先有合理 fallback;後端 i18n 表更新前不會看到 raw code。
8.2 給 Architect
- 新端點建議:
GET /api/conversion/active- 回
{ active: bool, job?: { id, status, source_filename, target_chip, started_at, expires_at } } - 用於 idle / 重新整理時恢復狀態
- 沒這個端點 = 前端要自己用 localStorage 記 jobId,跨瀏覽器 / 私密模式 / 多裝置壞掉
- 回
- Polling 對 backend 的負擔:5–10 秒/次/user。建議在 visionA backend 對同一個 jobId 做 2–3 秒 cache,避免 hammer converter。預期 10+ concurrent 時必要。
- Upload XHR onprogress 的精確度:streaming proxy 模式下,前端進度 = browser → backend 的進度,不是 backend → converter 的進度。如果 backend buffer 過深,前端 100% 完成但 backend 還在傳,使用者會等不耐煩。建議 backend 在 forward 完成後才回 200,把這段 buffer 算進進度。
- Cancel 時的清理:使用者按「取消上傳」/ 重新整理 → backend 偵測到 stream 結束 → 務必對 converter 發 cancel,否則該 user 的下一個 submit 會撞 409 直到 converter idle。
- Job expires_at 的來源:success 畫面顯示「6 天 21 小時後清除」需要確切時間。如果 converter 不直接給,backend 自行
created_at + 7d推算並回。 - Phase 1 升級時的相容性:未來 converter 提供 sub-progress(百分比)/ webhook → 前端只要在
processing畫面把 indeterminate 換 determinate progress、減少 polling 頻率即可,UI 結構不變。
8.3 給 Frontend
實作備忘(不是要你照做,是 design 角度的提醒):
useConversionStore建議結構:{ state: 'idle' | 'uploading' | 'processing' | 'success' | 'failed' | 'expired'; job: ConversionJob | null; // 來自 GET /jobs/active 或 polling uploadProgress: number; // 0–100 uploadEta: number; // seconds, 移動平均算 pollErrorCount: number; // actions hydrate(): Promise<void>; // mount 時打 GET /jobs/active submitUpload(payload): Promise<void>; cancelUpload(): void; importToModels(name): Promise<void>; requestDownload(): Promise<void>; reset(): void; }usePageTitle(title)hook:上傳中 / processing 動態改document.title,cleanup 還原。- Indeterminate Progress:shadcn
Progress不帶 value 時是空條,需要加 CSS animation(建議bg-gradient-to-r from-primary via-primary/40 to-primary+animate-[shimmer_2s_linear_infinite],並對prefers-reduced-motionfallback 為純色)。 beforeunload警告:只在 uploading 狀態 attach;processing 不需要(離開不會中斷後端)。- Test 重點:state machine 的轉移是核心邏輯;建議寫 component test(手動觸發 store action、assert UI),加上
GET /jobs/active三種回應的 visual snapshot。
9. 不在 Phase 0.8 範圍
對齊 PRD §5 + wireframe §13:
| 項目 | 何時做 | 影響 UX 的部分 |
|---|---|---|
| 轉檔歷史清單 | Phase 1 / 之後 | 目前使用者只能看「眼前這個 job」,跑完換新的舊的看不到(converter 7 天 GC 也會清) |
| 取消正在跑的 job | Phase 1 | processing 畫面沒有取消按鈕(converter 已支援,UI 不暴露) |
| 多 chip 同時轉 | converter 不支援 | RadioGroup 單選、不是 Checkbox |
| SSE / WebSocket | Phase 1 量大時 | 純 polling、有 5–10 秒延遲(人眼可接受) |
| 進階參數(FP16 等) | Phase 1 | Upload Dialog 沒有「進階」摺疊區 |
| 模型版本 / A/B | Phase 2 | model.SourceJobID 已預埋,可追溯但 UI 不展示 |
| Webhook(converter → visionA push) | Phase 0.8 純 polling | backend 不訂閱 |
| 上傳離開頁面繼續跑 | Phase 1 | Dialog 關閉 = 上傳取消 |
10. KPI / 驗收與設計的對應
| KPI(PRD §8) | 設計面如何支撐 |
|---|---|
| 第一個內部使用者轉檔成功率 > 80% | 失敗訊息精準、suggestions 引導;前端驗證提早攔下不支援格式 |
| 上傳到 NEF P95 < 10 分鐘 | Polling 間隔合理、不擋使用者離開頁面、tab title 通知 |
| 「加到模型庫」點擊率 > 50% | 兩個按鈕視覺權重相當(不偏左 / 不預設 highlight)、Dialog 摩擦力低 |
| 失敗錯誤訊息可理解率 100% | §F5 對照表 + i18n unknown fallback |
11. 待 Reviewer 確認的設計選擇
整理本流程中我做了選擇但可逆的決定,給 Design / PM 後續 review:
| # | 決策點 | 我的選擇 | 替代方案 | 影響 |
|---|---|---|---|---|
| Q1 | sidebar icon | Wand2 ✨ |
FileCog / Replace |
視覺風格 |
| Q2 | sidebar 位置 | 模型庫之後 | 設定之前 / 工作區之後 | 心智模型 |
| Q3 | 「加到模型庫」是否需 Dialog 確認名稱 | 需要、單欄位 | 靜默 import / 多欄位(含描述) | 摩擦力 vs 控制感 |
| Q4 | 「開始新轉檔」位置 | 結果卡片外下方 | 結果卡片內、與兩個 action 並列 | 誤點風險 |
| Q5 | uploading 階段 Dialog 內顯示進度 vs 全頁切換 | Dialog 內 | 上傳完直接全頁切 processing 並顯示進度 | 視覺一致性 vs 多狀態切換次數 |
| Q6 | processing 不給取消按鈕 | 不給 | 給 + 確認 dialog | UX 安全 vs 控制感 |
| Q7 | success 兩按鈕順序 | 「加到模型庫」左、「下載」右 | 反過來 | 主動作優先級 |
如果使用者 / PM / Architect 對任一項有不同意見,文件以這份為準,調整後回頭更新此表 + wireframe + i18n。