nexo-brain 5.3.26 → 5.3.28
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.
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/src/hook_guardrails.py +44 -0
- package/src/server.py +3 -0
- package/src/tools_sessions.py +6 -1
- package/src/dashboard/static/favicon 2.svg +0 -32
- package/src/dashboard/static/nexo-logo 2.png +0 -0
- package/src/dashboard/static/nexo-logo 2.svg +0 -40
- package/src/dashboard/static/style 2.css +0 -2458
- package/src/dashboard/templates/adaptive 2.html +0 -118
- package/src/dashboard/templates/artifacts 2.html +0 -133
- package/src/dashboard/templates/backups 2.html +0 -136
- package/src/dashboard/templates/base 2.html +0 -417
- package/src/dashboard/templates/calendar 2.html +0 -591
- package/src/dashboard/templates/chat 2.html +0 -356
- package/src/dashboard/templates/claims 2.html +0 -259
- package/src/dashboard/templates/cortex 2.html +0 -321
- package/src/dashboard/templates/credentials 2.html +0 -128
- package/src/dashboard/templates/crons 2.html +0 -370
- package/src/dashboard/templates/dashboard 2.html +0 -494
- package/src/dashboard/templates/dreams 2.html +0 -252
- package/src/dashboard/templates/email 2.html +0 -160
- package/src/dashboard/templates/evolution 2.html +0 -189
- package/src/dashboard/templates/feed 2.html +0 -249
- package/src/dashboard/templates/followup_health 2.html +0 -170
- package/src/dashboard/templates/graph 2.html +0 -201
- package/src/dashboard/templates/guard 2.html +0 -259
- package/src/dashboard/templates/inbox 2.html +0 -251
- package/src/dashboard/templates/memory 2.html +0 -420
- package/src/dashboard/templates/operations 2.html +0 -608
- package/src/dashboard/templates/plugins 2.html +0 -185
- package/src/dashboard/templates/protocol 2.html +0 -199
- package/src/dashboard/templates/rules 2.html +0 -246
- package/src/dashboard/templates/sentiment 2.html +0 -247
- package/src/dashboard/templates/sessions 2.html +0 -218
- package/src/dashboard/templates/skills 2.html +0 -329
- package/src/dashboard/templates/somatic 2.html +0 -73
- package/src/dashboard/templates/triggers 2.html +0 -133
- package/src/dashboard/templates/trust 2.html +0 -360
- package/src/db/__init__ 2.py +0 -259
- package/src/db/_core 2.py +0 -437
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_episodic 2.py +0 -762
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_goal_profiles 2.py +0 -376
- package/src/db/_hot_context 2.py +0 -660
- package/src/db/_outcomes 2.py +0 -800
- package/src/db/_personal_scripts 2.py +0 -582
- package/src/db/_sessions 2.py +0 -330
- package/src/db/_tasks 2.py +0 -91
- package/src/db/_watchers 2.py +0 -173
- package/src/doctor/formatters 2.py +0 -52
- package/src/doctor/models 2.py +0 -69
- package/src/doctor/planes 2.py +0 -87
- package/src/doctor/providers/__init__ 2.py +0 -1
- package/src/doctor/providers/deep 2.py +0 -367
- package/src/evolution_cycle 2.py +0 -519
- package/src/hooks/auto_capture 2.py +0 -208
- package/src/hooks/caffeinate-guard 2.sh +0 -8
- package/src/hooks/capture-session 2.sh +0 -21
- package/src/hooks/capture-tool-logs 2.sh +0 -158
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/heartbeat-enforcement 2.py +0 -90
- package/src/hooks/heartbeat-posttool 2.sh +0 -18
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -152
- package/src/hooks/pre-compact 2.sh +0 -169
- package/src/hooks/protocol-guardrail 2.sh +0 -10
- package/src/hooks/protocol-pretool-guardrail 2.sh +0 -9
- package/src/hooks/session-stop 2.sh +0 -52
- package/src/kg_populate 2.py +0 -292
- package/src/maintenance 2.py +0 -53
- package/src/memory_backends 2.py +0 -71
- package/src/migrate_embeddings 2.py +0 -124
- package/src/nexo_sdk 2.py +0 -103
- package/src/observability 2.py +0 -199
- package/src/plugin_loader 2.py +0 -217
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -127
- package/src/plugins/claims_tools 2.py +0 -119
- package/src/plugins/cognitive_memory 2.py +0 -609
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -1155
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -560
- package/src/plugins/evolution 2.py +0 -167
- package/src/plugins/goal_engine 2.py +0 -142
- package/src/plugins/guard 2.py +0 -862
- package/src/plugins/impact 2.py +0 -29
- package/src/plugins/knowledge_graph_tools 2.py +0 -137
- package/src/plugins/media_memory_tools 2.py +0 -98
- package/src/plugins/memory_export 2.py +0 -196
- package/src/plugins/outcomes 2.py +0 -130
- package/src/plugins/personal_scripts 2.py +0 -117
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/protocol 2.py +0 -1449
- package/src/plugins/simple_api 2.py +0 -106
- package/src/plugins/skills 2.py +0 -341
- package/src/plugins/state_watchers 2.py +0 -79
- package/src/plugins/update 2.py +0 -986
- package/src/plugins/user_state_tools 2.py +0 -43
- package/src/plugins/workflow 2.py +0 -588
- package/src/protocol_settings 2.py +0 -59
- package/src/public_contribution 2.py +0 -466
- package/src/public_evolution_queue 2.py +0 -241
- package/src/requirements 2.txt +0 -14
- package/src/retroactive_learnings 2.py +0 -373
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +0 -331
- package/src/rules/migrate 2.py +0 -207
- package/src/runtime_power 2.py +0 -874
- package/src/script_registry 2.py +0 -1559
- package/src/scripts/check-context 2.py +0 -272
- package/src/scripts/deep-sleep/apply_findings 2.py +0 -2327
- package/src/scripts/deep-sleep/collect 2.py +0 -928
- package/src/scripts/deep-sleep/extract 2.py +0 -330
- package/src/scripts/deep-sleep/extract-prompt 2.md +0 -285
- package/src/scripts/deep-sleep/synthesize 2.py +0 -312
- package/src/scripts/deep-sleep/synthesize-prompt 2.md +0 -336
- package/src/scripts/nexo-agent-run 2.py +0 -75
- package/src/scripts/nexo-auto-update 2.py +0 -6
- package/src/scripts/nexo-backup 2.sh +0 -25
- package/src/scripts/nexo-brain-activation 2.sh +0 -140
- package/src/scripts/nexo-catchup 2.py +0 -300
- package/src/scripts/nexo-cognitive-decay 2.py +0 -257
- package/src/scripts/nexo-cortex-cycle 2.py +0 -293
- package/src/scripts/nexo-cron-wrapper 2.sh +0 -53
- package/src/scripts/nexo-daily-self-audit 2.py +0 -2161
- package/src/scripts/nexo-dashboard 2.sh +0 -29
- package/src/scripts/nexo-deep-sleep 2.sh +0 -86
- package/src/scripts/nexo-evolution-run 2.py +0 -1664
- package/src/scripts/nexo-followup-hygiene 2.py +0 -139
- package/src/scripts/nexo-hook-record 2.py +0 -42
- package/src/scripts/nexo-immune 2.py +0 -936
- package/src/scripts/nexo-impact-scorer 2.py +0 -117
- package/src/scripts/nexo-inbox-hook 2.sh +0 -74
- package/src/scripts/nexo-install 2.py +0 -6
- package/src/scripts/nexo-learning-housekeep 2.py +0 -401
- package/src/scripts/nexo-learning-validator 2.py +0 -266
- package/src/scripts/nexo-migrate 2.py +0 -260
- package/src/scripts/nexo-outcome-checker 2.py +0 -127
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -456
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -35
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -354
- package/src/scripts/nexo-reflection 2.py +0 -256
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-sleep 2.py +0 -631
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-sync-clients 2.py +0 -16
- package/src/scripts/nexo-synthesis 2.py +0 -475
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -306
- package/src/scripts/nexo-watchdog 2.sh +0 -1207
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/scripts/rehydrate_learnings_from_archive 2.py +0 -245
- package/src/server 2.py +0 -1296
- package/src/skills/run-nexo-audit-phase/guide 2.md +0 -43
- package/src/skills/run-nexo-audit-phase/skill 2.json +0 -59
- package/src/skills/run-nexo-core-fix-cycle/guide 2.md +0 -17
- package/src/skills/run-nexo-core-fix-cycle/script 2.py +0 -276
- package/src/skills/run-nexo-core-fix-cycle/skill 2.json +0 -58
- package/src/skills/run-release-final-audit/guide 2.md +0 -16
- package/src/skills/run-release-final-audit/script 2.py +0 -259
- package/src/skills/run-release-final-audit/skill 2.json +0 -77
- package/src/skills/run-runtime-doctor/guide 2.md +0 -12
- package/src/skills/run-runtime-doctor/script 2.py +0 -21
- package/src/skills/run-runtime-doctor/skill 2.json +0 -25
- package/src/skills_runtime 2.py +0 -932
- package/src/state_watchers_runtime 2.py +0 -475
- package/src/storage_router 2.py +0 -32
- package/src/system_catalog 2.py +0 -786
- package/src/tools_coordination 2.py +0 -103
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_drive 2.py +0 -487
- package/src/tools_hot_context 2.py +0 -163
- package/src/tools_learnings 2.py +0 -612
- package/src/tools_menu 2.py +0 -229
- package/src/tools_reminders 2.py +0 -88
- package/src/tools_reminders_crud 2.py +0 -363
- package/src/tools_sessions 2.py +0 -1054
- package/src/tools_system_catalog 2.py +0 -19
- package/src/tools_task_history 2.py +0 -57
- package/src/tools_transcripts 2.py +0 -98
- package/src/transcript_utils 2.py +0 -412
- package/src/user_context 2.py +0 -46
- package/src/user_data_portability 2.py +0 -328
- package/src/user_state_model 2.py +0 -170
- package/templates/CLAUDE.md 2.template +0 -108
- package/templates/CODEX.AGENTS.md 2.template +0 -66
- package/templates/launchagents/README 2.md +0 -132
- package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +0 -39
- package/templates/launchagents/com.nexo.catchup 2.plist +0 -39
- package/templates/launchagents/com.nexo.cognitive-decay 2.plist +0 -40
- package/templates/launchagents/com.nexo.dashboard 2.plist +0 -43
- package/templates/launchagents/com.nexo.deep-sleep 2.plist +0 -43
- package/templates/launchagents/com.nexo.evolution 2.plist +0 -44
- package/templates/launchagents/com.nexo.followup-hygiene 2.plist +0 -45
- package/templates/launchagents/com.nexo.immune 2.plist +0 -41
- package/templates/launchagents/com.nexo.postmortem 2.plist +0 -45
- package/templates/launchagents/com.nexo.self-audit 2.plist +0 -47
- package/templates/launchagents/com.nexo.synthesis 2.plist +0 -45
- package/templates/launchagents/com.nexo.watchdog 2.plist +0 -37
- package/templates/nexo_helper 2.py +0 -301
- package/templates/openclaw 2.json +0 -13
- package/templates/plugin-template 2.py +0 -40
- package/templates/script-template 2.py +0 -59
- package/templates/script-template 2.sh +0 -13
- package/templates/skill-script-template 2.py +0 -48
- package/templates/skill-template 2.md +0 -33
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
{% extends "base.html" %}
|
|
2
|
-
|
|
3
|
-
{% block title %}NEXO Chat{% endblock %}
|
|
4
|
-
{% block page_title %}NEXO Chat{% endblock %}
|
|
5
|
-
{% block page_subtitle %}
|
|
6
|
-
<span class="text-[10px] px-1.5 py-0.5 bg-nexo-500/20 text-nexo-400 rounded font-bold uppercase ml-2">Beta</span>
|
|
7
|
-
{% endblock %}
|
|
8
|
-
|
|
9
|
-
{% block content %}
|
|
10
|
-
<div class="flex flex-col" style="height: calc(100vh - 7.5rem);">
|
|
11
|
-
|
|
12
|
-
<!-- Chat messages area -->
|
|
13
|
-
<div id="chat-messages" class="flex-1 overflow-y-auto px-2 py-4 space-y-4 scroll-smooth">
|
|
14
|
-
<!-- Welcome message rendered by JS -->
|
|
15
|
-
</div>
|
|
16
|
-
|
|
17
|
-
<!-- Suggested prompts -->
|
|
18
|
-
<div id="suggestions" class="flex items-center gap-2 px-2 py-3 overflow-x-auto flex-shrink-0">
|
|
19
|
-
<button onclick="sendSuggestion(this)" class="suggestion-pill whitespace-nowrap px-3 py-1.5 rounded-lg text-xs transition-all">
|
|
20
|
-
What happened last night?
|
|
21
|
-
</button>
|
|
22
|
-
<button onclick="sendSuggestion(this)" class="suggestion-pill whitespace-nowrap px-3 py-1.5 rounded-lg text-xs transition-all">
|
|
23
|
-
Trust status
|
|
24
|
-
</button>
|
|
25
|
-
<button onclick="sendSuggestion(this)" class="suggestion-pill whitespace-nowrap px-3 py-1.5 rounded-lg text-xs transition-all">
|
|
26
|
-
Active skills
|
|
27
|
-
</button>
|
|
28
|
-
<button onclick="sendSuggestion(this)" class="suggestion-pill whitespace-nowrap px-3 py-1.5 rounded-lg text-xs transition-all">
|
|
29
|
-
Recent crons
|
|
30
|
-
</button>
|
|
31
|
-
<button onclick="sendSuggestion(this)" class="suggestion-pill whitespace-nowrap px-3 py-1.5 rounded-lg text-xs transition-all">
|
|
32
|
-
Pending followups
|
|
33
|
-
</button>
|
|
34
|
-
</div>
|
|
35
|
-
|
|
36
|
-
<!-- Input bar -->
|
|
37
|
-
<div class="flex-shrink-0 border-t border-slate-800/50 bg-slate-900/30 backdrop-blur-sm rounded-b-xl px-3 py-3">
|
|
38
|
-
<div class="flex items-end gap-2 max-w-3xl mx-auto">
|
|
39
|
-
<div class="flex-1 relative">
|
|
40
|
-
<textarea
|
|
41
|
-
id="chat-input"
|
|
42
|
-
rows="1"
|
|
43
|
-
placeholder="Ask me anything..."
|
|
44
|
-
class="w-full bg-slate-900/80 border border-slate-700/50 rounded-xl px-4 py-3 text-sm text-slate-200 placeholder-slate-600 resize-none focus:outline-none focus:border-nexo-500/50 focus:ring-1 focus:ring-nexo-500/20 transition-all"
|
|
45
|
-
onkeydown="handleKeydown(event)"
|
|
46
|
-
oninput="autoResize(this)"
|
|
47
|
-
></textarea>
|
|
48
|
-
</div>
|
|
49
|
-
<button
|
|
50
|
-
id="send-btn"
|
|
51
|
-
onclick="sendMessage()"
|
|
52
|
-
class="w-10 h-10 rounded-xl bg-nexo-500 hover:bg-nexo-600 text-white flex items-center justify-center transition-all duration-200 hover:scale-105 active:scale-95 disabled:opacity-30 disabled:hover:scale-100 flex-shrink-0"
|
|
53
|
-
disabled
|
|
54
|
-
>
|
|
55
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19V5m0 0l-7 7m7-7l7 7"/></svg>
|
|
56
|
-
</button>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
<style>
|
|
62
|
-
#chat-messages::-webkit-scrollbar { width: 4px; }
|
|
63
|
-
#chat-messages::-webkit-scrollbar-thumb { background: #334155; border-radius: 2px; }
|
|
64
|
-
|
|
65
|
-
.suggestion-pill {
|
|
66
|
-
background: rgba(30, 41, 59, 0.6);
|
|
67
|
-
border: 1px solid rgba(51, 65, 85, 0.4);
|
|
68
|
-
color: #94a3b8;
|
|
69
|
-
}
|
|
70
|
-
.suggestion-pill:hover {
|
|
71
|
-
background: rgba(124, 58, 237, 0.12);
|
|
72
|
-
border-color: rgba(124, 58, 237, 0.3);
|
|
73
|
-
color: #c4b5fd;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.bubble-user {
|
|
77
|
-
background: rgba(124, 58, 237, 0.2);
|
|
78
|
-
border: 1px solid rgba(124, 58, 237, 0.25);
|
|
79
|
-
border-radius: 16px 16px 4px 16px;
|
|
80
|
-
}
|
|
81
|
-
.bubble-nexo {
|
|
82
|
-
background: rgba(30, 41, 59, 0.6);
|
|
83
|
-
border: 1px solid rgba(51, 65, 85, 0.4);
|
|
84
|
-
border-radius: 16px 16px 16px 4px;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
@keyframes bubbleIn {
|
|
88
|
-
0% { opacity: 0; transform: translateY(8px) scale(0.97); }
|
|
89
|
-
100% { opacity: 1; transform: translateY(0) scale(1); }
|
|
90
|
-
}
|
|
91
|
-
.chat-bubble { animation: bubbleIn 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
|
|
92
|
-
|
|
93
|
-
@keyframes typingDot {
|
|
94
|
-
0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
|
|
95
|
-
30% { opacity: 1; transform: translateY(-4px); }
|
|
96
|
-
}
|
|
97
|
-
.typing-dot {
|
|
98
|
-
width: 6px; height: 6px; border-radius: 50%; background: #7C3AED;
|
|
99
|
-
display: inline-block;
|
|
100
|
-
}
|
|
101
|
-
.typing-dot:nth-child(1) { animation: typingDot 1.4s ease-in-out infinite; }
|
|
102
|
-
.typing-dot:nth-child(2) { animation: typingDot 1.4s ease-in-out infinite 0.2s; }
|
|
103
|
-
.typing-dot:nth-child(3) { animation: typingDot 1.4s ease-in-out infinite 0.4s; }
|
|
104
|
-
|
|
105
|
-
.data-table-toggle {
|
|
106
|
-
cursor: pointer;
|
|
107
|
-
transition: color 0.15s;
|
|
108
|
-
}
|
|
109
|
-
.data-table-toggle:hover { color: #c4b5fd; }
|
|
110
|
-
|
|
111
|
-
.data-section {
|
|
112
|
-
max-height: 0;
|
|
113
|
-
overflow: hidden;
|
|
114
|
-
transition: max-height 0.3s ease;
|
|
115
|
-
}
|
|
116
|
-
.data-section.open {
|
|
117
|
-
max-height: 500px;
|
|
118
|
-
overflow-y: auto;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
#chat-input {
|
|
122
|
-
max-height: 120px;
|
|
123
|
-
line-height: 1.5;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.nexo-avatar {
|
|
127
|
-
width: 28px; height: 28px;
|
|
128
|
-
background: linear-gradient(135deg, #7C3AED, #EC4899);
|
|
129
|
-
border-radius: 8px;
|
|
130
|
-
display: flex; align-items: center; justify-content: center;
|
|
131
|
-
font-size: 11px; font-weight: 700; color: white;
|
|
132
|
-
flex-shrink: 0;
|
|
133
|
-
}
|
|
134
|
-
</style>
|
|
135
|
-
{% endblock %}
|
|
136
|
-
|
|
137
|
-
{% block scripts %}
|
|
138
|
-
<script>
|
|
139
|
-
const STORAGE_KEY = 'nexo-chat-history';
|
|
140
|
-
let chatHistory = [];
|
|
141
|
-
let isWaiting = false;
|
|
142
|
-
|
|
143
|
-
function loadHistory() {
|
|
144
|
-
try {
|
|
145
|
-
const stored = sessionStorage.getItem(STORAGE_KEY);
|
|
146
|
-
if (stored) chatHistory = JSON.parse(stored);
|
|
147
|
-
} catch {}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function saveHistory() {
|
|
151
|
-
try {
|
|
152
|
-
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(chatHistory.slice(-100)));
|
|
153
|
-
} catch {}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function autoResize(el) {
|
|
157
|
-
el.style.height = 'auto';
|
|
158
|
-
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
|
|
159
|
-
document.getElementById('send-btn').disabled = !el.value.trim();
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function handleKeydown(e) {
|
|
163
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
164
|
-
e.preventDefault();
|
|
165
|
-
sendMessage();
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function scrollToBottom() {
|
|
170
|
-
const container = document.getElementById('chat-messages');
|
|
171
|
-
requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function renderMessage(msg) {
|
|
175
|
-
if (msg.role === 'user') {
|
|
176
|
-
return `
|
|
177
|
-
<div class="flex justify-end chat-bubble">
|
|
178
|
-
<div class="bubble-user px-4 py-2.5 max-w-lg">
|
|
179
|
-
<p class="text-sm text-slate-200 leading-relaxed">${escapeHtml(msg.content)}</p>
|
|
180
|
-
</div>
|
|
181
|
-
</div>
|
|
182
|
-
`;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// NEXO response
|
|
186
|
-
let dataHtml = '';
|
|
187
|
-
if (msg.data && msg.data.length > 0) {
|
|
188
|
-
const cols = Object.keys(msg.data[0]);
|
|
189
|
-
dataHtml = `
|
|
190
|
-
<div class="mt-3">
|
|
191
|
-
<button class="data-table-toggle text-[10px] uppercase tracking-wider text-slate-500 font-semibold flex items-center gap-1" onclick="toggleDataSection(this)">
|
|
192
|
-
<svg class="w-3 h-3 transition-transform duration-200 data-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
|
193
|
-
${msg.data.length} results
|
|
194
|
-
</button>
|
|
195
|
-
<div class="data-section mt-2">
|
|
196
|
-
<div class="bg-slate-950/50 rounded-lg border border-slate-800/30 overflow-x-auto">
|
|
197
|
-
<table class="w-full text-xs">
|
|
198
|
-
<thead>
|
|
199
|
-
<tr class="border-b border-slate-800/40">
|
|
200
|
-
${cols.map(c => `<th class="px-3 py-2 text-left text-[10px] uppercase tracking-wider text-slate-600 font-semibold">${escapeHtml(c)}</th>`).join('')}
|
|
201
|
-
</tr>
|
|
202
|
-
</thead>
|
|
203
|
-
<tbody class="divide-y divide-slate-800/20">
|
|
204
|
-
${msg.data.slice(0, 20).map(row => `
|
|
205
|
-
<tr class="hover:bg-slate-800/20 transition-colors">
|
|
206
|
-
${cols.map(c => `<td class="px-3 py-1.5 font-mono text-slate-400 whitespace-nowrap">${escapeHtml(String(row[c] ?? '--'))}</td>`).join('')}
|
|
207
|
-
</tr>
|
|
208
|
-
`).join('')}
|
|
209
|
-
</tbody>
|
|
210
|
-
</table>
|
|
211
|
-
${msg.data.length > 20 ? `<div class="px-3 py-2 text-[10px] text-slate-600 border-t border-slate-800/30">+ ${msg.data.length - 20} more rows</div>` : ''}
|
|
212
|
-
</div>
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
`;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return `
|
|
219
|
-
<div class="flex items-start gap-2.5 chat-bubble">
|
|
220
|
-
<div class="nexo-avatar">N</div>
|
|
221
|
-
<div class="bubble-nexo px-4 py-2.5 max-w-2xl">
|
|
222
|
-
<div class="text-sm text-slate-300 leading-relaxed chat-answer">${formatAnswer(msg.content)}</div>
|
|
223
|
-
${dataHtml}
|
|
224
|
-
${msg.query_type ? `<div class="mt-2 text-[9px] text-slate-600 font-mono">query: ${escapeHtml(msg.query_type)}</div>` : ''}
|
|
225
|
-
</div>
|
|
226
|
-
</div>
|
|
227
|
-
`;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function formatAnswer(text) {
|
|
231
|
-
if (!text) return '';
|
|
232
|
-
// Basic markdown-like formatting
|
|
233
|
-
let html = escapeHtml(text);
|
|
234
|
-
html = html.replace(/\*\*(.*?)\*\*/g, '<strong class="text-slate-100 font-semibold">$1</strong>');
|
|
235
|
-
html = html.replace(/`(.*?)`/g, '<code class="text-xs bg-slate-800/60 px-1 py-0.5 rounded text-nexo-300 font-mono">$1</code>');
|
|
236
|
-
html = html.replace(/\n/g, '<br>');
|
|
237
|
-
return html;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function toggleDataSection(btn) {
|
|
241
|
-
const section = btn.nextElementSibling;
|
|
242
|
-
const chevron = btn.querySelector('.data-chevron');
|
|
243
|
-
section.classList.toggle('open');
|
|
244
|
-
chevron.style.transform = section.classList.contains('open') ? 'rotate(90deg)' : '';
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function showTyping() {
|
|
248
|
-
const container = document.getElementById('chat-messages');
|
|
249
|
-
const typingEl = document.createElement('div');
|
|
250
|
-
typingEl.id = 'typing-indicator';
|
|
251
|
-
typingEl.className = 'flex items-start gap-2.5 chat-bubble';
|
|
252
|
-
typingEl.innerHTML = `
|
|
253
|
-
<div class="nexo-avatar">N</div>
|
|
254
|
-
<div class="bubble-nexo px-4 py-3">
|
|
255
|
-
<div class="flex items-center gap-1.5">
|
|
256
|
-
<div class="typing-dot"></div>
|
|
257
|
-
<div class="typing-dot"></div>
|
|
258
|
-
<div class="typing-dot"></div>
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
`;
|
|
262
|
-
container.appendChild(typingEl);
|
|
263
|
-
scrollToBottom();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function hideTyping() {
|
|
267
|
-
const el = document.getElementById('typing-indicator');
|
|
268
|
-
if (el) el.remove();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function renderAllMessages() {
|
|
272
|
-
const container = document.getElementById('chat-messages');
|
|
273
|
-
if (chatHistory.length === 0) {
|
|
274
|
-
container.innerHTML = `
|
|
275
|
-
<div class="flex items-start gap-2.5 chat-bubble">
|
|
276
|
-
<div class="nexo-avatar">N</div>
|
|
277
|
-
<div class="bubble-nexo px-4 py-3 max-w-xl">
|
|
278
|
-
<p class="text-sm text-slate-300 leading-relaxed">I'm <strong class="text-nexo-400">NEXO Chat</strong>. Ask me anything about the system.</p>
|
|
279
|
-
<p class="text-xs text-slate-500 mt-2">I can query diaries, crons, trust, skills, followups, and more.</p>
|
|
280
|
-
</div>
|
|
281
|
-
</div>
|
|
282
|
-
`;
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
container.innerHTML = chatHistory.map(renderMessage).join('');
|
|
286
|
-
scrollToBottom();
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function sendSuggestion(btn) {
|
|
290
|
-
const text = btn.textContent.trim();
|
|
291
|
-
document.getElementById('chat-input').value = text;
|
|
292
|
-
document.getElementById('send-btn').disabled = false;
|
|
293
|
-
sendMessage();
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async function sendMessage() {
|
|
297
|
-
const input = document.getElementById('chat-input');
|
|
298
|
-
const text = input.value.trim();
|
|
299
|
-
if (!text || isWaiting) return;
|
|
300
|
-
|
|
301
|
-
// Add user message
|
|
302
|
-
chatHistory.push({ role: 'user', content: text });
|
|
303
|
-
saveHistory();
|
|
304
|
-
renderAllMessages();
|
|
305
|
-
|
|
306
|
-
// Clear input
|
|
307
|
-
input.value = '';
|
|
308
|
-
input.style.height = 'auto';
|
|
309
|
-
document.getElementById('send-btn').disabled = true;
|
|
310
|
-
|
|
311
|
-
// Hide suggestions after first message
|
|
312
|
-
document.getElementById('suggestions').style.display = chatHistory.filter(m => m.role === 'user').length > 1 ? 'none' : '';
|
|
313
|
-
|
|
314
|
-
// Show typing
|
|
315
|
-
isWaiting = true;
|
|
316
|
-
showTyping();
|
|
317
|
-
|
|
318
|
-
try {
|
|
319
|
-
const res = await fetch('/api/chat', {
|
|
320
|
-
method: 'POST',
|
|
321
|
-
headers: { 'Content-Type': 'application/json' },
|
|
322
|
-
body: JSON.stringify({ message: text })
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
hideTyping();
|
|
326
|
-
|
|
327
|
-
if (!res.ok) {
|
|
328
|
-
const errText = await res.text().catch(() => 'Error');
|
|
329
|
-
chatHistory.push({ role: 'nexo', content: `Error: ${res.status} - ${errText}` });
|
|
330
|
-
} else {
|
|
331
|
-
const data = await res.json();
|
|
332
|
-
chatHistory.push({
|
|
333
|
-
role: 'nexo',
|
|
334
|
-
content: data.answer || 'Sin respuesta.',
|
|
335
|
-
data: data.data || null,
|
|
336
|
-
query_type: data.query_type || null
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
} catch (err) {
|
|
340
|
-
hideTyping();
|
|
341
|
-
chatHistory.push({ role: 'nexo', content: `Error de conexion: ${err.message}` });
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
isWaiting = false;
|
|
345
|
-
saveHistory();
|
|
346
|
-
renderAllMessages();
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Initialize
|
|
350
|
-
loadHistory();
|
|
351
|
-
renderAllMessages();
|
|
352
|
-
|
|
353
|
-
// Focus input
|
|
354
|
-
document.getElementById('chat-input').focus();
|
|
355
|
-
</script>
|
|
356
|
-
{% endblock %}
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
{% extends "base.html" %}
|
|
2
|
-
|
|
3
|
-
{% block title %}Claims Network{% endblock %}
|
|
4
|
-
{% block page_title %}Claims Network{% endblock %}
|
|
5
|
-
|
|
6
|
-
{% block content %}
|
|
7
|
-
<div class="space-y-5">
|
|
8
|
-
<!-- Status counts -->
|
|
9
|
-
<div class="grid grid-cols-4 gap-4" id="status-cards">
|
|
10
|
-
<div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-4 card">
|
|
11
|
-
<div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-2">Total Claims</div>
|
|
12
|
-
<div class="text-xl font-mono font-semibold text-slate-200" id="total-claims">--</div>
|
|
13
|
-
</div>
|
|
14
|
-
<div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-4 card">
|
|
15
|
-
<div class="text-xs uppercase tracking-wider text-emerald-400/70 font-medium mb-2">Verified</div>
|
|
16
|
-
<div class="text-xl font-mono font-semibold text-emerald-400" id="verified-claims">--</div>
|
|
17
|
-
</div>
|
|
18
|
-
<div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-4 card">
|
|
19
|
-
<div class="text-xs uppercase tracking-wider text-amber-400/70 font-medium mb-2">Pending</div>
|
|
20
|
-
<div class="text-xl font-mono font-semibold text-amber-400" id="pending-claims">--</div>
|
|
21
|
-
</div>
|
|
22
|
-
<div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-4 card">
|
|
23
|
-
<div class="text-xs uppercase tracking-wider text-red-400/70 font-medium mb-2">Disputed</div>
|
|
24
|
-
<div class="text-xl font-mono font-semibold text-red-400" id="disputed-claims">--</div>
|
|
25
|
-
</div>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<!-- Filter bar -->
|
|
29
|
-
<div class="flex items-center gap-2 flex-wrap">
|
|
30
|
-
<span class="text-xs text-slate-500">Filter:</span>
|
|
31
|
-
<button onclick="filterClaims('all')" class="filter-btn active px-2.5 py-1 rounded-md text-xs bg-slate-800 text-slate-300 hover:bg-slate-700 transition-colors" data-filter="all">All</button>
|
|
32
|
-
<button onclick="filterClaims('verified')" class="filter-btn px-2.5 py-1 rounded-md text-xs bg-slate-800/50 text-slate-400 hover:bg-slate-700 transition-colors" data-filter="verified">Verified</button>
|
|
33
|
-
<button onclick="filterClaims('pending')" class="filter-btn px-2.5 py-1 rounded-md text-xs bg-slate-800/50 text-slate-400 hover:bg-slate-700 transition-colors" data-filter="pending">Pending</button>
|
|
34
|
-
<button onclick="filterClaims('disputed')" class="filter-btn px-2.5 py-1 rounded-md text-xs bg-slate-800/50 text-slate-400 hover:bg-slate-700 transition-colors" data-filter="disputed">Disputed</button>
|
|
35
|
-
<div class="flex-1"></div>
|
|
36
|
-
<input type="text" id="claim-search" placeholder="Search claims..." class="px-3 py-1.5 rounded-lg bg-slate-800/50 border border-slate-700/50 text-xs text-slate-200 placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-violet-500 w-48" oninput="searchClaims(this.value)">
|
|
37
|
-
</div>
|
|
38
|
-
|
|
39
|
-
<div class="grid grid-cols-3 gap-5">
|
|
40
|
-
<!-- Network visualization -->
|
|
41
|
-
<div class="col-span-2 bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
|
|
42
|
-
<div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-4">Claim Network</div>
|
|
43
|
-
<canvas id="network-canvas" class="w-full rounded-lg" style="height: 420px; background: rgba(15,23,42,0.5);"></canvas>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<!-- Claim detail panel -->
|
|
47
|
-
<div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
|
|
48
|
-
<div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-4">Claim Details</div>
|
|
49
|
-
<div id="claim-detail" class="text-xs text-slate-600 text-center py-12">
|
|
50
|
-
Click a node to see details
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<!-- Claims list -->
|
|
56
|
-
<div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
|
|
57
|
-
<div class="text-xs uppercase tracking-wider text-slate-400 font-medium mb-4">All Claims</div>
|
|
58
|
-
<div class="space-y-2" id="claims-list">
|
|
59
|
-
<div class="text-xs text-slate-600 py-4 text-center">loading...</div>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
{% endblock %}
|
|
64
|
-
|
|
65
|
-
{% block scripts %}
|
|
66
|
-
<script>
|
|
67
|
-
let allClaims = [];
|
|
68
|
-
let allLinks = [];
|
|
69
|
-
let currentFilter = 'all';
|
|
70
|
-
let nodes = [];
|
|
71
|
-
let selectedNode = null;
|
|
72
|
-
|
|
73
|
-
const STATUS_COLORS = {
|
|
74
|
-
verified: { bg: 'bg-emerald-500/10', text: 'text-emerald-400', fill: '#10B981' },
|
|
75
|
-
pending: { bg: 'bg-amber-500/10', text: 'text-amber-400', fill: '#F59E0B' },
|
|
76
|
-
disputed: { bg: 'bg-red-500/10', text: 'text-red-400', fill: '#EF4444' },
|
|
77
|
-
unknown: { bg: 'bg-slate-500/10', text: 'text-slate-400', fill: '#64748b' },
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
function filterClaims(status) {
|
|
81
|
-
currentFilter = status;
|
|
82
|
-
document.querySelectorAll('.filter-btn').forEach(b => {
|
|
83
|
-
b.classList.toggle('active', b.dataset.filter === status);
|
|
84
|
-
b.classList.toggle('bg-slate-800', b.dataset.filter === status);
|
|
85
|
-
b.classList.toggle('text-slate-300', b.dataset.filter === status);
|
|
86
|
-
b.classList.toggle('bg-slate-800/50', b.dataset.filter !== status);
|
|
87
|
-
b.classList.toggle('text-slate-400', b.dataset.filter !== status);
|
|
88
|
-
});
|
|
89
|
-
renderClaimsList();
|
|
90
|
-
drawNetwork();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function searchClaims(query) {
|
|
94
|
-
const q = query.toLowerCase();
|
|
95
|
-
document.querySelectorAll('#claims-list .claim-item').forEach(el => {
|
|
96
|
-
const text = el.textContent.toLowerCase();
|
|
97
|
-
el.style.display = text.includes(q) ? '' : 'none';
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function getFilteredClaims() {
|
|
102
|
-
if (currentFilter === 'all') return allClaims;
|
|
103
|
-
return allClaims.filter(c => (c.verification_status || 'pending') === currentFilter);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function renderClaimsList() {
|
|
107
|
-
const container = document.getElementById('claims-list');
|
|
108
|
-
const claims = getFilteredClaims();
|
|
109
|
-
if (!claims.length) { container.innerHTML = '<div class="text-xs text-slate-600 py-4 text-center">No claims found</div>'; return; }
|
|
110
|
-
|
|
111
|
-
container.innerHTML = claims.map(c => {
|
|
112
|
-
const status = c.verification_status || 'pending';
|
|
113
|
-
const sc = STATUS_COLORS[status] || STATUS_COLORS.unknown;
|
|
114
|
-
const conf = c.confidence != null ? (c.confidence * 100).toFixed(0) + '%' : '--';
|
|
115
|
-
return `<div class="claim-item flex items-start gap-3 py-2.5 px-3 rounded-lg hover:bg-slate-800/30 transition-colors cursor-pointer border border-transparent hover:border-slate-800/50" onclick="selectClaim(${c.id})">
|
|
116
|
-
<span class="w-1.5 h-1.5 rounded-full mt-1.5 flex-shrink-0" style="background:${sc.fill}"></span>
|
|
117
|
-
<div class="flex-1 min-w-0">
|
|
118
|
-
<div class="text-xs text-slate-300 leading-relaxed">${escapeHtml(c.text || '--')}</div>
|
|
119
|
-
<div class="flex items-center gap-2 mt-1">
|
|
120
|
-
<span class="px-1.5 py-0.5 rounded text-[10px] font-medium ${sc.bg} ${sc.text}">${status}</span>
|
|
121
|
-
${c.domain ? `<span class="text-[10px] text-slate-500">${escapeHtml(c.domain)}</span>` : ''}
|
|
122
|
-
<span class="text-[10px] text-slate-600 font-mono">conf: ${conf}</span>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
</div>`;
|
|
126
|
-
}).join('');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function selectClaim(id) {
|
|
130
|
-
const c = allClaims.find(x => x.id === id);
|
|
131
|
-
if (!c) return;
|
|
132
|
-
selectedNode = id;
|
|
133
|
-
|
|
134
|
-
const status = c.verification_status || 'pending';
|
|
135
|
-
const sc = STATUS_COLORS[status] || STATUS_COLORS.unknown;
|
|
136
|
-
const linkedClaims = allLinks.filter(l => l.source_claim_id === id || l.target_claim_id === id);
|
|
137
|
-
|
|
138
|
-
document.getElementById('claim-detail').innerHTML = `
|
|
139
|
-
<div class="space-y-4 text-left">
|
|
140
|
-
<div>
|
|
141
|
-
<span class="px-1.5 py-0.5 rounded text-[10px] font-medium ${sc.bg} ${sc.text}">${status}</span>
|
|
142
|
-
${c.domain ? `<span class="ml-2 text-[10px] text-slate-500">${escapeHtml(c.domain)}</span>` : ''}
|
|
143
|
-
</div>
|
|
144
|
-
<p class="text-sm text-slate-200 leading-relaxed">${escapeHtml(c.text || '--')}</p>
|
|
145
|
-
<div class="grid grid-cols-2 gap-3">
|
|
146
|
-
<div><div class="text-[10px] text-slate-500 uppercase">Confidence</div><div class="text-sm font-mono text-slate-300">${c.confidence != null ? (c.confidence * 100).toFixed(1) + '%' : '--'}</div></div>
|
|
147
|
-
<div><div class="text-[10px] text-slate-500 uppercase">ID</div><div class="text-sm font-mono text-slate-300">#${c.id}</div></div>
|
|
148
|
-
</div>
|
|
149
|
-
${linkedClaims.length ? `
|
|
150
|
-
<div class="border-t border-slate-800/50 pt-3">
|
|
151
|
-
<div class="text-[10px] text-slate-500 uppercase mb-2">Linked Claims (${linkedClaims.length})</div>
|
|
152
|
-
<div class="space-y-1.5">${linkedClaims.map(l => {
|
|
153
|
-
const otherId = l.source_claim_id === id ? l.target_claim_id : l.source_claim_id;
|
|
154
|
-
const other = allClaims.find(x => x.id === otherId);
|
|
155
|
-
return `<div class="text-xs text-slate-400 flex items-center gap-2">
|
|
156
|
-
<span class="text-[10px] text-violet-400 font-mono">${escapeHtml(l.relation || 'related')}</span>
|
|
157
|
-
<span class="truncate">${other ? escapeHtml((other.text || '').substring(0, 60)) : '#' + otherId}</span>
|
|
158
|
-
</div>`;
|
|
159
|
-
}).join('')}</div>
|
|
160
|
-
</div>` : ''}
|
|
161
|
-
</div>`;
|
|
162
|
-
|
|
163
|
-
drawNetwork();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function drawNetwork() {
|
|
167
|
-
const canvas = document.getElementById('network-canvas');
|
|
168
|
-
const ctx = canvas.getContext('2d');
|
|
169
|
-
const rect = canvas.getBoundingClientRect();
|
|
170
|
-
canvas.width = rect.width * window.devicePixelRatio;
|
|
171
|
-
canvas.height = rect.height * window.devicePixelRatio;
|
|
172
|
-
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
|
173
|
-
const w = rect.width, h = rect.height;
|
|
174
|
-
|
|
175
|
-
ctx.clearRect(0, 0, w, h);
|
|
176
|
-
|
|
177
|
-
const filtered = getFilteredClaims();
|
|
178
|
-
const idSet = new Set(filtered.map(c => c.id));
|
|
179
|
-
|
|
180
|
-
if (!nodes.length) {
|
|
181
|
-
nodes = filtered.map((c, i) => {
|
|
182
|
-
const angle = (2 * Math.PI * i) / filtered.length;
|
|
183
|
-
const r = Math.min(w, h) * 0.35;
|
|
184
|
-
return { id: c.id, x: w / 2 + r * Math.cos(angle) + (Math.random() - 0.5) * 40, y: h / 2 + r * Math.sin(angle) + (Math.random() - 0.5) * 40, claim: c };
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Draw links
|
|
189
|
-
const visLinks = allLinks.filter(l => idSet.has(l.source_claim_id) && idSet.has(l.target_claim_id));
|
|
190
|
-
visLinks.forEach(l => {
|
|
191
|
-
const src = nodes.find(n => n.id === l.source_claim_id);
|
|
192
|
-
const tgt = nodes.find(n => n.id === l.target_claim_id);
|
|
193
|
-
if (!src || !tgt) return;
|
|
194
|
-
ctx.beginPath();
|
|
195
|
-
ctx.moveTo(src.x, src.y);
|
|
196
|
-
ctx.lineTo(tgt.x, tgt.y);
|
|
197
|
-
ctx.strokeStyle = (selectedNode === src.id || selectedNode === tgt.id) ? 'rgba(124,58,237,0.5)' : 'rgba(51,65,85,0.3)';
|
|
198
|
-
ctx.lineWidth = (selectedNode === src.id || selectedNode === tgt.id) ? 2 : 1;
|
|
199
|
-
ctx.stroke();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// Draw nodes
|
|
203
|
-
nodes.forEach(n => {
|
|
204
|
-
if (!idSet.has(n.id)) return;
|
|
205
|
-
const status = n.claim.verification_status || 'pending';
|
|
206
|
-
const color = (STATUS_COLORS[status] || STATUS_COLORS.unknown).fill;
|
|
207
|
-
const isSelected = selectedNode === n.id;
|
|
208
|
-
const radius = isSelected ? 8 : 5;
|
|
209
|
-
|
|
210
|
-
ctx.beginPath();
|
|
211
|
-
ctx.arc(n.x, n.y, radius, 0, 2 * Math.PI);
|
|
212
|
-
ctx.fillStyle = color;
|
|
213
|
-
ctx.fill();
|
|
214
|
-
if (isSelected) {
|
|
215
|
-
ctx.strokeStyle = '#fff';
|
|
216
|
-
ctx.lineWidth = 2;
|
|
217
|
-
ctx.stroke();
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
canvas_click: {
|
|
223
|
-
const canvas = document.getElementById('network-canvas');
|
|
224
|
-
canvas.addEventListener('click', e => {
|
|
225
|
-
const rect = canvas.getBoundingClientRect();
|
|
226
|
-
const x = e.clientX - rect.left;
|
|
227
|
-
const y = e.clientY - rect.top;
|
|
228
|
-
let closest = null, minDist = 20;
|
|
229
|
-
nodes.forEach(n => {
|
|
230
|
-
const d = Math.hypot(n.x - x, n.y - y);
|
|
231
|
-
if (d < minDist) { minDist = d; closest = n; }
|
|
232
|
-
});
|
|
233
|
-
if (closest) selectClaim(closest.id);
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async function loadClaims() {
|
|
238
|
-
const data = await fetchJSON('/api/claims');
|
|
239
|
-
if (!data) return;
|
|
240
|
-
|
|
241
|
-
allClaims = data.claims || [];
|
|
242
|
-
allLinks = data.links || [];
|
|
243
|
-
const counts = data.status_counts || {};
|
|
244
|
-
|
|
245
|
-
document.getElementById('total-claims').textContent = allClaims.length;
|
|
246
|
-
document.getElementById('verified-claims').textContent = counts.verified || 0;
|
|
247
|
-
document.getElementById('pending-claims').textContent = counts.pending || 0;
|
|
248
|
-
document.getElementById('disputed-claims').textContent = counts.disputed || 0;
|
|
249
|
-
|
|
250
|
-
nodes = [];
|
|
251
|
-
renderClaimsList();
|
|
252
|
-
drawNetwork();
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
loadClaims();
|
|
256
|
-
setInterval(loadClaims, 60000);
|
|
257
|
-
window.addEventListener('resize', () => { if (allClaims.length) { nodes = []; drawNetwork(); } });
|
|
258
|
-
</script>
|
|
259
|
-
{% endblock %}
|