From 949a707e0fdfa9685993342edfb62fc470dcae6f Mon Sep 17 00:00:00 2001 From: cfy666 <3087823110@qq.com> Date: Sun, 21 Jun 2026 22:58:02 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E9=9D=A2=E6=9D=BF=20UI=EF=BC=8C=E6=83=85=E6=99=AF?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E6=94=B9=E4=B8=BA=E5=8D=A1=E7=89=87=E7=BD=91?= =?UTF-8?q?=E6=A0=BC=EF=BC=8C=E6=8E=A7=E5=88=B6=E6=8C=89=E9=92=AE=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=20SVG=20=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 情景选择从 ChatPanel header 下拉菜单迁移至视频面板下方卡片网格 - 摄像头/麦克风按钮改为圆形 SVG 图标按钮 - 识别/中断/停止按钮改为药丸形状 SVG 图标按钮 - 设备选择器改为水平布局,优化空间利用 - 移除 ChatPanel 空状态中重复的情景卡片 - .gitignore 忽略 .claudian/ 和笔记目录 --- .gitignore | 4 +- frontend/src/App.css | 250 ++++++++++++++++++-- frontend/src/App.tsx | 86 +++++-- frontend/src/components/ChatPanel/index.tsx | 24 +- 4 files changed, 306 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index 2427d71..ee73c2d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ Thumbs.db # ---- Obsidian ---- .obsidian/ -.claudian/sessions/conv-1781943335504-q62bzosye.meta.json +.claudian/ +修改过程笔记/ +学习复盘/ diff --git a/frontend/src/App.css b/frontend/src/App.css index e4e9938..c5fe341 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -495,7 +495,10 @@ body { display: flex; flex-direction: column; padding: 16px 20px; - gap: 10px; + gap: 12px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--color-border) transparent; } .video-container { @@ -652,7 +655,7 @@ body { flex-direction: column; align-items: center; gap: 10px; - padding: 8px 0; + padding: 10px 0 4px; } .video-controls__row { @@ -1387,11 +1390,228 @@ body { .video-controls__toolbar { display: flex; - gap: 6px; + gap: 8px; flex-wrap: wrap; justify-content: center; + align-items: center; } +/* ---- Round Control Buttons (Camera / Mic) ---- */ + +.btn-ctrl-round { + width: 44px; + height: 44px; + border-radius: 50%; + border: 2px solid var(--color-border); + background: var(--color-surface-2); + color: var(--color-text-muted); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: all var(--transition-fast); + flex-shrink: 0; + position: relative; +} + +.btn-ctrl-round:hover { + background: var(--color-surface-3); + border-color: var(--color-surface-3); + color: var(--color-text); + transform: scale(1.06); +} + +.btn-ctrl-round:active { + transform: scale(0.95); +} + +.btn-ctrl-round--on { + background: rgba(52, 211, 153, 0.1); + border-color: rgba(52, 211, 153, 0.3); + color: var(--color-success); +} + +.btn-ctrl-round--on:hover { + background: rgba(52, 211, 153, 0.18); + border-color: rgba(52, 211, 153, 0.45); +} + +.btn-ctrl-round--off { + background: rgba(248, 113, 113, 0.08); + border-color: rgba(248, 113, 113, 0.2); + color: var(--color-error); +} + +.btn-ctrl-round--off:hover { + background: rgba(248, 113, 113, 0.15); + border-color: rgba(248, 113, 113, 0.35); +} + +.btn-ctrl-round--speaking { + animation: micPulse 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite; + box-shadow: 0 0 14px rgba(52, 211, 153, 0.45); +} + +/* ---- Pill Control Buttons (Recognize / Interrupt / Stop) ---- */ + +.btn-ctrl-pill { + padding: 8px 16px; + border-radius: 22px; + font-size: 0.78rem; + font-weight: 500; + cursor: pointer; + transition: all var(--transition-fast); + display: inline-flex; + align-items: center; + gap: 5px; + border: 1px solid var(--color-border); + background: var(--color-surface-2); + color: var(--color-text-muted); + white-space: nowrap; +} + +.btn-ctrl-pill:hover:not(:disabled) { + background: var(--color-surface-3); + color: var(--color-text); + border-color: var(--color-surface-3); +} + +.btn-ctrl-pill:active:not(:disabled) { + transform: translateY(1px); +} + +.btn-ctrl-pill:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-ctrl-pill--recognize { + background: rgba(59, 130, 246, 0.1); + color: var(--color-primary); + border-color: rgba(59, 130, 246, 0.25); +} + +.btn-ctrl-pill--recognize:hover:not(:disabled) { + background: rgba(59, 130, 246, 0.2); + border-color: rgba(59, 130, 246, 0.4); +} + +.btn-ctrl-pill--interrupt { + background: rgba(251, 191, 36, 0.1); + color: var(--color-warning); + border-color: rgba(251, 191, 36, 0.25); +} + +.btn-ctrl-pill--interrupt:hover:not(:disabled) { + background: rgba(251, 191, 36, 0.2); + border-color: rgba(251, 191, 36, 0.4); +} + +.btn-ctrl-pill--stop { + background: rgba(248, 113, 113, 0.1); + color: var(--color-error); + border-color: rgba(248, 113, 113, 0.2); +} + +.btn-ctrl-pill--stop:hover:not(:disabled) { + background: rgba(248, 113, 113, 0.18); + border-color: rgba(248, 113, 113, 0.35); +} + +/* ---- Scenario Picker Grid (video panel) ---- */ + +.scenario-picker { + display: flex; + flex-direction: column; + gap: 8px; +} + +.scenario-picker__title { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 2px; +} + +.scenario-picker__label { + font-size: 0.65rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--color-text-muted); + opacity: 0.7; +} + +.scenario-picker__active { + font-size: 0.7rem; + color: var(--color-primary); + font-weight: 500; +} + +.scenario-picker__grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 6px; +} + +.scenario-picker__card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + padding: 10px 4px; + border-radius: var(--radius-sm); + border: 1px solid var(--color-border); + background: var(--color-surface-2); + color: var(--color-text-muted); + cursor: pointer; + transition: all var(--transition-fast); + text-align: center; + min-width: 0; +} + +.scenario-picker__card:hover { + background: var(--color-surface-3); + border-color: var(--color-surface-3); + color: var(--color-text); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(10, 10, 15, 0.15); +} + +.scenario-picker__card:active { + transform: translateY(0); +} + +.scenario-picker__card--active { + background: rgba(59, 130, 246, 0.08); + border-color: rgba(59, 130, 246, 0.35); + color: var(--color-primary); +} + +.scenario-picker__card--active:hover { + background: rgba(59, 130, 246, 0.14); + border-color: rgba(59, 130, 246, 0.45); + color: var(--color-primary); +} + +.scenario-picker__card-icon { + font-size: 1.3rem; + line-height: 1; +} + +.scenario-picker__card-name { + font-size: 0.68rem; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + letter-spacing: 0.01em; +} + +/* ---- Legacy button styles kept for backward compat ---- */ + .btn--recognize { background: rgba(59, 130, 246, 0.1); color: var(--color-primary); @@ -1416,35 +1636,37 @@ body { /* Device selectors */ .video-controls__devices { display: flex; - gap: 12px; + gap: 8px; justify-content: center; + width: 100%; } .device-select-wrapper { display: flex; - flex-direction: column; - gap: 4px; align-items: center; + gap: 6px; + flex: 1; + min-width: 0; } .device-select-label { - font-size: 0.65rem; + font-size: 0.72rem; color: var(--color-text-muted); - font-weight: 500; - letter-spacing: 0.01em; + flex-shrink: 0; } .device-select { - padding: 5px 10px; - font-size: 0.72rem; - border-radius: var(--radius-sm); + flex: 1; + min-width: 0; + padding: 5px 8px; + font-size: 0.7rem; + border-radius: 6px; border: 1px solid var(--color-border); background: var(--color-surface-2); color: var(--color-text-muted); cursor: pointer; transition: all var(--transition-fast); - min-width: 120px; - text-align: center; + text-overflow: ellipsis; } .device-select:hover { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 89e84d6..09b1910 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -401,6 +401,27 @@ function AppContent() { )} + {/* ---- 情景选择卡片网格(视频下方) ---- */} +
+
+ {tr("settings.scenario")} + {activeScenario.icon} {activeScenario.nameKey ? tr(activeScenario.nameKey) : activeScenario.name} +
+
+ {allScenarios.map((sc) => ( + + ))} +
+
+ {/* 视频下方控制区(三层结构) */}
{!isConnected ? ( @@ -415,33 +436,68 @@ function AppContent() { {/* 视频通话态:核心控制工具栏 */}
- {/* 识别画面按钮 */} {isProcessing && ( - )} -
@@ -502,18 +558,6 @@ function AppContent() {
{tr("chat.title")}
- {isConnected && stats.queryCount > 0 && ( {stats.queryCount} {tr("statusbar.recognitions")} diff --git a/frontend/src/components/ChatPanel/index.tsx b/frontend/src/components/ChatPanel/index.tsx index a0e2d8c..d213195 100644 --- a/frontend/src/components/ChatPanel/index.tsx +++ b/frontend/src/components/ChatPanel/index.tsx @@ -121,34 +121,14 @@ export function ChatPanel({
)} - {/* 空状态:情景选择 + 场景卡片 */} + {/* 空状态:欢迎信息 */} {isEmpty && (
{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")} - {/* 情景选择卡片(非 free_chat 时隐藏,因为已通过 header 切换) */} - {isFreeChat && onSelectScenario && ( -
-
{t("scenario.choose")}
- {scenarios.filter(s => s.id !== "free_chat").map((sc) => ( - - ))} -
- )} - - {/* 视觉分析快捷卡片(仅 free_chat 模式) */} + {/* 视觉分析快捷卡片(仅 free_chat 模式 + 已连接) */} {isFreeChat && isConnected && (
{sceneCards.map((card) => ( From 6cbabb63bbd37b07f6bb1673ac9c28a4b22db01f Mon Sep 17 00:00:00 2001 From: cfy666 <3087823110@qq.com> Date: Sun, 21 Jun 2026 23:23:22 +0800 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=E6=83=85=E6=99=AF=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E4=BB=8E=E5=8D=A1=E7=89=87=E7=BD=91=E6=A0=BC=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=A8=AA=E5=90=91=E6=BB=9A=E5=8A=A8=E8=8A=AF=E7=89=87?= =?UTF-8?q?=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 scenario-picker 网格布局改为 scenario-strip 横向滚动芯片条 - 新增「新建情景」快捷入口芯片(+按钮) - 新增 scenario.createChip 三语言翻译键 --- frontend/src/App.css | 120 ++++++++++++++++++++------------- frontend/src/App.tsx | 26 ++++--- frontend/src/lib/i18n/en-US.ts | 1 + frontend/src/lib/i18n/ja-JP.ts | 1 + frontend/src/lib/i18n/zh-CN.ts | 1 + 5 files changed, 93 insertions(+), 56 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index c5fe341..e6ad6e2 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1518,95 +1518,123 @@ body { border-color: rgba(248, 113, 113, 0.35); } -/* ---- Scenario Picker Grid (video panel) ---- */ +/* ---- Scenario Chip Strip (horizontal scroll, video panel) ---- */ -.scenario-picker { +.scenario-strip { display: flex; flex-direction: column; gap: 8px; + flex-shrink: 0; } -.scenario-picker__title { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 2px; -} - -.scenario-picker__label { +.scenario-strip__label { font-size: 0.65rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-muted); opacity: 0.7; + padding: 0 2px; } -.scenario-picker__active { - font-size: 0.7rem; - color: var(--color-primary); - font-weight: 500; -} - -.scenario-picker__grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 6px; -} - -.scenario-picker__card { +.scenario-strip__scroll { display: flex; - flex-direction: column; + gap: 6px; + overflow-x: auto; + padding-bottom: 4px; + /* Thin scrollbar - visible but unobtrusive */ + scrollbar-width: thin; + scrollbar-color: var(--color-border) transparent; +} + +.scenario-strip__scroll::-webkit-scrollbar { + height: 3px; +} + +.scenario-strip__scroll::-webkit-scrollbar-track { + background: transparent; +} + +.scenario-strip__scroll::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 2px; +} + +.scenario-strip__scroll:hover::-webkit-scrollbar-thumb { + background: var(--color-text-muted); +} + +/* Individual scenario chip */ +.scenario-chip { + display: inline-flex; align-items: center; - justify-content: center; - gap: 4px; - padding: 10px 4px; - border-radius: var(--radius-sm); + gap: 5px; + padding: 7px 12px; + border-radius: 20px; border: 1px solid var(--color-border); background: var(--color-surface-2); color: var(--color-text-muted); cursor: pointer; transition: all var(--transition-fast); - text-align: center; - min-width: 0; + white-space: nowrap; + flex-shrink: 0; + font-size: 0.74rem; + font-weight: 500; + line-height: 1; } -.scenario-picker__card:hover { +.scenario-chip:hover { background: var(--color-surface-3); border-color: var(--color-surface-3); color: var(--color-text); transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(10, 10, 15, 0.15); + box-shadow: 0 2px 6px rgba(10, 10, 15, 0.12); } -.scenario-picker__card:active { +.scenario-chip:active { transform: translateY(0); } -.scenario-picker__card--active { - background: rgba(59, 130, 246, 0.08); +/* Active chip */ +.scenario-chip--active { + background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.35); color: var(--color-primary); } -.scenario-picker__card--active:hover { - background: rgba(59, 130, 246, 0.14); +.scenario-chip--active:hover { + background: rgba(59, 130, 246, 0.16); border-color: rgba(59, 130, 246, 0.45); color: var(--color-primary); } -.scenario-picker__card-icon { - font-size: 1.3rem; +/* "+" create new scenario chip */ +.scenario-chip--add { + border-style: dashed; + opacity: 0.7; +} + +.scenario-chip--add:hover { + opacity: 1; + border-style: solid; + border-color: var(--color-primary); + color: var(--color-primary); + background: rgba(59, 130, 246, 0.06); +} + +.scenario-chip--add .scenario-chip__icon { + font-size: 0.88rem; + font-weight: 300; line-height: 1; } -.scenario-picker__card-name { - font-size: 0.68rem; - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; +.scenario-chip__icon { + font-size: 0.9rem; + line-height: 1; + flex-shrink: 0; +} + +.scenario-chip__name { letter-spacing: 0.01em; } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 09b1910..d485be6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -401,24 +401,30 @@ function AppContent() { )}
- {/* ---- 情景选择卡片网格(视频下方) ---- */} -
-
- {tr("settings.scenario")} - {activeScenario.icon} {activeScenario.nameKey ? tr(activeScenario.nameKey) : activeScenario.name} -
-
+ {/* ---- 情景选择芯片条(视频下方) ---- */} +
+ {tr("settings.scenario")} +
{allScenarios.map((sc) => ( ))} + {/* 新建情景快捷入口 */} +
diff --git a/frontend/src/lib/i18n/en-US.ts b/frontend/src/lib/i18n/en-US.ts index 83bab31..e8dcdab 100644 --- a/frontend/src/lib/i18n/en-US.ts +++ b/frontend/src/lib/i18n/en-US.ts @@ -174,6 +174,7 @@ export const enUS: TranslationMap = { "settings.scenario.custom": "My Scenarios", "settings.scenario.empty": "No custom scenarios yet. Click the button above to create one.", "scenario.create.button": "New Scenario", + "scenario.createChip": "New", "scenario.create.title": "Create Custom Scenario", "scenario.edit.title": "Edit Scenario", "scenario.create.name": "Scenario Name", diff --git a/frontend/src/lib/i18n/ja-JP.ts b/frontend/src/lib/i18n/ja-JP.ts index 770efec..af8b317 100644 --- a/frontend/src/lib/i18n/ja-JP.ts +++ b/frontend/src/lib/i18n/ja-JP.ts @@ -174,6 +174,7 @@ export const jaJP: TranslationMap = { "settings.scenario.custom": "マイシナリオ", "settings.scenario.empty": "カスタムシナリオはまだありません。上のボタンをクリックして作成してください。", "scenario.create.button": "新しいシナリオ", + "scenario.createChip": "新規", "scenario.create.title": "カスタムシナリオを作成", "scenario.edit.title": "シナリオを編集", "scenario.create.name": "シナリオ名", diff --git a/frontend/src/lib/i18n/zh-CN.ts b/frontend/src/lib/i18n/zh-CN.ts index f934f0d..fd60598 100644 --- a/frontend/src/lib/i18n/zh-CN.ts +++ b/frontend/src/lib/i18n/zh-CN.ts @@ -174,6 +174,7 @@ export const zhCN: TranslationMap = { "settings.scenario.custom": "我的情景", "settings.scenario.empty": "还没有自建情景,点击上方按钮创建", "scenario.create.button": "创建新情景", + "scenario.createChip": "新建", "scenario.create.title": "创建自建情景", "scenario.edit.title": "编辑情景", "scenario.create.name": "情景名称", From e24fb2cc19353bb89caf2d2fd529948098c45bb3 Mon Sep 17 00:00:00 2001 From: cfy666 <3087823110@qq.com> Date: Sun, 21 Jun 2026 23:50:27 +0800 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20=E5=B0=86=E6=83=85=E6=99=AF?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E5=92=8C=E8=AE=BE=E5=A4=87=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=99=A8=E7=9A=84=20emoji=20=E7=BB=9F=E4=B8=80=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=E4=B8=BA=20SVG=20=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 scenarioIcons.tsx 提取情景图标渲染逻辑 - 情景芯片、设备选择器、警告提示等 emoji 全部改为 Feather-style SVG - 调整 CSS 确保 SVG 图标居中对齐 --- frontend/src/App.css | 11 +- frontend/src/App.tsx | 39 +++++- frontend/src/components/ChatPanel/index.tsx | 19 +-- frontend/src/components/ConfigPanel/index.tsx | 4 +- .../components/CreateScenarioModal/index.tsx | 22 ++-- .../components/EditScenarioModal/index.tsx | 32 +++-- frontend/src/components/LandingPage/index.tsx | 44 +++---- .../src/components/SessionSidebar/index.tsx | 6 +- frontend/src/lib/i18n/zh-CN.ts | 6 +- frontend/src/lib/scenarioIcons.tsx | 124 ++++++++++++++++++ 10 files changed, 232 insertions(+), 75 deletions(-) create mode 100644 frontend/src/lib/scenarioIcons.tsx 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";