style: 重构为 Web 端双栏布局,突出对话展示

- 重构 App.tsx 为双栏布局:左侧视频面板 + 右侧聊天面板
- 左侧 340px 固定宽度视频区(4:3 比例)
- 右侧对话区 flex:1 占满剩余空间
- Header 改为导航栏风格:标题+副标题+状态标签+设置按钮
- 视频下方控制栏:观察模式/打断/结束对话按钮
- 优化间距、字号、配色,适配桌面端

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-13 16:16:37 +08:00
parent 7e2537104d
commit dd64ed23ee
3 changed files with 506 additions and 402 deletions

View File

@@ -1,5 +1,5 @@
// ============================================================
// CamTalk — 主应用组件
// CamTalk — 主应用组件Web 端双栏布局)
// ============================================================
import { useState } from "react";
@@ -39,15 +39,20 @@ function App() {
return (
<div className="app">
<header className="app-header">
<h1>CamTalk</h1>
<div className="header-right">
<span className={`status status--${connectionStatus}`}>
{isConnected
? "已连接"
: connectionStatus === "connecting"
? "连接中..."
: "未连接"}
{/* ---- 顶部导航栏 ---- */}
<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
@@ -69,81 +74,92 @@ function App() {
/>
)}
<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>
{/* ---- 主体:左侧视频 + 右侧聊天 ---- */}
<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>
)}
</>
)}
</footer>
{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>