diff --git a/frontend/src/App.css b/frontend/src/App.css index e6ad6e2..9afbf9a 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1623,15 +1623,17 @@ body { } .scenario-chip--add .scenario-chip__icon { - font-size: 0.88rem; - font-weight: 300; - line-height: 1; + display: inline-flex; + align-items: center; } .scenario-chip__icon { font-size: 0.9rem; line-height: 1; flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; } .scenario-chip__name { @@ -1681,6 +1683,9 @@ body { font-size: 0.72rem; color: var(--color-text-muted); flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; } .device-select { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d485be6..df95d42 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,11 +19,34 @@ import { EditScenarioModal } from "./components/EditScenarioModal"; import { AuthProvider, useAuth } from "./lib/auth"; import { loadConfig, loadTheme, saveTheme } from "./lib/storage"; import { I18nContext, parseLocale, t } from "./lib/i18n"; +import { renderScenarioIconById } from "./lib/scenarioIcons"; import type { Locale } from "./lib/i18n"; import type { Theme } from "./types"; import type { UserScenario } from "./lib/api/scenarios"; import "./App.css"; +// ---- 统一 SVG 图标系统(Feather-style,stroke-based) ---- + +const SVG_PROPS = { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" as const, strokeLinejoin: "round" as const }; + +/** 系统情景 SVG 图标映射 */ +function scenarioSvgIcon(id: string) { + switch (id) { + case "free_chat": + return (); + case "interviewer": + return (); + case "english_teacher": + return (); + case "debate": + return (); + case "interpreter": + return (); + default: + return null; + } +} + /** 内部组件,确保在 I18nContext.Provider 内部使用 hooks */ function AppContent() { const { isAuthenticated, isLoading, user, logout, accessToken } = useAuth(); @@ -378,7 +401,7 @@ function AppContent() {
{tr("video.playing")}
)} {vadError && ( -
⚠️ {vadError}
+
{vadError}
)} {!isConnected && !stream && (
@@ -412,7 +435,9 @@ function AppContent() { onClick={() => handleSelectScenario(sc.id)} title={sc.description ? sc.description : sc.descKey ? tr(sc.descKey) : sc.name} > - {sc.icon} + + {scenarioSvgIcon(sc.id) || renderScenarioIconById(sc.icon, 14) || sc.icon} + {sc.nameKey ? tr(sc.nameKey) : sc.name} ))} @@ -422,7 +447,9 @@ function AppContent() { onClick={() => setShowCreateScenario(true)} title={tr("scenario.create.button")} > - + + + + {tr("scenario.createChip")}
@@ -510,7 +537,7 @@ function AppContent() { {/* 设备选择器 */}
- +
- 📹 {tr("video.ended")} + {tr("video.ended")} {tr("video.ended.hint")}
diff --git a/frontend/src/components/ChatPanel/index.tsx b/frontend/src/components/ChatPanel/index.tsx index d213195..8bfbd0e 100644 --- a/frontend/src/components/ChatPanel/index.tsx +++ b/frontend/src/components/ChatPanel/index.tsx @@ -4,7 +4,7 @@ // 增强:空状态情景选择卡片、语音输入按钮 // ============================================================ -import { useEffect, useRef, useState } from "react"; +import { ReactNode, useEffect, useRef, useState } from "react"; import { useI18n } from "../../lib/i18n"; import { scenarios } from "../../lib/scenarios"; import type { ChatMessage } from "../../types"; @@ -28,12 +28,13 @@ interface ChatPanelProps { } /** 场景卡片数据(视觉分析快捷) */ -function getSceneCards(t: (key: string) => string) { +function getSceneCards(t: (key: string) => string): { icon: ReactNode; titleKey: string; descKey: string; prompt: string }[] { + const svgProps = { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" as const, strokeLinejoin: "round" as const }; 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") }, + { 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") }, ]; } @@ -124,7 +125,7 @@ export function ChatPanel({ {/* 空状态:欢迎信息 */} {isEmpty && (
- {scenarios.find(s => s.id === activeScenario)?.icon || "💬"} + {scenarios.find(s => s.id === activeScenario)?.icon || }

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

{isConnected ? t("chat.welcome.hint") : t("chat.empty.hint")} @@ -242,7 +243,7 @@ export function ChatPanel({ onClick={onToggleMic} title={t("chat.input.voice")} > - 🎤 + )} )} diff --git a/frontend/src/components/ConfigPanel/index.tsx b/frontend/src/components/ConfigPanel/index.tsx index 00a0cae..f7b84ca 100644 --- a/frontend/src/components/ConfigPanel/index.tsx +++ b/frontend/src/components/ConfigPanel/index.tsx @@ -197,7 +197,7 @@ export function ConfigPanel({ onClick={() => onEditScenario(sc as unknown as UserScenario)} title={t("common.edit")} > - ✏️ +
diff --git a/frontend/src/components/CreateScenarioModal/index.tsx b/frontend/src/components/CreateScenarioModal/index.tsx index 5d15c71..4d45f84 100644 --- a/frontend/src/components/CreateScenarioModal/index.tsx +++ b/frontend/src/components/CreateScenarioModal/index.tsx @@ -6,24 +6,17 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useI18n } from "../../lib/i18n"; import type { CreateScenarioRequest } from "../../lib/api/scenarios"; +import { SCENARIO_ICON_SET, DEFAULT_SCENARIO_ICON_ID } from "../../lib/scenarioIcons"; interface CreateScenarioModalProps { onClose: () => void; onSubmit: (data: CreateScenarioRequest) => Promise; } -// 预设常用图标 -const PRESET_ICONS = [ - "✨", "🎭", "🎨", "🎯", "🎪", "🎬", - "📖", "📚", "📝", "📋", "📌", "📍", - "🔬", "🔭", "🔮", "💡", "💼", "💻", - "🎓", "🎤", "🎵", "🎸", "🎹", "🎺", -]; - export function CreateScenarioModal({ onClose, onSubmit }: CreateScenarioModalProps) { const { t } = useI18n(); const [name, setName] = useState(""); - const [icon, setIcon] = useState("✨"); + const [icon, setIcon] = useState(DEFAULT_SCENARIO_ICON_ID); const [description, setDescription] = useState(""); const [prompt, setPrompt] = useState(""); const [greeting, setGreeting] = useState(""); @@ -134,14 +127,15 @@ export function CreateScenarioModal({ onClose, onSubmit }: CreateScenarioModalPr
- {PRESET_ICONS.map((ic) => ( + {SCENARIO_ICON_SET.map((ic) => ( ))}
diff --git a/frontend/src/components/EditScenarioModal/index.tsx b/frontend/src/components/EditScenarioModal/index.tsx index 1f6aad0..1aec034 100644 --- a/frontend/src/components/EditScenarioModal/index.tsx +++ b/frontend/src/components/EditScenarioModal/index.tsx @@ -6,6 +6,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useI18n } from "../../lib/i18n"; import type { UpdateScenarioRequest, UserScenario } from "../../lib/api/scenarios"; +import { SCENARIO_ICON_SET } from "../../lib/scenarioIcons"; interface EditScenarioModalProps { scenario: UserScenario; @@ -13,14 +14,6 @@ interface EditScenarioModalProps { onSubmit: (id: string, data: UpdateScenarioRequest) => Promise; } -// 预设常用图标 -const PRESET_ICONS = [ - "✨", "🎭", "🎨", "🎯", "🎪", "🎬", - "📖", "📚", "📝", "📋", "📌", "📍", - "🔬", "🔭", "🔮", "💡", "💼", "💻", - "🎓", "🎤", "🎵", "🎸", "🎹", "🎺", -]; - export function EditScenarioModal({ scenario, onClose, onSubmit }: EditScenarioModalProps) { const { t } = useI18n(); const [name, setName] = useState(scenario.name); @@ -135,14 +128,27 @@ export function EditScenarioModal({ scenario, onClose, onSubmit }: EditScenarioM
- {PRESET_ICONS.map((ic) => ( + {/* 如果当前图标是旧版 emoji(不在 SVG 图标集中),显示为可选项 */} + {!SCENARIO_ICON_SET.some((ic) => ic.id === icon) && ( + )} + {SCENARIO_ICON_SET.map((ic) => ( + ))}
diff --git a/frontend/src/components/LandingPage/index.tsx b/frontend/src/components/LandingPage/index.tsx index 9a94714..dc63f11 100644 --- a/frontend/src/components/LandingPage/index.tsx +++ b/frontend/src/components/LandingPage/index.tsx @@ -180,13 +180,13 @@ export function LandingPage() {
{[ - { icon: "📷", label: "摄像头采集" }, - { icon: "🧠", label: "边缘预处理" }, - { icon: "🔌", label: "WebSocket" }, - { icon: "⚡", label: "Eino 编排" }, - { icon: "👁️", label: "视觉理解" }, - { icon: "💬", label: "流式回复" }, - { icon: "🔊", label: "语音输出" }, + { icon: , label: "摄像头采集" }, + { icon: , label: "边缘预处理" }, + { icon: , label: "WebSocket" }, + { icon: , label: "Eino 编排" }, + { icon: , label: "视觉理解" }, + { icon: , label: "流式回复" }, + { icon: , label: "语音输出" }, ].map((step, i, arr) => (
@@ -212,27 +212,27 @@ export function LandingPage() {
{[ { - num: "01", icon: "⚡", + num: "01", icon: , title: "流式并行推送", desc: "LLM 文本流与 TTS 音频流并行输出。用户先看到文字,紧接着听到语音,感知延迟低于 0.5 秒,接近真人对话节奏。", }, { - num: "02", icon: "🧠", + num: "02", icon: , title: "声明式 AI 编排", desc: "基于 CloudWeGo Eino Graph 的 7 节点 DAG 流水线(STT → History → ChatModel → Splitter → TTS),类型安全、可扩展、易测试。", }, { - num: "03", icon: "💰", + num: "03", icon: , title: "端云协同降本", desc: "浏览器端 VAD 语音检测 + 关键帧像素比较 + 混合采样策略,节省 70% 带宽,月成本从 $5,000 降至 $300,降幅 90%。", }, { - num: "04", icon: "🎯", + num: "04", icon: , title: "多场景智能模式", desc: "5 种 AI 角色(自由对话 / 模拟面试 / 英语老师 / 辩论对手 / 同声翻译)× 3 种视觉模式 × 观察模式,灵活覆盖学习与工作。", }, { - num: "05", icon: "🏗️", + num: "05", icon: , title: "生产级工程架构", desc: "三级存储自动降级(Memory → Redis → PostgreSQL)、JWT 双 token 认证、Docker Compose 一键部署、完善的错误处理与降级策略。", }, @@ -259,11 +259,11 @@ export function LandingPage() {
{[ - { avatar: "🧑‍🎓", title: "语言学习者", desc: "对着课本或实物,与 AI 英语外教用英语自由对话,实时纠正语法和发音" }, - { avatar: "💼", title: "面试准备者", desc: "开启模拟面试模式,AI 面试官通过摄像头观察你的表情与状态,给出针对性反馈" }, - { avatar: "🌍", title: "跨境交流者", desc: "出国旅行时对着外文菜单、路牌实时翻译,AI 语音播报翻译结果" }, - { avatar: "👁️", title: "视障人士", desc: "AI 实时描述摄像头画面中的环境、障碍物和文字,提供无障碍信息辅助" }, - { avatar: "🔬", title: "学生 / 教师", desc: "对着题目问「怎么做?」,AI 看到画面后逐步讲解,就像身边有一位私教" }, + { avatar: , title: "语言学习者", desc: "对着课本或实物,与 AI 英语外教用英语自由对话,实时纠正语法和发音" }, + { avatar: , title: "面试准备者", desc: "开启模拟面试模式,AI 面试官通过摄像头观察你的表情与状态,给出针对性反馈" }, + { avatar: , title: "跨境交流者", desc: "出国旅行时对着外文菜单、路牌实时翻译,AI 语音播报翻译结果" }, + { avatar: , title: "视障人士", desc: "AI 实时描述摄像头画面中的环境、障碍物和文字,提供无障碍信息辅助" }, + { avatar: , title: "学生 / 教师", desc: "对着题目问「怎么做?」,AI 看到画面后逐步讲解,就像身边有一位私教" }, ].map((u) => (
{u.avatar}
@@ -283,11 +283,11 @@ export function LandingPage() {
{[ - { icon: "💬", title: "自由对话", desc: "对着摄像头随意聊天,AI 实时理解画面并语音回答", tag: "通用" }, - { icon: "🗣️", title: "英语老师", desc: "AI 外教结合摄像头场景进行英语口语教学,实时纠正语法", tag: "学习" }, - { icon: "🎤", title: "模拟面试", desc: "AI 面试官根据你的回答追问,通过摄像头观察你的表现", tag: "求职" }, - { icon: "⚔️", title: "辩论对手", desc: "AI 反驳你的观点,锻炼你的逻辑思维和表达能力", tag: "思维" }, - { icon: "🌐", title: "同声翻译", desc: "实时识别画面中的外语文字并语音翻译,口语化输出", tag: "工具" }, + { icon: , title: "自由对话", desc: "对着摄像头随意聊天,AI 实时理解画面并语音回答", tag: "通用" }, + { icon: , title: "英语老师", desc: "AI 外教结合摄像头场景进行英语口语教学,实时纠正语法", tag: "学习" }, + { icon: , title: "模拟面试", desc: "AI 面试官根据你的回答追问,通过摄像头观察你的表现", tag: "求职" }, + { icon: , title: "辩论对手", desc: "AI 反驳你的观点,锻炼你的逻辑思维和表达能力", tag: "思维" }, + { icon: , title: "同声翻译", desc: "实时识别画面中的外语文字并语音翻译,口语化输出", tag: "工具" }, ].map((s) => (
{s.icon}
diff --git a/frontend/src/components/SessionSidebar/index.tsx b/frontend/src/components/SessionSidebar/index.tsx index 74de42d..560b1a9 100644 --- a/frontend/src/components/SessionSidebar/index.tsx +++ b/frontend/src/components/SessionSidebar/index.tsx @@ -187,7 +187,7 @@ export function SessionSidebar({ <> {/* 视频标记图标 */} {session.messageCount > 5 && ( - 📹 + )}
{session.title}
@@ -207,7 +207,7 @@ export function SessionSidebar({ }} title={t("sidebar.rename")} > - ✏ +
diff --git a/frontend/src/lib/i18n/zh-CN.ts b/frontend/src/lib/i18n/zh-CN.ts index fd60598..2a40cea 100644 --- a/frontend/src/lib/i18n/zh-CN.ts +++ b/frontend/src/lib/i18n/zh-CN.ts @@ -60,8 +60,8 @@ export const zhCN: TranslationMap = { // Controls "controls.connecting": "连接中...", - "controls.start": "🎙️ 开始对话", - "controls.startVideo": "🎙️ 开始视频通话", + "controls.start": "开始对话", + "controls.startVideo": "开始视频通话", "controls.cameraOff": "关闭摄像头", "controls.cameraOn": "开启摄像头", "controls.micOff": "关闭麦克风", @@ -70,7 +70,7 @@ export const zhCN: TranslationMap = { "controls.stop": "结束对话", "controls.stopVideo": "结束视频", "controls.endSession": "结束会话", - "controls.resumeVideo": "📹 重新开始视频", + "controls.resumeVideo": "重新开始视频", // Chat panel "chat.title": "对话", diff --git a/frontend/src/lib/scenarioIcons.tsx b/frontend/src/lib/scenarioIcons.tsx new file mode 100644 index 0000000..b7d830f --- /dev/null +++ b/frontend/src/lib/scenarioIcons.tsx @@ -0,0 +1,124 @@ +// ============================================================ +// scenarioIcons — 自建情景 SVG 图标集(Feather-style) +// 职责:为 CreateScenarioModal / EditScenarioModal 提供图标选项 +// 为情景芯片条渲染自建情景的 SVG 图标 +// ============================================================ + +import React from "react"; + +const P = { viewBox: "0 0 24 24", fill: "none" as const, stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" as const, strokeLinejoin: "round" as const }; + +export interface ScenarioIconDef { + id: string; + label: string; + render: (size?: number) => React.ReactElement; +} + +/** 自建情景可选的 Feather SVG 图标集 */ +export const SCENARIO_ICON_SET: ScenarioIconDef[] = [ + { + id: "sparkles", label: "通用助手", + render: (s = 16) => (), + }, + { + id: "theater", label: "角色扮演", + render: (s = 16) => (), + }, + { + id: "palette", label: "创意设计", + render: (s = 16) => (), + }, + { + id: "target", label: "目标导向", + render: (s = 16) => (), + }, + { + id: "film", label: "影视娱乐", + render: (s = 16) => (), + }, + { + id: "book-open", label: "阅读学习", + render: (s = 16) => (), + }, + { + id: "book", label: "教程指南", + render: (s = 16) => (), + }, + { + id: "edit", label: "写作编辑", + render: (s = 16) => (), + }, + { + id: "clipboard", label: "任务管理", + render: (s = 16) => (), + }, + { + id: "map-pin", label: "导航定位", + render: (s = 16) => (), + }, + { + id: "search", label: "搜索探索", + render: (s = 16) => (), + }, + { + id: "lightbulb", label: "灵感启发", + render: (s = 16) => (), + }, + { + id: "briefcase", label: "商务办公", + render: (s = 16) => (), + }, + { + id: "code", label: "编程开发", + render: (s = 16) => (), + }, + { + id: "graduation-cap", label: "教育培训", + render: (s = 16) => (), + }, + { + id: "mic", label: "语音对话", + render: (s = 16) => (), + }, + { + id: "music", label: "音乐艺术", + render: (s = 16) => (), + }, + { + id: "globe", label: "翻译跨语言", + render: (s = 16) => (), + }, + { + id: "heart", label: "健康关怀", + render: (s = 16) => (), + }, + { + id: "shield", label: "安全防护", + render: (s = 16) => (), + }, + { + id: "zap", label: "快速高效", + render: (s = 16) => (), + }, + { + id: "coffee", label: "休闲聊天", + render: (s = 16) => (), + }, + { + id: "tool", label: "工具助手", + render: (s = 16) => (), + }, + { + id: "message-circle", label: "对话问答", + render: (s = 16) => (), + }, +]; + +/** 根据图标 ID 渲染 SVG,找不到返回 null */ +export function renderScenarioIconById(iconId: string, size = 14): React.ReactElement | null { + const def = SCENARIO_ICON_SET.find((d) => d.id === iconId); + return def ? def.render(size) : null; +} + +/** 默认图标 ID */ +export const DEFAULT_SCENARIO_ICON_ID = "sparkles";