import type { CheckInPlanTemplate, ClosingLine, PlanQuestion, QuestionSuggestion } from "../api/types";
export const MAX_OWN_QUESTIONS = 3;
export const MAX_QUESTION_LENGTH = 200;
export type Precheck = { ok: true; text: string } | { ok: false; reason: string; rule: string };
const imperative =
/^(please\s+)?(stop|start|take|keep|continue|reduce|increase|double|halve|cut|avoid|change|switch|use|don'?t|do not|never|always|come|go|call|book|drink|eat|finish|skip|hold|restart|resume)\b/i;
const request =
/\b(can|could|would|will) you (please )?(stop|start|take|keep|continue|reduce|increase|double|halve|avoid|change|switch|skip|hold|restart|resume|come)\b/i;
const advice =
/\b(you should|you must|you need to|you have to|you ought to|make sure|i want you to|i'd like you to|it is best to|it's best to)\b/i;
const reassurance = /\b(that'?s normal|nothing to worry|don'?t worry|it'?s fine|it is fine|you'?re fine|you are fine|it'?s safe|it is safe)\b/i;
const result =
/\b(your|the)\s+(results?|egfr|kidney (test|function|result)s?|potassium|inr|creatinine|blood tests?|scan|x-ray|lab(oratory)? results?)\b.*\b(is|are|was|were|shows?|showed|came back|looks?)\b/i;
const diagnosis = /\b(you (have|may have|might have|probably have|definitely have|are suffering from)|diagnos[ei]s|diagnosed)\b/i;
/**
* YCI-4 pre-check, before the server's own check: one question, under 200 characters,
* and no advice, instructions, results, diagnosis or medicine change.
*/
export function precheckQuestion(raw: string, patientFirstName: string, object = "them"): Precheck {
const text = raw.replace(/\s+/g, " ").trim();
const call = `Call ${patientFirstName} to tell ${object} this.`;
if (!text) return { ok: false, rule: "empty", reason: "Write a question for Yasmar to ask." };
if (text.length > MAX_QUESTION_LENGTH)
return {
ok: false,
rule: "too_long",
reason: `Keep it to ${MAX_QUESTION_LENGTH} characters. This one has ${text.length}.`,
};
if (imperative.test(text) || request.test(text) || advice.test(text))
return {
ok: false,
rule: "instruction",
reason: `Yasmar can ask questions but cannot give instructions. ${call}`,
};
if (reassurance.test(text))
return { ok: false, rule: "reassurance", reason: `Yasmar cannot reassure or give advice. ${call}` };
if (result.test(text))
return { ok: false, rule: "result", reason: `Yasmar cannot share or explain results. ${call}` };
if (diagnosis.test(text))
return { ok: false, rule: "diagnosis", reason: `Yasmar cannot give a diagnosis. ${call}` };
const marks = (text.match(/\?/g) ?? []).length;
if (!text.endsWith("?"))
return { ok: false, rule: "not_question", reason: "Write it as a question that ends with a question mark." };
if (marks > 1)
return { ok: false, rule: "several", reason: "Ask one thing at a time. Split this into separate questions." };
return { ok: true, text };
}
export const fromSuggestion = (s: QuestionSuggestion): PlanQuestion => ({
id: s.id,
text: s.text,
origin: "bank",
bankId: s.bankId,
reason: s.reason,
findingId: s.source.kind === "finding" ? s.source.findingId : null,
});
/** YCI-20: "I'll let Doctor Okafor know today" only if the clinician will read them today. */
export function closingAvailable(line: ClosingLine, readToday: boolean, reminder = false): boolean {
if (line.requires === "read_today") return readToday;
if (line.requires === "reminder") return reminder;
return true;
}
export type ScriptLine = {
part: "opening" | "identity" | "consent" | "feeling" | "question" | "last" | "closing" | "safety";
text: string;
reason: string | null;
editable: boolean;
};
/** YCI-2: exactly what Yasmar will say, in order. Fixed lines are marked not editable. */
export function buildScript(
template: Pick<
CheckInPlanTemplate,
"opening" | "identity" | "consent" | "feeling" | "alwaysLast" | "closings" | "safetyNet"
>,
questions: PlanQuestion[],
closingIds: string[],
readToday: boolean,
): ScriptLine[] {
const fixed = (part: ScriptLine["part"], text: string): ScriptLine => ({ part, text, reason: null, editable: false });
const closings = template.closings.filter(
(c) => (c.fixed || closingIds.includes(c.id)) && closingAvailable(c, readToday),
);
return [
...template.opening.map((t) => fixed("opening", t)),
...template.identity.map((t) => fixed("identity", t)),
fixed("consent", template.consent),
fixed("feeling", template.feeling),
...questions.map((q) => ({ part: "question" as const, text: q.text, reason: q.reason, editable: true })),
fixed("last", template.alwaysLast),
...closings.map((c) => ({ part: "closing" as const, text: c.text, reason: null, editable: !c.fixed })),
fixed("safety", template.safetyNet),
];
}
/** Move a question up or down in the clinician's order (YCI-16: Yasmar keeps this order). */
export function moveQuestion(list: PlanQuestion[], id: string, by: -1 | 1): PlanQuestion[] {
const i = list.findIndex((q) => q.id === id);
const j = i + by;
if (i < 0 || j < 0 || j >= list.length) return list;
const next = [...list];
[next[i], next[j]] = [next[j], next[i]];
return next;
}