pi-interview 0.6.2 → 0.8.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/form/script.js CHANGED
@@ -6,6 +6,10 @@
6
6
  const cwd = data.cwd || "";
7
7
  const gitBranch = data.gitBranch || "";
8
8
  const timeout = typeof data.timeout === "number" ? data.timeout : 0;
9
+ const askModels = Array.isArray(data.askModels)
10
+ ? data.askModels.filter((model) => model && typeof model.value === "string" && typeof model.provider === "string")
11
+ : [];
12
+ const defaultAskModel = typeof data.defaultAskModel === "string" ? data.defaultAskModel : null;
9
13
 
10
14
  const titleEl = document.getElementById("form-title");
11
15
  const descriptionEl = document.getElementById("form-description");
@@ -37,6 +41,12 @@
37
41
  const imagePathState = new Map();
38
42
  const attachState = new Map();
39
43
  const attachPathState = new Map();
44
+ const optionKeyState = new Map();
45
+ const choiceNoteState = new Map();
46
+ const optionInsightState = {
47
+ active: null,
48
+ pinned: new Map(),
49
+ };
40
50
  const nav = {
41
51
  questionIndex: 0,
42
52
  optionIndex: 0,
@@ -76,6 +86,12 @@
76
86
  dismissed: false,
77
87
  knownIds: new Set(),
78
88
  };
89
+ const ASK_PROMPT_CHIPS = [
90
+ { key: "explain", label: "Explain this", prompt: "Explain this better." },
91
+ { key: "why", label: "Why this option?", prompt: "Why is this option like that?" },
92
+ { key: "tradeoffs", label: "Tradeoffs", prompt: "What are the tradeoffs of this option?" },
93
+ { key: "fail", label: "When would this fail?", prompt: "When would this option fail or be the wrong choice?" },
94
+ ];
79
95
 
80
96
  function updateCountdownBadge(secondsLeft, totalSeconds) {
81
97
  if (!countdownBadge || !countdownValue || !countdownRingProgress) return;
@@ -513,15 +529,111 @@
513
529
  return typeof option === "string" ? option : option.label;
514
530
  }
515
531
 
532
+ function questionCanClarifyOption(question) {
533
+ return (question.type === "single" || question.type === "multi")
534
+ && Array.isArray(question.options)
535
+ && question.options.length > 0
536
+ && question.options.every((option) => typeof option === "string");
537
+ }
538
+
539
+ function isChoiceResponseValue(value) {
540
+ return value && typeof value === "object" && !Array.isArray(value) && typeof value.option === "string";
541
+ }
542
+
543
+ function normalizeChoiceResponseValue(value) {
544
+ if (!isChoiceResponseValue(value)) return null;
545
+ const option = value.option.trim();
546
+ if (!option) return null;
547
+ const note = typeof value.note === "string" ? value.note.trim() : "";
548
+ return note ? { option, note } : { option };
549
+ }
550
+
551
+ function renameChoiceAnswerValue(question, value, previousOption, nextOption) {
552
+ if (!nextOption || (question.type !== "single" && question.type !== "multi")) {
553
+ return value;
554
+ }
555
+
556
+ if (question.type === "single") {
557
+ const choiceValue = normalizeChoiceResponseValue(value);
558
+ if (!choiceValue || choiceValue.option !== previousOption) return value;
559
+ return choiceValue.note ? { option: nextOption, note: choiceValue.note } : { option: nextOption };
560
+ }
561
+
562
+ if (!Array.isArray(value)) return value;
563
+ return value.map((item) => {
564
+ const choiceValue = normalizeChoiceResponseValue(item);
565
+ if (!choiceValue || choiceValue.option !== previousOption) return item;
566
+ return choiceValue.note ? { option: nextOption, note: choiceValue.note } : { option: nextOption };
567
+ });
568
+ }
569
+
570
+ function preserveChoiceAnswerValue(question, value, validLabels) {
571
+ if (question.type === "single") {
572
+ const choiceValue = normalizeChoiceResponseValue(value);
573
+ if (!choiceValue || !validLabels.has(choiceValue.option)) return "";
574
+ return choiceValue;
575
+ }
576
+
577
+ if (question.type === "multi") {
578
+ if (!Array.isArray(value)) return [];
579
+ return value
580
+ .map((item) => normalizeChoiceResponseValue(item))
581
+ .filter((item) => item && validLabels.has(item.option));
582
+ }
583
+
584
+ return value;
585
+ }
586
+
587
+ function getChoiceNotes(questionId) {
588
+ return choiceNoteState.get(questionId) || new Map();
589
+ }
590
+
591
+ function getChoiceNote(questionId, optionLabel) {
592
+ return getChoiceNotes(questionId).get(optionLabel) || "";
593
+ }
594
+
595
+ function setChoiceNote(questionId, optionLabel, note) {
596
+ const normalizedNote = typeof note === "string" ? note.trim() : "";
597
+ const existing = choiceNoteState.get(questionId) || new Map();
598
+ if (!normalizedNote) {
599
+ existing.delete(optionLabel);
600
+ if (existing.size === 0) {
601
+ choiceNoteState.delete(questionId);
602
+ return;
603
+ }
604
+ choiceNoteState.set(questionId, existing);
605
+ return;
606
+ }
607
+ existing.set(optionLabel, normalizedNote);
608
+ choiceNoteState.set(questionId, existing);
609
+ }
610
+
611
+ function clearChoiceNotes(questionId) {
612
+ choiceNoteState.delete(questionId);
613
+ }
614
+
615
+ function getSelectedOptionLabels(questionId) {
616
+ return Array.from(
617
+ formEl.querySelectorAll(`input[name="${escapeSelector(questionId)}"]:checked`)
618
+ )
619
+ .map((input) => input.value)
620
+ .filter((value) => value && value !== "__other__");
621
+ }
622
+
623
+ function syncChoiceNotesWithSelection(question) {
624
+ if (!questionCanClarifyOption(question)) return;
625
+ }
626
+
516
627
  function isRichOption(option) {
517
628
  return typeof option === "object" && option !== null && "label" in option;
518
629
  }
519
630
 
520
631
  function syncRecommendations(question, options) {
632
+ const optionLabels = options.map((option) => getOptionLabel(option));
521
633
  if (!question.recommended) return;
522
634
 
523
635
  if (question.type === "single") {
524
- if (typeof question.recommended === "string" && options.includes(question.recommended)) {
636
+ if (typeof question.recommended === "string" && optionLabels.includes(question.recommended)) {
525
637
  return;
526
638
  }
527
639
  delete question.recommended;
@@ -537,7 +649,7 @@
537
649
 
538
650
  const nextRecommended = (Array.isArray(question.recommended)
539
651
  ? question.recommended
540
- : [question.recommended]).filter((option) => options.includes(option));
652
+ : [question.recommended]).filter((option) => optionLabels.includes(option));
541
653
  if (nextRecommended.length === 0) {
542
654
  delete question.recommended;
543
655
  delete question.conviction;
@@ -546,6 +658,983 @@
546
658
  question.recommended = nextRecommended;
547
659
  }
548
660
 
661
+ function makeClientId(prefix = "id") {
662
+ if (window.crypto && typeof window.crypto.randomUUID === "function") {
663
+ return `${prefix}-${window.crypto.randomUUID()}`;
664
+ }
665
+ return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
666
+ }
667
+
668
+ function questionSupportsOptionInsights(question) {
669
+ return (question.type === "single" || question.type === "multi") &&
670
+ Array.isArray(question.options) &&
671
+ question.options.length > 0;
672
+ }
673
+
674
+ function questionCanAskAboutOption(question) {
675
+ return !!data.canGenerate && questionSupportsOptionInsights(question);
676
+ }
677
+
678
+ function normalizeOptionKeysFromData() {
679
+ const raw = data.optionKeysByQuestion && typeof data.optionKeysByQuestion === "object"
680
+ ? data.optionKeysByQuestion
681
+ : {};
682
+
683
+ questions.forEach((question) => {
684
+ if (!questionSupportsOptionInsights(question)) return;
685
+ const rawKeys = Array.isArray(raw[question.id]) ? raw[question.id] : [];
686
+ const keys = rawKeys.length === question.options.length && rawKeys.every((key) => typeof key === "string" && key)
687
+ ? [...rawKeys]
688
+ : question.options.map(() => makeClientId(`opt-${question.id}`));
689
+ optionKeyState.set(question.id, keys);
690
+ });
691
+ }
692
+
693
+ function getOptionKeys(questionId) {
694
+ return optionKeyState.get(questionId) || [];
695
+ }
696
+
697
+ function setOptionKeys(questionId, keys) {
698
+ optionKeyState.set(questionId, Array.isArray(keys) ? [...keys] : []);
699
+ }
700
+
701
+ function getOptionIndexByKey(questionId, optionKey) {
702
+ return getOptionKeys(questionId).indexOf(optionKey);
703
+ }
704
+
705
+ function getOptionTextByKey(questionId, optionKey) {
706
+ const question = questions.find((q) => q.id === questionId);
707
+ if (!question || !Array.isArray(question.options)) return "";
708
+ const index = getOptionIndexByKey(questionId, optionKey);
709
+ if (index < 0 || index >= question.options.length) return "";
710
+ return getOptionLabel(question.options[index]);
711
+ }
712
+
713
+ function providerLabel(provider) {
714
+ if (!provider) return "";
715
+ if (provider === "openai") return "OpenAI";
716
+ if (provider === "google") return "Google";
717
+ if (provider === "anthropic") return "Anthropic";
718
+ return provider.charAt(0).toUpperCase() + provider.slice(1);
719
+ }
720
+
721
+ function parseModelValue(modelValue) {
722
+ if (typeof modelValue !== "string") return { provider: "", model: "" };
723
+ const slashIndex = modelValue.indexOf("/");
724
+ if (slashIndex <= 0 || slashIndex === modelValue.length - 1) {
725
+ return { provider: "", model: modelValue };
726
+ }
727
+ return {
728
+ provider: modelValue.slice(0, slashIndex),
729
+ model: modelValue.slice(slashIndex + 1),
730
+ };
731
+ }
732
+
733
+ function getModelsForProvider(provider) {
734
+ return askModels.filter((model) => model.provider === provider);
735
+ }
736
+
737
+ function getFirstProvider() {
738
+ return askModels[0]?.provider || "";
739
+ }
740
+
741
+ const ASK_DEPTH_OPTIONS = [
742
+ { key: "quick", label: "Quick" },
743
+ { key: "standard", label: "Standard" },
744
+ { key: "deep", label: "Deep" },
745
+ ];
746
+
747
+ function createDefaultActiveInsight(questionId, optionKey) {
748
+ const selectedModel = askModels.some((model) => model.value === defaultAskModel)
749
+ ? defaultAskModel
750
+ : (askModels[0]?.value || null);
751
+ const parsed = parseModelValue(selectedModel);
752
+ return {
753
+ questionId,
754
+ optionKey,
755
+ prompt: "",
756
+ selectedChip: null,
757
+ loading: false,
758
+ error: "",
759
+ result: null,
760
+ advancedOpen: false,
761
+ selectedProvider: parsed.provider || getFirstProvider(),
762
+ selectedModel,
763
+ selectedDepth: "standard",
764
+ abortController: null,
765
+ };
766
+ }
767
+
768
+ function getActiveInsight(questionId, optionKey) {
769
+ const active = optionInsightState.active;
770
+ return active && active.questionId === questionId && active.optionKey === optionKey
771
+ ? active
772
+ : null;
773
+ }
774
+
775
+ function getPinnedInsights(questionId, optionKey) {
776
+ const questionInsights = optionInsightState.pinned.get(questionId) || [];
777
+ return questionInsights.filter((insight) => insight.optionKey === optionKey);
778
+ }
779
+
780
+ function normalizeSavedOptionInsights(input) {
781
+ if (!Array.isArray(input)) return [];
782
+ return input
783
+ .filter((item) => item && typeof item === "object")
784
+ .map((item) => ({
785
+ id: typeof item.id === "string" ? item.id : makeClientId("insight"),
786
+ questionId: typeof item.questionId === "string" ? item.questionId : "",
787
+ optionKey: typeof item.optionKey === "string" ? item.optionKey : "",
788
+ optionText: typeof item.optionText === "string" ? item.optionText : "",
789
+ prompt: typeof item.prompt === "string" ? item.prompt : "",
790
+ summary: typeof item.summary === "string" ? item.summary : "",
791
+ bullets: Array.isArray(item.bullets)
792
+ ? item.bullets.filter((bullet) => typeof bullet === "string" && bullet.trim()).map((bullet) => bullet.trim())
793
+ : [],
794
+ suggestedText: typeof item.suggestedText === "string" ? item.suggestedText : undefined,
795
+ modelUsed: typeof item.modelUsed === "string" ? item.modelUsed : item.modelUsed === null ? null : undefined,
796
+ createdAt: typeof item.createdAt === "string" ? item.createdAt : new Date().toISOString(),
797
+ }))
798
+ .filter((item) => item.questionId && item.optionKey && item.summary);
799
+ }
800
+
801
+ function restoreSavedOptionInsights(input) {
802
+ optionInsightState.pinned.clear();
803
+ normalizeSavedOptionInsights(input).forEach((insight) => {
804
+ const existing = optionInsightState.pinned.get(insight.questionId) || [];
805
+ existing.push(insight);
806
+ optionInsightState.pinned.set(insight.questionId, existing);
807
+ });
808
+ }
809
+
810
+ function serializeSavedOptionInsights() {
811
+ return Array.from(optionInsightState.pinned.values())
812
+ .flat()
813
+ .map((insight) => ({
814
+ id: insight.id,
815
+ questionId: insight.questionId,
816
+ optionKey: insight.optionKey,
817
+ optionText: insight.optionText,
818
+ prompt: insight.prompt,
819
+ summary: insight.summary,
820
+ bullets: Array.isArray(insight.bullets) ? [...insight.bullets] : [],
821
+ suggestedText: insight.suggestedText,
822
+ modelUsed: insight.modelUsed ?? null,
823
+ createdAt: insight.createdAt,
824
+ }));
825
+ }
826
+
827
+ function removePinnedInsight(questionId, insightId) {
828
+ const existing = optionInsightState.pinned.get(questionId) || [];
829
+ const next = existing.filter((insight) => insight.id !== insightId);
830
+ if (next.length > 0) {
831
+ optionInsightState.pinned.set(questionId, next);
832
+ } else {
833
+ optionInsightState.pinned.delete(questionId);
834
+ }
835
+ }
836
+
837
+ function updatePinnedInsightOptionText(questionId, optionKey, optionText) {
838
+ const existing = optionInsightState.pinned.get(questionId) || [];
839
+ existing.forEach((insight) => {
840
+ if (insight.optionKey === optionKey) {
841
+ insight.optionText = optionText;
842
+ }
843
+ });
844
+ }
845
+
846
+ function pruneQuestionOptionInsights(questionId) {
847
+ const validKeys = new Set(getOptionKeys(questionId));
848
+ const existing = optionInsightState.pinned.get(questionId) || [];
849
+ const next = existing.filter((insight) => validKeys.has(insight.optionKey));
850
+ if (next.length > 0) {
851
+ optionInsightState.pinned.set(questionId, next);
852
+ } else {
853
+ optionInsightState.pinned.delete(questionId);
854
+ }
855
+
856
+ const active = optionInsightState.active;
857
+ if (!active || active.questionId !== questionId || validKeys.has(active.optionKey)) {
858
+ return;
859
+ }
860
+ if (active.abortController) {
861
+ active.abortController.abort();
862
+ }
863
+ optionInsightState.active = null;
864
+ }
865
+
866
+ function closeOptionInsightPanel(questionId, optionKey) {
867
+ const active = optionInsightState.active;
868
+ if (!active) return;
869
+ if (questionId && active.questionId !== questionId) return;
870
+ if (optionKey && active.optionKey !== optionKey) return;
871
+ if (active.abortController) {
872
+ active.abortController.abort();
873
+ }
874
+ optionInsightState.active = null;
875
+ }
876
+
877
+ function openOptionInsightPanel(question, optionKey) {
878
+ if (!questionCanAskAboutOption(question)) return;
879
+ const currentValue = getQuestionValue(question);
880
+ const active = getActiveInsight(question.id, optionKey);
881
+ if (active) {
882
+ closeOptionInsightPanel(question.id, optionKey);
883
+ replaceQuestionOptionList(question, currentValue, optionKey);
884
+ return;
885
+ }
886
+
887
+ const previousActive = optionInsightState.active;
888
+ if (previousActive?.abortController) {
889
+ previousActive.abortController.abort();
890
+ }
891
+ if (previousActive && (previousActive.questionId !== question.id || previousActive.optionKey !== optionKey)) {
892
+ const previousQuestion = questions.find((item) => item.id === previousActive.questionId);
893
+ if (previousQuestion) {
894
+ const previousValue = getQuestionValue(previousQuestion);
895
+ optionInsightState.active = null;
896
+ replaceQuestionOptionList(previousQuestion, previousValue, previousActive.optionKey);
897
+ }
898
+ }
899
+
900
+ optionInsightState.active = createDefaultActiveInsight(question.id, optionKey);
901
+ replaceQuestionOptionList(question, currentValue, optionKey, { focusComposer: true });
902
+ }
903
+
904
+ function getSelectedInsightModel(activeInsight) {
905
+ if (!activeInsight) return null;
906
+ return typeof activeInsight.selectedModel === "string" && activeInsight.selectedModel
907
+ ? activeInsight.selectedModel
908
+ : defaultAskModel;
909
+ }
910
+
911
+ function getInsightModelLabel(activeInsight) {
912
+ const selectedModel = getSelectedInsightModel(activeInsight);
913
+ if (!selectedModel) return "No model selected";
914
+ const parsed = parseModelValue(selectedModel);
915
+ return `${providerLabel(parsed.provider)} / ${parsed.model}`;
916
+ }
917
+
918
+ function applyQuestionValue(question, value) {
919
+ populateForm({ [question.id]: value }, { preserveChoiceNotes: true });
920
+ if (question.type === "multi") {
921
+ updateDoneState(question.id);
922
+ }
923
+ }
924
+
925
+ function replaceQuestionOptionList(question, preserveValue, focusOptionKey, options = {}) {
926
+ const card = containerEl.querySelector(`.question-card[data-question-id="${escapeSelector(question.id)}"]`);
927
+ const currentList = card?.querySelector('.option-list');
928
+ const title = card?.querySelector('.question-title');
929
+ if (!card || !currentList || !title) return;
930
+
931
+ const nextList = createChoiceQuestionList(question, title, options);
932
+ currentList.replaceWith(nextList);
933
+ applyQuestionValue(question, preserveValue);
934
+
935
+ if (nav.cards[nav.questionIndex] === card && !nav.inSubmitArea && focusOptionKey) {
936
+ const optionIndex = getOptionIndexByKey(question.id, focusOptionKey);
937
+ if (optionIndex >= 0) {
938
+ nav.optionIndex = optionIndex;
939
+ highlightOption(card, optionIndex, false);
940
+ }
941
+ }
942
+
943
+ if (options.focusComposer) {
944
+ requestAnimationFrame(() => {
945
+ const composer = card.querySelector(`.option-insight-input[data-question-id="${escapeSelector(question.id)}"][data-option-key="${escapeSelector(focusOptionKey || "")}"]`);
946
+ composer?.focus();
947
+ });
948
+ }
949
+ }
950
+
951
+ async function runOptionAction(question, optionKey, action, text) {
952
+ const preservedValue = getQuestionValue(question);
953
+ const previousText = getOptionTextByKey(question.id, optionKey);
954
+ try {
955
+ const response = await fetch("/option-action", {
956
+ method: "POST",
957
+ headers: { "Content-Type": "application/json" },
958
+ body: JSON.stringify({ token: sessionToken, questionId: question.id, optionKey, action, text }),
959
+ });
960
+ const result = await response.json();
961
+ if (!result.ok) throw new Error(result.error || "Option action failed");
962
+
963
+ if (result.question && Array.isArray(result.question.options)) {
964
+ question.options = result.question.options;
965
+ question.recommended = result.question.recommended;
966
+ question.conviction = result.question.conviction;
967
+ }
968
+ if (Array.isArray(result.optionKeys)) {
969
+ setOptionKeys(question.id, result.optionKeys);
970
+ pruneQuestionOptionInsights(question.id);
971
+ }
972
+
973
+ if (action === "replace-text") {
974
+ const nextText = getOptionTextByKey(question.id, optionKey);
975
+ updatePinnedInsightOptionText(question.id, optionKey, nextText);
976
+ if (optionInsightState.active && optionInsightState.active.questionId === question.id && optionInsightState.active.optionKey === optionKey && optionInsightState.active.result) {
977
+ optionInsightState.active.result.suggestedText = nextText;
978
+ }
979
+ }
980
+
981
+ let nextValue = preservedValue;
982
+ if (action === "replace-text" && text) {
983
+ nextValue = renameChoiceAnswerValue(question, preservedValue, previousText, text);
984
+ }
985
+
986
+ replaceQuestionOptionList(question, nextValue, optionKey);
987
+ debounceSave();
988
+ refreshCountdown();
989
+ return true;
990
+ } catch (err) {
991
+ const active = getActiveInsight(question.id, optionKey);
992
+ if (active) {
993
+ active.error = err instanceof Error ? err.message : "Option action failed";
994
+ replaceQuestionOptionList(question, preservedValue, optionKey);
995
+ }
996
+ return false;
997
+ }
998
+ }
999
+
1000
+ async function submitOptionInsight(question, optionKey) {
1001
+ const active = getActiveInsight(question.id, optionKey);
1002
+ if (!active) return;
1003
+ const prompt = active.prompt.trim();
1004
+ if (!prompt) {
1005
+ active.error = "Prompt is required";
1006
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey, { focusComposer: true });
1007
+ return;
1008
+ }
1009
+
1010
+ if (active.loading) {
1011
+ active.abortController?.abort();
1012
+ return;
1013
+ }
1014
+
1015
+ active.loading = true;
1016
+ active.error = "";
1017
+ active.abortController = new AbortController();
1018
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1019
+
1020
+ try {
1021
+ const modelOverride = getSelectedInsightModel(active);
1022
+ const response = await fetch("/option-insight", {
1023
+ method: "POST",
1024
+ headers: { "Content-Type": "application/json" },
1025
+ body: JSON.stringify({
1026
+ token: sessionToken,
1027
+ questionId: question.id,
1028
+ optionKey,
1029
+ prompt,
1030
+ model: modelOverride && modelOverride !== defaultAskModel ? modelOverride : null,
1031
+ depth: active.selectedDepth || "standard",
1032
+ }),
1033
+ signal: active.abortController.signal,
1034
+ });
1035
+ const result = await response.json();
1036
+ if (!result.ok) throw new Error(result.error || "Option insight failed");
1037
+ active.result = {
1038
+ summary: result.summary,
1039
+ bullets: Array.isArray(result.bullets) ? result.bullets : [],
1040
+ suggestedText: typeof result.suggestedText === "string" ? result.suggestedText : undefined,
1041
+ modelUsed: typeof result.modelUsed === "string" ? result.modelUsed : null,
1042
+ };
1043
+ active.error = "";
1044
+ const optionText = typeof result.optionText === "string" ? result.optionText : getOptionTextByKey(question.id, optionKey);
1045
+ updatePinnedInsightOptionText(question.id, optionKey, optionText);
1046
+ refreshCountdown();
1047
+ } catch (err) {
1048
+ if (!(err instanceof Error && err.name === "AbortError")) {
1049
+ active.error = err instanceof Error ? err.message : "Option insight failed";
1050
+ }
1051
+ } finally {
1052
+ if (optionInsightState.active && optionInsightState.active.questionId === question.id && optionInsightState.active.optionKey === optionKey) {
1053
+ optionInsightState.active.loading = false;
1054
+ optionInsightState.active.abortController = null;
1055
+ }
1056
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1057
+ }
1058
+ }
1059
+
1060
+ function pinActiveInsight(question, optionKey) {
1061
+ const active = getActiveInsight(question.id, optionKey);
1062
+ if (!active || !active.result) return;
1063
+ const optionText = getOptionTextByKey(question.id, optionKey);
1064
+ const questionInsights = optionInsightState.pinned.get(question.id) || [];
1065
+ questionInsights.push({
1066
+ id: makeClientId("insight"),
1067
+ questionId: question.id,
1068
+ optionKey,
1069
+ optionText,
1070
+ prompt: active.prompt.trim(),
1071
+ summary: active.result.summary,
1072
+ bullets: Array.isArray(active.result.bullets) ? [...active.result.bullets] : [],
1073
+ suggestedText: active.result.suggestedText,
1074
+ modelUsed: active.result.modelUsed ?? null,
1075
+ createdAt: new Date().toISOString(),
1076
+ });
1077
+ optionInsightState.pinned.set(question.id, questionInsights);
1078
+ debounceSave();
1079
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1080
+ }
1081
+
1082
+ function createPinnedInsightCard(question, optionKey, insight) {
1083
+ const card = document.createElement("div");
1084
+ card.className = "option-insight-pinned";
1085
+
1086
+ const head = document.createElement("div");
1087
+ head.className = "option-insight-pinned-head";
1088
+
1089
+ const prompt = document.createElement("div");
1090
+ prompt.className = "option-insight-pinned-prompt";
1091
+ prompt.textContent = insight.prompt;
1092
+ head.appendChild(prompt);
1093
+
1094
+ const unpin = document.createElement("button");
1095
+ unpin.type = "button";
1096
+ unpin.className = "option-insight-unpin";
1097
+ unpin.textContent = "Unpin";
1098
+ unpin.addEventListener("click", (event) => {
1099
+ event.preventDefault();
1100
+ event.stopPropagation();
1101
+ removePinnedInsight(question.id, insight.id);
1102
+ debounceSave();
1103
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1104
+ });
1105
+ head.appendChild(unpin);
1106
+ card.appendChild(head);
1107
+
1108
+ const summary = document.createElement("p");
1109
+ summary.className = "option-insight-summary pinned";
1110
+ summary.textContent = insight.summary;
1111
+ card.appendChild(summary);
1112
+
1113
+ if (Array.isArray(insight.bullets) && insight.bullets.length > 0) {
1114
+ const list = document.createElement("ul");
1115
+ list.className = "option-insight-bullets";
1116
+ insight.bullets.forEach((bullet) => {
1117
+ const item = document.createElement("li");
1118
+ item.textContent = bullet;
1119
+ list.appendChild(item);
1120
+ });
1121
+ card.appendChild(list);
1122
+ }
1123
+
1124
+ if (insight.suggestedText) {
1125
+ const suggestion = document.createElement("code");
1126
+ suggestion.className = "option-insight-suggested-text compact";
1127
+ suggestion.textContent = insight.suggestedText;
1128
+ card.appendChild(suggestion);
1129
+ }
1130
+
1131
+ if (insight.modelUsed) {
1132
+ const meta = document.createElement("div");
1133
+ meta.className = "option-insight-meta";
1134
+ meta.textContent = insight.modelUsed;
1135
+ card.appendChild(meta);
1136
+ }
1137
+
1138
+ return card;
1139
+ }
1140
+
1141
+ function createOptionInsightPanel(question, optionKey) {
1142
+ const active = getActiveInsight(question.id, optionKey);
1143
+ if (!active) return null;
1144
+
1145
+ const panel = document.createElement("div");
1146
+ panel.className = "option-insight-panel";
1147
+ panel.dataset.optionInsightFor = optionKey;
1148
+
1149
+ const chips = document.createElement("div");
1150
+ chips.className = "option-insight-chips";
1151
+ ASK_PROMPT_CHIPS.forEach((chip) => {
1152
+ const btn = document.createElement("button");
1153
+ btn.type = "button";
1154
+ btn.className = "option-insight-chip" + (active.selectedChip === chip.key ? " active" : "");
1155
+ btn.textContent = chip.label;
1156
+ btn.addEventListener("click", (event) => {
1157
+ event.preventDefault();
1158
+ event.stopPropagation();
1159
+ active.selectedChip = chip.key;
1160
+ active.prompt = chip.prompt;
1161
+ active.error = "";
1162
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey, { focusComposer: true });
1163
+ });
1164
+ chips.appendChild(btn);
1165
+ });
1166
+ panel.appendChild(chips);
1167
+
1168
+ const input = document.createElement("textarea");
1169
+ input.className = "option-insight-input";
1170
+ input.rows = 2;
1171
+ input.placeholder = "Ask why it works, where it fails, or how to rewrite it...";
1172
+ input.dataset.questionId = question.id;
1173
+ input.dataset.optionKey = optionKey;
1174
+ input.value = active.prompt;
1175
+ input.addEventListener("input", () => {
1176
+ active.prompt = input.value;
1177
+ active.selectedChip = null;
1178
+ active.error = "";
1179
+ });
1180
+ input.addEventListener("keydown", (event) => {
1181
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
1182
+ event.preventDefault();
1183
+ submitOptionInsight(question, optionKey);
1184
+ }
1185
+ });
1186
+ panel.appendChild(input);
1187
+
1188
+ const metaRow = document.createElement("div");
1189
+ metaRow.className = "option-insight-meta-row";
1190
+
1191
+ const modelBadge = document.createElement("button");
1192
+ modelBadge.type = "button";
1193
+ modelBadge.className = "option-insight-model-badge";
1194
+ const badgeLabel = document.createElement("span");
1195
+ badgeLabel.className = "badge-label";
1196
+ badgeLabel.textContent = "Model";
1197
+ const badgeValue = document.createElement("span");
1198
+ badgeValue.textContent = getInsightModelLabel(active);
1199
+ const badgeCaret = document.createElement("span");
1200
+ badgeCaret.className = "badge-caret";
1201
+ badgeCaret.textContent = active.advancedOpen ? "▾" : "▸";
1202
+ modelBadge.appendChild(badgeLabel);
1203
+ modelBadge.appendChild(badgeValue);
1204
+ modelBadge.appendChild(badgeCaret);
1205
+ modelBadge.addEventListener("click", (event) => {
1206
+ event.preventDefault();
1207
+ event.stopPropagation();
1208
+ active.advancedOpen = !active.advancedOpen;
1209
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1210
+ });
1211
+ metaRow.appendChild(modelBadge);
1212
+
1213
+ panel.appendChild(metaRow);
1214
+
1215
+ if (active.advancedOpen) {
1216
+ const advanced = document.createElement("div");
1217
+ advanced.className = "option-insight-advanced";
1218
+
1219
+ const providers = [...new Set(askModels.map((model) => model.provider))];
1220
+ const providerRow = document.createElement("div");
1221
+ providerRow.className = "option-insight-provider-row";
1222
+ providers.forEach((provider) => {
1223
+ const btn = document.createElement("button");
1224
+ btn.type = "button";
1225
+ const isSelected = (active.selectedProvider || providers[0] || "") === provider;
1226
+ btn.className = "option-insight-provider-btn" + (isSelected ? " is-selected" : "");
1227
+ btn.textContent = providerLabel(provider);
1228
+ btn.addEventListener("click", (event) => {
1229
+ event.preventDefault();
1230
+ event.stopPropagation();
1231
+ active.selectedProvider = provider;
1232
+ const providerModels = getModelsForProvider(provider);
1233
+ active.selectedModel = providerModels[0]?.value || null;
1234
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1235
+ });
1236
+ providerRow.appendChild(btn);
1237
+ });
1238
+ advanced.appendChild(providerRow);
1239
+
1240
+ const providerModels = getModelsForProvider(active.selectedProvider);
1241
+ const modelRow = document.createElement("div");
1242
+ modelRow.className = "option-insight-model-row";
1243
+ providerModels.forEach((modelOption) => {
1244
+ const btn = document.createElement("button");
1245
+ btn.type = "button";
1246
+ const isSelected = active.selectedModel === modelOption.value;
1247
+ btn.className = "option-insight-model-btn" + (isSelected ? " is-selected" : "");
1248
+ btn.textContent = modelOption.label;
1249
+ btn.addEventListener("click", (event) => {
1250
+ event.preventDefault();
1251
+ event.stopPropagation();
1252
+ active.selectedModel = modelOption.value;
1253
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1254
+ });
1255
+ modelRow.appendChild(btn);
1256
+ });
1257
+ advanced.appendChild(modelRow);
1258
+
1259
+ const depthRow = document.createElement("div");
1260
+ depthRow.className = "option-insight-depth-row";
1261
+ ASK_DEPTH_OPTIONS.forEach((depth) => {
1262
+ const btn = document.createElement("button");
1263
+ btn.type = "button";
1264
+ const isSelected = active.selectedDepth === depth.key;
1265
+ btn.className = "option-insight-depth-btn" + (isSelected ? " is-selected" : "");
1266
+ btn.textContent = depth.label;
1267
+ btn.addEventListener("click", (event) => {
1268
+ event.preventDefault();
1269
+ event.stopPropagation();
1270
+ active.selectedDepth = depth.key;
1271
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1272
+ });
1273
+ depthRow.appendChild(btn);
1274
+ });
1275
+ advanced.appendChild(depthRow);
1276
+
1277
+ panel.appendChild(advanced);
1278
+ }
1279
+
1280
+ const actions = document.createElement("div");
1281
+ actions.className = "option-insight-actions";
1282
+
1283
+ const askButton = document.createElement("button");
1284
+ askButton.type = "button";
1285
+ askButton.className = "option-insight-submit" + (active.loading ? " loading" : "");
1286
+ askButton.textContent = active.loading ? "Cancel" : "Ask";
1287
+ askButton.addEventListener("click", (event) => {
1288
+ event.preventDefault();
1289
+ event.stopPropagation();
1290
+ submitOptionInsight(question, optionKey);
1291
+ });
1292
+ actions.appendChild(askButton);
1293
+
1294
+ panel.appendChild(actions);
1295
+
1296
+ if (active.loading && !active.result) {
1297
+ const loading = document.createElement("div");
1298
+ loading.className = "option-insight-loading";
1299
+ const spinner = document.createElement("span");
1300
+ spinner.className = "option-insight-spinner";
1301
+ spinner.textContent = "Thinking";
1302
+ loading.appendChild(spinner);
1303
+ const dots = document.createElement("span");
1304
+ dots.className = "option-insight-dots";
1305
+ dots.textContent = "...";
1306
+ loading.appendChild(dots);
1307
+ panel.appendChild(loading);
1308
+ }
1309
+
1310
+ if (active.error) {
1311
+ const error = document.createElement("div");
1312
+ error.className = "option-insight-error";
1313
+ error.textContent = active.error;
1314
+ panel.appendChild(error);
1315
+ }
1316
+
1317
+ if (active.result) {
1318
+ const result = document.createElement("div");
1319
+ result.className = "option-insight-result";
1320
+
1321
+ const summary = document.createElement("p");
1322
+ summary.className = "option-insight-summary";
1323
+ summary.textContent = active.result.summary;
1324
+ result.appendChild(summary);
1325
+
1326
+ if (Array.isArray(active.result.bullets) && active.result.bullets.length > 0) {
1327
+ const list = document.createElement("ul");
1328
+ list.className = "option-insight-bullets";
1329
+ active.result.bullets.forEach((bullet) => {
1330
+ const item = document.createElement("li");
1331
+ item.textContent = bullet;
1332
+ list.appendChild(item);
1333
+ });
1334
+ result.appendChild(list);
1335
+ }
1336
+
1337
+ if (active.result.suggestedText) {
1338
+ const suggestionLabel = document.createElement("div");
1339
+ suggestionLabel.className = "option-insight-suggestion-label";
1340
+ suggestionLabel.textContent = "Suggested rewrite";
1341
+ result.appendChild(suggestionLabel);
1342
+
1343
+ const suggestion = document.createElement("code");
1344
+ suggestion.className = "option-insight-suggested-text";
1345
+ suggestion.textContent = active.result.suggestedText;
1346
+ result.appendChild(suggestion);
1347
+ }
1348
+
1349
+ if (active.result.modelUsed) {
1350
+ const meta = document.createElement("div");
1351
+ meta.className = "option-insight-meta";
1352
+ meta.textContent = active.result.modelUsed;
1353
+ result.appendChild(meta);
1354
+ }
1355
+
1356
+ const resultActions = document.createElement("div");
1357
+ resultActions.className = "option-insight-result-actions";
1358
+
1359
+ const pinBtn = document.createElement("button");
1360
+ pinBtn.type = "button";
1361
+ pinBtn.className = "option-insight-secondary";
1362
+ pinBtn.textContent = "Pin";
1363
+ pinBtn.addEventListener("click", (event) => {
1364
+ event.preventDefault();
1365
+ event.stopPropagation();
1366
+ pinActiveInsight(question, optionKey);
1367
+ });
1368
+ resultActions.appendChild(pinBtn);
1369
+
1370
+ const moveUpBtn = document.createElement("button");
1371
+ moveUpBtn.type = "button";
1372
+ moveUpBtn.className = "option-insight-secondary";
1373
+ moveUpBtn.textContent = "Move up";
1374
+ moveUpBtn.disabled = getOptionIndexByKey(question.id, optionKey) <= 0;
1375
+ moveUpBtn.addEventListener("click", (event) => {
1376
+ event.preventDefault();
1377
+ event.stopPropagation();
1378
+ runOptionAction(question, optionKey, "move-up");
1379
+ });
1380
+ resultActions.appendChild(moveUpBtn);
1381
+
1382
+ if (active.result.suggestedText) {
1383
+ const replaceBtn = document.createElement("button");
1384
+ replaceBtn.type = "button";
1385
+ replaceBtn.className = "option-insight-primary";
1386
+ replaceBtn.textContent = "Use rewrite";
1387
+ replaceBtn.addEventListener("click", (event) => {
1388
+ event.preventDefault();
1389
+ event.stopPropagation();
1390
+ runOptionAction(question, optionKey, "replace-text", active.result.suggestedText);
1391
+ });
1392
+ resultActions.appendChild(replaceBtn);
1393
+
1394
+ const addBtn = document.createElement("button");
1395
+ addBtn.type = "button";
1396
+ addBtn.className = "option-insight-secondary";
1397
+ addBtn.textContent = "Add rewrite as option";
1398
+ addBtn.addEventListener("click", (event) => {
1399
+ event.preventDefault();
1400
+ event.stopPropagation();
1401
+ runOptionAction(question, optionKey, "add-option", active.result.suggestedText);
1402
+ });
1403
+ resultActions.appendChild(addBtn);
1404
+ }
1405
+
1406
+ result.appendChild(resultActions);
1407
+ panel.appendChild(result);
1408
+ }
1409
+
1410
+ return panel;
1411
+ }
1412
+
1413
+ function createOptionNoteInput(question, optionLabel, isSelected) {
1414
+ if (!questionCanClarifyOption(question) || !isSelected) return null;
1415
+
1416
+ const wrap = document.createElement("div");
1417
+ wrap.className = "option-note-wrap";
1418
+
1419
+ const input = document.createElement("input");
1420
+ input.type = "text";
1421
+ input.className = "option-note-input";
1422
+ input.placeholder = "Optional clarification...";
1423
+ input.dataset.questionId = question.id;
1424
+ input.dataset.optionLabel = optionLabel;
1425
+ input.value = getChoiceNote(question.id, optionLabel);
1426
+ input.addEventListener("input", () => {
1427
+ setChoiceNote(question.id, optionLabel, input.value);
1428
+ debounceSave();
1429
+ });
1430
+ setupEdgeNavigation(input);
1431
+ wrap.appendChild(input);
1432
+
1433
+ return wrap;
1434
+ }
1435
+
1436
+ function createChoiceOptionRow(question, option, optionIndex, options = {}) {
1437
+ const optionLabel = getOptionLabel(option);
1438
+ const optionContent = isRichOption(option) ? option.content : null;
1439
+ const optionKey = getOptionKeys(question.id)[optionIndex] || null;
1440
+ const generatedSet = options.generatedKeys || new Set();
1441
+ const insightable = questionSupportsOptionInsights(question) && !!optionKey;
1442
+ const askable = questionCanAskAboutOption(question) && !!optionKey;
1443
+ const activeInsight = optionKey ? getActiveInsight(question.id, optionKey) : null;
1444
+
1445
+ const row = document.createElement("div");
1446
+ row.className = "option-row";
1447
+ if (generatedSet.has(optionKey)) {
1448
+ row.classList.add("generated");
1449
+ }
1450
+ if (activeInsight) {
1451
+ row.classList.add("ask-open");
1452
+ }
1453
+
1454
+ const main = document.createElement("div");
1455
+ main.className = "option-row-main";
1456
+
1457
+ const label = document.createElement("label");
1458
+ label.className = "option-item";
1459
+ if (optionContent) {
1460
+ label.classList.add("has-code");
1461
+ }
1462
+ const input = document.createElement("input");
1463
+ input.type = question.type === "single" ? "radio" : "checkbox";
1464
+ input.name = question.id;
1465
+ input.value = optionLabel;
1466
+ input.id = `q-${question.id}-${optionIndex}`;
1467
+
1468
+ input.addEventListener("change", () => {
1469
+ syncChoiceNotesWithSelection(question);
1470
+ debounceSave();
1471
+ if (question.type === "multi") {
1472
+ updateDoneState(question.id);
1473
+ }
1474
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1475
+ });
1476
+
1477
+ const body = document.createElement("div");
1478
+ body.className = "option-item-body";
1479
+
1480
+ const text = document.createElement("span");
1481
+ text.className = "option-item-label";
1482
+ text.textContent = optionLabel;
1483
+
1484
+ const recommended = question.recommended;
1485
+ const recommendedList = Array.isArray(recommended)
1486
+ ? recommended
1487
+ : recommended
1488
+ ? [recommended]
1489
+ : [];
1490
+ const shouldPreselect = recommendedList.length > 0 && question.conviction !== "slight";
1491
+
1492
+ if (recommendedList.includes(optionLabel)) {
1493
+ const pill = document.createElement("span");
1494
+ pill.className = "recommended-pill";
1495
+ pill.textContent = "Recommended";
1496
+ text.appendChild(pill);
1497
+ if (shouldPreselect) {
1498
+ input.checked = true;
1499
+ }
1500
+ }
1501
+
1502
+ body.appendChild(text);
1503
+
1504
+ if (optionContent) {
1505
+ const contentBlockEl = renderContentBlock(optionContent);
1506
+ if (contentBlockEl) {
1507
+ body.appendChild(contentBlockEl);
1508
+ }
1509
+ }
1510
+
1511
+ label.appendChild(input);
1512
+ label.appendChild(body);
1513
+
1514
+ main.appendChild(label);
1515
+ const selectedLabels = new Set(getSelectedOptionLabels(question.id));
1516
+ const noteInput = createOptionNoteInput(question, optionLabel, input.checked || selectedLabels.has(optionLabel));
1517
+
1518
+ if (insightable && optionKey) {
1519
+ if (askable) {
1520
+ const askButton = document.createElement("button");
1521
+ askButton.type = "button";
1522
+ askButton.className = "option-ask-btn";
1523
+ askButton.textContent = activeInsight ? "Hide" : "Ask";
1524
+ askButton.addEventListener("click", (event) => {
1525
+ event.preventDefault();
1526
+ event.stopPropagation();
1527
+ openOptionInsightPanel(question, optionKey);
1528
+ });
1529
+ main.appendChild(askButton);
1530
+
1531
+ const panel = createOptionInsightPanel(question, optionKey);
1532
+ row.appendChild(main);
1533
+ if (noteInput) row.appendChild(noteInput);
1534
+ if (panel) row.appendChild(panel);
1535
+ } else {
1536
+ row.appendChild(main);
1537
+ if (noteInput) row.appendChild(noteInput);
1538
+ }
1539
+
1540
+ const pinnedInsights = getPinnedInsights(question.id, optionKey);
1541
+ if (pinnedInsights.length > 0) {
1542
+ const pinnedWrap = document.createElement("div");
1543
+ pinnedWrap.className = "option-insight-pinned-list";
1544
+ pinnedInsights.forEach((insight) => {
1545
+ pinnedWrap.appendChild(createPinnedInsightCard(question, optionKey, insight));
1546
+ });
1547
+ row.appendChild(pinnedWrap);
1548
+ }
1549
+
1550
+ return row;
1551
+ }
1552
+
1553
+ row.appendChild(main);
1554
+ if (noteInput) row.appendChild(noteInput);
1555
+ return row;
1556
+ }
1557
+
1558
+ function createChoiceQuestionList(question, title, options = {}) {
1559
+ const list = document.createElement("div");
1560
+ list.className = "option-list";
1561
+ list.setAttribute("role", question.type === "single" ? "radiogroup" : "group");
1562
+ list.setAttribute("aria-labelledby", title.id);
1563
+
1564
+ const generatedKeys = new Set(options.generatedKeys || []);
1565
+
1566
+ question.options.forEach((option, optionIndex) => {
1567
+ list.appendChild(createChoiceOptionRow(question, option, optionIndex, { generatedKeys }));
1568
+ });
1569
+
1570
+ const generateMoreEl = createGenerateMoreUI(question, list);
1571
+ if (generateMoreEl) list.appendChild(generateMoreEl);
1572
+
1573
+ const otherLabel = document.createElement("label");
1574
+ otherLabel.className = "option-item option-other";
1575
+ const otherCheck = document.createElement("input");
1576
+ otherCheck.type = question.type === "single" ? "radio" : "checkbox";
1577
+ otherCheck.name = question.id;
1578
+ otherCheck.value = "__other__";
1579
+ otherCheck.id = `q-${question.id}-other`;
1580
+ const otherInput = document.createElement("textarea");
1581
+ otherInput.className = "other-input";
1582
+ otherInput.placeholder = "Other...";
1583
+ otherInput.rows = 1;
1584
+ otherInput.dataset.questionId = question.id;
1585
+ const autoResizeOther = () => {
1586
+ otherInput.style.height = "auto";
1587
+ otherInput.style.height = otherInput.scrollHeight + "px";
1588
+ };
1589
+ otherInput.addEventListener("input", () => {
1590
+ autoResizeOther();
1591
+ if (otherInput.value && !otherCheck.checked) {
1592
+ otherCheck.checked = true;
1593
+ if (question.type === "multi") updateDoneState(question.id);
1594
+ }
1595
+ debounceSave();
1596
+ });
1597
+ otherInput.addEventListener("focus", () => {
1598
+ if (!otherCheck.checked) {
1599
+ otherCheck.checked = true;
1600
+ if (question.type === "multi") updateDoneState(question.id);
1601
+ debounceSave();
1602
+ }
1603
+ });
1604
+ otherCheck.addEventListener("change", () => {
1605
+ debounceSave();
1606
+ if (question.type === "multi") updateDoneState(question.id);
1607
+ if (otherCheck.checked) otherInput.focus();
1608
+ });
1609
+ setupEdgeNavigation(otherInput);
1610
+ otherLabel.appendChild(otherCheck);
1611
+ otherLabel.appendChild(otherInput);
1612
+ list.appendChild(otherLabel);
1613
+
1614
+ if (question.type === "multi") {
1615
+ const doneItem = document.createElement("div");
1616
+ doneItem.className = "option-item done-item disabled";
1617
+ doneItem.setAttribute("tabindex", "0");
1618
+ doneItem.dataset.doneFor = question.id;
1619
+ doneItem.innerHTML = '<span class="done-check">✓</span><span>Done</span>';
1620
+ doneItem.addEventListener("click", () => {
1621
+ if (!doneItem.classList.contains("disabled")) {
1622
+ nextQuestion();
1623
+ }
1624
+ });
1625
+ doneItem.addEventListener("keydown", (e) => {
1626
+ if ((e.key === "Enter" || e.key === " ") && !doneItem.classList.contains("disabled")) {
1627
+ e.preventDefault();
1628
+ e.stopPropagation();
1629
+ nextQuestion();
1630
+ }
1631
+ });
1632
+ list.appendChild(doneItem);
1633
+ }
1634
+
1635
+ return list;
1636
+ }
1637
+
549
1638
  function renderContentBlock(block) {
550
1639
  if (!block || !block.source) return null;
551
1640
 
@@ -1203,8 +2292,14 @@
1203
2292
  return items;
1204
2293
  }
1205
2294
 
2295
+ function getTabStopsForCard(card) {
2296
+ return Array.from(
2297
+ card.querySelectorAll('input[type="radio"], input[type="checkbox"], .option-note-input, .option-ask-btn, .file-dropzone, .image-path-input, .done-item')
2298
+ );
2299
+ }
2300
+
1206
2301
  function isPathInput(el) {
1207
- return el && (el.classList.contains('image-path-input') || el.classList.contains('attach-inline-path') || el.classList.contains('other-input'));
2302
+ return el && (el.classList.contains('image-path-input') || el.classList.contains('attach-inline-path') || el.classList.contains('other-input') || el.classList.contains('option-note-input'));
1208
2303
  }
1209
2304
 
1210
2305
  function isDropzone(el) {
@@ -1260,7 +2355,7 @@
1260
2355
  function highlightOption(card, optionIndex, isKeyboard = true) {
1261
2356
  const options = getOptionsForCard(card);
1262
2357
  options.forEach((opt, i) => {
1263
- const item = isOptionInput(opt) ? opt.closest('.option-item') : opt;
2358
+ const item = isOptionInput(opt) ? (opt.closest('.option-row') || opt.closest('.option-item')) : opt;
1264
2359
  item?.classList.toggle('focused', i === optionIndex);
1265
2360
  });
1266
2361
  const current = options[optionIndex];
@@ -1272,8 +2367,31 @@
1272
2367
  }
1273
2368
  }
1274
2369
 
2370
+ function focusCardTabStop(card, target, isKeyboard = true) {
2371
+ if (!target) return;
2372
+
2373
+ clearOptionHighlight(card);
2374
+
2375
+ const row = target.closest?.('.option-row');
2376
+ const highlightTarget = row || (isOptionInput(target) ? target.closest('.option-item') : target);
2377
+ highlightTarget?.classList.add('focused');
2378
+
2379
+ const rowInput = row?.querySelector('input[type="radio"], input[type="checkbox"]');
2380
+ const options = getOptionsForCard(card);
2381
+ const navTarget = rowInput || target;
2382
+ const nextIndex = options.indexOf(navTarget);
2383
+ if (nextIndex >= 0) {
2384
+ nav.optionIndex = nextIndex;
2385
+ }
2386
+
2387
+ target.focus();
2388
+ if (isKeyboard) {
2389
+ card.classList.add('keyboard-nav');
2390
+ }
2391
+ }
2392
+
1275
2393
  function clearOptionHighlight(card) {
1276
- card.querySelectorAll('.option-item, .done-item, .file-dropzone, .image-path-input').forEach(item => {
2394
+ card.querySelectorAll('.option-row, .option-item, .done-item, .file-dropzone, .image-path-input').forEach(item => {
1277
2395
  item.classList.remove('focused');
1278
2396
  });
1279
2397
  }
@@ -1385,25 +2503,33 @@
1385
2503
  const options = getOptionsForCard(card);
1386
2504
  const textarea = card.querySelector('textarea');
1387
2505
  const isTextFocused = document.activeElement === textarea;
1388
-
2506
+ const inAskArea = document.activeElement?.closest('.option-insight-panel, .option-ask-btn, .option-insight-pinned');
2507
+ const inOptionNote = document.activeElement?.closest('.option-note-wrap');
2508
+
1389
2509
  if (event.key === 'Tab') {
1390
2510
  const inAttachArea = document.activeElement?.closest('.attach-inline');
1391
2511
  const inGenerateArea = document.activeElement?.closest('.generate-more');
1392
- if (inAttachArea || inGenerateArea) return;
2512
+ if (inAttachArea || inGenerateArea || inAskArea || inOptionNote) return;
1393
2513
 
1394
- event.preventDefault();
1395
-
1396
- if (options.length > 0) {
1397
- if (event.shiftKey) {
1398
- nav.optionIndex = (nav.optionIndex - 1 + options.length) % options.length;
1399
- } else {
1400
- nav.optionIndex = (nav.optionIndex + 1) % options.length;
1401
- }
1402
- highlightOption(card, nav.optionIndex);
2514
+ const tabStops = getTabStopsForCard(card);
2515
+ if (tabStops.length === 0) {
2516
+ return;
1403
2517
  }
2518
+
2519
+ event.preventDefault();
2520
+
2521
+ const activeIndex = tabStops.indexOf(document.activeElement);
2522
+ const fallbackIndex = options[nav.optionIndex] ? tabStops.indexOf(options[nav.optionIndex]) : -1;
2523
+ const currentIndex = activeIndex >= 0 ? activeIndex : (fallbackIndex >= 0 ? fallbackIndex : 0);
2524
+ const nextIndex = event.shiftKey
2525
+ ? (currentIndex - 1 + tabStops.length) % tabStops.length
2526
+ : (currentIndex + 1) % tabStops.length;
2527
+ focusCardTabStop(card, tabStops[nextIndex]);
1404
2528
  return;
1405
2529
  }
1406
-
2530
+
2531
+ if (inAskArea || inOptionNote) return;
2532
+
1407
2533
  if (event.key === 'ArrowLeft') {
1408
2534
  if (isTextFocused || isPathInput(document.activeElement)) {
1409
2535
  return;
@@ -1438,16 +2564,19 @@
1438
2564
  }
1439
2565
 
1440
2566
  if (event.key === 'Enter' || event.key === ' ') {
1441
- if (isPathInput(document.activeElement)) {
1442
- return;
1443
- }
1444
- if (document.activeElement?.closest('.attach-inline')) {
1445
- return;
1446
- }
1447
- if (document.activeElement?.closest('.generate-more')) {
1448
- return;
1449
- }
1450
- event.preventDefault();
2567
+ if (isPathInput(document.activeElement)) {
2568
+ return;
2569
+ }
2570
+ if (document.activeElement?.closest('.attach-inline')) {
2571
+ return;
2572
+ }
2573
+ if (document.activeElement?.closest('.generate-more')) {
2574
+ return;
2575
+ }
2576
+ if (document.activeElement?.closest('.option-insight-panel, .option-ask-btn')) {
2577
+ return;
2578
+ }
2579
+ event.preventDefault();
1451
2580
  const option = options[nav.optionIndex];
1452
2581
  if (option) {
1453
2582
  if (isDoneItem(option)) {
@@ -1482,6 +2611,17 @@
1482
2611
  }
1483
2612
  return;
1484
2613
  }
2614
+
2615
+ if ((event.key === 'a' || event.key === 'A') && isOptionInput(document.activeElement)) {
2616
+ event.preventDefault();
2617
+ const focusedInput = options[nav.optionIndex];
2618
+ const row = focusedInput?.closest('.option-row');
2619
+ const askButton = row?.querySelector('.option-ask-btn');
2620
+ if (askButton) {
2621
+ askButton.click();
2622
+ }
2623
+ return;
2624
+ }
1485
2625
  }
1486
2626
 
1487
2627
  if (textarea && !isTextFocused) {
@@ -1550,7 +2690,6 @@
1550
2690
 
1551
2691
  function createGenerateMoreUI(question, list) {
1552
2692
  if (!data.canGenerate) return null;
1553
- if (question.options.some(isRichOption)) return null;
1554
2693
 
1555
2694
  const container = document.createElement("div");
1556
2695
  container.className = "generate-more";
@@ -1609,15 +2748,6 @@
1609
2748
  }, timeoutMs);
1610
2749
  }
1611
2750
 
1612
- function getExistingOptions() {
1613
- const inputs = list.querySelectorAll(
1614
- 'input[name="' + escapeSelector(question.id) + '"]'
1615
- );
1616
- return Array.from(inputs)
1617
- .map((input) => input.value)
1618
- .filter((v) => v && v !== "__other__");
1619
- }
1620
-
1621
2751
  async function runGenerate(btn, mode) {
1622
2752
  if (generating) {
1623
2753
  if (abortController) abortController.abort();
@@ -1634,7 +2764,7 @@
1634
2764
  clearStatus();
1635
2765
 
1636
2766
  abortController = new AbortController();
1637
- const existingOptions = getExistingOptions();
2767
+ const currentValue = getQuestionValue(question);
1638
2768
 
1639
2769
  try {
1640
2770
  const response = await fetch("/generate", {
@@ -1643,7 +2773,6 @@
1643
2773
  body: JSON.stringify({
1644
2774
  token: sessionToken,
1645
2775
  questionId: question.id,
1646
- existingOptions,
1647
2776
  mode,
1648
2777
  }),
1649
2778
  signal: abortController.signal,
@@ -1651,25 +2780,23 @@
1651
2780
 
1652
2781
  const result = await response.json();
1653
2782
  if (!result.ok) throw new Error(result.error || "Generation failed");
1654
- if (!Array.isArray(result.options) || result.options.length === 0) {
2783
+ if (!Array.isArray(result.options)) {
2784
+ throw new Error("Generation returned invalid options");
2785
+ }
2786
+ if (mode === "review" && result.options.length === 0) {
1655
2787
  throw new Error("No options generated");
1656
2788
  }
2789
+ if (Array.isArray(result.optionKeys)) {
2790
+ setOptionKeys(question.id, result.optionKeys);
2791
+ pruneQuestionOptionInsights(question.id);
2792
+ }
1657
2793
 
1658
2794
  if (mode === "review") {
1659
2795
  if (typeof result.question !== "string" || !result.question.trim()) {
1660
2796
  throw new Error("No revised question returned");
1661
2797
  }
1662
2798
 
1663
- const seen = new Set();
1664
- const revisedOptions = result.options.filter((option) => {
1665
- const key = option.toLowerCase().trim();
1666
- if (seen.has(key)) return false;
1667
- seen.add(key);
1668
- return true;
1669
- });
1670
- if (revisedOptions.length === 0) {
1671
- throw new Error("No valid options returned for review");
1672
- }
2799
+ const revisedOptions = result.options;
1673
2800
 
1674
2801
  question.question = result.question.trim();
1675
2802
  question.options = revisedOptions;
@@ -1678,38 +2805,27 @@
1678
2805
  if (title) {
1679
2806
  title.innerHTML = renderLightMarkdown(question.question);
1680
2807
  }
1681
-
1682
- list
1683
- .querySelectorAll('.option-item:not(.option-other):not(.done-item)')
1684
- .forEach((el) => el.remove());
1685
- revisedOptions.forEach((optionText, i) => {
1686
- const optionEl = createGeneratedOption(question, optionText, i);
1687
- list.insertBefore(optionEl, container);
1688
- });
1689
- if (question.type === "multi") updateDoneState(question.id);
2808
+ const revisedLabels = new Set(revisedOptions.map((option) => getOptionLabel(option)));
2809
+ const nextValue = preserveChoiceAnswerValue(question, currentValue, revisedLabels);
2810
+ replaceQuestionOptionList(question, nextValue);
1690
2811
  debounceSave();
1691
2812
  showStatus(
1692
2813
  "Question updated and " + revisedOptions.length + " option" + (revisedOptions.length > 1 ? "s" : "") + " revised",
1693
2814
  2500,
1694
2815
  );
1695
2816
  } else {
1696
- const existingSet = new Set(existingOptions.map((o) => o.toLowerCase().trim()));
1697
- const seen = new Set();
1698
- const newOptions = result.options.filter((o) => {
1699
- const key = o.toLowerCase().trim();
1700
- if (existingSet.has(key) || seen.has(key)) return false;
1701
- seen.add(key);
1702
- return true;
1703
- });
2817
+ const newOptions = result.options;
1704
2818
 
1705
2819
  if (newOptions.length === 0) {
1706
2820
  showStatus("All generated options already exist", 3000);
1707
2821
  } else {
1708
2822
  question.options = question.options.concat(newOptions);
1709
- newOptions.forEach((optionText, i) => {
1710
- const optionEl = createGeneratedOption(question, optionText, i);
1711
- list.insertBefore(optionEl, container);
2823
+ const optionKeys = getOptionKeys(question.id);
2824
+ const generatedKeys = optionKeys.slice(-newOptions.length);
2825
+ replaceQuestionOptionList(question, currentValue, generatedKeys[0] || null, {
2826
+ generatedKeys,
1712
2827
  });
2828
+ debounceSave();
1713
2829
  showStatus(
1714
2830
  newOptions.length + " option" + (newOptions.length > 1 ? "s" : "") + " added",
1715
2831
  2500,
@@ -1739,33 +2855,6 @@
1739
2855
  return container;
1740
2856
  }
1741
2857
 
1742
- function createGeneratedOption(question, optionText, animIndex) {
1743
- const label = document.createElement("label");
1744
- label.className = "option-item generated";
1745
- label.style.animationDelay = (animIndex * 0.08) + "s";
1746
-
1747
- const input = document.createElement("input");
1748
- input.type = question.type === "single" ? "radio" : "checkbox";
1749
- input.name = question.id;
1750
- input.value = optionText;
1751
- input.setAttribute("tabindex", "-1");
1752
-
1753
- input.addEventListener("change", () => {
1754
- debounceSave();
1755
- if (question.type === "multi") {
1756
- updateDoneState(question.id);
1757
- }
1758
- });
1759
-
1760
- const text = document.createElement("span");
1761
- text.textContent = optionText;
1762
-
1763
- label.appendChild(input);
1764
- label.appendChild(text);
1765
-
1766
- return label;
1767
- }
1768
-
1769
2858
  function createQuestionCard(question, index, badgeNumber) {
1770
2859
  const card = document.createElement("section");
1771
2860
  card.className = "question-card";
@@ -1838,135 +2927,7 @@
1838
2927
  }
1839
2928
 
1840
2929
  if (question.type === "single" || question.type === "multi") {
1841
- const list = document.createElement("div");
1842
- list.className = "option-list";
1843
- list.setAttribute("role", question.type === "single" ? "radiogroup" : "group");
1844
- list.setAttribute("aria-labelledby", title.id);
1845
-
1846
- const recommended = question.recommended;
1847
- const recommendedList = Array.isArray(recommended)
1848
- ? recommended
1849
- : recommended
1850
- ? [recommended]
1851
- : [];
1852
- const shouldPreselect = recommendedList.length > 0 && question.conviction !== "slight";
1853
-
1854
- question.options.forEach((option, optionIndex) => {
1855
- const optionLabel = getOptionLabel(option);
1856
- const optionContent = isRichOption(option) ? option.content : null;
1857
-
1858
- const label = document.createElement("label");
1859
- label.className = "option-item";
1860
- if (optionContent) {
1861
- label.classList.add("has-code");
1862
- }
1863
-
1864
- const input = document.createElement("input");
1865
- input.type = question.type === "single" ? "radio" : "checkbox";
1866
- input.name = question.id;
1867
- input.value = optionLabel;
1868
- input.id = `q-${question.id}-${optionIndex}`;
1869
-
1870
- input.addEventListener("change", () => {
1871
- debounceSave();
1872
- if (question.type === "multi") {
1873
- updateDoneState(question.id);
1874
- }
1875
- });
1876
-
1877
- const text = document.createElement("span");
1878
- text.textContent = optionLabel;
1879
-
1880
- if (recommendedList.includes(optionLabel)) {
1881
- const pill = document.createElement("span");
1882
- pill.className = "recommended-pill";
1883
- pill.textContent = "Recommended";
1884
- text.appendChild(pill);
1885
-
1886
- if (shouldPreselect) {
1887
- input.checked = true;
1888
- }
1889
- }
1890
-
1891
- label.appendChild(input);
1892
- label.appendChild(text);
1893
-
1894
- if (optionContent) {
1895
- const contentBlockEl = renderContentBlock(optionContent);
1896
- if (contentBlockEl) {
1897
- label.appendChild(contentBlockEl);
1898
- }
1899
- }
1900
-
1901
- list.appendChild(label);
1902
- });
1903
-
1904
- const generateMoreEl = createGenerateMoreUI(question, list);
1905
- if (generateMoreEl) list.appendChild(generateMoreEl);
1906
-
1907
- const otherLabel = document.createElement("label");
1908
- otherLabel.className = "option-item option-other";
1909
- const otherCheck = document.createElement("input");
1910
- otherCheck.type = question.type === "single" ? "radio" : "checkbox";
1911
- otherCheck.name = question.id;
1912
- otherCheck.value = "__other__";
1913
- otherCheck.id = `q-${question.id}-other`;
1914
- const otherInput = document.createElement("textarea");
1915
- otherInput.className = "other-input";
1916
- otherInput.placeholder = "Other...";
1917
- otherInput.rows = 1;
1918
- otherInput.dataset.questionId = question.id;
1919
- const autoResizeOther = () => {
1920
- otherInput.style.height = "auto";
1921
- otherInput.style.height = otherInput.scrollHeight + "px";
1922
- };
1923
- otherInput.addEventListener("input", () => {
1924
- autoResizeOther();
1925
- if (otherInput.value && !otherCheck.checked) {
1926
- otherCheck.checked = true;
1927
- if (question.type === "multi") updateDoneState(question.id);
1928
- }
1929
- debounceSave();
1930
- });
1931
- otherInput.addEventListener("focus", () => {
1932
- if (!otherCheck.checked) {
1933
- otherCheck.checked = true;
1934
- if (question.type === "multi") updateDoneState(question.id);
1935
- debounceSave();
1936
- }
1937
- });
1938
- otherCheck.addEventListener("change", () => {
1939
- debounceSave();
1940
- if (question.type === "multi") updateDoneState(question.id);
1941
- if (otherCheck.checked) otherInput.focus();
1942
- });
1943
- setupEdgeNavigation(otherInput);
1944
- otherLabel.appendChild(otherCheck);
1945
- otherLabel.appendChild(otherInput);
1946
- list.appendChild(otherLabel);
1947
-
1948
- if (question.type === "multi") {
1949
- const doneItem = document.createElement("div");
1950
- doneItem.className = "option-item done-item disabled";
1951
- doneItem.setAttribute("tabindex", "0");
1952
- doneItem.dataset.doneFor = question.id;
1953
- doneItem.innerHTML = '<span class="done-check">✓</span><span>Done</span>';
1954
- doneItem.addEventListener("click", () => {
1955
- if (!doneItem.classList.contains("disabled")) {
1956
- nextQuestion();
1957
- }
1958
- });
1959
- doneItem.addEventListener("keydown", (e) => {
1960
- if ((e.key === "Enter" || e.key === " ") && !doneItem.classList.contains("disabled")) {
1961
- e.preventDefault();
1962
- e.stopPropagation();
1963
- nextQuestion();
1964
- }
1965
- });
1966
- list.appendChild(doneItem);
1967
- }
1968
-
1969
- card.appendChild(list);
2930
+ card.appendChild(createChoiceQuestionList(question, title));
1970
2931
  }
1971
2932
 
1972
2933
  if (question.type === "text") {
@@ -2196,17 +3157,7 @@
2196
3157
  if (files && files.length > 0) {
2197
3158
  const file = files[0];
2198
3159
  if (!file.type.startsWith("image/")) return;
2199
- if (question.type === "image") {
2200
- const input = card.querySelector('input[type="file"]');
2201
- if (input) {
2202
- const dt = new DataTransfer();
2203
- dt.items.add(file);
2204
- input.files = dt.files;
2205
- input.dispatchEvent(new Event("change"));
2206
- }
2207
- } else {
2208
- void addPastedImage(question, file);
2209
- }
3160
+ void addDroppedImage(question, file);
2210
3161
  }
2211
3162
  });
2212
3163
 
@@ -2306,8 +3257,9 @@
2306
3257
  input.value = "";
2307
3258
  return;
2308
3259
  }
2309
- } catch (_err) {
2310
- setFieldError(questionId, "Failed to validate image.");
3260
+ } catch (err) {
3261
+ const message = err instanceof Error ? err.message : "Failed to validate image.";
3262
+ setFieldError(questionId, message);
2311
3263
  input.value = "";
2312
3264
  return;
2313
3265
  }
@@ -2315,37 +3267,15 @@
2315
3267
  manager.addFile(questionId, file);
2316
3268
  }
2317
3269
 
2318
- function resolveQuestionContext(target) {
2319
- const element = target && target.closest ? target : null;
2320
- let card = element ? element.closest(".question-card") : null;
2321
-
2322
- if (!card) {
2323
- card = document.querySelector(".question-card.active");
2324
- }
2325
-
2326
- if (card?.dataset?.questionId) {
2327
- const question = questions.find((q) => q.id === card.dataset.questionId);
2328
- if (question) {
2329
- return { question, card };
2330
- }
2331
- }
2332
-
2333
- const question = questions[nav.questionIndex];
2334
- const fallbackCard = nav.cards[nav.questionIndex];
2335
- if (!question || !fallbackCard) return null;
2336
- return { question, card: fallbackCard };
2337
- }
2338
-
2339
3270
  function revealAttachmentArea(questionId) {
2340
3271
  const attachInline = document.querySelector(
2341
3272
  `[data-attach-inline-for="${escapeSelector(questionId)}"]`
2342
3273
  );
2343
- if (attachInline?.classList.contains("hidden")) {
2344
- attachInline.classList.remove("hidden");
2345
- }
3274
+ if (!attachInline?.classList.contains("hidden")) return;
3275
+ attachInline.classList.remove("hidden");
2346
3276
  }
2347
3277
 
2348
- async function addPastedImage(question, file) {
3278
+ async function addDroppedImage(question, file) {
2349
3279
  if (countUploadedFiles(question.id) + 1 > MAX_IMAGES) {
2350
3280
  setFieldError(question.id, `Only ${MAX_IMAGES} images allowed.`);
2351
3281
  return;
@@ -2357,59 +3287,20 @@
2357
3287
  setFieldError(question.id, validation.error);
2358
3288
  return;
2359
3289
  }
2360
- } catch (_err) {
2361
- setFieldError(question.id, "Failed to validate image.");
3290
+ } catch (err) {
3291
+ const message = err instanceof Error ? err.message : "Failed to validate image.";
3292
+ setFieldError(question.id, message);
2362
3293
  return;
2363
3294
  }
2364
3295
 
2365
3296
  setFieldError(question.id, "");
2366
3297
  if (question.type === "image") {
2367
3298
  questionImages.addFile(question.id, file);
2368
- } else {
2369
- revealAttachmentArea(question.id);
2370
- attachments.addFile(question.id, file);
2371
- }
2372
- }
2373
-
2374
- function handlePaste(event) {
2375
- if (nav.inSubmitArea || session.expired) return;
2376
- const clipboard = event.clipboardData;
2377
- if (!clipboard) return;
2378
-
2379
- const active = document.activeElement;
2380
- const isTextInput = active && (active.tagName === "TEXTAREA" || (active.tagName === "INPUT" && active.type === "text"));
2381
- if (isTextInput && clipboard.getData("text/plain")) {
2382
3299
  return;
2383
3300
  }
2384
3301
 
2385
- const context = resolveQuestionContext(event.target);
2386
- if (!context) return;
2387
-
2388
- const items = Array.from(clipboard.items || []);
2389
- const imageItem = items.find((item) => item.type && item.type.startsWith("image/"));
2390
-
2391
- if (imageItem) {
2392
- const file = imageItem.getAsFile();
2393
- if (!file) return;
2394
- event.preventDefault();
2395
- void addPastedImage(context.question, file);
2396
- return;
2397
- }
2398
-
2399
- const text = clipboard.getData("text/plain")?.trim();
2400
- const isPathLike = text && (text.startsWith("/") || text.startsWith("~") || text.match(/^[a-zA-Z]:\\/));
2401
- const hasImageExtension = text && /\.(png|jpe?g|gif|webp)$/i.test(text);
2402
-
2403
- if (isPathLike && hasImageExtension) {
2404
- event.preventDefault();
2405
- const normalizedPath = normalizePath(text);
2406
- if (context.question.type === "image") {
2407
- questionImages.addPath(context.question.id, normalizedPath);
2408
- } else {
2409
- revealAttachmentArea(context.question.id);
2410
- attachments.addPath(context.question.id, normalizedPath);
2411
- }
2412
- }
3302
+ revealAttachmentArea(question.id);
3303
+ attachments.addFile(question.id, file);
2413
3304
  }
2414
3305
 
2415
3306
  function countUploadedFiles(excludingId) {
@@ -2433,13 +3324,24 @@
2433
3324
  if (question.type === "single") {
2434
3325
  const selected = formEl.querySelector(`input[name="${escapeSelector(id)}"]:checked`);
2435
3326
  if (!selected) return "";
2436
- if (selected.value === "__other__") return getOtherValue(id);
2437
- return selected.value;
3327
+ if (selected.value === "__other__") {
3328
+ const otherValue = getOtherValue(id).trim();
3329
+ return otherValue ? { option: otherValue } : "";
3330
+ }
3331
+ const note = questionCanClarifyOption(question) ? getChoiceNote(id, selected.value) : "";
3332
+ return note ? { option: selected.value, note } : { option: selected.value };
2438
3333
  }
2439
3334
  if (question.type === "multi") {
2440
3335
  return Array.from(
2441
3336
  formEl.querySelectorAll(`input[name="${escapeSelector(id)}"]:checked`)
2442
- ).map((input) => input.value === "__other__" ? getOtherValue(id) : input.value).filter(v => v);
3337
+ ).map((input) => {
3338
+ if (input.value === "__other__") {
3339
+ const otherValue = getOtherValue(id).trim();
3340
+ return otherValue ? { option: otherValue } : null;
3341
+ }
3342
+ const note = questionCanClarifyOption(question) ? getChoiceNote(id, input.value) : "";
3343
+ return note ? { option: input.value, note } : { option: input.value };
3344
+ }).filter((value) => value && value.option);
2443
3345
  }
2444
3346
  if (question.type === "text") {
2445
3347
  const textarea = formEl.querySelector(`textarea[data-question-id="${escapeSelector(id)}"]`);
@@ -2465,31 +3367,54 @@
2465
3367
  }
2466
3368
 
2467
3369
  function collectPersistedData() {
2468
- const data = {};
3370
+ const answers = {};
2469
3371
  questions.forEach((question) => {
2470
3372
  if (question.type === "info" || question.type === "image") return;
2471
- data[question.id] = getQuestionValue(question);
3373
+ answers[question.id] = getQuestionValue(question);
2472
3374
  });
2473
- return data;
3375
+ return {
3376
+ answers,
3377
+ savedOptionInsights: serializeSavedOptionInsights(),
3378
+ };
2474
3379
  }
2475
3380
 
2476
- function populateForm(saved) {
3381
+ function getSavedSingleChoiceValue(value) {
3382
+ return normalizeChoiceResponseValue(value);
3383
+ }
3384
+
3385
+ function getSavedMultiChoiceValues(value) {
3386
+ if (!Array.isArray(value)) return [];
3387
+ return value.map((item) => normalizeChoiceResponseValue(item)).filter(Boolean);
3388
+ }
3389
+
3390
+ function populateForm(saved, options = {}) {
3391
+ const { preserveChoiceNotes = false } = options;
2477
3392
  if (!saved) return;
2478
3393
  questions.forEach((question) => {
3394
+ const hasSavedValue = Object.prototype.hasOwnProperty.call(saved, question.id);
2479
3395
  const value = saved[question.id];
2480
- if (question.type === "single" && typeof value === "string") {
3396
+ if (question.type === "single") {
2481
3397
  const radios = formEl.querySelectorAll(
2482
3398
  `input[name="${escapeSelector(question.id)}"]`
2483
3399
  );
2484
3400
  radios.forEach((radio) => {
2485
3401
  radio.checked = false;
2486
3402
  });
2487
- if (value !== "") {
3403
+ if (!preserveChoiceNotes) {
3404
+ clearChoiceNotes(question.id);
3405
+ }
3406
+ if (!hasSavedValue) return;
3407
+ const choiceValue = getSavedSingleChoiceValue(value);
3408
+ if (!choiceValue) return;
3409
+ if (choiceValue.option !== "") {
2488
3410
  const input = formEl.querySelector(
2489
- `input[name="${escapeSelector(question.id)}"][value="${escapeSelector(value)}"]`
3411
+ `input[name="${escapeSelector(question.id)}"][value="${escapeSelector(choiceValue.option)}"]`
2490
3412
  );
2491
3413
  if (input) {
2492
3414
  input.checked = true;
3415
+ if (questionCanClarifyOption(question) && choiceValue.note) {
3416
+ setChoiceNote(question.id, choiceValue.option, choiceValue.note);
3417
+ }
2493
3418
  } else {
2494
3419
  const otherCheck = formEl.querySelector(
2495
3420
  `input[name="${escapeSelector(question.id)}"][value="__other__"]`
@@ -2499,28 +3424,36 @@
2499
3424
  );
2500
3425
  if (otherCheck && otherInput) {
2501
3426
  otherCheck.checked = true;
2502
- otherInput.value = value;
3427
+ otherInput.value = choiceValue.option;
2503
3428
  otherInput.dispatchEvent(new Event("input", { bubbles: true }));
2504
3429
  }
2505
3430
  }
2506
3431
  }
2507
3432
  }
2508
- if (question.type === "multi" && Array.isArray(value)) {
3433
+ if (question.type === "multi") {
2509
3434
  const checkboxes = formEl.querySelectorAll(
2510
3435
  `input[name="${escapeSelector(question.id)}"]`
2511
3436
  );
2512
3437
  checkboxes.forEach((checkbox) => {
2513
3438
  checkbox.checked = false;
2514
3439
  });
3440
+ if (!preserveChoiceNotes) {
3441
+ clearChoiceNotes(question.id);
3442
+ }
3443
+ if (!hasSavedValue) return;
3444
+ const choiceValues = getSavedMultiChoiceValues(value);
2515
3445
  let otherValue = "";
2516
- value.forEach((val) => {
3446
+ choiceValues.forEach((choiceValue) => {
2517
3447
  const input = formEl.querySelector(
2518
- `input[name="${escapeSelector(question.id)}"][value="${escapeSelector(val)}"]`
3448
+ `input[name="${escapeSelector(question.id)}"][value="${escapeSelector(choiceValue.option)}"]`
2519
3449
  );
2520
3450
  if (input) {
2521
3451
  input.checked = true;
2522
- } else if (val) {
2523
- otherValue = val;
3452
+ if (questionCanClarifyOption(question) && choiceValue.note) {
3453
+ setChoiceNote(question.id, choiceValue.option, choiceValue.note);
3454
+ }
3455
+ } else if (choiceValue.option) {
3456
+ otherValue = choiceValue.option;
2524
3457
  }
2525
3458
  });
2526
3459
  if (otherValue) {
@@ -2556,16 +3489,29 @@
2556
3489
  }
2557
3490
  }
2558
3491
 
3492
+ function rerenderChoiceQuestions() {
3493
+ questions.forEach((question) => {
3494
+ if (question.type !== "single" && question.type !== "multi") return;
3495
+ replaceQuestionOptionList(question, getQuestionValue(question));
3496
+ });
3497
+ }
3498
+
2559
3499
  function loadProgress() {
2560
3500
  if (!session.storageKey) return;
2561
3501
  let loaded = false;
2562
3502
  try {
2563
3503
  const saved = localStorage.getItem(session.storageKey);
2564
3504
  if (saved) {
2565
- populateForm(JSON.parse(saved));
3505
+ const parsed = JSON.parse(saved);
3506
+ const answers = parsed && typeof parsed === "object" && parsed.answers && typeof parsed.answers === "object"
3507
+ ? parsed.answers
3508
+ : parsed;
3509
+ populateForm(answers);
3510
+ restoreSavedOptionInsights(parsed?.savedOptionInsights);
2566
3511
  questions.forEach((q) => {
2567
3512
  if (q.type === "multi") updateDoneState(q.id);
2568
3513
  });
3514
+ rerenderChoiceQuestions();
2569
3515
  loaded = true;
2570
3516
  }
2571
3517
  } catch (_err) {
@@ -2668,6 +3614,12 @@
2668
3614
  updateDoneState(q.id);
2669
3615
  }
2670
3616
  });
3617
+ rerenderChoiceQuestions();
3618
+ }
3619
+
3620
+ function populateFromSavedOptionInsights(savedOptionInsights) {
3621
+ restoreSavedOptionInsights(savedOptionInsights);
3622
+ rerenderChoiceQuestions();
2671
3623
  }
2672
3624
 
2673
3625
  function readFileBase64(file) {
@@ -2735,6 +3687,7 @@
2735
3687
  token: sessionToken,
2736
3688
  responses: payload.responses,
2737
3689
  images: payload.images,
3690
+ savedOptionInsights: serializeSavedOptionInsights(),
2738
3691
  submitted,
2739
3692
  }),
2740
3693
  });
@@ -2840,6 +3793,7 @@
2840
3793
  function init() {
2841
3794
  initTheme();
2842
3795
  clearReloadIntent();
3796
+ normalizeOptionKeysFromData();
2843
3797
 
2844
3798
  const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
2845
3799
  const modKey = document.querySelector(".mod-key");
@@ -2874,6 +3828,9 @@
2874
3828
  // Pre-populate: savedAnswers takes precedence over localStorage
2875
3829
  if (data.savedAnswers && Array.isArray(data.savedAnswers)) {
2876
3830
  populateFromSavedAnswers(data.savedAnswers);
3831
+ if (Array.isArray(data.savedOptionInsights)) {
3832
+ populateFromSavedOptionInsights(data.savedOptionInsights);
3833
+ }
2877
3834
  initStorageKeyOnly();
2878
3835
  } else {
2879
3836
  initStorage();
@@ -2978,8 +3935,6 @@
2978
3935
  }
2979
3936
  }
2980
3937
  }, true);
2981
- document.addEventListener("paste", handlePaste);
2982
-
2983
3938
  if (timeout > 0) {
2984
3939
  startCountdownDisplay();
2985
3940
  timers.expiration = setTimeout(() => {