Files
Cyrene_v2/frontend/web/src/admin/modules/Dashboard.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

116 lines
5.6 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 { 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>
);
}