@mooncompany/uplink-chat 0.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.
Potentially problematic release.
This version of @mooncompany/uplink-chat might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/bin/uplink.js +279 -0
- package/middleware/error-handler.js +69 -0
- package/package.json +93 -0
- package/public/css/agents.36b98c0f.css +1469 -0
- package/public/css/agents.css +1469 -0
- package/public/css/app.a6a7f8f5.css +2731 -0
- package/public/css/app.css +2731 -0
- package/public/css/artifacts.css +444 -0
- package/public/css/commands.css +55 -0
- package/public/css/connection.css +131 -0
- package/public/css/dashboard.css +233 -0
- package/public/css/developer.css +328 -0
- package/public/css/files.css +123 -0
- package/public/css/markdown.css +156 -0
- package/public/css/message-actions.css +278 -0
- package/public/css/mobile.css +614 -0
- package/public/css/panels-unified.css +483 -0
- package/public/css/premium.css +415 -0
- package/public/css/realtime.css +189 -0
- package/public/css/satellites.css +401 -0
- package/public/css/shortcuts.css +185 -0
- package/public/css/split-view.4def0262.css +673 -0
- package/public/css/split-view.css +673 -0
- package/public/css/theme-generator.css +391 -0
- package/public/css/themes.css +387 -0
- package/public/css/timestamps.css +54 -0
- package/public/css/variables.css +78 -0
- package/public/dist/bundle.b55050c4.js +15757 -0
- package/public/favicon.svg +24 -0
- package/public/img/agents/ada.png +0 -0
- package/public/img/agents/clarice.png +0 -0
- package/public/img/agents/dennis-nedry.png +0 -0
- package/public/img/agents/elliot-alderson.png +0 -0
- package/public/img/agents/main.png +0 -0
- package/public/img/agents/scotty.png +0 -0
- package/public/img/agents/top-flight-security.png +0 -0
- package/public/index.html +1083 -0
- package/public/js/agents-data.js +234 -0
- package/public/js/agents-ui.js +72 -0
- package/public/js/agents.js +1525 -0
- package/public/js/app.js +79 -0
- package/public/js/appearance-settings.js +111 -0
- package/public/js/artifacts.js +432 -0
- package/public/js/audio-queue.js +168 -0
- package/public/js/bootstrap.js +54 -0
- package/public/js/chat.js +1211 -0
- package/public/js/commands.js +581 -0
- package/public/js/connection-api.js +121 -0
- package/public/js/connection.js +1231 -0
- package/public/js/context-tracker.js +271 -0
- package/public/js/core.js +172 -0
- package/public/js/dashboard.js +452 -0
- package/public/js/developer.js +432 -0
- package/public/js/encryption.js +124 -0
- package/public/js/errors.js +122 -0
- package/public/js/event-bus.js +77 -0
- package/public/js/fetch-utils.js +171 -0
- package/public/js/file-handler.js +229 -0
- package/public/js/files.js +352 -0
- package/public/js/gateway-chat.js +538 -0
- package/public/js/logger.js +112 -0
- package/public/js/markdown.js +190 -0
- package/public/js/message-actions.js +431 -0
- package/public/js/message-renderer.js +288 -0
- package/public/js/missed-messages.js +235 -0
- package/public/js/mobile-debug.js +95 -0
- package/public/js/notifications.js +367 -0
- package/public/js/offline-queue.js +178 -0
- package/public/js/onboarding.js +543 -0
- package/public/js/panels.js +156 -0
- package/public/js/premium.js +412 -0
- package/public/js/realtime-voice.js +844 -0
- package/public/js/satellite-sync.js +256 -0
- package/public/js/satellite-ui.js +175 -0
- package/public/js/satellites.js +1516 -0
- package/public/js/settings.js +1087 -0
- package/public/js/shortcuts.js +381 -0
- package/public/js/split-chat.js +1234 -0
- package/public/js/split-resize.js +211 -0
- package/public/js/splitview.js +340 -0
- package/public/js/storage.js +408 -0
- package/public/js/streaming-handler.js +324 -0
- package/public/js/stt-settings.js +316 -0
- package/public/js/theme-generator.js +661 -0
- package/public/js/themes.js +164 -0
- package/public/js/timestamps.js +198 -0
- package/public/js/tts-settings.js +575 -0
- package/public/js/ui.js +267 -0
- package/public/js/update-notifier.js +143 -0
- package/public/js/utils/constants.js +165 -0
- package/public/js/utils/sanitize.js +93 -0
- package/public/js/utils/sse-parser.js +195 -0
- package/public/js/voice.js +883 -0
- package/public/manifest.json +58 -0
- package/public/moon_texture.jpg +0 -0
- package/public/sw.js +221 -0
- package/public/three.min.js +6 -0
- package/server/channel.js +529 -0
- package/server/chat.js +270 -0
- package/server/config-store.js +362 -0
- package/server/config.js +159 -0
- package/server/context.js +131 -0
- package/server/gateway-commands.js +211 -0
- package/server/gateway-proxy.js +318 -0
- package/server/index.js +22 -0
- package/server/logger.js +89 -0
- package/server/middleware/auth.js +188 -0
- package/server/middleware.js +218 -0
- package/server/openclaw-discover.js +308 -0
- package/server/premium/index.js +156 -0
- package/server/premium/license.js +140 -0
- package/server/realtime/bridge.js +837 -0
- package/server/realtime/index.js +349 -0
- package/server/realtime/tts-stream.js +446 -0
- package/server/routes/agents.js +564 -0
- package/server/routes/artifacts.js +174 -0
- package/server/routes/chat.js +311 -0
- package/server/routes/config-settings.js +345 -0
- package/server/routes/config.js +603 -0
- package/server/routes/files.js +307 -0
- package/server/routes/index.js +18 -0
- package/server/routes/media.js +451 -0
- package/server/routes/missed-messages.js +107 -0
- package/server/routes/premium.js +75 -0
- package/server/routes/push.js +156 -0
- package/server/routes/satellite.js +406 -0
- package/server/routes/status.js +251 -0
- package/server/routes/stt.js +35 -0
- package/server/routes/voice.js +260 -0
- package/server/routes/webhooks.js +203 -0
- package/server/routes.js +206 -0
- package/server/runtime-config.js +336 -0
- package/server/share.js +305 -0
- package/server/stt/faster-whisper.js +72 -0
- package/server/stt/groq.js +51 -0
- package/server/stt/index.js +196 -0
- package/server/stt/openai.js +49 -0
- package/server/sync.js +244 -0
- package/server/tailscale-https.js +175 -0
- package/server/tts.js +646 -0
- package/server/update-checker.js +172 -0
- package/server/utils/filename.js +129 -0
- package/server/utils.js +147 -0
- package/server/watchdog.js +318 -0
- package/server/websocket/broadcast.js +359 -0
- package/server/websocket/connections.js +339 -0
- package/server/websocket/index.js +215 -0
- package/server/websocket/routing.js +277 -0
- package/server/websocket/sync.js +102 -0
- package/server.js +404 -0
- package/utils/detect-tool-usage.js +93 -0
- package/utils/errors.js +158 -0
- package/utils/html-escape.js +84 -0
- package/utils/id-sanitize.js +94 -0
- package/utils/response.js +130 -0
- package/utils/with-retry.js +105 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// MESSAGE RENDERER MODULE
|
|
3
|
+
// Shared message rendering, avatars, and formatting
|
|
4
|
+
// Used by both chat.js and split-chat.js
|
|
5
|
+
// ============================================
|
|
6
|
+
|
|
7
|
+
import { UplinkMarkdown } from './markdown.js';
|
|
8
|
+
|
|
9
|
+
// ============================================
|
|
10
|
+
// AVATAR SYSTEM
|
|
11
|
+
// ============================================
|
|
12
|
+
|
|
13
|
+
// Cache for avatar availability checks (agentId -> boolean|undefined)
|
|
14
|
+
const avatarCache = {};
|
|
15
|
+
// Session-stable cache buster (changes on each page load, not per message)
|
|
16
|
+
const avatarCacheBust = Date.now();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build an agent avatar element for the given agent ID.
|
|
20
|
+
*/
|
|
21
|
+
function buildAgentAvatar(agentId) {
|
|
22
|
+
// Resolve agentId from current satellite if not provided
|
|
23
|
+
if (!agentId) {
|
|
24
|
+
const currentSat = window.UplinkSatellites?.getCurrentId?.() || 'main';
|
|
25
|
+
const satellites = window.UplinkSatellites?.getSatellites?.() || {};
|
|
26
|
+
const satellite = satellites[currentSat];
|
|
27
|
+
agentId = satellite?.agentId || 'main';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const container = document.createElement('div');
|
|
31
|
+
container.className = 'agent-avatar';
|
|
32
|
+
container.setAttribute('aria-hidden', 'true');
|
|
33
|
+
|
|
34
|
+
const avatarUrl = `/img/agents/${agentId}.png?t=${avatarCacheBust}`;
|
|
35
|
+
|
|
36
|
+
// Check cache first
|
|
37
|
+
if (avatarCache[agentId] === true) {
|
|
38
|
+
const img = document.createElement('img');
|
|
39
|
+
img.src = avatarUrl;
|
|
40
|
+
img.alt = '';
|
|
41
|
+
img.className = 'agent-avatar-img';
|
|
42
|
+
container.appendChild(img);
|
|
43
|
+
return container;
|
|
44
|
+
} else if (avatarCache[agentId] === false) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// First time — try image, fallback to null
|
|
49
|
+
const img = document.createElement('img');
|
|
50
|
+
img.src = avatarUrl;
|
|
51
|
+
img.alt = '';
|
|
52
|
+
img.className = 'agent-avatar-img';
|
|
53
|
+
|
|
54
|
+
img.onload = () => { avatarCache[agentId] = true; };
|
|
55
|
+
img.onerror = () => {
|
|
56
|
+
avatarCache[agentId] = false;
|
|
57
|
+
container.remove();
|
|
58
|
+
};
|
|
59
|
+
container.appendChild(img);
|
|
60
|
+
return container;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get agent emoji from the agents module.
|
|
65
|
+
*/
|
|
66
|
+
function getAgentEmoji(agentId) {
|
|
67
|
+
const agents = window.UplinkAgents?.getAgents?.() || [];
|
|
68
|
+
const agent = agents.find(a => a.id === agentId);
|
|
69
|
+
return agent?.identity?.emoji || '🤖';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================
|
|
73
|
+
// TEXT FORMATTING
|
|
74
|
+
// ============================================
|
|
75
|
+
|
|
76
|
+
function isValidHttpUrl(str) {
|
|
77
|
+
try {
|
|
78
|
+
const url = new URL(str);
|
|
79
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Format message text with markdown.
|
|
87
|
+
*/
|
|
88
|
+
function formatMessage(text) {
|
|
89
|
+
if (UplinkMarkdown?.render) {
|
|
90
|
+
const html = UplinkMarkdown.render(text);
|
|
91
|
+
if (UplinkMarkdown.highlightCode) {
|
|
92
|
+
const temp = document.createElement('div');
|
|
93
|
+
temp.innerHTML = html;
|
|
94
|
+
UplinkMarkdown.highlightCode(temp);
|
|
95
|
+
return temp.innerHTML;
|
|
96
|
+
}
|
|
97
|
+
return html;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Fallback basic formatting
|
|
101
|
+
if (!text) return '';
|
|
102
|
+
|
|
103
|
+
let formatted = text
|
|
104
|
+
.replace(/&/g, '&')
|
|
105
|
+
.replace(/</g, '<')
|
|
106
|
+
.replace(/>/g, '>');
|
|
107
|
+
|
|
108
|
+
formatted = formatted.replace(/```(\w*)\n?([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
|
|
109
|
+
formatted = formatted.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
110
|
+
formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
111
|
+
formatted = formatted.replace(/__(.+?)__/g, '<strong>$1</strong>');
|
|
112
|
+
formatted = formatted.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
113
|
+
formatted = formatted.replace(/_([^_]+)_/g, '<em>$1</em>');
|
|
114
|
+
formatted = formatted.replace(
|
|
115
|
+
/(https?:\/\/[^\s<]+)/g,
|
|
116
|
+
(match) => isValidHttpUrl(match)
|
|
117
|
+
? `<a href="${match}" target="_blank" rel="noopener noreferrer">${match}</a>`
|
|
118
|
+
: match
|
|
119
|
+
);
|
|
120
|
+
formatted = formatted.replace(/\n/g, '<br>');
|
|
121
|
+
|
|
122
|
+
return formatted;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Render markdown text to HTML (alias for formatMessage, used by split-chat).
|
|
127
|
+
*/
|
|
128
|
+
function renderMarkdown(text) {
|
|
129
|
+
if (UplinkMarkdown?.render) {
|
|
130
|
+
return UplinkMarkdown.render(text);
|
|
131
|
+
}
|
|
132
|
+
return formatMessage(text);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================
|
|
136
|
+
// MESSAGE RENDERING
|
|
137
|
+
// ============================================
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Add a message to a container element.
|
|
141
|
+
*/
|
|
142
|
+
function addMessageToContainer(options) {
|
|
143
|
+
const {
|
|
144
|
+
container,
|
|
145
|
+
text,
|
|
146
|
+
type,
|
|
147
|
+
imageUrl = null,
|
|
148
|
+
showAvatar = false,
|
|
149
|
+
agentId = null,
|
|
150
|
+
timestamp = null,
|
|
151
|
+
scroll = {}
|
|
152
|
+
} = options;
|
|
153
|
+
|
|
154
|
+
if (!container) return null;
|
|
155
|
+
|
|
156
|
+
const div = document.createElement('div');
|
|
157
|
+
div.className = `message ${type}`;
|
|
158
|
+
div.dataset.time = timestamp || Date.now();
|
|
159
|
+
|
|
160
|
+
if (type === 'system') {
|
|
161
|
+
div.setAttribute('role', 'alert');
|
|
162
|
+
div.setAttribute('aria-live', 'polite');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Add agent avatar
|
|
166
|
+
if (type === 'assistant' && showAvatar) {
|
|
167
|
+
const avatar = buildAgentAvatar(agentId);
|
|
168
|
+
if (avatar) {
|
|
169
|
+
div.prepend(avatar);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Image handling
|
|
174
|
+
if (imageUrl && imageUrl !== '__pending_upload__') {
|
|
175
|
+
const img = document.createElement('img');
|
|
176
|
+
img.src = imageUrl;
|
|
177
|
+
img.alt = type === 'user' ? 'Image shared by you' : 'Image from assistant';
|
|
178
|
+
img.loading = 'lazy'; // Lazy load to reduce unnecessary 404s on scroll
|
|
179
|
+
img.onerror = () => {
|
|
180
|
+
// Silently hide broken images — console 404 unavoidable but UX is clean
|
|
181
|
+
img.remove();
|
|
182
|
+
// Only show placeholder if this is a user-uploaded image (not old uploads)
|
|
183
|
+
if (imageUrl.startsWith('/uploads/') && !imageUrl.includes('upload-')) {
|
|
184
|
+
const placeholder = document.createElement('div');
|
|
185
|
+
placeholder.className = 'message-image-expired';
|
|
186
|
+
placeholder.textContent = '🖼️ Image no longer available';
|
|
187
|
+
placeholder.style.opacity = '0.5';
|
|
188
|
+
placeholder.style.fontSize = '0.85em';
|
|
189
|
+
div.insertBefore(placeholder, div.firstChild);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
div.appendChild(img);
|
|
193
|
+
} else if (imageUrl === '__pending_upload__') {
|
|
194
|
+
const placeholder = document.createElement('div');
|
|
195
|
+
placeholder.className = 'message-image-expired';
|
|
196
|
+
placeholder.textContent = '🖼️ Image (upload incomplete)';
|
|
197
|
+
div.appendChild(placeholder);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Text content
|
|
201
|
+
if (text) {
|
|
202
|
+
const textSpan = document.createElement('span');
|
|
203
|
+
textSpan.className = 'message-text';
|
|
204
|
+
textSpan.innerHTML = formatMessage(text);
|
|
205
|
+
div.appendChild(textSpan);
|
|
206
|
+
|
|
207
|
+
if (UplinkMarkdown?.highlightCode) {
|
|
208
|
+
UplinkMarkdown.highlightCode(textSpan);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
container.appendChild(div);
|
|
213
|
+
|
|
214
|
+
// Scroll management
|
|
215
|
+
const isNearBottom = scroll.isNearBottom !== undefined ? scroll.isNearBottom : true;
|
|
216
|
+
if (isNearBottom) {
|
|
217
|
+
container.scrollTop = container.scrollHeight;
|
|
218
|
+
} else if (type !== 'system' && scroll.onNewMessage) {
|
|
219
|
+
scroll.onNewMessage();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Auto-dismiss system messages after 15 seconds
|
|
223
|
+
if (type === 'system') {
|
|
224
|
+
setTimeout(() => {
|
|
225
|
+
if (div.parentNode) {
|
|
226
|
+
div.style.transition = 'opacity 0.3s, transform 0.3s';
|
|
227
|
+
div.style.opacity = '0';
|
|
228
|
+
div.style.transform = 'translateY(-10px)';
|
|
229
|
+
setTimeout(() => div.remove(), 300);
|
|
230
|
+
}
|
|
231
|
+
}, 15000);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return div;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ============================================
|
|
238
|
+
// HTML ESCAPING UTILITIES
|
|
239
|
+
// ============================================
|
|
240
|
+
|
|
241
|
+
function escapeHtml(text) {
|
|
242
|
+
const div = document.createElement('div');
|
|
243
|
+
div.textContent = text || '';
|
|
244
|
+
return div.innerHTML;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function escapeAttr(text) {
|
|
248
|
+
return (text || '')
|
|
249
|
+
.replace(/&/g, '&')
|
|
250
|
+
.replace(/"/g, '"')
|
|
251
|
+
.replace(/'/g, ''')
|
|
252
|
+
.replace(/</g, '<')
|
|
253
|
+
.replace(/>/g, '>');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ============================================
|
|
257
|
+
// PUBLIC API
|
|
258
|
+
// ============================================
|
|
259
|
+
|
|
260
|
+
export const UplinkMessageRenderer = {
|
|
261
|
+
buildAgentAvatar,
|
|
262
|
+
getAgentEmoji,
|
|
263
|
+
avatarCache,
|
|
264
|
+
avatarCacheBust,
|
|
265
|
+
formatMessage,
|
|
266
|
+
renderMarkdown,
|
|
267
|
+
isValidHttpUrl,
|
|
268
|
+
addMessageToContainer,
|
|
269
|
+
escapeHtml,
|
|
270
|
+
escapeAttr
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export {
|
|
274
|
+
buildAgentAvatar,
|
|
275
|
+
getAgentEmoji,
|
|
276
|
+
formatMessage,
|
|
277
|
+
renderMarkdown,
|
|
278
|
+
addMessageToContainer,
|
|
279
|
+
escapeHtml,
|
|
280
|
+
escapeAttr
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// Backward compat: assign to window
|
|
284
|
+
window.UplinkMessageRenderer = UplinkMessageRenderer;
|
|
285
|
+
|
|
286
|
+
if (typeof logger !== 'undefined') {
|
|
287
|
+
logger.debug('MessageRenderer: Module loaded');
|
|
288
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// MISSED MESSAGES MODULE
|
|
3
|
+
// Polls for messages when tab was closed
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
import { UplinkCore } from './core.js';
|
|
7
|
+
|
|
8
|
+
let pollingInterval = null;
|
|
9
|
+
let isPolling = false;
|
|
10
|
+
|
|
11
|
+
function init() {
|
|
12
|
+
if (window.UplinkLogger?.debug) {
|
|
13
|
+
window.UplinkLogger.debug('[Missed Messages] Initialized');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check for missed messages immediately on load
|
|
17
|
+
setTimeout(checkForMissedMessages, 1000);
|
|
18
|
+
|
|
19
|
+
// Start periodic polling every 30 seconds
|
|
20
|
+
startPolling();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function startPolling() {
|
|
24
|
+
if (pollingInterval) return; // Already polling
|
|
25
|
+
|
|
26
|
+
isPolling = true;
|
|
27
|
+
pollingInterval = setInterval(checkForMissedMessages, 30000); // 30 seconds
|
|
28
|
+
if (window.UplinkLogger?.debug) {
|
|
29
|
+
window.UplinkLogger.debug('[Missed Messages] Started polling every 30 seconds');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function stopPolling() {
|
|
34
|
+
if (pollingInterval) {
|
|
35
|
+
clearInterval(pollingInterval);
|
|
36
|
+
pollingInterval = null;
|
|
37
|
+
isPolling = false;
|
|
38
|
+
if (window.UplinkLogger?.debug) {
|
|
39
|
+
window.UplinkLogger.debug('[Missed Messages] Stopped polling');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getAuthHeaders() {
|
|
45
|
+
const settings = JSON.parse(localStorage.getItem('uplink-settings') || '{}');
|
|
46
|
+
const token = settings.gatewayToken || '';
|
|
47
|
+
const headers = {};
|
|
48
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
49
|
+
return headers;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function checkForMissedMessages() {
|
|
53
|
+
// M-27: Skip polling if offline or WebSocket disconnected
|
|
54
|
+
if (!navigator.onLine) {
|
|
55
|
+
if (window.UplinkLogger?.debug) {
|
|
56
|
+
window.UplinkLogger.debug('[Missed Messages] Skipping check - browser offline');
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Skip if WebSocket is not connected
|
|
62
|
+
if (window.UplinkConnection && !window.UplinkConnection.isConnected()) {
|
|
63
|
+
if (window.UplinkLogger?.debug) {
|
|
64
|
+
window.UplinkLogger.debug('[Missed Messages] Skipping check - WebSocket disconnected');
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch('/api/missed-messages', {
|
|
71
|
+
headers: getAuthHeaders(),
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
if (window.UplinkLogger?.warn) {
|
|
75
|
+
window.UplinkLogger.warn('[Missed Messages] Failed to fetch:', response.status);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const data = await response.json();
|
|
81
|
+
|
|
82
|
+
if (data.messages && data.messages.length > 0) {
|
|
83
|
+
if (window.UplinkLogger?.debug) {
|
|
84
|
+
window.UplinkLogger.debug(`[Missed Messages] Retrieved ${data.messages.length} missed messages`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Display missed messages
|
|
88
|
+
displayMissedMessages(data.messages);
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (window.UplinkLogger?.warn) {
|
|
92
|
+
window.UplinkLogger.warn('[Missed Messages] Error fetching:', error.message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Track displayed message hashes to prevent duplicates (H-45)
|
|
98
|
+
const displayedHashes = new Set();
|
|
99
|
+
const MAX_DISPLAYED_HASHES = 500;
|
|
100
|
+
|
|
101
|
+
function hashMessage(text) {
|
|
102
|
+
let hash = 5381;
|
|
103
|
+
for (let i = 0; i < text.length; i++) {
|
|
104
|
+
hash = ((hash << 5) + hash + text.charCodeAt(i)) | 0;
|
|
105
|
+
}
|
|
106
|
+
return hash;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function displayMissedMessages(messages) {
|
|
110
|
+
// Sort messages by timestamp
|
|
111
|
+
messages.sort((a, b) => a.timestamp - b.timestamp);
|
|
112
|
+
|
|
113
|
+
// Trim hash set if too large
|
|
114
|
+
if (displayedHashes.size > MAX_DISPLAYED_HASHES) {
|
|
115
|
+
const iter = displayedHashes.values();
|
|
116
|
+
const toRemove = displayedHashes.size - MAX_DISPLAYED_HASHES;
|
|
117
|
+
for (let i = 0; i < toRemove; i++) {
|
|
118
|
+
displayedHashes.delete(iter.next().value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let addedCount = 0;
|
|
123
|
+
for (const msg of messages) {
|
|
124
|
+
// Add each message to the chat
|
|
125
|
+
const messageText = typeof msg.message === 'string' ? msg.message : String(msg.message || '');
|
|
126
|
+
|
|
127
|
+
if (messageText) {
|
|
128
|
+
// Deduplicate: skip messages we've already displayed (H-45)
|
|
129
|
+
const hash = hashMessage(messageText);
|
|
130
|
+
if (displayedHashes.has(hash)) {
|
|
131
|
+
if (window.UplinkLogger?.debug) {
|
|
132
|
+
window.UplinkLogger.debug(`[Missed Messages] Skipping duplicate message (hash: ${hash})`);
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Also check if message is already visible in the chat DOM
|
|
138
|
+
const existingMessages = document.querySelectorAll('.message.assistant');
|
|
139
|
+
let alreadyVisible = false;
|
|
140
|
+
for (const el of existingMessages) {
|
|
141
|
+
if (el.dataset.originalText === messageText) {
|
|
142
|
+
alreadyVisible = true;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (alreadyVisible) {
|
|
147
|
+
if (window.UplinkLogger?.debug) {
|
|
148
|
+
window.UplinkLogger.debug(`[Missed Messages] Skipping already-visible message`);
|
|
149
|
+
}
|
|
150
|
+
displayedHashes.add(hash);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
// Use the chat module's addMessage if available
|
|
156
|
+
// Signature: addMessage(text, type, imageUrl, save)
|
|
157
|
+
if (window.UplinkChat && window.UplinkChat.addMessage) {
|
|
158
|
+
window.UplinkChat.addMessage(messageText, 'assistant');
|
|
159
|
+
} else if (window.addMessage) {
|
|
160
|
+
window.addMessage(messageText, 'assistant');
|
|
161
|
+
} else {
|
|
162
|
+
if (window.UplinkLogger?.warn) {
|
|
163
|
+
window.UplinkLogger.warn('[Missed Messages] No addMessage function available');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
displayedHashes.add(hash);
|
|
168
|
+
addedCount++;
|
|
169
|
+
if (window.UplinkLogger?.debug) {
|
|
170
|
+
window.UplinkLogger.debug(`[Missed Messages] Displayed missed message for satellite ${msg.satelliteId}`);
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (window.UplinkLogger?.error) {
|
|
174
|
+
window.UplinkLogger.error('[Missed Messages] Error displaying message:', err.message);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (addedCount > 0) {
|
|
181
|
+
// Show a notification that we recovered missed messages
|
|
182
|
+
showMissedMessageNotification(addedCount);
|
|
183
|
+
|
|
184
|
+
// Scroll to bottom to show new messages
|
|
185
|
+
if (window.scrollToBottom) {
|
|
186
|
+
setTimeout(window.scrollToBottom, 100);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function showMissedMessageNotification(count) {
|
|
192
|
+
// Create a temporary notification (CSP-safe — no inline styles)
|
|
193
|
+
const notification = document.createElement('div');
|
|
194
|
+
notification.className = 'missed-message-notification';
|
|
195
|
+
|
|
196
|
+
const inner = document.createElement('div');
|
|
197
|
+
inner.className = 'missed-message-banner';
|
|
198
|
+
inner.textContent = `📨 Retrieved ${count} missed message${count > 1 ? 's' : ''} from while you were away`;
|
|
199
|
+
notification.appendChild(inner);
|
|
200
|
+
|
|
201
|
+
// Insert at top of chat area
|
|
202
|
+
const chatContainer = document.getElementById('chatMessages') || document.body;
|
|
203
|
+
chatContainer.insertBefore(notification, chatContainer.firstChild);
|
|
204
|
+
|
|
205
|
+
// Remove after 4 seconds
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
if (notification.parentNode) {
|
|
208
|
+
notification.parentNode.removeChild(notification);
|
|
209
|
+
}
|
|
210
|
+
}, 4000);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check for missed messages on tab resume — but only if connected (avoid fetch errors during reconnect)
|
|
214
|
+
document.addEventListener('visibilitychange', () => {
|
|
215
|
+
if (!document.hidden && window.UplinkConnection?.isConnected?.()) {
|
|
216
|
+
if (window.UplinkLogger?.debug) {
|
|
217
|
+
window.UplinkLogger.debug('[Missed Messages] Tab became visible, checking for missed messages');
|
|
218
|
+
}
|
|
219
|
+
checkForMissedMessages();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Expose API
|
|
224
|
+
export const MissedMessages = {
|
|
225
|
+
check: checkForMissedMessages,
|
|
226
|
+
startPolling,
|
|
227
|
+
stopPolling,
|
|
228
|
+
isPolling: () => isPolling
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Backward compat: assign to window
|
|
232
|
+
window.MissedMessages = MissedMessages;
|
|
233
|
+
|
|
234
|
+
// Register and init
|
|
235
|
+
UplinkCore.registerModule('missed-messages', init);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Mobile Debug Panel
|
|
2
|
+
// Only loads on non-localhost (mobile via tunnel)
|
|
3
|
+
|
|
4
|
+
const isLocal = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
|
|
5
|
+
|
|
6
|
+
if (!isLocal) {
|
|
7
|
+
// Only activate on remote/tunnel connections
|
|
8
|
+
const logs = [];
|
|
9
|
+
|
|
10
|
+
function initDebug() {
|
|
11
|
+
const container = document.createElement('div');
|
|
12
|
+
container.id = 'mobileDebugContainer';
|
|
13
|
+
container.innerHTML = `
|
|
14
|
+
<div id="debugToggle" style="
|
|
15
|
+
position: fixed; bottom: 80px; right: 15px; width: 50px; height: 50px;
|
|
16
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
17
|
+
color: #fff; border-radius: 50%; display: flex; align-items: center;
|
|
18
|
+
justify-content: center; font-size: 22px; z-index: 99999;
|
|
19
|
+
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); cursor: pointer;
|
|
20
|
+
user-select: none; -webkit-tap-highlight-color: transparent;
|
|
21
|
+
">🔧</div>
|
|
22
|
+
<div id="debugPanel" style="
|
|
23
|
+
display: none; position: fixed; bottom: 140px; right: 10px; left: 10px;
|
|
24
|
+
max-height: 40vh; background: rgba(26, 26, 46, 0.98); color: #0f0;
|
|
25
|
+
font-family: 'SF Mono', Monaco, 'Courier New', monospace; font-size: 11px;
|
|
26
|
+
padding: 12px; border-radius: 12px; z-index: 99998; overflow-y: auto;
|
|
27
|
+
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5); border: 1px solid rgba(255,255,255,0.1);
|
|
28
|
+
word-break: break-all;
|
|
29
|
+
">
|
|
30
|
+
<div style="color:#888;margin-bottom:8px;">📱 Mobile Debug Console</div>
|
|
31
|
+
<div id="debugOutput"></div>
|
|
32
|
+
</div>
|
|
33
|
+
`;
|
|
34
|
+
document.body.appendChild(container);
|
|
35
|
+
|
|
36
|
+
const toggle = document.getElementById('debugToggle');
|
|
37
|
+
const panel = document.getElementById('debugPanel');
|
|
38
|
+
|
|
39
|
+
toggle.addEventListener('click', function() {
|
|
40
|
+
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log('🔧 Mobile debug panel ready - tap the button to view logs');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function escapeHtmlDebug(str) {
|
|
47
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function addLog(type, args) {
|
|
51
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
52
|
+
const color = type === 'ERR' ? '#ff6b6b' : type === 'WRN' ? '#ffd93d' : '#0f0';
|
|
53
|
+
const msg = escapeHtmlDebug(Array.from(args).map(function(a) {
|
|
54
|
+
if (a === null) return 'null';
|
|
55
|
+
if (a === undefined) return 'undefined';
|
|
56
|
+
if (typeof a === 'object') {
|
|
57
|
+
try { return JSON.stringify(a, null, 2); }
|
|
58
|
+
catch(e) { return String(a); }
|
|
59
|
+
}
|
|
60
|
+
return String(a);
|
|
61
|
+
}).join(' '));
|
|
62
|
+
|
|
63
|
+
logs.push({ type, msg, time: timestamp, color });
|
|
64
|
+
if (logs.length > 200) logs.shift();
|
|
65
|
+
|
|
66
|
+
const output = document.getElementById('debugOutput');
|
|
67
|
+
if (output) {
|
|
68
|
+
output.innerHTML = logs.map(function(l) {
|
|
69
|
+
return '<div style="color:' + l.color + ';margin:2px 0;"><span style="color:#666;">' + l.time + '</span> ' + l.msg + '</div>';
|
|
70
|
+
}).join('');
|
|
71
|
+
output.scrollTop = output.scrollHeight;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Override console methods
|
|
76
|
+
const origLog = console.log;
|
|
77
|
+
const origErr = console.error;
|
|
78
|
+
const origWarn = console.warn;
|
|
79
|
+
const origDebug = console.debug;
|
|
80
|
+
|
|
81
|
+
console.log = function() { addLog('LOG', arguments); origLog.apply(console, arguments); };
|
|
82
|
+
console.error = function() { addLog('ERR', arguments); origErr.apply(console, arguments); };
|
|
83
|
+
console.warn = function() { addLog('WRN', arguments); origWarn.apply(console, arguments); };
|
|
84
|
+
console.debug = function() { addLog('DBG', arguments); origDebug.apply(console, arguments); };
|
|
85
|
+
|
|
86
|
+
if (document.readyState === 'loading') {
|
|
87
|
+
document.addEventListener('DOMContentLoaded', initDebug);
|
|
88
|
+
} else {
|
|
89
|
+
initDebug();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Export empty object for module system compatibility
|
|
94
|
+
export const UplinkMobileDebug = {};
|
|
95
|
+
window.UplinkMobileDebug = UplinkMobileDebug;
|