Files
sundynix-agentix/sundynix-admin/src/pages/DatasourcesPage.tsx
T
2026-06-27 12:06:29 +08:00

247 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { ModelManager } from "../components/ModelManager";
// Mock GraphRAG 拓扑节点
const INITIAL_NODES = [
{ id: "node_1", x: 180, y: 50, label: "Beta Tech", type: "tenant", desc: "租户空间组织实体" },
{ id: "node_2", x: 80, y: 120, label: "知识库: 退款政策_2026.pdf", type: "document", desc: "主退款规则文档,字数: 3,420" },
{ id: "node_3", x: 280, y: 120, label: "知识库: 服务协议_v3.docx", type: "document", desc: "标准渠道协议模板" },
{ id: "node_4", x: 80, y: 220, label: "实体: 渠道退费限制", type: "concept", desc: "退款政策第 4 条:限制 30 天内申请" },
{ id: "node_5", x: 180, y: 220, label: "实体: 退款折算比率", type: "concept", desc: "公式:按合作月份比例计算退费" },
{ id: "node_6", x: 280, y: 220, label: "实体: 30天提前申请", type: "concept", desc: "退款前提:须有书面正式通知" }
];
// Mock GraphRAG 拓扑边关系
const EDGES = [
{ from: "node_1", to: "node_2", label: "contains" },
{ from: "node_1", to: "node_3", label: "contains" },
{ from: "node_2", to: "node_4", label: "rules" },
{ from: "node_2", to: "node_5", label: "defines" },
{ from: "node_2", to: "node_6", label: "requires" },
{ from: "node_4", to: "node_6", label: "aligns" }
];
const NODE_COLORS: Record<string, string> = {
tenant: "#7c3aed", // 紫罗兰 (Violet)
document: "#06b6d4", // 青色 (Cyan)
concept: "#10b981" // 翠绿 (Emerald)
};
export function DatasourcesPage() {
const [weights, setWeights] = useState({
vector: 45,
fullText: 35,
graph: 20
});
const [searchQuery, setSearchQuery] = useState("");
const [selectedNode, setSelectedNode] = useState<typeof INITIAL_NODES[0] | null>(null);
// 权重调整滑动条
const handleWeightChange = (key: "vector" | "fullText" | "graph", val: number) => {
setWeights((prev) => {
const next = { ...prev, [key]: val };
// 保持总和为 100% 的动态按比例计算
const diff = 100 - (next.vector + next.fullText + next.graph);
const otherKeys = (["vector", "fullText", "graph"] as const).filter((k) => k !== key);
// 平摊多余或不足的百分比
let share1 = Math.round(diff / 2);
let share2 = diff - share1;
next[otherKeys[0]] = Math.max(0, next[otherKeys[0]] + share1);
next[otherKeys[1]] = Math.max(0, next[otherKeys[1]] + share2);
return next;
});
};
// 搜索高亮节点
const filteredNodes = INITIAL_NODES.map((n) => {
const matched = searchQuery ? n.label.toLowerCase().includes(searchQuery.toLowerCase()) : false;
return { ...n, matched };
});
return (
<div className="flex flex-col gap-6">
{/* 1. Embedding 模型配置 (真实组件) */}
<ModelManager
kind="embedding"
title="Embedding 模型(embedding → mcp-go RAG"
baseUrlHint="https://dashscope.aliyuncs.com/compatible-mode/v1"
modelHint="text-embedding-v3"
/>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* 左侧 RAG 融合检索调优面板 */}
<div className="space-y-6">
<section className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
<h3 className="text-sm font-semibold text-gray-700">RAG (RRF )</h3>
<p className="text-[11px] text-gray-400 mb-4"> mcp-go (Reciprocal Rank Fusion) </p>
<div className="space-y-4">
{/* 向量检索路 */}
<div>
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span> (Milvus)</span>
<span className="font-semibold text-violet-600">{weights.vector}%</span>
</div>
<input
type="range"
min="0"
max="100"
className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-violet-600"
value={weights.vector}
onChange={(e) => handleWeightChange("vector", Number(e.target.value))}
/>
</div>
{/* 全文检索路 */}
<div>
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span> (Bleve)</span>
<span className="font-semibold text-cyan-600">{weights.fullText}%</span>
</div>
<input
type="range"
min="0"
max="100"
className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-cyan-500"
value={weights.fullText}
onChange={(e) => handleWeightChange("fullText", Number(e.target.value))}
/>
</div>
{/* 知识图谱检索路 */}
<div>
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span> (Neo4j)</span>
<span className="font-semibold text-emerald-600">{weights.graph}%</span>
</div>
<input
type="range"
min="0"
max="100"
className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-emerald-500"
value={weights.graph}
onChange={(e) => handleWeightChange("graph", Number(e.target.value))}
/>
</div>
<div className="rounded bg-violet-50/50 p-2.5 border border-violet-100/50 text-[10px] text-violet-700 leading-snug">
<strong></strong>GraphRAG 100%
</div>
</div>
</section>
{/* 节点详细信息面板 */}
{selectedNode && (
<section className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm animate-fadeIn">
<div className="flex items-center gap-2 mb-2">
<span className="h-3 w-3 rounded-full" style={{ backgroundColor: NODE_COLORS[selectedNode.type] }} />
<h4 className="text-xs font-bold text-gray-800">{selectedNode.label}</h4>
</div>
<p className="text-xs text-gray-500 mb-1">: <span className="font-semibold capitalize text-gray-600">{selectedNode.type}</span></p>
<p className="text-xs text-gray-600 leading-relaxed bg-gray-50 p-2 rounded border">{selectedNode.desc}</p>
</section>
)}
</div>
{/* 右侧 GraphRAG 图谱拓扑可视化渲染 */}
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm lg:col-span-2 flex flex-col">
<div className="mb-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h3 className="text-sm font-semibold text-gray-700">GraphRAG </h3>
<p className="text-[11px] text-gray-400"> LLM Neo4j </p>
</div>
{/* 实体过滤搜索 */}
<input
type="text"
className="rounded border px-2.5 py-1 text-xs focus:border-violet-500 focus:outline-none w-48 font-mono bg-gray-50/30"
placeholder="搜索定位实体节点..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{/* 拓扑网络画布 */}
<div className="relative border rounded-lg bg-gray-900 overflow-hidden flex-1 min-h-[300px] flex items-center justify-center">
<svg viewBox="0 0 360 300" className="w-full h-full select-none cursor-grab active:cursor-grabbing">
{/* 画边关系 */}
{EDGES.map((e, idx) => {
const fromNode = INITIAL_NODES.find((n) => n.id === e.from)!;
const toNode = INITIAL_NODES.find((n) => n.id === e.to)!;
const mx = (fromNode.x + toNode.x) / 2;
const my = (fromNode.y + toNode.y) / 2;
return (
<g key={idx}>
<line
x1={fromNode.x}
y1={fromNode.y}
x2={toNode.x}
y2={toNode.y}
stroke="#475569"
strokeWidth="1.5"
strokeDasharray="2 2"
/>
<text x={mx} y={my - 4} fill="#94a3b8" fontSize="8" textAnchor="middle" className="pointer-events-none">
{e.label}
</text>
</g>
);
})}
{/* 画节点 */}
{filteredNodes.map((n) => {
const color = NODE_COLORS[n.type];
const isSelected = selectedNode?.id === n.id;
return (
<g
key={n.id}
className="cursor-pointer group"
onClick={() => setSelectedNode(n)}
>
{/* 高亮光晕 */}
{(n.matched || isSelected) && (
<circle cx={n.x} cy={n.y} r="14" fill={color} opacity="0.3" className="animate-ping" />
)}
{/* 节点本体 */}
<circle
cx={n.x}
cy={n.y}
r={isSelected ? "9" : "7"}
fill={color}
stroke="#ffffff"
strokeWidth="2"
className="transition-all group-hover:scale-125"
/>
{/* 文字标签 */}
<text
x={n.x}
y={n.y - 12}
fill={n.matched ? "#ffffff" : isSelected ? "#a78bfa" : "#e2e8f0"}
fontSize="9"
fontWeight={n.matched || isSelected ? "bold" : "normal"}
textAnchor="middle"
className="pointer-events-none"
>
{n.label}
</text>
</g>
);
})}
</svg>
{/* 左下角小标识 */}
<div className="absolute bottom-2 left-2 flex gap-3 bg-slate-950/70 backdrop-blur border border-slate-800 rounded px-2.5 py-1 text-[9px] text-gray-400">
<span className="flex items-center gap-1.5"><span className="h-1.5 w-1.5 rounded-full bg-violet-600" /></span>
<span className="flex items-center gap-1.5"><span className="h-1.5 w-1.5 rounded-full bg-cyan-500" /></span>
<span className="flex items-center gap-1.5"><span className="h-1.5 w-1.5 rounded-full bg-emerald-500" /></span>
</div>
</div>
</div>
</div>
</div>
);
}