@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
package/public/js/ui.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// UI MODULE
|
|
3
|
+
// Starfield, mode switching, common UI utilities
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
import { UplinkCore } from './core.js';
|
|
7
|
+
import { UplinkStorage } from './storage.js';
|
|
8
|
+
|
|
9
|
+
// DOM elements
|
|
10
|
+
let starsContainer;
|
|
11
|
+
let textModeTab, voiceModeTab;
|
|
12
|
+
let textInputRow, voiceInputRow;
|
|
13
|
+
let encryptionBadge;
|
|
14
|
+
|
|
15
|
+
// Session timeout
|
|
16
|
+
const SESSION_TIMEOUT = 4 * 60 * 60 * 1000; // 4 hours
|
|
17
|
+
let sessionTimeoutId = null;
|
|
18
|
+
|
|
19
|
+
// AbortController for event listeners cleanup
|
|
20
|
+
let eventsAbortController = null;
|
|
21
|
+
|
|
22
|
+
function init() {
|
|
23
|
+
// Abort previous event listeners to prevent stacking if init called multiple times
|
|
24
|
+
if (eventsAbortController) {
|
|
25
|
+
eventsAbortController.abort();
|
|
26
|
+
}
|
|
27
|
+
eventsAbortController = new AbortController();
|
|
28
|
+
|
|
29
|
+
starsContainer = document.getElementById('stars');
|
|
30
|
+
textModeTab = document.getElementById('textModeTab');
|
|
31
|
+
voiceModeTab = document.getElementById('voiceModeTab');
|
|
32
|
+
textInputRow = document.getElementById('textInputRow');
|
|
33
|
+
voiceInputRow = document.getElementById('voiceInputRow');
|
|
34
|
+
encryptionBadge = document.getElementById('encryptionBadge');
|
|
35
|
+
|
|
36
|
+
// Generate starfield
|
|
37
|
+
if (starsContainer) generateStars();
|
|
38
|
+
|
|
39
|
+
// Mode switching
|
|
40
|
+
if (textModeTab && voiceModeTab) setupModeTabs();
|
|
41
|
+
|
|
42
|
+
// Visibility change handling
|
|
43
|
+
setupVisibilityHandling();
|
|
44
|
+
|
|
45
|
+
// Session timeout
|
|
46
|
+
setupSessionTimeout();
|
|
47
|
+
|
|
48
|
+
// Audio pre-warming
|
|
49
|
+
setupAudioPrewarm();
|
|
50
|
+
|
|
51
|
+
// Listen for unlocked event
|
|
52
|
+
window.addEventListener('uplink:unlocked', updateUI, { signal: eventsAbortController.signal });
|
|
53
|
+
|
|
54
|
+
// Dynamic bottom padding for messages
|
|
55
|
+
setupPaddingObserver();
|
|
56
|
+
|
|
57
|
+
console.log('UI: Initialized');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function generateStars() {
|
|
61
|
+
for (let i = 0; i < 150; i++) {
|
|
62
|
+
const star = document.createElement('div');
|
|
63
|
+
star.className = 'star';
|
|
64
|
+
star.style.left = Math.random() * 100 + '%';
|
|
65
|
+
star.style.top = Math.random() * 100 + '%';
|
|
66
|
+
star.style.width = star.style.height = (Math.random() * 2 + 0.5) + 'px';
|
|
67
|
+
star.style.setProperty('--opacity', Math.random() * 0.7 + 0.3);
|
|
68
|
+
star.style.setProperty('--duration', (Math.random() * 4 + 2) + 's');
|
|
69
|
+
star.style.animationDelay = -(Math.random() * 10) + 's';
|
|
70
|
+
starsContainer.appendChild(star);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function setupModeTabs() {
|
|
75
|
+
textModeTab.addEventListener('click', () => setMode('text'));
|
|
76
|
+
voiceModeTab.addEventListener('click', () => setMode('voice'));
|
|
77
|
+
|
|
78
|
+
// Desktop header mode toggle buttons
|
|
79
|
+
const headerTextMode = document.getElementById('headerTextMode');
|
|
80
|
+
const headerVoiceMode = document.getElementById('headerVoiceMode');
|
|
81
|
+
headerTextMode?.addEventListener('click', () => setMode('text'));
|
|
82
|
+
headerVoiceMode?.addEventListener('click', () => setMode('voice'));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setMode(mode) {
|
|
86
|
+
UplinkCore.mode = mode;
|
|
87
|
+
|
|
88
|
+
textModeTab?.classList.toggle('active', mode === 'text');
|
|
89
|
+
voiceModeTab?.classList.toggle('active', mode === 'voice');
|
|
90
|
+
|
|
91
|
+
// Sync desktop header mode buttons
|
|
92
|
+
const headerTextMode = document.getElementById('headerTextMode');
|
|
93
|
+
const headerVoiceMode = document.getElementById('headerVoiceMode');
|
|
94
|
+
headerTextMode?.classList.toggle('active', mode === 'text');
|
|
95
|
+
headerVoiceMode?.classList.toggle('active', mode === 'voice');
|
|
96
|
+
textInputRow?.classList.toggle('active', mode === 'text');
|
|
97
|
+
voiceInputRow?.classList.toggle('active', mode === 'voice');
|
|
98
|
+
|
|
99
|
+
// Start moon animation when switching to voice mode
|
|
100
|
+
if (mode === 'voice' && window.UplinkVoice?.startMoonAnimation) {
|
|
101
|
+
window.UplinkVoice.startMoonAnimation();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Recalculate messages padding for new input area height
|
|
105
|
+
setTimeout(updateMessagesPadding, 50);
|
|
106
|
+
|
|
107
|
+
// Save preference
|
|
108
|
+
UplinkStorage.saveSettings({ mode });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function updateUI() {
|
|
112
|
+
// Update encryption badge
|
|
113
|
+
updateEncryptionBadge();
|
|
114
|
+
|
|
115
|
+
// Apply saved mode
|
|
116
|
+
const settings = UplinkStorage.loadSettings();
|
|
117
|
+
if (settings.mode) setMode(settings.mode);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function updateEncryptionBadge() {
|
|
121
|
+
if (!encryptionBadge) return;
|
|
122
|
+
|
|
123
|
+
if (UplinkCore.encryptionEnabled) {
|
|
124
|
+
encryptionBadge.style.display = 'flex';
|
|
125
|
+
if (UplinkCore.state.currentPassword) {
|
|
126
|
+
encryptionBadge.classList.remove('warning');
|
|
127
|
+
encryptionBadge.innerHTML = '<span>🔒</span><span>Encrypted</span>';
|
|
128
|
+
} else {
|
|
129
|
+
encryptionBadge.classList.add('warning');
|
|
130
|
+
encryptionBadge.innerHTML = '<span>🔓</span><span>Locked</span>';
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
encryptionBadge.style.display = 'none';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function setupVisibilityHandling() {
|
|
138
|
+
document.addEventListener('visibilitychange', () => {
|
|
139
|
+
const paused = document.hidden;
|
|
140
|
+
|
|
141
|
+
// Pause star animations
|
|
142
|
+
document.querySelectorAll('.star').forEach(star => {
|
|
143
|
+
star.style.animationPlayState = paused ? 'paused' : 'running';
|
|
144
|
+
});
|
|
145
|
+
}, { signal: eventsAbortController.signal });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function setupSessionTimeout() {
|
|
149
|
+
function resetTimeout() {
|
|
150
|
+
if (!UplinkCore.encryptionEnabled || !UplinkCore.state.currentPassword) return;
|
|
151
|
+
|
|
152
|
+
if (sessionTimeoutId) clearTimeout(sessionTimeoutId);
|
|
153
|
+
|
|
154
|
+
sessionTimeoutId = setTimeout(() => {
|
|
155
|
+
console.log('Session timeout - clearing password');
|
|
156
|
+
UplinkCore.state.currentPassword = null;
|
|
157
|
+
updateEncryptionBadge();
|
|
158
|
+
|
|
159
|
+
// Show unlock screen
|
|
160
|
+
const onboarding = window.UplinkOnboarding;
|
|
161
|
+
if (onboarding) onboarding.showUnlock();
|
|
162
|
+
}, SESSION_TIMEOUT);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Reset on activity (using signal to prevent stacking)
|
|
166
|
+
const signal = eventsAbortController.signal;
|
|
167
|
+
document.addEventListener('click', resetTimeout, { signal });
|
|
168
|
+
document.addEventListener('keypress', resetTimeout, { signal });
|
|
169
|
+
document.addEventListener('touchstart', resetTimeout, { signal });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function setupAudioPrewarm() {
|
|
173
|
+
let prewarmed = false;
|
|
174
|
+
|
|
175
|
+
function prewarm() {
|
|
176
|
+
if (prewarmed) return;
|
|
177
|
+
prewarmed = true;
|
|
178
|
+
|
|
179
|
+
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
180
|
+
ctx.resume().catch(err => console.error('UI: AudioContext resume failed:', err));
|
|
181
|
+
|
|
182
|
+
const audio = document.getElementById('audio');
|
|
183
|
+
if (audio) audio.load();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
document.addEventListener('click', prewarm, { once: true });
|
|
187
|
+
document.addEventListener('touchstart', prewarm, { once: true });
|
|
188
|
+
document.addEventListener('keydown', prewarm, { once: true });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Dynamic padding for messages area to clear fixed input + audio player
|
|
192
|
+
function updateMessagesPadding() {
|
|
193
|
+
const messages = document.getElementById('messages');
|
|
194
|
+
if (!messages) return;
|
|
195
|
+
|
|
196
|
+
const inputArea = document.querySelector('.input-area');
|
|
197
|
+
const modeTabs = document.querySelector('.mode-tabs');
|
|
198
|
+
const audioBar = document.getElementById('audioPlayerBar');
|
|
199
|
+
|
|
200
|
+
let bottomHeight = 0;
|
|
201
|
+
if (inputArea) bottomHeight += inputArea.offsetHeight;
|
|
202
|
+
if (modeTabs) bottomHeight += modeTabs.offsetHeight;
|
|
203
|
+
if (audioBar && audioBar.style.display !== 'none') bottomHeight += audioBar.offsetHeight;
|
|
204
|
+
|
|
205
|
+
// Add a small buffer
|
|
206
|
+
bottomHeight += 16;
|
|
207
|
+
|
|
208
|
+
messages.style.paddingBottom = bottomHeight + 'px';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Observe input area size changes (textarea growing, mode switching)
|
|
212
|
+
function setupPaddingObserver() {
|
|
213
|
+
const inputArea = document.querySelector('.input-area');
|
|
214
|
+
if (!inputArea) return;
|
|
215
|
+
|
|
216
|
+
// ResizeObserver for when textarea grows/shrinks
|
|
217
|
+
if (window.ResizeObserver) {
|
|
218
|
+
const ro = new ResizeObserver(() => updateMessagesPadding());
|
|
219
|
+
ro.observe(inputArea);
|
|
220
|
+
// Also observe voice input row
|
|
221
|
+
const voiceRow = document.getElementById('voiceInputRow');
|
|
222
|
+
if (voiceRow) ro.observe(voiceRow);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Listen for audio player show/hide
|
|
226
|
+
const audioBar = document.getElementById('audioPlayerBar');
|
|
227
|
+
if (audioBar && window.MutationObserver) {
|
|
228
|
+
const mo = new MutationObserver(() => updateMessagesPadding());
|
|
229
|
+
mo.observe(audioBar, { attributes: true, attributeFilter: ['style'] });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Recalc on resize and mode switch
|
|
233
|
+
// Debounced to avoid excessive padding calculations during resize
|
|
234
|
+
window.addEventListener('resize', UplinkCore.debounce(updateMessagesPadding, 150));
|
|
235
|
+
|
|
236
|
+
// Initial calculation
|
|
237
|
+
updateMessagesPadding();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Cleanup function
|
|
241
|
+
function destroy() {
|
|
242
|
+
if (eventsAbortController) {
|
|
243
|
+
eventsAbortController.abort();
|
|
244
|
+
eventsAbortController = null;
|
|
245
|
+
}
|
|
246
|
+
if (sessionTimeoutId) {
|
|
247
|
+
clearTimeout(sessionTimeoutId);
|
|
248
|
+
sessionTimeoutId = null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Export API
|
|
253
|
+
export const UplinkUI = {
|
|
254
|
+
setMode,
|
|
255
|
+
updateEncryptionBadge,
|
|
256
|
+
updateMessagesPadding,
|
|
257
|
+
generateStars,
|
|
258
|
+
destroy
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
export { setMode, updateEncryptionBadge, updateMessagesPadding, generateStars, destroy };
|
|
262
|
+
|
|
263
|
+
// Backward compat: assign to window
|
|
264
|
+
window.UplinkUI = UplinkUI;
|
|
265
|
+
|
|
266
|
+
// Register and init
|
|
267
|
+
UplinkCore.registerModule('ui', init);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// UPDATE NOTIFIER MODULE
|
|
3
|
+
// Shows a banner when a new version is available
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if this version's update was already dismissed
|
|
8
|
+
*/
|
|
9
|
+
function isDismissed(version) {
|
|
10
|
+
try {
|
|
11
|
+
return localStorage.getItem('dismissed-update-' + version) === 'true';
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Mark this version's update as dismissed
|
|
19
|
+
*/
|
|
20
|
+
function dismissVersion(version) {
|
|
21
|
+
try {
|
|
22
|
+
localStorage.setItem('dismissed-update-' + version, 'true');
|
|
23
|
+
} catch {
|
|
24
|
+
// Ignore storage errors
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Remove existing update banner if present
|
|
30
|
+
*/
|
|
31
|
+
function removeBanner() {
|
|
32
|
+
const existing = document.querySelector('.update-banner');
|
|
33
|
+
if (existing) {
|
|
34
|
+
existing.classList.add('update-banner-hiding');
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
if (existing.parentNode) {
|
|
37
|
+
existing.parentNode.removeChild(existing);
|
|
38
|
+
}
|
|
39
|
+
}, 300);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Pre-fill the chat input with an update prompt for the bot
|
|
45
|
+
*/
|
|
46
|
+
function prefillBotPrompt(latest) {
|
|
47
|
+
const input = document.getElementById('userInput');
|
|
48
|
+
if (input) {
|
|
49
|
+
input.value = 'Update Uplink to the latest version (v' + latest + ') and restart the server.';
|
|
50
|
+
input.focus();
|
|
51
|
+
// Trigger input event so auto-resize works
|
|
52
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
53
|
+
}
|
|
54
|
+
removeBanner();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Show the update banner
|
|
59
|
+
*/
|
|
60
|
+
function showBanner(current, latest) {
|
|
61
|
+
if (isDismissed(latest)) return;
|
|
62
|
+
|
|
63
|
+
removeBanner();
|
|
64
|
+
|
|
65
|
+
const banner = document.createElement('div');
|
|
66
|
+
banner.className = 'update-banner';
|
|
67
|
+
|
|
68
|
+
// Banner text
|
|
69
|
+
const text = document.createElement('span');
|
|
70
|
+
text.className = 'update-banner-text';
|
|
71
|
+
text.textContent = '\u2B21 Uplink v' + latest + ' is available \u2014 you\'re on v' + current;
|
|
72
|
+
|
|
73
|
+
// "Let my bot handle it" button
|
|
74
|
+
const botBtn = document.createElement('button');
|
|
75
|
+
botBtn.className = 'update-banner-bot-btn';
|
|
76
|
+
botBtn.textContent = 'Let my bot handle it';
|
|
77
|
+
botBtn.addEventListener('click', function(e) {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
prefillBotPrompt(latest);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Dismiss button
|
|
83
|
+
const dismissBtn = document.createElement('button');
|
|
84
|
+
dismissBtn.className = 'update-banner-dismiss';
|
|
85
|
+
dismissBtn.textContent = '\u2715';
|
|
86
|
+
dismissBtn.setAttribute('aria-label', 'Dismiss update notification');
|
|
87
|
+
dismissBtn.addEventListener('click', function() {
|
|
88
|
+
dismissVersion(latest);
|
|
89
|
+
removeBanner();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
banner.appendChild(text);
|
|
93
|
+
banner.appendChild(botBtn);
|
|
94
|
+
banner.appendChild(dismissBtn);
|
|
95
|
+
|
|
96
|
+
// Insert at the top of the chat area
|
|
97
|
+
const chatMessages = document.getElementById('chatMessages');
|
|
98
|
+
if (chatMessages) {
|
|
99
|
+
chatMessages.insertBefore(banner, chatMessages.firstChild);
|
|
100
|
+
} else {
|
|
101
|
+
document.body.insertBefore(banner, document.body.firstChild);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Handle incoming WebSocket messages
|
|
107
|
+
*/
|
|
108
|
+
function handleWsMessage(data) {
|
|
109
|
+
if (!data || data.type !== 'update_available') return;
|
|
110
|
+
if (!data.current || !data.latest) return;
|
|
111
|
+
|
|
112
|
+
showBanner(data.current, data.latest);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Initialize the update notifier
|
|
117
|
+
*/
|
|
118
|
+
function init() {
|
|
119
|
+
// Listen for WebSocket messages via custom event
|
|
120
|
+
window.addEventListener('uplink:ws-message', function(e) {
|
|
121
|
+
if (e.detail) {
|
|
122
|
+
handleWsMessage(e.detail);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (window.UplinkLogger && window.UplinkLogger.debug) {
|
|
127
|
+
window.UplinkLogger.debug('[UpdateNotifier] Initialized');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Register with UplinkCore if available
|
|
132
|
+
UplinkCore.registerModule('update-notifier', init);
|
|
133
|
+
|
|
134
|
+
// Expose for testing
|
|
135
|
+
export const UplinkUpdateNotifier = {
|
|
136
|
+
showBanner: showBanner,
|
|
137
|
+
handleWsMessage: handleWsMessage,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
import { UplinkCore } from './core.js';
|
|
141
|
+
|
|
142
|
+
// Backward compat: assign to window
|
|
143
|
+
window.UplinkUpdateNotifier = UplinkUpdateNotifier;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// CONSTANTS UTILITY
|
|
3
|
+
// Centralized magic numbers and configuration
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
// ===========================================
|
|
7
|
+
// File Size Limits
|
|
8
|
+
// ===========================================
|
|
9
|
+
export const FILE_LIMITS = {
|
|
10
|
+
// Audio upload limit (25MB)
|
|
11
|
+
AUDIO_MAX_SIZE: 25 * 1024 * 1024,
|
|
12
|
+
|
|
13
|
+
// Image upload limit (10MB)
|
|
14
|
+
IMAGE_MAX_SIZE: 10 * 1024 * 1024,
|
|
15
|
+
|
|
16
|
+
// Video upload limit (50MB)
|
|
17
|
+
VIDEO_MAX_SIZE: 50 * 1024 * 1024,
|
|
18
|
+
|
|
19
|
+
// Generic file upload limit
|
|
20
|
+
DEFAULT_MAX_SIZE: 10 * 1024 * 1024,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ===========================================
|
|
24
|
+
// Timeout Values (milliseconds)
|
|
25
|
+
// ===========================================
|
|
26
|
+
export const TIMEOUTS = {
|
|
27
|
+
// Default fetch timeout
|
|
28
|
+
FETCH_DEFAULT: 45000,
|
|
29
|
+
|
|
30
|
+
// Extended timeout for AI chat operations
|
|
31
|
+
CHAT_REQUEST: 300000,
|
|
32
|
+
|
|
33
|
+
// Timeout for TTS generation
|
|
34
|
+
TTS_GENERATION: 60000,
|
|
35
|
+
|
|
36
|
+
// Timeout for transcription
|
|
37
|
+
TRANSCRIPTION: 45000,
|
|
38
|
+
|
|
39
|
+
// WebSocket ping interval
|
|
40
|
+
WS_PING_INTERVAL: 30000,
|
|
41
|
+
|
|
42
|
+
// WebSocket connection timeout
|
|
43
|
+
WS_CONNECTION: 10000,
|
|
44
|
+
|
|
45
|
+
// Reconnection delay (base)
|
|
46
|
+
RECONNECT_BASE: 1000,
|
|
47
|
+
|
|
48
|
+
// Maximum reconnection delay
|
|
49
|
+
RECONNECT_MAX: 30000,
|
|
50
|
+
|
|
51
|
+
// Typing indicator timeout
|
|
52
|
+
TYPING_INDICATOR: 60000,
|
|
53
|
+
|
|
54
|
+
// Request tracking cleanup
|
|
55
|
+
REQUEST_CLEANUP: 300000,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ===========================================
|
|
59
|
+
// Rate Limits
|
|
60
|
+
// ===========================================
|
|
61
|
+
export const RATE_LIMITS = {
|
|
62
|
+
// WebSocket messages per minute
|
|
63
|
+
WS_MESSAGES_PER_MINUTE: 30,
|
|
64
|
+
|
|
65
|
+
// WebSocket rate limit window (ms)
|
|
66
|
+
WS_WINDOW_MS: 60000,
|
|
67
|
+
|
|
68
|
+
// General API rate limit (requests per window)
|
|
69
|
+
API_REQUESTS_PER_WINDOW: 100,
|
|
70
|
+
|
|
71
|
+
// Strict rate limit (for sensitive endpoints)
|
|
72
|
+
STRICT_REQUESTS_PER_WINDOW: 10,
|
|
73
|
+
|
|
74
|
+
// Rate limit window (15 minutes)
|
|
75
|
+
API_WINDOW_MS: 15 * 60 * 1000,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ===========================================
|
|
79
|
+
// Retry Configuration
|
|
80
|
+
// ===========================================
|
|
81
|
+
export const RETRY = {
|
|
82
|
+
// Maximum retry attempts
|
|
83
|
+
MAX_ATTEMPTS: 3,
|
|
84
|
+
|
|
85
|
+
// Base delay between retries (ms)
|
|
86
|
+
BASE_DELAY: 1000,
|
|
87
|
+
|
|
88
|
+
// Maximum delay between retries (ms)
|
|
89
|
+
MAX_DELAY: 10000,
|
|
90
|
+
|
|
91
|
+
// Exponential backoff multiplier
|
|
92
|
+
BACKOFF_MULTIPLIER: 2,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// ===========================================
|
|
96
|
+
// Message Limits
|
|
97
|
+
// ===========================================
|
|
98
|
+
export const MESSAGE_LIMITS = {
|
|
99
|
+
// Maximum message length
|
|
100
|
+
MAX_LENGTH: 50000,
|
|
101
|
+
|
|
102
|
+
// Maximum messages in sync storage
|
|
103
|
+
MAX_SYNC_MESSAGES: 100,
|
|
104
|
+
|
|
105
|
+
// Maximum activity items to keep
|
|
106
|
+
MAX_ACTIVITY_ITEMS: 50,
|
|
107
|
+
|
|
108
|
+
// Truncation length for previews
|
|
109
|
+
PREVIEW_LENGTH: 100,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// ===========================================
|
|
113
|
+
// UI Constants
|
|
114
|
+
// ===========================================
|
|
115
|
+
export const UI = {
|
|
116
|
+
// Maximum textarea height
|
|
117
|
+
TEXTAREA_MAX_HEIGHT: 150,
|
|
118
|
+
|
|
119
|
+
// Scroll threshold for auto-scroll
|
|
120
|
+
SCROLL_THRESHOLD: 100,
|
|
121
|
+
|
|
122
|
+
// Animation durations
|
|
123
|
+
ANIMATION_FAST: 150,
|
|
124
|
+
ANIMATION_NORMAL: 300,
|
|
125
|
+
ANIMATION_SLOW: 500,
|
|
126
|
+
|
|
127
|
+
// Mobile breakpoint
|
|
128
|
+
MOBILE_BREAKPOINT: 768,
|
|
129
|
+
|
|
130
|
+
// Button feedback animation (copy, etc.)
|
|
131
|
+
BUTTON_FEEDBACK_DURATION: 1500,
|
|
132
|
+
|
|
133
|
+
// Text truncation lengths (L-07: magic numbers)
|
|
134
|
+
TEXT_TRUNCATE_REPLY: 150,
|
|
135
|
+
TEXT_TRUNCATE_FORK: 500,
|
|
136
|
+
TEXT_TRUNCATE_EDIT: 200,
|
|
137
|
+
|
|
138
|
+
// Form constraints
|
|
139
|
+
SATELLITE_NAME_MAX_LENGTH: 32,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// ===========================================
|
|
143
|
+
// Validation Patterns
|
|
144
|
+
// ===========================================
|
|
145
|
+
export const PATTERNS = {
|
|
146
|
+
// Satellite ID pattern
|
|
147
|
+
SATELLITE_ID: /^[a-zA-Z0-9_-]{1,32}$/,
|
|
148
|
+
|
|
149
|
+
// Share ID pattern
|
|
150
|
+
SHARE_ID: /^[a-zA-Z0-9]{1,12}$/,
|
|
151
|
+
|
|
152
|
+
// URL pattern (basic)
|
|
153
|
+
URL: /^https?:\/\/.+/i,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Default export for convenience
|
|
157
|
+
export default {
|
|
158
|
+
FILE_LIMITS,
|
|
159
|
+
TIMEOUTS,
|
|
160
|
+
RATE_LIMITS,
|
|
161
|
+
RETRY,
|
|
162
|
+
MESSAGE_LIMITS,
|
|
163
|
+
UI,
|
|
164
|
+
PATTERNS,
|
|
165
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// SANITIZE UTILITY
|
|
3
|
+
// DOMPurify wrapper for HTML sanitization
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sanitize HTML content using DOMPurify (if available) or basic escaping
|
|
8
|
+
*
|
|
9
|
+
* @param {string} html - The HTML string to sanitize
|
|
10
|
+
* @param {Object} options - DOMPurify configuration options
|
|
11
|
+
* @returns {string} - Sanitized HTML string
|
|
12
|
+
*/
|
|
13
|
+
export function sanitizeHTML(html, options = {}) {
|
|
14
|
+
if (typeof html !== 'string') {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Use DOMPurify if available (should be loaded via CDN or npm)
|
|
19
|
+
if (typeof DOMPurify !== 'undefined') {
|
|
20
|
+
const defaultOptions = {
|
|
21
|
+
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li', 'code', 'pre', 'blockquote', 'span', 'img', 'h2', 'h3', 'h4', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'del', 'hr'],
|
|
22
|
+
ALLOWED_ATTR: ['href', 'target', 'rel', 'class', 'src', 'alt'],
|
|
23
|
+
ALLOW_DATA_ATTR: false,
|
|
24
|
+
ADD_ATTR: ['target'],
|
|
25
|
+
...options
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Force safe link behavior
|
|
29
|
+
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
|
|
30
|
+
if (node.tagName === 'A') {
|
|
31
|
+
node.setAttribute('target', '_blank');
|
|
32
|
+
node.setAttribute('rel', 'noopener noreferrer');
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return DOMPurify.sanitize(html, defaultOptions);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Fallback: basic HTML escaping if DOMPurify not available
|
|
40
|
+
if (typeof window !== 'undefined' && window.UplinkLogger?.warn) {
|
|
41
|
+
window.UplinkLogger.warn('DOMPurify not loaded, falling back to basic escaping');
|
|
42
|
+
}
|
|
43
|
+
return escapeHTML(html);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Basic HTML escaping - converts special characters to HTML entities
|
|
48
|
+
* Use sanitizeHTML for untrusted content that may contain markup
|
|
49
|
+
*
|
|
50
|
+
* @param {string} str - The string to escape
|
|
51
|
+
* @returns {string} - Escaped string safe for HTML insertion
|
|
52
|
+
*/
|
|
53
|
+
export function escapeHTML(str) {
|
|
54
|
+
if (typeof str !== 'string') {
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const div = document.createElement('div');
|
|
59
|
+
div.textContent = str;
|
|
60
|
+
return div.innerHTML;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Escape HTML and convert newlines to <br> tags
|
|
65
|
+
* Useful for displaying multi-line text content
|
|
66
|
+
*
|
|
67
|
+
* @param {string} str - The string to escape
|
|
68
|
+
* @returns {string} - Escaped string with line breaks
|
|
69
|
+
*/
|
|
70
|
+
export function escapeHTMLWithBreaks(str) {
|
|
71
|
+
return escapeHTML(str).replace(/\n/g, '<br>');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sanitize a string for use in CSS class names
|
|
76
|
+
*
|
|
77
|
+
* @param {string} str - The string to sanitize
|
|
78
|
+
* @returns {string} - Safe CSS class name
|
|
79
|
+
*/
|
|
80
|
+
export function sanitizeClassName(str) {
|
|
81
|
+
if (typeof str !== 'string') {
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
return str.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 50);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Default export for convenience
|
|
88
|
+
export default {
|
|
89
|
+
sanitizeHTML,
|
|
90
|
+
escapeHTML,
|
|
91
|
+
escapeHTMLWithBreaks,
|
|
92
|
+
sanitizeClassName
|
|
93
|
+
};
|