Files
CamTalk/frontend/src/App.tsx

170 lines
5.6 KiB
TypeScript
Raw Normal View History

// ============================================================
// CamTalk — 主应用组件Web 端双栏布局)
// ============================================================
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="header">
<div className="header__left">
<h1 className="header__title">CamTalk</h1>
<span className="header__subtitle">AI </span>
</div>
<div className="header__right">
{isConnected && (stats.queryCount > 0 || stats.totalTokens > 0) && (
<span className="header__stats">
{stats.queryCount} · {stats.totalTokens} tokens
</span>
)}
<span className={`badge badge--${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)}
/>
)}
{/* ---- 主体:左侧视频 + 右侧聊天 ---- */}
<div className="workspace">
{/* 左侧:视频预览 + 控制栏 */}
<div className="video-panel">
<div className="video-container">
<VideoPreview ref={videoRef} isStreaming={!!stream} />
{config.detailLevel === "high" && (
<div className="detail-badge">HD</div>
)}
{isObserving && (
<div className="observation-badge">👁 </div>
)}
{isSpeaking && (
<div className="video-indicator">🎤 ...</div>
)}
{isAudioPlaying && config.ttsEnabled && (
<div className="video-indicator video-indicator--audio">🔊 ...</div>
)}
{isConnected && !isVADReady && !vadError && (
<div className="video-indicator video-indicator--loading">...</div>
)}
{vadError && (
<div className="video-indicator video-indicator--error"> {vadError}</div>
)}
{isProcessing && (
<div className="video-overlay">
<div className="video-overlay__spinner" />
</div>
)}
{!isConnected && !stream && (
<div className="video-placeholder">
<span className="video-placeholder__icon">📷</span>
<span></span>
</div>
)}
</div>
{/* 视频下方控制栏 */}
<div className="video-controls">
{!isConnected ? (
<button className="btn btn--primary btn--lg" onClick={startSession}>
{connectionStatus === "connecting" ? "连接中..." : "🎙️ 开始对话"}
</button>
) : (
<div className="video-controls__row">
<button
className={`btn ${mode === "observation" ? "btn--active" : "btn--secondary"}`}
onClick={toggleMode}
>
{mode === "observation" ? "👁️ 观察中" : "👁️ 观察模式"}
</button>
{isProcessing && (
<button className="btn btn--warning" onClick={interrupt}>
</button>
)}
<button className="btn btn--danger" onClick={stopSession}>
</button>
</div>
)}
</div>
</div>
{/* 右侧:聊天面板 */}
<div className="chat-panel-wrapper">
<div className="chat-panel-header">
<span></span>
{isConnected && mode === "observation" && (
<span className="chat-panel-header__mode"></span>
)}
</div>
<div className="chat-panel-body">
{connectionStatus === "disconnected" && messages.length > 0 && (
<div className="system-message system-message--warning">
...
</div>
)}
<ChatPanel
messages={messages}
currentReply={currentReply}
connectionStatus={connectionStatus}
/>
</div>
</div>
</div>
<ToastContainer />
</div>
);
}
export default App;