@productmaker/mcp 2.4.0 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2805 @@
1
+ <!doctype html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="color-scheme" content="light dark" />
7
+ <title>Carga segura para Meta · ProductMaker</title>
8
+ <style>
9
+ :root {
10
+ color-scheme: light dark;
11
+ font-family:
12
+ Inter,
13
+ ui-sans-serif,
14
+ system-ui,
15
+ -apple-system,
16
+ BlinkMacSystemFont,
17
+ "Segoe UI",
18
+ sans-serif;
19
+ --pm-bg: #f6f8fb;
20
+ --pm-card: #ffffff;
21
+ --pm-card-muted: #f0f4f8;
22
+ --pm-text: #172033;
23
+ --pm-muted: #667085;
24
+ --pm-line: #d7deea;
25
+ --pm-primary: #5b4ef5;
26
+ --pm-primary-strong: #4638dc;
27
+ --pm-primary-soft: #eeecff;
28
+ --pm-success: #087a55;
29
+ --pm-success-soft: #e9f9f2;
30
+ --pm-danger: #b42318;
31
+ --pm-danger-soft: #fff0ee;
32
+ --pm-warning: #8a4b08;
33
+ --pm-warning-soft: #fff6e5;
34
+ --pm-shadow: 0 18px 45px rgb(27 36 66 / 10%);
35
+ }
36
+
37
+ @media (prefers-color-scheme: dark) {
38
+ :root {
39
+ --pm-bg: #0f1320;
40
+ --pm-card: #171c2b;
41
+ --pm-card-muted: #202638;
42
+ --pm-text: #f3f5fb;
43
+ --pm-muted: #a8b0c2;
44
+ --pm-line: #343c50;
45
+ --pm-primary: #8c83ff;
46
+ --pm-primary-strong: #a59eff;
47
+ --pm-primary-soft: #292552;
48
+ --pm-success: #5ee1ae;
49
+ --pm-success-soft: #153c32;
50
+ --pm-danger: #ff8b83;
51
+ --pm-danger-soft: #482624;
52
+ --pm-warning: #ffc46b;
53
+ --pm-warning-soft: #49361d;
54
+ --pm-shadow: 0 18px 45px rgb(0 0 0 / 28%);
55
+ }
56
+ }
57
+
58
+ * {
59
+ box-sizing: border-box;
60
+ }
61
+
62
+ body {
63
+ margin: 0;
64
+ background: var(--pm-bg);
65
+ color: var(--pm-text);
66
+ }
67
+
68
+ button,
69
+ input,
70
+ select,
71
+ textarea {
72
+ font: inherit;
73
+ }
74
+
75
+ button,
76
+ select,
77
+ a {
78
+ -webkit-tap-highlight-color: transparent;
79
+ }
80
+
81
+ button:focus-visible,
82
+ select:focus-visible,
83
+ textarea:focus-visible,
84
+ a:focus-visible,
85
+ [tabindex]:focus-visible {
86
+ outline: 3px solid
87
+ color-mix(in srgb, var(--pm-primary) 35%, transparent);
88
+ outline-offset: 2px;
89
+ }
90
+
91
+ .app {
92
+ width: min(100%, 760px);
93
+ margin: 0 auto;
94
+ padding: 12px;
95
+ }
96
+
97
+ .card {
98
+ overflow: hidden;
99
+ border: 1px solid var(--pm-line);
100
+ border-radius: 26px;
101
+ background: var(--pm-card);
102
+ box-shadow: var(--pm-shadow);
103
+ }
104
+
105
+ .hero {
106
+ padding: 19px 22px 14px;
107
+ border-bottom: 1px solid
108
+ color-mix(in srgb, var(--pm-line) 65%, transparent);
109
+ background:
110
+ radial-gradient(
111
+ circle at 100% 0%,
112
+ var(--pm-primary-soft),
113
+ transparent 48%
114
+ ),
115
+ var(--pm-card);
116
+ }
117
+
118
+ .upload-header {
119
+ display: flex;
120
+ align-items: center;
121
+ justify-content: space-between;
122
+ gap: 12px;
123
+ }
124
+
125
+ .upload-title {
126
+ min-width: 0;
127
+ margin: 0;
128
+ overflow: hidden;
129
+ font-size: clamp(17px, 3vw, 24px);
130
+ font-weight: 760;
131
+ letter-spacing: -0.025em;
132
+ text-overflow: ellipsis;
133
+ white-space: nowrap;
134
+ }
135
+
136
+ .brand-mark {
137
+ display: grid;
138
+ width: 34px;
139
+ height: 34px;
140
+ flex: 0 0 auto;
141
+ place-items: center;
142
+ border: 1px solid
143
+ color-mix(in srgb, var(--pm-primary) 20%, var(--pm-line));
144
+ border-radius: 50%;
145
+ background: color-mix(
146
+ in srgb,
147
+ var(--pm-primary-soft) 70%,
148
+ var(--pm-card)
149
+ );
150
+ color: var(--pm-primary-strong);
151
+ font-size: 20px;
152
+ font-weight: 800;
153
+ line-height: 1;
154
+ }
155
+
156
+ .session-row {
157
+ display: flex;
158
+ min-height: 0;
159
+ align-items: center;
160
+ gap: 10px;
161
+ margin-top: 8px;
162
+ }
163
+
164
+ .session-id {
165
+ overflow: hidden;
166
+ max-width: 100%;
167
+ color: var(--pm-muted);
168
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
169
+ font-size: 10px;
170
+ text-overflow: ellipsis;
171
+ white-space: nowrap;
172
+ }
173
+
174
+ .fallback-link {
175
+ color: var(--pm-primary-strong);
176
+ font-size: 12px;
177
+ font-weight: 760;
178
+ text-decoration-thickness: 1px;
179
+ text-underline-offset: 3px;
180
+ }
181
+
182
+ .status {
183
+ display: none;
184
+ margin: 10px 0 0;
185
+ padding: 9px 11px;
186
+ border-radius: 11px;
187
+ color: var(--pm-muted);
188
+ font-size: 12px;
189
+ line-height: 1.4;
190
+ }
191
+
192
+ .status[data-tone="success"] {
193
+ display: block;
194
+ background: var(--pm-success-soft);
195
+ color: var(--pm-success);
196
+ }
197
+
198
+ .status[data-tone="warning"] {
199
+ display: block;
200
+ background: var(--pm-warning-soft);
201
+ color: var(--pm-warning);
202
+ }
203
+
204
+ .status[data-tone="danger"] {
205
+ display: block;
206
+ background: var(--pm-danger-soft);
207
+ color: var(--pm-danger);
208
+ }
209
+
210
+ .status[data-tone="neutral"] {
211
+ display: none;
212
+ }
213
+
214
+ .section {
215
+ padding: 12px 22px 22px;
216
+ }
217
+
218
+ .section + .section {
219
+ border-top: 1px solid var(--pm-line);
220
+ }
221
+
222
+ .dropzone {
223
+ display: grid;
224
+ min-height: 260px;
225
+ place-items: center;
226
+ padding: 20px;
227
+ border: 2px dashed var(--pm-line);
228
+ border-radius: 21px;
229
+ background: color-mix(
230
+ in srgb,
231
+ var(--pm-card-muted) 62%,
232
+ var(--pm-card)
233
+ );
234
+ text-align: center;
235
+ transition:
236
+ border-color 160ms ease,
237
+ background 160ms ease,
238
+ transform 160ms ease;
239
+ }
240
+
241
+ .dropzone[data-dragging="true"] {
242
+ border-color: var(--pm-primary);
243
+ background: var(--pm-primary-soft);
244
+ transform: translateY(-1px);
245
+ }
246
+
247
+ .dropzone[aria-disabled="true"] {
248
+ cursor: not-allowed;
249
+ opacity: 0.65;
250
+ }
251
+
252
+ .drop-trigger {
253
+ display: grid;
254
+ width: min(100%, 280px);
255
+ min-height: 190px;
256
+ place-items: center;
257
+ align-content: center;
258
+ gap: 16px;
259
+ border: 0;
260
+ border-radius: 18px;
261
+ background: transparent;
262
+ color: var(--pm-text);
263
+ cursor: pointer;
264
+ }
265
+
266
+ .drop-trigger:disabled {
267
+ cursor: not-allowed;
268
+ }
269
+
270
+ .drop-icon {
271
+ display: grid;
272
+ width: 58px;
273
+ height: 58px;
274
+ place-items: center;
275
+ border: 1px solid var(--pm-line);
276
+ border-radius: 50%;
277
+ background: var(--pm-card);
278
+ box-shadow: 0 7px 20px rgb(27 36 66 / 8%);
279
+ color: var(--pm-text);
280
+ font-size: 31px;
281
+ font-weight: 350;
282
+ line-height: 1;
283
+ }
284
+
285
+ .drop-title {
286
+ margin: 0;
287
+ font-size: 18px;
288
+ font-weight: 520;
289
+ letter-spacing: -0.02em;
290
+ }
291
+
292
+ .drop-help {
293
+ position: absolute;
294
+ width: 1px;
295
+ height: 1px;
296
+ overflow: hidden;
297
+ clip: rect(0 0 0 0);
298
+ clip-path: inset(50%);
299
+ white-space: nowrap;
300
+ }
301
+
302
+ .native-input {
303
+ position: absolute;
304
+ width: 1px;
305
+ height: 1px;
306
+ overflow: hidden;
307
+ clip: rect(0 0 0 0);
308
+ white-space: nowrap;
309
+ clip-path: inset(50%);
310
+ }
311
+
312
+ .button {
313
+ display: inline-flex;
314
+ min-height: 42px;
315
+ align-items: center;
316
+ justify-content: center;
317
+ gap: 8px;
318
+ padding: 10px 16px;
319
+ border: 1px solid transparent;
320
+ border-radius: 12px;
321
+ background: var(--pm-primary);
322
+ color: #ffffff;
323
+ cursor: pointer;
324
+ font-size: 13px;
325
+ font-weight: 780;
326
+ line-height: 1;
327
+ transition:
328
+ background 140ms ease,
329
+ opacity 140ms ease,
330
+ transform 140ms ease;
331
+ }
332
+
333
+ .button:hover:not(:disabled) {
334
+ background: var(--pm-primary-strong);
335
+ transform: translateY(-1px);
336
+ }
337
+
338
+ .button:disabled {
339
+ cursor: not-allowed;
340
+ opacity: 0.5;
341
+ }
342
+
343
+ .button.secondary {
344
+ border-color: var(--pm-line);
345
+ background: var(--pm-card);
346
+ color: var(--pm-text);
347
+ }
348
+
349
+ .button.secondary:hover:not(:disabled) {
350
+ background: var(--pm-card-muted);
351
+ }
352
+
353
+ .batch-error {
354
+ display: none;
355
+ margin: 10px 0 0;
356
+ color: var(--pm-danger);
357
+ font-size: 12px;
358
+ }
359
+
360
+ .batch-error:not(:empty) {
361
+ display: block;
362
+ }
363
+
364
+ .file-list {
365
+ display: grid;
366
+ grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
367
+ gap: 12px;
368
+ margin-top: 14px;
369
+ }
370
+
371
+ .file-card {
372
+ min-width: 0;
373
+ background: transparent;
374
+ }
375
+
376
+ .file-preview {
377
+ position: relative;
378
+ aspect-ratio: 1;
379
+ overflow: hidden;
380
+ border: 1px solid var(--pm-line);
381
+ border-radius: 16px;
382
+ background: var(--pm-card-muted);
383
+ }
384
+
385
+ .file-preview img,
386
+ .video-preview {
387
+ display: block;
388
+ width: 100%;
389
+ height: 100%;
390
+ object-fit: cover;
391
+ }
392
+
393
+ .video-preview {
394
+ display: grid;
395
+ place-items: center;
396
+ background: linear-gradient(145deg, #242a3d, #515c78);
397
+ color: #ffffff;
398
+ font-size: 37px;
399
+ }
400
+
401
+ .file-kind {
402
+ display: grid;
403
+ width: 100%;
404
+ height: 100%;
405
+ place-items: center;
406
+ color: var(--pm-muted);
407
+ font-size: 34px;
408
+ }
409
+
410
+ .file-head {
411
+ min-width: 0;
412
+ margin-top: 7px;
413
+ }
414
+
415
+ .file-label {
416
+ margin: 0;
417
+ overflow: hidden;
418
+ font-size: 12px;
419
+ font-weight: 700;
420
+ text-overflow: ellipsis;
421
+ white-space: nowrap;
422
+ }
423
+
424
+ .file-state {
425
+ margin: 3px 0 0;
426
+ overflow: hidden;
427
+ color: var(--pm-muted);
428
+ font-size: 11px;
429
+ line-height: 1.35;
430
+ text-overflow: ellipsis;
431
+ white-space: nowrap;
432
+ }
433
+
434
+ .file-state.error {
435
+ color: var(--pm-danger);
436
+ white-space: normal;
437
+ }
438
+
439
+ .overlay-button {
440
+ position: absolute;
441
+ z-index: 1;
442
+ border: 0;
443
+ cursor: pointer;
444
+ }
445
+
446
+ .remove-overlay {
447
+ top: 9px;
448
+ right: 9px;
449
+ display: grid;
450
+ width: 30px;
451
+ height: 30px;
452
+ place-items: center;
453
+ border: 1px solid rgb(23 32 51 / 12%);
454
+ border-radius: 50%;
455
+ background: rgb(255 255 255 / 90%);
456
+ color: #172033;
457
+ font-size: 23px;
458
+ font-weight: 350;
459
+ line-height: 1;
460
+ }
461
+
462
+ .remove-overlay:disabled {
463
+ cursor: not-allowed;
464
+ opacity: 0.72;
465
+ }
466
+
467
+ .retry-overlay {
468
+ top: 50%;
469
+ left: 50%;
470
+ padding: 10px 14px;
471
+ border: 1px solid rgb(255 255 255 / 76%);
472
+ border-radius: 11px;
473
+ background: rgb(255 255 255 / 94%);
474
+ box-shadow: 0 8px 24px rgb(27 36 66 / 16%);
475
+ color: var(--pm-text);
476
+ font-size: 12px;
477
+ font-weight: 780;
478
+ transform: translate(-50%, -50%);
479
+ }
480
+
481
+ .retry-overlay:hover {
482
+ background: var(--pm-primary-soft);
483
+ }
484
+
485
+ .progress {
486
+ position: absolute;
487
+ right: 10px;
488
+ bottom: 10px;
489
+ left: 10px;
490
+ height: 6px;
491
+ overflow: hidden;
492
+ border-radius: 999px;
493
+ background: rgb(255 255 255 / 75%);
494
+ }
495
+
496
+ .progress-bar {
497
+ width: 0;
498
+ height: 100%;
499
+ border-radius: inherit;
500
+ background: var(--pm-primary);
501
+ transition: width 120ms linear;
502
+ }
503
+
504
+ .destination-grid {
505
+ display: grid;
506
+ grid-template-columns: repeat(3, minmax(0, 1fr));
507
+ gap: 9px;
508
+ }
509
+
510
+ .field {
511
+ display: grid;
512
+ }
513
+
514
+ select,
515
+ textarea {
516
+ width: 100%;
517
+ border: 1px solid var(--pm-line);
518
+ border-radius: 11px;
519
+ background: var(--pm-card);
520
+ color: var(--pm-text);
521
+ }
522
+
523
+ select {
524
+ min-height: 42px;
525
+ padding: 8px 30px 8px 10px;
526
+ font-size: 12px;
527
+ }
528
+
529
+ textarea {
530
+ min-height: 135px;
531
+ padding: 12px;
532
+ resize: vertical;
533
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
534
+ font-size: 11px;
535
+ line-height: 1.5;
536
+ }
537
+
538
+ .destination-actions,
539
+ .handoff-actions {
540
+ display: flex;
541
+ flex-wrap: wrap;
542
+ gap: 10px;
543
+ margin-top: 12px;
544
+ }
545
+
546
+ .compatibility-control {
547
+ display: none !important;
548
+ }
549
+
550
+ .sr-only {
551
+ position: absolute;
552
+ width: 1px;
553
+ height: 1px;
554
+ padding: 0;
555
+ overflow: hidden;
556
+ clip: rect(0, 0, 0, 0);
557
+ white-space: nowrap;
558
+ border: 0;
559
+ }
560
+
561
+ [hidden] {
562
+ display: none !important;
563
+ }
564
+
565
+ @media (max-width: 640px) {
566
+ .app {
567
+ padding: 8px;
568
+ }
569
+
570
+ .card {
571
+ border-radius: 20px;
572
+ }
573
+
574
+ .hero,
575
+ .section {
576
+ padding-right: 14px;
577
+ padding-left: 14px;
578
+ }
579
+
580
+ .dropzone {
581
+ min-height: 230px;
582
+ }
583
+
584
+ .destination-grid {
585
+ grid-template-columns: 1fr;
586
+ }
587
+
588
+ .destination-actions .button,
589
+ .handoff-actions .button {
590
+ width: 100%;
591
+ }
592
+ }
593
+
594
+ @media (prefers-reduced-motion: reduce) {
595
+ *,
596
+ *::before,
597
+ *::after {
598
+ scroll-behavior: auto !important;
599
+ transition: none !important;
600
+ }
601
+ }
602
+ </style>
603
+ </head>
604
+ <body>
605
+ <main class="app" data-testid="meta-upload-root">
606
+ <article class="card">
607
+ <header class="hero">
608
+ <div class="upload-header">
609
+ <p class="upload-title" data-testid="meta-upload-title">
610
+ Upload image
611
+ </p>
612
+ <span class="brand-mark" aria-label="ProductMaker" role="img"
613
+ >↗</span
614
+ >
615
+ </div>
616
+ <div class="session-row">
617
+ <code class="session-id" data-testid="meta-upload-session-id" hidden
618
+ >Esperando sesión…</code
619
+ >
620
+ <a
621
+ class="fallback-link"
622
+ data-testid="meta-upload-fallback-link"
623
+ href="#"
624
+ target="_blank"
625
+ rel="noopener noreferrer"
626
+ hidden
627
+ >Abrir carga segura en otra pestaña</a
628
+ >
629
+ </div>
630
+ <p
631
+ class="status"
632
+ data-testid="meta-upload-session-status"
633
+ aria-live="polite"
634
+ ></p>
635
+ </header>
636
+
637
+ <section class="section" aria-label="Upload media">
638
+ <div
639
+ class="dropzone"
640
+ data-testid="meta-upload-dropzone"
641
+ data-dragging="false"
642
+ aria-disabled="true"
643
+ aria-describedby="meta-upload-constraints"
644
+ >
645
+ <button
646
+ class="drop-trigger"
647
+ type="button"
648
+ data-testid="meta-upload-picker-btn"
649
+ disabled
650
+ >
651
+ <span class="drop-icon" aria-hidden="true">+</span>
652
+ <span class="drop-title" data-testid="meta-upload-drop-title"
653
+ >Upload image</span
654
+ >
655
+ </button>
656
+ <button
657
+ class="compatibility-control"
658
+ type="button"
659
+ data-testid="meta-upload-library-btn"
660
+ hidden
661
+ disabled
662
+ aria-label="Choose from ChatGPT library"
663
+ ></button>
664
+ <input
665
+ class="native-input"
666
+ data-testid="meta-upload-input"
667
+ type="file"
668
+ accept="image/jpeg,image/png,image/webp,video/mp4,video/quicktime,video/webm"
669
+ multiple
670
+ disabled
671
+ />
672
+ </div>
673
+ <p class="drop-help" id="meta-upload-constraints">
674
+ JPEG, PNG, WebP, MP4, MOV o WebM. Hasta 10 archivos de 350 MB; las
675
+ imágenes grandes se comprimen y los videos no se transcodifican.
676
+ </p>
677
+ <p
678
+ class="batch-error"
679
+ data-testid="meta-upload-batch-error"
680
+ role="alert"
681
+ aria-live="assertive"
682
+ ></p>
683
+ <div
684
+ class="file-list"
685
+ data-testid="meta-upload-file-list"
686
+ aria-live="polite"
687
+ ></div>
688
+ <button
689
+ class="compatibility-control"
690
+ type="button"
691
+ data-testid="meta-upload-pending-refresh"
692
+ hidden
693
+ aria-label="Refresh upload status"
694
+ ></button>
695
+ </section>
696
+
697
+ <section
698
+ class="section"
699
+ data-testid="meta-upload-destination-section"
700
+ aria-label="Meta destination"
701
+ hidden
702
+ >
703
+ <div class="destination-grid">
704
+ <div class="field">
705
+ <label class="sr-only" for="portfolio-select">Portfolio</label>
706
+ <select
707
+ id="portfolio-select"
708
+ data-testid="meta-upload-portfolio-select"
709
+ >
710
+ <option value="">Portfolio</option>
711
+ </select>
712
+ </div>
713
+ <div class="field">
714
+ <label class="sr-only" for="ad-account-select">Ad account</label>
715
+ <select
716
+ id="ad-account-select"
717
+ data-testid="meta-upload-ad-account-select"
718
+ disabled
719
+ >
720
+ <option value="">Ad account</option>
721
+ </select>
722
+ </div>
723
+ <div class="field">
724
+ <label class="sr-only" for="page-select">Facebook Page</label>
725
+ <select id="page-select" data-testid="meta-upload-page-select">
726
+ <option value="">Facebook Page</option>
727
+ </select>
728
+ </div>
729
+ </div>
730
+ <div class="destination-actions">
731
+ <button
732
+ class="button"
733
+ type="button"
734
+ data-testid="meta-upload-submit-destination"
735
+ disabled
736
+ >
737
+ Continue
738
+ </button>
739
+ <button
740
+ class="compatibility-control"
741
+ type="button"
742
+ data-testid="meta-upload-check-status"
743
+ hidden
744
+ aria-label="Refresh upload status"
745
+ ></button>
746
+ </div>
747
+ </section>
748
+
749
+ <section
750
+ class="section"
751
+ data-testid="meta-upload-handoff-section"
752
+ aria-labelledby="handoff-heading"
753
+ hidden
754
+ >
755
+ <p id="handoff-heading" class="section-copy muted">
756
+ Copy this handoff into the chat.
757
+ </p>
758
+ <textarea data-testid="meta-upload-handoff-copy" readonly></textarea>
759
+ <div class="handoff-actions">
760
+ <button
761
+ class="button secondary"
762
+ type="button"
763
+ data-testid="meta-upload-copy-btn"
764
+ >
765
+ Copy handoff
766
+ </button>
767
+ </div>
768
+ </section>
769
+ </article>
770
+ </main>
771
+
772
+ <script>
773
+ (() => {
774
+ "use strict";
775
+
776
+ const ACCEPTED_TYPES = new Set([
777
+ "image/jpeg",
778
+ "image/png",
779
+ "image/webp",
780
+ "video/mp4",
781
+ "video/quicktime",
782
+ "video/webm",
783
+ ]);
784
+ const IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp"]);
785
+ const VIDEO_TYPES = new Set([
786
+ "video/mp4",
787
+ "video/quicktime",
788
+ "video/webm",
789
+ ]);
790
+ const MAX_FILES = 10;
791
+ const MAX_FILE_SIZE_BYTES = 350 * 1024 * 1024;
792
+ const MAX_VIDEO_DURATION_SECONDS = 180;
793
+ const MIN_DIMENSION = 600;
794
+ const IMAGE_COMPRESSION_THRESHOLD_BYTES = 1.8 * 1024 * 1024;
795
+ const IMAGE_COMPRESSION_MAX_BYTES = 1.8 * 1024 * 1024;
796
+ const IMAGE_MAX_DIMENSION = 2048;
797
+ const MAX_UPLOAD_ATTEMPTS = 3;
798
+ const TOOL_RESULT_TEXT_LIMIT = 100000;
799
+ const RPC_TIMEOUT_MS = 15000;
800
+ const APP_PROTOCOL_VERSION = "2026-01-26";
801
+
802
+ const elements = {
803
+ root: document.querySelector('[data-testid="meta-upload-root"]'),
804
+ title: document.querySelector('[data-testid="meta-upload-title"]'),
805
+ sessionId: document.querySelector(
806
+ '[data-testid="meta-upload-session-id"]',
807
+ ),
808
+ fallbackLink: document.querySelector(
809
+ '[data-testid="meta-upload-fallback-link"]',
810
+ ),
811
+ sessionStatus: document.querySelector(
812
+ '[data-testid="meta-upload-session-status"]',
813
+ ),
814
+ dropzone: document.querySelector(
815
+ '[data-testid="meta-upload-dropzone"]',
816
+ ),
817
+ dropTitle: document.querySelector(
818
+ '[data-testid="meta-upload-drop-title"]',
819
+ ),
820
+ pickerButton: document.querySelector(
821
+ '[data-testid="meta-upload-picker-btn"]',
822
+ ),
823
+ libraryPickerButton: document.querySelector(
824
+ '[data-testid="meta-upload-library-btn"]',
825
+ ),
826
+ input: document.querySelector('[data-testid="meta-upload-input"]'),
827
+ batchError: document.querySelector(
828
+ '[data-testid="meta-upload-batch-error"]',
829
+ ),
830
+ fileList: document.querySelector(
831
+ '[data-testid="meta-upload-file-list"]',
832
+ ),
833
+ destinationSection: document.querySelector(
834
+ '[data-testid="meta-upload-destination-section"]',
835
+ ),
836
+ portfolio: document.querySelector(
837
+ '[data-testid="meta-upload-portfolio-select"]',
838
+ ),
839
+ adAccount: document.querySelector(
840
+ '[data-testid="meta-upload-ad-account-select"]',
841
+ ),
842
+ page: document.querySelector(
843
+ '[data-testid="meta-upload-page-select"]',
844
+ ),
845
+ submitDestination: document.querySelector(
846
+ '[data-testid="meta-upload-submit-destination"]',
847
+ ),
848
+ checkStatus: document.querySelector(
849
+ '[data-testid="meta-upload-check-status"]',
850
+ ),
851
+ pendingRefresh: document.querySelector(
852
+ '[data-testid="meta-upload-pending-refresh"]',
853
+ ),
854
+ handoffSection: document.querySelector(
855
+ '[data-testid="meta-upload-handoff-section"]',
856
+ ),
857
+ handoffCopy: document.querySelector(
858
+ '[data-testid="meta-upload-handoff-copy"]',
859
+ ),
860
+ copyButton: document.querySelector(
861
+ '[data-testid="meta-upload-copy-btn"]',
862
+ ),
863
+ };
864
+
865
+ const state = {
866
+ disposed: false,
867
+ initialized: false,
868
+ standardHostReady: false,
869
+ hostCanMessage: false,
870
+ hostCanServerTools: false,
871
+ hostContainerDimensions: null,
872
+ rpcId: 0,
873
+ pendingRpc: new Map(),
874
+ pendingSizeReport: false,
875
+ forceSizeReport: false,
876
+ lastReportedHeight: 0,
877
+ session: null,
878
+ toolInput: {},
879
+ items: [],
880
+ assets: [],
881
+ destinations: { portfolios: [], adAccounts: [], pages: [] },
882
+ consumedBridgeIds: new Set(),
883
+ consumingBridgeFiles: false,
884
+ bridgeConsumptionQueued: false,
885
+ ingestionQueue: Promise.resolve(),
886
+ busyBatch: false,
887
+ uploadBlocked: false,
888
+ fallbackRequired: false,
889
+ workCancelled: false,
890
+ abortController: null,
891
+ activeXhrs: new Set(),
892
+ resizeObserver: null,
893
+ mutationObserver: null,
894
+ resizeHandler: null,
895
+ hostMessageHandler: null,
896
+ };
897
+
898
+ function isRecord(value) {
899
+ return (
900
+ value !== null && typeof value === "object" && !Array.isArray(value)
901
+ );
902
+ }
903
+
904
+ function capabilityEnabled(value) {
905
+ return value === true || isRecord(value);
906
+ }
907
+
908
+ function boundedString(value, maxLength = 300) {
909
+ return typeof value === "string" && value.length > 0
910
+ ? value.slice(0, maxLength)
911
+ : "";
912
+ }
913
+
914
+ function openAiBridge() {
915
+ return isRecord(window.openai) ? window.openai : null;
916
+ }
917
+
918
+ function hasOpenAiFunction(name) {
919
+ const bridge = openAiBridge();
920
+ return Boolean(bridge && typeof bridge[name] === "function");
921
+ }
922
+
923
+ function mediaKindForType(type) {
924
+ if (IMAGE_TYPES.has(type)) return "image";
925
+ if (VIDEO_TYPES.has(type)) return "video";
926
+ return null;
927
+ }
928
+
929
+ function defaultUploadTitle() {
930
+ return state.session?.mediaKind === "video"
931
+ ? "Upload video"
932
+ : "Upload image";
933
+ }
934
+
935
+ function updateUploadTitle() {
936
+ const namedItems = state.items.filter(
937
+ (item) =>
938
+ (item.file && typeof item.file.name === "string") ||
939
+ (typeof item.displayName === "string" &&
940
+ item.displayName.length > 0),
941
+ );
942
+ const title =
943
+ namedItems.length === 1
944
+ ? namedItems[0].file?.name || namedItems[0].displayName
945
+ : namedItems.length > 1
946
+ ? `${namedItems.length} files`
947
+ : defaultUploadTitle();
948
+ elements.title.textContent = title;
949
+ elements.dropTitle.textContent = defaultUploadTitle();
950
+ }
951
+
952
+ function safeSessionLimit() {
953
+ const proposed =
954
+ state.session && Number.isInteger(state.session.maxFiles)
955
+ ? state.session.maxFiles
956
+ : MAX_FILES;
957
+ return Math.max(1, Math.min(MAX_FILES, proposed));
958
+ }
959
+
960
+ function sessionExpired() {
961
+ if (!state.session) return false;
962
+ const expiresAt = Date.parse(state.session.expiresAt);
963
+ return !Number.isFinite(expiresAt) || expiresAt <= Date.now();
964
+ }
965
+
966
+ function setSessionStatus(message, tone = "neutral") {
967
+ elements.sessionStatus.textContent = message;
968
+ elements.sessionStatus.dataset.tone = tone;
969
+ scheduleIntrinsicSizeReport(true);
970
+ }
971
+
972
+ function setBatchError(message) {
973
+ elements.batchError.textContent = message;
974
+ scheduleIntrinsicSizeReport(true);
975
+ }
976
+
977
+ function parseFinitePositiveNumber(value) {
978
+ return typeof value === "number" &&
979
+ Number.isFinite(value) &&
980
+ value > 0
981
+ ? value
982
+ : null;
983
+ }
984
+
985
+ function parseHostContainerDimensions(value) {
986
+ if (!isRecord(value)) return null;
987
+ const height = parseFinitePositiveNumber(value.height);
988
+ const maxHeight = parseFinitePositiveNumber(value.maxHeight);
989
+ if (!height && !maxHeight) return null;
990
+ return { height, maxHeight };
991
+ }
992
+
993
+ function boundedPositiveHeight(value) {
994
+ const parsed = parseFinitePositiveNumber(value);
995
+ return parsed ? Math.max(1, Math.round(parsed)) : null;
996
+ }
997
+
998
+ function intrinsicHeightLimit() {
999
+ const dimensions = state.hostContainerDimensions;
1000
+ if (!dimensions) return null;
1001
+ return boundedPositiveHeight(
1002
+ dimensions.height || dimensions.maxHeight,
1003
+ );
1004
+ }
1005
+
1006
+ function measureIntrinsicWidth() {
1007
+ const candidates = [
1008
+ elements.root && elements.root.scrollWidth,
1009
+ document.body && document.body.scrollWidth,
1010
+ document.documentElement && document.documentElement.scrollWidth,
1011
+ ];
1012
+ for (const candidate of candidates) {
1013
+ const parsed = boundedPositiveHeight(candidate);
1014
+ if (parsed) return parsed;
1015
+ }
1016
+ return 1;
1017
+ }
1018
+
1019
+ function applyHostContainerDimensions() {
1020
+ const dimensions = state.hostContainerDimensions;
1021
+ if (!dimensions) return;
1022
+ if (dimensions.height) {
1023
+ document.documentElement.style.height = "100vh";
1024
+ } else {
1025
+ document.documentElement.style.removeProperty("height");
1026
+ }
1027
+ if (dimensions.maxHeight) {
1028
+ document.documentElement.style.maxHeight = `${boundedPositiveHeight(dimensions.maxHeight)}px`;
1029
+ } else {
1030
+ document.documentElement.style.removeProperty("max-height");
1031
+ }
1032
+ document.documentElement.style.overflowY = "auto";
1033
+ scheduleIntrinsicSizeReport(true);
1034
+ }
1035
+
1036
+ function measureIntrinsicHeight() {
1037
+ const candidates = [
1038
+ elements.root && elements.root.scrollHeight,
1039
+ document.body && document.body.scrollHeight,
1040
+ document.documentElement && document.documentElement.scrollHeight,
1041
+ ];
1042
+ let measured = 1;
1043
+ for (const candidate of candidates) {
1044
+ const parsed = boundedPositiveHeight(candidate);
1045
+ if (parsed) {
1046
+ measured = parsed;
1047
+ break;
1048
+ }
1049
+ }
1050
+ const limit = intrinsicHeightLimit();
1051
+ return limit ? Math.min(measured, limit) : measured;
1052
+ }
1053
+
1054
+ function reportIntrinsicHeight(force = false) {
1055
+ state.pendingSizeReport = false;
1056
+ state.forceSizeReport = false;
1057
+ const height = measureIntrinsicHeight();
1058
+ if (!force && height === state.lastReportedHeight) return;
1059
+ state.lastReportedHeight = height;
1060
+ if (state.standardHostReady) {
1061
+ postNotification("ui/notifications/size-changed", {
1062
+ width: measureIntrinsicWidth(),
1063
+ height,
1064
+ });
1065
+ }
1066
+ const bridge = openAiBridge();
1067
+ if (bridge && typeof bridge.notifyIntrinsicHeight === "function") {
1068
+ void Promise.resolve(bridge.notifyIntrinsicHeight(height)).catch(
1069
+ () => {},
1070
+ );
1071
+ }
1072
+ }
1073
+
1074
+ function scheduleIntrinsicSizeReport(force = false) {
1075
+ state.forceSizeReport = state.forceSizeReport || force;
1076
+ if (state.pendingSizeReport) return;
1077
+ state.pendingSizeReport = true;
1078
+ const flush = () => reportIntrinsicHeight(state.forceSizeReport);
1079
+ if (typeof window.requestAnimationFrame === "function") {
1080
+ window.requestAnimationFrame(() => flush());
1081
+ return;
1082
+ }
1083
+ window.setTimeout(flush, 0);
1084
+ }
1085
+
1086
+ function installIntrinsicSizeObserver() {
1087
+ if (
1088
+ state.resizeObserver ||
1089
+ state.mutationObserver ||
1090
+ state.resizeHandler
1091
+ )
1092
+ return;
1093
+ if (typeof ResizeObserver === "function" && elements.root) {
1094
+ state.resizeObserver = new ResizeObserver(() =>
1095
+ scheduleIntrinsicSizeReport(),
1096
+ );
1097
+ state.resizeObserver.observe(elements.root);
1098
+ } else if (typeof MutationObserver === "function" && elements.root) {
1099
+ state.mutationObserver = new MutationObserver(() =>
1100
+ scheduleIntrinsicSizeReport(),
1101
+ );
1102
+ state.mutationObserver.observe(elements.root, {
1103
+ attributes: true,
1104
+ childList: true,
1105
+ subtree: true,
1106
+ characterData: true,
1107
+ });
1108
+ }
1109
+ state.resizeHandler = () => scheduleIntrinsicSizeReport();
1110
+ window.addEventListener("resize", state.resizeHandler);
1111
+ }
1112
+
1113
+ function setFallbackVisibility(visible) {
1114
+ elements.fallbackLink.hidden = !visible;
1115
+ scheduleIntrinsicSizeReport(true);
1116
+ }
1117
+
1118
+ function requireFallback(
1119
+ message,
1120
+ tone = "warning",
1121
+ blockPicker = false,
1122
+ ) {
1123
+ state.fallbackRequired = true;
1124
+ state.uploadBlocked = state.uploadBlocked || blockPicker;
1125
+ setFallbackVisibility(true);
1126
+ if (message) setSessionStatus(message, tone);
1127
+ setPickerAvailability();
1128
+ }
1129
+
1130
+ function enqueueIngestion(task) {
1131
+ const next = state.ingestionQueue.then(task, task);
1132
+ state.ingestionQueue = next.catch(() => {});
1133
+ return next;
1134
+ }
1135
+
1136
+ function activeAbortSignal() {
1137
+ if (!state.abortController || state.abortController.signal.aborted) {
1138
+ state.abortController =
1139
+ typeof AbortController === "function"
1140
+ ? new AbortController()
1141
+ : null;
1142
+ }
1143
+ return state.abortController?.signal;
1144
+ }
1145
+
1146
+ function abortActiveWork() {
1147
+ state.abortController?.abort();
1148
+ for (const request of state.activeXhrs) request.abort();
1149
+ state.activeXhrs.clear();
1150
+ }
1151
+
1152
+ function cleanup() {
1153
+ if (state.disposed) return;
1154
+ state.disposed = true;
1155
+ abortActiveWork();
1156
+ state.resizeObserver?.disconnect();
1157
+ state.mutationObserver?.disconnect();
1158
+ if (state.resizeHandler)
1159
+ window.removeEventListener("resize", state.resizeHandler);
1160
+ if (state.hostMessageHandler)
1161
+ window.removeEventListener("message", state.hostMessageHandler);
1162
+ for (const item of state.items) revokePreviewUrl(item);
1163
+ for (const [id, pending] of state.pendingRpc) {
1164
+ window.clearTimeout(pending.timeout);
1165
+ pending.reject(new Error("resource_teardown"));
1166
+ state.pendingRpc.delete(id);
1167
+ }
1168
+ }
1169
+
1170
+ function normalizedBridgeReferenceId(reference) {
1171
+ return isRecord(reference)
1172
+ ? boundedString(
1173
+ reference.file_id || reference.fileId || reference.id,
1174
+ 300,
1175
+ )
1176
+ : "";
1177
+ }
1178
+
1179
+ function normalizedBridgeDownloadUrl(result) {
1180
+ return typeof result === "string"
1181
+ ? result
1182
+ : isRecord(result)
1183
+ ? boundedString(
1184
+ result.download_url || result.downloadUrl || result.url,
1185
+ 3000,
1186
+ )
1187
+ : "";
1188
+ }
1189
+
1190
+ function setPickerAvailability() {
1191
+ const disabled =
1192
+ !state.session ||
1193
+ sessionExpired() ||
1194
+ state.uploadBlocked ||
1195
+ state.busyBatch ||
1196
+ state.items.length >= safeSessionLimit();
1197
+ elements.input.disabled = disabled;
1198
+ elements.pickerButton.disabled = disabled;
1199
+ elements.libraryPickerButton.disabled = disabled;
1200
+ elements.dropzone.setAttribute("aria-disabled", String(disabled));
1201
+ }
1202
+
1203
+ function emitAnalytics(eventName, mediaKind, extra = {}) {
1204
+ const safeExtra = {};
1205
+ if (typeof extra.failureStage === "string")
1206
+ safeExtra.failureStage = extra.failureStage.slice(0, 40);
1207
+ if (typeof extra.errorType === "string")
1208
+ safeExtra.errorType = extra.errorType.slice(0, 60);
1209
+ window.dispatchEvent(
1210
+ new CustomEvent("productmaker:analytics", {
1211
+ detail: {
1212
+ eventName,
1213
+ mediaKind,
1214
+ surface: "mcp_remote_upload",
1215
+ transport: "browser_to_cdn",
1216
+ ...safeExtra,
1217
+ },
1218
+ }),
1219
+ );
1220
+ }
1221
+
1222
+ function initialize() {
1223
+ if (state.initialized || state.disposed) return;
1224
+ state.initialized = true;
1225
+ bindUi();
1226
+ installIntrinsicSizeObserver();
1227
+ setPickerAvailability();
1228
+ scheduleIntrinsicSizeReport(true);
1229
+ void initializeStandardHost();
1230
+ const bridge = openAiBridge();
1231
+ elements.libraryPickerButton.hidden = !(
1232
+ bridge &&
1233
+ typeof bridge.selectFiles === "function" &&
1234
+ typeof bridge.getFileDownloadUrl === "function"
1235
+ );
1236
+ if (bridge && isRecord(bridge.toolInput)) {
1237
+ mergeToolInput(bridge.toolInput);
1238
+ }
1239
+ if (bridge && isRecord(bridge.toolOutput)) {
1240
+ applyToolResult({ structuredContent: bridge.toolOutput });
1241
+ }
1242
+ }
1243
+
1244
+ function bindUi() {
1245
+ elements.pickerButton.addEventListener("click", () =>
1246
+ elements.input.click(),
1247
+ );
1248
+ elements.libraryPickerButton.addEventListener(
1249
+ "click",
1250
+ () => void chooseLibraryFiles(),
1251
+ );
1252
+ elements.input.addEventListener("change", () => {
1253
+ const files = elements.input.files
1254
+ ? Array.from(elements.input.files)
1255
+ : [];
1256
+ elements.input.value = "";
1257
+ void processLocalFiles(files);
1258
+ });
1259
+
1260
+ for (const eventName of ["dragenter", "dragover"]) {
1261
+ elements.dropzone.addEventListener(eventName, (event) => {
1262
+ event.preventDefault();
1263
+ if (elements.dropzone.getAttribute("aria-disabled") === "false") {
1264
+ elements.dropzone.dataset.dragging = "true";
1265
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
1266
+ }
1267
+ });
1268
+ }
1269
+ for (const eventName of ["dragleave", "drop"]) {
1270
+ elements.dropzone.addEventListener(eventName, (event) => {
1271
+ event.preventDefault();
1272
+ elements.dropzone.dataset.dragging = "false";
1273
+ });
1274
+ }
1275
+ elements.dropzone.addEventListener("drop", (event) => {
1276
+ if (elements.dropzone.getAttribute("aria-disabled") === "true")
1277
+ return;
1278
+ const files = event.dataTransfer
1279
+ ? Array.from(event.dataTransfer.files)
1280
+ : [];
1281
+ void processLocalFiles(files);
1282
+ });
1283
+
1284
+ elements.portfolio.addEventListener("change", () => {
1285
+ renderAdAccounts();
1286
+ updateDestinationSubmit();
1287
+ });
1288
+ elements.adAccount.addEventListener(
1289
+ "change",
1290
+ updateDestinationSubmit,
1291
+ );
1292
+ elements.page.addEventListener("change", updateDestinationSubmit);
1293
+ elements.submitDestination.addEventListener(
1294
+ "click",
1295
+ () => void submitDestination(),
1296
+ );
1297
+ elements.checkStatus.addEventListener(
1298
+ "click",
1299
+ () => void refreshSessionStatus(),
1300
+ );
1301
+ elements.pendingRefresh.addEventListener(
1302
+ "click",
1303
+ () => void refreshSessionStatus(),
1304
+ );
1305
+ elements.copyButton.addEventListener(
1306
+ "click",
1307
+ () => void copyHandoff(),
1308
+ );
1309
+
1310
+ state.hostMessageHandler = handleHostMessage;
1311
+ window.addEventListener("message", state.hostMessageHandler);
1312
+ }
1313
+
1314
+ async function initializeStandardHost() {
1315
+ try {
1316
+ const result = await rpcRequest("ui/initialize", {
1317
+ appInfo: { name: "ProductMaker Meta upload", version: "1.0.0" },
1318
+ appCapabilities: {
1319
+ availableDisplayModes: ["inline", "fullscreen"],
1320
+ },
1321
+ protocolVersion: APP_PROTOCOL_VERSION,
1322
+ });
1323
+ state.standardHostReady = true;
1324
+ const capabilities =
1325
+ isRecord(result) && isRecord(result.hostCapabilities)
1326
+ ? result.hostCapabilities
1327
+ : {};
1328
+ const hostContext =
1329
+ isRecord(result) && isRecord(result.hostContext)
1330
+ ? result.hostContext
1331
+ : {};
1332
+ state.hostContainerDimensions = parseHostContainerDimensions(
1333
+ hostContext.containerDimensions,
1334
+ );
1335
+ applyHostContainerDimensions();
1336
+ state.hostCanMessage = capabilityEnabled(capabilities.message);
1337
+ state.hostCanServerTools = capabilityEnabled(
1338
+ capabilities.serverTools,
1339
+ );
1340
+ postNotification("ui/notifications/initialized", {});
1341
+ scheduleIntrinsicSizeReport(true);
1342
+ } catch {
1343
+ state.standardHostReady = false;
1344
+ state.hostCanServerTools = false;
1345
+ }
1346
+ }
1347
+
1348
+ function rpcRequest(method, params) {
1349
+ return new Promise((resolve, reject) => {
1350
+ if (state.disposed) {
1351
+ reject(new Error("resource_teardown"));
1352
+ return;
1353
+ }
1354
+ state.rpcId += 1;
1355
+ const id = state.rpcId;
1356
+ const timeout = window.setTimeout(() => {
1357
+ state.pendingRpc.delete(id);
1358
+ reject(new Error("host_timeout"));
1359
+ }, RPC_TIMEOUT_MS);
1360
+ state.pendingRpc.set(id, { resolve, reject, timeout });
1361
+ window.parent.postMessage(
1362
+ { jsonrpc: "2.0", id, method, params },
1363
+ "*",
1364
+ );
1365
+ });
1366
+ }
1367
+
1368
+ function postNotification(method, params) {
1369
+ if (state.disposed) return;
1370
+ window.parent.postMessage({ jsonrpc: "2.0", method, params }, "*");
1371
+ }
1372
+
1373
+ function handleHostMessage(event) {
1374
+ if (event.source !== window.parent || !isRecord(event.data)) return;
1375
+ const message = event.data;
1376
+ if (message.method === "ui/resource-teardown") {
1377
+ cleanup();
1378
+ if (Object.prototype.hasOwnProperty.call(message, "id")) {
1379
+ window.parent.postMessage(
1380
+ { jsonrpc: "2.0", id: message.id, result: {} },
1381
+ "*",
1382
+ );
1383
+ }
1384
+ return;
1385
+ }
1386
+ if (message.method === "ui/notifications/tool-cancelled") {
1387
+ state.workCancelled = true;
1388
+ abortActiveWork();
1389
+ requireFallback(
1390
+ "La carga fue cancelada por el host. Puedes continuar desde el enlace seguro.",
1391
+ "warning",
1392
+ true,
1393
+ );
1394
+ return;
1395
+ }
1396
+ if (message.method === "ui/notifications/host-context-changed") {
1397
+ const params = isRecord(message.params) ? message.params : {};
1398
+ const hostContext = isRecord(params.hostContext)
1399
+ ? params.hostContext
1400
+ : params;
1401
+ state.hostContainerDimensions = parseHostContainerDimensions(
1402
+ hostContext.containerDimensions,
1403
+ );
1404
+ applyHostContainerDimensions();
1405
+ return;
1406
+ }
1407
+ if (Object.prototype.hasOwnProperty.call(message, "id")) {
1408
+ const pending = state.pendingRpc.get(message.id);
1409
+ if (!pending) return;
1410
+ window.clearTimeout(pending.timeout);
1411
+ state.pendingRpc.delete(message.id);
1412
+ if (isRecord(message.error))
1413
+ pending.reject(new Error("host_error"));
1414
+ else pending.resolve(message.result);
1415
+ return;
1416
+ }
1417
+
1418
+ if (message.method === "ui/notifications/tool-result") {
1419
+ applyToolResult(isRecord(message.params) ? message.params : {});
1420
+ }
1421
+ if (message.method === "ui/notifications/tool-input") {
1422
+ const params = isRecord(message.params) ? message.params : {};
1423
+ mergeToolInput(
1424
+ isRecord(params.arguments) ? params.arguments : params,
1425
+ );
1426
+ }
1427
+ }
1428
+
1429
+ function applyToolResult(result) {
1430
+ const candidate = isRecord(result.structuredContent)
1431
+ ? result.structuredContent
1432
+ : isRecord(result)
1433
+ ? result
1434
+ : null;
1435
+ if (!candidate) return;
1436
+ const session = parseSession(candidate);
1437
+ if (!session) return;
1438
+ const sessionChanged =
1439
+ !state.session || state.session.sessionId !== session.sessionId;
1440
+ if (sessionChanged) {
1441
+ state.uploadBlocked = false;
1442
+ state.fallbackRequired = false;
1443
+ state.workCancelled = false;
1444
+ if (state.abortController?.signal.aborted) {
1445
+ state.abortController = null;
1446
+ }
1447
+ }
1448
+ state.session = session;
1449
+ elements.sessionId.textContent = session.sessionId;
1450
+ elements.fallbackLink.href = session.uploadPageUrl;
1451
+ updateUploadTitle();
1452
+ if (sessionExpired()) {
1453
+ requireFallback(
1454
+ "La sesión está expirada. Abre el enlace seguro para solicitar una nueva.",
1455
+ "danger",
1456
+ true,
1457
+ );
1458
+ elements.pendingRefresh.hidden = true;
1459
+ } else {
1460
+ setFallbackVisibility(state.fallbackRequired);
1461
+ setSessionStatus("Sesión lista para recibir archivos.", "neutral");
1462
+ elements.pendingRefresh.hidden = true;
1463
+ }
1464
+ setPickerAvailability();
1465
+ void consumePendingBridgeFiles();
1466
+ scheduleIntrinsicSizeReport(true);
1467
+ }
1468
+
1469
+ function parseSession(candidate) {
1470
+ const sessionId = boundedString(candidate.sessionId, 100);
1471
+ const uploadPageUrl = boundedString(candidate.uploadPageUrl, 2000);
1472
+ const mediaKind = candidate.mediaKind;
1473
+ const assetRole = candidate.assetRole;
1474
+ const expiresAt = boundedString(candidate.expiresAt, 100);
1475
+ if (!sessionId || !["image", "video", "mixed"].includes(mediaKind))
1476
+ return null;
1477
+ if (!["media", "thumbnail"].includes(assetRole)) return null;
1478
+ if (!Number.isInteger(candidate.maxFiles) || !expiresAt) return null;
1479
+ try {
1480
+ const parsedUrl = new URL(uploadPageUrl);
1481
+ if (!safeUploadPageUrl(parsedUrl)) return null;
1482
+ } catch {
1483
+ return null;
1484
+ }
1485
+ return {
1486
+ sessionId,
1487
+ uploadPageUrl,
1488
+ mediaKind,
1489
+ assetRole,
1490
+ maxFiles: candidate.maxFiles,
1491
+ expiresAt,
1492
+ };
1493
+ }
1494
+
1495
+ function mergeToolInput(input) {
1496
+ state.toolInput = { ...state.toolInput, ...input };
1497
+ void consumePendingBridgeFiles();
1498
+ }
1499
+
1500
+ function consumePendingBridgeFiles() {
1501
+ if (
1502
+ state.disposed ||
1503
+ state.bridgeConsumptionQueued ||
1504
+ !state.session ||
1505
+ !Array.isArray(state.toolInput.files)
1506
+ )
1507
+ return;
1508
+ state.bridgeConsumptionQueued = true;
1509
+ void enqueueIngestion(async () => {
1510
+ state.bridgeConsumptionQueued = false;
1511
+ await consumePendingBridgeFilesNow();
1512
+ }).catch(() => {
1513
+ state.bridgeConsumptionQueued = false;
1514
+ });
1515
+ }
1516
+
1517
+ async function consumePendingBridgeFilesNow() {
1518
+ if (
1519
+ state.disposed ||
1520
+ !state.session ||
1521
+ !Array.isArray(state.toolInput.files) ||
1522
+ state.consumingBridgeFiles
1523
+ )
1524
+ return;
1525
+ state.consumingBridgeFiles = true;
1526
+ try {
1527
+ const pending = state.toolInput.files.filter((reference) => {
1528
+ const id = normalizedBridgeReferenceId(reference);
1529
+ return id && !state.consumedBridgeIds.has(id);
1530
+ });
1531
+ if (!pending.length) return;
1532
+ if (!hasOpenAiFunction("getFileDownloadUrl")) {
1533
+ requireFallback(
1534
+ "Este host no expone el puente de archivos. Usa el selector o el enlace seguro.",
1535
+ "warning",
1536
+ );
1537
+ return;
1538
+ }
1539
+ const availableSlots = Math.max(
1540
+ 0,
1541
+ safeSessionLimit() - state.items.length,
1542
+ );
1543
+ const files = [];
1544
+ for (const reference of pending.slice(0, availableSlots)) {
1545
+ const id = normalizedBridgeReferenceId(reference);
1546
+ try {
1547
+ const file = await downloadBridgeReference(reference);
1548
+ state.consumedBridgeIds.add(id);
1549
+ files.push(file);
1550
+ } catch {
1551
+ state.consumedBridgeIds.add(id);
1552
+ addBridgeFailure(reference);
1553
+ }
1554
+ }
1555
+ if (files.length) await processFiles(files);
1556
+ } finally {
1557
+ state.consumingBridgeFiles = false;
1558
+ const hasUnconsumed =
1559
+ Array.isArray(state.toolInput.files) &&
1560
+ state.toolInput.files.some((reference) => {
1561
+ const id = normalizedBridgeReferenceId(reference);
1562
+ return id && !state.consumedBridgeIds.has(id);
1563
+ });
1564
+ if (
1565
+ hasUnconsumed &&
1566
+ hasOpenAiFunction("getFileDownloadUrl") &&
1567
+ state.items.length < safeSessionLimit()
1568
+ ) {
1569
+ queueMicrotask(() => void consumePendingBridgeFiles());
1570
+ }
1571
+ }
1572
+ }
1573
+
1574
+ function processLocalFiles(files) {
1575
+ return enqueueIngestion(async () => {
1576
+ if (
1577
+ state.disposed ||
1578
+ !state.session ||
1579
+ sessionExpired() ||
1580
+ state.uploadBlocked
1581
+ )
1582
+ return;
1583
+ state.workCancelled = false;
1584
+ if (files.length > safeSessionLimit() - state.items.length) {
1585
+ setBatchError(
1586
+ `Puedes subir hasta ${safeSessionLimit()} archivos por sesión.`,
1587
+ );
1588
+ }
1589
+ await processFiles(files);
1590
+ });
1591
+ }
1592
+
1593
+ function chooseLibraryFiles() {
1594
+ return enqueueIngestion(async () => {
1595
+ if (
1596
+ state.disposed ||
1597
+ !state.session ||
1598
+ sessionExpired() ||
1599
+ state.uploadBlocked
1600
+ )
1601
+ return;
1602
+ const bridge = openAiBridge();
1603
+ if (
1604
+ !bridge ||
1605
+ typeof bridge.selectFiles !== "function" ||
1606
+ typeof bridge.getFileDownloadUrl !== "function"
1607
+ )
1608
+ return;
1609
+ try {
1610
+ const result = await bridge.selectFiles();
1611
+ const selected = Array.isArray(result) ? result : [];
1612
+ const availableSlots = Math.max(
1613
+ 0,
1614
+ safeSessionLimit() - state.items.length,
1615
+ );
1616
+ const files = [];
1617
+ for (const candidate of selected.slice(0, availableSlots)) {
1618
+ if (!isRecord(candidate)) continue;
1619
+ const fileId = boundedString(candidate.fileId, 300);
1620
+ const fileName = boundedString(candidate.fileName, 240);
1621
+ const mimeType = boundedString(candidate.mimeType, 100);
1622
+ if (!fileId || !fileName || !mimeType) continue;
1623
+ try {
1624
+ files.push(
1625
+ await downloadBridgeReference({
1626
+ fileId,
1627
+ fileName,
1628
+ mimeType,
1629
+ }),
1630
+ );
1631
+ } catch {
1632
+ addBridgeFailure({ fileId, fileName, mimeType });
1633
+ }
1634
+ }
1635
+ await processFiles(files);
1636
+ } catch {
1637
+ setSessionStatus(
1638
+ "No se pudo abrir el selector del host. Usa el selector del navegador.",
1639
+ "warning",
1640
+ );
1641
+ }
1642
+ });
1643
+ }
1644
+
1645
+ function isBrowserFile(value) {
1646
+ return (
1647
+ isRecord(value) &&
1648
+ typeof value.name === "string" &&
1649
+ typeof value.type === "string" &&
1650
+ typeof value.size === "number" &&
1651
+ typeof value.slice === "function"
1652
+ );
1653
+ }
1654
+
1655
+ async function downloadBridgeReference(reference) {
1656
+ const bridge = openAiBridge();
1657
+ if (!bridge || typeof bridge.getFileDownloadUrl !== "function") {
1658
+ throw new Error("bridge_download_unavailable");
1659
+ }
1660
+ const fileId = normalizedBridgeReferenceId(reference);
1661
+ if (!fileId) throw new Error("invalid_bridge_file");
1662
+ const resolved = await bridge.getFileDownloadUrl({ fileId });
1663
+ const temporaryUrl = normalizedBridgeDownloadUrl(resolved);
1664
+ const parsedUrl = new URL(temporaryUrl);
1665
+ if (!safeBridgeDownloadUrl(parsedUrl))
1666
+ throw new Error("unsafe_bridge_url");
1667
+ const response = await fetch(parsedUrl.toString(), {
1668
+ method: "GET",
1669
+ credentials: "omit",
1670
+ redirect: "error",
1671
+ signal: activeAbortSignal(),
1672
+ });
1673
+ if (!response.ok) throw new Error("bridge_download_failed");
1674
+ const blob = await readBoundedBridgeBlob(response);
1675
+ const preferredType = boundedString(
1676
+ reference.mime_type || reference.mimeType || reference.type,
1677
+ 100,
1678
+ );
1679
+ const contentType = ACCEPTED_TYPES.has(preferredType)
1680
+ ? preferredType
1681
+ : ACCEPTED_TYPES.has(blob.type)
1682
+ ? blob.type
1683
+ : "application/octet-stream";
1684
+ const fallbackName = contentType.startsWith("video/")
1685
+ ? "creative-video"
1686
+ : "creative-image";
1687
+ const name =
1688
+ boundedString(
1689
+ reference.file_name || reference.fileName || reference.name,
1690
+ 240,
1691
+ ) || fallbackName;
1692
+ return new File([blob], name, {
1693
+ type: contentType,
1694
+ lastModified: Date.now(),
1695
+ });
1696
+ }
1697
+
1698
+ async function readBoundedBridgeBlob(response) {
1699
+ const declaredLength = Number(
1700
+ response.headers.get("content-length") || "",
1701
+ );
1702
+ if (
1703
+ Number.isFinite(declaredLength) &&
1704
+ declaredLength > MAX_FILE_SIZE_BYTES
1705
+ ) {
1706
+ throw new Error("bridge_file_too_large");
1707
+ }
1708
+
1709
+ if (!response.body || typeof response.body.getReader !== "function") {
1710
+ const blob = await response.blob();
1711
+ if (blob.size > MAX_FILE_SIZE_BYTES) {
1712
+ throw new Error("bridge_file_too_large");
1713
+ }
1714
+ return blob;
1715
+ }
1716
+
1717
+ const reader = response.body.getReader();
1718
+ const chunks = [];
1719
+ let total = 0;
1720
+ try {
1721
+ while (true) {
1722
+ const next = await reader.read();
1723
+ if (next.done) break;
1724
+ const chunk = next.value;
1725
+ total += chunk.byteLength;
1726
+ if (total > MAX_FILE_SIZE_BYTES) {
1727
+ await reader.cancel();
1728
+ throw new Error("bridge_file_too_large");
1729
+ }
1730
+ chunks.push(chunk);
1731
+ }
1732
+ } finally {
1733
+ reader.releaseLock?.();
1734
+ }
1735
+ return new Blob(chunks, {
1736
+ type: response.headers.get("content-type") || "",
1737
+ });
1738
+ }
1739
+
1740
+ function addBridgeFailure(reference) {
1741
+ const type = boundedString(
1742
+ reference.mime_type || reference.mimeType || reference.type,
1743
+ 100,
1744
+ );
1745
+ const mediaKind = mediaKindForType(type) || "image";
1746
+ const item = createItem(null, mediaKind);
1747
+ item.displayName = boundedString(
1748
+ reference.file_name || reference.fileName || reference.name,
1749
+ 300,
1750
+ );
1751
+ item.status = "failed";
1752
+ item.error =
1753
+ "No se pudo leer el archivo temporal del host. Usa el selector o el enlace seguro.";
1754
+ item.failureStage = "bridge";
1755
+ state.items.push(item);
1756
+ requireFallback(
1757
+ "No se pudo leer el archivo temporal del host. Usa el selector o el enlace seguro.",
1758
+ "warning",
1759
+ );
1760
+ renderItems();
1761
+ emitAnalytics("ads_meta_remote_upload_error", mediaKind, {
1762
+ failureStage: "bridge",
1763
+ errorType: "bridge_download_failed",
1764
+ });
1765
+ }
1766
+
1767
+ async function processFiles(fileCandidates) {
1768
+ if (
1769
+ state.disposed ||
1770
+ !state.session ||
1771
+ sessionExpired() ||
1772
+ state.uploadBlocked ||
1773
+ state.busyBatch
1774
+ )
1775
+ return;
1776
+ const files = fileCandidates.filter(isBrowserFile);
1777
+ if (!files.length) return;
1778
+ setBatchError("");
1779
+ const availableSlots = Math.max(
1780
+ 0,
1781
+ safeSessionLimit() - state.items.length,
1782
+ );
1783
+ const selected = files.slice(0, availableSlots);
1784
+ if (files.length > availableSlots) {
1785
+ setBatchError(
1786
+ `Puedes subir hasta ${safeSessionLimit()} archivos por sesión.`,
1787
+ );
1788
+ }
1789
+ if (!selected.length) return;
1790
+
1791
+ state.busyBatch = true;
1792
+ setPickerAvailability();
1793
+ for (const file of selected) {
1794
+ const mediaKind = mediaKindForType(file.type) || "image";
1795
+ const item = createItem(file, mediaKind);
1796
+ state.items.push(item);
1797
+ renderItems();
1798
+ await prepareAndUpload(item);
1799
+ }
1800
+ state.busyBatch = false;
1801
+ setPickerAvailability();
1802
+ if (state.items.some((item) => item.status === "uploaded")) {
1803
+ await refreshSessionStatus();
1804
+ }
1805
+ }
1806
+
1807
+ function createItem(file, mediaKind) {
1808
+ return {
1809
+ id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`,
1810
+ uploadId: createClientUuid(),
1811
+ file,
1812
+ displayName: file?.name || "",
1813
+ uploadFile: file,
1814
+ mediaKind,
1815
+ status: "pending",
1816
+ progress: 0,
1817
+ attempts: 0,
1818
+ error: "",
1819
+ failureStage: "",
1820
+ stagingAssetId: "",
1821
+ previewUrl: "",
1822
+ };
1823
+ }
1824
+
1825
+ async function prepareAndUpload(item) {
1826
+ if (!item.file) return;
1827
+ item.status = "validating";
1828
+ item.progress = 0;
1829
+ item.error = "";
1830
+ item.failureStage = "";
1831
+ renderItems();
1832
+ try {
1833
+ const inspection = await inspectFile(item.file);
1834
+ item.mediaKind = inspection.mediaKind;
1835
+ item.uploadFile =
1836
+ inspection.mediaKind === "image"
1837
+ ? await compressImage(item.file, inspection)
1838
+ : item.file;
1839
+ emitAnalytics("ads_meta_remote_upload_started", item.mediaKind);
1840
+ await uploadWithRetries(item);
1841
+ item.status = "uploaded";
1842
+ item.progress = 100;
1843
+ item.error = "";
1844
+ emitAnalytics("ads_meta_remote_upload_completed", item.mediaKind);
1845
+ } catch (error) {
1846
+ const failure = safeFailure(error);
1847
+ item.status = "failed";
1848
+ item.error = failure.message;
1849
+ item.failureStage = failure.stage;
1850
+ emitAnalytics("ads_meta_remote_upload_error", item.mediaKind, {
1851
+ failureStage: failure.stage,
1852
+ errorType: failure.code,
1853
+ });
1854
+ if (failure.code === "session_expired") {
1855
+ requireFallback(
1856
+ "La sesión expiró. Abre el enlace seguro para crear otra.",
1857
+ "danger",
1858
+ true,
1859
+ );
1860
+ } else if (failure.stage !== "validation") {
1861
+ requireFallback(
1862
+ "La carga no terminó. Puedes reintentarlo o continuar desde el enlace seguro.",
1863
+ "warning",
1864
+ );
1865
+ }
1866
+ }
1867
+ renderItems();
1868
+ }
1869
+
1870
+ async function inspectFile(file) {
1871
+ const mediaKind = mediaKindForType(file.type);
1872
+ if (!ACCEPTED_TYPES.has(file.type) || !mediaKind) {
1873
+ throw validationError(
1874
+ "unsupported_mime",
1875
+ "Este tipo de archivo no es compatible.",
1876
+ );
1877
+ }
1878
+ if (
1879
+ state.session.mediaKind !== "mixed" &&
1880
+ state.session.mediaKind !== mediaKind
1881
+ ) {
1882
+ throw validationError(
1883
+ "wrong_media_kind",
1884
+ "El archivo no coincide con el tipo solicitado.",
1885
+ );
1886
+ }
1887
+ if (
1888
+ state.session.assetRole === "thumbnail" &&
1889
+ mediaKind !== "image"
1890
+ ) {
1891
+ throw validationError(
1892
+ "thumbnail_requires_image",
1893
+ "La miniatura debe ser una imagen.",
1894
+ );
1895
+ }
1896
+ if (file.size <= 0) {
1897
+ throw validationError("empty_file", "El archivo está vacío.");
1898
+ }
1899
+ if (file.size > MAX_FILE_SIZE_BYTES) {
1900
+ throw validationError(
1901
+ "file_too_large",
1902
+ "El archivo supera el límite de 350 MB.",
1903
+ );
1904
+ }
1905
+
1906
+ const inspection =
1907
+ mediaKind === "image"
1908
+ ? await inspectImage(file)
1909
+ : await inspectVideo(file);
1910
+ if (
1911
+ inspection.width < MIN_DIMENSION ||
1912
+ inspection.height < MIN_DIMENSION
1913
+ ) {
1914
+ throw validationError(
1915
+ "low_resolution",
1916
+ "La resolución mínima es 600 × 600 px.",
1917
+ );
1918
+ }
1919
+ if (
1920
+ mediaKind === "video" &&
1921
+ inspection.durationSeconds > MAX_VIDEO_DURATION_SECONDS
1922
+ ) {
1923
+ throw validationError(
1924
+ "video_too_long",
1925
+ "El video supera el máximo de 180 segundos.",
1926
+ );
1927
+ }
1928
+ return inspection;
1929
+ }
1930
+
1931
+ function inspectImage(file) {
1932
+ return new Promise((resolve, reject) => {
1933
+ const image = new Image();
1934
+ const objectUrl = URL.createObjectURL(file);
1935
+ const cleanup = () => URL.revokeObjectURL(objectUrl);
1936
+ image.onload = () => {
1937
+ cleanup();
1938
+ const width = image.naturalWidth;
1939
+ const height = image.naturalHeight;
1940
+ if (!validDimension(width) || !validDimension(height)) {
1941
+ reject(
1942
+ validationError(
1943
+ "invalid_metadata",
1944
+ "No se pudo leer la imagen.",
1945
+ ),
1946
+ );
1947
+ return;
1948
+ }
1949
+ resolve({ mediaKind: "image", width, height, image });
1950
+ };
1951
+ image.onerror = () => {
1952
+ cleanup();
1953
+ reject(
1954
+ validationError(
1955
+ "invalid_metadata",
1956
+ "No se pudo leer la imagen.",
1957
+ ),
1958
+ );
1959
+ };
1960
+ image.src = objectUrl;
1961
+ });
1962
+ }
1963
+
1964
+ function inspectVideo(file) {
1965
+ return new Promise((resolve, reject) => {
1966
+ const video = document.createElement("video");
1967
+ const objectUrl = URL.createObjectURL(file);
1968
+ const cleanup = () => URL.revokeObjectURL(objectUrl);
1969
+ video.preload = "metadata";
1970
+ video.onloadedmetadata = () => {
1971
+ cleanup();
1972
+ const width = video.videoWidth;
1973
+ const height = video.videoHeight;
1974
+ const durationSeconds = video.duration;
1975
+ if (
1976
+ !validDimension(width) ||
1977
+ !validDimension(height) ||
1978
+ !Number.isFinite(durationSeconds) ||
1979
+ durationSeconds < 0
1980
+ ) {
1981
+ reject(
1982
+ validationError(
1983
+ "invalid_metadata",
1984
+ "No se pudo leer el video.",
1985
+ ),
1986
+ );
1987
+ return;
1988
+ }
1989
+ resolve({ mediaKind: "video", width, height, durationSeconds });
1990
+ };
1991
+ video.onerror = () => {
1992
+ cleanup();
1993
+ reject(
1994
+ validationError(
1995
+ "invalid_metadata",
1996
+ "No se pudo leer el video.",
1997
+ ),
1998
+ );
1999
+ };
2000
+ video.src = objectUrl;
2001
+ });
2002
+ }
2003
+
2004
+ function validDimension(value) {
2005
+ return Number.isFinite(value) && value > 0;
2006
+ }
2007
+
2008
+ function validationError(code, message) {
2009
+ const error = new Error(code);
2010
+ error.safeCode = code;
2011
+ error.safeMessage = message;
2012
+ error.safeStage = "validation";
2013
+ return error;
2014
+ }
2015
+
2016
+ async function compressImage(file, inspection) {
2017
+ if (file.size <= IMAGE_COMPRESSION_THRESHOLD_BYTES) return file;
2018
+ try {
2019
+ const scale = Math.min(
2020
+ 1,
2021
+ IMAGE_MAX_DIMENSION /
2022
+ Math.max(inspection.width, inspection.height),
2023
+ );
2024
+ const canvas = document.createElement("canvas");
2025
+ canvas.width = Math.max(1, Math.round(inspection.width * scale));
2026
+ canvas.height = Math.max(1, Math.round(inspection.height * scale));
2027
+ if (canvas.width < MIN_DIMENSION || canvas.height < MIN_DIMENSION) {
2028
+ throw validationError(
2029
+ "low_resolution",
2030
+ "La resolución mínima final es 600 × 600 px.",
2031
+ );
2032
+ }
2033
+ const context = canvas.getContext("2d", {
2034
+ alpha: file.type !== "image/jpeg",
2035
+ });
2036
+ if (!context) throw new Error("canvas_unavailable");
2037
+ if (file.type === "image/png") {
2038
+ context.save();
2039
+ context.fillStyle = "#ffffff";
2040
+ context.fillRect(0, 0, canvas.width, canvas.height);
2041
+ context.restore();
2042
+ }
2043
+ context.drawImage(
2044
+ inspection.image,
2045
+ 0,
2046
+ 0,
2047
+ canvas.width,
2048
+ canvas.height,
2049
+ );
2050
+ let blob = await canvasBlob(canvas, file.type);
2051
+ if (
2052
+ file.type === "image/png" &&
2053
+ blob.size > IMAGE_COMPRESSION_MAX_BYTES
2054
+ ) {
2055
+ blob = await canvasBlob(canvas, "image/jpeg");
2056
+ }
2057
+ const compressed = new File([blob], file.name, {
2058
+ type: blob.type || file.type,
2059
+ lastModified: file.lastModified,
2060
+ });
2061
+ const output = compressed.size < file.size ? compressed : file;
2062
+ if (output.size > IMAGE_COMPRESSION_MAX_BYTES) {
2063
+ const error = new Error("compression_too_large");
2064
+ error.safeCode = "compression_too_large";
2065
+ error.safeMessage =
2066
+ "La imagen no pudo reducirse a un tamaño seguro.";
2067
+ error.safeStage = "compression";
2068
+ throw error;
2069
+ }
2070
+ return output;
2071
+ } catch (error) {
2072
+ if (
2073
+ error &&
2074
+ ["compression", "validation"].includes(error.safeStage)
2075
+ )
2076
+ throw error;
2077
+ const safeError = new Error("compression_failed");
2078
+ safeError.safeCode = "compression_failed";
2079
+ safeError.safeMessage = "No se pudo comprimir la imagen.";
2080
+ safeError.safeStage = "compression";
2081
+ throw safeError;
2082
+ }
2083
+ }
2084
+
2085
+ function canvasBlob(canvas, type) {
2086
+ return new Promise((resolve, reject) => {
2087
+ canvas.toBlob(
2088
+ (result) =>
2089
+ result
2090
+ ? resolve(result)
2091
+ : reject(new Error("compression_failed")),
2092
+ type,
2093
+ 0.85,
2094
+ );
2095
+ });
2096
+ }
2097
+
2098
+ async function uploadWithRetries(item) {
2099
+ let lastError = null;
2100
+ for (let attempt = 1; attempt <= MAX_UPLOAD_ATTEMPTS; attempt += 1) {
2101
+ item.attempts = attempt;
2102
+ item.status = "uploading";
2103
+ item.progress = 0;
2104
+ renderItems();
2105
+ try {
2106
+ const resolved = await resolveUpload(
2107
+ item.uploadFile,
2108
+ item.uploadId,
2109
+ );
2110
+ await putFileWithProgress(
2111
+ resolved.uploadUrl,
2112
+ item.uploadFile,
2113
+ (progress) => {
2114
+ item.progress = progress;
2115
+ renderItems();
2116
+ },
2117
+ );
2118
+ item.stagingAssetId = resolved.stagingAssetId;
2119
+ return;
2120
+ } catch (error) {
2121
+ if (error && error.message === "session_expired") throw error;
2122
+ if (state.disposed || state.workCancelled) throw error;
2123
+ lastError = error;
2124
+ }
2125
+ }
2126
+ const error = new Error("upload_failed");
2127
+ error.safeCode = "upload_failed";
2128
+ error.safeMessage =
2129
+ "La carga falló tras 3 intentos. Puedes reintentarla.";
2130
+ error.safeStage =
2131
+ lastError && lastError.safeStage ? lastError.safeStage : "upload";
2132
+ throw error;
2133
+ }
2134
+
2135
+ async function resolveUpload(file, uploadId) {
2136
+ const uploadPage = new URL(state.session.uploadPageUrl);
2137
+ if (!safeUploadPageUrl(uploadPage))
2138
+ throw new Error("unsafe_upload_page");
2139
+ const token = uploadPage.searchParams.get("token");
2140
+ if (!token) {
2141
+ const error = new Error("missing_token");
2142
+ error.safeStage = "resolve";
2143
+ throw error;
2144
+ }
2145
+ const resolveUrl = new URL(
2146
+ "/api/meta-upload/resolve",
2147
+ uploadPage.origin,
2148
+ );
2149
+ const response = await fetch(resolveUrl.toString(), {
2150
+ method: "POST",
2151
+ headers: { "content-type": "application/json" },
2152
+ body: JSON.stringify({
2153
+ token,
2154
+ uploadId,
2155
+ contentType: file.type,
2156
+ fileName: file.name,
2157
+ fileSize: file.size,
2158
+ }),
2159
+ credentials: "omit",
2160
+ signal: activeAbortSignal(),
2161
+ });
2162
+ if (!response.ok) {
2163
+ const error = new Error(
2164
+ response.status === 401 || response.status === 410
2165
+ ? "session_expired"
2166
+ : "resolve_failed",
2167
+ );
2168
+ error.safeStage = "resolve";
2169
+ throw error;
2170
+ }
2171
+ const body = await response.json();
2172
+ if (!isRecord(body)) throw new Error("invalid_resolve_response");
2173
+ const uploadUrl = boundedString(body.uploadUrl, 3000);
2174
+ const stagingAssetId = boundedString(body.stagingAssetId, 300);
2175
+ const parsedUploadUrl = new URL(uploadUrl);
2176
+ if (!safeWebProtocol(parsedUploadUrl) || !stagingAssetId) {
2177
+ const error = new Error("invalid_resolve_response");
2178
+ error.safeStage = "resolve";
2179
+ throw error;
2180
+ }
2181
+ return { uploadUrl: parsedUploadUrl.toString(), stagingAssetId };
2182
+ }
2183
+
2184
+ function createClientUuid() {
2185
+ if (typeof crypto !== "undefined") {
2186
+ if (typeof crypto.randomUUID === "function")
2187
+ return crypto.randomUUID();
2188
+ if (typeof crypto.getRandomValues === "function") {
2189
+ const bytes = new Uint8Array(16);
2190
+ crypto.getRandomValues(bytes);
2191
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
2192
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
2193
+ const hex = Array.from(bytes, (byte) =>
2194
+ byte.toString(16).padStart(2, "0"),
2195
+ ).join("");
2196
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
2197
+ }
2198
+ }
2199
+ return `00000000-0000-4000-8000-${Date.now().toString(16).padStart(12, "0").slice(-12)}`;
2200
+ }
2201
+
2202
+ function safeWebProtocol(url) {
2203
+ if (url.protocol === "https:") return true;
2204
+ return (
2205
+ url.protocol === "http:" &&
2206
+ ["localhost", "127.0.0.1", "::1"].includes(url.hostname)
2207
+ );
2208
+ }
2209
+
2210
+ function safeBridgeDownloadUrl(url) {
2211
+ if (
2212
+ url.protocol !== "https:" ||
2213
+ url.username ||
2214
+ url.password ||
2215
+ url.port
2216
+ )
2217
+ return false;
2218
+ const hostname = url.hostname.toLowerCase();
2219
+ return (
2220
+ hostname === "files.oaiusercontent.com" ||
2221
+ hostname.endsWith(".oaiusercontent.com")
2222
+ );
2223
+ }
2224
+
2225
+ function safeUploadPageUrl(url) {
2226
+ if (url.username || url.password) return false;
2227
+ if (
2228
+ [
2229
+ "https://productmaker.app",
2230
+ "https://www.productmaker.app",
2231
+ ].includes(url.origin)
2232
+ )
2233
+ return true;
2234
+ return (
2235
+ url.protocol === "http:" &&
2236
+ ["localhost", "127.0.0.1", "::1"].includes(url.hostname)
2237
+ );
2238
+ }
2239
+
2240
+ function putFileWithProgress(url, file, onProgress) {
2241
+ return new Promise((resolve, reject) => {
2242
+ const request = new XMLHttpRequest();
2243
+ let settled = false;
2244
+ const finish = (callback) => {
2245
+ if (settled) return;
2246
+ settled = true;
2247
+ state.activeXhrs.delete(request);
2248
+ callback();
2249
+ };
2250
+ request.open("PUT", url);
2251
+ request.timeout = 5 * 60 * 1000;
2252
+ request.setRequestHeader("Content-Type", file.type);
2253
+ state.activeXhrs.add(request);
2254
+ request.upload.addEventListener("progress", (event) => {
2255
+ if (event.lengthComputable && event.total > 0) {
2256
+ onProgress(Math.round((event.loaded / event.total) * 100));
2257
+ }
2258
+ });
2259
+ request.addEventListener("load", () => {
2260
+ if (request.status >= 200 && request.status < 300)
2261
+ finish(resolve);
2262
+ else finish(() => reject(uploadTransportError("put_failed")));
2263
+ });
2264
+ request.addEventListener("error", () =>
2265
+ finish(() => reject(uploadTransportError("network_error"))),
2266
+ );
2267
+ request.addEventListener("timeout", () =>
2268
+ finish(() => reject(uploadTransportError("upload_timeout"))),
2269
+ );
2270
+ request.send(file);
2271
+ });
2272
+ }
2273
+
2274
+ function uploadTransportError(code) {
2275
+ const error = new Error(code);
2276
+ error.safeStage = "put";
2277
+ return error;
2278
+ }
2279
+
2280
+ function safeFailure(error) {
2281
+ if (error && typeof error.safeMessage === "string") {
2282
+ return {
2283
+ code: boundedString(error.safeCode, 60) || "upload_failed",
2284
+ message: boundedString(error.safeMessage, 300),
2285
+ stage: boundedString(error.safeStage, 40) || "upload",
2286
+ };
2287
+ }
2288
+ if (error && error.message === "session_expired") {
2289
+ return {
2290
+ code: "session_expired",
2291
+ message:
2292
+ "La sesión expiró. Abre el enlace seguro para crear otra.",
2293
+ stage: "resolve",
2294
+ };
2295
+ }
2296
+ return {
2297
+ code: "upload_failed",
2298
+ message: "No se pudo completar la carga. Puedes reintentarla.",
2299
+ stage: (error && boundedString(error.safeStage, 40)) || "upload",
2300
+ };
2301
+ }
2302
+
2303
+ async function retryItem(index) {
2304
+ const item = state.items[index];
2305
+ if (
2306
+ !item ||
2307
+ item.status !== "failed" ||
2308
+ !item.file ||
2309
+ item.failureStage === "validation"
2310
+ )
2311
+ return;
2312
+ item.attempts = 0;
2313
+ await prepareAndUpload(item);
2314
+ if (item.status === "uploaded") await refreshSessionStatus();
2315
+ }
2316
+
2317
+ function removeItem(index) {
2318
+ if (state.busyBatch) return;
2319
+ const [item] = state.items.splice(index, 1);
2320
+ if (item) revokePreviewUrl(item);
2321
+ renderItems();
2322
+ setPickerAvailability();
2323
+ }
2324
+
2325
+ function revokePreviewUrl(item) {
2326
+ if (
2327
+ item.previewUrl &&
2328
+ typeof window.URL.revokeObjectURL === "function"
2329
+ ) {
2330
+ window.URL.revokeObjectURL(item.previewUrl);
2331
+ item.previewUrl = "";
2332
+ }
2333
+ }
2334
+
2335
+ function previewUrlFor(item) {
2336
+ if (
2337
+ !item.file ||
2338
+ item.mediaKind !== "image" ||
2339
+ !IMAGE_TYPES.has(item.file.type)
2340
+ )
2341
+ return "";
2342
+ if (
2343
+ !item.previewUrl &&
2344
+ typeof window.URL.createObjectURL === "function"
2345
+ ) {
2346
+ item.previewUrl = window.URL.createObjectURL(item.file);
2347
+ }
2348
+ return item.previewUrl;
2349
+ }
2350
+
2351
+ function renderItems() {
2352
+ const fragment = document.createDocumentFragment();
2353
+ state.items.forEach((item, index) => {
2354
+ const card = document.createElement("article");
2355
+ card.className = "file-card";
2356
+ card.dataset.testid = `meta-upload-file-${index}`;
2357
+
2358
+ const preview = document.createElement("div");
2359
+ preview.className = "file-preview";
2360
+ preview.dataset.testid = `meta-upload-file-preview-${index}`;
2361
+ const previewUrl = previewUrlFor(item);
2362
+ if (previewUrl) {
2363
+ const image = document.createElement("img");
2364
+ image.src = previewUrl;
2365
+ image.alt =
2366
+ item.file?.name || item.displayName || "Uploaded image";
2367
+ image.dataset.testid = `meta-upload-preview-${index}`;
2368
+ preview.append(image);
2369
+ } else if (item.mediaKind === "video") {
2370
+ const video = document.createElement("span");
2371
+ video.className = "video-preview";
2372
+ video.setAttribute("aria-hidden", "true");
2373
+ video.textContent = "▶";
2374
+ preview.append(video);
2375
+ } else {
2376
+ const placeholder = document.createElement("span");
2377
+ placeholder.className = "file-kind";
2378
+ placeholder.setAttribute("aria-hidden", "true");
2379
+ placeholder.textContent = "▧";
2380
+ preview.append(placeholder);
2381
+ }
2382
+
2383
+ if (item.status === "uploading" || item.status === "uploaded") {
2384
+ const progress = document.createElement("div");
2385
+ progress.className = "progress";
2386
+ progress.setAttribute("role", "progressbar");
2387
+ progress.setAttribute("aria-valuemin", "0");
2388
+ progress.setAttribute("aria-valuemax", "100");
2389
+ progress.setAttribute("aria-valuenow", String(item.progress));
2390
+ progress.dataset.testid = `meta-upload-progress-${index}`;
2391
+ const bar = document.createElement("div");
2392
+ bar.className = "progress-bar";
2393
+ bar.style.width = `${item.progress}%`;
2394
+ progress.append(bar);
2395
+ preview.append(progress);
2396
+ }
2397
+
2398
+ if (
2399
+ item.status === "failed" &&
2400
+ item.failureStage !== "validation" &&
2401
+ item.file
2402
+ ) {
2403
+ const retry = document.createElement("button");
2404
+ retry.type = "button";
2405
+ retry.className = "overlay-button retry-overlay";
2406
+ retry.dataset.testid = `meta-upload-retry-${index}`;
2407
+ retry.setAttribute("aria-label", `Retry ${item.file.name}`);
2408
+ retry.textContent = "Retry";
2409
+ retry.addEventListener("click", () => void retryItem(index));
2410
+ preview.append(retry);
2411
+ }
2412
+
2413
+ const remove = document.createElement("button");
2414
+ remove.type = "button";
2415
+ remove.className = "overlay-button remove-overlay";
2416
+ remove.dataset.testid = `meta-upload-remove-${index}`;
2417
+ remove.setAttribute(
2418
+ "aria-label",
2419
+ `Remove ${item.file?.name || item.displayName || `file ${index + 1}`}`,
2420
+ );
2421
+ remove.textContent = "×";
2422
+ remove.disabled = ["uploading", "validating", "uploaded"].includes(
2423
+ item.status,
2424
+ );
2425
+ remove.addEventListener("click", () => removeItem(index));
2426
+ preview.append(remove);
2427
+ card.append(preview);
2428
+
2429
+ const head = document.createElement("div");
2430
+ head.className = "file-head";
2431
+ const label = document.createElement("p");
2432
+ label.className = "file-label";
2433
+ label.textContent =
2434
+ item.file?.name || item.displayName || `File ${index + 1}`;
2435
+ const status = document.createElement("p");
2436
+ status.className = `file-state${item.status === "failed" ? " error" : ""}`;
2437
+ status.dataset.testid =
2438
+ item.status === "failed"
2439
+ ? `meta-upload-file-error-${index}`
2440
+ : `meta-upload-file-status-${index}`;
2441
+ status.textContent = itemStatusText(item);
2442
+ head.append(label, status);
2443
+ card.append(head);
2444
+ fragment.append(card);
2445
+ });
2446
+ elements.fileList.replaceChildren(fragment);
2447
+ updateUploadTitle();
2448
+ scheduleIntrinsicSizeReport(true);
2449
+ }
2450
+
2451
+ function itemStatusText(item) {
2452
+ if (item.status === "validating") return "Validando…";
2453
+ if (item.status === "uploading") return `Subiendo… ${item.progress}%`;
2454
+ if (item.status === "uploaded") return "Carga completada";
2455
+ if (item.status === "failed") return item.error || "Carga fallida";
2456
+ return "Pendiente";
2457
+ }
2458
+
2459
+ async function refreshSessionStatus() {
2460
+ if (!state.session) return;
2461
+ elements.checkStatus.disabled = true;
2462
+ elements.pendingRefresh.disabled = true;
2463
+ setSessionStatus("Comprobando el staging privado…", "neutral");
2464
+ try {
2465
+ const result = await callReadOnlyTool("check_meta_media_upload", {
2466
+ sessionId: state.session.sessionId,
2467
+ });
2468
+ const status = parseStatusResult(result);
2469
+ state.assets = status.assets;
2470
+ if (!status.assets.length) {
2471
+ if (status.legacyKindAmbiguous) {
2472
+ requireFallback(
2473
+ "El tipo del asset legacy es ambiguo. Vuelve a comprobar o usa el enlace seguro.",
2474
+ "warning",
2475
+ );
2476
+ } else {
2477
+ setFallbackVisibility(state.fallbackRequired);
2478
+ setSessionStatus(
2479
+ "La sesión sigue pendiente. Termina la carga o vuelve a comprobar.",
2480
+ "warning",
2481
+ );
2482
+ }
2483
+ elements.destinationSection.hidden = true;
2484
+ elements.pendingRefresh.hidden = true;
2485
+ return;
2486
+ }
2487
+ elements.pendingRefresh.hidden = true;
2488
+
2489
+ const partial =
2490
+ status.status === "pending" ||
2491
+ (state.items.length > 0 &&
2492
+ status.assets.length < state.items.length);
2493
+ if (partial) {
2494
+ requireFallback(
2495
+ "Carga parcial verificada. Puedes continuar desde el enlace seguro si algún archivo no terminó.",
2496
+ "warning",
2497
+ );
2498
+ } else {
2499
+ setFallbackVisibility(state.fallbackRequired);
2500
+ }
2501
+ setSessionStatus(
2502
+ partial
2503
+ ? "Carga parcial verificada. Continuaremos solo con los assets listos."
2504
+ : "Carga verificada. Elige ahora el destino de Meta.",
2505
+ partial ? "warning" : "success",
2506
+ );
2507
+ const destinationsResult = await callReadOnlyTool(
2508
+ "list_meta_destinations",
2509
+ {},
2510
+ );
2511
+ state.destinations = parseDestinationsResult(destinationsResult);
2512
+ renderDestinations();
2513
+ } catch (error) {
2514
+ const expired =
2515
+ sessionExpired() ||
2516
+ (error && /expired|expirad/i.test(String(error.message)));
2517
+ const toolsUnavailable =
2518
+ error && error.message === "host_tools_unavailable";
2519
+ const message = expired
2520
+ ? "La sesión está expirada. Abre el enlace seguro para solicitar una nueva."
2521
+ : toolsUnavailable
2522
+ ? "host_tools_unavailable: este host no permite comprobar el staging desde la App. Usa el enlace seguro."
2523
+ : "No se pudo comprobar la sesión. Conserva el enlace y vuelve a intentarlo.";
2524
+ requireFallback(message, "danger", expired);
2525
+ elements.pendingRefresh.hidden = true;
2526
+ } finally {
2527
+ elements.checkStatus.disabled = false;
2528
+ elements.pendingRefresh.hidden = true;
2529
+ elements.pendingRefresh.disabled = false;
2530
+ }
2531
+ }
2532
+
2533
+ async function callReadOnlyTool(name, args) {
2534
+ if (
2535
+ !["check_meta_media_upload", "list_meta_destinations"].includes(
2536
+ name,
2537
+ )
2538
+ ) {
2539
+ throw new Error("tool_not_allowed");
2540
+ }
2541
+ const bridge = openAiBridge();
2542
+ let result;
2543
+ if (bridge && typeof bridge.callTool === "function") {
2544
+ try {
2545
+ result = await bridge.callTool(name, args);
2546
+ } catch (error) {
2547
+ if (!state.hostCanServerTools) throw error;
2548
+ result = await rpcRequest("tools/call", {
2549
+ name,
2550
+ arguments: args,
2551
+ });
2552
+ }
2553
+ } else if (state.hostCanServerTools) {
2554
+ result = await rpcRequest("tools/call", { name, arguments: args });
2555
+ } else {
2556
+ throw new Error("host_tools_unavailable");
2557
+ }
2558
+ if (!isRecord(result) || result.isError === true)
2559
+ throw new Error("tool_call_failed");
2560
+ return result;
2561
+ }
2562
+
2563
+ function resultPayload(result) {
2564
+ if (isRecord(result.structuredContent))
2565
+ return result.structuredContent;
2566
+ if (!Array.isArray(result.content))
2567
+ throw new Error("missing_tool_result");
2568
+ const text = result.content
2569
+ .filter(
2570
+ (part) =>
2571
+ isRecord(part) &&
2572
+ part.type === "text" &&
2573
+ typeof part.text === "string",
2574
+ )
2575
+ .map((part) => part.text)
2576
+ .join("\n")
2577
+ .slice(0, TOOL_RESULT_TEXT_LIMIT);
2578
+ const parsed = JSON.parse(text);
2579
+ if (!isRecord(parsed)) throw new Error("invalid_tool_result");
2580
+ return parsed;
2581
+ }
2582
+
2583
+ function parseStatusResult(result) {
2584
+ const payload = resultPayload(result);
2585
+ const status = payload.status;
2586
+ if (!["pending", "uploaded"].includes(status))
2587
+ throw new Error("invalid_status");
2588
+ const assets = Array.isArray(payload.assets)
2589
+ ? payload.assets.slice(0, MAX_FILES).flatMap((asset) => {
2590
+ if (!isRecord(asset)) return [];
2591
+ const stagingAssetId = boundedString(asset.stagingAssetId, 300);
2592
+ const mediaKind = asset.mediaKind;
2593
+ return stagingAssetId && ["image", "video"].includes(mediaKind)
2594
+ ? [{ stagingAssetId, mediaKind }]
2595
+ : [];
2596
+ })
2597
+ : [];
2598
+ let legacyKindAmbiguous = false;
2599
+ const legacyStagingAssetId = boundedString(
2600
+ payload.stagingAssetId,
2601
+ 300,
2602
+ );
2603
+ if (!assets.length && legacyStagingAssetId) {
2604
+ const localKinds = new Set(
2605
+ state.items
2606
+ .filter((item) => item.status === "uploaded")
2607
+ .map((item) => item.mediaKind)
2608
+ .filter((mediaKind) => ["image", "video"].includes(mediaKind)),
2609
+ );
2610
+ const inferredMediaKind =
2611
+ localKinds.size === 1
2612
+ ? Array.from(localKinds)[0]
2613
+ : localKinds.size === 0 &&
2614
+ ["image", "video"].includes(state.session.mediaKind)
2615
+ ? state.session.mediaKind
2616
+ : null;
2617
+ if (inferredMediaKind) {
2618
+ assets.push({
2619
+ stagingAssetId: legacyStagingAssetId,
2620
+ mediaKind: inferredMediaKind,
2621
+ });
2622
+ } else {
2623
+ legacyKindAmbiguous = true;
2624
+ }
2625
+ }
2626
+ return { status, assets, legacyKindAmbiguous };
2627
+ }
2628
+
2629
+ function parseDestinationsResult(result) {
2630
+ const payload = resultPayload(result);
2631
+ return {
2632
+ portfolios: parseDestinationList(payload.portfolios, (entry) => ({
2633
+ id: boundedString(entry.id, 300),
2634
+ name: boundedString(entry.name, 300) || "Portfolio sin nombre",
2635
+ })),
2636
+ adAccounts: parseDestinationList(payload.adAccounts, (entry) => ({
2637
+ id: boundedString(entry.id, 300),
2638
+ name: boundedString(entry.name, 300) || "Cuenta sin nombre",
2639
+ currency: boundedString(entry.currency, 20),
2640
+ portfolioId: boundedString(entry.portfolioId, 300),
2641
+ accountStatus: Number(entry.accountStatus),
2642
+ })).filter(
2643
+ (entry) => entry.portfolioId && entry.accountStatus === 1,
2644
+ ),
2645
+ pages: parseDestinationList(payload.pages, (entry) => ({
2646
+ id: boundedString(entry.id, 300),
2647
+ name: boundedString(entry.name, 300) || "Página sin nombre",
2648
+ })),
2649
+ };
2650
+ }
2651
+
2652
+ function parseDestinationList(value, mapper) {
2653
+ if (!Array.isArray(value)) return [];
2654
+ return value.slice(0, 1000).flatMap((entry) => {
2655
+ if (!isRecord(entry)) return [];
2656
+ const mapped = mapper(entry);
2657
+ return mapped.id ? [mapped] : [];
2658
+ });
2659
+ }
2660
+
2661
+ function renderDestinations() {
2662
+ fillSelect(
2663
+ elements.portfolio,
2664
+ state.destinations.portfolios,
2665
+ "Portfolio",
2666
+ (entry) => entry.name,
2667
+ );
2668
+ fillSelect(
2669
+ elements.page,
2670
+ state.destinations.pages,
2671
+ "Facebook Page",
2672
+ (entry) => entry.name,
2673
+ );
2674
+ renderAdAccounts();
2675
+ elements.destinationSection.hidden = false;
2676
+ updateDestinationSubmit();
2677
+ scheduleIntrinsicSizeReport(true);
2678
+ }
2679
+
2680
+ function renderAdAccounts() {
2681
+ const portfolioId = elements.portfolio.value;
2682
+ const accounts = portfolioId
2683
+ ? state.destinations.adAccounts.filter(
2684
+ (account) => account.portfolioId === portfolioId,
2685
+ )
2686
+ : [];
2687
+ fillSelect(
2688
+ elements.adAccount,
2689
+ accounts,
2690
+ portfolioId ? "Ad account" : "Ad account",
2691
+ (entry) =>
2692
+ `${entry.name}${entry.currency ? ` · ${entry.currency}` : ""}`,
2693
+ );
2694
+ elements.adAccount.disabled = !portfolioId || !accounts.length;
2695
+ }
2696
+
2697
+ function fillSelect(select, entries, placeholder, labelFor) {
2698
+ const fragment = document.createDocumentFragment();
2699
+ const empty = document.createElement("option");
2700
+ empty.value = "";
2701
+ empty.textContent = placeholder;
2702
+ fragment.append(empty);
2703
+ for (const entry of entries) {
2704
+ const option = document.createElement("option");
2705
+ option.value = entry.id;
2706
+ option.textContent = labelFor(entry);
2707
+ fragment.append(option);
2708
+ }
2709
+ select.replaceChildren(fragment);
2710
+ select.value = "";
2711
+ }
2712
+
2713
+ function updateDestinationSubmit() {
2714
+ elements.submitDestination.disabled = !(
2715
+ elements.portfolio.value &&
2716
+ elements.adAccount.value &&
2717
+ elements.page.value &&
2718
+ state.assets.length
2719
+ );
2720
+ }
2721
+
2722
+ function exactHandoff() {
2723
+ return JSON.stringify({
2724
+ sessionId: state.session.sessionId,
2725
+ portfolioId: elements.portfolio.value,
2726
+ adAccountId: elements.adAccount.value,
2727
+ pageId: elements.page.value,
2728
+ assets: state.assets.slice(0, MAX_FILES).map((asset) => ({
2729
+ stagingAssetId: asset.stagingAssetId,
2730
+ mediaKind: asset.mediaKind,
2731
+ })),
2732
+ });
2733
+ }
2734
+
2735
+ async function submitDestination() {
2736
+ if (elements.submitDestination.disabled || !state.session) return;
2737
+ const handoff = exactHandoff();
2738
+ elements.submitDestination.disabled = true;
2739
+ try {
2740
+ const bridge = openAiBridge();
2741
+ if (bridge && typeof bridge.sendFollowUpMessage === "function") {
2742
+ await bridge.sendFollowUpMessage({ prompt: handoff });
2743
+ setSessionStatus(
2744
+ "Selección enviada al chat. Espera la confirmación antes de continuar.",
2745
+ "success",
2746
+ );
2747
+ return;
2748
+ }
2749
+ if (state.hostCanMessage) {
2750
+ await rpcRequest("ui/message", {
2751
+ role: "user",
2752
+ content: [{ type: "text", text: handoff }],
2753
+ });
2754
+ setSessionStatus(
2755
+ "Selección enviada al chat. Espera la confirmación antes de continuar.",
2756
+ "success",
2757
+ );
2758
+ return;
2759
+ }
2760
+ showCopyableHandoff(handoff);
2761
+ } catch {
2762
+ showCopyableHandoff(handoff);
2763
+ } finally {
2764
+ updateDestinationSubmit();
2765
+ }
2766
+ }
2767
+
2768
+ function showCopyableHandoff(handoff) {
2769
+ elements.handoffCopy.value = handoff;
2770
+ elements.handoffSection.hidden = false;
2771
+ setSessionStatus(
2772
+ "Copia el handoff exacto y pégalo en el chat.",
2773
+ "warning",
2774
+ );
2775
+ scheduleIntrinsicSizeReport(true);
2776
+ }
2777
+
2778
+ async function copyHandoff() {
2779
+ const handoff = elements.handoffCopy.value;
2780
+ if (!handoff) return;
2781
+ if (
2782
+ navigator.clipboard &&
2783
+ typeof navigator.clipboard.writeText === "function"
2784
+ ) {
2785
+ try {
2786
+ await navigator.clipboard.writeText(handoff);
2787
+ elements.copyButton.textContent = "Copiado";
2788
+ return;
2789
+ } catch {
2790
+ // Selection fallback below.
2791
+ }
2792
+ }
2793
+ elements.handoffCopy.focus();
2794
+ elements.handoffCopy.select();
2795
+ elements.copyButton.textContent = "Seleccionado para copiar";
2796
+ }
2797
+
2798
+ document.addEventListener("DOMContentLoaded", initialize, {
2799
+ once: true,
2800
+ });
2801
+ if (document.readyState !== "loading") initialize();
2802
+ })();
2803
+ </script>
2804
+ </body>
2805
+ </html>