@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,581 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// SLASH COMMANDS MODULE
|
|
3
|
+
// Handles /command parsing, execution, and autocomplete
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
import { UplinkCore } from './core.js';
|
|
7
|
+
|
|
8
|
+
// Command registry
|
|
9
|
+
const commands = {
|
|
10
|
+
help: {
|
|
11
|
+
description: 'Show available commands',
|
|
12
|
+
usage: '/help',
|
|
13
|
+
execute: showHelp
|
|
14
|
+
},
|
|
15
|
+
clear: {
|
|
16
|
+
description: 'Clear conversation history',
|
|
17
|
+
usage: '/clear',
|
|
18
|
+
execute: clearConversation
|
|
19
|
+
},
|
|
20
|
+
export: {
|
|
21
|
+
description: 'Export conversation',
|
|
22
|
+
usage: '/export [format]',
|
|
23
|
+
args: ['markdown', 'json'],
|
|
24
|
+
execute: exportConversation
|
|
25
|
+
},
|
|
26
|
+
status: {
|
|
27
|
+
description: 'Show connection and session status',
|
|
28
|
+
usage: '/status',
|
|
29
|
+
execute: showStatus
|
|
30
|
+
},
|
|
31
|
+
voice: {
|
|
32
|
+
description: 'Toggle voice mode',
|
|
33
|
+
usage: '/voice',
|
|
34
|
+
execute: toggleVoice
|
|
35
|
+
},
|
|
36
|
+
model: {
|
|
37
|
+
description: 'Show current model info',
|
|
38
|
+
usage: '/model',
|
|
39
|
+
execute: showModel
|
|
40
|
+
},
|
|
41
|
+
theme: {
|
|
42
|
+
description: 'Change color theme',
|
|
43
|
+
usage: '/theme [name]',
|
|
44
|
+
args: ['dark', 'light', 'midnight', 'sunset', 'forest', 'high-contrast'],
|
|
45
|
+
execute: changeTheme
|
|
46
|
+
},
|
|
47
|
+
stats: {
|
|
48
|
+
description: 'Show usage dashboard',
|
|
49
|
+
usage: '/stats',
|
|
50
|
+
execute: showDashboard
|
|
51
|
+
},
|
|
52
|
+
remind: {
|
|
53
|
+
description: 'Set a reminder',
|
|
54
|
+
usage: '/remind <time> <message>',
|
|
55
|
+
args: ['5m', '15m', '30m', '1h', '2h', 'tomorrow'],
|
|
56
|
+
execute: setReminder
|
|
57
|
+
},
|
|
58
|
+
satellites: {
|
|
59
|
+
description: 'Open satellites panel',
|
|
60
|
+
usage: '/satellites',
|
|
61
|
+
execute: () => window.UplinkSatellites?.toggleNavigator()
|
|
62
|
+
},
|
|
63
|
+
share: {
|
|
64
|
+
description: 'Create shareable link for this conversation',
|
|
65
|
+
usage: '/share [title]',
|
|
66
|
+
execute: createShareLink
|
|
67
|
+
},
|
|
68
|
+
memdump: {
|
|
69
|
+
description: 'Dump current session state to memory file',
|
|
70
|
+
usage: '/memdump',
|
|
71
|
+
execute: () => sendAgentCommand('/m')
|
|
72
|
+
},
|
|
73
|
+
m: {
|
|
74
|
+
description: 'Shortcut for /memdump',
|
|
75
|
+
usage: '/m',
|
|
76
|
+
execute: () => sendAgentCommand('/m')
|
|
77
|
+
},
|
|
78
|
+
compact: {
|
|
79
|
+
description: 'Compact session context',
|
|
80
|
+
usage: '/compact',
|
|
81
|
+
execute: () => sendAgentCommand('/compact')
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Commands that should be passed through to the gateway as-is
|
|
86
|
+
// (OpenClaw native slash commands — gateway intercepts these before the agent)
|
|
87
|
+
const gatewayPassthroughPrefixes = [
|
|
88
|
+
'help', 'commands', 'skill', 'status', 'allowlist', 'approve',
|
|
89
|
+
'context', 'tts', 'whoami', 'subagents', 'config', 'debug',
|
|
90
|
+
'usage', 'stop', 'restart', 'activation', 'send', 'reset',
|
|
91
|
+
'new', 'compact', 'think', 'verbose', 'reasoning', 'elevated',
|
|
92
|
+
'exec', 'model', 'models', 'queue', 'bash', 'prose'
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// Autocomplete state
|
|
96
|
+
let autocompleteVisible = false;
|
|
97
|
+
let autocompleteIndex = 0;
|
|
98
|
+
let filteredCommands = [];
|
|
99
|
+
|
|
100
|
+
// Create autocomplete dropdown with ARIA attributes
|
|
101
|
+
const autocompleteEl = document.createElement('div');
|
|
102
|
+
autocompleteEl.className = 'command-autocomplete';
|
|
103
|
+
autocompleteEl.style.display = 'none';
|
|
104
|
+
autocompleteEl.id = 'command-autocomplete-listbox';
|
|
105
|
+
autocompleteEl.role = 'listbox';
|
|
106
|
+
|
|
107
|
+
// Initialize when DOM is ready
|
|
108
|
+
function init() {
|
|
109
|
+
const textInput = document.getElementById('textInput');
|
|
110
|
+
const inputArea = document.querySelector('.input-area');
|
|
111
|
+
|
|
112
|
+
if (!textInput || !inputArea) {
|
|
113
|
+
logger.warn('Commands: Required elements not found, retrying...');
|
|
114
|
+
setTimeout(init, 100);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Insert autocomplete before input row
|
|
119
|
+
const textInputRow = document.getElementById('textInputRow');
|
|
120
|
+
textInputRow.parentNode.insertBefore(autocompleteEl, textInputRow);
|
|
121
|
+
|
|
122
|
+
// Add ARIA attributes to input for accessibility
|
|
123
|
+
textInput.role = 'combobox';
|
|
124
|
+
textInput.setAttribute('aria-autocomplete', 'list');
|
|
125
|
+
textInput.setAttribute('aria-controls', 'command-autocomplete-listbox');
|
|
126
|
+
textInput.setAttribute('aria-expanded', 'false');
|
|
127
|
+
textInput.setAttribute('aria-activedescendant', '');
|
|
128
|
+
|
|
129
|
+
// Listen for input changes
|
|
130
|
+
textInput.addEventListener('input', handleInput);
|
|
131
|
+
textInput.addEventListener('keydown', handleKeydown);
|
|
132
|
+
textInput.addEventListener('blur', () => {
|
|
133
|
+
// Delay hide to allow click on autocomplete
|
|
134
|
+
setTimeout(hideAutocomplete, 150);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Share button handler
|
|
138
|
+
const shareBtn = document.getElementById('shareBtn');
|
|
139
|
+
shareBtn?.addEventListener('click', () => createShareLink(''));
|
|
140
|
+
|
|
141
|
+
logger.debug('Commands: Initialized');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function handleInput(e) {
|
|
145
|
+
const value = e.target.value;
|
|
146
|
+
|
|
147
|
+
if (value.startsWith('/')) {
|
|
148
|
+
const query = value.slice(1).toLowerCase();
|
|
149
|
+
filteredCommands = Object.keys(commands).filter(cmd =>
|
|
150
|
+
cmd.startsWith(query)
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (filteredCommands.length > 0 && !value.includes(' ')) {
|
|
154
|
+
showAutocomplete(filteredCommands);
|
|
155
|
+
} else {
|
|
156
|
+
hideAutocomplete();
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
hideAutocomplete();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function handleKeydown(e) {
|
|
164
|
+
if (!autocompleteVisible) {
|
|
165
|
+
// Check if Enter on a command
|
|
166
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
167
|
+
const value = e.target.value.trim();
|
|
168
|
+
if (value.startsWith('/')) {
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
e.stopPropagation();
|
|
171
|
+
executeCommand(value);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
switch (e.key) {
|
|
179
|
+
case 'ArrowDown':
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
autocompleteIndex = Math.min(autocompleteIndex + 1, filteredCommands.length - 1);
|
|
182
|
+
updateAutocompleteSelection();
|
|
183
|
+
break;
|
|
184
|
+
case 'ArrowUp':
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
autocompleteIndex = Math.max(autocompleteIndex - 1, 0);
|
|
187
|
+
updateAutocompleteSelection();
|
|
188
|
+
break;
|
|
189
|
+
case 'Tab':
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
selectAutocomplete(filteredCommands[autocompleteIndex], false);
|
|
192
|
+
break;
|
|
193
|
+
case 'Enter':
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
e.stopPropagation();
|
|
196
|
+
selectAutocomplete(filteredCommands[autocompleteIndex], true);
|
|
197
|
+
break;
|
|
198
|
+
case 'Escape':
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
hideAutocomplete();
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function showAutocomplete(cmds) {
|
|
206
|
+
autocompleteEl.innerHTML = cmds.map((cmd, i) => `
|
|
207
|
+
<div class="command-option ${i === 0 ? 'selected' : ''}" data-command="${cmd}" role="option" id="command-option-${i}" aria-selected="${i === 0 ? 'true' : 'false'}">
|
|
208
|
+
<span class="command-name">/${cmd}</span>
|
|
209
|
+
<span class="command-desc">${commands[cmd].description}</span>
|
|
210
|
+
</div>
|
|
211
|
+
`).join('');
|
|
212
|
+
|
|
213
|
+
// Add click handlers
|
|
214
|
+
autocompleteEl.querySelectorAll('.command-option').forEach(el => {
|
|
215
|
+
el.addEventListener('click', () => {
|
|
216
|
+
selectAutocomplete(el.dataset.command, true);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
autocompleteEl.style.display = 'block';
|
|
221
|
+
autocompleteVisible = true;
|
|
222
|
+
autocompleteIndex = 0;
|
|
223
|
+
|
|
224
|
+
// Update ARIA attributes on input
|
|
225
|
+
const textInput = document.getElementById('textInput');
|
|
226
|
+
textInput.setAttribute('aria-expanded', 'true');
|
|
227
|
+
if (cmds.length > 0) {
|
|
228
|
+
textInput.setAttribute('aria-activedescendant', 'command-option-0');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function hideAutocomplete() {
|
|
233
|
+
autocompleteEl.style.display = 'none';
|
|
234
|
+
autocompleteVisible = false;
|
|
235
|
+
autocompleteIndex = 0;
|
|
236
|
+
|
|
237
|
+
// Update ARIA attributes on input
|
|
238
|
+
const textInput = document.getElementById('textInput');
|
|
239
|
+
if (textInput) {
|
|
240
|
+
textInput.setAttribute('aria-expanded', 'false');
|
|
241
|
+
textInput.setAttribute('aria-activedescendant', '');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function updateAutocompleteSelection() {
|
|
246
|
+
const textInput = document.getElementById('textInput');
|
|
247
|
+
|
|
248
|
+
autocompleteEl.querySelectorAll('.command-option').forEach((el, i) => {
|
|
249
|
+
const isSelected = i === autocompleteIndex;
|
|
250
|
+
el.classList.toggle('selected', isSelected);
|
|
251
|
+
el.setAttribute('aria-selected', isSelected ? 'true' : 'false');
|
|
252
|
+
|
|
253
|
+
if (isSelected && textInput) {
|
|
254
|
+
textInput.setAttribute('aria-activedescendant', `command-option-${i}`);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function selectAutocomplete(cmd, execute = false) {
|
|
260
|
+
const textInput = document.getElementById('textInput');
|
|
261
|
+
hideAutocomplete();
|
|
262
|
+
|
|
263
|
+
if (execute) {
|
|
264
|
+
// Execute the command directly
|
|
265
|
+
textInput.value = '';
|
|
266
|
+
executeCommand('/' + cmd);
|
|
267
|
+
} else {
|
|
268
|
+
// Just fill in the command (Tab was pressed)
|
|
269
|
+
textInput.value = '/' + cmd + ' ';
|
|
270
|
+
textInput.focus();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function executeCommand(input) {
|
|
275
|
+
const textInput = document.getElementById('textInput');
|
|
276
|
+
textInput.value = '';
|
|
277
|
+
|
|
278
|
+
const parts = input.slice(1).split(' ');
|
|
279
|
+
const cmdName = parts[0].toLowerCase();
|
|
280
|
+
const args = parts.slice(1);
|
|
281
|
+
|
|
282
|
+
// Check Uplink-local commands first
|
|
283
|
+
const cmd = commands[cmdName];
|
|
284
|
+
if (cmd) {
|
|
285
|
+
cmd.execute(args);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Check if it's a Gateway passthrough command (OpenClaw native)
|
|
290
|
+
const isGatewayCmd = gatewayPassthroughPrefixes.some(p => cmdName === p || cmdName.startsWith(p + '.'));
|
|
291
|
+
if (isGatewayCmd) {
|
|
292
|
+
// Send as a regular message — Gateway intercepts native /commands
|
|
293
|
+
sendAgentCommand(input);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Unknown command — still pass through to Gateway in case it's a native command
|
|
298
|
+
// Gateway will handle it or the agent will see it as a message
|
|
299
|
+
sendAgentCommand(input);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Send a message to the AI agent as a command
|
|
303
|
+
function sendAgentCommand(message) {
|
|
304
|
+
if (window.UplinkChat?.sendTextMessage) {
|
|
305
|
+
window.UplinkChat.sendTextMessage(message);
|
|
306
|
+
} else {
|
|
307
|
+
addSystemMessage('Chat module not available.');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Command implementations
|
|
312
|
+
function showHelp() {
|
|
313
|
+
const helpText = Object.entries(commands).map(([name, cmd]) =>
|
|
314
|
+
`**/${name}** — ${cmd.description}`
|
|
315
|
+
).join('\n');
|
|
316
|
+
|
|
317
|
+
addSystemMessage('**Available Commands:**\n' + helpText);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function clearConversation() {
|
|
321
|
+
if (confirm('Clear chat history? Your assistant still remembers the conversation.')) {
|
|
322
|
+
const messages = document.getElementById('messages');
|
|
323
|
+
const emptyState = document.getElementById('emptyState');
|
|
324
|
+
messages.innerHTML = '';
|
|
325
|
+
emptyState.style.display = 'flex';
|
|
326
|
+
messages.appendChild(emptyState);
|
|
327
|
+
|
|
328
|
+
// Clear from storage
|
|
329
|
+
localStorage.removeItem('uplink-history');
|
|
330
|
+
localStorage.removeItem('uplink-history-encrypted');
|
|
331
|
+
|
|
332
|
+
addSystemMessage('Chat cleared.');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function exportConversation(args) {
|
|
337
|
+
const format = args[0] || 'markdown';
|
|
338
|
+
const messages = document.querySelectorAll('.message:not(.system)');
|
|
339
|
+
|
|
340
|
+
if (messages.length === 0) {
|
|
341
|
+
addSystemMessage('No messages to export.');
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let content;
|
|
346
|
+
let filename;
|
|
347
|
+
let mimeType;
|
|
348
|
+
|
|
349
|
+
if (format === 'json') {
|
|
350
|
+
const data = Array.from(messages).map(msg => ({
|
|
351
|
+
role: msg.classList.contains('user') ? 'user' : 'assistant',
|
|
352
|
+
content: msg.textContent,
|
|
353
|
+
timestamp: new Date().toISOString()
|
|
354
|
+
}));
|
|
355
|
+
content = JSON.stringify(data, null, 2);
|
|
356
|
+
filename = `uplink-export-${Date.now()}.json`;
|
|
357
|
+
mimeType = 'application/json';
|
|
358
|
+
} else {
|
|
359
|
+
// Markdown format
|
|
360
|
+
const lines = Array.from(messages).map(msg => {
|
|
361
|
+
const role = msg.classList.contains('user') ? '**You:**' : '**Assistant:**';
|
|
362
|
+
return `${role}\n${msg.textContent}\n`;
|
|
363
|
+
});
|
|
364
|
+
content = `# Uplink Conversation Export\n\n${lines.join('\n---\n\n')}`;
|
|
365
|
+
filename = `uplink-export-${Date.now()}.md`;
|
|
366
|
+
mimeType = 'text/markdown';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Download
|
|
370
|
+
const blob = new Blob([content], { type: mimeType });
|
|
371
|
+
const url = URL.createObjectURL(blob);
|
|
372
|
+
const a = document.createElement('a');
|
|
373
|
+
a.href = url;
|
|
374
|
+
a.download = filename;
|
|
375
|
+
a.click();
|
|
376
|
+
URL.revokeObjectURL(url);
|
|
377
|
+
|
|
378
|
+
addSystemMessage(`Exported as ${filename}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function showStatus() {
|
|
382
|
+
const statusDot = document.querySelector('.status-dot');
|
|
383
|
+
const isConnected = statusDot && statusDot.style.background !== 'var(--error)';
|
|
384
|
+
|
|
385
|
+
// Get session info from localStorage
|
|
386
|
+
const config = JSON.parse(localStorage.getItem('uplink-config') || '{}');
|
|
387
|
+
const encrypted = config.encryptionEnabled ? 'Yes' : 'No';
|
|
388
|
+
|
|
389
|
+
// Count messages
|
|
390
|
+
const messageCount = document.querySelectorAll('.message:not(.system)').length;
|
|
391
|
+
|
|
392
|
+
const status = `**Status:**
|
|
393
|
+
• Connection: ${isConnected ? '🟢 Connected' : '🔴 Disconnected'}
|
|
394
|
+
• Gateway: ${config.gatewayUrl || 'Not configured'}
|
|
395
|
+
• Encryption: ${encrypted}
|
|
396
|
+
• Messages: ${messageCount}
|
|
397
|
+
• Session: Active`;
|
|
398
|
+
|
|
399
|
+
addSystemMessage(status);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function toggleVoice() {
|
|
403
|
+
const voiceModeTab = document.getElementById('voiceModeTab');
|
|
404
|
+
const textModeTab = document.getElementById('textModeTab');
|
|
405
|
+
const isVoice = voiceModeTab.classList.contains('active');
|
|
406
|
+
|
|
407
|
+
if (isVoice) {
|
|
408
|
+
textModeTab.click();
|
|
409
|
+
addSystemMessage('Switched to text mode.');
|
|
410
|
+
} else {
|
|
411
|
+
voiceModeTab.click();
|
|
412
|
+
addSystemMessage('Switched to voice mode.');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function showModel() {
|
|
417
|
+
const config = JSON.parse(localStorage.getItem('uplink-config') || '{}');
|
|
418
|
+
const gatewayUrl = config.gatewayUrl || 'http://localhost:18789';
|
|
419
|
+
|
|
420
|
+
const voiceName = config.voiceName || config.assistantName || 'Default';
|
|
421
|
+
const assistantName = config.assistantName || 'Assistant';
|
|
422
|
+
|
|
423
|
+
const modelInfo = `**Model Info:**
|
|
424
|
+
• Gateway: ${gatewayUrl}
|
|
425
|
+
• Model: openclaw (routed by gateway)
|
|
426
|
+
• Voice: ${voiceName} (ElevenLabs)
|
|
427
|
+
• Assistant: ${assistantName}
|
|
428
|
+
|
|
429
|
+
*Model selection is managed by the OpenClaw gateway.*`;
|
|
430
|
+
|
|
431
|
+
addSystemMessage(modelInfo);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function changeTheme(args) {
|
|
435
|
+
const themeName = args[0];
|
|
436
|
+
|
|
437
|
+
if (!themeName) {
|
|
438
|
+
// Show current theme and available options
|
|
439
|
+
const currentTheme = window.UplinkThemes?.get() || localStorage.getItem('uplink-theme') || 'dark';
|
|
440
|
+
const available = window.UplinkThemes?.list() || ['dark', 'light', 'midnight', 'sunset', 'forest', 'high-contrast'];
|
|
441
|
+
addSystemMessage(`**Current theme:** ${currentTheme}\n**Available:** ${available.join(', ')}`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Validate theme name
|
|
446
|
+
const available = window.UplinkThemes?.list() || ['dark', 'light', 'midnight', 'sunset', 'forest', 'high-contrast'];
|
|
447
|
+
if (!available.includes(themeName)) {
|
|
448
|
+
addSystemMessage(`Unknown theme: ${themeName}\n**Available:** ${available.join(', ')}`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Use the themes module if available
|
|
453
|
+
if (window.UplinkThemes && typeof window.UplinkThemes.apply === 'function') {
|
|
454
|
+
window.UplinkThemes.apply(themeName);
|
|
455
|
+
addSystemMessage(`Theme changed to: ${themeName}`);
|
|
456
|
+
} else {
|
|
457
|
+
// Fallback: try to set directly
|
|
458
|
+
document.documentElement.setAttribute('data-theme', themeName);
|
|
459
|
+
localStorage.setItem('uplink-theme', themeName);
|
|
460
|
+
addSystemMessage(`Theme changed to: ${themeName}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function showDashboard() {
|
|
465
|
+
if (window.UplinkDashboard) {
|
|
466
|
+
window.UplinkDashboard.show();
|
|
467
|
+
} else {
|
|
468
|
+
addSystemMessage('Dashboard module not loaded.');
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function setReminder(args) {
|
|
473
|
+
if (args.length < 2) {
|
|
474
|
+
addSystemMessage(`**Usage:** /remind <time> <message>\n\n**Examples:**\n• /remind 30m check the build\n• /remind 2h call mom\n• /remind tomorrow morning brief`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const timeArg = args[0];
|
|
479
|
+
const message = args.slice(1).join(' ');
|
|
480
|
+
|
|
481
|
+
// Format as a natural language request for the AI to process
|
|
482
|
+
const request = `Please set a reminder for ${timeArg} from now: "${message}"`;
|
|
483
|
+
|
|
484
|
+
// Send through chat (AI will use cron tool)
|
|
485
|
+
if (window.UplinkChat?.sendTextMessage) {
|
|
486
|
+
window.UplinkChat.sendTextMessage(request);
|
|
487
|
+
} else {
|
|
488
|
+
addSystemMessage('Chat module not available.');
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function createShareLink(args) {
|
|
493
|
+
const title = args.length > 0 ? args.join(' ') : null;
|
|
494
|
+
|
|
495
|
+
// Get conversation messages
|
|
496
|
+
const storage = window.UplinkStorage;
|
|
497
|
+
if (!storage) {
|
|
498
|
+
addSystemMessage('Storage module not available.');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const messages = storage.getHistory?.() || [];
|
|
503
|
+
|
|
504
|
+
if (messages.length === 0) {
|
|
505
|
+
addSystemMessage('No messages to share.');
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
addSystemMessage('Creating share link...');
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
const response = await fetch('/api/share/create', {
|
|
513
|
+
method: 'POST',
|
|
514
|
+
headers: { 'Content-Type': 'application/json' },
|
|
515
|
+
body: JSON.stringify({
|
|
516
|
+
title: title || `Conversation on ${new Date().toLocaleDateString()}`,
|
|
517
|
+
messages: messages.slice(-50) // Share last 50 messages max
|
|
518
|
+
})
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
if (!response.ok) {
|
|
522
|
+
addSystemMessage(`Error: Server returned ${response.status}`);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const data = await response.json();
|
|
526
|
+
|
|
527
|
+
if (data.error) {
|
|
528
|
+
addSystemMessage(`Error: ${data.error}`);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const shareUrl = window.location.origin + data.shareUrl;
|
|
533
|
+
|
|
534
|
+
// Copy to clipboard
|
|
535
|
+
try {
|
|
536
|
+
await navigator.clipboard.writeText(shareUrl);
|
|
537
|
+
addSystemMessage(`**Share link created!**\n\n${shareUrl}\n\n*(Copied to clipboard - expires in 7 days)*`);
|
|
538
|
+
} catch (e) {
|
|
539
|
+
addSystemMessage(`**Share link created!**\n\n${shareUrl}\n\n*(Expires in 7 days)*`);
|
|
540
|
+
}
|
|
541
|
+
} catch (e) {
|
|
542
|
+
addSystemMessage(`Failed to create share link: ${e.message}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Helper to add system messages
|
|
547
|
+
function addSystemMessage(text) {
|
|
548
|
+
// Use the global addMessage if available, otherwise create our own
|
|
549
|
+
if (typeof window.addMessage === 'function') {
|
|
550
|
+
window.addMessage(text, 'system');
|
|
551
|
+
} else {
|
|
552
|
+
const messages = document.getElementById('messages');
|
|
553
|
+
const emptyState = document.getElementById('emptyState');
|
|
554
|
+
if (emptyState) emptyState.style.display = 'none';
|
|
555
|
+
|
|
556
|
+
const div = document.createElement('div');
|
|
557
|
+
div.className = 'message system';
|
|
558
|
+
div.innerHTML = formatMarkdown(text);
|
|
559
|
+
messages.appendChild(div);
|
|
560
|
+
messages.scrollTop = messages.scrollHeight;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Simple markdown formatter for system messages
|
|
565
|
+
function formatMarkdown(text) {
|
|
566
|
+
return text
|
|
567
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
568
|
+
.replace(/\n/g, '<br>');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Expose for external use
|
|
572
|
+
export const UplinkCommands = {
|
|
573
|
+
execute: executeCommand,
|
|
574
|
+
list: () => Object.keys(commands)
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// Backward compat: assign to window
|
|
578
|
+
window.UplinkCommands = UplinkCommands;
|
|
579
|
+
|
|
580
|
+
// Register with core for coordinated initialization
|
|
581
|
+
UplinkCore.registerModule('commands', init);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// CONNECTION API MODULE
|
|
3
|
+
// HTTP API calls for chat endpoints
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
const logger = window.logger || console;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check server health
|
|
10
|
+
*/
|
|
11
|
+
export async function checkServerHealth() {
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch('/api/status', { method: 'GET', cache: 'no-cache' });
|
|
14
|
+
return res.ok;
|
|
15
|
+
} catch (e) {
|
|
16
|
+
logger.debug('Connection: Server health check failed', e);
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Fetch chat history
|
|
23
|
+
*/
|
|
24
|
+
export async function fetchChatHistory(satelliteId = 'main') {
|
|
25
|
+
try {
|
|
26
|
+
const url = satelliteId === 'main'
|
|
27
|
+
? '/api/history'
|
|
28
|
+
: `/api/history?satelliteId=${encodeURIComponent(satelliteId)}`;
|
|
29
|
+
|
|
30
|
+
const res = await fetch(url, { method: 'GET' });
|
|
31
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
32
|
+
|
|
33
|
+
const data = await res.json();
|
|
34
|
+
return data.messages || [];
|
|
35
|
+
} catch (err) {
|
|
36
|
+
logger.error('Connection: Failed to fetch history', err);
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Send a chat message (without streaming)
|
|
43
|
+
*/
|
|
44
|
+
export async function sendMessage(message, satelliteId = 'main', agentId = 'main') {
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch('/api/chat', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
message,
|
|
51
|
+
stream: false,
|
|
52
|
+
satelliteId,
|
|
53
|
+
agentId
|
|
54
|
+
})
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
58
|
+
|
|
59
|
+
const data = await res.json();
|
|
60
|
+
return data.response;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
logger.error('Connection: Failed to send message', err);
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Upload a file
|
|
69
|
+
*/
|
|
70
|
+
export async function uploadFile(file, satelliteId = 'main') {
|
|
71
|
+
const formData = new FormData();
|
|
72
|
+
formData.append('file', file);
|
|
73
|
+
if (satelliteId !== 'main') {
|
|
74
|
+
formData.append('satelliteId', satelliteId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch('/api/upload', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
body: formData
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
84
|
+
|
|
85
|
+
const data = await res.json();
|
|
86
|
+
return data.url;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
logger.error('Connection: Failed to upload file', err);
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Clear chat history
|
|
95
|
+
*/
|
|
96
|
+
export async function clearHistory(satelliteId = 'main') {
|
|
97
|
+
try {
|
|
98
|
+
const url = satelliteId === 'main'
|
|
99
|
+
? '/api/clear'
|
|
100
|
+
: `/api/clear?satelliteId=${encodeURIComponent(satelliteId)}`;
|
|
101
|
+
|
|
102
|
+
const res = await fetch(url, { method: 'POST' });
|
|
103
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
104
|
+
|
|
105
|
+
return true;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
logger.error('Connection: Failed to clear history', err);
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Expose on window for backward compatibility
|
|
113
|
+
if (typeof window !== 'undefined') {
|
|
114
|
+
window.UplinkConnectionAPI = {
|
|
115
|
+
checkServerHealth,
|
|
116
|
+
fetchChatHistory,
|
|
117
|
+
sendMessage,
|
|
118
|
+
uploadFile,
|
|
119
|
+
clearHistory
|
|
120
|
+
};
|
|
121
|
+
}
|