pi-interview 0.4.4 → 0.5.1

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 CHANGED
@@ -21,7 +21,10 @@ Restart pi to load the extension.
21
21
 
22
22
  ## Features
23
23
 
24
- - **Question Types**: Single-select, multi-select, text input, and image upload
24
+ - **Question Types**: Single-select, multi-select, text input, image upload, and info panels
25
+ - **Rich Media**: Embed images, Chart.js charts, Mermaid diagrams, tables, and HTML in questions
26
+ - **Pre-selection**: Recommended options show a "Recommended" badge and are pre-checked on load
27
+ - **Conviction & Weight**: Control recommendation strength (`conviction`) and visual prominence (`weight`)
25
28
  - **"Other" Option**: Single/multi select questions support custom text input
26
29
  - **Per-Question Attachments**: Attach images to any question via button, paste, or drag & drop
27
30
  - **Keyboard Navigation**: Full keyboard support with arrow keys, Tab, Enter
@@ -79,14 +82,22 @@ await interview({
79
82
  ```json
80
83
  {
81
84
  "title": "Project Setup",
82
- "description": "Optional description text",
85
+ "description": "Review my suggestions and adjust as needed.",
83
86
  "questions": [
87
+ {
88
+ "id": "context",
89
+ "type": "info",
90
+ "question": "Architecture context",
91
+ "context": "This project needs SSR and edge deployment support."
92
+ },
84
93
  {
85
94
  "id": "framework",
86
95
  "type": "single",
87
96
  "question": "Which framework?",
88
97
  "options": ["React", "Vue", "Svelte"],
89
- "recommended": "React"
98
+ "recommended": "React",
99
+ "conviction": "strong",
100
+ "weight": "critical"
90
101
  },
91
102
  {
92
103
  "id": "features",
@@ -96,6 +107,14 @@ await interview({
96
107
  "options": ["Auth", "Database", "API"],
97
108
  "recommended": ["Auth", "Database"]
98
109
  },
110
+ {
111
+ "id": "indent",
112
+ "type": "single",
113
+ "question": "Indent style?",
114
+ "options": ["Tabs", "Spaces (2)", "Spaces (4)"],
115
+ "recommended": "Spaces (2)",
116
+ "weight": "minor"
117
+ },
99
118
  {
100
119
  "id": "notes",
101
120
  "type": "text",
@@ -115,12 +134,15 @@ await interview({
115
134
  | Field | Type | Description |
116
135
  |-------|------|-------------|
117
136
  | `id` | string | Unique identifier |
118
- | `type` | string | `single`, `multi`, `text`, or `image` |
137
+ | `type` | string | `single`, `multi`, `text`, `image`, or `info` |
119
138
  | `question` | string | Question text |
120
139
  | `options` | string[] or object[] | Choices (required for single/multi). Can be strings or `{ label, code? }` objects |
121
- | `recommended` | string or string[] | Highlighted option(s) with `*` indicator |
140
+ | `recommended` | string or string[] | Shows "Recommended" badge and pre-selects option(s) |
141
+ | `conviction` | string | `"strong"` or `"slight"`. Slight opts out of pre-selection. Requires `recommended` |
142
+ | `weight` | string | `"critical"` (prominent card) or `"minor"` (compact card) |
122
143
  | `context` | string | Help text shown below question |
123
144
  | `codeBlock` | object | Code block displayed below question text |
145
+ | `media` | object or object[] | Media content: image, chart, mermaid, table, or html |
124
146
 
125
147
  ### Code Blocks
126
148
 
@@ -182,6 +204,64 @@ Questions and options can include code blocks for displaying code snippets, diff
182
204
 
183
205
  Line numbers are shown when `file` or `lines` is specified. Diff syntax (`+`/`-` lines) is automatically styled when `lang` is "diff".
184
206
 
207
+ ### Info Panels
208
+
209
+ Use `type: "info"` for non-interactive context panels. They display a title, context text, and optional media but have no input — they're skipped during keyboard navigation and excluded from responses.
210
+
211
+ ```json
212
+ {
213
+ "id": "overview",
214
+ "type": "info",
215
+ "question": "Architecture Overview",
216
+ "context": "The system uses a microservices architecture with three main services.",
217
+ "media": { "type": "mermaid", "mermaid": "graph LR\n A[API] --> B[Auth]\n A --> C[Data]" }
218
+ }
219
+ ```
220
+
221
+ ### Media Blocks
222
+
223
+ Questions can embed media via the `media` field (single object or array). Supported types:
224
+
225
+ | Type | Fields | Description |
226
+ |------|--------|-------------|
227
+ | `image` | `src`, `alt?`, `caption?` | Image (local path, URL, or data URI) |
228
+ | `table` | `table: { headers, rows, highlights? }`, `caption?` | Data table with optional row highlighting |
229
+ | `chart` | `chart: { type, data, options? }`, `caption?` | Chart.js chart (bar, line, pie, etc.) |
230
+ | `mermaid` | `mermaid: "graph LR\n..."`, `caption?` | Mermaid diagram |
231
+ | `html` | `html: "<div>...</div>"`, `caption?` | Raw HTML content |
232
+
233
+ All media types support `position`: `"above"` (default), `"below"`, or `"side"` (two-column layout).
234
+
235
+ ```json
236
+ {
237
+ "id": "db-choice",
238
+ "type": "single",
239
+ "question": "Which database?",
240
+ "media": {
241
+ "type": "table",
242
+ "table": {
243
+ "headers": ["Database", "Latency", "Cost"],
244
+ "rows": [["PostgreSQL", "~5ms", "$50/mo"], ["DynamoDB", "~2ms", "$80/mo"]],
245
+ "highlights": [0]
246
+ },
247
+ "caption": "Benchmark results from staging"
248
+ },
249
+ "options": ["PostgreSQL", "DynamoDB"],
250
+ "recommended": "PostgreSQL"
251
+ }
252
+ ```
253
+
254
+ ### Conviction & Weight
255
+
256
+ **Conviction** controls how strongly a recommendation is presented:
257
+ - Omitted (default): shows "Recommended" badge, pre-selects the option
258
+ - `"strong"`: same as default (use when very confident)
259
+ - `"slight"`: shows "Recommended" badge but does NOT pre-select (use when unsure)
260
+
261
+ **Weight** controls visual prominence:
262
+ - `"critical"`: thick accent border, tinted background — for decisions that matter most
263
+ - `"minor"`: compact card with smaller text and padding — for low-stakes preferences
264
+
185
265
  ## Keyboard Shortcuts
186
266
 
187
267
  | Key | Action |
@@ -240,7 +320,7 @@ The interview form supports light/dark themes with automatic OS detection and us
240
320
  | Theme | Description |
241
321
  |-------|-------------|
242
322
  | `default` | Monospace, IDE-inspired aesthetic |
243
- | `tufte` | Serif fonts (Cormorant Garamond), book-like feel |
323
+ | `tufte` | Serif fonts (Instrument Serif), book-like feel |
244
324
 
245
325
  ### Theme Modes
246
326
 
package/form/index.html CHANGED
@@ -4,6 +4,9 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Interview</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Space+Mono:wght@400;700&family=Instrument+Serif&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
7
10
  <link rel="stylesheet" href="/styles.css?session=__SESSION_TOKEN__">
8
11
  <link rel="stylesheet" href="/theme-light.css?session=__SESSION_TOKEN__" data-theme-link="light" media="(prefers-color-scheme: light)">
9
12
  <link rel="stylesheet" href="/theme-dark.css?session=__SESSION_TOKEN__" data-theme-link="dark" media="(prefers-color-scheme: dark)">
@@ -57,9 +60,8 @@
57
60
  <span class="shortcut-label">Cancel</span>
58
61
  </div>
59
62
  <div class="shortcut-divider"></div>
60
- <div class="shortcut recommended-hint">
61
- <span class="shortcut-keys"><span class="star">*</span></span>
62
- <span class="shortcut-label">Recommended</span>
63
+ <div class="shortcut">
64
+ <span class="recommended-pill">Recommended</span>
63
65
  </div>
64
66
  <div class="shortcut hidden" data-theme-shortcut>
65
67
  <span class="shortcut-keys" data-theme-keys></span>
@@ -115,6 +117,7 @@
115
117
  <div id="save-toast" class="save-toast hidden" aria-live="polite"></div>
116
118
  </main>
117
119
 
120
+ <!-- __CDN_SCRIPTS__ -->
118
121
  <script>
119
122
  window.__INTERVIEW_DATA__ = /* __INTERVIEW_DATA_PLACEHOLDER__ */;
120
123
  </script>
package/form/script.js CHANGED
@@ -498,6 +498,155 @@
498
498
  return container;
499
499
  }
500
500
 
501
+ function renderMediaImage(media) {
502
+ const figure = document.createElement("figure");
503
+ figure.className = "media-block media-image";
504
+
505
+ const img = document.createElement("img");
506
+ const isUrl = media.src.startsWith("http://") || media.src.startsWith("https://") || media.src.startsWith("data:");
507
+ img.src = isUrl ? media.src : `/media?path=${encodeURIComponent(media.src)}&session=${encodeURIComponent(sessionToken)}`;
508
+ img.alt = media.alt || "";
509
+ img.loading = "lazy";
510
+
511
+ figure.appendChild(img);
512
+ if (media.caption) {
513
+ const capEl = document.createElement("div");
514
+ capEl.className = "media-caption";
515
+ capEl.textContent = media.caption;
516
+ figure.appendChild(capEl);
517
+ }
518
+ return figure;
519
+ }
520
+
521
+ function renderMediaTable(media) {
522
+ const t = media.table;
523
+ const wrapper = document.createElement("div");
524
+ wrapper.className = "media-block media-table";
525
+
526
+ const tableScroll = document.createElement("div");
527
+ tableScroll.className = "media-table-scroll";
528
+
529
+ const table = document.createElement("table");
530
+ table.className = "data-table";
531
+
532
+ const thead = document.createElement("thead");
533
+ const headerRow = document.createElement("tr");
534
+ t.headers.forEach(h => {
535
+ const th = document.createElement("th");
536
+ th.textContent = h;
537
+ headerRow.appendChild(th);
538
+ });
539
+ thead.appendChild(headerRow);
540
+ table.appendChild(thead);
541
+
542
+ const tbody = document.createElement("tbody");
543
+ const highlights = new Set(t.highlights || []);
544
+ t.rows.forEach((row, i) => {
545
+ const tr = document.createElement("tr");
546
+ if (highlights.has(i)) tr.classList.add("highlighted-row");
547
+ row.forEach(cell => {
548
+ const td = document.createElement("td");
549
+ td.innerHTML = renderLightMarkdown(cell);
550
+ tr.appendChild(td);
551
+ });
552
+ tbody.appendChild(tr);
553
+ });
554
+ table.appendChild(tbody);
555
+
556
+ tableScroll.appendChild(table);
557
+ wrapper.appendChild(tableScroll);
558
+
559
+ if (media.caption) {
560
+ const capEl = document.createElement("div");
561
+ capEl.className = "media-caption";
562
+ capEl.textContent = media.caption;
563
+ wrapper.appendChild(capEl);
564
+ }
565
+ return wrapper;
566
+ }
567
+
568
+ function renderMediaChart(media) {
569
+ const wrapper = document.createElement("div");
570
+ wrapper.className = "media-block media-chart";
571
+
572
+ const canvas = document.createElement("canvas");
573
+ canvas.width = 600;
574
+ canvas.height = 300;
575
+ wrapper.appendChild(canvas);
576
+
577
+ if (media.caption) {
578
+ const capEl = document.createElement("div");
579
+ capEl.className = "media-caption";
580
+ capEl.textContent = media.caption;
581
+ wrapper.appendChild(capEl);
582
+ }
583
+
584
+ requestAnimationFrame(() => {
585
+ if (typeof Chart === "undefined") return;
586
+ const chartConfig = JSON.parse(JSON.stringify(media.chart));
587
+ chartConfig.options = chartConfig.options || {};
588
+ chartConfig.options.responsive = true;
589
+ chartConfig.options.maintainAspectRatio = true;
590
+ new Chart(canvas, chartConfig);
591
+ });
592
+
593
+ return wrapper;
594
+ }
595
+
596
+ function renderMediaMermaid(media) {
597
+ const wrapper = document.createElement("div");
598
+ wrapper.className = "media-block media-mermaid";
599
+
600
+ const pre = document.createElement("pre");
601
+ pre.className = "mermaid";
602
+ pre.textContent = media.mermaid;
603
+ wrapper.appendChild(pre);
604
+
605
+ if (media.caption) {
606
+ const capEl = document.createElement("div");
607
+ capEl.className = "media-caption";
608
+ capEl.textContent = media.caption;
609
+ wrapper.appendChild(capEl);
610
+ }
611
+ return wrapper;
612
+ }
613
+
614
+ function renderMediaHtml(media) {
615
+ const wrapper = document.createElement("div");
616
+ wrapper.className = "media-block media-html";
617
+ wrapper.innerHTML = media.html;
618
+ wrapper.querySelectorAll("script").forEach(s => s.remove());
619
+
620
+ if (media.caption) {
621
+ const capEl = document.createElement("div");
622
+ capEl.className = "media-caption";
623
+ capEl.textContent = media.caption;
624
+ wrapper.appendChild(capEl);
625
+ }
626
+ return wrapper;
627
+ }
628
+
629
+ function renderMediaBlock(media) {
630
+ if (!media || !media.type) return null;
631
+ if (media.maxHeight) {
632
+ const el = renderMediaBlockByType(media);
633
+ if (el) el.style.maxHeight = media.maxHeight;
634
+ return el;
635
+ }
636
+ return renderMediaBlockByType(media);
637
+ }
638
+
639
+ function renderMediaBlockByType(media) {
640
+ switch (media.type) {
641
+ case "image": return renderMediaImage(media);
642
+ case "table": return renderMediaTable(media);
643
+ case "chart": return renderMediaChart(media);
644
+ case "mermaid": return renderMediaMermaid(media);
645
+ case "html": return renderMediaHtml(media);
646
+ default: return null;
647
+ }
648
+ }
649
+
501
650
  function isPrintableKey(event) {
502
651
  if (event.metaKey || event.ctrlKey || event.altKey) return false;
503
652
  return event.key.length === 1;
@@ -974,7 +1123,10 @@
974
1123
  }
975
1124
 
976
1125
  function focusQuestion(index, fromDirection = 'next') {
977
- if (index < 0 || index >= nav.cards.length) return;
1126
+ while (index >= 0 && index < nav.cards.length && nav.cards[index].classList.contains('info-panel')) {
1127
+ index += fromDirection === 'prev' ? -1 : 1;
1128
+ }
1129
+ if (index < 0 || index >= nav.cards.length) return false;
978
1130
 
979
1131
  deactivateSubmitArea();
980
1132
 
@@ -1005,12 +1157,14 @@
1005
1157
  textarea.selectionStart = textarea.selectionEnd = textarea.value.length;
1006
1158
  }
1007
1159
  }
1008
-
1160
+ return true;
1009
1161
  }
1010
1162
 
1011
1163
  function nextQuestion() {
1012
1164
  if (nav.questionIndex < nav.cards.length - 1) {
1013
- focusQuestion(nav.questionIndex + 1, 'next');
1165
+ if (!focusQuestion(nav.questionIndex + 1, 'next')) {
1166
+ activateSubmitArea();
1167
+ }
1014
1168
  } else {
1015
1169
  activateSubmitArea();
1016
1170
  }
@@ -1218,21 +1372,43 @@
1218
1372
  document.addEventListener('keydown', handleQuestionKeydown);
1219
1373
 
1220
1374
  if (nav.cards.length > 0) {
1221
- setTimeout(() => focusQuestion(0), 100);
1375
+ setTimeout(() => {
1376
+ if (!focusQuestion(0)) {
1377
+ activateSubmitArea();
1378
+ }
1379
+ }, 100);
1222
1380
  }
1223
1381
  }
1224
1382
 
1225
- function createQuestionCard(question, index) {
1383
+ function createQuestionCard(question, index, badgeNumber) {
1226
1384
  const card = document.createElement("section");
1227
1385
  card.className = "question-card";
1228
1386
  card.setAttribute("role", "listitem");
1229
1387
  card.dataset.questionId = question.id;
1230
1388
 
1389
+ const colors = ['--q-color-1', '--q-color-2', '--q-color-3', '--q-color-4', '--q-color-5', '--q-color-6'];
1390
+ card.style.setProperty('--card-accent', `var(${colors[index % colors.length]})`);
1391
+ card.style.setProperty('--i', String(index));
1392
+
1393
+ if (question.weight === "minor") card.classList.add("weight-minor");
1394
+ if (question.weight === "critical") card.classList.add("weight-critical");
1395
+
1396
+ const header = document.createElement('div');
1397
+ header.className = 'question-header';
1398
+
1231
1399
  const title = document.createElement("h2");
1232
1400
  title.className = "question-title";
1233
1401
  title.id = `q-${question.id}-title`;
1234
- title.innerHTML = `${index + 1}. ${renderLightMarkdown(question.question)}`;
1235
- card.appendChild(title);
1402
+ title.innerHTML = renderLightMarkdown(question.question);
1403
+
1404
+ if (badgeNumber !== null) {
1405
+ const badge = document.createElement('span');
1406
+ badge.className = 'question-badge';
1407
+ badge.textContent = String(badgeNumber);
1408
+ header.appendChild(badge);
1409
+ }
1410
+ header.appendChild(title);
1411
+ card.appendChild(header);
1236
1412
 
1237
1413
  if (question.context) {
1238
1414
  const context = document.createElement("p");
@@ -1249,6 +1425,32 @@
1249
1425
  }
1250
1426
  }
1251
1427
 
1428
+ let belowMedia = [];
1429
+ let sideMedia = [];
1430
+ if (question.media) {
1431
+ const mediaList = Array.isArray(question.media) ? question.media : [question.media];
1432
+ const aboveMedia = mediaList.filter(m => !m.position || m.position === "above");
1433
+ belowMedia = mediaList.filter(m => m.position === "below");
1434
+ sideMedia = mediaList.filter(m => m.position === "side");
1435
+
1436
+ aboveMedia.forEach(m => {
1437
+ const el = renderMediaBlock(m);
1438
+ if (el) card.appendChild(el);
1439
+ });
1440
+ }
1441
+
1442
+ if (question.type === "info") {
1443
+ card.classList.add("info-panel");
1444
+ belowMedia.forEach(m => {
1445
+ const el = renderMediaBlock(m);
1446
+ if (el) card.appendChild(el);
1447
+ });
1448
+ if (sideMedia.length > 0) {
1449
+ applySideLayout(card, sideMedia);
1450
+ }
1451
+ return card;
1452
+ }
1453
+
1252
1454
  if (question.type === "single" || question.type === "multi") {
1253
1455
  const list = document.createElement("div");
1254
1456
  list.className = "option-list";
@@ -1261,6 +1463,7 @@
1261
1463
  : recommended
1262
1464
  ? [recommended]
1263
1465
  : [];
1466
+ const shouldPreselect = recommendedList.length > 0 && question.conviction !== "slight";
1264
1467
 
1265
1468
  question.options.forEach((option, optionIndex) => {
1266
1469
  const optionLabel = getOptionLabel(option);
@@ -1289,10 +1492,14 @@
1289
1492
  text.textContent = optionLabel;
1290
1493
 
1291
1494
  if (recommendedList.includes(optionLabel)) {
1292
- const star = document.createElement("span");
1293
- star.className = "recommended-star";
1294
- star.textContent = "*";
1295
- text.appendChild(star);
1495
+ const pill = document.createElement("span");
1496
+ pill.className = "recommended-pill";
1497
+ pill.textContent = "Recommended";
1498
+ text.appendChild(pill);
1499
+
1500
+ if (shouldPreselect) {
1501
+ input.checked = true;
1502
+ }
1296
1503
  }
1297
1504
 
1298
1505
  label.appendChild(input);
@@ -1308,6 +1515,7 @@
1308
1515
  list.appendChild(label);
1309
1516
  });
1310
1517
 
1518
+
1311
1519
  const otherLabel = document.createElement("label");
1312
1520
  otherLabel.className = "option-item option-other";
1313
1521
  const otherCheck = document.createElement("input");
@@ -1614,9 +1822,40 @@
1614
1822
  }
1615
1823
  });
1616
1824
 
1825
+ belowMedia.forEach(m => {
1826
+ const el = renderMediaBlock(m);
1827
+ if (el) card.appendChild(el);
1828
+ });
1829
+
1830
+ if (sideMedia.length > 0) {
1831
+ applySideLayout(card, sideMedia);
1832
+ }
1833
+
1617
1834
  return card;
1618
1835
  }
1619
1836
 
1837
+ function applySideLayout(card, sideMedia) {
1838
+ const grid = document.createElement("div");
1839
+ grid.className = "question-side-layout";
1840
+
1841
+ const mediaCol = document.createElement("div");
1842
+ mediaCol.className = "question-side-media";
1843
+ sideMedia.forEach(m => {
1844
+ const el = renderMediaBlock(m);
1845
+ if (el) mediaCol.appendChild(el);
1846
+ });
1847
+
1848
+ const contentCol = document.createElement("div");
1849
+ contentCol.className = "question-side-content";
1850
+ while (card.firstChild) {
1851
+ contentCol.appendChild(card.firstChild);
1852
+ }
1853
+
1854
+ grid.appendChild(mediaCol);
1855
+ grid.appendChild(contentCol);
1856
+ card.appendChild(grid);
1857
+ }
1858
+
1620
1859
  function loadImage(file) {
1621
1860
  return new Promise((resolve, reject) => {
1622
1861
  const img = new Image();
@@ -1748,7 +1987,13 @@
1748
1987
  if (nav.inSubmitArea || session.expired) return;
1749
1988
  const clipboard = event.clipboardData;
1750
1989
  if (!clipboard) return;
1751
-
1990
+
1991
+ const active = document.activeElement;
1992
+ const isTextInput = active && (active.tagName === "TEXTAREA" || (active.tagName === "INPUT" && active.type === "text"));
1993
+ if (isTextInput && clipboard.getData("text/plain")) {
1994
+ return;
1995
+ }
1996
+
1752
1997
  const context = resolveQuestionContext(event.target);
1753
1998
  if (!context) return;
1754
1999
 
@@ -1819,22 +2064,23 @@
1819
2064
  }
1820
2065
 
1821
2066
  function collectResponses() {
1822
- return questions.map((question) => {
1823
- const resp = { id: question.id, value: getQuestionValue(question) };
1824
- if (question.type !== "image") {
1825
- const attachPaths = attachments.getPaths(question.id);
1826
- if (attachPaths.length > 0) resp.attachments = attachPaths;
1827
- }
1828
- return resp;
1829
- });
2067
+ return questions
2068
+ .filter((question) => question.type !== "info")
2069
+ .map((question) => {
2070
+ const resp = { id: question.id, value: getQuestionValue(question) };
2071
+ if (question.type !== "image") {
2072
+ const attachPaths = attachments.getPaths(question.id);
2073
+ if (attachPaths.length > 0) resp.attachments = attachPaths;
2074
+ }
2075
+ return resp;
2076
+ });
1830
2077
  }
1831
2078
 
1832
2079
  function collectPersistedData() {
1833
2080
  const data = {};
1834
2081
  questions.forEach((question) => {
1835
- if (question.type !== "image") {
1836
- data[question.id] = getQuestionValue(question);
1837
- }
2082
+ if (question.type === "info" || question.type === "image") return;
2083
+ data[question.id] = getQuestionValue(question);
1838
2084
  });
1839
2085
  return data;
1840
2086
  }
@@ -1924,19 +2170,28 @@
1924
2170
 
1925
2171
  function loadProgress() {
1926
2172
  if (!session.storageKey) return;
2173
+ let loaded = false;
1927
2174
  try {
1928
2175
  const saved = localStorage.getItem(session.storageKey);
1929
2176
  if (saved) {
1930
2177
  populateForm(JSON.parse(saved));
1931
2178
  questions.forEach((q) => {
1932
- if (q.type === "multi") {
1933
- updateDoneState(q.id);
1934
- }
2179
+ if (q.type === "multi") updateDoneState(q.id);
1935
2180
  });
2181
+ loaded = true;
1936
2182
  }
1937
2183
  } catch (_err) {
1938
2184
  // ignore storage errors
1939
2185
  }
2186
+ if (!loaded) {
2187
+ questions.forEach(q => {
2188
+ if (q.type !== "multi") return;
2189
+ const recs = Array.isArray(q.recommended) ? q.recommended : q.recommended ? [q.recommended] : [];
2190
+ if (recs.length > 0 && q.conviction !== "slight") {
2191
+ updateDoneState(q.id);
2192
+ }
2193
+ });
2194
+ }
1940
2195
  }
1941
2196
 
1942
2197
  function clearProgress() {
@@ -2205,8 +2460,11 @@
2205
2460
  const shortId = sessionId.slice(0, 8);
2206
2461
  document.title = `${projectName}${gitBranch ? ` (${gitBranch})` : ""} | ${shortId}`;
2207
2462
 
2463
+ let badgeCount = 0;
2208
2464
  questions.forEach((question, index) => {
2209
- containerEl.appendChild(createQuestionCard(question, index));
2465
+ const showBadge = question.type !== "info";
2466
+ if (showBadge) badgeCount++;
2467
+ containerEl.appendChild(createQuestionCard(question, index, showBadge ? badgeCount : null));
2210
2468
  });
2211
2469
 
2212
2470
  // Pre-populate: savedAnswers takes precedence over localStorage
@@ -2328,6 +2586,16 @@
2328
2586
  }
2329
2587
 
2330
2588
  initQuestionNavigation();
2589
+
2590
+ if (typeof mermaid !== "undefined") {
2591
+ const isDark = document.documentElement.dataset.theme === "dark" ||
2592
+ (!document.documentElement.dataset.theme && window.matchMedia("(prefers-color-scheme: dark)").matches);
2593
+ mermaid.initialize({
2594
+ startOnLoad: false,
2595
+ theme: isDark ? "dark" : "default",
2596
+ });
2597
+ mermaid.run();
2598
+ }
2331
2599
  }
2332
2600
 
2333
2601
  window.__INTERVIEW_API__ = {