依 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)。
925 lines
64 KiB
Markdown
925 lines
64 KiB
Markdown
# Wireframe — 轉檔(`/conversion`)
|
||
|
||
> 文字版 wireframe。Phase 0.8 雲端版新增頁面 — 把使用者「ONNX → NEF」的轉檔旅程留在 visionA Cloud 內完成,不必再跳到 converter 站台。
|
||
>
|
||
> 對應流程文件:[`flows/flow-conversion.md`](../flows/flow-conversion.md)
|
||
> 對應 Feature spec:[`02-prd/features/feature-converter-integration.md`](../../02-prd/features/feature-converter-integration.md)
|
||
|
||
---
|
||
|
||
## 0. 設計對齊備註
|
||
|
||
- **版型**:沿用既有 AppShell(Sidebar + Header + main)。轉檔頁 `/conversion`(單一 route)內以 state 機切四個畫面 — `idle` / `uploading` / `processing` / `completed`(含 success / failed 兩支線),不開新分頁。
|
||
- **Sidebar 加位**:在「裝置 / 模型 / 工作區」之後、「叢集 / 設定」之前,與「Models」相鄰,視覺上把「上傳模型」與「轉檔產生模型」放成兩個並排入口,符合心智模型。Icon 採 Lucide [`Wand2`](https://lucide.dev/icons/wand-2) — 「魔法棒」隱喻「把一個格式變成另一個」,比 `Workflow`(流程感過重)/ `Cpu`(已被裝置語義佔用)/ `RefreshCw`(暗示同步、不是轉換)更貼切。**最終決定見 §10「icon 替代方案」**。
|
||
- **元件複用**:上傳區塊抄一份 `ModelUploadDialog` 改名 `ConversionUploadDialog`(不直接共用,因為需求差太多 — 轉檔有 chip 必填、ref images 多檔、500 MB 上限)。其餘 Dialog / Card / Button / Progress / Badge / EmptyState / Sonner toast 全部直接複用。
|
||
- **Design Tokens**:不新增任何 token。chip 選擇器底色用 `--accent`,進度條 `--primary`,error 用 `--destructive`,警示 banner 沿用 `bg-amber-50 / dark:bg-amber-950/30` + `border-amber-300`。
|
||
- **i18n**:所有文案都會走 i18n(zh-TW + en),key 命名空間 `conversion.*`,整理在 §11。
|
||
|
||
---
|
||
|
||
## 1. Sidebar 與進入點
|
||
|
||
### 1.1 Sidebar 變更(追加項目)
|
||
|
||
```
|
||
┌────────────────────────┐
|
||
│ [vA] visionA Cloud │ h-14, border-b
|
||
├────────────────────────┤
|
||
│ ▸ 儀表板 │
|
||
│ ▸ 裝置 │
|
||
│ ▸ 模型庫 │
|
||
│ ▸ 工作區 │
|
||
│ ▸ 轉檔 ← new │ ← Wand2 icon
|
||
│ ▸ 叢集 │
|
||
│ ▸ 設定 │
|
||
│ │
|
||
│ (flex-1) │
|
||
├────────────────────────┤
|
||
│ v0.1.0 · Phase 0.8 │
|
||
└────────────────────────┘
|
||
```
|
||
|
||
新增的 NavItem:
|
||
|
||
```ts
|
||
{ href: "/conversion", labelKey: "nav.conversion", icon: Wand2 }
|
||
```
|
||
|
||
i18n:
|
||
- `nav.conversion` → 繁中「轉檔」/ English「Convert」
|
||
|
||
放置位置決策:**模型庫之後**。理由:使用者心智「我有一個外部模型,想讓它能在我的 KL 裝置上跑」的下一步通常是「我已經知道有 Models 頁可以管模型,那 Convert 應該就在它附近」。
|
||
|
||
### 1.2 入口
|
||
|
||
- 主入口:Sidebar 「轉檔」tab → 進 `/conversion`
|
||
- 次要入口(Phase 0.8 不做但保留思考):`/models` 上傳 Dialog 內加一個「我有 ONNX,需要先轉檔 →」連結;先不做避免分支太多。
|
||
|
||
---
|
||
|
||
## 2. 頁面狀態總覽
|
||
|
||
`/conversion` 是單頁,內部依 state 機切四種畫面。state 由前端 store 決定(`useConversionStore`,等 frontend 時再實作),不靠 URL query。
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ idle ┌───────► processing ─────┐ │
|
||
│ (無進行中 job、 │ (polling) │ │
|
||
│ 顯示空狀態 + CTA) │ ▼ │
|
||
│ │ │ completed │
|
||
│ ▼ │ ├─ success │
|
||
│ uploading ───────┘ └─ failed │
|
||
│ (XHR 進度條 0–100%) (兩種分支) │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
| 狀態 | 觸發進入 | 觸發離開 |
|
||
|------|---------|---------|
|
||
| `idle` | 預設 / 完成後按「開始新轉檔」/ 失敗後按「重新開始」 | 點「開始轉檔」開 Upload Dialog |
|
||
| `uploading` | Upload Dialog 內按「開始上傳」 | XHR 完成(→ processing)/ 失敗(→ idle + toast)/ 取消 |
|
||
| `processing` | upload 完成、收到 `job_id` | poll 到 `succeeded` / `failed` |
|
||
| `completed.success` | poll 到 `status: succeeded` | 「開始新轉檔」回 `idle` |
|
||
| `completed.failed` | poll 到 `status: failed` 或拿到非 200 | 「重新開始」回 `idle` |
|
||
|
||
---
|
||
|
||
## 3. State A:`idle` — 無進行中 Job(預設畫面)
|
||
|
||
### 3.1 整體版型(Desktop, ≥ 1024px)
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ [Sidebar] [Header] │
|
||
│ ────────────────────────────────────────────────────────────────── │
|
||
│ mx-auto max-w-7xl px-6 py-8 space-y-6 │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||
│ │ 轉檔 text-2xl font-bold │ │
|
||
│ │ 把 ONNX / TFLite 模型轉成 .nef,跑在 Kneron 邊緣裝置上 │ │
|
||
│ │ text-muted-foreground │ │
|
||
│ └──────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||
│ │ EmptyState (rounded-xl border bg-card py-16 text-center) │ │
|
||
│ │ │ │
|
||
│ │ ✨ (Wand2, h-12 w-12, text-muted-foreground) │ │
|
||
│ │ │ │
|
||
│ │ 還沒有進行中的轉檔 text-lg font-semibold │ │
|
||
│ │ │ │
|
||
│ │ 上傳一個 ONNX / TFLite 模型,選擇目標 Kneron 晶片, │ │
|
||
│ │ 我們幫你產出可直接燒錄的 .nef 檔案 │ │
|
||
│ │ (max-w-md mx-auto text-sm text-muted-foreground) │ │
|
||
│ │ │ │
|
||
│ │ ┌────────────────────────────┐ │ │
|
||
│ │ │ [✨ 開始轉檔] │ │ │
|
||
│ │ │ variant=default size=lg │ │ │
|
||
│ │ └────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ 支援 .onnx / .tflite · 最大 500 MB │ │
|
||
│ │ text-xs text-muted-foreground │ │
|
||
│ │ │ │
|
||
│ └──────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||
│ │ ℹ 關於轉檔 │ │
|
||
│ │ text-sm text-muted-foreground (折疊區,預設展開) │ │
|
||
│ │ • 一次只能跑一個轉檔任務(包含其他分頁) │ │
|
||
│ │ • 完成後 7 天內可下載結果,過期自動清除 │ │
|
||
│ │ • 轉檔約耗時 1–10 分鐘,依模型大小而定 │ │
|
||
│ └──────────────────────────────────────────────────────────────┘ │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.2 互動
|
||
|
||
| 元素 | 互動 | 行為 |
|
||
|------|------|------|
|
||
| 「開始轉檔」CTA | click / Enter / Space | 開啟 `ConversionUploadDialog`(見 §4)|
|
||
|
||
### 3.3 邊界 — 已有 active job
|
||
|
||
進入 `/conversion` 時前端**先打一次** `GET /api/conversion/active`(visionA backend 提供,回傳該 user 是否有進行中的 job)。
|
||
|
||
| 回應 | UI 行為 |
|
||
|------|---------|
|
||
| `{ active: false }` | 顯示 `idle` 畫面(如上) |
|
||
| `{ active: true, jobId, status, ... }` | 直接跳 `processing` 畫面(見 §6),banner 加註「您離開前的轉檔仍在進行中」|
|
||
|
||
> 這個檢查也涵蓋「使用者開了第二個分頁」「重新整理」「離開後再回來」三種情境,**不需要前端額外狀態保存**。
|
||
|
||
---
|
||
|
||
## 4. Upload Dialog(`ConversionUploadDialog`)
|
||
|
||
複用 `Dialog`、`Input`、`Label`、`Select`、`Button`、`Progress` 元件;參考既有 `ModelUploadDialog` 改寫。
|
||
|
||
### 4.1 階段 A — 選檔與設定(`select`)
|
||
|
||
```
|
||
┌────────────────────────────────────────────────────────────┐
|
||
│ 開始轉檔 [✕] │
|
||
│ DialogHeader · DialogTitle · DialogDescription │
|
||
│ 上傳模型、選擇目標晶片,可選擇加上 reference images 提升精度 │
|
||
├────────────────────────────────────────────────────────────┤
|
||
│ space-y-5 px-6 py-4 │
|
||
│ │
|
||
│ Label: 來源模型 * │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ 📁 拖曳 .onnx / .tflite 到此處 │ │
|
||
│ │ │ │
|
||
│ │ 或 [選擇檔案] │ │
|
||
│ │ │ │
|
||
│ │ 支援格式:.onnx · .tflite · 最大 500 MB │ │
|
||
│ │ │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
│ border-2 border-dashed rounded-md bg-muted/50 h-32 │
|
||
│ 選了之後變成: │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ 📄 yolov5s.onnx · 28.4 MB [✕ 移除] │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Label: 任務名稱(選填) │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ yolov5s(預設帶檔名 stem,可改) │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
│ hint: text-xs text-muted-foreground │
|
||
│ 顯示用,不影響輸出檔名 │
|
||
│ │
|
||
│ Label: 目標晶片 * │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │
|
||
│ │ │KL520 │ │KL630 │ │KL720 │ │KL730 │ │ │
|
||
│ │ │ ● │ │ │ │ │ │ │ │ │
|
||
│ │ └──────┘ └──────┘ └──────┘ └──────┘ │ │
|
||
│ │ 4 個 ChipPill (RadioGroup),單選 │ │
|
||
│ │ active: bg-primary text-primary-foreground │ │
|
||
│ │ hover: bg-accent │ │
|
||
│ │ border rounded-md px-4 py-3 cursor-pointer min-w-20 │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Label: Reference images(選填) │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ 📁 拖曳圖片到此處(選填) │ │
|
||
│ │ 或 [選擇檔案] · 最多 100 張,每張 ≤ 10 MB │ │
|
||
│ │ border-2 border-dashed rounded-md bg-muted/30 h-20 │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
│ 選了之後: │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ 已選 12 張 ref images(共 24.3 MB) [移除全部] │ │
|
||
│ │ ▸ 縮圖 grid(hover 顯示 ✕ 個別移除) │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
│ hint: 加上 ref images 可提升量化後精度(可選) │
|
||
│ │
|
||
├────────────────────────────────────────────────────────────┤
|
||
│ DialogFooter │
|
||
│ [取消] [開始轉檔] │
|
||
│ variant=outline · variant=default │
|
||
│ │
|
||
│ 「開始轉檔」disabled 條件:未選檔 / 未選 chip / 任一檔超大 │
|
||
└────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**前端驗證(按下「開始轉檔」前):**
|
||
|
||
| 驗證 | 失敗訊息 |
|
||
|------|---------|
|
||
| 必須選檔 | 「請選擇 .onnx 或 .tflite 檔案」|
|
||
| 副檔名為 `.onnx` 或 `.tflite` | 「不支援的格式,請改用 ONNX 或 TFLite」|
|
||
| 模型 ≤ 500 MB | 「模型超過 500 MB 上限,請改用較小的模型」|
|
||
| 必須選 chip | 「請選擇目標晶片」|
|
||
| 每張 ref image ≤ 10 MB | 「{filename} 超過 10 MB,請移除或壓縮後再試」|
|
||
| ref images 總數 ≤ 100 張 | 「Reference images 上限 100 張」|
|
||
|
||
驗證失敗:error 顯示在對應欄位下方(`text-sm text-destructive`),不發 API。
|
||
|
||
### 4.2 階段 B — 上傳中(`uploading`)
|
||
|
||
按下「開始轉檔」後 Dialog 內容切換(**不關 Dialog**,使用者要看到進度):
|
||
|
||
```
|
||
┌────────────────────────────────────────────────────────────┐
|
||
│ 上傳中 [✕*] │
|
||
├────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ 📤 正在上傳到 visionA… │
|
||
│ text-base font-medium │
|
||
│ │
|
||
│ yolov5s.onnx · 28.4 MB │
|
||
│ text-sm text-muted-foreground │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────┐ │
|
||
│ │ ████████████████████░░░░░░░░░░░░░░░░ 42% │ │
|
||
│ └──────────────────────────────────────────────┘ │
|
||
│ Progress bar (h-2, --primary) │
|
||
│ │
|
||
│ 已上傳 11.9 / 28.4 MB · 預估剩餘 0:24 │
|
||
│ text-xs text-muted-foreground │
|
||
│ │
|
||
│ ⚠ 請勿關閉此分頁,否則上傳會中斷 │
|
||
│ text-xs text-amber-700 dark:text-amber-300 │
|
||
│ │
|
||
├────────────────────────────────────────────────────────────┤
|
||
│ [取消上傳] │
|
||
│ variant=outline │
|
||
└────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
- 進度由 XHR `upload.onprogress`(`loaded / total`)計算
|
||
- 預估剩餘 = 用最近 3 秒移動平均速度算 ETA;不足 1 秒顯示「即將完成…」
|
||
- `[✕*]` Dialog 右上角關閉按鈕在此階段**保留可點**,但點擊會問 AlertDialog「上傳尚未完成,確定取消?」
|
||
- 「取消上傳」:`xhr.abort()` → 回 `idle` 畫面 + toast「已取消上傳」
|
||
- `beforeunload`:上傳中觸發瀏覽器原生離開警告(visionA backend 已 cancel converter job)
|
||
|
||
### 4.3 階段 C — 上傳完成、轉檔啟動
|
||
|
||
XHR 200 後,visionA backend 已 forward 到 converter 並拿到 `job_id`:
|
||
- Dialog 自動關閉
|
||
- 主畫面切到 §6 `processing` 狀態
|
||
- toast「已開始轉檔(任務 #{shortJobId})」
|
||
|
||
### 4.4 階段 D — 上傳失敗
|
||
|
||
| 錯誤 | UI |
|
||
|------|----|
|
||
| 409 已有 active job | Dialog 關閉,主畫面切 `processing` 並 banner 提示「您已有一個轉檔正在進行中,已切換至該任務」|
|
||
| 4xx(檔案被拒)| Dialog 內顯示錯誤紅字 + 「重試」按鈕 |
|
||
| 5xx / 網路 | Dialog 內顯示「上傳失敗:{訊息}」+ 「重試」 |
|
||
|
||
---
|
||
|
||
## 5. State:`uploading`(在 Dialog 內,非全頁)
|
||
|
||
實際上 uploading 是 Dialog 內的階段(§4.2),主畫面背後仍是 `idle`。**主畫面不顯示進度條**,避免使用者以為要看兩處。
|
||
|
||
但**瀏覽器分頁標題**會更新:
|
||
|
||
```
|
||
visionA Cloud · 上傳中 (42%)
|
||
```
|
||
|
||
讓使用者就算切到別的分頁也能看到進度。
|
||
|
||
---
|
||
|
||
## 6. State:`processing` — 轉檔進行中
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ [Sidebar] [Header] │
|
||
│ ────────────────────────────────────────────────────────────────── │
|
||
│ mx-auto max-w-7xl px-6 py-8 space-y-6 │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||
│ │ 轉檔 │ │
|
||
│ │ text-2xl font-bold │ │
|
||
│ └──────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||
│ │ Card: 進行中 (border, rounded-xl, p-6) │ │
|
||
│ │ │ │
|
||
│ │ ┌──────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ 📄 yolov5s.onnx → KL720 🔵 轉檔中 │ │ │
|
||
│ │ │ Badge: bg-blue-500 text-white animate-pulse │ │ │
|
||
│ │ │ 任務 #a1b2c3d4 · 開始於 5 分鐘前 │ │ │
|
||
│ │ │ text-sm text-muted-foreground │ │ │
|
||
│ │ └──────────────────────────────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ 進度(stage indicator,不可點) │ │
|
||
│ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │
|
||
│ │ │ ✓ │────│ ● │────│ 3 │ │ │
|
||
│ │ │ 1 │ │ 2 │ │ │ │ │
|
||
│ │ └──────┘ └──────┘ └──────┘ │ │
|
||
│ │ 上傳完成 解析模型 編譯 NEF │ │
|
||
│ │ text-sm text-foreground text-muted-foreground │ │
|
||
│ │ │ │
|
||
│ │ ┌──────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ 處理中… │ │ │
|
||
│ │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━ animate-pulse │ │ │
|
||
│ │ └──────────────────────────────────────────────────────┘ │ │
|
||
│ │ Progress (indeterminate, h-1.5, --primary) │ │
|
||
│ │ hint: text-xs text-muted-foreground │ │
|
||
│ │ 通常需要 1–10 分鐘 · 你可以離開此頁面,回來時會自動更新進度 │ │
|
||
│ │ │ │
|
||
│ └──────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||
│ │ ℹ 你可以放著不管 │ │
|
||
│ │ text-sm text-muted-foreground │ │
|
||
│ │ • 我們會在背景持續查詢進度(每 5–10 秒一次) │ │
|
||
│ │ • 完成後分頁標題會通知你 │ │
|
||
│ │ • 此頁面關掉也沒關係,回來時會自動恢復 │ │
|
||
│ └──────────────────────────────────────────────────────────────┘ │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 6.1 Stage indicator 規格
|
||
|
||
三段式 stepper(**不是** ONNX → BIE → NEF 那種編譯內部階段,因為 converter Phase 1 不暴露細階段;簡化成使用者能感知的三段):
|
||
|
||
| Stage | 完成條件 | converter 狀態對應 |
|
||
|-------|---------|-------------------|
|
||
| 1. 上傳完成 | XHR 200 回到 visionA backend、拿到 job_id | (前端自己標完成)|
|
||
| 2. 解析模型 | converter status 變 `running` | poll status `running` 即標完成 stage 1 + 開始 stage 2 |
|
||
| 3. 編譯 NEF | converter status 變 `succeeded` | poll status `succeeded` 標 stage 3 完成 |
|
||
|
||
> **注意**:converter Phase 1 只回 `queued / running / succeeded / failed`,沒有 sub-stage。前端的「解析模型 / 編譯 NEF」純粹是 UI 上把 `running` 切成兩段視覺,不代表真實內部階段。如果 converter 後續加 progress 比例(已在 N4 後續規劃),可改成單一 progress bar 顯示百分比。
|
||
|
||
stage 視覺:
|
||
|
||
```
|
||
完成:
|
||
┌──┐ bg-primary text-primary-foreground rounded-full h-8 w-8
|
||
│✓ │ flex items-center justify-center
|
||
└──┘
|
||
文字:text-sm font-medium
|
||
|
||
當前:
|
||
┌──┐ ring-2 ring-primary bg-background rounded-full h-8 w-8
|
||
│● │ text-primary
|
||
└──┘
|
||
文字:text-sm font-medium
|
||
|
||
未完成:
|
||
┌──┐ bg-muted text-muted-foreground rounded-full h-8 w-8
|
||
│ 3│
|
||
└──┘
|
||
文字:text-sm text-muted-foreground
|
||
|
||
連線:h-0.5(已完成段 bg-primary、未完成段 bg-muted)
|
||
```
|
||
|
||
### 6.2 進度條策略
|
||
|
||
- Phase 0.8 converter 不給 progress 比例 → 用 **indeterminate progress**(shadcn `Progress` 不帶 `value` 屬性 + `animate-pulse`)
|
||
- 文字:`處理中…`(不要謊報百分比)
|
||
- 不要顯示「預估剩餘時間」,因為沒有可靠資料
|
||
|
||
> **Phase 1 待 converter 提供 progress 後升級**:改成 `<Progress value={pct} />`,文字改成 `處理中({pct}%)`。
|
||
|
||
### 6.3 Polling 行為
|
||
|
||
| 項目 | 規格 |
|
||
|------|------|
|
||
| 間隔 | 每 5 秒一次(前 60 秒)→ 每 10 秒(之後)|
|
||
| 端點 | `GET /api/conversion/{job_id}`(visionA backend 中繼)|
|
||
| 暫停 | 分頁不可見(`document.visibilityState !== 'visible'`)時暫停;回到可見立即補打一次 |
|
||
| 失敗重試 | 指數退避 1s / 2s / 4s / 8s / 上限 30s;連 5 次失敗顯示「無法取得轉檔狀態,請重試」+ retry 按鈕 |
|
||
| 終止 | 收到 `succeeded` / `failed` 或使用者離開頁面 |
|
||
|
||
### 6.4 邊界情境
|
||
|
||
| 情境 | UI 反應 |
|
||
|------|---------|
|
||
| 使用者關掉分頁、過 10 分鐘回來 | 重進 `/conversion` → §3.3 active job 檢查命中 → 直接落 `processing` 畫面 |
|
||
| 使用者開了第二個分頁 | 兩個分頁各自 polling 同一個 job,狀態同步(無需跨分頁通訊)|
|
||
| Polling 一直拿到 `queued` 超過 5 分鐘 | banner 提示「目前排隊較久,你可以離開此頁稍後再回」|
|
||
| 跑超過 15 分鐘還沒完 | 不主動終止;banner 加註「轉檔耗時較長,仍在進行中」|
|
||
|
||
---
|
||
|
||
## 7. State:`completed.success` — 轉檔成功
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ [Sidebar] [Header] │
|
||
│ ────────────────────────────────────────────────────────────────── │
|
||
│ mx-auto max-w-7xl px-6 py-8 space-y-6 │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||
│ │ 轉檔 │ │
|
||
│ └──────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||
│ │ Card: 完成 (border-green-300 bg-green-50/40 │ │
|
||
│ │ dark:bg-green-950/20 dark:border-green-800) │ │
|
||
│ │ │ │
|
||
│ │ ✓ 轉檔完成 │ │
|
||
│ │ CheckCircle2, h-6 w-6, text-green-600 │ │
|
||
│ │ text-lg font-semibold │ │
|
||
│ │ │ │
|
||
│ │ ┌──────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ yolov5s.onnx → yolov5s_kl720.nef │ │ │
|
||
│ │ │ text-sm font-mono │ │ │
|
||
│ │ │ │ │ │
|
||
│ │ │ 目標晶片:KL720 輸出大小:4.2 MB │ │ │
|
||
│ │ │ 耗時:3 分 14 秒 checksum:sha256:a1b2… │ │ │
|
||
│ │ │ text-sm text-muted-foreground │ │ │
|
||
│ │ │ 任務 #{shortJobId} │ │ │
|
||
│ │ └──────────────────────────────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ 接下來要做什麼? │ │
|
||
│ │ text-sm font-medium │ │
|
||
│ │ │ │
|
||
│ │ ┌────────────────────────┐ ┌────────────────────────┐ │ │
|
||
│ │ │ 📚 加到模型庫 │ │ ⬇ 下載 .nef │ │ │
|
||
│ │ │ 之後可以從模型庫部署 │ │ 存到本機自行使用 │ │ │
|
||
│ │ │ 到任何 KL720 裝置 │ │ │ │ │
|
||
│ │ │ │ │ │ │ │
|
||
│ │ │ [加到模型庫] │ │ [下載] │ │ │
|
||
│ │ │ variant=default │ │ variant=outline │ │ │
|
||
│ │ └────────────────────────┘ └────────────────────────┘ │ │
|
||
│ │ Card 內兩格 grid (md: grid-cols-2 gap-4) │ │
|
||
│ │ │ │
|
||
│ │ ⏳ 此轉檔結果將在 6 天 21 小時後自動清除,請在期限內完成處理 │ │
|
||
│ │ text-xs text-amber-700 dark:text-amber-300 │ │
|
||
│ │ │ │
|
||
│ └──────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||
│ │ [✨ 開始新轉檔] │ │
|
||
│ │ variant=outline w-full │ │
|
||
│ └──────────────────────────────────────────────────────────────┘ │
|
||
│ 完成 + 還沒按過任何按鈕也允許開新轉檔(converter 不再有 active job) │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 7.1 「加到模型庫」流程(按鈕)
|
||
|
||
```
|
||
1. 點擊 → 開 AlertDialog 確認 + 輸入欄位(複用 visionA 既有 import flow):
|
||
┌────────────────────────────────────────────────┐
|
||
│ 加到模型庫 │
|
||
│ │
|
||
│ Label: 模型名稱(預設帶 job 任務名) │
|
||
│ ┌──────────────────────────────────────────┐ │
|
||
│ │ yolov5s_kl720 │ │
|
||
│ └──────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Label: 描述(選填) │
|
||
│ ┌──────────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ └──────────────────────────────────────────┘ │
|
||
│ Textarea (rows=3) │
|
||
│ │
|
||
│ ▸ 來源:轉檔(job #{shortJobId})text-xs │
|
||
│ ▸ 目標晶片:KL720(自動帶入) │
|
||
│ │
|
||
│ [取消] [加到模型庫] │
|
||
└────────────────────────────────────────────────┘
|
||
2. 確認後呼叫 POST /api/conversion/{job_id}/promote-to-models
|
||
3. Loading:按鈕變 spinner + 「處理中…」(disabled)
|
||
4. 200 OK:
|
||
- toast「已加入模型庫」+ action「前往模型庫 →」連結到 /models/{model_id}
|
||
- 「加到模型庫」按鈕變綠勾 + 副標「✓ 已加入(前往查看 →)」(仍可點再加,但會 409)
|
||
5. 409 already imported:
|
||
- toast「此任務已加入過模型庫」+ 連結「查看現有模型 →」
|
||
6. 其他錯誤:toast 顯示錯誤訊息 + 「重試」
|
||
```
|
||
|
||
**為何不直接彈兩個按鈕沒有確認 dialog 也 OK?** 因為 PRD §F4 的決策是「半自動 = user 顯式選擇」,但「加到模型庫」會建立永久資源(出現在 `/models`),讓使用者**確認名稱**比靜默 import 更符合心智 — 跟 `ModelUploadDialog` 的做法一致。
|
||
|
||
> 👉 給 PM:上面的「模型名稱 / 描述」要不要做進 Phase 0.8 Dialog?如果你想最簡,可以直接用 job.name 自動填、不問使用者,省一個 Dialog。我這邊建議**保留**這個小 Dialog 但只給「名稱」一個欄位(描述放 Phase 1),理由:使用者「轉檔任務名」≠「模型庫名稱」的心智差異很常見。
|
||
|
||
### 7.2 「下載」流程(按鈕)
|
||
|
||
```
|
||
1. 點擊 → 按鈕短暫進 loading(spinner + 「準備下載…」)
|
||
2. 觸發 navigation:window.location.href = '/api/conversion/{job_id}/download'
|
||
(或用 anchor tag <a href="/api/conversion/{job_id}/download" download>,效果等價)
|
||
3. visionA backend 收到後 server-side 跟 MC 換 delegated token,
|
||
直接回 HTTP 302 Redirect → Location: <FAA-URL>/files/{key}?access_token=...
|
||
4. 瀏覽器自動跟著 302 跳轉到 FAA,內建下載管理器接管下載
|
||
- 按鈕回到原狀態(不變灰,使用者可重複下載 → 重新換新 token)
|
||
- toast「下載已開始」+ 副標「若沒看到下載提示,請檢查瀏覽器設定」
|
||
5. 4xx / 5xx(backend 還沒到 redirect 階段就 fail):
|
||
- 因為已 navigation,瀏覽器會顯示 backend 的錯誤頁;使用者按 back 即可
|
||
- 或前端可選用 fetch + 偵測 status 後才 navigate(避免 navigate 到錯誤頁)
|
||
```
|
||
|
||
**重點:token 不暴露給 frontend JS**,整個換 token 流程在 backend 內完成,前端只看得到 `/api/conversion/{job_id}/download` 這個 URL。
|
||
|
||
**Phase 0.8 短期方案(FAA 還沒加 CORS 前)**:靠 navigation download + 302 redirect,瀏覽器內建下載管理器接手,無自訂進度條。
|
||
**FAA 加 CORS 後(升級)**:可改用 `fetch('/api/conversion/{job_id}/download', { redirect: 'follow' })` + ReadableStream + 自訂進度條(Dialog 內顯示下載進度),仍維持 server-side 換 token 設計。本檔保留視覺位置,實作時再補。
|
||
|
||
### 7.3 「開始新轉檔」按鈕
|
||
|
||
直接重置 store state 回 `idle`,**不清除舊 job 紀錄**(仍可從 §3.3 active job 機制判斷,但因為剛剛已 completed,不會有 active;舊 job 結果如果使用者沒下載 / 沒 import,過 7 天自動 GC)。
|
||
|
||
⚠️ 邊界:使用者按「開始新轉檔」**之前**「加到模型庫」「下載」按鈕仍應**保持可用**(不在使用者按「開始新轉檔」時消失)— 因為使用者可能想兩個都做。「開始新轉檔」應該被視為**離開這個結果頁**的明確動作。
|
||
|
||
---
|
||
|
||
## 8. State:`completed.failed` — 轉檔失敗
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ [Sidebar] [Header] │
|
||
│ ────────────────────────────────────────────────────────────────── │
|
||
│ mx-auto max-w-7xl px-6 py-8 space-y-6 │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||
│ │ Card: 失敗 (border-destructive/40 bg-destructive/5 │ │
|
||
│ │ dark:bg-destructive/10) │ │
|
||
│ │ │ │
|
||
│ │ ⚠ 轉檔失敗 │ │
|
||
│ │ AlertCircle, h-6 w-6, text-destructive │ │
|
||
│ │ text-lg font-semibold │ │
|
||
│ │ │ │
|
||
│ │ {translatedErrorMessage} │ │
|
||
│ │ text-sm text-foreground │ │
|
||
│ │ 例:「模型內含不支援的運算子,無法量化到目標晶片」 │ │
|
||
│ │ │ │
|
||
│ │ ┌──────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ yolov5s.onnx → KL720(失敗) │ │ │
|
||
│ │ │ text-sm │ │ │
|
||
│ │ │ 錯誤代碼:QUANTIZATION_FAILED │ │ │
|
||
│ │ │ 任務 #{shortJobId} │ │ │
|
||
│ │ │ text-xs font-mono text-muted-foreground │ │ │
|
||
│ │ └──────────────────────────────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ 💡 你可以試試: │ │
|
||
│ │ text-sm font-medium │ │
|
||
│ │ • 確認模型已用標準 PyTorch / TensorFlow export │ │
|
||
│ │ • 簡化模型結構(移除 Custom Op) │ │
|
||
│ │ • 改用較小的 batch size 或 input shape │ │
|
||
│ │ text-sm text-muted-foreground │ │
|
||
│ │ (suggestions 依 error code 切換) │ │
|
||
│ │ │ │
|
||
│ │ ┌────────────────────────┐ ┌────────────────────────┐ │ │
|
||
│ │ │ [重新開始] │ │ [回模型庫] │ │ │
|
||
│ │ │ variant=default │ │ variant=outline │ │ │
|
||
│ │ └────────────────────────┘ └────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ 若持續發生,請複製任務 ID #{fullJobId} 聯絡支援團隊 │ │
|
||
│ │ [📋 複製任務 ID] │ │
|
||
│ │ text-xs text-muted-foreground · variant=ghost size=xs │ │
|
||
│ │ │ │
|
||
│ └──────────────────────────────────────────────────────────────┘ │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 8.1 錯誤訊息對照(依 PRD §F5)
|
||
|
||
| converter error code | 顯示文案(zh-TW) | suggestions 顯示 |
|
||
|---------------------|-----------------|-----------------|
|
||
| `UNSUPPORTED_FORMAT` | 此模型格式目前不支援,請改用 ONNX / TFLite | 確認檔案副檔名、用標準 export 工具 |
|
||
| `INVALID_CHECKSUM` | 檔案傳輸過程毀損,請重新上傳 | 重新開始上傳 |
|
||
| `QUANTIZATION_FAILED` | 模型內含不支援的運算子,無法量化到目標晶片 | 簡化模型、移除 Custom Op、改 input shape |
|
||
| `MODEL_TOO_LARGE` | 模型超過 500 MB 上限 | 改用較小模型 / Pruning |
|
||
| `QUOTA_EXCEEDED` | 系統暫時繁忙,請稍後再試 | 等 5 分鐘重試 |
|
||
| 其他 / unknown | 轉檔失敗,請稍後重試。若持續發生請聯絡支援團隊 | 複製 job ID 回報 |
|
||
|
||
i18n key:`conversion.error.{code}.message` / `conversion.error.{code}.suggestions`(見 §11)
|
||
|
||
### 8.2 「Job 已過期」特殊狀態
|
||
|
||
當使用者重新整理頁面、active job 檢查回 404(converter 7 天 GC 已清除):
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ Card: 已過期 (border-muted bg-muted/30) │
|
||
│ │
|
||
│ ⏰ 此轉檔結果已過期 │
|
||
│ Clock, h-6 w-6, text-muted-foreground │
|
||
│ │
|
||
│ 轉檔結果保留期為 7 天,目前已超過保留期限並自動清除。 │
|
||
│ │
|
||
│ [✨ 開始新轉檔] │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 響應式
|
||
|
||
沿用 design-spec.md §6 整體策略,逐狀態調整:
|
||
|
||
| 斷點 | `idle` | `uploading`(在 Dialog 內)| `processing` | `completed.success` |
|
||
|------|--------|--------------------------|--------------|--------------------|
|
||
| Mobile (< 640px) | EmptyState 縮窄 max-w-full、CTA 全寬;「關於轉檔」摺疊 | Dialog 改 fullscreen-on-mobile(shadcn 預設行為)| stage indicator 改縱向堆疊(icon 圓點 + 標籤一行);Card padding `p-4` | 兩個 action card 改縱向堆疊(grid-cols-1) |
|
||
| Tablet (640–1024) | 居中、max-w-2xl | Dialog 寬 max-w-lg | stage indicator 橫排但縮短連線 | grid-cols-2 |
|
||
| Desktop (≥ 1024) | 完整呈現 | 完整呈現 | 完整呈現 | grid-cols-2 |
|
||
|
||
`/conversion` 在 Mobile 不顯示「請使用桌面版」警告(不像 `/workspace` 那麼依賴大螢幕,可用)。但**500 MB 檔案在 Mobile 上傳**體驗極差,給一個 hint banner:
|
||
|
||
```
|
||
ℹ Mobile 設備上傳大型模型可能不穩定,建議使用桌面版瀏覽器
|
||
(只在 Mobile 顯示)
|
||
```
|
||
|
||
---
|
||
|
||
## 10. icon 替代方案(給設計討論)
|
||
|
||
任務指定 `Wand2` / `Workflow` / `Cpu` / `RefreshCw` 四選一,本 wireframe 採 **`Wand2`**。理由與替代方案:
|
||
|
||
| icon | 隱喻 | 採用? | 原因 |
|
||
|------|------|-------|------|
|
||
| **`Wand2`** ✨ | 「魔法轉換」一個東西變成另一個 | ✅ 採用 | 直覺、與既有 Boxes / Cable / LayoutDashboard 視覺密度相近 |
|
||
| `Workflow` | 流程 / 多步驟 | ❌ | 太流程感、容易跟 CI/CD 混淆 |
|
||
| `Cpu` | 晶片 / 硬體 | ❌ | 已被裝置語義佔據(`Devices` tab 概念上更該用這個) |
|
||
| `RefreshCw` | 同步 / 重整 | ❌ | 暗示週期性同步,不是一次性轉換 |
|
||
|
||
備案:如果使用者覺得 `Wand2` 太可愛不夠工程感,可改 `FileCog`(`Wand2` 的工程版)或 `Replace`(更精準的「A→B」隱喻)— 待 review 確認。
|
||
|
||
---
|
||
|
||
## 11. i18n key 規劃
|
||
|
||
### 11.1 Sidebar / 導航
|
||
|
||
```
|
||
nav.conversion → 轉檔 / Convert
|
||
```
|
||
|
||
### 11.2 頁面標題
|
||
|
||
```
|
||
conversion.title → 轉檔
|
||
conversion.subtitle → 把 ONNX / TFLite 模型轉成 .nef,跑在 Kneron 邊緣裝置上
|
||
```
|
||
|
||
### 11.3 idle 空狀態
|
||
|
||
```
|
||
conversion.idle.heading → 還沒有進行中的轉檔
|
||
conversion.idle.description → 上傳一個 ONNX / TFLite 模型,選擇目標 Kneron 晶片,我們幫你產出可直接燒錄的 .nef 檔案
|
||
conversion.idle.cta → 開始轉檔
|
||
conversion.idle.formats → 支援 .onnx / .tflite · 最大 500 MB
|
||
conversion.idle.about.title → 關於轉檔
|
||
conversion.idle.about.line1 → 一次只能跑一個轉檔任務(包含其他分頁)
|
||
conversion.idle.about.line2 → 完成後 7 天內可下載結果,過期自動清除
|
||
conversion.idle.about.line3 → 轉檔約耗時 1–10 分鐘,依模型大小而定
|
||
```
|
||
|
||
### 11.4 Upload Dialog
|
||
|
||
```
|
||
conversion.upload.title → 開始轉檔
|
||
conversion.upload.description → 上傳模型、選擇目標晶片,可選擇加上 reference images 提升精度
|
||
conversion.upload.source.label → 來源模型
|
||
conversion.upload.source.dropzone → 拖曳 .onnx / .tflite 到此處
|
||
conversion.upload.source.or → 或
|
||
conversion.upload.source.browse → 選擇檔案
|
||
conversion.upload.source.formatHint → 支援格式:.onnx · .tflite · 最大 500 MB
|
||
conversion.upload.source.remove → 移除
|
||
conversion.upload.name.label → 任務名稱(選填)
|
||
conversion.upload.name.hint → 顯示用,不影響輸出檔名
|
||
conversion.upload.chip.label → 目標晶片
|
||
conversion.upload.refImages.label → Reference images(選填)
|
||
conversion.upload.refImages.dropzone → 拖曳圖片到此處(選填)
|
||
conversion.upload.refImages.hint → 加上 ref images 可提升量化後精度(最多 100 張,每張 ≤ 10 MB)
|
||
conversion.upload.refImages.summary → 已選 {count} 張 ref images(共 {totalSize})
|
||
conversion.upload.refImages.removeAll → 移除全部
|
||
conversion.upload.cancel → 取消
|
||
conversion.upload.start → 開始轉檔
|
||
|
||
conversion.upload.error.noFile → 請選擇 .onnx 或 .tflite 檔案
|
||
conversion.upload.error.unsupported → 不支援的格式,請改用 ONNX 或 TFLite
|
||
conversion.upload.error.modelTooLarge → 模型超過 500 MB 上限,請改用較小的模型
|
||
conversion.upload.error.noChip → 請選擇目標晶片
|
||
conversion.upload.error.refTooLarge → {filename} 超過 10 MB,請移除或壓縮後再試
|
||
conversion.upload.error.refTooMany → Reference images 上限 100 張
|
||
```
|
||
|
||
### 11.5 Uploading 階段
|
||
|
||
```
|
||
conversion.uploading.title → 上傳中
|
||
conversion.uploading.heading → 正在上傳到 visionA…
|
||
conversion.uploading.progress → 已上傳 {loaded} / {total} · 預估剩餘 {eta}
|
||
conversion.uploading.almostDone → 即將完成…
|
||
conversion.uploading.warning → 請勿關閉此分頁,否則上傳會中斷
|
||
conversion.uploading.cancel → 取消上傳
|
||
conversion.uploading.cancelConfirm → 上傳尚未完成,確定取消?
|
||
conversion.uploading.tabTitle → visionA Cloud · 上傳中 ({pct}%)
|
||
conversion.uploading.toastCanceled → 已取消上傳
|
||
conversion.uploading.toastFailed → 上傳失敗:{reason}
|
||
conversion.uploading.toastStarted → 已開始轉檔(任務 #{shortJobId})
|
||
```
|
||
|
||
### 11.6 Processing 階段
|
||
|
||
```
|
||
conversion.processing.title → 轉檔
|
||
conversion.processing.cardHeading → 進行中
|
||
conversion.processing.statusBadge → 轉檔中
|
||
conversion.processing.startedAgo → 開始於 {time}
|
||
conversion.processing.stage1 → 上傳完成
|
||
conversion.processing.stage2 → 解析模型
|
||
conversion.processing.stage3 → 編譯 NEF
|
||
conversion.processing.processing → 處理中…
|
||
conversion.processing.hint → 通常需要 1–10 分鐘 · 你可以離開此頁面,回來時會自動更新進度
|
||
conversion.processing.background.title → 你可以放著不管
|
||
conversion.processing.background.l1 → 我們會在背景持續查詢進度(每 5–10 秒一次)
|
||
conversion.processing.background.l2 → 完成後分頁標題會通知你
|
||
conversion.processing.background.l3 → 此頁面關掉也沒關係,回來時會自動恢復
|
||
conversion.processing.queueLong → 目前排隊較久,你可以離開此頁稍後再回
|
||
conversion.processing.runLong → 轉檔耗時較長,仍在進行中
|
||
conversion.processing.pollFailed → 無法取得轉檔狀態,請重試
|
||
conversion.processing.bannerActive → 您離開前的轉檔仍在進行中
|
||
conversion.processing.bannerExisting → 您已有一個轉檔正在進行中,已切換至該任務
|
||
```
|
||
|
||
### 11.7 已有 active job 提示(idle 頁也會用到)
|
||
|
||
```
|
||
conversion.busy.title → 您已有一個轉檔正在進行中
|
||
conversion.busy.cta → 查看進度
|
||
```
|
||
|
||
### 11.8 Success 結果
|
||
|
||
```
|
||
conversion.success.heading → 轉檔完成
|
||
conversion.success.summary.chip → 目標晶片
|
||
conversion.success.summary.size → 輸出大小
|
||
conversion.success.summary.duration → 耗時
|
||
conversion.success.summary.checksum → checksum
|
||
conversion.success.summary.jobId → 任務
|
||
conversion.success.nextStep → 接下來要做什麼?
|
||
|
||
conversion.success.import.title → 加到模型庫
|
||
conversion.success.import.description → 之後可以從模型庫部署到任何 {chip} 裝置
|
||
conversion.success.import.cta → 加到模型庫
|
||
conversion.success.import.dialog.title → 加到模型庫
|
||
conversion.success.import.dialog.nameLabel → 模型名稱
|
||
conversion.success.import.dialog.descLabel → 描述(選填)
|
||
conversion.success.import.dialog.sourceLabel → 來源
|
||
conversion.success.import.dialog.sourceValue → 轉檔(job #{shortJobId})
|
||
conversion.success.import.dialog.confirm → 加到模型庫
|
||
conversion.success.import.dialog.cancel → 取消
|
||
conversion.success.import.processing → 處理中…
|
||
conversion.success.import.toastDone → 已加入模型庫
|
||
conversion.success.import.toastDoneAction → 前往模型庫 →
|
||
conversion.success.import.toastDup → 此任務已加入過模型庫
|
||
conversion.success.import.toastDupAction → 查看現有模型 →
|
||
conversion.success.import.statusDone → ✓ 已加入(前往查看 →)
|
||
|
||
conversion.success.download.title → 下載 .nef
|
||
conversion.success.download.description → 存到本機自行使用
|
||
conversion.success.download.cta → 下載
|
||
conversion.success.download.preparing → 準備下載…
|
||
conversion.success.download.toastStart → 下載已開始
|
||
conversion.success.download.toastHint → 若沒看到下載提示,請檢查瀏覽器設定
|
||
conversion.success.download.toastFail → 下載連結取得失敗
|
||
|
||
conversion.success.expiry → 此轉檔結果將在 {time} 後自動清除,請在期限內完成處理
|
||
conversion.success.startNew → 開始新轉檔
|
||
```
|
||
|
||
### 11.9 Failed 結果
|
||
|
||
```
|
||
conversion.failed.heading → 轉檔失敗
|
||
conversion.failed.errorCode → 錯誤代碼
|
||
conversion.failed.suggestionsTitle → 你可以試試:
|
||
conversion.failed.retry → 重新開始
|
||
conversion.failed.backToModels → 回模型庫
|
||
conversion.failed.contactSupport → 若持續發生,請複製任務 ID 聯絡支援團隊
|
||
conversion.failed.copyJobId → 複製任務 ID
|
||
conversion.failed.toastJobIdCopied → 已複製任務 ID
|
||
|
||
conversion.error.UNSUPPORTED_FORMAT.message → 此模型格式目前不支援,請改用 ONNX / TFLite
|
||
conversion.error.UNSUPPORTED_FORMAT.suggestions → ["確認檔案副檔名","用標準 export 工具"]
|
||
conversion.error.INVALID_CHECKSUM.message → 檔案傳輸過程毀損,請重新上傳
|
||
conversion.error.INVALID_CHECKSUM.suggestions → ["重新開始上傳"]
|
||
conversion.error.QUANTIZATION_FAILED.message → 模型內含不支援的運算子,無法量化到目標晶片
|
||
conversion.error.QUANTIZATION_FAILED.suggestions → ["簡化模型結構","移除 Custom Op","改用較小的 input shape"]
|
||
conversion.error.MODEL_TOO_LARGE.message → 模型超過 500 MB 上限
|
||
conversion.error.MODEL_TOO_LARGE.suggestions → ["改用較小模型","嘗試 Pruning / Quantization"]
|
||
conversion.error.QUOTA_EXCEEDED.message → 系統暫時繁忙,請稍後再試
|
||
conversion.error.QUOTA_EXCEEDED.suggestions → ["等 5 分鐘後重試"]
|
||
conversion.error.unknown.message → 轉檔失敗,請稍後重試。若持續發生請聯絡支援團隊
|
||
conversion.error.unknown.suggestions → ["複製任務 ID 回報給支援團隊"]
|
||
```
|
||
|
||
### 11.10 已過期 / 找不到
|
||
|
||
```
|
||
conversion.expired.heading → 此轉檔結果已過期
|
||
conversion.expired.description → 轉檔結果保留期為 7 天,目前已超過保留期限並自動清除。
|
||
conversion.expired.startNew → 開始新轉檔
|
||
```
|
||
|
||
---
|
||
|
||
## 12. 無障礙
|
||
|
||
| 區塊 | 規格 |
|
||
|------|------|
|
||
| Sidebar 「轉檔」項目 | `aria-current="page"`(沿用 sidebar 規格)|
|
||
| EmptyState | `role="status"`(無互動結構,純宣告)|
|
||
| Upload Dialog | shadcn `Dialog` 內建 focus trap、ESC 關閉 |
|
||
| Drop zone | `role="button"` + `tabIndex=0` + `aria-label="拖曳或選擇模型檔案"`;Enter / Space 觸發 file picker |
|
||
| Chip RadioGroup | `role="radiogroup"` + `aria-label="目標晶片"`;每個 chip `role="radio"` + `aria-checked` |
|
||
| 上傳進度 | `role="progressbar"` + `aria-valuenow={pct}`(uploading);`aria-busy="true"`(processing indeterminate)|
|
||
| Stage indicator | `<ol role="list">` + 每 stage `<li role="listitem">` + 當前 `aria-current="step"` + 完成 `aria-label="{name}(已完成)"` |
|
||
| Status banner / 錯誤 | `role="alert"` + `aria-live="assertive"`(completed.failed) |
|
||
| 成功 toast | sonner 已自帶 `role="status" aria-live="polite"` |
|
||
| Tab 標題更新 | `document.title` 變動時 SR 不會主動朗讀,但對視覺使用者足夠 |
|
||
| 觸控目標 | 兩個主要 action 按鈕高度 `h-10`(40px),符合 32px 桌面門檻 |
|
||
| Dark Mode | 所有色塊都用 token / Tailwind dark variant,無 hardcoded hex |
|
||
|
||
**色彩對比驗證(手動 sample)**:
|
||
- success card:`bg-green-50 text-foreground` → > 12:1(OK)
|
||
- failed card:`bg-destructive/5 text-foreground` → > 10:1(OK)
|
||
- amber expiry hint:`text-amber-700 on bg-card` → 5.7:1(≥ AA 4.5:1)
|
||
|
||
---
|
||
|
||
## 13. Phase 0.8 不做(明確列出)
|
||
|
||
對齊 PRD §5 Non-Goals,本 wireframe **不設計**以下元素:
|
||
|
||
- ❌ 轉檔歷史清單(`/conversion/history`)
|
||
- ❌ 取消正在跑的 job UI(`processing` 畫面**沒有** 取消按鈕,只有 hint「離開沒關係」)
|
||
- ❌ 多 chip 同時轉檔(chip RadioGroup 是單選,不是 Checkbox)
|
||
- ❌ SSE / WebSocket 進度推送(純 polling)
|
||
- ❌ 進階參數(FP16、自訂量化、batch size)
|
||
- ❌ 模型版本管理 / A/B 比較
|
||
- ❌ 配額計費 UI
|
||
|
||
---
|
||
|
||
## 14. 對 PM / Architect 的補充建議
|
||
|
||
> (也整理在交付回報中,這裡放完整版)
|
||
|
||
### 給 PM
|
||
|
||
1. **「加到模型庫」是否需要 Dialog 確認名稱?** 我的建議是**保留**單欄位 Dialog(只問模型名稱、預設帶 job name),描述放 Phase 1。如果你想最簡,可以直接靜默 import — 但這樣使用者沒辦法控制 `/models` 頁顯示的名稱。請決定。
|
||
2. **「開始新轉檔」的位置**:我把它放在 success 結果卡片**外面下方**,避免被誤點而離開結果頁;舊 job 的 import / download 按鈕仍在 result card 內可重複使用。建議 confirm。
|
||
3. **active job 檢查端點**:`/conversion` 載入時要打 `GET /api/conversion/active`(或類似),這個端點 visionA backend 是否已規劃?如果沒有,可以用「使用者按開始時才打」的方式 fallback,但體驗會差一點(使用者切回頁面要先按按鈕才知道有 job)。
|
||
4. **錯誤訊息對照表**:§8.1 的對照沿用 PRD §F5。如果 converter 未來新增 error code,需要同步更新這張表,建議在 backend 加 i18n fallback:未知 code 一律用 `conversion.error.unknown.*`。
|
||
|
||
### 給 Architect
|
||
|
||
1. **Active job 檢查 API**:建議加 `GET /api/conversion/active`,回 `{ active: bool, jobId?, status?, source_filename?, target_chip?, started_at? }`。沒這個 API 就要靠前端 store 或 localStorage 記 jobId,會讓「換瀏覽器 / 換分頁」這個情境壞掉。
|
||
2. **Polling 對 backend 的負擔**:visionA backend 中繼 polling 時(5–10 秒/次/user),記得 cache 個 2–3 秒避免 hammer converter。如果預期 10+ concurrent users,這層 cache 必要。
|
||
3. **Upload streaming proxy 的進度**:`XMLHttpRequest.upload.onprogress` 算的是 browser → visionA backend 的進度。如果 backend 是把 stream forward 到 converter,前端進度條 100% 不代表 converter 收到 100% — 之間有 backend buffering 延遲。如果這個延遲明顯,建議 backend 在 forward 完成後才 200,而不是 browser 上傳完就 200。請確認語意。
|
||
4. **Job 7 天 GC 提醒**:success card 的「6 天 21 小時後清除」需要 backend 從 `job.expires_at` 算。如果 converter 不直接給 expires_at,要 visionA backend 自行從 `created_at + 7d` 推算並回 frontend。
|
||
5. **延伸**:未來如果要支援「使用者主動取消轉檔」(Phase 1),UI 位置我會放在 `processing` 畫面右上角的 menu(kebab)— 但這個 wireframe 不畫,避免雛形階段引入複雜度。
|
||
|
||
### 給 Frontend
|
||
|
||
1. 「上傳中」分頁標題更新(`conversion.uploading.tabTitle`)需要 `document.title = ...` 動態改;上傳完成或頁面卸載要還原。建議寫個小 hook `usePageTitle(title)`。
|
||
2. Indeterminate progress bar:shadcn `Progress` 沒有 indeterminate 樣式,需要自己加 CSS animation(`animate-pulse` 或 stripe shimmer)。
|
||
3. `prefers-reduced-motion`:indeterminate progress 與 spinner 都要尊重;reduced-motion 下改用靜態 dot pattern 或純文字。
|
||
|
||
---
|
||
|
||
## 15. 對應的 Mermaid 流程圖
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
[*] --> idle
|
||
idle --> uploading : 點「開始轉檔」+ 選檔/選 chip + 送出
|
||
uploading --> idle : 取消 / 上傳失敗
|
||
uploading --> processing : XHR 200 + 拿到 job_id
|
||
processing --> success : poll 到 status=succeeded
|
||
processing --> failed : poll 到 status=failed
|
||
success --> idle : 點「開始新轉檔」
|
||
failed --> idle : 點「重新開始」
|
||
note right of processing
|
||
polling /api/conversion/{job_id}
|
||
每 5–10 秒一次
|
||
分頁不可見時暫停
|
||
end note
|
||
note right of idle
|
||
進入頁面先打 GET /jobs/active
|
||
若有 active 直接落 processing
|
||
end note
|
||
```
|