從 local-tool 複製出獨立的「visionA Agent」桌面應用(A3 純橋樑: tunnel client + 配對 UI + 設定,不開 HTTP port、不做本機裝置/推論 UI)。 Bundle ID 與 local-tool 不同(com.innovedus.visiona-agent vs visiona-local), 雙 app 可共存。fork 後不主動 sync,需要時手動 cherry-pick。 Backend / Wails Go(AB1-AB13): - internal/tunnel:6 狀態機(Idle/Connecting/Connected/Reconnecting/Failed/Stopped) + Pair/Unpair/Reconnect/Disconnect binding + ClientHooks event - internal/auth:encrypted file token store(AES-GCM + scrypt + machineID fallback salt + 13 tests) - internal/config:YAML validation + atomic write + 11 tests - internal/log:ring buffer + ExportLog 升級 zip - visionA-backend /api/pairing/exchange:SessionTokenStore + 17 new tests - 三平台 build 驗證(macOS DMG 160 MB / Windows EXE / Linux AppImage) - end-to-end 5 milestone 全綠(pairing → tunnel → forward → reuse 防護 → tunnel drop failover) Frontend / Next.js(AF1-AF7,沿用 visionA-frontend 基礎): - AppShell + Header + TabNav(StatusView / PairView / SettingsView 三 tab) - ConnectionStatusBadge 5 種狀態 - TokenInput regex 驗證 + 7 種錯誤 + 0.5s auto-switch 到狀態頁 - 設定頁 4 區塊(含重新配對 AlertDialog) - agent-api.ts 封裝 Wails bindings(mock/real 雙實作)+ 90 tests Phase 0.7 review-driven fix(Round 2): - A1 Session fixation 防護(RotateSessionID) - A3 mock pairing 預設改 false(必須明確 opt-in)+ startup log - A4 Pair 失敗後 state 清理矩陣(exchange/Save/Start fail 各自終態) - A5 Pair/Unpair/Reconnect lifecycleMu + 50 goroutine race test - F1 重新配對次按鈕 / F2 PairView Esc cancel / F3 Wails BrowserOpenURL / F4 Settings draft 持久 + 未儲存 badge 驗證:agent backend go test -race -count=3 ./... 4 packages 全綠 / agent frontend pnpm test 119 tests 全綠 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
311 lines
17 KiB
Markdown
311 lines
17 KiB
Markdown
# visionA Agent — Frontend
|
||
|
||
visionA Agent 是 visionA 雲端的 **local agent / tunnel bridge** — 一個桌面應用,把本機的 Kneron 邊緣裝置安全地連上 visionA 雲端。本目錄是它的前端實作:純靜態匯出(Next.js + React),由 Wails Go runtime 透過 `go:embed` 嵌入可執行檔,三平台(macOS / Windows / Linux)共用同一份程式碼。
|
||
|
||
> 定位:**「開了就忘掉它」的工具**。使用者配對一次後,Agent 只要維持連線、不出問題就好。UI 極簡,沒有裝置面板、推論視窗、行銷素材 — 只有連線狀態、配對、設定三頁。
|
||
|
||
## 技術堆疊
|
||
|
||
| 層級 | 技術 | 版本 |
|
||
|------|------|------|
|
||
| 框架 | Next.js(App Router,`output: "export"` static) | 16.1.x |
|
||
| 語言 | TypeScript(strict) | 5.x |
|
||
| UI | React | 19.2.x |
|
||
| 樣式 | Tailwind CSS | 4.x |
|
||
| 基礎元件 | Radix UI(`radix-ui` 單一套件,shadcn/ui 風格) | 1.4.x |
|
||
| 圖示 | lucide-react | 0.575.x |
|
||
| 主題 | next-themes(Light / Dark / System) | 0.4.x |
|
||
| Toast | sonner | 2.x |
|
||
| 動畫 | tw-animate-css | 1.4.x |
|
||
| 狀態管理 | zustand(依賴保留;雛形階段實際未用) | 5.x |
|
||
| 測試 | Vitest + @testing-library/react + jsdom | Vitest 4 / RTL 16 |
|
||
|
||
## 與其他前端專案的關係
|
||
|
||
| 專案 | 定位 | 是否為本專案的來源 |
|
||
|------|------|------------------|
|
||
| `visionA-frontend/`(雲端 Web) | 使用者的主要操作介面 — 裝置管理、模型管理、推論 | **是**。Design Tokens (`globals.css`)、Theme Provider、i18n 結構、`utils.ts`、Radix UI 基礎元件複製自此 |
|
||
| `local-tool/frontend/`(本機 GUI) | 開發者跑推論用的桌面工具 | **不是**。本專案**不是** local-tool 的 fork — 我們只借鑒其 Wails + Next.js `output:"export"` 的打包模式,UI 和邏輯完全獨立 |
|
||
| `local-agent/frontend/`(本目錄) | 連線橋樑 UI(狀態 / 配對 / 設定)| 獨立專案 |
|
||
|
||
**核心設計決策**:視覺上與 visionA-frontend 保持一致,讓同一位使用者在雲端 Web 與桌面 Agent 之間切換「能一眼認出是同家人」。
|
||
|
||
## 前置需求
|
||
|
||
- **Node.js** ≥ 20
|
||
- **pnpm** ≥ 9(專案使用 pnpm,**請勿** `npm install` / `yarn install`,會產生第二份 lockfile)
|
||
- (整合模式才需要)**Go** ≥ 1.22 與 Wails CLI v2 — 詳見 `../README.md`(local-agent 根目錄)
|
||
|
||
## 兩種開發模式
|
||
|
||
### 模式 A — 獨立前端開發(`pnpm dev`)
|
||
|
||
純前端迭代(調色、調文案、元件樣式、互動邏輯)時使用,**不需要** Wails binding。
|
||
|
||
```bash
|
||
pnpm install
|
||
pnpm dev
|
||
# → http://localhost:3000(支援 HMR)
|
||
```
|
||
|
||
此模式下:
|
||
- `src/hooks/use-*` 的 Wails binding 呼叫會自動走 **mock 實作**(見 `src/lib/agent-api.ts` 的 runtime 偵測)
|
||
- 連線狀態、配對結果、設定值皆為假資料;可切 `notPaired` / `online` / `reconnecting` / `error` 等變體驗證 UI
|
||
- 可直接切換 Light / Dark、zh-Hant / en 驗證 Design Tokens 與 i18n
|
||
|
||
### 模式 B — 與 Wails 整合(`wails dev`)
|
||
|
||
驗證與 Go backend(tunnel client / pairing / connstate)的真實互動時使用。
|
||
|
||
```bash
|
||
# 在 local-agent/ 根目錄
|
||
wails dev
|
||
# → Wails 會自動啟動 `pnpm dev` 並注入 Wails runtime;前端呼叫 window.go.main.App.* 走真實 binding
|
||
```
|
||
|
||
此模式下:
|
||
- `isWailsRuntime()` 會回 `true`,`agent-api.ts` 改走真實 Wails `window.go.main.App.*`
|
||
- 事件(`connection:status` / `connection:log` / `settings:updated` / `pairing:result`)由 Go broadcaster 推送
|
||
- Token 會真的寫入 OS keychain(macOS Keychain / Windows Credential Manager / Linux Secret Service)
|
||
|
||
## Build
|
||
|
||
```bash
|
||
pnpm build
|
||
```
|
||
|
||
產出到 `out/`(Next.js `output: "export"` 的靜態匯出結果)。Wails build 時透過 `wails.json` 的 `frontend:install` + `frontend:build` 呼叫此指令,然後透過 `assetdir: "./frontend/out"` 用 `//go:embed` 把 `out/` 整包嵌入最終可執行檔。
|
||
|
||
```bash
|
||
# 完整桌面應用 build(在 local-agent/ 根目錄)
|
||
wails build
|
||
```
|
||
|
||
### 為什麼是 `output: "export"`?
|
||
|
||
| 方案 | 是否可行 | 理由 |
|
||
|------|---------|------|
|
||
| Next.js server(`standalone`) | ❌ | Wails 不跑 Node server |
|
||
| Next.js SSR / SSG | ❌ | 同上,且 Wails 桌面環境無 HTTP server |
|
||
| **Next.js `output: "export"`** | ✅ | 純靜態,可 embed;`local-tool` 已長期驗證此模式穩定 |
|
||
| Vite + SPA | ❓ | 可行但放棄 — 會引入第二套 build pipeline,且 visionA-frontend 元件要改 import / `'use client'` directive |
|
||
|
||
詳見 `.autoflow/04-architecture/visiona-agent-tdd.md` §3.2。
|
||
|
||
## 可用腳本
|
||
|
||
| 指令 | 說明 |
|
||
|------|------|
|
||
| `pnpm dev` | 啟動開發伺服器(mock bindings,http://localhost:3000)|
|
||
| `pnpm build` | 產出靜態檔到 `out/`(供 Wails `go:embed`)|
|
||
| `pnpm lint` | ESLint 檢查(`eslint-config-next` + `react-hooks`)|
|
||
| `pnpm test` | 執行所有 Vitest 測試(one-shot)|
|
||
| `pnpm test:watch` | Watch 模式 |
|
||
|
||
## 專案結構
|
||
|
||
```
|
||
frontend/
|
||
├── next.config.ts # output: "export" + trailingSlash + images.unoptimized
|
||
├── package.json # name: visiona-agent-frontend
|
||
├── tsconfig.json
|
||
├── postcss.config.mjs
|
||
├── eslint.config.mjs
|
||
├── vitest.config.ts
|
||
├── components.json # shadcn/ui 產生器設定(保留以利未來新增元件)
|
||
├── public/
|
||
│ └── visiona-logo.png
|
||
└── src/
|
||
├── app/
|
||
│ ├── globals.css # Design Tokens + Tailwind 層(100% 從 visionA-frontend 複製)
|
||
│ ├── layout.tsx # Root layout(LocaleProvider / ThemeProvider / TooltipProvider / Toaster)
|
||
│ └── page.tsx # AgentApp — 3 tabs(Radix Tabs)單頁切換入口
|
||
├── components/
|
||
│ ├── theme-provider.tsx
|
||
│ ├── layout/ # Agent 專屬 layout:AppShell / Header / TabNav / ConnectionStatusBadge
|
||
│ ├── agent/ # 狀態頁元件:StatusHero / InfoCard / RecentLog;配對頁元件:TokenInput
|
||
│ └── ui/ # 24 個 Radix UI primitives(shadcn 風格封裝)+ EmptyState / Spinner / Sonner
|
||
├── views/ # 3 個 tab 對應的 view 元件(不走 Next.js routing,以 useState 切換)
|
||
│ ├── status-view.tsx
|
||
│ ├── pair-view.tsx
|
||
│ └── settings-view.tsx
|
||
├── hooks/ # Wails bindings 的 React wrapper
|
||
│ ├── use-connection-status.ts # GetStatus() + connection:status event
|
||
│ ├── use-recent-logs.ts # GetRecentLog() + connection:log event
|
||
│ ├── use-pair.ts # Pair(token) + pairing:result event
|
||
│ ├── use-settings.ts # GetSettings() + UpdateSettings(patch) + settings:updated
|
||
│ ├── use-test-connection.ts # TestRelay(url)
|
||
│ └── use-export-log.ts # ExportLog()
|
||
├── lib/
|
||
│ ├── utils.ts # cn() — clsx + tailwind-merge
|
||
│ ├── agent-api.ts # Wails binding 抽象層(真實 binding + mock 雙實作自動切換)
|
||
│ └── i18n/
|
||
│ ├── context.tsx # LocaleProvider / useLocale / useT
|
||
│ ├── sync.tsx # LocaleSync — mount 後從 localStorage 拉偏好
|
||
│ ├── index.ts # dictionaries 匯出入口
|
||
│ ├── types.ts # Locale / Dictionary / SUPPORTED_LOCALES
|
||
│ └── dictionaries/
|
||
│ ├── zh-Hant.ts # 繁中字典(預設,約 93 keys)
|
||
│ └── en.ts # English dictionary(key 集合與順序與 zh-Hant 完全一致)
|
||
├── types/ # 前端型別定義
|
||
│ ├── agent.ts # ConnectionState / ConnectionSnapshot / LogEntry / AgentSettings / PairError
|
||
│ └── api.ts # 與本地 server 互動的 envelope(預留)
|
||
└── tests/
|
||
└── setup.ts # @testing-library/jest-dom 全域 matcher
|
||
```
|
||
|
||
## 三個頁面對應
|
||
|
||
| Tab | 路徑 | 主要元件 | 對應 spec 章節 |
|
||
|-----|------|---------|---------------|
|
||
| 狀態(`status`,預設) | `views/status-view.tsx` | `StatusHero`(80px 大狀態圓)+ `InfoCard`(帳號/Relay/Session)+ `RecentLog`(最近 10 筆事件)| spec §4 |
|
||
| 配對(`pair`) | `views/pair-view.tsx` | `TokenInput`(格式驗證 + 貼上 trim + Enter 送出)+ 錯誤 Alert + 底部安全提示 | spec §5 |
|
||
| 設定(`settings`) | `views/settings-view.tsx` | 5 區塊:連線 / 行為 / Log / 關於 / 危險區域 | spec §6 |
|
||
|
||
Tab 切換走 **Radix Tabs**(controlled `value`/`onValueChange`),不走 Next.js routing。跨 view 的程式化切換透過 `window` CustomEvent `agent:switch-tab` 實現(見 `status-view.tsx` 與 `page.tsx`)。
|
||
|
||
## Design Tokens
|
||
|
||
完全沿用 `visionA-frontend`:不動一行 `globals.css`。
|
||
|
||
| Token 類別 | 來源 |
|
||
|-----------|------|
|
||
| 色彩(`--background` / `--primary` / `--muted-foreground` 等)| `globals.css`(`@layer base` 定義 light / dark 值)|
|
||
| 連線狀態色(`--status-online` / `--status-offline` / `--status-reconnecting` / `--status-idle` / `--status-error`)| 同上(visionA-frontend F2 定義)|
|
||
| 字型 | `font-sans`(UI)/ `font-mono`(Token / Log / Relay URL)|
|
||
| 圓角 / 陰影 / 間距 | Tailwind 4 預設 + 少量 `@theme` 擴充 |
|
||
|
||
**規則**:不允許在元件裡寫死 `#xxxxxx` 或 `rgb(...)`,一律透過 Tailwind utility class 引用 CSS 變數(例如 `bg-status-online`、`text-destructive`)。
|
||
|
||
## i18n
|
||
|
||
採用自製輕量 i18n(不依賴 `next-intl`),以 React Context + `localStorage` 管理當前 locale。
|
||
|
||
- 預設 locale:`zh-Hant`(繁中)
|
||
- 支援:`zh-Hant` / `en`
|
||
- 儲存 key:`visionA.locale`
|
||
- Fallback:找不到 key 時回傳 key 本身(production 靜默;dev 印 warning)
|
||
- 測試保證:`i18n.test.ts` 驗證兩語系 key 集合完全一致,避免漏譯
|
||
|
||
字典採**扁平 key** 結構(例:`nav.status`),共分 8 個區塊:
|
||
|
||
| 區塊 | 說明 |
|
||
|------|------|
|
||
| `app.*` | 產品名稱、標語 |
|
||
| `common.*` | 通用按鈕 / 文案(loading / cancel / save ...)|
|
||
| `nav.*` | Tab 導航標籤 |
|
||
| `connection.*` | 連線狀態(online / offline / reconnecting / notPaired / error) |
|
||
| `header.*` | Header 工具列(切主題、切語言)|
|
||
| `status.*` | 狀態頁(hero / info / action / confirm / log / empty)|
|
||
| `pair.*` | 配對頁(title / input / button / alert / error)|
|
||
| `settings.*` | 設定頁(section / relayUrl / behavior / log / about / reset)|
|
||
|
||
新增 i18n key 時的 checklist:
|
||
|
||
1. 兩個字典(`zh-Hant.ts` + `en.ts`)**同步新增** — 否則 `i18n.test.ts` 會失敗
|
||
2. 保持兩邊區塊順序一致(便於 diff 與 review)
|
||
3. 變數內插用 `{name}` 風格(例:`Attempt {n} of 5`),呼叫端以 `String.prototype.replace` 替換
|
||
4. 錯誤訊息要「說清楚發生什麼 + 使用者可以做什麼」
|
||
|
||
## 測試
|
||
|
||
```bash
|
||
pnpm test # one-shot 執行所有測試
|
||
pnpm test:watch # watch 模式
|
||
```
|
||
|
||
### 測試策略
|
||
|
||
| 層級 | 工具 | 範例 |
|
||
|------|------|------|
|
||
| 單元(元件) | Vitest + RTL | `button.test.tsx` / `token-input.test.tsx`(行為 + 格式驗證 regex)|
|
||
| 單元(hooks / utils) | Vitest | `i18n.test.ts`(字典完整性 + key 集合一致性)|
|
||
| 整合(view) | Vitest + RTL | `app-shell.test.tsx`(Header + Badge 組合)|
|
||
| E2E | — | 由 Testing Agent 負責,不在本 repo |
|
||
|
||
### 關鍵測試
|
||
|
||
- **`i18n.test.ts`** — 字典完整性守門員;兩語系 key 集合差異時立即失敗
|
||
- **`token-input.test.tsx`** — Pairing Token regex `^vAc_[a-f0-9]{32}$/i`(大小寫不敏感)+ 貼上清理 + Enter 送出
|
||
- **`status-hero.test.tsx`** — 5 種狀態變體(online / offline / reconnecting / notPaired / error)+ aria-label 組合 + icon `aria-hidden`
|
||
|
||
## 效能預算
|
||
|
||
本 Agent 不是 Web App,不需要滿足 Core Web Vitals。但仍有一些自律規則:
|
||
|
||
| 項目 | 目標 |
|
||
|------|------|
|
||
| Build 時間 | < 10s(Next.js 16 Turbopack 目前約 3-4s) |
|
||
| `out/` 總大小 | < 5 MB(static export 後,Wails embed 成本敏感) |
|
||
| 首次渲染到互動 | < 200ms(Wails WebView 2 本機載入,無網路延遲) |
|
||
| 冷啟動到可互動 | < 1s(含 Wails process init + WebView init)|
|
||
|
||
bundle 分析:`pnpm build` 時 Next.js 會印出每條 route 的大小。若單 route JS > 200KB,需檢討是否有多餘依賴或未做 code splitting。
|
||
|
||
## 無障礙規範
|
||
|
||
嚴格遵守 WCAG 2.2 AA(對齊 spec §9):
|
||
|
||
- **不只靠顏色**:每個連線狀態同時有「圓點顏色 + icon + 文字標籤」
|
||
- **語義化 HTML**:`<header>` / `<main>` / `<nav>` / `<section>` / `<dl><dt><dd>` / `<ol>`;避免 `<div>` 當按鈕
|
||
- **鍵盤導航**:所有操作 Tab 可達;Enter 送出表單;Esc 關閉 Dialog
|
||
- **ARIA**:
|
||
- `role="status" aria-live="polite"` — 狀態變化主動朗讀(StatusHero / ConnectionStatusBadge)
|
||
- Session Token 遮蔽版:`aria-label="Session token, ending in e7f8"`(不念完整字串)
|
||
- Tab:使用 Radix Tabs 原生 `role="tab"` / `role="tabpanel"` / `aria-selected`
|
||
- **焦點可見**:所有互動元件 `focus-visible:ring-[3px] ring-ring`
|
||
- **Reduce motion**:`motion-safe:` prefix 讓 `prefers-reduced-motion: reduce` 使用者停用 pulse / spin
|
||
|
||
## 環境變數
|
||
|
||
| 變數 | 說明 | 預設值 | 必要 |
|
||
|------|------|--------|------|
|
||
| `NODE_ENV` | `development` / `production`(由 Next.js 自動設定)| — | ✅ |
|
||
| — | — | — | — |
|
||
|
||
**Agent 前端刻意不讀取環境變數** — 所有使用者偏好(Relay URL、Log 等級、自啟動)由 Go backend 的 `agentconfig` 管理;前端只透過 Wails binding `GetSettings()` / `UpdateSettings(patch)` 讀寫。
|
||
|
||
## 安全
|
||
|
||
- **不儲存 Pairing Token**:使用者貼上的 token 經 `Pair(token)` 送到 Go backend,前端 state 在成功 / 失敗後清空。長期 Session Token 由 Go 寫入 OS keychain,**前端永遠拿不到完整 token**,只能看到遮蔽版 `sessionTokenPreview`(例如 `vAs_a1b2c3d4 ··· e7f8`)。
|
||
- **不 `dangerouslySetInnerHTML`**:本前端 0 處使用,已 grep 驗證。
|
||
- **外部 URL 開啟**:使用 `window.open(url, "_blank", "noopener,noreferrer")`;AF6 會改用 Wails `BrowserOpenURL` 以觸發 OS 預設瀏覽器(而非 in-app WebView)。
|
||
- **不讀取使用者憑證 / 帳號 / API Key**:Agent 的認證全靠 Pairing Token → Session Token 二段式換取,沒有使用者密碼。
|
||
|
||
## 路線圖 / 任務歷史
|
||
|
||
本 frontend 以 7 個任務逐步建置:
|
||
|
||
| Task | 範圍 | 狀態 |
|
||
|------|------|------|
|
||
| AF1 | 專案初始化(Next.js + Tailwind + Radix UI + i18n / ThemeProvider 基礎層) | ✅ |
|
||
| AF2 | Tab navigation Layout(AppShell / Header / TabNav / ConnectionStatusBadge) | ✅ |
|
||
| AF3 | 狀態頁(StatusHero + InfoCard + RecentLog + EmptyState) | ✅ |
|
||
| AF4 | 配對頁(TokenInput + PairView + 7 種錯誤訊息對照) | ✅ |
|
||
| AF5 | 設定頁(連線 / 行為 / Log / 關於 / 危險區域 5 區塊) | ✅ |
|
||
| AF6 | Wails bindings 整合(`agent-api.ts` 抽象層 + 真實 binding 取代 hooks 的 mock) | ✅(與 AF7 併發) |
|
||
| AF7 | i18n 字典審查 + README 完整版 + 前端收尾 | ✅ |
|
||
|
||
細節與原始 TDD 任務拆分詳見 `.autoflow/04-architecture/visiona-agent-tdd.md` §15.2。
|
||
|
||
## 參考文件
|
||
|
||
- `.autoflow/03-design/visiona-agent-spec.md` — UI 設計規格(3 頁面 + 全域 Header + 所有互動行為 + i18n key 清單)
|
||
- `.autoflow/04-architecture/visiona-agent-tdd.md` — 技術設計文件
|
||
- §2 整體架構
|
||
- §3 專案結構(§3.2 為何沿用 Next.js)
|
||
- §6 三個 UI 頁面對接(Wails bindings + events 規格)
|
||
- `.autoflow/04-architecture/adr/adr-008-tunnel-code-copy.md` — Tunnel client 複製策略
|
||
- `.autoflow/04-architecture/adr/adr-009-token-storage.md` — Token 儲存策略
|
||
|
||
## 給維護者的備忘
|
||
|
||
1. **不要**把 zustand store 加進來用於儲存狀態頁 / 配對頁的資料 — 連線狀態本質上是**後端主導的狀態**,前端以 Wails event 接收即可。若未來要做 UI-only 的複雜 state(例如多步表單),再引入 zustand。
|
||
2. **不要**在元件裡寫死 URL(雲端 pair 頁、docs、github)— 目前 `pair-view.tsx` / `settings-view.tsx` 的 URL 還是 placeholder,Phase 1 應改為從 `agentconfig` 或 `wailsjs` bindings 取得。
|
||
3. **不要**為了「看起來好看」新增動畫 — spec §1.3 明列「不做動畫秀」。
|
||
4. **不要**引入 routing — 三 tab 切換就是切換,Wails 環境下 URL 行為奇怪,避免之。
|
||
|
||
---
|
||
|
||
本 README 適用於任何接手 `local-agent/frontend/` 的工程師。有疑問請先讀 spec 與 TDD;不清楚的決策請去查 ADR;不確定的文案請去查 i18n 字典。
|