# 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 後升級**:改成 ``,文字改成 `處理中({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 ,效果等價) 3. visionA backend 收到後 server-side 跟 MC 換 delegated token, 直接回 HTTP 302 Redirect → Location: /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 | `
    ` + 每 stage `
  1. ` + 當前 `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 ```