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