/** * The answer side of ask_clarifying_questions: submitted payload, persisted * answers for the settled card, and the Mixpanel properties. */ import { isQuestionComplete, SOMETHING_ELSE_LABEL, type ClarifyingQuestion, type QuestionState, type SelectedAnswer, } from './questionsUtils'; /** * Completed questions only; the backend marks the rest skipped. `excludeIndex` * drops a just-skipped question, whose cleared state has not re-rendered yet. */ export function buildAnswersPayload( questions: ClarifyingQuestion[], states: Record, excludeIndex?: number, ): SelectedAnswer[] { return questions .map((question, index) => ({ question, index })) .filter(({ index }) => index !== excludeIndex) .filter(({ question, index }) => isQuestionComplete(question, states[index])) .map(({ question, index }) => { const state = states[index]; const customText = state.customText.trim(); const answer: SelectedAnswer = { question_index: index }; if (question.multi_select) { answer.selected_labels = state.answer.selected_labels ?? []; } else if (state.answer.selected_label) { answer.selected_label = state.answer.selected_label; } if (customText) answer.custom_text = customText; return answer; }); } /** Answer text for the settled summary, from persisted user_input.answers. */ export function answerTextFromSelected(answer: SelectedAnswer): string { const custom = answer.custom_text?.trim(); if (answer.selected_labels && answer.selected_labels.length > 0) { return answer.selected_labels .map((l) => (l === SOMETHING_ELSE_LABEL && custom ? custom : l)) .filter((l) => l !== SOMETHING_ELSE_LABEL || custom) .join(', '); } if (custom) return custom; if (answer.selected_label && answer.selected_label !== SOMETHING_ELSE_LABEL) return answer.selected_label; return ''; } /** Tolerant of shape drift in older persisted calls. */ export function persistedAnswers(userInput: unknown): SelectedAnswer[] { const answers = (userInput as { answers?: unknown } | null)?.answers; if (!Array.isArray(answers)) return []; return answers.filter( (a): a is SelectedAnswer => !!a && typeof a === 'object' && typeof (a as SelectedAnswer).question_index === 'number', ); } /** * Answered questions only — listing a skipped one reads as a dropped answer * (web QuestionListDisplay parity). */ export function answeredQuestionRows( questions: ClarifyingQuestion[], answers: SelectedAnswer[], ): { question: string; answerText: string }[] { const answersByQuestion = new Map(answers.map((answer) => [answer.question_index, answer])); return questions .map((question, index) => ({ question, answer: answersByQuestion.get(index) })) .filter((row) => !!row.answer) .map((row) => ({ question: row.question.question, answerText: answerTextFromSelected(row.answer!) })); } /** * Names and shapes mirror the web events so both stitch into one report; app_id * and platform are already super-properties here. */ export function buildClarificationSuggestedProperties(questions: ClarifyingQuestion[]) { return { number_of_questions: questions.length, options_per_question: questions.map((question) => question.options.length), multi_select_per_question: questions.map((question) => question.multi_select ?? false), }; } export function buildClarificationSubmittedProperties( questions: ClarifyingQuestion[], answers: SelectedAnswer[], ) { return { selected_answers: answers.filter( (answer) => (answer.selected_label && answer.selected_label !== SOMETHING_ELSE_LABEL) || (answer.selected_labels?.length ?? 0) > 0, ).length, something_else_answers: answers.filter((answer) => !!answer.custom_text).length, multi_select_questions: questions.filter((question) => question.multi_select).length, multi_select_answered: answers.filter( (answer) => questions[answer.question_index]?.multi_select && (answer.selected_labels?.length ?? 0) > 0, ).length, }; }