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/README.md +5 -3
- package/form/script.js +1230 -327
- package/form/styles.css +332 -17
- package/index.ts +409 -62
- package/package.json +1 -1
- package/server.ts +609 -81
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" &&
|
|
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) =>
|
|
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
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
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
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
|
|
1683
|
-
|
|
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
|
|
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
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
2310
|
-
|
|
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
|
-
|
|
2345
|
-
}
|
|
3222
|
+
if (!attachInline?.classList.contains("hidden")) return;
|
|
3223
|
+
attachInline.classList.remove("hidden");
|
|
2346
3224
|
}
|
|
2347
3225
|
|
|
2348
|
-
async function
|
|
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 (
|
|
2361
|
-
|
|
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
|
-
|
|
2400
|
-
|
|
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__")
|
|
2437
|
-
|
|
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) =>
|
|
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
|
|
3318
|
+
const answers = {};
|
|
2469
3319
|
questions.forEach((question) => {
|
|
2470
3320
|
if (question.type === "info" || question.type === "image") return;
|
|
2471
|
-
|
|
3321
|
+
answers[question.id] = getQuestionValue(question);
|
|
2472
3322
|
});
|
|
2473
|
-
return
|
|
3323
|
+
return {
|
|
3324
|
+
answers,
|
|
3325
|
+
savedOptionInsights: serializeSavedOptionInsights(),
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
function getSavedSingleChoiceValue(value) {
|
|
3330
|
+
return normalizeChoiceResponseValue(value);
|
|
2474
3331
|
}
|
|
2475
3332
|
|
|
2476
|
-
function
|
|
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"
|
|
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 (
|
|
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(
|
|
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 =
|
|
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"
|
|
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
|
-
|
|
3394
|
+
choiceValues.forEach((choiceValue) => {
|
|
2517
3395
|
const input = formEl.querySelector(
|
|
2518
|
-
`input[name="${escapeSelector(question.id)}"][value="${escapeSelector(
|
|
3396
|
+
`input[name="${escapeSelector(question.id)}"][value="${escapeSelector(choiceValue.option)}"]`
|
|
2519
3397
|
);
|
|
2520
3398
|
if (input) {
|
|
2521
3399
|
input.checked = true;
|
|
2522
|
-
|
|
2523
|
-
|
|
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
|
-
|
|
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(() => {
|