EchaRz commited on
Commit
f748a55
·
verified ·
1 Parent(s): babab52

Add html, database, index files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ cnnhealthnews2.db filter=lfs diff=lfs merge=lfs -text
37
+ news_index_compressed/index.faiss filter=lfs diff=lfs merge=lfs -text
38
+ relations_new.db filter=lfs diff=lfs merge=lfs -text
39
+ triplets_index_compressed/index.faiss filter=lfs diff=lfs merge=lfs -text
40
+ triplets_new.db filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ .vercel
Home.png ADDED
README.md CHANGED
@@ -1,12 +1,2 @@
1
- ---
2
- title: KnowledgeRetrievalProject
3
- emoji: 🏃
4
- colorFrom: gray
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- license: apache-2.0
9
- short_description: Retrieve knowledge from Indonesian News
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # Knowledge-Retrieval-Project
2
+ This repository contains the core code for a web application that lets users explore and retrieve knowledge built from Indonesian national news sources (and potentially other sources).
 
 
 
 
 
 
 
 
 
 
backend.py ADDED
@@ -0,0 +1,596 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import contextlib
3
+ import json
4
+ from http.server import BaseHTTPRequestHandler
5
+ from urllib.parse import urlparse, parse_qs
6
+ import traceback
7
+ from pydantic import BaseModel, Field
8
+ from typing import List, Dict, Tuple
9
+ import os
10
+ from langchain_community.vectorstores import FAISS
11
+ from langchain_community.embeddings import FakeEmbeddings
12
+ from langchain_community.vectorstores.utils import DistanceStrategy
13
+ from together import Together
14
+ import numpy as np
15
+ from collections import defaultdict
16
+
17
+ app = FastAPI(title="Knowledge Graph API")
18
+
19
+ # Enable CORS for frontend access
20
+ app.add_middleware(
21
+ CORSMiddleware,
22
+ allow_origins=["*"],
23
+ allow_credentials=True,
24
+ allow_methods=["*"],
25
+ allow_headers=["*"],
26
+ )
27
+
28
+ # Database configuration - UPDATE THESE PATHS
29
+ DATABASE_CONFIG = {
30
+ "triplets_db": "triplets_new.db",
31
+ "definitions_db": "relations_new.db",
32
+ "news_db": "cnnhealthnews2.db",
33
+ "triplets_table": "triplets",
34
+ "definitions_table": "relations",
35
+ "head_column": "head_entity",
36
+ "relation_column": "relation",
37
+ "tail_column": "tail_entity",
38
+ "definition_column": "definition",
39
+ "link_column": "link",
40
+ "title_column": "column",
41
+ "content_column": "content"
42
+ }
43
+
44
+ class GraphNode(BaseModel):
45
+ id: str
46
+ label: str
47
+ type: str = "entity"
48
+
49
+ class GraphEdge(BaseModel):
50
+ source: str
51
+ target: str
52
+ relation: str
53
+ definition: Optional[str] = None
54
+
55
+ class GraphData(BaseModel):
56
+ nodes: List[GraphNode]
57
+ edges: List[GraphEdge]
58
+
59
+ class TripletData(BaseModel):
60
+ head: str
61
+ relation: str
62
+ tail: str
63
+
64
+ class RelationDefinition(BaseModel):
65
+ relation: str
66
+ definition: str
67
+
68
+ class RetrieveTripletsResponse(BaseModel):
69
+ triplets: List[TripletData]
70
+ relations: List[RelationDefinition]
71
+
72
+ class NewsItem(BaseModel):
73
+ url: str
74
+ content: str
75
+ preview: str
76
+ title: str
77
+
78
+ class QueryRequest(BaseModel):
79
+ query: str
80
+
81
+ class QueryResponse(BaseModel):
82
+ answer: str
83
+ triplets: List[TripletData]
84
+ relations: List[RelationDefinition]
85
+ news_items: List[NewsItem]
86
+ graph_data: GraphData
87
+
88
+ class ExtractedInformationNews(BaseModel):
89
+ extracted_information: str = Field(description="Extracted information")
90
+ links: list = Field(description="citation links")
91
+
92
+ class ExtractedInformation(BaseModel):
93
+ extracted_information: str = Field(description="Extracted information")
94
+
95
+ @contextlib.contextmanager
96
+ def get_triplets_db():
97
+ conn = None
98
+ try:
99
+ conn = sqlite3.connect(DATABASE_CONFIG["triplets_db"])
100
+ yield conn
101
+ finally:
102
+ if conn:
103
+ conn.close()
104
+
105
+ @contextlib.contextmanager
106
+ def get_news_db():
107
+ conn = None
108
+ try:
109
+ conn = sqlite3.connect(DATABASE_CONFIG["news_db"])
110
+ yield conn
111
+ finally:
112
+ if conn:
113
+ conn.close()
114
+
115
+ @contextlib.contextmanager
116
+ def get_definitions_db():
117
+ conn = None
118
+ try:
119
+ conn = safe_connect(DATABASE_CONFIG["definitions_db"])
120
+ yield conn
121
+ finally:
122
+ if conn:
123
+ conn.close()
124
+
125
+ def retrieve_triplets(query: str) -> Tuple[List[Tuple[str, str, str]], List[Tuple[str, str]]]:
126
+ """
127
+ Args:
128
+ query (str): User query
129
+
130
+ Returns:
131
+ Tuple containing:
132
+ - List of triplets: [(head, relation, tail), ...]
133
+ - List of relations with definitions: [(relation, definition), ...]
134
+ """
135
+ API_KEY = os.environ.get("TOGETHER_API_KEY")
136
+ client = Together(api_key = API_KEY)
137
+
138
+ dummy_embeddings = FakeEmbeddings(size=768)
139
+ triplets_store = FAISS.load_local(
140
+ "triplets_index_compressed", dummy_embeddings, allow_dangerous_deserialization=True
141
+ )
142
+ triplets_store.index.nprobe = 100
143
+ triplets_store._normalize_L2 = True
144
+ triplets_store.distance_strategy = DistanceStrategy.COSINE
145
+
146
+ response = client.embeddings.create(
147
+ model = "Alibaba-NLP/gte-modernbert-base",
148
+ input = query
149
+ )
150
+
151
+ emb = np.array(response.data[0].embedding)
152
+ emb = emb / np.linalg.norm(emb)
153
+
154
+ related_head_entity = []
155
+ result_triplets = triplets_store.similarity_search_with_score_by_vector(emb, k=100)
156
+ for res, score in result_triplets:
157
+ if score > 0.7:
158
+ related_head_entity.append(res)
159
+
160
+ try:
161
+ all_triplets = []
162
+ with get_triplets_db() as conn:
163
+ head_col = DATABASE_CONFIG["head_column"]
164
+ rel_col = DATABASE_CONFIG["relation_column"]
165
+ tail_col = DATABASE_CONFIG["tail_column"]
166
+
167
+ for head_entity in related_head_entity:
168
+ he = head_entity.page_content
169
+ cursor = conn.cursor()
170
+ cursor.execute("SELECT * FROM triplets WHERE head_entity = (?)", ([he]))
171
+ rows = cursor.fetchall()
172
+ triplets = [(str(row[0]), str(row[1]), str(row[2])) for row in rows]
173
+ all_triplets += triplets
174
+
175
+ all_relations = []
176
+ relations = [relation for _, relation, _ in all_triplets]
177
+ with get_definitions_db() as conn:
178
+ rel_col = DATABASE_CONFIG["relation_column"]
179
+ def_col = DATABASE_CONFIG["definition_column"]
180
+
181
+ for rel in set(relations):
182
+ cursor = conn.cursor()
183
+ cursor.execute("SELECT * FROM relations WHERE relation = (?)", ([rel]))
184
+ rows = cursor.fetchall()
185
+ relation = [(str(row[0]), str(row[1])) for row in rows]
186
+ all_relations += relation
187
+
188
+ return all_triplets, all_relations
189
+
190
+ except Exception as e:
191
+ print(f"Error in retrieve_triplets: {e}")
192
+ return [], []
193
+
194
+ def retrieve_news(query: str) -> Dict[str, str]:
195
+ """
196
+ Args:
197
+ query (str): User query
198
+
199
+ Returns: Tuple
200
+ - Related content
201
+ - Links of the related content
202
+ """
203
+ API_KEY = os.environ.get("TOGETHER_API_KEY")
204
+ client = Together(api_key = API_KEY)
205
+
206
+ dummy_embeddings = FakeEmbeddings(size=768)
207
+ news_store = FAISS.load_local(
208
+ "news_index_compressed", dummy_embeddings, allow_dangerous_deserialization=True
209
+ )
210
+ news_store.index.nprobe = 100
211
+ news_store._normalize_L2 = True
212
+ news_store.distance_strategy = DistanceStrategy.COSINE
213
+
214
+ news_store._normalize_L2 = True
215
+ news_store.distance_strategy = DistanceStrategy.COSINE
216
+
217
+ response = client.embeddings.create(
218
+ model = "Alibaba-NLP/gte-modernbert-base",
219
+ input = query
220
+ )
221
+
222
+ emb = np.array(response.data[0].embedding)
223
+ emb = emb / np.linalg.norm(emb)
224
+
225
+ related_news_content = []
226
+ result_news= news_store.similarity_search_with_score_by_vector(emb, k=500)
227
+ for res, score in result_news:
228
+ if score > 0.7:
229
+ print(score)
230
+ related_news_content.append(res)
231
+
232
+ news_dict = defaultdict(list)
233
+ links = [res.metadata["link"] for res in related_news_content]
234
+ for idx, link in enumerate(links):
235
+ news_dict[link].append(related_news_content[idx].page_content)
236
+
237
+ content_only = [". ".join(sentences) for sentences in news_dict.values()]
238
+
239
+ return content_only, links
240
+
241
+
242
+ def extract_information_from_triplets(query: str,
243
+ triplets: List[Tuple[str, str, str]],
244
+ relations: List[Tuple[str, str]]) -> str:
245
+ """
246
+ REPLACE THIS FUNCTION WITH YOUR ACTUAL IMPLEMENTATION
247
+
248
+ Args:
249
+ triplets: List of triplets from retrieve_triplets
250
+ relations: List of relation definitions from retrieve_triplets
251
+
252
+ Returns:
253
+ str: Extracted information from triplets
254
+ """
255
+ system_prompt = f'''Given a a list of relational triplets and a list of relation and its definition. Extract the information from the triplets to answer query question.
256
+ If there is no related or useful information can be extracted from the triplets to answer the query question, inform "No related information found."
257
+ Give the output in paragraphs form narratively, you can explain the reason behind your answer in detail."
258
+ '''
259
+
260
+ user_prompt = f'''
261
+ query question: {query}
262
+ list of triplets: {triplets}
263
+ list of relations and their definition: {relations}
264
+ extracted information:
265
+ '''
266
+
267
+ API_KEY = os.environ.get("TOGETHER_API_KEY")
268
+ client = Together(api_key = API_KEY)
269
+
270
+ response = client.chat.completions.create(
271
+ model="meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
272
+ temperature = 0,
273
+ messages=[{
274
+ "role": "system",
275
+ "content": [
276
+ {"type": "text", "text":system_prompt}
277
+ ]
278
+ },
279
+ {
280
+ "role": "user",
281
+ "content": [
282
+ {"type": "text", "text":user_prompt},
283
+ ]
284
+ }]
285
+ )
286
+
287
+ return response.choices[0].message.content
288
+
289
+ def extract_information_from_news(query: str,
290
+ news_list: Dict[str, str]) -> Tuple[str, List[str]]:
291
+ """
292
+ Args:
293
+ news_list: List from retrieve_news
294
+
295
+ Returns:
296
+ Extracted information string
297
+ """
298
+ system_prompt = f'''Given a list of some information related to the query, extract all important information from the list to answer query question.
299
+ Every item in the list represent one information, if the information is ambiguous (e.g. contains unknown pronoun to which it refers), do not use that information to answer the query.
300
+ You don't have to use all the information, only use the information that has clarity and a good basis, but try to use as many information as possible.
301
+ If there is no related or useful information can be extracted from the news information to answer the query question, write "No related information found." as the extracted_information output.
302
+ Give the extracted_information output in paragraphs form detailedly.
303
+ The output must be in this form: {{"extracted_information": <output paragraphs>}}
304
+ '''
305
+
306
+ user_prompt = f'''
307
+ query: {query}
308
+ news list: {news_list}
309
+ output:
310
+ '''
311
+
312
+ response = client.chat.completions.create(
313
+ model="meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
314
+ response_format={
315
+ "type": "json_schema",
316
+ "schema": ExtractedInformation.model_json_schema(),
317
+ },
318
+ temperature = 0,
319
+ messages=[{
320
+ "role": "system",
321
+ "content": [
322
+ {"type": "text", "text":system_prompt}
323
+ ]
324
+ },
325
+ {
326
+ "role": "user",
327
+ "content": [
328
+ {"type": "text", "text":user_prompt},
329
+ ]
330
+ }]
331
+ )
332
+ response = json.loads(response.choices[0].message.content)
333
+ info = response['extracted_information']
334
+
335
+ return info
336
+
337
+ def extract_information(query:str, triplet_info: str, news_info: str, language:str) -> str:
338
+ """
339
+ Args:
340
+ triplet_info: Information extracted from triplets
341
+ news_info: Information extracted from news
342
+
343
+ Returns:
344
+ str: Final answer for the user
345
+ """
346
+ client = Together(api_key = API_KEY)
347
+ system_prompt = f'''Given information from two sources, combine the information and make a comprehensive and informative paragraph that answer the query.
348
+ Make sure the output paragraph includes all crucial information and given in detail.
349
+ If there is no related or useful information can be extracted from the triplets to answer the query question, inform "No related information found."
350
+ Remember this paragraph will be shown to user, so make sure it is based on facts and data, also use appropriate language.
351
+ The output must be in this form and in {language} language: {{"extracted_information": <output paragraphs>}}
352
+ '''
353
+
354
+ user_prompt = f'''
355
+ query: {query}
356
+ first source: {triplet_info}
357
+ second source: {news_info}
358
+ extracted information:
359
+ '''
360
+
361
+ response = client.chat.completions.create(
362
+ model="meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
363
+ response_format={
364
+ "type": "json_schema",
365
+ "schema": ExtractedInformation.model_json_schema(),
366
+ },
367
+ temperature = 0,
368
+ messages=[{
369
+ "role": "system",
370
+ "content": [
371
+ {"type": "text", "text":system_prompt}
372
+ ]
373
+ },
374
+ {
375
+ "role": "user",
376
+ "content": [
377
+ {"type": "text", "text":user_prompt},
378
+ ]
379
+ }]
380
+ )
381
+
382
+ response = json.loads(response.choices[0].message.content)
383
+ answer = response["extracted_information"]
384
+ return answer
385
+
386
+ def news_preview(links: list[str]) -> Tuple[str, str, str]:
387
+ try:
388
+ preview_contents = []
389
+ with get_news_db() as conn:
390
+ for i in links:
391
+ cursor = conn.cursor()
392
+ cursor.execute("SELECT link, title, content FROM CNNHEALTHNEWS2 WHERE link = (?)", ([i]))
393
+ rows = cursor.fetchall()
394
+ prevs = [(str(row[0]), str(row[1]), str(row[2])) for row in rows]
395
+ preview_contents += prevs
396
+
397
+ return preview_contents
398
+
399
+ except Exception as e:
400
+ print(f"Error in news_preview: {e}")
401
+ return ("", "", "")
402
+
403
+ class Language(BaseModel):
404
+ query: str = Field(description="Translated query")
405
+ language: str = Field(description="Query's language")
406
+
407
+ def query_language(query):
408
+ system_prompt = f'''Your task is to determine what language the question is written in and translate it to english if it is not in English.
409
+ The output must be in this form: {{query: <translated query>, language: <query's language>}}
410
+ '''
411
+
412
+ user_prompt = f'''
413
+ query: {query}
414
+ output:
415
+ '''
416
+ client = Together(api_key = API_KEY)
417
+
418
+ response = client.chat.completions.create(
419
+ model="meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
420
+ response_format={
421
+ "type": "json_schema",
422
+ "schema": Language.model_json_schema(),
423
+ },
424
+ temperature = 0,
425
+ messages=[{
426
+ "role": "system",
427
+ "content": [
428
+ {"type": "text", "text":system_prompt}
429
+ ]
430
+ },
431
+ {
432
+ "role": "user",
433
+ "content": [
434
+ {"type": "text", "text":user_prompt},
435
+ ]
436
+ }])
437
+
438
+ return json.loads(response.choices[0].message.content)
439
+
440
+ #API ENDPOINTS
441
+
442
+ @app.post("/api/query", response_model=QueryResponse)
443
+ def process_query(request: QueryRequest):
444
+ """Process user query and return comprehensive response"""
445
+ try:
446
+ # Step 1: Retrieve triplets
447
+ query = request.query
448
+ query = query_language(query)
449
+
450
+ triplets_data, relations_data = retrieve_triplets(query['query'])
451
+
452
+ # Step 2: Retrieve news
453
+ news_list, news_links = retrieve_news(query['query'])
454
+
455
+ # Step 3: Extract information from triplets
456
+ triplet_info = extract_information_from_triplets(query['query'], triplets_data, relations_data)
457
+
458
+ # Step 4: Extract information from news
459
+ news_info = extract_information_from_news(query['query'], news_list)
460
+
461
+ # Step 5: Generate final answer
462
+ final_answer = extract_information(query['query'], triplet_info, news_info, query['language'])
463
+
464
+ # Convert triplets to response format
465
+ triplets = [TripletData(head=t[0], relation=t[1], tail=t[2]) for t in triplets_data]
466
+ relations = [RelationDefinition(relation=r[0], definition=r[1]) for r in relations_data]
467
+
468
+ # Convert news to response format with previews
469
+ news_prev = news_preview(news_links)
470
+ news_items = []
471
+ for url, title, content in news_prev:
472
+ preview = content[:300] + "..." if len(content) > 300 else content
473
+ news_items.append(NewsItem(
474
+ url=url,
475
+ content=content,
476
+ preview=preview,
477
+ title=title
478
+ ))
479
+
480
+ # Create mini graph data for visualization
481
+ nodes_set = set()
482
+ edges = []
483
+
484
+ for triplet in triplets_data:
485
+ head, relation, tail = triplet
486
+ nodes_set.add(head)
487
+ nodes_set.add(tail)
488
+
489
+ # Find definition for this relation
490
+ definition = "No definition available"
491
+ for rel, def_text in relations_data:
492
+ if rel == relation:
493
+ definition = def_text
494
+ break
495
+
496
+ edges.append(GraphEdge(
497
+ source=head,
498
+ target=tail,
499
+ relation=relation,
500
+ definition=definition
501
+ ))
502
+
503
+ nodes = [GraphNode(id=node, label=node) for node in nodes_set]
504
+ graph_data = GraphData(nodes=nodes, edges=edges)
505
+
506
+ return QueryResponse(
507
+ answer=final_answer,
508
+ triplets=triplets,
509
+ relations=relations,
510
+ news_items=news_items,
511
+ graph_data=graph_data
512
+ )
513
+
514
+ except Exception as e:
515
+ print(f"Error in process_query: {e}")
516
+ raise HTTPException(status_code=500, detail=f"Query processing failed: {str(e)}")
517
+
518
+ @app.get("/api/graph", response_model=GraphData)
519
+ def get_graph_data(
520
+ search: Optional[str] = None,
521
+ triplets_db: sqlite3.Connection = Depends(get_triplets_connection),
522
+ definitions_db: sqlite3.Connection = Depends(get_definitions_connection)
523
+ ):
524
+ """Get complete graph data with nodes and edges."""
525
+
526
+ try:
527
+ # Build dynamic query based on configuration
528
+ table = DATABASE_CONFIG["triplets_table"]
529
+ head_col = DATABASE_CONFIG["head_column"]
530
+ rel_col = DATABASE_CONFIG["relation_column"]
531
+ tail_col = DATABASE_CONFIG["tail_column"]
532
+
533
+ base_query = f"SELECT {head_col}, {rel_col}, {tail_col} FROM {table}"
534
+ params = []
535
+
536
+ if search:
537
+ base_query += f" WHERE {head_col} LIKE ? OR {tail_col} LIKE ? OR {rel_col} LIKE ?"
538
+ search_term = f"%{search}%"
539
+ params = [search_term, search_term, search_term]
540
+
541
+ base_query += " LIMIT 1000"
542
+
543
+ # Get triplets
544
+ cursor = triplets_db.execute(base_query, params)
545
+ triplets = cursor.fetchall()
546
+
547
+ with get_definitions_db() as conn:
548
+ # Get definitions
549
+ def_table = DATABASE_CONFIG["definitions_table"]
550
+ def_col = DATABASE_CONFIG["definition_column"]
551
+ rel_col_def = DATABASE_CONFIG["relation_column"]
552
+
553
+ def_cursor = conn.execute(f"SELECT {rel_col_def}, {def_col} FROM {def_table}")
554
+ definitions = {row[0]: row[1] for row in def_cursor.fetchall()}
555
+
556
+ # Build nodes and edges
557
+ nodes_set = set()
558
+ edges = []
559
+
560
+ for triple in triplets:
561
+ head = triple[0]
562
+ relation = triple[1]
563
+ tail = triple[2]
564
+
565
+ # Add entities to nodes set
566
+ nodes_set.add(head)
567
+ nodes_set.add(tail)
568
+
569
+ # Create edge with definition
570
+ edge = GraphEdge(
571
+ source=head,
572
+ target=tail,
573
+ relation=relation,
574
+ definition=definitions.get(relation, "No definition available")
575
+ )
576
+ edges.append(edge)
577
+
578
+ # Convert nodes set to list of GraphNode objects
579
+ nodes = [GraphNode(id=node, label=node) for node in nodes_set]
580
+
581
+ return GraphData(nodes=nodes, edges=edges)
582
+
583
+ except Exception as e:
584
+ print(f"Error in get_graph_data: {e}")
585
+ raise HTTPException(status_code=500, detail=f"Database query failed: {str(e)}")
586
+
587
+ if __name__ == "__main__":
588
+ print("Starting Knowledge Graph API...")
589
+ print(f"Triplets DB: {DATABASE_CONFIG['triplets_db']}")
590
+ print(f"Definitions DB: {DATABASE_CONFIG['definitions_db']}")
591
+
592
+ import uvicorn
593
+ port = int(os.environ.get("PORT", 8000))
594
+ uvicorn.run(app, host="0.0.0.0", port=port)
595
+
596
+
cnnhealthnews2.db ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e79fa0fbf55962f43d8141b44d9e80e87e96b7c1222c066ed7262e49e5d40d65
3
+ size 12017664
explorepage.html ADDED
@@ -0,0 +1,774 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <link href="https://fonts.googleapis.com/css2?family=Mandali&display=swap" rel="stylesheet">
5
+ <link href="https://fonts.googleapis.com/css2?family=Varta:wght@400;500;600;700&display=swap" rel="stylesheet">
6
+ <meta charset="UTF-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <title>Knowledge Graph Explorer</title>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
10
+ <style>
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ body {
18
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
19
+ background: linear-gradient(135deg, #4a5568 0%, #2d3748 50%, #1a202c 100%);
20
+ color: white;
21
+ height: 100vh;
22
+ overflow: hidden;
23
+ }
24
+
25
+ .container {
26
+ display: flex;
27
+ height: 100vh;
28
+ }
29
+
30
+ /* Sidebar Styles */
31
+ .sidebar {
32
+ width: 400px;
33
+ background: rgba(0 0 0 0.25);
34
+ display: flex;
35
+ flex-direction: column;
36
+ transition: margin-left 0.3s ease;
37
+ position: relative;
38
+ z-index: 100;
39
+ }
40
+
41
+ .sidebar.collapsed {
42
+ margin-left: -400px;
43
+ }
44
+
45
+ /* Header */
46
+ .sidebar-header {
47
+ padding: 1.5rem;
48
+ }
49
+
50
+ .header-controls {
51
+ display: flex;
52
+ justify-content: space-between;
53
+ align-items: center;
54
+ margin-bottom: 1rem;
55
+ }
56
+
57
+ /* Menu Toggle - Always Visible */
58
+ .menu-toggle {
59
+ position: fixed;
60
+ top: 1.5rem;
61
+ left: 0.7rem;
62
+ z-index: 300;
63
+ background: none;
64
+ border: none;
65
+ color: white;
66
+ font-size: 1.5rem;
67
+ cursor: pointer;
68
+ padding: 0.75rem;
69
+ border-radius: 8px;
70
+ backdrop-filter: blur(10px);
71
+ }
72
+
73
+ .menu-toggle:hover {
74
+ background: rgba(0, 0, 0, 0.8);
75
+ }
76
+
77
+ .home-btn {
78
+ position: fixed;
79
+ background: none;
80
+ border: none;
81
+ color: white;
82
+ font-size: 1.5rem;
83
+ cursor: pointer;
84
+ padding: 0.5rem;
85
+ border-radius: 4px;
86
+ transition: background-color 0.3s ease;
87
+ }
88
+
89
+ .home-btn:hover {
90
+ background-color: rgba(0, 0, 0, 0.8);
91
+ }
92
+
93
+ .sidebar-title {
94
+ font-size: 2.5rem;
95
+ font-weight: 700;
96
+ color: #F8F3E7;
97
+ line-height: 1.2;
98
+ margin-top : 50px;
99
+ }
100
+
101
+ /* Search Section */
102
+ .search-section {
103
+ padding: 0 1.5rem 1.5rem;
104
+ }
105
+
106
+ .search-input {
107
+ width: 100%;
108
+ padding: 0.75rem 1rem;
109
+ background: rgb(248 243 231);
110
+ border-width: 2px;
111
+ border-style: solid;
112
+ border-color: #F3E7DD;
113
+ border-radius: 15px;
114
+ font-size: 0.9rem;
115
+ color: #797979;
116
+ margin-bottom: 1rem;
117
+ }
118
+
119
+ .search-input::placeholder {
120
+ color: #a0aec0;
121
+ }
122
+
123
+ .search-input:focus {
124
+ outline: none;
125
+ background: rgba(255, 255, 255, 1);
126
+ }
127
+
128
+ .reset-btn {
129
+ display: block; /* make it a block so margin works */
130
+ margin: 0 auto; /* this centers it horizontally */
131
+ background: rgb(110 131 131);
132
+ border: none;
133
+ color: white;
134
+ padding: 0.75rem 1.5rem;
135
+ border-radius: 20px;
136
+ font-size: 0.9rem;
137
+ font-weight: 500;
138
+ cursor: pointer;
139
+ max-width: 150px;
140
+ width: 100%;
141
+ box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2);
142
+ transition: all 0.3s ease;
143
+ }
144
+
145
+ .reset-btn:hover {
146
+ background: rgba(74, 85, 104, 1);
147
+ }
148
+
149
+ /* Instructions Panel */
150
+ .instructions-panel {
151
+ margin: 1rem;
152
+ background: rgb(248 243 231);
153
+ border-radius: 15px;
154
+ padding: 1.5rem;
155
+ color: #4a5568;
156
+ flex: 1;
157
+ margin-bottom: 7rem;
158
+ box-shadow: inset 0 4px 4px rgba(0,0,0,0.25);
159
+ }
160
+
161
+ .instructions-title {
162
+ font-size: 1.5rem;
163
+ font-weight: 800;
164
+ font-family: 'Varta', sans-serif;
165
+ color: #485656;
166
+ margin-bottom: 1rem;
167
+ text-align: center;
168
+ }
169
+
170
+ .instruction-item {
171
+ margin-bottom: 1rem;
172
+ font-family: 'Varta', sans-serif;
173
+ font-size: 1rem;
174
+ color: #485656;
175
+ line-height: 1.5;
176
+ font-weight: 300;
177
+ }
178
+
179
+ .instruction-item:last-child {
180
+ margin-bottom: 0;
181
+ }
182
+
183
+ .instruction-action {
184
+ font-weight: 700;
185
+ color: #485656;
186
+ }
187
+
188
+ /* Main Graph Area */
189
+ .main-content {
190
+ flex: 1;
191
+ position: relative;
192
+ background: rgb(77 83 109);
193
+ }
194
+
195
+ /* Home Button in Top-Right Corner */
196
+ .main-home-btn {
197
+ position: absolute;
198
+ top: 1.5rem;
199
+ right: 1.5rem;
200
+ z-index: 200;
201
+ background: rgba(0, 0, 0, 0.6);
202
+ border: none;
203
+ color: white;
204
+ font-size: 1.5rem;
205
+ cursor: pointer;
206
+ padding: 0.75rem;
207
+ border-radius: 8px;
208
+ transition: background-color 0.3s ease;
209
+ backdrop-filter: blur(10px);
210
+ }
211
+
212
+ .main-home-btn:hover {
213
+ background: rgba(0, 0, 0, 0.8);
214
+ }
215
+
216
+ /* Remove floating home - not needed anymore */
217
+ .floating-home {
218
+ display: none;
219
+ }
220
+
221
+ /* Graph Styles */
222
+ #graph {
223
+ width: 100%;
224
+ height: 100%;
225
+ }
226
+
227
+ .node {
228
+ cursor: pointer;
229
+ transition: all 0.3s ease;
230
+ filter: drop-shadow(0 0 6px rgba(76, 175, 80, 0.3));
231
+ }
232
+
233
+ .node:hover {
234
+ stroke-width: 3px;
235
+ filter: drop-shadow(0 0 12px rgba(76, 175, 80, 0.6));
236
+ }
237
+
238
+ .node.highlighted {
239
+ stroke: #4CAF50 !important;
240
+ stroke-width: 3px !important;
241
+ filter: drop-shadow(0 0 15px rgba(76, 175, 80, 0.8));
242
+ }
243
+
244
+ .node.selected {
245
+ stroke: #FFD700 !important;
246
+ stroke-width: 4px !important;
247
+ filter: drop-shadow(0 0 20px rgba(255, 215, 0, 0.8));
248
+ }
249
+
250
+ .node.dimmed {
251
+ opacity: 0.2;
252
+ filter: none;
253
+ }
254
+
255
+ .link {
256
+ stroke: rgba(255, 255, 255, 0.4);
257
+ stroke-width: 2px;
258
+ cursor: pointer;
259
+ transition: all 0.3s ease;
260
+ }
261
+
262
+ .link:hover {
263
+ stroke: #4CAF50;
264
+ stroke-width: 3px;
265
+ filter: drop-shadow(0 0 6px rgba(76, 175, 80, 0.5));
266
+ }
267
+
268
+ .link.highlighted {
269
+ stroke: #4CAF50 !important;
270
+ stroke-width: 3px !important;
271
+ filter: drop-shadow(0 0 8px rgba(76, 175, 80, 0.6));
272
+ }
273
+
274
+ .link.dimmed {
275
+ opacity: 0.1;
276
+ }
277
+
278
+ .node-label {
279
+ font-size: 11px;
280
+ font-weight: 600;
281
+ fill: white;
282
+ text-anchor: middle;
283
+ pointer-events: none;
284
+ text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
285
+ transition: all 0.3s ease;
286
+ }
287
+
288
+ .node-label.dimmed {
289
+ opacity: 0.2;
290
+ }
291
+
292
+ .node-label.highlighted {
293
+ fill: #4CAF50;
294
+ font-size: 13px;
295
+ text-shadow: 0 0 8px rgba(76, 175, 80, 0.8);
296
+ }
297
+
298
+ .tooltip {
299
+ position: absolute;
300
+ text-align: left;
301
+ padding: 1rem;
302
+ font-size: 0.9rem;
303
+ background: rgba(0, 0, 0, 0.9);
304
+ color: white;
305
+ border-radius: 8px;
306
+ pointer-events: none;
307
+ opacity: 0;
308
+ transition: opacity 0.3s;
309
+ max-width: 300px;
310
+ line-height: 1.5;
311
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
312
+ z-index: 1000;
313
+ }
314
+
315
+ .tooltip h4 {
316
+ margin: 0 0 0.5rem 0;
317
+ color: #4CAF50;
318
+ font-weight: 700;
319
+ }
320
+
321
+ .loading {
322
+ position: absolute;
323
+ top: 50%;
324
+ left: 50%;
325
+ transform: translate(-50%, -50%);
326
+ font-size: 1.1rem;
327
+ color: white;
328
+ text-align: center;
329
+ }
330
+
331
+ .loading-spinner {
332
+ border: 3px solid rgba(255, 255, 255, 0.3);
333
+ border-radius: 50%;
334
+ border-top: 3px solid white;
335
+ width: 40px;
336
+ height: 40px;
337
+ animation: spin 1s linear infinite;
338
+ margin: 0 auto 1rem;
339
+ }
340
+
341
+ @keyframes spin {
342
+ 0% { transform: rotate(0deg); }
343
+ 100% { transform: rotate(360deg); }
344
+ }
345
+
346
+ .error {
347
+ color: #ff6b6b;
348
+ background: rgba(255, 107, 107, 0.1);
349
+ padding: 1rem;
350
+ border-radius: 8px;
351
+ margin: 1rem;
352
+ border: 1px solid rgba(255, 107, 107, 0.3);
353
+ }
354
+
355
+ /* Responsive Design */
356
+ @media (max-width: 768px) {
357
+ .sidebar {
358
+ width: 100%;
359
+ position: absolute;
360
+ height: 100%;
361
+ z-index: 1000;
362
+ }
363
+
364
+ .sidebar.collapsed {
365
+ margin-left: -100%;
366
+ }
367
+ }
368
+ </style>
369
+ </head>
370
+ <body>
371
+ <div class="container">
372
+ <!-- Fixed Menu Toggle Button -->
373
+ <button class="menu-toggle" id="menuToggle">☰</button>
374
+
375
+ <!-- Sidebar -->
376
+ <div class="sidebar" id="sidebar">
377
+ <div class="sidebar-header">
378
+ <h1 class="sidebar-title">KNOWLEDGE<br>GRAPH</h1>
379
+ </div>
380
+
381
+ <div class="search-section">
382
+ <input
383
+ type="text"
384
+ class="search-input"
385
+ id="searchInput"
386
+ placeholder="Search nodes and relations..."
387
+ >
388
+ <button class="reset-btn" id="resetBtn">Reset Highlight</button>
389
+ </div>
390
+
391
+ <div class="instructions-panel">
392
+ <h3 class="instructions-title">HOW TO USE</h3>
393
+ <div class="instruction-item">
394
+ <span class="instruction-action">Click a node</span> to highlight its connections
395
+ </div>
396
+ <div class="instruction-item">
397
+ <span class="instruction-action">Hover over nodes and edges</span> for details
398
+ </div>
399
+ <div class="instruction-item">
400
+ <span class="instruction-action">Drag nodes</span> to reposition them
401
+ </div>
402
+ <div class="instruction-item">
403
+ <span class="instruction-action">Zoom and pan</span> to explore the graph
404
+ </div>
405
+ <div class="instruction-item">
406
+ <span class="instruction-action">Search</span> to filter nodes and relations
407
+ </div>
408
+ </div>
409
+ </div>
410
+
411
+ <!-- Main Graph Area -->
412
+ <div class="main-content">
413
+ <!-- Home Button in Top-Right -->
414
+ <button class="main-home-btn" id="mainHomeBtn">
415
+ <img src="Home.png" alt="Home" style="width: 20px; height: 20px;">
416
+ </button>
417
+ <div id="loading" class="loading">
418
+ <div class="loading-spinner"></div>
419
+ Loading knowledge graph...
420
+ </div>
421
+ <svg id="graph"></svg>
422
+ </div>
423
+ </div>
424
+
425
+ <div class="tooltip" id="tooltip"></div>
426
+
427
+ <script>
428
+ // Configuration
429
+ const API_BASE = '/api';
430
+
431
+ // Global variables
432
+ let graphData = { nodes: [], edges: [] };
433
+ let simulation;
434
+ let svg, g;
435
+ let currentSearch = '';
436
+ let selectedNode = null;
437
+ let highlightedElements = { nodes: new Set(), edges: new Set() };
438
+ let sidebarCollapsed = false;
439
+
440
+ // Initialize the application
441
+ async function init() {
442
+ setupEventListeners();
443
+ setupVisualizationSVG();
444
+ await loadGraphData();
445
+ hideLoading();
446
+ }
447
+
448
+ function setupEventListeners() {
449
+ // Sidebar toggle
450
+ document.getElementById('menuToggle').addEventListener('click', toggleSidebar);
451
+ document.getElementById('mainHomeBtn').addEventListener('click', goHome);
452
+
453
+ // Search and reset
454
+ document.getElementById('searchInput').addEventListener('input', debounce(handleSearch, 300));
455
+ document.getElementById('resetBtn').addEventListener('click', resetHighlighting);
456
+ }
457
+
458
+ function toggleSidebar() {
459
+ const sidebar = document.getElementById('sidebar');
460
+ sidebarCollapsed = !sidebarCollapsed;
461
+
462
+ if (sidebarCollapsed) {
463
+ sidebar.classList.add('collapsed');
464
+ } else {
465
+ sidebar.classList.remove('collapsed');
466
+ }
467
+ }
468
+
469
+ function goHome() {
470
+ // Navigate back to main page
471
+ window.location.href = 'index.html'; // Adjust path as needed
472
+ }
473
+
474
+ function setupVisualizationSVG() {
475
+ const container = document.querySelector('.main-content');
476
+ const containerRect = container.getBoundingClientRect();
477
+
478
+ svg = d3.select('#graph')
479
+ .attr('width', containerRect.width)
480
+ .attr('height', containerRect.height);
481
+
482
+ g = svg.append('g');
483
+
484
+ // Add zoom behavior
485
+ const zoom = d3.zoom()
486
+ .scaleExtent([0.1, 4])
487
+ .on('zoom', (event) => {
488
+ g.attr('transform', event.transform);
489
+ });
490
+
491
+ svg.call(zoom);
492
+
493
+ // Click on empty space to reset highlighting
494
+ svg.on('click', (event) => {
495
+ if (event.target === event.currentTarget) {
496
+ resetHighlighting();
497
+ }
498
+ });
499
+ }
500
+
501
+ async function loadGraphData(search = '') {
502
+ try {
503
+ showLoading();
504
+ const url = search
505
+ ? `${API_BASE}/graph?search=${encodeURIComponent(search)}`
506
+ : `${API_BASE}/graph`;
507
+
508
+ const response = await fetch(url);
509
+ if (!response.ok) throw new Error('Failed to fetch graph data');
510
+
511
+ graphData = await response.json();
512
+ renderGraph();
513
+ } catch (error) {
514
+ showError('Failed to load graph data: ' + error.message);
515
+ } finally {
516
+ hideLoading();
517
+ }
518
+ }
519
+
520
+ function renderGraph() {
521
+ if (!graphData.nodes || graphData.nodes.length === 0) {
522
+ showError('No data to display');
523
+ return;
524
+ }
525
+
526
+ // Clear existing elements
527
+ g.selectAll('*').remove();
528
+ resetHighlighting();
529
+
530
+ // Get container dimensions
531
+ const width = +svg.attr('width');
532
+ const height = +svg.attr('height');
533
+
534
+ // Create simulation
535
+ simulation = d3.forceSimulation(graphData.nodes)
536
+ .force('link', d3.forceLink(graphData.edges).id(d => d.id).distance(100))
537
+ .force('charge', d3.forceManyBody().strength(-300))
538
+ .force('center', d3.forceCenter(width / 2, height / 2))
539
+ .force('collision', d3.forceCollide().radius(30));
540
+
541
+ // Create links
542
+ const link = g.append('g')
543
+ .selectAll('line')
544
+ .data(graphData.edges)
545
+ .join('line')
546
+ .attr('class', 'link')
547
+ .on('mouseover', showEdgeTooltip)
548
+ .on('mouseout', hideTooltip);
549
+
550
+ // Create nodes
551
+ const node = g.append('g')
552
+ .selectAll('circle')
553
+ .data(graphData.nodes)
554
+ .join('circle')
555
+ .attr('class', 'node')
556
+ .attr('r', 12)
557
+ .attr('fill', d => getNodeColor(d))
558
+ .attr('stroke', '#fff')
559
+ .attr('stroke-width', 2)
560
+ .on('mouseover', showNodeTooltip)
561
+ .on('mouseout', hideTooltip)
562
+ .on('click', handleNodeClick)
563
+ .call(d3.drag()
564
+ .on('start', dragStarted)
565
+ .on('drag', dragged)
566
+ .on('end', dragEnded));
567
+
568
+ // Create labels
569
+ const labels = g.append('g')
570
+ .selectAll('text')
571
+ .data(graphData.nodes)
572
+ .join('text')
573
+ .attr('class', 'node-label')
574
+ .text(d => d.label.length > 12 ? d.label.substring(0, 12) + '...' : d.label);
575
+
576
+ // Update positions on simulation tick
577
+ simulation.on('tick', () => {
578
+ link
579
+ .attr('x1', d => d.source.x)
580
+ .attr('y1', d => d.source.y)
581
+ .attr('x2', d => d.target.x)
582
+ .attr('y2', d => d.target.y);
583
+
584
+ node
585
+ .attr('cx', d => d.x)
586
+ .attr('cy', d => d.y);
587
+
588
+ labels
589
+ .attr('x', d => d.x)
590
+ .attr('y', d => d.y + 20);
591
+ });
592
+ }
593
+
594
+ function getNodeColor(node) {
595
+ const colors = {
596
+ 'concept': '#4CAF50',
597
+ 'disease': '#f44336',
598
+ 'treatment': '#2196F3',
599
+ 'attribute': '#FF9800',
600
+ 'method': '#9C27B0',
601
+ 'default': '#607D8B'
602
+ };
603
+ return colors[node.type] || colors.default;
604
+ }
605
+
606
+ function showNodeTooltip(event, d) {
607
+ const tooltip = d3.select('#tooltip');
608
+ tooltip.transition().duration(200).style('opacity', 1);
609
+
610
+ const connectionCount = graphData.edges.filter(edge =>
611
+ edge.source.id === d.id || edge.target.id === d.id
612
+ ).length;
613
+
614
+ tooltip.html(`
615
+ <h4>${d.label}</h4>
616
+ <p><strong>Type:</strong> ${d.type || 'Node'}</p>
617
+ <p><strong>Connections:</strong> ${connectionCount}</p>
618
+ <p>Click to highlight connections</p>
619
+ `)
620
+ .style('left', (event.pageX + 10) + 'px')
621
+ .style('top', (event.pageY - 28) + 'px');
622
+ }
623
+
624
+ function showEdgeTooltip(event, d) {
625
+ const tooltip = d3.select('#tooltip');
626
+ tooltip.transition().duration(200).style('opacity', 1);
627
+ tooltip.html(`
628
+ <h4>${d.relation}</h4>
629
+ <p><strong>From:</strong> ${d.source.label || d.source.id}</p>
630
+ <p><strong>To:</strong> ${d.target.label || d.target.id}</p>
631
+ `)
632
+ .style('left', (event.pageX + 10) + 'px')
633
+ .style('top', (event.pageY - 28) + 'px');
634
+ }
635
+
636
+ function hideTooltip() {
637
+ d3.select('#tooltip').transition().duration(500).style('opacity', 0);
638
+ }
639
+
640
+ function handleNodeClick(event, d) {
641
+ event.stopPropagation();
642
+
643
+ if (selectedNode && selectedNode.id === d.id) {
644
+ resetHighlighting();
645
+ return;
646
+ }
647
+
648
+ selectedNode = d;
649
+ highlightConnections(d);
650
+ }
651
+
652
+ function highlightConnections(selectedNode) {
653
+ highlightedElements.nodes.clear();
654
+ highlightedElements.edges.clear();
655
+
656
+ graphData.edges.forEach(edge => {
657
+ if (edge.source.id === selectedNode.id || edge.target.id === selectedNode.id) {
658
+ highlightedElements.edges.add(edge);
659
+ highlightedElements.nodes.add(edge.source.id);
660
+ highlightedElements.nodes.add(edge.target.id);
661
+ }
662
+ });
663
+
664
+ applyHighlighting();
665
+ }
666
+
667
+ function applyHighlighting() {
668
+ g.selectAll('.node')
669
+ .classed('highlighted', d => highlightedElements.nodes.has(d.id) && (!selectedNode || d.id !== selectedNode.id))
670
+ .classed('selected', d => selectedNode && d.id === selectedNode.id)
671
+ .classed('dimmed', d => selectedNode && !highlightedElements.nodes.has(d.id));
672
+
673
+ g.selectAll('.link')
674
+ .classed('highlighted', d => highlightedElements.edges.has(d))
675
+ .classed('dimmed', d => selectedNode && !highlightedElements.edges.has(d));
676
+
677
+ g.selectAll('.node-label')
678
+ .classed('highlighted', d => highlightedElements.nodes.has(d.id))
679
+ .classed('dimmed', d => selectedNode && !highlightedElements.nodes.has(d.id));
680
+ }
681
+
682
+ function resetHighlighting() {
683
+ selectedNode = null;
684
+ highlightedElements.nodes.clear();
685
+ highlightedElements.edges.clear();
686
+
687
+ g.selectAll('.node')
688
+ .classed('highlighted', false)
689
+ .classed('selected', false)
690
+ .classed('dimmed', false);
691
+
692
+ g.selectAll('.link')
693
+ .classed('highlighted', false)
694
+ .classed('dimmed', false);
695
+
696
+ g.selectAll('.node-label')
697
+ .classed('highlighted', false)
698
+ .classed('dimmed', false);
699
+ }
700
+
701
+ async function handleSearch() {
702
+ const query = document.getElementById('searchInput').value.trim();
703
+ if (query === currentSearch) return;
704
+
705
+ currentSearch = query;
706
+ await loadGraphData(query);
707
+ }
708
+
709
+ function showError(message) {
710
+ const mainContent = document.querySelector('.main-content');
711
+ const errorDiv = document.createElement('div');
712
+ errorDiv.className = 'error';
713
+ errorDiv.textContent = message;
714
+ mainContent.appendChild(errorDiv);
715
+ setTimeout(() => errorDiv.remove(), 5000);
716
+ }
717
+
718
+ function showLoading() {
719
+ document.getElementById('loading').style.display = 'block';
720
+ }
721
+
722
+ function hideLoading() {
723
+ document.getElementById('loading').style.display = 'none';
724
+ }
725
+
726
+ function debounce(func, wait) {
727
+ let timeout;
728
+ return function executedFunction(...args) {
729
+ const later = () => {
730
+ clearTimeout(timeout);
731
+ func(...args);
732
+ };
733
+ clearTimeout(timeout);
734
+ timeout = setTimeout(later, wait);
735
+ };
736
+ }
737
+
738
+ // Drag functions
739
+ function dragStarted(event, d) {
740
+ if (!event.active) simulation.alphaTarget(0.3).restart();
741
+ d.fx = d.x;
742
+ d.fy = d.y;
743
+ }
744
+
745
+ function dragged(event, d) {
746
+ d.fx = event.x;
747
+ d.fy = event.y;
748
+ }
749
+
750
+ function dragEnded(event, d) {
751
+ if (!event.active) simulation.alphaTarget(0);
752
+ d.fx = null;
753
+ d.fy = null;
754
+ }
755
+
756
+ // Window resize handler
757
+ window.addEventListener('resize', () => {
758
+ const container = document.querySelector('.main-content');
759
+ const containerRect = container.getBoundingClientRect();
760
+
761
+ svg.attr('width', containerRect.width)
762
+ .attr('height', containerRect.height);
763
+
764
+ if (simulation) {
765
+ simulation.force('center', d3.forceCenter(containerRect.width / 2, containerRect.height / 2));
766
+ simulation.alpha(0.3).restart();
767
+ }
768
+ });
769
+
770
+ // Initialize when DOM is loaded
771
+ document.addEventListener('DOMContentLoaded', init);
772
+ </script>
773
+ </body>
774
+ </html>
index.html ADDED
@@ -0,0 +1,518 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Knowledge Graph</title>
7
+ <style>
8
+ /* Reset and base styles */
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
17
+ height: 100vh;
18
+ overflow: hidden;
19
+ position: relative;
20
+ background: linear-gradient(180deg, #F8F3E7 35%, #708686 90%);
21
+ }
22
+
23
+ /* Background Graph Network */
24
+ .background-network {
25
+ position: absolute;
26
+ top: 0;
27
+ left: 0;
28
+ width: 100%;
29
+ height: 100%;
30
+ z-index: 1;
31
+ pointer-events: none;
32
+ }
33
+
34
+ .network-lines {
35
+ position: absolute;
36
+ top: 0;
37
+ left: 0;
38
+ width: 100%;
39
+ height: 100%;
40
+ z-index: 1;
41
+ }
42
+
43
+ .network-nodes {
44
+ position: absolute;
45
+ top: 0;
46
+ left: 0;
47
+ width: 100%;
48
+ height: 100%;
49
+ z-index: 3;
50
+ }
51
+
52
+ .network-node {
53
+ position: absolute;
54
+ border-radius: 50%;
55
+ opacity: 1;
56
+ }
57
+
58
+ .network-line {
59
+ position: absolute;
60
+ background-color: #5f6b7a;
61
+ height: 1.5px;
62
+ opacity: 0.4;
63
+ transform-origin: left center;
64
+ }
65
+
66
+ /* Node colors and sizes - exact match to your design */
67
+ .node-teal { background-color: #A9C5C5; }
68
+ .node-purple-light { background-color: #DBCAE1; }
69
+ .node-blue-light { background-color: #B0C4DE; }
70
+
71
+ .node-small { width: 35px; height: 35px; }
72
+ .node-medium { width: 50px; height: 50px; }
73
+ .node-large { width: 85px; height: 85px; }
74
+
75
+ /* Login Button as Network Node */
76
+ .login-node {
77
+ position: absolute;
78
+ background-color: #64748b;
79
+ border-radius: 50%;
80
+ width: 80px;
81
+ height: 80px;
82
+ display: flex;
83
+ flex-direction: column;
84
+ align-items: center;
85
+ justify-content: center;
86
+ font-size: 12px;
87
+ font-weight: 500;
88
+ color: white;
89
+ cursor: pointer;
90
+ transition: all 0.3s ease;
91
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
92
+ z-index: 100;
93
+ pointer-events: auto;
94
+ opacity: 1;
95
+ }
96
+
97
+ .login-node:hover {
98
+ background-color: #475569;
99
+ transform: translateY(-2px);
100
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
101
+ }
102
+
103
+ .login-icon {
104
+ font-size: 20px;
105
+ margin-bottom: 2px;
106
+ }
107
+
108
+ /* Main Content */
109
+ .main-content {
110
+ position: relative;
111
+ z-index: 10;
112
+ height: 100vh;
113
+ display: flex;
114
+ flex-direction: column;
115
+ justify-content: center;
116
+ align-items: center;
117
+ text-align: center;
118
+ padding: 0 40px;
119
+ }
120
+
121
+ /* Title Section */
122
+ .title-section {
123
+ margin-bottom: 0px;
124
+ }
125
+
126
+ .main-title {
127
+ font-family: 'Kumbh Sans', sans-serif;
128
+ font-size: 4.5rem;
129
+ font-weight: 700;
130
+ color: #4D536D;
131
+ letter-spacing: -0.02em;
132
+ margin-top: 120px;
133
+ line-height: 1.1;
134
+ }
135
+
136
+ .subtitle {
137
+ font-family: 'Mandali', sans-serif;
138
+ font-size: 1.125rem;
139
+ color: #4D536D;
140
+ font-weight: 400;
141
+ line-height: 1.6;
142
+ max-width: 500px;
143
+ margin: 0 auto 10px;
144
+ }
145
+
146
+ .subtitle-second {
147
+ font-family: 'Mandali', sans-serif;
148
+ font-size: 1.125rem;
149
+ color: #4D536D;
150
+ font-weight: 400;
151
+ }
152
+
153
+ /* Search Section */
154
+ /* Ganti max-width kalo mau ubah lebar */
155
+ .search-container {
156
+ width: 100%;
157
+ max-width: 740px;
158
+ margin-top: 20px;
159
+ }
160
+
161
+ .search-input {
162
+ width: 100%;
163
+ padding: 16px 22px;
164
+ font-size: 1rem;
165
+ border: none;
166
+ border-radius: 25px;
167
+ background-color: rgb(243, 231, 221);
168
+ color: #64748b;
169
+ outline: none;
170
+ transition: all 0.3s ease;
171
+ box-shadow: 0 4px 4px rgba(0, 0, 0, 0.3);
172
+ margin-bottom: 15px;
173
+ }
174
+
175
+ .search-input::placeholder {
176
+ color: #797979;
177
+ font-weight: 400;
178
+ }
179
+
180
+ .search-input:focus {
181
+ background-color: rgba(255, 255, 255, 1);
182
+ box-shadow: 0 6px 30px rgba(0, 0, 0, 0.15);
183
+ transform: translateY(-2px);
184
+ }
185
+
186
+ .search-btn {
187
+ background-color: #4D536D;
188
+ color: white;
189
+ border: none;
190
+ border-radius: 20px;
191
+ padding: 12px 32px;
192
+ font-size: 1rem;
193
+ font-weight: 500;
194
+ cursor: pointer;
195
+ transition: all 0.3s ease;
196
+ box-shadow: 0 4px 4px rgba(0, 0, 0, 0.3);
197
+ }
198
+
199
+ .search-btn:hover {
200
+ background-color: #475569;
201
+ transform: translateY(-2px);
202
+ box-shadow: 0 6px 20px rgba(100, 116, 139, 0.4);
203
+ }
204
+
205
+ .search-btn:active {
206
+ transform: translateY(0);
207
+ }
208
+
209
+ /* Divider */
210
+ .divider-container {
211
+ display: flex;
212
+ align-items: center;
213
+ width: 100%;
214
+ max-width: 400px;
215
+ margin: 30px 0;
216
+ margin-top: 15px;
217
+ }
218
+
219
+ .divider-line {
220
+ flex: 1;
221
+ height: 1px;
222
+ background-color: rgb(255 255 255);
223
+ }
224
+
225
+ .divider-text {
226
+ margin: 0 20px;
227
+ color: #FFFFFF;
228
+ font-size: 1rem;
229
+ font-weight: 500;
230
+ }
231
+
232
+ /* Explore Button */
233
+ .explore-btn {
234
+ background-color: #4D536D;
235
+ color: white;
236
+ border: none;
237
+ border-radius: 25px;
238
+ padding: 16px 48px;
239
+ font-size: 1rem;
240
+ font-weight: 500;
241
+ cursor: pointer;
242
+ transition: all 0.3s ease;
243
+ box-shadow: 0 4px 4px rgba(0, 0, 0, 0.3);
244
+ width: 100%;
245
+ max-width: 520px;
246
+ }
247
+
248
+ .explore-btn:hover {
249
+ background-color: #475569;
250
+ transform: translateY(-3px);
251
+ box-shadow: 0 8px 30px rgba(100, 116, 139, 0.4);
252
+ }
253
+
254
+ .explore-btn:active {
255
+ transform: translateY(-1px);
256
+ }
257
+
258
+ /* Responsive Design */
259
+ @media (max-width: 768px) {
260
+ .main-title {
261
+ font-size: 3rem;
262
+ }
263
+
264
+ .subtitle, .subtitle-second {
265
+ font-size: 1rem;
266
+ }
267
+
268
+ .main-content {
269
+ padding: 0 20px;
270
+ }
271
+
272
+ .login-btn {
273
+ width: 60px;
274
+ height: 60px;
275
+ font-size: 10px;
276
+ }
277
+
278
+ .login-icon {
279
+ font-size: 16px;
280
+ }
281
+ }
282
+
283
+ @media (max-width: 480px) {
284
+ .main-title {
285
+ font-size: 2.5rem;
286
+ }
287
+
288
+ .login-container {
289
+ top: 20px;
290
+ right: 20px;
291
+ }
292
+ }
293
+
294
+ /* Animation for network elements */
295
+ @keyframes float {
296
+ 0%, 100% { transform: translateY(0px); }
297
+ 50% { transform: translateY(-10px); }
298
+ }
299
+
300
+ .network-node {
301
+ animation: float 6s ease-in-out infinite;
302
+ }
303
+
304
+ .network-node:nth-child(2n) {
305
+ animation-delay: -2s;
306
+ }
307
+
308
+ .network-node:nth-child(3n) {
309
+ animation-delay: -4s;
310
+ }
311
+ </style>
312
+ </head>
313
+ <body>
314
+ <!-- Background Network -->
315
+ <div class="background-network" id="backgroundNetwork">
316
+ <div class="network-lines" id="networkLines"></div>
317
+ <div class="network-nodes" id="networkNodes"></div>
318
+ </div>
319
+
320
+ <!-- Main Content -->
321
+ <div class="main-content">
322
+ <div class="title-section">
323
+ <h1 class="main-title">KNOWLEDGE GRAPH</h1>
324
+ <p class="subtitle">Explore health insights powered by national news</p>
325
+ <p class="subtitle-second">ask, discover, and learn.</p>
326
+ </div>
327
+
328
+ <div class="search-container">
329
+ <input
330
+ type="text"
331
+ class="search-input"
332
+ id="queryInput"
333
+ placeholder="Type Query"
334
+ onkeypress="handleKeyPress(event)"
335
+ >
336
+ <button class="search-btn" onclick="handleSearch()">Search</button>
337
+ </div>
338
+
339
+ <div class="divider-container">
340
+ <div class="divider-line"></div>
341
+ <span class="divider-text">OR</span>
342
+ <div class="divider-line"></div>
343
+ </div>
344
+
345
+ <button class="explore-btn" onclick="handleExplore()">Explore Knowledge Graph</button>
346
+ </div>
347
+
348
+ <script>
349
+ // Create background network - exact placement matching your image
350
+ function createBackgroundNetwork() {
351
+ const linesContainer = document.getElementById('networkLines');
352
+ const nodesContainer = document.getElementById('networkNodes');
353
+ const nodes = [];
354
+
355
+ // Exact node positions from your image
356
+ const nodeConfigs = [
357
+ // Top row (left to right, some truncated)
358
+ { x: -1, y: -3, size: 'large', color: 'blue-light' },
359
+ { x: 16, y: 3, size: 'medium', color: 'purple-light' },
360
+ { x: 32, y: -5, size: 'large', color: 'teal' },
361
+ { x: 55, y: 2, size: 'medium', color: 'blue-light' },
362
+ { x: 68, y: -4, size: 'small', color: 'purple-light' },
363
+ { x: 86, y: -1, size: 'small', color: 'purple-light' },
364
+ { x: 100, y: -1, size: 'small', color: 'blue-light' },
365
+
366
+ // Login node (top right as in image)
367
+ { x: 92, y: 2, size: 'login', color: 'login' },
368
+
369
+ // Second row
370
+ { x: 7, y: 15, size: 'small', color: 'teal' },
371
+ { x: 25, y: 12, size: 'small', color: 'blue-light' },
372
+ { x: 44, y: 12, size: 'medium', color: 'purple-light' },
373
+ { x: 58, y: 15, size: 'small', color: 'teal' },
374
+ { x: 72, y: 6, size: 'large', color: 'purple-light' },
375
+ { x: 82, y: 11, size: 'small', color: 'teal' },
376
+
377
+ // Third row (more scattered)
378
+ { x: 32, y: 18, size: 'medium', color: 'blue-light' },
379
+ { x: 100, y: 15, size: 'small', color: 'teal' },
380
+ ];
381
+
382
+ // Create nodes including login
383
+ nodeConfigs.forEach((config, index) => {
384
+ if (config.color === 'login') {
385
+ const loginNode = document.createElement('div');
386
+ loginNode.className = 'login-node';
387
+ loginNode.style.left = `${config.x}%`;
388
+ loginNode.style.top = `${config.y}%`;
389
+ loginNode.style.animation = 'float 6s ease-in-out infinite';
390
+ loginNode.style.animationDelay = '-2s';
391
+ loginNode.onclick = handleLogin;
392
+ loginNode.innerHTML = `
393
+ <div class="login-icon">👤</div>
394
+ <div>Login</div>
395
+ `;
396
+ nodesContainer.appendChild(loginNode);
397
+ } else {
398
+ const node = document.createElement('div');
399
+ node.className = `network-node node-${config.size} node-${config.color}`;
400
+ node.style.left = `${config.x}%`;
401
+ node.style.top = `${config.y}%`;
402
+ nodesContainer.appendChild(node);
403
+ }
404
+
405
+ nodes.push({
406
+ x: config.x,
407
+ y: config.y,
408
+ index: index,
409
+ isLogin: config.color === 'login'
410
+ });
411
+ });
412
+
413
+ // Connections matching your image exactly
414
+ const connections = [
415
+ // Top row main connections
416
+ [0, 1], [1, 2], [2, 3], [3, 4], [4, 5],
417
+
418
+ // Login connections (as shown in image)
419
+ [7, 5], [7, 13], [7, 6], [7, 15],
420
+
421
+ // Vertical connections from top to second row
422
+ [0, 8], [1, 9], [2, 9], [3, 10], [5, 12], [12, 13], [3, 12],
423
+
424
+ // Second row horizontal connections
425
+ [8, 9], [10, 11], [11, 12],
426
+
427
+ // Connections to third row
428
+ [9, 14], [10, 14],
429
+
430
+ // Some cross connections for the web-like structure
431
+ [2, 10], [4, 12]
432
+ ];
433
+
434
+ // Create connection lines
435
+ connections.forEach(([startIdx, endIdx]) => {
436
+ const startNode = nodes[startIdx];
437
+ const endNode = nodes[endIdx];
438
+
439
+ if (startNode && endNode) {
440
+ const line = document.createElement('div');
441
+ line.className = 'network-line';
442
+
443
+ const startSize = startNode.isLogin ? 40 : 20;
444
+ const endSize = endNode.isLogin ? 40 : 20;
445
+
446
+ const startX = (startNode.x * window.innerWidth) / 100 + startSize;
447
+ const startY = (startNode.y * window.innerHeight) / 100 + startSize;
448
+ const endX = (endNode.x * window.innerWidth) / 100 + endSize;
449
+ const endY = (endNode.y * window.innerHeight) / 100 + endSize;
450
+
451
+ const length = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2));
452
+ const angle = Math.atan2(endY - startY, endX - startX) * 180 / Math.PI;
453
+
454
+ line.style.left = `${startX}px`;
455
+ line.style.top = `${startY}px`;
456
+ line.style.width = `${length}px`;
457
+ line.style.transform = `rotate(${angle}deg)`;
458
+
459
+ linesContainer.appendChild(line);
460
+ }
461
+ });
462
+ }
463
+
464
+ // Event handlers
465
+ function handleLogin() {
466
+ alert('Login functionality will be implemented when you provide the login page design.');
467
+ console.log('Login clicked');
468
+ }
469
+
470
+ function handleSearch() {
471
+ const query = document.getElementById('queryInput').value.trim();
472
+ if (query) {
473
+ // Navigate to results page with query parameter
474
+ window.location.href = `search.html?q=${encodeURIComponent(query)}`;
475
+ } else {
476
+ alert('Please enter a query to search.');
477
+ }
478
+ }
479
+
480
+ function handleExplore() {
481
+ // alert('Explore Knowledge Graph functionality will be implemented when you provide the graph page design.');
482
+ console.log('Explore Knowledge Graph clicked');
483
+ window.location.href = 'explorepage.html'; // Change this line
484
+ }
485
+
486
+ function handleKeyPress(event) {
487
+ if (event.key === 'Enter') {
488
+ handleSearch();
489
+ }
490
+ }
491
+
492
+ // Initialize the background network when page loads
493
+ window.addEventListener('load', () => {
494
+ createBackgroundNetwork();
495
+ });
496
+
497
+ // Recreate network on window resize
498
+ window.addEventListener('resize', () => {
499
+ document.getElementById('networkLines').innerHTML = '';
500
+ document.getElementById('networkNodes').innerHTML = '';
501
+ createBackgroundNetwork();
502
+ });
503
+
504
+ // Add some interactivity to the input
505
+ document.addEventListener('DOMContentLoaded', () => {
506
+ const input = document.getElementById('queryInput');
507
+
508
+ input.addEventListener('focus', () => {
509
+ input.style.transform = 'translateY(-2px)';
510
+ });
511
+
512
+ input.addEventListener('blur', () => {
513
+ input.style.transform = 'translateY(0)';
514
+ });
515
+ });
516
+ </script>
517
+ </body>
518
+ </html>
news_index_compressed/index.faiss ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:985baf5c4bcef7970307185b6f0287b67d92a3f67a1fd9c2b32c87e9b20c1163
3
+ size 30973636
news_index_compressed/index.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9bf3c4ecaf35a6aefc26b045519fdc0408e2fd1ada5d3ea1c59b0d649e566669
3
+ size 14304568
relations_new.db ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:17deb71333b7cfde910d14b00609c2792b38520f3f5ee18e0b8065448dca2b17
3
+ size 1294336
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ langchain-community
2
+ faiss-cpu
3
+ pydantic
4
+ together
5
+ numpy
6
+ fastapi
7
+ uvicorn
search.html ADDED
@@ -0,0 +1,879 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link href="https://fonts.googleapis.com/css2?family=Varta:wght@400;500;600;700&display=swap" rel="stylesheet">
7
+ <title>Search Results - Knowledge Graph</title>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
9
+ <style>
10
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ body {
17
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
18
+ background: linear-gradient(180deg, #708686 0%, #F8F3E7 100%);
19
+ min-height: 100vh;
20
+ padding: 2rem;
21
+ }
22
+
23
+ .container {
24
+ max-width: 1450px;
25
+ margin: 0 auto;
26
+ }
27
+
28
+ /* Header Section */
29
+ .header {
30
+ display: flex;
31
+ align-items: center;
32
+ gap: 1rem;
33
+ margin-bottom: 1.5rem;
34
+ }
35
+
36
+ .home-btn {
37
+ position: absolute;
38
+ top: 1.5rem;
39
+ right: 1.5rem;
40
+ z-index: 200;
41
+ background: none;
42
+ border: none;
43
+ color: white;
44
+ font-size: 1.5rem;
45
+ cursor: pointer;
46
+ padding: 0.75rem;
47
+ border-radius: 8px;
48
+ transition: background-color 0.3s ease;
49
+ backdrop-filter: blur(10px);
50
+ }
51
+
52
+ .home-btn:hover {
53
+ background: white;
54
+ transform: translateY(-2px);
55
+ }
56
+
57
+ .search-container {
58
+ flex: 1;
59
+ display: flex;
60
+ gap: 1rem;
61
+ height: 50px;
62
+ margin-top: 4rem;
63
+ display: flex;
64
+ }
65
+
66
+ .search-input {
67
+ flex: 1;
68
+ padding: 16px 22px;
69
+ border: none;
70
+ border-radius: 25px;
71
+ background: rgb(248 243 231);
72
+ box-shadow: 0 4px 4px rgba(0, 0, 0, 0.3);
73
+ font-size: 1rem;
74
+ color: #797979;
75
+ }
76
+
77
+ .search-input::placeholder {
78
+ color: #797979;
79
+ font-weight: 400;
80
+ }
81
+
82
+ .search-input:focus {
83
+ outline: none;
84
+ background: white;
85
+ }
86
+
87
+ .search-btn {
88
+ background-color: #4D536D;
89
+ color: white;
90
+ border: none;
91
+ border-radius: 20px;
92
+ padding: 12px 32px;
93
+ font-size: 1rem;
94
+ font-weight: 500;
95
+ cursor: pointer;
96
+ transition: all 0.3s ease;
97
+ box-shadow: 0 4px 4px rgba(0, 0, 0, 0.3);
98
+ }
99
+
100
+ .search-btn:hover {
101
+ background: #2d3748;
102
+ transform: translateY(-2px);
103
+ }
104
+
105
+ /* Main Content Grid */
106
+ .content-grid {
107
+ display: grid;
108
+ grid-template-columns: 1fr 1fr;
109
+ gap: 2rem;
110
+ margin-bottom: 2rem;
111
+ }
112
+
113
+ /* Answer Section */
114
+ .answer-section {
115
+ background: rgb(248 243 231);
116
+ border-radius: 15px;
117
+ padding: 2rem;
118
+ color: #4a5568;
119
+ }
120
+
121
+ .answer-title {
122
+ font-size: 1.5rem;
123
+ font-family: 'Varta', sans-serif;
124
+ font-weight: 700;
125
+ margin-bottom: 1rem;
126
+ color: #000000;
127
+ }
128
+
129
+ .answer-content {
130
+ max-height: 300px;
131
+ overflow-y: auto;
132
+ line-height: 1.6;
133
+ font-family: 'Varta', sans-serif;
134
+ font-size: 1rem;
135
+ color: #000000;
136
+ white-space: pre-wrap;
137
+ }
138
+
139
+ .answer-content::-webkit-scrollbar {
140
+ width: 6px;
141
+ }
142
+
143
+ .answer-content::-webkit-scrollbar-track {
144
+ background: rgba(0, 0, 0, 0.1);
145
+ border-radius: 3px;
146
+ }
147
+
148
+ .answer-content::-webkit-scrollbar-thumb {
149
+ background: rgba(0, 0, 0, 0.3);
150
+ border-radius: 3px;
151
+ }
152
+
153
+ .answer-content::-webkit-scrollbar-thumb:hover {
154
+ background: rgba(0, 0, 0, 0.5);
155
+ }
156
+
157
+ /* Knowledge Graph Section */
158
+ .graph-section {
159
+ background: #4a5568;
160
+ border-radius: 15px;
161
+ padding: 2rem;
162
+ color: white;
163
+ }
164
+
165
+ .graph-title {
166
+ font-size: 1.5rem;
167
+ font-family: 'Varta', sans-serif;
168
+ font-weight: 700;
169
+ margin-bottom: 1rem;
170
+ }
171
+
172
+ .mini-graph-container {
173
+ width: 100%;
174
+ height: 300px;
175
+ background: rgba(0, 0, 0, 0.1);
176
+ border-radius: 10px;
177
+ position: relative;
178
+ overflow: hidden;
179
+ }
180
+
181
+ #miniGraph {
182
+ width: 100%;
183
+ height: 100%;
184
+ cursor: grab;
185
+ }
186
+
187
+ #miniGraph:active {
188
+ cursor: grabbing;
189
+ }
190
+
191
+ /* News Section */
192
+ .news-section {
193
+ background: rgba(77, 83, 109, 0.5);
194
+ border-radius: 15px;
195
+ padding: 2rem;
196
+ color: white;
197
+ grid-column: 1 / -1;
198
+ }
199
+
200
+ .news-title {
201
+ font-family: 'Varta', sans-serif;
202
+ font-size: 1.5rem;
203
+ font-weight: 700;
204
+ margin-bottom: 1.5rem;
205
+ }
206
+
207
+ .news-grid {
208
+ display: grid;
209
+ gap: 1rem;
210
+ margin-bottom: 2rem;
211
+ }
212
+
213
+ .news-item {
214
+ background: rgb(77, 83, 109);
215
+ border-radius: 10px;
216
+ padding: 1.5rem;
217
+ transition: all 0.3s ease;
218
+ }
219
+
220
+ .news-item:hover {
221
+ background: rgba(45, 55, 72, 1);
222
+ transform: translateY(-2px);
223
+ }
224
+
225
+ .news-item-title {
226
+ font-family: 'Varta', sans-serif;
227
+ font-size: 1.1rem;
228
+ font-weight: 600;
229
+ margin-bottom: 0.75rem;
230
+ color: #F8F3E7;
231
+ }
232
+
233
+ .news-item-preview {
234
+ font-family: 'Varta', sans-serif;
235
+ font-size: 0.95rem;
236
+ line-height: 1.5;
237
+ color: #F8F3E7;
238
+ margin-bottom: 0.7rem;
239
+ }
240
+
241
+ .read-full-btn {
242
+ background: rgba(255, 255, 255, 0.1);
243
+ color: #A9C5C5;
244
+ border: 1px solid rgba(255, 255, 255, 0.2);
245
+ padding: 0.5rem 1rem;
246
+ border-radius: 6px;
247
+ font-size: 0.875rem;
248
+ cursor: pointer;
249
+ transition: all 0.3s ease;
250
+ text-decoration: none;
251
+ display: inline-block;
252
+ }
253
+
254
+ .read-full-btn:hover {
255
+ background: rgba(255, 255, 255, 0.2);
256
+ color: white;
257
+ }
258
+
259
+ /* Pagination */
260
+ .pagination {
261
+ display: flex;
262
+ justify-content: center;
263
+ gap: 1rem;
264
+ align-items: center;
265
+ }
266
+
267
+ .page-btn {
268
+ background: rgba(255, 255, 255, 0.2);
269
+ color: white;
270
+ border: 1px solid rgba(255, 255, 255, 0.3);
271
+ padding: 0.75rem 1.5rem;
272
+ border-radius: 8px;
273
+ cursor: pointer;
274
+ transition: all 0.3s ease;
275
+ font-family: 'Varta', sans-serif;
276
+ font-size: 0.9rem;
277
+ font-weight: 500;
278
+ }
279
+
280
+ .page-btn:hover:not(:disabled) {
281
+ background: rgba(255, 255, 255, 0.3);
282
+ }
283
+
284
+ .page-btn:disabled {
285
+ opacity: 0.5;
286
+ cursor: not-allowed;
287
+ }
288
+
289
+ .page-info {
290
+ color: rgba(255, 255, 255, 0.8);
291
+ font-family: 'Varta', sans-serif;
292
+ font-size: 0.9rem;
293
+ }
294
+
295
+ /* Graph Tooltip */
296
+ .tooltip {
297
+ position: absolute;
298
+ text-align: left;
299
+ padding: 0.75rem;
300
+ font-size: 0.875rem;
301
+ background: rgba(0, 0, 0, 0.9);
302
+ color: white;
303
+ border-radius: 6px;
304
+ pointer-events: none;
305
+ opacity: 0;
306
+ transition: opacity 0.3s;
307
+ max-width: 250px;
308
+ line-height: 1.4;
309
+ z-index: 1000;
310
+ }
311
+
312
+ .tooltip h4 {
313
+ margin: 0 0 0.5rem 0;
314
+ color: #4CAF50;
315
+ font-size: 0.875rem;
316
+ }
317
+
318
+ /* Loading States */
319
+ .loading {
320
+ display: flex;
321
+ align-items: center;
322
+ justify-content: center;
323
+ height: 200px;
324
+ color: #666;
325
+ }
326
+
327
+ .loading-spinner {
328
+ border: 2px solid rgba(0, 0, 0, 0.1);
329
+ border-radius: 50%;
330
+ border-top: 2px solid #4a5568;
331
+ width: 20px;
332
+ height: 20px;
333
+ animation: spin 1s linear infinite;
334
+ margin-right: 0.5rem;
335
+ }
336
+
337
+ @keyframes spin {
338
+ 0% { transform: rotate(0deg); }
339
+ 100% { transform: rotate(360deg); }
340
+ }
341
+
342
+ /* Node and Link Styles */
343
+ .node {
344
+ cursor: pointer;
345
+ transition: all 0.3s ease;
346
+ }
347
+
348
+ .node:hover {
349
+ stroke-width: 3px;
350
+ }
351
+
352
+ .node.highlighted {
353
+ stroke: #4CAF50 !important;
354
+ stroke-width: 3px !important;
355
+ }
356
+
357
+ .node.selected {
358
+ stroke: #FFD700 !important;
359
+ stroke-width: 4px !important;
360
+ }
361
+
362
+ .node.dimmed {
363
+ opacity: 0.3;
364
+ }
365
+
366
+ .link {
367
+ stroke: rgba(255, 255, 255, 0.6);
368
+ stroke-width: 1.5px;
369
+ cursor: pointer;
370
+ transition: all 0.3s ease;
371
+ }
372
+
373
+ .link:hover {
374
+ stroke: #4CAF50;
375
+ stroke-width: 2px;
376
+ }
377
+
378
+ .link.highlighted {
379
+ stroke: #4CAF50 !important;
380
+ stroke-width: 2px !important;
381
+ }
382
+
383
+ .link.dimmed {
384
+ opacity: 0.1;
385
+ }
386
+
387
+ .node-label {
388
+ font-size: 10px;
389
+ font-weight: 600;
390
+ fill: white;
391
+ text-anchor: middle;
392
+ pointer-events: none;
393
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
394
+ transition: all 0.3s ease;
395
+ }
396
+
397
+ .node-label.dimmed {
398
+ opacity: 0.3;
399
+ }
400
+
401
+ .node-label.highlighted {
402
+ fill: #4CAF50;
403
+ font-size: 11px;
404
+ }
405
+
406
+ /* Responsive Design */
407
+ @media (max-width: 768px) {
408
+ .content-grid {
409
+ grid-template-columns: 1fr;
410
+ }
411
+
412
+ .header {
413
+ flex-direction: column;
414
+ gap: 1rem;
415
+ }
416
+
417
+ .search-container {
418
+ width: 100%;
419
+ }
420
+ }
421
+ </style>
422
+ </head>
423
+ <body>
424
+ <div class="container">
425
+ <!-- Header with Search -->
426
+ <div class="header">
427
+ <button class="home-btn" id="homeBtn" title="Go to Home">
428
+ <img src="Home.png" alt="Home" style="width: 20px; height: 20px;">
429
+ </button>
430
+ <div class="search-container">
431
+ <input
432
+ type="text"
433
+ class="search-input"
434
+ id="searchInput"
435
+ placeholder="Type Query"
436
+ >
437
+ <button class="search-btn" id="searchBtn">Search</button>
438
+ </div>
439
+ </div>
440
+
441
+ <!-- Main Content Grid -->
442
+ <div class="content-grid">
443
+ <!-- Answer Section -->
444
+ <div class="answer-section">
445
+ <h2 class="answer-title">Answer for: <span id="queryDisplay">[Query]</span></h2>
446
+ <div class="answer-content" id="answerContent">
447
+ <div class="loading">
448
+ <div class="loading-spinner"></div>
449
+ Loading answer...
450
+ </div>
451
+ </div>
452
+ </div>
453
+
454
+ <!-- Knowledge Graph Section -->
455
+ <div class="graph-section">
456
+ <h2 class="graph-title">Knowledge Graph</h2>
457
+ <div class="mini-graph-container">
458
+ <svg id="miniGraph"></svg>
459
+ </div>
460
+ </div>
461
+ </div>
462
+
463
+ <!-- News Section -->
464
+ <div class="news-section">
465
+ <h2 class="news-title">Recommended News</h2>
466
+ <div class="news-grid" id="newsGrid">
467
+ <div class="loading">
468
+ <div class="loading-spinner"></div>
469
+ Loading news...
470
+ </div>
471
+ </div>
472
+
473
+ <!-- Pagination -->
474
+ <div class="pagination">
475
+ <button class="page-btn" id="prevBtn" disabled>← Back</button>
476
+ <span class="page-info" id="pageInfo">Page 1</span>
477
+ <button class="page-btn" id="nextBtn">Next →</button>
478
+ </div>
479
+ </div>
480
+ </div>
481
+
482
+ <!-- Tooltip -->
483
+ <div class="tooltip" id="tooltip"></div>
484
+
485
+ <script>
486
+ // Configuration
487
+ const API_BASE = '/api';
488
+
489
+ // Global variables
490
+ let currentQuery = '';
491
+ let graphData = { nodes: [], edges: [] };
492
+ let newsData = [];
493
+ let currentPage = 1;
494
+ let newsPerPage = 3;
495
+ let simulation;
496
+ let svg, g;
497
+ let selectedNode = null;
498
+ let highlightedElements = { nodes: new Set(), edges: new Set() };
499
+
500
+ // Initialize the application
501
+ async function init() {
502
+ setupEventListeners();
503
+ setupMiniGraph();
504
+
505
+ // Get query from URL params or storage
506
+ const urlParams = new URLSearchParams(window.location.search);
507
+ const query = urlParams.get('q') || sessionStorage.getItem('currentQuery') || '';
508
+
509
+ if (query) {
510
+ document.getElementById('searchInput').value = query;
511
+ document.getElementById('queryDisplay').textContent = query;
512
+ currentQuery = query;
513
+ await handleSearch(false); // Don't update URL again
514
+ }
515
+ }
516
+
517
+ function setupEventListeners() {
518
+ // Navigation
519
+ document.getElementById('homeBtn').addEventListener('click', goHome);
520
+
521
+ // Search
522
+ document.getElementById('searchBtn').addEventListener('click', () => handleSearch(true));
523
+ document.getElementById('searchInput').addEventListener('keypress', (e) => {
524
+ if (e.key === 'Enter') handleSearch(true);
525
+ });
526
+
527
+ // Pagination
528
+ document.getElementById('prevBtn').addEventListener('click', () => changePage(-1));
529
+ document.getElementById('nextBtn').addEventListener('click', () => changePage(1));
530
+ }
531
+
532
+ function goHome() {
533
+ window.location.href = 'index.html';
534
+ }
535
+
536
+ function setupMiniGraph() {
537
+ const container = document.getElementById('miniGraph');
538
+ const containerRect = container.getBoundingClientRect();
539
+
540
+ svg = d3.select('#miniGraph')
541
+ .attr('width', containerRect.width)
542
+ .attr('height', containerRect.height);
543
+
544
+ g = svg.append('g');
545
+
546
+ // Add zoom behavior
547
+ const zoom = d3.zoom()
548
+ .scaleExtent([0.5, 3])
549
+ .on('zoom', (event) => {
550
+ g.attr('transform', event.transform);
551
+ });
552
+
553
+ svg.call(zoom);
554
+
555
+ // Reset on click
556
+ svg.on('click', (event) => {
557
+ if (event.target === event.currentTarget) {
558
+ resetHighlighting();
559
+ }
560
+ });
561
+ }
562
+
563
+ async function handleSearch(updateUrl = true) {
564
+ const query = document.getElementById('searchInput').value.trim();
565
+ if (!query) return;
566
+
567
+ currentQuery = query;
568
+ document.getElementById('queryDisplay').textContent = query;
569
+
570
+ // Reset all content areas to loading state immediately
571
+ document.getElementById('answerContent').innerHTML = `
572
+ <div class="loading">
573
+ <div class="loading-spinner"></div>
574
+ Loading answer...
575
+ </div>
576
+ `;
577
+
578
+ document.getElementById('newsGrid').innerHTML = `
579
+ <div class="loading">
580
+ <div class="loading-spinner"></div>
581
+ Loading news...
582
+ </div>
583
+ `;
584
+
585
+ // Clear existing graph
586
+ if (g) {
587
+ g.selectAll('*').remove();
588
+ resetHighlighting();
589
+ }
590
+
591
+ // Reset pagination
592
+ document.getElementById('pageInfo').textContent = 'Loading...';
593
+ document.getElementById('prevBtn').disabled = true;
594
+ document.getElementById('nextBtn').disabled = true;
595
+
596
+ if (updateUrl) {
597
+ const url = new URL(window.location);
598
+ url.searchParams.set('q', query);
599
+ window.history.pushState({}, '', url);
600
+ sessionStorage.setItem('currentQuery', query);
601
+ }
602
+
603
+ // Call API to get new results
604
+ await loadAnswer(query);
605
+ }
606
+
607
+ async function loadAnswer(query) {
608
+ try {
609
+ const response = await fetch(`${API_BASE}/query`, {
610
+ method: 'POST',
611
+ headers: {
612
+ 'Content-Type': 'application/json',
613
+ },
614
+ body: JSON.stringify({ query: query })
615
+ });
616
+
617
+ if (!response.ok) throw new Error('Failed to get answer');
618
+
619
+ const data = await response.json();
620
+
621
+ // Display the answer
622
+ document.getElementById('answerContent').textContent = data.answer || 'No answer available.';
623
+
624
+ // Extract and render graph data from the same response
625
+ if (data.graph_data) {
626
+ graphData = data.graph_data;
627
+ renderMiniGraph();
628
+ }
629
+
630
+ // Also handle news data here if it's in the same response
631
+ if (data.news_items) {
632
+ newsData = data.news_items;
633
+ currentPage = 1;
634
+ renderNews();
635
+ }
636
+
637
+ } catch (error) {
638
+ console.error('Answer loading error:', error);
639
+ document.getElementById('answerContent').textContent = 'Failed to load answer. Please try again.';
640
+ }
641
+ }
642
+
643
+ function renderMiniGraph() {
644
+ if (!graphData.nodes || graphData.nodes.length === 0) return;
645
+
646
+ // Clear existing
647
+ g.selectAll('*').remove();
648
+ resetHighlighting();
649
+
650
+ const width = +svg.attr('width');
651
+ const height = +svg.attr('height');
652
+
653
+ // Create simulation
654
+ simulation = d3.forceSimulation(graphData.nodes)
655
+ .force('link', d3.forceLink(graphData.edges).id(d => d.id).distance(60))
656
+ .force('charge', d3.forceManyBody().strength(-200))
657
+ .force('center', d3.forceCenter(width / 2, height / 2))
658
+ .force('collision', d3.forceCollide().radius(15));
659
+
660
+ // Create links
661
+ const link = g.append('g')
662
+ .selectAll('line')
663
+ .data(graphData.edges)
664
+ .join('line')
665
+ .attr('class', 'link')
666
+ .on('mouseover', showEdgeTooltip)
667
+ .on('mouseout', hideTooltip);
668
+
669
+ // Create nodes
670
+ const node = g.append('g')
671
+ .selectAll('circle')
672
+ .data(graphData.nodes)
673
+ .join('circle')
674
+ .attr('class', 'node')
675
+ .attr('r', 8)
676
+ .attr('fill', d => getNodeColor(d))
677
+ .attr('stroke', '#fff')
678
+ .attr('stroke-width', 2)
679
+ .on('mouseover', showNodeTooltip)
680
+ .on('mouseout', hideTooltip)
681
+ .on('click', handleNodeClick)
682
+ .call(d3.drag()
683
+ .on('start', dragStarted)
684
+ .on('drag', dragged)
685
+ .on('end', dragEnded));
686
+
687
+ // Create labels
688
+ const labels = g.append('g')
689
+ .selectAll('text')
690
+ .data(graphData.nodes)
691
+ .join('text')
692
+ .attr('class', 'node-label')
693
+ .text(d => d.label.length > 10 ? d.label.substring(0, 10) + '...' : d.label);
694
+
695
+ // Update positions
696
+ simulation.on('tick', () => {
697
+ link
698
+ .attr('x1', d => d.source.x)
699
+ .attr('y1', d => d.source.y)
700
+ .attr('x2', d => d.target.x)
701
+ .attr('y2', d => d.target.y);
702
+
703
+ node
704
+ .attr('cx', d => d.x)
705
+ .attr('cy', d => d.y);
706
+
707
+ labels
708
+ .attr('x', d => d.x)
709
+ .attr('y', d => d.y + 15);
710
+ });
711
+ }
712
+
713
+ function renderNews() {
714
+ const container = document.getElementById('newsGrid');
715
+
716
+ if (!newsData.length) {
717
+ container.innerHTML = '<div style="text-align: center; color: rgba(255,255,255,0.7);">No news articles found.</div>';
718
+ updatePagination();
719
+ return;
720
+ }
721
+
722
+ const startIdx = (currentPage - 1) * newsPerPage;
723
+ const endIdx = startIdx + newsPerPage;
724
+ const pageNews = newsData.slice(startIdx, endIdx);
725
+
726
+ container.innerHTML = pageNews.map(item => `
727
+ <div class="news-item">
728
+ <h3 class="news-item-title">${item.title}</h3>
729
+ <p class="news-item-preview">${item.preview}</p>
730
+ <a href="${item.url}" target="_blank" rel="noopener noreferrer" class="read-full-btn">
731
+ Read Full Article
732
+ </a>
733
+ </div>
734
+ `).join('');
735
+
736
+ updatePagination();
737
+ }
738
+
739
+ function updatePagination() {
740
+ const totalPages = Math.ceil(newsData.length / newsPerPage);
741
+
742
+ document.getElementById('pageInfo').textContent =
743
+ newsData.length ? `Page ${currentPage} of ${totalPages}` : 'No news';
744
+
745
+ document.getElementById('prevBtn').disabled = currentPage <= 1;
746
+ document.getElementById('nextBtn').disabled = currentPage >= totalPages;
747
+ }
748
+
749
+ function changePage(direction) {
750
+ const totalPages = Math.ceil(newsData.length / newsPerPage);
751
+ const newPage = currentPage + direction;
752
+
753
+ if (newPage >= 1 && newPage <= totalPages) {
754
+ currentPage = newPage;
755
+ renderNews();
756
+ }
757
+ }
758
+
759
+ function getNodeColor(node) {
760
+ const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3'];
761
+ const hash = node.id.split('').reduce((a, b) => {
762
+ a = ((a << 5) - a) + b.charCodeAt(0);
763
+ return a & a;
764
+ }, 0);
765
+ return colors[Math.abs(hash) % colors.length];
766
+ }
767
+
768
+ function showNodeTooltip(event, d) {
769
+ const tooltip = d3.select('#tooltip');
770
+ tooltip.transition().duration(200).style('opacity', 1);
771
+
772
+ tooltip.html(`
773
+ <h4>${d.label}</h4>
774
+ <p>Click to highlight connections</p>
775
+ `)
776
+ .style('left', (event.pageX + 10) + 'px')
777
+ .style('top', (event.pageY - 28) + 'px');
778
+ }
779
+
780
+ function showEdgeTooltip(event, d) {
781
+ const tooltip = d3.select('#tooltip');
782
+ tooltip.transition().duration(200).style('opacity', 1);
783
+ tooltip.html(`
784
+ <h4>${d.relation}</h4>
785
+ <p><strong>From:</strong> ${d.source.label}</p>
786
+ <p><strong>To:</strong> ${d.target.label}</p>
787
+ `)
788
+ .style('left', (event.pageX + 10) + 'px')
789
+ .style('top', (event.pageY - 28) + 'px');
790
+ }
791
+
792
+ function hideTooltip() {
793
+ d3.select('#tooltip').transition().duration(300).style('opacity', 0);
794
+ }
795
+
796
+ function handleNodeClick(event, d) {
797
+ event.stopPropagation();
798
+
799
+ if (selectedNode && selectedNode.id === d.id) {
800
+ resetHighlighting();
801
+ return;
802
+ }
803
+
804
+ selectedNode = d;
805
+ highlightConnections(d);
806
+ }
807
+
808
+ function highlightConnections(selectedNode) {
809
+ highlightedElements.nodes.clear();
810
+ highlightedElements.edges.clear();
811
+
812
+ graphData.edges.forEach(edge => {
813
+ if (edge.source.id === selectedNode.id || edge.target.id === selectedNode.id) {
814
+ highlightedElements.edges.add(edge);
815
+ highlightedElements.nodes.add(edge.source.id);
816
+ highlightedElements.nodes.add(edge.target.id);
817
+ }
818
+ });
819
+
820
+ applyHighlighting();
821
+ }
822
+
823
+ function applyHighlighting() {
824
+ g.selectAll('.node')
825
+ .classed('highlighted', d => highlightedElements.nodes.has(d.id) && (!selectedNode || d.id !== selectedNode.id))
826
+ .classed('selected', d => selectedNode && d.id === selectedNode.id)
827
+ .classed('dimmed', d => selectedNode && !highlightedElements.nodes.has(d.id));
828
+
829
+ g.selectAll('.link')
830
+ .classed('highlighted', d => highlightedElements.edges.has(d))
831
+ .classed('dimmed', d => selectedNode && !highlightedElements.edges.has(d));
832
+
833
+ g.selectAll('.node-label')
834
+ .classed('highlighted', d => highlightedElements.nodes.has(d.id))
835
+ .classed('dimmed', d => selectedNode && !highlightedElements.nodes.has(d.id));
836
+ }
837
+
838
+ function resetHighlighting() {
839
+ selectedNode = null;
840
+ highlightedElements.nodes.clear();
841
+ highlightedElements.edges.clear();
842
+
843
+ g.selectAll('.node')
844
+ .classed('highlighted', false)
845
+ .classed('selected', false)
846
+ .classed('dimmed', false);
847
+
848
+ g.selectAll('.link')
849
+ .classed('highlighted', false)
850
+ .classed('dimmed', false);
851
+
852
+ g.selectAll('.node-label')
853
+ .classed('highlighted', false)
854
+ .classed('dimmed', false);
855
+ }
856
+
857
+ // Drag functions
858
+ function dragStarted(event, d) {
859
+ if (!event.active) simulation.alphaTarget(0.3).restart();
860
+ d.fx = d.x;
861
+ d.fy = d.y;
862
+ }
863
+
864
+ function dragged(event, d) {
865
+ d.fx = event.x;
866
+ d.fy = event.y;
867
+ }
868
+
869
+ function dragEnded(event, d) {
870
+ if (!event.active) simulation.alphaTarget(0);
871
+ d.fx = null;
872
+ d.fy = null;
873
+ }
874
+
875
+ // Initialize when DOM loads
876
+ document.addEventListener('DOMContentLoaded', init);
877
+ </script>
878
+ </body>
879
+ </html>
triplets_index_compressed/index.faiss ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7453b3baa788f50f4e634802e04ddd4b433ea5507409fd162ae0bb68ae9c54b8
3
+ size 8861308
triplets_index_compressed/index.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e007729981f1979ad7901e581d207af59624eec3db5e11ced5b07b3019d5072c
3
+ size 1915352
triplets_new.db ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2db59a3c6accfca486def4ea32a1af344e04dd75769d4bb93ef7b6e609d1f3a0
3
+ size 3751936