feat(frontend): M9 模态 UI(mode 系统+共享 store+WS 客户端 + chat(思维链/工具回显/情绪徽标)/code/Agent 监控/后台思考页 + Miuix 设计 token + PWA);tsc+vite build 通过

This commit is contained in:
Aska committed 2026-09-06 07:44:40 +08:00
1 parent b3ffd51736
commit 4c48e94659
40 files changed
+5321

No files matched your search

+102
View File
@@ -0,0 +1,102 @@
import { useState } from "react";
import type { ChatMessage, ToolUsed } from "../lib/types";
import { Tag } from "./primitives";
/** Collapsible inline chain-of-thought block. */
export function ThinkingBlock({
text,
streaming,
}: {
text?: string;
streaming?: boolean;
}): React.ReactNode | null {
const [open, setOpen] = useState(false);
if (!text && !streaming) return null;
return (
<div className="mb-2 overflow-hidden rounded-[var(--mu-radius-sm)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-2)]/60">
<button
onClick={() => setOpen((v) => !v)}
className="flex w-full items-center gap-1.5 px-3 py-1.5 text-xs text-[var(--mu-text-lo)]"
>
<span
className={`inline-block transition-transform duration-200 ${open ? "rotate-90" : ""}`}
>
▸
</span>
<span>思维链</span>
{streaming && !text ? <span className="mu-blink">思考中…</span> : null}
</button>
{open && text ? (
<pre className="max-h-40 overflow-auto whitespace-pre-wrap px-3 pb-2 text-xs leading-relaxed text-[var(--mu-text-mid)]">
{text}
</pre>
) : null}
</div>
);
}
/** Tool echo card (工具回显). */
export function ToolCard({ tool }: { tool: ToolUsed }): React.ReactNode {
const [open, setOpen] = useState(false);
const status = tool.ok === undefined ? "neutral" : tool.ok ? "ok" : "err";
return (
<div className="mb-2 overflow-hidden rounded-[var(--mu-radius-sm)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-2)]/60">
<button
onClick={() => setOpen((v) => !v)}
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs"
>
<span
className={`inline-block transition-transform duration-200 ${open ? "rotate-90" : ""}`}
>
▸
</span>
<Tag tone={status}>
{tool.ok === false ? "✗" : tool.ok === true ? "✓" : "⚙"} {tool.name}
</Tag>
{tool.summary ? (
<span className="truncate text-[var(--mu-text-lo)]">{tool.summary}</span>
) : null}
</button>
{(open || tool.detail) && tool.detail ? (
<pre className="max-h-40 overflow-auto whitespace-pre-wrap px-3 pb-2 text-xs text-[var(--mu-text-mid)]">
{tool.detail}
</pre>
) : null}
</div>
);
}
/** A single chat message bubble. */
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>
);
}
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%]">
{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>
</div>
</div>
);
}