From e37dc7074d37407e18c654b51ba6b2616ddef3ee Mon Sep 17 00:00:00 2001 From: cfy666 <3087823110@qq.com> Date: Sat, 20 Jun 2026 21:12:06 +0800 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20=E9=A6=96=E9=A1=B5=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E4=BB=93=E5=BA=93=E9=93=BE=E6=8E=A5=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=20Gitea=20=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/LandingPage/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/LandingPage/index.tsx b/frontend/src/components/LandingPage/index.tsx index d305a8c..9a94714 100644 --- a/frontend/src/components/LandingPage/index.tsx +++ b/frontend/src/components/LandingPage/index.tsx @@ -92,8 +92,8 @@ export function LandingPage() { 路演视频 - - GitHub + + Gitea @@ -367,8 +367,8 @@ export function LandingPage() { 路演视频 - - GitHub + + Gitea -- 2.49.1 From ed95ce56a8059cc3bb141a8c17ad6a0eae4a8b5b Mon Sep 17 00:00:00 2001 From: cfy666 <3087823110@qq.com> Date: Sat, 20 Jun 2026 22:21:03 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E5=85=B3=E9=97=AD=E6=91=84=E5=83=8F?= =?UTF-8?q?=E5=A4=B4=E5=90=8E=E6=96=87=E6=9C=AC=E8=BE=93=E5=85=A5=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E5=8F=91=E9=80=81=E5=9B=BE=E5=83=8F=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复了用户关闭摄像头后,使用文本输入时 AI 仍会分析黑色画面的问题。 变更: - sendTextMessage: 只在摄像头开启时捕获画面 - 待发消息队列: 根据摄像头状态决定是否携带画面 效果: - 摄像头关闭时纯文本对话,不提及画面 - 节省 token 消耗和网络带宽 Co-Authored-By: Claude Opus 4.8 --- docs/fix-text-input-camera-issue.md | 135 +++++++++++++++++++++++++ frontend/src/hooks/useVisionSession.ts | 8 +- 2 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 docs/fix-text-input-camera-issue.md diff --git a/docs/fix-text-input-camera-issue.md b/docs/fix-text-input-camera-issue.md new file mode 100644 index 0000000..f613063 --- /dev/null +++ b/docs/fix-text-input-camera-issue.md @@ -0,0 +1,135 @@ +# 修复文本输入时仍调用摄像头的问题 + +## 问题描述 + +当用户关闭视频对话,改用文本输入聊天时,AI 仍然会说"画面是黑色的",然后才回答问题。这是因为即使摄像头关闭,前端仍然发送了图像数据(黑屏)给后端。 + +## 问题根源 + +### 前端问题 +在 `frontend/src/hooks/useVisionSession.ts` 中: + +1. **sendTextMessage 函数**(第 441 行):无论摄像头是否开启,都会调用 `captureFrame()` 捕获画面 +2. **待发消息队列处理**(第 140 行):连接成功后 flush 待发消息时,也会无条件捕获画面 + +```typescript +// 问题代码 +const frame = captureFrame(); // 即使摄像头关闭也会捕获(返回黑屏) +send({ + type: "query", + image: frame ? dataUrlToBase64(frame) : "", // 发送黑屏数据 + text: text.trim(), +}); +``` + +### 后端行为 +在 `backend/internal/eino/nodes_history.go` 中: + +- 第 68-86 行:只要收到图像数据(`len(imageData) > 0`),就会构建多模态消息发送给 LLM +- LLM 会分析这个黑色画面,导致回复中提到"画面是黑色的" + +## 解决方案 + +### 修改内容 + +修改了两处代码,都在 `frontend/src/hooks/useVisionSession.ts`: + +#### 1. sendTextMessage 函数(第 421-462 行) + +```typescript +// 修改前 +const frame = captureFrame(); + +// 修改后 +// 只在摄像头开启时才捕获画面,避免发送黑屏给 AI +const frame = isCameraOn ? captureFrame() : null; +``` + +并在依赖数组中添加了 `isCameraOn`: +```typescript +[captureFrame, send, connect, accessToken, isCameraOn] +``` + +#### 2. 待发消息队列处理(第 124-156 行) + +```typescript +// 修改前 +const frame = captureFrame(); + +// 修改后 +// 只在摄像头开启时才捕获画面,避免发送黑屏给 AI +const frame = isCameraOn ? captureFrame() : null; +``` + +### 效果 + +修改后: +- ✅ 摄像头**开启**时,文本输入仍然会携带当前画面(多模态对话) +- ✅ 摄像头**关闭**时,文本输入只发送文字(纯文本对话) +- ✅ AI 不会再说"画面是黑色的" +- ✅ 节省 token 消耗(不发送无用的图像数据) + +## 测试场景 + +1. **开启视频对话 → 关闭摄像头 → 文本输入聊天** + - 预期:AI 只回答文本问题,不提及画面 + +2. **直接文本输入(未开启摄像头)** + - 预期:纯文本对话,不发送图像数据 + +3. **开启摄像头 → 文本输入** + - 预期:AI 可以看到画面并结合文本回答 + +4. **未连接时文本输入 → 自动连接** + - 预期:待发消息在连接成功后发送,根据当时的摄像头状态决定是否携带画面 + +## 替代方案(未采用) + +### 方案二:后端过滤空图像 +在后端 `nodes_history.go` 中增加图像有效性检测: + +```go +// 检测图像是否有效(非全黑/全白) +func isValidImage(data []byte) bool { + // 检测文件大小(黑屏图像通常很小) + if len(data) < 1024 { // 小于 1KB + return false + } + // 可以进一步检测像素方差等 + return true +} + +// 在 NewHistoryLambda 中 +if len(imageData) > 0 && isValidImage(imageData) { + // 构建多模态消息 +} else { + // 纯文本消息 +} +``` + +**未采用原因**: +- 前端方案更直接,从源头解决问题 +- 避免后端做额外的图像检测(性能开销) +- 节省网络传输(不发送无用的图像数据) + +## 相关文件 + +- `frontend/src/hooks/useVisionSession.ts` - 核心会话逻辑 +- `backend/internal/eino/nodes_history.go` - 后端历史消息组装 +- `docs/02-接口文档.md` - WebSocket 协议定义 + +## 提交信息 + +``` +fix: 关闭摄像头后文本输入不再发送图像数据 + +修复了用户关闭摄像头后,使用文本输入时 AI 仍会分析黑色画面的问题。 + +变更: +- sendTextMessage: 只在摄像头开启时捕获画面 +- 待发消息队列: 根据摄像头状态决定是否携带画面 + +效果: +- 摄像头关闭时纯文本对话,不提及画面 +- 节省 token 消耗和网络带宽 +``` diff --git a/frontend/src/hooks/useVisionSession.ts b/frontend/src/hooks/useVisionSession.ts index 4177345..53f8ab2 100644 --- a/frontend/src/hooks/useVisionSession.ts +++ b/frontend/src/hooks/useVisionSession.ts @@ -137,7 +137,8 @@ export function useVisionSession(accessToken?: string | null, conversationId?: s const pending = pendingMessagesRef.current; pendingMessagesRef.current = []; for (const msg of pending) { - const frame = captureFrame(); + // 只在摄像头开启时才捕获画面,避免发送黑屏给 AI + const frame = isCameraOn ? captureFrame() : null; send({ type: "query", request_id: msg.requestId, @@ -438,7 +439,8 @@ export function useVisionSession(accessToken?: string | null, conversationId?: s } // 已连接:直接发送 - const frame = captureFrame(); + // 只在摄像头开启时才捕获画面,避免发送黑屏给 AI + const frame = isCameraOn ? captureFrame() : null; send({ type: "query", request_id: requestId, @@ -458,7 +460,7 @@ export function useVisionSession(accessToken?: string | null, conversationId?: s setIsProcessing(true); }, - [captureFrame, send, connect, accessToken], + [captureFrame, send, connect, accessToken, isCameraOn], ); return { -- 2.49.1 From 2fb23852ef1369edc208fa2b09129b95466186ff Mon Sep 17 00:00:00 2001 From: cfy666 <3087823110@qq.com> Date: Sat, 20 Jun 2026 23:24:32 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=83=85?= =?UTF-8?q?=E6=99=AF=E5=88=87=E6=8D=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/ai/llm/scenarios.go | 81 ++- backend/internal/eino/adapter.go | 12 +- backend/internal/ws/handler.go | 43 +- docs/fix-text-input-camera-issue.md | 135 ---- docs/情景切换功能完整文档.md | 664 ++++++++++++++++++++ frontend/src/App.css | 48 ++ frontend/src/components/ChatPanel/index.tsx | 15 + frontend/src/hooks/useVisionSession.ts | 2 + frontend/src/lib/i18n/en-US.ts | 5 + frontend/src/lib/i18n/ja-JP.ts | 5 + frontend/src/lib/i18n/zh-CN.ts | 5 + 11 files changed, 872 insertions(+), 143 deletions(-) delete mode 100644 docs/fix-text-input-camera-issue.md create mode 100644 docs/情景切换功能完整文档.md diff --git a/backend/internal/ai/llm/scenarios.go b/backend/internal/ai/llm/scenarios.go index d11252a..5cb0024 100644 --- a/backend/internal/ai/llm/scenarios.go +++ b/backend/internal/ai/llm/scenarios.go @@ -2,35 +2,84 @@ package llm import "strings" -// scenarioPrompt 定义单个情景的多语言 system prompt。 +// scenarioPrompt 定义单个情景的多语言 system prompt 和首句引导。 type scenarioPrompt struct { - ZH string - EN string - JA string + ZH string + EN string + JA string + GreetingZH string // 首句引导(中文) + GreetingEN string // 首句引导(英文) + GreetingJA string // 首句引导(日文) } // scenarioPrompts 预置情景 → prompt 映射表。 // key 为情景 ID(与前端 Scenario.id 对齐)。 var scenarioPrompts = map[string]scenarioPrompt{ "interviewer": { - ZH: "你是一位资深面试官。你通过摄像头观察面试者,并根据他们的背景和表现提出面试问题。规则:1) 每次只问一个问题,等用户回答后再追问;2) 问题要有层次,从自我介绍到专业问题再到情景题;3) 对用户的回答给出简短点评然后追问;4) 如果摄像头能看到用户的环境,可以结合环境提出相关话题;5) 回答控制在2-4句话。", + ZH: `你是一位资深面试官。你通过摄像头观察面试者,并根据他们的背景和表现提出面试问题。 + +【角色定位】 +- 你是面试官,不是助手或顾问 +- 你的目标是评估候选人的能力 +- 保持专业、客观、礼貌 + +【交互规则】 +1. 每次只问一个问题,等用户回答后再追问 +2. 问题要有层次:自我介绍 → 专业问题 → 情景题 → 反问环节 +3. 对用户的回答给出简短点评(优点+不足),然后追问 +4. 如果摄像头能看到用户的环境,可以结合环境提出相关话题 +5. 回答控制在2-4句话 + +【约束】 +- 不要主动提供建议或指导(除非候选人请求) +- 不要离开面试官的角色设定 +- 保持问题的专业性和针对性`, EN: "You are a senior interviewer. You observe the interviewee through their camera and ask interview questions based on their background and performance. Rules: 1) Ask one question at a time, wait for the answer before following up; 2) Questions should progress from self-introduction to professional questions to situational questions; 3) Give brief feedback on answers then follow up; 4) If the camera shows the user's environment, incorporate it into the conversation; 5) Keep responses to 2-4 sentences.", JA: "あなたはベテラン面接官です。カメラで面接者を見て、バックグラウンドと実績に基づいて面接質問をします。ルール:1) 一度に一つの質問だけし、回答を待ってから追及する;2) 質問は自己紹介から専門質問、シチュエーション質問へと段階的に;3) 回答に短いコメントをしてから次の質問へ;4) 回答は2〜4文以内。", + GreetingZH: "你好!我是今天的面试官。让我们先从自我介绍开始,请简单介绍一下你自己和你应聘的岗位。", + GreetingEN: "Hello! I'm your interviewer today. Let's start with a self-introduction. Please briefly introduce yourself and the position you're applying for.", + GreetingJA: "こんにちは!本日の面接官です。まず自己紹介から始めましょう。あなた自身と応募職種について簡単に教えてください。", }, "english_teacher": { ZH: "You are a friendly and patient English tutor. Speak in English with the user. Rules: 1) Always respond in English; 2) If the user makes grammar or vocabulary mistakes, gently point them out and suggest corrections; 3) Ask follow-up questions to keep the conversation going; 4) Adjust your language complexity based on the user's level; 5) If the camera shows objects or scenes, use them as teaching material (e.g., 'I can see a bookshelf behind you. What's your favorite book?'); 6) Keep responses to 3-5 sentences.", EN: "You are a friendly and patient English tutor. Speak in English with the user. Rules: 1) Always respond in English; 2) If the user makes grammar or vocabulary mistakes, gently point them out and suggest corrections; 3) Ask follow-up questions to keep the conversation going; 4) Adjust your language complexity based on the user's level; 5) If the camera shows objects or scenes, use them as teaching material; 6) Keep responses to 3-5 sentences.", JA: "You are a friendly and patient English tutor. Speak in English with the user. Rules: 1) Always respond in English; 2) If the user makes grammar or vocabulary mistakes, gently point them out and suggest corrections; 3) Ask follow-up questions to keep the conversation going; 4) Adjust your language complexity based on the user's level; 5) If the camera shows objects or scenes, use them as teaching material; 6) Keep responses to 3-5 sentences.", + GreetingZH: "Hi! I'm your English tutor. Let's practice English together! What would you like to talk about today?", + GreetingEN: "Hi! I'm your English tutor. Let's practice English together! What would you like to talk about today?", + GreetingJA: "Hi! I'm your English tutor. Let's practice English together! What would you like to talk about today?", }, "debate": { - ZH: "你是一位辩论赛对手。用户提出一个观点,你需要站在反方进行反驳。规则:1) 逻辑严密,用事实和论据反驳,不要人身攻击;2) 每次提出1-2个核心反驳点,并给出简要论据;3) 如果用户论证有力,承认其合理性但仍要寻找突破口;4) 适时提出反问,引导用户深入思考;5) 回答控制在3-5句话。", + ZH: `你是一位辩论赛对手。用户提出一个观点,你需要站在反方进行反驳。 + +【角色定位】 +- 你是辩论对手,不是评委或顾问 +- 你的目标是通过逻辑论证反驳对方观点 +- 保持理性、严谨、尊重对手 + +【交互规则】 +1. 逻辑严密,用事实和论据反驳,不要人身攻击 +2. 每次提出1-2个核心反驳点,并给出简要论据 +3. 如果用户论证有力,承认其合理性但仍要寻找突破口 +4. 适时提出反问,引导用户深入思考 +5. 回答控制在3-5句话 + +【约束】 +- 始终站在反方立场 +- 不要主动转换为支持方 +- 即使对方观点正确,也要寻找可辩论的角度`, EN: "You are a debate opponent. The user presents a viewpoint, and you argue against it. Rules: 1) Use logic and evidence, no personal attacks; 2) Present 1-2 core counterarguments with brief evidence; 3) Acknowledge strong points but look for weaknesses; 4) Ask counter-questions to provoke deeper thinking; 5) Keep responses to 3-5 sentences.", JA: "あなたはディベートの相手です。ユーザーが提示した观点に対して反論します。ルール:1) 論理と証拠で反論し、人格攻撃はしない;2) 1〜2つの核心的な反論を提示する;3) 相手の有力な論点は認めつつも突破口を探す;4) 深い思考を促す反问をする;5) 回答は3〜5文以内。", + GreetingZH: "你好!我是你的辩论对手。请提出一个你坚信的观点,我会站在反方立场与你辩论,帮你锻炼逻辑思维。", + GreetingEN: "Hello! I'm your debate opponent. Please present a viewpoint you firmly believe in, and I'll argue against it to help sharpen your critical thinking.", + GreetingJA: "こんにちは!あなたのディベート相手です。あなたが信じる观点を提示してください。反対の立場から論じて、論理的思考を鍛えます。", }, "interpreter": { ZH: "你是一名同声翻译员。将用户说的话实时翻译为目标语言。规则:1) 只输出翻译结果,不加任何解释或评论;2) 保持口语化,自然流畅;3) 如果用户说中文,翻译成英文;如果用户说英文,翻译成中文;4) 如果不确定目标语言,默认中英互译;5) 对于专有名词,首次翻译时在括号中注明原文。", EN: "You are a simultaneous interpreter. Translate what the user says in real-time. Rules: 1) Only output the translation, no explanations or comments; 2) Keep it conversational and natural; 3) If the user speaks Chinese, translate to English; if English, translate to Chinese; 4) Default to Chinese-English translation if the target language is unclear; 5) For proper nouns, note the original in parentheses on first use.", JA: "あなたは同時通訳者です。ユーザーの発言をリアルタイムで翻訳します。ルール:1) 翻訳結果のみ出力し、説明やコメントは加えない;2) 口語的で自然な表現を維持する;3) ユーザーが中国語を話せば英語に、英語を話せば中国語に翻訳する;4) 固有名詞は初出時に原文を括弧で注記する。", + GreetingZH: "我是你的同声翻译。请开始说话,我会实时将中文翻译成英文,或将英文翻译成中文。", + GreetingEN: "I'm your simultaneous interpreter. Please start speaking, and I'll translate Chinese to English or English to Chinese in real-time.", + GreetingJA: "私はあなたの同時通訳者です。お話しください。中国語を英語に、または英語を中国語にリアルタイムで翻訳します。", }, } @@ -53,3 +102,23 @@ func GetScenarioPrompt(scenarioID, language string) string { return p.EN } } + +// GetScenarioGreeting 根据情景 ID 和语言获取对应的首句引导。 +// 返回空字符串表示无此情景或不需要引导(自由对话)。 +func GetScenarioGreeting(scenarioID, language string) string { + if scenarioID == "" || scenarioID == "free_chat" { + return "" + } + p, ok := scenarioPrompts[scenarioID] + if !ok { + return "" + } + switch { + case strings.HasPrefix(language, "zh"): + return p.GreetingZH + case strings.HasPrefix(language, "ja"): + return p.GreetingJA + default: + return p.GreetingEN + } +} diff --git a/backend/internal/eino/adapter.go b/backend/internal/eino/adapter.go index 2896b75..c66b8fc 100644 --- a/backend/internal/eino/adapter.go +++ b/backend/internal/eino/adapter.go @@ -109,7 +109,17 @@ func (e *EinoOrchestrator) ProcessQuery( ctx = WithRequestID(ctx, req.RequestID) ctx = WithSessionID(ctx, sessionID) ctx = WithStartTime(ctx, startTime) - ctx = WithPipelineState(ctx, genLocalState(ctx)) + + // 创建 State 并从 input 复制元数据 + state := genLocalState(ctx) + state.SessionID = input.SessionID + state.RequestID = input.RequestID + state.ImageData = input.ImageData + state.Scenario = input.Scenario + state.Language = input.Language + state.DetailLevel = sess.Config.DetailLevel + state.TTSEnabled = input.TTSEnabled + ctx = WithPipelineState(ctx, state) // 6. 调用 Graph(Stream 模式 + 运行时 Callback) streamReader, err := e.graph.Runnable.Stream(ctx, input, e.callbacks) diff --git a/backend/internal/ws/handler.go b/backend/internal/ws/handler.go index 0339452..5236b10 100644 --- a/backend/internal/ws/handler.go +++ b/backend/internal/ws/handler.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" "github.com/gorilla/websocket" + "github.com/hhs/camtalk/internal/ai/llm" "github.com/hhs/camtalk/internal/auth" "github.com/hhs/camtalk/internal/config" "github.com/hhs/camtalk/internal/errors" @@ -277,7 +278,47 @@ func serveWS(c *gin.Context, sessionMgr session.Manager, orch orchestrator.Orche errors.SendWSError(client, errors.CodeInternalError, "", err) continue } - logger.Log.Infow("config updated", "session", sessionID) + + scenarioID := "" + if msg.Payload.Scenario != nil { + scenarioID = *msg.Payload.Scenario + } + logger.Log.Infow("config updated", "session", sessionID, "scenario", scenarioID) + + // 如果切换了情景(非自由对话),返回首句引导 + if scenarioID != "" && scenarioID != "free_chat" { + sess, err := client.sessionMgr.Get(context.Background(), sessionID) + if err == nil && sess != nil { + greeting := llm.GetScenarioGreeting(scenarioID, sess.Config.Language) + if greeting != "" { + // 发送首句作为 AI 消息 + _ = client.SendJSON(models.WsLLMChunk{ + Type: "llm_chunk", + RequestID: "scenario_greeting", + Delta: greeting, + Role: "assistant", + }) + + doneMsg := models.WsLLMDone{ + Type: "llm_done", + RequestID: "scenario_greeting", + FullText: greeting, + Model: "", + LatencyMs: 0, + } + doneMsg.TokensUsed.Prompt = 0 + doneMsg.TokensUsed.Completion = 0 + doneMsg.TokensUsed.Total = 0 + _ = client.SendJSON(doneMsg) + + // 追加首句到历史记录 + _ = client.sessionMgr.AppendMessage(context.Background(), sessionID, models.Message{ + Role: "assistant", + Content: greeting, + }) + } + } + } case "interrupt": logger.Log.Infow("interrupt received", "session", sessionID) diff --git a/docs/fix-text-input-camera-issue.md b/docs/fix-text-input-camera-issue.md deleted file mode 100644 index f613063..0000000 --- a/docs/fix-text-input-camera-issue.md +++ /dev/null @@ -1,135 +0,0 @@ -# 修复文本输入时仍调用摄像头的问题 - -## 问题描述 - -当用户关闭视频对话,改用文本输入聊天时,AI 仍然会说"画面是黑色的",然后才回答问题。这是因为即使摄像头关闭,前端仍然发送了图像数据(黑屏)给后端。 - -## 问题根源 - -### 前端问题 -在 `frontend/src/hooks/useVisionSession.ts` 中: - -1. **sendTextMessage 函数**(第 441 行):无论摄像头是否开启,都会调用 `captureFrame()` 捕获画面 -2. **待发消息队列处理**(第 140 行):连接成功后 flush 待发消息时,也会无条件捕获画面 - -```typescript -// 问题代码 -const frame = captureFrame(); // 即使摄像头关闭也会捕获(返回黑屏) -send({ - type: "query", - image: frame ? dataUrlToBase64(frame) : "", // 发送黑屏数据 - text: text.trim(), -}); -``` - -### 后端行为 -在 `backend/internal/eino/nodes_history.go` 中: - -- 第 68-86 行:只要收到图像数据(`len(imageData) > 0`),就会构建多模态消息发送给 LLM -- LLM 会分析这个黑色画面,导致回复中提到"画面是黑色的" - -## 解决方案 - -### 修改内容 - -修改了两处代码,都在 `frontend/src/hooks/useVisionSession.ts`: - -#### 1. sendTextMessage 函数(第 421-462 行) - -```typescript -// 修改前 -const frame = captureFrame(); - -// 修改后 -// 只在摄像头开启时才捕获画面,避免发送黑屏给 AI -const frame = isCameraOn ? captureFrame() : null; -``` - -并在依赖数组中添加了 `isCameraOn`: -```typescript -[captureFrame, send, connect, accessToken, isCameraOn] -``` - -#### 2. 待发消息队列处理(第 124-156 行) - -```typescript -// 修改前 -const frame = captureFrame(); - -// 修改后 -// 只在摄像头开启时才捕获画面,避免发送黑屏给 AI -const frame = isCameraOn ? captureFrame() : null; -``` - -### 效果 - -修改后: -- ✅ 摄像头**开启**时,文本输入仍然会携带当前画面(多模态对话) -- ✅ 摄像头**关闭**时,文本输入只发送文字(纯文本对话) -- ✅ AI 不会再说"画面是黑色的" -- ✅ 节省 token 消耗(不发送无用的图像数据) - -## 测试场景 - -1. **开启视频对话 → 关闭摄像头 → 文本输入聊天** - - 预期:AI 只回答文本问题,不提及画面 - -2. **直接文本输入(未开启摄像头)** - - 预期:纯文本对话,不发送图像数据 - -3. **开启摄像头 → 文本输入** - - 预期:AI 可以看到画面并结合文本回答 - -4. **未连接时文本输入 → 自动连接** - - 预期:待发消息在连接成功后发送,根据当时的摄像头状态决定是否携带画面 - -## 替代方案(未采用) - -### 方案二:后端过滤空图像 -在后端 `nodes_history.go` 中增加图像有效性检测: - -```go -// 检测图像是否有效(非全黑/全白) -func isValidImage(data []byte) bool { - // 检测文件大小(黑屏图像通常很小) - if len(data) < 1024 { // 小于 1KB - return false - } - // 可以进一步检测像素方差等 - return true -} - -// 在 NewHistoryLambda 中 -if len(imageData) > 0 && isValidImage(imageData) { - // 构建多模态消息 -} else { - // 纯文本消息 -} -``` - -**未采用原因**: -- 前端方案更直接,从源头解决问题 -- 避免后端做额外的图像检测(性能开销) -- 节省网络传输(不发送无用的图像数据) - -## 相关文件 - -- `frontend/src/hooks/useVisionSession.ts` - 核心会话逻辑 -- `backend/internal/eino/nodes_history.go` - 后端历史消息组装 -- `docs/02-接口文档.md` - WebSocket 协议定义 - -## 提交信息 - -``` -fix: 关闭摄像头后文本输入不再发送图像数据 - -修复了用户关闭摄像头后,使用文本输入时 AI 仍会分析黑色画面的问题。 - -变更: -- sendTextMessage: 只在摄像头开启时捕获画面 -- 待发消息队列: 根据摄像头状态决定是否携带画面 - -效果: -- 摄像头关闭时纯文本对话,不提及画面 -- 节省 token 消耗和网络带宽 -``` diff --git a/docs/情景切换功能完整文档.md b/docs/情景切换功能完整文档.md new file mode 100644 index 0000000..f031465 --- /dev/null +++ b/docs/情景切换功能完整文档.md @@ -0,0 +1,664 @@ +# 情景切换功能实现与修复完整文档 + +**项目**: CamTalk 多模态实时 AI 视觉对话助手 +**功能**: 情景切换(模拟面试官、英语老师、辩论对手、同声翻译) +**日期**: 2026-06-20 +**状态**: ✅ 已完成并修复 + +--- + +## 目录 + +1. [功能概述](#功能概述) +2. [实施内容](#实施内容) +3. [Bug 修复记录](#bug-修复记录) +4. [测试验证](#测试验证) +5. [部署指南](#部署指南) +6. [技术细节](#技术细节) +7. [后续优化建议](#后续优化建议) + +--- + +## 功能概述 + +### 什么是情景切换? + +情景切换功能允许用户选择不同的对话场景,AI 会根据选择的情景扮演不同的角色: + +| 情景 | AI 角色 | 主要功能 | +|------|---------|---------| +| 🎯 模拟面试官 | 资深面试官 | 提出面试问题,评估候选人能力,给出反馈 | +| 📚 英语老师 | 英语外教 | 全英文对话,纠正语法错误,引导深入交流 | +| ⚔️ 辩论对手 | 辩论选手 | 站在反方立场,用逻辑和证据反驳观点 | +| 🌐 同声翻译 | 翻译员 | 实时中英互译,口语化翻译,无额外解释 | +| 💬 自由对话 | 视觉助手 | 通用视觉对话助手(默认) | + +### 核心功能 + +1. **情景首句引导**:切换情景后,AI 自动发送第一句话引导用户进入角色 +2. **情景提示卡片**:对话顶部显示当前情景模式的蓝色提示卡片 +3. **增强 System Prompt**:每个情景有详细的角色定位、交互规则和约束 +4. **多语言支持**:完整支持中文、英文、日文界面 + +--- + +## 实施内容 + +### 后端实现 + +#### 1. 情景 Prompt 定义 + +**文件**: `backend/internal/ai/llm/scenarios.go` + +**变更内容**: +- 扩展 `scenarioPrompt` 结构体,新增首句引导字段(GreetingZH/EN/JA) +- 增强所有情景的 System Prompt(添加角色定位、交互规则、约束) +- 新增函数 `GetScenarioGreeting(scenarioID, language string) string` + +**示例 Prompt**(面试官): +```go +"interviewer": { + ZH: `你是一位资深面试官。你通过摄像头观察面试者... + +【角色定位】 +- 你是面试官,不是助手或顾问 +- 你的目标是评估候选人的能力 +- 保持专业、客观、礼貌 + +【交互规则】 +1. 每次只问一个问题,等用户回答后再追问 +2. 问题要有层次:自我介绍 → 专业问题 → 情景题 +3. 对用户的回答给出简短点评,然后追问 +...`, + GreetingZH: "你好!我是今天的面试官。让我们先从自我介绍开始...", +} +``` + +#### 2. 首句引导推送 + +**文件**: `backend/internal/ws/handler.go` + +**变更内容**: +在处理 `config` 消息时,如果切换到非自由对话情景,自动返回首句引导: + +```go +case "config": + // ... 更新配置 ... + + // 如果切换了情景(非自由对话),返回首句引导 + if scenarioID != "" && scenarioID != "free_chat" { + greeting := llm.GetScenarioGreeting(scenarioID, sess.Config.Language) + if greeting != "" { + // 发送 llm_chunk 和 llm_done 消息 + // 追加到历史记录 + } + } +``` + +**效果**:用户切换情景后,AI 立即自动说出首句,无需等待用户发送消息。 + +#### 3. State 初始化修复(关键 Bug 修复) + +**文件**: `backend/internal/eino/adapter.go` + +**问题**:`genLocalState()` 创建的是空 State,所有字段都是零值,导致 `state.Scenario = ""` + +**修复**: +```go +// ✅ 修复:从 input 复制元数据到 state +state := genLocalState(ctx) +state.SessionID = input.SessionID +state.RequestID = input.RequestID +state.ImageData = input.ImageData +state.Scenario = input.Scenario // ⬅️ 关键修复 +state.Language = input.Language +state.DetailLevel = sess.Config.DetailLevel +state.TTSEnabled = input.TTSEnabled +ctx = WithPipelineState(ctx, state) +``` + +--- + +### 前端实现 + +#### 1. 情景提示卡片 + +**文件**: `frontend/src/components/ChatPanel/index.tsx` + +**变更内容**: +在对话列表顶部(非空状态 + 非自由对话模式)添加情景提示卡片: + +```tsx +{messages.length > 0 && !isFreeChat && ( +
+
+ + {scenarios.find(s => s.id === activeScenario)?.icon} + +
+ {t(scenarios.find(s => s.id === activeScenario)?.nameKey || "")} +

{t(`scenario.${activeScenario}.hint`)}

+
+
+
+)} +``` + +**显示效果**: +- 蓝色渐变背景(135deg 从蓝到紫) +- 左侧大图标 + 右侧标题和说明 +- 最大宽度 520px,响应式布局 +- 柔和阴影和半透明边框 + +#### 2. WebSocket 消息修复(关键 Bug 修复) + +**文件**: `frontend/src/hooks/useVisionSession.ts` + +**问题**:发送 config 消息时缺少 `scenario` 字段,导致后端无法接收到情景切换信息 + +**修复位置 1**(连接成功时发送初始配置): +```typescript +// ✅ 修复:添加 scenario 字段 +send({ + type: "config", + payload: { + tts_enabled: config.ttsEnabled, + detail_level: config.detailLevel, + language: config.language, + scenario: config.scenario, // ⬅️ 关键修复 + }, +}); +``` + +**修复位置 2**(updateConfig 函数): +```typescript +// ✅ 修复:添加 scenario 字段 +send({ + type: "config", + payload: { + tts_enabled: next.ttsEnabled, + detail_level: next.detailLevel, + language: next.language, + scenario: next.scenario, // ⬅️ 关键修复 + }, +}); +``` + +#### 3. 样式实现 + +**文件**: `frontend/src/App.css` + +新增情景提示卡片样式: +```css +.scenario-hint-card { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border-radius: var(--radius-sm); + background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(99, 102, 241, 0.08) 100%); + border: 1px solid rgba(59, 130, 246, 0.2); + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06); +} +``` + +#### 4. 多语言翻译 + +**文件**: `frontend/src/lib/i18n/{zh-CN,en-US,ja-JP}.ts` + +新增翻译 key: +```typescript +"scenario.interviewer.hint": "AI 会扮演面试官,逐步提出专业问题并点评你的回答", +"scenario.englishTeacher.hint": "AI 会用英语对话,纠正语法错误并引导深入交流", +"scenario.debate.hint": "AI 会站在反方立场,用逻辑和证据反驳你的观点", +"scenario.interpreter.hint": "AI 会实时翻译你的话(中英互译),无解释评论", +``` + +--- + +## Bug 修复记录 + +### Bug #1:后端 State 未初始化 Scenario + +**严重性**: 🔴 Critical(核心功能完全失效) + +**症状**: +- 切换到任何情景后,AI 仍使用默认通用助手 Prompt +- AI 回答:"我是通义千问,阿里巴巴集团研发的超大规模语言模型..." +- 完全不遵循情景角色设定 + +**根因**: +`backend/internal/eino/adapter.go` 中,`genLocalState()` 创建的是空 State: +```go +❌ ctx = WithPipelineState(ctx, genLocalState(ctx)) +``` + +导致 `state.Scenario = ""`(空字符串),`nodes_history.go` 读取到空值后使用默认 Prompt。 + +**数据流分析**: +``` +input.Scenario = "interviewer" + ↓ +❌ state.Scenario = "" (未初始化!) + ↓ +nodes_history.go 读取到 "" + ↓ +llm.GetScenarioPrompt("", "zh-CN") 返回 "" + ↓ +使用默认 Prompt → AI 回答 "我是通义千问..." +``` + +**修复**: +从 `PipelineInput` 复制元数据到 `PipelineState`: +```go +✅ state := genLocalState(ctx) + state.Scenario = input.Scenario // 关键修复 + state.Language = input.Language + state.ImageData = input.ImageData + // ... 复制其他字段 + ctx = WithPipelineState(ctx, state) +``` + +--- + +### Bug #2:前端未发送 scenario 字段 + +**严重性**: 🔴 Critical(前后端数据流断层) + +**症状**: +- 后端日志显示:`config updated scenario=""` +- 会话配置中 scenario 未更新,始终为默认值 `free_chat` +- WebSocket 消息缺少 scenario 字段 + +**根因**: +`frontend/src/hooks/useVisionSession.ts` 发送 config 消息时缺少 `scenario` 字段: +```typescript +❌ send({ + type: "config", + payload: { + tts_enabled: config.ttsEnabled, + detail_level: config.detailLevel, + language: config.language, + // 缺少 scenario: config.scenario + }, +}); +``` + +**修复**: +在两处发送 config 的地方添加 `scenario` 字段(第 128 行和第 168 行)。 + +--- + +### 完整数据流(修复后) + +``` +用户切换情景到"模拟面试官" + ↓ +前端 updateConfig({scenario: "interviewer"}) + ↓ +✅ 发送 WebSocket: {type: "config", payload: {scenario: "interviewer"}} + ↓ +后端 handler.go 接收并保存 + ↓ +sess.Config.Scenario = "interviewer" + ↓ +用户发送消息 "你是谁?" + ↓ +buildPipelineInput() → input.Scenario = "interviewer" + ↓ +✅ adapter.go 复制:state.Scenario = input.Scenario + ↓ +nodes_history.go 读取 state.Scenario = "interviewer" + ↓ +llm.GetScenarioPrompt("interviewer", "zh-CN") + ↓ +返回:"你是一位资深面试官..." + ↓ +llm.BuildSystemPrompt(..., scenarioPrompt) + ↓ +注入到 ChatModel System Message + ↓ +LLM 生成回复:"我是今天的面试官..." + ↓ +✅ 情景生效! +``` + +--- + +## 测试验证 + +### 编译验证 + +✅ **后端**: +```bash +cd backend && go build -o /tmp/camtalk_fix ./cmd/server +# 产物:48MB,无编译错误 +``` + +✅ **前端**: +```bash +cd frontend && npm run lint +# ESLint 检查通过(无新增错误) +``` + +--- + +### 功能测试清单 + +| 测试项 | 操作步骤 | 预期结果 | 验证方法 | +|--------|---------|---------|---------| +| **首句引导** | 切换到"模拟面试官" | AI 自动说:"你好!我是今天的面试官..." | 观察聊天框 | +| **情景生效** | 问 "你是谁?" | AI 回答:"我是今天的面试官..." | 观察回复内容 | +| **提示卡片** | 发送一条消息后查看顶部 | 显示蓝色卡片:"🎯 模拟面试官 \| AI 会扮演面试官..." | 观察 UI | +| **语言联动** | 切换到"英语老师" | 语言自动切换到 en-US,AI 用英语回复 | 观察配置和回复 | +| **持久化** | 切换情景后刷新页面 | 情景配置保持,首句仍在历史中 | 刷新浏览器 | +| **多情景** | 依次测试所有情景 | 每个情景 AI 回复风格明显不同 | 对比回复 | + +--- + +### 日志验证 + +**查看日志**: +```bash +tail -f /tmp/camtalk_server.log | grep -E "config updated|历史组装完成" +``` + +**修复前**(Bug): +``` +config updated session=xxx scenario="" ← ❌ 空字符串 +历史组装完成 ... scenario=free_chat ← ❌ 始终是默认值 +``` + +**修复后**(正常): +``` +config updated session=xxx scenario=interviewer ← ✅ 正确接收 +历史组装完成 ... scenario=interviewer ← ✅ 正确传递 +``` + +--- + +## 部署指南 + +### 部署步骤 + +#### 1. 停止旧服务(如果正在运行) + +```bash +# 查找并停止占用 8080 端口的进程 +lsof -ti:8080 | xargs kill -9 +``` + +#### 2. 启动后端 + +```bash +cd backend +go run ./cmd/server +# 或编译后运行 +# go build -o camtalk ./cmd/server && ./camtalk +``` + +**验证后端启动**: +```bash +curl http://localhost:8080/api/health +# 预期输出:{"status":"ok","version":"dev","uptime_seconds":10,"active_sessions":0} +``` + +#### 3. 启动前端(如已运行则刷新浏览器) + +```bash +cd frontend +npm run dev +# 访问 http://localhost:5173 +``` + +**前端无需重启**:Vite 会自动热更新(HMR),只需刷新浏览器页面即可。 + +--- + +### 快速验证 + +1. **打开浏览器** → http://localhost:5173 +2. **登录系统** +3. **切换情景** → 右侧配置面板 → 对话情景 → 模拟面试官 +4. **观察现象**: + - ✨ AI 立即说:"你好!我是今天的面试官。让我们先从自我介绍开始..." + - ✨ 对话框顶部显示蓝色提示卡片 +5. **验证效果** → 发送:"你是谁?" + - ✅ **正确回复**:"我是今天的面试官..." + - ❌ **错误回复**:"我是通义千问..." + +--- + +## 技术细节 + +### Eino 框架 State 机制 + +项目使用 **CloudWeGo Eino** 框架进行 AI 编排,State 在节点间共享数据: + +```go +// 1. 定义 State 结构 +type PipelineState struct { + Scenario string // 必须显式赋值 + ... +} + +// 2. 注册 State 生成函数 +g := compose.NewGraph[I, O]( + compose.WithGenLocalState(genLocalState), +) + +// 3. 节点通过 stateFromCtx(ctx) 读取 +state := stateFromCtx(ctx) +scenario := state.Scenario +``` + +**关键点**:`genLocalState` 只是创建空结构体,**必须在调用 Graph 前手动赋值**! + +--- + +### WebSocket 协议 + +**客户端 → 服务端**(config 消息): +```json +{ + "type": "config", + "payload": { + "tts_enabled": true, + "detail_level": "low", + "language": "zh-CN", + "scenario": "interviewer" + } +} +``` + +**服务端 → 客户端**(首句引导): +```json +// llm_chunk +{ + "type": "llm_chunk", + "request_id": "scenario_greeting", + "delta": "你好!我是今天的面试官...", + "role": "assistant" +} + +// llm_done +{ + "type": "llm_done", + "request_id": "scenario_greeting", + "full_text": "你好!我是今天的面试官...", + "tokens_used": {"prompt": 0, "completion": 0, "total": 0} +} +``` + +--- + +### System Prompt 构建流程 + +``` +sess.Config.Scenario = "interviewer" + ↓ +PipelineInput.Scenario = "interviewer" + ↓ +PipelineState.Scenario = "interviewer" (adapter.go 复制) + ↓ +nodes_history.go 读取 state.Scenario + ↓ +scenarioPrompt := llm.GetScenarioPrompt("interviewer", "zh-CN") + ↓ +返回:"你是一位资深面试官。你通过摄像头观察面试者..." + ↓ +systemPrompt := llm.BuildSystemPrompt(language, detailLevel, scenarioPrompt) + ↓ +messages[0] = {Role: "system", Content: systemPrompt} + ↓ +ChatModel 接收到情景 Prompt + ↓ +LLM 按情景角色生成回复 +``` + +--- + +## 后续优化建议 + +### P2(强烈推荐) + +1. **情景切换时创建新会话** + - 避免历史对话干扰新情景 + - 弹窗确认:"切换情景会创建新会话,当前对话将保存。是否继续?" + - 实现难度:⭐⭐ + - 用户价值:⭐⭐⭐⭐ + +2. **进一步增强 System Prompt** + - 增加示例对话(Few-shot Prompting) + - 增加"禁止事项"列表 + - 实现难度:⭐ + - 效果提升:⭐⭐⭐ + +### P3(可选) + +1. **情景专属 UI 主题色** + - 面试官 → 深蓝色 + - 英语老师 → 绿色 + - 辩论 → 红色 + - 翻译 → 紫色 + +2. **切换动画与音效** + - 切换时播放短音效 + - 聊天面板淡出淡入动画 + +--- + +## 修改文件清单 + +### 后端(3 个文件) + +- ✅ `backend/internal/eino/adapter.go` — 修复 State 初始化 +- ✅ `backend/internal/ws/handler.go` — 添加首句引导 +- ✅ `backend/internal/ai/llm/scenarios.go` — 增强 Prompt + 首句 + +### 前端(5 个文件) + +- ✅ `frontend/src/hooks/useVisionSession.ts` — 修复 scenario 发送 +- ✅ `frontend/src/components/ChatPanel/index.tsx` — 添加提示卡片 +- ✅ `frontend/src/App.css` — 卡片样式 +- ✅ `frontend/src/lib/i18n/zh-CN.ts` — 中文翻译 +- ✅ `frontend/src/lib/i18n/en-US.ts` — 英文翻译 +- ✅ `frontend/src/lib/i18n/ja-JP.ts` — 日文翻译 + +--- + +## 经验教训 + +1. **数据流完整性验证** + - 从用户输入 → WebSocket → 后端逻辑 → LLM → 回复 + - 每个环节都需要日志验证 + +2. **框架封装层的隐式约定** + - Eino State 需要显式初始化 + - 不能依赖零值或默认值 + +3. **端到端测试的重要性** + - 单元测试通过 ≠ 功能正常工作 + - 必须包含实际对话验证 + +4. **前后端协议同步** + - WebSocket 消息字段必须对齐 + - 代码 review 需要覆盖完整数据流 + +--- + +## 提交信息 + +```bash +git add backend/internal/eino/adapter.go \ + backend/internal/ws/handler.go \ + backend/internal/ai/llm/scenarios.go \ + frontend/src/hooks/useVisionSession.ts \ + frontend/src/components/ChatPanel/index.tsx \ + frontend/src/App.css \ + frontend/src/lib/i18n/*.ts \ + docs/情景切换功能完整文档.md + +git commit -m "feat: 实现情景切换功能 + 修复两个关键 Bug + +功能实现: +- 后端:添加情景首句引导(面试官/英语老师/辩论/翻译) +- 后端:增强所有情景的 System Prompt(角色定位+规则+约束) +- 前端:对话顶部添加情景提示卡片(蓝色渐变+图标+说明) +- i18n:完整支持中英日三语 + +Bug 修复: +- Bug #1: adapter.go 未初始化 PipelineState.Scenario + 根因:genLocalState() 创建空 State,未从 input 复制元数据 + 影响:所有情景均失效,AI 使用默认 Prompt + 修复:从 PipelineInput 复制 Scenario 等字段到 State + +- Bug #2: useVisionSession.ts 未发送 scenario 字段 + 根因:config 消息 payload 缺少 scenario 字段 + 影响:后端无法接收到情景切换信息 + 修复:在两处发送 config 的地方添加 scenario 字段 + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +## 附录:故障排查 + +### 如果情景仍然不生效 + +1. **检查后端日志**: + ```bash + grep "config updated" /tmp/camtalk_server.log | tail -5 + grep "历史组装完成" /tmp/camtalk_server.log | tail -5 + ``` + + - 如果 `scenario=` 是空的,说明前端未发送或后端未接收 + - 如果 `scenario=interviewer` 正确,但 AI 回复仍是通用的,可能是 LLM 模型问题 + +2. **检查前端 WebSocket 消息**(浏览器 DevTools → Network → WS): + ```json + { + "type": "config", + "payload": { + "scenario": "interviewer" // 确认存在 + } + } + ``` + +3. **检查会话配置是否保存**: + - 切换情景后,LocalStorage 中应该有 `camtalk_config` + - 内容应包含 `"scenario": "interviewer"` + +4. **清除缓存重试**: + ```bash + # 浏览器:清除 LocalStorage + # 后端:重启服务 + # 前端:刷新页面 + ``` + +--- + +**文档版本**: 1.0 +**最后更新**: 2026-06-20 +**维护人员**: CamTalk 开发团队 diff --git a/frontend/src/App.css b/frontend/src/App.css index 7d8bdd0..0bf76ce 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1674,6 +1674,54 @@ body { line-height: 1.4; } +/* ---- Scenario Hint Card (非空状态顶部提示) ---- */ + +.chat-panel__scenario-hint { + display: flex; + justify-content: center; + padding: 12px 24px 0; +} + +.scenario-hint-card { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border-radius: var(--radius-sm); + background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(99, 102, 241, 0.08) 100%); + border: 1px solid rgba(59, 130, 246, 0.2); + max-width: 520px; + width: 100%; + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.06); +} + +.scenario-hint-card__icon { + font-size: 1.8rem; + flex-shrink: 0; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1)); +} + +.scenario-hint-card__text { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; +} + +.scenario-hint-card__text strong { + font-size: 0.9rem; + font-weight: 600; + color: var(--color-text); + letter-spacing: 0.01em; +} + +.scenario-hint-card__text p { + font-size: 0.75rem; + color: var(--color-text-muted); + line-height: 1.5; + margin: 0; +} + /* ---- Voice Input Button ---- */ .chat-input__voice { diff --git a/frontend/src/components/ChatPanel/index.tsx b/frontend/src/components/ChatPanel/index.tsx index 27e14cb..a0e2d8c 100644 --- a/frontend/src/components/ChatPanel/index.tsx +++ b/frontend/src/components/ChatPanel/index.tsx @@ -106,6 +106,21 @@ export function ChatPanel({ return (
+ {/* 情景提示卡片(非空状态 + 非自由对话) */} + {messages.length > 0 && !isFreeChat && ( +
+
+ + {scenarios.find(s => s.id === activeScenario)?.icon} + +
+ {t(scenarios.find(s => s.id === activeScenario)?.nameKey || "")} +

{t(`scenario.${activeScenario}.hint`)}

+
+
+
+ )} + {/* 空状态:情景选择 + 场景卡片 */} {isEmpty && (
diff --git a/frontend/src/hooks/useVisionSession.ts b/frontend/src/hooks/useVisionSession.ts index 53f8ab2..1cdeee9 100644 --- a/frontend/src/hooks/useVisionSession.ts +++ b/frontend/src/hooks/useVisionSession.ts @@ -130,6 +130,7 @@ export function useVisionSession(accessToken?: string | null, conversationId?: s tts_enabled: config.ttsEnabled, detail_level: config.detailLevel, language: config.language, + scenario: config.scenario, }, }); @@ -169,6 +170,7 @@ export function useVisionSession(accessToken?: string | null, conversationId?: s tts_enabled: next.ttsEnabled, detail_level: next.detailLevel, language: next.language, + scenario: next.scenario, }, }); } diff --git a/frontend/src/lib/i18n/en-US.ts b/frontend/src/lib/i18n/en-US.ts index ed2e92c..1a9e8f8 100644 --- a/frontend/src/lib/i18n/en-US.ts +++ b/frontend/src/lib/i18n/en-US.ts @@ -33,14 +33,19 @@ export const enUS: TranslationMap = { "scenario.choose": "Choose a scenario", "scenario.freeChat": "Free Chat", "scenario.freeChat.desc": "AI visual assistant, chat freely", + "scenario.freeChat.hint": "AI will act as a visual assistant to answer your questions", "scenario.interviewer": "Mock Interviewer", "scenario.interviewer.desc": "AI plays interviewer for practice", + "scenario.interviewer.hint": "AI will act as an interviewer, asking professional questions and providing feedback", "scenario.englishTeacher": "English Tutor", "scenario.englishTeacher.desc": "AI English tutor, corrects grammar", + "scenario.englishTeacher.hint": "AI will converse in English, correct grammar mistakes, and guide deeper conversations", "scenario.debate": "Debate Opponent", "scenario.debate.desc": "AI challenges your viewpoints", + "scenario.debate.hint": "AI will argue against your viewpoints using logic and evidence", "scenario.interpreter": "Interpreter", "scenario.interpreter.desc": "Real-time Chinese-English translation", + "scenario.interpreter.hint": "AI will translate your speech in real-time (Chinese-English), without explanations", // Video indicators "video.observing": "👁️ Observing", diff --git a/frontend/src/lib/i18n/ja-JP.ts b/frontend/src/lib/i18n/ja-JP.ts index 5d6e1db..5c1e53e 100644 --- a/frontend/src/lib/i18n/ja-JP.ts +++ b/frontend/src/lib/i18n/ja-JP.ts @@ -33,14 +33,19 @@ export const jaJP: TranslationMap = { "scenario.choose": "シナリオを選択", "scenario.freeChat": "フリーチャット", "scenario.freeChat.desc": "AIビジュアルアシスタントと自由に会話", + "scenario.freeChat.hint": "AIがビジュアルアシスタントとしてあなたの質問に答えます", "scenario.interviewer": "模擬面接官", "scenario.interviewer.desc": "AIが面接官として模擬面接を実施", + "scenario.interviewer.hint": "AIが面接官として専門的な質問をし、フィードバックを提供します", "scenario.englishTeacher": "英語の先生", "scenario.englishTeacher.desc": "AI英語教師が文法を修正し会話を導く", + "scenario.englishTeacher.hint": "AIが英語で会話し、文法の誤りを修正して深い対話を導きます", "scenario.debate": "ディベート相手", "scenario.debate.desc": "AIがあなたの观点に反論し論理的思考を鍛える", + "scenario.debate.hint": "AIが反対の立場から論理と証拠であなたの观点に反論します", "scenario.interpreter": "同時通訳", "scenario.interpreter.desc": "リアルタイムで中日相互翻訳", + "scenario.interpreter.hint": "AIがあなたの発言をリアルタイムで翻訳します(中日相互翻訳、説明なし)", // Video indicators "video.observing": "👁️ 観察中", diff --git a/frontend/src/lib/i18n/zh-CN.ts b/frontend/src/lib/i18n/zh-CN.ts index 8d7d686..5f9f3f7 100644 --- a/frontend/src/lib/i18n/zh-CN.ts +++ b/frontend/src/lib/i18n/zh-CN.ts @@ -33,14 +33,19 @@ export const zhCN: TranslationMap = { "scenario.choose": "选择对话情景", "scenario.freeChat": "自由对话", "scenario.freeChat.desc": "AI 视觉助手,自由聊天", + "scenario.freeChat.hint": "AI 会作为视觉助手回答你的问题", "scenario.interviewer": "模拟面试官", "scenario.interviewer.desc": "AI 扮演面试官,进行模拟面试", + "scenario.interviewer.hint": "AI 会扮演面试官,逐步提出专业问题并点评你的回答", "scenario.englishTeacher": "英语老师", "scenario.englishTeacher.desc": "AI 英语外教,纠正语法并引导对话", + "scenario.englishTeacher.hint": "AI 会用英语对话,纠正语法错误并引导深入交流", "scenario.debate": "辩论对手", "scenario.debate.desc": "AI 反驳你的观点,锻炼逻辑思维", + "scenario.debate.hint": "AI 会站在反方立场,用逻辑和证据反驳你的观点", "scenario.interpreter": "同声翻译", "scenario.interpreter.desc": "实时中英互译,口语化翻译", + "scenario.interpreter.hint": "AI 会实时翻译你的话(中英互译),无解释评论", // Video indicators "video.observing": "👁️ 观察中", -- 2.49.1