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

+36 -3
View File
@@ -1,13 +1,25 @@
import { useEffect, useRef, useState } from "react";
import { Button } from "./primitives";
import { Icon, type IconName } from "./Icon";
import { useModeStore } from "../store/useModeStore";
import type { Mode } from "../lib/types";
export interface ChatInputProps {
onSend: (text: string) => void;
onSend: (text: string, mode: Mode) => void;
sending: boolean;
onVoice: () => void;
placeholder?: string;
}
const INPUT_MODES: ReadonlyArray<{ mode: Mode; label: string; icon: IconName }> = [
{ mode: "chat", label: "对话", icon: "chat" },
{ mode: "code", label: "代码", icon: "terminal" },
{ mode: "home", label: "家居", icon: "home" },
{ mode: "work", label: "工作", icon: "folder" },
{ mode: "docs", label: "知识库", icon: "docs" },
{ mode: "vision", label: "视觉", icon: "vision" },
];
export function ChatInput({
onSend,
sending,
@@ -16,6 +28,8 @@ export function ChatInput({
}: ChatInputProps): React.ReactNode {
const [value, setValue] = useState("");
const taRef = useRef<HTMLTextAreaElement>(null);
const mode = useModeStore((s) => s.mode);
const setMode = useModeStore((s) => s.setMode);
// auto-grow the textarea up to a max height
useEffect(() => {
@@ -28,12 +42,31 @@ export function ChatInput({
const submit = () => {
const t = value.trim();
if (!t || sending) return;
onSend(t);
onSend(t, mode);
setValue("");
};
return (
<div className="glass sticky bottom-0 border-t border-[var(--mu-glass-border)] p-3 md:p-4">
{/* input-mode switcher: sets the mode for the NEXT message (design/07 §1). */}
<div className="mx-auto mb-2 flex max-w-2xl items-center gap-1 overflow-x-auto pb-0.5">
<span className="mr-1 shrink-0 text-[10px] text-[var(--mu-text-lo)]">行为模式</span>
{INPUT_MODES.map(({ mode: m, label, icon }) => (
<button
key={m}
onClick={() => setMode(m)}
className={`inline-flex shrink-0 items-center gap-1 rounded-[var(--mu-radius-pill)] px-2.5 py-1 text-xs transition-all duration-200 ease-[var(--mu-spring)] active:scale-95 ${
mode === m
? "bg-[var(--mu-accent)]/15 text-[var(--mu-accent-hi)]"
: "bg-[var(--mu-surface-4)] text-[var(--mu-text-mid)] hover:bg-[var(--mu-surface-5)]"
}`}
>
<Icon name={icon} size={13} />
{label}
</button>
))}
</div>
<div className="mx-auto flex max-w-2xl items-end gap-2 rounded-[var(--mu-radius-lg)] bg-[var(--mu-surface-2)] p-2 shadow-[var(--mu-shadow-1)]">
<textarea
ref={taRef}
@@ -54,7 +87,7 @@ export function ChatInput({
aria-label="语音"
className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-[var(--mu-radius)] bg-[var(--mu-surface-4)] text-base transition-all duration-200 ease-[var(--mu-spring)] hover:bg-[var(--mu-surface-5)] active:scale-95"
>
🎙
<Icon name="mic" size={18} />
</button>
<Button
variant="primary"