"use client"; /** * AgentApp — visionA Agent 的單一 SPA 入口(三 tab in-place 切換) * * 架構決策(見 TDD §3 與 spec §3): * - 不使用 Next.js routing:三個 tab(狀態 / 配對 / 設定)由 Radix Tabs 管理 * client state,切換即時無載入延遲,符合「桌面 utility」體驗預期 * - 首次開啟停在狀態頁(spec §3.2) * * AF3-AF5 更新: * - 從 `defaultValue`(uncontrolled)改為 `value`/`onValueChange`(controlled), * 以支援 StatusView / PairView 透過 CustomEvent `agent:switch-tab` 程式化切換 tab。 * 例如:配對成功後 pair-view 要自動切回 status,未配對時 status-view 的 CTA 要切到 pair。 * - 監聽 `agent:switch-tab` CustomEvent,收到時更新 state(單向資料流:view → event → page)。 * * 檔名維持 `page.tsx`(Next.js App Router 慣例),但元件名 `AgentApp` 反映其角色。 */ import { useEffect, useState } from "react"; import { AppShell } from "@/components/layout/app-shell"; import { TabNav, type AgentTabValue } from "@/components/layout/tab-nav"; import { Tabs, TabsContent } from "@/components/ui/tabs"; import { PairView } from "@/views/pair-view"; import { SettingsView } from "@/views/settings-view"; import { AGENT_SWITCH_TAB_EVENT, StatusView } from "@/views/status-view"; export default function AgentApp() { const [tab, setTab] = useState("status"); useEffect(() => { // 監聽 view 發出的程式化切 tab 請求(例:pair-view 配對成功後 → 切 status) function handleSwitch(e: Event) { const detail = (e as CustomEvent<{ value: string }>).detail; if (!detail?.value) return; // 收斂成 AgentTabValue,避免外部事件傳入未知字串 if (detail.value === "status" || detail.value === "pair" || detail.value === "settings") { setTab(detail.value); } } window.addEventListener(AGENT_SWITCH_TAB_EVENT, handleSwitch); return () => window.removeEventListener(AGENT_SWITCH_TAB_EVENT, handleSwitch); }, []); return ( {/* Tabs 控制端(controlled): - value / onValueChange — 讓 CustomEvent 可以程式化切換 - 水平 padding 用 px-4(與 Header px-4 對齊),top padding 給 Tab 一點呼吸空間 - max-w-2xl + mx-auto — spec §4.5 指示「大螢幕不會過寬」,內容收斂在 672px */} setTab(v as AgentTabValue)} className="mx-auto w-full max-w-2xl px-4 py-3" data-testid="agent-tabs" > ); }