@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.
- package/CHANGELOG.md +33 -0
- package/bin/archive-progress.cjs +335 -0
- package/bin/context-budget-audit.cjs +432 -0
- package/bin/gsd-t.js +80 -1
- package/bin/log-tail.cjs +81 -0
- package/bin/orchestrator.js +233 -47
- package/commands/gsd-t-design-decompose.md +26 -2
- package/docs/context-budget-recovery-plan.md +170 -0
- package/package.json +1 -1
- package/scripts/gsd-t-design-review-server.js +157 -3
- package/scripts/gsd-t-design-review.html +676 -14
|
@@ -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. "make border thinner" or "reduce gap to 8px"..."></textarea>
|
|
1028
|
+
<textarea class="feedback-comment" id="feedback-comment" placeholder="Suggest changes, e.g. "make border thinner" or "reduce gap to 8px"... (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">×</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 & 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 &&
|
|
1219
|
+
if (itemChanges.length > 0 && hasComment) {
|
|
957
1220
|
statusClass = "changed";
|
|
958
1221
|
} else if (itemChanges.length > 0) {
|
|
959
1222
|
statusClass = "changed";
|
|
960
|
-
} else if (
|
|
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 = "×";
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2226
|
-
|
|
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,"&").replace(/</g,"<").replace(/>/g,">"); }
|
|
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
|