# v2.6 — 啟動進度面板(Startup Progress Panel) > 本章對應 **R5-E1 ~ R5-E6**(perceived performance 階段化啟動)。 > 上層索引:`../design-spec-v2.md` > 相關:`v2/control-panel.md §5`(狀態機 Starting state)、`v2/control-panel.md §7`(啟動流程) > 版本:**v2.1**(新增章節)· 建立日期:2026-04-14 --- ## 1. 定位與動機 **問題**:v2 第一版把 AC-1.3 定為「10 秒上限」硬指標,但 Architect §11-2 分析估計樂觀 ~4 秒 / 悲觀 ~8 秒 / Windows + Defender 首次掃描最壞 ~11 秒。強求 10 秒會卡 Windows 使用者。 **使用者決策(R5-E)**:把問題從「要多快」翻轉成「**讓使用者感覺進度有在推動**」,採 Nielsen Norman *perceived performance* 原則 — > 使用者能忍受 60 秒,只要每一秒都有視覺反饋。使用者不能忍受 10 秒,如果其中 8 秒是白畫面。 **本章職責**:設計 Starting state 時浮在 log panel 上方的**階段化啟動進度面板**,讓使用者知道: - 現在在做什麼(階段編號 + 名稱 + 描述) - 進度到哪裡(6 階段的哪一階,視覺進度條) - 快 OK 了 vs 卡住了(超時提示) - 出錯了怎麼辦(Error state 三個救援按鈕) --- ## 2. Wireframe ### 2.1 正常啟動中(階段 3「啟動本機伺服器」進行中) ``` ├───────────────────────────────────────────────────────────────────┤ │ ☑ Follow tail ☑ Show timestamps 🔍 [ Filter ... ] │ ├───────────────────────────────────────────────────────────────────┤ │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ 正在啟動 visionA-local · Starting visionA-local │ │ │ │ │ │ │ │ ✅ 1 · 初始化控制台 完成 │ │ │ │ Initializing control panel │ │ │ │ │ │ │ │ ✅ 2 · 檢查 Python 執行環境 完成 │ │ │ │ Checking Python runtime │ │ │ │ │ │ │ │ 🔄 3 · 啟動本機伺服器 (spinner) │ │ │ │ Starting local server... │ │ │ │ │ │ │ │ ⏳ 4 · 偵測 Kneron 裝置 等待中 │ │ │ │ Detecting Kneron devices │ │ │ │ │ │ │ │ ⏳ 5 · 開啟瀏覽器 等待中 │ │ │ │ Opening browser │ │ │ │ │ │ │ │ ⏳ 6 · 等待 Web UI 連線 等待中 │ │ │ │ Waiting for Web UI to connect │ │ │ │ │ │ │ │ ▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ 進度 3 / 6 │ │ │ └───────────────────────────────────────────────────────────────┘ │ ├───────────────────────────────────────────────────────────────────┤ │ 10:23:40 INFO HTTP server binding on 127.0.0.1:3721 │ ← log panel │ 10:23:41 INFO wails ipc ready │ 照常顯示 │ ... │ ├───────────────────────────────────────────────────────────────────┤ ``` ### 2.2 階段卡超過 20 秒(Retry Hint,R5-E3) ``` │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ 正在啟動 visionA-local · Starting visionA-local │ │ │ │ │ │ │ │ ✅ 1 · 初始化控制台 完成 │ │ │ │ ✅ 2 · 檢查 Python 執行環境 完成 │ │ │ │ │ │ │ │ 🔄 3 · 啟動本機伺服器 (spinner) │ │ │ │ Starting local server... │ │ │ │ ⚠ 這個步驟花的時間比預期久,正在重試... │ │ │ │ This step is taking longer than expected, retrying... │ │ │ │ │ │ │ │ ⏳ 4 · 偵測 Kneron 裝置 等待中 │ │ │ │ ... │ │ │ │ │ │ │ │ ▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱ 進度 3 / 6 · 已等待 22 秒 │ │ │ └───────────────────────────────────────────────────────────────┘ │ ``` ### 2.3 Error 狀態(R5-E4:60 秒總上限或任一階段失敗) ``` │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ ❌ 啟動失敗 · Startup failed │ │ │ │ │ │ │ │ 啟動時間超過 60 秒,可能是系統環境異常或網路中斷。 │ │ │ │ Startup exceeded 60 seconds. Your environment may have │ │ │ │ issues or the network is interrupted. │ │ │ │ │ │ │ │ 失敗階段:3 · 啟動本機伺服器 │ │ │ │ Failed stage: 3 · Starting local server │ │ │ │ │ │ │ │ [ 🔄 重試 Retry ] [ 📋 檢視 log View Log ] │ │ │ │ [ 🐞 回報問題 Report Issue (hold) ] │ │ │ └───────────────────────────────────────────────────────────────┘ │ ``` --- ## 3. 元件規格 ### 3.1 Panel 容器 | 屬性 | 值 | |------|----| | Root element | `
` | | 位置 | log controls 下方、log panel 上方(和 Error banner 同一個 slot) | | 背景 | `color.surface-1` | | 邊框 | 1 px `color.border` | | 圓角 | `radius.md`(沿用 tokens) | | Padding | 16 px | | Max width | 控制台 content 寬度(約 688 px) | | 進入動畫 | fade-in 200 ms(`prefers-reduced-motion` 時跳變) | | 收尾動畫 | 所有階段完成後 fade-out 200 ms,之後 unmount | ### 3.2 Panel header | 元素 | 類型 | 樣式 | |------|------|------| | 標題 | `

` | 14 px SemiBold,`color.foreground` | | 文字(zh-TW) | `正在啟動 visionA-local` | | | 文字(en) | `Starting visionA-local` | | **i18n key**:`startup.panel.title.zh` / `startup.panel.title.en` ### 3.3 階段列 `` 每個階段是一個獨立列,包含: | 子元素 | 類型 | 內容 | 狀態變化 | |------|------|------|---------| | Icon(圓圈) | 20×20 px `` | ✅(完成)/ 🔄(進行中,旋轉 spinner)/ ⏳(等待)/ ❌(失敗) | 見 §3.4 狀態對照 | | 階段編號 | `` | `{n}` `·` | 14 px Medium,muted-foreground | | Label(雙語併呈) | `
` | 第一行:zh-TW 14 px;第二行:en 12 px muted-foreground | 見 §4 文案 | | 狀態標籤 | `` | 完成 / 進行中 / 等待中 / 失敗 | 右對齊 | **範例結構**: ```html
3 ·
啟動本機伺服器
Starting local server
進行中
``` ### 3.4 階段狀態對照 | 資料狀態 | Icon | Icon 顏色 | Label 顏色 | 狀態文字(zh / en) | |---------|------|----------|-----------|------------------| | `pending` | ⏳ (outline circle) | `color.muted-foreground` | `color.muted-foreground` | `等待中` / `Waiting` | | `running` | 🔄 旋轉 spinner 16 px | `color.primary` | `color.foreground` Bold | `進行中` / `Running` | | `running-slow` | 🔄 旋轉 spinner + ⚠ 小圖示 | `color.warning` | `color.foreground` Bold | `正在重試...` / `Retrying...` | | `done` | ✅ filled check | `color.success` | `color.muted-foreground`(略淡) | `完成` / `Done` | | `failed` | ❌ filled cross | `color.destructive` | `color.destructive` Bold | `失敗` / `Failed` | | `skipped` | ⏭ filled skip | `color.muted-foreground` | `color.muted-foreground` 斜體 | `跳過(依偏好設定)` / `Skipped (per preference)` | **動畫**: - pending → running:spinner fade-in 150 ms - running → done:spinner → check icon 交叉淡入 200 ms;整行 label 漸變淡 - running → running-slow:⚠ icon slide-in-left 200 ms - running → failed:spinner → ❌ icon,整行背景 `color.destructive/5` 淡入 - `prefers-reduced-motion: reduce` → 全部 fade 降為 0 ms 跳變,spinner 改為靜態點 ### 3.5 進度條 | 屬性 | 值 | |------|----| | 類型 | 6 格離散進度條(不是連續 bar),每格對應一階段 | | 已完成格 | `color.success` 填滿 | | 進行中格 | `color.primary` 填滿 + 脈衝動畫(opacity 0.6 ↔ 1.0, 1.5 s) | | 等待中格 | `color.border` 空格 | | 失敗格 | `color.destructive` 填滿 | | 高度 | 6 px | | 格間距 | 2 px | | 附加文字 | 右側 `進度 {current} / 6` + 卡超 20 秒時附 `· 已等待 {elapsed} 秒` | ARIA:`
` ### 3.6 Retry Hint(R5-E3) 當任一階段 `running` 狀態超過 **20 秒**未變 `done`,該 StageItem 下方浮出 hint line: | 屬性 | 值 | |------|----| | 觸發 | `stage.state === 'running' && (now - stage.startedAt) > 20_000ms` | | 隱藏 | 階段狀態變 `done` 或 `failed` 後隨 StageItem 一起消失 | | 顏色 | `color.warning` text | | Icon | ⚠ 14×14 | | 雙語 | 第一行中文、第二行英文(12 px muted) | **文案**: - zh:`這個步驟花的時間比預期久,正在重試...` - en:`This step is taking longer than expected, retrying...` **i18n key**:`startup.timeout.message.zh` / `startup.timeout.message.en` ### 3.7 Error State(R5-E4) 任一階段 `failed` 或總計超過 **60 秒** → Panel 整體換為 Error mode: - StageItem 列表隱藏(只保留失敗的那一階段顯示為 ❌) - 進度條換成 Error 樣式(整條 `color.destructive/20` 背景) - 大標題 `啟動失敗 / Startup failed` - 說明文字(雙語) - 三顆按鈕: | 按鈕 | 類型 | 行為 | |------|------|------| | 🔄 重試 / Retry | Button `primary` `md` | 重置進度面板,重新跑階段 1 | | 📋 檢視 log / View Log | Button `ghost` `md` | 收起 panel,focus 到 log panel 最後一條 ERROR 行,flash 2 次 | | 🐞 回報問題 / Report Issue **【hold】** | Button `ghost` `md` | **現階段不實作**(待 PM 提供 GitHub Issue repo URL) | **R5-D1 OS 原生通知並存**:進入 Error state 時同時呼叫 `sendCrashNotification()` 發 OS non-blocking toast(和 `control-panel.md §6.2` 的 Error banner 一致)。 --- ## 4. 6 階段文字定版(R5-E5 定稿) | # | 階段 | Label (zh-TW) | Label (en) | Description (zh-TW) | Description (en) | 完成條件(技術訊號) | |---|------|-------------|------------|--------------------|------------------|------------------| | 1 | 初始化控制台 | 初始化控制台 | Initializing control panel | 準備 visionA-local 桌面環境 | Preparing visionA-local desktop | Wails `OnStartup` 完成、i18n 載入、面板 mount | | 2 | 檢查 Python 執行環境 | 檢查 Python 執行環境 | Checking Python runtime | 首次啟動可能需要較長時間 | First launch may take longer | `ensurePythonRuntime()` 回傳 + 驅動檢查通過 | | 3 | 啟動本機伺服器 | 啟動本機伺服器 | Starting local server | 在 127.0.0.1:3721 啟動服務 | Starting service on 127.0.0.1:3721 | `/api/health` 回 200(首次成功) | | 4 | 偵測 Kneron 裝置 | 偵測 Kneron 裝置 | Detecting Kneron devices | 掃描已連接的硬體 | Scanning connected hardware | Go server 回傳 devices scan 結果(不論有無裝置都算成功) | | 5 | 開啟瀏覽器 | 開啟瀏覽器 | Opening browser | 在預設瀏覽器開啟 Web UI | Opening the Web UI in your default browser | `OpenInBrowser()` 呼叫完成(不等瀏覽器實際開好) | | 6 | 等待 Web UI 連線 | 等待 Web UI 連線 | Waiting for Web UI to connect | 正在與瀏覽器建立即時連線 | Establishing realtime connection with the browser | **WebSocket hub 收到第一個 client 連線**(R5-E6) | ### 4.1 特殊文案 **階段 2 首次啟動提示(常態)**: - Description 固定顯示「首次啟動可能需要較長時間 / First launch may take longer」 - 這不是超時 hint,是預設的 description,讓使用者看到就不焦慮(即使只花 0.5 秒也顯示) **階段 5 Linux / Settings OFF 情境**: - 狀態直接從 `pending` 跳到 `skipped` - 狀態文字顯示「跳過(依偏好設定)/ Skipped (per preference)」 - Icon 用 ⏭ - 進度條該格仍算推進(不當失敗,不擋住階段 6) **階段 6 Settings OFF 情境**: - 狀態從 `pending` 跳到 `running`,label description 改為 - zh:`請點擊控制台的「在瀏覽器開啟」按鈕` - en:`Please click "Open in Browser" in the Control Panel` - 當使用者手動點 Open in Browser 並成功建立 WebSocket 連線後 → `done` - **不套 20 秒 retry hint**(因為是等待人為動作,不是系統卡住) --- ## 5. 成功狀態收尾(Running state 轉場) 當階段 6 狀態變 `done`: 1. 進度條最後一格填滿 `color.success`,脈衝動畫停止 2. 停留 500 ms 讓使用者看到「全綠」 3. 整個 Panel fade-out 200 ms 4. Panel unmount 5. 控制台 Status 區域變 `Running · Browser opened`(Toggle ON 首次)或純 `Running` 6. Primary controls 啟用(Open in Browser 等) **總轉場時間**:約 700 ms(500 ms 停留 + 200 ms fade)。 **`prefers-reduced-motion: reduce`**:省略 500 ms 停留 + 200 ms fade,直接 unmount。 --- ## 6. 無障礙考量 | 項目 | 設計 | |------|------| | **Role** | Panel root `
` | | **Live region** | Panel 下加 `
` 宣告階段變化:`階段 {n}:{label},{status}`(zh)或 `Stage {n}: {label}, {status}`(en) | | **Focus trap** | Panel 顯示期間**不 trap focus**(Starting state 使用者不應該需要操作 panel 以外的元素,但允許使用者切換視窗或捲 log panel) | | **Keyboard** | `⌘0` / `Ctrl+0` focus 進度面板第一個可聚焦元素(Retry 按鈕或 panel root)
`Esc`:若 panel 已進入 Error state → 不動作(避免誤關);若進度已跑完正在 fade-out → 立即 unmount | | **色彩對比** | Stage label / Description / 狀態文字 ≥ 4.5:1;failed / running-slow hint ≥ 4.5:1(critical 信號不妥協,對齊 control-panel §10) | | **Reduced motion** | 所有 fade / spinner / 脈衝動畫降為 0 ms 跳變;spinner 改為靜態點;Retry hint 直接顯示不滑入 | | **字級可縮放** | 使用 `rem` 定義字級 | | **Icon 替代文字** | 每個 icon 有 `aria-hidden="true"`(狀態透過 live region 宣告),或 `role="img" aria-label="..."` | --- ## 7. i18n key 清單 全部加到 `desktop-control` namespace 的 `startup.*` 子樹: | Key | zh-TW | en | |-----|-------|----| | `startup.panel.title` | 正在啟動 visionA-local | Starting visionA-local | | `startup.panel.ariaLabel` | 啟動進度:階段 {current} / {max} | Startup progress: stage {current} / {max} | | `startup.progressLabel` | 進度 {current} / {max} | Progress {current} / {max} | | `startup.progressWithElapsed` | 進度 {current} / {max} · 已等待 {elapsed} 秒 | Progress {current} / {max} · {elapsed}s elapsed | | `startup.stage.1.label` | 初始化控制台 | Initializing control panel | | `startup.stage.1.description` | 準備 visionA-local 桌面環境 | Preparing visionA-local desktop | | `startup.stage.2.label` | 檢查 Python 執行環境 | Checking Python runtime | | `startup.stage.2.description` | 首次啟動可能需要較長時間 | First launch may take longer | | `startup.stage.3.label` | 啟動本機伺服器 | Starting local server | | `startup.stage.3.description` | 在 127.0.0.1:{port} 啟動服務 | Starting service on 127.0.0.1:{port} | | `startup.stage.4.label` | 偵測 Kneron 裝置 | Detecting Kneron devices | | `startup.stage.4.description` | 掃描已連接的硬體 | Scanning connected hardware | | `startup.stage.5.label` | 開啟瀏覽器 | Opening browser | | `startup.stage.5.description` | 在預設瀏覽器開啟 Web UI | Opening the Web UI in your default browser | | `startup.stage.5.skipped.label` | 跳過(依偏好設定) | Skipped (per preference) | | `startup.stage.6.label` | 等待 Web UI 連線 | Waiting for Web UI to connect | | `startup.stage.6.description` | 正在與瀏覽器建立即時連線 | Establishing realtime connection with the browser | | `startup.stage.6.manualHint` | 請點擊控制台的「在瀏覽器開啟」按鈕 | Please click "Open in Browser" in the Control Panel | | `startup.status.pending` | 等待中 | Waiting | | `startup.status.running` | 進行中 | Running | | `startup.status.done` | 完成 | Done | | `startup.status.failed` | 失敗 | Failed | | `startup.status.skipped` | 跳過(依偏好設定) | Skipped (per preference) | | `startup.timeout.message` | 這個步驟花的時間比預期久,正在重試... | This step is taking longer than expected, retrying... | | `startup.error.title` | 啟動失敗 | Startup failed | | `startup.error.description.timeout` | 啟動時間超過 60 秒,可能是系統環境異常或網路中斷。 | Startup exceeded 60 seconds. Your environment may have issues or the network is interrupted. | | `startup.error.description.stageFailed` | 階段「{stageLabel}」執行失敗。 | Stage "{stageLabel}" failed. | | `startup.error.failedStage` | 失敗階段:{n} · {label} | Failed stage: {n} · {label} | | `startup.error.retry` | 重試 | Retry | | `startup.error.viewLog` | 檢視 log | View Log | | `startup.error.report` | 回報問題 | Report Issue | | `startup.liveRegion.stageUpdate` | 階段 {n}:{label},{status} | Stage {n}: {label}, {status} | --- ## 8. 資料模型(交給 Architect / Frontend 參考) ```typescript type StageState = | 'pending' | 'running' | 'running-slow' // UI 派生狀態:running 且 elapsed > 20s | 'done' | 'failed' | 'skipped'; interface Stage { id: 1 | 2 | 3 | 4 | 5 | 6; state: StageState; startedAt: number | null; // epoch ms finishedAt: number | null; errorMessage?: string; } interface StartupProgressState { currentStage: 1 | 2 | 3 | 4 | 5 | 6 | 'done' | 'error'; stages: Record<1 | 2 | 3 | 4 | 5 | 6, Stage>; startedAt: number; // 整個啟動流程開始時間 totalElapsedMs: number; // 從 startedAt 算起 errorReason?: 'timeout' | 'stageFailed'; failedStageId?: number; } ``` **訊號來源(給 Architect 看)**: - 階段 1 完成:Wails `OnStartup` 回呼結束 + 面板 mount 事件 - 階段 2 完成:Go `ensurePythonRuntime()` 回傳 + driver check OK - 階段 3 完成:`/api/health` 首次回 200 - 階段 4 完成:Go server 裝置掃描回傳(有 / 無 / 錯誤都算) - 階段 5 完成:`OpenInBrowser()` 呼叫 return(或 `skipped` 若 toggle 關) - 階段 6 完成:WebSocket hub 收到第一個 client 連線事件(**R5-E6**) 每個階段的 `startedAt` 由前端 React 在「前一階段變 done 時」設定當下時間。`totalElapsedMs` 每秒更新用於超時判斷。 **60 秒總計時 timer**: - Panel mount 時 `setTimeout(fireError, 60_000)` - 每個階段 `done` 時不清 timer - 只要最後階段(6)`done` 前 timer 觸發 → 進 Error mode(reason: `timeout`) - 階段 6 `done` 時 `clearTimeout` **20 秒階段卡頓 timer**: - 每個階段變 `running` 時 `setTimeout(markSlow, 20_000)` - 階段變 `done` 或 `failed` 時 `clearTimeout` - 觸發時 `stage.state` 不真的改成 `running-slow`(仍是 `running`),而是 UI 層根據 `now - startedAt > 20_000` 派生出 `running-slow` 樣式和 hint line --- ## 9. 開發備忘(交給 Frontend / Architect) 1. **技術實作位置**:本面板跑在 Wails 控制台前端(vanilla HTML/JS/CSS,非 Next.js),路徑約 `visiona-local/frontend/` 下 2. **狀態同步**:Go 端每個階段完成時透過 `wailsRuntime.EventsEmit('startup:stage', {id, state})`;前端 listen 後 setState 並更新 panel 3. **Go 端需新增事件**: - `startup:stage` payload `{id: 1..6, state: 'running'|'done'|'failed'|'skipped', errorMessage?: string}` - 最終 `startup:complete` 或 `startup:error` 作為 panel 淡出訊號 4. **階段 6 WebSocket 訊號**:需要在 Go WebSocket hub 的 `OnConnect` 事件中,若是**這個 process 生命週期的第一次 client 連線**,emit `startup:stage {id: 6, state: 'done'}`;後續連線不再 emit 5. **階段 5 Linux 跳過時**:Go 端若讀到 `Preferences.OpenBrowserOnStart == false`,emit `startup:stage {id: 5, state: 'skipped'}` 6. **Architect 待補 TDD**:本面板需 TDD 對應章節(建議位於 `04-architecture/v2/startup-progress.md` 或併入 `control-panel.md`)落地事件協議 + Go 端 stage emitter 位置 --- ## 10. 與 v2 Starting state 的差異對照 | 面向 | v2(第一版) | v2.1 | |------|-------------|------| | Starting 視覺 | 只有 header 一顆 spinner + `Starting...` 文字 | 浮出 6 階段進度面板,每階段有 label + description + 狀態 icon | | 時間上限 | 5 秒超時進 Error(v1 寫死) | **60 秒總上限**(R5-E1),任一階段 20 秒進 Retry hint(R5-E3) | | 使用者可見性 | 零(只有「啟動中」三字) | 每階段明確的中英雙語 label + description | | Error state 入口 | 一律從 watchServer 失敗 | 新增「60 秒 timeout」與「階段失敗」兩條路徑,都走 Error mode | | 無障礙 | 基本 `aria-live` | 新增 `role="progressbar"` + `aria-valuenow` + live region 逐階段宣告 | --- ## 11. 懸而未決問題(交 Orchestrator) 1. **20 秒 retry hint 的「正在重試」字面**:目前文案寫「正在重試...」(retrying),但實際上 Go 端不一定真的在 retry,可能只是單純慢。是否改為較中性的「正在處理中,請稍候... / Still working, please wait...」?Design 建議維持「正在重試」—— 使用者對「retry」比「still working」更有信心感,即使技術上沒在 retry,心理預期是「系統知道有問題、在努力中」。待使用者最終確認。 2. **階段 6 WebSocket 連線訊號失敗的情境**:若 OS open browser 成功但使用者的預設瀏覽器被某些安全軟體攔截(Windows Defender SmartScreen 或家長控制),階段 6 永遠等不到 WebSocket → 60 秒後進 Error mode。此時 Error 說明是否應該特別提示「請檢查瀏覽器是否被安全軟體攔截」?Design 建議:不做特殊偵測,通用說明 + 看 log details 即可,否則要辨識各種安全軟體失敗 pattern,過度設計。 3. **階段卡頓超過 20 秒仍未完成,使用者按 Retry 的語意**:是「重置整個啟動流程」(從階段 1 開始)還是「重試當前階段」(從當前階段重跑)?Design 建議:**重置整個啟動流程**(簡單明確、使用者心智模型一致),Go 端只需要暴露一個 `RestartStartupSequence()` API。待 Architect 確認可行。 --- **下一步**:交 Architect 審閱技術落地(事件協議、WebSocket 訊號、Go 端 stage emitter 位置)、交 M8-5 Frontend Agent 實作 Wails 控制台啟動進度面板。