From 188782fa66cf3357047b16134a13ca0a58b8467a Mon Sep 17 00:00:00 2001 From: cfy666 <3087823110@qq.com> Date: Sat, 13 Jun 2026 19:01:33 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20LIVE=20?= =?UTF-8?q?=E8=AE=A1=E6=97=B6=E5=99=A8=E5=92=8C=E8=AF=AD=E8=A8=80=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E7=BB=84=EF=BC=88P0=20=E4=BC=98=E5=8C=96=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 视频区左上角显示 LIVE 绿色圆点 + 连接时长计时器 - Header 添加中文/EN/日 语言按钮组,一键切换语言 - live-badge、lang-group 样式 Co-Authored-By: Claude --- frontend/src/App.css | 56 +++++++++++++++++++++++++++++++++++++++++++- frontend/src/App.tsx | 56 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index b6260b9..bddd055 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -143,6 +143,35 @@ body { color: var(--color-text); } +.lang-group { + display: flex; + gap: 2px; + background: var(--color-surface-2); + border-radius: var(--radius-sm); + padding: 2px; +} + +.lang-btn { + background: none; + border: none; + color: var(--color-text-muted); + font-size: 0.72rem; + font-weight: 600; + padding: 4px 10px; + border-radius: 4px; + cursor: pointer; + transition: all 0.15s; +} + +.lang-btn:hover { + color: var(--color-text); +} + +.lang-btn--active { + background: var(--color-primary); + color: white; +} + /* ---- Workspace (双栏) ---- */ .workspace { @@ -154,7 +183,7 @@ body { /* ---- 左侧视频面板 ---- */ .video-panel { - width: 400px; + width: 420px; flex-shrink: 0; display: flex; flex-direction: column; @@ -216,6 +245,31 @@ body { animation: pulse 1.5s ease-in-out infinite; } +.live-badge { + position: absolute; + top: 12px; + left: 12px; + display: flex; + align-items: center; + gap: 6px; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(8px); + color: white; + padding: 4px 12px; + border-radius: var(--radius-sm); + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.5px; +} + +.live-badge__dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--color-success); + 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; } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bde81ef..4fcea63 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,7 @@ // CamTalk — 主应用组件(Web 端双栏布局) // ============================================================ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useVisionSession } from "./hooks/useVisionSession"; import { VideoPreview } from "./components/VideoPreview"; import { ChatPanel } from "./components/ChatPanel"; @@ -15,6 +15,8 @@ import "./App.css"; function App() { const [showConfig, setShowConfig] = useState(false); const [theme, setTheme] = useState(loadTheme); + const [elapsed, setElapsed] = useState(0); + const timerRef = useRef | null>(null); // 切换主题时更新 的 data-theme 属性 useEffect(() => { @@ -26,6 +28,12 @@ function App() { saveTheme(t); }, []); + const formatTime = (s: number) => { + const m = Math.floor(s / 60); + const sec = s % 60; + return `${m.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}`; + }; + const { messages, currentReply, @@ -50,6 +58,31 @@ function App() { const isConnected = connectionStatus === "connected"; + // 连接计时器 + const elapsedRef = useRef(0); + useEffect(() => { + if (isConnected) { + elapsedRef.current = 0; + setElapsed(0); // eslint-disable-line react-hooks/set-state-in-effect -- 连接时重置计时器 + } + }, [isConnected]); + + useEffect(() => { + if (!isConnected) { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + return; + } + const id = setInterval(() => { + elapsedRef.current += 1; + setElapsed(elapsedRef.current); + }, 1000); + timerRef.current = id; + return () => clearInterval(id); + }, [isConnected]); + return (
{/* ---- 顶部导航栏 ---- */} @@ -64,6 +97,21 @@ function App() { {stats.queryCount} 次请求 · {stats.totalTokens} tokens )} +
+ {[ + { code: "zh-CN", label: "中文" }, + { code: "en-US", label: "EN" }, + { code: "ja-JP", label: "日" }, + ].map((l) => ( + + ))} +
{isConnected ? "已连接" : connectionStatus === "connecting" ? "连接中..." : "未连接"} @@ -93,6 +141,12 @@ function App() {
+ {isConnected && ( +
+ + LIVE {formatTime(elapsed)} +
+ )} {config.detailLevel === "high" && (
HD
)} -- 2.49.1 From 00f10088d00f51360959d2833711f6edf530cd5c Mon Sep 17 00:00:00 2001 From: cfy666 <3087823110@qq.com> Date: Sat, 13 Jun 2026 20:01:37 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E5=BA=95=E9=83=A8=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E6=A0=8F=E6=B7=BB=E5=8A=A0=E6=91=84=E5=83=8F=E5=A4=B4?= =?UTF-8?q?/=E9=BA=A6=E5=85=8B=E9=A3=8E=E5=BC=80=E5=85=B3=EF=BC=8C?= =?UTF-8?q?=E8=AF=B4=E8=AF=9D=E6=97=B6=E9=BA=A6=E5=85=8B=E9=A3=8E=E8=84=89?= =?UTF-8?q?=E5=86=B2=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useVisionSession 新增 isCameraOn/isMicOn 状态 + toggleCamera/toggleMic - 底部控制栏添加 📷🎤 图标按钮,开启绿色/关闭红色 - 说话时麦克风按钮绿色脉冲缩放动画(micPulse) - 按钮样式优化:nowrap 防换行,flex-wrap 自适应 Co-Authored-By: Claude --- frontend/src/App.css | 43 +++++++++++++++++++++++++- frontend/src/App.tsx | 33 +++++++++++--------- frontend/src/hooks/useVisionSession.ts | 32 +++++++++++++++++++ 3 files changed, 92 insertions(+), 16 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index bddd055..330c48c 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -332,7 +332,48 @@ body { .video-controls__row { display: flex; - gap: 8px; + gap: 6px; + flex-wrap: wrap; + justify-content: center; +} + +.btn--ctrl { + padding: 6px 12px; + font-size: 0.78rem; + border-radius: var(--radius-sm); + border: 1px solid var(--color-border); + background: var(--color-surface-2); + color: var(--color-text-muted); + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; +} + +.btn--ctrl:hover { + background: var(--color-border); + color: var(--color-text); +} + +.btn--ctrl-on { + background: rgba(34, 197, 94, 0.15); + color: var(--color-success); + border-color: rgba(34, 197, 94, 0.3); +} + +.btn--ctrl-off { + background: rgba(239, 68, 68, 0.1); + color: var(--color-error); + border-color: rgba(239, 68, 68, 0.2); +} + +.btn--speaking { + animation: micPulse 0.8s ease-in-out infinite; + box-shadow: 0 0 12px rgba(34, 197, 94, 0.5); +} + +@keyframes micPulse { + 0%, 100% { box-shadow: 0 0 8px rgba(34, 197, 94, 0.4); transform: scale(1); } + 50% { box-shadow: 0 0 16px rgba(34, 197, 94, 0.8); transform: scale(1.05); } } /* ---- 右侧聊天面板 ---- */ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4fcea63..ffd567b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -54,6 +54,10 @@ function App() { startSession, stopSession, interrupt, + isCameraOn, + isMicOn, + toggleCamera, + toggleMic, } = useVisionSession(); const isConnected = connectionStatus === "connected"; @@ -97,21 +101,6 @@ function App() { {stats.queryCount} 次请求 · {stats.totalTokens} tokens )} -
- {[ - { code: "zh-CN", label: "中文" }, - { code: "en-US", label: "EN" }, - { code: "ja-JP", label: "日" }, - ].map((l) => ( - - ))} -
{isConnected ? "已连接" : connectionStatus === "connecting" ? "连接中..." : "未连接"} @@ -186,6 +175,20 @@ function App() { ) : (
+ +