@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,352 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// FILES MODULE
|
|
3
|
+
// Drag & drop, enhanced file preview
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
import { UplinkCore } from './core.js';
|
|
7
|
+
|
|
8
|
+
const logger = window.UplinkLogger || console;
|
|
9
|
+
|
|
10
|
+
// State
|
|
11
|
+
let dropZoneVisible = false;
|
|
12
|
+
|
|
13
|
+
// Pending file state (previously on window.*)
|
|
14
|
+
let pendingImage = null;
|
|
15
|
+
let pendingFile = null;
|
|
16
|
+
|
|
17
|
+
// Create drop zone overlay
|
|
18
|
+
const dropZone = document.createElement('div');
|
|
19
|
+
dropZone.className = 'drop-zone';
|
|
20
|
+
dropZone.innerHTML = `
|
|
21
|
+
<div class="drop-zone-content">
|
|
22
|
+
<div class="drop-zone-icon">📎</div>
|
|
23
|
+
<div class="drop-zone-text">Drop files here</div>
|
|
24
|
+
<div class="drop-zone-hint">Images, PDFs, text files, code</div>
|
|
25
|
+
</div>
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
function init() {
|
|
29
|
+
const app = document.querySelector('.app');
|
|
30
|
+
if (!app) {
|
|
31
|
+
setTimeout(init, 100);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Add drop zone to DOM
|
|
36
|
+
app.appendChild(dropZone);
|
|
37
|
+
|
|
38
|
+
// Set up drag and drop listeners on the entire app
|
|
39
|
+
app.addEventListener('dragenter', handleDragEnter);
|
|
40
|
+
app.addEventListener('dragover', handleDragOver);
|
|
41
|
+
app.addEventListener('dragleave', handleDragLeave);
|
|
42
|
+
app.addEventListener('drop', handleDrop);
|
|
43
|
+
|
|
44
|
+
// Enhance file preview
|
|
45
|
+
enhanceFilePreview();
|
|
46
|
+
|
|
47
|
+
logger.debug('Files: Initialized');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function handleDragEnter(e) {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
e.stopPropagation();
|
|
53
|
+
|
|
54
|
+
// Only show for files
|
|
55
|
+
if (e.dataTransfer.types.includes('Files')) {
|
|
56
|
+
showDropZone();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function handleDragOver(e) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
e.stopPropagation();
|
|
63
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handleDragLeave(e) {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
e.stopPropagation();
|
|
69
|
+
|
|
70
|
+
// Only hide if leaving the app container
|
|
71
|
+
const rect = dropZone.getBoundingClientRect();
|
|
72
|
+
if (
|
|
73
|
+
e.clientX < rect.left ||
|
|
74
|
+
e.clientX > rect.right ||
|
|
75
|
+
e.clientY < rect.top ||
|
|
76
|
+
e.clientY > rect.bottom
|
|
77
|
+
) {
|
|
78
|
+
hideDropZone();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function handleDrop(e) {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
e.stopPropagation();
|
|
85
|
+
hideDropZone();
|
|
86
|
+
|
|
87
|
+
const files = Array.from(e.dataTransfer.files);
|
|
88
|
+
if (files.length === 0) return;
|
|
89
|
+
|
|
90
|
+
// Process the first file (can extend to handle multiple)
|
|
91
|
+
const file = files[0];
|
|
92
|
+
processFile(file);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function showDropZone() {
|
|
96
|
+
if (dropZoneVisible) return;
|
|
97
|
+
dropZoneVisible = true;
|
|
98
|
+
dropZone.classList.add('visible');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function hideDropZone() {
|
|
102
|
+
dropZoneVisible = false;
|
|
103
|
+
dropZone.classList.remove('visible');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function processFile(file) {
|
|
107
|
+
const reader = new FileReader();
|
|
108
|
+
|
|
109
|
+
reader.onload = (loadEvent) => {
|
|
110
|
+
const fileContent = loadEvent.target.result;
|
|
111
|
+
|
|
112
|
+
// Show preview based on file type
|
|
113
|
+
if (file.type.startsWith('image/')) {
|
|
114
|
+
showImagePreview(fileContent, file.name);
|
|
115
|
+
} else if (isTextFile(file)) {
|
|
116
|
+
showTextPreview(fileContent, file.name, file.type);
|
|
117
|
+
} else {
|
|
118
|
+
// PDF, DOCX, XLSX, and other binary files — store blob for upload
|
|
119
|
+
showGenericPreview(file.name, file.type, file.size);
|
|
120
|
+
pendingFile = {
|
|
121
|
+
blob: file,
|
|
122
|
+
name: file.name,
|
|
123
|
+
type: file.type || 'application/octet-stream',
|
|
124
|
+
size: file.size,
|
|
125
|
+
isText: false,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Read as appropriate format
|
|
131
|
+
if (file.type.startsWith('image/')) {
|
|
132
|
+
reader.readAsDataURL(file);
|
|
133
|
+
} else if (isTextFile(file)) {
|
|
134
|
+
reader.readAsText(file);
|
|
135
|
+
} else {
|
|
136
|
+
// For binary files, still trigger onload but we use the raw File object
|
|
137
|
+
reader.readAsArrayBuffer(file);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isTextFile(file) {
|
|
142
|
+
const textTypes = [
|
|
143
|
+
'text/',
|
|
144
|
+
'application/json',
|
|
145
|
+
'application/javascript',
|
|
146
|
+
'application/xml',
|
|
147
|
+
'application/x-yaml'
|
|
148
|
+
];
|
|
149
|
+
const textExtensions = [
|
|
150
|
+
'.txt', '.md', '.json', '.js', '.ts', '.py', '.html', '.css',
|
|
151
|
+
'.csv', '.xml', '.yaml', '.yml', '.sh', '.bat', '.ps1', '.sql',
|
|
152
|
+
'.env', '.gitignore', '.dockerfile'
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
if (textTypes.some(t => file.type.startsWith(t))) return true;
|
|
156
|
+
if (textExtensions.some(ext => file.name.toLowerCase().endsWith(ext))) return true;
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function showImagePreview(dataUrl, filename) {
|
|
161
|
+
const preview = document.getElementById('imagePreview');
|
|
162
|
+
const previewImg = document.getElementById('previewImg');
|
|
163
|
+
const fileLabel = preview.querySelector('.file-label');
|
|
164
|
+
|
|
165
|
+
previewImg.src = dataUrl;
|
|
166
|
+
previewImg.style.display = 'block';
|
|
167
|
+
if (fileLabel) fileLabel.style.display = 'none';
|
|
168
|
+
preview.classList.add('visible');
|
|
169
|
+
|
|
170
|
+
// Store for sending
|
|
171
|
+
pendingImage = dataUrl;
|
|
172
|
+
pendingFile = { data: dataUrl, name: filename, type: 'image' };
|
|
173
|
+
|
|
174
|
+
// Scroll messages to bottom so preview doesn't hide last message
|
|
175
|
+
requestAnimationFrame(() => {
|
|
176
|
+
const messagesEl = document.getElementById('messages');
|
|
177
|
+
if (messagesEl) messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function showPdfPreview(filename) {
|
|
182
|
+
const preview = document.getElementById('imagePreview');
|
|
183
|
+
const previewImg = document.getElementById('previewImg');
|
|
184
|
+
|
|
185
|
+
previewImg.style.display = 'none';
|
|
186
|
+
|
|
187
|
+
let fileLabel = preview.querySelector('.file-label');
|
|
188
|
+
if (!fileLabel) {
|
|
189
|
+
fileLabel = document.createElement('div');
|
|
190
|
+
fileLabel.className = 'file-label';
|
|
191
|
+
preview.appendChild(fileLabel);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fileLabel.textContent = `📄 ${filename}`;
|
|
195
|
+
fileLabel.style.display = 'block';
|
|
196
|
+
preview.classList.add('visible');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function showTextPreview(content, filename, mimeType) {
|
|
200
|
+
const preview = document.getElementById('imagePreview');
|
|
201
|
+
const previewImg = document.getElementById('previewImg');
|
|
202
|
+
|
|
203
|
+
previewImg.style.display = 'none';
|
|
204
|
+
|
|
205
|
+
let fileLabel = preview.querySelector('.file-label');
|
|
206
|
+
if (!fileLabel) {
|
|
207
|
+
fileLabel = document.createElement('div');
|
|
208
|
+
fileLabel.className = 'file-label';
|
|
209
|
+
preview.appendChild(fileLabel);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Show filename with icon based on type
|
|
213
|
+
const icon = getFileIcon(filename);
|
|
214
|
+
fileLabel.textContent = `${icon} ${filename}`;
|
|
215
|
+
fileLabel.style.display = 'block';
|
|
216
|
+
preview.classList.add('visible');
|
|
217
|
+
|
|
218
|
+
// Store content for sending
|
|
219
|
+
pendingFile = {
|
|
220
|
+
data: content,
|
|
221
|
+
name: filename,
|
|
222
|
+
type: mimeType || 'text/plain',
|
|
223
|
+
isText: true
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function showGenericPreview(filename, mimeType, size) {
|
|
228
|
+
const preview = document.getElementById('imagePreview');
|
|
229
|
+
const previewImg = document.getElementById('previewImg');
|
|
230
|
+
|
|
231
|
+
previewImg.style.display = 'none';
|
|
232
|
+
|
|
233
|
+
let fileLabel = preview.querySelector('.file-label');
|
|
234
|
+
if (!fileLabel) {
|
|
235
|
+
fileLabel = document.createElement('div');
|
|
236
|
+
fileLabel.className = 'file-label';
|
|
237
|
+
preview.appendChild(fileLabel);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const icon = getFileIcon(filename);
|
|
241
|
+
const sizeStr = formatFileSize(size);
|
|
242
|
+
// Safely build HTML: icon + filename (text) + size span
|
|
243
|
+
fileLabel.innerHTML = '';
|
|
244
|
+
fileLabel.appendChild(document.createTextNode(`${icon} ${filename} `));
|
|
245
|
+
const sizeSpan = document.createElement('span');
|
|
246
|
+
sizeSpan.className = 'file-size';
|
|
247
|
+
sizeSpan.textContent = `(${sizeStr})`;
|
|
248
|
+
fileLabel.appendChild(sizeSpan);
|
|
249
|
+
fileLabel.style.display = 'block';
|
|
250
|
+
preview.classList.add('visible');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getFileIcon(filename) {
|
|
254
|
+
const ext = filename.split('.').pop().toLowerCase();
|
|
255
|
+
const icons = {
|
|
256
|
+
pdf: '📄',
|
|
257
|
+
doc: '📝', docx: '📝',
|
|
258
|
+
xls: '📊', xlsx: '📊', csv: '📊',
|
|
259
|
+
ppt: '📽️', pptx: '📽️',
|
|
260
|
+
zip: '🗜️', rar: '🗜️', '7z': '🗜️',
|
|
261
|
+
mp3: '🎵', wav: '🎵', flac: '🎵',
|
|
262
|
+
mp4: '🎬', mov: '🎬', avi: '🎬',
|
|
263
|
+
js: '📜', ts: '📜', py: '🐍', json: '📋',
|
|
264
|
+
html: '🌐', css: '🎨',
|
|
265
|
+
md: '📑', txt: '📄'
|
|
266
|
+
};
|
|
267
|
+
return icons[ext] || '📁';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function formatFileSize(bytes) {
|
|
271
|
+
if (bytes < 1024) return bytes + ' B';
|
|
272
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
273
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function enhanceFilePreview() {
|
|
277
|
+
// Get the existing file input
|
|
278
|
+
const fileInput = document.getElementById('fileInput');
|
|
279
|
+
if (!fileInput) return;
|
|
280
|
+
|
|
281
|
+
// Accept all files — validation happens server-side
|
|
282
|
+
// Removed restrictive accept list (caused file dialog freezes on Windows/Chrome)
|
|
283
|
+
|
|
284
|
+
// Override the existing change handler to use our enhanced preview
|
|
285
|
+
const originalHandler = fileInput.onchange;
|
|
286
|
+
fileInput.addEventListener('change', (e) => {
|
|
287
|
+
const file = e.target.files[0];
|
|
288
|
+
if (file) {
|
|
289
|
+
processFile(file);
|
|
290
|
+
fileInput.value = ''; // Reset for re-selection
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Wire up the remove/cancel button on the image preview
|
|
295
|
+
const removeBtn = document.getElementById('removePreview');
|
|
296
|
+
if (removeBtn) {
|
|
297
|
+
removeBtn.addEventListener('click', (e) => {
|
|
298
|
+
e.preventDefault();
|
|
299
|
+
e.stopPropagation();
|
|
300
|
+
const preview = document.getElementById('imagePreview');
|
|
301
|
+
const previewImg = document.getElementById('previewImg');
|
|
302
|
+
if (preview) preview.classList.remove('visible');
|
|
303
|
+
if (previewImg) {
|
|
304
|
+
previewImg.src = '';
|
|
305
|
+
previewImg.style.display = 'none';
|
|
306
|
+
}
|
|
307
|
+
const fileLabel = preview?.querySelector('.file-label');
|
|
308
|
+
if (fileLabel) fileLabel.style.display = 'none';
|
|
309
|
+
// Clear pending state
|
|
310
|
+
pendingImage = null;
|
|
311
|
+
pendingFile = null;
|
|
312
|
+
logger.debug('Files: Preview cleared');
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function destroy() {
|
|
318
|
+
const app = document.getElementById('app');
|
|
319
|
+
if (app) {
|
|
320
|
+
app.removeEventListener('dragenter', handleDragEnter);
|
|
321
|
+
app.removeEventListener('dragover', handleDragOver);
|
|
322
|
+
app.removeEventListener('dragleave', handleDragLeave);
|
|
323
|
+
app.removeEventListener('drop', handleDrop);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (dropZone.parentNode) {
|
|
327
|
+
dropZone.parentNode.removeChild(dropZone);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
logger.debug('Files: Destroyed');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Expose for external use
|
|
334
|
+
export const UplinkFiles = {
|
|
335
|
+
process: processFile,
|
|
336
|
+
showDropZone,
|
|
337
|
+
hideDropZone,
|
|
338
|
+
destroy,
|
|
339
|
+
getFileIcon,
|
|
340
|
+
// Pending file state accessors (replaces window.pendingImage/pendingFile)
|
|
341
|
+
getPendingImage: () => pendingImage,
|
|
342
|
+
getPendingFile: () => pendingFile,
|
|
343
|
+
clearPending: () => { pendingImage = null; pendingFile = null; },
|
|
344
|
+
setPendingImage: (img) => { pendingImage = img; },
|
|
345
|
+
setPendingFile: (file) => { pendingFile = file; }
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// Backward compat: assign to window
|
|
349
|
+
window.UplinkFiles = UplinkFiles;
|
|
350
|
+
|
|
351
|
+
// Register with core for coordinated initialization
|
|
352
|
+
UplinkCore.registerModule('files', init);
|