@productmaker/mcp 2.9.2 → 2.9.4

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.
@@ -66,21 +66,16 @@
66
66
  }
67
67
 
68
68
  button,
69
- input,
70
- select,
71
- textarea {
69
+ input {
72
70
  font: inherit;
73
71
  }
74
72
 
75
73
  button,
76
- select,
77
74
  a {
78
75
  -webkit-tap-highlight-color: transparent;
79
76
  }
80
77
 
81
78
  button:focus-visible,
82
- select:focus-visible,
83
- textarea:focus-visible,
84
79
  a:focus-visible,
85
80
  [tabindex]:focus-visible {
86
81
  outline: 3px solid
@@ -102,73 +97,10 @@
102
97
  box-shadow: var(--pm-shadow);
103
98
  }
104
99
 
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 {
100
+ .upload-feedback {
137
101
  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;
102
+ gap: 8px;
103
+ margin-top: 10px;
172
104
  }
173
105
 
174
106
  .fallback-link {
@@ -212,20 +144,23 @@
212
144
  }
213
145
 
214
146
  .section {
215
- padding: 12px 22px 22px;
147
+ padding: 18px;
216
148
  }
217
149
 
218
- .section + .section {
219
- border-top: 1px solid var(--pm-line);
150
+ .media-grid {
151
+ display: grid;
152
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
153
+ gap: 12px;
154
+ align-items: start;
220
155
  }
221
156
 
222
157
  .dropzone {
223
158
  display: grid;
224
- min-height: 260px;
159
+ aspect-ratio: 1;
160
+ min-width: 0;
225
161
  place-items: center;
226
- padding: 20px;
227
162
  border: 2px dashed var(--pm-line);
228
- border-radius: 21px;
163
+ border-radius: 16px;
229
164
  background: color-mix(
230
165
  in srgb,
231
166
  var(--pm-card-muted) 62%,
@@ -251,13 +186,11 @@
251
186
 
252
187
  .drop-trigger {
253
188
  display: grid;
254
- width: min(100%, 280px);
255
- min-height: 190px;
189
+ width: 100%;
190
+ height: 100%;
256
191
  place-items: center;
257
- align-content: center;
258
- gap: 16px;
259
192
  border: 0;
260
- border-radius: 18px;
193
+ border-radius: 14px;
261
194
  background: transparent;
262
195
  color: var(--pm-text);
263
196
  cursor: pointer;
@@ -267,26 +200,29 @@
267
200
  cursor: not-allowed;
268
201
  }
269
202
 
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%);
203
+ .add-symbol {
279
204
  color: var(--pm-text);
280
- font-size: 31px;
281
- font-weight: 350;
205
+ font-size: 44px;
206
+ font-weight: 300;
282
207
  line-height: 1;
283
208
  }
284
209
 
285
- .drop-title {
286
- margin: 0;
287
- font-size: 18px;
288
- font-weight: 520;
289
- letter-spacing: -0.02em;
210
+ .complete-button {
211
+ justify-self: start;
212
+ min-width: 76px;
213
+ margin-top: 10px;
214
+ padding: 10px 20px;
215
+ border: 0;
216
+ border-radius: 12px;
217
+ background: var(--pm-primary);
218
+ color: #ffffff;
219
+ cursor: pointer;
220
+ font-weight: 780;
221
+ }
222
+
223
+ .complete-button:disabled {
224
+ cursor: not-allowed;
225
+ opacity: 0.5;
290
226
  }
291
227
 
292
228
  .drop-help {
@@ -309,47 +245,6 @@
309
245
  clip-path: inset(50%);
310
246
  }
311
247
 
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
248
  .batch-error {
354
249
  display: none;
355
250
  margin: 10px 0 0;
@@ -362,10 +257,7 @@
362
257
  }
363
258
 
364
259
  .file-list {
365
- display: grid;
366
- grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
367
- gap: 12px;
368
- margin-top: 14px;
260
+ display: contents;
369
261
  }
370
262
 
371
263
  .file-card {
@@ -501,64 +393,11 @@
501
393
  transition: width 120ms linear;
502
394
  }
503
395
 
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
396
  .compatibility-control {
547
397
  /* Retained for older hosts; recovery is visible only through the secure fallback. */
548
398
  display: none !important;
549
399
  }
550
400
 
551
- .sr-only {
552
- position: absolute;
553
- width: 1px;
554
- height: 1px;
555
- padding: 0;
556
- overflow: hidden;
557
- clip: rect(0, 0, 0, 0);
558
- white-space: nowrap;
559
- border: 0;
560
- }
561
-
562
401
  [hidden] {
563
402
  display: none !important;
564
403
  }
@@ -572,23 +411,13 @@
572
411
  border-radius: 20px;
573
412
  }
574
413
 
575
- .hero,
576
414
  .section {
577
415
  padding-right: 14px;
578
416
  padding-left: 14px;
579
417
  }
580
418
 
581
- .dropzone {
582
- min-height: 230px;
583
- }
584
-
585
- .destination-grid {
586
- grid-template-columns: 1fr;
587
- }
588
-
589
- .destination-actions .button,
590
- .handoff-actions .button {
591
- width: 100%;
419
+ .media-grid {
420
+ grid-template-columns: repeat(2, minmax(0, 1fr));
592
421
  }
593
422
  }
594
423
 
@@ -605,71 +434,46 @@
605
434
  <body>
606
435
  <main class="app" data-testid="meta-upload-root">
607
436
  <article class="card">
608
- <header class="hero">
609
- <div class="upload-header">
610
- <p class="upload-title" data-testid="meta-upload-title">
611
- Upload image
612
- </p>
613
- <span class="brand-mark" aria-label="ProductMaker" role="img"
614
- >↗</span
615
- >
616
- </div>
617
- <div class="session-row">
618
- <code class="session-id" data-testid="meta-upload-session-id" hidden
619
- >Esperando sesión…</code
620
- >
621
- <a
622
- class="fallback-link"
623
- data-testid="meta-upload-fallback-link"
624
- href="#"
625
- target="_blank"
626
- rel="noopener noreferrer"
627
- hidden
628
- >Abrir carga segura en otra pestaña</a
629
- >
630
- </div>
631
- <p
632
- class="status"
633
- data-testid="meta-upload-session-status"
634
- aria-live="polite"
635
- ></p>
636
- </header>
637
-
638
437
  <section class="section" aria-label="Upload media">
639
- <div
640
- class="dropzone"
641
- data-testid="meta-upload-dropzone"
642
- data-dragging="false"
643
- aria-disabled="true"
644
- aria-describedby="meta-upload-constraints"
645
- >
646
- <button
647
- class="drop-trigger"
648
- type="button"
649
- data-testid="meta-upload-picker-btn"
650
- disabled
438
+ <div class="media-grid">
439
+ <div
440
+ class="file-list"
441
+ data-testid="meta-upload-file-list"
442
+ aria-live="polite"
443
+ ></div>
444
+ <div
445
+ class="dropzone"
446
+ data-testid="meta-upload-dropzone"
447
+ data-dragging="false"
448
+ aria-disabled="true"
449
+ aria-describedby="meta-upload-constraints"
651
450
  >
652
- <span class="drop-icon" aria-hidden="true">+</span>
653
- <span class="drop-title" data-testid="meta-upload-drop-title"
654
- >Upload image</span
451
+ <button
452
+ class="drop-trigger"
453
+ type="button"
454
+ data-testid="meta-upload-add-tile"
455
+ aria-label="Agregar otro archivo"
456
+ disabled
655
457
  >
656
- </button>
657
- <button
658
- class="compatibility-control"
659
- type="button"
660
- data-testid="meta-upload-library-btn"
661
- hidden
662
- disabled
663
- aria-label="Choose from ChatGPT library"
664
- ></button>
665
- <input
666
- class="native-input"
667
- data-testid="meta-upload-input"
668
- type="file"
669
- accept="image/jpeg,image/png,image/webp,video/mp4,video/quicktime,video/webm"
670
- multiple
671
- disabled
672
- />
458
+ <span class="add-symbol" aria-hidden="true">+</span>
459
+ </button>
460
+ <button
461
+ class="compatibility-control"
462
+ type="button"
463
+ data-testid="meta-upload-library-btn"
464
+ hidden
465
+ disabled
466
+ aria-label="Choose from ChatGPT library"
467
+ ></button>
468
+ <input
469
+ class="native-input"
470
+ data-testid="meta-upload-input"
471
+ type="file"
472
+ accept="image/jpeg,image/png,image/webp,video/mp4,video/quicktime,video/webm"
473
+ multiple
474
+ disabled
475
+ />
476
+ </div>
673
477
  </div>
674
478
  <p class="drop-help" id="meta-upload-constraints">
675
479
  JPEG, PNG, WebP, MP4, MOV o WebM. Hasta 10 archivos de 350 MB; las
@@ -681,77 +485,31 @@
681
485
  role="alert"
682
486
  aria-live="assertive"
683
487
  ></p>
684
- <div
685
- class="file-list"
686
- data-testid="meta-upload-file-list"
687
- aria-live="polite"
688
- ></div>
689
- </section>
690
-
691
- <section
692
- class="section"
693
- data-testid="meta-upload-destination-section"
694
- aria-label="Meta destination"
695
- hidden
696
- >
697
- <div class="destination-grid">
698
- <div class="field">
699
- <label class="sr-only" for="portfolio-select">Portfolio</label>
700
- <select
701
- id="portfolio-select"
702
- data-testid="meta-upload-portfolio-select"
703
- >
704
- <option value="">Portfolio</option>
705
- </select>
706
- </div>
707
- <div class="field">
708
- <label class="sr-only" for="ad-account-select">Ad account</label>
709
- <select
710
- id="ad-account-select"
711
- data-testid="meta-upload-ad-account-select"
712
- disabled
713
- >
714
- <option value="">Ad account</option>
715
- </select>
716
- </div>
717
- <div class="field">
718
- <label class="sr-only" for="page-select">Facebook Page</label>
719
- <select id="page-select" data-testid="meta-upload-page-select">
720
- <option value="">Facebook Page</option>
721
- </select>
722
- </div>
723
- </div>
724
- <div class="destination-actions">
725
- <button
726
- class="button"
727
- type="button"
728
- data-testid="meta-upload-submit-destination"
729
- disabled
730
- >
731
- Continue
732
- </button>
733
- </div>
734
- </section>
735
-
736
- <section
737
- class="section"
738
- data-testid="meta-upload-handoff-section"
739
- aria-labelledby="handoff-heading"
740
- hidden
741
- >
742
- <p id="handoff-heading" class="section-copy muted">
743
- Copy this handoff into the chat.
744
- </p>
745
- <textarea data-testid="meta-upload-handoff-copy" readonly></textarea>
746
- <div class="handoff-actions">
747
- <button
748
- class="button secondary"
749
- type="button"
750
- data-testid="meta-upload-copy-btn"
488
+ <div class="upload-feedback">
489
+ <a
490
+ class="fallback-link"
491
+ data-testid="meta-upload-fallback-link"
492
+ href="#"
493
+ target="_blank"
494
+ rel="noopener noreferrer"
495
+ hidden
496
+ >Abrir carga segura en otra pestaña</a
751
497
  >
752
- Copy handoff
753
- </button>
498
+ <p
499
+ class="status"
500
+ data-testid="meta-upload-session-status"
501
+ aria-live="polite"
502
+ ></p>
754
503
  </div>
504
+ <button
505
+ class="complete-button"
506
+ type="button"
507
+ data-testid="meta-upload-complete-btn"
508
+ hidden
509
+ disabled
510
+ >
511
+ OK
512
+ </button>
755
513
  </section>
756
514
  </article>
757
515
  </main>
@@ -788,10 +546,6 @@
788
546
 
789
547
  const elements = {
790
548
  root: document.querySelector('[data-testid="meta-upload-root"]'),
791
- title: document.querySelector('[data-testid="meta-upload-title"]'),
792
- sessionId: document.querySelector(
793
- '[data-testid="meta-upload-session-id"]',
794
- ),
795
549
  fallbackLink: document.querySelector(
796
550
  '[data-testid="meta-upload-fallback-link"]',
797
551
  ),
@@ -801,11 +555,8 @@
801
555
  dropzone: document.querySelector(
802
556
  '[data-testid="meta-upload-dropzone"]',
803
557
  ),
804
- dropTitle: document.querySelector(
805
- '[data-testid="meta-upload-drop-title"]',
806
- ),
807
558
  pickerButton: document.querySelector(
808
- '[data-testid="meta-upload-picker-btn"]',
559
+ '[data-testid="meta-upload-add-tile"]',
809
560
  ),
810
561
  libraryPickerButton: document.querySelector(
811
562
  '[data-testid="meta-upload-library-btn"]',
@@ -814,33 +565,12 @@
814
565
  batchError: document.querySelector(
815
566
  '[data-testid="meta-upload-batch-error"]',
816
567
  ),
568
+ completeButton: document.querySelector(
569
+ '[data-testid="meta-upload-complete-btn"]',
570
+ ),
817
571
  fileList: document.querySelector(
818
572
  '[data-testid="meta-upload-file-list"]',
819
573
  ),
820
- destinationSection: document.querySelector(
821
- '[data-testid="meta-upload-destination-section"]',
822
- ),
823
- portfolio: document.querySelector(
824
- '[data-testid="meta-upload-portfolio-select"]',
825
- ),
826
- adAccount: document.querySelector(
827
- '[data-testid="meta-upload-ad-account-select"]',
828
- ),
829
- page: document.querySelector(
830
- '[data-testid="meta-upload-page-select"]',
831
- ),
832
- submitDestination: document.querySelector(
833
- '[data-testid="meta-upload-submit-destination"]',
834
- ),
835
- handoffSection: document.querySelector(
836
- '[data-testid="meta-upload-handoff-section"]',
837
- ),
838
- handoffCopy: document.querySelector(
839
- '[data-testid="meta-upload-handoff-copy"]',
840
- ),
841
- copyButton: document.querySelector(
842
- '[data-testid="meta-upload-copy-btn"]',
843
- ),
844
574
  };
845
575
 
846
576
  const state = {
@@ -859,7 +589,8 @@
859
589
  toolInput: {},
860
590
  items: [],
861
591
  assets: [],
862
- destinations: { portfolios: [], adAccounts: [], pages: [] },
592
+ completionNotified: false,
593
+ completionNotificationInFlight: false,
863
594
  consumedBridgeIds: new Set(),
864
595
  consumingBridgeFiles: false,
865
596
  bridgeConsumptionQueued: false,
@@ -907,29 +638,6 @@
907
638
  return null;
908
639
  }
909
640
 
910
- function defaultUploadTitle() {
911
- return state.session?.mediaKind === "video"
912
- ? "Upload video"
913
- : "Upload image";
914
- }
915
-
916
- function updateUploadTitle() {
917
- const namedItems = state.items.filter(
918
- (item) =>
919
- (item.file && typeof item.file.name === "string") ||
920
- (typeof item.displayName === "string" &&
921
- item.displayName.length > 0),
922
- );
923
- const title =
924
- namedItems.length === 1
925
- ? namedItems[0].file?.name || namedItems[0].displayName
926
- : namedItems.length > 1
927
- ? `${namedItems.length} files`
928
- : defaultUploadTitle();
929
- elements.title.textContent = title;
930
- elements.dropTitle.textContent = defaultUploadTitle();
931
- }
932
-
933
641
  function safeSessionLimit() {
934
642
  const proposed =
935
643
  state.session && Number.isInteger(state.session.maxFiles)
@@ -955,6 +663,38 @@
955
663
  scheduleIntrinsicSizeReport(true);
956
664
  }
957
665
 
666
+ function canMessageHost() {
667
+ return (
668
+ hasOpenAiFunction("sendFollowUpMessage") || state.hostCanMessage
669
+ );
670
+ }
671
+
672
+ function canCompleteUpload() {
673
+ return (
674
+ !state.disposed &&
675
+ Boolean(state.session) &&
676
+ !sessionExpired() &&
677
+ !state.busyBatch &&
678
+ !state.completionNotified &&
679
+ !state.completionNotificationInFlight &&
680
+ state.assets.length > 0 &&
681
+ state.items.length > 0 &&
682
+ state.assets.length === state.items.length &&
683
+ state.items.every((item) => item.status === "uploaded") &&
684
+ canMessageHost()
685
+ );
686
+ }
687
+
688
+ function renderCompletionAction() {
689
+ const hasVerifiedAssets = state.assets.length > 0;
690
+ elements.completeButton.hidden =
691
+ state.completionNotified || !hasVerifiedAssets || !canMessageHost();
692
+ elements.completeButton.disabled = !canCompleteUpload();
693
+ elements.completeButton.textContent =
694
+ state.completionNotificationInFlight ? "…" : "OK";
695
+ scheduleIntrinsicSizeReport(true);
696
+ }
697
+
958
698
  function parseFinitePositiveNumber(value) {
959
699
  return typeof value === "number" &&
960
700
  Number.isFinite(value) &&
@@ -1173,6 +913,8 @@
1173
913
  !state.session ||
1174
914
  sessionExpired() ||
1175
915
  state.uploadBlocked ||
916
+ state.completionNotified ||
917
+ state.completionNotificationInFlight ||
1176
918
  state.busyBatch ||
1177
919
  state.items.length >= safeSessionLimit();
1178
920
  elements.input.disabled = disabled;
@@ -1262,24 +1004,6 @@
1262
1004
  void processLocalFiles(files);
1263
1005
  });
1264
1006
 
1265
- elements.portfolio.addEventListener("change", () => {
1266
- renderAdAccounts();
1267
- updateDestinationSubmit();
1268
- });
1269
- elements.adAccount.addEventListener(
1270
- "change",
1271
- updateDestinationSubmit,
1272
- );
1273
- elements.page.addEventListener("change", updateDestinationSubmit);
1274
- elements.submitDestination.addEventListener(
1275
- "click",
1276
- () => void submitDestination(),
1277
- );
1278
- elements.copyButton.addEventListener(
1279
- "click",
1280
- () => void copyHandoff(),
1281
- );
1282
-
1283
1007
  state.hostMessageHandler = handleHostMessage;
1284
1008
  window.addEventListener("message", state.hostMessageHandler);
1285
1009
  }
@@ -1411,17 +1135,28 @@
1411
1135
  const sessionChanged =
1412
1136
  !state.session || state.session.sessionId !== session.sessionId;
1413
1137
  if (sessionChanged) {
1138
+ abortActiveWork();
1139
+ for (const item of state.items) revokePreviewUrl(item);
1140
+ state.items = [];
1141
+ state.assets = [];
1142
+ state.consumedBridgeIds.clear();
1143
+ state.consumingBridgeFiles = false;
1144
+ state.bridgeConsumptionQueued = false;
1145
+ state.ingestionQueue = Promise.resolve();
1146
+ state.busyBatch = false;
1147
+ elements.input.value = "";
1148
+ renderItems();
1414
1149
  state.uploadBlocked = false;
1415
1150
  state.fallbackRequired = false;
1151
+ state.completionNotified = false;
1152
+ state.completionNotificationInFlight = false;
1416
1153
  state.workCancelled = false;
1417
1154
  if (state.abortController?.signal.aborted) {
1418
1155
  state.abortController = null;
1419
1156
  }
1420
1157
  }
1421
1158
  state.session = session;
1422
- elements.sessionId.textContent = session.sessionId;
1423
1159
  elements.fallbackLink.href = session.uploadPageUrl;
1424
- updateUploadTitle();
1425
1160
  if (sessionExpired()) {
1426
1161
  requireFallback(
1427
1162
  "La sesión está expirada. Abre el enlace seguro para solicitar una nueva.",
@@ -1433,6 +1168,7 @@
1433
1168
  setSessionStatus("Sesión lista para recibir archivos.", "neutral");
1434
1169
  }
1435
1170
  setPickerAvailability();
1171
+ renderCompletionAction();
1436
1172
  void consumePendingBridgeFiles();
1437
1173
  scheduleIntrinsicSizeReport(true);
1438
1174
  }
@@ -1548,7 +1284,9 @@
1548
1284
  state.disposed ||
1549
1285
  !state.session ||
1550
1286
  sessionExpired() ||
1551
- state.uploadBlocked
1287
+ state.uploadBlocked ||
1288
+ state.completionNotified ||
1289
+ state.completionNotificationInFlight
1552
1290
  )
1553
1291
  return;
1554
1292
  state.workCancelled = false;
@@ -1567,7 +1305,9 @@
1567
1305
  state.disposed ||
1568
1306
  !state.session ||
1569
1307
  sessionExpired() ||
1570
- state.uploadBlocked
1308
+ state.uploadBlocked ||
1309
+ state.completionNotified ||
1310
+ state.completionNotificationInFlight
1571
1311
  )
1572
1312
  return;
1573
1313
  const bridge = openAiBridge();
@@ -1741,6 +1481,8 @@
1741
1481
  !state.session ||
1742
1482
  sessionExpired() ||
1743
1483
  state.uploadBlocked ||
1484
+ state.completionNotified ||
1485
+ state.completionNotificationInFlight ||
1744
1486
  state.busyBatch
1745
1487
  )
1746
1488
  return;
@@ -1760,7 +1502,10 @@
1760
1502
  if (!selected.length) return;
1761
1503
 
1762
1504
  state.busyBatch = true;
1505
+ state.assets = [];
1506
+ state.completionNotified = false;
1763
1507
  setPickerAvailability();
1508
+ renderCompletionAction();
1764
1509
  for (const file of selected) {
1765
1510
  const mediaKind = mediaKindForType(file.type) || "image";
1766
1511
  const item = createItem(file, mediaKind);
@@ -1770,6 +1515,7 @@
1770
1515
  }
1771
1516
  state.busyBatch = false;
1772
1517
  setPickerAvailability();
1518
+ renderCompletionAction();
1773
1519
  if (state.items.some((item) => item.status === "uploaded")) {
1774
1520
  await refreshSessionStatus();
1775
1521
  }
@@ -2415,7 +2161,7 @@
2415
2161
  fragment.append(card);
2416
2162
  });
2417
2163
  elements.fileList.replaceChildren(fragment);
2418
- updateUploadTitle();
2164
+ renderCompletionAction();
2419
2165
  scheduleIntrinsicSizeReport(true);
2420
2166
  }
2421
2167
 
@@ -2429,13 +2175,21 @@
2429
2175
 
2430
2176
  async function refreshSessionStatus() {
2431
2177
  if (!state.session) return;
2178
+ const requestedSessionId = state.session.sessionId;
2432
2179
  setSessionStatus("Comprobando el staging privado…", "neutral");
2433
2180
  try {
2434
2181
  const result = await callReadOnlyTool("check_meta_media_upload", {
2435
- sessionId: state.session.sessionId,
2182
+ sessionId: requestedSessionId,
2436
2183
  });
2437
- const status = parseStatusResult(result);
2184
+ if (
2185
+ state.disposed ||
2186
+ !state.session ||
2187
+ state.session.sessionId !== requestedSessionId
2188
+ )
2189
+ return;
2190
+ const status = parseStatusResult(result, requestedSessionId);
2438
2191
  state.assets = status.assets;
2192
+ renderCompletionAction();
2439
2193
  if (!status.assets.length) {
2440
2194
  if (status.legacyKindAmbiguous) {
2441
2195
  requireFallback(
@@ -2448,7 +2202,6 @@
2448
2202
  "warning",
2449
2203
  );
2450
2204
  }
2451
- elements.destinationSection.hidden = true;
2452
2205
  return;
2453
2206
  }
2454
2207
 
@@ -2467,15 +2220,9 @@
2467
2220
  setSessionStatus(
2468
2221
  partial
2469
2222
  ? "Carga parcial verificada. Continuaremos solo con los assets listos."
2470
- : "Carga verificada. Elige ahora el destino de Meta.",
2223
+ : "Carga completada.",
2471
2224
  partial ? "warning" : "success",
2472
2225
  );
2473
- const destinationsResult = await callReadOnlyTool(
2474
- "list_meta_destinations",
2475
- {},
2476
- );
2477
- state.destinations = parseDestinationsResult(destinationsResult);
2478
- renderDestinations();
2479
2226
  } catch (error) {
2480
2227
  const expired =
2481
2228
  sessionExpired() ||
@@ -2492,11 +2239,7 @@
2492
2239
  }
2493
2240
 
2494
2241
  async function callReadOnlyTool(name, args) {
2495
- if (
2496
- !["check_meta_media_upload", "list_meta_destinations"].includes(
2497
- name,
2498
- )
2499
- ) {
2242
+ if (name !== "check_meta_media_upload") {
2500
2243
  throw new Error("tool_not_allowed");
2501
2244
  }
2502
2245
  const bridge = openAiBridge();
@@ -2541,8 +2284,10 @@
2541
2284
  return parsed;
2542
2285
  }
2543
2286
 
2544
- function parseStatusResult(result) {
2287
+ function parseStatusResult(result, expectedSessionId) {
2545
2288
  const payload = resultPayload(result);
2289
+ if (payload.sessionId !== expectedSessionId)
2290
+ throw new Error("status_session_mismatch");
2546
2291
  const status = payload.status;
2547
2292
  if (!["pending", "uploaded"].includes(status))
2548
2293
  throw new Error("invalid_status");
@@ -2587,175 +2332,84 @@
2587
2332
  return { status, assets, legacyKindAmbiguous };
2588
2333
  }
2589
2334
 
2590
- function parseDestinationsResult(result) {
2591
- const payload = resultPayload(result);
2592
- return {
2593
- portfolios: parseDestinationList(payload.portfolios, (entry) => ({
2594
- id: boundedString(entry.id, 300),
2595
- name: boundedString(entry.name, 300) || "Portfolio sin nombre",
2596
- })),
2597
- adAccounts: parseDestinationList(payload.adAccounts, (entry) => ({
2598
- id: boundedString(entry.id, 300),
2599
- name: boundedString(entry.name, 300) || "Cuenta sin nombre",
2600
- currency: boundedString(entry.currency, 20),
2601
- portfolioId: boundedString(entry.portfolioId, 300),
2602
- accountStatus: Number(entry.accountStatus),
2603
- })).filter(
2604
- (entry) => entry.portfolioId && entry.accountStatus === 1,
2605
- ),
2606
- pages: parseDestinationList(payload.pages, (entry) => ({
2607
- id: boundedString(entry.id, 300),
2608
- name: boundedString(entry.name, 300) || "Página sin nombre",
2609
- })),
2610
- };
2611
- }
2612
-
2613
- function parseDestinationList(value, mapper) {
2614
- if (!Array.isArray(value)) return [];
2615
- return value.slice(0, 1000).flatMap((entry) => {
2616
- if (!isRecord(entry)) return [];
2617
- const mapped = mapper(entry);
2618
- return mapped.id ? [mapped] : [];
2619
- });
2620
- }
2621
-
2622
- function renderDestinations() {
2623
- fillSelect(
2624
- elements.portfolio,
2625
- state.destinations.portfolios,
2626
- "Portfolio",
2627
- (entry) => entry.name,
2628
- );
2629
- fillSelect(
2630
- elements.page,
2631
- state.destinations.pages,
2632
- "Facebook Page",
2633
- (entry) => entry.name,
2634
- );
2635
- renderAdAccounts();
2636
- elements.destinationSection.hidden = false;
2637
- updateDestinationSubmit();
2638
- scheduleIntrinsicSizeReport(true);
2639
- }
2640
-
2641
- function renderAdAccounts() {
2642
- const portfolioId = elements.portfolio.value;
2643
- const accounts = portfolioId
2644
- ? state.destinations.adAccounts.filter(
2645
- (account) => account.portfolioId === portfolioId,
2646
- )
2647
- : [];
2648
- fillSelect(
2649
- elements.adAccount,
2650
- accounts,
2651
- portfolioId ? "Ad account" : "Ad account",
2652
- (entry) =>
2653
- `${entry.name}${entry.currency ? ` · ${entry.currency}` : ""}`,
2654
- );
2655
- elements.adAccount.disabled = !portfolioId || !accounts.length;
2656
- }
2657
-
2658
- function fillSelect(select, entries, placeholder, labelFor) {
2659
- const fragment = document.createDocumentFragment();
2660
- const empty = document.createElement("option");
2661
- empty.value = "";
2662
- empty.textContent = placeholder;
2663
- fragment.append(empty);
2664
- for (const entry of entries) {
2665
- const option = document.createElement("option");
2666
- option.value = entry.id;
2667
- option.textContent = labelFor(entry);
2668
- fragment.append(option);
2669
- }
2670
- select.replaceChildren(fragment);
2671
- select.value = "";
2672
- }
2673
-
2674
- function updateDestinationSubmit() {
2675
- elements.submitDestination.disabled = !(
2676
- elements.portfolio.value &&
2677
- elements.adAccount.value &&
2678
- elements.page.value &&
2679
- state.assets.length
2680
- );
2681
- }
2682
-
2683
- function exactHandoff() {
2684
- return JSON.stringify({
2685
- sessionId: state.session.sessionId,
2686
- portfolioId: elements.portfolio.value,
2687
- adAccountId: elements.adAccount.value,
2688
- pageId: elements.page.value,
2689
- assets: state.assets.slice(0, MAX_FILES).map((asset) => ({
2690
- stagingAssetId: asset.stagingAssetId,
2691
- mediaKind: asset.mediaKind,
2692
- })),
2693
- });
2694
- }
2695
-
2696
- async function submitDestination() {
2697
- if (elements.submitDestination.disabled || !state.session) return;
2698
- const handoff = exactHandoff();
2699
- elements.submitDestination.disabled = true;
2335
+ async function notifyChatUploadComplete() {
2336
+ if (
2337
+ state.completionNotified ||
2338
+ state.completionNotificationInFlight ||
2339
+ !state.session
2340
+ )
2341
+ return;
2342
+ const completionSessionId = state.session.sessionId;
2343
+ state.completionNotificationInFlight = true;
2344
+ setPickerAvailability();
2345
+ renderCompletionAction();
2346
+ const assets = state.assets
2347
+ .map(
2348
+ (asset) => `${asset.mediaKind} asset_id ${asset.stagingAssetId}`,
2349
+ )
2350
+ .join(", ");
2351
+ const message = `The ProductMaker upload is complete. Continue my previous request using these uploaded assets: ${assets}. Do not ask me to upload them again. Do not publish anything until you have asked me for the Meta portfolio, ad account, and Facebook Page.`;
2700
2352
  try {
2701
2353
  const bridge = openAiBridge();
2702
2354
  if (bridge && typeof bridge.sendFollowUpMessage === "function") {
2703
- await bridge.sendFollowUpMessage({ prompt: handoff });
2704
- setSessionStatus(
2705
- "Selección enviada al chat. Espera la confirmación antes de continuar.",
2706
- "success",
2707
- );
2355
+ await bridge.sendFollowUpMessage({ prompt: message });
2356
+ if (
2357
+ !state.session ||
2358
+ state.session.sessionId !== completionSessionId
2359
+ )
2360
+ return;
2361
+ state.completionNotified = true;
2362
+ setPickerAvailability();
2363
+ renderCompletionAction();
2708
2364
  return;
2709
2365
  }
2710
2366
  if (state.hostCanMessage) {
2711
- await rpcRequest("ui/message", {
2367
+ const messageResult = await rpcRequest("ui/message", {
2712
2368
  role: "user",
2713
- content: [{ type: "text", text: handoff }],
2369
+ content: [{ type: "text", text: message }],
2714
2370
  });
2715
- setSessionStatus(
2716
- "Selección enviada al chat. Espera la confirmación antes de continuar.",
2717
- "success",
2718
- );
2371
+ if (isRecord(messageResult) && messageResult.isError === true)
2372
+ throw new Error("host_message_rejected");
2373
+ if (
2374
+ !state.session ||
2375
+ state.session.sessionId !== completionSessionId
2376
+ )
2377
+ return;
2378
+ state.completionNotified = true;
2379
+ setPickerAvailability();
2380
+ renderCompletionAction();
2719
2381
  return;
2720
2382
  }
2721
- showCopyableHandoff(handoff);
2383
+ setSessionStatus(
2384
+ "La carga terminó, pero este host no puede continuar el chat desde el widget. Vuelve al chat para seguir.",
2385
+ "warning",
2386
+ );
2722
2387
  } catch {
2723
- showCopyableHandoff(handoff);
2724
- } finally {
2725
- updateDestinationSubmit();
2726
- }
2727
- }
2728
-
2729
- function showCopyableHandoff(handoff) {
2730
- elements.handoffCopy.value = handoff;
2731
- elements.handoffSection.hidden = false;
2732
- setSessionStatus(
2733
- "Copia el handoff exacto y pégalo en el chat.",
2734
- "warning",
2735
- );
2736
- scheduleIntrinsicSizeReport(true);
2737
- }
2738
-
2739
- async function copyHandoff() {
2740
- const handoff = elements.handoffCopy.value;
2741
- if (!handoff) return;
2742
- if (
2743
- navigator.clipboard &&
2744
- typeof navigator.clipboard.writeText === "function"
2745
- ) {
2746
- try {
2747
- await navigator.clipboard.writeText(handoff);
2748
- elements.copyButton.textContent = "Copiado";
2388
+ if (
2389
+ !state.session ||
2390
+ state.session.sessionId !== completionSessionId
2391
+ )
2749
2392
  return;
2750
- } catch {
2751
- // Selection fallback below.
2393
+ setSessionStatus(
2394
+ "No pudimos continuar el chat. Intenta nuevamente.",
2395
+ "danger",
2396
+ );
2397
+ } finally {
2398
+ if (
2399
+ state.session &&
2400
+ state.session.sessionId === completionSessionId
2401
+ ) {
2402
+ state.completionNotificationInFlight = false;
2403
+ setPickerAvailability();
2404
+ renderCompletionAction();
2752
2405
  }
2753
2406
  }
2754
- elements.handoffCopy.focus();
2755
- elements.handoffCopy.select();
2756
- elements.copyButton.textContent = "Seleccionado para copiar";
2757
2407
  }
2758
2408
 
2409
+ elements.completeButton.addEventListener("click", () => {
2410
+ if (canCompleteUpload()) void notifyChatUploadComplete();
2411
+ });
2412
+
2759
2413
  document.addEventListener("DOMContentLoaded", initialize, {
2760
2414
  once: true,
2761
2415
  });