local-tool/: visionA-local desktop app
- M1: Wails shell + Go server + Next.js UI + Mock mode (macOS dmg ready)
- M2: i18n (zh-TW/en) + Settings 4-tab refactor
- M3: Embedded Python 3.12 runtime (python-build-standalone) + KneronPLUS wheels
- M4: Windows Inno Setup script (build on Windows runner)
- M5: Linux AppImage script + udev rule (build on Linux runner)
- M6: ffmpeg (GPL, pending legal review) + yt-dlp bundled
- Lifecycle: watchServer health check, fatal native dialog,
Wails IPC raise endpoint, stale process cleanup
.autoflow/: full PRD / Design Spec / Architecture / Testing docs
(4 rounds tri-party discussion + cross review)
.github/workflows/: macOS / Windows / Linux build CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
255 lines
13 KiB
Markdown
255 lines
13 KiB
Markdown
# 08 — 錯誤狀態與空狀態設計方向
|
||
|
||
## 8.1 錯誤狀態分級
|
||
|
||
| 級別 | 範例情境 | 視覺語彙 | UX 回應 |
|
||
|------|---------|---------|--------|
|
||
| **Critical** | Server 啟動失敗、崩潰 | 全螢幕錯誤頁 + 紅色 | 提供重啟、查看日誌、回報問題 |
|
||
| **Error** | 模型載入失敗、USB 連線中斷 | Toast (destructive) + 頁面 inline error | 提供重試、排錯提示 |
|
||
| **Warning** | Mock 模式運行中、未簽章警告 | Badge / Banner (warning) | 持續顯示,資訊性質 |
|
||
| **Info** | 操作成功、模型已切換 | Toast (success/info) | 3 秒自動消失 |
|
||
|
||
## 8.2 全域錯誤頁(Critical)
|
||
|
||
當 Server 無法啟動或崩潰,整個前端顯示:
|
||
|
||
```
|
||
┌──────────────────────────────────────────────┐
|
||
│ │
|
||
│ ⚠ │
|
||
│ │
|
||
│ Server 無法啟動 │
|
||
│ │
|
||
│ 錯誤原因:Port 3721 已被其他程式佔用 │
|
||
│ │
|
||
│ [重新啟動 Server] [更改 Port] │
|
||
│ [查看日誌] [回報問題] │
|
||
│ │
|
||
│ 技術資訊 ▾ (點擊展開 stacktrace) │
|
||
│ │
|
||
└──────────────────────────────────────────────┘
|
||
```
|
||
|
||
**規格**:
|
||
- 置中佈局,最大寬度 560px
|
||
- 主標題 `font.size.3xl / semibold`
|
||
- 錯誤原因用純文字(友善語氣,非 raw error)
|
||
- 技術資訊預設收合,展開後顯示 monospace 的 raw error log
|
||
- CTA 按鈕橫排,主要動作用 `primary`
|
||
|
||
**常見錯誤 + 對應文案**:
|
||
|
||
| 錯誤類型 | 友善描述 | 建議動作 |
|
||
|---------|---------|---------|
|
||
| Port 衝突 | Port {port} 已被其他程式佔用 | 更改 Port / 關閉佔用程式 |
|
||
| Python venv 建立失敗 | 無法建立 Python 環境,可能缺少系統套件 | 查看日誌 / 重新安裝 |
|
||
| KneronPLUS 載入失敗 | 無法載入 Kneron SDK | 檢查裝置 / 切到 Mock 模式 |
|
||
| 磁碟空間不足 | 磁碟空間不足,無法啟動 | 清理磁碟 |
|
||
| 未知錯誤 | Server 發生未預期錯誤 | 重啟 / 查看日誌 / 回報 |
|
||
|
||
## 8.3 Inline Error(Error level)
|
||
|
||
當頁面部分區塊載入失敗:
|
||
|
||
```
|
||
┌─ Models ─────────────────────────┐
|
||
│ │
|
||
│ ⚠ │
|
||
│ 無法載入模型清單 │
|
||
│ 可能是 server 暫時無法回應 │
|
||
│ │
|
||
│ [重試] │
|
||
│ │
|
||
└──────────────────────────────────┘
|
||
```
|
||
|
||
**規格**:置中於所在區塊,icon 48px,提供單一主要動作(重試)。
|
||
|
||
## 8.4 Toast 規格
|
||
|
||
```
|
||
位置:右上角(距邊 16px)
|
||
寬度:固定 360px(行動裝置尺寸下改全寬 - 32px)
|
||
高度:自適應(最低 56px)
|
||
堆疊:多個 toast 垂直堆疊,間隔 8px
|
||
最多同時顯示:4 個(超過的排隊)
|
||
```
|
||
|
||
**Variants**:
|
||
|
||
| Variant | 背景 | 前景 | icon | 預設停留 |
|
||
|---------|------|------|------|---------|
|
||
| `success` | `color.success` (8% opacity) | `color.success` | ✓ | 3s |
|
||
| `info` | `color.info` (8% opacity) | `color.info` | ⓘ | 3s |
|
||
| `warning` | `color.warning` (10% opacity) | `color.warning` | ⚠ | 5s |
|
||
| `destructive` | `color.destructive` (10% opacity) | `color.destructive` | ⊗ | 持續(需手動關) |
|
||
|
||
**結構**:
|
||
|
||
```
|
||
┌─────────────────────────────────────┐
|
||
│ [icon] 主要訊息 ✕ │
|
||
│ 次要說明(可選) │
|
||
│ [動作](可選) │
|
||
└─────────────────────────────────────┘
|
||
```
|
||
|
||
**動畫**:
|
||
- 進入:slide-in-right + fade-in,`motion.duration.normal` + `motion.easing.decelerate`
|
||
- 離開:fade-out,`motion.duration.fast`
|
||
|
||
## 8.5 空狀態(Empty States)
|
||
|
||
所有空狀態遵循統一結構:**Icon + 標題 + 次要描述 + 主要 CTA (+ 次要 CTA)**
|
||
|
||
### 通用樣板
|
||
|
||
```
|
||
┌──────────────────────────────────────┐
|
||
│ │
|
||
│ [icon 64px] │
|
||
│ │
|
||
│ [標題 — 說明沒有什麼] │
|
||
│ [次要描述 — 為什麼沒有、該做什麼] │
|
||
│ │
|
||
│ [主要 CTA] [次要 CTA] │
|
||
│ │
|
||
└──────────────────────────────────────┘
|
||
```
|
||
|
||
### 空狀態清單
|
||
|
||
| 頁面 | 情境 | Icon | 標題 | 描述 | 主 CTA | 次 CTA |
|
||
|------|------|------|------|------|-------|-------|
|
||
| Models | 無任何模型 | 📦 | 還沒有模型 | 上傳一個 .nef 檔案開始使用,或啟用預置模型 | 上傳模型 | 啟用預置模型 |
|
||
| Devices (Real) | 無裝置 | 🔌 | 沒有偵測到 Kneron 裝置 | 接上 USB 裝置後會自動顯示 | 重新掃描 | 切到 Mock 模式 |
|
||
| Devices (Mock) | 永遠有 3 顆假裝置 | — | — | — | — | — |
|
||
| Workspace | 無裝置 | ▶ | 還沒有可用的裝置 | 先到 Devices 接上裝置,或切到 Mock 模式 | 前往 Devices | 切到 Mock |
|
||
| Workspace > Batch | 無上傳圖片 | 🖼 | 還沒有圖片 | 拖放圖片檔到此處,或點選上傳 | 上傳圖片 | — |
|
||
| Workspace > Video | 無上傳影片 | 🎞 | 還沒有影片 | 拖放影片檔開始推論 | 上傳影片 | — |
|
||
| Dashboard > Activity | 無活動紀錄 | 🕒 | 還沒有任何活動 | 開始使用後,這裡會顯示最近的事件 | — | — |
|
||
| Settings > 模型 > 自訂路徑 | 無自訂路徑 | 📁 | 沒有自訂模型路徑 | 新增資料夾讓 App 自動載入其中的 .nef | 新增路徑 | — |
|
||
|
||
### 空狀態的「正向語氣」原則
|
||
|
||
- 不用「沒有」「無」開頭 — 用「還沒有」(暗示「之後會有」)
|
||
- 主 CTA 用動詞 —「上傳」「前往」「切換」
|
||
- 描述要告訴使用者**為什麼會空** +**怎麼讓它不空**
|
||
|
||
## 8.6 載入狀態
|
||
|
||
| 場景 | 方案 |
|
||
|------|------|
|
||
| 頁面初次載入 | Skeleton(沿用 shadcn skeleton 元件) |
|
||
| 清單刷新 | Top bar 細線 progress bar(2px,無確定進度) |
|
||
| 按鈕執行中 | Button 內 spinner + 文字改為「...中」 |
|
||
| 長時間操作(> 3 秒) | Modal + 進度條 + 可取消按鈕 |
|
||
| Mock 模式假進度 | 遵循真實時間,不要故意 fake 慢 |
|
||
|
||
## 8.7 破壞性操作確認
|
||
|
||
| 操作 | 確認強度 |
|
||
|------|---------|
|
||
| 切換 Mock/Real 模式 | Dialog 確認(說明會中斷正在進行的推論) |
|
||
| 刪除單一模型 | Dialog 確認(顯示模型名稱) |
|
||
| 清除所有快取 | Dialog 確認 |
|
||
| 重置所有設定 | 雙重確認(需輸入 `RESET` 字樣) |
|
||
| 結束 App(主視窗關閉) | **無確認**(Q7 決策:傳統式,直接結束) |
|
||
|
||
所有確認對話框使用 `destructive` 按鈕樣式在主動作上,次要動作(取消)用 `ghost`。
|
||
|
||
## 8.8 Python sidecar 專用狀態(第四輪新增)
|
||
|
||
**背景**:Architect `tray-and-lifecycle.md §4.3`(或等效章節)規劃 Python sidecar「空閒 N 分鐘自動 kill、崩潰時 Go server 自動重啟最多 3 次」。這些生命週期事件對使用者來說都是**「按下 Start 後 1-3 秒無回應」**的體驗斷裂,必須有明確的前端呈現。
|
||
|
||
### 8.8.1 自動重啟 loading(崩潰後)
|
||
|
||
**觸發**:Go server 偵測到 Python sidecar exit code ≠ 0,前端透過 WebSocket 事件 `sidecar.state` 收到 `restarting`。
|
||
|
||
**呈現**:Workspace 的 Camera Feed 區塊**覆蓋一層半透明 overlay**,顯示:
|
||
|
||
```
|
||
┌──── Camera Feed ────────────────┐
|
||
│ │
|
||
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||
│ ░ ░ │
|
||
│ ░ ⟳ ░ │
|
||
│ ░ 推論引擎正在重新啟動 ░ │
|
||
│ ░ (第 1/3 次) ░ │
|
||
│ ░ ░ │
|
||
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||
│ │
|
||
└─────────────────────────────────┘
|
||
```
|
||
|
||
- 背景:`color.background` + 60% opacity 黑色覆蓋
|
||
- Spinner:`motion.duration.normal`,不用 progress bar(不知道要多久)
|
||
- 文案:「推論引擎正在重新啟動(第 X/3 次)」
|
||
- 不阻擋使用者切換到其他頁面(overlay 只蓋在 Camera Feed,Sidebar/Header 仍可互動)
|
||
- 重啟成功 → overlay 淡出(300ms)→ 恢復影像串流
|
||
- **3 次重啟都失敗** → 進入 8.8.3 的 Critical 錯誤頁
|
||
|
||
### 8.8.2 空閒被 kill 後的冷啟動(使用者再次按 Start)
|
||
|
||
**觸發**:使用者閒置超過 Architect 設定的 N 分鐘,sidecar 被 Go server 主動 kill(節省記憶體)。使用者下次按 `[▶ Start]` 時,sidecar 需要 2-3 秒冷啟動。
|
||
|
||
**呈現**:Start 按鈕進入 loading 狀態 + Camera Feed 顯示 skeleton:
|
||
|
||
```
|
||
[▶ Start] → [⟳ 正在啟動推論引擎...] (按鈕 disabled,aria-busy=true)
|
||
|
||
Camera Feed 區域:
|
||
┌──── Camera Feed ────────────────┐
|
||
│ │
|
||
│ [Skeleton + 「首次啟動 │
|
||
│ 需要 2-3 秒,請稍候」] │
|
||
│ │
|
||
└─────────────────────────────────┘
|
||
```
|
||
|
||
- Start 按鈕文字即時改為「正在啟動推論引擎...」+ spinner,**不要只靠 disabled** 讓使用者以為當掉
|
||
- 預期時間上限 5 秒,超過 → 改顯示「啟動時間較長,請稍候」,10 秒 → 進入錯誤狀態
|
||
- 這是**使用者首次按 Start(冷啟動)**或**閒置重啟**時的標準體驗,文案相同
|
||
|
||
### 8.8.3 Sidecar 3 次重啟失敗(Critical)
|
||
|
||
**觸發**:Go server 連續重啟 sidecar 3 次都失敗,發送 `sidecar.state = failed`。
|
||
|
||
**呈現**:Workspace 整區切換為 Critical 錯誤頁(沿用 §8.2 通用全域錯誤頁樣板,但文案與動作為 sidecar-specific):
|
||
|
||
```
|
||
┌──────────────────────────────────────────────┐
|
||
│ │
|
||
│ ⚠ │
|
||
│ │
|
||
│ 推論引擎無法啟動 │
|
||
│ │
|
||
│ 已嘗試重新啟動 3 次,Python 推論引擎 │
|
||
│ 仍無法正常運作。 │
|
||
│ │
|
||
│ 可能的原因: │
|
||
│ • KneronPLUS SDK 與 OS 不相容 │
|
||
│ • 內嵌 Python 環境損毀 │
|
||
│ • 系統資源不足 │
|
||
│ │
|
||
│ [手動重試] [切換到系統 Python] │
|
||
│ [切到 Mock 模式] [查看 Python 日誌] │
|
||
│ │
|
||
│ 技術資訊 ▾ (點擊展開 stacktrace) │
|
||
│ │
|
||
└──────────────────────────────────────────────┘
|
||
```
|
||
|
||
- 「切換到系統 Python」連結到 Settings > 進階的 Python 執行模式(見 `03-wireframes §3.5`)
|
||
- 「查看 Python 日誌」開啟 `server-log-viewer` 並 filter 到 sidecar 類別
|
||
- OS 原生通知:**同時** shell out 原生通知(因為 Server 崩潰屬 R4-8 定義的嚴重事件),文案「visionA-local:推論引擎無法啟動」
|
||
|
||
### 8.8.4 與 Architect 的 API 依賴
|
||
|
||
本節所有呈現都依賴 Architect 提供:
|
||
- WebSocket 事件 `sidecar.state`,payload: `{ state: 'starting' | 'running' | 'idle' | 'restarting' | 'failed', attempt?: number }`
|
||
- `POST /api/sidecar/restart`(手動重試按鈕)
|
||
- `GET /api/sidecar/logs?since=<timestamp>`(日誌 viewer)
|
||
|
||
**若 Architect 未提供**以上 API/事件,本節狀態無法落地,需在 M2 前與 Architect 對齊。
|