@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.
- package/CHANGELOG.md +34 -0
- package/bin/archive-progress.js +335 -0
- package/bin/context-budget-audit.js +432 -0
- package/bin/gsd-t.js +79 -1
- package/bin/log-tail.js +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 +197 -1
- package/scripts/gsd-t-design-review.html +727 -37
|
@@ -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;
|
|
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
|
|
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. "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>
|
|
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 &&
|
|
1219
|
+
if (itemChanges.length > 0 && hasComment) {
|
|
955
1220
|
statusClass = "changed";
|
|
956
1221
|
} else if (itemChanges.length > 0) {
|
|
957
1222
|
statusClass = "changed";
|
|
958
|
-
} else if (
|
|
1223
|
+
} else if (hasComment) {
|
|
959
1224
|
statusClass = "rejected";
|
|
960
1225
|
}
|
|
961
1226
|
|
|
962
|
-
|
|
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
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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 = "×";
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2531
|
+
const withChanges = [];
|
|
2532
|
+
const withComments = [];
|
|
2533
|
+
const withExclusions = [];
|
|
2534
|
+
const approved = [];
|
|
2160
2535
|
|
|
2161
|
-
|
|
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
|
-
|
|
2198
|
-
|
|
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,"&").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
|
+
|
|
2242
2932
|
// ── Init ──────────────────────────────────────────
|
|
2243
2933
|
connectSSE();
|
|
2244
2934
|
// Also poll periodically as backup
|