feat:添加对话情景功能
This commit is contained in:
@@ -15,10 +15,11 @@ type Service interface {
|
|||||||
|
|
||||||
// Request 推理请求。
|
// Request 推理请求。
|
||||||
type Request struct {
|
type Request struct {
|
||||||
Image []byte // JPEG 图片(已从 Base64 解码)
|
Image []byte // JPEG 图片(已从 Base64 解码)
|
||||||
Text string // 用户语音识别后的文本
|
Text string // 用户语音识别后的文本
|
||||||
History []models.Message // 最近 N 轮对话历史
|
History []models.Message // 最近 N 轮对话历史
|
||||||
Language string // 语言,如 "zh-CN"
|
Language string // 语言,如 "zh-CN"
|
||||||
|
SystemPrompt string // 情景自定义 system prompt(非空时覆盖默认 prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chunk 流式推理的一个增量片段。
|
// Chunk 流式推理的一个增量片段。
|
||||||
|
|||||||
@@ -209,10 +209,10 @@ func (o *OpenAIService) parseSSEStream(body io.Reader, ch chan<- Chunk) {
|
|||||||
func (o *OpenAIService) buildMessages(req Request) []chatMessage {
|
func (o *OpenAIService) buildMessages(req Request) []chatMessage {
|
||||||
var messages []chatMessage
|
var messages []chatMessage
|
||||||
|
|
||||||
// System prompt
|
// System prompt(情景覆盖优先)
|
||||||
messages = append(messages, chatMessage{
|
messages = append(messages, chatMessage{
|
||||||
Role: "system",
|
Role: "system",
|
||||||
Content: []contentPart{{Type: "text", Text: BuildSystemPrompt(req.Language, "")}},
|
Content: []contentPart{{Type: "text", Text: BuildSystemPrompt(req.Language, "", req.SystemPrompt)}},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 历史消息
|
// 历史消息
|
||||||
|
|||||||
@@ -242,9 +242,9 @@ func TestBuildSystemPrompt(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got := BuildSystemPrompt(tt.language, tt.detailLevel)
|
got := BuildSystemPrompt(tt.language, tt.detailLevel, "")
|
||||||
if !strings.Contains(got, tt.wantContain) {
|
if !strings.Contains(got, tt.wantContain) {
|
||||||
t.Errorf("BuildSystemPrompt(%q, %q) should contain %q", tt.language, tt.detailLevel, tt.wantContain)
|
t.Errorf("BuildSystemPrompt(%q, %q, \"\") should contain %q", tt.language, tt.detailLevel, tt.wantContain)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,26 @@ package llm
|
|||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
// BuildSystemPrompt 根据语言和细节级别构建系统提示词。
|
// BuildSystemPrompt 根据语言、细节级别和情景 prompt 构建系统提示词。
|
||||||
func BuildSystemPrompt(language, detailLevel string) string {
|
// scenarioPrompt 非空时,覆盖默认视觉助手 prompt。
|
||||||
|
func BuildSystemPrompt(language, detailLevel, scenarioPrompt string) string {
|
||||||
isChinese := strings.HasPrefix(language, "zh")
|
isChinese := strings.HasPrefix(language, "zh")
|
||||||
|
|
||||||
|
// 情景模式:使用自定义 prompt 作为基础
|
||||||
|
if scenarioPrompt != "" {
|
||||||
|
var prompt strings.Builder
|
||||||
|
prompt.WriteString(scenarioPrompt)
|
||||||
|
if detailLevel == "high" {
|
||||||
|
if isChinese {
|
||||||
|
prompt.WriteString(" 请在涉及视觉内容时提供更详细的描述,包括颜色、位置、数量等细节。")
|
||||||
|
} else {
|
||||||
|
prompt.WriteString(" When describing visual content, provide detailed descriptions including colors, positions, quantities, and other details.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prompt.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认模式:视觉助手
|
||||||
var prompt strings.Builder
|
var prompt strings.Builder
|
||||||
if isChinese {
|
if isChinese {
|
||||||
prompt.WriteString("你是一个视觉助手。用户通过摄像头看到一个场景,并用语音向你提问。请用简洁自然的中文回答。如果涉及视觉描述,先说\"我看到……\"。回答控制在3-5句话以内,除非用户要求详细说明。")
|
prompt.WriteString("你是一个视觉助手。用户通过摄像头看到一个场景,并用语音向你提问。请用简洁自然的中文回答。如果涉及视觉描述,先说\"我看到……\"。回答控制在3-5句话以内,除非用户要求详细说明。")
|
||||||
|
|||||||
55
backend/internal/ai/llm/scenarios.go
Normal file
55
backend/internal/ai/llm/scenarios.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package llm
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// scenarioPrompt 定义单个情景的多语言 system prompt。
|
||||||
|
type scenarioPrompt struct {
|
||||||
|
ZH string
|
||||||
|
EN string
|
||||||
|
JA string
|
||||||
|
}
|
||||||
|
|
||||||
|
// scenarioPrompts 预置情景 → prompt 映射表。
|
||||||
|
// key 为情景 ID(与前端 Scenario.id 对齐)。
|
||||||
|
var scenarioPrompts = map[string]scenarioPrompt{
|
||||||
|
"interviewer": {
|
||||||
|
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文以内。",
|
||||||
|
},
|
||||||
|
"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.",
|
||||||
|
},
|
||||||
|
"debate": {
|
||||||
|
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文以内。",
|
||||||
|
},
|
||||||
|
"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) 固有名詞は初出時に原文を括弧で注記する。",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScenarioPrompt 根据情景 ID 和语言获取对应的 system prompt。
|
||||||
|
// 返回空字符串表示无此情景(使用默认 prompt)。
|
||||||
|
func GetScenarioPrompt(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.ZH
|
||||||
|
case strings.HasPrefix(language, "ja"):
|
||||||
|
return p.JA
|
||||||
|
default:
|
||||||
|
return p.EN
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ type SessionConfig struct {
|
|||||||
TTSEnabled bool `json:"tts_enabled"`
|
TTSEnabled bool `json:"tts_enabled"`
|
||||||
DetailLevel string `json:"detail_level"` // "low" | "high"
|
DetailLevel string `json:"detail_level"` // "low" | "high"
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
|
Scenario string `json:"scenario"` // 情景 ID,如 "free_chat"、"interviewer"
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultSessionTitle 默认会话标题。
|
// DefaultSessionTitle 默认会话标题。
|
||||||
@@ -24,7 +25,7 @@ const DefaultSessionTitle = "新对话"
|
|||||||
|
|
||||||
// DefaultConfig 默认会话配置。
|
// DefaultConfig 默认会话配置。
|
||||||
func DefaultConfig() SessionConfig {
|
func DefaultConfig() SessionConfig {
|
||||||
return SessionConfig{TTSEnabled: true, DetailLevel: "low", Language: "zh-CN"}
|
return SessionConfig{TTSEnabled: true, DetailLevel: "low", Language: "zh-CN", Scenario: "free_chat"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionConfigPatch 会话配置增量更新(指针字段表示"未传则不更新")。
|
// SessionConfigPatch 会话配置增量更新(指针字段表示"未传则不更新")。
|
||||||
@@ -32,6 +33,7 @@ type SessionConfigPatch struct {
|
|||||||
TTSEnabled *bool `json:"tts_enabled,omitempty"`
|
TTSEnabled *bool `json:"tts_enabled,omitempty"`
|
||||||
DetailLevel *string `json:"detail_level,omitempty"`
|
DetailLevel *string `json:"detail_level,omitempty"`
|
||||||
Language *string `json:"language,omitempty"`
|
Language *string `json:"language,omitempty"`
|
||||||
|
Scenario *string `json:"scenario,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply 将 patch 中的非 nil 字段覆盖到 cfg。
|
// Apply 将 patch 中的非 nil 字段覆盖到 cfg。
|
||||||
@@ -45,6 +47,9 @@ func (p SessionConfigPatch) Apply(cfg *SessionConfig) {
|
|||||||
if p.Language != nil {
|
if p.Language != nil {
|
||||||
cfg.Language = *p.Language
|
cfg.Language = *p.Language
|
||||||
}
|
}
|
||||||
|
if p.Scenario != nil {
|
||||||
|
cfg.Scenario = *p.Scenario
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// User 用户。
|
// User 用户。
|
||||||
@@ -81,6 +86,7 @@ type WsConfig struct {
|
|||||||
TTSEnabled *bool `json:"tts_enabled,omitempty"`
|
TTSEnabled *bool `json:"tts_enabled,omitempty"`
|
||||||
DetailLevel *string `json:"detail_level,omitempty"`
|
DetailLevel *string `json:"detail_level,omitempty"`
|
||||||
Language *string `json:"language,omitempty"`
|
Language *string `json:"language,omitempty"`
|
||||||
|
Scenario *string `json:"scenario,omitempty"`
|
||||||
} `json:"payload"`
|
} `json:"payload"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -193,12 +193,13 @@ func (p *Pipeline) ProcessQuery(
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Step 2+3: LLM 流式推理 + TTS 并行合成
|
// Step 2+3: LLM 流式推理 + TTS 并行合成
|
||||||
log.Infow("开始 LLM 推理", "request_id", req.RequestID)
|
log.Infow("开始 LLM 推理", "request_id", req.RequestID, "scenario", sess.Config.Scenario)
|
||||||
llmReq := llm.Request{
|
llmReq := llm.Request{
|
||||||
Image: image,
|
Image: image,
|
||||||
Text: userText,
|
Text: userText,
|
||||||
History: history,
|
History: history,
|
||||||
Language: sess.Config.Language,
|
Language: sess.Config.Language,
|
||||||
|
SystemPrompt: llm.GetScenarioPrompt(sess.Config.Scenario, sess.Config.Language),
|
||||||
}
|
}
|
||||||
|
|
||||||
llmStream, err := p.llmService.ChatStream(ctx, llmReq)
|
llmStream, err := p.llmService.ChatStream(ctx, llmReq)
|
||||||
|
|||||||
@@ -276,6 +276,7 @@ func serveWS(c *gin.Context, sessionMgr session.Manager, orch orchestrator.Orche
|
|||||||
TTSEnabled: msg.Payload.TTSEnabled,
|
TTSEnabled: msg.Payload.TTSEnabled,
|
||||||
DetailLevel: msg.Payload.DetailLevel,
|
DetailLevel: msg.Payload.DetailLevel,
|
||||||
Language: msg.Payload.Language,
|
Language: msg.Payload.Language,
|
||||||
|
Scenario: msg.Payload.Scenario,
|
||||||
}
|
}
|
||||||
if err := client.sessionMgr.UpdateConfig(context.Background(), sessionID, patch); err != nil {
|
if err := client.sessionMgr.UpdateConfig(context.Background(), sessionID, patch); err != nil {
|
||||||
errors.SendWSError(client, errors.CodeInternalError, "", err)
|
errors.SendWSError(client, errors.CodeInternalError, "", err)
|
||||||
|
|||||||
@@ -760,6 +760,37 @@ body {
|
|||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Scenario Quick Selector (chat header) ---- */
|
||||||
|
.scenario-selector {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
background: var(--color-surface-2);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 4px 26px 4px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23888' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 8px center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-selector:hover {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
color: var(--color-text);
|
||||||
|
background-color: var(--color-surface-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-selector:focus {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
.chat-panel-header__stats {
|
.chat-panel-header__stats {
|
||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -877,6 +908,31 @@ body {
|
|||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- System message (scenario switch, etc.) ---- */
|
||||||
|
.chat-message--system {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 6px 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message--system::before,
|
||||||
|
.chat-message--system::after {
|
||||||
|
content: "";
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message--system__text {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-message__role {
|
.chat-message__role {
|
||||||
font-size: 0.62rem;
|
font-size: 0.62rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -1505,6 +1561,75 @@ body {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Scenario Selection Cards ---- */
|
||||||
|
|
||||||
|
.scenario-cards {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 24px;
|
||||||
|
max-width: 380px;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-cards__title {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--color-surface-2);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-card:hover {
|
||||||
|
background: var(--color-surface-3);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-card:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-card__icon {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-card__text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-card__title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-card__desc {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Voice Input Button ---- */
|
/* ---- Voice Input Button ---- */
|
||||||
|
|
||||||
.chat-input__voice {
|
.chat-input__voice {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { AuthPage } from "./components/AuthPage";
|
|||||||
import { AuthProvider, useAuth } from "./lib/auth";
|
import { AuthProvider, useAuth } from "./lib/auth";
|
||||||
import { loadConfig, loadTheme, saveTheme } from "./lib/storage";
|
import { loadConfig, loadTheme, saveTheme } from "./lib/storage";
|
||||||
import { I18nContext, parseLocale, t } from "./lib/i18n";
|
import { I18nContext, parseLocale, t } from "./lib/i18n";
|
||||||
|
import { scenarios } from "./lib/scenarios";
|
||||||
import type { Locale } from "./lib/i18n";
|
import type { Locale } from "./lib/i18n";
|
||||||
import type { Theme } from "./types";
|
import type { Theme } from "./types";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
@@ -180,6 +181,27 @@ function AppContent() {
|
|||||||
sendTextMessage(prompt);
|
sendTextMessage(prompt);
|
||||||
}, [sendTextMessage]);
|
}, [sendTextMessage]);
|
||||||
|
|
||||||
|
// ---- 情景选择 ----
|
||||||
|
const handleSelectScenario = useCallback((scenarioId: string) => {
|
||||||
|
const sc = scenarios.find(s => s.id === scenarioId);
|
||||||
|
const updates: Partial<import("./types").SessionConfig> = { scenario: scenarioId };
|
||||||
|
// 如果情景有默认语言,同步切换
|
||||||
|
if (sc?.defaultLanguage) {
|
||||||
|
updates.language = sc.defaultLanguage;
|
||||||
|
}
|
||||||
|
updateConfig(updates);
|
||||||
|
// 插入系统提示消息
|
||||||
|
const scenarioName = sc ? `${sc.icon} ${tr(sc.nameKey)}` : scenarioId;
|
||||||
|
setMessages(prev => [...prev, {
|
||||||
|
role: "system",
|
||||||
|
content: tr("chat.scenarioSwitched").replace("{name}", scenarioName),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}]);
|
||||||
|
}, [updateConfig, setMessages, tr]);
|
||||||
|
|
||||||
|
// 当前情景对象
|
||||||
|
const activeScenario = scenarios.find(s => s.id === (config.scenario || "free_chat")) || scenarios[0];
|
||||||
|
|
||||||
// ---- 键盘快捷键 ----
|
// ---- 键盘快捷键 ----
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: KeyboardEvent) => {
|
const handler = (e: KeyboardEvent) => {
|
||||||
@@ -443,6 +465,16 @@ function AppContent() {
|
|||||||
<div className="chat-panel-header">
|
<div className="chat-panel-header">
|
||||||
<span>{tr("chat.title")}</span>
|
<span>{tr("chat.title")}</span>
|
||||||
<div className="chat-panel-header__right">
|
<div className="chat-panel-header__right">
|
||||||
|
<select
|
||||||
|
className="scenario-selector"
|
||||||
|
value={activeScenario.id}
|
||||||
|
onChange={(e) => handleSelectScenario(e.target.value)}
|
||||||
|
title={tr("settings.scenario")}
|
||||||
|
>
|
||||||
|
{scenarios.map((sc) => (
|
||||||
|
<option key={sc.id} value={sc.id}>{sc.icon} {tr(sc.nameKey)}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
{isConnected && mode === "observation" && (
|
{isConnected && mode === "observation" && (
|
||||||
<span className="chat-panel-header__mode">{tr("chat.mode.observation")}</span>
|
<span className="chat-panel-header__mode">{tr("chat.mode.observation")}</span>
|
||||||
)}
|
)}
|
||||||
@@ -465,14 +497,16 @@ function AppContent() {
|
|||||||
messages={messages}
|
messages={messages}
|
||||||
currentReply={currentReply}
|
currentReply={currentReply}
|
||||||
connectionStatus={connectionStatus}
|
connectionStatus={connectionStatus}
|
||||||
|
currentScenario={config.scenario}
|
||||||
isProcessing={isProcessing}
|
isProcessing={isProcessing}
|
||||||
isVADReady={isVADReady}
|
isVADReady={isVADReady}
|
||||||
vadError={vadError}
|
vadError={vadError ?? undefined}
|
||||||
isMicOn={isMicOn}
|
isMicOn={isMicOn}
|
||||||
isSpeaking={isSpeaking}
|
isSpeaking={isSpeaking}
|
||||||
onSendText={sendTextMessage}
|
onSendText={sendTextMessage}
|
||||||
onToggleMic={toggleMic}
|
onToggleMic={toggleMic}
|
||||||
onSceneCard={handleSceneCard}
|
onSceneCard={handleSceneCard}
|
||||||
|
onSelectScenario={handleSelectScenario}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// ChatPanel — 消息展示面板
|
// ChatPanel — 消息展示面板
|
||||||
// 职责:渲染对话消息列表、流式光标、元数据、自动滚动、文本输入
|
// 职责:渲染对话消息列表、流式光标、元数据、自动滚动、文本输入
|
||||||
// 增强:空状态场景卡片、语音输入按钮
|
// 增强:空状态情景选择卡片、语音输入按钮
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useI18n } from "../../lib/i18n";
|
import { useI18n } from "../../lib/i18n";
|
||||||
|
import { scenarios } from "../../lib/scenarios";
|
||||||
import type { ChatMessage } from "../../types";
|
import type { ChatMessage } from "../../types";
|
||||||
import type { ConnectionStatus } from "../../lib/websocket";
|
import type { ConnectionStatus } from "../../lib/websocket";
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ interface ChatPanelProps {
|
|||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
currentReply?: string;
|
currentReply?: string;
|
||||||
connectionStatus: ConnectionStatus;
|
connectionStatus: ConnectionStatus;
|
||||||
|
currentScenario?: string;
|
||||||
isProcessing?: boolean;
|
isProcessing?: boolean;
|
||||||
isVADReady?: boolean;
|
isVADReady?: boolean;
|
||||||
vadError?: string;
|
vadError?: string;
|
||||||
@@ -21,9 +23,10 @@ interface ChatPanelProps {
|
|||||||
onSendText?: (text: string) => void;
|
onSendText?: (text: string) => void;
|
||||||
onToggleMic?: () => void;
|
onToggleMic?: () => void;
|
||||||
onSceneCard?: (prompt: string) => void;
|
onSceneCard?: (prompt: string) => void;
|
||||||
|
onSelectScenario?: (scenarioId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 场景卡片数据 */
|
/** 场景卡片数据(视觉分析快捷) */
|
||||||
function getSceneCards(t: (key: string) => string) {
|
function getSceneCards(t: (key: string) => string) {
|
||||||
return [
|
return [
|
||||||
{ icon: "👁", titleKey: "scene.describe", descKey: "scene.describe.desc", prompt: t("scene.describe") },
|
{ icon: "👁", titleKey: "scene.describe", descKey: "scene.describe.desc", prompt: t("scene.describe") },
|
||||||
@@ -37,6 +40,7 @@ export function ChatPanel({
|
|||||||
messages,
|
messages,
|
||||||
currentReply,
|
currentReply,
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
|
currentScenario,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
isVADReady,
|
isVADReady,
|
||||||
vadError,
|
vadError,
|
||||||
@@ -45,6 +49,7 @@ export function ChatPanel({
|
|||||||
onSendText,
|
onSendText,
|
||||||
onToggleMic,
|
onToggleMic,
|
||||||
onSceneCard,
|
onSceneCard,
|
||||||
|
onSelectScenario,
|
||||||
}: ChatPanelProps) {
|
}: ChatPanelProps) {
|
||||||
const bottomRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -55,6 +60,8 @@ export function ChatPanel({
|
|||||||
const isConnected = connectionStatus === "connected";
|
const isConnected = connectionStatus === "connected";
|
||||||
const isEmpty = messages.length === 0 && !currentReply;
|
const isEmpty = messages.length === 0 && !currentReply;
|
||||||
const sceneCards = getSceneCards(t);
|
const sceneCards = getSceneCards(t);
|
||||||
|
const activeScenario = currentScenario || "free_chat";
|
||||||
|
const isFreeChat = activeScenario === "free_chat";
|
||||||
|
|
||||||
// 用户上滚时暂停自动滚动,滚到底部时恢复
|
// 用户上滚时暂停自动滚动,滚到底部时恢复
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -97,46 +104,74 @@ export function ChatPanel({
|
|||||||
return (
|
return (
|
||||||
<div className="chat-panel">
|
<div className="chat-panel">
|
||||||
<div className="chat-panel__messages" ref={containerRef}>
|
<div className="chat-panel__messages" ref={containerRef}>
|
||||||
{/* 空状态:场景卡片 */}
|
{/* 空状态:情景选择 + 场景卡片 */}
|
||||||
{isEmpty && (
|
{isEmpty && (
|
||||||
<div className="chat-panel__welcome">
|
<div className="chat-panel__welcome">
|
||||||
<span className="chat-panel__welcome-icon">💬</span>
|
<span className="chat-panel__welcome-icon">{scenarios.find(s => s.id === activeScenario)?.icon || "💬"}</span>
|
||||||
<p>{isConnected ? t("chat.welcome.prompt") : t("chat.empty.prompt")}</p>
|
<p>{isConnected ? t("chat.welcome.prompt") : t("chat.empty.prompt")}</p>
|
||||||
<span className="chat-panel__welcome-hint">{isConnected ? t("chat.welcome.hint") : t("chat.empty.hint")}</span>
|
<span className="chat-panel__welcome-hint">{isConnected ? t("chat.welcome.hint") : t("chat.empty.hint")}</span>
|
||||||
|
|
||||||
{/* 场景快捷卡片 */}
|
{/* 情景选择卡片(非 free_chat 时隐藏,因为已通过 header 切换) */}
|
||||||
<div className="scene-cards">
|
{isFreeChat && onSelectScenario && (
|
||||||
{sceneCards.map((card) => (
|
<div className="scenario-cards">
|
||||||
<button
|
<div className="scenario-cards__title">{t("scenario.choose")}</div>
|
||||||
key={card.titleKey}
|
{scenarios.filter(s => s.id !== "free_chat").map((sc) => (
|
||||||
className="scene-card"
|
<button
|
||||||
onClick={() => handleSceneCard(card.prompt)}
|
key={sc.id}
|
||||||
>
|
className="scenario-card"
|
||||||
<span className="scene-card__icon">{card.icon}</span>
|
onClick={() => onSelectScenario(sc.id)}
|
||||||
<div className="scene-card__text">
|
>
|
||||||
<span className="scene-card__title">{t(card.titleKey)}</span>
|
<span className="scenario-card__icon">{sc.icon}</span>
|
||||||
<span className="scene-card__desc">{t(card.descKey)}</span>
|
<div className="scenario-card__text">
|
||||||
</div>
|
<span className="scenario-card__title">{t(sc.nameKey)}</span>
|
||||||
</button>
|
<span className="scenario-card__desc">{t(sc.descKey)}</span>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 视觉分析快捷卡片(仅 free_chat 模式) */}
|
||||||
|
{isFreeChat && isConnected && (
|
||||||
|
<div className="scene-cards">
|
||||||
|
{sceneCards.map((card) => (
|
||||||
|
<button
|
||||||
|
key={card.titleKey}
|
||||||
|
className="scene-card"
|
||||||
|
onClick={() => handleSceneCard(card.prompt)}
|
||||||
|
>
|
||||||
|
<span className="scene-card__icon">{card.icon}</span>
|
||||||
|
<div className="scene-card__text">
|
||||||
|
<span className="scene-card__title">{t(card.titleKey)}</span>
|
||||||
|
<span className="scene-card__desc">{t(card.descKey)}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{messages.map((msg, index) => (
|
{messages.map((msg, index) => (
|
||||||
<div key={index} className={`chat-message chat-message--${msg.role}`}>
|
msg.role === "system" ? (
|
||||||
<div className="chat-message__role">
|
<div key={index} className="chat-message chat-message--system">
|
||||||
{msg.role === "user" ? t("chat.userLabel") : "AI"}
|
<span className="chat-message--system__text">{msg.content}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="chat-message__content">{msg.content}</div>
|
) : (
|
||||||
{msg.role === "assistant" && msg.tokensUsed !== undefined && (
|
<div key={index} className={`chat-message chat-message--${msg.role}`}>
|
||||||
<div className="chat-message__meta">
|
<div className="chat-message__role">
|
||||||
{msg.tokensUsed} tokens
|
{msg.role === "user" ? t("chat.userLabel") : "AI"}
|
||||||
{msg.latencyMs !== undefined && ` · ${(msg.latencyMs / 1000).toFixed(1)}s`}
|
|
||||||
{msg.model && ` · ${msg.model}`}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="chat-message__content">{msg.content}</div>
|
||||||
</div>
|
{msg.role === "assistant" && msg.tokensUsed !== undefined && (
|
||||||
|
<div className="chat-message__meta">
|
||||||
|
{msg.tokensUsed} tokens
|
||||||
|
{msg.latencyMs !== undefined && ` · ${(msg.latencyMs / 1000).toFixed(1)}s`}
|
||||||
|
{msg.model && ` · ${msg.model}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* 流式回复(尚未完成) */}
|
{/* 流式回复(尚未完成) */}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// ConfigPanel — 右侧抽屉式配置面板
|
// ConfigPanel — 右侧抽屉式配置面板
|
||||||
// 职责:主题切换、TTS 开关、detail level 切换、语言选择
|
// 职责:主题切换、TTS 开关、detail level 切换、语言选择、情景选择
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
import { useI18n } from "../../lib/i18n";
|
import { useI18n } from "../../lib/i18n";
|
||||||
|
import { scenarios } from "../../lib/scenarios";
|
||||||
import type { SessionConfig, Theme } from "../../types";
|
import type { SessionConfig, Theme } from "../../types";
|
||||||
|
|
||||||
interface ConfigPanelProps {
|
interface ConfigPanelProps {
|
||||||
@@ -89,6 +90,21 @@ export function ConfigPanel({ config, theme, username, onUpdate, onThemeChange,
|
|||||||
<option value="ja-JP">日本語</option>
|
<option value="ja-JP">日本語</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label className="config-row">
|
||||||
|
<div className="config-row__info">
|
||||||
|
<span className="config-row__label">{t("settings.scenario")}</span>
|
||||||
|
<span className="config-row__desc">{t("settings.scenario.desc")}</span>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={config.scenario || "free_chat"}
|
||||||
|
onChange={(e) => onUpdate({ scenario: e.target.value })}
|
||||||
|
>
|
||||||
|
{scenarios.map((sc) => (
|
||||||
|
<option key={sc.id} value={sc.id}>{sc.icon} {t(sc.nameKey)}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{username && onLogout && (
|
{username && onLogout && (
|
||||||
|
|||||||
@@ -63,14 +63,6 @@ export function useSessionList() {
|
|||||||
saveSessionSummaries(updated);
|
saveSessionSummaries(updated);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/** 保存当前会话的消息历史 */
|
|
||||||
const saveCurrentSession = useCallback((messages: ChatMessage[]) => {
|
|
||||||
const id = sessionsRef.current.length > 0 ? sessionsRef.current[0].id : null;
|
|
||||||
// 找到 activeSessionId 对应的会话
|
|
||||||
// 这里不依赖 activeSessionId state,而是通过参数传入
|
|
||||||
return messages;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/** 保存指定会话的消息并更新摘要 */
|
/** 保存指定会话的消息并更新摘要 */
|
||||||
const persistSession = useCallback((sessionId: string, messages: ChatMessage[]) => {
|
const persistSession = useCallback((sessionId: string, messages: ChatMessage[]) => {
|
||||||
if (!sessionId) return;
|
if (!sessionId) return;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// 来源:docs/03-接口文档.md §五 错误码
|
// 来源:docs/03-接口文档.md §五 错误码
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
import type { ErrorCode } from "../types";
|
// ErrorCode type reserved for future use
|
||||||
|
|
||||||
/** 将错误码转为用户友好文案(需要传入翻译函数) */
|
/** 将错误码转为用户友好文案(需要传入翻译函数) */
|
||||||
export function getErrorMessage(code: string, translate: (key: string) => string): string {
|
export function getErrorMessage(code: string, translate: (key: string) => string): string {
|
||||||
|
|||||||
@@ -26,6 +26,21 @@ export const enUS: TranslationMap = {
|
|||||||
"settings.detail.high": "High",
|
"settings.detail.high": "High",
|
||||||
"settings.language": "Language",
|
"settings.language": "Language",
|
||||||
"settings.language.desc": "Interaction language preference",
|
"settings.language.desc": "Interaction language preference",
|
||||||
|
"settings.scenario": "Scenario",
|
||||||
|
"settings.scenario.desc": "Choose the AI's role",
|
||||||
|
|
||||||
|
// Scenarios
|
||||||
|
"scenario.choose": "Choose a scenario",
|
||||||
|
"scenario.freeChat": "Free Chat",
|
||||||
|
"scenario.freeChat.desc": "AI visual assistant, chat freely",
|
||||||
|
"scenario.interviewer": "Mock Interviewer",
|
||||||
|
"scenario.interviewer.desc": "AI plays interviewer for practice",
|
||||||
|
"scenario.englishTeacher": "English Tutor",
|
||||||
|
"scenario.englishTeacher.desc": "AI English tutor, corrects grammar",
|
||||||
|
"scenario.debate": "Debate Opponent",
|
||||||
|
"scenario.debate.desc": "AI challenges your viewpoints",
|
||||||
|
"scenario.interpreter": "Interpreter",
|
||||||
|
"scenario.interpreter.desc": "Real-time Chinese-English translation",
|
||||||
|
|
||||||
// Video indicators
|
// Video indicators
|
||||||
"video.observing": "👁️ Observing",
|
"video.observing": "👁️ Observing",
|
||||||
@@ -63,6 +78,7 @@ export const enUS: TranslationMap = {
|
|||||||
"chat.userLabel": "You",
|
"chat.userLabel": "You",
|
||||||
"chat.input.placeholder": "Type a message...",
|
"chat.input.placeholder": "Type a message...",
|
||||||
"chat.send": "Send",
|
"chat.send": "Send",
|
||||||
|
"chat.scenarioSwitched": "Switched to {name} mode",
|
||||||
|
|
||||||
// Session messages
|
// Session messages
|
||||||
"session.changeDetected": "👁️ Scene change detected",
|
"session.changeDetected": "👁️ Scene change detected",
|
||||||
|
|||||||
@@ -26,6 +26,21 @@ export const jaJP: TranslationMap = {
|
|||||||
"settings.detail.high": "高",
|
"settings.detail.high": "高",
|
||||||
"settings.language": "言語",
|
"settings.language": "言語",
|
||||||
"settings.language.desc": "インタラクション言語の設定",
|
"settings.language.desc": "インタラクション言語の設定",
|
||||||
|
"settings.scenario": "シナリオ",
|
||||||
|
"settings.scenario.desc": "AIの役割を選択",
|
||||||
|
|
||||||
|
// Scenarios
|
||||||
|
"scenario.choose": "シナリオを選択",
|
||||||
|
"scenario.freeChat": "フリーチャット",
|
||||||
|
"scenario.freeChat.desc": "AIビジュアルアシスタントと自由に会話",
|
||||||
|
"scenario.interviewer": "模擬面接官",
|
||||||
|
"scenario.interviewer.desc": "AIが面接官として模擬面接を実施",
|
||||||
|
"scenario.englishTeacher": "英語の先生",
|
||||||
|
"scenario.englishTeacher.desc": "AI英語教師が文法を修正し会話を導く",
|
||||||
|
"scenario.debate": "ディベート相手",
|
||||||
|
"scenario.debate.desc": "AIがあなたの观点に反論し論理的思考を鍛える",
|
||||||
|
"scenario.interpreter": "同時通訳",
|
||||||
|
"scenario.interpreter.desc": "リアルタイムで中日相互翻訳",
|
||||||
|
|
||||||
// Video indicators
|
// Video indicators
|
||||||
"video.observing": "👁️ 観察中",
|
"video.observing": "👁️ 観察中",
|
||||||
@@ -63,6 +78,7 @@ export const jaJP: TranslationMap = {
|
|||||||
"chat.userLabel": "あなた",
|
"chat.userLabel": "あなた",
|
||||||
"chat.input.placeholder": "メッセージを入力...",
|
"chat.input.placeholder": "メッセージを入力...",
|
||||||
"chat.send": "送信",
|
"chat.send": "送信",
|
||||||
|
"chat.scenarioSwitched": "{name} モードに切り替えました",
|
||||||
|
|
||||||
// Session messages
|
// Session messages
|
||||||
"session.changeDetected": "👁️ シーン変化を検出",
|
"session.changeDetected": "👁️ シーン変化を検出",
|
||||||
|
|||||||
@@ -26,6 +26,21 @@ export const zhCN: TranslationMap = {
|
|||||||
"settings.detail.high": "高",
|
"settings.detail.high": "高",
|
||||||
"settings.language": "语言",
|
"settings.language": "语言",
|
||||||
"settings.language.desc": "交互语言偏好",
|
"settings.language.desc": "交互语言偏好",
|
||||||
|
"settings.scenario": "对话情景",
|
||||||
|
"settings.scenario.desc": "选择 AI 扮演的角色",
|
||||||
|
|
||||||
|
// Scenarios
|
||||||
|
"scenario.choose": "选择对话情景",
|
||||||
|
"scenario.freeChat": "自由对话",
|
||||||
|
"scenario.freeChat.desc": "AI 视觉助手,自由聊天",
|
||||||
|
"scenario.interviewer": "模拟面试官",
|
||||||
|
"scenario.interviewer.desc": "AI 扮演面试官,进行模拟面试",
|
||||||
|
"scenario.englishTeacher": "英语老师",
|
||||||
|
"scenario.englishTeacher.desc": "AI 英语外教,纠正语法并引导对话",
|
||||||
|
"scenario.debate": "辩论对手",
|
||||||
|
"scenario.debate.desc": "AI 反驳你的观点,锻炼逻辑思维",
|
||||||
|
"scenario.interpreter": "同声翻译",
|
||||||
|
"scenario.interpreter.desc": "实时中英互译,口语化翻译",
|
||||||
|
|
||||||
// Video indicators
|
// Video indicators
|
||||||
"video.observing": "👁️ 观察中",
|
"video.observing": "👁️ 观察中",
|
||||||
@@ -63,6 +78,7 @@ export const zhCN: TranslationMap = {
|
|||||||
"chat.userLabel": "你",
|
"chat.userLabel": "你",
|
||||||
"chat.input.placeholder": "输入文字对话...",
|
"chat.input.placeholder": "输入文字对话...",
|
||||||
"chat.send": "发送",
|
"chat.send": "发送",
|
||||||
|
"chat.scenarioSwitched": "已切换到 {name} 模式",
|
||||||
|
|
||||||
// Session messages
|
// Session messages
|
||||||
"session.changeDetected": "👁️ 画面变化检测",
|
"session.changeDetected": "👁️ 画面变化检测",
|
||||||
|
|||||||
47
frontend/src/lib/scenarios.ts
Normal file
47
frontend/src/lib/scenarios.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// ============================================================
|
||||||
|
// scenarios — 对话情景定义
|
||||||
|
// 职责:定义预置情景的展示数据和交互行为
|
||||||
|
// 注意:system prompt 由后端 scenarios.go 管理,此处仅定义 UI 展示数据
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
export interface Scenario {
|
||||||
|
id: string;
|
||||||
|
icon: string;
|
||||||
|
nameKey: string; // i18n key
|
||||||
|
descKey: string; // i18n key
|
||||||
|
defaultLanguage?: string; // 可选:切换情景时自动设置的语言
|
||||||
|
}
|
||||||
|
|
||||||
|
export const scenarios: Scenario[] = [
|
||||||
|
{
|
||||||
|
id: "free_chat",
|
||||||
|
icon: "💬",
|
||||||
|
nameKey: "scenario.freeChat",
|
||||||
|
descKey: "scenario.freeChat.desc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "interviewer",
|
||||||
|
icon: "🎯",
|
||||||
|
nameKey: "scenario.interviewer",
|
||||||
|
descKey: "scenario.interviewer.desc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "english_teacher",
|
||||||
|
icon: "📚",
|
||||||
|
nameKey: "scenario.englishTeacher",
|
||||||
|
descKey: "scenario.englishTeacher.desc",
|
||||||
|
defaultLanguage: "en-US",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "debate",
|
||||||
|
icon: "⚔️",
|
||||||
|
nameKey: "scenario.debate",
|
||||||
|
descKey: "scenario.debate.desc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "interpreter",
|
||||||
|
icon: "🌐",
|
||||||
|
nameKey: "scenario.interpreter",
|
||||||
|
descKey: "scenario.interpreter.desc",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -15,6 +15,7 @@ const DEFAULT_CONFIG: SessionConfig = {
|
|||||||
ttsEnabled: true,
|
ttsEnabled: true,
|
||||||
detailLevel: "low",
|
detailLevel: "low",
|
||||||
language: "zh-CN",
|
language: "zh-CN",
|
||||||
|
scenario: "free_chat",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 加载配置,无存储时返回默认值 */
|
/** 加载配置,无存储时返回默认值 */
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface SessionConfig {
|
|||||||
ttsEnabled: boolean;
|
ttsEnabled: boolean;
|
||||||
detailLevel: "low" | "high";
|
detailLevel: "low" | "high";
|
||||||
language: string;
|
language: string;
|
||||||
|
scenario: string; // 情景 ID,如 "free_chat"、"interviewer"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Theme = "dark" | "light";
|
export type Theme = "dark" | "light";
|
||||||
@@ -32,7 +33,7 @@ export interface SessionSummary {
|
|||||||
// ---- 聊天消息 ----
|
// ---- 聊天消息 ----
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
role: "user" | "assistant";
|
role: "user" | "assistant" | "system";
|
||||||
content: string;
|
content: string;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user