172 lines · 5.4 KB · TypeScript
RawDownload
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<UserInputQuestion>,  draftAnswers: Record<string, PendingUserInputDraftAnswer>,): Record<string, string | string[]> | null {  const answers: Record<string, string | string[]> = {};   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<UserInputQuestion>,  draftAnswers: Record<string, PendingUserInputDraftAnswer>,): number {  return questions.reduce((count, question) => {    return resolvePendingUserInputAnswer(question, draftAnswers[question.id]) ? count + 1 : count;  }, 0);} export function findFirstUnansweredPendingUserInputQuestionIndex(  questions: ReadonlyArray<UserInputQuestion>,  draftAnswers: Record<string, PendingUserInputDraftAnswer>,): 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<UserInputQuestion>,  draftAnswers: Record<string, PendingUserInputDraftAnswer>,  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),  };}