@kolbo/mcp 1.86.3 → 1.86.4
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.
- package/package.json +1 -1
- package/src/apps/widgets/upload.js +456 -457
- package/src/tools/media.js +656 -656
|
@@ -1,457 +1,456 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { widgetPage } = require('../html');
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Upload widget — media_upload_widget tool.
|
|
7
|
-
*
|
|
8
|
-
* Lets claude.ai users upload LOCAL files (images / video / audio / documents)
|
|
9
|
-
* into their Kolbo media library from inside the chat. Chat attachments are
|
|
10
|
-
* unreachable from remote MCP servers — this widget is the bridge: the file is
|
|
11
|
-
* POSTed straight from the iframe to api.kolbo.ai/mcp/upload with a
|
|
12
|
-
* short-lived, upload-only ticket (never the user's API key).
|
|
13
|
-
*
|
|
14
|
-
* Mobile (Claude iOS/Android): WebKit drops <input type=file> selections inside
|
|
15
|
-
* cross-origin MCP App iframes, so the primary CTA openLinks a TOP-LEVEL
|
|
16
|
-
* upload page (/mcp/upload-ui) where the native picker works. Desktop keeps
|
|
17
|
-
* the in-chat picker; empty-selection after pick also offers the external page.
|
|
18
|
-
*
|
|
19
|
-
* structuredContent: {
|
|
20
|
-
* widget: 'upload', title, upload_url, upload_ui_url, token, expires_at (epoch ms),
|
|
21
|
-
* accept (input accept attr), max_files, max_mb: {image,video,audio,document},
|
|
22
|
-
* project_id?
|
|
23
|
-
* }
|
|
24
|
-
*
|
|
25
|
-
* Flow (desktop): pick/drop files -> client-side type+size validation -> XHR
|
|
26
|
-
* upload (2 concurrent, per-file progress + thumbnails) -> per-file CDN URL.
|
|
27
|
-
* Every completed file is pushed into the model context silently; the
|
|
28
|
-
* "Use these files" button sends one chat message with all URLs.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
const BODY = `
|
|
32
|
-
<div class="k-card">
|
|
33
|
-
<div class="k-head">
|
|
34
|
-
<span class="k-logo" id="logo"></span>
|
|
35
|
-
<span class="k-title" id="title">Upload media</span>
|
|
36
|
-
<span class="k-spacer"></span>
|
|
37
|
-
<span class="k-chip" id="count-chip" style="display:none"></span>
|
|
38
|
-
</div>
|
|
39
|
-
<div class="k-body">
|
|
40
|
-
<div id="drop" style="border:1.5px dashed var(--border);border-radius:12px;padding:22px 14px;text-align:center;cursor:pointer;transition:border-color .15s,background .15s;-webkit-tap-highlight-color:transparent">
|
|
41
|
-
<div id="drop-icon" style="font-size:26px;line-height:1;margin-bottom:8px;color:var(--text-muted)"></div>
|
|
42
|
-
<div id="drop-title" style="font-size:13px;font-weight:600">Click or drop files here</div>
|
|
43
|
-
<div id="accept-hint" style="font-size:11.5px;color:var(--text-muted);margin-top:4px;line-height:1.35"></div>
|
|
44
|
-
</div>
|
|
45
|
-
<input type="file" id="picker" multiple accept="*/*" style="position:absolute;width:1px;height:1px;opacity:0;overflow:hidden;clip:rect(0,0,0,0)">
|
|
46
|
-
<div id="rows" style="margin-top:10px"></div>
|
|
47
|
-
<div class="k-actions" id="actions" style="display:none"></div>
|
|
48
|
-
<div id="notice" style="display:none;margin-top:8px;font-size:12px;color:var(--text-muted);line-height:1.4"></div>
|
|
49
|
-
</div>
|
|
50
|
-
<div class="k-footer">
|
|
51
|
-
<span><a href="#" id="kolbo-link">Kolbo.AI</a> Media Library</span>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
el('
|
|
59
|
-
el('
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
var
|
|
63
|
-
var
|
|
64
|
-
var
|
|
65
|
-
var
|
|
66
|
-
var
|
|
67
|
-
var
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
var
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (mime.indexOf('
|
|
94
|
-
if (mime.indexOf('
|
|
95
|
-
if (mime.indexOf('
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (b
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
var
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
el('notice').
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
var
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
var
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
el('
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
el('
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
el('actions').
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
el('btn-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
el('drop').style.
|
|
180
|
-
el('
|
|
181
|
-
el('notice').
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
};
|
|
198
|
-
el('drop').
|
|
199
|
-
el('drop').
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
el('picker').
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
el('notice').
|
|
211
|
-
el('
|
|
212
|
-
el('actions').style.
|
|
213
|
-
el('
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
var
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
c.
|
|
239
|
-
c.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
} catch (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
var
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
var
|
|
261
|
-
var
|
|
262
|
-
var
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
var next =
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
fd.append('
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
xhr.
|
|
297
|
-
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
var
|
|
305
|
-
var
|
|
306
|
-
var
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
it.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
it.
|
|
334
|
-
it.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
it.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
it.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
var
|
|
360
|
-
|
|
361
|
-
if (it.status === '
|
|
362
|
-
else if (it.status === '
|
|
363
|
-
else
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
'<span style="
|
|
375
|
-
'<span style="
|
|
376
|
-
'
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
el('rows').
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
var
|
|
392
|
-
|
|
393
|
-
if (items[i].
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
var
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
});
|
|
411
|
-
var
|
|
412
|
-
|
|
413
|
-
el('count-chip').
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
el('actions').style.
|
|
417
|
-
|
|
418
|
-
'<button class="k-btn ghost" id="btn-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
el('actions').
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
module.exports = { uploadWidgetHtml };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { widgetPage } = require('../html');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Upload widget — media_upload_widget tool.
|
|
7
|
+
*
|
|
8
|
+
* Lets claude.ai users upload LOCAL files (images / video / audio / documents)
|
|
9
|
+
* into their Kolbo media library from inside the chat. Chat attachments are
|
|
10
|
+
* unreachable from remote MCP servers — this widget is the bridge: the file is
|
|
11
|
+
* POSTed straight from the iframe to api.kolbo.ai/mcp/upload with a
|
|
12
|
+
* short-lived, upload-only ticket (never the user's API key).
|
|
13
|
+
*
|
|
14
|
+
* Mobile (Claude iOS/Android): WebKit drops <input type=file> selections inside
|
|
15
|
+
* cross-origin MCP App iframes, so the primary CTA openLinks a TOP-LEVEL
|
|
16
|
+
* upload page (/mcp/upload-ui) where the native picker works. Desktop keeps
|
|
17
|
+
* the in-chat picker; empty-selection after pick also offers the external page.
|
|
18
|
+
*
|
|
19
|
+
* structuredContent: {
|
|
20
|
+
* widget: 'upload', title, upload_url, upload_ui_url, token, expires_at (epoch ms),
|
|
21
|
+
* accept (input accept attr), max_files, max_mb: {image,video,audio,document},
|
|
22
|
+
* project_id?
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* Flow (desktop): pick/drop files -> client-side type+size validation -> XHR
|
|
26
|
+
* upload (2 concurrent, per-file progress + thumbnails) -> per-file CDN URL.
|
|
27
|
+
* Every completed file is pushed into the model context silently; the
|
|
28
|
+
* "Use these files" button sends one chat message with all URLs.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const BODY = `
|
|
32
|
+
<div class="k-card">
|
|
33
|
+
<div class="k-head">
|
|
34
|
+
<span class="k-logo" id="logo"></span>
|
|
35
|
+
<span class="k-title" id="title">Upload media</span>
|
|
36
|
+
<span class="k-spacer"></span>
|
|
37
|
+
<span class="k-chip" id="count-chip" style="display:none"></span>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="k-body">
|
|
40
|
+
<div id="drop" style="border:1.5px dashed var(--border);border-radius:12px;padding:22px 14px;text-align:center;cursor:pointer;transition:border-color .15s,background .15s;-webkit-tap-highlight-color:transparent">
|
|
41
|
+
<div id="drop-icon" style="font-size:26px;line-height:1;margin-bottom:8px;color:var(--text-muted)"></div>
|
|
42
|
+
<div id="drop-title" style="font-size:13px;font-weight:600">Click or drop files here</div>
|
|
43
|
+
<div id="accept-hint" style="font-size:11.5px;color:var(--text-muted);margin-top:4px;line-height:1.35"></div>
|
|
44
|
+
</div>
|
|
45
|
+
<input type="file" id="picker" multiple accept="*/*" style="position:absolute;width:1px;height:1px;opacity:0;overflow:hidden;clip:rect(0,0,0,0)">
|
|
46
|
+
<div id="rows" style="margin-top:10px"></div>
|
|
47
|
+
<div class="k-actions" id="actions" style="display:none"></div>
|
|
48
|
+
<div id="notice" style="display:none;margin-top:8px;font-size:12px;color:var(--text-muted);line-height:1.4"></div>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="k-footer">
|
|
51
|
+
<span><a href="#" id="kolbo-link">Kolbo.AI</a> Media Library</span>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const SCRIPT = `
|
|
57
|
+
el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
|
|
58
|
+
el('drop-icon').innerHTML = ICONS.upload;
|
|
59
|
+
el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink('https://app.kolbo.ai/media-library'); };
|
|
60
|
+
|
|
61
|
+
var state = null;
|
|
62
|
+
var items = []; // {file, kind, status, pct, url, err, id, thumb}
|
|
63
|
+
var nextItemId = 1;
|
|
64
|
+
var CONCURRENCY = 2;
|
|
65
|
+
var active = 0;
|
|
66
|
+
var sent = false;
|
|
67
|
+
var pickerArmed = false;
|
|
68
|
+
|
|
69
|
+
var KINDS = {
|
|
70
|
+
image: { exts: ['jpg','jpeg','png','webp','gif','heic','heif','avif','bmp','tif','tiff'], icon: ICONS.image },
|
|
71
|
+
video: { exts: ['mp4','mov','webm','m4v','mkv','avi'], icon: ICONS.video },
|
|
72
|
+
audio: { exts: ['mp3','wav','m4a','aac','ogg','flac'], icon: ICONS.audio },
|
|
73
|
+
document: { exts: ['pdf','txt','md','csv','json','docx','xlsx','pptx','doc','xls'], icon: ICONS.document }
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function isMobileHost() {
|
|
77
|
+
var ua = navigator.userAgent || '';
|
|
78
|
+
if (/iPhone|iPad|iPod|Android/i.test(ua)) return true;
|
|
79
|
+
// iPadOS desktop-UA spoof
|
|
80
|
+
if (navigator.maxTouchPoints > 1 && /MacIntel/.test(navigator.platform || '')) return true;
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function classify(file) {
|
|
85
|
+
var name = (file && file.name) || '';
|
|
86
|
+
var ext = name.indexOf('.') >= 0 ? String(name).split('.').pop().toLowerCase() : '';
|
|
87
|
+
if (ext) {
|
|
88
|
+
for (var k in KINDS) { if (KINDS[k].exts.indexOf(ext) !== -1) return k; }
|
|
89
|
+
}
|
|
90
|
+
// Mobile Photos often yields a MIME with a weak/missing filename.
|
|
91
|
+
var mime = String((file && file.type) || '').toLowerCase();
|
|
92
|
+
if (mime.indexOf('image/') === 0) return 'image';
|
|
93
|
+
if (mime.indexOf('video/') === 0) return 'video';
|
|
94
|
+
if (mime.indexOf('audio/') === 0) return 'audio';
|
|
95
|
+
if (mime === 'application/pdf' || mime.indexOf('text/') === 0 || mime.indexOf('application/vnd.') === 0 || mime === 'application/msword' || mime === 'application/json') return 'document';
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function filenameFor(it) {
|
|
100
|
+
var n = (it.file && it.file.name) || '';
|
|
101
|
+
if (n && n.indexOf('.') !== -1) return n;
|
|
102
|
+
var ext = { image: 'jpg', video: 'mp4', audio: 'mp3', document: 'pdf' }[it.kind] || 'bin';
|
|
103
|
+
return 'upload-' + it.id + '.' + ext;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function fmtSize(b) {
|
|
107
|
+
if (b == null) return '';
|
|
108
|
+
if (b > 1024 * 1024) return (Math.round(b / 1024 / 102.4) / 10) + 'MB';
|
|
109
|
+
return Math.max(1, Math.round(b / 1024)) + 'KB';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function expired() { return state && state.expires_at && Date.now() > state.expires_at; }
|
|
113
|
+
|
|
114
|
+
function uploadUiUrl() {
|
|
115
|
+
if (!state) return '';
|
|
116
|
+
var base = state.upload_ui_url || String(state.upload_url || '').replace(/\\/upload\\/?$/, '/upload-ui');
|
|
117
|
+
if (!base) return '';
|
|
118
|
+
var cfg = {
|
|
119
|
+
token: state.token,
|
|
120
|
+
upload_url: state.upload_url,
|
|
121
|
+
title: state.title || 'Upload media',
|
|
122
|
+
kinds: state.kinds,
|
|
123
|
+
max_files: state.max_files,
|
|
124
|
+
max_mb: state.max_mb,
|
|
125
|
+
project_id: state.project_id,
|
|
126
|
+
expires_at: state.expires_at
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
var json = JSON.stringify(cfg);
|
|
130
|
+
var b64 = btoa(unescape(encodeURIComponent(json))).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');
|
|
131
|
+
return base + '#' + b64;
|
|
132
|
+
} catch (e) {
|
|
133
|
+
return base + '#' + encodeURIComponent(JSON.stringify(cfg));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function openExternalUploader() {
|
|
138
|
+
var url = uploadUiUrl();
|
|
139
|
+
if (!url) return;
|
|
140
|
+
window.kolbo.openLink(url);
|
|
141
|
+
el('notice').style.display = '';
|
|
142
|
+
el('notice').innerHTML = ICONS.open + ' Uploader opened. After files finish, return here and paste the copied message into Claude.';
|
|
143
|
+
window.kolbo.notifySize();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function boot(sc) {
|
|
147
|
+
if (!sc || sc.widget !== 'upload') return;
|
|
148
|
+
state = sc;
|
|
149
|
+
if (sc.title) el('title').textContent = sc.title;
|
|
150
|
+
var kinds = sc.kinds && sc.kinds.length ? sc.kinds : ['image','video','audio','document'];
|
|
151
|
+
var exts = [];
|
|
152
|
+
kinds.forEach(function (k) { if (KINDS[k]) exts = exts.concat(KINDS[k].exts); });
|
|
153
|
+
el('picker').setAttribute('accept', exts.map(function (e) { return '.' + e; }).concat(kinds.map(function (k) { return k + '/*'; })).join(','));
|
|
154
|
+
var maxN = sc.max_files || 20;
|
|
155
|
+
var mobile = isMobileHost();
|
|
156
|
+
if (mobile) {
|
|
157
|
+
el('drop-title').textContent = 'Tap to open uploader';
|
|
158
|
+
el('accept-hint').textContent = kinds.join(' · ') + (maxN === 1 ? ' — one file' : ' — up to ' + maxN + ' files') + '. Opens a full-screen picker (required on iPhone & Android).';
|
|
159
|
+
} else {
|
|
160
|
+
el('drop-title').textContent = 'Click or drop files here';
|
|
161
|
+
el('accept-hint').textContent = kinds.join(' · ') + (maxN === 1 ? ' — one file' : ' — up to ' + maxN + ' files');
|
|
162
|
+
}
|
|
163
|
+
if (expired()) return showExpired();
|
|
164
|
+
// Always offer the external path — desktop users may be inside a host that
|
|
165
|
+
// also sandboxes file inputs; mobile uses it as the primary CTA.
|
|
166
|
+
el('actions').style.display = '';
|
|
167
|
+
el('actions').innerHTML = mobile
|
|
168
|
+
? '<button class="k-btn primary" id="btn-external" style="width:100%;min-height:44px">' + ICONS.upload + ' Open uploader</button>' +
|
|
169
|
+
'<button class="k-btn ghost" id="btn-inline">Try in-chat picker</button>'
|
|
170
|
+
: '<button class="k-btn ghost" id="btn-external">' + ICONS.open + ' Open full-screen uploader</button>';
|
|
171
|
+
el('btn-external').onclick = function (e) { e.preventDefault(); e.stopPropagation(); openExternalUploader(); };
|
|
172
|
+
var inline = el('btn-inline');
|
|
173
|
+
if (inline) inline.onclick = function (e) { e.preventDefault(); e.stopPropagation(); armInlinePicker(); };
|
|
174
|
+
window.kolbo.notifySize();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function showExpired() {
|
|
178
|
+
el('drop').style.pointerEvents = 'none';
|
|
179
|
+
el('drop').style.opacity = '0.5';
|
|
180
|
+
el('notice').style.display = '';
|
|
181
|
+
el('notice').innerHTML = ICONS.clock + ' This upload window expired. Ask Claude to open a new upload widget.';
|
|
182
|
+
window.kolbo.notifySize();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function armInlinePicker() {
|
|
186
|
+
pickerArmed = true;
|
|
187
|
+
el('picker').click();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---- picking ----
|
|
191
|
+
el('drop').onclick = function () {
|
|
192
|
+
if (!state) return;
|
|
193
|
+
if (expired()) return showExpired();
|
|
194
|
+
if (isMobileHost()) return openExternalUploader();
|
|
195
|
+
armInlinePicker();
|
|
196
|
+
};
|
|
197
|
+
el('drop').ondragover = function (e) { e.preventDefault(); el('drop').style.borderColor = 'var(--accent, #7c6cff)'; };
|
|
198
|
+
el('drop').ondragleave = function () { el('drop').style.borderColor = 'var(--border)'; };
|
|
199
|
+
el('drop').ondrop = function (e) {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
el('drop').style.borderColor = 'var(--border)';
|
|
202
|
+
addFiles(e.dataTransfer && e.dataTransfer.files);
|
|
203
|
+
};
|
|
204
|
+
el('picker').onchange = function () {
|
|
205
|
+
var files = el('picker').files;
|
|
206
|
+
// iOS/WebKit cross-origin iframe: picker UI runs, selection is dropped → empty FileList.
|
|
207
|
+
if (pickerArmed && (!files || !files.length)) {
|
|
208
|
+
pickerArmed = false;
|
|
209
|
+
el('notice').style.display = '';
|
|
210
|
+
el('notice').innerHTML = ICONS.warn + ' In-chat picker could not receive the file on this device. Use the full-screen uploader instead.';
|
|
211
|
+
el('actions').style.display = '';
|
|
212
|
+
el('actions').innerHTML = '<button class="k-btn primary" id="btn-external" style="width:100%;min-height:44px">' + ICONS.upload + ' Open uploader</button>';
|
|
213
|
+
el('btn-external').onclick = function (e) { e.preventDefault(); openExternalUploader(); };
|
|
214
|
+
window.kolbo.notifySize();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
pickerArmed = false;
|
|
218
|
+
addFiles(files);
|
|
219
|
+
el('picker').value = '';
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
function makeThumb(it) {
|
|
223
|
+
if (it.kind === 'image') {
|
|
224
|
+
try { it.thumb = URL.createObjectURL(it.file); } catch (e) {}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (it.kind !== 'video') return;
|
|
228
|
+
try {
|
|
229
|
+
var url = URL.createObjectURL(it.file);
|
|
230
|
+
var v = document.createElement('video');
|
|
231
|
+
v.muted = true; v.playsInline = true; v.preload = 'metadata'; v.src = url;
|
|
232
|
+
var done = false;
|
|
233
|
+
function snap() {
|
|
234
|
+
if (done) return;
|
|
235
|
+
try {
|
|
236
|
+
var c = document.createElement('canvas');
|
|
237
|
+
c.width = 68; c.height = 68;
|
|
238
|
+
c.getContext('2d').drawImage(v, 0, 0, 68, 68);
|
|
239
|
+
it.thumb = c.toDataURL('image/jpeg', 0.7);
|
|
240
|
+
done = true;
|
|
241
|
+
render();
|
|
242
|
+
} catch (e) {}
|
|
243
|
+
try { URL.revokeObjectURL(url); } catch (e2) {}
|
|
244
|
+
}
|
|
245
|
+
v.addEventListener('loadeddata', function () {
|
|
246
|
+
try { v.currentTime = Math.min(0.25, (v.duration || 1) * 0.1); } catch (e) { snap(); }
|
|
247
|
+
});
|
|
248
|
+
v.addEventListener('seeked', snap);
|
|
249
|
+
setTimeout(snap, 1500);
|
|
250
|
+
} catch (e) {}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function addFiles(list) {
|
|
254
|
+
if (!list || !state) return;
|
|
255
|
+
if (expired()) return showExpired();
|
|
256
|
+
var maxFiles = state.max_files || 20;
|
|
257
|
+
for (var i = 0; i < list.length; i++) {
|
|
258
|
+
if (items.length >= maxFiles) break;
|
|
259
|
+
var f = list[i];
|
|
260
|
+
var kind = classify(f);
|
|
261
|
+
var it = { file: f, kind: kind, status: 'queued', pct: 0, url: null, err: null, id: nextItemId++, thumb: null };
|
|
262
|
+
var allowedKinds = state.kinds && state.kinds.length ? state.kinds : ['image','video','audio','document'];
|
|
263
|
+
if (!kind || allowedKinds.indexOf(kind) === -1) {
|
|
264
|
+
it.status = 'error'; it.err = 'Unsupported file type';
|
|
265
|
+
} else {
|
|
266
|
+
var capMb = (state.max_mb && state.max_mb[kind]) || 50;
|
|
267
|
+
if (f.size > capMb * 1024 * 1024) { it.status = 'error'; it.err = kind + ' files are limited to ' + capMb + 'MB'; }
|
|
268
|
+
else makeThumb(it);
|
|
269
|
+
}
|
|
270
|
+
items.push(it);
|
|
271
|
+
}
|
|
272
|
+
render();
|
|
273
|
+
pump();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---- upload queue ----
|
|
277
|
+
function pump() {
|
|
278
|
+
if (expired()) return showExpired();
|
|
279
|
+
while (active < CONCURRENCY) {
|
|
280
|
+
var next = null;
|
|
281
|
+
for (var i = 0; i < items.length; i++) { if (items[i].status === 'queued') { next = items[i]; break; } }
|
|
282
|
+
if (!next) break;
|
|
283
|
+
upload(next);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function upload(it) {
|
|
288
|
+
it.status = 'uploading';
|
|
289
|
+
active++;
|
|
290
|
+
render();
|
|
291
|
+
var fd = new FormData();
|
|
292
|
+
fd.append('file', it.file, filenameFor(it));
|
|
293
|
+
if (state.project_id) fd.append('project_id', state.project_id);
|
|
294
|
+
var xhr = new XMLHttpRequest();
|
|
295
|
+
xhr.open('POST', state.upload_url, true);
|
|
296
|
+
xhr.setRequestHeader('Authorization', 'Bearer ' + state.token);
|
|
297
|
+
// A stalled upload used to sit at N% forever: there was no timeout and no
|
|
298
|
+
// ontimeout/onabort handler, only onload/onerror. A file above the CDN body
|
|
299
|
+
// cap in front of the API dies exactly this way — the edge sees Content-Length,
|
|
300
|
+
// kills the connection a few percent in, and the browser never reports it, so
|
|
301
|
+
// the row froze at 1% with no error and no retry. Watch PROGRESS rather than
|
|
302
|
+
// total elapsed time, so a genuinely slow large upload is never punished.
|
|
303
|
+
var STALL_MS = 90000;
|
|
304
|
+
var lastTick = Date.now();
|
|
305
|
+
var stalled = false;
|
|
306
|
+
var watchdog = setInterval(function () {
|
|
307
|
+
if (Date.now() - lastTick < STALL_MS) return;
|
|
308
|
+
stalled = true;
|
|
309
|
+
clearInterval(watchdog);
|
|
310
|
+
try { xhr.abort(); } catch (e) {}
|
|
311
|
+
}, 5000);
|
|
312
|
+
function settle() { clearInterval(watchdog); }
|
|
313
|
+
xhr.upload.onprogress = function (e) {
|
|
314
|
+
lastTick = Date.now();
|
|
315
|
+
if (e.lengthComputable) { it.pct = Math.round((e.loaded / e.total) * 100); renderRow(it); }
|
|
316
|
+
};
|
|
317
|
+
xhr.onabort = function () {
|
|
318
|
+
settle();
|
|
319
|
+
if (!stalled) return;
|
|
320
|
+
active--;
|
|
321
|
+
it.status = 'error';
|
|
322
|
+
it.err = 'Upload stalled — the file may be too large for this connection';
|
|
323
|
+
render();
|
|
324
|
+
pump();
|
|
325
|
+
};
|
|
326
|
+
xhr.onload = function () {
|
|
327
|
+
settle();
|
|
328
|
+
active--;
|
|
329
|
+
var res = null;
|
|
330
|
+
try { res = JSON.parse(xhr.responseText); } catch (e) {}
|
|
331
|
+
if (xhr.status >= 200 && xhr.status < 300 && res && res.success && res.media && res.media.url) {
|
|
332
|
+
it.status = 'done';
|
|
333
|
+
it.url = res.media.url;
|
|
334
|
+
if (!it.thumb && res.media.thumbnail_url) it.thumb = res.media.thumbnail_url;
|
|
335
|
+
try {
|
|
336
|
+
window.kolbo.updateModelContext('Upload widget: "' + filenameFor(it) + '" (' + it.kind + ') uploaded to the Kolbo media library. URL: ' + it.url);
|
|
337
|
+
} catch (e) {}
|
|
338
|
+
} else {
|
|
339
|
+
it.status = 'error';
|
|
340
|
+
it.err = (res && res.error) || ('Upload failed (' + xhr.status + ')');
|
|
341
|
+
}
|
|
342
|
+
render();
|
|
343
|
+
pump();
|
|
344
|
+
};
|
|
345
|
+
xhr.onerror = function () {
|
|
346
|
+
settle();
|
|
347
|
+
active--;
|
|
348
|
+
it.status = 'error';
|
|
349
|
+
it.err = 'Network error — try the full-screen uploader';
|
|
350
|
+
render();
|
|
351
|
+
pump();
|
|
352
|
+
};
|
|
353
|
+
xhr.send(fd);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ---- rendering ----
|
|
357
|
+
function rowHtml(it) {
|
|
358
|
+
var icon = it.kind && KINDS[it.kind] ? KINDS[it.kind].icon : ICONS.file;
|
|
359
|
+
var right = '';
|
|
360
|
+
if (it.status === 'queued') right = '<span style="color:var(--text-muted)">queued</span>';
|
|
361
|
+
else if (it.status === 'uploading') right = '<span style="color:var(--text-muted)">' + it.pct + '%</span>';
|
|
362
|
+
else if (it.status === 'done') right = '<span style="color:#4ade80">' + ICONS.check + ' uploaded</span>';
|
|
363
|
+
else right = '<span class="k-error" style="padding:0;border:0;background:none">' + ICONS.x + ' ' + esc(it.err || 'failed') + '</span> <a href="#" data-retry="' + it.id + '" style="font-size:11px">retry</a>';
|
|
364
|
+
var bar = it.status === 'uploading'
|
|
365
|
+
? '<div style="height:3px;border-radius:2px;background:var(--surface);margin-top:5px;overflow:hidden"><div id="bar-' + it.id + '" style="height:100%;width:' + it.pct + '%;background:var(--accent,#7c6cff);transition:width .2s"></div></div>'
|
|
366
|
+
: '';
|
|
367
|
+
var left = it.thumb
|
|
368
|
+
? '<img data-thumb="' + it.id + '" src="' + it.thumb + '" alt="" style="width:34px;height:34px;object-fit:cover;border-radius:7px;flex:none;border:1px solid var(--border);background:var(--surface)">'
|
|
369
|
+
: '<span style="width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;border-radius:7px;background:var(--surface);border:1px solid var(--border);color:var(--text-muted)">' + icon + '</span>';
|
|
370
|
+
return '<div id="row-' + it.id + '" style="padding:8px 10px;border:1px solid var(--border);border-radius:10px;margin-bottom:6px;background:var(--surface)">' +
|
|
371
|
+
'<div style="display:flex;align-items:center;gap:8px;font-size:12.5px">' +
|
|
372
|
+
left +
|
|
373
|
+
'<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + esc(filenameFor(it)) + '">' + esc(filenameFor(it)) + '</span>' +
|
|
374
|
+
'<span style="color:var(--text-muted);font-size:11px">' + fmtSize(it.file.size) + '</span>' +
|
|
375
|
+
'<span id="status-' + it.id + '" style="font-size:11.5px">' + right + '</span>' +
|
|
376
|
+
'</div>' + bar + '</div>';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function renderRow(it) {
|
|
380
|
+
var s = el('status-' + it.id);
|
|
381
|
+
if (s && it.status === 'uploading') s.innerHTML = '<span style="color:var(--text-muted)">' + it.pct + '%</span>';
|
|
382
|
+
var b = el('bar-' + it.id);
|
|
383
|
+
if (b) b.style.width = it.pct + '%';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function render() {
|
|
387
|
+
el('rows').innerHTML = items.map(rowHtml).join('');
|
|
388
|
+
Array.prototype.forEach.call(el('rows').querySelectorAll('[data-thumb]'), function (img) {
|
|
389
|
+
img.onerror = function () {
|
|
390
|
+
var id = Number(img.getAttribute('data-thumb'));
|
|
391
|
+
for (var i = 0; i < items.length; i++) {
|
|
392
|
+
if (items[i].id !== id) continue;
|
|
393
|
+
if (items[i].url && img.src !== items[i].url) { img.src = items[i].url; return; }
|
|
394
|
+
items[i].thumb = null;
|
|
395
|
+
}
|
|
396
|
+
render();
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
Array.prototype.forEach.call(el('rows').querySelectorAll('[data-retry]'), function (a) {
|
|
400
|
+
a.onclick = function (e) {
|
|
401
|
+
e.preventDefault();
|
|
402
|
+
var id = Number(a.getAttribute('data-retry'));
|
|
403
|
+
for (var i = 0; i < items.length; i++) {
|
|
404
|
+
if (items[i].id === id) { items[i].status = 'queued'; items[i].err = null; items[i].pct = 0; }
|
|
405
|
+
}
|
|
406
|
+
render();
|
|
407
|
+
pump();
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
var done = items.filter(function (i) { return i.status === 'done'; });
|
|
411
|
+
var busy = items.some(function (i) { return i.status === 'uploading' || i.status === 'queued'; });
|
|
412
|
+
el('count-chip').style.display = items.length ? '' : 'none';
|
|
413
|
+
el('count-chip').textContent = done.length + '/' + items.length + ' uploaded';
|
|
414
|
+
if (done.length && !busy && !sent) {
|
|
415
|
+
el('actions').style.display = '';
|
|
416
|
+
el('actions').innerHTML = '<button class="k-btn primary" id="btn-use" style="min-height:44px">Use ' + (done.length === 1 ? 'this file' : 'these ' + done.length + ' files') + '</button>' +
|
|
417
|
+
'<button class="k-btn ghost" id="btn-more">Add more</button>' +
|
|
418
|
+
'<button class="k-btn ghost" id="btn-external">' + ICONS.open + ' Full-screen</button>';
|
|
419
|
+
el('btn-use').onclick = function () {
|
|
420
|
+
if (sent) return;
|
|
421
|
+
sent = true;
|
|
422
|
+
var lines = done.map(function (i, idx) { return (idx + 1) + '. ' + filenameFor(i) + ' (' + i.kind + '): ' + i.url; });
|
|
423
|
+
window.kolbo.sendMessage('I uploaded ' + done.length + ' file(s) to my Kolbo media library:\\n' + lines.join('\\n') + '\\nContinue with these files.');
|
|
424
|
+
el('actions').innerHTML = '<span style="font-size:12px;color:var(--text-muted)">' + ICONS.check + ' Sent to Claude — continuing…</span>';
|
|
425
|
+
window.kolbo.notifySize();
|
|
426
|
+
};
|
|
427
|
+
el('btn-more').onclick = function () {
|
|
428
|
+
if (isMobileHost()) openExternalUploader();
|
|
429
|
+
else armInlinePicker();
|
|
430
|
+
};
|
|
431
|
+
el('btn-external').onclick = function (e) { e.preventDefault(); openExternalUploader(); };
|
|
432
|
+
} else if (!items.length) {
|
|
433
|
+
// keep boot() actions (external / inline)
|
|
434
|
+
} else if (!done.length || busy) {
|
|
435
|
+
el('actions').style.display = '';
|
|
436
|
+
el('actions').innerHTML = (busy ? '<span style="font-size:12px;color:var(--text-muted)">Uploading…</span>' : '') +
|
|
437
|
+
'<button class="k-btn ghost" id="btn-external">' + ICONS.open + ' Full-screen uploader</button>';
|
|
438
|
+
el('btn-external').onclick = function (e) { e.preventDefault(); openExternalUploader(); };
|
|
439
|
+
}
|
|
440
|
+
window.kolbo.notifySize();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
window.kolbo.onToolResult(function (result) {
|
|
444
|
+
var sc = result.structuredContent || structured(result);
|
|
445
|
+
if (sc && sc.widget === 'upload') return boot(sc);
|
|
446
|
+
var card = document.querySelector('.k-card');
|
|
447
|
+
if (card && !state) card.style.display = 'none';
|
|
448
|
+
window.kolbo.notifySize();
|
|
449
|
+
});
|
|
450
|
+
`;
|
|
451
|
+
|
|
452
|
+
function uploadWidgetHtml() {
|
|
453
|
+
return widgetPage({ title: 'Kolbo Upload', body: BODY, script: SCRIPT });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
module.exports = { uploadWidgetHtml };
|