import { Fragment, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from "react"; import { useNavigate } from "react-router-dom"; import type { Catalog, DialogButton } from "../api/types"; import { AppShell, Notice, TopBar } from "../components/AppShell"; import { HotlineCard, telHref, useHelp } from "../components/HelpSheet"; import { useToast } from "../components/Toast"; import { Icon, type IconName } from "../components/Icon"; import { useCatalog } from "../content/reference"; import { ru } from "../content/ru"; import { useDialog } from "../dialog/DialogContext"; import { classify, lines, type Step, type Survey } from "../dialog/step"; /* * Focus screen: renders the engine's current step as one screen of docs/mock — consent (A4), age (A5), * onboarding and scales (B1–B5, G4), daily check-in (D1–D6), medication (E1–E3, E5), crisis (G2), plan (G3), * versioned questionnaires and the diary (stage 1 / stage 2: C-SSRS, PHQ, TSQ, PCL-5, RUS-AUDIT, дневник). */ const PHONE_RE = /(\+7[\d\s()-]{9,}\d|\b112\b|\b10[23]\b)/g; function RichText({ text }: { text: string }) { return ( <> {text.split(PHONE_RE).map((part, i) => i % 2 === 1 ? ( {part} ) : ( {part} ), )} ); } function Body({ texts }: { texts: string[] }) { if (!texts.length) return null; return (
{texts.map((t, i) => { const ls = lines(t); return (
{ls.map((l, j) => l.list ? ( ) : (

), )}
); })}
); } function Progress({ n, total, adaptive = false }: { n: number; total: number; adaptive?: boolean }) { // Adaptive forms (C-SSRS, alcohol, diary) skip items by earlier answers: «из N» would be wrong, so no bar. if (adaptive) return (
{ru.flow.questionN(n)}
); return (
{ru.flow.question(n, total)}
); } // RUS-AUDIT item 2: the standard-drink table of the approved form (no calculator — config/scales/rus_audit.yaml). function DrinksTable({ table }: { table: NonNullable }) { return (

{ru.flow.drinks}

{ru.flow.drinksLead}

{table.header.map((h) => ( ))} {table.rows.map(([label, cells]) => ( {cells.map((c, i) => ( ))} ))}
СП {h}
{label.replace(/\s*СП$/, "")}{c}

{/* The form name stays hidden while answering: only the definition of a standard drink is shown. */} {table.title.replace(/\s*Таблица бланка RUS-AUDIT:?/, "")} {table.note}

); } function Head({ title, lead, icon }: { title: string; lead: string[]; icon?: IconName }) { return (
{icon && ( )} {title &&

{title}

}
); } function Choice({ options, busy, onSubmit, }: { options: DialogButton[]; busy: boolean; onSubmit: (b: DialogButton) => void; }) { const [picked, setPicked] = useState(null); return ( <>
{options.map((b) => { const on = picked?.data === b.data; return ( ); })}
); } function NumberScale({ options, busy, onSubmit, anchors, }: { options: DialogButton[]; busy: boolean; onSubmit: (b: DialogButton) => void; anchors?: string[]; }) { const [picked, setPicked] = useState(null); return ( <>
{picked ? picked.label : "–"}
{options.map((b) => ( ))}
{anchors && anchors.length === 2 && (
{anchors[0]} {anchors[1]}
)}
); } // Sent when a free-text step is submitted empty: a comment is never required (app/dialog/flows/meds.py). const NO_COMMENT: DialogButton = { label: ru.flow.sendEmpty, data: "text:skip" }; function FreeText({ skip, busy, onSend, onSkip, onExit, }: { skip?: DialogButton; busy: boolean; onSend: (text: string) => void; onSkip: (b: DialogButton) => void; onExit: () => void; }) { const [text, setText] = useState(""); return ( <>