feat(web): 前端收敛为「共享消息流 + 按消息 mode 渲染」,并补全 emoji→MD3 图标
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

架构变更(主人要求,design/07 §1/§2 已改定稿):把「9 个行为模式 = 9 个独立页面」改为
「一个共享页面 + 每条消息绑 mode,消息框按对应 mode 渲染」。
- ChatMessage 增 mode?(types.ts);useAppStore addUserMessage/beginAssistant 带 mode;
  useSendChat.send(text, mode) / chatStream body 带 mode(通知后端按 mode 回复)。
- MessageBubble 按 message.mode 分发到 components/messages/ 各形态渲染器
  (chat 气泡 / code 等宽代码块 / home 设备卡 / docs 文档卡 / work 任务卡 / vision 视觉卡)。
- ChatInput 顶部加「行为模式」chip 切换下一条消息的 mode(默认 chat);输入框上方显示当前模式。
- ModeRouter 收敛为只渲染 ChatShell;删除 9 个旧独立 mode 页面;Nav/TopBar 的 mode 改为
  「输入模式」语义;settings 归管理面板(design/10)。

同时补全上一轮 emoji→MD3 图标(约 23 文件):新增 Icon.tsx(MD3 SVG、IconName、零依赖、
离线优先),全部界面 emoji 替换为 <Icon name=... />;正文说明箭头保留。

npm run build 绿(tsc 0 错,79→71 模块,JS 284KB/gzip 86KB)。
This commit is contained in:
Aska committed 2026-09-06 13:32:44 +08:00
1 parent 265b207699
commit 4aab0f501a
28 files changed
+539 -950

No files matched your search

+22 -22
View File
@@ -1,32 +1,37 @@
import { useModeStore } from "../store/useModeStore";
import { sendUp } from "../lib/ws";
import { MODES, type Mode } from "../lib/types";
import { Icon, type IconName } from "./Icon";
const modeMeta: Record<Mode, { label: string; icon: string }> = {
chat: { label: "对话", icon: "💬" },
code: { label: "代码", icon: "🖥" },
home: { label: "家居", icon: "🏠" },
work: { label: "工作", icon: "🗂" },
docs: { label: "知识库", icon: "📚" },
voice: { label: "语音", icon: "🎙" },
vision: { label: "视觉", icon: "👁" },
agents: { label: "子代理", icon: "🧩" },
settings: { label: "设置", icon: "⚙" },
const NAV_MODES: ReadonlyArray<Mode> = [
"chat", "code", "home", "work", "docs", "voice", "vision", "agents",
];
const modeMeta: Record<Mode, { label: string; icon: IconName }> = {
chat: { label: "对话", icon: "chat" },
code: { label: "代码", icon: "terminal" },
home: { label: "家居", icon: "home" },
work: { label: "工作", icon: "folder" },
docs: { label: "知识库", icon: "docs" },
voice: { label: "语音", icon: "mic" },
vision: { label: "视觉", icon: "vision" },
agents: { label: "子代理", icon: "agent" },
settings: { label: "设置", icon: "settings" },
};
/** Sets the input mode — the mode of the NEXT message (design/07 §1). In the
* single-page design this no longer switches pages; it only shapes the next
* bubble and notifies the backend for a mode-aware reply. */
function useSwitchMode(): (m: Mode) => void {
const setMode = useModeStore((s) => s.setMode);
const setSelfMode = useModeStore((s) => s.setSelfMode);
return (m: Mode) => {
setMode(m);
setSelfMode(null);
sendUp({ type: "mode_switch", data: { mode: m } });
};
}
export function Sidebar(): React.ReactNode {
const mode = useModeStore((s) => s.mode);
const selfMode = useModeStore((s) => s.selfMode);
const switchMode = useSwitchMode();
return (
@@ -34,7 +39,7 @@ export function Sidebar(): React.ReactNode {
<div className="px-2 py-2 text-xs font-semibold uppercase tracking-wider text-[var(--mu-text-lo)]">
行为模式
</div>
{MODES.map((m) => {
{NAV_MODES.map((m) => {
const meta = modeMeta[m];
const active = m === mode;
return (
@@ -47,13 +52,8 @@ export function Sidebar(): React.ReactNode {
: "text-[var(--mu-text-mid)] hover:bg-[var(--mu-surface-4)]"
}`}
>
<span className="text-base">{meta.icon}</span>
<Icon name={meta.icon} size={18} />
<span>{meta.label}</span>
{m === selfMode ? (
<span className="ml-auto rounded-full bg-[var(--mu-warn)]/15 px-1.5 py-0.5 text-[10px] text-[var(--mu-warn)]">
自切
</span>
) : null}
</button>
);
})}
@@ -66,7 +66,7 @@ export function BottomNav(): React.ReactNode {
const switchMode = useSwitchMode();
return (
<nav className="glass fixed inset-x-0 bottom-0 z-30 flex h-[var(--mu-bottomnav-h)] items-stretch border-t border-[var(--mu-glass-border)] md:hidden">
{MODES.slice(0, 5).map((m) => {
{NAV_MODES.slice(0, 5).map((m) => {
const meta = modeMeta[m];
const active = m === mode;
return (
@@ -77,7 +77,7 @@ export function BottomNav(): React.ReactNode {
active ? "text-[var(--mu-accent-hi)]" : "text-[var(--mu-text-lo)]"
}`}
>
<span className="text-base">{meta.icon}</span>
<Icon name={meta.icon} size={18} />
<span>{meta.label}</span>
</button>
);