diff --git a/.gitignore b/.gitignore index 90df6d4..f549755 100644 --- a/.gitignore +++ b/.gitignore @@ -172,6 +172,7 @@ cython_debug/ hyperdb/templates/data.js hyperdb/templates/g6.min.js +tests/db/ # UV package manager .venv/ diff --git a/docs/assets/vis_hg.jpg b/docs/assets/vis_hg.jpg index 34c4c63..6ebefed 100644 Binary files a/docs/assets/vis_hg.jpg and b/docs/assets/vis_hg.jpg differ diff --git a/hyperdb/draw.py b/hyperdb/draw.py index 225063e..c3291e6 100644 --- a/hyperdb/draw.py +++ b/hyperdb/draw.py @@ -6,65 +6,171 @@ import webbrowser from pathlib import Path from typing import Any, Dict -from urllib.parse import urlparse +from urllib.parse import parse_qs, urlparse from .hypergraph import HypergraphDB -class HypergraphViewer: - """Hypergraph visualization tool""" +class HypergraphAPIHandler(http.server.BaseHTTPRequestHandler): + """HTTP request handler with API endpoints""" - def __init__(self, hypergraph_db: HypergraphDB, port: int = 8080): + def __init__(self, hypergraph_db: HypergraphDB, *args, **kwargs): self.hypergraph_db = hypergraph_db - self.port = port - self.html_content = self._generate_html_with_data() - - def _generate_html_with_data(self): - """Generate HTML content with embedded data""" - # Get all data - database_info = { + super().__init__(*args, **kwargs) + + def log_message(self, format, *args): + """Disable default logging""" + pass + + def do_GET(self): + """Handle GET requests""" + parsed_path = urlparse(self.path) + path = parsed_path.path + query_params = parse_qs(parsed_path.query) + + # CORS headers + self.send_response(200) + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "Content-Type") + + # Route handling + if path == "/" or path == "/index.html": + self.send_header("Content-type", "text/html; charset=utf-8") + self.end_headers() + self.wfile.write(self._get_html_template().encode("utf-8")) + + elif path == "/api/database/info": + self.send_header("Content-type", "application/json; charset=utf-8") + self.end_headers() + response = self._get_database_info() + self.wfile.write(json.dumps(response, ensure_ascii=False).encode("utf-8")) + + elif path == "/api/vertices": + self.send_header("Content-type", "application/json; charset=utf-8") + self.end_headers() + + # Parse query parameters + page = int(query_params.get("page", ["1"])[0]) + page_size = int(query_params.get("page_size", ["50"])[0]) + search = query_params.get("search", [""])[0] + sort_by = query_params.get("sort_by", ["degree"])[0] + sort_order = query_params.get("sort_order", ["desc"])[0] + + response = self._get_vertices(page, page_size, search, sort_by, sort_order) + self.wfile.write(json.dumps(response, ensure_ascii=False).encode("utf-8")) + + elif path == "/api/graph": + self.send_header("Content-type", "application/json; charset=utf-8") + self.end_headers() + + vertex_id = query_params.get("vertex_id", [""])[0] + if vertex_id: + response = self._get_graph_data(vertex_id) + else: + response = {"error": "vertex_id parameter is required"} + + self.wfile.write(json.dumps(response, ensure_ascii=False).encode("utf-8")) + + else: + self.send_header("Content-type", "text/plain; charset=utf-8") + self.end_headers() + self.wfile.write(b"404 Not Found") + + def do_OPTIONS(self): + """Handle OPTIONS requests for CORS preflight""" + self.send_response(200) + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "Content-Type") + self.end_headers() + + def _get_database_info(self) -> Dict[str, Any]: + """Get database information""" + return { "name": "current_hypergraph", "vertices": self.hypergraph_db.num_v, "edges": self.hypergraph_db.num_e, } - # Get vertex list - vertices = list(self.hypergraph_db.all_v)[:100] + def _get_vertices(self, page: int, page_size: int, search: str, sort_by: str, sort_order: str) -> Dict[str, Any]: + """Get vertices with pagination and search""" + hg = self.hypergraph_db + + # Get all vertices + all_vertices = list(hg.all_v) + + # Prepare vertex data with search scoring vertex_data = [] + search_lower = search.lower() if search else "" + + for v_id in all_vertices: + v_data = hg.v(v_id, {}) + degree = hg.degree_v(v_id) + entity_type = v_data.get("entity_type", "") + description = v_data.get("description", "") + + # Calculate search score + score = 0 + if search_lower: + if search_lower in str(v_id).lower(): + score += 3 + if search_lower in entity_type.lower(): + score += 2 + if search_lower in description.lower(): + score += 1 + + # Skip if no match + if score == 0: + continue - for v_id in vertices: - v_data = self.hypergraph_db.v(v_id, {}) vertex_data.append( { "id": v_id, - "degree": self.hypergraph_db.degree_v(v_id), - "entity_type": v_data.get("entity_type", ""), - "description": ( - v_data.get("description", "")[:100] + "..." - if len(v_data.get("description", "")) > 100 - else v_data.get("description", "") - ), + "degree": degree, + "entity_type": entity_type, + "description": (description[:100] + "..." if len(description) > 100 else description), + "score": score, } ) - # Sort by degree - vertex_data.sort(key=lambda x: x["degree"], reverse=True) - - # Get graph data for all vertices - graph_data = {} - for vertex in vertex_data: - vertex_id = vertex["id"] - graph_data[vertex_id] = self._get_vertex_neighbor_data(self.hypergraph_db, vertex_id) - - # Embed data into HTML - return self._get_html_template(database_info, vertex_data, graph_data) + # Sort vertices + if search_lower: + # Sort by search score if searching (no degree filtering) + vertex_data.sort(key=lambda x: x["score"], reverse=True) + elif sort_by == "degree": + # First, separate by degree threshold (degree > 50 goes to the end) + vertex_data.sort(key=lambda x: (x["degree"] > 50, -x["degree"] if sort_order == "desc" else x["degree"])) + elif sort_by == "id": + # First, separate by degree threshold (degree > 50 goes to the end) + vertex_data.sort(key=lambda x: (x["degree"] > 50, str(x["id"])), reverse=(sort_order == "desc")) + + # Remove score from output + for v in vertex_data: + v.pop("score", None) + + # Pagination + total = len(vertex_data) + start = (page - 1) * page_size + end = start + page_size + paginated_data = vertex_data[start:end] + + return { + "data": paginated_data, + "pagination": { + "page": page, + "page_size": page_size, + "total": total, + "total_pages": (total + page_size - 1) // page_size, + }, + } - def _get_vertex_neighbor_data(self, hypergraph_db: HypergraphDB, vertex_id: str) -> Dict[str, Any]: - """Get vertex neighbor data""" - hg = hypergraph_db + def _get_graph_data(self, vertex_id: str) -> Dict[str, Any]: + """Get graph data for a vertex""" + hg = self.hypergraph_db if not hg.has_v(vertex_id): - raise ValueError(f"Vertex {vertex_id} not found") + return {"error": f"Vertex {vertex_id} not found"} # Get all neighbor hyperedges of the vertex neighbor_edges = hg.nbr_e_of_v(vertex_id) @@ -83,7 +189,8 @@ def _get_vertex_neighbor_data(self, hypergraph_db: HypergraphDB, vertex_id: str) edges_data[edge_key] = { "keywords": edge_data.get("keywords", ""), "summary": edge_data.get("summary", ""), - "weight": len(edge_tuple), # Hyperedge weight equals the number of vertices it contains + "weight": len(edge_tuple), + **edge_data, } # Get data for all vertices @@ -99,13 +206,8 @@ def _get_vertex_neighbor_data(self, hypergraph_db: HypergraphDB, vertex_id: str) return {"vertices": vertices_data, "edges": edges_data} - def _get_html_template(self, database_info: Dict, vertex_data: list, graph_data: Dict) -> str: - """Get HTML template with embedded data""" - # Serialize data to JSON string - embedded_data = {"database": database_info, "vertices": vertex_data, "graphs": graph_data} - data_json = json.dumps(embedded_data, ensure_ascii=False) - - # Read HTML template file + def _get_html_template(self) -> str: + """Get HTML template without embedded data""" template_path = Path(__file__).parent / "templates" / "hypergraph_viewer.html" try: @@ -114,31 +216,24 @@ def _get_html_template(self, database_info: Dict, vertex_data: list, graph_data: except FileNotFoundError: raise FileNotFoundError(f"HTML template file not found: {template_path}") - # Replace placeholders in template - html_content = html_template.replace("{{DATA_JSON}}", data_json) + # Replace placeholder with empty object (data will be loaded via API) + html_content = html_template.replace("{{DATA_JSON}}", "{}") return html_content - def start_server(self, open_browser: bool = True): - """Start simple HTTP server""" - class CustomHTTPRequestHandler(http.server.BaseHTTPRequestHandler): - def __init__(self, html_content, *args, **kwargs): - self.html_content = html_content - super().__init__(*args, **kwargs) +class HypergraphViewer: + """Hypergraph visualization tool""" - def do_GET(self): - self.send_response(200) - self.send_header("Content-type", "text/html; charset=utf-8") - self.end_headers() - self.wfile.write(self.html_content.encode("utf-8")) + def __init__(self, hypergraph_db: HypergraphDB, port: int = 8080): + self.hypergraph_db = hypergraph_db + self.port = port - def log_message(self, format, *args): - # Disable log output - pass + def start_server(self, open_browser: bool = True): + """Start HTTP server with API endpoints""" def run_server(): - handler = lambda *args, **kwargs: CustomHTTPRequestHandler(self.html_content, *args, **kwargs) + handler = lambda *args, **kwargs: HypergraphAPIHandler(self.hypergraph_db, *args, **kwargs) self.httpd = socketserver.TCPServer(("127.0.0.1", self.port), handler) self.httpd.serve_forever() diff --git a/hyperdb/templates/hypergraph_viewer.html b/hyperdb/templates/hypergraph_viewer.html index 988e36e..2d0fc9f 100644 --- a/hyperdb/templates/hypergraph_viewer.html +++ b/hyperdb/templates/hypergraph_viewer.html @@ -5,7 +5,7 @@ Hypergraph Visualization - + @@ -29,11 +29,11 @@ const { useState, useEffect, useMemo, useRef } = React; const { Graph } = window.G6; - // 嵌入的数据 - const embeddedData = {{DATA_JSON}}; - // const embeddedData = datas; + // API base path + // const API_BASE = window.location.origin; + const API_BASE = "http://localhost:8080"; - // 常量配置 + // Configuration constants const COLORS = [ "#F6BD16", "#00C9C9", @@ -47,25 +47,30 @@ "#c8ff00", ]; - // 6个预定义颜色,用于循环分配给不同的实体类型 + // 6 predefined colors, cycled for different entity types const ENTITY_TYPE_COLORS_PALETTE = [ "#00C9C9", "#a68fff", "#F08F56", "#0d7c4f", "#004ac9", - "#f056d1" + "#f056d1", ]; const DEFAULT_NODE_COLOR = "#8b5cf6"; - // 动态生成实体类型颜色映射 + // Dynamically generate entity type color mapping const generateEntityTypeColors = (vertices) => { - const entityTypes = [...new Set(vertices.map(v => v.entity_type).filter(Boolean))]; + const entityTypes = [ + ...new Set(vertices.map((v) => v.entity_type).filter(Boolean)), + ]; const colorMap = {}; entityTypes.forEach((entityType, index) => { - colorMap[entityType] = ENTITY_TYPE_COLORS_PALETTE[index % ENTITY_TYPE_COLORS_PALETTE.length]; + colorMap[entityType] = + ENTITY_TYPE_COLORS_PALETTE[ + index % ENTITY_TYPE_COLORS_PALETTE.length + ]; }); return colorMap; @@ -73,16 +78,7 @@ const LAYOUT_THRESHOLD = 100; const EDGE_SEPARATOR = "|#|"; - // 工具函数 - const calculateMatchScore = (vertex, searchLower) => { - let score = 0; - if (vertex.id.toString().toLowerCase().includes(searchLower)) - score += 3; - if (vertex.entity_type?.toLowerCase().includes(searchLower)) score += 2; - if (vertex.description?.toLowerCase().includes(searchLower)) score += 1; - return score; - }; - + // Utility functions const createBubbleStyle = (baseColor) => ({ fill: baseColor, stroke: baseColor, @@ -91,8 +87,8 @@ pixelGroup: 4, edgeR0: 10, edgeR1: 60, - nodeR0: 15, - nodeR1: 50, + nodeR0: 1, + nodeR1: 5, morphBuffer: 10, threshold: 4, memberInfluenceFactor: 1, @@ -111,71 +107,208 @@ return description.split("").join(" "); }; + // API call functions + const fetchDatabaseInfo = async () => { + if (typeof datas !== "undefined") { + return Promise.resolve(datas.database); + } + const response = await fetch(`${API_BASE}/api/database/info`); + return await response.json(); + }; + + const fetchVertices = async ( + page, + pageSize, + search, + sortBy, + sortOrder + ) => { + if (typeof datas !== "undefined") { + const toLower = (v) => (v ?? "").toString().toLowerCase(); + let list = Array.isArray(datas.vertices) ? datas.vertices : []; + + if (search) { + const q = toLower(search); + list = list.filter( + (v) => + toLower(v.id).includes(q) || + toLower(v.entity_type).includes(q) || + toLower(v.description).includes(q) + ); + } + + if (sortBy === "degree") { + list.sort((a, b) => + sortOrder === "asc" + ? (a.degree || 0) - (b.degree || 0) + : (b.degree || 0) - (a.degree || 0) + ); + } else if (sortBy === "id") { + list.sort((a, b) => { + const cmp = String(a.id).localeCompare(String(b.id)); + return sortOrder === "asc" ? cmp : -cmp; + }); + } + + const total = list.length; + const total_pages = Math.max(1, Math.ceil(total / pageSize)); + const cur = Math.min(Math.max(1, page), total_pages); + const start = (cur - 1) * pageSize; + const data = list.slice(start, start + pageSize); + return Promise.resolve({ + data, + pagination: { page: cur, page_size: pageSize, total, total_pages }, + }); + } + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + search: search || "", + sort_by: sortBy, + sort_order: sortOrder, + }); + const response = await fetch(`${API_BASE}/api/vertices?${params}`); + return await response.json(); + }; + + const fetchGraphData = async (vertexId) => { + if (typeof datas !== "undefined") { + const entry = datas.graphs[vertexId]; + if (entry && entry.vertices && entry.edges) { + return Promise.resolve({ + vertices: entry.vertices, + edges: entry.edges, + }); + } + return Promise.resolve({ vertices: {}, edges: {} }); + } + const params = new URLSearchParams({ vertex_id: vertexId }); + const response = await fetch(`${API_BASE}/api/graph?${params}`); + return await response.json(); + }; + function HyperGraphViewer() { const containerRef = useRef(null); const graphRef = useRef(null); - const [databases, setDatabases] = useState([embeddedData.database]); - const [vertices, setVertices] = useState(embeddedData.vertices); - const [selectedVertex, setSelectedVertex] = useState( - embeddedData.vertices.length > 0 ? embeddedData.vertices[0].id : "" - ); + const searchTimeoutRef = useRef(null); + + const [databaseInfo, setDatabaseInfo] = useState({ + vertices: 0, + edges: 0, + }); + const [vertices, setVertices] = useState([]); + const [pagination, setPagination] = useState({ + page: 1, + page_size: 50, + total: 0, + total_pages: 0, + }); + const [selectedVertex, setSelectedVertex] = useState(""); const [graphData, setGraphData] = useState(null); const [loading, setLoading] = useState(false); + const [loadingVertices, setLoadingVertices] = useState(false); const [error, setError] = useState(""); const [searchTerm, setSearchTerm] = useState(""); - const [searchResults, setSearchResults] = useState([]); - const [filteredVertices, setFilteredVertices] = useState( - embeddedData.vertices - ); - const [visualizationMode, setVisualizationMode] = useState("hyper"); // 'hyper' or 'graph' - const [graphVersion, setGraphVersion] = useState(0); + const [sortBy, setSortBy] = useState("degree"); + const [sortOrder, setSortOrder] = useState("desc"); + const [visualizationMode, setVisualizationMode] = useState("hyper"); const [hoverHyperedge, setHoverHyperedge] = useState(null); const [hoverNode, setHoverNode] = useState(null); - // 生成实体类型颜色映射 + // Generate entity type color mapping const entityTypeColors = useMemo(() => { - return generateEntityTypeColors(embeddedData.vertices); - }, [embeddedData.vertices]); - // 搜索功能 + return generateEntityTypeColors(vertices); + }, [vertices]); + + // Load database info + useEffect(() => { + fetchDatabaseInfo().then(setDatabaseInfo).catch(console.error); + }, []); + + // Load vertices list + const loadVertices = async (page, search, sortBy, sortOrder) => { + setLoadingVertices(true); + try { + const result = await fetchVertices( + page, + pagination.page_size, + search, + sortBy, + sortOrder + ); + setVertices(result.data); + setPagination(result.pagination); + + // If no vertex selected and data exists, select the first one + if (result.data.length > 0) { + setSelectedVertex(result.data[0].id); + } + } catch (err) { + console.error("Failed to load vertices:", err); + } finally { + setLoadingVertices(false); + } + }; + + // Initial load of vertices + useEffect(() => { + loadVertices(1, "", sortBy, sortOrder); + }, []); + + // Search debounce useEffect(() => { - if (!searchTerm.trim()) { - setFilteredVertices(embeddedData.vertices); - setSearchResults([]); - return; + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); } - const searchLower = searchTerm.toLowerCase(); - const results = embeddedData.vertices - .map((vertex) => ({ - ...vertex, - score: calculateMatchScore(vertex, searchLower), - })) - .filter((item) => item.score > 0) - .sort((a, b) => b.score - a.score) - .map(({ score, ...vertex }) => vertex); - - setFilteredVertices(results); - setSearchResults(results); + searchTimeoutRef.current = setTimeout(() => { + loadVertices(1, searchTerm, sortBy, sortOrder); + }, 300); + + return () => { + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + }; }, [searchTerm]); - // 加载图数据(从嵌入数据获取) + // Reload when sort changes + useEffect(() => { + if (vertices.length > 0) { + loadVertices(pagination.page, searchTerm, sortBy, sortOrder); + } + }, [sortBy, sortOrder]); + + // Load when page changes + const handlePageChange = (newPage) => { + loadVertices(newPage, searchTerm, sortBy, sortOrder); + }; + + // Load graph data useEffect(() => { if (selectedVertex) { setLoading(true); setError(""); - // 从嵌入数据获取图数据 - const data = embeddedData.graphs[selectedVertex]; - if (data) { - setGraphData(data); - } else { - setError("Graph data not found for this vertex"); - } - setLoading(false); + fetchGraphData(selectedVertex) + .then((data) => { + if (data.error) { + setError(data.error); + } else { + setGraphData(data); + } + }) + .catch((err) => { + setError("Failed to load graph data"); + console.error(err); + }) + .finally(() => { + setLoading(false); + }); } }, [selectedVertex]); - // 转换数据为 G6 Graph 格式,根据可视化模式处理 + // Convert data to G6 Graph format const graphDataFormatted = useMemo(() => { if (!graphData) return null; @@ -192,7 +325,7 @@ const edgeEntries = Object.entries(graphData.edges); if (visualizationMode === "graph") { - // Graph模式:只显示维度为2的球棍图 + // Graph mode: only show dimension 2 ball-and-stick diagram const edgeSet = new Set(); edgeEntries.forEach(([key, edge]) => { @@ -213,7 +346,7 @@ } }); - // 过滤未连接的节点 + // Filter unconnected nodes const connectedNodes = new Set(); hyperData.edges.forEach((edge) => { connectedNodes.add(edge.source); @@ -223,10 +356,26 @@ connectedNodes.has(node.id) ); } else { - // Hyper模式:使用bubble-sets插件 + // Hyper mode: render 2-node entries as normal edges, others as bubble-sets + const edgeSet = new Set(); edgeEntries.forEach(([key, edge], i) => { const nodes = key.split(EDGE_SEPARATOR); + if (nodes.length === 2) { + const [a, b] = nodes; + const edgeId = a < b ? `${a}-${b}` : `${b}-${a}`; + if (!edgeSet.has(edgeId)) { + edgeSet.add(edgeId); + hyperData.edges.push({ + id: edgeId, + source: a, + target: b, + ...edge, + }); + } + return; + } + plugins.push({ key: `bubble-sets-${key}`, type: "bubble-sets", @@ -234,6 +383,7 @@ keywords: edge.keywords || "", summary: edge.summary || "", weight: edge.weight || nodes.length, + description: edge.description || edge.summary || "", ...createBubbleStyle(COLORS[i % COLORS.length]), }); @@ -243,9 +393,31 @@ members: nodes, }); }); + + // Assign cluster by hyperEdges for layout grouping + // Pick the heaviest hyperedge containing the node as its primary cluster + const nodeIdToCandidateClusters = new Map(); + hyperData.hyperEdges.forEach((he) => { + const weight = + he.weight || + (Array.isArray(he.members) ? he.members.length : 1); + (he.members || []).forEach((m) => { + const list = nodeIdToCandidateClusters.get(m) || []; + list.push({ id: he.id, weight }); + nodeIdToCandidateClusters.set(m, list); + }); + }); + hyperData.nodes = hyperData.nodes.map((n) => { + const candidates = nodeIdToCandidateClusters.get(n.id) || []; + if (candidates.length === 0) return n; + const primary = candidates.reduce((best, cur) => + cur.weight > best.weight ? cur : best + ); + return { ...n, cluster: primary.id }; + }); } - // 添加tooltip插件 + // Add tooltip plugin const excludedKeys = new Set([ "id", "entity_name", @@ -270,7 +442,7 @@ item.description )}

`; } - // 展示所有剩余属性 + // Display all remaining properties Object.entries(item).forEach(([key, value]) => { if (!excludedKeys.has(key)) { result += `

${key}: ${value}

`; @@ -290,7 +462,7 @@ node: { palette: { field: "cluster" }, style: { - size: isGraph ? 20 : 25, + size: hyperData.nodes.length > LAYOUT_THRESHOLD ? 15 : 20, labelText: (d) => d.id, fill: (d) => getNodeColor(d, selectedVertex, entityTypeColors), }, @@ -298,30 +470,28 @@ edge: { style: { size: isGraph ? 3 : 2, - stroke: isGraph ? "#a68fff" : undefined, - lineWidth: isGraph ? 2 : undefined, + stroke: "#a68fff", + lineWidth: 1, }, }, layout: { type: - hyperData.nodes.length > LAYOUT_THRESHOLD - ? "force-atlas2" - : "force", + hyperData.nodes.length > LAYOUT_THRESHOLD ? "force" : "force", clustering: !isGraph, preventOverlap: true, - nodeClusterBy: isGraph ? undefined : "entity_type", - gravity: 20, - linkDistance: isGraph ? 100 : 150, + nodeClusterBy: isGraph ? undefined : "cluster", + gravity: 50, + linkDistance: 50, }, autoFit: "center", }; }, [graphData, selectedVertex, visualizationMode, entityTypeColors]); - // 初始化图形 + // Initialize graph useEffect(() => { if (!graphDataFormatted || !containerRef.current) return; - // 销毁之前的图形实例并清空画布 + // Destroy previous graph instance and clear canvas if (graphRef.current && !graphRef.current.destroyed) { graphRef.current.clear(); if (containerRef.current) { @@ -348,15 +518,11 @@ graph.on("pointermove", (e) => { if (e.targetType === "bubble-sets") { - console.log("bubble-sets", e.target); const target = e.target.options; const newHyperedge = { - keywords: target.keywords || "", - summary: target.summary || "", + ...target, members: Array.isArray(target.members) ? target.members : [], - weight: target.weight, }; - // 只在数据不同时才更新 setHoverHyperedge((prev) => { if ( !prev || @@ -375,7 +541,6 @@ const target = graphDataFormatted.data.nodes.find( (node) => node.id === e.target.id ); - // 只在节点不同时才更新 setHoverNode((prev) => { if (!prev || prev.id !== target?.id) { return target; @@ -385,7 +550,7 @@ } }); - // 添加窗口大小变化监听 + // Add window resize listener const handleResize = () => { if (graphRef.current && containerRef.current) { graphRef.current.setSize( @@ -405,12 +570,11 @@ if (containerRef.current) { containerRef.current.innerHTML = ""; } - // 清理右侧悬停信息 setHoverHyperedge(null); }; }, [graphDataFormatted, visualizationMode]); - // 默认选中"最大的"节点与超边(首次或每次数据/模式变化时) + // Select the "largest" node and hyperedges by default useEffect(() => { if (!graphDataFormatted) return; @@ -433,7 +597,8 @@ setHoverHyperedge({ keywords: hyperWithMax.keywords || "", - summary: hyperWithMax.summary || "", + description: + hyperWithMax.description || hyperWithMax.summary || "", members: hyperWithMax.members || [], weight: getWeight(hyperWithMax), }); @@ -443,7 +608,7 @@ return (
-
+

Hypergraph-DB

@@ -451,27 +616,19 @@

Database Information
-
+
Vertices: - {embeddedData.database.vertices} - -
-
- - Hyperedges: - - - {embeddedData.database.edges} + {databaseInfo.vertices}
- Displayed Vertices: + Hyperedges: - {vertices.length} + {databaseInfo.edges}
@@ -480,20 +637,14 @@

Search & Vertex List

- {/* 搜索框 */} + {/* Search and Sort */}
-
+
setSearchTerm(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && searchResults.length > 0) { - setSelectedVertex(searchResults[0].id); - e.target.blur(); - } - }} className="w-full px-4 py-2 pl-10 pr-4 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" />
@@ -532,50 +683,58 @@

)}

- {searchTerm && ( -
- Found {searchResults.length} matching results - {searchResults.length > 0 && ( - - (Press Enter to select first result) - - )} -
- )} + + {/* Sort Selection */} + {/*
+ + +
*/} + +
+ Found {pagination.total} results +
-
- {filteredVertices.length === 0 ? ( + {/* Vertices List */} +
+ {loadingVertices ? (
- {searchTerm - ? "No matching vertices found" - : "No vertex data available"} + Loading vertices... +
+ ) : vertices.length === 0 ? ( +
+ No matching vertices found
) : ( - filteredVertices.map((vertex) => { + vertices.map((vertex) => { const isSelected = selectedVertex === vertex.id; - const isSearchMatch = searchResults.some( - (r) => r.id === vertex.id - ); - const highlightClass = isSelected - ? "bg-primary-50 border-l-4 border-l-primary-500 shadow-md" - : isSearchMatch - ? "bg-yellow-50 border-l-4 border-l-yellow-400 shadow-sm" - : ""; return (
setSelectedVertex(vertex.id)} >
{vertex.id} - {isSearchMatch && ( - - Match - - )}
@@ -603,6 +762,29 @@

}) )}

+ + {/* Pagination Controls */} + {pagination.total_pages > 1 && ( +
+ + + {pagination.page} / {pagination.total_pages} + + +
+ )}
@@ -613,7 +795,7 @@

Visualization {selectedVertex && `- ${selectedVertex}`}

- {/* 可视化模式选择器 */} + {/* Visualization Mode Selector */}
Mode:
@@ -659,7 +841,7 @@

)} {loading && ( -
+
🔄
@@ -672,115 +854,112 @@

)} - {!loading && ( -
-
- {visualizationMode === "hyper" && ( -
-
- HyperGraph Detail -
- {hoverHyperedge && ( -
-
- HyperEdge -
- {hoverHyperedge.keywords && ( -
- Keywords: -
- {hoverHyperedge.keywords - .split(/,|,|、|。|/) - .map((keyword, i) => ( - - {keyword} - - ))} -
-
- )} - {hoverHyperedge.summary && ( -
-
Summary:
-
- {hoverHyperedge.summary} -
+
+
+ {visualizationMode === "hyper" && ( +
+
+ HyperGraph Detail +
+ {hoverHyperedge && ( +
+
+ HyperEdge +
+ {hoverHyperedge.description && ( +
+
Description:
+
+ {hoverHyperedge.description}
- )} - {hoverHyperedge.members?.length > 0 && ( -
-
- Nodes ({hoverHyperedge.members.length}): -
-
- {hoverHyperedge.members.map((member, i) => ( +
+ )} + {hoverHyperedge.keywords && ( +
+ Keywords: +
+ {hoverHyperedge.keywords + .split(/,|,|、|。|/) + .map((keyword, i) => ( - {member} + {keyword} ))} -
- )} -
- )} - {hoverNode && ( -
-
- Node
- {hoverNode.entity_name && ( -
- Name: - - {hoverNode.entity_name} - -
- )} - {hoverNode.entity_type && ( -
- Type: - - {hoverNode.entity_type} - -
- )} - {hoverNode.description && ( -
- - Description: - - - {formatDescription(hoverNode.description)} - + )} + + {hoverHyperedge.members?.length > 0 && ( +
+
+ Nodes ({hoverHyperedge.members.length}):
- )} - {hoverNode.additional_properties && ( -
- - Additional Properties: - - - {formatDescription( - hoverNode.additional_properties - )} - +
+ {hoverHyperedge.members.map((member, i) => ( + + {member} + + ))}
- )} +
+ )} +
+ )} + {hoverNode && ( +
+
+ Node
- )} -
- )} -
- )} + {hoverNode.entity_name && ( +
+ Name: + + {hoverNode.entity_name} + +
+ )} + {hoverNode.entity_type && ( +
+ Type: + + {hoverNode.entity_type} + +
+ )} + {hoverNode.description && ( +
+ Description: + + {formatDescription(hoverNode.description)} + +
+ )} + {hoverNode.additional_properties && ( +
+ + Additional Properties: + + + {formatDescription( + hoverNode.additional_properties + )} + +
+ )} +
+ )} +
+ )} +