@tekyzinc/gsd-t 2.73.25 → 2.74.11

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.
@@ -248,6 +248,82 @@
248
248
  }
249
249
  .btn-outline:hover { border-color: var(--text); color: var(--text); }
250
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
+
251
327
  /* ── Center: iframe ─────────────────────────────── */
252
328
  .preview-pane {
253
329
  flex: 1;
@@ -555,6 +631,144 @@
555
631
  .feedback-comment:focus { border-color: var(--accent); }
556
632
  .feedback-comment::placeholder { color: #94a3b8; }
557
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
+
558
772
  /* ── Auto-review results ────────────────────────── */
559
773
  .auto-review {
560
774
  padding: 8px;
@@ -713,6 +927,13 @@
713
927
  <span class="phase-badge" id="phase-badge">Elements</span>
714
928
  </div>
715
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>
716
937
  <button class="inspect-toggle" id="gallery-toggle" title="Show all components in a grid" style="margin-right:4px">
717
938
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
718
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"/>
@@ -729,6 +950,18 @@
729
950
  </div>
730
951
  </div>
731
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
+
732
965
  <!-- ── Main ────────────────────────────────────────── -->
733
966
  <div class="main">
734
967
  <!-- Left sidebar: component list -->
@@ -792,7 +1025,33 @@
792
1025
  <div id="changes-list"></div>
793
1026
  </div>
794
1027
  <div class="feedback-panel" id="feedback-panel" style="display:none">
795
- <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>
796
1055
  </div>
797
1056
  </div>
798
1057
  </div>
@@ -809,6 +1068,7 @@
809
1068
  let inspectActive = false;
810
1069
  const changes = new Map(); // componentId → [{path, property, oldValue, newValue}]
811
1070
  const comments = new Map(); // componentId → string
1071
+ const commentAttachments = new Map(); // componentId → [{ name, dataUrl }]
812
1072
  const excluded = new Set(); // componentIds removed from review
813
1073
  let currentElementPath = null;
814
1074
  let currentStyles = null;
@@ -830,6 +1090,7 @@
830
1090
  const changesCount = document.getElementById("changes-count");
831
1091
  const feedbackPanel = document.getElementById("feedback-panel");
832
1092
  const feedbackComment = document.getElementById("feedback-comment");
1093
+ const feedbackAttachments = document.getElementById("feedback-attachments");
833
1094
  const resetStyles = document.getElementById("reset-styles");
834
1095
  const undoAllChanges = document.getElementById("undo-all-changes");
835
1096
  const connectionStatus = document.getElementById("connection-status");
@@ -952,12 +1213,14 @@
952
1213
  filteredQueue.forEach((item, idx) => {
953
1214
  const itemChanges = changes.get(item.id) || [];
954
1215
  const itemComment = comments.get(item.id) || "";
1216
+ const itemAttachments = commentAttachments.get(item.id) || [];
1217
+ const hasComment = itemComment || itemAttachments.length > 0;
955
1218
  let statusClass = "pending";
956
- if (itemChanges.length > 0 && itemComment) {
1219
+ if (itemChanges.length > 0 && hasComment) {
957
1220
  statusClass = "changed";
958
1221
  } else if (itemChanges.length > 0) {
959
1222
  statusClass = "changed";
960
- } else if (itemComment) {
1223
+ } else if (hasComment) {
961
1224
  statusClass = "rejected";
962
1225
  }
963
1226
 
@@ -1064,11 +1327,89 @@
1064
1327
 
1065
1328
  // Load existing comment for this element
1066
1329
  feedbackComment.value = comments.get(item.id) || "";
1330
+ renderAttachments(item.id);
1067
1331
 
1068
1332
  // Show component changes
1069
1333
  renderChanges(item.id);
1070
1334
  }
1071
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
+
1072
1413
  function renderComponentInfo(item) {
1073
1414
  // Show component details in the info zone
1074
1415
  inspectorInfo.innerHTML = "";
@@ -2152,7 +2493,12 @@
2152
2493
  const total = queue.length;
2153
2494
  const excludedCount = excluded.size;
2154
2495
  const changed = Array.from(changes.keys()).filter(id => (changes.get(id) || []).length > 0).length;
2155
- 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;
2156
2502
 
2157
2503
  const parts = [];
2158
2504
  if (changed > 0) parts.push(`<span><span class="component-status changed" style="display:inline-block"></span> ${changed} changed</span>`);
@@ -2167,7 +2513,14 @@
2167
2513
  : "Submit — Approve All";
2168
2514
  }
2169
2515
 
2170
- 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() {
2171
2524
  // Save current element's comment
2172
2525
  if (selectedIdx >= 0 && filteredQueue[selectedIdx]) {
2173
2526
  const comment = feedbackComment.value.trim();
@@ -2175,13 +2528,146 @@
2175
2528
  else comments.delete(filteredQueue[selectedIdx].id);
2176
2529
  }
2177
2530
 
2178
- // All comments are accepted — questions, exclusions, and change requests alike
2531
+ const withChanges = [];
2532
+ const withComments = [];
2533
+ const withExclusions = [];
2534
+ const approved = [];
2535
+
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...";
2179
2664
 
2180
- // Build feedback: each element gets its changes and comments
2665
+ // Build feedback: each element gets its changes, comments, and attachments
2181
2666
  const feedback = queue.map(item => ({
2182
2667
  id: item.id,
2183
2668
  changes: changes.get(item.id) || [],
2184
2669
  comment: comments.get(item.id) || "",
2670
+ attachments: commentAttachments.get(item.id) || [],
2185
2671
  }));
2186
2672
 
2187
2673
  try {
@@ -2191,11 +2677,7 @@
2191
2677
  body: JSON.stringify(feedback),
2192
2678
  });
2193
2679
  if (res.ok) {
2194
- const hasWork = feedback.some(f => f.changes.length > 0 || f.comment);
2195
- submitAll.textContent = hasWork
2196
- ? "Submitted — builder will apply changes..."
2197
- : "Approved — moving to next step...";
2198
- submitAll.disabled = true;
2680
+ const hasWork = feedback.some(f => f.changes.length > 0 || f.comment || (f.attachments && f.attachments.length > 0));
2199
2681
 
2200
2682
  // Also send source changes
2201
2683
  const allChanges = [];
@@ -2220,10 +2702,15 @@
2220
2702
  body: JSON.stringify({ excludedIds: Array.from(excluded) }),
2221
2703
  });
2222
2704
  }
2705
+
2706
+ // Show status screen and start listening for requeue
2707
+ hideStagingModal();
2708
+ showPostSubmit(hasWork ? "fixing" : "approved");
2709
+ listenForRequeue();
2223
2710
  }
2224
2711
  } catch (err) {
2225
- submitAll.textContent = "Error — retry";
2226
- submitAll.disabled = false;
2712
+ stagingConfirm.textContent = "Error — retry";
2713
+ stagingConfirm.disabled = false;
2227
2714
  }
2228
2715
  });
2229
2716
 
@@ -2267,6 +2754,181 @@
2267
2754
  }
2268
2755
  });
2269
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
+
2270
2932
  // ── Init ──────────────────────────────────────────
2271
2933
  connectSSE();
2272
2934
  // Also poll periodically as backup