@kolbo/mcp 1.79.7 → 1.81.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.
- package/README.md +6 -2
- package/package.json +1 -1
- package/src/apps/bridge.js +2 -0
- package/src/apps/html.js +90 -1
- package/src/apps/index.js +153 -5
- package/src/apps/theme.js +42 -4
- package/src/apps/widgets/generation.js +198 -84
- package/src/apps/widgets/list.js +4 -1
- package/src/apps/widgets/mediaGrid.js +10 -3
- package/src/apps/widgets/transcript.js +4 -1
- package/src/client.js +14 -6
- package/src/toolAnnotations.js +5 -2
- package/src/tools/_shared.js +159 -24
- package/src/tools/agents.js +1 -1
- package/src/tools/docs.js +1 -1
- package/src/tools/generate.js +45 -21
- package/src/tools/moodboards.js +3 -2
- package/src/tools/presets.js +37 -36
- package/src/tools/projects.js +148 -5
- package/src/tools/visual_dna.js +126 -9
package/README.md
CHANGED
|
@@ -153,6 +153,7 @@ Every generation tool also accepts an optional `project_id` arg that routes the
|
|
|
153
153
|
| Tool | Description |
|
|
154
154
|
|------|-------------|
|
|
155
155
|
| `create_visual_dna` | Create a profile from URLs or local files |
|
|
156
|
+
| `update_visual_dna` | Edit name, description, stills, sheet, or type in place (never delete+recreate) |
|
|
156
157
|
| `list_visual_dnas` | List your profiles |
|
|
157
158
|
| `get_visual_dna` | Fetch one profile |
|
|
158
159
|
| `delete_visual_dna` | Delete a profile |
|
|
@@ -162,6 +163,7 @@ Every generation tool also accepts an optional `project_id` arg that routes the
|
|
|
162
163
|
|------|-------------|
|
|
163
164
|
| `list_moodboards` | Browse presets + your moodboards |
|
|
164
165
|
| `get_moodboard` | Fetch one moodboard with all image URLs |
|
|
166
|
+
| `create_moodboard` / `update_moodboard` / `delete_moodboard` | Create / edit in place / delete |
|
|
165
167
|
|
|
166
168
|
**Color DNA** — sticky, account-wide: the ACTIVE palette strict-grades every generation until deactivated. Opt a single generation out with `skip_color_palette`.
|
|
167
169
|
| Tool | Description |
|
|
@@ -238,7 +240,8 @@ Every generation tool also accepts an optional `project_id` arg that routes the
|
|
|
238
240
|
| `list_voices` | TTS voices (presets + cloned) |
|
|
239
241
|
| `list_presets` | Generation presets across image/image-edit/video/music/text-to-video catalogs. Pass the selected exact id as `preset_id`; never claim a preset was applied without it. |
|
|
240
242
|
| `list_cinematic_presets` | "Cinema mode" presets grouped by dimension (camera, lens, focal_length, aperture, angle, shot_type, color_palette, lighting) — pass ids via the `cinematic` arg on `generate_image` / `generate_image_edit`. Only when the user wants a specific cinematic look |
|
|
241
|
-
| `list_projects` | List owned + shared projects (id, name, role, is_default) — call first to resolve a project name into the `project_id` you pass to generation tools |
|
|
243
|
+
| `list_projects` | List owned + shared projects (id, name, description, role, is_default) — call first to resolve a project name into the `project_id` you pass to generation tools |
|
|
244
|
+
| `get_project` | Full project record including the unclipped description — read this before `update_project` |
|
|
242
245
|
| `move_session` | Move ONE session (generation, chat, transcription…) and ALL its generations + media to another project |
|
|
243
246
|
| `bulk_move_sessions` | Move up to 100 sessions into one project in a single call — mixed types allowed, per-session failures reported |
|
|
244
247
|
| `list_session_generations` | A session's generations as complete groups (prompt + all its outputs) — the ids the two organize tools below take |
|
|
@@ -246,7 +249,7 @@ Every generation tool also accepts an optional `project_id` arg that routes the
|
|
|
246
249
|
| `split_session` | Carve selected generations out into a brand-new named session, atomically |
|
|
247
250
|
| `undo_session_organization` | Reverse a move/split within 15 minutes, using the `operation_id` it returned |
|
|
248
251
|
| `create_doc` / `list_docs` / `get_doc` / `update_doc` / `share_doc` / `delete_doc` | AI Docs (Magic Pad): author project-scoped HTML documents, edit them, get public share links |
|
|
249
|
-
| `generate_character_sheet` | Generate a multi-angle character sheet from reference images (credits) → pass URL to create_visual_dna
|
|
252
|
+
| `generate_character_sheet` | Generate a multi-angle character sheet from reference images (credits) → pass URL to create_visual_dna or update_visual_dna |
|
|
250
253
|
| `list_visual_dna_folders` / `create_visual_dna_folder` / `update_visual_dna_folder` / `delete_visual_dna_folder` / `move_visual_dna_to_folder` | Organize Visual DNA characters into user folders (create/rename/recolor/delete + move DNAs in/out) |
|
|
251
254
|
| `create_project` / `update_project` / `archive_project` / `unarchive_project` | Project lifecycle (create/rename/describe/archive; deletion stays in-app) |
|
|
252
255
|
| `list_agents` / `create_agent` / `update_agent` / `delete_agent` | Custom chat agents (reusable named personas; `description` is the system instruction) |
|
|
@@ -254,6 +257,7 @@ Every generation tool also accepts an optional `project_id` arg that routes the
|
|
|
254
257
|
| `list_sessions` | Enumerate sessions across all types, filterable by project, `type`, and `types[]` |
|
|
255
258
|
| `rename_session` / `delete_session` / `restore_session` | Rename a session; soft-delete leftovers after a move; restore from trash |
|
|
256
259
|
| `add_project_context` / `list_project_context` / `delete_project_context` / `get_project_profile` / `regenerate_project_profile` | Project knowledge base (RAG): feed scripts/URLs/notes, read the synthesized living brief |
|
|
260
|
+
| `list_project_assets` / `link_project_asset` / `unlink_project_asset` / `update_project_asset` | Project cast: tag Visual DNAs / moodboards onto a project, write each DNA's description and purpose note |
|
|
257
261
|
| `create_moodboard` / `update_moodboard` / `delete_moodboard` | Build/edit moodboards from image URLs (AI style analysis → master prompt) |
|
|
258
262
|
| `clone_voice` / `import_elevenlabs_voice` / `delete_voice` | Custom voices: clone from an audio sample, import by ElevenLabs ID, delete |
|
|
259
263
|
| `trim_video` | Frame-accurate server-side trim of a Kolbo-hosted video (async job, tool waits) |
|
package/package.json
CHANGED
package/src/apps/bridge.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* .callTool(name, args) — Promise<CallToolResult>
|
|
24
24
|
* .sendMessage(text) — append a user chat message (returns Promise)
|
|
25
25
|
* .openLink(url) — open external URL
|
|
26
|
+
* .copyText(text) — copy via the host clipboard (ui/copy-text)
|
|
26
27
|
* .notifySize() — report content size to host
|
|
27
28
|
*/
|
|
28
29
|
|
|
@@ -183,6 +184,7 @@ const BRIDGE_JS = `
|
|
|
183
184
|
return request('ui/message', { role: 'user', content: [{ type: 'text', text: text }] });
|
|
184
185
|
},
|
|
185
186
|
openLink: function (url) { return request('ui/open-link', { url: url }); },
|
|
187
|
+
copyText: function (text) { return request('ui/copy-text', { text: text }); },
|
|
186
188
|
// Hand a piece of this widget's media to the host's composer. Dragging it
|
|
187
189
|
// out cannot work: a widget is a sandboxed cross-origin iframe, so a native
|
|
188
190
|
// HTML5 drag started in here never delivers its dataTransfer to the host
|
package/src/apps/html.js
CHANGED
|
@@ -57,7 +57,10 @@ var ICONS = {
|
|
|
57
57
|
audio: _svg('<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>'),
|
|
58
58
|
document: _svg('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8"/><path d="M8 17h8"/>'),
|
|
59
59
|
cube: _svg('<path d="M21 8l-9-5-9 5 9 5z"/><path d="M3 8v8l9 5 9-5V8"/><path d="M12 13v8"/>'),
|
|
60
|
-
file: _svg('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/>')
|
|
60
|
+
file: _svg('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/>'),
|
|
61
|
+
copy: _svg('<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>'),
|
|
62
|
+
chevronDown: _svg('<path d="M6 9l6 6 6-6"/>'),
|
|
63
|
+
chevronUp: _svg('<path d="M18 15l-6-6-6 6"/>')
|
|
61
64
|
};
|
|
62
65
|
// Media-kind → icon (accepts model kind / media_type strings).
|
|
63
66
|
function kindIcon(kind) {
|
|
@@ -82,6 +85,36 @@ function downloadUrl(u) {
|
|
|
82
85
|
if (!u) return u;
|
|
83
86
|
return 'https://api.kolbo.ai/mcp/download?url=' + encodeURIComponent(u);
|
|
84
87
|
}
|
|
88
|
+
// Copy from a sandboxed widget iframe. Order: Clipboard API (Kolbo Code
|
|
89
|
+
// grants clipboard-write) → execCommand → host ui/copy-text (parent clipboard).
|
|
90
|
+
function writeClipboard(text) {
|
|
91
|
+
text = String(text || '');
|
|
92
|
+
function fallback() {
|
|
93
|
+
if (!document.body) return false;
|
|
94
|
+
var ta = document.createElement('textarea');
|
|
95
|
+
ta.value = text;
|
|
96
|
+
ta.setAttribute('readonly', '');
|
|
97
|
+
ta.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0';
|
|
98
|
+
document.body.appendChild(ta);
|
|
99
|
+
ta.focus();
|
|
100
|
+
ta.select();
|
|
101
|
+
var ok = false;
|
|
102
|
+
try { ok = document.execCommand('copy'); } catch (e) {}
|
|
103
|
+
document.body.removeChild(ta);
|
|
104
|
+
return ok;
|
|
105
|
+
}
|
|
106
|
+
function host() {
|
|
107
|
+
if (!window.kolbo || !window.kolbo.copyText) return Promise.resolve(false);
|
|
108
|
+
return window.kolbo.copyText(text).then(function () { return true; }).catch(function () { return false; });
|
|
109
|
+
}
|
|
110
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
111
|
+
return navigator.clipboard.writeText(text).then(function () { return true; }).catch(function () {
|
|
112
|
+
return fallback() ? Promise.resolve(true) : host();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (fallback()) return Promise.resolve(true);
|
|
116
|
+
return host();
|
|
117
|
+
}
|
|
85
118
|
function fmtCredits(n) { return (n == null) ? '' : (Math.round(n * 100) / 100) + ' cr'; }
|
|
86
119
|
function fmtDur(s) { if (s == null) return ''; s = Math.round(s); return s >= 60 ? Math.floor(s/60) + 'm ' + (s%60) + 's' : s + 's'; }
|
|
87
120
|
function applyTheme(ctx) {
|
|
@@ -105,6 +138,62 @@ function modelChipHTML(name, iconUrl) {
|
|
|
105
138
|
function monogram(name) {
|
|
106
139
|
return '<span class="k-mono-icon">' + esc(String(name).trim().charAt(0).toUpperCase()) + '</span>';
|
|
107
140
|
}
|
|
141
|
+
// In-widget preview overlay — every card (generation chips, media grid, list
|
|
142
|
+
// rows) uses this so a DNA / reference / library thumb opens a popup in Kolbo
|
|
143
|
+
// Code, Claude Code, Codex, and any other host. Host fullscreen is not required.
|
|
144
|
+
function ensurePeek() {
|
|
145
|
+
if (el('peek')) return el('peek');
|
|
146
|
+
var card = document.querySelector('.k-card') || document.body;
|
|
147
|
+
var box = document.createElement('div');
|
|
148
|
+
box.id = 'peek';
|
|
149
|
+
box.className = 'k-peek';
|
|
150
|
+
box.hidden = true;
|
|
151
|
+
box.innerHTML = '<button type="button" class="k-peek-close" id="peek-close" aria-label="Close"></button>'
|
|
152
|
+
+ '<img id="peek-img" alt="">'
|
|
153
|
+
+ '<video id="peek-video" muted playsinline controls></video>'
|
|
154
|
+
+ '<div class="k-peek-cap" id="peek-cap"></div>';
|
|
155
|
+
card.appendChild(box);
|
|
156
|
+
el('peek-close').innerHTML = ICONS.x;
|
|
157
|
+
el('peek-close').onclick = function (e) { e.stopPropagation(); closePeek(); };
|
|
158
|
+
box.onclick = function (e) { if (e.target === box) closePeek(); };
|
|
159
|
+
return box;
|
|
160
|
+
}
|
|
161
|
+
function peekAttrs(url, kind, cap) {
|
|
162
|
+
if (!url) return '';
|
|
163
|
+
return ' data-peek="' + esc(url) + '" data-peek-kind="' + esc(kind || 'image') + '" data-peek-cap="' + esc(cap || '') + '"';
|
|
164
|
+
}
|
|
165
|
+
function bindPeekHits(root) {
|
|
166
|
+
if (!root) return;
|
|
167
|
+
Array.prototype.forEach.call(root.querySelectorAll('[data-peek]'), function (node) {
|
|
168
|
+
node.onclick = function (e) {
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
e.stopPropagation();
|
|
171
|
+
openPeek(node.getAttribute('data-peek'), node.getAttribute('data-peek-kind'), node.getAttribute('data-peek-cap'));
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function openPeek(url, kind, cap) {
|
|
176
|
+
if (!url) return;
|
|
177
|
+
var box = ensurePeek();
|
|
178
|
+
var img = el('peek-img');
|
|
179
|
+
var vid = el('peek-video');
|
|
180
|
+
var video = kind === 'video';
|
|
181
|
+
if (video) { vid.setAttribute('src', url); img.removeAttribute('src'); }
|
|
182
|
+
else { img.setAttribute('src', url); vid.removeAttribute('src'); }
|
|
183
|
+
el('peek-cap').textContent = cap || '';
|
|
184
|
+
box.hidden = false;
|
|
185
|
+
if (window.kolbo && window.kolbo.notifySize) window.kolbo.notifySize();
|
|
186
|
+
}
|
|
187
|
+
function closePeek() {
|
|
188
|
+
var box = el('peek');
|
|
189
|
+
if (!box || box.hidden) return;
|
|
190
|
+
box.hidden = true;
|
|
191
|
+
el('peek-img').removeAttribute('src');
|
|
192
|
+
var vid = el('peek-video');
|
|
193
|
+
if (vid) { try { vid.pause(); } catch (e) {} vid.removeAttribute('src'); }
|
|
194
|
+
if (window.kolbo && window.kolbo.notifySize) window.kolbo.notifySize();
|
|
195
|
+
}
|
|
196
|
+
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closePeek(); });
|
|
108
197
|
// Pull structuredContent out of a tools/call result (host bridge shape).
|
|
109
198
|
function structured(res) {
|
|
110
199
|
if (!res) return null;
|
package/src/apps/index.js
CHANGED
|
@@ -193,8 +193,19 @@ const ICON_CDN_BASE = 'https://kolbo-general-media.fra1.cdn.digitaloceanspaces.c
|
|
|
193
193
|
|
|
194
194
|
function resolveAvatarUrl(avatar) {
|
|
195
195
|
if (!avatar) return null;
|
|
196
|
-
if (/^https?:\/\//i.test(avatar))
|
|
197
|
-
|
|
196
|
+
if (/^https?:\/\//i.test(avatar)) {
|
|
197
|
+
try {
|
|
198
|
+
const parsed = new URL(avatar);
|
|
199
|
+
const file = parsed.pathname.match(/\/(?:models_icons|assets)\/([^/]+)$/);
|
|
200
|
+
// api.kolbo.ai / app.kolbo.ai avatars 404 or get bot-blocked inside
|
|
201
|
+
// sandboxed widget iframes. The CDN copy is the same file.
|
|
202
|
+
if (file && /(?:^|\.)kolbo\.ai$|digitaloceanspaces\.com$/i.test(parsed.hostname)) {
|
|
203
|
+
return `${ICON_CDN_BASE}/${file[1]}`;
|
|
204
|
+
}
|
|
205
|
+
} catch (_) { /* keep the original absolute URL */ }
|
|
206
|
+
return avatar;
|
|
207
|
+
}
|
|
208
|
+
return `${ICON_CDN_BASE}/${encodeURIComponent(String(avatar).replace(/^\/+/, ''))}`;
|
|
198
209
|
}
|
|
199
210
|
|
|
200
211
|
async function modelCatalog(client) {
|
|
@@ -219,7 +230,11 @@ async function modelCatalog(client) {
|
|
|
219
230
|
// the ONLY thing that tells two same-named variants apart. See canonicalModelId.
|
|
220
231
|
const raw = m.types !== undefined ? m.types : m.type;
|
|
221
232
|
const types = (Array.isArray(raw) ? raw : [raw]).filter(Boolean).map(String);
|
|
222
|
-
const
|
|
233
|
+
const aspects = Array.isArray(m.supported_aspect_ratios)
|
|
234
|
+
? m.supported_aspect_ratios
|
|
235
|
+
: (Array.isArray(m.supportedAspectRatios) ? m.supportedAspectRatios : []);
|
|
236
|
+
const aspectsByType = m.supported_aspect_ratios_by_type || m.supportedAspectRatiosByType || null;
|
|
237
|
+
const info = { icon, eta, id: m.identifier || null, name: m.name || null, types, aspects, aspectsByType };
|
|
223
238
|
all.push(info);
|
|
224
239
|
// Display names collide across variants ("Nano Banana 2" names both the
|
|
225
240
|
// t2i model and its editing sibling) — on collision keep the model with
|
|
@@ -251,8 +266,138 @@ async function modelInfoMap(client) {
|
|
|
251
266
|
/** Resolve one model's { icon, eta, name }; missing → all null. */
|
|
252
267
|
async function modelInfo(client, modelName) {
|
|
253
268
|
if (!modelName) return { icon: null, eta: null, name: null };
|
|
254
|
-
const
|
|
255
|
-
|
|
269
|
+
const catalog = await modelCatalog(client);
|
|
270
|
+
const exact = catalog.byKey.get(String(modelName).toLowerCase());
|
|
271
|
+
if (exact) return exact;
|
|
272
|
+
// Same separator-insensitive / prefix leniency as canonicalModelId — the
|
|
273
|
+
// chip used to miss "gpt-image-2" when the catalog only keyed the editor
|
|
274
|
+
// sibling, and the widget fell back to a first-letter monogram.
|
|
275
|
+
const want = normId(modelName);
|
|
276
|
+
if (!want) return { icon: null, eta: null, name: null };
|
|
277
|
+
const rows = catalog.all || [];
|
|
278
|
+
return rows.find((row) => normId(row.id) === want || normId(row.name) === want)
|
|
279
|
+
|| rows.find((row) => normId(row.id).startsWith(want) || normId(row.name).startsWith(want))
|
|
280
|
+
|| { icon: null, eta: null, name: null };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Snap a requested aspect ratio onto the catalog enum for that model.
|
|
285
|
+
* Every image/video generate_* tool must call this after canonicalModelId —
|
|
286
|
+
* list_models already prints supported_aspect_ratios, but the LLM still
|
|
287
|
+
* invents 21:9 / "widescreen" / "21/9". Fail open only when the catalog is
|
|
288
|
+
* empty or the model is unpublished (hidden ids still reach the API).
|
|
289
|
+
*/
|
|
290
|
+
const ASPECT_ALIASES = {
|
|
291
|
+
square: '1:1',
|
|
292
|
+
landscape: '16:9',
|
|
293
|
+
widescreen: '16:9',
|
|
294
|
+
horizontal: '16:9',
|
|
295
|
+
portrait: '9:16',
|
|
296
|
+
vertical: '9:16',
|
|
297
|
+
story: '9:16',
|
|
298
|
+
stories: '9:16',
|
|
299
|
+
reel: '9:16',
|
|
300
|
+
reels: '9:16',
|
|
301
|
+
ultrawide: '21:9',
|
|
302
|
+
cinema: '21:9',
|
|
303
|
+
cinematic: '21:9',
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
function normalizeAspectRatio(requested) {
|
|
307
|
+
if (requested == null || requested === '') return requested;
|
|
308
|
+
const raw = String(requested).trim();
|
|
309
|
+
const lower = raw.toLowerCase();
|
|
310
|
+
if (lower === 'auto' || lower === 'adaptive') return lower;
|
|
311
|
+
if (ASPECT_ALIASES[lower]) return ASPECT_ALIASES[lower];
|
|
312
|
+
const pair = lower.match(/^(\d+(?:\.\d+)?)\s*[:x×/\-]\s*(\d+(?:\.\d+)?)$/);
|
|
313
|
+
if (!pair) return raw;
|
|
314
|
+
const a = Number(pair[1]);
|
|
315
|
+
const b = Number(pair[2]);
|
|
316
|
+
if (!Number.isFinite(a) || !Number.isFinite(b) || a <= 0 || b <= 0) return raw;
|
|
317
|
+
return Number.isInteger(a) && Number.isInteger(b) ? `${a}:${b}` : `${a}:${b}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function parseAspectDecimal(ratio) {
|
|
321
|
+
const normalized = normalizeAspectRatio(ratio);
|
|
322
|
+
const parts = String(normalized || '').split(':').map(Number);
|
|
323
|
+
if (parts.length !== 2 || parts.some((n) => !Number.isFinite(n) || n <= 0)) return 1;
|
|
324
|
+
return parts[0] / parts[1];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function aspectKey(ratio) {
|
|
328
|
+
return String(normalizeAspectRatio(ratio) || '').toLowerCase();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function closestAspectRatio(requested, supported) {
|
|
332
|
+
const normalized = normalizeAspectRatio(requested);
|
|
333
|
+
if (!normalized) return requested;
|
|
334
|
+
if (normalized === 'auto' || normalized === 'adaptive') return normalized;
|
|
335
|
+
if (!Array.isArray(supported) || supported.length === 0) return normalized;
|
|
336
|
+
const exact = supported.find((ratio) => aspectKey(ratio) === aspectKey(normalized));
|
|
337
|
+
if (exact) return exact;
|
|
338
|
+
const concrete = supported.filter((ratio) => {
|
|
339
|
+
const key = aspectKey(ratio);
|
|
340
|
+
return key && key !== 'auto' && key !== 'adaptive';
|
|
341
|
+
});
|
|
342
|
+
if (!concrete.length) return normalized;
|
|
343
|
+
const target = parseAspectDecimal(normalized);
|
|
344
|
+
let best = concrete[0];
|
|
345
|
+
let bestDist = Infinity;
|
|
346
|
+
for (const ratio of concrete) {
|
|
347
|
+
const dist = Math.abs(parseAspectDecimal(ratio) - target);
|
|
348
|
+
if (dist < bestDist) {
|
|
349
|
+
bestDist = dist;
|
|
350
|
+
best = ratio;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return best;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function findCatalogModel(catalog, modelId, type) {
|
|
357
|
+
if (!catalog || !modelId) return null;
|
|
358
|
+
const key = String(modelId).toLowerCase().trim();
|
|
359
|
+
const want = normId(key);
|
|
360
|
+
if (!want) return null;
|
|
361
|
+
const byKey = catalog.byKey && catalog.byKey.get(key);
|
|
362
|
+
if (byKey) return byKey;
|
|
363
|
+
const all = catalog.all || [];
|
|
364
|
+
const types = (Array.isArray(type) ? type : [type]).filter(Boolean);
|
|
365
|
+
const candidates = all.filter((info) =>
|
|
366
|
+
(info.id && (info.id.toLowerCase() === key || normId(info.id) === want))
|
|
367
|
+
|| (info.name && (info.name.toLowerCase() === key || normId(info.name) === want))
|
|
368
|
+
);
|
|
369
|
+
if (!candidates.length) return null;
|
|
370
|
+
if (types.length) {
|
|
371
|
+
const typed = candidates.filter((info) => Array.isArray(info.types) && info.types.some((t) => types.includes(t)));
|
|
372
|
+
if (typed.length) return typed[0];
|
|
373
|
+
}
|
|
374
|
+
return candidates[0];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function supportedAspectsFor(info, type) {
|
|
378
|
+
if (!info) return [];
|
|
379
|
+
const byType = type && info.aspectsByType;
|
|
380
|
+
if (byType && typeof byType === 'object') {
|
|
381
|
+
const typed = Array.isArray(type)
|
|
382
|
+
? type.map((t) => byType[t]).find((arr) => Array.isArray(arr) && arr.length)
|
|
383
|
+
: byType[type];
|
|
384
|
+
if (Array.isArray(typed) && typed.length) return typed;
|
|
385
|
+
}
|
|
386
|
+
return Array.isArray(info.aspects) ? info.aspects : [];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function resolveCatalogAspectRatio(client, modelId, requested, type) {
|
|
390
|
+
if (!requested) return requested;
|
|
391
|
+
const normalized = normalizeAspectRatio(requested);
|
|
392
|
+
if (!modelId) return normalized;
|
|
393
|
+
try {
|
|
394
|
+
const catalog = await modelCatalog(client);
|
|
395
|
+
const info = findCatalogModel(catalog, modelId, type);
|
|
396
|
+
if (!info) return normalized;
|
|
397
|
+
return closestAspectRatio(normalized, supportedAspectsFor(info, type));
|
|
398
|
+
} catch (_) {
|
|
399
|
+
return normalized;
|
|
400
|
+
}
|
|
256
401
|
}
|
|
257
402
|
|
|
258
403
|
/* ------------------------------------------------------------------ */
|
|
@@ -512,6 +657,9 @@ module.exports = {
|
|
|
512
657
|
modelInfoMap,
|
|
513
658
|
voiceInfo,
|
|
514
659
|
canonicalModelId,
|
|
660
|
+
normalizeAspectRatio,
|
|
661
|
+
closestAspectRatio,
|
|
662
|
+
resolveCatalogAspectRatio,
|
|
515
663
|
resolveAvatarUrl,
|
|
516
664
|
widgetHtml, // exported for smoke tests
|
|
517
665
|
};
|
package/src/apps/theme.js
CHANGED
|
@@ -59,6 +59,7 @@ body {
|
|
|
59
59
|
|
|
60
60
|
/* ---- Card shell ---- */
|
|
61
61
|
.k-card {
|
|
62
|
+
position: relative;
|
|
62
63
|
background: var(--card);
|
|
63
64
|
backdrop-filter: blur(20px) saturate(180%);
|
|
64
65
|
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
|
@@ -81,10 +82,20 @@ body {
|
|
|
81
82
|
.k-head .k-spacer { flex: 1; }
|
|
82
83
|
|
|
83
84
|
.k-body { padding: 14px 16px; }
|
|
84
|
-
.k-prompt { color: var(--text-muted); font-size: 12.5px; margin-bottom:
|
|
85
|
-
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
86
|
-
|
|
85
|
+
.k-prompt { color: var(--text-muted); font-size: 12.5px; margin-bottom: 4px; word-break: break-word;
|
|
86
|
+
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
87
|
+
user-select: text; -webkit-user-select: text; cursor: text; }
|
|
87
88
|
.k-prompt.expanded { -webkit-line-clamp: unset; }
|
|
89
|
+
.k-text-tools { display: flex; gap: 2px; justify-content: flex-end; margin: 0 0 10px; }
|
|
90
|
+
.k-text-btn {
|
|
91
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
92
|
+
padding: 3px 8px; border: 0; border-radius: 6px;
|
|
93
|
+
background: transparent; color: var(--text-faint);
|
|
94
|
+
font-size: 11px; font-weight: 600; font-family: inherit;
|
|
95
|
+
cursor: pointer;
|
|
96
|
+
}
|
|
97
|
+
.k-text-btn:hover { color: var(--text); background: var(--surface-2); }
|
|
98
|
+
.k-text-btn svg { width: 12px; height: 12px; }
|
|
88
99
|
/* @VisualDNA / #Moodboard mentions are load-bearing prompt syntax, not prose —
|
|
89
100
|
the server resolves them to the actual asset. Mark them so a glance at the
|
|
90
101
|
prompt shows which references it pulls in. */
|
|
@@ -96,8 +107,10 @@ body {
|
|
|
96
107
|
}
|
|
97
108
|
/* Single-line media caption (scene / batch prompt under the viewer) */
|
|
98
109
|
.k-caption { font-size: 11px; color: var(--text-faint); margin: 2px 2px 0;
|
|
99
|
-
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
110
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
111
|
+
user-select: text; -webkit-user-select: text; cursor: text; }
|
|
100
112
|
.k-caption.expanded { white-space: normal; word-break: break-word; }
|
|
113
|
+
.k-caption + .k-text-tools { margin: 0 2px 6px; justify-content: flex-start; }
|
|
101
114
|
|
|
102
115
|
/* ---- Chips ---- */
|
|
103
116
|
.k-chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 12px; }
|
|
@@ -122,6 +135,26 @@ body {
|
|
|
122
135
|
}
|
|
123
136
|
.k-chip img.k-voice-thumb { width: 18px; height: 18px; border-radius: 999px; margin-left: -3px; }
|
|
124
137
|
.k-ref-thumb { width: 26px; height: 26px; border-radius: 6px; object-fit: cover; border: 1px solid var(--border-strong); }
|
|
138
|
+
.k-peek-hit { cursor: zoom-in; }
|
|
139
|
+
.k-peek {
|
|
140
|
+
position: absolute; inset: 0; z-index: 20;
|
|
141
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
142
|
+
gap: 10px; padding: 28px 16px 16px;
|
|
143
|
+
background: rgba(8, 10, 16, 0.92);
|
|
144
|
+
}
|
|
145
|
+
.k-peek[hidden] { display: none !important; }
|
|
146
|
+
.k-peek img, .k-peek video {
|
|
147
|
+
display: none; max-width: 100%; max-height: min(360px, 70vh);
|
|
148
|
+
border-radius: 10px; object-fit: contain; background: #000;
|
|
149
|
+
}
|
|
150
|
+
.k-peek img[src], .k-peek video[src] { display: block; }
|
|
151
|
+
.k-peek-close {
|
|
152
|
+
position: absolute; top: 8px; right: 8px;
|
|
153
|
+
width: 28px; height: 28px; border: 0; border-radius: 999px;
|
|
154
|
+
background: var(--surface-2); color: var(--text); cursor: pointer;
|
|
155
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
156
|
+
}
|
|
157
|
+
.k-peek-cap { font-size: 12px; color: var(--text-muted); text-align: center; max-width: 100%; word-break: break-word; }
|
|
125
158
|
/* Visual DNA chips: the character's face, so you can see WHICH DNA is locked in. */
|
|
126
159
|
.k-dna-face { width: 18px; height: 18px; border-radius: 999px; object-fit: cover; margin-left: -3px; background: var(--border-strong); }
|
|
127
160
|
.k-dna-stack .k-dna-stack-item { display: inline-flex; }
|
|
@@ -264,6 +297,10 @@ html.k-fullscreen .k-actions { flex: none; padding-top: 8px; }
|
|
|
264
297
|
box-shadow: 0 2px 12px rgba(59, 130, 246, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.25); }
|
|
265
298
|
.k-btn.primary:hover { background: #2f74e8; }
|
|
266
299
|
.k-btn.ghost { background: transparent; box-shadow: none; border-color: transparent; color: var(--text-muted); }
|
|
300
|
+
.k-btn.danger { background: var(--error); border-color: var(--error); color: #fff;
|
|
301
|
+
box-shadow: 0 2px 12px rgba(239, 68, 68, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.25); }
|
|
302
|
+
.k-btn.danger:hover { background: #dc2626; }
|
|
303
|
+
.k-stop-ask { color: var(--text-muted); font-size: 12px; font-weight: 500; margin-right: 4px; }
|
|
267
304
|
.k-btn.ghost:hover { color: var(--text); background: var(--surface-2); }
|
|
268
305
|
.k-btn svg { width: 13px; height: 13px; }
|
|
269
306
|
/* Inline SVG icons (replace emoji) — align with text, inherit color, never shrink. */
|
|
@@ -333,6 +370,7 @@ html.k-fullscreen .k-actions { flex: none; padding-top: 8px; }
|
|
|
333
370
|
.k-prompt-row { flex-wrap: wrap; }
|
|
334
371
|
.k-prompt-row .k-input { min-width: 0; flex: 1 1 100%; }
|
|
335
372
|
.k-prompt-row .k-btn { flex: 1 1 auto; }
|
|
373
|
+
.k-text-btn { min-height: 32px; padding: 6px 10px; }
|
|
336
374
|
}
|
|
337
375
|
.k-play {
|
|
338
376
|
width: 32px; height: 32px; border-radius: 50%; flex: none; border: 1px solid rgba(255,255,255,0.18);
|