@bridge_gpt/mcp-server 0.1.11 → 0.1.13
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/build/commands.generated.js +1 -1
- package/build/decision-page-schema.js +101 -0
- package/build/decision-page-schema.test.js +248 -0
- package/build/decision-page-template.js +222 -109
- package/build/index.js +126 -33
- package/build/pipelines.generated.js +19 -64
- package/build/version.generated.js +1 -1
- package/package.json +1 -1
- package/pipelines/check-ci-ticket.json +3 -8
- package/pipelines/implement-ticket.json +3 -8
- package/pipelines/pr-ticket.json +3 -8
- package/pipelines/review-ticket.json +7 -37
|
@@ -25,6 +25,13 @@ function safeJsonForScript(value) {
|
|
|
25
25
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
26
26
|
}
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
|
+
// UI copy constants
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const COPY_SUCCESS_LABEL = "Copied!";
|
|
31
|
+
const COPY_FALLBACK_PROMPT_LABEL = "Auto-copy unavailable. Press Ctrl+C / Cmd+C to copy.";
|
|
32
|
+
const PAGE_INTRO_ASK_COPY = "Have a question about an item? Choose 'Ask about this' for that card; we'll discuss before the changes are made.";
|
|
33
|
+
const PAGE_INTRO_NO_DECISIONS = "All suggestions were confirmed as improvements. No decisions are needed from you.";
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
28
35
|
// Template
|
|
29
36
|
// ---------------------------------------------------------------------------
|
|
30
37
|
const DEFAULT_ASSETS = { faviconBase64: "", logoBase64: "", fontsRelPath: "" };
|
|
@@ -35,6 +42,7 @@ export function generateDecisionPageHtml(data, assets = DEFAULT_ASSETS) {
|
|
|
35
42
|
? `<link rel="icon" type="image/png" sizes="32x32" href="data:image/png;base64,${assets.faviconBase64}">`
|
|
36
43
|
: "";
|
|
37
44
|
const fontFaces = assets.fontsRelPath ? renderFontFaces(assets.fontsRelPath) : "";
|
|
45
|
+
const pageIntro = hasDecisions ? PAGE_INTRO_ASK_COPY : PAGE_INTRO_NO_DECISIONS;
|
|
38
46
|
return `<!DOCTYPE html>
|
|
39
47
|
<html lang="en">
|
|
40
48
|
<head>
|
|
@@ -143,14 +151,20 @@ ${fontFaces}
|
|
|
143
151
|
font-weight: 600;
|
|
144
152
|
margin: 0 0 0.5rem 0;
|
|
145
153
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
border-left: 4px solid var(--primary-color);
|
|
149
|
-
padding: 0.75rem 1rem;
|
|
154
|
+
/* Card sub-sections (Original question, Why it matters, Why the AI recommended this) */
|
|
155
|
+
.card-section {
|
|
150
156
|
margin-bottom: 1rem;
|
|
157
|
+
}
|
|
158
|
+
.card-section-label {
|
|
151
159
|
font-size: 0.875rem;
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
color: var(--secondary-color);
|
|
162
|
+
margin-bottom: 0.25rem;
|
|
163
|
+
text-transform: none;
|
|
164
|
+
}
|
|
165
|
+
.card-section-body {
|
|
166
|
+
font-size: 0.95rem;
|
|
167
|
+
color: var(--text-color);
|
|
154
168
|
white-space: pre-wrap;
|
|
155
169
|
}
|
|
156
170
|
|
|
@@ -158,8 +172,13 @@ ${fontFaces}
|
|
|
158
172
|
.radio-group {
|
|
159
173
|
display: flex;
|
|
160
174
|
flex-direction: column;
|
|
161
|
-
gap: 0.
|
|
162
|
-
margin-bottom:
|
|
175
|
+
gap: 0.75rem;
|
|
176
|
+
margin-bottom: 1rem;
|
|
177
|
+
}
|
|
178
|
+
.radio-option-wrapper {
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
gap: 0.25rem;
|
|
163
182
|
}
|
|
164
183
|
.radio-option {
|
|
165
184
|
display: flex;
|
|
@@ -176,9 +195,42 @@ ${fontFaces}
|
|
|
176
195
|
font-weight: 400;
|
|
177
196
|
margin-bottom: 0;
|
|
178
197
|
}
|
|
198
|
+
.radio-option-consequence {
|
|
199
|
+
font-size: 0.9rem;
|
|
200
|
+
color: var(--secondary-color);
|
|
201
|
+
margin-left: 1.75rem;
|
|
202
|
+
white-space: pre-wrap;
|
|
203
|
+
}
|
|
179
204
|
.ai-recommendation {
|
|
180
205
|
color: var(--primary-color);
|
|
181
206
|
font-weight: 600;
|
|
207
|
+
white-space: nowrap;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Codebase evidence disclosure */
|
|
211
|
+
.codebase-evidence {
|
|
212
|
+
margin-bottom: 1rem;
|
|
213
|
+
border: 1px solid var(--border-color);
|
|
214
|
+
border-radius: 0.25rem;
|
|
215
|
+
background: #fafafa;
|
|
216
|
+
}
|
|
217
|
+
.codebase-evidence summary {
|
|
218
|
+
cursor: pointer;
|
|
219
|
+
padding: 0.5rem 0.75rem;
|
|
220
|
+
font-size: 0.875rem;
|
|
221
|
+
font-weight: 600;
|
|
222
|
+
color: var(--secondary-color);
|
|
223
|
+
user-select: none;
|
|
224
|
+
}
|
|
225
|
+
.codebase-evidence summary:hover {
|
|
226
|
+
color: var(--text-color);
|
|
227
|
+
}
|
|
228
|
+
.codebase-evidence-body {
|
|
229
|
+
padding: 0.75rem 1rem;
|
|
230
|
+
border-top: 1px solid var(--border-color);
|
|
231
|
+
font-size: 0.875rem;
|
|
232
|
+
color: #374151;
|
|
233
|
+
white-space: pre-wrap;
|
|
182
234
|
}
|
|
183
235
|
|
|
184
236
|
/* Comment textareas */
|
|
@@ -356,7 +408,7 @@ ${renderHeader(assets)}
|
|
|
356
408
|
<main class="main-content">
|
|
357
409
|
<div class="container">
|
|
358
410
|
<h1>Review Decisions: ${escapeHtml(ticket_key)}</h1>
|
|
359
|
-
<p class="page-intro"
|
|
411
|
+
<p class="page-intro">${escapeHtml(pageIntro)}</p>
|
|
360
412
|
|
|
361
413
|
${hasDecisions ? renderForm(data) : renderNoDecisions()}
|
|
362
414
|
|
|
@@ -371,6 +423,13 @@ ${hasDecisions ? renderScript(data) : ""}
|
|
|
371
423
|
// ---------------------------------------------------------------------------
|
|
372
424
|
// Font faces
|
|
373
425
|
// ---------------------------------------------------------------------------
|
|
426
|
+
// fontsRelPath comes from server-controlled env vars, so this is not an
|
|
427
|
+
// injection vector — but a deploy path containing a single quote or backslash
|
|
428
|
+
// would silently break CSS parsing and stop fonts from loading. Escape both
|
|
429
|
+
// so the url('...') stays well-formed regardless of deploy layout.
|
|
430
|
+
function escapeCssUrl(value) {
|
|
431
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
432
|
+
}
|
|
374
433
|
function renderFontFaces(fontsRelPath) {
|
|
375
434
|
const weights = [
|
|
376
435
|
{ weight: "400", style: "normal", file: "SourceSansPro-Regular.ttf" },
|
|
@@ -380,9 +439,10 @@ function renderFontFaces(fontsRelPath) {
|
|
|
380
439
|
{ weight: "700", style: "normal", file: "SourceSansPro-Bold.ttf" },
|
|
381
440
|
{ weight: "700", style: "italic", file: "SourceSansPro-BoldItalic.ttf" },
|
|
382
441
|
];
|
|
442
|
+
const safePath = escapeCssUrl(fontsRelPath);
|
|
383
443
|
return weights.map(({ weight, style, file }) => ` @font-face {
|
|
384
444
|
font-family: 'Source Sans Pro';
|
|
385
|
-
src: url('${
|
|
445
|
+
src: url('${safePath}/${escapeCssUrl(file)}') format('truetype');
|
|
386
446
|
font-weight: ${weight};
|
|
387
447
|
font-style: ${style};
|
|
388
448
|
}`).join("\n");
|
|
@@ -420,11 +480,11 @@ function renderForm(data) {
|
|
|
420
480
|
html += `
|
|
421
481
|
<div class="general-comment-area">
|
|
422
482
|
<label for="general-comment">General Comment (optional)</label>
|
|
423
|
-
<textarea id="general-comment" name="general_comment" placeholder="Any overall feedback or guidance..."></textarea>
|
|
483
|
+
<textarea id="general-comment" name="general_comment" placeholder="Any overall feedback or guidance..." data-testid="general-comment"></textarea>
|
|
424
484
|
</div>
|
|
425
485
|
|
|
426
486
|
<div class="submit-area">
|
|
427
|
-
<button type="button" id="submit-btn" class="btn btn-primary">Submit Decisions</button>
|
|
487
|
+
<button type="button" id="submit-btn" class="btn btn-primary" data-testid="submit-decisions">Submit Decisions</button>
|
|
428
488
|
</div>
|
|
429
489
|
</form>
|
|
430
490
|
</div>
|
|
@@ -435,7 +495,7 @@ function renderForm(data) {
|
|
|
435
495
|
<button type="button" class="btn btn-secondary" data-copy-json-btn data-default-label="Copy JSON">Copy JSON</button>
|
|
436
496
|
</div>
|
|
437
497
|
<p>Copy the JSON below and paste it back to the agent.</p>
|
|
438
|
-
<pre id="json-output"></pre>
|
|
498
|
+
<pre id="json-output" data-testid="decision-output-json"></pre>
|
|
439
499
|
<div class="copy-area">
|
|
440
500
|
<button type="button" id="copy-btn" class="btn btn-secondary" data-copy-json-btn data-default-label="Copy to Clipboard">Copy to Clipboard</button>
|
|
441
501
|
</div>
|
|
@@ -446,39 +506,74 @@ function renderDecisionItem(item) {
|
|
|
446
506
|
const id = escapeHtml(item.id);
|
|
447
507
|
const radioName = `radio-${id}`;
|
|
448
508
|
const textareaId = `comment-${id}`;
|
|
509
|
+
const titleId = `card-title-${id}`;
|
|
510
|
+
const originalQuestionLabelId = `original-question-label-${id}`;
|
|
511
|
+
const whyItMattersLabelId = `why-it-matters-label-${id}`;
|
|
512
|
+
const radioGroupLabelId = `radio-group-label-${id}`;
|
|
513
|
+
const recommendationLabelId = `recommendation-label-${id}`;
|
|
449
514
|
let optionsHtml = "";
|
|
450
515
|
for (let i = 0; i < item.options.length; i++) {
|
|
451
516
|
const val = `opt-${i}`;
|
|
452
517
|
const label = item.options[i];
|
|
518
|
+
const consequence = item.option_consequences[i] ?? "";
|
|
453
519
|
const isRecommended = i === item.recommendation_index;
|
|
454
520
|
const checked = isRecommended ? " checked" : "";
|
|
455
|
-
const
|
|
456
|
-
const
|
|
521
|
+
const labelText = escapeHtml(label);
|
|
522
|
+
const recommendationMarker = isRecommended
|
|
523
|
+
? ` <span class="ai-recommendation" data-testid="ai-recommendation-marker">(AI recommended)</span>`
|
|
524
|
+
: "";
|
|
457
525
|
optionsHtml += `
|
|
458
|
-
<div class="radio-option">
|
|
459
|
-
<
|
|
460
|
-
|
|
526
|
+
<div class="radio-option-wrapper" data-testid="option-wrapper" data-option-index="${i}">
|
|
527
|
+
<div class="radio-option">
|
|
528
|
+
<input type="radio" id="${radioName}-${val}" name="${radioName}" value="${val}"${checked} data-testid="decision-option">
|
|
529
|
+
<label for="${radioName}-${val}">${labelText}</label>${recommendationMarker}
|
|
530
|
+
</div>
|
|
531
|
+
<div class="radio-option-consequence" data-testid="option-consequence">${escapeHtml(consequence)}</div>
|
|
461
532
|
</div>`;
|
|
462
533
|
}
|
|
463
|
-
// Auto-append "None of these"
|
|
534
|
+
// Auto-append "Ask about this" then "None of these" — neither has a consequence line.
|
|
464
535
|
optionsHtml += `
|
|
465
|
-
<div class="radio-option">
|
|
466
|
-
<
|
|
467
|
-
|
|
536
|
+
<div class="radio-option-wrapper" data-testid="option-wrapper" data-option-index="ask">
|
|
537
|
+
<div class="radio-option">
|
|
538
|
+
<input type="radio" id="${radioName}-ask" name="${radioName}" value="ask" data-testid="decision-option">
|
|
539
|
+
<label for="${radioName}-ask">Ask about this</label>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
<div class="radio-option-wrapper" data-testid="option-wrapper" data-option-index="none">
|
|
543
|
+
<div class="radio-option">
|
|
544
|
+
<input type="radio" id="${radioName}-none" name="${radioName}" value="none" data-testid="decision-option">
|
|
545
|
+
<label for="${radioName}-none">None of these</label>
|
|
546
|
+
</div>
|
|
468
547
|
</div>`;
|
|
469
548
|
// Encode option labels for client-side JS to resolve chosen_label
|
|
470
549
|
const labelsJson = escapeHtml(JSON.stringify(item.options));
|
|
471
550
|
return `
|
|
472
|
-
<
|
|
473
|
-
<
|
|
474
|
-
<
|
|
475
|
-
|
|
476
|
-
|
|
551
|
+
<section class="card" data-item-id="${id}" data-item-type="decision" data-source="${escapeHtml(item.source)}" data-labels="${labelsJson}" data-testid="decision-card" aria-labelledby="${titleId}">
|
|
552
|
+
<h3 class="card-title" id="${titleId}" data-testid="decision-card-title">${escapeHtml(item.question)}</h3>
|
|
553
|
+
<section class="card-section" data-testid="original-question-section" aria-labelledby="${originalQuestionLabelId}">
|
|
554
|
+
<div class="card-section-label" id="${originalQuestionLabelId}">Original question</div>
|
|
555
|
+
<div class="card-section-body" data-testid="original-question-body">${escapeHtml(item.original_question)}</div>
|
|
556
|
+
</section>
|
|
557
|
+
<section class="card-section" data-testid="why-it-matters-section" aria-labelledby="${whyItMattersLabelId}">
|
|
558
|
+
<div class="card-section-label" id="${whyItMattersLabelId}">Why it matters</div>
|
|
559
|
+
<div class="card-section-body" data-testid="why-it-matters-body">${escapeHtml(item.why_it_matters)}</div>
|
|
560
|
+
</section>
|
|
561
|
+
<section class="radio-group" role="radiogroup" data-testid="decision-radio-group" aria-labelledby="${radioGroupLabelId}">
|
|
562
|
+
<div class="card-section-label" id="${radioGroupLabelId}">Choose an option</div>${optionsHtml}
|
|
563
|
+
</section>
|
|
564
|
+
<section class="card-section" data-testid="recommendation-explanation-section" aria-labelledby="${recommendationLabelId}">
|
|
565
|
+
<div class="card-section-label" id="${recommendationLabelId}">Why the AI recommended this</div>
|
|
566
|
+
<div class="card-section-body" data-testid="recommendation-explanation-body">${escapeHtml(item.recommendation_explanation)}</div>
|
|
567
|
+
</section>
|
|
477
568
|
<div class="comment-area">
|
|
478
569
|
<label for="${textareaId}">Comment</label>
|
|
479
|
-
<textarea id="${textareaId}" name="${textareaId}" placeholder="Required for None of these selections..."></textarea>
|
|
570
|
+
<textarea id="${textareaId}" name="${textareaId}" placeholder="Required for None of these selections..." data-testid="decision-comment"></textarea>
|
|
480
571
|
</div>
|
|
481
|
-
|
|
572
|
+
<details class="codebase-evidence" data-testid="codebase-evidence">
|
|
573
|
+
<summary>Codebase evidence</summary>
|
|
574
|
+
<div class="codebase-evidence-body" data-testid="codebase-evidence-body">${escapeHtml(item.codebase_evidence)}</div>
|
|
575
|
+
</details>
|
|
576
|
+
</section>`;
|
|
482
577
|
}
|
|
483
578
|
function renderImprovements(improvements) {
|
|
484
579
|
if (improvements.length === 0)
|
|
@@ -491,16 +586,18 @@ function renderImprovements(improvements) {
|
|
|
491
586
|
? "confidence-medium"
|
|
492
587
|
: "confidence-low";
|
|
493
588
|
items += `
|
|
494
|
-
<li>
|
|
589
|
+
<li data-testid="confirmed-improvement-item">
|
|
495
590
|
<span class="improvement-title">${escapeHtml(imp.title)}</span>
|
|
496
591
|
<span class="confidence-tag ${confClass}">${escapeHtml(imp.confidence)}</span>
|
|
497
592
|
<div class="improvement-action">${escapeHtml(imp.action)}</div>
|
|
498
593
|
</li>`;
|
|
499
594
|
}
|
|
500
|
-
return ` <
|
|
595
|
+
return ` <section data-testid="confirmed-improvements">
|
|
596
|
+
<h2>Confirmed Improvements</h2>
|
|
501
597
|
<p>These improvements have been confirmed and will be applied. No action needed from you.</p>
|
|
502
598
|
<ul class="improvements-list">${items}
|
|
503
|
-
</ul
|
|
599
|
+
</ul>
|
|
600
|
+
</section>`;
|
|
504
601
|
}
|
|
505
602
|
// ---------------------------------------------------------------------------
|
|
506
603
|
// Embedded script
|
|
@@ -508,92 +605,102 @@ function renderImprovements(improvements) {
|
|
|
508
605
|
function renderScript(data) {
|
|
509
606
|
return ` <script>
|
|
510
607
|
(function() {
|
|
511
|
-
var submitBtn
|
|
512
|
-
var copyButtons
|
|
513
|
-
var formContainer
|
|
514
|
-
var postSubmitContainer
|
|
515
|
-
var jsonOutput
|
|
516
|
-
|
|
517
|
-
submitBtn.addEventListener("click", function() {
|
|
518
|
-
var cards = document.querySelectorAll(".card[data-item-id]");
|
|
519
|
-
var valid = true;
|
|
520
|
-
|
|
521
|
-
// Validate: every card must have a selection, and "None of these" requires a comment.
|
|
522
|
-
cards.forEach(function(card) {
|
|
523
|
-
var itemId = card.getAttribute("data-item-id");
|
|
524
|
-
var selected = card.querySelector('input[type="radio"]:checked');
|
|
525
|
-
var textarea = document.getElementById("comment-" + itemId);
|
|
526
|
-
var existingMsg = card.querySelector(".validation-msg");
|
|
527
|
-
if (existingMsg) existingMsg.remove();
|
|
528
|
-
textarea.classList.remove("validation-error");
|
|
529
|
-
|
|
530
|
-
if (!selected) {
|
|
531
|
-
var radioGroup = card.querySelector(".radio-group");
|
|
532
|
-
var msg = document.createElement("div");
|
|
533
|
-
msg.className = "validation-msg";
|
|
534
|
-
msg.textContent = "Please select an option.";
|
|
535
|
-
radioGroup.appendChild(msg);
|
|
536
|
-
valid = false;
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
if (selected.value === "none" && !textarea.value.trim()) {
|
|
541
|
-
textarea.classList.add("validation-error");
|
|
542
|
-
var msg = document.createElement("div");
|
|
543
|
-
msg.className = "validation-msg";
|
|
544
|
-
msg.textContent = "A comment is required when selecting None of these.";
|
|
545
|
-
textarea.parentNode.appendChild(msg);
|
|
546
|
-
valid = false;
|
|
547
|
-
}
|
|
548
|
-
});
|
|
608
|
+
var submitBtn;
|
|
609
|
+
var copyButtons;
|
|
610
|
+
var formContainer;
|
|
611
|
+
var postSubmitContainer;
|
|
612
|
+
var jsonOutput;
|
|
549
613
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
var selected = card.querySelector('input[type="radio"]:checked');
|
|
557
|
-
var textarea = document.getElementById("comment-" + itemId);
|
|
558
|
-
var source = card.getAttribute("data-source") || "";
|
|
559
|
-
var labels = JSON.parse(card.getAttribute("data-labels") || "[]");
|
|
560
|
-
var chosenLabel = "";
|
|
561
|
-
if (selected.value === "none") {
|
|
562
|
-
chosenLabel = "None of these";
|
|
563
|
-
} else {
|
|
564
|
-
var idx = parseInt(selected.value.replace("opt-", ""), 10);
|
|
565
|
-
chosenLabel = labels[idx] || "";
|
|
566
|
-
}
|
|
567
|
-
decisions[itemId] = {
|
|
568
|
-
choice: selected.value,
|
|
569
|
-
chosen_label: chosenLabel,
|
|
570
|
-
comment: textarea ? textarea.value : "",
|
|
571
|
-
source: source
|
|
572
|
-
};
|
|
573
|
-
});
|
|
614
|
+
function init() {
|
|
615
|
+
submitBtn = document.getElementById("submit-btn");
|
|
616
|
+
copyButtons = Array.prototype.slice.call(document.querySelectorAll("[data-copy-json-btn]"));
|
|
617
|
+
formContainer = document.getElementById("form-container");
|
|
618
|
+
postSubmitContainer = document.getElementById("post-submit-container");
|
|
619
|
+
jsonOutput = document.getElementById("json-output");
|
|
574
620
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
general_comment: document.getElementById("general-comment").value
|
|
579
|
-
};
|
|
621
|
+
submitBtn.addEventListener("click", function() {
|
|
622
|
+
var cards = document.querySelectorAll(".card[data-item-id]");
|
|
623
|
+
var valid = true;
|
|
580
624
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
625
|
+
// Validate: every card must have a selection, and "None of these" requires a comment.
|
|
626
|
+
cards.forEach(function(card) {
|
|
627
|
+
var itemId = card.getAttribute("data-item-id");
|
|
628
|
+
var selected = card.querySelector('input[type="radio"]:checked');
|
|
629
|
+
var textarea = document.getElementById("comment-" + itemId);
|
|
630
|
+
var existingMsg = card.querySelector(".validation-msg");
|
|
631
|
+
if (existingMsg) existingMsg.remove();
|
|
632
|
+
textarea.classList.remove("validation-error");
|
|
585
633
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
634
|
+
if (!selected) {
|
|
635
|
+
var radioGroup = card.querySelector(".radio-group");
|
|
636
|
+
var msg = document.createElement("div");
|
|
637
|
+
msg.className = "validation-msg";
|
|
638
|
+
msg.textContent = "Please select an option.";
|
|
639
|
+
radioGroup.appendChild(msg);
|
|
640
|
+
valid = false;
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (selected.value === "none" && !textarea.value.trim()) {
|
|
645
|
+
textarea.classList.add("validation-error");
|
|
646
|
+
var msg = document.createElement("div");
|
|
647
|
+
msg.className = "validation-msg";
|
|
648
|
+
msg.textContent = "A comment is required when selecting None of these.";
|
|
649
|
+
textarea.parentNode.appendChild(msg);
|
|
650
|
+
valid = false;
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
if (!valid) return;
|
|
655
|
+
|
|
656
|
+
// Build output JSON
|
|
657
|
+
var decisions = {};
|
|
658
|
+
cards.forEach(function(card) {
|
|
659
|
+
var itemId = card.getAttribute("data-item-id");
|
|
660
|
+
var selected = card.querySelector('input[type="radio"]:checked');
|
|
661
|
+
var textarea = document.getElementById("comment-" + itemId);
|
|
662
|
+
var source = card.getAttribute("data-source") || "";
|
|
663
|
+
var labels = JSON.parse(card.getAttribute("data-labels") || "[]");
|
|
664
|
+
var chosenLabel = "";
|
|
665
|
+
if (selected.value === "none") {
|
|
666
|
+
chosenLabel = "None of these";
|
|
667
|
+
} else if (selected.value === "ask") {
|
|
668
|
+
chosenLabel = "Ask about this";
|
|
669
|
+
} else {
|
|
670
|
+
var idx = parseInt(selected.value.replace("opt-", ""), 10);
|
|
671
|
+
chosenLabel = labels[idx] || "";
|
|
672
|
+
}
|
|
673
|
+
decisions[itemId] = {
|
|
674
|
+
choice: selected.value,
|
|
675
|
+
chosen_label: chosenLabel,
|
|
676
|
+
comment: textarea ? textarea.value : "",
|
|
677
|
+
source: source
|
|
678
|
+
};
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
var output = {
|
|
682
|
+
ticket_key: ${safeJsonForScript(data.ticket_key)},
|
|
683
|
+
decisions: decisions,
|
|
684
|
+
general_comment: document.getElementById("general-comment").value
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
jsonOutput.textContent = JSON.stringify(output, null, 2);
|
|
688
|
+
formContainer.classList.add("hidden");
|
|
689
|
+
postSubmitContainer.classList.remove("hidden");
|
|
589
690
|
});
|
|
590
|
-
|
|
691
|
+
|
|
692
|
+
copyButtons.forEach(function(copyBtn) {
|
|
693
|
+
copyBtn.addEventListener("click", function() {
|
|
694
|
+
copyJson();
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
}
|
|
591
698
|
|
|
592
699
|
function copyJson() {
|
|
593
700
|
var text = jsonOutput.textContent;
|
|
594
701
|
try {
|
|
595
702
|
navigator.clipboard.writeText(text).then(function() {
|
|
596
|
-
setCopyButtonLabel(
|
|
703
|
+
setCopyButtonLabel(${safeJsonForScript(COPY_SUCCESS_LABEL)});
|
|
597
704
|
setTimeout(resetCopyButtonLabels, 2000);
|
|
598
705
|
}).catch(function() {
|
|
599
706
|
selectAndPrompt();
|
|
@@ -621,7 +728,13 @@ function renderScript(data) {
|
|
|
621
728
|
var sel = window.getSelection();
|
|
622
729
|
sel.removeAllRanges();
|
|
623
730
|
sel.addRange(range);
|
|
624
|
-
setCopyButtonLabel(
|
|
731
|
+
setCopyButtonLabel(${safeJsonForScript(COPY_FALLBACK_PROMPT_LABEL)});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (document.readyState !== "loading") {
|
|
735
|
+
init();
|
|
736
|
+
} else {
|
|
737
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
625
738
|
}
|
|
626
739
|
})();
|
|
627
740
|
</script>`;
|