From fe053f94dce42562fb448b8e6a7107711678ff7c Mon Sep 17 00:00:00 2001 From: cfy666 <3087823110@qq.com> Date: Sun, 14 Jun 2026 15:56:59 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E9=9A=90=E8=97=8F=E8=A7=82=E5=AF=9F?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E6=8C=89=E9=92=AE=EF=BC=8C=E6=B8=85=E7=90=86?= =?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=20toggleMode=20=E5=BC=95?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2ef06ad..bc607b0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -69,7 +69,6 @@ function AppContent() { stats, mode, isObserving, - toggleMode, startSession, stopSession, interrupt, @@ -292,12 +291,6 @@ function AppContent() { > 🎤 - {isProcessing && (

CamTalk

{tr("app.title")}
- {isConnected && stats.queryCount > 0 && ( - - {stats.queryCount} {tr("app.stats.requests")} - - )} {isConnected ? tr("status.connected") : connectionStatus === "connecting" ? tr("status.connecting") : tr("status.disconnected")} @@ -191,6 +233,18 @@ function AppContent() {
+ {/* ---- 会话历史侧边栏(Overlay 抽屉式) ---- */} + setSidebarOpen((v) => !v)} + onNewSession={handleNewSession} + onSelectSession={handleSelectSession} + onDeleteSession={handleDeleteSession} + onRenameSession={renameSession} + /> + {showConfig && ( )} - {/* ---- 主体:侧边栏 + 视频 + 聊天 ---- */} + {/* ---- 主体:视频 + 聊天(两栏) ---- */}
- {/* 最左侧:会话历史侧边栏 */} - setSidebarCollapsed((v) => !v)} - onNewSession={handleNewSession} - onSelectSession={handleSelectSession} - onDeleteSession={handleDeleteSession} - onRenameSession={renameSession} - /> - - {/* 中间:视频预览 + 控制栏 */} + {/* 左侧:视频预览 + 控制栏 */}
@@ -228,8 +270,15 @@ function AppContent() { {config.detailLevel === "high" && (
HD
)} - {isObserving && ( -
{tr("video.observing")}
+ {/* AI 视觉状态指示 */} + {isConnected && visionMode !== "chat" && ( +
+ + + + + {isObserving ? tr("video.observing") : "AI"} +
)} {isSpeaking && (
{tr("video.listening")}
@@ -269,37 +318,110 @@ function AppContent() { )}
- {/* 视频下方控制栏 */} + {/* 视频下方控制区(三层结构) */}
{!isConnected ? ( - - ) : ( -
- - - {isProcessing && ( - - )} - -
+ + +
+ + ) : ( + <> + {/* 通话态:核心控制工具栏 */} +
+ + + {/* 识别画面按钮(按需模式下显示) */} + {visionMode === "ondemand" && ( + + )} + {isProcessing && ( + + )} + +
+ {/* 通话态模式切换 */} +
+ + + +
+ )}
@@ -308,9 +430,18 @@ function AppContent() {
{tr("chat.title")} - {isConnected && mode === "observation" && ( - {tr("chat.mode.observation")} - )} +
+ {isConnected && mode === "observation" && ( + {tr("chat.mode.observation")} + )} + {isConnected && stats.queryCount > 0 && ( + + {stats.queryCount} {tr("statusbar.recognitions")} + {stats.totalTokens > 0 && ` · ${stats.totalTokens.toLocaleString()} ${tr("statusbar.tokens")}`} + {` · ${formatTime(elapsed)}`} + + )} +
{connectionStatus === "disconnected" && messages.length > 0 && ( @@ -322,7 +453,11 @@ function AppContent() { messages={messages} currentReply={currentReply} connectionStatus={connectionStatus} + isMicOn={isMicOn} + isSpeaking={isSpeaking} onSendText={sendTextMessage} + onToggleMic={toggleMic} + onSceneCard={handleSceneCard} />
diff --git a/frontend/src/components/ChatPanel/index.tsx b/frontend/src/components/ChatPanel/index.tsx index 4ff7902..5be520a 100644 --- a/frontend/src/components/ChatPanel/index.tsx +++ b/frontend/src/components/ChatPanel/index.tsx @@ -1,6 +1,7 @@ // ============================================================ // ChatPanel — 消息展示面板 // 职责:渲染对话消息列表、流式光标、元数据、自动滚动、文本输入 +// 增强:空状态场景卡片、语音输入按钮 // ============================================================ import { useEffect, useRef, useState } from "react"; @@ -12,10 +13,33 @@ interface ChatPanelProps { messages: ChatMessage[]; currentReply?: string; connectionStatus: ConnectionStatus; + isMicOn?: boolean; + isSpeaking?: boolean; onSendText?: (text: string) => void; + onToggleMic?: () => void; + onSceneCard?: (prompt: string) => void; } -export function ChatPanel({ messages, currentReply, connectionStatus, onSendText }: ChatPanelProps) { +/** 场景卡片数据 */ +function getSceneCards(t: (key: string) => string) { + return [ + { icon: "👁", titleKey: "scene.describe", descKey: "scene.describe.desc", prompt: t("scene.describe") }, + { icon: "🔤", titleKey: "scene.text", descKey: "scene.text.desc", prompt: t("scene.text") }, + { icon: "📦", titleKey: "scene.object", descKey: "scene.object.desc", prompt: t("scene.object") }, + { icon: "💡", titleKey: "scene.suggest", descKey: "scene.suggest.desc", prompt: t("scene.suggest") }, + ]; +} + +export function ChatPanel({ + messages, + currentReply, + connectionStatus, + isMicOn, + isSpeaking, + onSendText, + onToggleMic, + onSceneCard, +}: ChatPanelProps) { const bottomRef = useRef(null); const containerRef = useRef(null); const isAutoScroll = useRef(true); @@ -23,6 +47,8 @@ export function ChatPanel({ messages, currentReply, connectionStatus, onSendText const { t } = useI18n(); const isConnected = connectionStatus === "connected"; + const isEmpty = messages.length === 0 && !currentReply; + const sceneCards = getSceneCards(t); // 用户上滚时暂停自动滚动,滚到底部时恢复 useEffect(() => { @@ -53,15 +79,41 @@ export function ChatPanel({ messages, currentReply, connectionStatus, onSendText setInputText(""); }; + // 点击场景卡片 + const handleSceneCard = (prompt: string) => { + if (onSceneCard) { + onSceneCard(prompt); + } else if (onSendText) { + onSendText(prompt); + } + }; + return (
- {/* 空状态提示 */} - {messages.length === 0 && !currentReply && ( + {/* 空状态:场景卡片 */} + {isEmpty && (
💬

{isConnected ? t("chat.welcome.prompt") : t("chat.empty.prompt")}

{isConnected ? t("chat.welcome.hint") : t("chat.empty.hint")} + + {/* 场景快捷卡片 */} +
+ {sceneCards.map((card) => ( + + ))} +
)} @@ -106,6 +158,17 @@ export function ChatPanel({ messages, currentReply, connectionStatus, onSendText onChange={(e) => setInputText(e.target.value)} disabled={connectionStatus === "connecting"} /> + {/* 语音输入按钮 */} + {onToggleMic && ( + + )} - -
- ); - } + const handleSelect = (id: string) => { + onSelectSession(id); + // 选择后自动收起侧边栏 + onToggle(); + }; + + // 过滤 + 分组 + const grouped = useMemo(() => { + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + const filtered = searchQuery.trim() + ? sessions.filter((s) => + s.title.toLowerCase().includes(searchQuery.toLowerCase()) || + s.preview.toLowerCase().includes(searchQuery.toLowerCase()) + ) + : sessions; + + const groups: Record = { + today: [], + yesterday: [], + earlier: [], + }; + + for (const session of filtered) { + const group = getTimeGroup(session.lastActiveAt, today, yesterday); + groups[group].push(session); + } + + return groups; + }, [sessions, searchQuery]); + + if (!open) return null; + + const groupLabels: Record = { + today: t("sidebar.today"), + yesterday: t("sidebar.yesterday"), + earlier: t("sidebar.earlier"), + }; return ( -
-
- - -
+ <> +
+
+ {/* 头部:新建 + 收起 */} +
+ + +
-
- {sessions.length === 0 ? ( -
{t("sidebar.empty")}
- ) : ( - sessions.map((session) => ( -
onSelectSession(session.id)} - > - {editingId === session.id ? ( -
e.stopPropagation()}> - setEditTitle(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") handleConfirmRename(); - if (e.key === "Escape") handleCancelRename(); - }} - autoFocus - /> - - -
- ) : ( - <> -
-
{session.title}
-
- {session.messageCount > 0 && ( - {session.messageCount}{t("sidebar.messages")} + {/* 搜索栏 */} +
+ setSearchQuery(e.target.value)} + /> +
+ + {/* 会话列表(按时间分组) */} +
+ {sessions.length === 0 ? ( +
{t("sidebar.empty")}
+ ) : ( + (["today", "yesterday", "earlier"] as TimeGroup[]).map((groupKey) => { + const items = grouped[groupKey]; + if (items.length === 0) return null; + return ( +
+
{groupLabels[groupKey]}
+ {items.map((session) => ( +
handleSelect(session.id)} + > + {editingId === session.id ? ( +
e.stopPropagation()}> + setEditTitle(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleConfirmRename(); + if (e.key === "Escape") handleCancelRename(); + }} + autoFocus + /> + + +
+ ) : ( + <> + {/* 视频标记图标 */} + {session.messageCount > 5 && ( + 📹 + )} +
+
{session.title}
+
+ {session.messageCount > 0 && ( + {session.messageCount}{t("sidebar.messages")} + )} + {formatRelativeTime(session.lastActiveAt)} +
+
+
+ + +
+ )} - {formatRelativeTime(session.lastActiveAt)}
-
-
- - -
- - )} -
- )) - )} + ))} +
+ ); + }) + )} +
-
+ ); } diff --git a/frontend/src/hooks/useVisionSession.ts b/frontend/src/hooks/useVisionSession.ts index 739afbf..497b996 100644 --- a/frontend/src/hooks/useVisionSession.ts +++ b/frontend/src/hooks/useVisionSession.ts @@ -482,5 +482,6 @@ export function useVisionSession() { toggleCamera, toggleMic, sendTextMessage, + captureFrame, }; } diff --git a/frontend/src/lib/i18n/en-US.ts b/frontend/src/lib/i18n/en-US.ts index eec5531..b24bcb4 100644 --- a/frontend/src/lib/i18n/en-US.ts +++ b/frontend/src/lib/i18n/en-US.ts @@ -89,6 +89,42 @@ export const enUS: TranslationMap = { "sidebar.messages": " messages", "sidebar.rename": "Rename", "sidebar.delete": "Delete", + "sidebar.search": "Search conversations...", + "sidebar.today": "Today", + "sidebar.yesterday": "Yesterday", + "sidebar.earlier": "Earlier", + "sidebar.pinned": "Pinned", + "sidebar.video": "Video", + "sidebar.clearHistory": "Clear History", + + // Scene cards (empty state) + "scene.describe": "Describe my surroundings", + "scene.describe.desc": "Let AI observe and describe the current scene", + "scene.text": "Recognize text in view", + "scene.text.desc": "Extract and translate text from the scene", + "scene.object": "Analyze this object", + "scene.object.desc": "Identify objects and provide information", + "scene.suggest": "Give me suggestions", + "scene.suggest.desc": "Practical suggestions based on the current scene", + + // Video controls (enhanced) + "controls.recognize": "Analyze Scene", + "controls.device.camera": "Camera", + "controls.device.mic": "Microphone", + "controls.device.default": "Default", + "controls.mode.realtime": "Realtime", + "controls.mode.ondemand": "On-demand", + "controls.mode.chat": "Chat only", + + // Status bar + "statusbar.ready": "Ready · Select devices to start", + "statusbar.calling": "In call", + "statusbar.duration": "Duration", + "statusbar.recognitions": "recognitions", + "statusbar.tokens": "Tokens", + + // Input enhanced + "chat.input.voice": "Voice input", // Device errors "error.vadInit": "VAD initialization failed", diff --git a/frontend/src/lib/i18n/ja-JP.ts b/frontend/src/lib/i18n/ja-JP.ts index 947973b..37bc1e8 100644 --- a/frontend/src/lib/i18n/ja-JP.ts +++ b/frontend/src/lib/i18n/ja-JP.ts @@ -89,6 +89,42 @@ export const jaJP: TranslationMap = { "sidebar.messages": "件のメッセージ", "sidebar.rename": "名前を変更", "sidebar.delete": "削除", + "sidebar.search": "会話を検索...", + "sidebar.today": "今日", + "sidebar.yesterday": "昨日", + "sidebar.earlier": "それ以前", + "sidebar.pinned": "ピン留め", + "sidebar.video": "ビデオ", + "sidebar.clearHistory": "履歴をクリア", + + // Scene cards (empty state) + "scene.describe": "周囲のシーンを説明して", + "scene.describe.desc": "AIが現在のシーンを観察して説明します", + "scene.text": "画面のテキストを認識", + "scene.text.desc": "シーン内のテキストを抽出・翻訳します", + "scene.object": "この物を分析して", + "scene.object.desc": "物体を識別して関連情報を提供します", + "scene.suggest": "アドバイスをください", + "scene.suggest.desc": "現在のシーンに基づいた実用的な提案", + + // Video controls (enhanced) + "controls.recognize": "シーンを分析", + "controls.device.camera": "カメラ", + "controls.device.mic": "マイク", + "controls.device.default": "デフォルト", + "controls.mode.realtime": "リアルタイム", + "controls.mode.ondemand": "オンデマンド", + "controls.mode.chat": "チャットのみ", + + // Status bar + "statusbar.ready": "準備完了 · デバイスを選択して開始", + "statusbar.calling": "通話中", + "statusbar.duration": "通話時間", + "statusbar.recognitions": "回の認識", + "statusbar.tokens": "トークン", + + // Input enhanced + "chat.input.voice": "音声入力", // Device errors "error.vadInit": "VAD初期化に失敗しました", diff --git a/frontend/src/lib/i18n/zh-CN.ts b/frontend/src/lib/i18n/zh-CN.ts index d085cde..1f49ae0 100644 --- a/frontend/src/lib/i18n/zh-CN.ts +++ b/frontend/src/lib/i18n/zh-CN.ts @@ -89,6 +89,42 @@ export const zhCN: TranslationMap = { "sidebar.messages": "条消息", "sidebar.rename": "重命名", "sidebar.delete": "删除", + "sidebar.search": "搜索对话...", + "sidebar.today": "今天", + "sidebar.yesterday": "昨天", + "sidebar.earlier": "更早", + "sidebar.pinned": "置顶", + "sidebar.video": "视频", + "sidebar.clearHistory": "清除历史", + + // Scene cards (empty state) + "scene.describe": "描述我面前的场景", + "scene.describe.desc": "让 AI 观察并描述当前画面", + "scene.text": "识别画面中的文字", + "scene.text.desc": "提取并翻译画面里的文字", + "scene.object": "帮我分析这个物品", + "scene.object.desc": "识别物体并给出相关信息", + "scene.suggest": "给我一些建议", + "scene.suggest.desc": "基于当前场景给出实用建议", + + // Video controls (enhanced) + "controls.recognize": "识别画面", + "controls.device.camera": "摄像头", + "controls.device.mic": "麦克风", + "controls.device.default": "默认", + "controls.mode.realtime": "实时分析", + "controls.mode.ondemand": "按需识别", + "controls.mode.chat": "纯聊天", + + // Status bar + "statusbar.ready": "就绪 · 选择设备后开始通话", + "statusbar.calling": "通话中", + "statusbar.duration": "通话时长", + "statusbar.recognitions": "次识别", + "statusbar.tokens": "Token", + + // Input enhanced + "chat.input.voice": "语音输入", // Device errors "error.vadInit": "VAD 初始化失败",