pi-interview 0.4.5 → 0.5.2

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/form/script.js CHANGED
@@ -60,6 +60,14 @@
60
60
  heartbeat: null,
61
61
  queuePoll: null,
62
62
  };
63
+ function closeWindow() {
64
+ if (window.glimpse && typeof window.glimpse.close === "function") {
65
+ window.glimpse.close();
66
+ } else {
67
+ window.close();
68
+ }
69
+ }
70
+
63
71
  let filePickerOpen = false;
64
72
  const CLOSE_DELAY = 10;
65
73
  const RING_CIRCUMFERENCE = 100.53;
@@ -162,7 +170,7 @@
162
170
 
163
171
  if (closeIn <= 0) {
164
172
  clearInterval(timers.countdown);
165
- cancelInterview("timeout").finally(() => window.close());
173
+ cancelInterview("timeout").finally(() => closeWindow());
166
174
  }
167
175
  }, 1000);
168
176
  }
@@ -498,6 +506,155 @@
498
506
  return container;
499
507
  }
500
508
 
509
+ function renderMediaImage(media) {
510
+ const figure = document.createElement("figure");
511
+ figure.className = "media-block media-image";
512
+
513
+ const img = document.createElement("img");
514
+ const isUrl = media.src.startsWith("http://") || media.src.startsWith("https://") || media.src.startsWith("data:");
515
+ img.src = isUrl ? media.src : `/media?path=${encodeURIComponent(media.src)}&session=${encodeURIComponent(sessionToken)}`;
516
+ img.alt = media.alt || "";
517
+ img.loading = "lazy";
518
+
519
+ figure.appendChild(img);
520
+ if (media.caption) {
521
+ const capEl = document.createElement("div");
522
+ capEl.className = "media-caption";
523
+ capEl.textContent = media.caption;
524
+ figure.appendChild(capEl);
525
+ }
526
+ return figure;
527
+ }
528
+
529
+ function renderMediaTable(media) {
530
+ const t = media.table;
531
+ const wrapper = document.createElement("div");
532
+ wrapper.className = "media-block media-table";
533
+
534
+ const tableScroll = document.createElement("div");
535
+ tableScroll.className = "media-table-scroll";
536
+
537
+ const table = document.createElement("table");
538
+ table.className = "data-table";
539
+
540
+ const thead = document.createElement("thead");
541
+ const headerRow = document.createElement("tr");
542
+ t.headers.forEach(h => {
543
+ const th = document.createElement("th");
544
+ th.textContent = h;
545
+ headerRow.appendChild(th);
546
+ });
547
+ thead.appendChild(headerRow);
548
+ table.appendChild(thead);
549
+
550
+ const tbody = document.createElement("tbody");
551
+ const highlights = new Set(t.highlights || []);
552
+ t.rows.forEach((row, i) => {
553
+ const tr = document.createElement("tr");
554
+ if (highlights.has(i)) tr.classList.add("highlighted-row");
555
+ row.forEach(cell => {
556
+ const td = document.createElement("td");
557
+ td.innerHTML = renderLightMarkdown(cell);
558
+ tr.appendChild(td);
559
+ });
560
+ tbody.appendChild(tr);
561
+ });
562
+ table.appendChild(tbody);
563
+
564
+ tableScroll.appendChild(table);
565
+ wrapper.appendChild(tableScroll);
566
+
567
+ if (media.caption) {
568
+ const capEl = document.createElement("div");
569
+ capEl.className = "media-caption";
570
+ capEl.textContent = media.caption;
571
+ wrapper.appendChild(capEl);
572
+ }
573
+ return wrapper;
574
+ }
575
+
576
+ function renderMediaChart(media) {
577
+ const wrapper = document.createElement("div");
578
+ wrapper.className = "media-block media-chart";
579
+
580
+ const canvas = document.createElement("canvas");
581
+ canvas.width = 600;
582
+ canvas.height = 300;
583
+ wrapper.appendChild(canvas);
584
+
585
+ if (media.caption) {
586
+ const capEl = document.createElement("div");
587
+ capEl.className = "media-caption";
588
+ capEl.textContent = media.caption;
589
+ wrapper.appendChild(capEl);
590
+ }
591
+
592
+ requestAnimationFrame(() => {
593
+ if (typeof Chart === "undefined") return;
594
+ const chartConfig = JSON.parse(JSON.stringify(media.chart));
595
+ chartConfig.options = chartConfig.options || {};
596
+ chartConfig.options.responsive = true;
597
+ chartConfig.options.maintainAspectRatio = true;
598
+ new Chart(canvas, chartConfig);
599
+ });
600
+
601
+ return wrapper;
602
+ }
603
+
604
+ function renderMediaMermaid(media) {
605
+ const wrapper = document.createElement("div");
606
+ wrapper.className = "media-block media-mermaid";
607
+
608
+ const pre = document.createElement("pre");
609
+ pre.className = "mermaid";
610
+ pre.textContent = media.mermaid;
611
+ wrapper.appendChild(pre);
612
+
613
+ if (media.caption) {
614
+ const capEl = document.createElement("div");
615
+ capEl.className = "media-caption";
616
+ capEl.textContent = media.caption;
617
+ wrapper.appendChild(capEl);
618
+ }
619
+ return wrapper;
620
+ }
621
+
622
+ function renderMediaHtml(media) {
623
+ const wrapper = document.createElement("div");
624
+ wrapper.className = "media-block media-html";
625
+ wrapper.innerHTML = media.html;
626
+ wrapper.querySelectorAll("script").forEach(s => s.remove());
627
+
628
+ if (media.caption) {
629
+ const capEl = document.createElement("div");
630
+ capEl.className = "media-caption";
631
+ capEl.textContent = media.caption;
632
+ wrapper.appendChild(capEl);
633
+ }
634
+ return wrapper;
635
+ }
636
+
637
+ function renderMediaBlock(media) {
638
+ if (!media || !media.type) return null;
639
+ if (media.maxHeight) {
640
+ const el = renderMediaBlockByType(media);
641
+ if (el) el.style.maxHeight = media.maxHeight;
642
+ return el;
643
+ }
644
+ return renderMediaBlockByType(media);
645
+ }
646
+
647
+ function renderMediaBlockByType(media) {
648
+ switch (media.type) {
649
+ case "image": return renderMediaImage(media);
650
+ case "table": return renderMediaTable(media);
651
+ case "chart": return renderMediaChart(media);
652
+ case "mermaid": return renderMediaMermaid(media);
653
+ case "html": return renderMediaHtml(media);
654
+ default: return null;
655
+ }
656
+ }
657
+
501
658
  function isPrintableKey(event) {
502
659
  if (event.metaKey || event.ctrlKey || event.altKey) return false;
503
660
  return event.key.length === 1;
@@ -974,7 +1131,10 @@
974
1131
  }
975
1132
 
976
1133
  function focusQuestion(index, fromDirection = 'next') {
977
- if (index < 0 || index >= nav.cards.length) return;
1134
+ while (index >= 0 && index < nav.cards.length && nav.cards[index].classList.contains('info-panel')) {
1135
+ index += fromDirection === 'prev' ? -1 : 1;
1136
+ }
1137
+ if (index < 0 || index >= nav.cards.length) return false;
978
1138
 
979
1139
  deactivateSubmitArea();
980
1140
 
@@ -1005,12 +1165,14 @@
1005
1165
  textarea.selectionStart = textarea.selectionEnd = textarea.value.length;
1006
1166
  }
1007
1167
  }
1008
-
1168
+ return true;
1009
1169
  }
1010
1170
 
1011
1171
  function nextQuestion() {
1012
1172
  if (nav.questionIndex < nav.cards.length - 1) {
1013
- focusQuestion(nav.questionIndex + 1, 'next');
1173
+ if (!focusQuestion(nav.questionIndex + 1, 'next')) {
1174
+ activateSubmitArea();
1175
+ }
1014
1176
  } else {
1015
1177
  activateSubmitArea();
1016
1178
  }
@@ -1043,7 +1205,7 @@
1043
1205
  if (event.key === 'Escape') {
1044
1206
  if (!expiredOverlay.classList.contains('hidden')) {
1045
1207
  if (timers.countdown) clearInterval(timers.countdown);
1046
- cancelInterview("user").finally(() => window.close());
1208
+ cancelInterview("user").finally(() => closeWindow());
1047
1209
  return;
1048
1210
  }
1049
1211
  showSessionExpired();
@@ -1218,21 +1380,43 @@
1218
1380
  document.addEventListener('keydown', handleQuestionKeydown);
1219
1381
 
1220
1382
  if (nav.cards.length > 0) {
1221
- setTimeout(() => focusQuestion(0), 100);
1383
+ setTimeout(() => {
1384
+ if (!focusQuestion(0)) {
1385
+ activateSubmitArea();
1386
+ }
1387
+ }, 100);
1222
1388
  }
1223
1389
  }
1224
1390
 
1225
- function createQuestionCard(question, index) {
1391
+ function createQuestionCard(question, index, badgeNumber) {
1226
1392
  const card = document.createElement("section");
1227
1393
  card.className = "question-card";
1228
1394
  card.setAttribute("role", "listitem");
1229
1395
  card.dataset.questionId = question.id;
1230
1396
 
1397
+ const colors = ['--q-color-1', '--q-color-2', '--q-color-3', '--q-color-4', '--q-color-5', '--q-color-6'];
1398
+ card.style.setProperty('--card-accent', `var(${colors[index % colors.length]})`);
1399
+ card.style.setProperty('--i', String(index));
1400
+
1401
+ if (question.weight === "minor") card.classList.add("weight-minor");
1402
+ if (question.weight === "critical") card.classList.add("weight-critical");
1403
+
1404
+ const header = document.createElement('div');
1405
+ header.className = 'question-header';
1406
+
1231
1407
  const title = document.createElement("h2");
1232
1408
  title.className = "question-title";
1233
1409
  title.id = `q-${question.id}-title`;
1234
- title.innerHTML = `${index + 1}. ${renderLightMarkdown(question.question)}`;
1235
- card.appendChild(title);
1410
+ title.innerHTML = renderLightMarkdown(question.question);
1411
+
1412
+ if (badgeNumber !== null) {
1413
+ const badge = document.createElement('span');
1414
+ badge.className = 'question-badge';
1415
+ badge.textContent = String(badgeNumber);
1416
+ header.appendChild(badge);
1417
+ }
1418
+ header.appendChild(title);
1419
+ card.appendChild(header);
1236
1420
 
1237
1421
  if (question.context) {
1238
1422
  const context = document.createElement("p");
@@ -1249,6 +1433,32 @@
1249
1433
  }
1250
1434
  }
1251
1435
 
1436
+ let belowMedia = [];
1437
+ let sideMedia = [];
1438
+ if (question.media) {
1439
+ const mediaList = Array.isArray(question.media) ? question.media : [question.media];
1440
+ const aboveMedia = mediaList.filter(m => !m.position || m.position === "above");
1441
+ belowMedia = mediaList.filter(m => m.position === "below");
1442
+ sideMedia = mediaList.filter(m => m.position === "side");
1443
+
1444
+ aboveMedia.forEach(m => {
1445
+ const el = renderMediaBlock(m);
1446
+ if (el) card.appendChild(el);
1447
+ });
1448
+ }
1449
+
1450
+ if (question.type === "info") {
1451
+ card.classList.add("info-panel");
1452
+ belowMedia.forEach(m => {
1453
+ const el = renderMediaBlock(m);
1454
+ if (el) card.appendChild(el);
1455
+ });
1456
+ if (sideMedia.length > 0) {
1457
+ applySideLayout(card, sideMedia);
1458
+ }
1459
+ return card;
1460
+ }
1461
+
1252
1462
  if (question.type === "single" || question.type === "multi") {
1253
1463
  const list = document.createElement("div");
1254
1464
  list.className = "option-list";
@@ -1261,6 +1471,7 @@
1261
1471
  : recommended
1262
1472
  ? [recommended]
1263
1473
  : [];
1474
+ const shouldPreselect = recommendedList.length > 0 && question.conviction !== "slight";
1264
1475
 
1265
1476
  question.options.forEach((option, optionIndex) => {
1266
1477
  const optionLabel = getOptionLabel(option);
@@ -1289,10 +1500,14 @@
1289
1500
  text.textContent = optionLabel;
1290
1501
 
1291
1502
  if (recommendedList.includes(optionLabel)) {
1292
- const star = document.createElement("span");
1293
- star.className = "recommended-star";
1294
- star.textContent = "*";
1295
- text.appendChild(star);
1503
+ const pill = document.createElement("span");
1504
+ pill.className = "recommended-pill";
1505
+ pill.textContent = "Recommended";
1506
+ text.appendChild(pill);
1507
+
1508
+ if (shouldPreselect) {
1509
+ input.checked = true;
1510
+ }
1296
1511
  }
1297
1512
 
1298
1513
  label.appendChild(input);
@@ -1308,6 +1523,7 @@
1308
1523
  list.appendChild(label);
1309
1524
  });
1310
1525
 
1526
+
1311
1527
  const otherLabel = document.createElement("label");
1312
1528
  otherLabel.className = "option-item option-other";
1313
1529
  const otherCheck = document.createElement("input");
@@ -1614,9 +1830,40 @@
1614
1830
  }
1615
1831
  });
1616
1832
 
1833
+ belowMedia.forEach(m => {
1834
+ const el = renderMediaBlock(m);
1835
+ if (el) card.appendChild(el);
1836
+ });
1837
+
1838
+ if (sideMedia.length > 0) {
1839
+ applySideLayout(card, sideMedia);
1840
+ }
1841
+
1617
1842
  return card;
1618
1843
  }
1619
1844
 
1845
+ function applySideLayout(card, sideMedia) {
1846
+ const grid = document.createElement("div");
1847
+ grid.className = "question-side-layout";
1848
+
1849
+ const mediaCol = document.createElement("div");
1850
+ mediaCol.className = "question-side-media";
1851
+ sideMedia.forEach(m => {
1852
+ const el = renderMediaBlock(m);
1853
+ if (el) mediaCol.appendChild(el);
1854
+ });
1855
+
1856
+ const contentCol = document.createElement("div");
1857
+ contentCol.className = "question-side-content";
1858
+ while (card.firstChild) {
1859
+ contentCol.appendChild(card.firstChild);
1860
+ }
1861
+
1862
+ grid.appendChild(mediaCol);
1863
+ grid.appendChild(contentCol);
1864
+ card.appendChild(grid);
1865
+ }
1866
+
1620
1867
  function loadImage(file) {
1621
1868
  return new Promise((resolve, reject) => {
1622
1869
  const img = new Image();
@@ -1748,7 +1995,13 @@
1748
1995
  if (nav.inSubmitArea || session.expired) return;
1749
1996
  const clipboard = event.clipboardData;
1750
1997
  if (!clipboard) return;
1751
-
1998
+
1999
+ const active = document.activeElement;
2000
+ const isTextInput = active && (active.tagName === "TEXTAREA" || (active.tagName === "INPUT" && active.type === "text"));
2001
+ if (isTextInput && clipboard.getData("text/plain")) {
2002
+ return;
2003
+ }
2004
+
1752
2005
  const context = resolveQuestionContext(event.target);
1753
2006
  if (!context) return;
1754
2007
 
@@ -1819,22 +2072,23 @@
1819
2072
  }
1820
2073
 
1821
2074
  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
- });
2075
+ return questions
2076
+ .filter((question) => question.type !== "info")
2077
+ .map((question) => {
2078
+ const resp = { id: question.id, value: getQuestionValue(question) };
2079
+ if (question.type !== "image") {
2080
+ const attachPaths = attachments.getPaths(question.id);
2081
+ if (attachPaths.length > 0) resp.attachments = attachPaths;
2082
+ }
2083
+ return resp;
2084
+ });
1830
2085
  }
1831
2086
 
1832
2087
  function collectPersistedData() {
1833
2088
  const data = {};
1834
2089
  questions.forEach((question) => {
1835
- if (question.type !== "image") {
1836
- data[question.id] = getQuestionValue(question);
1837
- }
2090
+ if (question.type === "info" || question.type === "image") return;
2091
+ data[question.id] = getQuestionValue(question);
1838
2092
  });
1839
2093
  return data;
1840
2094
  }
@@ -1924,19 +2178,28 @@
1924
2178
 
1925
2179
  function loadProgress() {
1926
2180
  if (!session.storageKey) return;
2181
+ let loaded = false;
1927
2182
  try {
1928
2183
  const saved = localStorage.getItem(session.storageKey);
1929
2184
  if (saved) {
1930
2185
  populateForm(JSON.parse(saved));
1931
2186
  questions.forEach((q) => {
1932
- if (q.type === "multi") {
1933
- updateDoneState(q.id);
1934
- }
2187
+ if (q.type === "multi") updateDoneState(q.id);
1935
2188
  });
2189
+ loaded = true;
1936
2190
  }
1937
2191
  } catch (_err) {
1938
2192
  // ignore storage errors
1939
2193
  }
2194
+ if (!loaded) {
2195
+ questions.forEach(q => {
2196
+ if (q.type !== "multi") return;
2197
+ const recs = Array.isArray(q.recommended) ? q.recommended : q.recommended ? [q.recommended] : [];
2198
+ if (recs.length > 0 && q.conviction !== "slight") {
2199
+ updateDoneState(q.id);
2200
+ }
2201
+ });
2202
+ }
1940
2203
  }
1941
2204
 
1942
2205
  function clearProgress() {
@@ -2166,7 +2429,7 @@
2166
2429
  session.ended = true;
2167
2430
  successOverlay.classList.remove("hidden");
2168
2431
  setTimeout(() => {
2169
- window.close();
2432
+ closeWindow();
2170
2433
  }, 800);
2171
2434
  } catch (err) {
2172
2435
  if (isNetworkError(err)) {
@@ -2205,8 +2468,11 @@
2205
2468
  const shortId = sessionId.slice(0, 8);
2206
2469
  document.title = `${projectName}${gitBranch ? ` (${gitBranch})` : ""} | ${shortId}`;
2207
2470
 
2471
+ let badgeCount = 0;
2208
2472
  questions.forEach((question, index) => {
2209
- containerEl.appendChild(createQuestionCard(question, index));
2473
+ const showBadge = question.type !== "info";
2474
+ if (showBadge) badgeCount++;
2475
+ containerEl.appendChild(createQuestionCard(question, index, showBadge ? badgeCount : null));
2210
2476
  });
2211
2477
 
2212
2478
  // Pre-populate: savedAnswers takes precedence over localStorage
@@ -2248,7 +2514,10 @@
2248
2514
  if (!url) return;
2249
2515
  const selectedOption = queueSessionSelect.options[queueSessionSelect.selectedIndex];
2250
2516
  if (selectedOption?.disabled) return;
2251
- window.open(url, "_blank", "noopener");
2517
+ const opened = window.open(url, "_blank", "noopener");
2518
+ if (!opened) {
2519
+ window.location.href = url;
2520
+ }
2252
2521
  });
2253
2522
  }
2254
2523
  window.addEventListener("pagehide", (event) => {
@@ -2281,7 +2550,7 @@
2281
2550
  closeTabBtn.addEventListener("click", async () => {
2282
2551
  if (timers.countdown) clearInterval(timers.countdown);
2283
2552
  await cancelInterview("user");
2284
- window.close();
2553
+ closeWindow();
2285
2554
  });
2286
2555
 
2287
2556
  stayBtn.addEventListener("click", () => {
@@ -2328,6 +2597,16 @@
2328
2597
  }
2329
2598
 
2330
2599
  initQuestionNavigation();
2600
+
2601
+ if (typeof mermaid !== "undefined") {
2602
+ const isDark = document.documentElement.dataset.theme === "dark" ||
2603
+ (!document.documentElement.dataset.theme && window.matchMedia("(prefers-color-scheme: dark)").matches);
2604
+ mermaid.initialize({
2605
+ startOnLoad: false,
2606
+ theme: isDark ? "dark" : "default",
2607
+ });
2608
+ mermaid.run();
2609
+ }
2331
2610
  }
2332
2611
 
2333
2612
  window.__INTERVIEW_API__ = {