66import webbrowser
77from pathlib import Path
88from typing import Any , Dict
9- from urllib .parse import urlparse
9+ from urllib .parse import parse_qs , urlparse
1010
1111from .hypergraph import HypergraphDB
1212
1313
14- class HypergraphViewer :
15- """Hypergraph visualization tool """
14+ class HypergraphAPIHandler ( http . server . BaseHTTPRequestHandler ) :
15+ """HTTP request handler with API endpoints """
1616
17- def __init__ (self , hypergraph_db : HypergraphDB , port : int = 8080 ):
17+ def __init__ (self , hypergraph_db : HypergraphDB , * args , ** kwargs ):
1818 self .hypergraph_db = hypergraph_db
19- self .port = port
20- self .html_content = self ._generate_html_with_data ()
21-
22- def _generate_html_with_data (self ):
23- """Generate HTML content with embedded data"""
24- # Get all data
25- database_info = {
19+ super ().__init__ (* args , ** kwargs )
20+
21+ def log_message (self , format , * args ):
22+ """Disable default logging"""
23+ pass
24+
25+ def do_GET (self ):
26+ """Handle GET requests"""
27+ parsed_path = urlparse (self .path )
28+ path = parsed_path .path
29+ query_params = parse_qs (parsed_path .query )
30+
31+ # CORS headers
32+ self .send_response (200 )
33+ self .send_header ("Access-Control-Allow-Origin" , "*" )
34+ self .send_header ("Access-Control-Allow-Methods" , "GET, POST, OPTIONS" )
35+ self .send_header ("Access-Control-Allow-Headers" , "Content-Type" )
36+
37+ # Route handling
38+ if path == "/" or path == "/index.html" :
39+ self .send_header ("Content-type" , "text/html; charset=utf-8" )
40+ self .end_headers ()
41+ self .wfile .write (self ._get_html_template ().encode ("utf-8" ))
42+
43+ elif path == "/api/database/info" :
44+ self .send_header ("Content-type" , "application/json; charset=utf-8" )
45+ self .end_headers ()
46+ response = self ._get_database_info ()
47+ self .wfile .write (json .dumps (response , ensure_ascii = False ).encode ("utf-8" ))
48+
49+ elif path == "/api/vertices" :
50+ self .send_header ("Content-type" , "application/json; charset=utf-8" )
51+ self .end_headers ()
52+
53+ # Parse query parameters
54+ page = int (query_params .get ("page" , ["1" ])[0 ])
55+ page_size = int (query_params .get ("page_size" , ["50" ])[0 ])
56+ search = query_params .get ("search" , ["" ])[0 ]
57+ sort_by = query_params .get ("sort_by" , ["degree" ])[0 ]
58+ sort_order = query_params .get ("sort_order" , ["desc" ])[0 ]
59+
60+ response = self ._get_vertices (page , page_size , search , sort_by , sort_order )
61+ self .wfile .write (json .dumps (response , ensure_ascii = False ).encode ("utf-8" ))
62+
63+ elif path == "/api/graph" :
64+ self .send_header ("Content-type" , "application/json; charset=utf-8" )
65+ self .end_headers ()
66+
67+ vertex_id = query_params .get ("vertex_id" , ["" ])[0 ]
68+ if vertex_id :
69+ response = self ._get_graph_data (vertex_id )
70+ else :
71+ response = {"error" : "vertex_id parameter is required" }
72+
73+ self .wfile .write (json .dumps (response , ensure_ascii = False ).encode ("utf-8" ))
74+
75+ else :
76+ self .send_header ("Content-type" , "text/plain; charset=utf-8" )
77+ self .end_headers ()
78+ self .wfile .write (b"404 Not Found" )
79+
80+ def do_OPTIONS (self ):
81+ """Handle OPTIONS requests for CORS preflight"""
82+ self .send_response (200 )
83+ self .send_header ("Access-Control-Allow-Origin" , "*" )
84+ self .send_header ("Access-Control-Allow-Methods" , "GET, POST, OPTIONS" )
85+ self .send_header ("Access-Control-Allow-Headers" , "Content-Type" )
86+ self .end_headers ()
87+
88+ def _get_database_info (self ) -> Dict [str , Any ]:
89+ """Get database information"""
90+ return {
2691 "name" : "current_hypergraph" ,
2792 "vertices" : self .hypergraph_db .num_v ,
2893 "edges" : self .hypergraph_db .num_e ,
2994 }
3095
31- # Get vertex list
32- vertices = list (self .hypergraph_db .all_v )[:100 ]
96+ def _get_vertices (self , page : int , page_size : int , search : str , sort_by : str , sort_order : str ) -> Dict [str , Any ]:
97+ """Get vertices with pagination and search"""
98+ hg = self .hypergraph_db
99+
100+ # Get all vertices
101+ all_vertices = list (hg .all_v )
102+
103+ # Prepare vertex data with search scoring
33104 vertex_data = []
105+ search_lower = search .lower () if search else ""
106+
107+ for v_id in all_vertices :
108+ v_data = hg .v (v_id , {})
109+ degree = hg .degree_v (v_id )
110+ entity_type = v_data .get ("entity_type" , "" )
111+ description = v_data .get ("description" , "" )
112+
113+ # Calculate search score
114+ score = 0
115+ if search_lower :
116+ if search_lower in str (v_id ).lower ():
117+ score += 3
118+ if search_lower in entity_type .lower ():
119+ score += 2
120+ if search_lower in description .lower ():
121+ score += 1
122+
123+ # Skip if no match
124+ if score == 0 :
125+ continue
34126
35- for v_id in vertices :
36- v_data = self .hypergraph_db .v (v_id , {})
37127 vertex_data .append (
38128 {
39129 "id" : v_id ,
40- "degree" : self .hypergraph_db .degree_v (v_id ),
41- "entity_type" : v_data .get ("entity_type" , "" ),
42- "description" : (
43- v_data .get ("description" , "" )[:100 ] + "..."
44- if len (v_data .get ("description" , "" )) > 100
45- else v_data .get ("description" , "" )
46- ),
130+ "degree" : degree ,
131+ "entity_type" : entity_type ,
132+ "description" : (description [:100 ] + "..." if len (description ) > 100 else description ),
133+ "score" : score ,
47134 }
48135 )
49136
50- # Sort by degree
51- vertex_data .sort (key = lambda x : x ["degree" ], reverse = True )
52-
53- # Get graph data for all vertices
54- graph_data = {}
55- for vertex in vertex_data :
56- vertex_id = vertex ["id" ]
57- graph_data [vertex_id ] = self ._get_vertex_neighbor_data (self .hypergraph_db , vertex_id )
58-
59- # Embed data into HTML
60- return self ._get_html_template (database_info , vertex_data , graph_data )
137+ # Sort vertices
138+ if search_lower :
139+ # Sort by search score if searching (no degree filtering)
140+ vertex_data .sort (key = lambda x : x ["score" ], reverse = True )
141+ elif sort_by == "degree" :
142+ # First, separate by degree threshold (degree > 50 goes to the end)
143+ vertex_data .sort (key = lambda x : (x ["degree" ] > 50 , - x ["degree" ] if sort_order == "desc" else x ["degree" ]))
144+ elif sort_by == "id" :
145+ # First, separate by degree threshold (degree > 50 goes to the end)
146+ vertex_data .sort (key = lambda x : (x ["degree" ] > 50 , str (x ["id" ])), reverse = (sort_order == "desc" ))
147+
148+ # Remove score from output
149+ for v in vertex_data :
150+ v .pop ("score" , None )
151+
152+ # Pagination
153+ total = len (vertex_data )
154+ start = (page - 1 ) * page_size
155+ end = start + page_size
156+ paginated_data = vertex_data [start :end ]
157+
158+ return {
159+ "data" : paginated_data ,
160+ "pagination" : {
161+ "page" : page ,
162+ "page_size" : page_size ,
163+ "total" : total ,
164+ "total_pages" : (total + page_size - 1 ) // page_size ,
165+ },
166+ }
61167
62- def _get_vertex_neighbor_data (self , hypergraph_db : HypergraphDB , vertex_id : str ) -> Dict [str , Any ]:
63- """Get vertex neighbor data"""
64- hg = hypergraph_db
168+ def _get_graph_data (self , vertex_id : str ) -> Dict [str , Any ]:
169+ """Get graph data for a vertex """
170+ hg = self . hypergraph_db
65171
66172 if not hg .has_v (vertex_id ):
67- raise ValueError ( f"Vertex { vertex_id } not found" )
173+ return { "error" : f"Vertex { vertex_id } not found" }
68174
69175 # Get all neighbor hyperedges of the vertex
70176 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)
83189 edges_data [edge_key ] = {
84190 "keywords" : edge_data .get ("keywords" , "" ),
85191 "summary" : edge_data .get ("summary" , "" ),
86- "weight" : len (edge_tuple ), # Hyperedge weight equals the number of vertices it contains
192+ "weight" : len (edge_tuple ),
193+ ** edge_data ,
87194 }
88195
89196 # Get data for all vertices
@@ -99,13 +206,8 @@ def _get_vertex_neighbor_data(self, hypergraph_db: HypergraphDB, vertex_id: str)
99206
100207 return {"vertices" : vertices_data , "edges" : edges_data }
101208
102- def _get_html_template (self , database_info : Dict , vertex_data : list , graph_data : Dict ) -> str :
103- """Get HTML template with embedded data"""
104- # Serialize data to JSON string
105- embedded_data = {"database" : database_info , "vertices" : vertex_data , "graphs" : graph_data }
106- data_json = json .dumps (embedded_data , ensure_ascii = False )
107-
108- # Read HTML template file
209+ def _get_html_template (self ) -> str :
210+ """Get HTML template without embedded data"""
109211 template_path = Path (__file__ ).parent / "templates" / "hypergraph_viewer.html"
110212
111213 try :
@@ -114,31 +216,24 @@ def _get_html_template(self, database_info: Dict, vertex_data: list, graph_data:
114216 except FileNotFoundError :
115217 raise FileNotFoundError (f"HTML template file not found: { template_path } " )
116218
117- # Replace placeholders in template
118- html_content = html_template .replace ("{{DATA_JSON}}" , data_json )
219+ # Replace placeholder with empty object (data will be loaded via API)
220+ html_content = html_template .replace ("{{DATA_JSON}}" , "{}" )
119221
120222 return html_content
121223
122- def start_server (self , open_browser : bool = True ):
123- """Start simple HTTP server"""
124224
125- class CustomHTTPRequestHandler (http .server .BaseHTTPRequestHandler ):
126- def __init__ (self , html_content , * args , ** kwargs ):
127- self .html_content = html_content
128- super ().__init__ (* args , ** kwargs )
225+ class HypergraphViewer :
226+ """Hypergraph visualization tool"""
129227
130- def do_GET (self ):
131- self .send_response (200 )
132- self .send_header ("Content-type" , "text/html; charset=utf-8" )
133- self .end_headers ()
134- self .wfile .write (self .html_content .encode ("utf-8" ))
228+ def __init__ (self , hypergraph_db : HypergraphDB , port : int = 8080 ):
229+ self .hypergraph_db = hypergraph_db
230+ self .port = port
135231
136- def log_message (self , format , * args ):
137- # Disable log output
138- pass
232+ def start_server (self , open_browser : bool = True ):
233+ """Start HTTP server with API endpoints"""
139234
140235 def run_server ():
141- handler = lambda * args , ** kwargs : CustomHTTPRequestHandler (self .html_content , * args , ** kwargs )
236+ handler = lambda * args , ** kwargs : HypergraphAPIHandler (self .hypergraph_db , * args , ** kwargs )
142237 self .httpd = socketserver .TCPServer (("127.0.0.1" , self .port ), handler )
143238 self .httpd .serve_forever ()
144239
0 commit comments