@kolbo/mcp 1.79.6 → 1.80.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.
@@ -15,7 +15,9 @@ const { widgetPage } = require('../html');
15
15
 
16
16
  * model, model_icon, prompt, count,
17
17
  * settings: { duration, resolution, aspect_ratio, quality, audio, voice, mode,
18
- * enhance_prompt, web_search, visual_dna, moodboard, preset, cinematic },
18
+ * enhance_prompt, web_search, visual_dna, moodboard, preset,
19
+ * preset_id, preset_name, cinematic },
20
+ * visual_dnas: [{ id, name, thumbnail }],
19
21
  * reference_images, // all reference thumbnail URLs (optional)
20
22
  * reference_image, // legacy single-thumbnail fallback
21
23
  * urls, thumbnail_url, title, duration, credits_used,
@@ -74,28 +76,67 @@ var TOOL_TITLES = {
74
76
  get_generation_status: 'Generations'
75
77
  };
76
78
 
77
- // Long text is clamped by CSS (.k-prompt 2 lines / .k-caption 1 line). When it
78
- // actually overflows, make it click-to-expand so the full prompt is readable.
79
- function makeExpandable(node) {
79
+ // Long text is clamped by CSS (.k-prompt 2 lines / .k-caption 1 line). Expand
80
+ // lives on a separate button so the text itself stays selectable.
81
+ function stripTools(node) {
82
+ var prev = node && node.nextSibling;
83
+ if (prev && prev.classList && prev.classList.contains('k-text-tools') && prev.parentNode) {
84
+ prev.parentNode.removeChild(prev);
85
+ }
86
+ }
87
+ function setPrompt(html, raw) {
88
+ var node = el('prompt');
89
+ if (!html) {
90
+ node.innerHTML = '';
91
+ node.style.display = 'none';
92
+ stripTools(node);
93
+ return;
94
+ }
95
+ node.innerHTML = html;
96
+ node.style.display = '';
97
+ makeExpandable(node, raw);
98
+ }
99
+ function makeExpandable(node, raw) {
80
100
  if (!node) return;
81
- node.classList.remove('k-clamped');
101
+ node.classList.remove('k-clamped', 'expanded');
102
+ node.onclick = null;
103
+ node.removeAttribute('title');
104
+ stripTools(node);
105
+ var text = raw != null ? String(raw) : (node.innerText || node.textContent || '');
106
+ if (!text || !node.parentNode) return;
82
107
  // Synchronous layout read — rAF would never fire in a hidden/backgrounded
83
108
  // iframe, leaving long prompts stuck without the expand affordance.
84
- if (node.scrollHeight > node.clientHeight + 2 || node.scrollWidth > node.clientWidth + 2) {
85
- node.classList.add('k-clamped');
86
- node.title = node.title || 'Show full text';
87
- node.onclick = function () {
88
- node.classList.toggle('expanded');
89
- node.title = node.classList.contains('expanded') ? 'Collapse' : 'Show full text';
90
- window.kolbo.notifySize();
91
- };
92
- }
109
+ var overflow = node.scrollHeight > node.clientHeight + 2 || node.scrollWidth > node.clientWidth + 2;
110
+ if (overflow) node.classList.add('k-clamped');
111
+ var tools = document.createElement('div');
112
+ tools.className = 'k-text-tools';
113
+ tools.innerHTML =
114
+ '<button type="button" class="k-text-btn" data-act="copy">' + ICONS.copy + ' Copy</button>' +
115
+ (overflow ? '<button type="button" class="k-text-btn" data-act="expand">' + ICONS.chevronDown + ' Expand</button>' : '');
116
+ node.parentNode.insertBefore(tools, node.nextSibling);
117
+ var copyBtn = tools.querySelector('[data-act="copy"]');
118
+ if (copyBtn) copyBtn.onclick = function (e) {
119
+ e.preventDefault();
120
+ e.stopPropagation();
121
+ writeClipboard(text).then(function (ok) {
122
+ copyBtn.innerHTML = ok ? (ICONS.check + ' Copied') : 'Could not copy';
123
+ setTimeout(function () { copyBtn.innerHTML = ICONS.copy + ' Copy'; }, 1600);
124
+ });
125
+ };
126
+ var exp = tools.querySelector('[data-act="expand"]');
127
+ if (exp) exp.onclick = function (e) {
128
+ e.preventDefault();
129
+ e.stopPropagation();
130
+ var on = node.classList.toggle('expanded');
131
+ exp.innerHTML = on ? (ICONS.chevronUp + ' Collapse') : (ICONS.chevronDown + ' Expand');
132
+ if (window.kolbo && window.kolbo.notifySize) window.kolbo.notifySize();
133
+ };
93
134
  }
94
135
 
95
136
  function renderList(sc) {
96
137
  state = sc;
97
138
  el('tool-title').textContent = sc.title || 'List';
98
- el('prompt').style.display = 'none';
139
+ setPrompt('');
99
140
  el('chips').innerHTML = '';
100
141
  el('credits').textContent = '';
101
142
  var items = sc.items || [];
@@ -128,9 +169,7 @@ function boot(sc) {
128
169
  if (isListPayload(sc, sc.tool)) return renderList(sc);
129
170
  state = sc;
130
171
  el('tool-title').textContent = TOOL_TITLES[sc.tool] || 'Generation';
131
- el('prompt').innerHTML = promptHTML(sc.prompt);
132
- el('prompt').style.display = sc.prompt ? '' : 'none';
133
- makeExpandable(el('prompt'));
172
+ setPrompt(sc.prompt ? promptHTML(sc.prompt) : '', sc.prompt);
134
173
  renderChips(sc);
135
174
  el('credits').textContent = sc.credits_used != null ? fmtCredits(sc.credits_used) : '';
136
175
  // Every legitimate completed payload sets phase:'completed' explicitly
@@ -182,10 +221,37 @@ function promptHTML(text) {
182
221
  });
183
222
  }
184
223
 
224
+ function ownedHost(url) {
225
+ try { return /(?:^|\\.)kolbo\\.ai$|digitaloceanspaces\\.com$/i.test(new URL(url).hostname); }
226
+ catch (e) { return false; }
227
+ }
228
+ function preferKolbo(urls) {
229
+ var list = (urls || []).filter(function (u) { return typeof u === 'string' && u; });
230
+ var ours = list.filter(ownedHost);
231
+ return ours.length ? ours : list;
232
+ }
233
+ function displayKind(sc) {
234
+ var urls = preferKolbo(sc.urls || []);
235
+ var first = (urls[0] || '').split('?')[0].toLowerCase();
236
+ if (/\\.(mp4|mov|webm|mkv)$/.test(first) || /video-elements-results|generated-videos/.test(first)) return 'video';
237
+ if (/\\.(mp3|wav|m4a|aac|ogg|flac)$/.test(first)) return 'audio';
238
+ var kind = sc.kind;
239
+ if (kind === 'video' || kind === 'scenes' || kind === 'audio' || kind === '3d' || kind === 'model3d') {
240
+ return kind === 'scenes' ? 'video' : kind;
241
+ }
242
+ var tool = sc.tool || '';
243
+ if (/video|elements|lipsync|first_last_frame/.test(tool)) return 'video';
244
+ if (/music|speech|sound/.test(tool)) return 'audio';
245
+ if (/3d/.test(tool)) return '3d';
246
+ if (kind === 'status') return '';
247
+ return kind || 'image';
248
+ }
249
+
185
250
  function renderChips(sc) {
186
251
  var h = modelChipHTML(modelLabel(sc), sc.model_icon);
187
252
  var s = sc.settings || {};
188
- if (sc.kind) h += chip(iconFor(sc.kind) + ' ' + sc.kind);
253
+ var kind = displayKind(sc);
254
+ if (kind) h += chip(iconFor(kind) + ' ' + kind);
189
255
  if (s.duration) h += chip(ICONS.clock + ' ' + fmtDur(s.duration) + (s.shots > 1 ? ' · ' + s.shots + ' shots' : ''));
190
256
  else if (s.shots > 1) h += chip(s.shots + ' shots');
191
257
  if (s.resolution) h += chip(esc(s.resolution));
@@ -197,11 +263,14 @@ function renderChips(sc) {
197
263
  // falling back to the old count/boolean shape for payloads generated before
198
264
  // the ids were carried.
199
265
  h += dnaChipsHTML(sc, s);
200
- var mbIds = s.moodboard_ids || (s.moodboard_id ? [s.moodboard_id] : []);
201
- if (mbIds.length) h += chipT(mbIds.length > 1 ? mbIds.length + ' moodboards' : 'moodboard', mbIds.join('\\n'));
202
- else if (s.moodboard) h += chip('moodboard');
203
- if (s.preset_id) h += chipT('preset', s.preset_id);
204
- else if (s.preset) h += chip('preset');
266
+ h += moodboardChipsHTML(sc, s);
267
+ if (s.preset_id || s.preset_name || s.preset) {
268
+ var presetName = s.preset_name || (typeof s.preset === 'string' && s.preset !== 'true' ? s.preset : '');
269
+ var face = s.preset_thumbnail
270
+ ? '<img src="' + esc(s.preset_thumbnail) + '" alt="" loading="lazy" onerror="this.style.display=\\'none\\'">'
271
+ : '';
272
+ h += chipT(face + esc(presetName || 'preset'), s.preset_id || presetName);
273
+ }
205
274
  if (s.cinematic) h += chip('cinematic');
206
275
  if (s.audio) h += chip(ICONS.sound + ' audio');
207
276
  var voice = voiceLabel(sc);
@@ -215,6 +284,7 @@ function renderChips(sc) {
215
284
  if (sc.count > 1) h += chip('×' + sc.count);
216
285
  h += referenceHTML(sc);
217
286
  el('chips').innerHTML = h;
287
+ bindPeekHits(el('chips'));
218
288
  }
219
289
 
220
290
  var REF_VIDEO_RE = /\\.(mp4|mov|webm|mkv|avi|m4v)(\\?|#|$)/i;
@@ -251,15 +321,16 @@ function referenceHTML(sc) {
251
321
  for (var i = 0; i < refs.length; i++) {
252
322
  var url = esc(refs[i].url);
253
323
  var title = 'Reference ' + refs[i].kind + ' ' + (i + 1) + ' of ' + refs.length;
324
+ var peek = ' data-peek="' + url + '" data-peek-kind="' + refs[i].kind + '" data-peek-cap="' + title + '"';
254
325
  if (refs[i].kind === 'video') {
255
326
  // #t=0.1 so the poster is a real frame, not a black canvas.
256
- h += '<video class="k-ref-thumb" src="' + url + '#t=0.1" muted playsinline preload="metadata" title="'
257
- + title + '" onerror="this.style.display=\\'none\\'"></video>';
327
+ h += '<video class="k-ref-thumb k-peek-hit" src="' + url + '#t=0.1" muted playsinline preload="metadata" title="'
328
+ + title + '"' + peek + ' onerror="this.style.display=\\'none\\'"></video>';
258
329
  } else if (refs[i].kind === 'audio') {
259
330
  h += '<span class="k-chip" title="' + title + '">' + ICONS.sound + ' audio ref</span>';
260
331
  } else {
261
- h += '<img class="k-ref-thumb" src="' + url + '" alt="" loading="lazy" title="' + title
262
- + '" onerror="this.style.display=\\'none\\'">';
332
+ h += '<img class="k-ref-thumb k-peek-hit" src="' + url + '" alt="" loading="lazy" title="' + title + '"'
333
+ + peek + ' onerror="this.style.display=\\'none\\'">';
263
334
  }
264
335
  }
265
336
  return h;
@@ -288,7 +359,8 @@ function dnaChipsHTML(sc, s) {
288
359
  var h = '';
289
360
  if (dnas.length <= DNA_CHIP_MAX) {
290
361
  for (var i = 0; i < dnas.length; i++) {
291
- h += '<span class="k-chip" title="' + esc(dnas[i].id) + '">'
362
+ h += '<span class="k-chip' + (dnas[i].thumbnail ? ' k-peek-hit' : '') + '" title="' + esc(dnas[i].id) + '"'
363
+ + peekAttrs(dnas[i].thumbnail, 'image', dnas[i].name) + '>'
292
364
  + dnaFaceHTML(dnas[i], 'k-dna-face') + esc(dnas[i].name) + '</span>';
293
365
  }
294
366
  return h;
@@ -297,11 +369,31 @@ function dnaChipsHTML(sc, s) {
297
369
  var stack = '';
298
370
  for (var j = 0; j < dnas.length; j++) {
299
371
  names.push(dnas[j].name);
300
- if (j < 4) stack += '<span class="k-dna-stack-item">' + dnaFaceHTML(dnas[j], 'k-dna-face') + '</span>';
372
+ if (j < 4) {
373
+ stack += '<span class="k-dna-stack-item' + (dnas[j].thumbnail ? ' k-peek-hit' : '') + '"'
374
+ + peekAttrs(dnas[j].thumbnail, 'image', dnas[j].name) + '>'
375
+ + dnaFaceHTML(dnas[j], 'k-dna-face') + '</span>';
376
+ }
301
377
  }
302
378
  return '<span class="k-chip k-dna-stack" title="' + esc(names.join('\\n')) + '">'
303
379
  + stack + dnas.length + ' Visual DNA</span>';
304
380
  }
381
+ function moodboardChipsHTML(sc, s) {
382
+ var boards = Array.isArray(sc.moodboards) ? sc.moodboards : [];
383
+ if (!boards.length) {
384
+ var ids = s.moodboard_ids || (s.moodboard_id ? [s.moodboard_id] : []);
385
+ if (ids.length) return chipT(ids.length > 1 ? ids.length + ' moodboards' : 'moodboard', ids.join('\\n'));
386
+ if (s.moodboard) return chip('moodboard');
387
+ return '';
388
+ }
389
+ var h = '';
390
+ for (var i = 0; i < boards.length; i++) {
391
+ h += '<span class="k-chip' + (boards[i].thumbnail ? ' k-peek-hit' : '') + '" title="' + esc(boards[i].id) + '"'
392
+ + peekAttrs(boards[i].thumbnail, 'image', boards[i].name) + '>'
393
+ + dnaFaceHTML(boards[i], 'k-dna-face') + esc(boards[i].name) + '</span>';
394
+ }
395
+ return h;
396
+ }
305
397
  function chip(inner) { return '<span class="k-chip">' + inner + '</span>'; }
306
398
  // Same chip with a hover title — used to surface the asset id behind a
307
399
  // "2 Visual DNA" / "preset" label without spending chip width on it.
@@ -324,13 +416,13 @@ function isBatch(sc) { return !!(sc && sc.generation_ids && sc.generation_ids.le
324
416
  function renderGenerating(sc) {
325
417
  setPhaseChip('Generating', true);
326
418
  var n = Math.min(sc.count || 1, isBatch(sc) ? 8 : 4);
327
- var shape = sc.kind === 'video' || sc.kind === 'scenes' ? 'video' : (sc.kind === 'audio' ? 'video' : 'square');
419
+ var kind = displayKind(sc);
420
+ var shape = kind === 'video' || kind === 'audio' ? 'video' : 'square';
328
421
  var cells = '';
329
422
  for (var i = 0; i < n; i++) {
330
423
  var cap = (sc.prompts && sc.prompts[i])
331
424
  ? '<span class="k-skel-cap" title="' + esc(sc.prompts[i]) + '">' + esc(sc.prompts[i]) + '</span>' : '';
332
- cells += '<div class="k-skel ' + shape + '" data-cell="' + i + '">' +
333
- (i === 0 ? '<span class="k-gen-badge"><span class="k-spin"></span>Generating</span>' : '') + cap + '</div>';
425
+ cells += '<div class="k-skel ' + shape + '" data-cell="' + i + '">' + cap + '</div>';
334
426
  }
335
427
  // Grid class caps at n4 — the auto-fill rule handles any larger batch count.
336
428
  el('stage').innerHTML = '<div class="k-gen-grid n' + Math.min(n, 4) + '">' + cells + '</div>';
@@ -355,48 +447,66 @@ function cancelSpec(sc) {
355
447
  function renderStopButton(sc) {
356
448
  var spec = cancelSpec(sc);
357
449
  if (!spec) { el('actions').innerHTML = ''; return; }
358
- el('actions').innerHTML = '<button class="k-btn ghost" id="stop-btn">' + ICONS.x + ' Stop</button>';
359
- el('stop-btn').onclick = function () {
360
- var btn = el('stop-btn');
450
+ var armTimer = null;
451
+ function idle() {
452
+ clearTimeout(armTimer);
453
+ el('actions').innerHTML = '<button class="k-btn ghost" id="stop-btn">' + ICONS.x + ' Stop</button>';
454
+ el('stop-btn').onclick = function () {
455
+ el('actions').innerHTML =
456
+ '<span class="k-stop-ask">Stop this generation?</span>' +
457
+ '<button class="k-btn ghost" id="stop-keep">Keep</button>' +
458
+ '<button class="k-btn danger" id="stop-btn">' + ICONS.x + ' Stop</button>';
459
+ el('stop-keep').onclick = idle;
460
+ el('stop-btn').onclick = function () { stopNow(sc, spec); };
461
+ if (window.kolbo && window.kolbo.notifySize) window.kolbo.notifySize();
462
+ armTimer = setTimeout(idle, 5000);
463
+ };
464
+ }
465
+ idle();
466
+ }
467
+
468
+ function stopNow(sc, spec) {
469
+ var btn = el('stop-btn');
470
+ if (btn) {
361
471
  btn.disabled = true;
362
472
  btn.innerHTML = '<span class="k-spin"></span> Stopping';
363
- // Stop polling immediately so a long-wait status call that lands mid-cancel
364
- // cannot repaint the card back into "Generating".
365
- cancelRequested = true;
366
- clearTimeout(pollTimer);
367
- // Batch: cancel every id; report combined refund. Entries that already
368
- // finished return cancelled:false — only resume polling if ALL did.
369
- var call = spec.batch
370
- ? Promise.all(spec.batch.map(function (id) {
371
- return window.kolbo.callTool('cancel_generation', { generation_id: id })
372
- .then(function (r) { return structured(r) || {}; })
373
- .catch(function () { return {}; });
374
- })).then(function (sts) {
375
- var refund = 0;
376
- sts.forEach(function (s) { if (s.credits_refunded) refund += s.credits_refunded; });
377
- return {
378
- cancelled: sts.some(function (s) { return s.cancelled !== false; }),
379
- credits_refunded: refund || undefined
380
- };
381
- })
382
- : window.kolbo.callTool(spec.tool, spec.args).then(function (r) { return structured(r) || {}; });
383
- call.then(function (st) {
384
- if (st.cancelled === false) {
385
- // Already terminal let the normal poll path report the real outcome
386
- // instead of claiming a cancel that did not happen.
387
- cancelRequested = false;
388
- btn.disabled = false;
389
- btn.innerHTML = ICONS.x + ' Stop';
390
- return schedulePoll(sc);
391
- }
392
- renderCancelled(st.credits_refunded);
393
- }).catch(function (e) {
473
+ }
474
+ var keep = el('stop-keep');
475
+ if (keep) keep.disabled = true;
476
+ // Stop polling immediately so a long-wait status call that lands mid-cancel
477
+ // cannot repaint the card back into "Generating".
478
+ cancelRequested = true;
479
+ clearTimeout(pollTimer);
480
+ // Batch: cancel every id; report combined refund. Entries that already
481
+ // finished return cancelled:false only resume polling if ALL did.
482
+ var call = spec.batch
483
+ ? Promise.all(spec.batch.map(function (id) {
484
+ return window.kolbo.callTool('cancel_generation', { generation_id: id })
485
+ .then(function (r) { return structured(r) || {}; })
486
+ .catch(function () { return {}; });
487
+ })).then(function (sts) {
488
+ var refund = 0;
489
+ sts.forEach(function (s) { if (s.credits_refunded) refund += s.credits_refunded; });
490
+ return {
491
+ cancelled: sts.some(function (s) { return s.cancelled !== false; }),
492
+ credits_refunded: refund || undefined
493
+ };
494
+ })
495
+ : window.kolbo.callTool(spec.tool, spec.args).then(function (r) { return structured(r) || {}; });
496
+ call.then(function (st) {
497
+ if (st.cancelled === false) {
498
+ // Already terminal — let the normal poll path report the real outcome
499
+ // instead of claiming a cancel that did not happen.
394
500
  cancelRequested = false;
395
- btn.disabled = false;
396
- btn.innerHTML = ICONS.x + ' Stop';
397
- schedulePoll(sc);
398
- });
399
- };
501
+ renderStopButton(sc);
502
+ return schedulePoll(sc);
503
+ }
504
+ renderCancelled(st.credits_refunded);
505
+ }).catch(function () {
506
+ cancelRequested = false;
507
+ renderStopButton(sc);
508
+ schedulePoll(sc);
509
+ });
400
510
  }
401
511
 
402
512
  function renderCancelled(creditsRefunded) {
@@ -608,12 +718,13 @@ function renderResult(sc) {
608
718
  if (sc.kind === 'status' && Array.isArray(sc.items)) return renderStatusGrid(sc);
609
719
  if (sc.batch && sc.scenes && sc.scenes.length) return renderBatchGrid(sc);
610
720
  if (sc.kind === 'scenes' && sc.scenes && sc.scenes.length) return renderScenes(sc);
611
- var urls = sc.urls || [];
721
+ var urls = preferKolbo(sc.urls || []);
722
+ var kind = displayKind(Object.assign({}, sc, { urls: urls }));
612
723
  if (!urls.length) return renderError('No output received');
613
- if (sc.kind === 'image') renderImages(sc, urls);
614
- else if (sc.kind === 'video') renderVideo(sc, urls);
615
- else if (sc.kind === 'audio') renderAudio(sc, urls);
616
- else if (sc.kind === '3d') render3d(sc, urls);
724
+ if (kind === 'image') renderImages(sc, urls);
725
+ else if (kind === 'video') renderVideo(sc, urls);
726
+ else if (kind === 'audio') renderAudio(sc, urls);
727
+ else if (kind === '3d') render3d(sc, urls);
617
728
  else renderLinks(urls);
618
729
  renderActions(sc);
619
730
  window.kolbo.notifySize();
@@ -624,7 +735,12 @@ function renderImages(sc, urls) {
624
735
  // If the host CSP still blocks the image, degrade to open-in-browser rows
625
736
  // instead of a broken empty viewer.
626
737
  var viewer = '<div class="k-viewer"><img id="main-img" src="' + esc(urls[selected]) + '" alt="" onerror="window.__imgFail && window.__imgFail()">' + dlBtnHTML(urls[selected]) + '</div>';
627
- window.__imgFail = function () { renderLinks(urls); window.kolbo.notifySize(); };
738
+ window.__imgFail = function () {
739
+ var look = displayKind({ urls: urls, tool: state && state.tool, kind: 'image' });
740
+ if (look === 'video') renderVideo(state || { urls: urls }, urls);
741
+ else renderLinks(urls);
742
+ window.kolbo.notifySize();
743
+ };
628
744
  // Click → expand into an in-Claude fullscreen viewer (all actions stay
629
745
  // available); click again (or Exit) collapses back. Hosts that refuse
630
746
  // fullscreen fall back to opening the original in a new tab.
@@ -857,7 +973,7 @@ function renderScenes(sc) {
857
973
  '<div class="k-viewer">' + mediaHtml + dlBtnHTML(it.url) + '</div>' +
858
974
  '<div class="k-caption" id="scene-cap">' + esc(it.label) + '</div>' +
859
975
  thumbs;
860
- makeExpandable(el('scene-cap'));
976
+ makeExpandable(el('scene-cap'), it.label);
861
977
  wireDlButtons(el('stage'));
862
978
  if (it.type === 'image') {
863
979
  var main = el('scene-main');
@@ -1035,15 +1151,13 @@ function bootPre(toolName, args) {
1035
1151
  el('tool-title').textContent = toolName === 'list_sessions' ? 'Sessions' : 'List';
1036
1152
  setPhaseChip('Loading', true);
1037
1153
  el('stage').innerHTML = '';
1038
- el('prompt').style.display = 'none';
1154
+ setPrompt('');
1039
1155
  return;
1040
1156
  }
1041
1157
  el('tool-title').textContent = TOOL_TITLES[toolName] || 'Generation';
1042
1158
  if (args && (args.prompt || args.text || (Array.isArray(args.prompts) && args.prompts.length))) {
1043
- el('prompt').innerHTML = promptHTML(args.prompt || args.text ||
1044
- (args.prompts.length + ' prompts — ' + args.prompts.join(' · ')));
1045
- el('prompt').style.display = '';
1046
- makeExpandable(el('prompt'));
1159
+ var raw = args.prompt || args.text || (args.prompts.length + ' prompts — ' + args.prompts.join(' · '));
1160
+ setPrompt(promptHTML(raw), raw);
1047
1161
  }
1048
1162
  setPhaseChip('Preparing', true);
1049
1163
  if (!el('stage').innerHTML) {
@@ -74,7 +74,9 @@ function apply(result) {
74
74
  function itemHTML(item, i) {
75
75
  var clickable = !!item.use_hint;
76
76
  var avatar = item.thumbnail
77
- ? '<img class="k-audio-art" src="' + esc(item.thumbnail) + '" alt="" loading="lazy" onerror="this.outerHTML=monogram(\\'' + esc(item.title || '?').replace(/'/g, '') + '\\')">'
77
+ ? '<img class="k-audio-art k-peek-hit" src="' + esc(item.thumbnail) + '" alt="" loading="lazy"'
78
+ + peekAttrs(item.thumbnail, 'image', item.title)
79
+ + ' onerror="this.outerHTML=monogram(\\'' + esc(item.title || '?').replace(/'/g, '') + '\\')">'
78
80
  : monogram(item.title || '?');
79
81
  return '<div class="k-audio-row" data-i="' + i + '"' + (clickable ? ' style="cursor:pointer"' : '') + '>' +
80
82
  avatar +
@@ -87,6 +89,7 @@ function itemHTML(item, i) {
87
89
  }
88
90
 
89
91
  function wire() {
92
+ bindPeekHits(el('stage'));
90
93
  Array.prototype.forEach.call(document.querySelectorAll('[data-open]'), function (b) {
91
94
  b.onclick = function (e) {
92
95
  e.stopPropagation();
@@ -79,8 +79,11 @@ function cellHTML(item, i) {
79
79
  // thumbnail-less item gets, so a dead URL degrades instead of looking broken.
80
80
  var fallbackCell = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-faint);font-size:22px">' +
81
81
  kindIcon(item.media_type) + '</div>';
82
+ var peekUrl = item.url || item.thumbnail;
82
83
  var media = item.thumbnail
83
- ? '<img src="' + esc(item.thumbnail) + '" loading="lazy" alt="" onerror="this.parentNode.innerHTML=this.getAttribute(\\'data-fb\\')" data-fb="' + esc(fallbackCell) + '">'
84
+ ? '<img class="k-peek-hit" src="' + esc(item.thumbnail) + '" loading="lazy" alt=""'
85
+ + peekAttrs(peekUrl, item.media_type === 'video' ? 'video' : 'image', item.title)
86
+ + ' onerror="this.parentNode.innerHTML=this.getAttribute(\\'data-fb\\')" data-fb="' + esc(fallbackCell) + '">'
84
87
  : fallbackCell;
85
88
  return '<div class="k-cell" data-i="' + idx + '">' +
86
89
  '<div class="k-cell-media">' + media +
@@ -100,7 +103,7 @@ function audioRowHTML(item) {
100
103
  var idx = state.items.indexOf(item);
101
104
  var src = item.preview_audio || item.url;
102
105
  return '<div class="k-audio-row k-generated-audio" data-i="' + idx + '">' +
103
- (item.thumbnail ? '<img class="k-audio-art" src="' + esc(item.thumbnail) + '">' : '<div class="k-audio-art"></div>') +
106
+ (item.thumbnail ? '<img class="k-audio-art k-peek-hit" src="' + esc(item.thumbnail) + '"' + peekAttrs(item.thumbnail, 'image', item.title) + '>' : '<div class="k-audio-art"></div>') +
104
107
  '<div class="k-audio-meta"><div class="k-audio-title">' + esc(item.title || '') + '</div>' +
105
108
  '<div class="k-audio-sub">' + esc(item.subtitle || '') + '</div></div>' +
106
109
  '<button class="k-btn" data-use="' + idx + '">Use</button>' +
@@ -109,8 +112,12 @@ function audioRowHTML(item) {
109
112
  }
110
113
 
111
114
  function wire() {
115
+ bindPeekHits(el('stage'));
112
116
  Array.prototype.forEach.call(document.querySelectorAll('.k-cell'), function (c) {
113
- c.onclick = function () { useItem(+c.getAttribute('data-i')); };
117
+ c.onclick = function (e) {
118
+ if (e.target && e.target.closest && e.target.closest('[data-peek]')) return;
119
+ useItem(+c.getAttribute('data-i'));
120
+ };
114
121
  });
115
122
  Array.prototype.forEach.call(document.querySelectorAll('[data-use]'), function (b) {
116
123
  b.onclick = function (e) { e.stopPropagation(); useItem(+b.getAttribute('data-use')); };
@@ -77,7 +77,10 @@ function boot(sc) {
77
77
  });
78
78
  var copyBtn = el('btn-copy');
79
79
  if (copyBtn) copyBtn.onclick = function () {
80
- try { navigator.clipboard.writeText(state.text || ''); copyBtn.innerHTML = 'Copied ' + ICONS.check; } catch (e) {}
80
+ writeClipboard(state.text || '').then(function (ok) {
81
+ copyBtn.innerHTML = ok ? ('Copied ' + ICONS.check) : 'Could not copy';
82
+ setTimeout(function () { copyBtn.innerHTML = 'Copy text'; }, 1600);
83
+ });
81
84
  };
82
85
  window.kolbo.notifySize();
83
86
  }