EchaRz commited on
Commit
8edd78e
·
verified ·
1 Parent(s): cac5404

Change API_BASE

Browse files
Files changed (1) hide show
  1. search.html +878 -878
search.html CHANGED
@@ -1,879 +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="/static/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 = 'http://0.0.0.0:7860/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 = 'http://0.0.0.0:7860/';
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>
 
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="/static/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 = '/';
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>