universal-agent-context 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. uacs/__init__.py +12 -0
  2. uacs/adapters/__init__.py +19 -0
  3. uacs/adapters/agent_skill_adapter.py +202 -0
  4. uacs/adapters/agents_md_adapter.py +330 -0
  5. uacs/adapters/base.py +261 -0
  6. uacs/adapters/clinerules_adapter.py +39 -0
  7. uacs/adapters/cursorrules_adapter.py +39 -0
  8. uacs/api.py +262 -0
  9. uacs/cli/__init__.py +6 -0
  10. uacs/cli/context.py +349 -0
  11. uacs/cli/main.py +195 -0
  12. uacs/cli/mcp.py +115 -0
  13. uacs/cli/memory.py +142 -0
  14. uacs/cli/packages.py +309 -0
  15. uacs/cli/skills.py +144 -0
  16. uacs/cli/utils.py +24 -0
  17. uacs/config/repositories.yaml +26 -0
  18. uacs/context/__init__.py +0 -0
  19. uacs/context/agent_context.py +406 -0
  20. uacs/context/shared_context.py +661 -0
  21. uacs/context/unified_context.py +332 -0
  22. uacs/mcp_server_entry.py +80 -0
  23. uacs/memory/__init__.py +5 -0
  24. uacs/memory/simple_memory.py +255 -0
  25. uacs/packages/__init__.py +26 -0
  26. uacs/packages/manager.py +413 -0
  27. uacs/packages/models.py +60 -0
  28. uacs/packages/sources.py +270 -0
  29. uacs/protocols/__init__.py +5 -0
  30. uacs/protocols/mcp/__init__.py +8 -0
  31. uacs/protocols/mcp/manager.py +77 -0
  32. uacs/protocols/mcp/skills_server.py +700 -0
  33. uacs/skills_validator.py +367 -0
  34. uacs/utils/__init__.py +5 -0
  35. uacs/utils/paths.py +24 -0
  36. uacs/visualization/README.md +132 -0
  37. uacs/visualization/__init__.py +36 -0
  38. uacs/visualization/models.py +195 -0
  39. uacs/visualization/static/index.html +857 -0
  40. uacs/visualization/storage.py +402 -0
  41. uacs/visualization/visualization.py +328 -0
  42. uacs/visualization/web_server.py +364 -0
  43. universal_agent_context-0.2.0.dist-info/METADATA +873 -0
  44. universal_agent_context-0.2.0.dist-info/RECORD +47 -0
  45. universal_agent_context-0.2.0.dist-info/WHEEL +4 -0
  46. universal_agent_context-0.2.0.dist-info/entry_points.txt +2 -0
  47. universal_agent_context-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,857 @@
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>UACS Context Visualizer</title>
7
+ <script src="https://d3js.org/d3.v7.min.js"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.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, Oxygen, Ubuntu, Cantarell, sans-serif;
18
+ background: #0f0f23;
19
+ color: #cccccc;
20
+ overflow: hidden;
21
+ }
22
+
23
+ .header {
24
+ background: #1a1a2e;
25
+ padding: 20px;
26
+ border-bottom: 2px solid #16213e;
27
+ display: flex;
28
+ justify-content: space-between;
29
+ align-items: center;
30
+ }
31
+
32
+ .header h1 {
33
+ color: #00d4ff;
34
+ font-size: 24px;
35
+ font-weight: 600;
36
+ }
37
+
38
+ .header .status {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: 10px;
42
+ }
43
+
44
+ .status-indicator {
45
+ width: 10px;
46
+ height: 10px;
47
+ border-radius: 50%;
48
+ background: #ff4444;
49
+ animation: pulse 2s infinite;
50
+ }
51
+
52
+ .status-indicator.connected {
53
+ background: #44ff44;
54
+ }
55
+
56
+ @keyframes pulse {
57
+ 0%, 100% { opacity: 1; }
58
+ 50% { opacity: 0.5; }
59
+ }
60
+
61
+ .nav {
62
+ background: #16213e;
63
+ padding: 15px 20px;
64
+ display: flex;
65
+ gap: 15px;
66
+ border-bottom: 1px solid #0f0f23;
67
+ }
68
+
69
+ .nav-button {
70
+ background: transparent;
71
+ border: 2px solid #00d4ff;
72
+ color: #00d4ff;
73
+ padding: 10px 20px;
74
+ border-radius: 5px;
75
+ cursor: pointer;
76
+ font-size: 14px;
77
+ font-weight: 500;
78
+ transition: all 0.3s;
79
+ }
80
+
81
+ .nav-button:hover {
82
+ background: #00d4ff;
83
+ color: #0f0f23;
84
+ }
85
+
86
+ .nav-button.active {
87
+ background: #00d4ff;
88
+ color: #0f0f23;
89
+ }
90
+
91
+ .container {
92
+ display: flex;
93
+ height: calc(100vh - 130px);
94
+ }
95
+
96
+ .main-view {
97
+ flex: 1;
98
+ padding: 20px;
99
+ overflow: auto;
100
+ }
101
+
102
+ .sidebar {
103
+ width: 350px;
104
+ background: #1a1a2e;
105
+ padding: 20px;
106
+ border-left: 2px solid #16213e;
107
+ overflow: auto;
108
+ }
109
+
110
+ .view {
111
+ display: none;
112
+ }
113
+
114
+ .view.active {
115
+ display: block;
116
+ }
117
+
118
+ .card {
119
+ background: #1a1a2e;
120
+ border: 1px solid #16213e;
121
+ border-radius: 8px;
122
+ padding: 20px;
123
+ margin-bottom: 20px;
124
+ }
125
+
126
+ .card h3 {
127
+ color: #00d4ff;
128
+ margin-bottom: 15px;
129
+ font-size: 18px;
130
+ }
131
+
132
+ .stat-grid {
133
+ display: grid;
134
+ grid-template-columns: repeat(2, 1fr);
135
+ gap: 15px;
136
+ }
137
+
138
+ .stat-item {
139
+ background: #0f0f23;
140
+ padding: 15px;
141
+ border-radius: 5px;
142
+ border-left: 3px solid #00d4ff;
143
+ }
144
+
145
+ .stat-label {
146
+ color: #888888;
147
+ font-size: 12px;
148
+ margin-bottom: 5px;
149
+ }
150
+
151
+ .stat-value {
152
+ color: #00d4ff;
153
+ font-size: 24px;
154
+ font-weight: 600;
155
+ }
156
+
157
+ #graph-container {
158
+ width: 100%;
159
+ height: 600px;
160
+ background: #1a1a2e;
161
+ border-radius: 8px;
162
+ border: 1px solid #16213e;
163
+ }
164
+
165
+ .node {
166
+ cursor: pointer;
167
+ }
168
+
169
+ .node circle {
170
+ fill: #00d4ff;
171
+ stroke: #16213e;
172
+ stroke-width: 2px;
173
+ }
174
+
175
+ .node.summary circle {
176
+ fill: #ff6b35;
177
+ }
178
+
179
+ .node text {
180
+ fill: #cccccc;
181
+ font-size: 12px;
182
+ pointer-events: none;
183
+ }
184
+
185
+ .link {
186
+ stroke: #16213e;
187
+ stroke-opacity: 0.6;
188
+ stroke-width: 2px;
189
+ }
190
+
191
+ .chart-container {
192
+ position: relative;
193
+ height: 300px;
194
+ margin-top: 20px;
195
+ }
196
+
197
+ .topic-cluster {
198
+ background: #0f0f23;
199
+ padding: 10px 15px;
200
+ border-radius: 5px;
201
+ margin-bottom: 10px;
202
+ border-left: 3px solid #00d4ff;
203
+ display: flex;
204
+ justify-content: space-between;
205
+ align-items: center;
206
+ }
207
+
208
+ .topic-name {
209
+ color: #cccccc;
210
+ font-size: 14px;
211
+ }
212
+
213
+ .topic-count {
214
+ color: #00d4ff;
215
+ font-weight: 600;
216
+ }
217
+
218
+ .heatmap-cell {
219
+ fill: #1a1a2e;
220
+ stroke: #0f0f23;
221
+ stroke-width: 2;
222
+ }
223
+
224
+ .heatmap-cell:hover {
225
+ stroke: #00d4ff;
226
+ stroke-width: 3;
227
+ }
228
+
229
+ .empty-state {
230
+ text-align: center;
231
+ padding: 60px 20px;
232
+ color: #666666;
233
+ }
234
+
235
+ .empty-state svg {
236
+ width: 100px;
237
+ height: 100px;
238
+ margin-bottom: 20px;
239
+ opacity: 0.3;
240
+ }
241
+
242
+ .loading {
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: center;
246
+ height: 100%;
247
+ font-size: 18px;
248
+ color: #666666;
249
+ }
250
+
251
+ .metric-bar {
252
+ height: 40px;
253
+ background: #0f0f23;
254
+ border-radius: 5px;
255
+ overflow: hidden;
256
+ margin-bottom: 15px;
257
+ }
258
+
259
+ .metric-bar-fill {
260
+ height: 100%;
261
+ background: linear-gradient(90deg, #00d4ff, #0088ff);
262
+ display: flex;
263
+ align-items: center;
264
+ padding: 0 15px;
265
+ color: white;
266
+ font-weight: 600;
267
+ font-size: 14px;
268
+ transition: width 0.5s ease;
269
+ }
270
+ </style>
271
+ </head>
272
+ <body>
273
+ <div class="header">
274
+ <h1>UACS Context Visualizer</h1>
275
+ <div class="status">
276
+ <div class="status-indicator" id="statusIndicator"></div>
277
+ <span id="statusText">Connecting...</span>
278
+ </div>
279
+ </div>
280
+
281
+ <div class="nav">
282
+ <button class="nav-button active" data-view="conversation">Conversation Flow</button>
283
+ <button class="nav-button" data-view="dashboard">Token Dashboard</button>
284
+ <button class="nav-button" data-view="deduplication">Deduplication</button>
285
+ <button class="nav-button" data-view="quality">Quality Distribution</button>
286
+ <button class="nav-button" data-view="topics">Topic Clusters</button>
287
+ </div>
288
+
289
+ <div class="container">
290
+ <div class="main-view">
291
+ <!-- Conversation Flow View -->
292
+ <div class="view active" id="conversation-view">
293
+ <div id="graph-container"></div>
294
+ </div>
295
+
296
+ <!-- Token Dashboard View -->
297
+ <div class="view" id="dashboard-view">
298
+ <div class="card">
299
+ <h3>Token Usage Overview</h3>
300
+ <div class="chart-container">
301
+ <canvas id="tokenChart"></canvas>
302
+ </div>
303
+ </div>
304
+ <div class="card">
305
+ <h3>Compression Efficiency</h3>
306
+ <div class="chart-container">
307
+ <canvas id="compressionChart"></canvas>
308
+ </div>
309
+ </div>
310
+ </div>
311
+
312
+ <!-- Deduplication View -->
313
+ <div class="view" id="deduplication-view">
314
+ <div class="card">
315
+ <h3>Deduplication Statistics</h3>
316
+ <div id="dedupStats"></div>
317
+ </div>
318
+ <div class="card">
319
+ <h3>Duplicate Content Heatmap</h3>
320
+ <div id="dedupHeatmap"></div>
321
+ </div>
322
+ </div>
323
+
324
+ <!-- Quality Distribution View -->
325
+ <div class="view" id="quality-view">
326
+ <div class="card">
327
+ <h3>Content Quality Distribution</h3>
328
+ <div class="chart-container">
329
+ <canvas id="qualityChart"></canvas>
330
+ </div>
331
+ </div>
332
+ <div class="card">
333
+ <h3>Quality Metrics</h3>
334
+ <div id="qualityMetrics"></div>
335
+ </div>
336
+ </div>
337
+
338
+ <!-- Topic Clusters View -->
339
+ <div class="view" id="topics-view">
340
+ <div class="card">
341
+ <h3>Topic Network</h3>
342
+ <div id="topicGraph" style="height: 500px;"></div>
343
+ </div>
344
+ </div>
345
+ </div>
346
+
347
+ <div class="sidebar">
348
+ <div class="card">
349
+ <h3>Statistics</h3>
350
+ <div class="stat-grid" id="statsGrid">
351
+ <div class="loading">Loading...</div>
352
+ </div>
353
+ </div>
354
+ <div class="card">
355
+ <h3>Recent Topics</h3>
356
+ <div id="topicsList">
357
+ <div class="loading">Loading...</div>
358
+ </div>
359
+ </div>
360
+ </div>
361
+ </div>
362
+
363
+ <script>
364
+ // Global state
365
+ let ws = null;
366
+ let currentData = {};
367
+ let charts = {};
368
+
369
+ // Initialize
370
+ document.addEventListener('DOMContentLoaded', () => {
371
+ setupNavigation();
372
+ connectWebSocket();
373
+ initializeCharts();
374
+ });
375
+
376
+ // Navigation
377
+ function setupNavigation() {
378
+ document.querySelectorAll('.nav-button').forEach(button => {
379
+ button.addEventListener('click', () => {
380
+ const view = button.dataset.view;
381
+ switchView(view);
382
+ });
383
+ });
384
+ }
385
+
386
+ function switchView(viewName) {
387
+ // Update buttons
388
+ document.querySelectorAll('.nav-button').forEach(btn => {
389
+ btn.classList.toggle('active', btn.dataset.view === viewName);
390
+ });
391
+
392
+ // Update views
393
+ document.querySelectorAll('.view').forEach(view => {
394
+ view.classList.toggle('active', view.id === `${viewName}-view`);
395
+ });
396
+
397
+ // Refresh view with current data
398
+ if (currentData.graph) {
399
+ updateView(viewName, currentData);
400
+ }
401
+ }
402
+
403
+ // WebSocket connection
404
+ function connectWebSocket() {
405
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
406
+ const wsUrl = `${protocol}//${window.location.host}/ws`;
407
+
408
+ ws = new WebSocket(wsUrl);
409
+
410
+ ws.onopen = () => {
411
+ updateStatus(true);
412
+ };
413
+
414
+ ws.onmessage = (event) => {
415
+ const data = JSON.parse(event.data);
416
+ currentData = data;
417
+ updateAllViews(data);
418
+ };
419
+
420
+ ws.onerror = (error) => {
421
+ console.error('WebSocket error:', error);
422
+ updateStatus(false);
423
+ };
424
+
425
+ ws.onclose = () => {
426
+ updateStatus(false);
427
+ setTimeout(connectWebSocket, 5000);
428
+ };
429
+ }
430
+
431
+ function updateStatus(connected) {
432
+ const indicator = document.getElementById('statusIndicator');
433
+ const text = document.getElementById('statusText');
434
+
435
+ if (connected) {
436
+ indicator.classList.add('connected');
437
+ text.textContent = 'Connected';
438
+ } else {
439
+ indicator.classList.remove('connected');
440
+ text.textContent = 'Disconnected';
441
+ }
442
+ }
443
+
444
+ // Update all views
445
+ function updateAllViews(data) {
446
+ updateStatsGrid(data.stats);
447
+ updateTopicsList(data.topics);
448
+
449
+ // Update active view
450
+ const activeView = document.querySelector('.nav-button.active').dataset.view;
451
+ updateView(activeView, data);
452
+ }
453
+
454
+ function updateView(viewName, data) {
455
+ switch(viewName) {
456
+ case 'conversation':
457
+ updateConversationFlow(data.graph);
458
+ break;
459
+ case 'dashboard':
460
+ updateDashboard(data.stats);
461
+ break;
462
+ case 'deduplication':
463
+ updateDeduplication(data.graph);
464
+ break;
465
+ case 'quality':
466
+ updateQuality(data.quality);
467
+ break;
468
+ case 'topics':
469
+ updateTopics(data.topics);
470
+ break;
471
+ }
472
+ }
473
+
474
+ // Stats grid
475
+ function updateStatsGrid(stats) {
476
+ const grid = document.getElementById('statsGrid');
477
+ grid.innerHTML = `
478
+ <div class="stat-item">
479
+ <div class="stat-label">Entries</div>
480
+ <div class="stat-value">${stats.entry_count || 0}</div>
481
+ </div>
482
+ <div class="stat-item">
483
+ <div class="stat-label">Summaries</div>
484
+ <div class="stat-value">${stats.summary_count || 0}</div>
485
+ </div>
486
+ <div class="stat-item">
487
+ <div class="stat-label">Total Tokens</div>
488
+ <div class="stat-value">${(stats.total_tokens || 0).toLocaleString()}</div>
489
+ </div>
490
+ <div class="stat-item">
491
+ <div class="stat-label">Tokens Saved</div>
492
+ <div class="stat-value">${(stats.tokens_saved || 0).toLocaleString()}</div>
493
+ </div>
494
+ <div class="stat-item">
495
+ <div class="stat-label">Compression</div>
496
+ <div class="stat-value">${stats.compression_ratio || '0%'}</div>
497
+ </div>
498
+ <div class="stat-item">
499
+ <div class="stat-label">Avg Quality</div>
500
+ <div class="stat-value">${stats.avg_quality || '0.00'}</div>
501
+ </div>
502
+ `;
503
+ }
504
+
505
+ // Topics list
506
+ function updateTopicsList(topics) {
507
+ const list = document.getElementById('topicsList');
508
+ if (!topics || !topics.clusters || topics.clusters.length === 0) {
509
+ list.innerHTML = '<div style="color: #666; text-align: center;">No topics yet</div>';
510
+ return;
511
+ }
512
+
513
+ list.innerHTML = topics.clusters.slice(0, 5).map(cluster => `
514
+ <div class="topic-cluster">
515
+ <span class="topic-name">${cluster.topic}</span>
516
+ <span class="topic-count">${cluster.count}</span>
517
+ </div>
518
+ `).join('');
519
+ }
520
+
521
+ // Conversation Flow
522
+ function updateConversationFlow(graph) {
523
+ const container = document.getElementById('graph-container');
524
+ container.innerHTML = '';
525
+
526
+ if (!graph.nodes || graph.nodes.length === 0) {
527
+ container.innerHTML = '<div class="empty-state"><p>No context entries yet</p></div>';
528
+ return;
529
+ }
530
+
531
+ const width = container.clientWidth;
532
+ const height = container.clientHeight;
533
+
534
+ const svg = d3.select('#graph-container')
535
+ .append('svg')
536
+ .attr('width', width)
537
+ .attr('height', height);
538
+
539
+ const simulation = d3.forceSimulation(graph.nodes)
540
+ .force('link', d3.forceLink(graph.edges).id(d => d.id).distance(100))
541
+ .force('charge', d3.forceManyBody().strength(-300))
542
+ .force('center', d3.forceCenter(width / 2, height / 2));
543
+
544
+ const link = svg.append('g')
545
+ .selectAll('line')
546
+ .data(graph.edges)
547
+ .enter()
548
+ .append('line')
549
+ .attr('class', 'link');
550
+
551
+ const node = svg.append('g')
552
+ .selectAll('g')
553
+ .data(graph.nodes)
554
+ .enter()
555
+ .append('g')
556
+ .attr('class', d => `node ${d.type}`)
557
+ .call(d3.drag()
558
+ .on('start', dragstarted)
559
+ .on('drag', dragged)
560
+ .on('end', dragended));
561
+
562
+ node.append('circle')
563
+ .attr('r', d => d.type === 'summary' ? 15 : 10);
564
+
565
+ node.append('text')
566
+ .attr('dx', 15)
567
+ .attr('dy', 5)
568
+ .text(d => d.id.substring(0, 8));
569
+
570
+ simulation.on('tick', () => {
571
+ link
572
+ .attr('x1', d => d.source.x)
573
+ .attr('y1', d => d.source.y)
574
+ .attr('x2', d => d.target.x)
575
+ .attr('y2', d => d.target.y);
576
+
577
+ node.attr('transform', d => `translate(${d.x},${d.y})`);
578
+ });
579
+
580
+ function dragstarted(event) {
581
+ if (!event.active) simulation.alphaTarget(0.3).restart();
582
+ event.subject.fx = event.subject.x;
583
+ event.subject.fy = event.subject.y;
584
+ }
585
+
586
+ function dragged(event) {
587
+ event.subject.fx = event.x;
588
+ event.subject.fy = event.y;
589
+ }
590
+
591
+ function dragended(event) {
592
+ if (!event.active) simulation.alphaTarget(0);
593
+ event.subject.fx = null;
594
+ event.subject.fy = null;
595
+ }
596
+ }
597
+
598
+ // Initialize Charts
599
+ function initializeCharts() {
600
+ // Token Chart
601
+ const tokenCtx = document.getElementById('tokenChart');
602
+ charts.token = new Chart(tokenCtx, {
603
+ type: 'doughnut',
604
+ data: {
605
+ labels: ['Used', 'Saved'],
606
+ datasets: [{
607
+ data: [0, 0],
608
+ backgroundColor: ['#00d4ff', '#ff6b35']
609
+ }]
610
+ },
611
+ options: {
612
+ responsive: true,
613
+ maintainAspectRatio: false,
614
+ plugins: {
615
+ legend: {
616
+ labels: { color: '#cccccc' }
617
+ }
618
+ }
619
+ }
620
+ });
621
+
622
+ // Compression Chart
623
+ const compressionCtx = document.getElementById('compressionChart');
624
+ charts.compression = new Chart(compressionCtx, {
625
+ type: 'bar',
626
+ data: {
627
+ labels: ['Entries', 'Summaries'],
628
+ datasets: [{
629
+ label: 'Count',
630
+ data: [0, 0],
631
+ backgroundColor: '#00d4ff'
632
+ }]
633
+ },
634
+ options: {
635
+ responsive: true,
636
+ maintainAspectRatio: false,
637
+ plugins: {
638
+ legend: {
639
+ labels: { color: '#cccccc' }
640
+ }
641
+ },
642
+ scales: {
643
+ y: {
644
+ ticks: { color: '#cccccc' },
645
+ grid: { color: '#16213e' }
646
+ },
647
+ x: {
648
+ ticks: { color: '#cccccc' },
649
+ grid: { color: '#16213e' }
650
+ }
651
+ }
652
+ }
653
+ });
654
+
655
+ // Quality Chart
656
+ const qualityCtx = document.getElementById('qualityChart');
657
+ charts.quality = new Chart(qualityCtx, {
658
+ type: 'bar',
659
+ data: {
660
+ labels: ['High', 'Medium', 'Low'],
661
+ datasets: [{
662
+ label: 'Entries',
663
+ data: [0, 0, 0],
664
+ backgroundColor: ['#44ff44', '#ffaa00', '#ff4444']
665
+ }]
666
+ },
667
+ options: {
668
+ responsive: true,
669
+ maintainAspectRatio: false,
670
+ plugins: {
671
+ legend: {
672
+ labels: { color: '#cccccc' }
673
+ }
674
+ },
675
+ scales: {
676
+ y: {
677
+ ticks: { color: '#cccccc' },
678
+ grid: { color: '#16213e' }
679
+ },
680
+ x: {
681
+ ticks: { color: '#cccccc' },
682
+ grid: { color: '#16213e' }
683
+ }
684
+ }
685
+ }
686
+ });
687
+ }
688
+
689
+ // Update Dashboard
690
+ function updateDashboard(stats) {
691
+ if (charts.token) {
692
+ charts.token.data.datasets[0].data = [
693
+ stats.total_tokens || 0,
694
+ stats.tokens_saved || 0
695
+ ];
696
+ charts.token.update();
697
+ }
698
+
699
+ if (charts.compression) {
700
+ charts.compression.data.datasets[0].data = [
701
+ stats.entry_count || 0,
702
+ stats.summary_count || 0
703
+ ];
704
+ charts.compression.update();
705
+ }
706
+ }
707
+
708
+ // Update Deduplication
709
+ function updateDeduplication(graph) {
710
+ const statsDiv = document.getElementById('dedupStats');
711
+ const uniqueHashes = new Set();
712
+
713
+ if (graph.nodes) {
714
+ graph.nodes.forEach(node => {
715
+ if (node.type === 'entry') {
716
+ uniqueHashes.add(node.id);
717
+ }
718
+ });
719
+ }
720
+
721
+ const duplicatesPrevented = Math.max(0, (graph.nodes?.length || 0) - uniqueHashes.size);
722
+ const dedupRate = graph.nodes?.length > 0
723
+ ? ((duplicatesPrevented / graph.nodes.length) * 100).toFixed(1)
724
+ : 0;
725
+
726
+ statsDiv.innerHTML = `
727
+ <div class="stat-grid">
728
+ <div class="stat-item">
729
+ <div class="stat-label">Unique Entries</div>
730
+ <div class="stat-value">${uniqueHashes.size}</div>
731
+ </div>
732
+ <div class="stat-item">
733
+ <div class="stat-label">Duplicates Prevented</div>
734
+ <div class="stat-value">${duplicatesPrevented}</div>
735
+ </div>
736
+ </div>
737
+ <div class="metric-bar">
738
+ <div class="metric-bar-fill" style="width: ${100 - dedupRate}%">
739
+ ${100 - dedupRate}% Unique
740
+ </div>
741
+ </div>
742
+ `;
743
+ }
744
+
745
+ // Update Quality
746
+ function updateQuality(quality) {
747
+ if (!quality) return;
748
+
749
+ if (charts.quality) {
750
+ charts.quality.data.datasets[0].data = [
751
+ quality.high_quality || 0,
752
+ quality.medium_quality || 0,
753
+ quality.low_quality || 0
754
+ ];
755
+ charts.quality.update();
756
+ }
757
+
758
+ const metricsDiv = document.getElementById('qualityMetrics');
759
+ const avgQuality = quality.average || 0;
760
+ const percentage = (avgQuality * 100).toFixed(1);
761
+
762
+ metricsDiv.innerHTML = `
763
+ <div class="metric-bar">
764
+ <div class="metric-bar-fill" style="width: ${percentage}%">
765
+ Average Quality: ${avgQuality.toFixed(2)}
766
+ </div>
767
+ </div>
768
+ <div class="stat-grid" style="margin-top: 20px;">
769
+ <div class="stat-item">
770
+ <div class="stat-label">High Quality</div>
771
+ <div class="stat-value">${quality.high_quality || 0}</div>
772
+ </div>
773
+ <div class="stat-item">
774
+ <div class="stat-label">Medium Quality</div>
775
+ <div class="stat-value">${quality.medium_quality || 0}</div>
776
+ </div>
777
+ <div class="stat-item">
778
+ <div class="stat-label">Low Quality</div>
779
+ <div class="stat-value">${quality.low_quality || 0}</div>
780
+ </div>
781
+ </div>
782
+ `;
783
+ }
784
+
785
+ // Update Topics
786
+ function updateTopics(topics) {
787
+ const container = document.getElementById('topicGraph');
788
+ container.innerHTML = '';
789
+
790
+ if (!topics || !topics.clusters || topics.clusters.length === 0) {
791
+ container.innerHTML = '<div class="empty-state"><p>No topics yet</p></div>';
792
+ return;
793
+ }
794
+
795
+ const width = container.clientWidth;
796
+ const height = container.clientHeight;
797
+
798
+ const nodes = topics.clusters.map(c => ({
799
+ id: c.topic,
800
+ count: c.count
801
+ }));
802
+
803
+ const svg = d3.select('#topicGraph')
804
+ .append('svg')
805
+ .attr('width', width)
806
+ .attr('height', height);
807
+
808
+ const simulation = d3.forceSimulation(nodes)
809
+ .force('charge', d3.forceManyBody().strength(-100))
810
+ .force('center', d3.forceCenter(width / 2, height / 2))
811
+ .force('collision', d3.forceCollide().radius(d => Math.sqrt(d.count) * 10 + 5));
812
+
813
+ const node = svg.append('g')
814
+ .selectAll('g')
815
+ .data(nodes)
816
+ .enter()
817
+ .append('g')
818
+ .attr('class', 'node');
819
+
820
+ node.append('circle')
821
+ .attr('r', d => Math.sqrt(d.count) * 10 + 5)
822
+ .attr('fill', '#00d4ff');
823
+
824
+ node.append('text')
825
+ .attr('text-anchor', 'middle')
826
+ .attr('dy', 5)
827
+ .text(d => d.id)
828
+ .style('fill', '#0f0f23')
829
+ .style('font-weight', '600');
830
+
831
+ simulation.on('tick', () => {
832
+ node.attr('transform', d => `translate(${d.x},${d.y})`);
833
+ });
834
+ }
835
+
836
+ // Fetch initial data
837
+ async function fetchInitialData() {
838
+ try {
839
+ const [graph, stats, topics, quality] = await Promise.all([
840
+ fetch('/api/graph').then(r => r.json()),
841
+ fetch('/api/stats').then(r => r.json()),
842
+ fetch('/api/topics').then(r => r.json()),
843
+ fetch('/api/quality').then(r => r.json())
844
+ ]);
845
+
846
+ currentData = { graph, stats, topics, quality };
847
+ updateAllViews(currentData);
848
+ } catch (error) {
849
+ console.error('Error fetching initial data:', error);
850
+ }
851
+ }
852
+
853
+ // Fetch initial data on load
854
+ fetchInitialData();
855
+ </script>
856
+ </body>
857
+ </html>