Files
CamTalk/frontend/src/App.tsx
cfy666 7e3762b589
All checks were successful
Backend CI / ci (pull_request) Successful in 1m0s
Frontend CI / ci (pull_request) Successful in 1m0s
feat: 实现会话配置与多轮上下文
- 新增 ConfigPanel 组件:TTS 开关、detail level、语言选择
- 新增 storage.ts:配置持久化到 localStorage
- useVisionSession:连接后自动发送 config,维护最近 10 轮对话历史
- App:齿轮按钮展开配置面板,HD 角标,TTS 播放联动 ttsEnabled

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 14:19:47 +08:00

134 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 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,
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>
)}
{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>
<footer className="app-footer">
{!isConnected ? (
<button className="btn btn--primary" onClick={startSession}>
{connectionStatus === "connecting" ? "连接中..." : "开始对话"}
</button>
) : (
<>
<button className="btn btn--secondary" onClick={stopSession}>
</button>
{isProcessing && (
<button className="btn btn--warning" onClick={interrupt}>
</button>
)}
</>
)}
</footer>
<ToastContainer />
</div>
);
}
export default App;