nexo-brain 2.3.2 → 2.5.0

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 (83) hide show
  1. package/README.md +77 -8
  2. package/bin/nexo-brain.js +230 -22
  3. package/bin/nexo.js +55 -0
  4. package/community/skills/.gitkeep +1 -0
  5. package/package.json +5 -2
  6. package/src/auto_update.py +158 -8
  7. package/src/cli.py +605 -0
  8. package/src/cognitive/_ingest.py +1 -1
  9. package/src/cognitive/_memory.py +4 -4
  10. package/src/crons/manifest.json +8 -0
  11. package/src/dashboard/app.py +709 -37
  12. package/src/dashboard/templates/adaptive.html +112 -218
  13. package/src/dashboard/templates/artifacts.html +133 -0
  14. package/src/dashboard/templates/backups.html +136 -0
  15. package/src/dashboard/templates/base.html +413 -0
  16. package/src/dashboard/templates/calendar.html +523 -652
  17. package/src/dashboard/templates/chat.html +356 -0
  18. package/src/dashboard/templates/claims.html +259 -0
  19. package/src/dashboard/templates/cortex.html +262 -0
  20. package/src/dashboard/templates/credentials.html +128 -0
  21. package/src/dashboard/templates/crons.html +370 -0
  22. package/src/dashboard/templates/dashboard.html +384 -572
  23. package/src/dashboard/templates/dreams.html +252 -0
  24. package/src/dashboard/templates/email.html +160 -0
  25. package/src/dashboard/templates/evolution.html +189 -0
  26. package/src/dashboard/templates/feed.html +249 -0
  27. package/src/dashboard/templates/followup_health.html +170 -0
  28. package/src/dashboard/templates/graph.html +191 -269
  29. package/src/dashboard/templates/guard.html +259 -0
  30. package/src/dashboard/templates/inbox.html +220 -336
  31. package/src/dashboard/templates/memory.html +317 -197
  32. package/src/dashboard/templates/operations.html +498 -652
  33. package/src/dashboard/templates/plugins.html +185 -0
  34. package/src/dashboard/templates/rules.html +246 -0
  35. package/src/dashboard/templates/sentiment.html +247 -0
  36. package/src/dashboard/templates/sessions.html +215 -171
  37. package/src/dashboard/templates/skills.html +329 -0
  38. package/src/dashboard/templates/somatic.html +68 -172
  39. package/src/dashboard/templates/triggers.html +133 -0
  40. package/src/dashboard/templates/trust.html +360 -0
  41. package/src/db/__init__.py +5 -0
  42. package/src/db/_schema.py +25 -1
  43. package/src/db/_sessions.py +22 -0
  44. package/src/db/_skills.py +983 -252
  45. package/src/doctor/__init__.py +1 -0
  46. package/src/doctor/formatters.py +52 -0
  47. package/src/doctor/models.py +44 -0
  48. package/src/doctor/orchestrator.py +42 -0
  49. package/src/doctor/providers/__init__.py +1 -0
  50. package/src/doctor/providers/boot.py +206 -0
  51. package/src/doctor/providers/deep.py +292 -0
  52. package/src/doctor/providers/runtime.py +686 -0
  53. package/src/hooks/capture-tool-logs.sh +18 -4
  54. package/src/hooks/post-compact.sh +5 -1
  55. package/src/hooks/pre-compact.sh +1 -1
  56. package/src/plugin_loader.py +14 -0
  57. package/src/plugins/doctor.py +36 -0
  58. package/src/plugins/evolution.py +2 -1
  59. package/src/plugins/skills.py +135 -175
  60. package/src/requirements.txt +1 -0
  61. package/src/script_registry.py +322 -0
  62. package/src/scripts/deep-sleep/apply_findings.py +63 -33
  63. package/src/scripts/deep-sleep/collect.py +38 -9
  64. package/src/scripts/deep-sleep/extract-prompt.md +14 -0
  65. package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
  66. package/src/scripts/deep-sleep/synthesize.py +37 -1
  67. package/src/scripts/nexo-dashboard.sh +29 -0
  68. package/src/scripts/nexo-day-orchestrator.sh +139 -0
  69. package/src/scripts/nexo-evolution-run.py +2 -1
  70. package/src/scripts/nexo-learning-housekeep.py +1 -1
  71. package/src/scripts/nexo-watchdog.sh +1 -1
  72. package/src/server.py +9 -5
  73. package/src/skills/run-runtime-doctor/guide.md +12 -0
  74. package/src/skills/run-runtime-doctor/script.py +21 -0
  75. package/src/skills/run-runtime-doctor/skill.json +25 -0
  76. package/src/skills_runtime.py +347 -0
  77. package/src/tools_menu.py +3 -2
  78. package/src/tools_sessions.py +126 -0
  79. package/src/user_context.py +46 -0
  80. package/templates/nexo_helper.py +45 -0
  81. package/templates/script-template.py +44 -0
  82. package/templates/skill-script-template.py +39 -0
  83. package/templates/skill-template.md +33 -0
@@ -1,279 +1,201 @@
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>NEXO Brain Knowledge Graph</title>
7
- <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
8
- <script src="https://cdn.tailwindcss.com"></script>
9
- <script>tailwind.config={theme:{extend:{fontFamily:{display:['Space Grotesk','system-ui','sans-serif'],mono:['JetBrains Mono','monospace']}}}}</script>
10
- <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
11
- <style>
12
- html, body { height: 100%; overflow: hidden; }
13
- .node circle { transition: filter 0.15s ease; }
14
- .node:hover circle { filter: drop-shadow(0 0 10px currentColor); }
15
- .link-label { fill: #64748b; font-size: 10px; pointer-events: none; opacity: 0.6; }
16
- </style>
17
- </head>
18
- <body class="bg-gray-950 text-slate-200 h-full">
19
-
20
- <!-- Sidebar: 56px icon-only fixed left -->
21
- <aside class="fixed left-0 top-0 bottom-0 w-14 bg-slate-900 border-r border-slate-800 flex flex-col items-center py-4 z-50">
22
- <!-- Logo -->
23
- <a href="/" class="mb-6 flex items-center justify-center w-8 h-8">
24
- <img src="/static/nexo-logo.png" alt="NEXO" class="w-7 h-7" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
25
- <div style="display:none" class="w-7 h-7 rounded-lg bg-violet-600 items-center justify-center text-white text-xs font-bold font-display">N</div>
26
- </a>
27
-
28
- <!-- Nav icons -->
29
- <nav class="flex flex-col items-center gap-1 flex-1">
30
- <!-- Dashboard -->
31
- <a href="/" title="Dashboard" class="w-9 h-9 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-200 hover:bg-slate-800 transition-colors">
32
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
33
- </a>
34
- <!-- Operations -->
35
- <a href="/ops" title="Operations" class="w-9 h-9 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-200 hover:bg-slate-800 transition-colors">
36
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
37
- </a>
38
- <!-- Calendar -->
39
- <a href="/calendar" title="Calendar" class="w-9 h-9 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-200 hover:bg-slate-800 transition-colors">
40
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
41
- </a>
42
- <!-- Inbox -->
43
- <a href="/inbox" title="Inbox" class="w-9 h-9 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-200 hover:bg-slate-800 transition-colors">
44
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>
45
- </a>
46
-
47
- <div class="w-6 h-px bg-slate-800 my-1"></div>
48
-
49
- <!-- Memory -->
50
- <a href="/memory" title="Memory" class="w-9 h-9 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-200 hover:bg-slate-800 transition-colors">
51
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a7 7 0 0 1 7 7c0 2.38-1.19 4.47-3 5.74V17a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 0 1 7-7z"/><path d="M9 21h6"/><path d="M10 21v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-1"/></svg>
52
- </a>
53
- <!-- Graph — ACTIVE -->
54
- <a href="/graph" title="Knowledge Graph" class="w-9 h-9 flex items-center justify-center rounded-lg bg-violet-600/20 text-violet-400 transition-colors">
55
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
56
- </a>
57
- <!-- Sessions -->
58
- <a href="/sessions" title="Sessions" class="w-9 h-9 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-200 hover:bg-slate-800 transition-colors">
59
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
60
- </a>
61
- <!-- Somatic -->
62
- <a href="/somatic" title="Somatic" class="w-9 h-9 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-200 hover:bg-slate-800 transition-colors">
63
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
64
- </a>
65
- <!-- Adaptive -->
66
- <a href="/adaptive" title="Adaptive" class="w-9 h-9 flex items-center justify-center rounded-lg text-slate-400 hover:text-slate-200 hover:bg-slate-800 transition-colors">
67
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>
68
- </a>
69
- </nav>
70
-
71
- <!-- Trust score -->
72
- <div class="flex flex-col items-center gap-0.5 mt-2">
73
- <span class="text-xs font-display font-semibold text-slate-500 uppercase tracking-widest">Trust</span>
74
- <span id="trust-score" class="text-sm font-mono font-bold text-violet-400">--</span>
75
- </div>
76
- </aside>
77
-
78
- <!-- Main content: full height flex -->
79
- <main class="ml-14 min-h-screen bg-gray-950 text-slate-200 flex flex-col" style="height:100vh">
80
- <header class="h-12 border-b border-slate-800/50 flex items-center gap-4 px-6 flex-shrink-0">
81
- <h1 class="text-sm font-display font-semibold">Knowledge Graph</h1>
82
- <input type="number" id="center" placeholder="Node ID"
83
- class="bg-slate-800 border border-slate-700 rounded-lg px-3 py-1 text-xs w-24 text-slate-200 focus:outline-none focus:ring-1 focus:ring-violet-500">
84
- <select id="depth" class="bg-slate-800 border border-slate-700 rounded-lg px-2 py-1 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-violet-500">
85
- <option>1</option><option selected>2</option><option>3</option>
86
- </select>
87
- <button onclick="loadGraph()" class="px-3 py-1 text-xs bg-violet-600 text-white rounded-lg hover:bg-violet-500 transition-colors">Load</button>
88
- <span id="info" class="text-xs text-slate-500 font-mono"></span>
89
- <div class="flex items-center gap-3 ml-auto text-xs text-slate-400">
90
- <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-violet-500 inline-block"></span>area</span>
91
- <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-emerald-500 inline-block"></span>file</span>
92
- <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-blue-500 inline-block"></span>learning</span>
93
- <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-amber-500 inline-block"></span>decision</span>
94
- <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-pink-500 inline-block"></span>entity</span>
95
- <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-slate-500 inline-block"></span>change</span>
96
- <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-cyan-500 inline-block"></span>memory</span>
97
- </div>
98
- </header>
99
-
100
- <div class="flex-1 flex overflow-hidden">
101
- <!-- Graph canvas -->
102
- <div id="graph-container" class="flex-1 overflow-hidden">
103
- <svg id="svg" class="w-full h-full"></svg>
104
- </div>
105
-
106
- <!-- Node detail sidebar -->
107
- <div id="sidebar" class="overflow-hidden bg-slate-900 border-l border-slate-800 transition-all duration-300 flex-shrink-0" style="width:0">
108
- <div class="p-5 w-80">
109
- <div class="flex items-center justify-between mb-3">
110
- <h3 id="sb-label" class="text-sm font-display font-semibold text-violet-400 pr-4 leading-tight"></h3>
111
- <button onclick="closeSidebar()" class="text-slate-400 hover:text-white text-lg leading-none flex-shrink-0">&times;</button>
112
- </div>
113
- <div id="sb-meta" class="text-xs text-slate-500 mb-3 font-mono space-y-0.5"></div>
114
- <div class="text-xs font-display font-semibold text-slate-400 uppercase tracking-wide mb-2">Connections</div>
115
- <div id="sb-neighbors" class="space-y-1"></div>
116
- </div>
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Knowledge Graph{% endblock %}
4
+ {% block page_title %}Knowledge Graph{% endblock %}
5
+
6
+ {% block head %}
7
+ <style>
8
+ .node circle { transition: filter 0.15s ease; }
9
+ .node:hover circle { filter: drop-shadow(0 0 10px currentColor); }
10
+ .link-label { fill: #64748b; font-size: 10px; pointer-events: none; opacity: 0.6; }
11
+ </style>
12
+ {% endblock %}
13
+
14
+ {% block header_actions %}
15
+ <input type="number" id="center" placeholder="Node ID"
16
+ class="bg-slate-800 border border-slate-700 rounded-lg px-3 py-1 text-xs w-24 text-slate-200 focus:outline-none focus:ring-1 focus:ring-violet-500">
17
+ <select id="depth" class="bg-slate-800 border border-slate-700 rounded-lg px-2 py-1 text-xs text-slate-200 focus:outline-none focus:ring-1 focus:ring-violet-500">
18
+ <option>1</option><option selected>2</option><option>3</option>
19
+ </select>
20
+ <button onclick="loadGraph()" class="px-3 py-1 text-xs bg-violet-600 text-white rounded-lg hover:bg-violet-500 transition-colors">Load</button>
21
+ <span id="info" class="text-xs text-slate-500 font-mono"></span>
22
+ <div class="flex items-center gap-3 ml-2 text-xs text-slate-400">
23
+ <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-violet-500 inline-block"></span>area</span>
24
+ <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-emerald-500 inline-block"></span>file</span>
25
+ <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-blue-500 inline-block"></span>learning</span>
26
+ <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-amber-500 inline-block"></span>decision</span>
27
+ <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-pink-500 inline-block"></span>entity</span>
28
+ <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-slate-500 inline-block"></span>change</span>
29
+ <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-cyan-500 inline-block"></span>memory</span>
30
+ </div>
31
+ {% endblock %}
32
+
33
+ {% block content %}
34
+ <div class="flex overflow-hidden" style="height: calc(100vh - 8.5rem);">
35
+ <!-- Graph canvas -->
36
+ <div id="graph-container" class="flex-1 overflow-hidden">
37
+ <svg id="svg" class="w-full h-full"></svg>
38
+ </div>
39
+
40
+ <!-- Node detail sidebar -->
41
+ <div id="sidebar" class="overflow-hidden bg-slate-900 border-l border-slate-800 transition-all duration-300 flex-shrink-0" style="width:0">
42
+ <div class="p-5 w-80">
43
+ <div class="flex items-center justify-between mb-3">
44
+ <h3 id="sb-label" class="text-sm font-display font-semibold text-violet-400 pr-4 leading-tight"></h3>
45
+ <button onclick="closeSidebar()" class="text-slate-400 hover:text-white text-lg leading-none flex-shrink-0">&times;</button>
117
46
  </div>
47
+ <div id="sb-meta" class="text-xs text-slate-500 mb-3 font-mono space-y-0.5"></div>
48
+ <div class="text-xs font-display font-semibold text-slate-400 uppercase tracking-wide mb-2">Connections</div>
49
+ <div id="sb-neighbors" class="space-y-1"></div>
118
50
  </div>
119
- </main>
120
-
121
- <script src="https://d3js.org/d3.v7.min.js"></script>
122
- <script>
123
- const typeColors = {
124
- area: '#8B5CF6',
125
- file: '#10B981',
126
- learning: '#3B82F6',
127
- decision: '#F59E0B',
128
- entity: '#EC4899',
129
- change: '#64748B',
130
- memory: '#06B6D4'
131
- };
132
-
133
- function nodeColor(type) { return typeColors[type] || '#64748B'; }
134
- function nodeRadius(d) { return d.depth === 0 ? 16 : d.depth === 1 ? 10 : 7; }
135
-
136
- let currentSim = null;
137
-
138
- async function loadGraph() {
139
- const center = document.getElementById('center').value;
140
- const depth = document.getElementById('depth').value;
141
- const url = center ? `/api/graph?center=${center}&depth=${depth}` : '/api/graph';
142
- const data = await fetch(url).then(r => r.json());
143
-
144
- if (!data.nodes || data.nodes.length === 0) {
145
- const hints = (data.hints || []).map(h => `${h.label} (id:${h.id})`).join(', ');
146
- document.getElementById('info').textContent = hints ? `No subgraph found. Top: ${hints}` : 'No graph data found';
147
- if (data.hints && data.hints.length > 0) {
148
- document.getElementById('center').value = data.hints[0].id;
149
- loadGraph();
150
- }
151
- return;
51
+ </div>
52
+ </div>
53
+ {% endblock %}
54
+
55
+ {% block scripts %}
56
+ <script src="https://d3js.org/d3.v7.min.js"></script>
57
+ <script>
58
+ const typeColors = {
59
+ area: '#8B5CF6',
60
+ file: '#10B981',
61
+ learning: '#3B82F6',
62
+ decision: '#F59E0B',
63
+ entity: '#EC4899',
64
+ change: '#64748B',
65
+ memory: '#06B6D4'
66
+ };
67
+
68
+ function nodeColor(type) { return typeColors[type] || '#64748B'; }
69
+ function nodeRadius(d) { return d.depth === 0 ? 16 : d.depth === 1 ? 10 : 7; }
70
+
71
+ let currentSim = null;
72
+
73
+ async function loadGraph() {
74
+ const center = document.getElementById('center').value;
75
+ const depth = document.getElementById('depth').value;
76
+ const url = center ? `/api/graph?center=${center}&depth=${depth}` : '/api/graph';
77
+ const data = await fetch(url).then(r => r.json());
78
+
79
+ if (!data.nodes || data.nodes.length === 0) {
80
+ const hints = (data.hints || []).map(h => `${h.label} (id:${h.id})`).join(', ');
81
+ document.getElementById('info').textContent = hints ? `No subgraph found. Top: ${hints}` : 'No graph data found';
82
+ if (data.hints && data.hints.length > 0) {
83
+ document.getElementById('center').value = data.hints[0].id;
84
+ loadGraph();
152
85
  }
153
-
154
- document.getElementById('info').textContent = `${data.nodes.length} nodes · ${data.edges.length} edges`;
155
- renderGraph(data);
86
+ return;
156
87
  }
157
88
 
158
- function renderGraph(data) {
159
- const svg = d3.select('#svg');
160
- svg.selectAll('*').remove();
161
- const width = svg.node().clientWidth;
162
- const height = svg.node().clientHeight;
163
- const g = svg.append('g');
164
-
165
- svg.call(d3.zoom().scaleExtent([0.1, 8]).on('zoom', e => g.attr('transform', e.transform)));
166
-
167
- if (currentSim) currentSim.stop();
168
- currentSim = d3.forceSimulation(data.nodes)
169
- .force('link', d3.forceLink(data.edges).id(d => d.id).distance(80))
170
- .force('charge', d3.forceManyBody().strength(-300))
171
- .force('center', d3.forceCenter(width / 2, height / 2))
172
- .force('collision', d3.forceCollide().radius(d => nodeRadius(d) + 20));
173
-
174
- // Edges
175
- const link = g.selectAll('.link').data(data.edges).join('line')
176
- .attr('class', 'link')
177
- .attr('stroke', '#334155')
178
- .attr('stroke-width', d => Math.max(1, (d.weight || 0.5) * 2.5))
179
- .attr('stroke-opacity', d => Math.min(0.6, 0.2 + (d.weight || 0.3) * 0.4));
180
-
181
- // Edge labels
182
- const linkLabel = g.selectAll('.link-label').data(data.edges).join('text')
183
- .attr('class', 'link-label')
184
- .text(d => d.relation);
185
-
186
- // Nodes
187
- const node = g.selectAll('.node').data(data.nodes).join('g')
188
- .attr('class', 'node')
189
- .style('cursor', 'pointer')
190
- .on('click', (e, d) => showNodeDetail(d))
191
- .call(d3.drag()
192
- .on('start', (e, d) => { if (!e.active) currentSim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
193
- .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
194
- .on('end', (e, d) => { if (!e.active) currentSim.alphaTarget(0); d.fx = null; d.fy = null; }));
195
-
196
- node.append('circle')
197
- .attr('r', d => nodeRadius(d))
198
- .attr('fill', d => nodeColor(d.type))
199
- .attr('stroke', d => d.depth === 0 ? '#fff' : nodeColor(d.type))
200
- .attr('stroke-width', d => d.depth === 0 ? 2.5 : 1)
201
- .attr('stroke-opacity', d => d.depth === 0 ? 0.8 : 0.3)
202
- .style('color', d => nodeColor(d.type));
203
-
204
- // Labels for center + depth 1
205
- node.filter(d => d.depth <= 1).append('text')
206
- .attr('dx', d => nodeRadius(d) + 5)
207
- .attr('dy', 4)
208
- .attr('fill', '#94a3b8')
209
- .attr('font-size', d => d.depth === 0 ? '13px' : '11px')
210
- .attr('font-weight', d => d.depth === 0 ? '600' : '400')
211
- .attr('font-family', 'Space Grotesk, system-ui, sans-serif')
212
- .text(d => d.label.length > 40 ? d.label.slice(0, 37) + '...' : d.label);
213
-
214
- // Tooltip for depth 2+
215
- node.filter(d => d.depth > 1).append('title')
216
- .text(d => `[${d.type}] ${d.label}`);
217
-
218
- currentSim.on('tick', () => {
219
- link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
220
- .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
221
- linkLabel.attr('x', d => (d.source.x + d.target.x) / 2)
222
- .attr('y', d => (d.source.y + d.target.y) / 2);
223
- node.attr('transform', d => `translate(${d.x},${d.y})`);
224
- });
89
+ document.getElementById('info').textContent = `${data.nodes.length} nodes · ${data.edges.length} edges`;
90
+ renderGraph(data);
91
+ }
92
+
93
+ function renderGraph(data) {
94
+ const svg = d3.select('#svg');
95
+ svg.selectAll('*').remove();
96
+ const width = svg.node().clientWidth;
97
+ const height = svg.node().clientHeight;
98
+ const g = svg.append('g');
99
+
100
+ svg.call(d3.zoom().scaleExtent([0.1, 8]).on('zoom', e => g.attr('transform', e.transform)));
101
+
102
+ if (currentSim) currentSim.stop();
103
+ currentSim = d3.forceSimulation(data.nodes)
104
+ .force('link', d3.forceLink(data.edges).id(d => d.id).distance(80))
105
+ .force('charge', d3.forceManyBody().strength(-300))
106
+ .force('center', d3.forceCenter(width / 2, height / 2))
107
+ .force('collision', d3.forceCollide().radius(d => nodeRadius(d) + 20));
108
+
109
+ const link = g.selectAll('.link').data(data.edges).join('line')
110
+ .attr('class', 'link')
111
+ .attr('stroke', '#334155')
112
+ .attr('stroke-width', d => Math.max(1, (d.weight || 0.5) * 2.5))
113
+ .attr('stroke-opacity', d => Math.min(0.6, 0.2 + (d.weight || 0.3) * 0.4));
114
+
115
+ const linkLabel = g.selectAll('.link-label').data(data.edges).join('text')
116
+ .attr('class', 'link-label')
117
+ .text(d => d.relation);
118
+
119
+ const node = g.selectAll('.node').data(data.nodes).join('g')
120
+ .attr('class', 'node')
121
+ .style('cursor', 'pointer')
122
+ .on('click', (e, d) => showNodeDetail(d))
123
+ .call(d3.drag()
124
+ .on('start', (e, d) => { if (!e.active) currentSim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
125
+ .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
126
+ .on('end', (e, d) => { if (!e.active) currentSim.alphaTarget(0); d.fx = null; d.fy = null; }));
127
+
128
+ node.append('circle')
129
+ .attr('r', d => nodeRadius(d))
130
+ .attr('fill', d => nodeColor(d.type))
131
+ .attr('stroke', d => d.depth === 0 ? '#fff' : nodeColor(d.type))
132
+ .attr('stroke-width', d => d.depth === 0 ? 2.5 : 1)
133
+ .attr('stroke-opacity', d => d.depth === 0 ? 0.8 : 0.3)
134
+ .style('color', d => nodeColor(d.type));
135
+
136
+ node.filter(d => d.depth <= 1).append('text')
137
+ .attr('dx', d => nodeRadius(d) + 5)
138
+ .attr('dy', 4)
139
+ .attr('fill', '#94a3b8')
140
+ .attr('font-size', d => d.depth === 0 ? '13px' : '11px')
141
+ .attr('font-weight', d => d.depth === 0 ? '600' : '400')
142
+ .attr('font-family', 'Space Grotesk, system-ui, sans-serif')
143
+ .text(d => d.label.length > 40 ? d.label.slice(0, 37) + '...' : d.label);
144
+
145
+ node.filter(d => d.depth > 1).append('title')
146
+ .text(d => `[${d.type}] ${d.label}`);
147
+
148
+ currentSim.on('tick', () => {
149
+ link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
150
+ .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
151
+ linkLabel.attr('x', d => (d.source.x + d.target.x) / 2)
152
+ .attr('y', d => (d.source.y + d.target.y) / 2);
153
+ node.attr('transform', d => `translate(${d.x},${d.y})`);
154
+ });
155
+ }
156
+
157
+ async function showNodeDetail(d) {
158
+ document.getElementById('sb-label').textContent = d.label;
159
+ document.getElementById('sb-meta').innerHTML =
160
+ `<div>Type: <span style="color:${nodeColor(d.type)}">${d.type}</span></div>` +
161
+ `<div>ID: ${d.id}</div>` +
162
+ `<div>Depth: ${d.depth}</div>`;
163
+
164
+ document.getElementById('sidebar').style.width = '320px';
165
+
166
+ const resp = await fetch(`/api/graph?center=${d.id}&depth=1`);
167
+ const data = await resp.json();
168
+ const nbDiv = document.getElementById('sb-neighbors');
169
+
170
+ if (!data.edges || data.edges.length === 0) {
171
+ nbDiv.innerHTML = '<p class="text-sm text-slate-500">No connections found</p>';
172
+ return;
225
173
  }
226
174
 
227
- async function showNodeDetail(d) {
228
- document.getElementById('sb-label').textContent = d.label;
229
- document.getElementById('sb-meta').innerHTML =
230
- `<div>Type: <span style="color:${nodeColor(d.type)}">${d.type}</span></div>` +
231
- `<div>ID: ${d.id}</div>` +
232
- `<div>Depth: ${d.depth}</div>`;
233
-
234
- document.getElementById('sidebar').style.width = '320px';
235
-
236
- const resp = await fetch(`/api/graph?center=${d.id}&depth=1`);
237
- const data = await resp.json();
238
- const nbDiv = document.getElementById('sb-neighbors');
239
-
240
- if (!data.edges || data.edges.length === 0) {
241
- nbDiv.innerHTML = '<p class="text-sm text-slate-500">No connections found</p>';
242
- return;
243
- }
244
-
245
- const neighbors = data.nodes.filter(n => n.id !== d.id).slice(0, 20);
246
- nbDiv.innerHTML = `<p class="text-xs text-slate-500 mb-2">${data.edges.length} connections</p>` +
247
- neighbors.map(n => {
248
- const label = n.label.length > 45 ? n.label.slice(0, 42) + '...' : n.label;
249
- return `<div class="neighbor-item px-2 py-1.5 rounded-md hover:bg-slate-800 cursor-pointer transition-colors" data-id="${n.id}">
250
- <span class="text-xs font-semibold" style="color:${nodeColor(n.type)}">[${n.type}]</span>
251
- <span class="text-sm text-slate-300 ml-1">${label.replace(/</g, '&lt;')}</span>
252
- <div class="text-xs text-slate-600 mt-0.5">Click to center</div>
253
- </div>`;
254
- }).join('');
255
-
256
- nbDiv.querySelectorAll('.neighbor-item').forEach(el => {
257
- el.addEventListener('click', () => {
258
- document.getElementById('center').value = el.dataset.id;
259
- loadGraph();
260
- });
175
+ const neighbors = data.nodes.filter(n => n.id !== d.id).slice(0, 20);
176
+ nbDiv.innerHTML = `<p class="text-xs text-slate-500 mb-2">${data.edges.length} connections</p>` +
177
+ neighbors.map(n => {
178
+ const label = n.label.length > 45 ? n.label.slice(0, 42) + '...' : n.label;
179
+ return `<div class="neighbor-item px-2 py-1.5 rounded-md hover:bg-slate-800 cursor-pointer transition-colors" data-id="${n.id}">
180
+ <span class="text-xs font-semibold" style="color:${nodeColor(n.type)}">[${n.type}]</span>
181
+ <span class="text-sm text-slate-300 ml-1">${label.replace(/</g, '&lt;')}</span>
182
+ <div class="text-xs text-slate-600 mt-0.5">Click to center</div>
183
+ </div>`;
184
+ }).join('');
185
+
186
+ nbDiv.querySelectorAll('.neighbor-item').forEach(el => {
187
+ el.addEventListener('click', () => {
188
+ document.getElementById('center').value = el.dataset.id;
189
+ loadGraph();
261
190
  });
262
- }
263
-
264
- function closeSidebar() {
265
- document.getElementById('sidebar').style.width = '0';
266
- }
191
+ });
192
+ }
267
193
 
268
- // Load trust score
269
- fetch('/api/stats').then(r => r.json()).then(data => {
270
- if (data.trust_score != null) {
271
- document.getElementById('trust-score').textContent = Math.round(data.trust_score);
272
- }
273
- }).catch(() => {});
194
+ function closeSidebar() {
195
+ document.getElementById('sidebar').style.width = '0';
196
+ }
274
197
 
275
- // Auto-load on page open
276
- loadGraph();
277
- </script>
278
- </body>
279
- </html>
198
+ // Auto-load on page open
199
+ loadGraph();
200
+ </script>
201
+ {% endblock %}