@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,543 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// ONBOARDING MODULE
|
|
3
|
+
// Setup flow, unlock screen
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
import { UplinkCore } from './core.js';
|
|
7
|
+
|
|
8
|
+
// ========== CONSTANTS ==========
|
|
9
|
+
// Init retry
|
|
10
|
+
const INIT_RETRY_DELAY_MS = 100;
|
|
11
|
+
const MAX_INIT_RETRIES = 50;
|
|
12
|
+
|
|
13
|
+
// URL validation debounce
|
|
14
|
+
const URL_VALIDATE_DEBOUNCE_MS = 1500; // Longer debounce to avoid rate limiting
|
|
15
|
+
|
|
16
|
+
// Unlock rate limiting
|
|
17
|
+
const MAX_UNLOCK_ATTEMPTS = 5;
|
|
18
|
+
const LOCKOUT_DURATION_MS = 60000;
|
|
19
|
+
|
|
20
|
+
// DOM elements
|
|
21
|
+
let onboardingScreen, unlockScreen;
|
|
22
|
+
let onboardUserName, onboardBotName, onboardGatewayUrl, onboardGatewayToken;
|
|
23
|
+
let urlStatus, urlError;
|
|
24
|
+
let onboardVoiceToggle, onboardEncryptToggle, onboardPasswordFields;
|
|
25
|
+
let onboardPassword, onboardPasswordConfirm, passwordError, onboardSubmit;
|
|
26
|
+
let unlockBotName, unlockPassword, unlockError, unlockSubmit;
|
|
27
|
+
let forgotLink, forgotConfirm, forgotCancel, forgotReset;
|
|
28
|
+
|
|
29
|
+
// Validation state
|
|
30
|
+
let urlValidateTimeout = null;
|
|
31
|
+
let urlValidState = null;
|
|
32
|
+
|
|
33
|
+
// Unlock rate limiting
|
|
34
|
+
let unlockAttempts = 0;
|
|
35
|
+
let unlockLockoutUntil = 0;
|
|
36
|
+
|
|
37
|
+
// Init retry tracking
|
|
38
|
+
let initRetryCount = 0;
|
|
39
|
+
|
|
40
|
+
function init() {
|
|
41
|
+
// Get DOM elements
|
|
42
|
+
onboardingScreen = document.getElementById('onboardingScreen');
|
|
43
|
+
unlockScreen = document.getElementById('unlockScreen');
|
|
44
|
+
|
|
45
|
+
if (!onboardingScreen || !unlockScreen) {
|
|
46
|
+
initRetryCount++;
|
|
47
|
+
if (initRetryCount >= MAX_INIT_RETRIES) {
|
|
48
|
+
console.error('Onboarding: Max retries reached, required DOM elements not found');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
console.warn(`Onboarding: Elements not found, retrying... (${initRetryCount}/${MAX_INIT_RETRIES})`);
|
|
52
|
+
setTimeout(init, INIT_RETRY_DELAY_MS);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Onboarding elements
|
|
57
|
+
onboardUserName = document.getElementById('onboardUserName');
|
|
58
|
+
onboardBotName = document.getElementById('onboardBotName');
|
|
59
|
+
onboardGatewayUrl = document.getElementById('onboardGatewayUrl');
|
|
60
|
+
onboardGatewayToken = document.getElementById('onboardGatewayToken');
|
|
61
|
+
urlStatus = document.getElementById('urlStatus');
|
|
62
|
+
urlError = document.getElementById('urlError');
|
|
63
|
+
onboardVoiceToggle = document.getElementById('onboardVoiceToggle');
|
|
64
|
+
onboardEncryptToggle = document.getElementById('onboardEncryptToggle');
|
|
65
|
+
onboardPasswordFields = document.getElementById('onboardPasswordFields');
|
|
66
|
+
onboardPassword = document.getElementById('onboardPassword');
|
|
67
|
+
onboardPasswordConfirm = document.getElementById('onboardPasswordConfirm');
|
|
68
|
+
passwordError = document.getElementById('passwordError');
|
|
69
|
+
onboardSubmit = document.getElementById('onboardSubmit');
|
|
70
|
+
|
|
71
|
+
// Unlock elements
|
|
72
|
+
unlockBotName = document.getElementById('unlockBotName');
|
|
73
|
+
unlockPassword = document.getElementById('unlockPassword');
|
|
74
|
+
unlockError = document.getElementById('unlockError');
|
|
75
|
+
unlockSubmit = document.getElementById('unlockSubmit');
|
|
76
|
+
forgotLink = document.getElementById('forgotLink');
|
|
77
|
+
forgotConfirm = document.getElementById('forgotConfirm');
|
|
78
|
+
forgotCancel = document.getElementById('forgotCancel');
|
|
79
|
+
forgotReset = document.getElementById('forgotReset');
|
|
80
|
+
|
|
81
|
+
// Set up event listeners
|
|
82
|
+
setupOnboardingEvents();
|
|
83
|
+
setupUnlockEvents();
|
|
84
|
+
|
|
85
|
+
// Check initial state (async)
|
|
86
|
+
checkState();
|
|
87
|
+
|
|
88
|
+
console.log('Onboarding: Initialized');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function checkState() {
|
|
92
|
+
// First check server config
|
|
93
|
+
try {
|
|
94
|
+
const serverRes = await fetch('/api/config/needs-onboarding');
|
|
95
|
+
const serverData = await serverRes.json();
|
|
96
|
+
|
|
97
|
+
if (serverData.needsOnboarding) {
|
|
98
|
+
// Load any existing server config to prefill
|
|
99
|
+
const configRes = await fetch('/api/config');
|
|
100
|
+
const config = await configRes.json();
|
|
101
|
+
|
|
102
|
+
if (onboardBotName && config.assistantName) {
|
|
103
|
+
onboardBotName.value = config.assistantName;
|
|
104
|
+
}
|
|
105
|
+
if (onboardGatewayUrl && config.gatewayUrl) {
|
|
106
|
+
onboardGatewayUrl.value = config.gatewayUrl;
|
|
107
|
+
}
|
|
108
|
+
if (onboardUserName && config.userName) {
|
|
109
|
+
onboardUserName.value = config.userName;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Show token status if already configured
|
|
113
|
+
if (config.hasGatewayToken && onboardGatewayToken) {
|
|
114
|
+
onboardGatewayToken.placeholder = 'Token already configured ✓ (leave blank to keep)';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
showOnboarding();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.warn('Onboarding: Could not reach server');
|
|
122
|
+
const errorEl = document.querySelector('.onboarding-error') || document.createElement('div');
|
|
123
|
+
errorEl.className = 'onboarding-error';
|
|
124
|
+
errorEl.textContent = 'Cannot reach Uplink server. Is it still running?';
|
|
125
|
+
const screen = document.getElementById('onboardingScreen');
|
|
126
|
+
if (screen && !screen.querySelector('.onboarding-error')) {
|
|
127
|
+
screen.querySelector('.overlay-card')?.prepend(errorEl);
|
|
128
|
+
}
|
|
129
|
+
showOnboarding();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Fall back to local config
|
|
134
|
+
const core = window.UplinkCore;
|
|
135
|
+
const config = core?.loadConfig();
|
|
136
|
+
|
|
137
|
+
if (!config || !config.gatewayUrl) {
|
|
138
|
+
// First visit - show onboarding
|
|
139
|
+
showOnboarding();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (config.encryptionEnabled) {
|
|
144
|
+
// Need to unlock
|
|
145
|
+
if (unlockBotName) unlockBotName.textContent = config.agentName || 'Assistant';
|
|
146
|
+
showUnlock();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Ready to go
|
|
151
|
+
hideAll();
|
|
152
|
+
window.dispatchEvent(new CustomEvent('uplink:unlocked'));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function showOnboarding() {
|
|
156
|
+
if (onboardingScreen) onboardingScreen.classList.add('visible');
|
|
157
|
+
// Auto-detect OpenClaw gateway
|
|
158
|
+
autoDetectGateway();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Auto-detect OpenClaw gateway at common locations
|
|
163
|
+
* If found, auto-fill URL and focus token field
|
|
164
|
+
*/
|
|
165
|
+
async function autoDetectGateway() {
|
|
166
|
+
// Skip if URL already filled
|
|
167
|
+
if (onboardGatewayUrl?.value.trim()) return;
|
|
168
|
+
|
|
169
|
+
const commonLocations = [
|
|
170
|
+
'http://localhost:18789',
|
|
171
|
+
'http://127.0.0.1:18789'
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
if (urlStatus) urlStatus.className = 'url-status checking';
|
|
175
|
+
|
|
176
|
+
for (const url of commonLocations) {
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetch('/api/gateway/validate', {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: { 'Content-Type': 'application/json' },
|
|
181
|
+
body: JSON.stringify({ url })
|
|
182
|
+
});
|
|
183
|
+
const data = await res.json();
|
|
184
|
+
|
|
185
|
+
if (data.valid) {
|
|
186
|
+
// Found OpenClaw!
|
|
187
|
+
if (onboardGatewayUrl) {
|
|
188
|
+
onboardGatewayUrl.value = url;
|
|
189
|
+
onboardGatewayUrl.classList.add('success');
|
|
190
|
+
onboardGatewayUrl.classList.remove('error');
|
|
191
|
+
}
|
|
192
|
+
if (urlStatus) urlStatus.className = 'url-status valid';
|
|
193
|
+
if (urlError) {
|
|
194
|
+
urlError.textContent = '✓ OpenClaw detected! Enter your token below.';
|
|
195
|
+
urlError.classList.add('visible');
|
|
196
|
+
urlError.style.color = 'var(--success-color, #4ade80)';
|
|
197
|
+
}
|
|
198
|
+
urlValidState = true;
|
|
199
|
+
|
|
200
|
+
// Focus token field
|
|
201
|
+
onboardGatewayToken?.focus();
|
|
202
|
+
console.log('Onboarding: Auto-detected OpenClaw at', url);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
} catch (e) {
|
|
206
|
+
// Continue to next location
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Not found - clear status and let user enter manually
|
|
211
|
+
if (urlStatus) urlStatus.className = 'url-status';
|
|
212
|
+
console.log('Onboarding: OpenClaw not detected, manual entry required');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function showUnlock() {
|
|
216
|
+
if (unlockScreen) {
|
|
217
|
+
unlockScreen.classList.add('visible');
|
|
218
|
+
unlockPassword?.focus();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function hideAll() {
|
|
223
|
+
onboardingScreen?.classList.remove('visible');
|
|
224
|
+
unlockScreen?.classList.remove('visible');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ========== Onboarding Events ==========
|
|
228
|
+
|
|
229
|
+
function setupOnboardingEvents() {
|
|
230
|
+
// URL validation
|
|
231
|
+
onboardGatewayUrl?.addEventListener('input', () => {
|
|
232
|
+
clearTimeout(urlValidateTimeout);
|
|
233
|
+
urlValidateTimeout = setTimeout(() => {
|
|
234
|
+
validateGatewayUrl(onboardGatewayUrl.value.trim());
|
|
235
|
+
}, URL_VALIDATE_DEBOUNCE_MS);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Voice toggle
|
|
239
|
+
onboardVoiceToggle?.addEventListener('click', () => {
|
|
240
|
+
onboardVoiceToggle.classList.toggle('on');
|
|
241
|
+
const enabled = onboardVoiceToggle.classList.contains('on');
|
|
242
|
+
onboardVoiceToggle.setAttribute('aria-checked', enabled ? 'true' : 'false');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Voice toggle keyboard handler
|
|
246
|
+
onboardVoiceToggle?.addEventListener('keydown', (e) => {
|
|
247
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
248
|
+
e.preventDefault();
|
|
249
|
+
onboardVoiceToggle.click();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Encryption toggle
|
|
254
|
+
onboardEncryptToggle?.addEventListener('click', () => {
|
|
255
|
+
onboardEncryptToggle.classList.toggle('on');
|
|
256
|
+
const enabled = onboardEncryptToggle.classList.contains('on');
|
|
257
|
+
onboardEncryptToggle.setAttribute('aria-checked', enabled ? 'true' : 'false');
|
|
258
|
+
onboardPasswordFields?.classList.toggle('visible', enabled);
|
|
259
|
+
if (enabled) onboardPassword?.focus();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Encryption toggle keyboard handler
|
|
263
|
+
onboardEncryptToggle?.addEventListener('keydown', (e) => {
|
|
264
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
265
|
+
e.preventDefault();
|
|
266
|
+
onboardEncryptToggle.click();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Submit
|
|
271
|
+
onboardSubmit?.addEventListener('click', submitOnboarding);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function validateGatewayUrl(url) {
|
|
275
|
+
if (!url) {
|
|
276
|
+
if (urlStatus) urlStatus.className = 'url-status';
|
|
277
|
+
urlValidState = null;
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Mobile keyboard fixes: trim whitespace, fix common autocorrect issues
|
|
282
|
+
url = url.trim().toLowerCase();
|
|
283
|
+
url = url.replace(/\s+/g, ''); // Remove any spaces mobile keyboards might add
|
|
284
|
+
url = url.replace(/local\s*host/gi, 'localhost'); // Fix "local host" autocorrect
|
|
285
|
+
|
|
286
|
+
// Auto-convert ws:// to http://
|
|
287
|
+
url = url.replace(/^ws:\/\//, 'http://').replace(/^wss:\/\//, 'https://');
|
|
288
|
+
|
|
289
|
+
// Add http:// if no protocol specified
|
|
290
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
291
|
+
url = 'http://' + url;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (onboardGatewayUrl) onboardGatewayUrl.value = url;
|
|
295
|
+
|
|
296
|
+
if (urlStatus) urlStatus.className = 'url-status checking';
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const res = await fetch('/api/gateway/validate', {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
headers: { 'Content-Type': 'application/json' },
|
|
302
|
+
body: JSON.stringify({ url })
|
|
303
|
+
});
|
|
304
|
+
const data = await res.json();
|
|
305
|
+
|
|
306
|
+
if (data.valid) {
|
|
307
|
+
if (urlStatus) urlStatus.className = 'url-status valid';
|
|
308
|
+
urlError?.classList.remove('visible');
|
|
309
|
+
onboardGatewayUrl?.classList.remove('error');
|
|
310
|
+
onboardGatewayUrl?.classList.add('success');
|
|
311
|
+
urlValidState = true;
|
|
312
|
+
return true;
|
|
313
|
+
} else {
|
|
314
|
+
if (urlStatus) urlStatus.className = 'url-status invalid';
|
|
315
|
+
if (urlError) {
|
|
316
|
+
urlError.textContent = data.error || 'Could not connect to gateway';
|
|
317
|
+
urlError.classList.add('visible');
|
|
318
|
+
urlError.style.color = ''; // Reset to default error color
|
|
319
|
+
}
|
|
320
|
+
onboardGatewayUrl?.classList.add('error');
|
|
321
|
+
onboardGatewayUrl?.classList.remove('success');
|
|
322
|
+
urlValidState = false;
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
} catch (e) {
|
|
326
|
+
if (urlStatus) urlStatus.className = 'url-status invalid';
|
|
327
|
+
if (urlError) {
|
|
328
|
+
urlError.textContent = 'Validation failed';
|
|
329
|
+
urlError.classList.add('visible');
|
|
330
|
+
urlError.style.color = ''; // Reset to default error color
|
|
331
|
+
}
|
|
332
|
+
urlValidState = false;
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function submitOnboarding() {
|
|
338
|
+
const userName = onboardUserName?.value.trim() || '';
|
|
339
|
+
const botName = onboardBotName?.value.trim() || 'Assistant';
|
|
340
|
+
const url = onboardGatewayUrl?.value.trim();
|
|
341
|
+
const token = onboardGatewayToken?.value.trim();
|
|
342
|
+
const voiceEnabled = onboardVoiceToggle?.classList.contains('on');
|
|
343
|
+
const encrypt = onboardEncryptToggle?.classList.contains('on');
|
|
344
|
+
const pass = onboardPassword?.value;
|
|
345
|
+
const passConfirm = onboardPasswordConfirm?.value;
|
|
346
|
+
|
|
347
|
+
// Validate URL
|
|
348
|
+
if (!url) {
|
|
349
|
+
if (urlError) {
|
|
350
|
+
urlError.textContent = 'Gateway URL is required';
|
|
351
|
+
urlError.classList.add('visible');
|
|
352
|
+
}
|
|
353
|
+
onboardGatewayUrl?.focus();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (urlValidState === null) {
|
|
358
|
+
const valid = await validateGatewayUrl(url);
|
|
359
|
+
if (!valid) return;
|
|
360
|
+
} else if (!urlValidState) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Validate password if encryption enabled
|
|
365
|
+
if (encrypt) {
|
|
366
|
+
if (!pass) {
|
|
367
|
+
if (passwordError) {
|
|
368
|
+
passwordError.textContent = 'Password is required';
|
|
369
|
+
passwordError.classList.add('visible');
|
|
370
|
+
}
|
|
371
|
+
onboardPassword?.focus();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (pass !== passConfirm) {
|
|
375
|
+
if (passwordError) {
|
|
376
|
+
passwordError.textContent = 'Passwords do not match';
|
|
377
|
+
passwordError.classList.add('visible');
|
|
378
|
+
}
|
|
379
|
+
onboardPasswordConfirm?.focus();
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (pass.length < 8) {
|
|
383
|
+
if (passwordError) {
|
|
384
|
+
passwordError.textContent = 'Password must be at least 8 characters';
|
|
385
|
+
passwordError.classList.add('visible');
|
|
386
|
+
}
|
|
387
|
+
onboardPassword?.focus();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
passwordError?.classList.remove('visible');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Save to server config
|
|
394
|
+
try {
|
|
395
|
+
const serverConfig = {
|
|
396
|
+
userName,
|
|
397
|
+
assistantName: botName,
|
|
398
|
+
gatewayUrl: url,
|
|
399
|
+
encryptHistory: encrypt,
|
|
400
|
+
onboardingComplete: true,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// Only include token if provided (server may have it from .env)
|
|
404
|
+
if (token) {
|
|
405
|
+
serverConfig.gatewayToken = token;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const res = await fetch('/api/config', {
|
|
409
|
+
method: 'POST',
|
|
410
|
+
headers: { 'Content-Type': 'application/json' },
|
|
411
|
+
body: JSON.stringify(serverConfig)
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
if (!res.ok) {
|
|
415
|
+
const data = await res.json();
|
|
416
|
+
if (urlError) {
|
|
417
|
+
urlError.textContent = data.error || 'Failed to save configuration';
|
|
418
|
+
urlError.classList.add('visible');
|
|
419
|
+
}
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
} catch (e) {
|
|
423
|
+
console.warn('Onboarding: Could not save to server, continuing with local config');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Save local config (for client-side features)
|
|
427
|
+
const core = window.UplinkCore;
|
|
428
|
+
if (core) {
|
|
429
|
+
core.state.agentName = botName;
|
|
430
|
+
core.state.gatewayUrl = url;
|
|
431
|
+
core.state.audioResponses = voiceEnabled;
|
|
432
|
+
core.state.encryptionEnabled = encrypt;
|
|
433
|
+
if (encrypt) core.state.currentPassword = pass;
|
|
434
|
+
core.saveConfig();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Hide onboarding
|
|
438
|
+
hideAll();
|
|
439
|
+
window.dispatchEvent(new CustomEvent('uplink:unlocked'));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ========== Unlock Events ==========
|
|
443
|
+
|
|
444
|
+
function setupUnlockEvents() {
|
|
445
|
+
unlockSubmit?.addEventListener('click', tryUnlock);
|
|
446
|
+
unlockPassword?.addEventListener('keydown', (e) => {
|
|
447
|
+
if (e.key === 'Enter') tryUnlock();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
forgotLink?.addEventListener('click', () => {
|
|
451
|
+
forgotConfirm?.classList.add('visible');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
forgotCancel?.addEventListener('click', () => {
|
|
455
|
+
forgotConfirm?.classList.remove('visible');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
forgotReset?.addEventListener('click', resetEncryption);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function tryUnlock() {
|
|
462
|
+
// Check lockout
|
|
463
|
+
if (Date.now() < unlockLockoutUntil) {
|
|
464
|
+
const remaining = Math.ceil((unlockLockoutUntil - Date.now()) / 1000);
|
|
465
|
+
if (unlockError) {
|
|
466
|
+
unlockError.textContent = `Too many attempts. Try again in ${remaining}s`;
|
|
467
|
+
unlockError.classList.add('visible');
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const pass = unlockPassword?.value;
|
|
473
|
+
if (!pass) {
|
|
474
|
+
if (unlockError) {
|
|
475
|
+
unlockError.textContent = 'Enter your password';
|
|
476
|
+
unlockError.classList.add('visible');
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Try to verify password
|
|
482
|
+
const crypto = window.UplinkEncryption;
|
|
483
|
+
if (crypto) {
|
|
484
|
+
const valid = await crypto.verifyPassword(pass);
|
|
485
|
+
if (!valid) {
|
|
486
|
+
unlockAttempts++;
|
|
487
|
+
if (unlockAttempts >= MAX_UNLOCK_ATTEMPTS) {
|
|
488
|
+
unlockLockoutUntil = Date.now() + LOCKOUT_DURATION_MS;
|
|
489
|
+
if (unlockError) unlockError.textContent = `Too many attempts. Try again in ${LOCKOUT_DURATION_MS / 1000}s`;
|
|
490
|
+
} else {
|
|
491
|
+
if (unlockError) {
|
|
492
|
+
unlockError.textContent = `Incorrect password (${MAX_UNLOCK_ATTEMPTS - unlockAttempts} attempts remaining)`;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
unlockError?.classList.add('visible');
|
|
496
|
+
unlockPassword?.select();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Success
|
|
502
|
+
unlockAttempts = 0;
|
|
503
|
+
const core = window.UplinkCore;
|
|
504
|
+
if (core) core.state.currentPassword = pass;
|
|
505
|
+
|
|
506
|
+
hideAll();
|
|
507
|
+
unlockError?.classList.remove('visible');
|
|
508
|
+
window.dispatchEvent(new CustomEvent('uplink:unlocked'));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function resetEncryption() {
|
|
512
|
+
// Clear encrypted data
|
|
513
|
+
localStorage.removeItem('uplink-history-encrypted');
|
|
514
|
+
localStorage.removeItem('uplink-history');
|
|
515
|
+
|
|
516
|
+
const crypto = window.UplinkEncryption;
|
|
517
|
+
if (crypto) crypto.clearEncryption();
|
|
518
|
+
|
|
519
|
+
// Disable encryption in config
|
|
520
|
+
const core = window.UplinkCore;
|
|
521
|
+
if (core) {
|
|
522
|
+
core.state.encryptionEnabled = false;
|
|
523
|
+
core.state.currentPassword = null;
|
|
524
|
+
core.saveConfig();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
hideAll();
|
|
528
|
+
window.dispatchEvent(new CustomEvent('uplink:unlocked'));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Export API
|
|
532
|
+
export const UplinkOnboarding = {
|
|
533
|
+
show: showOnboarding,
|
|
534
|
+
showUnlock,
|
|
535
|
+
hide: hideAll,
|
|
536
|
+
checkState
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// Backward compat: assign to window
|
|
540
|
+
window.UplinkOnboarding = UplinkOnboarding;
|
|
541
|
+
|
|
542
|
+
// Register and init
|
|
543
|
+
UplinkCore.registerModule('onboarding', init);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// PANELS MODULE
|
|
3
|
+
// Central panel management with mutual exclusivity
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
// Registered panels: { name: { element, isOpen(), open(), close() } }
|
|
7
|
+
const panels = {};
|
|
8
|
+
let currentPanel = null;
|
|
9
|
+
let backdrop = null;
|
|
10
|
+
|
|
11
|
+
// Create backdrop element
|
|
12
|
+
function createBackdrop() {
|
|
13
|
+
if (backdrop) return;
|
|
14
|
+
|
|
15
|
+
backdrop = document.createElement('div');
|
|
16
|
+
backdrop.className = 'panel-backdrop';
|
|
17
|
+
backdrop.addEventListener('click', () => closeAll());
|
|
18
|
+
document.body.appendChild(backdrop);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Show/hide backdrop
|
|
22
|
+
function showBackdrop() {
|
|
23
|
+
if (!backdrop) createBackdrop();
|
|
24
|
+
backdrop.classList.add('visible');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hideBackdrop() {
|
|
28
|
+
if (backdrop) backdrop.classList.remove('visible');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register a panel with the manager
|
|
33
|
+
* @param {string} name - Unique panel identifier
|
|
34
|
+
* @param {object} config - { element, isOpen, open, close }
|
|
35
|
+
*/
|
|
36
|
+
export function register(name, config) {
|
|
37
|
+
panels[name] = {
|
|
38
|
+
element: config.element,
|
|
39
|
+
isOpen: config.isOpen || (() => config.element?.classList.contains('visible')),
|
|
40
|
+
open: config.open || (() => config.element?.classList.add('visible')),
|
|
41
|
+
close: config.close || (() => config.element?.classList.remove('visible'))
|
|
42
|
+
};
|
|
43
|
+
console.log(`Panels: Registered "${name}"`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Open a panel (closes any currently open panel first)
|
|
48
|
+
* @param {string} name - Panel to open
|
|
49
|
+
* @returns {boolean} - Success
|
|
50
|
+
*/
|
|
51
|
+
export function open(name) {
|
|
52
|
+
if (!panels[name]) {
|
|
53
|
+
console.warn(`Panels: Unknown panel "${name}"`);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Close current panel if different
|
|
58
|
+
if (currentPanel && currentPanel !== name) {
|
|
59
|
+
close(currentPanel, true); // Skip hiding backdrop
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Open requested panel
|
|
63
|
+
panels[name].open();
|
|
64
|
+
currentPanel = name;
|
|
65
|
+
|
|
66
|
+
// Show backdrop
|
|
67
|
+
showBackdrop();
|
|
68
|
+
|
|
69
|
+
// Dispatch event for other components to react
|
|
70
|
+
window.dispatchEvent(new CustomEvent('panelOpened', { detail: { name } }));
|
|
71
|
+
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Close a specific panel
|
|
77
|
+
* @param {string} name - Panel to close
|
|
78
|
+
* @param {boolean} keepBackdrop - Don't hide backdrop (used when switching panels)
|
|
79
|
+
*/
|
|
80
|
+
export function close(name, keepBackdrop = false) {
|
|
81
|
+
if (!panels[name]) return;
|
|
82
|
+
|
|
83
|
+
panels[name].close();
|
|
84
|
+
if (currentPanel === name) {
|
|
85
|
+
currentPanel = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Hide backdrop unless we're switching panels
|
|
89
|
+
if (!keepBackdrop && !currentPanel) {
|
|
90
|
+
hideBackdrop();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
window.dispatchEvent(new CustomEvent('panelClosed', { detail: { name } }));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Close all panels
|
|
98
|
+
*/
|
|
99
|
+
export function closeAll() {
|
|
100
|
+
Object.keys(panels).forEach(name => close(name, true));
|
|
101
|
+
hideBackdrop();
|
|
102
|
+
currentPanel = null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Toggle a panel (open if closed, close if open)
|
|
107
|
+
* @param {string} name - Panel to toggle
|
|
108
|
+
* @returns {boolean} - New state (true = open)
|
|
109
|
+
*/
|
|
110
|
+
export function toggle(name) {
|
|
111
|
+
if (!panels[name]) {
|
|
112
|
+
console.warn(`Panels: Unknown panel "${name}"`);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (panels[name].isOpen()) {
|
|
117
|
+
close(name);
|
|
118
|
+
return false;
|
|
119
|
+
} else {
|
|
120
|
+
open(name);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if a panel is currently open
|
|
127
|
+
* @param {string} name - Panel to check
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
export function isOpen(name) {
|
|
131
|
+
return panels[name]?.isOpen() || false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get currently open panel name
|
|
136
|
+
* @returns {string|null}
|
|
137
|
+
*/
|
|
138
|
+
export function getCurrent() {
|
|
139
|
+
return currentPanel;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Export API
|
|
143
|
+
export const UplinkPanels = {
|
|
144
|
+
register,
|
|
145
|
+
open,
|
|
146
|
+
close,
|
|
147
|
+
closeAll,
|
|
148
|
+
toggle,
|
|
149
|
+
isOpen,
|
|
150
|
+
getCurrent
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Backward compat: assign to window
|
|
154
|
+
window.UplinkPanels = UplinkPanels;
|
|
155
|
+
|
|
156
|
+
console.log('Panels: Module loaded');
|