- 新增 useObservationMode Hook:定时 5s 采帧,similarity < 0.85 触发变化回调 - useVisionSession 新增 dialogue/observation 模式切换 - App Footer 加观察模式切换按钮(激活时蓝色脉冲) - 视频区左上角显示'👁️ 观察中'绿色角标 - 观察模式下画面变化自动发送 query Co-Authored-By: Claude <noreply@anthropic.com>
154 lines
4.5 KiB
TypeScript
154 lines
4.5 KiB
TypeScript
// ============================================================
|
||
// CamTalk — 主应用组件
|
||
// ============================================================
|
||
|
||
import { 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 "./App.css";
|
||
|
||
function App() {
|
||
const [showConfig, setShowConfig] = useState(false);
|
||
|
||
const {
|
||
messages,
|
||
currentReply,
|
||
isProcessing,
|
||
isAudioPlaying,
|
||
isSpeaking,
|
||
isVADReady,
|
||
vadError,
|
||
connectionStatus,
|
||
videoRef,
|
||
stream,
|
||
config,
|
||
updateConfig,
|
||
stats,
|
||
mode,
|
||
isObserving,
|
||
toggleMode,
|
||
startSession,
|
||
stopSession,
|
||
interrupt,
|
||
} = useVisionSession();
|
||
|
||
const isConnected = connectionStatus === "connected";
|
||
|
||
return (
|
||
<div className="app">
|
||
<header className="app-header">
|
||
<h1>CamTalk</h1>
|
||
<div className="header-right">
|
||
<span className={`status status--${connectionStatus}`}>
|
||
{isConnected
|
||
? "已连接"
|
||
: connectionStatus === "connecting"
|
||
? "连接中..."
|
||
: "未连接"}
|
||
</span>
|
||
{isConnected && (
|
||
<button
|
||
className="btn-icon"
|
||
onClick={() => setShowConfig((v) => !v)}
|
||
title="设置"
|
||
>
|
||
⚙️
|
||
</button>
|
||
)}
|
||
</div>
|
||
</header>
|
||
|
||
{showConfig && (
|
||
<ConfigPanel
|
||
config={config}
|
||
onUpdate={updateConfig}
|
||
onClose={() => setShowConfig(false)}
|
||
/>
|
||
)}
|
||
|
||
<main className="app-main">
|
||
<div className="video-section">
|
||
<VideoPreview ref={videoRef} isStreaming={!!stream} />
|
||
{config.detailLevel === "high" && (
|
||
<div className="detail-badge">HD</div>
|
||
)}
|
||
{isObserving && (
|
||
<div className="observation-badge">👁️ 观察中</div>
|
||
)}
|
||
{isSpeaking && <div className="vad-indicator">🎤 正在聆听...</div>}
|
||
{isAudioPlaying && config.ttsEnabled && (
|
||
<div className="vad-indicator vad-indicator--audio">🔊 正在播放...</div>
|
||
)}
|
||
{isConnected && !isVADReady && !vadError && (
|
||
<div className="vad-indicator vad-indicator--loading">
|
||
正在初始化语音检测...
|
||
</div>
|
||
)}
|
||
{vadError && (
|
||
<div className="vad-indicator vad-indicator--error">
|
||
⚠️ {vadError}
|
||
</div>
|
||
)}
|
||
{isProcessing && (
|
||
<div className="video-overlay">
|
||
<div className="video-overlay__spinner" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="chat-section">
|
||
{connectionStatus === "disconnected" && messages.length > 0 && (
|
||
<div className="system-message system-message--warning">
|
||
连接已断开,正在重连...
|
||
</div>
|
||
)}
|
||
<ChatPanel
|
||
messages={messages}
|
||
currentReply={currentReply}
|
||
connectionStatus={connectionStatus}
|
||
/>
|
||
</div>
|
||
</main>
|
||
|
||
{isConnected && (stats.queryCount > 0 || stats.totalTokens > 0) && (
|
||
<div className="stats-bar">
|
||
{stats.queryCount > 0 && <span>{stats.queryCount} 次请求</span>}
|
||
{stats.totalTokens > 0 && <span>· {stats.totalTokens} tokens</span>}
|
||
</div>
|
||
)}
|
||
|
||
<footer className="app-footer">
|
||
{!isConnected ? (
|
||
<button className="btn btn--primary" onClick={startSession}>
|
||
{connectionStatus === "connecting" ? "连接中..." : "开始对话"}
|
||
</button>
|
||
) : (
|
||
<>
|
||
<button
|
||
className={`btn ${mode === "observation" ? "btn--active" : "btn--secondary"}`}
|
||
onClick={toggleMode}
|
||
>
|
||
{mode === "observation" ? "👁️ 观察中" : "👁️ 观察模式"}
|
||
</button>
|
||
<button className="btn btn--secondary" onClick={stopSession}>
|
||
结束对话
|
||
</button>
|
||
{isProcessing && (
|
||
<button className="btn btn--warning" onClick={interrupt}>
|
||
打断
|
||
</button>
|
||
)}
|
||
</>
|
||
)}
|
||
</footer>
|
||
|
||
<ToastContainer />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default App;
|