// ============================================================ // CamTalk — 主应用组件(Web 端双栏布局) // ============================================================ import { useCallback, useEffect, useRef, useState } from "react"; import { useVisionSession } from "./hooks/useVisionSession"; import { VideoPreview } from "./components/VideoPreview"; import { ChatPanel } from "./components/ChatPanel"; import { ConfigPanel } from "./components/ConfigPanel"; import { ToastContainer } from "./components/Toast"; import { loadTheme, saveTheme } from "./lib/storage"; import type { Theme } from "./types"; import "./App.css"; function App() { const [showConfig, setShowConfig] = useState(false); const [theme, setTheme] = useState(loadTheme); const [elapsed, setElapsed] = useState(0); const timerRef = useRef | null>(null); // 切换主题时更新 的 data-theme 属性 useEffect(() => { document.documentElement.setAttribute("data-theme", theme); }, [theme]); const handleThemeChange = useCallback((t: Theme) => { setTheme(t); saveTheme(t); }, []); const formatTime = (s: number) => { const m = Math.floor(s / 60); const sec = s % 60; return `${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}`; }; const { messages, currentReply, isProcessing, isAudioPlaying, isSpeaking, isVADReady, vadError, connectionStatus, videoRef, stream, config, updateConfig, stats, mode, isObserving, toggleMode, startSession, stopSession, interrupt, isCameraOn, isMicOn, toggleCamera, toggleMic, } = useVisionSession(); const isConnected = connectionStatus === "connected"; // 连接计时器 const elapsedRef = useRef(0); useEffect(() => { if (isConnected) { elapsedRef.current = 0; setElapsed(0); // eslint-disable-line react-hooks/set-state-in-effect -- 连接时重置计时器 } }, [isConnected]); useEffect(() => { if (!isConnected) { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } return; } const id = setInterval(() => { elapsedRef.current += 1; setElapsed(elapsedRef.current); }, 1000); timerRef.current = id; return () => clearInterval(id); }, [isConnected]); return (
{/* ---- 顶部导航栏 ---- */}

CamTalk

AI 视觉对话助手
{isConnected && (stats.queryCount > 0 || stats.totalTokens > 0) && ( {stats.queryCount} 次请求 · {stats.totalTokens} tokens )} {isConnected ? "已连接" : connectionStatus === "connecting" ? "连接中..." : "未连接"}
{showConfig && ( setShowConfig(false)} /> )} {/* ---- 主体:左侧视频 + 右侧聊天 ---- */}
{/* 左侧:视频预览 + 控制栏 */}
{isConnected && (
LIVE {formatTime(elapsed)}
)} {config.detailLevel === "high" && (
HD
)} {isObserving && (
👁️ 观察中
)} {isSpeaking && (
🎤 正在聆听...
)} {isAudioPlaying && config.ttsEnabled && (
🔊 正在播放...
)} {isConnected && !isVADReady && !vadError && (
正在初始化语音检测...
)} {vadError && (
⚠️ {vadError}
)} {isProcessing && (
)} {!isConnected && !stream && (
📷 点击右侧按钮开始对话
)}
{/* 视频下方控制栏 */}
{!isConnected ? ( ) : (
{isProcessing && ( )}
)}
{/* 右侧:聊天面板 */}
对话 {isConnected && mode === "observation" && ( 观察模式 )}
{connectionStatus === "disconnected" && messages.length > 0 && (
连接已断开,正在重连...
)}
); } export default App;