/* ========================================================= Данные клинических кейсов n:true — назначить/выбрать нужно n:false — лишнее/противопоказано (выбирать не надо) voice: pitch/rate/preferMale/breath — параметры озвучки ========================================================= */ const CASES = [ { id:"stemi", theme:"aks", themeLabel:"ОКС / инфаркт", badge:"b-aks", title:"Острый инфаркт миокарда с подъёмом ST", severity:3, patient:{avatar:"👨‍🦰", name:"Пациент А.", age:"58 лет, мужчина", ctx:"Поступил по скорой, ~2 ч от начала боли", voice:{pitch:0.85, rate:0.95, preferMale:true, breath:230, volume:1}}, complaint:"Доктор, часа два назад как прижало за грудиной. Давит сильно, будто слон на грудь сел. Отдаёт в левую руку и в челюсть. Дышать тяжело, бросило в холодный пот, стало страшно. Таблетку нитроглицерина под язык клал — не отпускает.", anamnesis:[ "Боль возникла в покое, после эмоционального стресса на работе.", "Курит 30 лет по пачке в день. Артериальная гипертензия 8 лет.", "Отец умер от инфаркта миокарда в 60 лет.", "Ранее подобных приступов не было. Регулярной терапии не получает." ], objective:{ vitals:{hr:"102", bp:"150/95", rr:"22", spo2:"93%"}, exam:{general:"Кожа бледная, влажная, холодный пот. Пациент тревожен.", heart:"Тоны приглушены, тахикардия, ритм правильный.", lungs:"В нижних отделах с обеих сторон — мелкопузырчатые незвучные хрипы.", other:"Отёков нет."} }, investigations:[ {t:"ЭКГ в 12 отведениях", n:true}, {t:"Тропонин I/T (количественно)", n:true}, {t:"Общий анализ крови", n:true}, {t:"Коагулограмма (МНО, АЧТВ, фибриноген)", n:true}, {t:"Креатинин, мочевина, электролиты, глюкоза", n:true}, {t:"Рентгенография органов грудной клетки", n:true}, {t:"Эхокардиография", n:true}, {t:"Компьютерная томография головного мозга", n:false}, {t:"Люмбальная пункция", n:false}, {t:"Фиброгастроскопия", n:false} ], medications:[ {t:"Аспирин 250–325 мг разжевать", n:true}, {t:"Ингибитор P2Y12 (тикагрелор/прасугрел) нагрузочно", n:true}, {t:"Антикоагулянт (эноксапарин / НФГ)", n:true}, {t:"Морфин при выраженном болевом синдроме", n:true}, {t:"Кислород при SpO₂ < 90%", n:true}, {t:"Нитраты при боли и АД > 100", n:true}, {t:"Бета-блокатор (после стабилизации)", n:true}, {t:"Статин в высокой дозе", n:true}, {t:"Антибиотики широкого спектра", n:false}, {t:"Нестероидные противовоспалительные (ибупрофен)", n:false} ], diagnosis:{ options:[ {t:"Острый инфаркт миокарда с подъёмом ST (STEMI), передне-перегородочный", c:true}, {t:"Острый перикардит", c:false}, {t:"Тромбоэмболия лёгочной артерии", c:false}, {t:"Расслоение аорты", c:false}, {t:"Нестабильная стенокардия", c:false} ], explanation:"Типичный ангинозный синдром, подъём ST в передних отведениях и реципрокная депрессия — STEMI. Время = гибнущий миокард, показана экстренная реперфузия." }, emergency:{ time:60, intro:"Пациент в острейшей стадии. У вас 60 секунд, чтобы оказать экстренную помощь.", actions:[ {t:"Вызвать реанимационную бригаду, активировать STEMI-маршрут (door-to-balloon ≤ 90 мин)", n:true}, {t:"Двойная антитромбоцитарная терапия (аспирин + P2Y12)", n:true}, {t:"Ввести антикоагулянт", n:true}, {t:"Обеспечить венозный доступ и мониторинг ЭКГ", n:true}, {t:"Обезболивание (морфин) + нитраты при АД > 100", n:true}, {t:"Принять решение о реперфузии (ЧКВ / тромболизис)", n:true}, {t:"Ввести антибиотики широкого спектра", n:false}, {t:"Назначить НПВС", n:false}, {t:"Выполнить люмбальную пункцию", n:false} ] }, extra:"ЭКГ: подъём ST в I, aVL, V1–V4; реципрокная депрессия в II, III, aVF. Тропонин I повышен.", protocol:"Клинический протокол МЗ РК «Острый коронарный синдром»", protocolNote:"Немедленная реперфузия: первичное ЧКВ (цель door-to-balloon ≤ 90 мин); при недоступности ЧКВ ≤ 120 мин — фибринолитическая терапия (алтеплаза/тенектеплаза), в т.ч. на догоспитальном этапе. Обязательны: ацетилсалициловая кислота + ингибитор P2Y12 (клопидогрел/тикагрелор), антикоагулянт, морфин при боли, кислород при SpO₂ < 90%.", keyPoints:[ {t:"good", x:"Время = миокард: чем раньше реперфузия, тем больше спасённой мышцы."}, {t:"bad", x:"Не откладывайте реперфузию, ожидая тропонин — ЭКГ и клиника достаточны для STEMI."}, {t:"bad", x:"НПВС противопоказаны при ОКС — усиливают риск разрыва и кровотечения."} ] }, { id:"nstemi", theme:"aks", themeLabel:"ОКС / инфаркт", badge:"b-aks", title:"Острый коронарный синдром без подъёма ST", severity:2, patient:{avatar:"👩‍🦳", name:"Пациентка Б.", age:"65 лет, женщина", ctx:"Поступила через сутки от начала симптомов", voice:{pitch:1.12, rate:0.96, preferMale:false, breath:240, volume:1}}, complaint:"Вчера ночью проснулась от боли за грудиной. Давящая, минут двадцать держалась, отдала в спину. Сегодня повторилось дважды, уже при небольшой нагрузке — просто по квартире походила. Раньше такого никогда не было. Как-то страшно, думаю, это сердце.", anamnesis:[ "Сахарный диабет 2 типа 12 лет, ожирение, гиперхолестеринемия.", "Гипертония, препараты принимает нерегулярно.", "Боли провоцируются нагрузкой, купируются в покое за 10–15 мин (прогрессирующая стенокардия).", "За последний месяц снизилась толерантность к нагрузке." ], objective:{ vitals:{hr:"88", bp:"140/85", rr:"18", spo2:"96%"}, exam:{general:"Состояние средней тяжести. Кожные покровы обычной окраски. ИМТ 32.", heart:"Тоны ритмичные, приглушены. Шумов нет.", lungs:"Дыхание везикулярное, хрипов нет.", other:"Отёков нет."} }, investigations:[ {t:"ЭКГ в 12 отведениях (повторно)", n:true}, {t:"Тропонин с оценкой динамики через 3–6 ч", n:true}, {t:"Общий анализ крови, биохимия, коагулограмма", n:true}, {t:"Глюкоза, липидограмма", n:true}, {t:"Креатинин, электролиты", n:true}, {t:"Эхокардиография", n:true}, {t:"Оценка риска по шкале GRACE", n:true}, {t:"Компьютерная томография грудной клетки с контрастом", n:false}, {t:"Люмбальная пункция", n:false}, {t:"Фиброгастроскопия", n:false} ], medications:[ {t:"Аспирин", n:true}, {t:"Ингибитор P2Y12 (тикагрелор/клопидогрел)", n:true}, {t:"Антикоагулянт (фондапаринукс/эноксапарин/НФГ)", n:true}, {t:"Бета-блокатор (при отсутствии противопоказаний)", n:true}, {t:"Статин в высокой дозе", n:true}, {t:"Нитраты при боли", n:true}, {t:"Морфин при сильной боли (осторожно)", n:true}, {t:"Тромболизис (алтеплаза)", n:false}, {t:"Нестероидные противовоспалительные", n:false}, {t:"Антибиотики", n:false} ], diagnosis:{ options:[ {t:"Острый коронарный синдром без подъёма ST (NSTEMI), высокий риск", c:true}, {t:"Стабильная стенокардия напряжения", c:false}, {t:"Костохондрит", c:false}, {t:"Гастроэзофагеальная рефлюксная болезнь", c:false}, {t:"Острый перикардит", c:false} ], explanation:"Депрессия ST + подъём тропонина + высокий риск по GRACE = NSTEMI. Тромболизис при NSTEMI НЕ показан — показана ранняя инвазивная стратегия." }, emergency:{ time:60, intro:"Пациентка с NSTEMI высокого риска. У вас 60 секунд на оказание экстренной помощи.", actions:[ {t:"Двойная антитромбоцитарная терапия + антикоагулянт", n:true}, {t:"Обеспечить мониторинг и венозный доступ", n:true}, {t:"Провести стратификацию риска (GRACE)", n:true}, {t:"Принять решение о ранней инвазивной стратегии (ЧКВ ≤ 24 ч)", n:true}, {t:"Нитраты/обезболивание при боли", n:true}, {t:"Кислород при SpO₂ < 90%", n:true}, {t:"Провести тромболизис алтеплазой", n:false}, {t:"Ввести антибиотики", n:false}, {t:"Назначить НПВС", n:false} ] }, extra:"ЭКГ: горизонтальная депрессия ST в V4–V6, инверсия T. Тропонин повышен с динамикой. GRACE — высокий риск.", protocol:"Клинический протокол МЗ РК «Острый коронарный синдром»", protocolNote:"При ОКС без подъёма ST тромболизис НЕ показан. Стратификация риска (шкала GRACE) определяет раннюю инвазивную стратегию (ЧКВ ≤ 24 ч при высоком риске). Двойная антитромбоцитарная терапия + антикоагулянт, статины, бета-блокатор при отсутствии противопоказаний.", keyPoints:[ {t:"good", x:"Женщины и диабетики часто имеют атипичную клинику — не сбрасывайте со счетов."}, {t:"bad", x:"Не ставьте «стабильную стенокардию» при недавней прогрессии симптомов — это ОКС."}, {t:"bad", x:"Тромболизис при NSTEMI не показан — это частая и опасная ошибка."} ] }, { id:"asthma", theme:"odn", themeLabel:"ОДН / астма / ХОБЛ", badge:"b-odn", title:"Острый тяжёлый приступ бронхиальной астмы", severity:3, patient:{avatar:"👩", name:"Пациентка В.", age:"24 года, женщина", ctx:"Доставлена СМП, одышка 1 час", voice:{pitch:1.25, rate:1.05, preferMale:false, breath:420, volume:0.9}}, complaint:"Не могу... выдохнуть... свистит вся грудь... началось час назад... ингалятор уже третий раз пшикала — не помогает... тяжело... страшно задохнуться.", anamnesis:[ "Бронхиальная астма с детства, базисную терапию (ИГКС) принимает нерегулярно.", "Неделю назад перенесла ОРВИ с кашлем.", "За последние дни использовала сальбутамол более 10 раз в сутки.", "Аллергия на пыльцу, домашнюю пыль." ], objective:{ vitals:{hr:"120", bp:"135/85", rr:"30", spo2:"91%"}, exam:{general:"Положение ортопноэ, опирается на руки. Речь прерывистая, отдельные фразы. Работа вспомогательной мускулатуры.", heart:"Тахикардия.", lungs:"Выдох резко удлинён, свистящие хрипы по всем полям.", other:"Втянутые межрёберные промежутки, парадоксальный пульс."} }, investigations:[ {t:"Пульсоксиметрия", n:true}, {t:"Пикфлоуметрия (пиковая скорость выдоха)", n:true}, {t:"Газометрия артериальной крови (при тяжёлом течении)", n:true}, {t:"ЭКГ (исключить аритмию/ишемию)", n:true}, {t:"Рентгенография органов грудной клетки", n:true}, {t:"Общий анализ крови", n:true}, {t:"Компьютерная томография головного мозга", n:false}, {t:"Люмбальная пункция", n:false}, {t:"Бронхоскопия", n:false}, {t:"Эхокардиография (первоочерёдно)", n:false} ], medications:[ {t:"Сальбутамол через небулайзер (повторно/непрерывно)", n:true}, {t:"Ипратропия бромид через небулайзер", n:true}, {t:"Системные ГКС (преднизолон/гидрокортизон)", n:true}, {t:"Магния сульфат в/в при тяжёлом приступе", n:true}, {t:"Кислород (до SpO₂ 94–98%)", n:true}, {t:"Седативные препараты (диазепам)", n:false}, {t:"Опиоиды (морфин)", n:false}, {t:"Бета-блокаторы (пропранолол)", n:false}, {t:"Нестероидные противовоспалительные", n:false} ], diagnosis:{ options:[ {t:"Острый тяжёлый приступ бронхиальной астмы (угроза астматического статуса)", c:true}, {t:"Паническая атака", c:false}, {t:"Обострение ХОБЛ", c:false}, {t:"Тромбоэмболия лёгочной артерии", c:false}, {t:"Инородное тело дыхательных путей", c:false} ], explanation:"Экспираторная одышка, свистящие хрипы, SpO₂ 91%, ПСВ < 50% — острый тяжёлый приступ астмы. Основа: бронходилататоры и системные ГКС." }, emergency:{ time:60, intro:"Пациентка задыхается. У вас 60 секунд оказать экстренную помощь.", actions:[ {t:"Кислород до SpO₂ 94–98%", n:true}, {t:"Сальбутамол небулайзер 5 мг, повторно", n:true}, {t:"Ипратропия бромид небулайзер", n:true}, {t:"Системные ГКС как можно раньше", n:true}, {t:"Магния сульфат в/в", n:true}, {t:"Мониторинг, подготовка к ИВЛ", n:true}, {t:"Ввести седативное (диазепам)", n:false}, {t:"Ввести морфин", n:false}, {t:"Дать бета-блокатор", n:false} ] }, extra:"ПСВ < 50% от должного. При жизнеугрожающем статусе — «немое лёгкое» (исчезновение хрипов при нарастающей ОДН).", protocol:"Клинический протокол МЗ РК «Бронхиальная астма»", protocolNote:"Острый тяжёлый приступ: кислород (SpO₂ 94–98%), β2-агонисты короткого действия (сальбутамол) через небулайзер повторно/непрерывно, ипратропия бромид, системные ГКС как можно раньше, магния сульфат в/в. Седативные и опиоиды противопоказаны (угнетение дыхания). При жизнеугрожающем статусе — ИВЛ.", keyPoints:[ {t:"bad", x:"Никогда не давайте седативные/опиоиды при астме — это угнетает дыхание и ведёт к остановке."}, {t:"good", x:"Системные ГКС работают через 4–6 ч, поэтому вводите их сразу, не ждите ухудшения."}, {t:"bad", x:"«Немое лёгкое» = плохо: хрипы исчезли из-за отсутствия резерва — предлетальное состояние."} ] }, { id:"copd", theme:"odn", themeLabel:"ОДН / астма / ХОБЛ", badge:"b-odn", title:"Обострение ХОБЛ с острой дыхательной недостаточностью", severity:2, patient:{avatar:"👴", name:"Пациент Г.", age:"68 лет, мужчина", ctx:"Направлен врачом общей практики", voice:{pitch:0.82, rate:0.9, preferMale:true, breath:300, volume:1}}, complaint:"Уже неделю кашель усилился, мокрота стала густая, жёлто-зелёная. Дышать совсем тяжело, особенно со вчера. Раньше хоть по дому ходил, а сейчас до туалета не дойти — задыхаюсь. Сплю полусидя, две подушки подкладываю. Лихорадит три дня.", anamnesis:[ "ХОБЛ диагностирована 10 лет назад. Курит 40 лет (40 пачко-лет).", "Последние 3 дня — лихорадка до 38.5 °C, гнойная мокрота.", "Целевая SpO₂ 88–92% (компенсированная гиперкапния).", "Дома — ипратропия бромид/фенотерол." ], objective:{ vitals:{hr:"110", bp:"160/90", rr:"28", spo2:"86%", temp:"38.3°C"}, exam:{general:"Состояние тяжёлое. Цианоз губ, акроцианоз.", heart:"Громкий II тон над лёгочной артерией. Тахикардия.", lungs:"Грудная клетка бочкообразная, коробочный звук. Сухие свистящие хрипы.", other:"Периферические отёки голеней — cor pulmonale."} }, investigations:[ {t:"Пульсоксиметрия", n:true}, {t:"Газометрия артериальной крови", n:true}, {t:"Рентгенография органов грудной клетки", n:true}, {t:"ЭКГ", n:true}, {t:"Общий анализ крови, СРБ, прокальцитонин", n:true}, {t:"Мокрота на флору и чувствительность", n:true}, {t:"Биохимия, электролиты", n:true}, {t:"Компьютерная томография головного мозга", n:false}, {t:"Люмбальная пункция", n:false}, {t:"Бронхоскопия", n:false} ], medications:[ {t:"Бронходилататоры небулайзер (ипратропий + фенотерол)", n:true}, {t:"Системные ГКС (преднизолон 30–40 мг)", n:true}, {t:"Антибиотики при гнойной мокроте/лихорадке", n:true}, {t:"Контролируемая О₂-терапия (цель SpO₂ 88–92%)", n:true}, {t:"NIV/BiPAP при тяжёлой гиперкапнической ОДН", n:true}, {t:"Максимальный O₂ до SpO₂ 99–100%", n:false}, {t:"Седативные препараты", n:false}, {t:"Наркотические анальгетики", n:false} ], diagnosis:{ options:[ {t:"Обострение ХОБЛ, ОДН 2 типа (гиперкапническая)", c:true}, {t:"Острый приступ бронхиальной астмы", c:false}, {t:"Отёк лёгких", c:false}, {t:"Внебольничная пневмония без ОДН", c:false}, {t:"Тромбоэмболия лёгочной артерии", c:false} ], explanation:"ХОБЛ в анамнезе, гнойная мокрота, лихорадка, SpO₂ 86%, гиперкапния по газометрии — обострение ХОБЛ с ОДН 2 типа. Кислород титровать до 88–92%." }, emergency:{ time:60, intro:"Пациент с гиперкапнической ОДН. У вас 60 секунд оказать экстренную помощь.", actions:[ {t:"Контролируемая О₂-терапия (Venturi 24–28%, цель SpO₂ 88–92%)", n:true}, {t:"Бронходилататоры через небулайзер", n:true}, {t:"Системные ГКС", n:true}, {t:"Антибиотики при признаках инфекции", n:true}, {t:"NIV/BiPAP при pH<7.35 / гиперкапнии", n:true}, {t:"Мониторинг газометрии", n:true}, {t:"Дать максимальный O₂ до SpO₂ 99–100%", n:false}, {t:"Седация для комфорта", n:false}, {t:"Сразу интубировать без попытки NIV", n:false} ] }, extra:"Газометрия: PaO₂ 52 ↓, PaCO₂ 68 ↑, pH 7.31 (дыхательный ацидоз). Гиперкапническая ОДН 2 типа.", protocol:"Клинический протокол МЗ РК «Хроническая обструктивная болезнь легких»", protocolNote:"Контролируемая оксигенотерапия (целевой SpO₂ 88–92%) — избыток O₂ вызывает CO₂-наркоз. Бронходилататоры через небулайзер, системные ГКС, антибиотики при признаках инфекции. При гиперкапнической ОДН (pH < 7.35) — неинвазивная вентиляция лёгких (NIV).", keyPoints:[ {t:"bad", x:"Не давайте 100% O₂ «на всякий случай» при ХОБЛ — это ведёт к CO₂-наркозу."}, {t:"good", x:"NIV снижает потребность в интубации и летальность при обострении ХОБЛ с гиперкапнией."}, {t:"good", x:"Целевой SpO₂ для пациента с ХОБЛ — 88–92%, а не 94–98%."} ] }, { id:"hemoshock", theme:"shock", themeLabel:"Шок и травма", badge:"b-shock", title:"Травматический геморрагический шок", severity:3, patient:{avatar:"🧑", name:"Пациент Д.", age:"30 лет, мужчина", ctx:"ДТП, доставлен бригадой СМП", voice:{pitch:0.92, rate:0.85, preferMale:true, breath:520, volume:0.7}}, complaint:"Всё кружится... холодно почему-то... пить очень хочу... ногу не чувствую... где я... ничего не вижу ясно...", anamnesis:[ "ДТП: лобовое столкновение, пациент за рулём, не пристёгнут.", "Закрытый перелом бедра, подозрение на перелом таза.", "Наружное кровотечение из раны бедра, обширная гематома.", "Время от травмы до поступления ~40 минут." ], objective:{ vitals:{hr:"130", bp:"80/40", rr:"28", spo2:"95%"}, exam:{general:"Сознание оглушение (GCS 13). Резкая бледность, холодный липкий пот.", heart:"Тоны глухие, нитевидный пульс.", lungs:"Дыхание проводится по всем полям, хрипов нет.", other:"Левое бедро деформировано, укорочено, обширная гематома. Кровь на одежде."} }, investigations:[ {t:"Группа крови, Rh, кросс-матч", n:true}, {t:"Общий анализ крови (Hb, Ht)", n:true}, {t:"Коагулограмма", n:true}, {t:"Лактат, газометрия (дефицит базы)", n:true}, {t:"Креатинин, электролиты", n:true}, {t:"Рентгенография органов грудной клетки", n:true}, {t:"УЗИ FAST (поиск внутреннего кровотечения)", n:true}, {t:"Люмбальная пункция", n:false}, {t:"Фиброгастроскопия", n:false}, {t:"ЭЭГ", n:false} ], medications:[ {t:"Кристаллоиды (солевые растворы)", n:true}, {t:"Эритроцитарная масса", n:true}, {t:"Свежезамороженная плазма", n:true}, {t:"Тромбоцитный концентрат при коагулопатии", n:true}, {t:"Транексамовая кислота", n:true}, {t:"Норадреналин временно при рефрактерном шоке", n:true}, {t:"Антикоагулянты (гепарин/эноксапарин)", n:false}, {t:"Тромболизис", n:false}, {t:"Диуретики", n:false} ], diagnosis:{ options:[ {t:"Травматический геморрагический шок III–IV степени", c:true}, {t:"Кардиогенный шок", c:false}, {t:"Септический шок", c:false}, {t:"Анафилактический шок", c:false}, {t:"Нейрогенный шок", c:false} ], explanation:"Травма, массивная кровопотеря, ЧСС 130, АД 80/40, шоковый индекс 1.6, бледность, олигурия — геморрагический шок III–IV. Приоритет — остановка кровотечения и восстановление ОЦК." }, emergency:{ time:60, intro:"Пациент в геморрагическом шоке, нестабилен. У вас 60 секунд на оказание экстренной помощи.", actions:[ {t:"Немедленная остановка наружного кровотечения (прямое давление/жгут)", n:true}, {t:"ABCDE, иммобилизация шейного отдела", n:true}, {t:"Два крупных венозных доступа", n:true}, {t:"Согревание пациента (борьба с гипотермией)", n:true}, {t:"Инфузия кристаллоидов + ранняя гемотрансфузия (1:1:1)", n:true}, {t:"Тазовый бинт при переломе таза", n:true}, {t:"Экстренный хирургический гемостаз", n:true}, {t:"Начать антикоагулянтную терапию", n:false}, {t:"Провести тромболизис", n:false} ] }, extra:"Класс шока III–IV (потеря > 30% ОЦК). Лактат повышен, дефицит базы > −6 ммоль/л. Шоковый индекс 1.6.", protocol:"Клинические протоколы МЗ РК «Шок», «Острое кровотечение»", protocolNote:"Приоритет — остановка кровотечения (damage control). ABCDE, иммобилизация. Сбалансированная инфузия кристаллоидов + ранняя гемотрансфузия (эритроциты/плазма/тромбоциты), транексамовая кислота. Борьба с триадой смерти (гипотермия, ацидоз, коагулопатия). Экстренный хирургический гемостаз. Антикоагулянты и тромболизис противопоказаны.", keyPoints:[ {t:"bad", x:"Не тратьте время на полное обследование при нестабильной гемодинамике — пациент гибнет от кровопотери."}, {t:"good", x:"Боритесь с триадой смерти: гипотермия + ацидоз + коагулопатия."}, {t:"bad", x:"Избыток кристаллоидов до гемотрансфузии усугубляет коагулопатию и гипотермию."} ] }, { id:"sepsis", theme:"shock", themeLabel:"Шок и травма", badge:"b-shock", title:"Септический шок", severity:3, patient:{avatar:"🧔", name:"Пациент Е.", age:"55 лет, мужчина", ctx:"Направлен из приёмного покоя", voice:{pitch:0.9, rate:0.92, preferMale:true, breath:340, volume:0.9}}, complaint:"Три дня назад начался озноб, температура под тридцать девять, кашель с мокротой. Со вчера стало хуже — давление упало, в себя прихожу с трудом. Раньше ноги никогда так не отекали, а сейчас задыхаюсь. Пить хочу, но тошнит.", anamnesis:[ "Сахарный диабет 2 типа, плохо компенсирован.", "Заболел как ОРВИ 5 дней назад, затем кашель с гнойной мокротой.", "Самостоятельно принимал парацетамол и «антибиотик из аптечки».", "Аллергия на пенициллин." ], objective:{ vitals:{hr:"125", bp:"85/50", rr:"26", spo2:"92%", temp:"38.9°C"}, exam:{general:"Состояние тяжёлое. Кожа горячая, пятнистая сыпь. Сознание спутано.", heart:"Тоны глухие, тахикардия. САД 85 после инфузии.", lungs:"В правой нижней доле — ослабленное дыхание, влажные хрипы, притупление.", other:"Олигурия. Капиллярное наполнение > 3 сек."} }, investigations:[ {t:"Гемокультуры (до антибиотиков!)", n:true}, {t:"Лактат", n:true}, {t:"Общий анализ крови с формулой, СРБ, прокальцитонин", n:true}, {t:"Культура мокроты и мочи", n:true}, {t:"Газометрия, коагулограмма (ДВС-скрининг)", n:true}, {t:"Креатинин, печёночные пробы, электролиты", n:true}, {t:"Рентгенография органов грудной клетки", n:true}, {t:"ЭКГ", n:true}, {t:"Люмбальная пункция", n:false}, {t:"Фиброгастроскопия", n:false} ], medications:[ {t:"Антибиотики широкого спектра в/в в 1-й час", n:true}, {t:"Кристаллоиды 30 мл/кг", n:true}, {t:"Норадреналин (вазопрессор 1-й линии)", n:true}, {t:"Вазопрессин (2-я линия)", n:true}, {t:"Инсулин (контроль гликемии)", n:true}, {t:"Гидрокортизон при рефрактерном шоке", n:true}, {t:"Глюкокортикоиды рутинно в высоких дозах", n:false}, {t:"Седативные препараты", n:false}, {t:"Диуретики в начале терапии", n:false} ], diagnosis:{ options:[ {t:"Септический шок (на фоне внебольничной пневмонии)", c:true}, {t:"Гиповолемический шок", c:false}, {t:"Кардиогенный шок", c:false}, {t:"Анафилактический шок", c:false}, {t:"Тромбоэмболия лёгочной артерии", c:false} ], explanation:"Инфекционный очаг (пневмония), лихорадка, гипотензия после инфузии, лактат 4.2, олигурия, спутанность сознания — септический шок. Выполняйте hour-1 bundle." }, emergency:{ time:60, intro:"Пациент в септическом шоке. У вас 60 секунд выполнить Sepsis hour-1 bundle.", actions:[ {t:"Измерить лактат", n:true}, {t:"Взять гемокультуры до антибиотиков", n:true}, {t:"Ввести антибиотики широкого спектра в/в в 1-й час", n:true}, {t:"Инфузия кристаллоидов 30 мл/кг", n:true}, {t:"Норадреналин при стойкой гипотензии (САД ≥ 65)", n:true}, {t:"Мониторинг диуреза, АД, лактата", n:true}, {t:"Ждать посевов 24 ч перед антибиотиками", n:false}, {t:"Только вазопрессоры без инфузии", n:false}, {t:"Только жаропонижающие и наблюдение", n:false} ] }, extra:"Лактат 4.2 ↑. Лейкоциты 22×10⁹/л, сдвиг. Креатинин повышен. Рентген: инфильтрация нижней доли справа.", protocol:"Клинический протокол МЗ РК «Сепсис и септический шок»", protocolNote:"Часовой пакет (hour-1 bundle): измерить лактат, взять гемокультуры до антибиотиков, ввести антибиотики широкого спектра в/в в 1-й час, инфузия кристаллоидов 30 мл/кг, норадреналин при стойкой гипотензии (целевое САД ≥ 65 мм рт.ст.). Контроль источника инфекции.", keyPoints:[ {t:"bad", x:"Не ждите посевов, чтобы начать антибиотики — каждый час задержки повышает летальность."}, {t:"good", x:"Норадреналин — препарат первого ряда при септическом шоке после адекватной инфузии."}, {t:"good", x:"Контроль источника инфекции так же важен, как антибиотики."} ] } ]; /* ========================================================= Озвучка (Web Speech API) — человеческий голос ========================================================= */ let voices=[]; function loadVoices(){ if("speechSynthesis" in window) voices=window.speechSynthesis.getVoices(); } if("speechSynthesis" in window){ loadVoices(); window.speechSynthesis.onvoiceschanged=loadVoices; } function pickVoice(preferMale){ const ru = voices.filter(v=>v.lang && v.lang.toLowerCase().startsWith("ru")); if(!ru.length) return null; const maleRe=/pavel|yuri|maxim|dmitri|alexander|male|мужской/i; const femaleRe=/irina|katya|milena|tatiana|anna|maria|female|женский/i; if(preferMale){ const m=ru.find(v=>maleRe.test(v.name)); if(m) return m; } else { const f=ru.find(v=>femaleRe.test(v.name)); if(f) return f; } const q=ru.find(v=>/google|yandex|microsoft|neural|premium/i.test(v.name)); if(q) return q; return ru[0]; } let speakState={playing:false, queue:[], idx:0, timer:null, btn:null, avatar:null, opts:null}; function stopSpeak(){ if("speechSynthesis" in window) window.speechSynthesis.cancel(); if(speakState.timer){ clearTimeout(speakState.timer); speakState.timer=null; } speakState.playing=false; if(speakState.btn){ speakState.btn.classList.remove("is-playing"); speakState.btn.textContent="▶ Прослушать"; } if(speakState.avatar) speakState.avatar.classList.remove("is-speaking"); speakState.queue=[]; speakState.idx=0; } function splitPhrases(text){ return text.split(/(?<=[.!?…])\s+|(?<=…)\s*/).map(s=>s.trim()).filter(Boolean); } function speakPatient(text, opts, btn, avatar){ const synth=window.speechSynthesis; if(!synth){ return; } if(speakState.playing){ stopSpeak(); return; } stopSpeak(); const phrases=splitPhrases(text); if(!phrases.length) return; speakState.queue=phrases; speakState.idx=0; speakState.btn=btn; speakState.avatar=avatar; speakState.opts=opts; speakState.playing=true; btn.classList.add("is-playing"); btn.textContent="⏹ Остановить"; if(avatar) avatar.classList.add("is-speaking"); playNext(); } function playNext(){ const synth=window.speechSynthesis; if(!speakState.playing || speakState.idx>=speakState.queue.length){ stopSpeak(); return; } const phrase=speakState.queue[speakState.idx]; const opts=speakState.opts||{}; const u=new SpeechSynthesisUtterance(phrase); u.lang="ru-RU"; const v=pickVoice(opts.preferMale); if(v) u.voice=v; u.rate=opts.rate!=null?opts.rate:0.97; u.pitch=opts.pitch!=null?opts.pitch:1; u.volume=opts.volume!=null?opts.volume:1; u.onend=()=>{ if(!speakState.playing) return; speakState.idx++; if(speakState.idx>=speakState.queue.length){ stopSpeak(); return; } speakState.timer=setTimeout(playNext, opts.breath!=null?opts.breath:230); }; u.onerror=()=>{ stopSpeak(); }; synth.speak(u); } window.addEventListener("beforeunload",()=>{ if("speechSynthesis" in window) window.speechSynthesis.cancel(); }); /* ========================================================= Геймификация: профиль, уровни, достижения, рейтинг ========================================================= */ const STORE={profile:"medigra_profile_v1", progress:"medigra_progress_v1", results:"medigra_results_v1"}; const AVATARS=["🧑‍⚕️","👨‍⚕️","👩‍⚕️","🧑‍🎓","👨‍🎓","👩‍🎓","🩺","⚕️"]; const LEVELS=[ {lvl:1,name:"Стажёр",xp:0}, {lvl:2,name:"Студент",xp:500}, {lvl:3,name:"Интерн",xp:1500}, {lvl:4,name:"Ординатор",xp:3000}, {lvl:5,name:"Врач",xp:5200}, {lvl:6,name:"Специалист",xp:8000}, {lvl:7,name:"Эксперт",xp:12000}, {lvl:8,name:"Профессор",xp:18000} ]; const ACHIEVEMENTS=[ {id:"first", name:"Первый шаг", desc:"Решить первый кейс", icon:"👣", check:s=>s.completedCases>=1}, {id:"dx100", name:"Точный диагноз", desc:"100% за шаг диагноза", icon:"🎯", check:s=>s.perfectDxCount>=1}, {id:"perfect", name:"Безупречный кейс", desc:"100% за весь кейс", icon:"💎", check:s=>s.perfectCases>=1}, {id:"noerr", name:"Чистая работа", desc:"Кейс без лишних назначений", icon:"✨", check:s=>s.noErrorCases>=1}, {id:"fast", name:"Молниеносная реакция", desc:"Завершить помощь с запасом > 30 сек", icon:"⚡", check:s=>s.fastCases>=1}, {id:"aks", name:"Мастер ОКС", desc:"Пройти все кейсы темы ОКС", icon:"❤️", check:s=>s.themeDone.aks}, {id:"odn", name:"Мастер дыхания", desc:"Пройти все кейсы темы ОДН", icon:"🫁", check:s=>s.themeDone.odn}, {id:"shock", name:"Мастер шока", desc:"Пройти все кейсы темы шок", icon:"🩸", check:s=>s.themeDone.shock}, {id:"alldx", name:"Великий диагност", desc:"Все диагнозы верны", icon:"🧠", check:s=>s.allDxCases>=6}, {id:"all", name:"Полное прохождение", desc:"Пройти все 6 кейсов", icon:"🗺️", check:s=>s.completedCases>=6}, {id:"prof", name:"Профессор", desc:"Достичь максимального уровня", icon:"👑", check:s=>s.maxLevel} ]; function load(key, def){ try{ const v=JSON.parse(localStorage.getItem(key)); return v||def; }catch(e){ return def; } } function save(key, val){ try{ localStorage.setItem(key, JSON.stringify(val)); }catch(e){} } let profile=load(STORE.profile, {name:"", avatar:"🧑‍⚕️", xp:0, achievements:[]}); let progress=load(STORE.progress, {}); let results=load(STORE.results, []); function saveAll(){ save(STORE.profile,profile); save(STORE.progress,progress); save(STORE.results,results); } function levelInfo(xp){ let cur=LEVELS[0], idx=0; for(let i=0;i=LEVELS[i].xp){ cur=LEVELS[i]; idx=i; } } const next=LEVELS[idx+1]; const into=xp-cur.xp; const span=next?(next.xp-cur.xp):1; const pct=next?Math.min(100,Math.round(into/span*100)):100; return {cur, next, idx, into, span, pct, isMax:!next}; } function starsFor(score){ return score>=85?3:score>=70?2:score>=55?1:0; } function computeStats(){ let completed=0, perfectCases=0, noErrorCases=0, fastCases=0, perfectDxCount=0, allDx=0; const themeDone={aks:false,odn:false,shock:false}; CASES.forEach(c=>{ const p=progress[c.id]; if(p&&p.completed){ completed++; if(p.stars>=3) perfectCases++; if(p.noErrors) noErrorCases++; if(p.fast) fastCases++; if(p.perfectDx) perfectDxCount++; if(p.dxCorrect) allDx++; } }); ["aks","odn","shock"].forEach(t=>{ const arr=CASES.filter(c=>c.theme===t); themeDone[t]=arr.every(c=>progress[c.id]&&progress[c.id].completed); }); return {completedCases:completed, perfectCases, noErrorCases, fastCases, perfectDxCount, allDxCases:allDx, themeDone, maxLevel:levelInfo(profile.xp).isMax}; } function countFP(items,picked){ let n=0; items.forEach((it,i)=>{ if(!it.n && picked.has(i)) n++; }); return n; } function checkAchievements(){ const stats=computeStats(); const newly=[]; ACHIEVEMENTS.forEach(a=>{ if(!profile.achievements.includes(a.id) && a.check(stats)){ profile.achievements.push(a.id); newly.push(a); } }); if(newly.length) save(STORE.profile,profile); return newly; } function awardXp(c, score){ const prev=progress[c.id]||{bestScore:0}; const delta=Math.max(0, Math.round((score-(prev.bestScore||0))*3)); if(delta>0){ profile.xp+=delta; save(STORE.profile,profile); } return delta; } function recordCase(c, score, meta){ const prev=progress[c.id]||{completed:false,bestScore:0,stars:0}; progress[c.id]={ completed:true, bestScore:Math.max(prev.bestScore||0,score), stars:Math.max(prev.stars||0,starsFor(score)), dxCorrect: prev.dxCorrect||meta.dxCorrect, perfectDx: prev.perfectDx||meta.perfectDx, noErrors: prev.noErrors||meta.noErrors, fast: prev.fast||meta.fast, played:(prev.played||0)+1 }; save(STORE.progress,progress); } /* ---- тосты ---- */ function toast(kind, title, sub){ const wrap=document.getElementById("toasts"); const el=document.createElement("div"); el.className="toast "+kind; const ico = kind==="lvl"?"⬆️": kind==="achv"?"🏅":"✨"; el.innerHTML=`
${ico}
${title}${sub}
`; wrap.appendChild(el); setTimeout(()=>{ el.style.transition="opacity .4s, transform .4s"; el.style.opacity="0"; el.style.transform="translateX(120%)"; setTimeout(()=>el.remove(),420); }, 4200); } /* ========================================================= Рендер интерфейса ========================================================= */ const $=id=>document.getElementById(id); function renderPlayerBar(){ const li=levelInfo(profile.xp); const bar=$("playerBar"); bar.innerHTML=`
${profile.avatar}
${profile.name||"Гость"} · ${li.cur.name}
Уровень ${li.cur.lvl}${li.next?` · до ${li.next.name} ${li.next.xp-profile.xp} XP`:" · максимум"}
`; } function renderHeroStats(){ const stats=computeStats(); const li=levelInfo(profile.xp); $("heroStats").innerHTML=`
${li.cur.lvl}уровень · ${li.cur.name}
${profile.xp}опыт (XP)
${stats.completedCases}/6кейсов пройдено
${profile.achievements.length}/${ACHIEVEMENTS.length}достижений
`; } function renderThemes(){ const wrap=$("themes"); const themes=[ {key:"aks", ico:"❤️", name:"ОКС и инфаркт", desc:"STEMI, NSTEMI — диагностика и реперфузия.", cls:"t-aks"}, {key:"odn", ico:"🫁", name:"ОДН · астма · ХОБЛ", desc:"Астматический статус, обострение ХОБЛ.", cls:"t-odn"}, {key:"shock", ico:"🩸", name:"Шок и травма", desc:"Геморрагический и септический шок.", cls:"t-shock"} ]; wrap.innerHTML=themes.map(t=>{ const arr=CASES.filter(c=>c.theme===t.key); const done=arr.filter(c=>progress[c.id]&&progress[c.id].completed).length; const pct=Math.round(done/arr.length*100); const stars=arr.reduce((a,c)=>a+((progress[c.id]&&progress[c.id].stars)||0),0); const maxStars=arr.length*3; return `
${t.ico}

${t.name}

${t.desc}

${done}/${arr.length} кейсов${"★".repeat(stars)}${"☆".repeat(maxStars-stars)}
`; }).join(""); wrap.querySelectorAll(".theme").forEach(el=>el.addEventListener("click",()=>{ setFilter(el.dataset.theme); document.getElementById("cases").scrollIntoView({behavior:"smooth"}); })); } function sevDots(n){let s="";for(let i=0;i<3;i++)s+=``;return s;} function renderCards(filter="all"){ const grid=$("caseGrid"); grid.innerHTML=""; CASES.filter(c=>filter==="all"||c.theme===filter).forEach(c=>{ const p=progress[c.id]; const done=p&&p.completed; const stars=done?p.stars:0; const starsHTML=[0,1,2].map(i=>i★`).join(""); const el=document.createElement("article"); el.className=`case-card t-${c.theme}`; el.innerHTML=`
${c.patient.avatar}

${c.title}

${c.patient.name}, ${c.patient.age.split(",")[0]}

Говорящий пациент. Назначьте обследования и лечение, поставьте диагноз, окажите экстренную помощь на время.

${starsHTML}
${done?`Лучший результат: ${p.bestScore}% · сыграно ${p.played||1}`:"Ещё не пройден"}
${done?'✓ Пройден':''}
`; el.querySelector(".case-btn").addEventListener("click",e=>{e.stopPropagation(); startExam("single",c.id);}); grid.appendChild(el); }); } function setFilter(filter){ document.querySelectorAll(".filter").forEach(f=>f.classList.toggle("is-active", f.dataset.filter===filter)); renderCards(filter); } function renderAchv(){ const stats=computeStats(); const grid=$("achvGrid"); grid.innerHTML=ACHIEVEMENTS.map(a=>{ const unlocked=profile.achievements.includes(a.id); return `
${unlocked?"":'🔒'}
${a.icon}

${a.name}

${a.desc}

`; }).join(""); $("achvCount").textContent=`Открыто ${profile.achievements.length} из ${ACHIEVEMENTS.length}.`; } function renderBoard(){ const wrap=$("boardList"); const sorted=[...results].sort((a,b)=>b.score-a.score).slice(0,10); if(!sorted.length){ wrap.innerHTML=`
Пока нет результатов. Пройдите полный экзамен, чтобы попасть в рейтинг.
`; return; } wrap.innerHTML=sorted.map((r,i)=>{ const me = r.name===profile.name && r.avatar===profile.avatar; return `
${i+1}
${r.avatar}
${r.name}${r.date}
${r.score}%
`; }).join(""); } function refreshAll(){ renderPlayerBar(); renderHeroStats(); renderThemes(); renderCards(currentFilter); renderAchv(); renderBoard(); } let currentFilter="all"; /* ========================================================= Профиль (модал) ========================================================= */ let profOnDone=null; function openProfile(onDone){ profOnDone=onDone||null; $("profName").value=profile.name||""; const av=$("profAvatars"); av.innerHTML=AVATARS.map(a=>`
${a}
`).join(""); av.querySelectorAll(".pm-av").forEach(e=>e.addEventListener("click",()=>{ av.querySelectorAll(".pm-av").forEach(x=>x.classList.remove("sel")); e.classList.add("sel"); })); $("modalProfile").hidden=false; } function closeProfile(){ $("modalProfile").hidden=true; if(profOnDone){ const cb=profOnDone; profOnDone=null; cb(); } } $("profSave").addEventListener("click",()=>{ profile.name=($("profName").value||"").trim().slice(0,20)||"Гость"; const sel=document.querySelector(".pm-av.sel"); if(sel) profile.avatar=sel.dataset.a; save(STORE.profile,profile); renderPlayerBar(); renderHeroStats(); renderBoard(); closeProfile(); }); $("modalProfile").addEventListener("click",e=>{ if(e.target.hasAttribute("data-close-p")){ $("modalProfile").hidden=true; } }); $("btnProfile").addEventListener("click",()=>openProfile()); $("btnAchv").addEventListener("click",()=>document.getElementById("achv").scrollIntoView({behavior:"smooth"})); $("btnBoard").addEventListener("click",()=>document.getElementById("board").scrollIntoView({behavior:"smooth"})); /* ========================================================= Экзамен (полноэкранный wizard) ========================================================= */ const exam=$("exam"), examBody=$("examBody"); const STAGES=[ {key:"patient", label:"Пациент"}, {key:"investigations", label:"Обследование"}, {key:"medications", label:"Лечение"}, {key:"diagnosis", label:"Диагноз"}, {key:"emergency", label:"Помощь ⏱"}, {key:"result", label:"Результат"} ]; let state=null, timerHandle=null; function startExam(mode, caseId){ const queue = mode==="full" ? CASES.map(c=>c.id) : [caseId]; state={mode, queue, qIdx:0, stage:0, answers:{}, caseScores:[], emergencyLeft:0, emergencyManual:false}; exam.hidden=false; document.body.style.overflow="hidden"; loadCase(); } function closeExam(){ stopSpeak(); stopTimer(); exam.hidden=true; document.body.style.overflow=""; refreshAll(); state=null; } function curCase(){ return CASES.find(c=>c.id===state.queue[state.qIdx]); } function loadCase(){ stopTimer(); state.stage=0; state.answers={investigations:new Set(), medications:new Set(), diagnosis:null, emergency:new Set(), submitted:{i:false,m:false,d:false,e:false}}; state.emergencyLeft=0; state.emergencyManual=false; stopSpeak(); renderExamBar(); renderStageTrack(); renderPatient(); } function renderExamBar(){ $("examProgressLabel").textContent=`Кейс ${state.qIdx+1} из ${state.queue.length}`; $("examDots").innerHTML=state.queue.map((_,i)=>{ const done=i`; }).join(""); } function renderStageTrack(){ $("stageTrack").innerHTML=STAGES.map((s,i)=>{ let cls=""; if(i${mark}${s.label}${i→':""}`; }).join(""); } function ring(pct, size, stroke, color, track, num){ const r=(size-stroke)/2, c=2*Math.PI*r, off=c*(1-Math.max(0,Math.min(100,pct))/100); return `
${num}
`; } function patientBlock(c){ return `
${c.patient.avatar}

${c.title}

${c.patient.name} · ${c.patient.age} · ${c.patient.ctx}
${c.themeLabel}
🔊 Жалобы пациентаВключите звук и нажмите — пациент говорит голосом
«${c.complaint}»
`; } function vitalsHTML(v){ let extra=""; if(v.temp) extra=`
${v.temp}Температура
`; return `
${v.hr}ЧСС
${v.bp}АД
${v.rr}ЧДД
${v.spo2}SpO₂
${extra}
`; } function bindSpeak(c, root){ const btn=root.querySelector("#speakBtn"), av=root.querySelector("#pAvatar"); if(btn) btn.addEventListener("click",()=>speakPatient(c.complaint, c.patient.voice, btn, av)); } function renderPatient(){ const c=curCase(); examBody.innerHTML=`
${patientBlock(c)}
📋 Анамнез
    ${c.anamnesis.map(a=>`
  • ${a}
  • `).join("")}
🩺 Объективный осмотр
${vitalsHTML(c.objective.vitals)}

Общий осмотр

${c.objective.exam.general}

Сердечно-сосудистая система

${c.objective.exam.heart}

Дыхательная система

${c.objective.exam.lungs}

Прочее

${c.objective.exam.other}

Знакомство с пациентом
`; bindSpeak(c, examBody); $("goNext").onclick=()=>{ state.stage=1; renderStageTrack(); renderMulti("investigations"); }; examBody.scrollTop=0; } const MULTI_CFG={ investigations:{tag:"Шаг 1 из 4", title:"Назначьте обследования", sub:"Выберите необходимые исследования. Не назначайте лишнее — за ошибки снижается балл.", btn:"Подтвердить назначения"}, medications:{tag:"Шаг 2 из 4", title:"Назначьте лечение", sub:"Выберите препараты из перечня. Избегайте противопоказанных и неэффективных.", btn:"Подтвердить лечение"}, emergency:{tag:"Шаг 4 из 4", title:"Экстренная помощь ⏱", sub:"", btn:"Завершить помощь"} }; function renderMulti(kind){ const c=curCase(); const cfg=MULTI_CFG[kind]; const items = kind==="investigations"?c.investigations : kind==="medications"?c.medications : c.emergency.actions; const ans=state.answers[kind]; const submitted = kind==="investigations"?state.answers.submitted.i : kind==="medications"?state.answers.submitted.m : state.answers.submitted.e; let emergencyTop=""; if(kind==="emergency" && !submitted) emergencyTop=`
`; examBody.innerHTML=`
${cfg.tag}

${cfg.title}

${kind==="emergency"?c.emergency.intro:cfg.sub}

${emergencyTop}
${items.map((it,i)=>{ const checked=ans.has(i)?"checked":""; return ``; }).join("")}
${submitted?`Балл за шаг: `:"Выберите варианты"}
`; const optList=examBody.querySelector("#optList"); optList.addEventListener("click",e=>{ if(submitted) return; const lbl=e.target.closest(".opt"); if(!lbl) return; const i=+lbl.dataset.i; if(ans.has(i)){ ans.delete(i); lbl.querySelector("input").checked=false; } else { ans.add(i); lbl.querySelector("input").checked=true; } }); const actBtn=examBody.querySelector("#actBtn"); if(submitted){ highlightMulti(kind); actBtn.onclick=()=>{ if(kind==="investigations"){ state.stage=2; renderStageTrack(); renderMulti("medications"); } else if(kind==="medications"){ state.stage=3; renderStageTrack(); renderDiagnosis(); } else { state.stage=5; renderStageTrack(); renderCaseResult(); } }; } else { actBtn.onclick=()=>{ if(kind==="investigations") state.answers.submitted.i=true; else if(kind==="medications") state.answers.submitted.m=true; else { state.answers.submitted.e=true; state.emergencyManual=true; stopTimer(); } renderMulti(kind); }; } if(kind==="emergency" && !submitted) startTimer(c); examBody.scrollTop=0; } function highlightMulti(kind){ const c=curCase(); const items = kind==="investigations"?c.investigations : kind==="medications"?c.medications : c.emergency.actions; const ans=state.answers[kind]; examBody.querySelectorAll(".opt").forEach(lbl=>{ const i=+lbl.dataset.i, it=items[i], got=ans.has(i); if(it.n&&got){ lbl.classList.add("correct"); lbl.querySelector(".opt-text").insertAdjacentHTML("beforeend",`верно`); } else if(!it.n&&got){ lbl.classList.add("wrong-pick"); lbl.querySelector(".opt-text").insertAdjacentHTML("beforeend",`лишнее`); } else if(it.n&&!got){ lbl.classList.add("missed"); lbl.querySelector(".opt-text").insertAdjacentHTML("beforeend",`пропущено`); } }); const ss=examBody.querySelector("#stepScore"); if(ss) ss.textContent=multiScore(items,ans)+"%"; } function renderDiagnosis(){ const c=curCase(); const submitted=state.answers.submitted.d; examBody.innerHTML=`
Шаг 3 из 4

Поставьте диагноз

Выберите наиболее вероятный диагноз из вариантов.

${c.diagnosis.options.map((o,i)=>{ const checked=state.answers.diagnosis===i?"checked":""; return ``; }).join("")}
${submitted?`Балл за шаг: `:"Выберите один вариант"}
`; const optList=examBody.querySelector("#optList"); optList.addEventListener("click",e=>{ if(submitted) return; const lbl=e.target.closest(".opt"); if(!lbl) return; state.answers.diagnosis=+lbl.dataset.i; optList.querySelectorAll("input").forEach((inp,ii)=>inp.checked=ii===state.answers.diagnosis); }); const actBtn=examBody.querySelector("#actBtn"); if(submitted){ highlightDiagnosis(); actBtn.onclick=()=>{ state.stage=4; renderStageTrack(); renderMulti("emergency"); }; } else { actBtn.onclick=()=>{ if(state.answers.diagnosis===null){ flashChip(); return; } state.answers.submitted.d=true; renderDiagnosis(); }; } examBody.scrollTop=0; } function highlightDiagnosis(){ const c=curCase(); examBody.querySelectorAll(".opt").forEach(lbl=>{ const i=+lbl.dataset.i, o=c.diagnosis.options[i]; if(o.c){ lbl.classList.add("correct"); lbl.querySelector(".opt-text").insertAdjacentHTML("beforeend",`верный диагноз`); } else if(i===state.answers.diagnosis){ lbl.classList.add("wrong-pick"); lbl.querySelector(".opt-text").insertAdjacentHTML("beforeend",`неверно`); } }); const sc=state.answers.diagnosis!==null&&c.diagnosis.options[state.answers.diagnosis].c?100:0; const ss=examBody.querySelector("#stepScore"); if(ss) ss.textContent=sc+"%"; } function flashChip(){ const chip=examBody.querySelector("#stepChip"); if(!chip) return; chip.style.background="var(--red-soft)"; chip.style.color="var(--red)"; setTimeout(()=>{ chip.style.background=""; chip.style.color=""; },700); } function startTimer(c){ const total=c.emergency.time||60; let left=total; const tw=examBody.querySelector("#timerWrap"); if(!tw) return; function tick(){ state.emergencyLeft=left; const pct=left/total*100; const color=left<=10?"var(--red)":"var(--gold)"; const danger=left<=10?' осталось мало времени!':""; tw.innerHTML=`${ring(pct,72,7,color,"rgba(255,255,255,.15)",left)}

⏱ Экстренная помощь

Отмечайте действия. Время пошло.${danger}

`; if(left<=0){ stopTimer(); if(!state.answers.submitted.e){ state.answers.submitted.e=true; state.emergencyManual=false; renderMulti("emergency"); } return; } left--; } tick(); timerHandle=setInterval(tick,1000); } function stopTimer(){ if(timerHandle){ clearInterval(timerHandle); timerHandle=null; } } function multiScore(items,picked){ let tp=0,tn=0,fp=0,fn=0; items.forEach((it,i)=>{ const need=it.n, got=picked.has(i); if(need&&got)tp++; else if(need&&!got)fn++; else if(!need&&got)fp++; else tn++; }); return Math.round(((tp+tn)/items.length)*100); } function computeCaseScore(c){ const s={ investigations:multiScore(c.investigations,state.answers.investigations), medications:multiScore(c.medications,state.answers.medications), diagnosis:(state.answers.diagnosis!==null&&c.diagnosis.options[state.answers.diagnosis].c)?100:0, emergency:multiScore(c.emergency.actions,state.answers.emergency) }; s.total=Math.round(s.investigations*0.2+s.medications*0.2+s.diagnosis*0.3+s.emergency*0.3); return s; } function breakRow(name,val,color){ return `
${name}
${val}%
`; } function renderCaseResult(){ const c=curCase(); const sc=computeCaseScore(c); const meta={ dxCorrect:state.answers.diagnosis!==null&&c.diagnosis.options[state.answers.diagnosis].c, perfectDx:sc.diagnosis===100, noErrors:countFP(c.investigations,state.answers.investigations)===0&&countFP(c.medications,state.answers.medications)===0&&countFP(c.emergency.actions,state.answers.emergency)===0, fast:state.emergencyManual&&state.emergencyLeft>30 }; // награды const prevLevel=levelInfo(profile.xp).idx; const xpGain=awardXp(c, sc.total); recordCase(c, sc.total, meta); const newAchv=checkAchievements(); const newLevel=levelInfo(profile.xp).idx; const stars=starsFor(sc.total); state.caseScores.push({id:c.id,title:c.title,theme:c.theme,themeLabel:c.themeLabel,badge:c.badge,score:sc.total}); if(newLevel>prevLevel){ const li=levelInfo(profile.xp); toast("lvl","Новый уровень!",`${li.cur.name} — уровень ${li.cur.lvl}`); } newAchv.forEach(a=>toast("achv","Достижение!",a.name)); const grade=gradeFor(sc.total), col=colorFor(sc.total); const starsHTML=[0,1,2].map(i=>i★`).join(""); examBody.innerHTML=`
${ring(sc.total,146,12,col,"var(--gray-100)",`${sc.total}%оценка за кейс`)}

${grade.title}

${grade.text}
Звёзды
${starsHTML}
Опыт
+${xpGain} XP
${newAchv.length?`
🏅
Награды
${newAchv.map(a=>a.icon).join(" ")}
`:""}
${breakRow("Обследования",sc.investigations,"var(--burgundy)")} ${breakRow("Лечение",sc.medications,"var(--teal)")} ${breakRow("Диагноз",sc.diagnosis,"var(--gold)")} ${breakRow("Экстренная помощь",sc.emergency,"var(--rust)")}

📝 Правильный диагноз

${c.diagnosis.options.find(o=>o.c).t}${c.diagnosis.explanation}

📋 Протокол МЗ РК

${c.protocol}${c.protocolNote}

🔬 Ключевые данные

${c.extra}

💡 Ключевые моменты и типичные ошибки

${c.keyPoints.map(p=>`
${p.t==="good"?"✅":"⚠️"}${p.x}
`).join("")}
Кейс ${state.qIdx+1} из ${state.queue.length} · ${sc.total}%
`; $("actBtn").onclick=nextAfterCase; examBody.scrollTop=0; } function nextLabel(){ if(state.mode==="single") return "Готово"; if(state.qIdx=85) return {title:"Отлично!",text:"Уверенное владение алгоритмом неотложной помощи."}; if(pct>=70) return {title:"Хорошо",text:"Хороший уровень, но есть моменты для повторения."}; if(pct>=55) return {title:"Удовлетворительно",text:"Базовые навыки есть, но много ошибок — повторите тему."}; return {title:"Неудовлетворительно",text:"Алгоритмы требуют серьёзной проработки."}; } function colorFor(pct){ return pct>=85?"var(--green)":pct>=70?"var(--teal)":pct>=55?"var(--amber)":"var(--red)"; } function gradeClass(pct){ return pct>=85?"g5":pct>=70?"g4":pct>=55?"g3":"g2"; } function renderFinal(){ stopTimer(); stopSpeak(); const scores=state.caseScores; const avg=Math.round(scores.reduce((a,s)=>a+s.score,0)/scores.length); const col=colorFor(avg), grade=gradeFor(avg); // запись в рейтинг results.push({name:profile.name||"Гость", avatar:profile.avatar, score:avg, date:new Date().toLocaleDateString("ru-RU")}); if(results.length>50) results=results.slice(results.length-50); save(STORE.results,results); const themes={}; scores.forEach(s=>{ (themes[s.theme]=themes[s.theme]||[]).push(s.score); }); const themeRows=Object.entries(themes).map(([k,arr])=>{ const t=CASES.find(c=>c.theme===k); const col=k==="aks"?"var(--burgundy)":k==="odn"?"var(--teal)":"var(--rust)"; return `
${Math.round(arr.reduce((a,b)=>a+b,0)/arr.length)}%${t.themeLabel}
`; }).join(""); examBody.innerHTML=`
${ring(avg,174,14,col,"var(--gray-100)",`${avg}%`)}

Итог экзамена

${grade.title}

${grade.text} Вы прошли ${scores.length} кейсов. Результат добавлен в рейтинг.

${themeRows}
${scores.map((s,i)=>`
${i+1} ${s.title} ${s.score}%
`).join("")}
`; $("again").onclick=()=>{ startExam("full"); }; $("closeFin").onclick=closeExam; examBody.scrollTop=0; } /* ---- обработчики ---- */ $("filters").addEventListener("click",e=>{ const b=e.target.closest(".filter"); if(!b) return; setFilter(b.dataset.filter); }); $("heroStart").addEventListener("click",()=>document.getElementById("map").scrollIntoView({behavior:"smooth"})); $("heroFull").addEventListener("click",()=>startExam("full")); $("startFullExam2").addEventListener("click",()=>startExam("full")); $("examExit").addEventListener("click",()=>{ if(confirm("Выйти из экзамена? Прогресс кейса не сохранится.")) closeExam(); }); document.addEventListener("keydown",e=>{ if(e.key==="Escape"&&!exam.hidden){ if(confirm("Выйти из экзамена?")) closeExam(); } }); /* ---- старт ---- */ refreshAll(); if(!profile.name){ /* мягко: ничего не навязываем */ }