visionA/docs/autoflow/03-design/flows/flow-conversion.md
jim800121chen fb7da5d180 chore(autoflow): migrate .autoflow/ 共享層文件至 docs/autoflow/
依 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)。
2026-05-04 16:55:55 +08:00

551 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 轉檔流程 — 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 510 秒一次 | 不做 SSE / WebSocket前端複雜度低不顯示即時百分比」(converter 不給) |
| D5 | converter API 不動 | UI 直接綁 visionA backend `/api/conversion/*` |
| D6 | Sidebar 獨立 tab不混 `/models` | 入口單一心智清楚 |
---
## 3. 流程全景圖Mermaid
```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 Aidle ──
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 CuploadingXHR 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 Dprocessingpolling──
loop 每 510 秒(分頁可見時)
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 Ecompleted.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 tokenserver-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 0100%)
├── 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可改
- chip4 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; // 060 秒
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 畫面 pollingtab 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
- 顯示成功 Cardwireframe §7
- toast「轉檔完成」+ action「下載 .nef」使用者直接 toast 點下載也 OK
**「加到模型庫」分支:**
```
1. 點按鈕 → 開 AlertDialog含名稱輸入欄
- 預設 name = job.name 或 source filename stem + "_" + chiplowercased
- 例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 / 5xxbackend 還沒到 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 jobconverter 自己 7 天 GC
### 5.6 completed.failed — 顯示錯誤 + 重試引導
**進入時:**
- 停止 polling
- 顯示 failed Cardwireframe §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 job409
PRD §F3 D1converter 端 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 importedB 的 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/sETA 顯示估計 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 的負擔**510 //user建議在 visionA backend 對同一個 jobId 23 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; // 0100
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;
}
```
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 狀態 attachprocessing 不需要(離開不會中斷後端)。
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、有 510 秒延遲(人眼可接受)|
| 進階參數FP16 等)| Phase 1 | Upload Dialog 沒有「進階」摺疊區 |
| 模型版本 / A/B | Phase 2 | model.SourceJobID 已預埋,可追溯但 UI 不展示 |
| Webhookconverter → visionA push| Phase 0.8 純 polling | backend 不訂閱 |
| 上傳離開頁面繼續跑 | Phase 1 | Dialog 關閉 = 上傳取消 |
---
## 10. KPI / 驗收與設計的對應
| KPIPRD §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**。