import type { UserInputQuestion } from "@t3tools/contracts"; export interface PendingUserInputDraftAnswer { selectedOptionLabels?: string[]; customAnswer?: string; } export interface PendingUserInputProgress { questionIndex: number; activeQuestion: UserInputQuestion | null; activeDraft: PendingUserInputDraftAnswer | undefined; selectedOptionLabels: string[]; customAnswer: string; resolvedAnswer: string | string[] | null; usingCustomAnswer: boolean; answeredQuestionCount: number; isLastQuestion: boolean; isComplete: boolean; canAdvance: boolean; } function normalizeDraftAnswer(value: string | undefined): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function normalizeSelectedOptionLabels(value: string[] | undefined): string[] { if (!Array.isArray(value)) { return []; } const normalized: string[] = []; for (const entry of value) { if (typeof entry !== "string") continue; const trimmed = entry.trim(); if (trimmed.length > 0) { normalized.push(trimmed); } } return Array.from(new Set(normalized)); } export function resolvePendingUserInputAnswer( question: UserInputQuestion, draft: PendingUserInputDraftAnswer | undefined, ): string | string[] | null { const customAnswer = normalizeDraftAnswer(draft?.customAnswer); if (customAnswer) { return customAnswer; } const selectedOptionLabels = normalizeSelectedOptionLabels(draft?.selectedOptionLabels); if (question.multiSelect) { return selectedOptionLabels.length > 0 ? selectedOptionLabels : null; } return selectedOptionLabels[0] ?? null; } export function setPendingUserInputCustomAnswer( draft: PendingUserInputDraftAnswer | undefined, customAnswer: string, ): PendingUserInputDraftAnswer { const selectedOptionLabels = customAnswer.trim().length > 0 ? undefined : normalizeSelectedOptionLabels(draft?.selectedOptionLabels); return { customAnswer, ...(selectedOptionLabels && selectedOptionLabels.length > 0 ? { selectedOptionLabels } : {}), }; } export function togglePendingUserInputOptionSelection( question: UserInputQuestion, draft: PendingUserInputDraftAnswer | undefined, optionLabel: string, ): PendingUserInputDraftAnswer { if (question.multiSelect) { const selectedOptionLabels = normalizeSelectedOptionLabels(draft?.selectedOptionLabels); const nextSelectedOptionLabels = selectedOptionLabels.includes(optionLabel) ? selectedOptionLabels.filter((label) => label !== optionLabel) : [...selectedOptionLabels, optionLabel]; return { customAnswer: "", ...(nextSelectedOptionLabels.length > 0 ? { selectedOptionLabels: nextSelectedOptionLabels } : {}), }; } return { customAnswer: "", selectedOptionLabels: [optionLabel], }; } export function buildPendingUserInputAnswers( questions: ReadonlyArray, draftAnswers: Record, ): Record | null { const answers: Record = {}; for (const question of questions) { const answer = resolvePendingUserInputAnswer(question, draftAnswers[question.id]); if (!answer) { return null; } answers[question.id] = answer; } return answers; } export function countAnsweredPendingUserInputQuestions( questions: ReadonlyArray, draftAnswers: Record, ): number { return questions.reduce((count, question) => { return resolvePendingUserInputAnswer(question, draftAnswers[question.id]) ? count + 1 : count; }, 0); } export function findFirstUnansweredPendingUserInputQuestionIndex( questions: ReadonlyArray, draftAnswers: Record, ): number { const unansweredIndex = questions.findIndex( (question) => !resolvePendingUserInputAnswer(question, draftAnswers[question.id]), ); return unansweredIndex === -1 ? Math.max(questions.length - 1, 0) : unansweredIndex; } export function derivePendingUserInputProgress( questions: ReadonlyArray, draftAnswers: Record, questionIndex: number, ): PendingUserInputProgress { const normalizedQuestionIndex = questions.length === 0 ? 0 : Math.max(0, Math.min(questionIndex, questions.length - 1)); const activeQuestion = questions[normalizedQuestionIndex] ?? null; const activeDraft = activeQuestion ? draftAnswers[activeQuestion.id] : undefined; const resolvedAnswer = activeQuestion ? resolvePendingUserInputAnswer(activeQuestion, activeDraft) : null; const customAnswer = activeDraft?.customAnswer ?? ""; const answeredQuestionCount = countAnsweredPendingUserInputQuestions(questions, draftAnswers); const isLastQuestion = questions.length === 0 ? true : normalizedQuestionIndex >= questions.length - 1; return { questionIndex: normalizedQuestionIndex, activeQuestion, activeDraft, selectedOptionLabels: normalizeSelectedOptionLabels(activeDraft?.selectedOptionLabels), customAnswer, resolvedAnswer, usingCustomAnswer: customAnswer.trim().length > 0, answeredQuestionCount, isLastQuestion, isComplete: buildPendingUserInputAnswers(questions, draftAnswers) !== null, canAdvance: Boolean(resolvedAnswer), }; }