@tekyzinc/gsd-t 2.73.24 → 2.74.10

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.
@@ -195,11 +195,13 @@
195
195
 
196
196
  .component-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
197
197
  .component-type { font-size: 10px; color: #94a3b8; }
198
- .remove-btn {
199
- display: none; background: none; border: none; color: #ef4444; font-size: 16px;
198
+ .remove-btn, .undo-remove-btn {
199
+ display: none; background: none; border: none; font-size: 14px;
200
200
  cursor: pointer; padding: 0 4px; line-height: 1; opacity: 0.6;
201
201
  }
202
- .remove-btn:hover { opacity: 1; }
202
+ .remove-btn { color: #ef4444; }
203
+ .undo-remove-btn { color: #22c55e; display: block; }
204
+ .remove-btn:hover, .undo-remove-btn:hover { opacity: 1; }
203
205
  .component-item:hover .remove-btn { display: block; }
204
206
 
205
207
  /* ── Submit bar ─────────────────────────────────── */
@@ -246,6 +248,82 @@
246
248
  }
247
249
  .btn-outline:hover { border-color: var(--text); color: var(--text); }
248
250
 
251
+ /* ── Staging modal ──────────────────────────────── */
252
+ .modal-overlay {
253
+ position: fixed; inset: 0; z-index: 9999;
254
+ background: rgba(0,0,0,0.7); backdrop-filter: blur(4px);
255
+ display: flex; align-items: center; justify-content: center;
256
+ }
257
+ .modal-overlay[hidden] { display: none; }
258
+ .modal {
259
+ background: var(--bg-surface); border: 1px solid var(--border);
260
+ border-radius: 12px; width: 520px; max-height: 80vh;
261
+ display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0,0,0,0.5);
262
+ }
263
+ .modal-header {
264
+ padding: 16px 20px; border-bottom: 1px solid var(--border);
265
+ font-weight: 600; font-size: 15px; display: flex; justify-content: space-between; align-items: center;
266
+ }
267
+ .modal-body {
268
+ padding: 16px 20px; overflow-y: auto; flex: 1;
269
+ font-size: 13px; line-height: 1.6;
270
+ }
271
+ .modal-footer {
272
+ padding: 12px 20px; border-top: 1px solid var(--border);
273
+ display: flex; gap: 8px; justify-content: flex-end;
274
+ }
275
+ .staging-section { margin-bottom: 14px; }
276
+ .staging-section h4 {
277
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;
278
+ color: var(--text-dim); margin-bottom: 6px;
279
+ }
280
+ .staging-item {
281
+ padding: 6px 10px; background: var(--bg); border-radius: 6px;
282
+ margin-bottom: 4px; font-size: 12px; font-family: var(--mono);
283
+ }
284
+ .staging-item .label { color: var(--text-muted); }
285
+ .staging-item .value { color: var(--text); }
286
+ .staging-change { color: var(--orange); }
287
+ .staging-comment { color: var(--accent); }
288
+ .staging-exclude { color: var(--red); }
289
+ .staging-approve { color: var(--green); }
290
+ .staging-summary {
291
+ display: flex; gap: 16px; padding: 10px 12px;
292
+ background: var(--bg); border-radius: 8px; margin-bottom: 14px;
293
+ }
294
+ .staging-summary .stat { text-align: center; }
295
+ .staging-summary .stat-num { font-size: 20px; font-weight: 700; }
296
+ .staging-summary .stat-label { font-size: 10px; color: var(--text-dim); text-transform: uppercase; }
297
+
298
+ /* ── Post-submit status screen ──────────────────── */
299
+ .post-submit-overlay {
300
+ position: fixed; inset: 0; z-index: 9998;
301
+ background: var(--bg);
302
+ display: flex; align-items: center; justify-content: center;
303
+ flex-direction: column; gap: 20px;
304
+ }
305
+ .post-submit-overlay[hidden] { display: none; }
306
+ .post-submit-icon { font-size: 56px; }
307
+ .post-submit-title {
308
+ font-size: 22px; font-weight: 700; text-align: center;
309
+ }
310
+ .post-submit-subtitle {
311
+ font-size: 14px; color: var(--text-muted); text-align: center;
312
+ max-width: 440px; line-height: 1.6;
313
+ }
314
+ .post-submit-phase {
315
+ margin-top: 8px; padding: 10px 20px;
316
+ background: var(--bg-surface); border: 1px solid var(--border);
317
+ border-radius: 8px; font-size: 12px; color: var(--text-dim);
318
+ font-family: var(--mono);
319
+ }
320
+ .post-submit-spinner {
321
+ width: 24px; height: 24px; border: 3px solid var(--border);
322
+ border-top-color: var(--accent); border-radius: 50%;
323
+ animation: post-spin 0.8s linear infinite; margin-top: 4px;
324
+ }
325
+ @keyframes post-spin { to { transform: rotate(360deg); } }
326
+
249
327
  /* ── Center: iframe ─────────────────────────────── */
250
328
  .preview-pane {
251
329
  flex: 1;
@@ -553,6 +631,144 @@
553
631
  .feedback-comment:focus { border-color: var(--accent); }
554
632
  .feedback-comment::placeholder { color: #94a3b8; }
555
633
 
634
+ .feedback-attachments {
635
+ display: none;
636
+ flex-wrap: wrap;
637
+ gap: 6px;
638
+ margin-top: 6px;
639
+ }
640
+ .feedback-attachments.has-items { display: flex; }
641
+ .feedback-attachment-thumb {
642
+ position: relative;
643
+ width: 72px;
644
+ height: 72px;
645
+ border: 1px solid var(--border);
646
+ border-radius: 6px;
647
+ overflow: hidden;
648
+ background: var(--bg);
649
+ cursor: pointer;
650
+ }
651
+ .feedback-attachment-thumb img {
652
+ width: 100%;
653
+ height: 100%;
654
+ object-fit: cover;
655
+ display: block;
656
+ }
657
+ .feedback-attachment-thumb .remove {
658
+ position: absolute;
659
+ top: 2px;
660
+ right: 2px;
661
+ width: 18px;
662
+ height: 18px;
663
+ border-radius: 50%;
664
+ background: rgba(0,0,0,0.65);
665
+ color: #fff;
666
+ border: none;
667
+ font-size: 12px;
668
+ line-height: 1;
669
+ cursor: pointer;
670
+ display: flex;
671
+ align-items: center;
672
+ justify-content: center;
673
+ padding: 0;
674
+ }
675
+ .feedback-attachment-thumb .remove:hover { background: #ef4444; }
676
+ .feedback-hint {
677
+ font-size: 10px;
678
+ color: var(--text-dim);
679
+ margin-top: 4px;
680
+ }
681
+
682
+ /* ── AI Assistant panel ────────────────────────────── */
683
+ .ai-panel {
684
+ display: none;
685
+ background: var(--bg-surface);
686
+ border-bottom: 1px solid var(--border);
687
+ flex-shrink: 0;
688
+ flex-direction: column;
689
+ max-height: 280px;
690
+ }
691
+ .ai-panel.open { display: flex; }
692
+
693
+ .ai-messages {
694
+ flex: 1;
695
+ overflow-y: auto;
696
+ padding: 12px 16px;
697
+ font-size: 13px;
698
+ line-height: 1.5;
699
+ }
700
+
701
+ .ai-msg { margin-bottom: 12px; white-space: pre-wrap; word-wrap: break-word; }
702
+ .ai-msg-label {
703
+ font-size: 10px;
704
+ font-weight: 700;
705
+ text-transform: uppercase;
706
+ letter-spacing: 0.05em;
707
+ margin-bottom: 2px;
708
+ }
709
+ .ai-msg-user .ai-msg-label { color: var(--accent); }
710
+ .ai-msg-assistant .ai-msg-label { color: var(--green); }
711
+ .ai-msg-text { color: var(--text); }
712
+ .ai-msg-error .ai-msg-text { color: var(--red); }
713
+ .ai-msg-actions {
714
+ margin-top: 6px;
715
+ display: flex;
716
+ gap: 6px;
717
+ }
718
+
719
+ .ai-input-row {
720
+ display: flex;
721
+ gap: 8px;
722
+ padding: 8px 12px;
723
+ border-top: 1px solid var(--border);
724
+ align-items: center;
725
+ }
726
+
727
+ .ai-input {
728
+ flex: 1;
729
+ background: var(--bg);
730
+ border: 1px solid var(--border);
731
+ border-radius: 6px;
732
+ color: var(--text);
733
+ font-family: var(--font);
734
+ font-size: 12px;
735
+ padding: 8px 10px;
736
+ outline: none;
737
+ }
738
+ .ai-input:focus { border-color: var(--accent); }
739
+ .ai-input::placeholder { color: #94a3b8; }
740
+ .ai-input:disabled { opacity: 0.5; }
741
+
742
+ .ai-clear {
743
+ font-size: 11px;
744
+ color: var(--text-dim);
745
+ background: none;
746
+ border: none;
747
+ cursor: pointer;
748
+ padding: 4px;
749
+ }
750
+ .ai-clear:hover { color: var(--text-muted); }
751
+
752
+ .ai-thinking {
753
+ display: inline-flex;
754
+ gap: 4px;
755
+ align-items: center;
756
+ padding: 4px 0;
757
+ }
758
+ .ai-thinking-dot {
759
+ width: 6px;
760
+ height: 6px;
761
+ border-radius: 50%;
762
+ background: var(--accent);
763
+ animation: ai-pulse 1.4s ease-in-out infinite;
764
+ }
765
+ .ai-thinking-dot:nth-child(2) { animation-delay: 0.2s; }
766
+ .ai-thinking-dot:nth-child(3) { animation-delay: 0.4s; }
767
+ @keyframes ai-pulse {
768
+ 0%, 80%, 100% { opacity: 0.2; transform: scale(0.8); }
769
+ 40% { opacity: 1; transform: scale(1); }
770
+ }
771
+
556
772
  /* ── Auto-review results ────────────────────────── */
557
773
  .auto-review {
558
774
  padding: 8px;
@@ -711,6 +927,13 @@
711
927
  <span class="phase-badge" id="phase-badge">Elements</span>
712
928
  </div>
713
929
  <div class="header-right">
930
+ <button class="inspect-toggle" id="ai-toggle" title="AI prompt assistant (Ctrl+K)" style="margin-right:4px">
931
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
932
+ <path d="M12 2a4 4 0 0 1 4 4v2h1a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3h-1v2a4 4 0 0 1-8 0v-2H7a3 3 0 0 1-3-3v-2a3 3 0 0 1 3-3h1V6a4 4 0 0 1 4-4z"/>
933
+ <circle cx="9" cy="11" r="1"/><circle cx="15" cy="11" r="1"/>
934
+ </svg>
935
+ AI
936
+ </button>
714
937
  <button class="inspect-toggle" id="gallery-toggle" title="Show all components in a grid" style="margin-right:4px">
715
938
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
716
939
  <rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
@@ -727,6 +950,18 @@
727
950
  </div>
728
951
  </div>
729
952
 
953
+ <!-- ── AI Assistant Panel ──────────────────────────── -->
954
+ <div class="ai-panel" id="ai-panel">
955
+ <div class="ai-messages" id="ai-messages">
956
+ <div class="ai-msg" style="color:var(--text-dim);font-size:12px">Ask about the selected component or describe a change you want. Press <kbd style="background:var(--bg-hover);padding:1px 5px;border-radius:3px;font-size:11px">Ctrl+K</kbd> to toggle.</div>
957
+ </div>
958
+ <div class="ai-input-row">
959
+ <input class="ai-input" id="ai-input" type="text" placeholder="Ask about this component or describe a fix..." autocomplete="off" />
960
+ <button class="btn btn-sm btn-primary" id="ai-send">Send</button>
961
+ <button class="ai-clear" id="ai-clear" title="Clear conversation">Clear</button>
962
+ </div>
963
+ </div>
964
+
730
965
  <!-- ── Main ────────────────────────────────────────── -->
731
966
  <div class="main">
732
967
  <!-- Left sidebar: component list -->
@@ -790,7 +1025,33 @@
790
1025
  <div id="changes-list"></div>
791
1026
  </div>
792
1027
  <div class="feedback-panel" id="feedback-panel" style="display:none">
793
- <textarea class="feedback-comment" id="feedback-comment" placeholder="Suggest changes, e.g. &quot;make border thinner&quot; or &quot;reduce gap to 8px&quot;..."></textarea>
1028
+ <textarea class="feedback-comment" id="feedback-comment" placeholder="Suggest changes, e.g. &quot;make border thinner&quot; or &quot;reduce gap to 8px&quot;... (paste an image with ⌘V)"></textarea>
1029
+ <div class="feedback-attachments" id="feedback-attachments"></div>
1030
+ <div class="feedback-hint">Tip: paste a screenshot (⌘V) to attach it to the comment.</div>
1031
+ </div>
1032
+ </div>
1033
+ </div>
1034
+
1035
+ <!-- Post-submit status screen -->
1036
+ <div class="post-submit-overlay" id="post-submit" hidden>
1037
+ <div class="post-submit-icon" id="post-submit-icon"></div>
1038
+ <div class="post-submit-title" id="post-submit-title"></div>
1039
+ <div class="post-submit-subtitle" id="post-submit-subtitle"></div>
1040
+ <div class="post-submit-spinner" id="post-submit-spinner"></div>
1041
+ <div class="post-submit-phase" id="post-submit-phase"></div>
1042
+ </div>
1043
+
1044
+ <!-- Staging modal -->
1045
+ <div class="modal-overlay" id="staging-modal" hidden>
1046
+ <div class="modal">
1047
+ <div class="modal-header">
1048
+ <span>Review Summary</span>
1049
+ <button class="btn btn-sm btn-outline" id="staging-cancel" style="font-size:16px;padding:2px 8px">&times;</button>
1050
+ </div>
1051
+ <div class="modal-body" id="staging-body"></div>
1052
+ <div class="modal-footer">
1053
+ <button class="btn btn-outline" id="staging-back">Back to Review</button>
1054
+ <button class="btn btn-primary" id="staging-confirm">Confirm &amp; Submit</button>
794
1055
  </div>
795
1056
  </div>
796
1057
  </div>
@@ -807,6 +1068,7 @@
807
1068
  let inspectActive = false;
808
1069
  const changes = new Map(); // componentId → [{path, property, oldValue, newValue}]
809
1070
  const comments = new Map(); // componentId → string
1071
+ const commentAttachments = new Map(); // componentId → [{ name, dataUrl }]
810
1072
  const excluded = new Set(); // componentIds removed from review
811
1073
  let currentElementPath = null;
812
1074
  let currentStyles = null;
@@ -828,6 +1090,7 @@
828
1090
  const changesCount = document.getElementById("changes-count");
829
1091
  const feedbackPanel = document.getElementById("feedback-panel");
830
1092
  const feedbackComment = document.getElementById("feedback-comment");
1093
+ const feedbackAttachments = document.getElementById("feedback-attachments");
831
1094
  const resetStyles = document.getElementById("reset-styles");
832
1095
  const undoAllChanges = document.getElementById("undo-all-changes");
833
1096
  const connectionStatus = document.getElementById("connection-status");
@@ -950,37 +1213,56 @@
950
1213
  filteredQueue.forEach((item, idx) => {
951
1214
  const itemChanges = changes.get(item.id) || [];
952
1215
  const itemComment = comments.get(item.id) || "";
1216
+ const itemAttachments = commentAttachments.get(item.id) || [];
1217
+ const hasComment = itemComment || itemAttachments.length > 0;
953
1218
  let statusClass = "pending";
954
- if (itemChanges.length > 0 && itemComment) {
1219
+ if (itemChanges.length > 0 && hasComment) {
955
1220
  statusClass = "changed";
956
1221
  } else if (itemChanges.length > 0) {
957
1222
  statusClass = "changed";
958
- } else if (itemComment) {
1223
+ } else if (hasComment) {
959
1224
  statusClass = "rejected";
960
1225
  }
961
1226
 
962
- if (excluded.has(item.id)) return; // skip excluded elements
1227
+ const isExcluded = excluded.has(item.id);
963
1228
 
964
1229
  const div = document.createElement("div");
965
- div.className = `component-item${idx === selectedIdx ? " selected" : ""}`;
966
- div.innerHTML = `
967
- <div class="component-status ${statusClass}"></div>
968
- <div class="component-name">${item.name || item.id}</div>
969
- <div class="component-type">${item.type || ""}</div>
970
- <button class="remove-btn" title="Remove from review">×</button>
971
- `;
972
- div.querySelector(".component-name").addEventListener("click", () => selectComponent(idx));
973
- div.querySelector(".component-status").addEventListener("click", () => selectComponent(idx));
974
- div.querySelector(".remove-btn").addEventListener("click", (e) => {
975
- e.stopPropagation();
976
- excluded.add(item.id);
977
- comments.set(item.id, "EXCLUDED — not in Figma design");
978
- if (selectedIdx === idx) {
979
- selectedIdx = Math.min(idx, filteredQueue.filter(q => !excluded.has(q.id)).length - 1);
980
- }
981
- renderComponentList();
982
- updateSubmitStats();
983
- });
1230
+ div.className = `component-item${idx === selectedIdx ? " selected" : ""}${isExcluded ? " excluded" : ""}`;
1231
+
1232
+ if (isExcluded) {
1233
+ div.innerHTML = `
1234
+ <div class="component-status" style="background:#ef4444"></div>
1235
+ <div class="component-name" style="text-decoration:line-through;opacity:0.5">${item.name || item.id}</div>
1236
+ <button class="undo-remove-btn" title="Restore element">↩</button>
1237
+ `;
1238
+ div.querySelector(".undo-remove-btn").addEventListener("click", (e) => {
1239
+ e.stopPropagation();
1240
+ excluded.delete(item.id);
1241
+ comments.delete(item.id);
1242
+ renderComponentList();
1243
+ updateSubmitStats();
1244
+ });
1245
+ } else {
1246
+ div.innerHTML = `
1247
+ <div class="component-status ${statusClass}"></div>
1248
+ <div class="component-name">${item.name || item.id}</div>
1249
+ <div class="component-type">${item.type || ""}</div>
1250
+ <button class="remove-btn" title="Remove from review">×</button>
1251
+ `;
1252
+ div.querySelector(".component-name").addEventListener("click", () => selectComponent(idx));
1253
+ div.querySelector(".component-status").addEventListener("click", () => selectComponent(idx));
1254
+ div.querySelector(".remove-btn").addEventListener("click", (e) => {
1255
+ e.stopPropagation();
1256
+ excluded.add(item.id);
1257
+ comments.set(item.id, "EXCLUDED — not in Figma design");
1258
+ if (selectedIdx === idx) {
1259
+ const remaining = filteredQueue.filter(q => !excluded.has(q.id));
1260
+ selectedIdx = remaining.length > 0 ? filteredQueue.indexOf(remaining[0]) : -1;
1261
+ }
1262
+ renderComponentList();
1263
+ updateSubmitStats();
1264
+ });
1265
+ }
984
1266
  componentList.appendChild(div);
985
1267
  });
986
1268
  }
@@ -1045,11 +1327,89 @@
1045
1327
 
1046
1328
  // Load existing comment for this element
1047
1329
  feedbackComment.value = comments.get(item.id) || "";
1330
+ renderAttachments(item.id);
1048
1331
 
1049
1332
  // Show component changes
1050
1333
  renderChanges(item.id);
1051
1334
  }
1052
1335
 
1336
+ // ── Attachment rendering and paste handling ──────────
1337
+ function renderAttachments(itemId) {
1338
+ const list = commentAttachments.get(itemId) || [];
1339
+ feedbackAttachments.innerHTML = "";
1340
+ if (list.length === 0) {
1341
+ feedbackAttachments.classList.remove("has-items");
1342
+ return;
1343
+ }
1344
+ feedbackAttachments.classList.add("has-items");
1345
+ list.forEach((att, idx) => {
1346
+ const thumb = document.createElement("div");
1347
+ thumb.className = "feedback-attachment-thumb";
1348
+ thumb.title = att.name || "pasted image";
1349
+ const img = document.createElement("img");
1350
+ img.src = att.dataUrl;
1351
+ img.alt = att.name || "pasted image";
1352
+ thumb.appendChild(img);
1353
+ const remove = document.createElement("button");
1354
+ remove.type = "button";
1355
+ remove.className = "remove";
1356
+ remove.innerHTML = "&times;";
1357
+ remove.title = "Remove attachment";
1358
+ remove.addEventListener("click", (e) => {
1359
+ e.stopPropagation();
1360
+ const current = commentAttachments.get(itemId) || [];
1361
+ current.splice(idx, 1);
1362
+ if (current.length === 0) commentAttachments.delete(itemId);
1363
+ else commentAttachments.set(itemId, current);
1364
+ renderAttachments(itemId);
1365
+ renderComponentList();
1366
+ updateSubmitStats();
1367
+ });
1368
+ thumb.appendChild(remove);
1369
+ thumb.addEventListener("click", () => {
1370
+ // Open in new tab for full-size view
1371
+ const w = window.open("", "_blank");
1372
+ if (w) w.document.write(`<img src="${att.dataUrl}" style="max-width:100%">`);
1373
+ });
1374
+ feedbackAttachments.appendChild(thumb);
1375
+ });
1376
+ }
1377
+
1378
+ feedbackComment.addEventListener("paste", (e) => {
1379
+ const item = filteredQueue[selectedIdx];
1380
+ if (!item) return;
1381
+ const cbItems = e.clipboardData && e.clipboardData.items;
1382
+ if (!cbItems) return;
1383
+ const images = [];
1384
+ for (const ci of cbItems) {
1385
+ if (ci.kind === "file" && ci.type.startsWith("image/")) {
1386
+ const file = ci.getAsFile();
1387
+ if (file) images.push(file);
1388
+ }
1389
+ }
1390
+ if (images.length === 0) return;
1391
+ e.preventDefault();
1392
+ const existing = commentAttachments.get(item.id) || [];
1393
+ let remaining = images.length;
1394
+ images.forEach((file, idx) => {
1395
+ const reader = new FileReader();
1396
+ reader.onload = () => {
1397
+ existing.push({
1398
+ name: file.name || `pasted-${Date.now()}-${idx}.png`,
1399
+ dataUrl: reader.result,
1400
+ });
1401
+ remaining--;
1402
+ if (remaining === 0) {
1403
+ commentAttachments.set(item.id, existing);
1404
+ renderAttachments(item.id);
1405
+ renderComponentList();
1406
+ updateSubmitStats();
1407
+ }
1408
+ };
1409
+ reader.readAsDataURL(file);
1410
+ });
1411
+ });
1412
+
1053
1413
  function renderComponentInfo(item) {
1054
1414
  // Show component details in the info zone
1055
1415
  inspectorInfo.innerHTML = "";
@@ -2133,7 +2493,12 @@
2133
2493
  const total = queue.length;
2134
2494
  const excludedCount = excluded.size;
2135
2495
  const changed = Array.from(changes.keys()).filter(id => (changes.get(id) || []).length > 0).length;
2136
- const commented = Array.from(comments.keys()).filter(id => !excluded.has(id)).length;
2496
+ const commentedIds = new Set();
2497
+ for (const id of comments.keys()) if (!excluded.has(id)) commentedIds.add(id);
2498
+ for (const id of commentAttachments.keys()) {
2499
+ if (!excluded.has(id) && (commentAttachments.get(id) || []).length > 0) commentedIds.add(id);
2500
+ }
2501
+ const commented = commentedIds.size;
2137
2502
 
2138
2503
  const parts = [];
2139
2504
  if (changed > 0) parts.push(`<span><span class="component-status changed" style="display:inline-block"></span> ${changed} changed</span>`);
@@ -2148,7 +2513,14 @@
2148
2513
  : "Submit — Approve All";
2149
2514
  }
2150
2515
 
2151
- submitAll.addEventListener("click", async () => {
2516
+ // ── Staging modal logic ───────────────────────────
2517
+ const stagingModal = document.getElementById("staging-modal");
2518
+ const stagingBody = document.getElementById("staging-body");
2519
+ const stagingCancel = document.getElementById("staging-cancel");
2520
+ const stagingBack = document.getElementById("staging-back");
2521
+ const stagingConfirm = document.getElementById("staging-confirm");
2522
+
2523
+ function buildStagingSummary() {
2152
2524
  // Save current element's comment
2153
2525
  if (selectedIdx >= 0 && filteredQueue[selectedIdx]) {
2154
2526
  const comment = feedbackComment.value.trim();
@@ -2156,13 +2528,146 @@
2156
2528
  else comments.delete(filteredQueue[selectedIdx].id);
2157
2529
  }
2158
2530
 
2159
- // All comments are accepted — questions, exclusions, and change requests alike
2531
+ const withChanges = [];
2532
+ const withComments = [];
2533
+ const withExclusions = [];
2534
+ const approved = [];
2160
2535
 
2161
- // Build feedback: each element gets its changes and comments
2536
+ for (const item of queue) {
2537
+ const ch = changes.get(item.id) || [];
2538
+ const cm = comments.get(item.id) || "";
2539
+ const atts = commentAttachments.get(item.id) || [];
2540
+ const ex = excluded.has(item.id);
2541
+ if (ex) { withExclusions.push(item); }
2542
+ else if (ch.length > 0 || cm || atts.length > 0) {
2543
+ if (ch.length > 0) withChanges.push({ item, changes: ch });
2544
+ if (cm || atts.length > 0) withComments.push({ item, comment: cm, attachments: atts });
2545
+ } else {
2546
+ approved.push(item);
2547
+ }
2548
+ }
2549
+
2550
+ let html = `<div class="staging-summary">
2551
+ <div class="stat"><div class="stat-num staging-approve">${approved.length}</div><div class="stat-label">Approved</div></div>
2552
+ <div class="stat"><div class="stat-num staging-change">${withChanges.length}</div><div class="stat-label">Style Changes</div></div>
2553
+ <div class="stat"><div class="stat-num staging-comment">${withComments.length}</div><div class="stat-label">Comments</div></div>
2554
+ <div class="stat"><div class="stat-num staging-exclude">${withExclusions.length}</div><div class="stat-label">Removed</div></div>
2555
+ </div>`;
2556
+
2557
+ if (withChanges.length > 0) {
2558
+ html += `<div class="staging-section"><h4>Style Changes</h4>`;
2559
+ for (const { item, changes: ch } of withChanges) {
2560
+ const name = item.name || item.id;
2561
+ html += `<div class="staging-item"><span class="label">${name}:</span> <span class="staging-change">${ch.length} change${ch.length > 1 ? "s" : ""}</span>`;
2562
+ for (const c of ch) {
2563
+ html += `<div style="margin-left:12px;font-size:11px;color:var(--text-dim)">${c.property}: ${c.oldValue} → ${c.newValue}</div>`;
2564
+ }
2565
+ html += `</div>`;
2566
+ }
2567
+ html += `</div>`;
2568
+ }
2569
+
2570
+ if (withComments.length > 0) {
2571
+ html += `<div class="staging-section"><h4>Comments / Requests</h4>`;
2572
+ for (const { item, comment, attachments } of withComments) {
2573
+ const name = item.name || item.id;
2574
+ const shownComment = comment
2575
+ ? `"${comment.length > 80 ? comment.slice(0, 80) + "…" : comment}"`
2576
+ : `<span style="color:var(--text-dim)">(image only)</span>`;
2577
+ const attCount = (attachments || []).length;
2578
+ const attBadge = attCount > 0
2579
+ ? ` <span style="color:var(--text-dim)">📎 ${attCount} image${attCount > 1 ? "s" : ""}</span>`
2580
+ : "";
2581
+ html += `<div class="staging-item"><span class="label">${name}:</span> <span class="staging-comment">${shownComment}</span>${attBadge}</div>`;
2582
+ }
2583
+ html += `</div>`;
2584
+ }
2585
+
2586
+ if (withExclusions.length > 0) {
2587
+ html += `<div class="staging-section"><h4>Removed Elements</h4>`;
2588
+ for (const item of withExclusions) {
2589
+ const name = item.name || item.id;
2590
+ html += `<div class="staging-item"><span class="staging-exclude">✕ ${name}</span></div>`;
2591
+ }
2592
+ html += `</div>`;
2593
+ }
2594
+
2595
+ if (approved.length > 0) {
2596
+ html += `<div class="staging-section"><h4>Approved (no changes)</h4>`;
2597
+ html += `<div class="staging-item"><span class="staging-approve">${approved.map(i => i.name || i.id).join(", ")}</span></div>`;
2598
+ html += `</div>`;
2599
+ }
2600
+
2601
+ return html;
2602
+ }
2603
+
2604
+ function showStagingModal() {
2605
+ stagingBody.innerHTML = buildStagingSummary();
2606
+ stagingModal.hidden = false;
2607
+ }
2608
+
2609
+ function hideStagingModal() {
2610
+ stagingModal.hidden = true;
2611
+ }
2612
+
2613
+ stagingCancel.addEventListener("click", hideStagingModal);
2614
+ stagingBack.addEventListener("click", hideStagingModal);
2615
+
2616
+ submitAll.addEventListener("click", () => {
2617
+ showStagingModal();
2618
+ });
2619
+
2620
+ // ── Post-submit status screen ─────────────────────
2621
+ const postSubmit = document.getElementById("post-submit");
2622
+ const postSubmitIcon = document.getElementById("post-submit-icon");
2623
+ const postSubmitTitle = document.getElementById("post-submit-title");
2624
+ const postSubmitSubtitle = document.getElementById("post-submit-subtitle");
2625
+ const postSubmitSpinner = document.getElementById("post-submit-spinner");
2626
+ const postSubmitPhase = document.getElementById("post-submit-phase");
2627
+
2628
+ function showPostSubmit(mode) {
2629
+ // mode: "approved" | "fixing"
2630
+ const phase = phaseBadge?.textContent?.toLowerCase() || "elements";
2631
+ const nextPhase = phase === "elements" ? "widgets" : phase === "widgets" ? "pages" : "next phase";
2632
+
2633
+ if (mode === "approved") {
2634
+ postSubmitIcon.textContent = "✅";
2635
+ postSubmitTitle.textContent = "All Approved!";
2636
+ postSubmitSubtitle.textContent = `All ${phase} have been approved. The builder is now moving on to ${nextPhase}. This page will refresh automatically when they're ready for review.`;
2637
+ postSubmitSpinner.style.display = "block";
2638
+ postSubmitPhase.textContent = `Phase: ${phase} → ${nextPhase}`;
2639
+ } else {
2640
+ postSubmitIcon.textContent = "🔧";
2641
+ postSubmitTitle.textContent = "Corrections Submitted";
2642
+ postSubmitSubtitle.textContent = `Your feedback is being applied now. The builder will fix the issues and run auto-review. This page will refresh automatically when they're ready for you to review again.`;
2643
+ postSubmitSpinner.style.display = "block";
2644
+ postSubmitPhase.textContent = `Fixing ${phase} — sit tight`;
2645
+ }
2646
+ postSubmit.hidden = false;
2647
+ }
2648
+
2649
+ // Listen for new queue items (means fixes are done, review again)
2650
+ function listenForRequeue() {
2651
+ const check = setInterval(() => {
2652
+ fetch("/review/api/queue").then(r => r.json()).then(data => {
2653
+ if (Array.isArray(data) && data.length > 0) {
2654
+ clearInterval(check);
2655
+ location.reload();
2656
+ }
2657
+ }).catch(() => {});
2658
+ }, 5000);
2659
+ }
2660
+
2661
+ stagingConfirm.addEventListener("click", async () => {
2662
+ stagingConfirm.disabled = true;
2663
+ stagingConfirm.textContent = "Submitting...";
2664
+
2665
+ // Build feedback: each element gets its changes, comments, and attachments
2162
2666
  const feedback = queue.map(item => ({
2163
2667
  id: item.id,
2164
2668
  changes: changes.get(item.id) || [],
2165
2669
  comment: comments.get(item.id) || "",
2670
+ attachments: commentAttachments.get(item.id) || [],
2166
2671
  }));
2167
2672
 
2168
2673
  try {
@@ -2172,11 +2677,7 @@
2172
2677
  body: JSON.stringify(feedback),
2173
2678
  });
2174
2679
  if (res.ok) {
2175
- const hasWork = feedback.some(f => f.changes.length > 0 || f.comment);
2176
- submitAll.textContent = hasWork
2177
- ? "Submitted — builder will apply changes..."
2178
- : "Approved — moving to next step...";
2179
- submitAll.disabled = true;
2680
+ const hasWork = feedback.some(f => f.changes.length > 0 || f.comment || (f.attachments && f.attachments.length > 0));
2180
2681
 
2181
2682
  // Also send source changes
2182
2683
  const allChanges = [];
@@ -2192,10 +2693,24 @@
2192
2693
  body: JSON.stringify({ changes: allChanges }),
2193
2694
  });
2194
2695
  }
2696
+
2697
+ // Delete contracts and source files for excluded elements
2698
+ if (excluded.size > 0) {
2699
+ await fetch("/review/api/exclude", {
2700
+ method: "POST",
2701
+ headers: { "Content-Type": "application/json" },
2702
+ body: JSON.stringify({ excludedIds: Array.from(excluded) }),
2703
+ });
2704
+ }
2705
+
2706
+ // Show status screen and start listening for requeue
2707
+ hideStagingModal();
2708
+ showPostSubmit(hasWork ? "fixing" : "approved");
2709
+ listenForRequeue();
2195
2710
  }
2196
2711
  } catch (err) {
2197
- submitAll.textContent = "Error — retry";
2198
- submitAll.disabled = false;
2712
+ stagingConfirm.textContent = "Error — retry";
2713
+ stagingConfirm.disabled = false;
2199
2714
  }
2200
2715
  });
2201
2716
 
@@ -2239,6 +2754,181 @@
2239
2754
  }
2240
2755
  });
2241
2756
 
2757
+ // ── AI Assistant ──────────────────────────────────
2758
+ const aiToggle = document.getElementById("ai-toggle");
2759
+ const aiPanel = document.getElementById("ai-panel");
2760
+ const aiMessages = document.getElementById("ai-messages");
2761
+ const aiInput = document.getElementById("ai-input");
2762
+ const aiSend = document.getElementById("ai-send");
2763
+ const aiClear = document.getElementById("ai-clear");
2764
+ let aiHistory = [];
2765
+ let aiStreaming = false;
2766
+
2767
+ aiToggle.addEventListener("click", () => {
2768
+ aiPanel.classList.toggle("open");
2769
+ aiToggle.classList.toggle("active");
2770
+ if (aiPanel.classList.contains("open")) aiInput.focus();
2771
+ });
2772
+
2773
+ document.addEventListener("keydown", (e) => {
2774
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
2775
+ e.preventDefault();
2776
+ aiToggle.click();
2777
+ }
2778
+ });
2779
+
2780
+ aiClear.addEventListener("click", () => {
2781
+ aiHistory = [];
2782
+ aiMessages.innerHTML = '<div class="ai-msg" style="color:var(--text-dim);font-size:12px">Conversation cleared.</div>';
2783
+ });
2784
+
2785
+ function escapeAiHtml(s) { return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); }
2786
+
2787
+ async function buildComponentContext() {
2788
+ const item = filteredQueue[selectedIdx];
2789
+ if (!item) return "(no component selected)";
2790
+
2791
+ const parts = [`Component: ${item.name}`, `Source: ${item.sourcePath || "unknown"}`];
2792
+
2793
+ if (item.measurements && item.measurements.length > 0) {
2794
+ parts.push("Measurements:");
2795
+ for (const m of item.measurements) {
2796
+ parts.push(` ${m.property}: ${m.actual} (expected: ${m.expected}, ${m.pass ? "PASS" : "FAIL"})`);
2797
+ }
2798
+ }
2799
+
2800
+ if (currentStyles) {
2801
+ parts.push("Computed styles (selected element at " + (currentElementPath || "root") + "):");
2802
+ for (const [k, v] of Object.entries(currentStyles)) {
2803
+ if (v && v !== "none" && v !== "normal" && v !== "auto" && v !== "0px") {
2804
+ parts.push(` ${k}: ${v}`);
2805
+ }
2806
+ }
2807
+ }
2808
+
2809
+ if (item.sourcePath) {
2810
+ try {
2811
+ const r = await fetch(`/review/api/contract?component=${encodeURIComponent(item.sourcePath)}`);
2812
+ if (r.ok) {
2813
+ const data = await r.json();
2814
+ if (data.content) parts.push("\nDesign contract:\n" + data.content);
2815
+ }
2816
+ } catch { /* skip */ }
2817
+ }
2818
+
2819
+ return parts.join("\n");
2820
+ }
2821
+
2822
+ async function sendAiMessage() {
2823
+ const text = aiInput.value.trim();
2824
+ if (!text || aiStreaming) return;
2825
+
2826
+ aiInput.value = "";
2827
+ aiStreaming = true;
2828
+ aiInput.disabled = true;
2829
+ aiSend.disabled = true;
2830
+
2831
+ // Render user message
2832
+ const userDiv = document.createElement("div");
2833
+ userDiv.className = "ai-msg ai-msg-user";
2834
+ userDiv.innerHTML = `<div class="ai-msg-label">You</div><div class="ai-msg-text">${escapeAiHtml(text)}</div>`;
2835
+ aiMessages.appendChild(userDiv);
2836
+
2837
+ // Render assistant placeholder
2838
+ const aDiv = document.createElement("div");
2839
+ aDiv.className = "ai-msg ai-msg-assistant";
2840
+ const aLabel = document.createElement("div");
2841
+ aLabel.className = "ai-msg-label";
2842
+ aLabel.textContent = "AI";
2843
+ const aText = document.createElement("div");
2844
+ aText.className = "ai-msg-text";
2845
+ aText.innerHTML = '<div class="ai-thinking"><div class="ai-thinking-dot"></div><div class="ai-thinking-dot"></div><div class="ai-thinking-dot"></div></div>';
2846
+ aDiv.appendChild(aLabel);
2847
+ aDiv.appendChild(aText);
2848
+ aiMessages.appendChild(aDiv);
2849
+ aiMessages.scrollTop = aiMessages.scrollHeight;
2850
+
2851
+ const context = await buildComponentContext();
2852
+ aiHistory.push({ role: "user", content: text });
2853
+
2854
+ let fullText = "";
2855
+ try {
2856
+ const res = await fetch("/review/api/ai-assist", {
2857
+ method: "POST",
2858
+ headers: { "Content-Type": "application/json" },
2859
+ body: JSON.stringify({ messages: aiHistory, componentContext: context }),
2860
+ });
2861
+
2862
+ const reader = res.body.getReader();
2863
+ const decoder = new TextDecoder();
2864
+ let buf = "";
2865
+
2866
+ while (true) {
2867
+ const { done, value } = await reader.read();
2868
+ if (done) break;
2869
+ buf += decoder.decode(value, { stream: true });
2870
+ const lines = buf.split("\n");
2871
+ buf = lines.pop();
2872
+ for (const line of lines) {
2873
+ if (line.startsWith("event: error")) {
2874
+ // Next data line has the error
2875
+ } else if (line.startsWith("data: ")) {
2876
+ try {
2877
+ const data = JSON.parse(line.slice(6));
2878
+ if (data.error) {
2879
+ aDiv.className = "ai-msg ai-msg-error";
2880
+ aText.textContent = data.error;
2881
+ } else if (data.text) {
2882
+ fullText += data.text;
2883
+ aText.textContent = fullText;
2884
+ aiMessages.scrollTop = aiMessages.scrollHeight;
2885
+ }
2886
+ } catch { /* skip */ }
2887
+ }
2888
+ }
2889
+ }
2890
+
2891
+ if (fullText) {
2892
+ aiHistory.push({ role: "assistant", content: fullText });
2893
+
2894
+ // Add "Use as comment" button
2895
+ const actions = document.createElement("div");
2896
+ actions.className = "ai-msg-actions";
2897
+ const useBtn = document.createElement("button");
2898
+ useBtn.className = "btn btn-sm btn-outline";
2899
+ useBtn.textContent = "Use as comment";
2900
+ useBtn.addEventListener("click", () => {
2901
+ const existing = feedbackComment.value.trim();
2902
+ feedbackComment.value = existing ? existing + "\n" + fullText : fullText;
2903
+ const item = filteredQueue[selectedIdx];
2904
+ if (item) {
2905
+ comments.set(item.id, feedbackComment.value.trim());
2906
+ renderComponentList();
2907
+ updateSubmitStats();
2908
+ }
2909
+ });
2910
+ actions.appendChild(useBtn);
2911
+ aDiv.appendChild(actions);
2912
+ }
2913
+ } catch (err) {
2914
+ aDiv.className = "ai-msg ai-msg-error";
2915
+ aText.textContent = "Connection error: " + err.message;
2916
+ }
2917
+
2918
+ aiStreaming = false;
2919
+ aiInput.disabled = false;
2920
+ aiSend.disabled = false;
2921
+ aiInput.focus();
2922
+ }
2923
+
2924
+ aiSend.addEventListener("click", sendAiMessage);
2925
+ aiInput.addEventListener("keydown", (e) => {
2926
+ if (e.key === "Enter" && !e.shiftKey) {
2927
+ e.preventDefault();
2928
+ sendAiMessage();
2929
+ }
2930
+ });
2931
+
2242
2932
  // ── Init ──────────────────────────────────────────
2243
2933
  connectSSE();
2244
2934
  // Also poll periodically as backup