@cloudinary/asset-management-mcp 0.8.1 → 0.9.0-rc.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.
Files changed (83) hide show
  1. package/README.md +2 -2
  2. package/bin/mcp-server.js +4849 -305
  3. package/bin/mcp-server.js.map +25 -21
  4. package/esm/landing-page.js +1 -1
  5. package/esm/lib/config.d.ts +3 -3
  6. package/esm/lib/config.js +3 -3
  7. package/esm/lib/config.js.map +1 -1
  8. package/esm/lib/encodings.d.ts +1 -0
  9. package/esm/lib/encodings.d.ts.map +1 -1
  10. package/esm/lib/encodings.js +26 -5
  11. package/esm/lib/encodings.js.map +1 -1
  12. package/esm/lib/security.d.ts +1 -1
  13. package/esm/lib/security.d.ts.map +1 -1
  14. package/esm/lib/security.js +29 -17
  15. package/esm/lib/security.js.map +1 -1
  16. package/esm/mcp-server/mcp-server.js +1 -1
  17. package/esm/mcp-server/mcp-server.js.map +1 -1
  18. package/esm/mcp-server/server.extensions.d.ts.map +1 -1
  19. package/esm/mcp-server/server.extensions.js +84 -0
  20. package/esm/mcp-server/server.extensions.js.map +1 -1
  21. package/esm/mcp-server/server.js +1 -1
  22. package/esm/mcp-server/server.js.map +1 -1
  23. package/esm/mcp-server/tools/assetsGetResourceByAssetId.d.ts.map +1 -1
  24. package/esm/mcp-server/tools/assetsGetResourceByAssetId.js +4 -0
  25. package/esm/mcp-server/tools/assetsGetResourceByAssetId.js.map +1 -1
  26. package/esm/mcp-server/tools/assetsListImages.d.ts +1 -1
  27. package/esm/mcp-server/tools/assetsListImages.d.ts.map +1 -1
  28. package/esm/mcp-server/tools/assetsListImages.js +4 -0
  29. package/esm/mcp-server/tools/assetsListImages.js.map +1 -1
  30. package/esm/mcp-server/tools/assetsListRawFiles.d.ts +1 -1
  31. package/esm/mcp-server/tools/assetsListRawFiles.d.ts.map +1 -1
  32. package/esm/mcp-server/tools/assetsListRawFiles.js +4 -0
  33. package/esm/mcp-server/tools/assetsListRawFiles.js.map +1 -1
  34. package/esm/mcp-server/tools/assetsListVideos.d.ts +1 -1
  35. package/esm/mcp-server/tools/assetsListVideos.d.ts.map +1 -1
  36. package/esm/mcp-server/tools/assetsListVideos.js +4 -0
  37. package/esm/mcp-server/tools/assetsListVideos.js.map +1 -1
  38. package/esm/mcp-server/tools/searchSearchAssets.d.ts.map +1 -1
  39. package/esm/mcp-server/tools/searchSearchAssets.js +4 -0
  40. package/esm/mcp-server/tools/searchSearchAssets.js.map +1 -1
  41. package/esm/mcp-server/tools/uploadUpload.d.ts.map +1 -1
  42. package/esm/mcp-server/tools/uploadUpload.js +4 -0
  43. package/esm/mcp-server/tools/uploadUpload.js.map +1 -1
  44. package/esm/mcp-server/tools.d.ts +2 -0
  45. package/esm/mcp-server/tools.d.ts.map +1 -1
  46. package/esm/mcp-server/tools.js +2 -0
  47. package/esm/mcp-server/tools.js.map +1 -1
  48. package/esm/mcp-server/widgets/asset-details-widget.d.ts +3 -0
  49. package/esm/mcp-server/widgets/asset-details-widget.d.ts.map +1 -0
  50. package/esm/mcp-server/widgets/asset-details-widget.js +299 -0
  51. package/esm/mcp-server/widgets/asset-details-widget.js.map +1 -0
  52. package/esm/mcp-server/widgets/asset-gallery-widget.d.ts +4 -0
  53. package/esm/mcp-server/widgets/asset-gallery-widget.d.ts.map +1 -0
  54. package/esm/mcp-server/widgets/asset-gallery-widget.js +1063 -0
  55. package/esm/mcp-server/widgets/asset-gallery-widget.js.map +1 -0
  56. package/esm/mcp-server/widgets/asset-upload-widget.d.ts +3 -0
  57. package/esm/mcp-server/widgets/asset-upload-widget.d.ts.map +1 -0
  58. package/esm/mcp-server/widgets/asset-upload-widget.js +1093 -0
  59. package/esm/mcp-server/widgets/asset-upload-widget.js.map +1 -0
  60. package/esm/mcp-server/widgets/widget-shared.d.ts +9 -0
  61. package/esm/mcp-server/widgets/widget-shared.d.ts.map +1 -0
  62. package/esm/mcp-server/widgets/widget-shared.js +2019 -0
  63. package/esm/mcp-server/widgets/widget-shared.js.map +1 -0
  64. package/esm/models/fieldsspec.d.ts +1 -1
  65. package/package.json +1 -1
  66. package/src/landing-page.ts +1 -1
  67. package/src/lib/config.ts +3 -3
  68. package/src/lib/encodings.ts +32 -4
  69. package/src/lib/security.ts +14 -2
  70. package/src/mcp-server/mcp-server.ts +1 -1
  71. package/src/mcp-server/server.extensions.ts +97 -0
  72. package/src/mcp-server/server.ts +1 -1
  73. package/src/mcp-server/tools/assetsGetResourceByAssetId.ts +4 -0
  74. package/src/mcp-server/tools/assetsListImages.ts +4 -0
  75. package/src/mcp-server/tools/assetsListRawFiles.ts +4 -0
  76. package/src/mcp-server/tools/assetsListVideos.ts +4 -0
  77. package/src/mcp-server/tools/searchSearchAssets.ts +4 -0
  78. package/src/mcp-server/tools/uploadUpload.ts +4 -0
  79. package/src/mcp-server/tools.ts +4 -0
  80. package/src/mcp-server/widgets/asset-details-widget.ts +313 -0
  81. package/src/mcp-server/widgets/asset-gallery-widget.ts +1077 -0
  82. package/src/mcp-server/widgets/asset-upload-widget.ts +1115 -0
  83. package/src/mcp-server/widgets/widget-shared.ts +2030 -0
@@ -0,0 +1,1093 @@
1
+ /*
2
+ * MCP App widget for uploading assets to Cloudinary.
3
+ * Attached to the upload-asset tool.
4
+ *
5
+ * Features:
6
+ * - Schema-driven form: fields are auto-generated from the Zod schema
7
+ * at build time via z.toJSONSchema(), grouped into collapsible sections.
8
+ * - Staged upload: file/URL is staged first with an explicit Upload button.
9
+ * - File prepopulation: display_name, format, resource_type inferred from
10
+ * the staged file metadata.
11
+ * - Folder picker: combobox with search-folders tool for autocomplete.
12
+ *
13
+ * Shares CLDS tokens, MCPApp client, helpers, and detail renderers
14
+ * with the gallery/details widgets via widget-shared.ts.
15
+ */
16
+ import { toJSONSchema } from "zod";
17
+ import { SHARED_CSS_TOKENS, SHARED_CSS_COMPONENTS, SHARED_JS_MCP_CLIENT, SHARED_JS_HELPERS, SHARED_JS_TOOLTIPS, SHARED_JS_MODAL, SHARED_JS_DETAIL_RENDERERS, SHARED_JS_HOST_CONTEXT, } from "./widget-shared.js";
18
+ import { UploadRequest$zodSchema } from "../../models/uploadrequest.js";
19
+ import { UploadResourceType$zodSchema } from "../../models/uploadresourcetype.js";
20
+ export const ASSET_UPLOAD_RESOURCE_URI = "ui://cloudinary/asset-upload.html";
21
+ export function getAssetUploadHtml() {
22
+ return ASSET_UPLOAD_HTML;
23
+ }
24
+ // ── Build-time schema generation ────────────────────────────────────
25
+ const uploadJsonSchema = toJSONSchema(UploadRequest$zodSchema);
26
+ const rtJsonSchema = toJSONSchema(UploadResourceType$zodSchema);
27
+ const allSchemaProperties = {
28
+ ...(uploadJsonSchema.properties || {}),
29
+ };
30
+ const rtCopy = { ...rtJsonSchema };
31
+ delete rtCopy["$schema"];
32
+ allSchemaProperties["resource_type"] = rtCopy;
33
+ const SCHEMA_JSON = JSON.stringify(allSchemaProperties);
34
+ // ── CSS ─────────────────────────────────────────────────────────────
35
+ const UPLOAD_CSS = /* css */ `
36
+ .upload-header {
37
+ display: flex; align-items: center; gap: 10px;
38
+ margin-bottom: var(--cld-sp-md); padding-bottom: var(--cld-sp-sm);
39
+ border-bottom: 1px solid var(--cld-border);
40
+ }
41
+ .upload-header h1 {
42
+ font-size: var(--cld-font-sm); font-weight: 600; color: var(--cld-text);
43
+ }
44
+ .upload-header-icon { font-size: 20px; }
45
+ .upload-header { position: relative; }
46
+ .back-link {
47
+ position: absolute; top: -2px; right: 0;
48
+ background: none; border: none; cursor: pointer;
49
+ color: var(--cld-accent); font-size: var(--cld-font-xs);
50
+ padding: 2px 6px; border-radius: 4px;
51
+ }
52
+ .back-link:hover { text-decoration: underline; background: var(--cld-accent-bg); }
53
+
54
+ .upload-result .detail-section { padding: 14px 16px; }
55
+ .upload-result .detail-section:first-child { padding-top: 0; }
56
+
57
+ .upload-another {
58
+ margin-top: 16px; text-align: center;
59
+ }
60
+ `;
61
+ // ── JS ──────────────────────────────────────────────────────────────
62
+ const UPLOAD_JS = /* js */ `
63
+ var LOG_PREFIX = "[upload]";
64
+ var MIN_HEIGHT = 120;
65
+ var UPLOAD_TOOL_TIMEOUT_MS = 120000;
66
+
67
+ /* ── Schema & config (generated at build time) ── */
68
+ var UPLOAD_SCHEMA = ${SCHEMA_JSON};
69
+
70
+ var FIELD_CONFIG = {
71
+ exclude: ["file", "api_key", "signature", "timestamp", "callback",
72
+ "eval", "on_success"],
73
+ sections: [
74
+ { id: "basic", label: "Basic", open: true,
75
+ fields: ["display_name", "format", "asset_folder", "tags"] },
76
+ { id: "naming", label: "Naming & Behavior",
77
+ fields: ["public_id", "context", "use_filename", "unique_filename",
78
+ "overwrite", "use_filename_as_display_name",
79
+ "unique_display_name", "public_id_prefix",
80
+ "use_asset_folder_as_public_id_prefix",
81
+ "filename_override", "discard_original_filename",
82
+ "backup", "allowed_formats", "invalidate",
83
+ "metadata", "folder"] },
84
+ { id: "delivery", label: "Delivery & Access",
85
+ fields: ["resource_type", "type", "access_mode", "access_control",
86
+ "upload_preset", "headers", "return_delete_token"] },
87
+ { id: "transforms", label: "Transformations",
88
+ fields: ["transformation", "eager", "eager_async",
89
+ "eager_notification_url", "responsive_breakpoints"] },
90
+ { id: "ai", label: "AI & Analysis",
91
+ fields: ["moderation", "detection", "ocr", "auto_tagging",
92
+ "background_removal", "categorization", "raw_convert",
93
+ "auto_transcription", "auto_chaptering",
94
+ "media_metadata", "quality_analysis", "accessibility_analysis",
95
+ "cinemagraph_analysis", "colors", "faces", "phash",
96
+ "visual_search"] }
97
+ ]
98
+ };
99
+
100
+ /* ── State ── */
101
+ var pendingCall = { name: null, args: null };
102
+ var capturedArgs = {};
103
+ var state = "idle";
104
+ var uploadOrigin = "user";
105
+ var pendingLocalFile = null;
106
+ var stagedFile = null;
107
+ var lastResult = null;
108
+ var folderCache = [];
109
+ var folderDebounce = null;
110
+
111
+ var app = new MCPApp({ name: "Cloudinary Upload", version: "1.0.0" });
112
+ setupHostContext(app);
113
+
114
+ /* ── Schema helpers ── */
115
+ function humanize(key) {
116
+ return key.replace(/_/g, " ").replace(/\\b[a-z]/g, function(c) {
117
+ return c.toUpperCase();
118
+ });
119
+ }
120
+
121
+ function getFieldType(prop) {
122
+ if (!prop) return "text";
123
+ if (prop.enum) return "select";
124
+ if (prop.type === "boolean") return "checkbox";
125
+ if (prop.type === "number" || prop.type === "integer") return "number";
126
+ if (prop.type === "string") return "text";
127
+ if (prop.type === "array") return "textarea";
128
+ if (prop.anyOf || prop.oneOf) {
129
+ var subs = prop.anyOf || prop.oneOf;
130
+ for (var i = 0; i < subs.length; i++) {
131
+ if (subs[i].type === "boolean") return "checkbox";
132
+ }
133
+ return "textarea";
134
+ }
135
+ return "text";
136
+ }
137
+
138
+ function shortDesc(desc) {
139
+ if (!desc) return "";
140
+ var first = desc.split("\\n")[0];
141
+ return first.length > 120 ? first.substring(0, 117) + "..." : first;
142
+ }
143
+
144
+ /* ── Field renderers ── */
145
+ function helpIcon(desc) {
146
+ if (!desc) return "";
147
+ return ' <span class="help-toggle" tabindex="0">?<span class="help-bubble">' + esc(shortDesc(desc)) + "</span></span>";
148
+ }
149
+
150
+ function renderField(key, prop) {
151
+ var id = "f-" + key;
152
+ var label = humanize(key);
153
+ var ft = getFieldType(prop);
154
+ var desc = (prop && prop.description) || "";
155
+ var val = capturedArgs[key];
156
+
157
+ var h = "";
158
+
159
+ if (ft === "checkbox") {
160
+ var checked = val != null ? !!val : (prop.default === true);
161
+ h += '<div class="upload-field upload-field-check">';
162
+ h += "<label>";
163
+ h += '<input id="' + id + '" type="checkbox"' + (checked ? " checked" : "") + ">";
164
+ h += "<span>" + esc(label) + "</span>";
165
+ h += "</label>" + helpIcon(desc) + "</div>";
166
+ } else if (ft === "select") {
167
+ h += '<div class="upload-field">';
168
+ h += '<label for="' + id + '">' + esc(label) + helpIcon(desc) + "</label>";
169
+ h += '<select id="' + id + '">';
170
+ h += '<option value="">(default)</option>';
171
+ var opts = prop.enum || [];
172
+ for (var i = 0; i < opts.length; i++) {
173
+ var sel = val === opts[i] ? " selected" : "";
174
+ h += '<option value="' + esc(opts[i]) + '"' + sel + ">" + esc(opts[i]) + "</option>";
175
+ }
176
+ h += "</select></div>";
177
+ } else if (ft === "number") {
178
+ h += '<div class="upload-field">';
179
+ h += '<label for="' + id + '">' + esc(label) + helpIcon(desc) + "</label>";
180
+ h += '<input id="' + id + '" type="number" step="any" value="' + esc(val != null ? String(val) : "") + '">';
181
+ h += "</div>";
182
+ } else if (ft === "textarea") {
183
+ var tv = val != null ? (typeof val === "string" ? val : JSON.stringify(val, null, 2)) : "";
184
+ h += '<div class="upload-field full-width">';
185
+ h += '<label for="' + id + '">' + esc(label) + helpIcon(desc) + "</label>";
186
+ h += '<textarea id="' + id + '" rows="2" placeholder="JSON or text">' + esc(tv) + "</textarea>";
187
+ h += "</div>";
188
+ } else {
189
+ h += '<div class="upload-field">';
190
+ h += '<label for="' + id + '">' + esc(label) + helpIcon(desc) + "</label>";
191
+ h += '<input id="' + id + '" type="text" value="' + esc(val != null ? String(val) : "") + '">';
192
+ h += "</div>";
193
+ }
194
+ return h;
195
+ }
196
+
197
+ function renderFolderField(prop) {
198
+ var id = "f-asset_folder";
199
+ var val = capturedArgs.asset_folder || "";
200
+ var desc = (prop && prop.description) || "";
201
+ var h = '<div class="upload-field" id="folder-field-wrap">';
202
+ h += '<label for="' + id + '">Asset Folder' + helpIcon(desc) + "</label>";
203
+ h += '<div class="combo-wrap">';
204
+ h += '<input id="' + id + '" type="text" value="' + esc(val) + '" autocomplete="off" placeholder="Type to search or create...">';
205
+ h += '<div class="combo-dropdown" id="folder-dropdown"></div>';
206
+ h += "</div></div>";
207
+ return h;
208
+ }
209
+
210
+ function renderFormSections() {
211
+ var h = "";
212
+ var rendered = {};
213
+ var sections = FIELD_CONFIG.sections;
214
+
215
+ for (var s = 0; s < sections.length; s++) {
216
+ var sec = sections[s];
217
+ var fieldsHtml = "";
218
+
219
+ for (var f = 0; f < sec.fields.length; f++) {
220
+ var key = sec.fields[f];
221
+ if (FIELD_CONFIG.exclude.indexOf(key) >= 0) continue;
222
+ var prop = UPLOAD_SCHEMA[key];
223
+ if (!prop) continue;
224
+
225
+ if (key === "asset_folder") {
226
+ fieldsHtml += renderFolderField(prop);
227
+ } else {
228
+ fieldsHtml += renderField(key, prop);
229
+ }
230
+ rendered[key] = true;
231
+ }
232
+ if (!fieldsHtml) continue;
233
+
234
+ if (sec.open) {
235
+ h += '<div class="upload-form">' + fieldsHtml + "</div>";
236
+ } else {
237
+ h += '<details class="upload-section">';
238
+ h += "<summary>" + esc(sec.label) + "</summary>";
239
+ h += '<div class="upload-form">' + fieldsHtml + "</div>";
240
+ h += "</details>";
241
+ }
242
+ }
243
+
244
+ var otherHtml = "";
245
+ var allKeys = Object.keys(UPLOAD_SCHEMA);
246
+ for (var k = 0; k < allKeys.length; k++) {
247
+ var okey = allKeys[k];
248
+ if (rendered[okey] || FIELD_CONFIG.exclude.indexOf(okey) >= 0) continue;
249
+ otherHtml += renderField(okey, UPLOAD_SCHEMA[okey]);
250
+ rendered[okey] = true;
251
+ }
252
+ if (otherHtml) {
253
+ h += '<details class="upload-section">';
254
+ h += "<summary>Other</summary>";
255
+ h += '<div class="upload-form">' + otherHtml + "</div>";
256
+ h += "</details>";
257
+ }
258
+
259
+ return h;
260
+ }
261
+
262
+ /* ── Dynamic form collection ── */
263
+ function collectFormArgs() {
264
+ var allKeys = Object.keys(UPLOAD_SCHEMA);
265
+ for (var i = 0; i < allKeys.length; i++) {
266
+ var key = allKeys[i];
267
+ if (FIELD_CONFIG.exclude.indexOf(key) >= 0) continue;
268
+ var prop = UPLOAD_SCHEMA[key];
269
+ var ft = getFieldType(prop);
270
+ var el = document.getElementById("f-" + key);
271
+ if (!el) continue;
272
+
273
+ if (ft === "checkbox") {
274
+ if (el.checked) capturedArgs[key] = true;
275
+ else delete capturedArgs[key];
276
+ } else if (ft === "number") {
277
+ var n = parseFloat(el.value);
278
+ if (!isNaN(n)) capturedArgs[key] = n;
279
+ else delete capturedArgs[key];
280
+ } else if (ft === "textarea") {
281
+ var tv = el.value.trim();
282
+ if (tv) {
283
+ try { capturedArgs[key] = JSON.parse(tv); }
284
+ catch (e) { capturedArgs[key] = tv; }
285
+ } else { delete capturedArgs[key]; }
286
+ } else {
287
+ var sv = (ft === "select") ? el.value : el.value.trim();
288
+ if (sv) capturedArgs[key] = sv;
289
+ else delete capturedArgs[key];
290
+ }
291
+ }
292
+ }
293
+
294
+ /* ── Extract args from tool input ── */
295
+ function extractUploadArgs(args) {
296
+ if (!args) return {};
297
+ var out = {};
298
+ if (args.resource_type) out.resource_type = args.resource_type;
299
+ var ua = args.upload_request || {};
300
+ var schemaKeys = Object.keys(UPLOAD_SCHEMA);
301
+ for (var i = 0; i < schemaKeys.length; i++) {
302
+ var k = schemaKeys[i];
303
+ if (k === "resource_type") continue;
304
+ if (ua[k] !== undefined && ua[k] !== null && ua[k] !== "") out[k] = ua[k];
305
+ }
306
+ return out;
307
+ }
308
+
309
+ /* ── File prepopulation ── */
310
+ var prevStagedName = "";
311
+
312
+ function prepopulateFromFile(file) {
313
+ var name = file.name || "";
314
+ var dot = name.lastIndexOf(".");
315
+ var base = dot > 0 ? name.substring(0, dot) : name;
316
+ var ext = dot > 0 ? name.substring(dot + 1).toLowerCase() : "";
317
+ var mime = (file.type || "").toLowerCase();
318
+
319
+ var canOverwrite = !capturedArgs.display_name || capturedArgs.display_name === prevStagedName;
320
+ if (base && canOverwrite) capturedArgs.display_name = base;
321
+ capturedArgs.format = ext || undefined;
322
+ if (!ext) delete capturedArgs.format;
323
+ prevStagedName = base;
324
+
325
+ if (mime.startsWith("image/")) capturedArgs.resource_type = "image";
326
+ else if (mime.startsWith("video/") || mime.startsWith("audio/")) capturedArgs.resource_type = "video";
327
+ else if (mime === "application/pdf") capturedArgs.resource_type = "image";
328
+ else capturedArgs.resource_type = "auto";
329
+ }
330
+
331
+ function prepopulateFromUrl(url) {
332
+ var path = url.split("?")[0].split("/").pop() || "";
333
+ var decoded = "";
334
+ try { decoded = decodeURIComponent(path); } catch (e) { decoded = path; }
335
+ var dot = decoded.lastIndexOf(".");
336
+ var base = dot > 0 ? decoded.substring(0, dot) : decoded;
337
+ var ext = dot > 0 ? decoded.substring(dot + 1).toLowerCase() : "";
338
+
339
+ var canOverwrite = !capturedArgs.display_name || capturedArgs.display_name === prevStagedName;
340
+ if (base && canOverwrite) capturedArgs.display_name = base;
341
+ capturedArgs.format = ext || undefined;
342
+ if (!ext) delete capturedArgs.format;
343
+ prevStagedName = base;
344
+ }
345
+
346
+ /* ── Folder picker ── */
347
+ var folderHighlight = -1;
348
+
349
+ function setupFolderPicker() {
350
+ var input = document.getElementById("f-asset_folder");
351
+ if (!input) return;
352
+
353
+ input.addEventListener("focus", function() {
354
+ fetchFolders("");
355
+ });
356
+
357
+ input.addEventListener("input", function() {
358
+ clearTimeout(folderDebounce);
359
+ folderHighlight = -1;
360
+ var q = input.value.trim();
361
+ folderDebounce = setTimeout(function() { fetchFolders(q); }, 300);
362
+ });
363
+
364
+ input.addEventListener("blur", function() {
365
+ setTimeout(hideFolderDropdown, 200);
366
+ });
367
+
368
+ input.addEventListener("keydown", function(e) {
369
+ var dropdown = document.getElementById("folder-dropdown");
370
+ if (!dropdown || dropdown.style.display === "none") return;
371
+ var items = dropdown.querySelectorAll(".combo-item");
372
+ if (!items.length) return;
373
+
374
+ if (e.key === "ArrowDown") {
375
+ e.preventDefault();
376
+ folderHighlight = Math.min(folderHighlight + 1, items.length - 1);
377
+ updateFolderHighlight(items);
378
+ } else if (e.key === "ArrowUp") {
379
+ e.preventDefault();
380
+ folderHighlight = Math.max(folderHighlight - 1, 0);
381
+ updateFolderHighlight(items);
382
+ } else if (e.key === "Enter" && folderHighlight >= 0 && items[folderHighlight]) {
383
+ e.preventDefault();
384
+ input.value = items[folderHighlight].dataset.value || "";
385
+ hideFolderDropdown();
386
+ } else if (e.key === "Escape") {
387
+ hideFolderDropdown();
388
+ }
389
+ });
390
+ }
391
+
392
+ function updateFolderHighlight(items) {
393
+ for (var i = 0; i < items.length; i++) {
394
+ items[i].classList.toggle("combo-item-active", i === folderHighlight);
395
+ }
396
+ if (items[folderHighlight]) {
397
+ items[folderHighlight].scrollIntoView({ block: "nearest" });
398
+ }
399
+ }
400
+
401
+ function fetchFolders(query) {
402
+ var args = { max_results: 50 };
403
+ if (query) args.expression = "name:" + query + "*";
404
+ app.callServerTool({ name: "search-folders", arguments: args })
405
+ .then(function(res) {
406
+ var data = ingestResult(res);
407
+ folderCache = (data && data.folders) || [];
408
+ folderHighlight = -1;
409
+ renderFolderDropdown();
410
+ })
411
+ .catch(function(err) {
412
+ console.warn(LOG_PREFIX, "folder search failed:", err);
413
+ folderCache = [];
414
+ });
415
+ }
416
+
417
+ function renderFolderDropdown() {
418
+ var dropdown = document.getElementById("folder-dropdown");
419
+ var input = document.getElementById("f-asset_folder");
420
+ if (!dropdown || !input) return;
421
+
422
+ var query = input.value.trim().toLowerCase();
423
+ var filtered = folderCache;
424
+ if (query) {
425
+ filtered = folderCache.filter(function(f) {
426
+ return (f.path || f.name || "").toLowerCase().indexOf(query) >= 0;
427
+ });
428
+ }
429
+
430
+ if (filtered.length === 0) {
431
+ dropdown.style.display = "none";
432
+ return;
433
+ }
434
+
435
+ var h = "";
436
+ for (var i = 0; i < Math.min(filtered.length, 30); i++) {
437
+ var fp = filtered[i].path || filtered[i].name || "";
438
+ h += '<div class="combo-item" data-value="' + esc(fp) + '">' + esc(fp) + "</div>";
439
+ }
440
+ dropdown.innerHTML = h;
441
+ dropdown.style.display = "block";
442
+
443
+ dropdown.onclick = function(e) {
444
+ var item = e.target;
445
+ while (item && !item.classList.contains("combo-item")) item = item.parentElement;
446
+ if (item && item.dataset.value != null) {
447
+ input.value = item.dataset.value;
448
+ dropdown.style.display = "none";
449
+ }
450
+ };
451
+ }
452
+
453
+ function hideFolderDropdown() {
454
+ var dd = document.getElementById("folder-dropdown");
455
+ if (dd) dd.style.display = "none";
456
+ }
457
+
458
+ /* ── Staging ── */
459
+ function stageLocalFile(file) {
460
+ collectFormArgs();
461
+ var reader = new FileReader();
462
+ reader.onload = function() {
463
+ stagedFile = { dataUri: reader.result, name: file.name, size: file.size, mime: file.type };
464
+ prepopulateFromFile(file);
465
+ renderPicker();
466
+ };
467
+ reader.onerror = function() {
468
+ showError("Read Error", "Could not read the selected file.");
469
+ };
470
+ reader.readAsDataURL(file);
471
+ }
472
+
473
+ function stageUrl(url) {
474
+ collectFormArgs();
475
+ var parts = url.split("?")[0].split("/");
476
+ var fname = parts[parts.length - 1] || url;
477
+ try { fname = decodeURIComponent(fname); } catch (e) {}
478
+ stagedFile = { url: url, name: fname };
479
+ prepopulateFromUrl(url);
480
+ renderPicker();
481
+ }
482
+
483
+ /* ── Main render: picker ── */
484
+ function renderPicker() {
485
+ state = "idle";
486
+ var root = document.getElementById("app");
487
+ var h = "";
488
+
489
+ h += '<div class="upload-header">';
490
+ if (lastResult) {
491
+ h += '<button class="back-link" id="back-to-result-btn">\\u2190 Back to Result</button>';
492
+ }
493
+ h += '<span class="upload-header-icon">\\u2B06\\uFE0F</span>';
494
+ h += "<h1>Upload to Cloudinary</h1>";
495
+ h += "</div>";
496
+
497
+ if (stagedFile) {
498
+ h += '<div class="upload-staged">';
499
+ h += '<div class="upload-staged-icon">\\u{1F4C4}</div>';
500
+ h += '<div class="upload-staged-info">';
501
+ h += '<div class="upload-staged-name">' + esc(stagedFile.name) + "</div>";
502
+ if (stagedFile.size) {
503
+ h += '<div class="upload-staged-meta">' + fmtBytes(stagedFile.size);
504
+ if (stagedFile.mime) h += " \\u00B7 " + esc(stagedFile.mime);
505
+ h += "</div>";
506
+ } else if (stagedFile.url) {
507
+ h += '<div class="upload-staged-meta">Remote URL</div>';
508
+ }
509
+ h += "</div>";
510
+ h += '<button class="upload-staged-clear" id="clear-staged-btn" title="Remove">\\u2715</button>';
511
+ h += "</div>";
512
+ } else {
513
+ h += '<div class="upload-zone" id="drop-zone">';
514
+ h += '<div class="upload-zone-icon">\\u{1F4C1}</div>';
515
+ h += '<div class="upload-zone-text">Drag & drop a file here</div>';
516
+ h += '<div class="upload-zone-hint">Images, videos, PDFs, and other files up to 60 MB</div>';
517
+ h += '<button class="upload-zone-btn" id="browse-btn">Browse Files</button>';
518
+ h += '<input type="file" id="file-input" style="display:none">';
519
+ h += "</div>";
520
+
521
+ h += '<div class="upload-or">or upload from URL</div>';
522
+
523
+ h += '<div class="upload-url-row">';
524
+ h += '<input class="upload-url-input" id="url-input" type="text" placeholder="https://example.com/image.jpg">';
525
+ h += '<button class="upload-url-btn" id="url-btn">Add URL</button>';
526
+ h += "</div>";
527
+ }
528
+
529
+ h += renderFormSections();
530
+
531
+ if (stagedFile) {
532
+ h += '<div class="upload-submit">';
533
+ h += '<button class="prompt-btn prompt-btn-primary upload-submit-btn" id="upload-btn">Upload to Cloudinary</button>';
534
+ h += "</div>";
535
+ }
536
+
537
+ root.innerHTML = h;
538
+
539
+ var backBtn = document.getElementById("back-to-result-btn");
540
+ if (backBtn && lastResult) {
541
+ backBtn.addEventListener("click", function() { renderResult(lastResult); });
542
+ }
543
+
544
+ if (stagedFile) {
545
+ document.getElementById("clear-staged-btn").addEventListener("click", function() {
546
+ collectFormArgs();
547
+ stagedFile = null;
548
+ if (capturedArgs.display_name === prevStagedName) delete capturedArgs.display_name;
549
+ delete capturedArgs.format;
550
+ delete capturedArgs.resource_type;
551
+ prevStagedName = "";
552
+ renderPicker();
553
+ });
554
+ document.getElementById("upload-btn").addEventListener("click", function() {
555
+ collectFormArgs();
556
+ uploadOrigin = "user";
557
+ var fileData = stagedFile.dataUri || stagedFile.url;
558
+ renderUploading(stagedFile.name, stagedFile.size ? fmtBytes(stagedFile.size) : "Remote URL");
559
+ doUpload(fileData, stagedFile.name);
560
+ });
561
+ } else {
562
+ setupDropZone();
563
+ }
564
+
565
+ setupFolderPicker();
566
+ }
567
+
568
+ /* ── Drop zone events ── */
569
+ function setupDropZone() {
570
+ var zone = document.getElementById("drop-zone");
571
+ var input = document.getElementById("file-input");
572
+ var browseBtn = document.getElementById("browse-btn");
573
+ var urlBtn = document.getElementById("url-btn");
574
+ var urlInput = document.getElementById("url-input");
575
+ if (!zone) return;
576
+
577
+ browseBtn.addEventListener("click", function(e) {
578
+ e.stopPropagation();
579
+ input.click();
580
+ });
581
+ zone.addEventListener("click", function() { input.click(); });
582
+
583
+ input.addEventListener("change", function() {
584
+ if (input.files && input.files.length) stageLocalFile(input.files[0]);
585
+ });
586
+
587
+ zone.addEventListener("dragenter", function(e) { e.preventDefault(); zone.classList.add("dragover"); });
588
+ zone.addEventListener("dragover", function(e) { e.preventDefault(); zone.classList.add("dragover"); });
589
+ zone.addEventListener("dragleave", function(e) { e.preventDefault(); zone.classList.remove("dragover"); });
590
+ zone.addEventListener("drop", function(e) {
591
+ e.preventDefault();
592
+ zone.classList.remove("dragover");
593
+ if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) {
594
+ stageLocalFile(e.dataTransfer.files[0]);
595
+ }
596
+ });
597
+
598
+ urlBtn.addEventListener("click", function() {
599
+ var url = urlInput.value.trim();
600
+ if (url) stageUrl(url);
601
+ });
602
+ urlInput.addEventListener("keydown", function(e) {
603
+ if (e.key === "Enter") {
604
+ var url = urlInput.value.trim();
605
+ if (url) stageUrl(url);
606
+ }
607
+ });
608
+ }
609
+
610
+ /* ── Uploading state ── */
611
+ function renderUploading(name, meta) {
612
+ state = "uploading";
613
+ var root = document.getElementById("app");
614
+ var h = "";
615
+
616
+ h += '<div class="upload-header">';
617
+ h += '<span class="upload-header-icon">\\u2B06\\uFE0F</span>';
618
+ h += "<h1>Uploading\\u2026</h1>";
619
+ h += "</div>";
620
+
621
+ h += '<div class="upload-preview">';
622
+ h += '<div class="upload-preview-icon">\\u{1F4C4}</div>';
623
+ h += '<div class="upload-preview-info">';
624
+ h += '<div class="upload-preview-name">' + esc(name) + "</div>";
625
+ h += '<div class="upload-preview-meta">' + esc(meta) + "</div>";
626
+ h += "</div></div>";
627
+
628
+ h += '<div class="upload-progress-wrap">';
629
+ h += '<div class="upload-progress-bar"><div class="upload-progress-fill" id="progress-fill"></div></div>';
630
+ h += '<div class="upload-progress-text" id="progress-text">Uploading to Cloudinary\\u2026</div>';
631
+ h += "</div>";
632
+
633
+ root.innerHTML = h;
634
+ animateProgress();
635
+ }
636
+
637
+ var progressInterval = null;
638
+ function animateProgress() {
639
+ clearInterval(progressInterval);
640
+ var fill = document.getElementById("progress-fill");
641
+ if (!fill) return;
642
+ var pct = 0;
643
+ progressInterval = setInterval(function() {
644
+ if (pct < 90) {
645
+ pct += (90 - pct) * 0.08;
646
+ fill.style.width = pct + "%";
647
+ }
648
+ }, 200);
649
+ }
650
+
651
+ function stopProgress(success) {
652
+ clearInterval(progressInterval);
653
+ var fill = document.getElementById("progress-fill");
654
+ var text = document.getElementById("progress-text");
655
+ if (fill) fill.style.width = "100%";
656
+ if (text) text.textContent = success ? "Upload complete" : "Upload failed";
657
+ }
658
+
659
+ function formatErrorMsg(msg) {
660
+ var idx = msg.indexOf("[");
661
+ if (idx !== -1 && msg.indexOf("Invalid arguments") !== -1) {
662
+ try {
663
+ var issues = JSON.parse(msg.substring(idx));
664
+ return issues.map(function(i) {
665
+ var field = (i.path || []).join(".");
666
+ return (field ? field + ": " : "") + (i.message || "invalid");
667
+ }).join("\\n");
668
+ } catch (e) {}
669
+ }
670
+ if (typeof msg === "string" && msg.length > 200) {
671
+ var short = msg.replace(/\\s+/g, " ").substring(0, 200) + "\\u2026";
672
+ return short;
673
+ }
674
+ return msg;
675
+ }
676
+
677
+ function renderUploadError(title, msg) {
678
+ msg = formatErrorMsg(msg);
679
+ state = "error";
680
+ stopProgress(false);
681
+
682
+ var wrap = document.querySelector(".upload-progress-wrap");
683
+ if (wrap) {
684
+ var header = document.querySelector(".upload-header h1");
685
+ if (header) header.textContent = title;
686
+ var icon = document.querySelector(".upload-header-icon");
687
+ if (icon) icon.textContent = "\\u26A0\\uFE0F";
688
+
689
+ var safeMsg = esc(msg).replace(/\\n/g, "<br>");
690
+ var h = '<div class="upload-error-msg">' + safeMsg + "</div>";
691
+ h += '<div class="upload-another" style="margin-top:14px">';
692
+ h += '<button class="prompt-btn prompt-btn-primary" id="retry-upload-btn">Try from Widget</button>';
693
+ h += "</div>";
694
+ wrap.innerHTML = h;
695
+ } else {
696
+ var root = document.getElementById("app");
697
+ var safeMsg = esc(msg).replace(/\\n/g, "<br>");
698
+ var h = '<div class="upload-header">';
699
+ h += '<span class="upload-header-icon">\\u26A0\\uFE0F</span>';
700
+ h += "<h1>" + esc(title) + "</h1>";
701
+ h += "</div>";
702
+ h += '<div class="upload-error-msg">' + safeMsg + "</div>";
703
+ h += '<div class="upload-another" style="margin-top:14px;text-align:center">';
704
+ h += '<button class="prompt-btn prompt-btn-primary" id="retry-upload-btn">Try from Widget</button>';
705
+ h += "</div>";
706
+ root.innerHTML = h;
707
+ }
708
+
709
+ var btn = document.getElementById("retry-upload-btn");
710
+ if (btn) btn.addEventListener("click", function() {
711
+ uploadOrigin = "user";
712
+ renderPicker();
713
+ });
714
+ }
715
+
716
+ function extractPathFromError(errMsg) {
717
+ if (!errMsg) return "";
718
+ var m = errMsg.match(/open '([^']+)'/);
719
+ if (m) return m[1];
720
+ m = errMsg.match(/read file: ([^.]+)/);
721
+ if (m) return m[1].trim();
722
+ return "";
723
+ }
724
+
725
+ function classifyFileError(errMsg) {
726
+ var lower = (errMsg || "").toLowerCase();
727
+ if (lower.indexOf("enoent") >= 0 || lower.indexOf("no such file") >= 0) {
728
+ return {
729
+ title: "File Not Found",
730
+ desc: "could not be found at the specified path.",
731
+ hint: "If the server is running remotely, it cannot access files on your device. Drop the file below or click Browse to select it."
732
+ };
733
+ }
734
+ if (lower.indexOf("eacces") >= 0 || lower.indexOf("eperm") >= 0 || lower.indexOf("permission") >= 0) {
735
+ return {
736
+ title: "File Access Denied",
737
+ desc: "could not be read due to insufficient permissions."
738
+ };
739
+ }
740
+ return {
741
+ title: "File Not Accessible",
742
+ desc: "couldn\\u2019t be read from the given path."
743
+ };
744
+ }
745
+
746
+ function renderLocalFileNeeded(expectedName, errMsg) {
747
+ state = "localFileNeeded";
748
+ var classified = classifyFileError(errMsg);
749
+ var root = document.getElementById("app");
750
+ var h = '<div class="upload-header">';
751
+ h += '<span class="upload-header-icon">\\u{1F4C1}</span>';
752
+ h += "<h1>" + esc(classified.title) + "</h1>";
753
+ h += "</div>";
754
+ h += '<div class="prompt" style="margin-bottom:16px">';
755
+ h += '<div class="prompt-desc">The file <strong>' + esc(expectedName)
756
+ + "</strong> " + classified.desc
757
+ + " Please select it from your device.</div>";
758
+ if (classified.hint) {
759
+ h += '<div style="margin-top:8px;font-size:11.5px;color:var(--cld-text3);">' + esc(classified.hint) + "</div>";
760
+ }
761
+ var filePath = extractPathFromError(errMsg);
762
+ if (filePath) {
763
+ h += '<div style="margin-top:6px;font-size:11px;color:var(--cld-text3);word-break:break-all;">Path: ' + esc(filePath) + "</div>";
764
+ }
765
+ h += "</div>";
766
+ h += '<div class="upload-zone" id="drop-zone">';
767
+ h += '<div class="upload-zone-icon">\\u{1F4C1}</div>';
768
+ h += '<div class="upload-zone-text">Drop <strong>' + esc(expectedName) + "</strong> here</div>";
769
+ h += '<div class="upload-zone-hint">Or click to browse your files</div>';
770
+ h += '<button class="upload-zone-btn" id="browse-btn">Browse Files</button>';
771
+ h += '<input type="file" id="file-input" style="display:none">';
772
+ h += "</div>";
773
+ root.innerHTML = h;
774
+
775
+ function onFileSelected(file) {
776
+ var reader = new FileReader();
777
+ reader.onload = function() {
778
+ prepopulateFromFile(file);
779
+ renderUploading(file.name, fmtBytes(file.size));
780
+ doUpload(reader.result, file.name);
781
+ };
782
+ reader.onerror = function() {
783
+ showError("Read Error", "Could not read the selected file.");
784
+ };
785
+ reader.readAsDataURL(file);
786
+ }
787
+
788
+ var zone = document.getElementById("drop-zone");
789
+ var input = document.getElementById("file-input");
790
+ var browseBtn = document.getElementById("browse-btn");
791
+
792
+ browseBtn.addEventListener("click", function(e) { e.stopPropagation(); input.click(); });
793
+ zone.addEventListener("click", function() { input.click(); });
794
+ input.addEventListener("change", function() {
795
+ if (input.files && input.files.length) onFileSelected(input.files[0]);
796
+ });
797
+ zone.addEventListener("dragenter", function(e) { e.preventDefault(); zone.classList.add("dragover"); });
798
+ zone.addEventListener("dragover", function(e) { e.preventDefault(); zone.classList.add("dragover"); });
799
+ zone.addEventListener("dragleave", function(e) { e.preventDefault(); zone.classList.remove("dragover"); });
800
+ zone.addEventListener("drop", function(e) {
801
+ e.preventDefault(); zone.classList.remove("dragover");
802
+ if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) {
803
+ onFileSelected(e.dataTransfer.files[0]);
804
+ }
805
+ });
806
+ }
807
+
808
+ /* ── Upload logic ── */
809
+ async function doUpload(fileData, displayHint) {
810
+ var uploadRequest = {};
811
+ var resourceType = "auto";
812
+
813
+ var keys = Object.keys(capturedArgs);
814
+ for (var i = 0; i < keys.length; i++) {
815
+ var k = keys[i];
816
+ if (k === "resource_type") {
817
+ resourceType = capturedArgs[k] || "auto";
818
+ } else {
819
+ uploadRequest[k] = capturedArgs[k];
820
+ }
821
+ }
822
+ uploadRequest.file = fileData;
823
+
824
+ if (pendingCall.args && pendingCall.args.resource_type) {
825
+ resourceType = pendingCall.args.resource_type;
826
+ }
827
+
828
+ console.log(LOG_PREFIX, "tools/call -> upload-asset");
829
+ try {
830
+ var res = await app.callServerTool({
831
+ name: "upload-asset",
832
+ arguments: { resource_type: resourceType, upload_request: uploadRequest },
833
+ });
834
+ var data = ingestResult(res);
835
+ console.log(LOG_PREFIX, "upload result:", JSON.stringify(data).substring(0, 300));
836
+ if (data && !data._truncated && !data.error && (data.public_id || data.asset_id || data.status === "pending")) {
837
+ stopProgress(true);
838
+ stagedFile = null;
839
+ if (uploadOrigin === "host") {
840
+ try {
841
+ app._rpc("ui/updateModelContext", {
842
+ content: [{ type: "text", text: "File uploaded successfully via widget. public_id: " + data.public_id + ", secure_url: " + (data.secure_url || data.url || "") }]
843
+ });
844
+ } catch (e) { console.warn(LOG_PREFIX, "updateModelContext failed:", e); }
845
+ }
846
+ setTimeout(function() { renderResult(data); }, 300);
847
+ } else if (data && data.error) {
848
+ stopProgress(false);
849
+ var msg = typeof data.error === "object" ? (data.error.message || JSON.stringify(data.error)) : String(data.error);
850
+ if (uploadOrigin === "host") { renderUploadError("Upload Failed", msg); }
851
+ else { showError("Upload Failed", formatErrorMsg(msg)); renderPicker(); }
852
+ } else if (data && data._error) {
853
+ stopProgress(false);
854
+ var srvMsg = data._message || "Server returned an error.";
855
+ if (uploadOrigin === "host") { renderUploadError("Upload Failed", srvMsg); }
856
+ else { showError("Upload Failed", formatErrorMsg(srvMsg)); renderPicker(); }
857
+ } else {
858
+ stopProgress(false);
859
+ var fallbackMsg = "Unexpected response from server.";
860
+ if (data && data._message) fallbackMsg = data._message;
861
+ else if (data && data.message) fallbackMsg = typeof data.message === "string" ? data.message : JSON.stringify(data.message);
862
+ else if (data) try { var snap = JSON.stringify(data).substring(0, 200); if (snap !== "{}") fallbackMsg = snap; } catch(e) {}
863
+ if (uploadOrigin === "host") { renderUploadError("Upload Failed", fallbackMsg); }
864
+ else { showError("Upload Failed", fallbackMsg); renderPicker(); }
865
+ }
866
+ } catch (e) {
867
+ stopProgress(false);
868
+ var errMsg = e && e.message ? e.message : String(e);
869
+ if (uploadOrigin === "host") { renderUploadError("Upload Failed", errMsg); }
870
+ else { showError("Upload Failed", errMsg); renderPicker(); }
871
+ }
872
+ }
873
+
874
+ /* ── Result view ── */
875
+ function renderResult(r) {
876
+ state = "result";
877
+ lastResult = r;
878
+ var root = document.getElementById("app");
879
+ var url = r.secure_url || r.url || "";
880
+ var name = r.display_name || r.public_id || "Asset";
881
+ var isPending = r.status === "pending" && !r.public_id;
882
+
883
+ var h = "";
884
+
885
+ h += '<div class="upload-header">';
886
+ h += '<span class="upload-header-icon">' + (isPending ? "\\u23F3" : "\\u2705") + '</span>';
887
+ h += "<h1>" + (isPending ? "Upload Queued" : "Upload Complete") + "</h1>";
888
+ h += "</div>";
889
+
890
+ h += '<div class="upload-result">';
891
+
892
+ if (isPending) {
893
+ h += '<div class="upload-result-body">';
894
+ h += '<div class="upload-result-title"><span class="success-icon">\\u2713</span> Upload accepted (async)</div>';
895
+ h += '<div class="detail-section"><div class="prompt-desc">The file has been queued for processing. It will be available shortly.</div>';
896
+ if (r.batch_id) h += '<div style="margin-top:8px;font-size:11px;color:var(--cld-text3);">Batch: ' + esc(r.batch_id) + "</div>";
897
+ h += "</div>";
898
+ } else {
899
+ h += '<div class="upload-result-hero">';
900
+ h += renderHeroPreview(r);
901
+ h += "</div>";
902
+
903
+ h += '<div class="upload-result-body">';
904
+ h += '<div class="upload-result-title"><span class="success-icon">\\u2713</span> ' + esc(name) + "</div>";
905
+
906
+ h += sectionStart("asset_info");
907
+ h += '<summary class="detail-section-title">Asset Info</summary>';
908
+ h += renderAssetGrid(r);
909
+ h += "</details>";
910
+
911
+ h += renderAudioInfo(r);
912
+ h += renderVideoInfo(r);
913
+ }
914
+
915
+ if (!isPending) {
916
+ if (r.tags && r.tags.length) {
917
+ h += renderTags(r.tags);
918
+ }
919
+
920
+ h += renderContext(r.context);
921
+ h += renderImageMetadata(r.image_metadata || r.media_metadata);
922
+ h += renderColors(r.colors, r.predominant);
923
+ h += renderModerationSection(r.moderation, r.moderation_kind, r.moderation_status);
924
+ h += renderAccessControl(r.access_control);
925
+ h += renderCoordinates(r.faces, r.coordinates);
926
+ h += renderLastUpdated(r.last_updated);
927
+ h += renderMetadata(r.metadata);
928
+ h += renderInfo(r.info);
929
+ h += renderDerived(r.derived, r.derived_next_cursor, r.asset_id);
930
+ h += renderDerivatives(r.derivatives);
931
+ h += renderRelatedAssets(r.related_assets);
932
+ h += renderVersions(r.versions);
933
+ h += renderEager(r.eager);
934
+ h += renderQualityAnalysis(r.quality_analysis, r.quality_score);
935
+ h += renderAccessibilityAnalysis(r.accessibility_analysis);
936
+ h += renderExtraFields(r);
937
+ h += renderRawResponse(r);
938
+ }
939
+
940
+ h += '<div class="upload-actions">';
941
+ if (url) h += '<button class="prompt-btn prompt-btn-primary" id="open-url-btn">Open in Browser</button>';
942
+ h += '<button class="prompt-btn" id="upload-another-btn">Upload Another</button>';
943
+ h += "</div>";
944
+
945
+ h += "</div></div>";
946
+
947
+ root.innerHTML = h;
948
+
949
+ root.addEventListener("click", function handler(e) {
950
+ var el = e.target;
951
+ while (el && el !== root) {
952
+ if (el.id === "open-url-btn") {
953
+ app._rpc("ui/open-link", { url: url });
954
+ return;
955
+ }
956
+ if (el.id === "upload-another-btn") {
957
+ capturedArgs = {};
958
+ stagedFile = null;
959
+ prevStagedName = "";
960
+ renderPicker();
961
+ return;
962
+ }
963
+ if (el.id === "load-more-derived-btn") {
964
+ loadMoreDerived(el);
965
+ return;
966
+ }
967
+ if (el.classList && el.classList.contains("link-val") && el.dataset.url) {
968
+ app._rpc("ui/open-link", { url: el.dataset.url });
969
+ return;
970
+ }
971
+ el = el.parentElement;
972
+ }
973
+ });
974
+ }
975
+
976
+ /* ── MCP event handlers ── */
977
+ app.ontoolinput = function(params) {
978
+ pendingCall.name = "upload-asset";
979
+ if (params.arguments) {
980
+ pendingCall.args = params.arguments;
981
+ capturedArgs = extractUploadArgs(params.arguments);
982
+
983
+ var req = params.arguments.upload_request || params.arguments || {};
984
+ var file = req.file || "";
985
+ if (file) {
986
+ uploadOrigin = "host";
987
+ var hint = req.display_name || "";
988
+ if (!hint && file.indexOf("://") !== -1) {
989
+ var parts = file.split("?")[0].split("/");
990
+ hint = parts[parts.length - 1] || file;
991
+ try { hint = decodeURIComponent(hint); } catch (e) {}
992
+ }
993
+ if (!hint) hint = "File";
994
+ pendingLocalFile = (file.indexOf("file://") === 0) ? hint : null;
995
+ renderUploading(hint, "Uploading via server\u2026");
996
+ }
997
+ }
998
+ };
999
+
1000
+ app.ontoolcancelled = function(params) {
1001
+ console.log(LOG_PREFIX, "tool cancelled:", params && params.reason);
1002
+ stopProgress(false);
1003
+ pendingLocalFile = null;
1004
+ var root = document.getElementById("app");
1005
+ var h = '<div class="prompt" style="padding:32px 24px">';
1006
+ h += '<div style="font-size:14px;font-weight:600;color:var(--cld-text);margin-bottom:4px">Upload Cancelled</div>';
1007
+ h += renderParamsList(pendingCall.args);
1008
+ h += '<div class="prompt-actions" style="margin-top:14px">';
1009
+ h += '<button class="prompt-btn prompt-btn-primary" id="cancelled-retry-btn">Upload from Widget</button>';
1010
+ h += '</div>';
1011
+ h += '</div>';
1012
+ root.innerHTML = h;
1013
+ document.getElementById("cancelled-retry-btn").addEventListener("click", function() {
1014
+ stagedFile = null; capturedArgs = {}; prevStagedName = ""; state = "idle";
1015
+ renderPicker();
1016
+ });
1017
+ requestAnimationFrame(function() { app.reportSize(document.documentElement.scrollHeight); });
1018
+ };
1019
+
1020
+ app.ontoolresult = function(result) {
1021
+ stopProgress(false);
1022
+ var data = ingestResult(result);
1023
+
1024
+ if (data && !data._truncated && !data._error && !data._parseError && !data.error && (data.public_id || data.asset_id || data.status === "pending")) {
1025
+ stopProgress(true);
1026
+ pendingLocalFile = null;
1027
+ console.log(LOG_PREFIX, "host upload result:", data.public_id || data.status);
1028
+ renderResult(data);
1029
+ return;
1030
+ }
1031
+
1032
+ var errMsg = "Upload failed";
1033
+ if (data) {
1034
+ if (data.error) {
1035
+ var raw = data.error;
1036
+ if (typeof raw === "object") errMsg = raw.message || raw.error || JSON.stringify(raw);
1037
+ else errMsg = String(raw);
1038
+ } else if (data._message) {
1039
+ errMsg = String(data._message);
1040
+ }
1041
+ console.warn(LOG_PREFIX, "host upload error:", errMsg);
1042
+ } else {
1043
+ console.warn(LOG_PREFIX, "host result unusable, data:", data);
1044
+ }
1045
+
1046
+ if (pendingLocalFile) {
1047
+ var localName = pendingLocalFile;
1048
+ pendingLocalFile = null;
1049
+ console.log(LOG_PREFIX, "file:// upload failed, showing local file picker for:", localName);
1050
+ renderLocalFileNeeded(localName, errMsg);
1051
+ } else if (uploadOrigin === "host") {
1052
+ renderUploadError("Upload Failed", errMsg);
1053
+ } else {
1054
+ renderUploadError("Upload Failed", errMsg);
1055
+ }
1056
+ };
1057
+
1058
+ app.connect().then(function() {
1059
+ console.log(LOG_PREFIX, "ready, state=" + state);
1060
+ setupResize(app, MIN_HEIGHT);
1061
+ if (state === "idle") renderPicker();
1062
+ }).catch(function(err) {
1063
+ showError("Connection Failed", err && err.message ? err.message : String(err));
1064
+ });
1065
+ `;
1066
+ // ── HTML template ───────────────────────────────────────────────────
1067
+ const ASSET_UPLOAD_HTML = /* html */ `<!DOCTYPE html>
1068
+ <html lang="en">
1069
+ <head>
1070
+ <meta charset="UTF-8">
1071
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1072
+ <title>Cloudinary Upload</title>
1073
+ <style>
1074
+ ${SHARED_CSS_TOKENS}
1075
+ ${SHARED_CSS_COMPONENTS}
1076
+ ${UPLOAD_CSS}
1077
+ </style>
1078
+ </head>
1079
+ <body>
1080
+ <div id="app"><div class="status">Preparing upload&hellip;</div></div>
1081
+
1082
+ <script>
1083
+ ${SHARED_JS_MCP_CLIENT}
1084
+ ${SHARED_JS_HELPERS}
1085
+ ${SHARED_JS_TOOLTIPS}
1086
+ ${SHARED_JS_MODAL}
1087
+ ${SHARED_JS_DETAIL_RENDERERS}
1088
+ ${SHARED_JS_HOST_CONTEXT}
1089
+ ${UPLOAD_JS}
1090
+ </script>
1091
+ </body>
1092
+ </html>`;
1093
+ //# sourceMappingURL=asset-upload-widget.js.map