/**
* A questionnaire, as the decisions pane shows it.
*
* The definition is immutable and the answer is owned by the server's record,
* so this never writes to the document — an agent rewriting the plan cannot
* overwrite a decision. What the plan node carries is a projection, kept so
* the source reads correctly on its own.
*/
import { QuestionView, useQuestionnaire } from "@chopin/question/react";
import { useCellValue } from "@mdxeditor/gurx";
import { Provenance, SidecarCard } from "../card";
import { widgets$ } from "../widget-options";
import type { Transport } from "@chopin/question/react";
import type { Answer } from "@chopin/question";
import type { Questionnaire, QuestionnaireNode } from "@chopin/dialect";
/** The plan stores the chosen text; the shared view wants answer records. */
function answers(value: Questionnaire): Answer[] | undefined {
if (value.questions.some(question => question.answer === undefined)) return undefined;
return value.questions.map(question => ({
question: question.prompt,
custom: question.answer ?? "",
}));
}
/** The document calls the question text `prompt`; the domain calls it `question`. */
function definition(value: Questionnaire) {
return {
questions: value.questions.map(question => ({
id: question.id,
header: question.header,
question: question.prompt,
multiple: question.multiple,
options: question.options.map(option => ({
id: option.id,
label: option.label,
description: option.description ?? "",
})),
})),
};
}
export type QuestionnaireCardProps = {
value: Questionnaire;
wire?: Transport;
connected?: boolean;
/** How much prose each decision lives in. */
places?: { [question: string]: number };
onQuestionEnter?: (question: string) => void;
onQuestionLeave?: (question: string) => void;
/** Take the reader to that prose. Without it the shared view's jump is inert. */
onQuestionSelect?: (question: string) => void;
};
export function QuestionnaireCard(
{
connected = false,
onQuestionEnter,
onQuestionLeave,
onQuestionSelect,
places,
value,
wire,
}: QuestionnaireCardProps,
) {
let resolved = answers(value);
let pointing = { places, onQuestionEnter, onQuestionLeave, onQuestionSelect };
return resolved
?
: ;
}
type Pointing = {
places?: { [question: string]: number };
onQuestionEnter?: (question: string) => void;
onQuestionLeave?: (question: string) => void;
onQuestionSelect?: (question: string) => void;
};
function Undecided(
{ connected, value, wire, ...pointing }:
& { connected: boolean; value: Questionnaire; wire?: Transport }
& Pointing,
) {
let state = useQuestionnaire({
id: value.id,
bridge: wire,
connected,
definition: definition(value),
});
let answerable = connected && !!state.definition;
return (
);
}
function Decided(
{ resolved, value, ...pointing }: { resolved: Answer[]; value: Questionnaire } & Pointing,
) {
return (
}
>
);
}
function InlineQuestionnaire({ value }: { value: Questionnaire }) {
let options = useCellValue(widgets$);
return (
options.questions?.highlight(value.id, question)}
onQuestionLeave={() => options.questions?.clear()}
onQuestionSelect={question => options.questions?.reveal(value.id, question)}
places={options.questions?.counts(value.id)}
value={value}
wire={options.wire}
/>
);
}
export function renderQuestionnaire(node: QuestionnaireNode) {
// React renders decorators after Lexical's read transaction has ended.
return ;
}