# 轉檔流程 — visionA Cloud > Phase 0.8 雲端版新增流程。使用者把 ONNX / TFLite 模型轉成 `.nef`,全程在 visionA Cloud 內完成,不必跳到 converter 站台。 > > 對應 wireframe:[`wireframes/wireframe-conversion.md`](../wireframes/wireframe-conversion.md) > 對應 Feature spec:[`02-prd/features/feature-converter-integration.md`](../../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) ```mermaid sequenceDiagram autonumber participant U as User
(Browser) participant FE as visionA Frontend
(/conversion) participant BE as visionA Backend
(/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
(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
Location: /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) - 完成後點「開始新轉檔」 - 失敗後點「重新開始」 **載入流程:** 1. Mount 時打 `GET /api/conversion/active` 2. 若 `active=true` → 跳 `processing` 或對應 completed 狀態 3. 若 `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 內顯示進度 **送出:** 1. 構造 `FormData`(model file + ref images + 任務名稱 + chip) 2. `XMLHttpRequest` POST 到 `/api/conversion/init` 3. `xhr.upload.onprogress` 更新 progress(`loaded / total`) 4. 計算預估剩餘:取最近 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 策略:** ```typescript 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 ,效果等價) 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: ```typescript 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 1. **「加到模型庫」確認 Dialog 內欄位**:建議**只保留模型名稱**一個欄位,預設 `{job.name}_{chip.toLowerCase()}`。描述 / tags 留 Phase 1。如果 PM 認為要做最簡 UX「點下去就 import 不問」,請明確 confirm,我移除 Dialog(直接走 `/models/{id}` 後使用者再去改名)。 2. **`GET /jobs/active` 端點**:UX 設計依賴「進頁面就知道有沒有 active job」。如果這 API 沒列在 PRD §F 段,請補上;否則建議用 query param `?resume=true` + 前端 localStorage jobId 替代(但體驗較差,跨瀏覽器壞掉)。 3. **「開始新轉檔」 vs 結果保留**:success 畫面下方有兩個動作(result card 內的「加模型庫 / 下載」+ 卡片外面的「開始新轉檔」)。我有意把「開始新轉檔」放結果卡片**外**而不是並列,避免「使用者剛轉完想下載結果,結果一不小心點到開新的」造成困擾。如果 PM 覺得這樣動線不夠順,可以改放結果卡片內,但加 confirm dialog。 4. **錯誤訊息 i18n fallback**:`conversion.error.unknown.*` 用於未知 code,未來 converter 加新 code 時,前端**先**有合理 fallback;後端 i18n 表更新前不會看到 raw code。 ### 8.2 給 Architect 1. **新端點建議:`GET /api/conversion/active`** - 回 `{ active: bool, job?: { id, status, source_filename, target_chip, started_at, expires_at } }` - 用於 idle / 重新整理時恢復狀態 - 沒這個端點 = 前端要自己用 localStorage 記 jobId,跨瀏覽器 / 私密模式 / 多裝置壞掉 2. **Polling 對 backend 的負擔**:5–10 秒/次/user。建議在 visionA backend 對同一個 jobId 做 2–3 秒 cache,避免 hammer converter。預期 10+ concurrent 時必要。 3. **Upload XHR onprogress 的精確度**:streaming proxy 模式下,前端進度 = browser → backend 的進度,不是 backend → converter 的進度。如果 backend buffer 過深,前端 100% 完成但 backend 還在傳,使用者會等不耐煩。建議 backend 在 forward 完成後才回 200,把這段 buffer 算進進度。 4. **Cancel 時的清理**:使用者按「取消上傳」/ 重新整理 → backend 偵測到 stream 結束 → **務必**對 converter 發 cancel,否則該 user 的下一個 submit 會撞 409 直到 converter idle。 5. **Job expires_at 的來源**:success 畫面顯示「6 天 21 小時後清除」需要確切時間。如果 converter 不直接給,backend 自行 `created_at + 7d` 推算並回。 6. **Phase 1 升級時的相容性**:未來 converter 提供 sub-progress(百分比)/ webhook → 前端只要在 `processing` 畫面把 indeterminate 換 determinate progress、減少 polling 頻率即可,UI 結構不變。 ### 8.3 給 Frontend 實作備忘(不是要你照做,是 design 角度的提醒): 1. **`useConversionStore`** 建議結構: ```typescript { 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; // mount 時打 GET /jobs/active submitUpload(payload): Promise; cancelUpload(): void; importToModels(name): Promise; requestDownload(): Promise; reset(): void; } ``` 2. **`usePageTitle(title)`** hook:上傳中 / processing 動態改 `document.title`,cleanup 還原。 3. **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-motion` fallback 為純色)。 4. **`beforeunload` 警告**:只在 uploading 狀態 attach;processing 不需要(離開不會中斷後端)。 5. **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**。