feat(admin): upgrade admin console UI widgets, add plan & status management, and redesign system status dashboard

This commit is contained in:
Blizzard
2026-07-18 22:18:12 +08:00
parent 197531ea33
commit 0d2929931f
16 changed files with 1979 additions and 503 deletions
+203 -13
View File
@@ -1,12 +1,7 @@
import { useEffect, useState } from "react";
import { ModelManager } from "../components/ModelManager";
import { adminDatasources, type DatasourceKB } from "../api";
import { adminDatasources, listModels, migrateKbStorage, type DatasourceKB, type MigrateKbResult } from "../api";
// 数据源 & RAG:真数据(全平台知识库清单,来自 sundynix_kb/sundynix_doc)。
// 此前该页 GraphRAG 拓扑图与「向量/全文/图谱权重滑块」全 mock——而且权重概念本身是虚构的:
// mcp-go 的 RRF 融合是各路等权的倒排互惠融合(rrfK=60 平滑常数),没有"每路占几成"的权重。
// 故删假滑块+假拓扑,换成真数据源清单 + 诚实的检索管线说明。
const KIND_LABEL: Record<string, string> = { general: "通用", folder: "文件夹", project: "项目", case: "案例" };
const fmtWords = (n: number) => (n >= 1e4 ? `${(n / 1e4).toFixed(1)}` : `${n}`);
@@ -15,6 +10,17 @@ export function DatasourcesPage() {
const [rows, setRows] = useState<DatasourceKB[]>([]);
const [loading, setLoading] = useState(true);
const [err, setErr] = useState("");
// 运行时的 active embedding 模型展示
const [activeEmbedding, setActiveEmbedding] = useState<string>("加载中…");
// 运维工具折叠状态
const [opsOpen, setOpsOpen] = useState(false);
// 迁移 Modal 状态
const [migrationModal, setMigrationModal] = useState<"none" | "confirm" | "running" | "done">("none");
const [migrationResult, setMigrationResult] = useState<MigrateKbResult | null>(null);
const [migrationErr, setMigrationErr] = useState("");
useEffect(() => {
adminDatasources()
@@ -25,19 +31,203 @@ export function DatasourcesPage() {
})
.catch((e) => setErr((e as Error).message))
.finally(() => setLoading(false));
// 获取激活的 embedding 模型
listModels("embedding")
.then((models) => {
const active = models.find((m) => m.active);
if (active) {
setActiveEmbedding(`${active.model} (${active.provider})`);
} else {
setActiveEmbedding("未配置/未激活 (将回退代码默认)");
}
})
.catch(() => setActiveEmbedding("获取失败"));
}, []);
const startMigration = async () => {
setMigrationModal("running");
setMigrationErr("");
try {
const res = await migrateKbStorage();
setMigrationResult(res);
setMigrationModal("done");
} catch (e) {
setMigrationErr((e as Error).message);
setMigrationModal("confirm"); // 退回到确认态展示错误
}
};
const totalWords = rows.reduce((a, r) => a + r.total_words, 0);
return (
<div className="flex flex-col gap-6">
{/* Embedding 模型配置(真实组件) */}
<ModelManager
kind="embedding"
title="Embedding 模型(embedding → mcp-go RAG"
baseUrlHint="https://dashscope.aliyuncs.com/compatible-mode/v1"
modelHint="text-embedding-v3"
/>
{/* 顶部激活模型只读展示 & 引导 */}
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm flex items-center justify-between">
<div>
<h4 className="text-sm font-semibold text-gray-700"> (Embedding)</h4>
<div className="mt-1 flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-emerald-500 animate-pulse"></span>
<code className="text-xs font-mono font-medium text-gray-800">{activeEmbedding}</code>
</div>
</div>
<a
href="#/models"
onClick={(e) => {
// 如果使用 hash 路由或 react-router,可正常导航。这里提示用户去模型页配置
window.location.hash = "#/models";
}}
className="text-xs text-violet-600 hover:text-violet-700 font-medium hover:underline flex items-center gap-1"
>
</a>
</div>
{/* 运维工具 (RAG 迁移) */}
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
<button
onClick={() => setOpsOpen(!opsOpen)}
className="flex w-full items-center justify-between font-semibold text-gray-700 focus:outline-none"
>
<div className="flex items-center gap-2">
<svg className="h-4 w-4 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span className="text-sm font-semibold text-gray-700"></span>
</div>
<span className="text-xs text-gray-400">{opsOpen ? "收起 ▲" : "展开 ▼"}</span>
</button>
{opsOpen && (
<div className="mt-4 border-t border-gray-100 pt-4 space-y-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3 text-xs bg-amber-50/50 border border-amber-100 p-4 rounded-lg">
<div className="space-y-1">
<div className="font-semibold text-amber-800 flex items-center gap-1.5">
<span></span>
<span className="bg-amber-100 text-amber-700 px-1.5 py-0.5 rounded text-[9px] font-bold"></span>
</div>
<p className="text-gray-500 leading-relaxed max-w-xl">
<code className="bg-gray-100 px-1 rounded">owner/kb_id/doc</code> <code className="bg-gray-100 px-1 rounded">space_id/kb_id/doc</code>
</p>
</div>
<button
onClick={() => setMigrationModal("confirm")}
className="self-start md:self-center shrink-0 bg-amber-600 hover:bg-amber-700 text-white text-xs font-semibold py-2 px-4 rounded-lg shadow-sm transition"
>
</button>
</div>
</div>
)}
</div>
{/* 迁移 Modal */}
{migrationModal !== "none" && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="relative mx-4 w-full max-w-md overflow-hidden rounded-2xl bg-white shadow-2xl">
<div className="flex items-center justify-between border-b border-gray-100 px-6 py-5">
<div>
<h3 className="text-base font-semibold text-gray-900">
{migrationModal === "confirm" && "确认开始存储迁移?"}
{migrationModal === "running" && "正在迁移中…"}
{migrationModal === "done" && "迁移完成 🎉"}
</h3>
<p className="mt-0.5 text-xs text-gray-400"> KB space_id</p>
</div>
{migrationModal !== "running" && (
<button
onClick={() => setMigrationModal("none")}
className="text-xl leading-none text-gray-400 hover:text-gray-600"
>
×
</button>
)}
</div>
<div className="space-y-4 px-6 py-5 text-sm">
{migrationModal === "confirm" && (
<>
<p className="text-gray-600 leading-relaxed">
Space
</p>
<div className="rounded-lg border border-amber-100 bg-amber-50 p-3 text-xs text-amber-700 space-y-1">
<div className="font-semibold"> </div>
<div>· </div>
<div>· <code className="font-mono bg-amber-100/50 px-1">sundynix_doc</code> </div>
<div>· </div>
</div>
{migrationErr && (
<div className="rounded-lg border border-rose-100 bg-rose-50 px-3 py-2 text-xs text-rose-600">
{migrationErr}
</div>
)}
</>
)}
{migrationModal === "running" && (
<div className="flex flex-col items-center justify-center py-6 space-y-3">
<svg className="animate-spin h-8 w-8 text-amber-600" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span className="text-xs text-gray-500 font-medium animate-pulse"></span>
</div>
)}
{migrationModal === "done" && migrationResult && (
<div className="space-y-3">
<div className="rounded-lg border border-emerald-100 bg-emerald-50 px-3 py-2 text-xs text-emerald-700">
RRF
</div>
<div className="divide-y divide-gray-100 border rounded-xl overflow-hidden bg-gray-50/50">
<div className="flex justify-between p-3 text-xs">
<span className="text-gray-500"></span>
<span className="font-semibold text-gray-800 font-mono">{migrationResult.total}</span>
</div>
<div className="flex justify-between p-3 text-xs">
<span className="text-gray-500"></span>
<span className="font-semibold text-amber-600 font-mono">{migrationResult.enqueued}</span>
</div>
<div className="flex justify-between p-3 text-xs">
<span className="text-gray-500"></span>
<span className="font-semibold text-gray-600 font-mono">{migrationResult.skipped}</span>
</div>
</div>
</div>
)}
</div>
<div className="flex items-center justify-end gap-2 border-t border-gray-100 px-6 py-4">
{migrationModal === "confirm" && (
<>
<button
onClick={() => setMigrationModal("none")}
className="rounded-lg border border-gray-200 px-4 py-2 text-xs text-gray-500 hover:bg-gray-50"
>
</button>
<button
onClick={() => void startMigration()}
className="rounded-lg bg-amber-600 hover:bg-amber-700 px-4 py-2 text-xs font-semibold text-white shadow-sm transition"
>
</button>
</>
)}
{migrationModal === "done" && (
<button
onClick={() => setMigrationModal("none")}
className="rounded-lg bg-violet-600 hover:bg-violet-700 px-4 py-2 text-xs font-semibold text-white shadow-sm transition"
>
</button>
)}
</div>
</div>
</div>
)}
{/* 检索管线说明(诚实:三路 + RRF 等权融合,非可调权重) */}
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">