@clipboard-health/tribunal 1.1.0

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/src/html.js ADDED
@@ -0,0 +1,914 @@
1
+ // cspell:words Fraunces Newsreader opsz wght onum liga lede Csvg Crect
2
+ import { formatModelSpec } from "./models.js";
3
+ const ROLE_ACCENTS = {
4
+ advocate: { label: "For", description: "Strongest reasonable case in favor." },
5
+ skeptic: { label: "Against", description: "Strongest reasonable case against." },
6
+ analyst: { label: "Neutral", description: "Decision space, tradeoffs, dependencies." },
7
+ };
8
+ const ROLE_ORDER = ["advocate", "skeptic", "analyst"];
9
+ const REPORT_STYLES = `
10
+ :root {
11
+ --paper: #F4EFE4;
12
+ --paper-dim: #EAE2D0;
13
+ --paper-edge: #E0D7C2;
14
+ --ink: #1A1614;
15
+ --ink-soft: #5C534B;
16
+ --ink-faint: #8B7F73;
17
+ --rule: #1A1614;
18
+ --accent: #7E1A2C;
19
+ --advocate: #3D5A3D;
20
+ --skeptic: #7E1A2C;
21
+ --analyst: #1E3A52;
22
+ }
23
+
24
+ * { box-sizing: border-box; }
25
+
26
+ html, body {
27
+ margin: 0;
28
+ padding: 0;
29
+ background:
30
+ radial-gradient(ellipse 80% 60% at 50% 0%, rgba(126, 26, 44, 0.05), transparent 70%),
31
+ radial-gradient(ellipse 60% 40% at 50% 100%, rgba(30, 58, 82, 0.04), transparent 70%),
32
+ var(--paper);
33
+ background-attachment: fixed;
34
+ color: var(--ink);
35
+ }
36
+
37
+ body {
38
+ font-family: 'Newsreader', Georgia, serif;
39
+ font-size: 18px;
40
+ line-height: 1.55;
41
+ font-feature-settings: "onum" 1, "kern" 1, "liga" 1;
42
+ font-optical-sizing: auto;
43
+ -webkit-font-smoothing: antialiased;
44
+ }
45
+
46
+ body::before {
47
+ content: "";
48
+ position: fixed;
49
+ inset: 0;
50
+ pointer-events: none;
51
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
52
+ opacity: 0.6;
53
+ z-index: 0;
54
+ mix-blend-mode: multiply;
55
+ }
56
+
57
+ .report {
58
+ max-width: 940px;
59
+ margin: 0 auto;
60
+ padding: 56px 56px 96px;
61
+ position: relative;
62
+ z-index: 1;
63
+ }
64
+
65
+ /* MASTHEAD */
66
+ .masthead {
67
+ border-top: 4px double var(--rule);
68
+ border-bottom: 4px double var(--rule);
69
+ padding: 18px 0 26px;
70
+ margin-bottom: 56px;
71
+ text-align: center;
72
+ }
73
+
74
+ .masthead__top {
75
+ display: flex;
76
+ justify-content: space-between;
77
+ align-items: center;
78
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
79
+ font-size: 11px;
80
+ text-transform: uppercase;
81
+ letter-spacing: 0.18em;
82
+ color: var(--ink-soft);
83
+ margin-bottom: 18px;
84
+ }
85
+
86
+ .masthead__title {
87
+ font-family: 'Fraunces', serif;
88
+ font-variation-settings: "opsz" 144, "SOFT" 60, "wght" 700;
89
+ font-size: clamp(86px, 14vw, 168px);
90
+ line-height: 0.82;
91
+ letter-spacing: -0.045em;
92
+ margin: 0;
93
+ text-transform: uppercase;
94
+ }
95
+
96
+ .masthead__subtitle {
97
+ font-family: 'Newsreader', serif;
98
+ font-style: italic;
99
+ font-size: 17px;
100
+ margin: 18px auto 0;
101
+ color: var(--ink-soft);
102
+ max-width: 56ch;
103
+ }
104
+
105
+ /* SHARED */
106
+ .section-label {
107
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
108
+ font-size: 11px;
109
+ letter-spacing: 0.26em;
110
+ text-transform: uppercase;
111
+ color: var(--accent);
112
+ border-top: 2px solid var(--ink);
113
+ padding-top: 10px;
114
+ align-self: start;
115
+ }
116
+
117
+ /* QUESTION */
118
+ .question {
119
+ margin: 0 0 72px;
120
+ display: grid;
121
+ grid-template-columns: 160px 1fr;
122
+ gap: 36px;
123
+ }
124
+
125
+ .question__text {
126
+ font-family: 'Fraunces', serif;
127
+ font-variation-settings: "opsz" 60, "SOFT" 50, "wght" 500;
128
+ font-size: clamp(28px, 3.6vw, 42px);
129
+ line-height: 1.12;
130
+ margin: 0;
131
+ letter-spacing: -0.018em;
132
+ }
133
+
134
+ .question__context {
135
+ margin-top: 22px;
136
+ font-size: 16px;
137
+ color: var(--ink-soft);
138
+ font-style: italic;
139
+ border-left: 2px solid var(--rule);
140
+ padding-left: 16px;
141
+ white-space: pre-wrap;
142
+ }
143
+
144
+ /* VERDICT */
145
+ .verdict {
146
+ margin: 0 0 32px;
147
+ }
148
+
149
+ .verdict__grid {
150
+ display: grid;
151
+ grid-template-columns: 160px 1fr;
152
+ gap: 36px;
153
+ }
154
+
155
+ .verdict__answer {
156
+ font-family: 'Newsreader', serif;
157
+ font-size: 21px;
158
+ line-height: 1.55;
159
+ margin: 0;
160
+ }
161
+
162
+ .verdict__answer::first-letter {
163
+ font-family: 'Fraunces', serif;
164
+ font-variation-settings: "opsz" 144, "SOFT" 70, "wght" 700;
165
+ font-size: 84px;
166
+ line-height: 0.84;
167
+ float: left;
168
+ margin: 8px 14px -4px 0;
169
+ color: var(--accent);
170
+ }
171
+
172
+ .recommendation {
173
+ background: var(--paper-dim);
174
+ border-left: 6px solid var(--accent);
175
+ padding: 28px 32px;
176
+ margin-top: 32px;
177
+ position: relative;
178
+ }
179
+
180
+ .recommendation::after {
181
+ content: "§";
182
+ position: absolute;
183
+ right: 24px;
184
+ top: 22px;
185
+ font-family: 'Fraunces', serif;
186
+ font-style: italic;
187
+ font-size: 32px;
188
+ color: var(--accent);
189
+ opacity: 0.35;
190
+ }
191
+
192
+ .recommendation__label {
193
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
194
+ font-size: 11px;
195
+ letter-spacing: 0.26em;
196
+ text-transform: uppercase;
197
+ color: var(--accent);
198
+ margin-bottom: 10px;
199
+ }
200
+
201
+ .recommendation__text {
202
+ font-family: 'Fraunces', serif;
203
+ font-variation-settings: "opsz" 60, "SOFT" 30, "wght" 600;
204
+ font-size: clamp(20px, 2.4vw, 28px);
205
+ line-height: 1.25;
206
+ margin: 0;
207
+ letter-spacing: -0.012em;
208
+ }
209
+
210
+ /* CONFIDENCE */
211
+ .confidence {
212
+ display: grid;
213
+ grid-template-columns: 160px 1fr auto;
214
+ gap: 36px;
215
+ align-items: center;
216
+ margin: 36px 0 0;
217
+ padding: 22px 0;
218
+ border-top: 1px solid var(--ink);
219
+ border-bottom: 1px solid var(--ink);
220
+ }
221
+
222
+ .confidence__bar {
223
+ height: 14px;
224
+ background: repeating-linear-gradient(90deg, var(--paper-edge) 0 2px, transparent 2px 8px);
225
+ position: relative;
226
+ border: 1px solid var(--ink);
227
+ }
228
+
229
+ .confidence__bar::after {
230
+ content: "";
231
+ position: absolute;
232
+ inset: 0;
233
+ width: var(--confidence, 0%);
234
+ background: var(--accent);
235
+ transition: width 700ms cubic-bezier(0.22, 1, 0.36, 1);
236
+ }
237
+
238
+ .confidence__value {
239
+ font-family: 'Fraunces', serif;
240
+ font-variation-settings: "opsz" 60, "wght" 700;
241
+ font-size: 28px;
242
+ color: var(--accent);
243
+ letter-spacing: -0.02em;
244
+ }
245
+
246
+ /* KEY TAKEAWAYS */
247
+ .takeaways {
248
+ display: grid;
249
+ grid-template-columns: 160px 1fr;
250
+ gap: 36px;
251
+ margin: 56px 0;
252
+ }
253
+
254
+ .takeaways__list {
255
+ list-style: none;
256
+ margin: 0;
257
+ padding: 0;
258
+ display: grid;
259
+ gap: 18px;
260
+ counter-reset: takeaway;
261
+ }
262
+
263
+ .takeaways__list li {
264
+ counter-increment: takeaway;
265
+ position: relative;
266
+ padding-left: 64px;
267
+ font-family: 'Newsreader', serif;
268
+ font-size: 19px;
269
+ line-height: 1.5;
270
+ min-height: 36px;
271
+ }
272
+
273
+ .takeaways__list li::before {
274
+ content: counter(takeaway, decimal-leading-zero);
275
+ position: absolute;
276
+ left: 0;
277
+ top: -4px;
278
+ font-family: 'Fraunces', serif;
279
+ font-variation-settings: "opsz" 36, "SOFT" 50, "wght" 700;
280
+ font-size: 30px;
281
+ color: var(--accent);
282
+ line-height: 1;
283
+ }
284
+
285
+ /* SPLIT */
286
+ .split {
287
+ display: grid;
288
+ grid-template-columns: 1fr 1fr;
289
+ gap: 56px;
290
+ margin: 48px 0;
291
+ border-top: 1px solid var(--ink);
292
+ padding-top: 28px;
293
+ }
294
+
295
+ .split__col h3 {
296
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
297
+ font-size: 11px;
298
+ letter-spacing: 0.26em;
299
+ text-transform: uppercase;
300
+ margin: 0 0 18px;
301
+ color: var(--accent);
302
+ }
303
+
304
+ .split__col ul {
305
+ list-style: none;
306
+ margin: 0;
307
+ padding: 0;
308
+ display: grid;
309
+ gap: 14px;
310
+ }
311
+
312
+ .split__col li {
313
+ padding-left: 22px;
314
+ position: relative;
315
+ font-size: 17px;
316
+ line-height: 1.5;
317
+ }
318
+
319
+ .split__col li::before {
320
+ content: "";
321
+ position: absolute;
322
+ left: 0;
323
+ top: 12px;
324
+ width: 14px;
325
+ height: 1px;
326
+ background: var(--ink);
327
+ }
328
+
329
+ .split__empty {
330
+ font-style: italic;
331
+ color: var(--ink-faint);
332
+ margin: 0;
333
+ }
334
+
335
+ /* TESTIMONIES */
336
+ .testimonies {
337
+ margin: 88px 0 64px;
338
+ padding-top: 30px;
339
+ border-top: 4px double var(--rule);
340
+ }
341
+
342
+ .testimonies__title {
343
+ font-family: 'Fraunces', serif;
344
+ font-variation-settings: "opsz" 96, "SOFT" 60, "wght" 700;
345
+ font-size: clamp(48px, 7.6vw, 84px);
346
+ line-height: 0.9;
347
+ margin: 0 0 14px;
348
+ letter-spacing: -0.035em;
349
+ text-transform: uppercase;
350
+ }
351
+
352
+ .testimonies__lede {
353
+ font-family: 'Newsreader', serif;
354
+ font-style: italic;
355
+ font-size: 18px;
356
+ color: var(--ink-soft);
357
+ margin: 0 0 36px;
358
+ max-width: 56ch;
359
+ }
360
+
361
+ .testimony {
362
+ --role: var(--ink);
363
+ border-top: 1px solid var(--ink);
364
+ }
365
+
366
+ .testimony:last-of-type { border-bottom: 1px solid var(--ink); }
367
+
368
+ .testimony[data-role="advocate"] { --role: var(--advocate); }
369
+ .testimony[data-role="skeptic"] { --role: var(--skeptic); }
370
+ .testimony[data-role="analyst"] { --role: var(--analyst); }
371
+
372
+ .testimony summary {
373
+ list-style: none;
374
+ cursor: pointer;
375
+ padding: 26px 0;
376
+ display: grid;
377
+ grid-template-columns: 70px 1fr auto;
378
+ align-items: baseline;
379
+ gap: 26px;
380
+ position: relative;
381
+ transition: background 240ms ease;
382
+ }
383
+
384
+ .testimony summary:hover { background: linear-gradient(90deg, transparent, var(--paper-dim) 30%, var(--paper-dim) 70%, transparent); }
385
+ .testimony summary::-webkit-details-marker { display: none; }
386
+ .testimony summary::marker { display: none; content: ""; }
387
+
388
+ .testimony__index {
389
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
390
+ font-size: 13px;
391
+ letter-spacing: 0.18em;
392
+ color: var(--role);
393
+ font-weight: 700;
394
+ align-self: center;
395
+ }
396
+
397
+ .testimony__role {
398
+ font-family: 'Fraunces', serif;
399
+ font-variation-settings: "opsz" 72, "SOFT" 40, "wght" 600;
400
+ font-size: clamp(28px, 4vw, 44px);
401
+ text-transform: uppercase;
402
+ line-height: 1;
403
+ letter-spacing: -0.025em;
404
+ color: var(--role);
405
+ display: block;
406
+ }
407
+
408
+ .testimony__tagline {
409
+ display: block;
410
+ margin-top: 6px;
411
+ font-family: 'Newsreader', serif;
412
+ font-style: italic;
413
+ font-size: 15px;
414
+ color: var(--ink-soft);
415
+ }
416
+
417
+ .testimony__chevron {
418
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
419
+ font-size: 11px;
420
+ letter-spacing: 0.22em;
421
+ color: var(--ink-soft);
422
+ text-transform: uppercase;
423
+ display: inline-flex;
424
+ gap: 14px;
425
+ align-items: center;
426
+ align-self: center;
427
+ }
428
+
429
+ .testimony__chevron::after {
430
+ content: "+";
431
+ font-family: 'Fraunces', serif;
432
+ font-variation-settings: "opsz" 36, "wght" 500;
433
+ font-size: 28px;
434
+ line-height: 0;
435
+ width: 36px;
436
+ height: 36px;
437
+ border: 1px solid var(--role);
438
+ display: inline-flex;
439
+ align-items: center;
440
+ justify-content: center;
441
+ transition: transform 280ms ease, background 280ms ease, color 280ms ease;
442
+ color: var(--role);
443
+ padding-bottom: 4px;
444
+ }
445
+
446
+ .testimony[open] .testimony__chevron span { opacity: 0.5; }
447
+ .testimony[open] .testimony__chevron::after {
448
+ content: "−";
449
+ background: var(--role);
450
+ color: var(--paper);
451
+ }
452
+
453
+ .testimony__body {
454
+ padding: 6px 0 40px 96px;
455
+ display: grid;
456
+ gap: 30px;
457
+ animation: revealTestimony 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
458
+ }
459
+
460
+ @keyframes revealTestimony {
461
+ from { opacity: 0; transform: translateY(-8px); }
462
+ to { opacity: 1; transform: none; }
463
+ }
464
+
465
+ .testimony__summary {
466
+ font-family: 'Fraunces', serif;
467
+ font-variation-settings: "opsz" 36, "SOFT" 30, "wght" 400;
468
+ font-style: italic;
469
+ font-size: 22px;
470
+ line-height: 1.4;
471
+ margin: 0;
472
+ border-left: 3px solid var(--role);
473
+ padding-left: 22px;
474
+ }
475
+
476
+ .testimony__heading {
477
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
478
+ font-size: 11px;
479
+ letter-spacing: 0.26em;
480
+ text-transform: uppercase;
481
+ margin: 0 0 14px;
482
+ color: var(--role);
483
+ }
484
+
485
+ .claims {
486
+ display: grid;
487
+ gap: 18px;
488
+ }
489
+
490
+ .claim {
491
+ padding: 20px 24px 22px;
492
+ background: var(--paper-dim);
493
+ border-left: 3px solid var(--role);
494
+ display: grid;
495
+ gap: 10px;
496
+ }
497
+
498
+ .claim__top {
499
+ display: flex;
500
+ justify-content: space-between;
501
+ align-items: baseline;
502
+ gap: 16px;
503
+ }
504
+
505
+ .claim__text {
506
+ font-family: 'Newsreader', serif;
507
+ font-size: 19px;
508
+ line-height: 1.35;
509
+ font-weight: 600;
510
+ margin: 0;
511
+ }
512
+
513
+ .claim__confidence {
514
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
515
+ font-size: 13px;
516
+ letter-spacing: 0.05em;
517
+ color: var(--role);
518
+ font-weight: 700;
519
+ white-space: nowrap;
520
+ }
521
+
522
+ .claim__bar {
523
+ height: 3px;
524
+ background: rgba(26, 22, 20, 0.08);
525
+ position: relative;
526
+ }
527
+
528
+ .claim__bar::after {
529
+ content: "";
530
+ position: absolute;
531
+ inset: 0;
532
+ width: var(--confidence);
533
+ background: var(--role);
534
+ }
535
+
536
+ .claim__reasoning {
537
+ font-size: 16px;
538
+ line-height: 1.55;
539
+ color: var(--ink-soft);
540
+ margin: 0;
541
+ }
542
+
543
+ .claim__assumptions {
544
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
545
+ font-size: 11.5px;
546
+ color: var(--ink-faint);
547
+ margin: 0;
548
+ letter-spacing: 0.04em;
549
+ text-transform: uppercase;
550
+ }
551
+
552
+ .testimony__questions {
553
+ list-style: none;
554
+ padding: 0;
555
+ margin: 0;
556
+ display: grid;
557
+ gap: 10px;
558
+ }
559
+
560
+ .testimony__questions li {
561
+ padding-left: 26px;
562
+ position: relative;
563
+ font-size: 16px;
564
+ line-height: 1.5;
565
+ }
566
+
567
+ .testimony__questions li::before {
568
+ content: "?";
569
+ position: absolute;
570
+ left: 0;
571
+ top: -2px;
572
+ font-family: 'Fraunces', serif;
573
+ font-style: italic;
574
+ font-weight: 700;
575
+ font-size: 22px;
576
+ color: var(--role);
577
+ }
578
+
579
+ .testimony__model {
580
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
581
+ font-size: 11px;
582
+ letter-spacing: 0.2em;
583
+ text-transform: uppercase;
584
+ color: var(--ink-faint);
585
+ }
586
+
587
+ /* META */
588
+ .meta {
589
+ margin-top: 72px;
590
+ padding-top: 26px;
591
+ border-top: 4px double var(--rule);
592
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
593
+ font-size: 12px;
594
+ color: var(--ink-soft);
595
+ }
596
+
597
+ .meta__row {
598
+ display: grid;
599
+ grid-template-columns: 140px 1fr;
600
+ gap: 24px;
601
+ padding: 8px 0;
602
+ border-bottom: 1px dotted rgba(26, 22, 20, 0.18);
603
+ }
604
+
605
+ .meta__row:last-child { border-bottom: 0; }
606
+
607
+ .meta__label {
608
+ text-transform: uppercase;
609
+ letter-spacing: 0.22em;
610
+ color: var(--ink);
611
+ }
612
+
613
+ .meta__value { word-break: break-word; }
614
+
615
+ .meta__warning {
616
+ color: var(--accent);
617
+ }
618
+
619
+ @media (max-width: 760px) {
620
+ .report { padding: 32px 24px 64px; }
621
+ .question,
622
+ .verdict__grid,
623
+ .takeaways,
624
+ .confidence { grid-template-columns: 1fr; gap: 18px; }
625
+ .section-label { border-top: none; padding-top: 0; border-left: 2px solid var(--ink); padding-left: 10px; }
626
+ .split { grid-template-columns: 1fr; gap: 32px; }
627
+ .testimony summary { grid-template-columns: 1fr auto; gap: 18px; }
628
+ .testimony__index { display: none; }
629
+ .testimony__body { padding-left: 0; }
630
+ }
631
+
632
+ @media (prefers-reduced-motion: reduce) {
633
+ .testimony__body { animation: none; }
634
+ .confidence__bar::after { transition: none; }
635
+ }
636
+ `;
637
+ export function renderHtmlReport(input) {
638
+ const { context, generatedAt, query, response } = input;
639
+ const { metadata, perspectives, result } = response;
640
+ const orderedPerspectives = sortPerspectives(perspectives);
641
+ const titleSuffix = query.length > 80 ? `${query.slice(0, 80)}…` : query;
642
+ return `<!DOCTYPE html>
643
+ <html lang="en">
644
+ <head>
645
+ <meta charset="utf-8">
646
+ <meta name="viewport" content="width=device-width, initial-scale=1">
647
+ <title>Tribunal — ${escapeHtml(titleSuffix)}</title>
648
+ <link rel="preconnect" href="https://fonts.googleapis.com">
649
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
650
+ <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght,SOFT@9..144,400..900,0..100&family=Newsreader:ital,opsz,wght@0,6..72,400..700;1,6..72,400..700&family=JetBrains+Mono:wght@400..700&display=swap" rel="stylesheet">
651
+ <style>${REPORT_STYLES}</style>
652
+ </head>
653
+ <body>
654
+ <main class="report">
655
+ ${renderMasthead(generatedAt)}
656
+ ${renderQuestion(query, context)}
657
+ ${renderVerdict(result)}
658
+ ${renderTakeaways(result.keyTakeaways)}
659
+ ${renderSplitSection({
660
+ hideWhenEmpty: false,
661
+ left: { heading: "Consensus", items: result.consensus, emptyLabel: "No consensus surfaced." },
662
+ right: {
663
+ heading: "Disagreements",
664
+ items: result.disagreements,
665
+ emptyLabel: "No disagreements surfaced.",
666
+ },
667
+ })}
668
+ ${renderSplitSection({
669
+ hideWhenEmpty: true,
670
+ left: { heading: "Caveats", items: result.caveats, emptyLabel: "No caveats noted." },
671
+ right: {
672
+ heading: "Open Questions",
673
+ items: result.openQuestions,
674
+ emptyLabel: "No open questions remain.",
675
+ },
676
+ })}
677
+ ${renderTestimonies(orderedPerspectives)}
678
+ ${renderMeta(metadata)}
679
+ </main>
680
+ </body>
681
+ </html>`;
682
+ }
683
+ function sortPerspectives(perspectives) {
684
+ return perspectives.toSorted((left, right) => ROLE_ORDER.indexOf(left.role) - ROLE_ORDER.indexOf(right.role));
685
+ }
686
+ function renderMasthead(generatedAt) {
687
+ const date = generatedAt.toUTCString().replace("GMT", "UTC");
688
+ const caseNumber = generatedAt
689
+ .toISOString()
690
+ .slice(0, 19)
691
+ .replaceAll("-", "")
692
+ .replaceAll(":", "")
693
+ .replace("T", "-");
694
+ return `<header class="masthead">
695
+ <div class="masthead__top">
696
+ <span>Case No. ${escapeHtml(caseNumber)}</span>
697
+ <span>${escapeHtml(date)}</span>
698
+ </div>
699
+ <h1 class="masthead__title">Tribunal</h1>
700
+ <p class="masthead__subtitle">A structured second opinion from advocate, skeptic, and analyst — synthesized by the deliberator.</p>
701
+ </header>`;
702
+ }
703
+ function renderQuestion(query, context) {
704
+ const contextBlock = context === undefined || context.trim().length === 0
705
+ ? ""
706
+ : `<div class="question__context">${escapeHtml(context)}</div>`;
707
+ return `<section class="question">
708
+ <div class="section-label">The Question</div>
709
+ <div>
710
+ <p class="question__text">${escapeHtml(query)}</p>
711
+ ${contextBlock}
712
+ </div>
713
+ </section>`;
714
+ }
715
+ function renderVerdict(result) {
716
+ const recommendation = result.recommendation ?? "No recommendation.";
717
+ const confidencePercent = Math.round(result.confidence * 100);
718
+ return `<section class="verdict">
719
+ <div class="verdict__grid">
720
+ <div class="section-label">The Verdict</div>
721
+ <div>
722
+ <p class="verdict__answer">${escapeHtml(result.answer)}</p>
723
+ <div class="recommendation">
724
+ <div class="recommendation__label">Recommendation</div>
725
+ <p class="recommendation__text">${escapeHtml(recommendation)}</p>
726
+ </div>
727
+ </div>
728
+ </div>
729
+ <div class="confidence" style="--confidence:${confidencePercent}%">
730
+ <div class="section-label">Confidence</div>
731
+ <div class="confidence__bar" aria-hidden="true"></div>
732
+ <div class="confidence__value">${confidencePercent}%</div>
733
+ </div>
734
+ </section>`;
735
+ }
736
+ function renderTakeaways(takeaways) {
737
+ if (takeaways.length === 0) {
738
+ return "";
739
+ }
740
+ const items = takeaways.map((item) => ` <li>${escapeHtml(item)}</li>`).join("\n");
741
+ return `<section class="takeaways">
742
+ <div class="section-label">Key Takeaways</div>
743
+ <ol class="takeaways__list">
744
+ ${items}
745
+ </ol>
746
+ </section>`;
747
+ }
748
+ function renderSplitSection(input) {
749
+ const { hideWhenEmpty, left, right } = input;
750
+ if (hideWhenEmpty && left.items.length === 0 && right.items.length === 0) {
751
+ return "";
752
+ }
753
+ return `<section class="split">
754
+ <div class="split__col">
755
+ <h3>${escapeHtml(left.heading)}</h3>
756
+ ${renderInlineList(left.items, left.emptyLabel)}
757
+ </div>
758
+ <div class="split__col">
759
+ <h3>${escapeHtml(right.heading)}</h3>
760
+ ${renderInlineList(right.items, right.emptyLabel)}
761
+ </div>
762
+ </section>`;
763
+ }
764
+ function renderInlineList(items, emptyLabel) {
765
+ if (items.length === 0) {
766
+ return `<p class="split__empty"><em>${escapeHtml(emptyLabel)}</em></p>`;
767
+ }
768
+ const lis = items.map((item) => ` <li>${escapeHtml(item)}</li>`).join("\n");
769
+ return `<ul>
770
+ ${lis}
771
+ </ul>`;
772
+ }
773
+ function renderTestimonies(perspectives) {
774
+ if (perspectives.length === 0) {
775
+ return "";
776
+ }
777
+ const items = perspectives
778
+ .map((perspective, index) => renderTestimony(perspective, index + 1))
779
+ .join("\n");
780
+ return `<section class="testimonies">
781
+ <h2 class="testimonies__title">Testimonies</h2>
782
+ <p class="testimonies__lede">Each specialist argued their assigned position. The deliberator above weighed the merits, not the model names.</p>
783
+ ${items}
784
+ </section>`;
785
+ }
786
+ function renderTestimony(perspective, index) {
787
+ const accent = ROLE_ACCENTS[perspective.role];
788
+ const claims = perspective.result.claims.map(renderClaim).join("\n");
789
+ const openQuestions = renderTestimonyQuestions(perspective.result.openQuestions);
790
+ return ` <details class="testimony" data-role="${escapeHtml(perspective.role)}">
791
+ <summary>
792
+ <span class="testimony__index">${formatIndex(index)}</span>
793
+ <span>
794
+ <span class="testimony__role">${escapeHtml(capitalize(perspective.role))}</span>
795
+ <span class="testimony__tagline">${escapeHtml(accent.label)} · ${escapeHtml(accent.description)}</span>
796
+ </span>
797
+ <span class="testimony__chevron"><span>Open</span></span>
798
+ </summary>
799
+ <div class="testimony__body">
800
+ <p class="testimony__summary">${escapeHtml(perspective.result.summary)}</p>
801
+ <div>
802
+ <h4 class="testimony__heading">Claims</h4>
803
+ <div class="claims">
804
+ ${claims}
805
+ </div>
806
+ </div>
807
+ ${openQuestions}
808
+ <div class="testimony__model">${escapeHtml(formatModelSpec(perspective.metadata.model))} · ${formatLatency(perspective.metadata.latencyMs)}</div>
809
+ </div>
810
+ </details>`;
811
+ }
812
+ function renderClaim(claim) {
813
+ const percent = Math.round(claim.confidence * 100);
814
+ const assumptions = claim.assumptions.length === 0
815
+ ? ""
816
+ : ` <p class="claim__assumptions">Assumptions: ${escapeHtml(claim.assumptions.join("; "))}</p>`;
817
+ return ` <article class="claim" style="--confidence:${percent}%">
818
+ <div class="claim__top">
819
+ <p class="claim__text">${escapeHtml(claim.claim)}</p>
820
+ <span class="claim__confidence">${percent}%</span>
821
+ </div>
822
+ <div class="claim__bar" aria-hidden="true"></div>
823
+ <p class="claim__reasoning">${escapeHtml(claim.reasoning)}</p>
824
+ ${assumptions}
825
+ </article>`;
826
+ }
827
+ function renderTestimonyQuestions(openQuestions) {
828
+ if (openQuestions.length === 0) {
829
+ return "";
830
+ }
831
+ const items = openQuestions
832
+ .map((question) => ` <li>${escapeHtml(question)}</li>`)
833
+ .join("\n");
834
+ return ` <div>
835
+ <h4 class="testimony__heading">Open questions</h4>
836
+ <ul class="testimony__questions">
837
+ ${items}
838
+ </ul>
839
+ </div>`;
840
+ }
841
+ function renderMeta(metadata) {
842
+ const rows = [
843
+ ["Models", formatModelSet(metadata.models)],
844
+ ["Tokens", formatTokens(metadata.totalUsage)],
845
+ ["Cost", formatCost(metadata.estimatedCostUsd)],
846
+ ["Latency", formatLatency(metadata.latencyMs)],
847
+ ["Warnings", formatWarnings(metadata.warnings)],
848
+ ];
849
+ const html = rows
850
+ .map(([label, value]) => ` <div class="meta__row"><span class="meta__label">${escapeHtml(label ?? "")}</span><span class="meta__value">${value ?? ""}</span></div>`)
851
+ .join("\n");
852
+ return `<footer class="meta">
853
+ ${html}
854
+ </footer>`;
855
+ }
856
+ function formatModelSet(models) {
857
+ return ["advocate", "skeptic", "analyst", "deliberator"]
858
+ .map((role) => `${role}=${escapeHtml(formatModelSpec(models[role]))}`)
859
+ .join(" · ");
860
+ }
861
+ function formatTokens(usage) {
862
+ if (usage === undefined) {
863
+ return "unknown";
864
+ }
865
+ const parts = [];
866
+ if (usage.inputTokens !== undefined) {
867
+ parts.push(`in ${usage.inputTokens.toLocaleString("en-US")}`);
868
+ }
869
+ if (usage.outputTokens !== undefined) {
870
+ parts.push(`out ${usage.outputTokens.toLocaleString("en-US")}`);
871
+ }
872
+ if (usage.totalTokens !== undefined) {
873
+ parts.push(`total ${usage.totalTokens.toLocaleString("en-US")}`);
874
+ }
875
+ return parts.length === 0 ? "unknown" : parts.join(" · ");
876
+ }
877
+ function formatCost(estimatedCostUsd) {
878
+ if (estimatedCostUsd === null) {
879
+ return "unknown";
880
+ }
881
+ return `$${estimatedCostUsd.toFixed(6)}`;
882
+ }
883
+ function formatLatency(latencyMs) {
884
+ if (latencyMs === undefined) {
885
+ return "unknown";
886
+ }
887
+ if (latencyMs >= 1000) {
888
+ return `${(latencyMs / 1000).toFixed(1)}s`;
889
+ }
890
+ return `${latencyMs}ms`;
891
+ }
892
+ function formatWarnings(warnings) {
893
+ if (warnings.length === 0) {
894
+ return "none";
895
+ }
896
+ return warnings
897
+ .map((warning) => `<span class="meta__warning">${escapeHtml(warning)}</span>`)
898
+ .join(" · ");
899
+ }
900
+ function formatIndex(value) {
901
+ return value.toString().padStart(2, "0");
902
+ }
903
+ function capitalize(value) {
904
+ return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
905
+ }
906
+ function escapeHtml(value) {
907
+ return value
908
+ .replaceAll("&", "&amp;")
909
+ .replaceAll("<", "&lt;")
910
+ .replaceAll(">", "&gt;")
911
+ .replaceAll('"', "&quot;")
912
+ .replaceAll("'", "&#39;");
913
+ }
914
+ //# sourceMappingURL=html.js.map