@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,1077 @@
1
+ /*
2
+ * MCP App widget for displaying Cloudinary assets in an interactive
3
+ * gallery. Attached to list-images, list-videos, list-files, and
4
+ * search-assets tools.
5
+ *
6
+ * Shares CLDS tokens, MCPApp client, helpers, and detail renderers
7
+ * with the details widget via widget-shared.ts.
8
+ */
9
+
10
+ import {
11
+ SHARED_CSS_TOKENS,
12
+ SHARED_CSS_COMPONENTS,
13
+ SHARED_JS_MCP_CLIENT,
14
+ SHARED_JS_HELPERS,
15
+ SHARED_JS_TOOLTIPS,
16
+ SHARED_JS_MODAL,
17
+ SHARED_JS_DETAIL_RENDERERS,
18
+ SHARED_JS_HOST_CONTEXT,
19
+ } from "./widget-shared.js";
20
+
21
+ export const ASSET_GALLERY_RESOURCE_URI = "ui://cloudinary/asset-gallery.html";
22
+ export const MCP_APP_MIME_TYPE = "text/html;profile=mcp-app";
23
+
24
+ export function getAssetGalleryHtml(): string {
25
+ return ASSET_GALLERY_HTML;
26
+ }
27
+
28
+ const GALLERY_CSS = /* css */ `
29
+ /* ── Header ── */
30
+ .header {
31
+ display: flex; align-items: center; justify-content: space-between;
32
+ margin-bottom: var(--cld-sp-sm);
33
+ }
34
+ .header-left { display: flex; align-items: center; gap: 10px; }
35
+ .header h1 {
36
+ font-size: 13px; font-weight: 600; color: var(--cld-text);
37
+ }
38
+ .count-badge {
39
+ font-size: var(--cld-font-xxs); color: var(--cld-text3); background: var(--cld-bg3);
40
+ padding: 2px 8px; border-radius: 20px; font-weight: 500;
41
+ }
42
+ .select-all-btn {
43
+ font-size: 12px; color: var(--cld-text2); background: none;
44
+ border: 1px solid var(--cld-border); border-radius: var(--cld-radius-sm);
45
+ padding: 4px 10px; cursor: pointer; font-family: inherit;
46
+ transition: color 0.15s, border-color 0.15s;
47
+ }
48
+ .select-all-btn:hover { color: var(--cld-accent); border-color: var(--cld-accent); }
49
+ .refresh-btn {
50
+ background: none; border: 1px solid var(--cld-border); border-radius: var(--cld-radius-sm);
51
+ color: var(--cld-text2); cursor: pointer; font-size: 14px; padding: 2px 7px;
52
+ line-height: 1; transition: background 0.15s, color 0.15s;
53
+ }
54
+ .refresh-btn:hover { background: var(--cld-bg3); color: var(--cld-text); }
55
+
56
+ /* ── Filter bar ── */
57
+ .filter-row {
58
+ margin-bottom: var(--cld-sp-md); display: flex; gap: 8px; align-items: center;
59
+ }
60
+ .filter-text-wrap { position: relative; flex: 1; }
61
+ .filter-input {
62
+ width: 100%; height: 36px; padding: 0 12px 0 34px;
63
+ border: 1px solid var(--cld-border); border-radius: var(--cld-radius);
64
+ background: var(--cld-bg); font-size: 12.5px; color: var(--cld-text);
65
+ outline: none; font-family: inherit;
66
+ transition: border-color 0.18s, box-shadow 0.18s;
67
+ }
68
+ .filter-input::placeholder { color: var(--cld-text3); }
69
+ .filter-input:focus {
70
+ border-color: var(--cld-accent);
71
+ box-shadow: 0 0 0 3px rgba(52,72,197,0.1);
72
+ }
73
+ [data-theme="dark"] .filter-input:focus { box-shadow: 0 0 0 3px rgba(13,154,255,0.15); }
74
+ .filter-icon {
75
+ position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
76
+ color: var(--cld-text3); pointer-events: none; display: flex; align-items: center;
77
+ }
78
+ .filter-clear {
79
+ position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
80
+ background: none; border: none; color: var(--cld-text3); cursor: pointer;
81
+ font-size: 14px; line-height: 1; padding: 2px 4px; border-radius: 4px;
82
+ display: none; font-family: inherit;
83
+ }
84
+ .filter-clear:hover { color: var(--cld-text); background: var(--cld-border); }
85
+ .filter-clear.visible { display: block; }
86
+
87
+ /* Aspect-ratio dropdown */
88
+ .aspect-dropdown { position: relative; flex-shrink: 0; user-select: none; }
89
+ .aspect-btn {
90
+ height: 36px; padding: 0 10px; border: 1px solid var(--cld-border);
91
+ border-radius: var(--cld-radius); background: var(--cld-bg);
92
+ font-size: 12.5px; color: var(--cld-text); cursor: pointer;
93
+ display: flex; align-items: center; gap: 6px; white-space: nowrap;
94
+ transition: border-color 0.18s, box-shadow 0.18s, background 0.18s;
95
+ font-family: inherit; outline: none;
96
+ }
97
+ .aspect-btn:hover { border-color: var(--cld-border2); }
98
+ .aspect-btn.active {
99
+ border-color: var(--cld-accent); background: var(--cld-accent-bg);
100
+ color: var(--cld-accent); font-weight: 600;
101
+ }
102
+ .aspect-btn-chevron { color: var(--cld-text3); flex-shrink: 0; transition: transform 0.18s; }
103
+ .aspect-btn.open .aspect-btn-chevron { transform: rotate(180deg); }
104
+ .aspect-menu {
105
+ position: absolute; top: calc(100% + 6px); right: 0;
106
+ background: var(--cld-bg); border: 1px solid var(--cld-border);
107
+ border-radius: 10px; box-shadow: var(--cld-shadow-md);
108
+ padding: 4px; min-width: 160px; z-index: 50; display: none;
109
+ }
110
+ .aspect-menu.open { display: block; }
111
+ .aspect-option {
112
+ display: flex; align-items: center; gap: 10px;
113
+ padding: 7px 10px; border-radius: 6px; font-size: 12.5px;
114
+ color: var(--cld-text); cursor: pointer; transition: background 0.18s;
115
+ }
116
+ .aspect-option:hover { background: var(--cld-bg3); }
117
+ .aspect-option.selected { color: var(--cld-accent); font-weight: 600; }
118
+ .aspect-opt-icon { color: var(--cld-text3); display: flex; align-items: center; flex-shrink: 0; }
119
+ .aspect-option.selected .aspect-opt-icon { color: var(--cld-accent); }
120
+ .aspect-check { margin-left: auto; color: var(--cld-accent); opacity: 0; }
121
+ .aspect-option.selected .aspect-check { opacity: 1; }
122
+ .no-results {
123
+ grid-column: 1 / -1; padding: 60px 20px;
124
+ text-align: center; color: var(--cld-text3); font-size: 13px;
125
+ }
126
+
127
+ /* ── Grid ── */
128
+ .grid {
129
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
130
+ gap: var(--cld-sp-sm);
131
+ }
132
+
133
+ /* ── Card ── */
134
+ .card {
135
+ position: relative; background: var(--cld-bg2); border: 2px solid transparent;
136
+ border-radius: var(--cld-radius); overflow: hidden;
137
+ transition: box-shadow 0.18s ease, transform 0.18s ease, border-color 0.18s ease;
138
+ cursor: default; outline: none;
139
+ }
140
+ .card:hover { box-shadow: 0 8px 24px rgba(0,0,0,0.18); transform: translateY(-2px); }
141
+ .card.selected {
142
+ border-color: var(--cld-accent);
143
+ box-shadow: 0 0 0 1px var(--cld-accent), 0 6px 20px rgba(52,72,197,0.2);
144
+ }
145
+
146
+ /* Thumbnail */
147
+ .thumb {
148
+ position: relative; width: 100%; aspect-ratio: 4/3; background: var(--cld-bg3);
149
+ overflow: hidden; display: flex; align-items: center; justify-content: center;
150
+ }
151
+ .thumb img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s ease; }
152
+ .card:hover .thumb img { transform: scale(1.04); }
153
+ .thumb .badge {
154
+ position: absolute; bottom: 6px; left: 6px; background: rgba(0,0,0,0.65);
155
+ color: #fff; font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px;
156
+ text-transform: uppercase; letter-spacing: 0.5px; backdrop-filter: blur(4px);
157
+ z-index: 2;
158
+ }
159
+ .thumb .placeholder { color: var(--cld-text3); font-size: 28px; }
160
+ .thumb .audio-placeholder { color: var(--cld-text3); font-size: 32px; font-weight: 700; }
161
+ .thumb.link:hover { opacity: 0.9; }
162
+
163
+ /* Selection checkbox */
164
+ .card-check {
165
+ position: absolute; top: 8px; left: 8px; width: 20px; height: 20px;
166
+ border-radius: 50%; background: rgba(255,255,255,0.9);
167
+ border: 2px solid rgba(255,255,255,0.9);
168
+ display: flex; align-items: center; justify-content: center;
169
+ opacity: 0; transition: opacity 0.18s, background 0.18s;
170
+ z-index: 5; box-shadow: 0 1px 4px rgba(0,0,0,0.25); pointer-events: none;
171
+ }
172
+ .card:hover .card-check, .card.selected .card-check { opacity: 1; pointer-events: auto; cursor: pointer; }
173
+ .card.selected .card-check { background: var(--cld-accent); border-color: var(--cld-accent); }
174
+ .card-check svg { width: 11px; height: 11px; opacity: 0; transition: opacity 0.18s; }
175
+ .card.selected .card-check svg { opacity: 1; }
176
+
177
+ /* Tags overlay on thumbnail */
178
+ .tags-overlay {
179
+ position: absolute; top: 8px; right: 8px;
180
+ display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end;
181
+ opacity: 0; transition: opacity 0.18s; z-index: 4; pointer-events: none;
182
+ }
183
+ .card:hover .tags-overlay { opacity: 1; }
184
+ .grid.filtering .tags-overlay { opacity: 1; }
185
+ .tag-overlay {
186
+ font-size: 10px; color: white;
187
+ background: rgba(10, 12, 18, 0.55); padding: 2px 7px; border-radius: 20px;
188
+ backdrop-filter: blur(6px); font-weight: 600; letter-spacing: 0.02em;
189
+ }
190
+ .tag-overlay.tag-match { background: rgba(52, 72, 197, 0.82); }
191
+ .tag-overlay mark {
192
+ background: rgba(255, 213, 79, 0.5); color: white;
193
+ border-radius: 2px; padding: 0 1px;
194
+ }
195
+
196
+ /* Floating action buttons */
197
+ .card-actions {
198
+ position: absolute; bottom: 10px; left: 0; right: 0;
199
+ display: flex; align-items: center; justify-content: center; gap: 5px;
200
+ opacity: 0; transform: translateY(6px);
201
+ transition: opacity 0.18s, transform 0.18s; z-index: 5;
202
+ }
203
+ .card:hover .card-actions { opacity: 1; transform: translateY(0); }
204
+ .action-btn {
205
+ height: 28px; padding: 0 11px; border-radius: 20px; border: none;
206
+ font-size: 11px; font-weight: 600; cursor: pointer;
207
+ display: flex; align-items: center; gap: 4px;
208
+ transition: transform 0.12s ease, box-shadow 0.12s ease;
209
+ white-space: nowrap; box-shadow: 0 2px 8px rgba(0,0,0,0.25);
210
+ font-family: inherit;
211
+ }
212
+ .action-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
213
+ .action-btn:active { transform: translateY(0); }
214
+ .action-btn.act-original { background: rgba(255,255,255,0.92); color: #111318; backdrop-filter: blur(8px); }
215
+ [data-theme="dark"] .action-btn.act-original { background: rgba(40,44,56,0.92); color: #e0e4ec; }
216
+ .action-btn.act-optimized { background: var(--cld-accent); color: white; }
217
+ .action-btn.act-download { background: rgba(10,12,18,0.7); color: white; backdrop-filter: blur(8px); padding: 0 9px; font-size: 13px; }
218
+ [data-theme="dark"] .action-btn.act-download { background: rgba(255,255,255,0.2); }
219
+
220
+ /* ── Info section ── */
221
+ .info { padding: var(--cld-sp-xs) var(--cld-sp-sm) var(--cld-sp-sm); }
222
+ .info .name {
223
+ font-size: 13px; font-weight: 600; color: var(--cld-text);
224
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 6px;
225
+ }
226
+ .info .name .link { color: inherit; text-decoration: none; }
227
+ .info .name .link:hover { color: var(--cld-accent); text-decoration: underline; }
228
+ .pills { display: flex; flex-wrap: wrap; gap: 4px; }
229
+ .pill {
230
+ font-size: 10px; color: var(--cld-text2); background: var(--cld-bg);
231
+ padding: 2px 7px; border-radius: 4px; border: 1px solid var(--cld-border); white-space: nowrap;
232
+ }
233
+ .tags { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 4px; }
234
+ .tag {
235
+ font-size: var(--cld-font-xxs); background: var(--cld-chip-tag-bg);
236
+ color: var(--cld-chip-tag-fg); padding: 2px 7px; border-radius: var(--cld-radius-lg); font-weight: 500;
237
+ }
238
+ .tag-more { background: var(--cld-bg3); color: var(--cld-text3); }
239
+ .card-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; }
240
+ .date { font-size: 10px; color: var(--cld-text3); }
241
+ .details-link {
242
+ font-size: 11px; color: var(--cld-accent); cursor: pointer;
243
+ font-weight: 500; margin-left: auto;
244
+ }
245
+ .details-link:hover { text-decoration: underline; }
246
+
247
+ /* ── Skeleton loading ── */
248
+ .skeleton {
249
+ background: linear-gradient(90deg, var(--cld-bg3) 25%, var(--cld-bg4) 50%, var(--cld-bg3) 75%);
250
+ background-size: 200% 100%; animation: shimmer 1.4s infinite;
251
+ border-radius: var(--cld-radius); aspect-ratio: 4/3;
252
+ }
253
+ @keyframes shimmer {
254
+ 0% { background-position: 200% 0; }
255
+ 100% { background-position: -200% 0; }
256
+ }
257
+
258
+ /* ── Multi-select bar ── */
259
+ .select-bar {
260
+ position: fixed; bottom: 16px; left: 50%;
261
+ transform: translateX(-50%) translateY(80px);
262
+ background: #1a1d24; color: white; border-radius: 40px;
263
+ padding: 0 6px 0 16px; height: 48px;
264
+ display: flex; align-items: center; gap: 4px;
265
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
266
+ transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s;
267
+ opacity: 0; pointer-events: none; z-index: 100; white-space: nowrap;
268
+ }
269
+ .select-bar.visible { transform: translateX(-50%) translateY(0); opacity: 1; pointer-events: all; }
270
+ .select-bar-spacer { height: 72px; }
271
+ .select-count { font-size: 13px; font-weight: 600; margin-right: 8px; }
272
+ .bar-btn {
273
+ height: 36px; padding: 0 14px; border: none; border-radius: 30px;
274
+ font-size: 12px; font-weight: 600; cursor: pointer;
275
+ transition: background 0.18s; display: flex; align-items: center; gap: 6px;
276
+ font-family: inherit;
277
+ }
278
+ .bar-btn.bar-primary { background: var(--cld-accent); color: white; }
279
+ .bar-btn.bar-primary:hover { opacity: 0.85; }
280
+ .bar-btn.bar-secondary { background: rgba(255,255,255,0.12); color: white; }
281
+ .bar-btn.bar-secondary:hover { background: rgba(255,255,255,0.2); }
282
+ .bar-btn.bar-ghost { background: none; color: rgba(255,255,255,0.6); padding: 0 10px; }
283
+ .bar-btn.bar-ghost:hover { color: white; }
284
+ .bar-divider { width: 1px; height: 20px; background: rgba(255,255,255,0.15); margin: 0 4px; }
285
+
286
+ /* ── Toast ── */
287
+ .gallery-toast {
288
+ position: fixed; bottom: 86px; right: 20px;
289
+ transform: translateY(20px);
290
+ opacity: 0;
291
+ background: #1a1d24; color: white;
292
+ padding: 9px 18px; border-radius: 24px;
293
+ font-size: 12px; font-weight: 600;
294
+ box-shadow: 0 4px 16px rgba(0,0,0,0.25);
295
+ transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s;
296
+ z-index: 999; pointer-events: none;
297
+ }
298
+ .gallery-toast.show { transform: translateY(0); opacity: 1; }
299
+ `;
300
+
301
+ const GALLERY_JS = /* js */ `
302
+ var LOG_PREFIX = "[gallery]";
303
+ var MIN_HEIGHT = 120;
304
+ var allResources = [];
305
+ var lastCursor = null;
306
+ var pendingCall = { name: null, args: null };
307
+ var selected = new Set();
308
+ var filterQuery = "";
309
+ var aspectFilter = "";
310
+ var app = new MCPApp({ name: "Cloudinary Asset Gallery", version: "1.0.0" });
311
+ setupHostContext(app);
312
+
313
+ function optimizedUrl(url, resource) {
314
+ if (!url) return "";
315
+ if (resource && resource.resource_type === "raw") return url;
316
+ return insertTransformation(url, "f_auto,q_auto", resource) || url;
317
+ }
318
+ function downloadUrl(url, resource) {
319
+ if (!url) return "";
320
+ return insertTransformation(url, "fl_attachment", resource) || url;
321
+ }
322
+
323
+ var _toastTimer;
324
+ function showToast(msg) {
325
+ var t = document.getElementById("gallery-toast");
326
+ if (!t) return;
327
+ t.textContent = msg;
328
+ t.classList.add("show");
329
+ clearTimeout(_toastTimer);
330
+ _toastTimer = setTimeout(function() { t.classList.remove("show"); }, 2000);
331
+ }
332
+
333
+ function getAspect(r) {
334
+ if (!r.width || !r.height) return "";
335
+ var ratio = r.width / r.height;
336
+ if (ratio > 1.1) return "landscape";
337
+ if (ratio < 0.9) return "portrait";
338
+ return "square";
339
+ }
340
+
341
+ function highlightText(text, query) {
342
+ if (!query) return esc(text);
343
+ var lo = text.toLowerCase();
344
+ var idx = lo.indexOf(query);
345
+ if (idx === -1) return esc(text);
346
+ return esc(text.slice(0, idx))
347
+ + "<mark>" + esc(text.slice(idx, idx + query.length)) + "</mark>"
348
+ + esc(text.slice(idx + query.length));
349
+ }
350
+
351
+ function updateSelectBar() {
352
+ var bar = document.getElementById("select-bar");
353
+ var countEl = document.getElementById("select-count");
354
+ if (!bar || !countEl) return;
355
+ var n = selected.size;
356
+ countEl.textContent = n + " selected";
357
+ bar.classList.toggle("visible", n > 0);
358
+ var spacer = document.getElementById("select-bar-spacer");
359
+ if (spacer) spacer.style.display = n > 0 ? "" : "none";
360
+ var btn = document.getElementById("select-all-btn");
361
+ if (btn) {
362
+ var visible = getVisibleIndices();
363
+ var allSelected = visible.length > 0 && visible.every(function(i) { return selected.has(i); });
364
+ btn.textContent = allSelected ? "Deselect all" : "Select all";
365
+ }
366
+ var optBtn = document.getElementById("bar-copy-optimized");
367
+ if (optBtn) {
368
+ var hasOptimizable = false;
369
+ selected.forEach(function(i) { var r = allResources[i]; if (r && r.resource_type !== "raw") hasOptimizable = true; });
370
+ optBtn.style.display = hasOptimizable ? "" : "none";
371
+ }
372
+ }
373
+
374
+ function getVisibleIndices() {
375
+ var visible = [];
376
+ for (var i = 0; i < allResources.length; i++) {
377
+ var card = document.getElementById("card-" + i);
378
+ if (card && card.style.display !== "none") visible.push(i);
379
+ }
380
+ return visible;
381
+ }
382
+
383
+ function toggleSelectAll() {
384
+ var visible = getVisibleIndices();
385
+ var allSelected = visible.length > 0 && visible.every(function(i) { return selected.has(i); });
386
+ if (allSelected) {
387
+ clearSelection();
388
+ } else {
389
+ for (var vi = 0; vi < visible.length; vi++) {
390
+ selected.add(visible[vi]);
391
+ var card = document.getElementById("card-" + visible[vi]);
392
+ if (card) card.classList.add("selected");
393
+ }
394
+ updateSelectBar();
395
+ }
396
+ }
397
+
398
+ function clearSelection() {
399
+ selected.forEach(function(i) {
400
+ var card = document.getElementById("card-" + i);
401
+ if (card) card.classList.remove("selected");
402
+ });
403
+ selected.clear();
404
+ updateSelectBar();
405
+ }
406
+
407
+ function toggleCard(idx) {
408
+ var card = document.getElementById("card-" + idx);
409
+ if (!card) return;
410
+ if (selected.has(idx)) {
411
+ selected.delete(idx);
412
+ card.classList.remove("selected");
413
+ } else {
414
+ selected.add(idx);
415
+ card.classList.add("selected");
416
+ }
417
+ updateSelectBar();
418
+ }
419
+
420
+ function copyAssetUrl(type, idx) {
421
+ var r = allResources[idx];
422
+ if (!r) return;
423
+ var url = r.secure_url || r.url || "";
424
+ var copyUrl = type === "optimized" ? optimizedUrl(url, r) : url;
425
+ if (!copyUrl) return;
426
+ try {
427
+ navigator.clipboard.writeText(copyUrl).then(function() {
428
+ showToast(type === "optimized" ? "\\u2728 Optimized URL copied" : "URL copied");
429
+ });
430
+ } catch(e) { showError("Copy Failed", String(e)); }
431
+ }
432
+
433
+ function downloadOne(idx) {
434
+ var r = allResources[idx];
435
+ if (!r) return;
436
+ var url = r.secure_url || r.url || "";
437
+ var dl = downloadUrl(url, r);
438
+ if (!dl) return;
439
+ app._rpc("ui/open-link", { url: dl });
440
+ showToast("Downloading " + (r.display_name || r.filename || r.public_id || "asset"));
441
+ }
442
+
443
+ function copySelectedUrls(type) {
444
+ var urls = [];
445
+ selected.forEach(function(i) {
446
+ var r = allResources[i];
447
+ if (!r) return;
448
+ var url = r.secure_url || r.url || "";
449
+ urls.push(type === "optimized" ? optimizedUrl(url, r) : url);
450
+ });
451
+ if (!urls.length) return;
452
+ try {
453
+ navigator.clipboard.writeText(urls.join("\\n")).then(function() {
454
+ showToast(urls.length + " " + (type === "optimized" ? "optimized " : "") + "URLs copied");
455
+ });
456
+ } catch(e) { showError("Copy Failed", String(e)); }
457
+ }
458
+
459
+ function downloadSelected() {
460
+ var count = 0;
461
+ selected.forEach(function(i) {
462
+ var r = allResources[i];
463
+ if (!r) return;
464
+ var url = r.secure_url || r.url || "";
465
+ var dl = downloadUrl(url, r);
466
+ if (dl) { app._rpc("ui/open-link", { url: dl }); count++; }
467
+ });
468
+ if (count) showToast("Downloading " + count + " asset" + (count > 1 ? "s" : ""));
469
+ }
470
+
471
+ function handleFilter() {
472
+ var input = document.getElementById("filter-input");
473
+ filterQuery = input ? input.value.trim().toLowerCase() : "";
474
+
475
+ var clearBtn = document.getElementById("filter-clear");
476
+ if (clearBtn) clearBtn.classList.toggle("visible", filterQuery.length > 0);
477
+
478
+ var aspectBtn = document.getElementById("aspect-btn");
479
+ if (aspectBtn) aspectBtn.classList.toggle("active", aspectFilter !== "");
480
+
481
+ var anyFilter = filterQuery.length > 0 || aspectFilter !== "";
482
+ var grid = document.getElementById("gallery-grid");
483
+ if (grid) grid.classList.toggle("filtering", anyFilter);
484
+
485
+ var visibleCount = 0;
486
+ for (var i = 0; i < allResources.length; i++) {
487
+ var r = allResources[i];
488
+ var card = document.getElementById("card-" + i);
489
+ if (!card) continue;
490
+
491
+ var name = (r.public_id || r.filename || "").toLowerCase();
492
+ var tags = r.tags || [];
493
+ var textMatch = !filterQuery
494
+ || name.indexOf(filterQuery) !== -1
495
+ || tags.some(function(t) { return t.toLowerCase().indexOf(filterQuery) !== -1; });
496
+
497
+ var aspectMatch = !aspectFilter || getAspect(r) === aspectFilter;
498
+ var match = textMatch && aspectMatch;
499
+ card.style.display = match ? "" : "none";
500
+
501
+ var tagsEl = document.getElementById("tags-overlay-" + i);
502
+ if (tagsEl && tags.length) {
503
+ var maxOv = 3;
504
+ var matchedTags = [];
505
+ var otherTags = [];
506
+ for (var ti = 0; ti < tags.length; ti++) {
507
+ var isMatch = filterQuery && tags[ti].toLowerCase().indexOf(filterQuery) !== -1;
508
+ if (isMatch) matchedTags.push(tags[ti]);
509
+ else otherTags.push(tags[ti]);
510
+ }
511
+ var shown = matchedTags.slice();
512
+ var remaining = maxOv - shown.length;
513
+ if (remaining > 0) shown = shown.concat(otherTags.slice(0, remaining));
514
+ var hidden = tags.length - shown.length;
515
+ var hiddenTags = tags.filter(function(t) { return shown.indexOf(t) === -1; });
516
+ tagsEl.innerHTML = shown.map(function(t) {
517
+ var matched = filterQuery && t.toLowerCase().indexOf(filterQuery) !== -1;
518
+ return '<span class="tag-overlay' + (matched ? ' tag-match' : '') + '">' + highlightText(t, filterQuery) + '</span>';
519
+ }).join("") + (hidden > 0 ? '<span class="tag-overlay" title="' + esc(hiddenTags.join(", ")) + '">+' + hidden + '</span>' : '');
520
+ }
521
+
522
+ if (match) visibleCount++;
523
+ }
524
+
525
+ var badge = document.getElementById("count-badge");
526
+ if (badge) {
527
+ badge.textContent = anyFilter
528
+ ? visibleCount + " of " + allResources.length
529
+ : allResources.length + (lastCursor ? "+" : "") + " items";
530
+ }
531
+
532
+ var noRes = document.getElementById("no-results");
533
+ if (visibleCount === 0 && anyFilter) {
534
+ if (!noRes && grid) {
535
+ noRes = document.createElement("div");
536
+ noRes.id = "no-results";
537
+ noRes.className = "no-results";
538
+ grid.appendChild(noRes);
539
+ }
540
+ if (noRes) noRes.textContent = "No results" + (filterQuery ? ' for "' + filterQuery + '"' : "") + (aspectFilter ? " in " + aspectFilter + " images" : "");
541
+ } else if (noRes) {
542
+ noRes.remove();
543
+ }
544
+ }
545
+
546
+ function clearFilter() {
547
+ var input = document.getElementById("filter-input");
548
+ if (input) input.value = "";
549
+ aspectFilter = "";
550
+ var label = document.getElementById("aspect-btn-label");
551
+ if (label) label.textContent = "All orientations";
552
+ document.querySelectorAll(".aspect-option").forEach(function(o) {
553
+ o.classList.toggle("selected", o.getAttribute("data-value") === "");
554
+ });
555
+ handleFilter();
556
+ }
557
+
558
+ function toggleAspectMenu(e) {
559
+ e.stopPropagation();
560
+ var btn = document.getElementById("aspect-btn");
561
+ var menu = document.getElementById("aspect-menu");
562
+ if (!btn || !menu) return;
563
+ var open = menu.classList.toggle("open");
564
+ btn.classList.toggle("open", open);
565
+ }
566
+
567
+ function selectAspect(val) {
568
+ aspectFilter = val;
569
+ var labels = { "": "All orientations", landscape: "Landscape", portrait: "Portrait", square: "Square" };
570
+ var label = document.getElementById("aspect-btn-label");
571
+ if (label) label.textContent = labels[aspectFilter] || "All orientations";
572
+ document.querySelectorAll(".aspect-option").forEach(function(o) {
573
+ o.classList.toggle("selected", o.getAttribute("data-value") === aspectFilter);
574
+ });
575
+ var menu = document.getElementById("aspect-menu");
576
+ var btn = document.getElementById("aspect-btn");
577
+ if (menu) menu.classList.remove("open");
578
+ if (btn) btn.classList.remove("open");
579
+ handleFilter();
580
+ }
581
+
582
+ function render() {
583
+ var root = document.getElementById("app");
584
+
585
+ if (allResources.length === 0) {
586
+ selected.clear();
587
+ root.innerHTML = '<div class="status"><div class="icon">\\u{1F4F7}</div>No assets found.</div>';
588
+ return;
589
+ }
590
+
591
+ var h = "";
592
+
593
+ // Header
594
+ h += '<div class="header">';
595
+ h += '<div class="header-left">';
596
+ h += '<h1>Results</h1>';
597
+ h += '<span class="count-badge" id="count-badge">' + allResources.length + (lastCursor ? "+" : "") + ' items</span>';
598
+ h += '</div>';
599
+ h += '<div style="display:flex;align-items:center;gap:8px">';
600
+ h += '<button class="select-all-btn" id="select-all-btn">Select all</button>';
601
+ h += '<button class="refresh-btn" id="refresh-gallery" title="Refresh">\\u21BB</button>';
602
+ h += '</div>';
603
+ h += '</div>';
604
+
605
+ // Filter bar
606
+ h += '<div class="filter-row">';
607
+ h += '<div class="filter-text-wrap">';
608
+ h += '<span class="filter-icon"><svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="6.5" cy="6.5" r="4.5"/><path d="M10.5 10.5l3 3"/></svg></span>';
609
+ h += '<input class="filter-input" id="filter-input" type="text" placeholder="Filter by filename or tag\\u2026" autocomplete="off" spellcheck="false">';
610
+ h += '<button class="filter-clear" id="filter-clear">\\u2715</button>';
611
+ h += '</div>';
612
+ h += '<div class="aspect-dropdown" id="aspect-dropdown">';
613
+ h += '<button class="aspect-btn" id="aspect-btn">';
614
+ h += '<span id="aspect-btn-label">All orientations</span>';
615
+ h += '<svg class="aspect-btn-chevron" width="11" height="11" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="2,4 6,8 10,4"/></svg>';
616
+ h += '</button>';
617
+ h += '<div class="aspect-menu" id="aspect-menu">';
618
+ var aspects = [
619
+ { val: "", label: "All orientations", icon: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="1" width="12" height="12" rx="1.5"/></svg>' },
620
+ { val: "landscape", label: "Landscape", icon: '<svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="currentColor" stroke-width="1.5"><rect x=".75" y=".75" width="12.5" height="8.5" rx="1.5"/></svg>' },
621
+ { val: "portrait", label: "Portrait", icon: '<svg width="10" height="14" viewBox="0 0 10 14" fill="none" stroke="currentColor" stroke-width="1.5"><rect x=".75" y=".75" width="8.5" height="12.5" rx="1.5"/></svg>' },
622
+ { val: "square", label: "Square", icon: '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5"><rect x=".75" y=".75" width="10.5" height="10.5" rx="1.5"/></svg>' },
623
+ ];
624
+ for (var ai = 0; ai < aspects.length; ai++) {
625
+ var ao = aspects[ai];
626
+ h += '<div class="aspect-option' + (ao.val === aspectFilter ? ' selected' : '') + '" data-value="' + ao.val + '">';
627
+ h += '<span class="aspect-opt-icon">' + ao.icon + '</span>';
628
+ h += ao.label;
629
+ h += '<svg class="aspect-check" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="2,6 5,9 10,3"/></svg>';
630
+ h += '</div>';
631
+ }
632
+ h += '</div></div></div>';
633
+
634
+ // Grid
635
+ h += '<div class="grid" id="gallery-grid">';
636
+ for (var i = 0; i < allResources.length; i++) {
637
+ var r = allResources[i];
638
+ var url = r.secure_url || r.url || "";
639
+ var thumb = thumbUrl(url, 300, 225, r);
640
+ var name = r.display_name || r.filename || r.public_id || "unknown";
641
+ var fmt = (r.format || "").toUpperCase();
642
+ var dims = (r.width && r.height) ? r.width + "\\u00d7" + r.height : "";
643
+ var size = r.bytes ? fmtBytes(r.bytes) : "";
644
+ var date = fmtDate(r.created_at);
645
+ var tags = r.tags || [];
646
+ var rt = r.resource_type || "";
647
+ var audio = isAudioResource(r);
648
+ var dur = r.duration ? fmtDuration(r.duration) : "";
649
+
650
+ h += '<div class="card" id="card-' + i + '">';
651
+ h += '<div class="thumb">';
652
+ if (thumb) {
653
+ h += '<img src="' + esc(thumb) + '" alt="' + esc(name) + '" loading="lazy" data-audio="' + (audio ? "1" : "") + '">';
654
+ if (audio) {
655
+ h += '<div class="thumb-overlay playable" data-play="' + i + '"><div class="audio-icon">\\u266B</div></div>';
656
+ } else if (rt === "video") {
657
+ h += '<div class="thumb-overlay playable" data-play="' + i + '"><div class="play-icon"></div></div>';
658
+ }
659
+ if (dur) h += '<div class="duration-badge">' + dur + "</div>";
660
+ } else if (rt === "raw") {
661
+ h += '<div class="file-icon">' + fileTypeIcon(r.format) + "</div>";
662
+ } else {
663
+ h += '<span class="placeholder">\\u{1F5BC}</span>';
664
+ }
665
+
666
+ // Selection checkbox
667
+ h += '<div class="card-check"><svg viewBox="0 0 12 12" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="2,6 5,9 10,3"/></svg></div>';
668
+
669
+ // Tags overlay (capped at 3)
670
+ if (tags.length) {
671
+ var maxOverlay = 3;
672
+ h += '<div class="tags-overlay" id="tags-overlay-' + i + '">';
673
+ for (var ti = 0; ti < Math.min(tags.length, maxOverlay); ti++) h += '<span class="tag-overlay">' + esc(tags[ti]) + '</span>';
674
+ if (tags.length > maxOverlay) h += '<span class="tag-overlay" title="' + esc(tags.slice(maxOverlay).join(", ")) + '">+' + (tags.length - maxOverlay) + '</span>';
675
+ h += '</div>';
676
+ }
677
+
678
+ // Floating action buttons
679
+ if (url) {
680
+ h += '<div class="card-actions">';
681
+ h += '<button class="action-btn act-original" data-copy-original="' + i + '">Copy URL</button>';
682
+ if (rt !== "raw") h += '<button class="action-btn act-optimized" data-copy-optimized="' + i + '">\\u2728 Optimized</button>';
683
+ h += '<button class="action-btn act-download" data-download="' + i + '" title="Download">\\u2193</button>';
684
+ h += '</div>';
685
+ }
686
+
687
+ if (fmt) h += '<span class="badge">' + esc(fmt) + "</span>";
688
+ h += "</div>";
689
+
690
+ // Info section
691
+ h += '<div class="info">';
692
+ h += '<div class="name" title="' + esc(name) + '">';
693
+ if (url) h += '<span class="link" data-url="' + esc(url) + '">' + esc(name) + "</span>";
694
+ else h += esc(name);
695
+ h += "</div>";
696
+
697
+ var pills = [];
698
+ if (dims) pills.push(dims);
699
+ if (dur) pills.push(dur);
700
+ if (size) pills.push(size);
701
+ if (pills.length) {
702
+ h += '<div class="pills">';
703
+ for (var p = 0; p < pills.length; p++) h += '<span class="pill">' + pills[p] + "</span>";
704
+ h += "</div>";
705
+ }
706
+
707
+ if (tags.length) {
708
+ var maxTags = 3;
709
+ h += '<div class="tags">';
710
+ for (var t = 0; t < Math.min(tags.length, maxTags); t++) h += '<span class="tag">' + esc(tags[t]) + "</span>";
711
+ if (tags.length > maxTags) h += '<span class="tag tag-more" title="' + esc(tags.slice(maxTags).join(", ")) + '">+' + (tags.length - maxTags) + '</span>';
712
+ h += "</div>";
713
+ }
714
+
715
+ h += '<div class="card-footer">';
716
+ if (date) h += '<div class="date">' + date + "</div>";
717
+ h += '<span class="details-link" data-idx="' + i + '">Details</span>';
718
+ h += "</div>";
719
+
720
+ h += "</div></div>";
721
+ }
722
+ h += "</div>";
723
+
724
+ if (lastCursor) {
725
+ h += '<div style="text-align:center;padding:16px 0;">';
726
+ h += '<button class="prompt-btn prompt-btn-primary" id="load-more-btn">Load More</button>';
727
+ h += "</div>";
728
+ }
729
+
730
+ // Spacer so select bar doesn't cover Load More
731
+ h += '<div class="select-bar-spacer" id="select-bar-spacer" style="display:none"></div>';
732
+
733
+ // Multi-select bar
734
+ h += '<div class="select-bar" id="select-bar">';
735
+ h += '<span class="select-count" id="select-count">0 selected</span>';
736
+ h += '<div class="bar-divider"></div>';
737
+ h += '<button class="bar-btn bar-primary" id="bar-copy-optimized" style="display:none">\\u2728 Copy Optimized</button>';
738
+ h += '<button class="bar-btn bar-secondary" id="bar-copy-original">Copy Original</button>';
739
+ h += '<button class="bar-btn bar-secondary" id="bar-download">\\u2193 Download All</button>';
740
+ h += '<div class="bar-divider"></div>';
741
+ h += '<button class="bar-btn bar-ghost" id="bar-clear">\\u2715</button>';
742
+ h += '</div>';
743
+
744
+ // Toast
745
+ h += '<div class="gallery-toast" id="gallery-toast"></div>';
746
+
747
+ root.innerHTML = h;
748
+
749
+ // Re-apply selection state
750
+ selected.forEach(function(i) {
751
+ var card = document.getElementById("card-" + i);
752
+ if (card) card.classList.add("selected");
753
+ });
754
+ updateSelectBar();
755
+
756
+ // Image error fallback
757
+ var imgs = root.querySelectorAll(".thumb img");
758
+ for (var ii = 0; ii < imgs.length; ii++) {
759
+ imgs[ii].addEventListener("error", function() {
760
+ this.style.display = "none";
761
+ var isAudio = this.getAttribute("data-audio") === "1";
762
+ var ph = isAudio
763
+ ? '<div class="audio-placeholder">\\u266B</div>'
764
+ : '<span class="placeholder">\\u{1F5BC}</span>';
765
+ this.parentElement.insertAdjacentHTML("afterbegin", ph);
766
+ });
767
+ }
768
+
769
+ requestAnimationFrame(function() {
770
+ app.reportSize(Math.max(document.documentElement.scrollHeight, MIN_HEIGHT));
771
+ });
772
+ }
773
+
774
+ // One-time event delegation (survives re-renders)
775
+ var _eventsAttached = false;
776
+ function attachEvents() {
777
+ if (_eventsAttached) return;
778
+ _eventsAttached = true;
779
+ var root = document.getElementById("app");
780
+
781
+ root.addEventListener("input", function(e) {
782
+ if (e.target && e.target.id === "filter-input") handleFilter();
783
+ });
784
+
785
+ document.addEventListener("click", function(e) {
786
+ var dd = document.getElementById("aspect-dropdown");
787
+ if (dd && !dd.contains(e.target)) {
788
+ var menu = document.getElementById("aspect-menu");
789
+ var btn = document.getElementById("aspect-btn");
790
+ if (menu) menu.classList.remove("open");
791
+ if (btn) btn.classList.remove("open");
792
+ }
793
+ });
794
+
795
+ root.addEventListener("click", function(e) {
796
+ var el = e.target;
797
+ while (el && el !== root) {
798
+ if (el.id === "load-more-btn") { loadMore(); return; }
799
+ if (el.id === "refresh-gallery") { refreshGallery(); return; }
800
+ if (el.id === "select-all-btn") { toggleSelectAll(); return; }
801
+ if (el.id === "filter-clear") { clearFilter(); return; }
802
+ if (el.id === "bar-copy-optimized") { copySelectedUrls("optimized"); return; }
803
+ if (el.id === "bar-copy-original") { copySelectedUrls("original"); return; }
804
+ if (el.id === "bar-download") { downloadSelected(); return; }
805
+ if (el.id === "bar-clear") { clearSelection(); return; }
806
+ if (el.id === "aspect-btn" || el.parentElement && el.parentElement.id === "aspect-btn") {
807
+ toggleAspectMenu(e); return;
808
+ }
809
+ if (el.classList && el.classList.contains("aspect-option")) {
810
+ selectAspect(el.getAttribute("data-value") || ""); return;
811
+ }
812
+ if (el.dataset && el.dataset.copyOriginal != null) {
813
+ e.stopPropagation();
814
+ copyAssetUrl("original", parseInt(el.dataset.copyOriginal, 10)); return;
815
+ }
816
+ if (el.dataset && el.dataset.copyOptimized != null) {
817
+ e.stopPropagation();
818
+ copyAssetUrl("optimized", parseInt(el.dataset.copyOptimized, 10)); return;
819
+ }
820
+ if (el.dataset && el.dataset.download != null) {
821
+ e.stopPropagation();
822
+ downloadOne(parseInt(el.dataset.download, 10)); return;
823
+ }
824
+ if (el.classList && el.classList.contains("card-check")) {
825
+ var cardEl = el.closest(".card");
826
+ if (cardEl) { var ci = parseInt(cardEl.id.replace("card-", ""), 10); if (!isNaN(ci)) toggleCard(ci); }
827
+ return;
828
+ }
829
+ if (el.dataset && el.dataset.play != null) {
830
+ playMedia(parseInt(el.dataset.play, 10)); return;
831
+ }
832
+ if (el.classList && el.classList.contains("details-link") && el.dataset.idx != null) {
833
+ showDetails(parseInt(el.dataset.idx, 10)); return;
834
+ }
835
+ if (el.classList && el.classList.contains("link") && el.dataset.url) {
836
+ app._rpc("ui/open-link", { url: el.dataset.url }); return;
837
+ }
838
+ if (el.classList && el.classList.contains("card")) {
839
+ var idx = parseInt(el.id.replace("card-", ""), 10);
840
+ if (!isNaN(idx)) toggleCard(idx);
841
+ return;
842
+ }
843
+ el = el.parentElement;
844
+ }
845
+ });
846
+ }
847
+
848
+ function playMedia(idx) {
849
+ var r = allResources[idx];
850
+ if (!r) return;
851
+
852
+ var url = r.secure_url || r.url || "";
853
+ var name = r.display_name || r.public_id || r.filename || "Asset";
854
+ var sub = (r.format || "").toUpperCase();
855
+ if (r.duration) sub += " \\u00b7 " + fmtDuration(r.duration);
856
+ if (r.bytes) sub += " \\u00b7 " + fmtBytes(r.bytes);
857
+
858
+ var header = modalHeader(name, url, sub, r);
859
+ var body = renderMediaModalBody(r);
860
+ openModal(header, body);
861
+ }
862
+
863
+ // Details modal (calls get-asset-details for full data)
864
+ async function showDetails(idx) {
865
+ var r = allResources[idx];
866
+ if (!r) return;
867
+
868
+ var url = r.secure_url || r.url || "";
869
+ var name = r.display_name || r.public_id || r.filename || "Asset";
870
+ var sub = (r.format || "").toUpperCase();
871
+ if (r.width && r.height) sub += " \\u00b7 " + r.width + "\\u00d7" + r.height;
872
+ if (r.duration) sub += " \\u00b7 " + fmtDuration(r.duration);
873
+
874
+ var header = modalHeader(name, url, sub, r);
875
+ var loadingBody = '<div class="modal-loading"><div class="spinner"></div><div>Loading asset details\\u2026</div></div>';
876
+ openModal(header, loadingBody);
877
+
878
+ try {
879
+ var res = await app.callServerTool({
880
+ name: "get-asset-details",
881
+ arguments: { asset_id: r.asset_id },
882
+ });
883
+ var data = ingestResult(res);
884
+ if (data && !data._error && !data._truncated && !data._parseError) {
885
+ console.log(LOG_PREFIX, "details loaded for", r.asset_id);
886
+ var modalBody = document.querySelector(".modal-body");
887
+ if (modalBody) modalBody.innerHTML = renderFullDetails(data);
888
+ } else {
889
+ var errDetail = (data && data._message) ? data._message.substring(0, 300) : "Could not parse asset details from the server response.";
890
+ var mb = document.querySelector(".modal-body");
891
+ if (mb) mb.innerHTML = renderModalError("Unexpected Response", errDetail);
892
+ }
893
+ } catch (e) {
894
+ var errMsg = String(e && e.message ? e.message : e);
895
+ var isTimeout = errMsg.indexOf("timeout") !== -1 || errMsg.indexOf("Timeout") !== -1;
896
+ var title = isTimeout ? "Request Timed Out" : "Failed to Load Details";
897
+ var detail = isTimeout
898
+ ? "The server did not respond within " + (TOOL_CALL_TIMEOUT_MS / 1000) + "s. The MCP server may be overloaded or disconnected."
899
+ : errMsg;
900
+ console.error(LOG_PREFIX, "showDetails error:", errMsg);
901
+ var mb2 = document.querySelector(".modal-body");
902
+ if (mb2) mb2.innerHTML = renderModalError(title, detail);
903
+ }
904
+ }
905
+
906
+ // Bootstrap
907
+ app.ontoolinput = function(params) {
908
+ if (params.name) pendingCall.name = params.name;
909
+ if (params.arguments) pendingCall.args = params.arguments;
910
+ showReadyPrompt(pendingCall, fetchDirect);
911
+ };
912
+
913
+ app.ontoolcancelled = function(params) {
914
+ console.log(LOG_PREFIX, "tool cancelled:", params && params.reason);
915
+ showCancelledPrompt(pendingCall, fetchDirect);
916
+ };
917
+
918
+ function inferToolName(data) {
919
+ if (data.total_count !== undefined) return "search-assets";
920
+ var resources = data.resources;
921
+ if (!resources || !resources.length) return null;
922
+ var rt = resources[0].resource_type;
923
+ if (rt === "video") return "list-videos";
924
+ if (rt === "raw") return "list-files";
925
+ return "list-images";
926
+ }
927
+
928
+ app.ontoolresult = function(result) {
929
+ var data = ingestResult(result);
930
+ if (data && data.resources) {
931
+ console.log(LOG_PREFIX, "host result:", data.resources.length, "resources");
932
+ allResources = data.resources;
933
+ lastCursor = data.next_cursor || null;
934
+ pendingCall.name = inferToolName(data) || pendingCall.name;
935
+ render(); attachEvents();
936
+ return;
937
+ }
938
+
939
+ if (data && data._error) {
940
+ showPersistentError("Error", data._message || JSON.stringify(data));
941
+ return;
942
+ }
943
+
944
+ if (data && (data._truncated || data._parseError)) {
945
+ console.log(LOG_PREFIX, "host result not JSON, auto-refetching as JSON");
946
+ fetchDirect();
947
+ return;
948
+ }
949
+
950
+ console.warn(LOG_PREFIX, "host result unusable");
951
+ showFetchPrompt();
952
+ };
953
+
954
+ function showFetchPrompt() {
955
+ var name = pendingCall.name || "list-images";
956
+ var root = document.getElementById("app");
957
+ var h = '<div class="prompt">';
958
+ h += '<div class="prompt-icon">\\u{1F4E6}</div>';
959
+ h += '<div class="prompt-title">Could Not Display Results</div>';
960
+ h += '<div class="prompt-desc">';
961
+ h += "The response from <strong>" + esc(name) + "</strong> could not be rendered. ";
962
+ h += "You can try fetching the data directly from the server.";
963
+ h += "</div>";
964
+ h += '<div class="prompt-actions">';
965
+ h += '<button class="prompt-btn prompt-btn-primary" id="fetch-direct-btn">Fetch Directly</button>';
966
+ h += "</div></div>";
967
+ root.innerHTML = h;
968
+ document.getElementById("fetch-direct-btn").addEventListener("click", function() { fetchDirect(); });
969
+ }
970
+
971
+ function jsonArgs(src) {
972
+ var a = {};
973
+ for (var k in src) a[k] = src[k];
974
+ return a;
975
+ }
976
+
977
+ async function fetchDirect() {
978
+ var name = pendingCall.name || "list-images";
979
+ var args = jsonArgs(pendingCall.args || {});
980
+ console.log(LOG_PREFIX, "fetchDirect ->", name);
981
+
982
+ document.getElementById("app").innerHTML = '<div class="status">Fetching assets\\u2026</div>';
983
+ try {
984
+ var res = await app.callServerTool({ name: name, arguments: args });
985
+ var data = ingestResult(res);
986
+ if (data && data._error) {
987
+ showPersistentError("Error", data._message || JSON.stringify(data));
988
+ } else if (data && data.resources) {
989
+ console.log(LOG_PREFIX, "direct fetch:", data.resources.length, "resources");
990
+ allResources = data.resources;
991
+ lastCursor = data.next_cursor || null;
992
+ render(); attachEvents();
993
+ } else {
994
+ showPersistentError("No Data", "Server returned no assets.");
995
+ }
996
+ } catch (e) {
997
+ showPersistentError("Fetch Failed", e && e.message ? e.message : String(e));
998
+ }
999
+ }
1000
+
1001
+ async function loadMore() {
1002
+ if (!lastCursor) return;
1003
+ var name = pendingCall.name || "list-images";
1004
+ var args = name === "search-assets"
1005
+ ? { request: { next_cursor: lastCursor } }
1006
+ : { next_cursor: lastCursor };
1007
+
1008
+ var btn = document.getElementById("load-more-btn");
1009
+ if (btn) { btn.textContent = "Loading\\u2026"; btn.disabled = true; }
1010
+
1011
+ try {
1012
+ var res = await app.callServerTool({ name: name, arguments: args });
1013
+ var data = ingestResult(res);
1014
+ if (data && data.resources) {
1015
+ allResources = allResources.concat(data.resources);
1016
+ lastCursor = data.next_cursor || null;
1017
+ render(); attachEvents();
1018
+ } else {
1019
+ showError("No Data", "Server returned no additional assets.");
1020
+ }
1021
+ } catch (e) {
1022
+ showError("Load More Failed", e && e.message ? e.message : String(e));
1023
+ if (btn) { btn.textContent = "Load More"; btn.disabled = false; }
1024
+ }
1025
+ }
1026
+
1027
+ function refreshGallery() {
1028
+ allResources = [];
1029
+ lastCursor = null;
1030
+ fetchDirect();
1031
+ }
1032
+
1033
+
1034
+ document.addEventListener("keydown", function(e) {
1035
+ if (e.key === "Escape") {
1036
+ if (document.querySelector(".modal-overlay")) { closeModal(); return; }
1037
+ if (filterQuery || aspectFilter) { clearFilter(); return; }
1038
+ if (selected.size > 0) { clearSelection(); return; }
1039
+ }
1040
+ });
1041
+
1042
+ app.connect().then(function() {
1043
+ console.log(LOG_PREFIX, "ready");
1044
+ setupResize(app, MIN_HEIGHT);
1045
+ }).catch(function(err) {
1046
+ console.warn(LOG_PREFIX, "connect failed:", err && err.message ? err.message : String(err));
1047
+ });
1048
+
1049
+ `;
1050
+
1051
+ const ASSET_GALLERY_HTML = /* html */ `<!DOCTYPE html>
1052
+ <html lang="en">
1053
+ <head>
1054
+ <meta charset="UTF-8">
1055
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1056
+ <title>Cloudinary Asset Gallery</title>
1057
+ <style>
1058
+ ${SHARED_CSS_TOKENS}
1059
+ ${SHARED_CSS_COMPONENTS}
1060
+ ${GALLERY_CSS}
1061
+ </style>
1062
+ </head>
1063
+ <body>
1064
+ <div id="app"><div class="status">Loading assets&hellip;</div></div>
1065
+ <div class="gallery-toast" id="gallery-toast"></div>
1066
+
1067
+ <script>
1068
+ ${SHARED_JS_MCP_CLIENT}
1069
+ ${SHARED_JS_HELPERS}
1070
+ ${SHARED_JS_TOOLTIPS}
1071
+ ${SHARED_JS_MODAL}
1072
+ ${SHARED_JS_DETAIL_RENDERERS}
1073
+ ${SHARED_JS_HOST_CONTEXT}
1074
+ ${GALLERY_JS}
1075
+ </script>
1076
+ </body>
1077
+ </html>`;