Files
Cyrene_v2/frontend/web/src/admin/modules/Observability.tsx
T
Aska 4aab0f501a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
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)。
2026-09-06 13:32:44 +08:00

142 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { useAppStore } from "../../store/useAppStore";
import { useAuditStore } from "../audit";
import { CardGrid, Notice, PageHeader, StatCard } from "../components";
import { Button, Surface, Tag } from "../../components/primitives";
import { Icon } from "../../components/Icon";
type Tab = "logs" | "trace" | "metrics" | "audit";
const TABS: ReadonlyArray<{ id: Tab; label: string }> = [
{ id: "logs", label: "实时日志" },
{ id: "trace", label: "Tracing" },
{ id: "metrics", label: "指标" },
{ id: "audit", label: "审计" },
];
export function ObservabilityModule(): React.ReactNode {
const [tab, setTab] = useState<Tab>("logs");
const deviceUpdates = useAppStore((s) => s.deviceUpdates);
const audit = useAuditStore((s) => s.records);
const clearAudit = useAuditStore((s) => s.clear);
return (
<div className="space-y-5">
<PageHeader title="可观测性" subtitle="实时日志 tail · Tracing(LLM/工具/编排) · 指标 · 审计" />
<div className="flex flex-wrap items-center gap-2">
{TABS.map((t) => (
<button
key={t.id}
onClick={() => setTab(t.id)}
className={`rounded-[var(--mu-radius)] px-3 py-1.5 text-xs font-medium transition-all duration-200 ease-[var(--mu-spring)] ${
tab === t.id ? "bg-[var(--mu-accent)]/20 text-[var(--mu-accent-hi)]" : "bg-[var(--mu-surface-4)] text-[var(--mu-text-mid)]"
}`}
>
{t.label}
</button>
))}
</div>
{tab === "logs" ? <LogsTail lines={deviceUpdates} /> : null}
{tab === "trace" ? <TraceView /> : null}
{tab === "metrics" ? <MetricsView /> : null}
{tab === "audit" ? <AuditView records={audit} onClear={clearAudit} /> : null}
</div>
);
}
function LogsTail({ lines }: { lines: string[] }): React.ReactNode {
return (
<Surface className="space-y-3">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold">实时日志流(WS 事件回显)</h3>
<Tag tone="ok">tail</Tag>
</div>
{lines.length === 0 ? (
<Notice>暂无实时事件。接入 /ws/chat 后,此处回显 stream / tool / device_update / reminder 等事件。</Notice>
) : (
<pre className="max-h-80 overflow-auto whitespace-pre-wrap rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 font-mono text-xs text-[var(--mu-text-mid)]">
{lines.join("\n")}
</pre>
)}
<Notice>完整后端日志分级 tail / 过滤 / 导出需后端 M10+ 提供。</Notice>
</Surface>
);
}
function TraceView(): React.ReactNode {
return (
<Surface className="space-y-3">
<h3 className="text-sm font-semibold">Tracing 视图</h3>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
{["LLM 调用链", "工具链", "编排链"].map((x) => (
<div key={x} className="rounded-[var(--mu-radius)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-3)]/60 p-4 text-center text-xs text-[var(--mu-text-mid)]">
<div className="mb-2 text-lg">{x}</div>
{x} 占位 —— 后端 M10+ 提供 span/trace 数据
</div>
))}
</div>
</Surface>
);
}
function MetricsView(): React.ReactNode {
return (
<div className="space-y-4">
<CardGrid>
<StatCard icon={<Icon name="gear" size={18} />} label="请求 QPS" value="—" sub="网关指标占位" tone="accent" />
<StatCard icon={<Icon name="robot" size={18} />} label="LLM 调用次数" value="—" sub="用量/费用占位" tone="neutral" />
<StatCard icon={<Icon name="agent" size={18} />} label="子代理事件" value="—" sub="编排指标占位" tone="ok" />
<StatCard icon={<Icon name="memory" size={18} />} label="工时/Token" value="—" sub="成本占位" tone="warn" />
</CardGrid>
<Notice>指标时序图 / 导出需后端 M10+ 提供。</Notice>
</div>
);
}
interface AuditViewProps {
records: ReturnType<typeof useAuditStore.getState>["records"];
onClear: () => void;
}
function AuditView({ records, onClear }: AuditViewProps): React.ReactNode {
return (
<Surface className="space-y-3">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold">审计日志(who / what / when / result)</h3>
<span className="text-xs text-[var(--mu-text-lo)]">管理 + 敏感操作全程落审计</span>
<span className="ml-auto">
<Button size="sm" variant="ghost" onClick={onClear}>
清空本地
</Button>
</span>
</div>
{records.length === 0 ? (
<Notice>暂无审计记录。本面板的管理/敏感操作(策略写入、主人档案编辑、API key 签发等)都会在此登记。</Notice>
) : (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[var(--mu-glass-border)] text-left text-xs uppercase tracking-wider text-[var(--mu-text-lo)]">
<th className="px-2 py-1.5">who</th>
<th className="px-2 py-1.5">what</th>
<th className="px-2 py-1.5">when</th>
<th className="px-2 py-1.5">result</th>
</tr>
</thead>
<tbody>
{records.map((r) => (
<tr key={r.id} className="border-b border-[var(--mu-glass-border)]/60 last:border-0">
<td className="px-2 py-2 text-xs">{r.who}</td>
<td className="px-2 py-2 text-xs font-mono">{r.what}</td>
<td className="px-2 py-2 text-xs text-[var(--mu-text-lo)]">{new Date(r.when).toLocaleString()}</td>
<td className="px-2 py-2"><Tag tone={r.result === "ok" ? "ok" : "err"}>{r.result}</Tag></td>
</tr>
))}
</tbody>
</table>
)}
</Surface>
);
}