refactor: 将情景图标和设备选择器的 emoji 统一替换为 SVG 图标

- 新增 scenarioIcons.tsx 提取情景图标渲染逻辑
- 情景芯片、设备选择器、警告提示等 emoji 全部改为 Feather-style SVG
- 调整 CSS 确保 SVG 图标居中对齐
This commit is contained in:
2026-06-21 23:50:27 +08:00
parent 6cbabb63bb
commit e24fb2cc19
10 changed files with 232 additions and 75 deletions

View File

@@ -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: <svg {...svgProps}><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>, titleKey: "scene.describe", descKey: "scene.describe.desc", prompt: t("scene.describe") },
{ icon: <svg {...svgProps}><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg>, titleKey: "scene.text", descKey: "scene.text.desc", prompt: t("scene.text") },
{ icon: <svg {...svgProps}><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>, titleKey: "scene.object", descKey: "scene.object.desc", prompt: t("scene.object") },
{ icon: <svg {...svgProps}><line x1="9" y1="18" x2="15" y2="18"/><line x1="10" y1="22" x2="14" y2="22"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>, titleKey: "scene.suggest", descKey: "scene.suggest.desc", prompt: t("scene.suggest") },
];
}
@@ -124,7 +125,7 @@ export function ChatPanel({
{/* 空状态:欢迎信息 */}
{isEmpty && (
<div className="chat-panel__welcome">
<span className="chat-panel__welcome-icon">{scenarios.find(s => s.id === activeScenario)?.icon || "💬"}</span>
<span className="chat-panel__welcome-icon">{scenarios.find(s => s.id === activeScenario)?.icon || <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>}</span>
<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>
@@ -242,7 +243,7 @@ export function ChatPanel({
onClick={onToggleMic}
title={t("chat.input.voice")}
>
🎤
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
</button>
)}
<button
@@ -251,7 +252,7 @@ export function ChatPanel({
disabled={!inputText.trim() || connectionStatus === "connecting"}
title={t("chat.send")}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</form>
)}

View File

@@ -197,7 +197,7 @@ export function ConfigPanel({
onClick={() => onEditScenario(sc as unknown as UserScenario)}
title={t("common.edit")}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
<button
className={`scenario-action-btn scenario-action-btn--delete ${
@@ -206,7 +206,7 @@ export function ConfigPanel({
onClick={() => handleDelete(sc.id)}
title={deleteConfirm === sc.id ? t("common.confirmDelete") : t("common.delete")}
>
{deleteConfirm === sc.id ? "✓" : "🗑"}
{deleteConfirm === sc.id ? "✓" : <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>}
</button>
</div>
</div>

View File

@@ -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<void>;
}
// 预设常用图标
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
<div className="form-group">
<label className="form-label">{t("scenario.create.icon")}</label>
<div className="icon-picker">
{PRESET_ICONS.map((ic) => (
{SCENARIO_ICON_SET.map((ic) => (
<button
key={ic}
key={ic.id}
type="button"
className={`icon-picker__item ${icon === ic ? "icon-picker__item--active" : ""}`}
onClick={() => setIcon(ic)}
className={`icon-picker__item ${icon === ic.id ? "icon-picker__item--active" : ""}`}
onClick={() => setIcon(ic.id)}
title={ic.label}
>
{ic}
{ic.render(22)}
</button>
))}
</div>

View File

@@ -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<void>;
}
// 预设常用图标
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
<div className="form-group">
<label className="form-label">{t("scenario.create.icon")}</label>
<div className="icon-picker">
{PRESET_ICONS.map((ic) => (
{/* 如果当前图标是旧版 emoji不在 SVG 图标集中),显示为可选项 */}
{!SCENARIO_ICON_SET.some((ic) => ic.id === icon) && (
<button
key={ic}
key="__legacy"
type="button"
className={`icon-picker__item ${icon === ic ? "icon-picker__item--active" : ""}`}
onClick={() => setIcon(ic)}
className={`icon-picker__item icon-picker__item--active`}
onClick={() => {/* already selected, no-op */}}
title="当前图标(旧版)"
>
{ic}
{icon}
</button>
)}
{SCENARIO_ICON_SET.map((ic) => (
<button
key={ic.id}
type="button"
className={`icon-picker__item ${icon === ic.id ? "icon-picker__item--active" : ""}`}
onClick={() => setIcon(ic.id)}
title={ic.label}
>
{ic.render(22)}
</button>
))}
</div>

View File

@@ -180,13 +180,13 @@ export function LandingPage() {
<div className="lp-flow-wrapper lp-fade-in">
<div className="lp-flow-steps">
{[
{ icon: "📷", label: "摄像头采集" },
{ icon: "🧠", label: "边缘预处理" },
{ icon: "🔌", label: "WebSocket" },
{ icon: "⚡", label: "Eino 编排" },
{ icon: "👁️", label: "视觉理解" },
{ icon: "💬", label: "流式回复" },
{ icon: "🔊", label: "语音输出" },
{ icon: <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>, label: "摄像头采集" },
{ icon: <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M12 2a5 5 0 0 1 5 5c0 1.5-.7 2.8-1.7 3.7L12 14l-3.3-3.3A5 5 0 0 1 12 2z"/><path d="M12 14v8"/><path d="M8 18h8"/></svg>, label: "边缘预处理" },
{ icon: <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>, label: "WebSocket" },
{ icon: <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>, label: "Eino 编排" },
{ icon: <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>, label: "视觉理解" },
{ icon: <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>, label: "流式回复" },
{ icon: <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>, label: "语音输出" },
].map((step, i, arr) => (
<div key={i} style={{ display: "contents" }}>
<div className="lp-flow-step">
@@ -212,27 +212,27 @@ export function LandingPage() {
<div className="lp-features-grid">
{[
{
num: "01", icon: "⚡",
num: "01", icon: <svg width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>,
title: "流式并行推送",
desc: "LLM 文本流与 TTS 音频流并行输出。用户先看到文字,紧接着听到语音,感知延迟低于 0.5 秒,接近真人对话节奏。",
},
{
num: "02", icon: "🧠",
num: "02", icon: <svg width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M12 2a5 5 0 0 1 5 5c0 1.5-.7 2.8-1.7 3.7L12 14l-3.3-3.3A5 5 0 0 1 12 2z"/><path d="M12 14v8"/><path d="M8 18h8"/></svg>,
title: "声明式 AI 编排",
desc: "基于 CloudWeGo Eino Graph 的 7 节点 DAG 流水线STT → History → ChatModel → Splitter → TTS类型安全、可扩展、易测试。",
},
{
num: "03", icon: "💰",
num: "03", icon: <svg width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>,
title: "端云协同降本",
desc: "浏览器端 VAD 语音检测 + 关键帧像素比较 + 混合采样策略,节省 70% 带宽,月成本从 $5,000 降至 $300降幅 90%。",
},
{
num: "04", icon: "🎯",
num: "04", icon: <svg width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>,
title: "多场景智能模式",
desc: "5 种 AI 角色(自由对话 / 模拟面试 / 英语老师 / 辩论对手 / 同声翻译)× 3 种视觉模式 × 观察模式,灵活覆盖学习与工作。",
},
{
num: "05", icon: "🏗️",
num: "05", icon: <svg width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><rect x="4" y="2" width="16" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12" y2="18"/><line x1="12" y1="6" x2="12" y2="6"/><line x1="4" y1="10" x2="20" y2="10"/><line x1="8" y1="2" x2="8" y2="10"/><line x1="16" y1="10" x2="16" y2="18"/></svg>,
title: "生产级工程架构",
desc: "三级存储自动降级Memory → Redis → PostgreSQL、JWT 双 token 认证、Docker Compose 一键部署、完善的错误处理与降级策略。",
},
@@ -259,11 +259,11 @@ export function LandingPage() {
</div>
<div className="lp-users-grid">
{[
{ avatar: "🧑‍🎓", title: "语言学习者", desc: "对着课本或实物,与 AI 英语外教用英语自由对话,实时纠正语法和发音" },
{ avatar: "💼", title: "面试准备者", desc: "开启模拟面试模式AI 面试官通过摄像头观察你的表情与状态,给出针对性反馈" },
{ avatar: "🌍", title: "跨境交流者", desc: "出国旅行时对着外文菜单、路牌实时翻译AI 语音播报翻译结果" },
{ avatar: "👁️", title: "视障人士", desc: "AI 实时描述摄像头画面中的环境、障碍物和文字,提供无障碍信息辅助" },
{ avatar: "🔬", title: "学生 / 教师", desc: "对着题目问「怎么做AI 看到画面后逐步讲解,就像身边有一位私教" },
{ avatar: <svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>, title: "语言学习者", desc: "对着课本或实物,与 AI 英语外教用英语自由对话,实时纠正语法和发音" },
{ avatar: <svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>, title: "面试准备者", desc: "开启模拟面试模式AI 面试官通过摄像头观察你的表情与状态,给出针对性反馈" },
{ avatar: <svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>, title: "跨境交流者", desc: "出国旅行时对着外文菜单、路牌实时翻译AI 语音播报翻译结果" },
{ avatar: <svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>, title: "视障人士", desc: "AI 实时描述摄像头画面中的环境、障碍物和文字,提供无障碍信息辅助" },
{ avatar: <svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>, title: "学生 / 教师", desc: "对着题目问「怎么做AI 看到画面后逐步讲解,就像身边有一位私教" },
].map((u) => (
<div className="lp-user-card lp-fade-in" key={u.title}>
<div className="lp-user-card__avatar">{u.avatar}</div>
@@ -283,11 +283,11 @@ export function LandingPage() {
</div>
<div className="lp-scenes-list">
{[
{ 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: <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>, title: "自由对话", desc: "对着摄像头随意聊天AI 实时理解画面并语音回答", tag: "通用" },
{ icon: <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>, title: "英语老师", desc: "AI 外教结合摄像头场景进行英语口语教学,实时纠正语法", tag: "学习" },
{ icon: <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>, title: "模拟面试", desc: "AI 面试官根据你的回答追问,通过摄像头观察你的表现", tag: "求职" },
{ icon: <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M8 10h8"/><path d="M8 14h4"/></svg>, title: "辩论对手", desc: "AI 反驳你的观点,锻炼你的逻辑思维和表达能力", tag: "思维" },
{ icon: <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>, title: "同声翻译", desc: "实时识别画面中的外语文字并语音翻译,口语化输出", tag: "工具" },
].map((s) => (
<div className="lp-scene-row lp-fade-in" key={s.title}>
<div className="lp-scene-row__icon">{s.icon}</div>

View File

@@ -187,7 +187,7 @@ export function SessionSidebar({
<>
{/* 视频标记图标 */}
{session.messageCount > 5 && (
<span className="sidebar__item-icon" title={t("sidebar.video")}>📹</span>
<span className="sidebar__item-icon" title={t("sidebar.video")}><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg></span>
)}
<div className="sidebar__item-content">
<div className="sidebar__item-title">{session.title}</div>
@@ -207,7 +207,7 @@ export function SessionSidebar({
}}
title={t("sidebar.rename")}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
<button
className="sidebar__action-btn sidebar__action-btn--danger"
@@ -217,7 +217,7 @@ export function SessionSidebar({
}}
title={t("sidebar.delete")}
>
🗑
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
</div>
</>