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.
- package/README.md +77 -8
- package/bin/nexo-brain.js +230 -22
- package/bin/nexo.js +55 -0
- package/community/skills/.gitkeep +1 -0
- package/package.json +5 -2
- package/src/auto_update.py +158 -8
- package/src/cli.py +605 -0
- package/src/cognitive/_ingest.py +1 -1
- package/src/cognitive/_memory.py +4 -4
- package/src/crons/manifest.json +8 -0
- package/src/dashboard/app.py +709 -37
- package/src/dashboard/templates/adaptive.html +112 -218
- package/src/dashboard/templates/artifacts.html +133 -0
- package/src/dashboard/templates/backups.html +136 -0
- package/src/dashboard/templates/base.html +413 -0
- package/src/dashboard/templates/calendar.html +523 -652
- package/src/dashboard/templates/chat.html +356 -0
- package/src/dashboard/templates/claims.html +259 -0
- package/src/dashboard/templates/cortex.html +262 -0
- package/src/dashboard/templates/credentials.html +128 -0
- package/src/dashboard/templates/crons.html +370 -0
- package/src/dashboard/templates/dashboard.html +384 -572
- package/src/dashboard/templates/dreams.html +252 -0
- package/src/dashboard/templates/email.html +160 -0
- package/src/dashboard/templates/evolution.html +189 -0
- package/src/dashboard/templates/feed.html +249 -0
- package/src/dashboard/templates/followup_health.html +170 -0
- package/src/dashboard/templates/graph.html +191 -269
- package/src/dashboard/templates/guard.html +259 -0
- package/src/dashboard/templates/inbox.html +220 -336
- package/src/dashboard/templates/memory.html +317 -197
- package/src/dashboard/templates/operations.html +498 -652
- package/src/dashboard/templates/plugins.html +185 -0
- package/src/dashboard/templates/rules.html +246 -0
- package/src/dashboard/templates/sentiment.html +247 -0
- package/src/dashboard/templates/sessions.html +215 -171
- package/src/dashboard/templates/skills.html +329 -0
- package/src/dashboard/templates/somatic.html +68 -172
- package/src/dashboard/templates/triggers.html +133 -0
- package/src/dashboard/templates/trust.html +360 -0
- package/src/db/__init__.py +5 -0
- package/src/db/_schema.py +25 -1
- package/src/db/_sessions.py +22 -0
- package/src/db/_skills.py +983 -252
- package/src/doctor/__init__.py +1 -0
- package/src/doctor/formatters.py +52 -0
- package/src/doctor/models.py +44 -0
- package/src/doctor/orchestrator.py +42 -0
- package/src/doctor/providers/__init__.py +1 -0
- package/src/doctor/providers/boot.py +206 -0
- package/src/doctor/providers/deep.py +292 -0
- package/src/doctor/providers/runtime.py +686 -0
- package/src/hooks/capture-tool-logs.sh +18 -4
- package/src/hooks/post-compact.sh +5 -1
- package/src/hooks/pre-compact.sh +1 -1
- package/src/plugin_loader.py +14 -0
- package/src/plugins/doctor.py +36 -0
- package/src/plugins/evolution.py +2 -1
- package/src/plugins/skills.py +135 -175
- package/src/requirements.txt +1 -0
- package/src/script_registry.py +322 -0
- package/src/scripts/deep-sleep/apply_findings.py +63 -33
- package/src/scripts/deep-sleep/collect.py +38 -9
- package/src/scripts/deep-sleep/extract-prompt.md +14 -0
- package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
- package/src/scripts/deep-sleep/synthesize.py +37 -1
- package/src/scripts/nexo-dashboard.sh +29 -0
- package/src/scripts/nexo-day-orchestrator.sh +139 -0
- package/src/scripts/nexo-evolution-run.py +2 -1
- package/src/scripts/nexo-learning-housekeep.py +1 -1
- package/src/scripts/nexo-watchdog.sh +1 -1
- package/src/server.py +9 -5
- package/src/skills/run-runtime-doctor/guide.md +12 -0
- package/src/skills/run-runtime-doctor/script.py +21 -0
- package/src/skills/run-runtime-doctor/skill.json +25 -0
- package/src/skills_runtime.py +347 -0
- package/src/tools_menu.py +3 -2
- package/src/tools_sessions.py +126 -0
- package/src/user_context.py +46 -0
- package/templates/nexo_helper.py +45 -0
- package/templates/script-template.py +44 -0
- package/templates/skill-script-template.py +39 -0
- package/templates/skill-template.md +33 -0
|
@@ -1,367 +1,251 @@
|
|
|
1
|
-
|
|
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 — Inbox</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
|
-
</head>
|
|
12
|
-
<body class="bg-gray-950 text-slate-200">
|
|
13
|
-
<div class="flex min-h-screen">
|
|
1
|
+
{% extends "base.html" %}
|
|
14
2
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<!-- Logo -->
|
|
18
|
-
<a href="/" class="mb-6 flex items-center justify-center w-8 h-8">
|
|
19
|
-
<img src="/static/nexo-logo.png" alt="NEXO" class="w-7 h-7" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
|
|
20
|
-
<div class="w-7 h-7 rounded-lg bg-violet-600 items-center justify-center hidden">
|
|
21
|
-
<span class="text-white font-display font-bold text-xs">N</span>
|
|
22
|
-
</div>
|
|
23
|
-
</a>
|
|
3
|
+
{% block title %}Inbox{% endblock %}
|
|
4
|
+
{% block page_title %}Inbox{% endblock %}
|
|
24
5
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<a href="/" title="Dashboard" class="w-9 h-9 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors">
|
|
29
|
-
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
30
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
|
31
|
-
</svg>
|
|
32
|
-
</a>
|
|
33
|
-
<!-- Operations -->
|
|
34
|
-
<a href="/ops" title="Operations" class="w-9 h-9 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors">
|
|
35
|
-
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
36
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
|
37
|
-
</svg>
|
|
38
|
-
</a>
|
|
39
|
-
<!-- Calendar -->
|
|
40
|
-
<a href="/calendar" title="Calendar" class="w-9 h-9 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors">
|
|
41
|
-
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
42
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
43
|
-
</svg>
|
|
44
|
-
</a>
|
|
45
|
-
<!-- Inbox — ACTIVE -->
|
|
46
|
-
<a href="/inbox" title="Inbox" class="w-9 h-9 rounded-lg flex items-center justify-center bg-violet-500/10 text-violet-400 transition-colors relative">
|
|
47
|
-
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
48
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
49
|
-
</svg>
|
|
50
|
-
<span id="inbox-badge" class="absolute -top-0.5 -right-0.5 w-4 h-4 bg-pink-500 rounded-full text-white text-xs font-bold flex items-center justify-center hidden"></span>
|
|
51
|
-
</a>
|
|
6
|
+
{% block header_actions %}
|
|
7
|
+
<button onclick="markAllRead()" class="text-xs px-2.5 py-1 rounded-md bg-slate-800 text-slate-400 hover:bg-slate-700 transition-colors">Mark all read</button>
|
|
8
|
+
{% endblock %}
|
|
52
9
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
</a>
|
|
61
|
-
<!-- Graph -->
|
|
62
|
-
<a href="/graph" title="Graph" class="w-9 h-9 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors">
|
|
63
|
-
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
64
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" />
|
|
65
|
-
</svg>
|
|
66
|
-
</a>
|
|
67
|
-
<!-- Sessions -->
|
|
68
|
-
<a href="/sessions" title="Sessions" class="w-9 h-9 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors">
|
|
69
|
-
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
70
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
71
|
-
</svg>
|
|
72
|
-
</a>
|
|
73
|
-
<!-- Somatic -->
|
|
74
|
-
<a href="/somatic" title="Somatic" class="w-9 h-9 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors">
|
|
75
|
-
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
76
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
|
77
|
-
</svg>
|
|
78
|
-
</a>
|
|
79
|
-
<!-- Adaptive -->
|
|
80
|
-
<a href="/adaptive" title="Adaptive" class="w-9 h-9 rounded-lg flex items-center justify-center text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors">
|
|
81
|
-
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
|
82
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
|
83
|
-
</svg>
|
|
84
|
-
</a>
|
|
85
|
-
</nav>
|
|
86
|
-
|
|
87
|
-
<!-- Trust score at bottom -->
|
|
88
|
-
<div class="mt-auto flex flex-col items-center gap-0.5">
|
|
89
|
-
<span class="text-xs uppercase tracking-widest text-slate-500 font-mono">trust</span>
|
|
90
|
-
<span id="trust-score" class="text-xs font-mono font-medium text-violet-400">--</span>
|
|
91
|
-
</div>
|
|
92
|
-
</aside>
|
|
93
|
-
|
|
94
|
-
<!-- Main: offset by sidebar width -->
|
|
95
|
-
<main class="ml-14 min-h-screen bg-gray-950 text-slate-200 flex flex-col w-full">
|
|
96
|
-
<header class="h-12 border-b border-slate-800/50 flex items-center justify-between px-6 flex-shrink-0">
|
|
97
|
-
<h1 class="text-sm font-display font-semibold">Inbox</h1>
|
|
98
|
-
<button onclick="markAllRead()" class="text-xs px-2.5 py-1 rounded-md bg-slate-800 text-slate-400 hover:bg-slate-700 transition-colors">Mark all read</button>
|
|
99
|
-
</header>
|
|
100
|
-
|
|
101
|
-
<div id="chat-area" class="flex-1 overflow-y-auto p-6 space-y-3">
|
|
102
|
-
<div class="flex items-center justify-center h-full">
|
|
103
|
-
<p class="text-slate-500 text-sm">Loading messages...</p>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
<!-- Reply indicator -->
|
|
108
|
-
<div id="reply-bar" class="hidden border-t border-violet-500/30 bg-violet-500/5 px-6 py-2 flex-shrink-0">
|
|
109
|
-
<div class="flex items-center gap-2 max-w-3xl mx-auto">
|
|
110
|
-
<div class="w-0.5 h-8 bg-violet-500 rounded-full flex-shrink-0"></div>
|
|
111
|
-
<div class="flex-1 min-w-0">
|
|
112
|
-
<span class="text-xs text-violet-400 font-display font-medium" id="reply-author"></span>
|
|
113
|
-
<p class="text-xs text-slate-400 truncate" id="reply-preview"></p>
|
|
114
|
-
</div>
|
|
115
|
-
<button onclick="cancelReply()" class="text-slate-500 hover:text-slate-300 text-lg leading-none flex-shrink-0">×</button>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
10
|
+
{% block content %}
|
|
11
|
+
<div class="flex flex-col" style="height: calc(100vh - 8.5rem);">
|
|
12
|
+
<div id="chat-area" class="flex-1 overflow-y-auto space-y-3">
|
|
13
|
+
<div class="flex items-center justify-center h-full">
|
|
14
|
+
<p class="text-slate-500 text-sm">Loading messages...</p>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
118
17
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
18
|
+
<!-- Reply indicator -->
|
|
19
|
+
<div id="reply-bar" class="hidden border-t border-violet-500/30 bg-violet-500/5 px-6 py-2 flex-shrink-0">
|
|
20
|
+
<div class="flex items-center gap-2 max-w-3xl mx-auto">
|
|
21
|
+
<div class="w-0.5 h-8 bg-violet-500 rounded-full flex-shrink-0"></div>
|
|
22
|
+
<div class="flex-1 min-w-0">
|
|
23
|
+
<span class="text-xs text-violet-400 font-display font-medium" id="reply-author"></span>
|
|
24
|
+
<p class="text-xs text-slate-400 truncate" id="reply-preview"></p>
|
|
126
25
|
</div>
|
|
127
|
-
|
|
26
|
+
<button onclick="cancelReply()" class="text-slate-500 hover:text-slate-300 text-lg leading-none flex-shrink-0">×</button>
|
|
27
|
+
</div>
|
|
128
28
|
</div>
|
|
129
29
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return `
|
|
181
|
-
<div class="flex justify-end" data-id="${msg.id}" id="msg-${msg.id}">
|
|
182
|
-
<div class="max-w-lg bg-violet-600/20 border border-violet-500/20 rounded-lg px-4 py-3">
|
|
183
|
-
${quoteHtml}
|
|
184
|
-
<div class="text-xs text-slate-500 mb-1 font-display">User</div>
|
|
185
|
-
<div class="text-sm text-slate-200 leading-relaxed">${escapeHtml(msg.content).replace(/\n/g, '<br>')}</div>
|
|
186
|
-
<div class="flex items-center justify-end mt-1">
|
|
187
|
-
<span class="text-xs text-slate-500 font-mono">${time}</span>
|
|
188
|
-
${replyBtn}
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
</div>`;
|
|
192
|
-
} else {
|
|
193
|
-
return `
|
|
194
|
-
<div class="flex justify-start" data-id="${msg.id}" id="msg-${msg.id}">
|
|
195
|
-
<div class="max-w-lg bg-slate-800 border border-slate-700 rounded-lg px-4 py-3">
|
|
196
|
-
${quoteHtml}
|
|
197
|
-
<div class="text-xs text-pink-400 mb-1 font-display font-medium">NEXO${unreadDot}</div>
|
|
198
|
-
<div class="text-sm text-slate-200 leading-relaxed">${escapeHtml(msg.content).replace(/\n/g, '<br>')}</div>
|
|
199
|
-
<div class="flex items-center mt-1">
|
|
200
|
-
<span class="text-xs text-slate-500 font-mono">${time}</span>
|
|
201
|
-
${replyBtn}
|
|
202
|
-
</div>
|
|
30
|
+
<div class="border-t border-slate-800/50 pt-4 flex-shrink-0">
|
|
31
|
+
<div class="flex gap-3 max-w-3xl mx-auto">
|
|
32
|
+
<textarea id="inbox-msg" placeholder="Leave a note for NEXO..." rows="2"
|
|
33
|
+
class="flex-1 bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-violet-500 resize-none"
|
|
34
|
+
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendNote()}"></textarea>
|
|
35
|
+
<button onclick="sendNote()" class="px-4 py-2 bg-violet-600 text-white rounded-lg text-sm hover:bg-violet-500 transition-colors self-end font-display font-medium">Send</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
{% endblock %}
|
|
40
|
+
|
|
41
|
+
{% block scripts %}
|
|
42
|
+
<script>
|
|
43
|
+
let messages = [];
|
|
44
|
+
let pollTimer = null;
|
|
45
|
+
let replyingTo = null;
|
|
46
|
+
|
|
47
|
+
function inboxEscapeHtml(str) {
|
|
48
|
+
return String(str)
|
|
49
|
+
.replace(/&/g, '&')
|
|
50
|
+
.replace(/</g, '<')
|
|
51
|
+
.replace(/>/g, '>')
|
|
52
|
+
.replace(/"/g, '"');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function authorName(direction) {
|
|
56
|
+
return direction === 'to_nexo' ? 'User' : 'NEXO';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderMessage(msg) {
|
|
60
|
+
const isFromUser = msg.direction === 'to_nexo';
|
|
61
|
+
const time = new Date(msg.created_at).toLocaleString('es-ES', {
|
|
62
|
+
day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit'
|
|
63
|
+
});
|
|
64
|
+
const unreadDot = (!msg.read && !isFromUser)
|
|
65
|
+
? '<span class="inline-block w-1.5 h-1.5 rounded-full bg-violet-400 ml-1.5 align-middle"></span>'
|
|
66
|
+
: '';
|
|
67
|
+
|
|
68
|
+
let quoteHtml = '';
|
|
69
|
+
if (msg.reply_to) {
|
|
70
|
+
const parent = messages.find(m => m.id === msg.reply_to);
|
|
71
|
+
if (parent) {
|
|
72
|
+
const parentAuthor = authorName(parent.direction);
|
|
73
|
+
const parentPreview = inboxEscapeHtml((parent.content || '').slice(0, 120));
|
|
74
|
+
quoteHtml = `
|
|
75
|
+
<div class="flex items-start gap-2 mb-2 cursor-pointer opacity-70 hover:opacity-100 transition-opacity" onclick="scrollToMsg(${parent.id})">
|
|
76
|
+
<div class="w-0.5 self-stretch bg-violet-500/50 rounded-full flex-shrink-0"></div>
|
|
77
|
+
<div class="min-w-0">
|
|
78
|
+
<span class="text-xs font-display font-medium ${parent.direction === 'to_nexo' ? 'text-slate-400' : 'text-pink-400/70'}">${parentAuthor}</span>
|
|
79
|
+
<p class="text-xs text-slate-500 truncate">${parentPreview}</p>
|
|
203
80
|
</div>
|
|
204
81
|
</div>`;
|
|
205
82
|
}
|
|
206
83
|
}
|
|
207
84
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
85
|
+
const replyBtn = `<button onclick="startReply(${msg.id})" class="text-slate-600 hover:text-violet-400 transition-colors text-xs font-mono ml-2" title="Reply">↩</button>`;
|
|
86
|
+
|
|
87
|
+
if (isFromUser) {
|
|
88
|
+
return `
|
|
89
|
+
<div class="flex justify-end" data-id="${msg.id}" id="msg-${msg.id}">
|
|
90
|
+
<div class="max-w-lg bg-violet-600/20 border border-violet-500/20 rounded-lg px-4 py-3">
|
|
91
|
+
${quoteHtml}
|
|
92
|
+
<div class="text-xs text-slate-500 mb-1 font-display">User</div>
|
|
93
|
+
<div class="text-sm text-slate-200 leading-relaxed">${inboxEscapeHtml(msg.content).replace(/\n/g, '<br>')}</div>
|
|
94
|
+
<div class="flex items-center justify-end mt-1">
|
|
95
|
+
<span class="text-xs text-slate-500 font-mono">${time}</span>
|
|
96
|
+
${replyBtn}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>`;
|
|
100
|
+
} else {
|
|
101
|
+
return `
|
|
102
|
+
<div class="flex justify-start" data-id="${msg.id}" id="msg-${msg.id}">
|
|
103
|
+
<div class="max-w-lg bg-slate-800 border border-slate-700 rounded-lg px-4 py-3">
|
|
104
|
+
${quoteHtml}
|
|
105
|
+
<div class="text-xs text-pink-400 mb-1 font-display font-medium">NEXO${unreadDot}</div>
|
|
106
|
+
<div class="text-sm text-slate-200 leading-relaxed">${inboxEscapeHtml(msg.content).replace(/\n/g, '<br>')}</div>
|
|
107
|
+
<div class="flex items-center mt-1">
|
|
108
|
+
<span class="text-xs text-slate-500 font-mono">${time}</span>
|
|
109
|
+
${replyBtn}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>`;
|
|
216
113
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function startReply(msgId) {
|
|
117
|
+
const msg = messages.find(m => m.id === msgId);
|
|
118
|
+
if (!msg) return;
|
|
119
|
+
replyingTo = msg;
|
|
120
|
+
document.getElementById('reply-bar').classList.remove('hidden');
|
|
121
|
+
document.getElementById('reply-author').textContent = authorName(msg.direction);
|
|
122
|
+
document.getElementById('reply-preview').textContent = (msg.content || '').slice(0, 150);
|
|
123
|
+
document.getElementById('inbox-msg').focus();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cancelReply() {
|
|
127
|
+
replyingTo = null;
|
|
128
|
+
document.getElementById('reply-bar').classList.add('hidden');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function scrollToMsg(msgId) {
|
|
132
|
+
const el = document.getElementById('msg-' + msgId);
|
|
133
|
+
if (el) {
|
|
134
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
135
|
+
el.firstElementChild.classList.add('ring-1', 'ring-violet-500/50');
|
|
136
|
+
setTimeout(() => el.firstElementChild.classList.remove('ring-1', 'ring-violet-500/50'), 2000);
|
|
221
137
|
}
|
|
138
|
+
}
|
|
222
139
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
140
|
+
function renderMessages() {
|
|
141
|
+
const area = document.getElementById('chat-area');
|
|
142
|
+
if (messages.length === 0) {
|
|
143
|
+
area.innerHTML = `
|
|
144
|
+
<div class="flex items-center justify-center h-full">
|
|
145
|
+
<p class="text-slate-500 text-sm">No messages yet. Leave a note for NEXO.</p>
|
|
146
|
+
</div>`;
|
|
147
|
+
return;
|
|
230
148
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
149
|
+
area.innerHTML = `<div class="space-y-3">${messages.map(renderMessage).join('')}</div>`;
|
|
150
|
+
area.scrollTop = area.scrollHeight;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function loadMessages() {
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch('/api/inbox?limit=200');
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
let detail = `HTTP ${res.status}`;
|
|
158
|
+
try { const b = await res.json(); detail = b.error || b.detail || detail; } catch {}
|
|
159
|
+
throw new Error(detail);
|
|
240
160
|
}
|
|
241
|
-
|
|
242
|
-
|
|
161
|
+
const data = await res.json();
|
|
162
|
+
messages = data.notes || [];
|
|
163
|
+
messages.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
|
164
|
+
renderMessages();
|
|
165
|
+
markIncomingRead();
|
|
166
|
+
} catch (e) {
|
|
167
|
+
document.getElementById('chat-area').innerHTML =
|
|
168
|
+
`<div class="flex items-center justify-center h-full"><p class="text-red-400 text-sm">Error loading messages: ${inboxEscapeHtml(e.message)}</p></div>`;
|
|
243
169
|
}
|
|
170
|
+
}
|
|
244
171
|
|
|
245
|
-
|
|
172
|
+
async function markIncomingRead() {
|
|
173
|
+
const unread = messages.filter(m => m.direction === 'to_user' && !m.read);
|
|
174
|
+
for (const msg of unread) {
|
|
246
175
|
try {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
messages.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
|
251
|
-
renderMessages();
|
|
252
|
-
markIncomingRead();
|
|
253
|
-
updateBadge();
|
|
254
|
-
} catch (e) {
|
|
255
|
-
document.getElementById('chat-area').innerHTML =
|
|
256
|
-
`<div class="flex items-center justify-center h-full"><p class="text-red-400 text-sm">Error loading messages: ${escapeHtml(e.message)}</p></div>`;
|
|
257
|
-
}
|
|
176
|
+
await fetch(`/api/inbox/${msg.id}/read`, { method: 'PUT' });
|
|
177
|
+
msg.read = true;
|
|
178
|
+
} catch (e) { /* ignore */ }
|
|
258
179
|
}
|
|
180
|
+
}
|
|
259
181
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
await fetch(`/api/inbox/${msg.id}/read`, { method: 'PUT' });
|
|
265
|
-
msg.read = true;
|
|
266
|
-
} catch (e) { /* ignore */ }
|
|
267
|
-
}
|
|
268
|
-
}
|
|
182
|
+
async function sendNote() {
|
|
183
|
+
const textarea = document.getElementById('inbox-msg');
|
|
184
|
+
const content = textarea.value.trim();
|
|
185
|
+
if (!content) return;
|
|
269
186
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const content = textarea.value.trim();
|
|
273
|
-
if (!content) return;
|
|
274
|
-
|
|
275
|
-
const body = { direction: 'to_nexo', content };
|
|
276
|
-
if (replyingTo) body.reply_to = replyingTo.id;
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
const res = await fetch('/api/inbox', {
|
|
280
|
-
method: 'POST',
|
|
281
|
-
headers: { 'Content-Type': 'application/json' },
|
|
282
|
-
body: JSON.stringify(body)
|
|
283
|
-
});
|
|
284
|
-
const data = await res.json();
|
|
285
|
-
textarea.value = '';
|
|
286
|
-
cancelReply();
|
|
287
|
-
messages.push({
|
|
288
|
-
id: data.note?.id || Date.now(),
|
|
289
|
-
direction: 'to_nexo',
|
|
290
|
-
content,
|
|
291
|
-
reply_to: body.reply_to || null,
|
|
292
|
-
created_at: data.note?.created_at || new Date().toISOString(),
|
|
293
|
-
read: true
|
|
294
|
-
});
|
|
295
|
-
renderMessages();
|
|
296
|
-
showToast('Note sent');
|
|
297
|
-
} catch (e) {
|
|
298
|
-
showToast('Error: ' + e.message);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
187
|
+
const body = { direction: 'to_nexo', content };
|
|
188
|
+
if (replyingTo) body.reply_to = replyingTo.id;
|
|
301
189
|
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
190
|
+
try {
|
|
191
|
+
const res = await fetch('/api/inbox', {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: { 'Content-Type': 'application/json' },
|
|
194
|
+
body: JSON.stringify(body)
|
|
195
|
+
});
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
let detail = `HTTP ${res.status}`;
|
|
198
|
+
try { const b = await res.json(); detail = b.error || b.detail || detail; } catch {}
|
|
199
|
+
throw new Error(detail);
|
|
309
200
|
}
|
|
201
|
+
const data = await res.json();
|
|
202
|
+
textarea.value = '';
|
|
203
|
+
cancelReply();
|
|
204
|
+
messages.push({
|
|
205
|
+
id: data.note?.id || Date.now(),
|
|
206
|
+
direction: 'to_nexo',
|
|
207
|
+
content,
|
|
208
|
+
reply_to: body.reply_to || null,
|
|
209
|
+
created_at: data.note?.created_at || new Date().toISOString(),
|
|
210
|
+
read: true
|
|
211
|
+
});
|
|
310
212
|
renderMessages();
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
function updateBadge() {
|
|
316
|
-
const unreadCount = messages.filter(m => m.direction === 'to_user' && !m.read).length;
|
|
317
|
-
const badge = document.getElementById('inbox-badge');
|
|
318
|
-
if (unreadCount > 0) {
|
|
319
|
-
badge.textContent = unreadCount > 9 ? '9+' : unreadCount;
|
|
320
|
-
badge.classList.remove('hidden');
|
|
321
|
-
} else {
|
|
322
|
-
badge.classList.add('hidden');
|
|
323
|
-
}
|
|
213
|
+
showToast('Note sent');
|
|
214
|
+
} catch (e) {
|
|
215
|
+
showToast('Error: ' + e.message);
|
|
324
216
|
}
|
|
217
|
+
}
|
|
325
218
|
|
|
326
|
-
|
|
219
|
+
async function markAllRead() {
|
|
220
|
+
const unread = messages.filter(m => !m.read);
|
|
221
|
+
for (const msg of unread) {
|
|
327
222
|
try {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
newMessages.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
|
332
|
-
const prevIds = messages.map(m => m.id).join(',');
|
|
333
|
-
const newIds = newMessages.map(m => m.id).join(',');
|
|
334
|
-
if (prevIds !== newIds || newMessages.length !== messages.length) {
|
|
335
|
-
messages = newMessages;
|
|
336
|
-
renderMessages();
|
|
337
|
-
markIncomingRead();
|
|
338
|
-
updateBadge();
|
|
339
|
-
}
|
|
340
|
-
} catch (e) { /* silent */ }
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function showToast(text) {
|
|
344
|
-
const toast = document.getElementById('toast');
|
|
345
|
-
toast.textContent = text;
|
|
346
|
-
toast.classList.remove('opacity-0', 'translate-y-2');
|
|
347
|
-
toast.classList.add('opacity-100', 'translate-y-0');
|
|
348
|
-
setTimeout(() => {
|
|
349
|
-
toast.classList.remove('opacity-100', 'translate-y-0');
|
|
350
|
-
toast.classList.add('opacity-0', 'translate-y-2');
|
|
351
|
-
}, 2500);
|
|
223
|
+
await fetch(`/api/inbox/${msg.id}/read`, { method: 'PUT' });
|
|
224
|
+
msg.read = true;
|
|
225
|
+
} catch (e) { /* ignore */ }
|
|
352
226
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
227
|
+
renderMessages();
|
|
228
|
+
showToast('All messages marked as read');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function pollNewMessages() {
|
|
232
|
+
try {
|
|
233
|
+
const res = await fetch('/api/inbox?limit=200');
|
|
234
|
+
const data = await res.json();
|
|
235
|
+
const newMessages = data.notes || [];
|
|
236
|
+
newMessages.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
|
237
|
+
const prevIds = messages.map(m => m.id).join(',');
|
|
238
|
+
const newIds = newMessages.map(m => m.id).join(',');
|
|
239
|
+
if (prevIds !== newIds || newMessages.length !== messages.length) {
|
|
240
|
+
messages = newMessages;
|
|
241
|
+
renderMessages();
|
|
242
|
+
markIncomingRead();
|
|
359
243
|
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
</
|
|
367
|
-
|
|
244
|
+
} catch (e) { /* silent */ }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Initial load + polling
|
|
248
|
+
loadMessages();
|
|
249
|
+
pollTimer = setInterval(pollNewMessages, 10000);
|
|
250
|
+
</script>
|
|
251
|
+
{% endblock %}
|