@kolbo/mcp 1.88.4 → 1.89.1
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/README.md +8 -2
- package/package.json +1 -1
- package/src/apps/bridge.js +53 -19
- package/src/apps/html.js +2 -0
- package/src/apps/index.js +8 -8
- package/src/apps/theme.js +58 -10
- package/src/apps/widgets/generation.js +13 -4
- package/src/apps/widgets/list.js +67 -25
- package/src/apps/widgets/mediaGrid.js +148 -79
- package/src/index.js +269 -267
- package/src/toolAnnotations.js +11 -9
- package/src/tools/_shared.js +10 -5
- package/src/tools/agents.js +51 -25
- package/src/tools/chat.js +3 -1
- package/src/tools/editor.js +22 -0
- package/src/tools/generate.js +19 -7
- package/src/tools/media.js +10 -10
- package/src/tools/models.js +11 -5
- package/src/tools/visual_dna.js +1 -1
|
@@ -6,6 +6,12 @@ const { widgetPage } = require('../html');
|
|
|
6
6
|
* Media grid widget — media library, stock search, presets, voices, moodboards,
|
|
7
7
|
* visual DNAs, music library.
|
|
8
8
|
*
|
|
9
|
+
* Layout: a contact sheet, not a list. One PAGE of square tiles at a time with
|
|
10
|
+
* a pager underneath, so the card keeps a constant height no matter how many
|
|
11
|
+
* results there are. Forward past the last loaded page still goes through the
|
|
12
|
+
* server's own paging contract (page_tool + next_args) — the pages already
|
|
13
|
+
* fetched just stay in state so Back is instant.
|
|
14
|
+
*
|
|
9
15
|
* structuredContent contract:
|
|
10
16
|
* {
|
|
11
17
|
* widget: 'media-grid',
|
|
@@ -17,6 +23,7 @@ const { widgetPage } = require('../html');
|
|
|
17
23
|
* use_hint // unused — Use pastes `id` into the composer
|
|
18
24
|
* }],
|
|
19
25
|
* total, has_more,
|
|
26
|
+
* page_tool, next_args, // paging contract — the server names its own args
|
|
20
27
|
* import_tool_hint // e.g. 'import via import_stock_asset' — shown on Use
|
|
21
28
|
* }
|
|
22
29
|
*/
|
|
@@ -38,83 +45,125 @@ const SCRIPT = `
|
|
|
38
45
|
el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
|
|
39
46
|
el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink('https://app.kolbo.ai'); };
|
|
40
47
|
var state = null;
|
|
48
|
+
var page = 0;
|
|
49
|
+
|
|
50
|
+
function isAudio(item) { return item && item.media_type === 'audio'; }
|
|
51
|
+
// Audio comes down as full-width rows (a name + a player), so half a dozen of
|
|
52
|
+
// those already fill the card where a dozen square tiles do.
|
|
53
|
+
function pageSize() {
|
|
54
|
+
var items = (state && state.items) || [];
|
|
55
|
+
var audio = items.filter(isAudio).length;
|
|
56
|
+
return audio > items.length / 2 ? 6 : 12;
|
|
57
|
+
}
|
|
58
|
+
function pageCount() { return Math.max(1, Math.ceil(((state && state.items) || []).length / pageSize())); }
|
|
59
|
+
// Only a tool that told us HOW to page can go past what is already loaded.
|
|
60
|
+
// Every other grid used to render a Load more that did nothing.
|
|
61
|
+
function hasMore() {
|
|
62
|
+
if (!state) return false;
|
|
63
|
+
var shown = (state.items || []).length;
|
|
64
|
+
var flagged = typeof state.has_next === 'boolean' ? state.has_next : (state.total != null && state.total > shown);
|
|
65
|
+
return !!(flagged && state.page_tool && (state.next_args || state.query || state.page));
|
|
66
|
+
}
|
|
41
67
|
|
|
42
68
|
function boot(sc) {
|
|
43
69
|
if (!sc || !sc.items) return;
|
|
44
70
|
state = sc;
|
|
71
|
+
page = 0;
|
|
45
72
|
el('title').textContent = sc.title || 'Library';
|
|
46
|
-
if (sc.total != null) { el('count-chip').style.display = ''; el('count-chip').textContent = sc.total + ' results'; }
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
if (audioItems.length) {
|
|
60
|
-
h += audioItems.map(audioRowHTML).join('');
|
|
61
|
-
}
|
|
62
|
-
var shown = visualItems.length + audioItems.length;
|
|
63
|
-
// Only a tool that told us HOW to page (page_tool + next_args / query) gets a
|
|
64
|
-
// button. Every other grid used to render a Load more that did nothing —
|
|
65
|
-
// fetchNextPage returned early with no page_tool — which read as "broken".
|
|
66
|
-
if (sc.total != null && sc.total > shown && sc.page_tool && (sc.next_args || sc.query || sc.page)) {
|
|
67
|
-
h += '<button class="k-btn" id="load-more" style="width:100%;margin-top:10px">Load more (' +
|
|
68
|
-
shown + ' of ' + sc.total + ' shown)</button>';
|
|
69
|
-
} else if (sc.total != null && sc.total > shown) {
|
|
70
|
-
h += '<div style="text-align:center;font-size:12px;color:var(--text-muted);margin-top:10px">' + shown + ' of ' + sc.total + ' shown</div>';
|
|
73
|
+
if (sc.total != null && sc.total >= 0) { el('count-chip').style.display = ''; el('count-chip').textContent = sc.total + ' results'; }
|
|
74
|
+
else { el('count-chip').style.display = 'none'; }
|
|
75
|
+
render();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function render() {
|
|
79
|
+
var items = (state && state.items) || [];
|
|
80
|
+
if (!items.length) {
|
|
81
|
+
el('stage').classList.add('k-empty');
|
|
82
|
+
el('stage').innerHTML = 'No results';
|
|
83
|
+
window.kolbo.notifySize();
|
|
84
|
+
return;
|
|
71
85
|
}
|
|
86
|
+
var size = pageSize();
|
|
87
|
+
if (page > pageCount() - 1) page = pageCount() - 1;
|
|
88
|
+
var slice = items.slice(page * size, page * size + size);
|
|
89
|
+
var visual = slice.filter(function (i) { return !isAudio(i); });
|
|
90
|
+
var audio = slice.filter(isAudio);
|
|
91
|
+
var h = '';
|
|
92
|
+
if (visual.length) h += '<div class="k-grid">' + visual.map(tileHTML).join('') + '</div>';
|
|
93
|
+
if (audio.length) h += '<div' + (visual.length ? ' style="margin-top:8px"' : '') + '>' + audio.map(audioRowHTML).join('') + '</div>';
|
|
94
|
+
h += pagerHTML();
|
|
72
95
|
el('stage').innerHTML = h;
|
|
73
96
|
el('stage').classList.remove('k-empty');
|
|
74
97
|
wire();
|
|
75
98
|
window.kolbo.notifySize();
|
|
76
99
|
}
|
|
77
100
|
|
|
78
|
-
function
|
|
101
|
+
function pagerHTML() {
|
|
102
|
+
var n = pageCount();
|
|
103
|
+
var more = hasMore();
|
|
104
|
+
if (n <= 1 && !more) return '';
|
|
105
|
+
var mid;
|
|
106
|
+
if (n <= 8) {
|
|
107
|
+
var dots = '';
|
|
108
|
+
for (var i = 0; i < n; i++) {
|
|
109
|
+
dots += '<button class="k-dot' + (i === page ? ' on' : '') + '" data-page="' + i + '" aria-label="Page ' + (i + 1) + '"></button>';
|
|
110
|
+
}
|
|
111
|
+
// A dimmed trailing dot says "there is more behind Next" without claiming
|
|
112
|
+
// to know how much — the server only ever promises the NEXT page.
|
|
113
|
+
if (more) dots += '<span class="k-dot ghost"></span>';
|
|
114
|
+
mid = '<div class="k-dots">' + dots + '</div>';
|
|
115
|
+
} else {
|
|
116
|
+
mid = '<span class="k-pager-label">' + (page + 1) + ' / ' + n + (more ? '+' : '') + '</span>';
|
|
117
|
+
}
|
|
118
|
+
return '<div class="k-pager">' +
|
|
119
|
+
'<button class="k-pager-btn" id="page-prev" aria-label="Previous"' + (page === 0 ? ' disabled' : '') + '>' + ICONS.chevronLeft + '</button>' +
|
|
120
|
+
mid +
|
|
121
|
+
'<button class="k-pager-btn" id="page-next" aria-label="Next"' + (page >= n - 1 && !more ? ' disabled' : '') + '>' + ICONS.chevronRight + '</button>' +
|
|
122
|
+
'</div>';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function tileHTML(item) {
|
|
79
126
|
var idx = state.items.indexOf(item);
|
|
80
127
|
var isVideo = item.media_type === 'video' && item.url;
|
|
81
128
|
// onerror matters: a thumbnail that 404s / expires / can't be fetched by the
|
|
82
129
|
// host webview left a BLACK tile with no glyph and no label hint — visually
|
|
83
130
|
// identical to "corrupted media". Fall back to the same kind icon a
|
|
84
131
|
// thumbnail-less item gets, so a dead URL degrades instead of looking broken.
|
|
85
|
-
|
|
86
|
-
|
|
132
|
+
// outerHTML, not parentNode.innerHTML: the parent is now the whole tile, and
|
|
133
|
+
// wiping it would take the caption and the Use/Download buttons with it.
|
|
134
|
+
var fallback = '<div class="k-tile-fb">' + kindIcon(item.media_type) + '</div>';
|
|
87
135
|
var peekUrl = item.url || item.thumbnail;
|
|
88
136
|
var media = item.thumbnail
|
|
89
137
|
? '<img class="k-peek-hit" src="' + esc(item.thumbnail) + '" loading="lazy" alt=""'
|
|
90
138
|
+ peekAttrs(peekUrl, item.media_type === 'video' ? 'video' : 'image', item.title)
|
|
91
|
-
+ ' onerror="this.
|
|
92
|
-
:
|
|
93
|
-
|
|
94
|
-
'<div class="k-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
'<button class="k-
|
|
139
|
+
+ ' onerror="this.outerHTML=this.getAttribute(\\'data-fb\\')" data-fb="' + esc(fallback) + '">'
|
|
140
|
+
: fallback;
|
|
141
|
+
var cap = (item.title || item.subtitle)
|
|
142
|
+
? '<div class="k-tile-cap">' +
|
|
143
|
+
(item.title ? '<div class="k-tile-t">' + esc(item.title) + '</div>' : '') +
|
|
144
|
+
(item.subtitle ? '<div class="k-tile-s">' + esc(item.subtitle) + '</div>' : '') +
|
|
145
|
+
'</div>'
|
|
146
|
+
: '';
|
|
147
|
+
// Clicking the tile sends the id, but nothing on a bare thumbnail signals
|
|
148
|
+
// that — the explicit buttons keep the affordance visible (and on touch
|
|
149
|
+
// hosts CSS keeps them permanently on, since there is no hover there).
|
|
150
|
+
var acts = '<div class="k-tile-acts">' +
|
|
151
|
+
'<button class="k-tile-act" data-use="' + idx + '" title="Use" aria-label="Use">' + ICONS.arrowRight + '</button>' +
|
|
152
|
+
(item.url ? '<button class="k-tile-act" data-dl="' + esc(item.url) + '" title="Download" aria-label="Download">' + ICONS.download + '</button>' : '') +
|
|
104
153
|
'</div>';
|
|
154
|
+
return '<div class="k-tile" data-i="' + idx + '" title="' + esc(item.title || '') + '">' + media +
|
|
155
|
+
(isVideo ? '<button class="k-play k-tile-play" data-video-play="' + esc(item.url) + '">' + ICONS.play + '</button>' : '') +
|
|
156
|
+
cap + acts + '</div>';
|
|
105
157
|
}
|
|
106
158
|
|
|
107
159
|
function audioRowHTML(item) {
|
|
108
160
|
var idx = state.items.indexOf(item);
|
|
109
161
|
var src = item.preview_audio || item.url;
|
|
110
|
-
// Same onerror contract as
|
|
162
|
+
// Same onerror contract as tileHTML, and for the same reason: a thumbnail the
|
|
111
163
|
// host cannot fetch (CSP, 404, expired) otherwise renders the browser's
|
|
112
|
-
// broken-image glyph.
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
// this.outerHTML, NOT this.parentNode.innerHTML: here the <img> IS the art and
|
|
116
|
-
// its parent is the whole row, so the cell version would erase the title,
|
|
117
|
-
// subtitle, Use button and player along with it.
|
|
164
|
+
// broken-image glyph. this.outerHTML, NOT this.parentNode.innerHTML: here the
|
|
165
|
+
// <img> IS the art and its parent is the whole row, so wiping the parent would
|
|
166
|
+
// erase the title, subtitle, Use button and player along with it.
|
|
118
167
|
var artFallback = '<div class="k-audio-art k-audio-art-fallback">' + kindIcon('audio') + '</div>';
|
|
119
168
|
return '<div class="k-audio-row k-generated-audio" data-i="' + idx + '">' +
|
|
120
169
|
(item.thumbnail
|
|
@@ -130,41 +179,59 @@ function audioRowHTML(item) {
|
|
|
130
179
|
}
|
|
131
180
|
|
|
132
181
|
function wire() {
|
|
133
|
-
|
|
134
|
-
|
|
182
|
+
var stage = el('stage');
|
|
183
|
+
bindPeekHits(stage);
|
|
184
|
+
Array.prototype.forEach.call(stage.querySelectorAll('.k-tile, .k-audio-row'), function (c) {
|
|
135
185
|
c.onclick = function (e) {
|
|
136
|
-
if (e.target && e.target.closest && e.target.closest('[data-peek]')) return;
|
|
186
|
+
if (e.target && e.target.closest && e.target.closest('[data-peek],[data-use],[data-dl],[data-video-play]')) return;
|
|
137
187
|
useItem(+c.getAttribute('data-i'));
|
|
138
188
|
};
|
|
139
189
|
});
|
|
140
|
-
Array.prototype.forEach.call(
|
|
190
|
+
Array.prototype.forEach.call(stage.querySelectorAll('[data-use]'), function (b) {
|
|
141
191
|
b.onclick = function (e) { e.stopPropagation(); useItem(+b.getAttribute('data-use')); };
|
|
142
192
|
});
|
|
193
|
+
Array.prototype.forEach.call(stage.querySelectorAll('.k-tile-act[data-dl]'), function (b) {
|
|
194
|
+
b.onclick = function (e) { e.stopPropagation(); window.kolbo.openLink(downloadUrl(b.getAttribute('data-dl'))); };
|
|
195
|
+
});
|
|
196
|
+
Array.prototype.forEach.call(stage.querySelectorAll('[data-page]'), function (d) {
|
|
197
|
+
d.onclick = function () { var p = +d.getAttribute('data-page'); if (p !== page) { page = p; render(); } };
|
|
198
|
+
});
|
|
143
199
|
// Native <audio controls> already gives play/pause + a seek bar + duration;
|
|
144
200
|
// just stop clicks on it from bubbling up to the row's "Use" handler, and
|
|
145
201
|
// pause any other preview when a new one starts.
|
|
146
|
-
var players =
|
|
202
|
+
var players = stage.querySelectorAll('.k-audio-player');
|
|
147
203
|
Array.prototype.forEach.call(players, function (player) {
|
|
148
204
|
player.onclick = function (e) { e.stopPropagation(); };
|
|
149
205
|
player.addEventListener('play', function () {
|
|
150
206
|
Array.prototype.forEach.call(players, function (other) { if (other !== player) other.pause(); });
|
|
151
207
|
});
|
|
152
208
|
});
|
|
153
|
-
// Video
|
|
154
|
-
//
|
|
155
|
-
|
|
209
|
+
// Video tiles stay a static thumbnail until the play overlay is clicked, then
|
|
210
|
+
// swap the <img> for a native <video controls>. Replace only the image — the
|
|
211
|
+
// caption goes because the control bar lands in exactly that strip.
|
|
212
|
+
Array.prototype.forEach.call(stage.querySelectorAll('[data-video-play]'), function (b) {
|
|
156
213
|
b.onclick = function (e) {
|
|
157
214
|
e.stopPropagation();
|
|
158
|
-
var
|
|
159
|
-
var
|
|
160
|
-
|
|
161
|
-
|
|
215
|
+
var tile = b.parentNode;
|
|
216
|
+
var v = document.createElement('video');
|
|
217
|
+
v.src = b.getAttribute('data-video-play');
|
|
218
|
+
v.controls = true; v.autoplay = true; v.playsInline = true;
|
|
219
|
+
v.setAttribute('style', 'width:100%;height:100%;object-fit:contain;background:#000');
|
|
220
|
+
v.onclick = function (ev) { ev.stopPropagation(); };
|
|
221
|
+
var img = tile.querySelector('img');
|
|
222
|
+
if (img) tile.replaceChild(v, img); else tile.insertBefore(v, tile.firstChild);
|
|
223
|
+
var cap = tile.querySelector('.k-tile-cap');
|
|
224
|
+
if (cap && cap.parentNode) cap.parentNode.removeChild(cap);
|
|
225
|
+
if (b.parentNode) b.parentNode.removeChild(b);
|
|
162
226
|
};
|
|
163
227
|
});
|
|
164
|
-
var
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
228
|
+
var prev = el('page-prev');
|
|
229
|
+
if (prev) prev.onclick = function () { if (page > 0) { page--; render(); } };
|
|
230
|
+
var next = el('page-next');
|
|
231
|
+
if (next) next.onclick = function () {
|
|
232
|
+
if (page < pageCount() - 1) { page++; render(); return; }
|
|
233
|
+
fetchNextPage(next);
|
|
234
|
+
};
|
|
168
235
|
}
|
|
169
236
|
|
|
170
237
|
// Fetch the next page IN the widget and append it.
|
|
@@ -174,14 +241,12 @@ function wire() {
|
|
|
174
241
|
// does — window.kolbo.callTool, the same bridge call every generation card
|
|
175
242
|
// polls status with. And the message could not have worked anyway: the payload
|
|
176
243
|
// carried no page number and none of the filters, so the model had nothing to
|
|
177
|
-
// reconstruct the query from and would re-run page 1 or something else.
|
|
178
|
-
// button appeared to do nothing.
|
|
244
|
+
// reconstruct the query from and would re-run page 1 or something else.
|
|
179
245
|
function fetchNextPage(btn) {
|
|
180
246
|
if (!state || !state.page_tool || btn.disabled) return;
|
|
181
|
-
var
|
|
247
|
+
var nextPage = (state.page || 1) + 1;
|
|
182
248
|
btn.disabled = true;
|
|
183
|
-
|
|
184
|
-
btn.innerHTML = '<span class="k-spin"></span> Loading';
|
|
249
|
+
btn.innerHTML = '<span class="k-spin"></span>';
|
|
185
250
|
// The server knows its own paging arg names (page/limit, offset, cursor…):
|
|
186
251
|
// when it shipped next_args, send exactly that. The query+page shape is the
|
|
187
252
|
// legacy fallback list_media has always used.
|
|
@@ -191,7 +256,7 @@ function fetchNextPage(btn) {
|
|
|
191
256
|
} else {
|
|
192
257
|
var q = state.query || {};
|
|
193
258
|
for (var k in q) { if (q[k] !== undefined && q[k] !== null && q[k] !== '') args[k] = q[k]; }
|
|
194
|
-
args.page =
|
|
259
|
+
args.page = nextPage;
|
|
195
260
|
if (state.page_size) args.page_size = state.page_size;
|
|
196
261
|
}
|
|
197
262
|
|
|
@@ -199,26 +264,30 @@ function fetchNextPage(btn) {
|
|
|
199
264
|
var sc = structured(res);
|
|
200
265
|
if (res && res.isError) {
|
|
201
266
|
btn.disabled = false;
|
|
202
|
-
btn.
|
|
267
|
+
btn.innerHTML = ICONS.chevronRight;
|
|
268
|
+
btn.title = 'Could not load more — try again';
|
|
203
269
|
return;
|
|
204
270
|
}
|
|
205
271
|
var more = (sc && sc.items) || [];
|
|
206
272
|
if (!more.length) {
|
|
207
|
-
// Nothing came back:
|
|
208
|
-
// looks like it has pages behind it.
|
|
209
|
-
|
|
273
|
+
// Nothing came back: retire the control rather than leaving a Next that
|
|
274
|
+
// still looks like it has pages behind it.
|
|
275
|
+
state.has_next = false;
|
|
276
|
+
state.next_args = undefined;
|
|
277
|
+
render();
|
|
210
278
|
return;
|
|
211
279
|
}
|
|
212
280
|
state.items = (state.items || []).concat(more);
|
|
213
|
-
state.page = (sc && sc.page) ||
|
|
281
|
+
state.page = (sc && sc.page) || nextPage;
|
|
214
282
|
// The next page's args come from THIS response; none means this was the last page.
|
|
215
283
|
state.next_args = sc && sc.next_args;
|
|
216
|
-
if (sc && sc.
|
|
217
|
-
|
|
218
|
-
|
|
284
|
+
if (sc && sc.total != null && sc.total >= 0) state.total = sc.total;
|
|
285
|
+
state.has_next = sc && typeof sc.has_next === 'boolean' ? sc.has_next : undefined;
|
|
286
|
+
page = page + 1;
|
|
287
|
+
render();
|
|
219
288
|
}).catch(function () {
|
|
220
289
|
btn.disabled = false;
|
|
221
|
-
btn.
|
|
290
|
+
btn.innerHTML = ICONS.chevronRight;
|
|
222
291
|
});
|
|
223
292
|
}
|
|
224
293
|
|