@bridge_gpt/mcp-server 0.1.7 → 0.1.9
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/README.md +191 -93
- package/build/agent-utils.js +110 -0
- package/build/agents.generated.js +13 -0
- package/build/commands.generated.js +9 -8
- package/build/decision-page-template.js +628 -0
- package/build/index.js +172 -2
- package/build/pipelines.generated.js +79 -3
- package/package.json +3 -2
- package/pipelines/implement-ticket.json +10 -0
- package/pipelines/plan-epic.json +44 -0
- package/pipelines/review-ticket.json +11 -1
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision Page Template
|
|
3
|
+
*
|
|
4
|
+
* Generates a self-contained HTML page for capturing user decisions on
|
|
5
|
+
* ticket review findings. The page renders recommendation-driven
|
|
6
|
+
* decision items with custom options from resolution guide decision
|
|
7
|
+
* trees plus an informational list of confirmed improvements. On
|
|
8
|
+
* submit, the page produces JSON output for the agent to consume.
|
|
9
|
+
*/
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// HTML escaping
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const ESCAPE_MAP = {
|
|
14
|
+
"&": "&",
|
|
15
|
+
"<": "<",
|
|
16
|
+
">": ">",
|
|
17
|
+
'"': """,
|
|
18
|
+
"'": "'",
|
|
19
|
+
};
|
|
20
|
+
export function escapeHtml(str) {
|
|
21
|
+
return str.replace(/[&<>"']/g, (ch) => ESCAPE_MAP[ch]);
|
|
22
|
+
}
|
|
23
|
+
/** Escape a string for safe embedding inside a <script> tag via JSON. */
|
|
24
|
+
function safeJsonForScript(value) {
|
|
25
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Template
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const DEFAULT_ASSETS = { faviconBase64: "", logoBase64: "", fontsRelPath: "" };
|
|
31
|
+
export function generateDecisionPageHtml(data, assets = DEFAULT_ASSETS) {
|
|
32
|
+
const { ticket_key, actionable_items, clear_improvements } = data;
|
|
33
|
+
const hasDecisions = actionable_items.length > 0;
|
|
34
|
+
const faviconLink = assets.faviconBase64
|
|
35
|
+
? `<link rel="icon" type="image/png" sizes="32x32" href="data:image/png;base64,${assets.faviconBase64}">`
|
|
36
|
+
: "";
|
|
37
|
+
const fontFaces = assets.fontsRelPath ? renderFontFaces(assets.fontsRelPath) : "";
|
|
38
|
+
return `<!DOCTYPE html>
|
|
39
|
+
<html lang="en">
|
|
40
|
+
<head>
|
|
41
|
+
<meta charset="UTF-8">
|
|
42
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
43
|
+
<title>Review Decisions: ${escapeHtml(ticket_key)}</title>
|
|
44
|
+
${faviconLink}
|
|
45
|
+
<style>
|
|
46
|
+
${fontFaces}
|
|
47
|
+
:root {
|
|
48
|
+
--primary-color: #E2624B;
|
|
49
|
+
--primary-dark: #C51616;
|
|
50
|
+
--secondary-color: #6b7280;
|
|
51
|
+
--light-grey: #eee;
|
|
52
|
+
--secondary-button-background: #fdf4ec;
|
|
53
|
+
--light-orange: #F2B27A;
|
|
54
|
+
--dark-orange: #e17055;
|
|
55
|
+
--background-color: #fffdf0;
|
|
56
|
+
--surface-color: #ffffff;
|
|
57
|
+
--text-color: #393D4E;
|
|
58
|
+
--border-color: #e6e4d5;
|
|
59
|
+
--error-color: #ef4444;
|
|
60
|
+
--success-color: #10b981;
|
|
61
|
+
--box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
62
|
+
}
|
|
63
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
64
|
+
body {
|
|
65
|
+
font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
|
66
|
+
background-color: var(--background-color);
|
|
67
|
+
color: var(--text-color);
|
|
68
|
+
line-height: 1.5;
|
|
69
|
+
min-height: 100vh;
|
|
70
|
+
}
|
|
71
|
+
.hidden { display: none !important; }
|
|
72
|
+
|
|
73
|
+
/* Header */
|
|
74
|
+
.site-header {
|
|
75
|
+
background: var(--surface-color);
|
|
76
|
+
box-shadow: var(--box-shadow);
|
|
77
|
+
padding: 0 1rem;
|
|
78
|
+
height: 60px;
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
}
|
|
82
|
+
.site-header .header-inner {
|
|
83
|
+
max-width: 1200px;
|
|
84
|
+
width: 100%;
|
|
85
|
+
margin: 0 auto;
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: 1rem;
|
|
89
|
+
}
|
|
90
|
+
.site-header img {
|
|
91
|
+
height: 32px;
|
|
92
|
+
width: auto;
|
|
93
|
+
}
|
|
94
|
+
.site-header .header-title {
|
|
95
|
+
font-size: 1rem;
|
|
96
|
+
font-weight: 600;
|
|
97
|
+
color: var(--secondary-color);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Main content */
|
|
101
|
+
.main-content {
|
|
102
|
+
max-width: 1200px;
|
|
103
|
+
margin: 2rem auto;
|
|
104
|
+
padding: 0 1rem;
|
|
105
|
+
}
|
|
106
|
+
.container {
|
|
107
|
+
background: var(--surface-color);
|
|
108
|
+
border-radius: 0.5rem;
|
|
109
|
+
box-shadow: var(--box-shadow);
|
|
110
|
+
padding: 2rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
h1 {
|
|
114
|
+
font-size: 1.5rem;
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
margin-bottom: 0.5rem;
|
|
117
|
+
}
|
|
118
|
+
h2 {
|
|
119
|
+
font-size: 1rem;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
border-bottom: 1px solid var(--border-color);
|
|
122
|
+
padding-bottom: 0.5rem;
|
|
123
|
+
margin-top: 2rem;
|
|
124
|
+
margin-bottom: 1rem;
|
|
125
|
+
}
|
|
126
|
+
.page-intro {
|
|
127
|
+
color: var(--secondary-color);
|
|
128
|
+
margin-bottom: 1.5rem;
|
|
129
|
+
font-size: 0.95rem;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Cards */
|
|
133
|
+
.card {
|
|
134
|
+
background: var(--surface-color);
|
|
135
|
+
border: 1px solid var(--border-color);
|
|
136
|
+
border-radius: 0.5rem;
|
|
137
|
+
box-shadow: var(--box-shadow);
|
|
138
|
+
padding: 1.5rem;
|
|
139
|
+
margin-bottom: 1rem;
|
|
140
|
+
}
|
|
141
|
+
.card-title {
|
|
142
|
+
font-size: 1.1rem;
|
|
143
|
+
font-weight: 600;
|
|
144
|
+
margin: 0 0 0.5rem 0;
|
|
145
|
+
}
|
|
146
|
+
.card-context {
|
|
147
|
+
background: #eef2ff;
|
|
148
|
+
border-left: 4px solid var(--primary-color);
|
|
149
|
+
padding: 0.75rem 1rem;
|
|
150
|
+
margin-bottom: 1rem;
|
|
151
|
+
font-size: 0.875rem;
|
|
152
|
+
color: #374151;
|
|
153
|
+
border-radius: 0 0.25rem 0.25rem 0;
|
|
154
|
+
white-space: pre-wrap;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Radio groups */
|
|
158
|
+
.radio-group {
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-direction: column;
|
|
161
|
+
gap: 0.5rem;
|
|
162
|
+
margin-bottom: 0.5rem;
|
|
163
|
+
}
|
|
164
|
+
.radio-option {
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: 0.5rem;
|
|
168
|
+
}
|
|
169
|
+
.radio-option input[type="radio"] {
|
|
170
|
+
margin: 0;
|
|
171
|
+
accent-color: var(--primary-color);
|
|
172
|
+
}
|
|
173
|
+
.radio-option label {
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
font-size: 0.95rem;
|
|
176
|
+
font-weight: 400;
|
|
177
|
+
margin-bottom: 0;
|
|
178
|
+
}
|
|
179
|
+
.ai-recommendation {
|
|
180
|
+
color: var(--primary-color);
|
|
181
|
+
font-weight: 600;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* Comment textareas */
|
|
185
|
+
.comment-area {
|
|
186
|
+
margin-top: 0.75rem;
|
|
187
|
+
}
|
|
188
|
+
.comment-area label {
|
|
189
|
+
display: block;
|
|
190
|
+
font-size: 0.875rem;
|
|
191
|
+
font-weight: 500;
|
|
192
|
+
color: var(--secondary-color);
|
|
193
|
+
margin-bottom: 0.5rem;
|
|
194
|
+
}
|
|
195
|
+
.comment-area textarea {
|
|
196
|
+
width: 100%;
|
|
197
|
+
min-height: 60px;
|
|
198
|
+
padding: 0.5rem;
|
|
199
|
+
border: 1px solid var(--border-color);
|
|
200
|
+
border-radius: 0.25rem;
|
|
201
|
+
font-family: inherit;
|
|
202
|
+
font-size: 1rem;
|
|
203
|
+
resize: vertical;
|
|
204
|
+
background-color: #fff;
|
|
205
|
+
}
|
|
206
|
+
.comment-area textarea.validation-error {
|
|
207
|
+
border-color: var(--error-color);
|
|
208
|
+
outline-color: var(--error-color);
|
|
209
|
+
}
|
|
210
|
+
.validation-msg {
|
|
211
|
+
color: var(--error-color);
|
|
212
|
+
font-size: 0.8rem;
|
|
213
|
+
margin-top: 0.25rem;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* General comment */
|
|
217
|
+
.general-comment-area {
|
|
218
|
+
margin-top: 1.5rem;
|
|
219
|
+
}
|
|
220
|
+
.general-comment-area label {
|
|
221
|
+
display: block;
|
|
222
|
+
font-size: 0.875rem;
|
|
223
|
+
font-weight: 500;
|
|
224
|
+
margin-bottom: 0.5rem;
|
|
225
|
+
}
|
|
226
|
+
.general-comment-area textarea {
|
|
227
|
+
width: 100%;
|
|
228
|
+
min-height: 80px;
|
|
229
|
+
padding: 0.5rem;
|
|
230
|
+
border: 1px solid var(--border-color);
|
|
231
|
+
border-radius: 0.25rem;
|
|
232
|
+
font-family: inherit;
|
|
233
|
+
font-size: 1rem;
|
|
234
|
+
resize: vertical;
|
|
235
|
+
background-color: #fff;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* Buttons */
|
|
239
|
+
.btn {
|
|
240
|
+
cursor: pointer;
|
|
241
|
+
padding: 0.75rem 2rem;
|
|
242
|
+
border-radius: 4px;
|
|
243
|
+
border: 1px solid transparent;
|
|
244
|
+
font-size: 1rem;
|
|
245
|
+
line-height: 1.25rem;
|
|
246
|
+
font-weight: 600;
|
|
247
|
+
transition: background-color 0.3s ease;
|
|
248
|
+
}
|
|
249
|
+
.btn-primary {
|
|
250
|
+
background-color: var(--primary-color);
|
|
251
|
+
color: white;
|
|
252
|
+
}
|
|
253
|
+
.btn-primary:hover {
|
|
254
|
+
background-color: var(--primary-dark);
|
|
255
|
+
}
|
|
256
|
+
.btn-secondary {
|
|
257
|
+
background-color: var(--secondary-button-background);
|
|
258
|
+
border: 1px solid var(--light-orange);
|
|
259
|
+
color: #333;
|
|
260
|
+
}
|
|
261
|
+
.btn-secondary:hover {
|
|
262
|
+
background-color: var(--dark-orange);
|
|
263
|
+
color: white;
|
|
264
|
+
}
|
|
265
|
+
.submit-area {
|
|
266
|
+
margin-top: 2rem;
|
|
267
|
+
text-align: center;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* Post-submit */
|
|
271
|
+
.post-submit-container {
|
|
272
|
+
margin-top: 1.5rem;
|
|
273
|
+
}
|
|
274
|
+
.post-submit-header {
|
|
275
|
+
display: flex;
|
|
276
|
+
align-items: center;
|
|
277
|
+
justify-content: space-between;
|
|
278
|
+
gap: 1rem;
|
|
279
|
+
margin-bottom: 0.5rem;
|
|
280
|
+
}
|
|
281
|
+
.post-submit-header h2 {
|
|
282
|
+
margin: 0;
|
|
283
|
+
padding: 0;
|
|
284
|
+
border: 0;
|
|
285
|
+
}
|
|
286
|
+
.post-submit-container pre {
|
|
287
|
+
background: #1e1e1e;
|
|
288
|
+
color: #d4d4d4;
|
|
289
|
+
padding: 1.5rem;
|
|
290
|
+
border-radius: 0.5rem;
|
|
291
|
+
overflow-x: auto;
|
|
292
|
+
max-height: 500px;
|
|
293
|
+
overflow-y: auto;
|
|
294
|
+
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
|
|
295
|
+
font-size: 0.85rem;
|
|
296
|
+
line-height: 1.5;
|
|
297
|
+
white-space: pre-wrap;
|
|
298
|
+
word-wrap: break-word;
|
|
299
|
+
}
|
|
300
|
+
.copy-area {
|
|
301
|
+
margin-top: 1rem;
|
|
302
|
+
text-align: center;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* Confidence tags — matches Bridge status badge colors */
|
|
306
|
+
.confidence-tag {
|
|
307
|
+
display: inline-block;
|
|
308
|
+
padding: 0.1rem 0.5rem;
|
|
309
|
+
border-radius: 3px;
|
|
310
|
+
font-size: 0.75rem;
|
|
311
|
+
font-weight: 600;
|
|
312
|
+
text-transform: uppercase;
|
|
313
|
+
margin-left: 0.5rem;
|
|
314
|
+
}
|
|
315
|
+
.confidence-high { background: #dcfce7; color: #166534; }
|
|
316
|
+
.confidence-medium { background: #fef3c7; color: #92400e; }
|
|
317
|
+
.confidence-low { background: #fef2f2; color: #991b1b; }
|
|
318
|
+
|
|
319
|
+
/* Improvements list */
|
|
320
|
+
.improvements-list {
|
|
321
|
+
list-style: none;
|
|
322
|
+
padding: 0;
|
|
323
|
+
}
|
|
324
|
+
.improvements-list li {
|
|
325
|
+
padding: 0.75rem 0;
|
|
326
|
+
border-bottom: 1px solid var(--border-color);
|
|
327
|
+
}
|
|
328
|
+
.improvements-list li:last-child { border-bottom: none; }
|
|
329
|
+
.improvement-title { font-weight: 600; }
|
|
330
|
+
.improvement-action {
|
|
331
|
+
font-size: 0.9rem;
|
|
332
|
+
color: var(--secondary-color);
|
|
333
|
+
margin-top: 0.25rem;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* No decisions state */
|
|
337
|
+
.no-decisions-msg {
|
|
338
|
+
text-align: center;
|
|
339
|
+
padding: 2rem;
|
|
340
|
+
color: var(--success-color);
|
|
341
|
+
font-size: 1.2rem;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
@media (max-width: 768px) {
|
|
345
|
+
.main-content { padding: 0 1rem; margin: 1.5rem auto; }
|
|
346
|
+
.container { padding: 1.5rem; }
|
|
347
|
+
.post-submit-header {
|
|
348
|
+
align-items: stretch;
|
|
349
|
+
flex-direction: column;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
</style>
|
|
353
|
+
</head>
|
|
354
|
+
<body>
|
|
355
|
+
${renderHeader(assets)}
|
|
356
|
+
<main class="main-content">
|
|
357
|
+
<div class="container">
|
|
358
|
+
<h1>Review Decisions: ${escapeHtml(ticket_key)}</h1>
|
|
359
|
+
<p class="page-intro">The AI has analyzed this ticket and needs your input to finalize the review. Please make your selections below, then submit to generate the JSON output.</p>
|
|
360
|
+
|
|
361
|
+
${hasDecisions ? renderForm(data) : renderNoDecisions()}
|
|
362
|
+
|
|
363
|
+
${renderImprovements(clear_improvements)}
|
|
364
|
+
|
|
365
|
+
</div>
|
|
366
|
+
</main>
|
|
367
|
+
${hasDecisions ? renderScript(data) : ""}
|
|
368
|
+
</body>
|
|
369
|
+
</html>`;
|
|
370
|
+
}
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
// Font faces
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
function renderFontFaces(fontsRelPath) {
|
|
375
|
+
const weights = [
|
|
376
|
+
{ weight: "400", style: "normal", file: "SourceSansPro-Regular.ttf" },
|
|
377
|
+
{ weight: "400", style: "italic", file: "SourceSansPro-Italic.ttf" },
|
|
378
|
+
{ weight: "600", style: "normal", file: "SourceSansPro-SemiBold.ttf" },
|
|
379
|
+
{ weight: "600", style: "italic", file: "SourceSansPro-SemiBoldItalic.ttf" },
|
|
380
|
+
{ weight: "700", style: "normal", file: "SourceSansPro-Bold.ttf" },
|
|
381
|
+
{ weight: "700", style: "italic", file: "SourceSansPro-BoldItalic.ttf" },
|
|
382
|
+
];
|
|
383
|
+
return weights.map(({ weight, style, file }) => ` @font-face {
|
|
384
|
+
font-family: 'Source Sans Pro';
|
|
385
|
+
src: url('${fontsRelPath}/${file}') format('truetype');
|
|
386
|
+
font-weight: ${weight};
|
|
387
|
+
font-style: ${style};
|
|
388
|
+
}`).join("\n");
|
|
389
|
+
}
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// Section renderers
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
function renderHeader(assets) {
|
|
394
|
+
const logoImg = assets.logoBase64
|
|
395
|
+
? `<img src="data:image/png;base64,${assets.logoBase64}" alt="Bridge">`
|
|
396
|
+
: "";
|
|
397
|
+
return ` <header class="site-header">
|
|
398
|
+
<div class="header-inner">
|
|
399
|
+
${logoImg}
|
|
400
|
+
<span class="header-title">Bridge GPT</span>
|
|
401
|
+
</div>
|
|
402
|
+
</header>`;
|
|
403
|
+
}
|
|
404
|
+
function renderNoDecisions() {
|
|
405
|
+
return ` <div class="no-decisions-msg">
|
|
406
|
+
<p>No decisions needed. All suggestions were confirmed as improvements.</p>
|
|
407
|
+
</div>`;
|
|
408
|
+
}
|
|
409
|
+
function renderForm(data) {
|
|
410
|
+
const { actionable_items } = data;
|
|
411
|
+
let html = ` <div id="form-container">
|
|
412
|
+
<form id="decision-form">`;
|
|
413
|
+
if (actionable_items.length > 0) {
|
|
414
|
+
html += `
|
|
415
|
+
<h2>Review Decisions</h2>`;
|
|
416
|
+
for (const item of actionable_items) {
|
|
417
|
+
html += renderDecisionItem(item);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
html += `
|
|
421
|
+
<div class="general-comment-area">
|
|
422
|
+
<label for="general-comment">General Comment (optional)</label>
|
|
423
|
+
<textarea id="general-comment" name="general_comment" placeholder="Any overall feedback or guidance..."></textarea>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<div class="submit-area">
|
|
427
|
+
<button type="button" id="submit-btn" class="btn btn-primary">Submit Decisions</button>
|
|
428
|
+
</div>
|
|
429
|
+
</form>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
<div id="post-submit-container" class="post-submit-container hidden">
|
|
433
|
+
<div class="post-submit-header">
|
|
434
|
+
<h2>Output JSON</h2>
|
|
435
|
+
<button type="button" class="btn btn-secondary" data-copy-json-btn data-default-label="Copy JSON">Copy JSON</button>
|
|
436
|
+
</div>
|
|
437
|
+
<p>Copy the JSON below and paste it back to the agent.</p>
|
|
438
|
+
<pre id="json-output"></pre>
|
|
439
|
+
<div class="copy-area">
|
|
440
|
+
<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
|
+
</div>
|
|
442
|
+
</div>`;
|
|
443
|
+
return html;
|
|
444
|
+
}
|
|
445
|
+
function renderDecisionItem(item) {
|
|
446
|
+
const id = escapeHtml(item.id);
|
|
447
|
+
const radioName = `radio-${id}`;
|
|
448
|
+
const textareaId = `comment-${id}`;
|
|
449
|
+
let optionsHtml = "";
|
|
450
|
+
for (let i = 0; i < item.options.length; i++) {
|
|
451
|
+
const val = `opt-${i}`;
|
|
452
|
+
const label = item.options[i];
|
|
453
|
+
const isRecommended = i === item.recommendation_index;
|
|
454
|
+
const checked = isRecommended ? " checked" : "";
|
|
455
|
+
const labelClass = isRecommended ? ' class="ai-recommendation"' : "";
|
|
456
|
+
const labelText = escapeHtml(label) + (isRecommended ? " (AI recommendation)" : "");
|
|
457
|
+
optionsHtml += `
|
|
458
|
+
<div class="radio-option">
|
|
459
|
+
<input type="radio" id="${radioName}-${val}" name="${radioName}" value="${val}"${checked}>
|
|
460
|
+
<label for="${radioName}-${val}"${labelClass}>${labelText}</label>
|
|
461
|
+
</div>`;
|
|
462
|
+
}
|
|
463
|
+
// Auto-append "None of these"
|
|
464
|
+
optionsHtml += `
|
|
465
|
+
<div class="radio-option">
|
|
466
|
+
<input type="radio" id="${radioName}-none" name="${radioName}" value="none">
|
|
467
|
+
<label for="${radioName}-none">None of these</label>
|
|
468
|
+
</div>`;
|
|
469
|
+
// Encode option labels for client-side JS to resolve chosen_label
|
|
470
|
+
const labelsJson = escapeHtml(JSON.stringify(item.options));
|
|
471
|
+
return `
|
|
472
|
+
<div class="card" data-item-id="${id}" data-item-type="decision" data-source="${escapeHtml(item.source)}" data-labels="${labelsJson}">
|
|
473
|
+
<div class="card-title">${escapeHtml(item.question)}</div>
|
|
474
|
+
<div class="card-context">${escapeHtml(item.context)}</div>
|
|
475
|
+
<div class="radio-group">${optionsHtml}
|
|
476
|
+
</div>
|
|
477
|
+
<div class="comment-area">
|
|
478
|
+
<label for="${textareaId}">Comment</label>
|
|
479
|
+
<textarea id="${textareaId}" name="${textareaId}" placeholder="Required for None of these selections..."></textarea>
|
|
480
|
+
</div>
|
|
481
|
+
</div>`;
|
|
482
|
+
}
|
|
483
|
+
function renderImprovements(improvements) {
|
|
484
|
+
if (improvements.length === 0)
|
|
485
|
+
return "";
|
|
486
|
+
let items = "";
|
|
487
|
+
for (const imp of improvements) {
|
|
488
|
+
const confClass = imp.confidence.toLowerCase() === "high"
|
|
489
|
+
? "confidence-high"
|
|
490
|
+
: imp.confidence.toLowerCase() === "medium"
|
|
491
|
+
? "confidence-medium"
|
|
492
|
+
: "confidence-low";
|
|
493
|
+
items += `
|
|
494
|
+
<li>
|
|
495
|
+
<span class="improvement-title">${escapeHtml(imp.title)}</span>
|
|
496
|
+
<span class="confidence-tag ${confClass}">${escapeHtml(imp.confidence)}</span>
|
|
497
|
+
<div class="improvement-action">${escapeHtml(imp.action)}</div>
|
|
498
|
+
</li>`;
|
|
499
|
+
}
|
|
500
|
+
return ` <h2>Confirmed Improvements</h2>
|
|
501
|
+
<p>These improvements have been confirmed and will be applied. No action needed from you.</p>
|
|
502
|
+
<ul class="improvements-list">${items}
|
|
503
|
+
</ul>`;
|
|
504
|
+
}
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Embedded script
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
function renderScript(data) {
|
|
509
|
+
return ` <script>
|
|
510
|
+
(function() {
|
|
511
|
+
var submitBtn = document.getElementById("submit-btn");
|
|
512
|
+
var copyButtons = Array.prototype.slice.call(document.querySelectorAll("[data-copy-json-btn]"));
|
|
513
|
+
var formContainer = document.getElementById("form-container");
|
|
514
|
+
var postSubmitContainer = document.getElementById("post-submit-container");
|
|
515
|
+
var jsonOutput = document.getElementById("json-output");
|
|
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
|
+
});
|
|
549
|
+
|
|
550
|
+
if (!valid) return;
|
|
551
|
+
|
|
552
|
+
// Build output JSON
|
|
553
|
+
var decisions = {};
|
|
554
|
+
cards.forEach(function(card) {
|
|
555
|
+
var itemId = card.getAttribute("data-item-id");
|
|
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
|
+
});
|
|
574
|
+
|
|
575
|
+
var output = {
|
|
576
|
+
ticket_key: ${safeJsonForScript(data.ticket_key)},
|
|
577
|
+
decisions: decisions,
|
|
578
|
+
general_comment: document.getElementById("general-comment").value
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
jsonOutput.textContent = JSON.stringify(output, null, 2);
|
|
582
|
+
formContainer.classList.add("hidden");
|
|
583
|
+
postSubmitContainer.classList.remove("hidden");
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
copyButtons.forEach(function(copyBtn) {
|
|
587
|
+
copyBtn.addEventListener("click", function() {
|
|
588
|
+
copyJson();
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
function copyJson() {
|
|
593
|
+
var text = jsonOutput.textContent;
|
|
594
|
+
try {
|
|
595
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
596
|
+
setCopyButtonLabel("Copied!");
|
|
597
|
+
setTimeout(resetCopyButtonLabels, 2000);
|
|
598
|
+
}).catch(function() {
|
|
599
|
+
selectAndPrompt();
|
|
600
|
+
});
|
|
601
|
+
} catch (e) {
|
|
602
|
+
selectAndPrompt();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function setCopyButtonLabel(label) {
|
|
607
|
+
copyButtons.forEach(function(button) {
|
|
608
|
+
button.textContent = label;
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function resetCopyButtonLabels() {
|
|
613
|
+
copyButtons.forEach(function(button) {
|
|
614
|
+
button.textContent = button.getAttribute("data-default-label") || "Copy JSON";
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function selectAndPrompt() {
|
|
619
|
+
var range = document.createRange();
|
|
620
|
+
range.selectNodeContents(jsonOutput);
|
|
621
|
+
var sel = window.getSelection();
|
|
622
|
+
sel.removeAllRanges();
|
|
623
|
+
sel.addRange(range);
|
|
624
|
+
setCopyButtonLabel("Auto-copy unavailable. Press Ctrl+C / Cmd+C to copy.");
|
|
625
|
+
}
|
|
626
|
+
})();
|
|
627
|
+
</script>`;
|
|
628
|
+
}
|