架构变更(主人要求,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)。
116 lines
5.6 KiB
TypeScript
116 lines
5.6 KiB
TypeScript
import { useAsyncData } from "../../lib/useAsyncData";
|
||
import {
|
||
adminListChannels,
|
||
adminListIoT,
|
||
adminListMemories,
|
||
adminListPersonas,
|
||
adminListPlugins,
|
||
adminListSessions,
|
||
} from "../../lib/api";
|
||
import { useAppStore } from "../../store/useAppStore";
|
||
import { navigateAdmin, type AdminSectionId } from "../router";
|
||
import { CardGrid, StatCard } from "../components";
|
||
import { Surface, Tag } from "../../components/primitives";
|
||
import { Icon, type IconName } from "../../components/Icon";
|
||
import { useAutoReload } from "../hooks";
|
||
|
||
interface DashCard {
|
||
id: AdminSectionId;
|
||
label: string;
|
||
icon: IconName;
|
||
value: number | string;
|
||
sub?: string;
|
||
tone: "ok" | "warn" | "accent" | "neutral" | "err";
|
||
}
|
||
|
||
export function DashboardModule(): React.ReactNode {
|
||
const personas = useAsyncData(adminListPersonas, []);
|
||
const sessions = useAsyncData(adminListSessions, []);
|
||
const memories = useAsyncData(adminListMemories, []);
|
||
const plugins = useAsyncData(adminListPlugins, []);
|
||
const channels = useAsyncData(adminListChannels, []);
|
||
const iot = useAsyncData(adminListIoT, []);
|
||
useAutoReload(personas.reload, 10000);
|
||
useAutoReload(sessions.reload, 10000);
|
||
useAutoReload(memories.reload, 20000);
|
||
useAutoReload(plugins.reload, 15000);
|
||
useAutoReload(channels.reload, 10000);
|
||
useAutoReload(iot.reload, 8000);
|
||
|
||
// WS-backed live signals (design/10 §3): subagents + backthink.
|
||
const agents = useAppStore((s) => s.agents);
|
||
const backthink = useAppStore((s) => s.backthink);
|
||
const wsStatus = useAppStore((s) => s.wsStatus);
|
||
|
||
const nPersonas = personas.data?.length ?? 0;
|
||
const nSessions = sessions.data?.length ?? 0;
|
||
const nMemories = memories.data?.length ?? 0;
|
||
const nPlugins = plugins.data?.length ?? 0;
|
||
const nChannels = channels.data?.length ?? 0;
|
||
const nDevices = iot.data?.length ?? 0;
|
||
const offlineChannels = (channels.data ?? []).filter((c) => !c.connected).length;
|
||
const onlineDevices = (iot.data ?? []).filter((d) => d.online).length;
|
||
const runningAgents = agents.filter((a) => a.status === "running").length;
|
||
|
||
const cards: DashCard[] = [
|
||
{ id: "personas", label: "人格", icon: "dna", value: nPersonas, sub: "启停/默认/路由", tone: "accent" },
|
||
{ id: "sessions", label: "活跃会话", icon: "chat", value: nSessions, sub: "渠道 × actor", tone: "ok" },
|
||
{ id: "memories", label: "记忆", icon: "memory", value: nMemories, sub: "三类作用域隔离", tone: "accent" },
|
||
{ id: "plugins", label: "插件与能力", icon: "plugin", value: nPlugins, sub: "含 MCP/渠道", tone: "neutral" },
|
||
{ id: "channels", label: "渠道", icon: "channel", value: `${nChannels - offlineChannels}/${nChannels}`,
|
||
sub: offlineChannels > 0 ? `${offlineChannels} 离线` : "全部在线", tone: offlineChannels > 0 ? "warn" : "ok" },
|
||
{ id: "iot", label: "IoT 设备", icon: "device", value: `${onlineDevices}/${nDevices}`,
|
||
sub: onlineDevices === nDevices && nDevices > 0 ? "全部在线" : "设备状态", tone: nDevices > 0 && onlineDevices === nDevices ? "ok" : "neutral" },
|
||
{ id: "agents", label: "Agent 运行", icon: "agent", value: runningAgents, sub: `${agents.length} 个已登记`, tone: runningAgents > 0 ? "accent" : "neutral" },
|
||
{ id: "backthink", label: "后台思考活跃度", icon: "night", value: backthink.length, sub: "实时 (WS)", tone: backthink.length > 0 ? "warn" : "neutral" },
|
||
];
|
||
|
||
return (
|
||
<div className="space-y-5">
|
||
<div className="flex flex-wrap items-center gap-3">
|
||
<div>
|
||
<h2 className="text-lg font-semibold">仪表盘</h2>
|
||
<p className="text-sm text-[var(--mu-text-lo)]">多而全的概览 · 最详细信息在各独立页面</p>
|
||
</div>
|
||
<div className="ml-auto flex items-center gap-2 text-xs text-[var(--mu-text-lo)]">
|
||
<Tag tone={wsStatus === "connected" ? "ok" : "neutral"}>
|
||
<span className={`mr-1 inline-block h-1.5 w-1.5 rounded-full ${wsStatus === "connected" ? "bg-[var(--mu-ok)]" : "mu-blink"}`} />
|
||
实时通道 · {wsStatus}
|
||
</Tag>
|
||
</div>
|
||
</div>
|
||
|
||
<CardGrid>
|
||
{cards.map((c) => (
|
||
<button key={c.id} onClick={() => navigateAdmin(c.id)} className="text-left" title="点击下钻">
|
||
<StatCard icon={<Icon name={c.icon} size={18} />} label={c.label} value={c.value} sub={c.sub} tone={c.tone} />
|
||
</button>
|
||
))}
|
||
</CardGrid>
|
||
|
||
<Surface className="space-y-3">
|
||
<div className="flex items-center gap-2">
|
||
<h3 className="text-sm font-semibold">资源 / LLM / 业务概览</h3>
|
||
<span className="text-xs text-[var(--mu-text-lo)]">占位(后端 M10+ 提供)</span>
|
||
</div>
|
||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||
<MiniMetric label="LLM 供应商/模型" value="—" hint="多模态检测 / fallback / 角色路由" />
|
||
<MiniMetric label="存储" value="SQLite" hint="默认;PG/pgvector 可选增强" />
|
||
<MiniMetric label="工具循环上限" value="8 轮" hint="后台声明 / 前台 2s 转后台" />
|
||
</div>
|
||
<div className="pt-2 text-xs text-[var(--mu-text-lo)]">卡片即入口,点击上溯卡片下钻到对应模块。</div>
|
||
</Surface>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function MiniMetric({ label, value, hint }: { label: string; value: string; hint: string }): React.ReactNode {
|
||
return (
|
||
<div className="rounded-[var(--mu-radius)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-3)]/60 p-3">
|
||
<div className="text-xs text-[var(--mu-text-lo)]">{label}</div>
|
||
<div className="mt-1 text-lg font-semibold">{value}</div>
|
||
<div className="text-xs text-[var(--mu-text-lo)]">{hint}</div>
|
||
</div>
|
||
);
|
||
}
|