Files
pi-ask/src/state/result.ts
T

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)
);
}