pi-interview 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,931 @@
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
+ function createDefaultActiveInsight(questionId, optionKey) {
742
+ const selectedModel = askModels.some((model) => model.value === defaultAskModel)
743
+ ? defaultAskModel
744
+ : (askModels[0]?.value || null);
745
+ const parsed = parseModelValue(selectedModel);
746
+ return {
747
+ questionId,
748
+ optionKey,
749
+ prompt: "",
750
+ selectedChip: null,
751
+ loading: false,
752
+ error: "",
753
+ result: null,
754
+ advancedOpen: false,
755
+ selectedProvider: parsed.provider || getFirstProvider(),
756
+ selectedModel,
757
+ abortController: null,
758
+ };
759
+ }
760
+
761
+ function getActiveInsight(questionId, optionKey) {
762
+ const active = optionInsightState.active;
763
+ return active && active.questionId === questionId && active.optionKey === optionKey
764
+ ? active
765
+ : null;
766
+ }
767
+
768
+ function getPinnedInsights(questionId, optionKey) {
769
+ const questionInsights = optionInsightState.pinned.get(questionId) || [];
770
+ return questionInsights.filter((insight) => insight.optionKey === optionKey);
771
+ }
772
+
773
+ function normalizeSavedOptionInsights(input) {
774
+ if (!Array.isArray(input)) return [];
775
+ return input
776
+ .filter((item) => item && typeof item === "object")
777
+ .map((item) => ({
778
+ id: typeof item.id === "string" ? item.id : makeClientId("insight"),
779
+ questionId: typeof item.questionId === "string" ? item.questionId : "",
780
+ optionKey: typeof item.optionKey === "string" ? item.optionKey : "",
781
+ optionText: typeof item.optionText === "string" ? item.optionText : "",
782
+ prompt: typeof item.prompt === "string" ? item.prompt : "",
783
+ summary: typeof item.summary === "string" ? item.summary : "",
784
+ bullets: Array.isArray(item.bullets)
785
+ ? item.bullets.filter((bullet) => typeof bullet === "string" && bullet.trim()).map((bullet) => bullet.trim())
786
+ : [],
787
+ suggestedText: typeof item.suggestedText === "string" ? item.suggestedText : undefined,
788
+ modelUsed: typeof item.modelUsed === "string" ? item.modelUsed : item.modelUsed === null ? null : undefined,
789
+ createdAt: typeof item.createdAt === "string" ? item.createdAt : new Date().toISOString(),
790
+ }))
791
+ .filter((item) => item.questionId && item.optionKey && item.summary);
792
+ }
793
+
794
+ function restoreSavedOptionInsights(input) {
795
+ optionInsightState.pinned.clear();
796
+ normalizeSavedOptionInsights(input).forEach((insight) => {
797
+ const existing = optionInsightState.pinned.get(insight.questionId) || [];
798
+ existing.push(insight);
799
+ optionInsightState.pinned.set(insight.questionId, existing);
800
+ });
801
+ }
802
+
803
+ function serializeSavedOptionInsights() {
804
+ return Array.from(optionInsightState.pinned.values())
805
+ .flat()
806
+ .map((insight) => ({
807
+ id: insight.id,
808
+ questionId: insight.questionId,
809
+ optionKey: insight.optionKey,
810
+ optionText: insight.optionText,
811
+ prompt: insight.prompt,
812
+ summary: insight.summary,
813
+ bullets: Array.isArray(insight.bullets) ? [...insight.bullets] : [],
814
+ suggestedText: insight.suggestedText,
815
+ modelUsed: insight.modelUsed ?? null,
816
+ createdAt: insight.createdAt,
817
+ }));
818
+ }
819
+
820
+ function removePinnedInsight(questionId, insightId) {
821
+ const existing = optionInsightState.pinned.get(questionId) || [];
822
+ const next = existing.filter((insight) => insight.id !== insightId);
823
+ if (next.length > 0) {
824
+ optionInsightState.pinned.set(questionId, next);
825
+ } else {
826
+ optionInsightState.pinned.delete(questionId);
827
+ }
828
+ }
829
+
830
+ function updatePinnedInsightOptionText(questionId, optionKey, optionText) {
831
+ const existing = optionInsightState.pinned.get(questionId) || [];
832
+ existing.forEach((insight) => {
833
+ if (insight.optionKey === optionKey) {
834
+ insight.optionText = optionText;
835
+ }
836
+ });
837
+ }
838
+
839
+ function pruneQuestionOptionInsights(questionId) {
840
+ const validKeys = new Set(getOptionKeys(questionId));
841
+ const existing = optionInsightState.pinned.get(questionId) || [];
842
+ const next = existing.filter((insight) => validKeys.has(insight.optionKey));
843
+ if (next.length > 0) {
844
+ optionInsightState.pinned.set(questionId, next);
845
+ } else {
846
+ optionInsightState.pinned.delete(questionId);
847
+ }
848
+
849
+ const active = optionInsightState.active;
850
+ if (!active || active.questionId !== questionId || validKeys.has(active.optionKey)) {
851
+ return;
852
+ }
853
+ if (active.abortController) {
854
+ active.abortController.abort();
855
+ }
856
+ optionInsightState.active = null;
857
+ }
858
+
859
+ function closeOptionInsightPanel(questionId, optionKey) {
860
+ const active = optionInsightState.active;
861
+ if (!active) return;
862
+ if (questionId && active.questionId !== questionId) return;
863
+ if (optionKey && active.optionKey !== optionKey) return;
864
+ if (active.abortController) {
865
+ active.abortController.abort();
866
+ }
867
+ optionInsightState.active = null;
868
+ }
869
+
870
+ function openOptionInsightPanel(question, optionKey) {
871
+ if (!questionCanAskAboutOption(question)) return;
872
+ const currentValue = getQuestionValue(question);
873
+ const active = getActiveInsight(question.id, optionKey);
874
+ if (active) {
875
+ closeOptionInsightPanel(question.id, optionKey);
876
+ replaceQuestionOptionList(question, currentValue, optionKey);
877
+ return;
878
+ }
879
+
880
+ const previousActive = optionInsightState.active;
881
+ if (previousActive?.abortController) {
882
+ previousActive.abortController.abort();
883
+ }
884
+ if (previousActive && (previousActive.questionId !== question.id || previousActive.optionKey !== optionKey)) {
885
+ const previousQuestion = questions.find((item) => item.id === previousActive.questionId);
886
+ if (previousQuestion) {
887
+ const previousValue = getQuestionValue(previousQuestion);
888
+ optionInsightState.active = null;
889
+ replaceQuestionOptionList(previousQuestion, previousValue, previousActive.optionKey);
890
+ }
891
+ }
892
+
893
+ optionInsightState.active = createDefaultActiveInsight(question.id, optionKey);
894
+ replaceQuestionOptionList(question, currentValue, optionKey, { focusComposer: true });
895
+ }
896
+
897
+ function getSelectedInsightModel(activeInsight) {
898
+ if (!activeInsight) return null;
899
+ return typeof activeInsight.selectedModel === "string" && activeInsight.selectedModel
900
+ ? activeInsight.selectedModel
901
+ : defaultAskModel;
902
+ }
903
+
904
+ function getInsightModelLabel(activeInsight) {
905
+ const selectedModel = getSelectedInsightModel(activeInsight);
906
+ if (!selectedModel) return "No model selected";
907
+ const parsed = parseModelValue(selectedModel);
908
+ return `${providerLabel(parsed.provider)} / ${parsed.model}`;
909
+ }
910
+
911
+ function applyQuestionValue(question, value) {
912
+ populateForm({ [question.id]: value }, { preserveChoiceNotes: true });
913
+ if (question.type === "multi") {
914
+ updateDoneState(question.id);
915
+ }
916
+ }
917
+
918
+ function replaceQuestionOptionList(question, preserveValue, focusOptionKey, options = {}) {
919
+ const card = containerEl.querySelector(`.question-card[data-question-id="${escapeSelector(question.id)}"]`);
920
+ const currentList = card?.querySelector('.option-list');
921
+ const title = card?.querySelector('.question-title');
922
+ if (!card || !currentList || !title) return;
923
+
924
+ const nextList = createChoiceQuestionList(question, title, options);
925
+ currentList.replaceWith(nextList);
926
+ applyQuestionValue(question, preserveValue);
927
+
928
+ if (nav.cards[nav.questionIndex] === card && !nav.inSubmitArea && focusOptionKey) {
929
+ const optionIndex = getOptionIndexByKey(question.id, focusOptionKey);
930
+ if (optionIndex >= 0) {
931
+ nav.optionIndex = optionIndex;
932
+ highlightOption(card, optionIndex, false);
933
+ }
934
+ }
935
+
936
+ if (options.focusComposer) {
937
+ requestAnimationFrame(() => {
938
+ const composer = card.querySelector(`.option-insight-input[data-question-id="${escapeSelector(question.id)}"][data-option-key="${escapeSelector(focusOptionKey || "")}"]`);
939
+ composer?.focus();
940
+ });
941
+ }
942
+ }
943
+
944
+ async function runOptionAction(question, optionKey, action, text) {
945
+ const preservedValue = getQuestionValue(question);
946
+ const previousText = getOptionTextByKey(question.id, optionKey);
947
+ try {
948
+ const response = await fetch("/option-action", {
949
+ method: "POST",
950
+ headers: { "Content-Type": "application/json" },
951
+ body: JSON.stringify({ token: sessionToken, questionId: question.id, optionKey, action, text }),
952
+ });
953
+ const result = await response.json();
954
+ if (!result.ok) throw new Error(result.error || "Option action failed");
955
+
956
+ if (result.question && Array.isArray(result.question.options)) {
957
+ question.options = result.question.options;
958
+ question.recommended = result.question.recommended;
959
+ question.conviction = result.question.conviction;
960
+ }
961
+ if (Array.isArray(result.optionKeys)) {
962
+ setOptionKeys(question.id, result.optionKeys);
963
+ pruneQuestionOptionInsights(question.id);
964
+ }
965
+
966
+ if (action === "replace-text") {
967
+ const nextText = getOptionTextByKey(question.id, optionKey);
968
+ updatePinnedInsightOptionText(question.id, optionKey, nextText);
969
+ if (optionInsightState.active && optionInsightState.active.questionId === question.id && optionInsightState.active.optionKey === optionKey && optionInsightState.active.result) {
970
+ optionInsightState.active.result.suggestedText = nextText;
971
+ }
972
+ }
973
+
974
+ let nextValue = preservedValue;
975
+ if (action === "replace-text" && text) {
976
+ nextValue = renameChoiceAnswerValue(question, preservedValue, previousText, text);
977
+ }
978
+
979
+ replaceQuestionOptionList(question, nextValue, optionKey);
980
+ debounceSave();
981
+ refreshCountdown();
982
+ return true;
983
+ } catch (err) {
984
+ const active = getActiveInsight(question.id, optionKey);
985
+ if (active) {
986
+ active.error = err instanceof Error ? err.message : "Option action failed";
987
+ replaceQuestionOptionList(question, preservedValue, optionKey);
988
+ }
989
+ return false;
990
+ }
991
+ }
992
+
993
+ async function submitOptionInsight(question, optionKey) {
994
+ const active = getActiveInsight(question.id, optionKey);
995
+ if (!active) return;
996
+ const prompt = active.prompt.trim();
997
+ if (!prompt) {
998
+ active.error = "Prompt is required";
999
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey, { focusComposer: true });
1000
+ return;
1001
+ }
1002
+
1003
+ if (active.loading) {
1004
+ active.abortController?.abort();
1005
+ return;
1006
+ }
1007
+
1008
+ active.loading = true;
1009
+ active.error = "";
1010
+ active.abortController = new AbortController();
1011
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1012
+
1013
+ try {
1014
+ const modelOverride = getSelectedInsightModel(active);
1015
+ const response = await fetch("/option-insight", {
1016
+ method: "POST",
1017
+ headers: { "Content-Type": "application/json" },
1018
+ body: JSON.stringify({
1019
+ token: sessionToken,
1020
+ questionId: question.id,
1021
+ optionKey,
1022
+ prompt,
1023
+ model: modelOverride && modelOverride !== defaultAskModel ? modelOverride : null,
1024
+ }),
1025
+ signal: active.abortController.signal,
1026
+ });
1027
+ const result = await response.json();
1028
+ if (!result.ok) throw new Error(result.error || "Option insight failed");
1029
+ active.result = {
1030
+ summary: result.summary,
1031
+ bullets: Array.isArray(result.bullets) ? result.bullets : [],
1032
+ suggestedText: typeof result.suggestedText === "string" ? result.suggestedText : undefined,
1033
+ modelUsed: typeof result.modelUsed === "string" ? result.modelUsed : null,
1034
+ };
1035
+ active.error = "";
1036
+ const optionText = typeof result.optionText === "string" ? result.optionText : getOptionTextByKey(question.id, optionKey);
1037
+ updatePinnedInsightOptionText(question.id, optionKey, optionText);
1038
+ refreshCountdown();
1039
+ } catch (err) {
1040
+ if (!(err instanceof Error && err.name === "AbortError")) {
1041
+ active.error = err instanceof Error ? err.message : "Option insight failed";
1042
+ }
1043
+ } finally {
1044
+ if (optionInsightState.active && optionInsightState.active.questionId === question.id && optionInsightState.active.optionKey === optionKey) {
1045
+ optionInsightState.active.loading = false;
1046
+ optionInsightState.active.abortController = null;
1047
+ }
1048
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1049
+ }
1050
+ }
1051
+
1052
+ function pinActiveInsight(question, optionKey) {
1053
+ const active = getActiveInsight(question.id, optionKey);
1054
+ if (!active || !active.result) return;
1055
+ const optionText = getOptionTextByKey(question.id, optionKey);
1056
+ const questionInsights = optionInsightState.pinned.get(question.id) || [];
1057
+ questionInsights.push({
1058
+ id: makeClientId("insight"),
1059
+ questionId: question.id,
1060
+ optionKey,
1061
+ optionText,
1062
+ prompt: active.prompt.trim(),
1063
+ summary: active.result.summary,
1064
+ bullets: Array.isArray(active.result.bullets) ? [...active.result.bullets] : [],
1065
+ suggestedText: active.result.suggestedText,
1066
+ modelUsed: active.result.modelUsed ?? null,
1067
+ createdAt: new Date().toISOString(),
1068
+ });
1069
+ optionInsightState.pinned.set(question.id, questionInsights);
1070
+ debounceSave();
1071
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1072
+ }
1073
+
1074
+ function createPinnedInsightCard(question, optionKey, insight) {
1075
+ const card = document.createElement("div");
1076
+ card.className = "option-insight-pinned";
1077
+
1078
+ const head = document.createElement("div");
1079
+ head.className = "option-insight-pinned-head";
1080
+
1081
+ const prompt = document.createElement("div");
1082
+ prompt.className = "option-insight-pinned-prompt";
1083
+ prompt.textContent = insight.prompt;
1084
+ head.appendChild(prompt);
1085
+
1086
+ const unpin = document.createElement("button");
1087
+ unpin.type = "button";
1088
+ unpin.className = "option-insight-unpin";
1089
+ unpin.textContent = "Unpin";
1090
+ unpin.addEventListener("click", (event) => {
1091
+ event.preventDefault();
1092
+ event.stopPropagation();
1093
+ removePinnedInsight(question.id, insight.id);
1094
+ debounceSave();
1095
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1096
+ });
1097
+ head.appendChild(unpin);
1098
+ card.appendChild(head);
1099
+
1100
+ const summary = document.createElement("p");
1101
+ summary.className = "option-insight-summary pinned";
1102
+ summary.textContent = insight.summary;
1103
+ card.appendChild(summary);
1104
+
1105
+ if (Array.isArray(insight.bullets) && insight.bullets.length > 0) {
1106
+ const list = document.createElement("ul");
1107
+ list.className = "option-insight-bullets";
1108
+ insight.bullets.forEach((bullet) => {
1109
+ const item = document.createElement("li");
1110
+ item.textContent = bullet;
1111
+ list.appendChild(item);
1112
+ });
1113
+ card.appendChild(list);
1114
+ }
1115
+
1116
+ if (insight.suggestedText) {
1117
+ const suggestion = document.createElement("code");
1118
+ suggestion.className = "option-insight-suggested-text compact";
1119
+ suggestion.textContent = insight.suggestedText;
1120
+ card.appendChild(suggestion);
1121
+ }
1122
+
1123
+ if (insight.modelUsed) {
1124
+ const meta = document.createElement("div");
1125
+ meta.className = "option-insight-meta";
1126
+ meta.textContent = insight.modelUsed;
1127
+ card.appendChild(meta);
1128
+ }
1129
+
1130
+ return card;
1131
+ }
1132
+
1133
+ function createOptionInsightPanel(question, optionKey) {
1134
+ const active = getActiveInsight(question.id, optionKey);
1135
+ if (!active) return null;
1136
+
1137
+ const panel = document.createElement("div");
1138
+ panel.className = "option-insight-panel";
1139
+ panel.dataset.optionInsightFor = optionKey;
1140
+
1141
+ const chips = document.createElement("div");
1142
+ chips.className = "option-insight-chips";
1143
+ ASK_PROMPT_CHIPS.forEach((chip) => {
1144
+ const btn = document.createElement("button");
1145
+ btn.type = "button";
1146
+ btn.className = "option-insight-chip" + (active.selectedChip === chip.key ? " active" : "");
1147
+ btn.textContent = chip.label;
1148
+ btn.addEventListener("click", (event) => {
1149
+ event.preventDefault();
1150
+ event.stopPropagation();
1151
+ active.selectedChip = chip.key;
1152
+ active.prompt = chip.prompt;
1153
+ active.error = "";
1154
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey, { focusComposer: true });
1155
+ });
1156
+ chips.appendChild(btn);
1157
+ });
1158
+ panel.appendChild(chips);
1159
+
1160
+ const input = document.createElement("textarea");
1161
+ input.className = "option-insight-input";
1162
+ input.rows = 2;
1163
+ input.placeholder = "Ask why it works, where it fails, or how to rewrite it...";
1164
+ input.dataset.questionId = question.id;
1165
+ input.dataset.optionKey = optionKey;
1166
+ input.value = active.prompt;
1167
+ input.addEventListener("input", () => {
1168
+ active.prompt = input.value;
1169
+ active.selectedChip = null;
1170
+ active.error = "";
1171
+ });
1172
+ input.addEventListener("keydown", (event) => {
1173
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
1174
+ event.preventDefault();
1175
+ submitOptionInsight(question, optionKey);
1176
+ }
1177
+ });
1178
+ panel.appendChild(input);
1179
+
1180
+ const metaRow = document.createElement("div");
1181
+ metaRow.className = "option-insight-meta-row";
1182
+
1183
+ const model = document.createElement("div");
1184
+ model.className = "option-insight-model";
1185
+ model.textContent = getInsightModelLabel(active);
1186
+ metaRow.appendChild(model);
1187
+
1188
+ const advancedToggle = document.createElement("button");
1189
+ advancedToggle.type = "button";
1190
+ advancedToggle.className = "option-insight-advanced-toggle";
1191
+ advancedToggle.textContent = active.advancedOpen ? "Advanced model settings ▾" : "Advanced model settings ▸";
1192
+ advancedToggle.addEventListener("click", (event) => {
1193
+ event.preventDefault();
1194
+ event.stopPropagation();
1195
+ active.advancedOpen = !active.advancedOpen;
1196
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1197
+ });
1198
+ metaRow.appendChild(advancedToggle);
1199
+
1200
+ panel.appendChild(metaRow);
1201
+
1202
+ if (active.advancedOpen) {
1203
+ const advanced = document.createElement("div");
1204
+ advanced.className = "option-insight-advanced";
1205
+
1206
+ const providerSelect = document.createElement("select");
1207
+ providerSelect.className = "option-insight-select";
1208
+ const providers = [...new Set(askModels.map((model) => model.provider))];
1209
+ providers.forEach((provider) => {
1210
+ const option = document.createElement("option");
1211
+ option.value = provider;
1212
+ option.textContent = providerLabel(provider);
1213
+ providerSelect.appendChild(option);
1214
+ });
1215
+ providerSelect.value = active.selectedProvider || providers[0] || "";
1216
+ providerSelect.addEventListener("change", () => {
1217
+ active.selectedProvider = providerSelect.value;
1218
+ const providerModels = getModelsForProvider(active.selectedProvider);
1219
+ active.selectedModel = providerModels[0]?.value || null;
1220
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1221
+ });
1222
+ advanced.appendChild(providerSelect);
1223
+
1224
+ const modelSelect = document.createElement("select");
1225
+ modelSelect.className = "option-insight-select";
1226
+ const providerModels = getModelsForProvider(active.selectedProvider);
1227
+ providerModels.forEach((modelOption) => {
1228
+ const option = document.createElement("option");
1229
+ option.value = modelOption.value;
1230
+ option.textContent = modelOption.label;
1231
+ modelSelect.appendChild(option);
1232
+ });
1233
+ modelSelect.value = active.selectedModel || providerModels[0]?.value || "";
1234
+ modelSelect.addEventListener("change", () => {
1235
+ active.selectedModel = modelSelect.value;
1236
+ });
1237
+ advanced.appendChild(modelSelect);
1238
+
1239
+ panel.appendChild(advanced);
1240
+ }
1241
+
1242
+ const actions = document.createElement("div");
1243
+ actions.className = "option-insight-actions";
1244
+
1245
+ const askButton = document.createElement("button");
1246
+ askButton.type = "button";
1247
+ askButton.className = "option-insight-submit" + (active.loading ? " loading" : "");
1248
+ askButton.textContent = active.loading ? "Cancel" : "Ask";
1249
+ askButton.addEventListener("click", (event) => {
1250
+ event.preventDefault();
1251
+ event.stopPropagation();
1252
+ submitOptionInsight(question, optionKey);
1253
+ });
1254
+ actions.appendChild(askButton);
1255
+
1256
+ panel.appendChild(actions);
1257
+
1258
+ if (active.error) {
1259
+ const error = document.createElement("div");
1260
+ error.className = "option-insight-error";
1261
+ error.textContent = active.error;
1262
+ panel.appendChild(error);
1263
+ }
1264
+
1265
+ if (active.result) {
1266
+ const result = document.createElement("div");
1267
+ result.className = "option-insight-result";
1268
+
1269
+ const summary = document.createElement("p");
1270
+ summary.className = "option-insight-summary";
1271
+ summary.textContent = active.result.summary;
1272
+ result.appendChild(summary);
1273
+
1274
+ if (Array.isArray(active.result.bullets) && active.result.bullets.length > 0) {
1275
+ const list = document.createElement("ul");
1276
+ list.className = "option-insight-bullets";
1277
+ active.result.bullets.forEach((bullet) => {
1278
+ const item = document.createElement("li");
1279
+ item.textContent = bullet;
1280
+ list.appendChild(item);
1281
+ });
1282
+ result.appendChild(list);
1283
+ }
1284
+
1285
+ if (active.result.suggestedText) {
1286
+ const suggestionLabel = document.createElement("div");
1287
+ suggestionLabel.className = "option-insight-suggestion-label";
1288
+ suggestionLabel.textContent = "Suggested rewrite";
1289
+ result.appendChild(suggestionLabel);
1290
+
1291
+ const suggestion = document.createElement("code");
1292
+ suggestion.className = "option-insight-suggested-text";
1293
+ suggestion.textContent = active.result.suggestedText;
1294
+ result.appendChild(suggestion);
1295
+ }
1296
+
1297
+ if (active.result.modelUsed) {
1298
+ const meta = document.createElement("div");
1299
+ meta.className = "option-insight-meta";
1300
+ meta.textContent = active.result.modelUsed;
1301
+ result.appendChild(meta);
1302
+ }
1303
+
1304
+ const resultActions = document.createElement("div");
1305
+ resultActions.className = "option-insight-result-actions";
1306
+
1307
+ const pinBtn = document.createElement("button");
1308
+ pinBtn.type = "button";
1309
+ pinBtn.className = "option-insight-secondary";
1310
+ pinBtn.textContent = "Pin";
1311
+ pinBtn.addEventListener("click", (event) => {
1312
+ event.preventDefault();
1313
+ event.stopPropagation();
1314
+ pinActiveInsight(question, optionKey);
1315
+ });
1316
+ resultActions.appendChild(pinBtn);
1317
+
1318
+ const moveUpBtn = document.createElement("button");
1319
+ moveUpBtn.type = "button";
1320
+ moveUpBtn.className = "option-insight-secondary";
1321
+ moveUpBtn.textContent = "Move up";
1322
+ moveUpBtn.disabled = getOptionIndexByKey(question.id, optionKey) <= 0;
1323
+ moveUpBtn.addEventListener("click", (event) => {
1324
+ event.preventDefault();
1325
+ event.stopPropagation();
1326
+ runOptionAction(question, optionKey, "move-up");
1327
+ });
1328
+ resultActions.appendChild(moveUpBtn);
1329
+
1330
+ if (active.result.suggestedText) {
1331
+ const replaceBtn = document.createElement("button");
1332
+ replaceBtn.type = "button";
1333
+ replaceBtn.className = "option-insight-primary";
1334
+ replaceBtn.textContent = "Use rewrite";
1335
+ replaceBtn.addEventListener("click", (event) => {
1336
+ event.preventDefault();
1337
+ event.stopPropagation();
1338
+ runOptionAction(question, optionKey, "replace-text", active.result.suggestedText);
1339
+ });
1340
+ resultActions.appendChild(replaceBtn);
1341
+
1342
+ const addBtn = document.createElement("button");
1343
+ addBtn.type = "button";
1344
+ addBtn.className = "option-insight-secondary";
1345
+ addBtn.textContent = "Add rewrite as option";
1346
+ addBtn.addEventListener("click", (event) => {
1347
+ event.preventDefault();
1348
+ event.stopPropagation();
1349
+ runOptionAction(question, optionKey, "add-option", active.result.suggestedText);
1350
+ });
1351
+ resultActions.appendChild(addBtn);
1352
+ }
1353
+
1354
+ result.appendChild(resultActions);
1355
+ panel.appendChild(result);
1356
+ }
1357
+
1358
+ return panel;
1359
+ }
1360
+
1361
+ function createOptionNoteInput(question, optionLabel, isSelected) {
1362
+ if (!questionCanClarifyOption(question) || !isSelected) return null;
1363
+
1364
+ const wrap = document.createElement("div");
1365
+ wrap.className = "option-note-wrap";
1366
+
1367
+ const input = document.createElement("input");
1368
+ input.type = "text";
1369
+ input.className = "option-note-input";
1370
+ input.placeholder = "Optional clarification...";
1371
+ input.dataset.questionId = question.id;
1372
+ input.dataset.optionLabel = optionLabel;
1373
+ input.value = getChoiceNote(question.id, optionLabel);
1374
+ input.addEventListener("input", () => {
1375
+ setChoiceNote(question.id, optionLabel, input.value);
1376
+ debounceSave();
1377
+ });
1378
+ setupEdgeNavigation(input);
1379
+ wrap.appendChild(input);
1380
+
1381
+ return wrap;
1382
+ }
1383
+
1384
+ function createChoiceOptionRow(question, option, optionIndex, options = {}) {
1385
+ const optionLabel = getOptionLabel(option);
1386
+ const optionContent = isRichOption(option) ? option.content : null;
1387
+ const optionKey = getOptionKeys(question.id)[optionIndex] || null;
1388
+ const generatedSet = options.generatedKeys || new Set();
1389
+ const insightable = questionSupportsOptionInsights(question) && !!optionKey;
1390
+ const askable = questionCanAskAboutOption(question) && !!optionKey;
1391
+ const activeInsight = optionKey ? getActiveInsight(question.id, optionKey) : null;
1392
+
1393
+ const row = document.createElement("div");
1394
+ row.className = "option-row";
1395
+ if (generatedSet.has(optionKey)) {
1396
+ row.classList.add("generated");
1397
+ }
1398
+ if (activeInsight) {
1399
+ row.classList.add("ask-open");
1400
+ }
1401
+
1402
+ const main = document.createElement("div");
1403
+ main.className = "option-row-main";
1404
+
1405
+ const label = document.createElement("label");
1406
+ label.className = "option-item";
1407
+ if (optionContent) {
1408
+ label.classList.add("has-code");
1409
+ }
1410
+ const input = document.createElement("input");
1411
+ input.type = question.type === "single" ? "radio" : "checkbox";
1412
+ input.name = question.id;
1413
+ input.value = optionLabel;
1414
+ input.id = `q-${question.id}-${optionIndex}`;
1415
+
1416
+ input.addEventListener("change", () => {
1417
+ syncChoiceNotesWithSelection(question);
1418
+ debounceSave();
1419
+ if (question.type === "multi") {
1420
+ updateDoneState(question.id);
1421
+ }
1422
+ replaceQuestionOptionList(question, getQuestionValue(question), optionKey);
1423
+ });
1424
+
1425
+ const body = document.createElement("div");
1426
+ body.className = "option-item-body";
1427
+
1428
+ const text = document.createElement("span");
1429
+ text.className = "option-item-label";
1430
+ text.textContent = optionLabel;
1431
+
1432
+ const recommended = question.recommended;
1433
+ const recommendedList = Array.isArray(recommended)
1434
+ ? recommended
1435
+ : recommended
1436
+ ? [recommended]
1437
+ : [];
1438
+ const shouldPreselect = recommendedList.length > 0 && question.conviction !== "slight";
1439
+
1440
+ if (recommendedList.includes(optionLabel)) {
1441
+ const pill = document.createElement("span");
1442
+ pill.className = "recommended-pill";
1443
+ pill.textContent = "Recommended";
1444
+ text.appendChild(pill);
1445
+ if (shouldPreselect) {
1446
+ input.checked = true;
1447
+ }
1448
+ }
1449
+
1450
+ body.appendChild(text);
1451
+
1452
+ if (optionContent) {
1453
+ const contentBlockEl = renderContentBlock(optionContent);
1454
+ if (contentBlockEl) {
1455
+ body.appendChild(contentBlockEl);
1456
+ }
1457
+ }
1458
+
1459
+ label.appendChild(input);
1460
+ label.appendChild(body);
1461
+
1462
+ main.appendChild(label);
1463
+ const selectedLabels = new Set(getSelectedOptionLabels(question.id));
1464
+ const noteInput = createOptionNoteInput(question, optionLabel, input.checked || selectedLabels.has(optionLabel));
1465
+
1466
+ if (insightable && optionKey) {
1467
+ if (askable) {
1468
+ const askButton = document.createElement("button");
1469
+ askButton.type = "button";
1470
+ askButton.className = "option-ask-btn";
1471
+ askButton.textContent = activeInsight ? "Hide" : "Ask";
1472
+ askButton.addEventListener("click", (event) => {
1473
+ event.preventDefault();
1474
+ event.stopPropagation();
1475
+ openOptionInsightPanel(question, optionKey);
1476
+ });
1477
+ main.appendChild(askButton);
1478
+
1479
+ const panel = createOptionInsightPanel(question, optionKey);
1480
+ row.appendChild(main);
1481
+ if (noteInput) row.appendChild(noteInput);
1482
+ if (panel) row.appendChild(panel);
1483
+ } else {
1484
+ row.appendChild(main);
1485
+ if (noteInput) row.appendChild(noteInput);
1486
+ }
1487
+
1488
+ const pinnedInsights = getPinnedInsights(question.id, optionKey);
1489
+ if (pinnedInsights.length > 0) {
1490
+ const pinnedWrap = document.createElement("div");
1491
+ pinnedWrap.className = "option-insight-pinned-list";
1492
+ pinnedInsights.forEach((insight) => {
1493
+ pinnedWrap.appendChild(createPinnedInsightCard(question, optionKey, insight));
1494
+ });
1495
+ row.appendChild(pinnedWrap);
1496
+ }
1497
+
1498
+ return row;
1499
+ }
1500
+
1501
+ row.appendChild(main);
1502
+ if (noteInput) row.appendChild(noteInput);
1503
+ return row;
1504
+ }
1505
+
1506
+ function createChoiceQuestionList(question, title, options = {}) {
1507
+ const list = document.createElement("div");
1508
+ list.className = "option-list";
1509
+ list.setAttribute("role", question.type === "single" ? "radiogroup" : "group");
1510
+ list.setAttribute("aria-labelledby", title.id);
1511
+
1512
+ const generatedKeys = new Set(options.generatedKeys || []);
1513
+
1514
+ question.options.forEach((option, optionIndex) => {
1515
+ list.appendChild(createChoiceOptionRow(question, option, optionIndex, { generatedKeys }));
1516
+ });
1517
+
1518
+ const generateMoreEl = createGenerateMoreUI(question, list);
1519
+ if (generateMoreEl) list.appendChild(generateMoreEl);
1520
+
1521
+ const otherLabel = document.createElement("label");
1522
+ otherLabel.className = "option-item option-other";
1523
+ const otherCheck = document.createElement("input");
1524
+ otherCheck.type = question.type === "single" ? "radio" : "checkbox";
1525
+ otherCheck.name = question.id;
1526
+ otherCheck.value = "__other__";
1527
+ otherCheck.id = `q-${question.id}-other`;
1528
+ const otherInput = document.createElement("textarea");
1529
+ otherInput.className = "other-input";
1530
+ otherInput.placeholder = "Other...";
1531
+ otherInput.rows = 1;
1532
+ otherInput.dataset.questionId = question.id;
1533
+ const autoResizeOther = () => {
1534
+ otherInput.style.height = "auto";
1535
+ otherInput.style.height = otherInput.scrollHeight + "px";
1536
+ };
1537
+ otherInput.addEventListener("input", () => {
1538
+ autoResizeOther();
1539
+ if (otherInput.value && !otherCheck.checked) {
1540
+ otherCheck.checked = true;
1541
+ if (question.type === "multi") updateDoneState(question.id);
1542
+ }
1543
+ debounceSave();
1544
+ });
1545
+ otherInput.addEventListener("focus", () => {
1546
+ if (!otherCheck.checked) {
1547
+ otherCheck.checked = true;
1548
+ if (question.type === "multi") updateDoneState(question.id);
1549
+ debounceSave();
1550
+ }
1551
+ });
1552
+ otherCheck.addEventListener("change", () => {
1553
+ debounceSave();
1554
+ if (question.type === "multi") updateDoneState(question.id);
1555
+ if (otherCheck.checked) otherInput.focus();
1556
+ });
1557
+ setupEdgeNavigation(otherInput);
1558
+ otherLabel.appendChild(otherCheck);
1559
+ otherLabel.appendChild(otherInput);
1560
+ list.appendChild(otherLabel);
1561
+
1562
+ if (question.type === "multi") {
1563
+ const doneItem = document.createElement("div");
1564
+ doneItem.className = "option-item done-item disabled";
1565
+ doneItem.setAttribute("tabindex", "0");
1566
+ doneItem.dataset.doneFor = question.id;
1567
+ doneItem.innerHTML = '<span class="done-check">✓</span><span>Done</span>';
1568
+ doneItem.addEventListener("click", () => {
1569
+ if (!doneItem.classList.contains("disabled")) {
1570
+ nextQuestion();
1571
+ }
1572
+ });
1573
+ doneItem.addEventListener("keydown", (e) => {
1574
+ if ((e.key === "Enter" || e.key === " ") && !doneItem.classList.contains("disabled")) {
1575
+ e.preventDefault();
1576
+ e.stopPropagation();
1577
+ nextQuestion();
1578
+ }
1579
+ });
1580
+ list.appendChild(doneItem);
1581
+ }
1582
+
1583
+ return list;
1584
+ }
1585
+
549
1586
  function renderContentBlock(block) {
550
1587
  if (!block || !block.source) return null;
551
1588
 
@@ -1203,8 +2240,14 @@
1203
2240
  return items;
1204
2241
  }
1205
2242
 
2243
+ function getTabStopsForCard(card) {
2244
+ return Array.from(
2245
+ card.querySelectorAll('input[type="radio"], input[type="checkbox"], .option-note-input, .option-ask-btn, .file-dropzone, .image-path-input, .done-item')
2246
+ );
2247
+ }
2248
+
1206
2249
  function isPathInput(el) {
1207
- return el && (el.classList.contains('image-path-input') || el.classList.contains('attach-inline-path') || el.classList.contains('other-input'));
2250
+ 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
2251
  }
1209
2252
 
1210
2253
  function isDropzone(el) {
@@ -1260,7 +2303,7 @@
1260
2303
  function highlightOption(card, optionIndex, isKeyboard = true) {
1261
2304
  const options = getOptionsForCard(card);
1262
2305
  options.forEach((opt, i) => {
1263
- const item = isOptionInput(opt) ? opt.closest('.option-item') : opt;
2306
+ const item = isOptionInput(opt) ? (opt.closest('.option-row') || opt.closest('.option-item')) : opt;
1264
2307
  item?.classList.toggle('focused', i === optionIndex);
1265
2308
  });
1266
2309
  const current = options[optionIndex];
@@ -1272,8 +2315,31 @@
1272
2315
  }
1273
2316
  }
1274
2317
 
2318
+ function focusCardTabStop(card, target, isKeyboard = true) {
2319
+ if (!target) return;
2320
+
2321
+ clearOptionHighlight(card);
2322
+
2323
+ const row = target.closest?.('.option-row');
2324
+ const highlightTarget = row || (isOptionInput(target) ? target.closest('.option-item') : target);
2325
+ highlightTarget?.classList.add('focused');
2326
+
2327
+ const rowInput = row?.querySelector('input[type="radio"], input[type="checkbox"]');
2328
+ const options = getOptionsForCard(card);
2329
+ const navTarget = rowInput || target;
2330
+ const nextIndex = options.indexOf(navTarget);
2331
+ if (nextIndex >= 0) {
2332
+ nav.optionIndex = nextIndex;
2333
+ }
2334
+
2335
+ target.focus();
2336
+ if (isKeyboard) {
2337
+ card.classList.add('keyboard-nav');
2338
+ }
2339
+ }
2340
+
1275
2341
  function clearOptionHighlight(card) {
1276
- card.querySelectorAll('.option-item, .done-item, .file-dropzone, .image-path-input').forEach(item => {
2342
+ card.querySelectorAll('.option-row, .option-item, .done-item, .file-dropzone, .image-path-input').forEach(item => {
1277
2343
  item.classList.remove('focused');
1278
2344
  });
1279
2345
  }
@@ -1385,25 +2451,33 @@
1385
2451
  const options = getOptionsForCard(card);
1386
2452
  const textarea = card.querySelector('textarea');
1387
2453
  const isTextFocused = document.activeElement === textarea;
1388
-
2454
+ const inAskArea = document.activeElement?.closest('.option-insight-panel, .option-ask-btn, .option-insight-pinned');
2455
+ const inOptionNote = document.activeElement?.closest('.option-note-wrap');
2456
+
1389
2457
  if (event.key === 'Tab') {
1390
2458
  const inAttachArea = document.activeElement?.closest('.attach-inline');
1391
2459
  const inGenerateArea = document.activeElement?.closest('.generate-more');
1392
- if (inAttachArea || inGenerateArea) return;
2460
+ if (inAttachArea || inGenerateArea || inAskArea || inOptionNote) return;
1393
2461
 
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);
2462
+ const tabStops = getTabStopsForCard(card);
2463
+ if (tabStops.length === 0) {
2464
+ return;
1403
2465
  }
2466
+
2467
+ event.preventDefault();
2468
+
2469
+ const activeIndex = tabStops.indexOf(document.activeElement);
2470
+ const fallbackIndex = options[nav.optionIndex] ? tabStops.indexOf(options[nav.optionIndex]) : -1;
2471
+ const currentIndex = activeIndex >= 0 ? activeIndex : (fallbackIndex >= 0 ? fallbackIndex : 0);
2472
+ const nextIndex = event.shiftKey
2473
+ ? (currentIndex - 1 + tabStops.length) % tabStops.length
2474
+ : (currentIndex + 1) % tabStops.length;
2475
+ focusCardTabStop(card, tabStops[nextIndex]);
1404
2476
  return;
1405
2477
  }
1406
-
2478
+
2479
+ if (inAskArea || inOptionNote) return;
2480
+
1407
2481
  if (event.key === 'ArrowLeft') {
1408
2482
  if (isTextFocused || isPathInput(document.activeElement)) {
1409
2483
  return;
@@ -1438,16 +2512,19 @@
1438
2512
  }
1439
2513
 
1440
2514
  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();
2515
+ if (isPathInput(document.activeElement)) {
2516
+ return;
2517
+ }
2518
+ if (document.activeElement?.closest('.attach-inline')) {
2519
+ return;
2520
+ }
2521
+ if (document.activeElement?.closest('.generate-more')) {
2522
+ return;
2523
+ }
2524
+ if (document.activeElement?.closest('.option-insight-panel, .option-ask-btn')) {
2525
+ return;
2526
+ }
2527
+ event.preventDefault();
1451
2528
  const option = options[nav.optionIndex];
1452
2529
  if (option) {
1453
2530
  if (isDoneItem(option)) {
@@ -1482,6 +2559,17 @@
1482
2559
  }
1483
2560
  return;
1484
2561
  }
2562
+
2563
+ if ((event.key === 'a' || event.key === 'A') && isOptionInput(document.activeElement)) {
2564
+ event.preventDefault();
2565
+ const focusedInput = options[nav.optionIndex];
2566
+ const row = focusedInput?.closest('.option-row');
2567
+ const askButton = row?.querySelector('.option-ask-btn');
2568
+ if (askButton) {
2569
+ askButton.click();
2570
+ }
2571
+ return;
2572
+ }
1485
2573
  }
1486
2574
 
1487
2575
  if (textarea && !isTextFocused) {
@@ -1550,7 +2638,6 @@
1550
2638
 
1551
2639
  function createGenerateMoreUI(question, list) {
1552
2640
  if (!data.canGenerate) return null;
1553
- if (question.options.some(isRichOption)) return null;
1554
2641
 
1555
2642
  const container = document.createElement("div");
1556
2643
  container.className = "generate-more";
@@ -1609,15 +2696,6 @@
1609
2696
  }, timeoutMs);
1610
2697
  }
1611
2698
 
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
2699
  async function runGenerate(btn, mode) {
1622
2700
  if (generating) {
1623
2701
  if (abortController) abortController.abort();
@@ -1634,7 +2712,7 @@
1634
2712
  clearStatus();
1635
2713
 
1636
2714
  abortController = new AbortController();
1637
- const existingOptions = getExistingOptions();
2715
+ const currentValue = getQuestionValue(question);
1638
2716
 
1639
2717
  try {
1640
2718
  const response = await fetch("/generate", {
@@ -1643,7 +2721,6 @@
1643
2721
  body: JSON.stringify({
1644
2722
  token: sessionToken,
1645
2723
  questionId: question.id,
1646
- existingOptions,
1647
2724
  mode,
1648
2725
  }),
1649
2726
  signal: abortController.signal,
@@ -1651,25 +2728,23 @@
1651
2728
 
1652
2729
  const result = await response.json();
1653
2730
  if (!result.ok) throw new Error(result.error || "Generation failed");
1654
- if (!Array.isArray(result.options) || result.options.length === 0) {
2731
+ if (!Array.isArray(result.options)) {
2732
+ throw new Error("Generation returned invalid options");
2733
+ }
2734
+ if (mode === "review" && result.options.length === 0) {
1655
2735
  throw new Error("No options generated");
1656
2736
  }
2737
+ if (Array.isArray(result.optionKeys)) {
2738
+ setOptionKeys(question.id, result.optionKeys);
2739
+ pruneQuestionOptionInsights(question.id);
2740
+ }
1657
2741
 
1658
2742
  if (mode === "review") {
1659
2743
  if (typeof result.question !== "string" || !result.question.trim()) {
1660
2744
  throw new Error("No revised question returned");
1661
2745
  }
1662
2746
 
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
- }
2747
+ const revisedOptions = result.options;
1673
2748
 
1674
2749
  question.question = result.question.trim();
1675
2750
  question.options = revisedOptions;
@@ -1678,38 +2753,27 @@
1678
2753
  if (title) {
1679
2754
  title.innerHTML = renderLightMarkdown(question.question);
1680
2755
  }
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);
2756
+ const revisedLabels = new Set(revisedOptions.map((option) => getOptionLabel(option)));
2757
+ const nextValue = preserveChoiceAnswerValue(question, currentValue, revisedLabels);
2758
+ replaceQuestionOptionList(question, nextValue);
1690
2759
  debounceSave();
1691
2760
  showStatus(
1692
2761
  "Question updated and " + revisedOptions.length + " option" + (revisedOptions.length > 1 ? "s" : "") + " revised",
1693
2762
  2500,
1694
2763
  );
1695
2764
  } 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
- });
2765
+ const newOptions = result.options;
1704
2766
 
1705
2767
  if (newOptions.length === 0) {
1706
2768
  showStatus("All generated options already exist", 3000);
1707
2769
  } else {
1708
2770
  question.options = question.options.concat(newOptions);
1709
- newOptions.forEach((optionText, i) => {
1710
- const optionEl = createGeneratedOption(question, optionText, i);
1711
- list.insertBefore(optionEl, container);
2771
+ const optionKeys = getOptionKeys(question.id);
2772
+ const generatedKeys = optionKeys.slice(-newOptions.length);
2773
+ replaceQuestionOptionList(question, currentValue, generatedKeys[0] || null, {
2774
+ generatedKeys,
1712
2775
  });
2776
+ debounceSave();
1713
2777
  showStatus(
1714
2778
  newOptions.length + " option" + (newOptions.length > 1 ? "s" : "") + " added",
1715
2779
  2500,
@@ -1739,33 +2803,6 @@
1739
2803
  return container;
1740
2804
  }
1741
2805
 
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
2806
  function createQuestionCard(question, index, badgeNumber) {
1770
2807
  const card = document.createElement("section");
1771
2808
  card.className = "question-card";
@@ -1838,135 +2875,7 @@
1838
2875
  }
1839
2876
 
1840
2877
  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);
2878
+ card.appendChild(createChoiceQuestionList(question, title));
1970
2879
  }
1971
2880
 
1972
2881
  if (question.type === "text") {
@@ -2196,17 +3105,7 @@
2196
3105
  if (files && files.length > 0) {
2197
3106
  const file = files[0];
2198
3107
  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
- }
3108
+ void addDroppedImage(question, file);
2210
3109
  }
2211
3110
  });
2212
3111
 
@@ -2306,8 +3205,9 @@
2306
3205
  input.value = "";
2307
3206
  return;
2308
3207
  }
2309
- } catch (_err) {
2310
- setFieldError(questionId, "Failed to validate image.");
3208
+ } catch (err) {
3209
+ const message = err instanceof Error ? err.message : "Failed to validate image.";
3210
+ setFieldError(questionId, message);
2311
3211
  input.value = "";
2312
3212
  return;
2313
3213
  }
@@ -2315,37 +3215,15 @@
2315
3215
  manager.addFile(questionId, file);
2316
3216
  }
2317
3217
 
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
3218
  function revealAttachmentArea(questionId) {
2340
3219
  const attachInline = document.querySelector(
2341
3220
  `[data-attach-inline-for="${escapeSelector(questionId)}"]`
2342
3221
  );
2343
- if (attachInline?.classList.contains("hidden")) {
2344
- attachInline.classList.remove("hidden");
2345
- }
3222
+ if (!attachInline?.classList.contains("hidden")) return;
3223
+ attachInline.classList.remove("hidden");
2346
3224
  }
2347
3225
 
2348
- async function addPastedImage(question, file) {
3226
+ async function addDroppedImage(question, file) {
2349
3227
  if (countUploadedFiles(question.id) + 1 > MAX_IMAGES) {
2350
3228
  setFieldError(question.id, `Only ${MAX_IMAGES} images allowed.`);
2351
3229
  return;
@@ -2357,59 +3235,20 @@
2357
3235
  setFieldError(question.id, validation.error);
2358
3236
  return;
2359
3237
  }
2360
- } catch (_err) {
2361
- setFieldError(question.id, "Failed to validate image.");
3238
+ } catch (err) {
3239
+ const message = err instanceof Error ? err.message : "Failed to validate image.";
3240
+ setFieldError(question.id, message);
2362
3241
  return;
2363
3242
  }
2364
3243
 
2365
3244
  setFieldError(question.id, "");
2366
3245
  if (question.type === "image") {
2367
3246
  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
- return;
2383
- }
2384
-
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
3247
  return;
2397
3248
  }
2398
3249
 
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
- }
3250
+ revealAttachmentArea(question.id);
3251
+ attachments.addFile(question.id, file);
2413
3252
  }
2414
3253
 
2415
3254
  function countUploadedFiles(excludingId) {
@@ -2433,13 +3272,24 @@
2433
3272
  if (question.type === "single") {
2434
3273
  const selected = formEl.querySelector(`input[name="${escapeSelector(id)}"]:checked`);
2435
3274
  if (!selected) return "";
2436
- if (selected.value === "__other__") return getOtherValue(id);
2437
- return selected.value;
3275
+ if (selected.value === "__other__") {
3276
+ const otherValue = getOtherValue(id).trim();
3277
+ return otherValue ? { option: otherValue } : "";
3278
+ }
3279
+ const note = questionCanClarifyOption(question) ? getChoiceNote(id, selected.value) : "";
3280
+ return note ? { option: selected.value, note } : { option: selected.value };
2438
3281
  }
2439
3282
  if (question.type === "multi") {
2440
3283
  return Array.from(
2441
3284
  formEl.querySelectorAll(`input[name="${escapeSelector(id)}"]:checked`)
2442
- ).map((input) => input.value === "__other__" ? getOtherValue(id) : input.value).filter(v => v);
3285
+ ).map((input) => {
3286
+ if (input.value === "__other__") {
3287
+ const otherValue = getOtherValue(id).trim();
3288
+ return otherValue ? { option: otherValue } : null;
3289
+ }
3290
+ const note = questionCanClarifyOption(question) ? getChoiceNote(id, input.value) : "";
3291
+ return note ? { option: input.value, note } : { option: input.value };
3292
+ }).filter((value) => value && value.option);
2443
3293
  }
2444
3294
  if (question.type === "text") {
2445
3295
  const textarea = formEl.querySelector(`textarea[data-question-id="${escapeSelector(id)}"]`);
@@ -2465,31 +3315,54 @@
2465
3315
  }
2466
3316
 
2467
3317
  function collectPersistedData() {
2468
- const data = {};
3318
+ const answers = {};
2469
3319
  questions.forEach((question) => {
2470
3320
  if (question.type === "info" || question.type === "image") return;
2471
- data[question.id] = getQuestionValue(question);
3321
+ answers[question.id] = getQuestionValue(question);
2472
3322
  });
2473
- return data;
3323
+ return {
3324
+ answers,
3325
+ savedOptionInsights: serializeSavedOptionInsights(),
3326
+ };
3327
+ }
3328
+
3329
+ function getSavedSingleChoiceValue(value) {
3330
+ return normalizeChoiceResponseValue(value);
2474
3331
  }
2475
3332
 
2476
- function populateForm(saved) {
3333
+ function getSavedMultiChoiceValues(value) {
3334
+ if (!Array.isArray(value)) return [];
3335
+ return value.map((item) => normalizeChoiceResponseValue(item)).filter(Boolean);
3336
+ }
3337
+
3338
+ function populateForm(saved, options = {}) {
3339
+ const { preserveChoiceNotes = false } = options;
2477
3340
  if (!saved) return;
2478
3341
  questions.forEach((question) => {
3342
+ const hasSavedValue = Object.prototype.hasOwnProperty.call(saved, question.id);
2479
3343
  const value = saved[question.id];
2480
- if (question.type === "single" && typeof value === "string") {
3344
+ if (question.type === "single") {
2481
3345
  const radios = formEl.querySelectorAll(
2482
3346
  `input[name="${escapeSelector(question.id)}"]`
2483
3347
  );
2484
3348
  radios.forEach((radio) => {
2485
3349
  radio.checked = false;
2486
3350
  });
2487
- if (value !== "") {
3351
+ if (!preserveChoiceNotes) {
3352
+ clearChoiceNotes(question.id);
3353
+ }
3354
+ if (!hasSavedValue) return;
3355
+ const choiceValue = getSavedSingleChoiceValue(value);
3356
+ if (!choiceValue) return;
3357
+ if (choiceValue.option !== "") {
2488
3358
  const input = formEl.querySelector(
2489
- `input[name="${escapeSelector(question.id)}"][value="${escapeSelector(value)}"]`
3359
+ `input[name="${escapeSelector(question.id)}"][value="${escapeSelector(choiceValue.option)}"]`
2490
3360
  );
2491
3361
  if (input) {
2492
3362
  input.checked = true;
3363
+ if (questionCanClarifyOption(question) && choiceValue.note) {
3364
+ setChoiceNote(question.id, choiceValue.option, choiceValue.note);
3365
+ }
2493
3366
  } else {
2494
3367
  const otherCheck = formEl.querySelector(
2495
3368
  `input[name="${escapeSelector(question.id)}"][value="__other__"]`
@@ -2499,28 +3372,36 @@
2499
3372
  );
2500
3373
  if (otherCheck && otherInput) {
2501
3374
  otherCheck.checked = true;
2502
- otherInput.value = value;
3375
+ otherInput.value = choiceValue.option;
2503
3376
  otherInput.dispatchEvent(new Event("input", { bubbles: true }));
2504
3377
  }
2505
3378
  }
2506
3379
  }
2507
3380
  }
2508
- if (question.type === "multi" && Array.isArray(value)) {
3381
+ if (question.type === "multi") {
2509
3382
  const checkboxes = formEl.querySelectorAll(
2510
3383
  `input[name="${escapeSelector(question.id)}"]`
2511
3384
  );
2512
3385
  checkboxes.forEach((checkbox) => {
2513
3386
  checkbox.checked = false;
2514
3387
  });
3388
+ if (!preserveChoiceNotes) {
3389
+ clearChoiceNotes(question.id);
3390
+ }
3391
+ if (!hasSavedValue) return;
3392
+ const choiceValues = getSavedMultiChoiceValues(value);
2515
3393
  let otherValue = "";
2516
- value.forEach((val) => {
3394
+ choiceValues.forEach((choiceValue) => {
2517
3395
  const input = formEl.querySelector(
2518
- `input[name="${escapeSelector(question.id)}"][value="${escapeSelector(val)}"]`
3396
+ `input[name="${escapeSelector(question.id)}"][value="${escapeSelector(choiceValue.option)}"]`
2519
3397
  );
2520
3398
  if (input) {
2521
3399
  input.checked = true;
2522
- } else if (val) {
2523
- otherValue = val;
3400
+ if (questionCanClarifyOption(question) && choiceValue.note) {
3401
+ setChoiceNote(question.id, choiceValue.option, choiceValue.note);
3402
+ }
3403
+ } else if (choiceValue.option) {
3404
+ otherValue = choiceValue.option;
2524
3405
  }
2525
3406
  });
2526
3407
  if (otherValue) {
@@ -2556,16 +3437,29 @@
2556
3437
  }
2557
3438
  }
2558
3439
 
3440
+ function rerenderChoiceQuestions() {
3441
+ questions.forEach((question) => {
3442
+ if (question.type !== "single" && question.type !== "multi") return;
3443
+ replaceQuestionOptionList(question, getQuestionValue(question));
3444
+ });
3445
+ }
3446
+
2559
3447
  function loadProgress() {
2560
3448
  if (!session.storageKey) return;
2561
3449
  let loaded = false;
2562
3450
  try {
2563
3451
  const saved = localStorage.getItem(session.storageKey);
2564
3452
  if (saved) {
2565
- populateForm(JSON.parse(saved));
3453
+ const parsed = JSON.parse(saved);
3454
+ const answers = parsed && typeof parsed === "object" && parsed.answers && typeof parsed.answers === "object"
3455
+ ? parsed.answers
3456
+ : parsed;
3457
+ populateForm(answers);
3458
+ restoreSavedOptionInsights(parsed?.savedOptionInsights);
2566
3459
  questions.forEach((q) => {
2567
3460
  if (q.type === "multi") updateDoneState(q.id);
2568
3461
  });
3462
+ rerenderChoiceQuestions();
2569
3463
  loaded = true;
2570
3464
  }
2571
3465
  } catch (_err) {
@@ -2668,6 +3562,12 @@
2668
3562
  updateDoneState(q.id);
2669
3563
  }
2670
3564
  });
3565
+ rerenderChoiceQuestions();
3566
+ }
3567
+
3568
+ function populateFromSavedOptionInsights(savedOptionInsights) {
3569
+ restoreSavedOptionInsights(savedOptionInsights);
3570
+ rerenderChoiceQuestions();
2671
3571
  }
2672
3572
 
2673
3573
  function readFileBase64(file) {
@@ -2735,6 +3635,7 @@
2735
3635
  token: sessionToken,
2736
3636
  responses: payload.responses,
2737
3637
  images: payload.images,
3638
+ savedOptionInsights: serializeSavedOptionInsights(),
2738
3639
  submitted,
2739
3640
  }),
2740
3641
  });
@@ -2840,6 +3741,7 @@
2840
3741
  function init() {
2841
3742
  initTheme();
2842
3743
  clearReloadIntent();
3744
+ normalizeOptionKeysFromData();
2843
3745
 
2844
3746
  const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
2845
3747
  const modKey = document.querySelector(".mod-key");
@@ -2874,6 +3776,9 @@
2874
3776
  // Pre-populate: savedAnswers takes precedence over localStorage
2875
3777
  if (data.savedAnswers && Array.isArray(data.savedAnswers)) {
2876
3778
  populateFromSavedAnswers(data.savedAnswers);
3779
+ if (Array.isArray(data.savedOptionInsights)) {
3780
+ populateFromSavedOptionInsights(data.savedOptionInsights);
3781
+ }
2877
3782
  initStorageKeyOnly();
2878
3783
  } else {
2879
3784
  initStorage();
@@ -2978,8 +3883,6 @@
2978
3883
  }
2979
3884
  }
2980
3885
  }, true);
2981
- document.addEventListener("paste", handlePaste);
2982
-
2983
3886
  if (timeout > 0) {
2984
3887
  startCountdownDisplay();
2985
3888
  timers.expiration = setTimeout(() => {