267 lines
6.5 KiB
TypeScript
267 lines
6.5 KiB
TypeScript
import {
|
|
CANCELLED_SUMMARY,
|
|
ELABORATED_SUMMARY,
|
|
ELABORATION_INSTRUCTION,
|
|
SUBMITTED_SUMMARY,
|
|
} from "../constants/text.ts";
|
|
import { formatElaborationLines, formatResultLines } from "../result-format.ts";
|
|
import type {
|
|
AskContinuationPayload,
|
|
AskElaborationPayload,
|
|
AskResult,
|
|
AskResultAnswer,
|
|
AskState,
|
|
AskStateAnswer,
|
|
} from "../types.ts";
|
|
import {
|
|
cloneResultAnswer,
|
|
getExtraOptionNotes,
|
|
hasAnswerNotes,
|
|
isAnswerAnswered,
|
|
isAnswerEmpty,
|
|
isOptionSelected,
|
|
isResultAnswerCommitted,
|
|
isResultAnswerEmpty,
|
|
serializeAnswer,
|
|
} from "./answers.ts";
|
|
import { getQuestionOptionByValue } from "./selectors.ts";
|
|
|
|
export type ReviewAnswer = AskResult["answers"][string] & {
|
|
extraOptionNotes?: Array<{
|
|
label: string;
|
|
note: string;
|
|
}>;
|
|
};
|
|
|
|
export function toAskResult(state: AskState): AskResult {
|
|
const answers = Object.fromEntries(
|
|
Object.entries(state.answers)
|
|
.map(
|
|
([questionId, answer]) => [questionId, serializeAnswer(answer)] as const
|
|
)
|
|
.filter(([, answer]) =>
|
|
state.mode === "elaborate"
|
|
? isResultAnswerCommitted(answer)
|
|
: !isResultAnswerEmpty(answer)
|
|
)
|
|
);
|
|
|
|
return {
|
|
title: state.title,
|
|
cancelled: state.cancelled,
|
|
mode: state.mode,
|
|
questions: state.questions.map((question) => ({
|
|
id: question.id,
|
|
label: question.label,
|
|
prompt: question.prompt,
|
|
type: question.requestedType ?? question.type,
|
|
...(question.presentedType &&
|
|
question.presentedType !== question.requestedType
|
|
? { presentedType: question.presentedType }
|
|
: {}),
|
|
})),
|
|
answers,
|
|
continuation:
|
|
state.mode === "elaborate"
|
|
? serializeContinuation(state, answers)
|
|
: undefined,
|
|
elaboration:
|
|
state.mode === "elaborate" ? serializeElaboration(state) : undefined,
|
|
};
|
|
}
|
|
|
|
export function summarizeResult(result: AskResult): string {
|
|
if (result.cancelled) {
|
|
return CANCELLED_SUMMARY;
|
|
}
|
|
if (result.mode === "elaborate") {
|
|
const lines = formatElaborationLines(result, { mode: "summary" });
|
|
return lines.join("\n") || ELABORATED_SUMMARY;
|
|
}
|
|
|
|
const lines = formatResultLines(result, { mode: "summary" });
|
|
return lines.join("\n") || SUBMITTED_SUMMARY;
|
|
}
|
|
|
|
export function hasAnswerContent(state: AskState, questionId: string): boolean {
|
|
const answer = state.answers[questionId];
|
|
return !!answer && !isAnswerEmpty(answer);
|
|
}
|
|
|
|
function serializeContinuation(
|
|
state: AskState,
|
|
answers: AskResult["answers"]
|
|
): AskContinuationPayload {
|
|
const affectedQuestionIds: string[] = [];
|
|
const preservedAnswers: AskContinuationPayload["preservedAnswers"] = {};
|
|
const questionStates: AskContinuationPayload["questionStates"] = {};
|
|
|
|
for (const question of state.questions) {
|
|
const answer = state.answers[question.id];
|
|
const hasClarificationNeed = hasAnswerNotes(answer);
|
|
const committedAnswer = answers[question.id];
|
|
const answered = !!committedAnswer;
|
|
|
|
if (hasClarificationNeed) {
|
|
affectedQuestionIds.push(question.id);
|
|
questionStates[question.id] = { status: "needs_clarification" };
|
|
continue;
|
|
}
|
|
|
|
if (answered) {
|
|
preservedAnswers[question.id] = cloneResultAnswer(committedAnswer);
|
|
questionStates[question.id] = { status: "answered" };
|
|
continue;
|
|
}
|
|
|
|
questionStates[question.id] = { status: "unanswered" };
|
|
}
|
|
|
|
return {
|
|
affectedQuestionIds,
|
|
preservedAnswers,
|
|
questionStates,
|
|
strategy: "refine_only",
|
|
};
|
|
}
|
|
|
|
function serializeElaboration(state: AskState): AskElaborationPayload {
|
|
const items = state.questions.flatMap((question) =>
|
|
serializeElaborationItemsForQuestion(question, state.answers[question.id])
|
|
);
|
|
|
|
return {
|
|
instruction: ELABORATION_INSTRUCTION,
|
|
nextAction: "clarify_then_reask",
|
|
items,
|
|
};
|
|
}
|
|
|
|
function serializeElaborationItemsForQuestion(
|
|
question: AskState["questions"][number],
|
|
answer: AskStateAnswer | undefined
|
|
): AskElaborationPayload["items"] {
|
|
if (!(answer && hasAnswerNotes(answer))) {
|
|
return [];
|
|
}
|
|
|
|
const questionContext = createElaborationQuestionContext(question);
|
|
const serializedAnswer = toCommittedResultAnswer(answer);
|
|
const answered = isAnswerAnswered(answer);
|
|
const items: AskElaborationPayload["items"] = [];
|
|
|
|
if (answer.note) {
|
|
items.push({
|
|
target: { kind: "question" },
|
|
question: questionContext,
|
|
answered,
|
|
answer: serializedAnswer,
|
|
note: answer.note,
|
|
});
|
|
}
|
|
|
|
for (const [value, note] of Object.entries(answer.optionNotes ?? {})) {
|
|
const option = getQuestionOptionByValue(question, value);
|
|
if (!(option && note)) {
|
|
continue;
|
|
}
|
|
|
|
items.push({
|
|
target: {
|
|
kind: "option",
|
|
optionValue: value,
|
|
},
|
|
question: questionContext,
|
|
option: cloneOption(option),
|
|
selected: isOptionSelected(answer, value),
|
|
answered,
|
|
answer: serializedAnswer,
|
|
note,
|
|
});
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
function createElaborationQuestionContext(
|
|
question: AskState["questions"][number]
|
|
) {
|
|
return {
|
|
id: question.id,
|
|
label: question.label,
|
|
prompt: question.prompt,
|
|
type: question.requestedType ?? question.type,
|
|
...(question.presentedType &&
|
|
question.presentedType !== question.requestedType
|
|
? { presentedType: question.presentedType }
|
|
: {}),
|
|
options: question.options.map(cloneOption),
|
|
};
|
|
}
|
|
|
|
function cloneOption(option: AskState["questions"][number]["options"][number]) {
|
|
return {
|
|
value: option.value,
|
|
label: option.label,
|
|
...(option.description ? { description: option.description } : {}),
|
|
...(option.preview ? { preview: option.preview } : {}),
|
|
...(option.recommended === undefined
|
|
? {}
|
|
: { recommended: option.recommended }),
|
|
};
|
|
}
|
|
|
|
function toCommittedResultAnswer(
|
|
answer: AskStateAnswer | undefined
|
|
): AskResultAnswer | undefined {
|
|
if (!(answer && isAnswerAnswered(answer))) {
|
|
return;
|
|
}
|
|
return cloneResultAnswer(serializeAnswer(answer));
|
|
}
|
|
|
|
export function toReviewAnswer(
|
|
question: AskState["questions"][number],
|
|
answer: AskStateAnswer | undefined,
|
|
showAllNotes: boolean
|
|
): ReviewAnswer | undefined {
|
|
if (!answer) {
|
|
return;
|
|
}
|
|
|
|
const serialized = serializeAnswer(answer);
|
|
const hasCommittedAnswer = isResultAnswerCommitted(serialized);
|
|
if (!showAllNotes) {
|
|
return hasCommittedAnswer ? serialized : undefined;
|
|
}
|
|
|
|
const extraOptionNotes = getExtraOptionNotes({
|
|
answer,
|
|
questionOptions: question.options,
|
|
selectedValues: serialized.values,
|
|
});
|
|
if (
|
|
!(hasCommittedAnswer || serialized.note) &&
|
|
extraOptionNotes.length === 0
|
|
) {
|
|
return;
|
|
}
|
|
|
|
return {
|
|
...serialized,
|
|
extraOptionNotes:
|
|
extraOptionNotes.length > 0 ? extraOptionNotes : undefined,
|
|
};
|
|
}
|
|
|
|
export function shouldRenderAnswersIndividually(answer: ReviewAnswer): boolean {
|
|
if (!answer.labels.length) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
answer.labels.length > 1 ||
|
|
Boolean(answer.optionNotes && Object.keys(answer.optionNotes).length > 0)
|
|
);
|
|
}
|