feat(web): 前端收敛为「共享消息流 + 按消息 mode 渲染」,并补全 emoji→MD3 图标
架构变更(主人要求,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:
1 parent
265b207699
commit
4aab0f501a
28 files changed
+539
-950
No files matched your search
@@ -1,6 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import type { ChatMessage, ToolUsed } from "../lib/types";
|
||||
import type { ChatMessage, Mode, ToolUsed } from "../lib/types";
|
||||
import { Tag } from "./primitives";
|
||||
import { Icon, type IconName } from "./Icon";
|
||||
import { modeBody } from "./messages";
|
||||
|
||||
/** Collapsible inline chain-of-thought block. */
|
||||
export function ThinkingBlock({
|
||||
@@ -52,7 +54,10 @@ export function ToolCard({ tool }: { tool: ToolUsed }): React.ReactNode {
|
||||
▸
|
||||
</span>
|
||||
<Tag tone={status}>
|
||||
{tool.ok === false ? "✗" : tool.ok === true ? "✓" : "⚙"} {tool.name}
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Icon name={tool.ok === false ? "close" : tool.ok === true ? "check" : "gear"} size={14} />
|
||||
{tool.name}
|
||||
</span>
|
||||
</Tag>
|
||||
{tool.summary ? (
|
||||
<span className="truncate text-[var(--mu-text-lo)]">{tool.summary}</span>
|
||||
@@ -67,36 +72,75 @@ export function ToolCard({ tool }: { tool: ToolUsed }): React.ReactNode {
|
||||
);
|
||||
}
|
||||
|
||||
/** A single chat message bubble. */
|
||||
/** A single message bubble. Per design/07 §1 the bubble shape is driven by the
|
||||
* message's own `mode`: the same role renders differently under different modes,
|
||||
* and a single session may mix shapes. Falls back to `chat` when mode is absent
|
||||
* (legacy persisted messages). */
|
||||
export function MessageBubble({ message }: { message: ChatMessage }): React.ReactNode {
|
||||
const isUser = message.role === "user";
|
||||
if (isUser) {
|
||||
return (
|
||||
<div className="mu-rise-in flex justify-end">
|
||||
<div className="max-w-[80%] whitespace-pre-wrap rounded-[var(--mu-radius-lg)] bg-[var(--mu-accent)] px-4 py-2.5 text-sm text-white shadow-[var(--mu-shadow-1)]">
|
||||
{message.content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const mode = message.mode ?? "chat";
|
||||
if (message.role === "user") {
|
||||
return <UserBubble message={message} mode={mode} />;
|
||||
}
|
||||
return <AssistantBubble message={message} mode={mode} />;
|
||||
}
|
||||
|
||||
/** User-side bubble. The outer accent pill is shared; the inner content may be
|
||||
* mode-shaped (e.g. code request renders in a mono pre). */
|
||||
function UserBubble({ message, mode }: { message: ChatMessage; mode: Mode }): React.ReactNode {
|
||||
return (
|
||||
<div className="mu-rise-in flex justify-end">
|
||||
<div className="max-w-[80%] rounded-[var(--mu-radius-lg)] bg-[var(--mu-accent)] px-4 py-2.5 text-sm text-white shadow-[var(--mu-shadow-1)]">
|
||||
{mode === "code" ? (
|
||||
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed">{message.content}</pre>
|
||||
) : (
|
||||
<span className="whitespace-pre-wrap">{message.content}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Assistant-side bubble. Outer layout shared; the body renders per mode via the
|
||||
* mode-specific body (chat / code / docs / home / work / vision …). */
|
||||
function AssistantBubble({ message, mode }: { message: ChatMessage; mode: Mode }): React.ReactNode {
|
||||
return (
|
||||
<div className="mu-rise-in flex gap-3">
|
||||
<img src="/icon.svg" alt="" className="mt-1 h-7 w-7 shrink-0" />
|
||||
<div className="min-w-0 max-w-[85%]">
|
||||
<ModeTag mode={mode} />
|
||||
{message.mood ? (
|
||||
<div className="mb-1 text-xs text-[var(--mu-text-lo)]">心情 · {message.mood}</div>
|
||||
) : null}
|
||||
<ThinkingBlock text={message.thinking} streaming={message.streaming} />
|
||||
{message.tools?.map((t, i) => <ToolCard key={i} tool={t} />)}
|
||||
<div className="rounded-[var(--mu-radius-lg)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-3)] px-4 py-2.5 text-sm text-[var(--mu-text-hi)]">
|
||||
{message.content ? (
|
||||
<span className="whitespace-pre-wrap">{message.content}</span>
|
||||
) : message.streaming ? (
|
||||
<span className="mu-blink text-[var(--mu-text-lo)]">生成中…</span>
|
||||
) : null}
|
||||
</div>
|
||||
{modeBody(mode, message)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Small chip showing the message's behavior mode. */
|
||||
function ModeTag({ mode }: { mode: Mode }): React.ReactNode {
|
||||
if (mode === "chat") return null;
|
||||
return (
|
||||
<div className="mb-1 inline-flex items-center gap-1 rounded-[var(--mu-radius-sm)] bg-[var(--mu-surface-4)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--mu-text-mid)]">
|
||||
<Icon name={modeIcon(mode)} size={11} />
|
||||
{modeLabel(mode)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function modeLabel(mode: Mode): string {
|
||||
const map: Record<Mode, string> = {
|
||||
chat: "对话", code: "代码", home: "家居", work: "工作",
|
||||
docs: "知识库", voice: "语音", vision: "视觉", agents: "子代理", settings: "设置",
|
||||
};
|
||||
return map[mode];
|
||||
}
|
||||
function modeIcon(mode: Mode): IconName {
|
||||
const map: Record<Mode, IconName> = {
|
||||
chat: "chat", code: "terminal", home: "home", work: "folder",
|
||||
docs: "docs", voice: "mic", vision: "vision", agents: "agent", settings: "settings",
|
||||
};
|
||||
return map[mode];
|
||||
}
|
||||
Reference in new issue
Block a user