# 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=`(日誌 viewer) **若 Architect 未提供**以上 API/事件,本節狀態無法落地,需在 M2 前與 Architect 對齊。