@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,271 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// CONTEXT TRACKER MODULE
|
|
3
|
+
// Shows context window usage in the header
|
|
4
|
+
// Fetches real context data from gateway session store
|
|
5
|
+
// ============================================
|
|
6
|
+
|
|
7
|
+
import { UplinkCore } from './core.js';
|
|
8
|
+
|
|
9
|
+
// Default context window size (tokens)
|
|
10
|
+
const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
11
|
+
|
|
12
|
+
// Poll interval (fetch after each message, plus periodic refresh)
|
|
13
|
+
const REFRESH_INTERVAL_MS = 60000; // 1 minute periodic refresh
|
|
14
|
+
|
|
15
|
+
// State
|
|
16
|
+
let contextUsed = 0;
|
|
17
|
+
let contextMax = DEFAULT_CONTEXT_WINDOW;
|
|
18
|
+
let displayedTokens = 0; // Animated display value
|
|
19
|
+
let animationFrame = null;
|
|
20
|
+
let refreshTimer = null;
|
|
21
|
+
let currentSatelliteId = 'main';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Format token count to human-readable string
|
|
25
|
+
* @param {number} tokens - Token count
|
|
26
|
+
* @returns {string} - Formatted string (e.g., "22k", "1.2k", "200k")
|
|
27
|
+
*/
|
|
28
|
+
function formatTokens(tokens) {
|
|
29
|
+
if (tokens >= 1000000) {
|
|
30
|
+
return (tokens / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
|
|
31
|
+
}
|
|
32
|
+
if (tokens >= 1000) {
|
|
33
|
+
return (tokens / 1000).toFixed(tokens >= 10000 ? 0 : 1).replace(/\.0$/, '') + 'k';
|
|
34
|
+
}
|
|
35
|
+
return String(tokens);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get color based on usage percentage
|
|
40
|
+
* @param {number} pct - Usage percentage (0-100)
|
|
41
|
+
* @returns {string} - CSS color value
|
|
42
|
+
*/
|
|
43
|
+
function getUsageColor(pct) {
|
|
44
|
+
if (pct >= 90) return 'var(--error)'; // Red — critical
|
|
45
|
+
if (pct >= 75) return 'var(--warning)'; // Yellow — getting full
|
|
46
|
+
return 'var(--success)'; // Green — healthy
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Animate the token count text smoothly toward a target value
|
|
51
|
+
* Uses eased lerp over ~600ms to match the CSS bar transition
|
|
52
|
+
*/
|
|
53
|
+
function animateTokenCount(target, max, textEl) {
|
|
54
|
+
if (animationFrame) cancelAnimationFrame(animationFrame);
|
|
55
|
+
|
|
56
|
+
const start = displayedTokens;
|
|
57
|
+
const diff = target - start;
|
|
58
|
+
if (diff === 0) return;
|
|
59
|
+
|
|
60
|
+
const duration = 600; // ms, matches CSS transition
|
|
61
|
+
const startTime = performance.now();
|
|
62
|
+
|
|
63
|
+
function tick(now) {
|
|
64
|
+
const elapsed = now - startTime;
|
|
65
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
66
|
+
// Ease-out cubic
|
|
67
|
+
const eased = 1 - Math.pow(1 - progress, 3);
|
|
68
|
+
|
|
69
|
+
displayedTokens = Math.round(start + diff * eased);
|
|
70
|
+
const isMobile = window.innerWidth <= 768;
|
|
71
|
+
const pct = Math.min(100, Math.round((displayedTokens / max) * 100));
|
|
72
|
+
textEl.textContent = isMobile ? `${pct}%` : `${formatTokens(displayedTokens)}/${formatTokens(max)}`;
|
|
73
|
+
|
|
74
|
+
if (progress < 1) {
|
|
75
|
+
animationFrame = requestAnimationFrame(tick);
|
|
76
|
+
} else {
|
|
77
|
+
displayedTokens = target;
|
|
78
|
+
animationFrame = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
animationFrame = requestAnimationFrame(tick);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Update the context tracker display
|
|
87
|
+
*/
|
|
88
|
+
function updateDisplay() {
|
|
89
|
+
const badge = document.getElementById('contextBadge');
|
|
90
|
+
const barFill = document.getElementById('contextBarFill');
|
|
91
|
+
const text = document.getElementById('contextText');
|
|
92
|
+
|
|
93
|
+
if (!badge) return;
|
|
94
|
+
|
|
95
|
+
if (contextUsed <= 0) {
|
|
96
|
+
badge.style.display = 'none';
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
badge.style.display = 'flex';
|
|
101
|
+
|
|
102
|
+
const pct = Math.min(100, Math.round((contextUsed / contextMax) * 100));
|
|
103
|
+
const color = getUsageColor(pct);
|
|
104
|
+
|
|
105
|
+
// Update bar fill
|
|
106
|
+
if (barFill) {
|
|
107
|
+
barFill.style.width = pct + '%';
|
|
108
|
+
barFill.style.background = color;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Animate text — smooth count toward target value
|
|
112
|
+
if (text) {
|
|
113
|
+
animateTokenCount(contextUsed, contextMax, text);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Update tooltip — full details on hover
|
|
117
|
+
badge.title = `Context: ${contextUsed.toLocaleString()} / ${contextMax.toLocaleString()} tokens (${pct}%)`;
|
|
118
|
+
|
|
119
|
+
// Update ARIA
|
|
120
|
+
badge.setAttribute('aria-label', `Context window usage: ${pct}% — ${formatTokens(contextUsed)} of ${formatTokens(contextMax)} tokens used`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Fetch context data from the server
|
|
125
|
+
*/
|
|
126
|
+
async function fetchContext() {
|
|
127
|
+
try {
|
|
128
|
+
const satelliteId = window.UplinkSatellites?.getCurrentSatellite() || 'main';
|
|
129
|
+
currentSatelliteId = satelliteId;
|
|
130
|
+
|
|
131
|
+
const agentId = window.UplinkSatellites?.getCurrentAgentId?.() || 'main';
|
|
132
|
+
const response = await fetch(`/api/session/context?satelliteId=${encodeURIComponent(satelliteId)}&agentId=${encodeURIComponent(agentId)}`);
|
|
133
|
+
if (!response.ok) return;
|
|
134
|
+
|
|
135
|
+
const data = await response.json();
|
|
136
|
+
if (!data.ok && data.error) {
|
|
137
|
+
if (window.logger) logger.warn('ContextTracker: Fetch error', data.error);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
contextUsed = data.totalTokens || 0;
|
|
142
|
+
contextMax = data.contextTokens || DEFAULT_CONTEXT_WINDOW;
|
|
143
|
+
updateDisplay();
|
|
144
|
+
} catch (e) {
|
|
145
|
+
if (window.logger) logger.warn('ContextTracker: Fetch failed', e.message);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Called after a message completes — refresh context data
|
|
151
|
+
* Multiple reads: immediate-ish for responsiveness, then again after
|
|
152
|
+
* pruning has had time to settle the token count
|
|
153
|
+
*/
|
|
154
|
+
function refresh() {
|
|
155
|
+
setTimeout(fetchContext, 2000); // Quick read
|
|
156
|
+
setTimeout(fetchContext, 8000); // Post-pruning settle
|
|
157
|
+
setTimeout(fetchContext, 20000); // Final settle (catches slower prune cycles)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Reset the tracker (e.g., on clear chat)
|
|
162
|
+
*/
|
|
163
|
+
function reset() {
|
|
164
|
+
contextUsed = 0;
|
|
165
|
+
displayedTokens = 0;
|
|
166
|
+
updateDisplay();
|
|
167
|
+
// Fetch fresh data after reset
|
|
168
|
+
setTimeout(fetchContext, 2000);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get current state
|
|
173
|
+
* @returns {Object} - Current context tracker state
|
|
174
|
+
*/
|
|
175
|
+
function getState() {
|
|
176
|
+
return {
|
|
177
|
+
contextUsed,
|
|
178
|
+
contextMax,
|
|
179
|
+
percentage: contextMax > 0 ? Math.round((contextUsed / contextMax) * 100) : 0,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Initialize the module
|
|
185
|
+
*/
|
|
186
|
+
function init() {
|
|
187
|
+
// Initial fetch
|
|
188
|
+
fetchContext();
|
|
189
|
+
|
|
190
|
+
// Periodic refresh
|
|
191
|
+
refreshTimer = setInterval(fetchContext, REFRESH_INTERVAL_MS);
|
|
192
|
+
|
|
193
|
+
// Re-render on resize (switches between token count and percentage)
|
|
194
|
+
// Debounced to avoid excessive reflows during resize
|
|
195
|
+
window.addEventListener('resize', UplinkCore.debounce(updateDisplay, 150));
|
|
196
|
+
|
|
197
|
+
// Refresh on tab resume — but only if already connected (avoid fetch errors during reconnect)
|
|
198
|
+
document.addEventListener('visibilitychange', () => {
|
|
199
|
+
if (document.visibilityState === 'visible' && window.UplinkConnection?.isConnected?.()) {
|
|
200
|
+
fetchContext();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Refresh on reconnect (session may have changed while disconnected)
|
|
205
|
+
if (window.UplinkConnection?.onConnection) {
|
|
206
|
+
window.UplinkConnection.onConnection((event) => {
|
|
207
|
+
if (event === 'connected') {
|
|
208
|
+
setTimeout(fetchContext, 1000);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Watch for system messages mentioning compaction
|
|
214
|
+
const observer = new MutationObserver((mutations) => {
|
|
215
|
+
for (const mutation of mutations) {
|
|
216
|
+
for (const node of mutation.addedNodes) {
|
|
217
|
+
if (node.nodeType !== 1) continue;
|
|
218
|
+
// Check if it's a system message about compaction
|
|
219
|
+
const text = node.textContent || '';
|
|
220
|
+
if (node.classList?.contains('system') && /compact/i.test(text)) {
|
|
221
|
+
setTimeout(fetchContext, 3000); // Delay for store to update
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
const messagesEl = document.getElementById('messages');
|
|
227
|
+
if (messagesEl) {
|
|
228
|
+
observer.observe(messagesEl, { childList: true });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Listen for chat clear events to reset
|
|
232
|
+
const settingsClearBtn = document.getElementById('settingsClearBtn');
|
|
233
|
+
if (settingsClearBtn) {
|
|
234
|
+
settingsClearBtn.addEventListener('click', () => {
|
|
235
|
+
setTimeout(reset, 500);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (window.logger) {
|
|
240
|
+
logger.debug('ContextTracker: Initialized');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Cleanup function - stop timers
|
|
246
|
+
*/
|
|
247
|
+
function cleanup() {
|
|
248
|
+
if (refreshTimer) {
|
|
249
|
+
clearInterval(refreshTimer);
|
|
250
|
+
refreshTimer = null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Expose API
|
|
255
|
+
export const UplinkContextTracker = {
|
|
256
|
+
init,
|
|
257
|
+
refresh, // Call after message completes
|
|
258
|
+
fetchContext, // Manual fetch
|
|
259
|
+
reset,
|
|
260
|
+
getState,
|
|
261
|
+
cleanup,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Backward compat: assign to window
|
|
265
|
+
window.UplinkContextTracker = UplinkContextTracker;
|
|
266
|
+
|
|
267
|
+
// Register with core for coordinated initialization
|
|
268
|
+
UplinkCore.registerModule('context-tracker', init);
|
|
269
|
+
|
|
270
|
+
// Cleanup on page unload
|
|
271
|
+
window.addEventListener('beforeunload', cleanup);
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// CORE MODULE
|
|
3
|
+
// Shared state, config, initialization coordinator
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
import { UplinkLogger } from './logger.js';
|
|
7
|
+
|
|
8
|
+
// Global state - other modules read/write via UplinkCore
|
|
9
|
+
const state = {
|
|
10
|
+
agentName: 'Assistant',
|
|
11
|
+
gatewayUrl: '',
|
|
12
|
+
audioResponses: true,
|
|
13
|
+
encryptionEnabled: false,
|
|
14
|
+
currentPassword: null,
|
|
15
|
+
mode: 'text', // 'text' or 'voice'
|
|
16
|
+
chatState: 'idle' // 'idle', 'processing', 'recording'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Module registry - tracks which modules are loaded
|
|
20
|
+
const modules = {};
|
|
21
|
+
|
|
22
|
+
// Initialization queue - modules register init functions
|
|
23
|
+
const initQueue = [];
|
|
24
|
+
let initialized = false;
|
|
25
|
+
|
|
26
|
+
// Load config from localStorage
|
|
27
|
+
function loadConfig() {
|
|
28
|
+
try {
|
|
29
|
+
const config = JSON.parse(localStorage.getItem('uplink-config') || 'null');
|
|
30
|
+
if (config) {
|
|
31
|
+
state.agentName = config.agentName || 'Assistant';
|
|
32
|
+
state.gatewayUrl = config.gatewayUrl || '';
|
|
33
|
+
state.audioResponses = config.audioResponses !== undefined ? config.audioResponses : true;
|
|
34
|
+
state.encryptionEnabled = config.encryptionEnabled || false;
|
|
35
|
+
}
|
|
36
|
+
return config;
|
|
37
|
+
} catch (e) {
|
|
38
|
+
UplinkLogger.error('Core: Failed to load config', e);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Save config to localStorage
|
|
44
|
+
function saveConfig() {
|
|
45
|
+
try {
|
|
46
|
+
localStorage.setItem('uplink-config', JSON.stringify({
|
|
47
|
+
agentName: state.agentName,
|
|
48
|
+
gatewayUrl: state.gatewayUrl,
|
|
49
|
+
audioResponses: state.audioResponses,
|
|
50
|
+
encryptionEnabled: state.encryptionEnabled
|
|
51
|
+
}));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
UplinkLogger.error('Core: Failed to save config', e);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Register a module
|
|
58
|
+
function registerModule(name, initFn) {
|
|
59
|
+
modules[name] = { loaded: true, initialized: false };
|
|
60
|
+
if (initFn) {
|
|
61
|
+
if (initialized) {
|
|
62
|
+
// Already initialized, run immediately
|
|
63
|
+
try {
|
|
64
|
+
initFn();
|
|
65
|
+
modules[name].initialized = true;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error(`Core: Module ${name} failed to initialize`, e);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
initQueue.push({ name, fn: initFn });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Run initialization
|
|
76
|
+
function init() {
|
|
77
|
+
if (initialized) return;
|
|
78
|
+
|
|
79
|
+
UplinkLogger.debug('Core: Initializing...');
|
|
80
|
+
loadConfig();
|
|
81
|
+
|
|
82
|
+
// Run all queued init functions
|
|
83
|
+
initQueue.forEach(({ name, fn }) => {
|
|
84
|
+
try {
|
|
85
|
+
fn();
|
|
86
|
+
modules[name].initialized = true;
|
|
87
|
+
UplinkLogger.debug(`Core: Module ${name} initialized`);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
UplinkLogger.error(`Core: Module ${name} failed to initialize`, e);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
initialized = true;
|
|
94
|
+
UplinkLogger.debug('Core: Ready');
|
|
95
|
+
|
|
96
|
+
// Register service worker for PWA + force update if stale
|
|
97
|
+
if ('serviceWorker' in navigator) {
|
|
98
|
+
navigator.serviceWorker.register('/sw.js')
|
|
99
|
+
.then(reg => {
|
|
100
|
+
UplinkLogger.debug('Core: Service Worker registered', reg.scope);
|
|
101
|
+
// Check for updates immediately, then every 5 minutes
|
|
102
|
+
reg.update();
|
|
103
|
+
setInterval(() => reg.update(), 5 * 60 * 1000);
|
|
104
|
+
})
|
|
105
|
+
.catch(err => {
|
|
106
|
+
UplinkLogger.warn('Core: Service Worker registration failed', err);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Dispatch ready event
|
|
111
|
+
window.dispatchEvent(new CustomEvent('uplink:ready'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check if a module is available
|
|
115
|
+
function hasModule(name) {
|
|
116
|
+
return modules[name]?.initialized === true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Utility: Debounce function for performance optimization
|
|
120
|
+
function debounce(fn, delay) {
|
|
121
|
+
let timeoutId;
|
|
122
|
+
return function debounced(...args) {
|
|
123
|
+
clearTimeout(timeoutId);
|
|
124
|
+
timeoutId = setTimeout(() => fn.apply(this, args), delay);
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Expose API
|
|
129
|
+
export const UplinkCore = {
|
|
130
|
+
state,
|
|
131
|
+
loadConfig,
|
|
132
|
+
saveConfig,
|
|
133
|
+
registerModule,
|
|
134
|
+
hasModule,
|
|
135
|
+
init,
|
|
136
|
+
debounce,
|
|
137
|
+
|
|
138
|
+
// State shortcuts
|
|
139
|
+
get agentName() { return state.agentName; },
|
|
140
|
+
set agentName(v) { state.agentName = v; saveConfig(); },
|
|
141
|
+
get gatewayUrl() { return state.gatewayUrl; },
|
|
142
|
+
set gatewayUrl(v) { state.gatewayUrl = v; saveConfig(); },
|
|
143
|
+
get audioResponses() { return state.audioResponses; },
|
|
144
|
+
set audioResponses(v) { state.audioResponses = v; saveConfig(); },
|
|
145
|
+
get encryptionEnabled() { return state.encryptionEnabled; },
|
|
146
|
+
set encryptionEnabled(v) { state.encryptionEnabled = v; saveConfig(); },
|
|
147
|
+
get mode() { return state.mode; },
|
|
148
|
+
set mode(v) { state.mode = v; },
|
|
149
|
+
get chatState() { return state.chatState; },
|
|
150
|
+
set chatState(v) { state.chatState = v; }
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
state,
|
|
155
|
+
loadConfig,
|
|
156
|
+
saveConfig,
|
|
157
|
+
registerModule,
|
|
158
|
+
hasModule,
|
|
159
|
+
init,
|
|
160
|
+
debounce
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Backward compat: assign to window
|
|
164
|
+
window.UplinkCore = UplinkCore;
|
|
165
|
+
|
|
166
|
+
// Auto-init when DOM ready
|
|
167
|
+
if (document.readyState === 'loading') {
|
|
168
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
169
|
+
} else {
|
|
170
|
+
// Small delay to let other modules register
|
|
171
|
+
setTimeout(init, 10);
|
|
172
|
+
}
|