2026-06-12 17:46:25 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// CamTalk — 主应用组件
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
import { useVisionSession } from "./hooks/useVisionSession";
|
|
|
|
|
|
import { VideoPreview } from "./components/VideoPreview";
|
|
|
|
|
|
import { ChatPanel } from "./components/ChatPanel";
|
2026-06-13 11:43:16 +08:00
|
|
|
|
import { ToastContainer } from "./components/Toast";
|
2026-06-12 17:46:25 +08:00
|
|
|
|
import "./App.css";
|
|
|
|
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
|
|
const {
|
|
|
|
|
|
messages,
|
|
|
|
|
|
currentReply,
|
|
|
|
|
|
isProcessing,
|
|
|
|
|
|
isSpeaking,
|
2026-06-13 11:10:29 +08:00
|
|
|
|
isVADReady,
|
|
|
|
|
|
vadError,
|
2026-06-12 17:46:25 +08:00
|
|
|
|
connectionStatus,
|
|
|
|
|
|
videoRef,
|
|
|
|
|
|
stream,
|
|
|
|
|
|
startSession,
|
|
|
|
|
|
stopSession,
|
|
|
|
|
|
interrupt,
|
|
|
|
|
|
} = useVisionSession();
|
|
|
|
|
|
|
2026-06-13 11:10:29 +08:00
|
|
|
|
const isConnected = connectionStatus === "connected";
|
|
|
|
|
|
|
2026-06-12 17:46:25 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="app">
|
|
|
|
|
|
<header className="app-header">
|
|
|
|
|
|
<h1>CamTalk</h1>
|
|
|
|
|
|
<span className={`status status--${connectionStatus}`}>
|
2026-06-13 11:10:29 +08:00
|
|
|
|
{isConnected
|
|
|
|
|
|
? "已连接"
|
|
|
|
|
|
: connectionStatus === "connecting"
|
|
|
|
|
|
? "连接中..."
|
|
|
|
|
|
: "未连接"}
|
2026-06-12 17:46:25 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<main className="app-main">
|
|
|
|
|
|
<div className="video-section">
|
|
|
|
|
|
<VideoPreview ref={videoRef} isStreaming={!!stream} />
|
|
|
|
|
|
{isSpeaking && <div className="vad-indicator">🎤 正在聆听...</div>}
|
2026-06-13 11:10:29 +08:00
|
|
|
|
{isConnected && !isVADReady && !vadError && (
|
|
|
|
|
|
<div className="vad-indicator vad-indicator--loading">
|
|
|
|
|
|
正在初始化语音检测...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{vadError && (
|
|
|
|
|
|
<div className="vad-indicator vad-indicator--error">
|
|
|
|
|
|
⚠️ {vadError}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-06-13 11:43:16 +08:00
|
|
|
|
{isProcessing && (
|
|
|
|
|
|
<div className="video-overlay">
|
|
|
|
|
|
<div className="video-overlay__spinner" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-06-12 17:46:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="chat-section">
|
2026-06-13 11:43:16 +08:00
|
|
|
|
{connectionStatus === "disconnected" && messages.length > 0 && (
|
|
|
|
|
|
<div className="system-message system-message--warning">
|
|
|
|
|
|
连接已断开,正在重连...
|
2026-06-12 17:46:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-06-13 11:43:16 +08:00
|
|
|
|
<ChatPanel
|
|
|
|
|
|
messages={messages}
|
|
|
|
|
|
currentReply={currentReply}
|
|
|
|
|
|
connectionStatus={connectionStatus}
|
|
|
|
|
|
/>
|
2026-06-12 17:46:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<footer className="app-footer">
|
2026-06-13 11:10:29 +08:00
|
|
|
|
{!isConnected ? (
|
2026-06-12 17:46:25 +08:00
|
|
|
|
<button className="btn btn--primary" onClick={startSession}>
|
2026-06-13 11:43:16 +08:00
|
|
|
|
{connectionStatus === "connecting" ? "连接中..." : "开始对话"}
|
2026-06-12 17:46:25 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<button className="btn btn--secondary" onClick={stopSession}>
|
|
|
|
|
|
结束对话
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{isProcessing && (
|
|
|
|
|
|
<button className="btn btn--warning" onClick={interrupt}>
|
|
|
|
|
|
打断
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</footer>
|
2026-06-13 11:43:16 +08:00
|
|
|
|
|
|
|
|
|
|
<ToastContainer />
|
2026-06-12 17:46:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default App;
|