feat: 初始化前端项目,搭建 React + TypeScript + Vite 工程结构

- 创建 Vite + React 18 + TypeScript 项目(严格模式)
- 安装 @ricky0123/vad-web、onnxruntime-web、uuid 依赖
- 定义全部 WebSocket 消息类型和数据模型(对齐接口文档)
- 实现 WebSocket 连接管理(心跳保活、指数退避重连)
- 实现音频编码工具(PCM↔Base64、DataURL 转换)
- 创建组件骨架:CameraManager、MicManager、EdgeProcessor、WebSocketManager、ChatPanel、VideoPreview
- 实现核心 useVisionSession Hook 骨架
- 配置 ESLint(_前缀变量忽略规则)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-12 17:46:25 +08:00
parent 10f676f3b4
commit b5c0d40609
26 changed files with 4236 additions and 0 deletions

72
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,72 @@
// ============================================================
// CamTalk — 主应用组件
// ============================================================
import { useVisionSession } from "./hooks/useVisionSession";
import { VideoPreview } from "./components/VideoPreview";
import { ChatPanel } from "./components/ChatPanel";
import "./App.css";
function App() {
const {
messages,
currentReply,
isProcessing,
isSpeaking,
connectionStatus,
videoRef,
stream,
startSession,
stopSession,
interrupt,
} = useVisionSession();
return (
<div className="app">
<header className="app-header">
<h1>CamTalk</h1>
<span className={`status status--${connectionStatus}`}>
{connectionStatus === "connected" ? "已连接" : connectionStatus === "connecting" ? "连接中..." : "未连接"}
</span>
</header>
<main className="app-main">
<div className="video-section">
<VideoPreview ref={videoRef} isStreaming={!!stream} />
{isSpeaking && <div className="vad-indicator">🎤 ...</div>}
</div>
<div className="chat-section">
<ChatPanel messages={messages} />
{currentReply && (
<div className="chat-message chat-message--assistant chat-message--streaming">
<div className="chat-message__role">AI</div>
<div className="chat-message__content">{currentReply}</div>
</div>
)}
</div>
</main>
<footer className="app-footer">
{connectionStatus !== "connected" ? (
<button className="btn btn--primary" onClick={startSession}>
</button>
) : (
<>
<button className="btn btn--secondary" onClick={stopSession}>
</button>
{isProcessing && (
<button className="btn btn--warning" onClick={interrupt}>
</button>
)}
</>
)}
</footer>
</div>
);
}
export default App;