From dd64ed23eed34aacec841ddae3c7060d21182f05 Mon Sep 17 00:00:00 2001 From: cfy666 <3087823110@qq.com> Date: Sat, 13 Jun 2026 16:16:37 +0800 Subject: [PATCH 1/2] =?UTF-8?q?style:=20=E9=87=8D=E6=9E=84=E4=B8=BA=20Web?= =?UTF-8?q?=20=E7=AB=AF=E5=8F=8C=E6=A0=8F=E5=B8=83=E5=B1=80=EF=BC=8C?= =?UTF-8?q?=E7=AA=81=E5=87=BA=E5=AF=B9=E8=AF=9D=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 App.tsx 为双栏布局:左侧视频面板 + 右侧聊天面板 - 左侧 340px 固定宽度视频区(4:3 比例) - 右侧对话区 flex:1 占满剩余空间 - Header 改为导航栏风格:标题+副标题+状态标签+设置按钮 - 视频下方控制栏:观察模式/打断/结束对话按钮 - 优化间距、字号、配色,适配桌面端 Co-Authored-By: Claude --- frontend/src/App.css | 718 +++++++++++++++++++++++------------------ frontend/src/App.tsx | 184 ++++++----- frontend/src/index.css | 6 - 3 files changed, 506 insertions(+), 402 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index ca328d8..a9f8ac0 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,19 +1,22 @@ /* ============================================================ - CamTalk — 主应用样式 + CamTalk — Web 端应用样式 + 双栏布局:左侧视频面板 + 右侧聊天面板 ============================================================ */ :root { --color-primary: #2563eb; --color-primary-hover: #1d4ed8; - --color-bg: #0f172a; - --color-surface: #1e293b; - --color-text: #f1f5f9; - --color-text-muted: #94a3b8; - --color-border: #334155; + --color-bg: #0b0f1a; + --color-surface: #151b2b; + --color-surface-2: #1c2438; + --color-text: #e2e8f0; + --color-text-muted: #64748b; + --color-border: #1e293b; --color-success: #22c55e; --color-warning: #f59e0b; --color-error: #ef4444; - --radius: 8px; + --radius: 10px; + --radius-sm: 6px; } * { @@ -22,241 +25,469 @@ box-sizing: border-box; } +html, body, #root { + height: 100%; +} + body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; background: var(--color-bg); color: var(--color-text); - min-height: 100vh; } +/* ---- App Shell ---- */ + .app { display: flex; flex-direction: column; - min-height: 100vh; - max-width: 900px; - margin: 0 auto; - padding: 0 16px; + height: 100vh; + overflow: hidden; } /* ---- Header ---- */ -.app-header { +.header { display: flex; align-items: center; justify-content: space-between; - padding: 16px 0; + padding: 0 20px; + height: 48px; + background: var(--color-surface); border-bottom: 1px solid var(--color-border); + flex-shrink: 0; } -.app-header h1 { - font-size: 1.5rem; +.header__left { + display: flex; + align-items: baseline; + gap: 12px; +} + +.header__title { + font-size: 1.2rem; font-weight: 700; + color: var(--color-text); } -.header-right { +.header__subtitle { + font-size: 0.8rem; + color: var(--color-text-muted); +} + +.header__right { display: flex; align-items: center; - gap: 8px; + gap: 12px; +} + +.header__stats { + font-size: 0.75rem; + color: var(--color-text-muted); + padding: 4px 10px; + background: var(--color-surface-2); + border-radius: 12px; +} + +.badge { + font-size: 0.75rem; + padding: 3px 10px; + border-radius: 12px; + font-weight: 500; +} + +.badge--connected { + color: var(--color-success); + background: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.3); +} + +.badge--connecting { + color: var(--color-warning); + background: rgba(245, 158, 11, 0.1); + border: 1px solid rgba(245, 158, 11, 0.3); +} + +.badge--disconnected { + color: var(--color-text-muted); + background: var(--color-surface-2); + border: 1px solid var(--color-border); } .btn-icon { background: none; border: none; - font-size: 1.2rem; + font-size: 1.1rem; cursor: pointer; - padding: 4px; - border-radius: 4px; - transition: background 0.2s; + padding: 6px; + border-radius: var(--radius-sm); + transition: background 0.15s; + color: var(--color-text-muted); } .btn-icon:hover { - background: var(--color-surface); + background: var(--color-surface-2); + color: var(--color-text); } -.status { - font-size: 0.85rem; - padding: 4px 12px; - border-radius: 12px; - background: var(--color-surface); +/* ---- Workspace (双栏) ---- */ + +.workspace { + flex: 1; + display: flex; + overflow: hidden; } -.status--connected { - color: var(--color-success); - border: 1px solid var(--color-success); +/* ---- 左侧视频面板 ---- */ + +.video-panel { + width: 340px; + flex-shrink: 0; + display: flex; + flex-direction: column; + padding: 16px; + gap: 12px; } -.status--connecting { - color: var(--color-warning); - border: 1px solid var(--color-warning); -} - -.status--disconnected { - color: var(--color-text-muted); +.video-container { + position: relative; + aspect-ratio: 4 / 3; + background: #0a0e17; + border-radius: var(--radius); + overflow: hidden; border: 1px solid var(--color-border); } -/* ---- Main ---- */ - -.app-main { - flex: 1; - display: flex; - flex-direction: column; - gap: 16px; - padding: 16px 0; - overflow: hidden; -} - -.video-section { - position: relative; - flex-shrink: 0; -} - -.vad-indicator { - position: absolute; - bottom: 12px; - left: 12px; - background: rgba(0, 0, 0, 0.7); - color: var(--color-success); - padding: 6px 12px; - border-radius: var(--radius); - font-size: 0.85rem; - animation: pulse 1.5s ease-in-out infinite; -} - -.vad-indicator--loading { - color: var(--color-warning); -} - -.vad-indicator--error { - color: var(--color-error); - animation: none; -} - -.vad-indicator--audio { - left: auto; - right: 12px; - color: var(--color-primary); -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.6; } -} - -.chat-section { - flex: 1; - display: flex; - flex-direction: column; - gap: 12px; - overflow: hidden; -} - -/* ---- Video Preview ---- */ - -.video-preview { - position: relative; +.video-container .video-preview { width: 100%; - aspect-ratio: 4 / 3; - background: var(--color-surface); - border-radius: var(--radius); - overflow: hidden; + height: 100%; + border-radius: 0; } -.video-preview__video { +.video-container .video-preview__video { width: 100%; height: 100%; object-fit: cover; + background: #000; } -.video-preview__placeholder { +.video-placeholder { position: absolute; inset: 0; display: flex; + flex-direction: column; align-items: center; justify-content: center; + gap: 8px; + color: var(--color-text-muted); + font-size: 0.82rem; +} + +.video-placeholder__icon { + font-size: 2.4rem; + opacity: 0.2; + margin-bottom: 4px; +} + +.video-indicator { + position: absolute; + bottom: 16px; + left: 16px; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(8px); + color: var(--color-success); + padding: 6px 14px; + border-radius: var(--radius-sm); + font-size: 0.8rem; + font-weight: 500; + animation: pulse 1.5s ease-in-out infinite; +} + +.video-indicator--loading { color: var(--color-warning); } +.video-indicator--audio { left: auto; right: 16px; color: var(--color-primary); } +.video-indicator--error { color: var(--color-error); animation: none; } + +.detail-badge { + position: absolute; + top: 12px; + right: 12px; + background: rgba(37, 99, 235, 0.85); + backdrop-filter: blur(8px); + color: white; + padding: 3px 10px; + border-radius: var(--radius-sm); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.5px; +} + +.observation-badge { + position: absolute; + top: 12px; + left: 12px; + background: rgba(34, 197, 94, 0.85); + backdrop-filter: blur(8px); + color: white; + padding: 3px 10px; + border-radius: var(--radius-sm); + font-size: 0.7rem; + font-weight: 700; + animation: pulse 2s ease-in-out infinite; +} + +.video-overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.35); + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(2px); +} + +.video-overlay__spinner { + width: 36px; + height: 36px; + border: 3px solid rgba(255, 255, 255, 0.2); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +/* ---- 视频控制栏 ---- */ + +.video-controls { + flex-shrink: 0; + display: flex; + justify-content: center; + padding: 4px 0; +} + +.video-controls__row { + display: flex; + gap: 8px; +} + +/* ---- 右侧聊天面板 ---- */ + +.chat-panel-wrapper { + flex: 1; + display: flex; + flex-direction: column; + background: var(--color-surface); + border-left: 1px solid var(--color-border); + min-width: 0; +} + +.chat-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + font-weight: 600; + font-size: 0.85rem; + border-bottom: 1px solid var(--color-border); + flex-shrink: 0; color: var(--color-text-muted); } -/* ---- Chat Panel ---- */ +.chat-panel-header__mode { + font-size: 0.7rem; + font-weight: 500; + color: var(--color-success); + background: rgba(34, 197, 94, 0.1); + padding: 2px 8px; + border-radius: 10px; +} -.chat-panel { +.chat-panel-body { + flex: 1; + overflow: hidden; display: flex; flex-direction: column; - gap: 12px; - overflow-y: auto; +} + +/* ---- Chat Panel (覆盖子组件样式) ---- */ + +.chat-panel { flex: 1; + overflow-y: auto; + padding: 12px 16px; + display: flex; + flex-direction: column; + gap: 10px; } .chat-panel--empty { display: flex; align-items: center; justify-content: center; - padding: 48px 0; + flex: 1; color: var(--color-text-muted); + font-size: 0.85rem; } .chat-message { - padding: 12px 16px; - border-radius: var(--radius); - background: var(--color-surface); + padding: 10px 14px; + border-radius: var(--radius-sm); + background: var(--color-surface-2); + max-width: 92%; } .chat-message--user { - border-left: 3px solid var(--color-primary); + border-left: 2px solid var(--color-primary); + align-self: flex-end; + background: rgba(37, 99, 235, 0.08); } .chat-message--assistant { - border-left: 3px solid var(--color-success); + border-left: 2px solid var(--color-success); } .chat-message--streaming { - opacity: 0.8; + opacity: 0.85; } .chat-message__role { - font-size: 0.75rem; + font-size: 0.65rem; font-weight: 600; text-transform: uppercase; + letter-spacing: 0.5px; color: var(--color-text-muted); - margin-bottom: 4px; + margin-bottom: 3px; } .chat-message__content { - font-size: 0.95rem; - line-height: 1.5; + font-size: 0.82rem; + line-height: 1.55; white-space: pre-wrap; } -/* ---- Footer ---- */ - -.app-footer { - display: flex; - gap: 12px; - justify-content: center; - padding: 16px 0; - border-top: 1px solid var(--color-border); -} - -/* ---- Stats Bar ---- */ - -.stats-bar { - text-align: center; - padding: 4px 0; - font-size: 0.75rem; +.chat-message__meta { + margin-top: 6px; + font-size: 0.65rem; color: var(--color-text-muted); - border-top: 1px solid var(--color-border); } +/* ---- Streaming Cursor ---- */ + +.cursor { + display: inline; + animation: blink 0.8s step-end infinite; + color: var(--color-success); +} + +/* ---- System Message ---- */ + +.system-message { + text-align: center; + padding: 8px 16px; + margin: 0 20px; + border-radius: var(--radius-sm); + font-size: 0.8rem; +} + +.system-message--warning { + background: rgba(245, 158, 11, 0.1); + color: var(--color-warning); + border: 1px solid rgba(245, 158, 11, 0.2); +} + +/* ---- Config Panel ---- */ + +.config-panel { + background: var(--color-surface); + border-bottom: 1px solid var(--color-border); + padding: 16px 24px; + display: flex; + align-items: center; + gap: 24px; + flex-shrink: 0; +} + +.config-panel__header { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + font-size: 0.85rem; +} + +.config-panel__close { + background: none; + border: none; + color: var(--color-text-muted); + cursor: pointer; + font-size: 0.9rem; + padding: 2px 6px; +} + +.config-panel__close:hover { + color: var(--color-text); +} + +.config-row { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.82rem; + color: var(--color-text-muted); +} + +.config-row select, +.config-row input[type="checkbox"] { + background: var(--color-surface-2); + color: var(--color-text); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + padding: 4px 8px; + font-size: 0.8rem; +} + +/* ---- Toast ---- */ + +.toast-container { + position: fixed; + top: 68px; + right: 20px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 8px; +} + +.toast { + padding: 10px 16px; + border-radius: var(--radius-sm); + font-size: 0.82rem; + cursor: pointer; + animation: slideIn 0.3s ease-out; + max-width: 320px; + backdrop-filter: blur(8px); +} + +.toast--error { background: rgba(239, 68, 68, 0.9); color: white; } +.toast--warning { background: rgba(245, 158, 11, 0.9); color: #000; } +.toast--info { background: rgba(37, 99, 235, 0.9); color: white; } + /* ---- Buttons ---- */ .btn { - padding: 10px 24px; + padding: 8px 20px; border: none; - border-radius: var(--radius); - font-size: 0.95rem; + border-radius: var(--radius-sm); + font-size: 0.85rem; font-weight: 600; cursor: pointer; - transition: background 0.2s; + transition: all 0.15s; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.btn--lg { + padding: 12px 32px; + font-size: 0.95rem; + border-radius: var(--radius); } .btn--primary { @@ -269,7 +500,7 @@ body { } .btn--secondary { - background: var(--color-surface); + background: var(--color-surface-2); color: var(--color-text); border: 1px solid var(--color-border); } @@ -283,28 +514,35 @@ body { color: #000; } -.btn--warning:hover { - opacity: 0.9; +.btn--warning:hover { opacity: 0.9; } + +.btn--danger { + background: rgba(239, 68, 68, 0.15); + color: var(--color-error); + border: 1px solid rgba(239, 68, 68, 0.3); +} + +.btn--danger:hover { + background: rgba(239, 68, 68, 0.25); } .btn--active { background: var(--color-primary); color: white; - box-shadow: 0 0 8px rgba(37, 99, 235, 0.5); + box-shadow: 0 0 12px rgba(37, 99, 235, 0.4); animation: pulseBtn 2s ease-in-out infinite; } -@keyframes pulseBtn { - 0%, 100% { box-shadow: 0 0 8px rgba(37, 99, 235, 0.5); } - 50% { box-shadow: 0 0 16px rgba(37, 99, 235, 0.8); } +/* ---- Animations ---- */ + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } } -/* ---- Streaming Cursor ---- */ - -.cursor { - display: inline; - animation: blink 0.8s step-end infinite; - color: var(--color-success); +@keyframes pulseBtn { + 0%, 100% { box-shadow: 0 0 12px rgba(37, 99, 235, 0.4); } + 50% { box-shadow: 0 0 20px rgba(37, 99, 235, 0.7); } } @keyframes blink { @@ -312,174 +550,30 @@ body { 50% { opacity: 0; } } -/* ---- Message Meta ---- */ - -.chat-message__meta { - margin-top: 6px; - font-size: 0.75rem; - color: var(--color-text-muted); -} - -/* ---- System Message ---- */ - -.system-message { - text-align: center; - padding: 8px 16px; - border-radius: var(--radius); - font-size: 0.85rem; -} - -.system-message--warning { - background: rgba(245, 158, 11, 0.15); - color: var(--color-warning); - border: 1px solid rgba(245, 158, 11, 0.3); -} - -/* ---- Toast ---- */ - -.toast-container { - position: fixed; - top: 16px; - right: 16px; - z-index: 1000; - display: flex; - flex-direction: column; - gap: 8px; -} - -.toast { - padding: 10px 16px; - border-radius: var(--radius); - font-size: 0.85rem; - cursor: pointer; - animation: slideIn 0.3s ease-out; - max-width: 320px; -} - -.toast--error { - background: rgba(239, 68, 68, 0.9); - color: white; -} - -.toast--warning { - background: rgba(245, 158, 11, 0.9); - color: #000; -} - -.toast--info { - background: rgba(37, 99, 235, 0.9); - color: white; -} - -@keyframes slideIn { - from { - opacity: 0; - transform: translateX(20px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -/* ---- Video Overlay ---- */ - -.video-overlay { - position: absolute; - inset: 0; - background: rgba(0, 0, 0, 0.3); - display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius); -} - -.video-overlay__spinner { - width: 32px; - height: 32px; - border: 3px solid rgba(255, 255, 255, 0.3); - border-top-color: white; - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - @keyframes spin { to { transform: rotate(360deg); } } -/* ---- Config Panel ---- */ - -.config-panel { - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius); - padding: 16px; - margin: 8px 0; +@keyframes slideIn { + from { opacity: 0; transform: translateX(20px); } + to { opacity: 1; transform: translateX(0); } } -.config-panel__header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; - font-weight: 600; +/* ---- Scrollbar ---- */ + +::-webkit-scrollbar { + width: 6px; } -.config-panel__close { - background: none; - border: none; - color: var(--color-text-muted); - cursor: pointer; - font-size: 1rem; - padding: 2px 6px; +::-webkit-scrollbar-track { + background: transparent; } -.config-panel__close:hover { - color: var(--color-text); +::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 3px; } -.config-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 0; - font-size: 0.9rem; -} - -.config-row select, -.config-row input[type="checkbox"] { - background: var(--color-bg); - color: var(--color-text); - border: 1px solid var(--color-border); - border-radius: 4px; - padding: 4px 8px; - font-size: 0.85rem; -} - -/* ---- Detail Badge ---- */ - -.detail-badge { - position: absolute; - top: 8px; - right: 8px; - background: rgba(37, 99, 235, 0.8); - color: white; - padding: 2px 8px; - border-radius: 4px; - font-size: 0.7rem; - font-weight: 700; - letter-spacing: 0.5px; -} - -.observation-badge { - position: absolute; - top: 8px; - left: 8px; - background: rgba(34, 197, 94, 0.8); - color: white; - padding: 2px 8px; - border-radius: 4px; - font-size: 0.7rem; - font-weight: 700; - animation: pulse 2s ease-in-out infinite; +::-webkit-scrollbar-thumb:hover { + background: var(--color-text-muted); } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b23082c..7c5918d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ // ============================================================ -// CamTalk — 主应用组件 +// CamTalk — 主应用组件(Web 端双栏布局) // ============================================================ import { useState } from "react"; @@ -39,15 +39,20 @@ function App() { return (
-
-

CamTalk

-
- - {isConnected - ? "已连接" - : connectionStatus === "connecting" - ? "连接中..." - : "未连接"} + {/* ---- 顶部导航栏 ---- */} +
+
+

CamTalk

+ AI 视觉对话助手 +
+
+ {isConnected && (stats.queryCount > 0 || stats.totalTokens > 0) && ( + + {stats.queryCount} 次请求 · {stats.totalTokens} tokens + + )} + + {isConnected ? "已连接" : connectionStatus === "connecting" ? "连接中..." : "未连接"} {isConnected && ( - ) : ( - <> - - - {isProcessing && ( - + {/* ---- 主体:左侧视频 + 右侧聊天 ---- */} +
+ {/* 左侧:视频预览 + 控制栏 */} +
+
+ + {config.detailLevel === "high" && ( +
HD
)} - - )} - + {isObserving && ( +
👁️ 观察中
+ )} + {isSpeaking && ( +
🎤 正在聆听...
+ )} + {isAudioPlaying && config.ttsEnabled && ( +
🔊 正在播放...
+ )} + {isConnected && !isVADReady && !vadError && ( +
正在初始化语音检测...
+ )} + {vadError && ( +
⚠️ {vadError}
+ )} + {isProcessing && ( +
+
+
+ )} + {!isConnected && !stream && ( +
+ 📷 + 点击右侧按钮开始对话 +
+ )} +
+ + {/* 视频下方控制栏 */} +
+ {!isConnected ? ( + + ) : ( +
+ + {isProcessing && ( + + )} + +
+ )} +
+
+ + {/* 右侧:聊天面板 */} +
+
+ 对话 + {isConnected && mode === "observation" && ( + 观察模式 + )} +
+
+ {connectionStatus === "disconnected" && messages.length > 0 && ( +
+ 连接已断开,正在重连... +
+ )} + +
+
+
diff --git a/frontend/src/index.css b/frontend/src/index.css index 7b0cf71..c509944 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,7 +1 @@ /* 全局重置 — 详细样式在 App.css 中定义 */ - -#root { - min-height: 100vh; - display: flex; - flex-direction: column; -} From 093dc26fa0516fab267da313bcd17980333a8569 Mon Sep 17 00:00:00 2001 From: cfy666 <3087823110@qq.com> Date: Sat, 13 Jun 2026 16:21:04 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=20.gitignore?= =?UTF-8?q?=EF=BC=8C=E5=BF=BD=E7=95=A5=E7=BC=96=E8=AF=91=E4=BA=A7=E7=89=A9?= =?UTF-8?q?=E5=92=8C=E4=B8=B4=E6=97=B6=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backend/server:Go 编译二进制 - .playwright-mcp/:浏览器测试临时日志 - *.png:调试截图 Co-Authored-By: Claude --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index d37bf7a..4c77b4c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ frontend/dist/ # ---- 后端 ---- backend/bin/ +backend/server # ---- 环境变量 ---- .env @@ -18,5 +19,11 @@ Thumbs.db .idea/ .vscode/ +# ---- Playwright MCP ---- +.playwright-mcp/ + +# ---- 截图 ---- +*.png + # ---- Obsidian ---- .obsidian/