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
@@ -0,0 +1,158 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { ChatMessage, Mode } from "../../lib/types";
|
||||
import { Tag } from "../primitives";
|
||||
import { Icon } from "../Icon";
|
||||
|
||||
/**
|
||||
* Mode-specific assistant message bodies (design/07 §2). The shared outer
|
||||
* layout (avatar / mood / thinking / tools) lives in MessageBubble — these
|
||||
* components only render the content region differently per mode, so the same
|
||||
* role under different modes looks different while a single session mixes them.
|
||||
*/
|
||||
|
||||
function shell(children: ReactNode): ReactNode {
|
||||
return (
|
||||
<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)]">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function fallbackText(message: ChatMessage): ReactNode {
|
||||
return message.content ? (
|
||||
<span className="whitespace-pre-wrap">{message.content}</span>
|
||||
) : message.streaming ? (
|
||||
<span className="mu-blink text-[var(--mu-text-lo)]">生成中…</span>
|
||||
) : null;
|
||||
}
|
||||
|
||||
/** code — mono code block + optional terminal-ish stream, not a plain bubble. */
|
||||
export function CodeMessageBody({ message }: { message: ChatMessage }): ReactNode {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{message.content ? (
|
||||
<pre className="overflow-x-auto whitespace-pre-wrap rounded-[var(--mu-radius-sm)] bg-[var(--mu-surface-2)] p-3 font-mono text-xs leading-relaxed text-[var(--mu-text-mid)]">
|
||||
{message.content}
|
||||
</pre>
|
||||
) : message.streaming ? (
|
||||
<div className="mu-blink text-xs text-[var(--mu-text-lo)]">正在执行 / 生成中…</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** docs — knowledge-base document card. */
|
||||
export function DocsMessageBody({ message }: { message: ChatMessage }): ReactNode {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{message.content ? (
|
||||
<div className="flex items-start gap-2">
|
||||
<Icon name="docs" size={18} className="mt-0.5 shrink-0 text-[var(--mu-accent)]" />
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="text-sm font-medium">{message.content}</div>
|
||||
<Tag tone="neutral">知识库</Tag>
|
||||
</div>
|
||||
</div>
|
||||
) : fallbackText(message)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** home — device card line (icons + state chips). */
|
||||
export function HomeMessageBody({ message }: { message: ChatMessage }): ReactNode {
|
||||
const devices = sampleDevices(message.content);
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{message.content ? (
|
||||
<span className="whitespace-pre-wrap">{message.content}</span>
|
||||
) : null}
|
||||
{devices.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2 pt-1">
|
||||
{devices.map((d) => (
|
||||
<Tag key={d} tone="accent">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Icon name="device" size={12} />
|
||||
{d}
|
||||
</span>
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{message.streaming ? <div className="mu-blink text-xs text-[var(--mu-text-lo)]">设备状态更新中…</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** work — task checklist card. */
|
||||
export function WorkMessageBody({ message }: { message: ChatMessage }): ReactNode {
|
||||
const tasks = (message.content ?? "").split("\n").filter((l) => l.trim());
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{tasks.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{tasks.map((t, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-sm">
|
||||
<Icon name="check" size={14} className="shrink-0 text-[var(--mu-ok)]" />
|
||||
<span className="min-w-0 whitespace-pre-wrap">{t}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : fallbackText(message)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** vision — image analysis summary card. */
|
||||
export function VisionMessageBody({ message }: { message: ChatMessage }): ReactNode {
|
||||
return shell(message.content ? (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2 text-xs text-[var(--mu-text-lo)]">
|
||||
<Icon name="vision" size={14} />
|
||||
视觉分析
|
||||
</div>
|
||||
<span className="whitespace-pre-wrap">{message.content}</span>
|
||||
</div>
|
||||
) : fallbackText(message));
|
||||
}
|
||||
|
||||
/** voice — speech session card. */
|
||||
export function VoiceMessageBody({ message }: { message: ChatMessage }): ReactNode {
|
||||
return shell(message.content ? (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Icon name="mic" size={16} className="text-[var(--mu-accent)]" />
|
||||
<span className="whitespace-pre-wrap">{message.content}</span>
|
||||
</div>
|
||||
) : fallbackText(message));
|
||||
}
|
||||
|
||||
/** agents — sub-agent progress card (kept for the shared flow; monitors live in messages). */
|
||||
export function AgentsMessageBody({ message }: { message: ChatMessage }): ReactNode {
|
||||
return shell(<span className="whitespace-pre-wrap">{message.content || fallbackText(message)}</span>);
|
||||
}
|
||||
|
||||
/** Reserved — settings is managed in the admin panel; an opaque content fallback. */
|
||||
export function SettingsMessageBody({ message }: { message: ChatMessage }): ReactNode {
|
||||
return shell(fallbackText(message));
|
||||
}
|
||||
|
||||
/** Map a mode to its message body component for the dispatcher. */
|
||||
export function modeBody(mode: Mode, message: ChatMessage): ReactNode {
|
||||
switch (mode) {
|
||||
case "code": return <CodeMessageBody message={message} />;
|
||||
case "docs": return <DocsMessageBody message={message} />;
|
||||
case "home": return <HomeMessageBody message={message} />;
|
||||
case "work": return <WorkMessageBody message={message} />;
|
||||
case "vision": return <VisionMessageBody message={message} />;
|
||||
case "voice": return <VoiceMessageBody message={message} />;
|
||||
case "agents": return <AgentsMessageBody message={message} />;
|
||||
case "settings": return <SettingsMessageBody message={message} />;
|
||||
default: return <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)]">{fallbackText(message)}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
/** tiny sample device extraction for the home card demo (content contains names). */
|
||||
function sampleDevices(content: string): string[] {
|
||||
if (!content) return [];
|
||||
const known = ["灯", "空调", "插座", "窗帘", "电视", "加湿器"];
|
||||
return known.filter((k) => content.includes(k));
|
||||
}
|
||||
Reference in new issue
Block a user