emergency-cases/app.js

509 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ===== Данные клинических кейсов ===== */
const CASES = [
{
id:"stemi",
theme:"aks", themeLabel:"ОКС / инфаркт", badge:"b-aks",
title:"Острый инфаркт миокарда с подъёмом ST",
severity:3,
patient:{avatar:"👨‍🦰", name:"Пациент А.", age:"58 лет, мужчина", ctx:"Поступил по скорой, ~2 ч от начала боли"},
complaint:"Доктор, часа два назад как прижало за грудиной — давит сильно, будто слон на грудь сел. Отдаёт в левую руку и в челюсть. Дышать тяжело, бросило в холодный пот, стало страшно. Таблетку нитроглицерина под язык клал — не отпускает.",
anamnesis:[
"Боль возникла в покое, после эмоционального стресса на работе.",
"Курит 30 лет по пачке в день. Артериальная гипертензия 8 лет.",
"Отец умер от инфаркта миокарда в 60 лет.",
"Ранее подобных приступов не было. Регулярно терапии не получает."
],
objective:{
vitals:{hr:"102", bp:"150/95", rr:"22", spo2:"93%"},
general:"Кожа бледная, влажная, холодный пот. Пациент тревожен, ищет удобное положение.",
heart:"Тоны сердца приглушены, тахикардия, ритм правильный.",
lungs:"В нижних отделах с обеих сторон — мелкопузырчатые незвучные хрипы.",
other:"Шумов на сонных артериях нет. Отёков нет."
},
extra:{title:"ЭКГ и биомаркеры", text:"ЭКГ: подъём сегмента ST в отведениях I, aVL, V1V4; реципрокная депрессия в II, III, aVF. Патологический зубец Q начинает формироваться. Тропонин I повышен."},
quiz:{
q:"Какой наиболее вероятный диагноз и тактика?",
options:[
{t:"Острый перикардит — НПВС, наблюдение", c:false},
{t:"STEMI (передне-перегородочный) — экстренная реперфузия", c:true},
{t:"ТЭЛА — антикоагуляция, тромболизис", c:false},
{t:"Расслоение аорты — контроль АД, экстренная хирургия", c:false}
],
feedback:{
ok:"Верно. Типичный болевой синдром, подъём ST в передних отведениях и реципрокные изменения — это STEMI. Время мышления = гибнущий миокард: показана экстренная реперфузия.",
no:"Не совсем. Подъём ST в V1V4 с реципрокной депрессией и типичной клиникой — это STEMI, требующий немедленной реперфузии."
}
},
diagnosis:"Острый инфаркт миокарда с подъёмом сегмента ST (STEMI), передне-перегородочная локализация. Острая левожелудочковая недостаточность (Killip II).",
algorithm:[
{s:"Вызовите реанимационную бригаду, активируйте STEMI-маршрут (door-to-balloon ≤ 90 мин)."},
{s:"Мониторинг ЭКГ, пульсоксиметрия, 2 венозных доступа."},
{s:"Обезболивание (морфин), кислород при SpO₂ < 90%, нитраты при сохраняющемся болевом синдроме и АД > 100."},
{s:"Двойная антитромбоцитарная терапия: аспирин + ингибитор P2Y12 (тикагрелор/прасугрел). Антикоагулянт (эноксапарин/НФГ)."},
{s:"Реперфузия — первичное ЧКВ предпочтительно; при недоступности ≤ 120 мин — тромболизис с последующим переводом."},
{s:"Бета-блокатор, статин в высокой дозе, ингибитор АПФ — после стабилизации."}
],
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:"Поступила через сутки от начала симптомов"},
complaint:"Вчера ночью проснулась от боли за грудиной — давящая, минут двадцать держалась, отдала в спину. Сегодня повторилось дважды, уже при небольшой нагрузке — просто по квартире походила. Раньше такого никогда не было. Как-то страшно, думаю, это сердце.",
anamnesis:[
"Сахарный диабет 2 типа 12 лет, ожирение, гиперхолестеринемия.",
"Гипертония, принимает периодически.",
"Боли провоцируются нагрузкой, купируются в покое за 1015 мин (прогрессирующая стенокардия).",
"За последний месяц снизилась толерантность к нагрузке."
],
objective:{
vitals:{hr:"88", bp:"140/85", rr:"18", spo2:"96%"},
general:"Состояние средней тяжести. Кожные покровы обычной окраски. ИМТ 32.",
heart:"Тоны ритмичные, приглушены. Шумов нет.",
lungs:"Дыхание везикулярное, хрипов нет.",
other:"Пульс на периферических артериях сохранён. Отёков нет."
},
extra:{title:"ЭКГ и лаборатория", text:"ЭКГ: горизонтальная депрессия ST в V4V6, инверсия зубца T. Тропонин I повышен (>99-го перцентиля), динамика нарастания. Оценка по шкале GRACE — высокий риск."},
quiz:{
q:"Какая тактика наиболее оправдана?",
options:[
{t:"Стабильная стенокардия — амбулаторное наблюдение", c:false},
{t:"NSTEMI высокого риска — ранняя инвазивная стратегия ≤ 24 ч", c:true},
{t:"Костохондрит — НПВС и покой", c:false},
{t:"ГЭРБ — ингибиторы протонной помпы", c:false}
],
feedback:{
ok:"Верно. Депрессия ST + подъём тропонина + высокий риск по GRACE = NSTEMI. Показана ранняя инвазивная стратегия.",
no:"Не совсем. У пациентки депрессия ST, рост тропонина и высокий риск — это NSTEMI, требующий раннего ЧКВ."
}
},
diagnosis:"Острый коронарный синдром без подъёма ST (NSTEMI). Высокий риск по шкале GRACE.",
algorithm:[
{s:"Госпитализация в ОРИТ/кардиологию, мониторинг ЭКГ и ритма."},
{s:"Двойная антиагрегантная терапия (аспирин + P2Y12-ингибитор) + антикоагулянт."},
{s:"Стратификация риска (GRACE/ESC). Высокий риск → ранняя инвазивная стратегия (ЧКВ ≤ 24 ч)."},
{s:"Симптоматически: нитраты при боли, бета-блокатор при отсутствии противопоказаний."},
{s:"Статины в высокой дозе, ингибитор АПФ, контроль гликемии."},
{s:"Вторичная профилактика и кардиореабилитация."}
],
keypoints:[
{t:"good",x:"Женщины и диабетики часто имеют атипичную клинику — не сбрасывайте со счетов."},
{t:"bad",x:"Не ставьте «стабильную стенокардию» при недавнем изменении/прогрессии симптомов — это ОКС."},
{t:"good",x:"Решение о раннем ЧКВ принимает стратификация риска, а не один тропонин."}
]
},
{
id:"asthma",
theme:"odn", themeLabel:"ОДН / астма / ХОБЛ", badge:"b-odn",
title:"Острый тяжёлый приступ бронхиальной астмы",
severity:3,
patient:{avatar:"👩", name:"Пациентка В.", age:"24 года, женщина", ctx:"Доставлена СМП, одышка 1 час"},
complaint:"Не могу... выдохнуть... свистит вся грудь... началось час назад... ингалятор... уже третий раз пшикала — не помогает... тяжело... говорит по словам... страшно задохнуться.",
anamnesis:[
"Бронхиальная астма с детства, базисную терапию (ИГКС) принимает нерегулярно.",
"Неделю назад перенесла ОРВИ с кашлем.",
"За последние дни ингалятор сальбутамола использовала всё чаще (более 10 раз в сутки).",
"Аллергия на пыльцу, домашнюю пыль."
],
objective:{
vitals:{hr:"120", bp:"135/85", rr:"30", spo2:"91%"},
general:"Положение ортопноэ, опирается на руки. Речь прерывистая, произносит отдельные фразы. Выраженная работа вспомогательной мускулатуры.",
lungs:"Выдох резко удлинён, свистящие сухие хрипы по всем полям. Участие в акте дыхания всех групп мышц.",
heart:"Тахикардия. Гиперзвуковой перкуторный звук над лёгкими.",
other:"Втянутые межрёберные промежутки, парадоксальный пульс."
},
extra:{title:"Дополнительные признаки", text:"Пиковая скорость выдоха (ПСВ) < 50% от должного. При угрожающем жизни статусе — «немое лёгкое» (исчезновение хрипов при нарастающей дыхательной недостаточности), брадикардия, нарушение сознания."},
quiz:{
q:"Что является приоритетом в неотложной помощи?",
options:[
{t:"Дать седативное, уложить пациента — panic attack", c:false},
{t:"Кислород + повторные небулайзеры Сальбутамола + системные ГКС", c:true},
{t:"Антибиотики широкого спектра внутривенно", c:false},
{t:"Наркотический анальгетик от боли в груди", c:false}
],
feedback:{
ok:"Верно. Тяжёлый приступ астмы: оксигенация, частые бронходилататоры через небулайзер, системные ГКС, при ухудшении — ИВЛ.",
no:"Не совсем. Седация и наркотики угнетают дыхание и противопоказаны. Основа — бронходилататоры и ГКС."
}
},
diagnosis:"Острый тяжёлый приступ бронхиальной астмы. Угроза астматического статуса.",
algorithm:[
{s:"Оценка ABCD. Мониторинг SpO₂, ЧДД, ЧСС. Кислород через маску, титровать до SpO₂ 9498%."},
{s:"Сальбутамол 5 мг через небулайзер, при тяжёлом — повторно каждые 1520 мин (или непрерывно). Добавить ипратропия бромид."},
{s:"Системные ГКС как можно раньше: преднизолон 4060 мг внутрь или гидрокортизон в/в."},
{s:"Магния сульфат 2 г в/в при тяжёлом приступе, не отвечающем на терапию."},
{s:"Критерии жизнеугрожающего статуса: «немое лёгкое», SpO₂ < 92%, нарушение сознания, брадикардия → ИВЛ, перевод в ОРИТ."},
{s:"После стабилизации — пересмотр базисной терапии (ступенчатое лечение, ИГКС), обучение пациента."}
],
keypoints:[
{t:"bad",x:"Никогда не давайте седативные/опиоиды при астме — это угнетает дыхание и ведёт к остановке."},
{t:"good",x:"Системные ГКС работают через 46 ч, поэтому вводите их сразу, не ждите ухудшения."},
{t:"bad",x:"«Немое лёгкое» = плохо, а не хорошо: хрипы исчезли из-за отсутствия дыхательного резерва — это предлетальное состояние."}
]
},
{
id:"copd",
theme:"odn", themeLabel:"ОДН / астма / ХОБЛ", badge:"b-odn",
title:"Обострение ХОБЛ с острой дыхательной недостаточностью",
severity:2,
patient:{avatar:"👴", name:"Пациент Г.", age:"68 лет, мужчина", ctx:"Направлен врачом общей практики"},
complaint:"Уже неделю кашель усилился, мокрота стала густая, жёлто-зелёная. Дышать совсем тяжело, особенно со вчера. Раньше хоть по дому ходил, а сейчас до туалета не дойти — задыхаюсь. Сплю полусидя, две подушки подкладываю. Лихорадит три дня.",
anamnesis:[
"ХОБЛ диагностирована 10 лет назад. Курит 40 лет (40 пачко-лет).",
"Последние 3 дня — лихорадка до 38.5 °C, гнойная мокрота.",
"Базовая SpO₂ целевая 8892% (компенсированная гиперкапния).",
"Дома — ипратропия бромид/фенотерол, длительные оксигены не получал."
],
objective:{
vitals:{hr:"110", bp:"160/90", rr:"28", spo2:"86%", temp:"38.3°C"},
general:"Состояние тяжёлое. Цианоз губ, акроцианоз. Разговорчив с трудом.",
lungs:"Грудная клетка бочкообразная, перкуторно — коробочный звук. Сухие свистящие хрипы на выдохе, ослабленное дыхание.",
heart:"Громкий II тон над лёгочной артерией. Тахикардия.",
other:"Периферические отёки голеней — признаки cor pulmonale."
},
extra:{title:"Газометрия артериальной крови", text:"PaO₂ 52 мм рт.ст. ↓, PaCO₂ 68 мм рт.ст. ↑, pH 7.31 (дыхательный ацидоз). Гиперкапническая ОДН 2 типа. Рентген: усиление лёгочного рисунка, без очаговой инфильтрации."},
quiz:{
q:"Какой подход к кислородотерапии правильный?",
options:[
{t:"Максимальный O₂ через маску до SpO₂ 99100%", c:false},
{t:"Титровать O₂ до целевого SpO₂ 8892% + NIV при тяжёлой ОДН", c:true},
{t:"Кислород не давать вообще — опасно", c:false},
{t:"Только ингаляции эфирных масел", c:false}
],
feedback:{
ok:"Верно. При ХОБЛ с гиперкапнией избыток кислорода снимает гипоксический драйв дыхания → рост PaCO₂ и кома. Титруйте до 8892%.",
no:"Не совсем. Неконтролируемый O₂ при ХОБЛ ведёт к нарастанию гиперкапнии и CO₂-наркозу. Нужен титрованный кислород и NIV."
}
},
diagnosis:"Обострение ХОБЛ. Острая дыхательная недостаточность 2 типа (гиперкапническая), декомпенсация cor pulmonale.",
algorithm:[
{s:"Контролируемая оксигенотерапия: начать с 2428% через Venturi-маску, титровать до SpO₂ 8892%."},
{s:"Бронходилататоры через небулайзер (ипратропия бромид + фенотерол/сальбутамол)."},
{s:"Системные ГКС: преднизолон 3040 мг/сут на 57 дней."},
{s:"Антибиотики при признаках инфекции (гнойная мокрота, лихорадка) — амоксициллин/клавуланат или макролид."},
{s:"При тяжёлой ОДН с гиперкапнией/ацидозом (pH < 7.35) — неинвазивная вентиляция (NIV/BiPAP)."},
{s:"Контроль газометрии, мониторинг. При отсутствии эффекта от NIV — интубация и ИВЛ."}
],
keypoints:[
{t:"bad",x:"Не давайте 100% O₂ «на всякий случай» при ХОБЛ — это классическая ошибка, ведущая к CO₂-наркозу."},
{t:"good",x:"NIV снижает потребность в интубации и летальность при обострении ХОБЛ с гиперкапнией."},
{t:"good",x:"Целевой SpO₂ для пациента с ХОБЛ — 8892%, а не 9498% как у большинства."}
]
},
{
id:"hemoshock",
theme:"shock", themeLabel:"Шок и травма", badge:"b-shock",
title:"Травматический геморрагический шок",
severity:3,
patient:{avatar:"🧑", name:"Пациент Д.", age:"30 лет, мужчина", ctx:"ДТП, доставлен бригадой СМП"},
complaint:"Всё кружится... холодно почему-то... пить очень хочу... ногу не чувствую... где я... ничего не вижу ясно...",
anamnesis:[
"ДТП: лобовое столкновение, пациент за рулём, не пристёгнут.",
"Закрытый перелом бедра, подозрение на перелом таза.",
"Видимое наружное кровотечение из раны бедра, обширная гематома.",
"Время от травмы до поступления ~40 минут."
],
objective:{
vitals:{hr:"130", bp:"80/40", rr:"28", spo2:"95%"},
general:"Сознание оглушение (GCS 13). Резкая бледность, кожа покрыта холодным липким потом. Акроцианоз.",
heart:"Тоны глухие, выраженная тахикардия, нитевидный пульс.",
lungs:"Дыхание проводится по всем полям, хрипов нет.",
other:"Левое бедро деформировано, укорочено, обширная пульсирующая гематома. Кровь на одежде. Живот мягкий, без перитонеальных знаков."
},
extra:{title:"Оценка шока", text:"Класс геморрагического шока IIIIV (потеря > 30% ОЦК). Дефицит базы > 6 ммоль/л, лактат повышен. Шоковый индекс (ЧСС/САД) = 130/80 = 1.6 — тяжёлый шок."},
quiz:{
q:"Что является абсолютным приоритетом?",
options:[
{t:"Полное рентген-обследование до любой терапии", c:false},
{t:"Остановка кровотечения + венозный доступ + инфузия/гемотрансфузия", c:true},
{t:"Введение обезболивающего и ожидание хирурга", c:false},
{t:"Только внутривенно коллоиды в большой дозе", c:false}
],
feedback:{
ok:"Верно. Принцип damage control: немедленная остановка кровотечения, восстановление ОЦК, раннее переливание крови в соотношении 1:1:1.",
no:"Не совсем. До полного обследования пациент может погибнуть от гиповолемии. Сначала — остановка кровотечения и инфузия."
}
},
diagnosis:"Травматический геморрагический шок IIIIV степени. Закрытый перелом бедра, перелом таза. Массивная кровопотеря.",
algorithm:[
{s:"ABCDE. Обеспечение проходимости дыхательных путей, оксигенация, иммобилизация шейного отдела."},
{s:"Немедленная остановка наружного кровотечения: прямое давление, тазовый бинт, жгут при артериальном кровотечении."},
{s:"Два крупных венозных доступа. Пробы на группу крови, кросс-матч, лактат."},
{s:"Согревание пациента, профилактика триады смерти: гипотермия — ацидоз — коагулопатия."},
{s:"Сбалансированная инфузия кристаллоидов + ранняя гемотрансфузия (эритроциты/плазма/тромбоциты ~1:1:1). Вазопрессоры временно, если инфузия неэффективна."},
{s:"Экстренный хирургический/ортопедический гемостаз (damage control surgery). Контроль внутреннего кровотечения."}
],
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:"Направлен из приёмного покоя"},
complaint:"Три дня назад начался озноб, температура под тридцать девять, кашель с мокротой. Со вчера стало хуже — давление упало, в себя прихожу с трудом. Раньше ноги никогда так не отекали, а сейчас задыхаюсь. Пить хочу, но тошнит.",
anamnesis:[
"Сахарный диабет 2 типа, плохо компенсирован.",
"Заболел как ОРВИ 5 дней назад, затем присоединился кашель с гнойной мокротой.",
"Самостоятельно принимал парацетамол и «антибиотик из аптечки».",
"Аллергия на пенициллин."
],
objective:{
vitals:{hr:"125", bp:"85/50", rr:"26", spo2:"92%", temp:"38.9°C"},
general:"Состояние тяжёлое. Кожа горячая, пятнистая сыпь. Сознание спутано, вялый.",
heart:"Тоны глухие, тахикардия. САД 85 после инфузии кристаллоидов.",
lungs:"В правой нижней доле — ослабленное дыхание, влажные мелкопузырчатые хрипы, притупление перкуторно.",
other:"Олигурия (< 0.5 мл/кг/ч). Капиллярное наполнение > 3 сек. Холодные на ощупь конечности на поздней стадии."
},
extra:{title:"Лаборатория и визуализация", text:"Лактат 4.2 ммоль/л ↑. Лейкоциты 22×10⁹/л, сдвиг формулы. Креатинин повышен. Рентген ОГК: инфильтрация нижней доли справа, плевральный выпот. Гемокультуры взяты до антибиотиков."},
quiz:{
q:"Что должно быть выполнено в первый час (Sepsis 1-hour bundle)?",
options:[
{t:"Только ждать результаты посевов 24 часа", c:false},
{t:"Лактат + гемокультуры + антибиотики + инфузия 30 мл/кг + вазопрессоры при гипотонии", c:true},
{t:"Только вазопрессоры без инфузии", c:false},
{t:"Только жаропонижающие и наблюдение", c:false}
],
feedback:{
ok:"Верно. Hour-1 bundle: измерить лактат, взять культуры до антибиотиков, ввести широкопрофильные антибиотики, инфузия 30 мл/кг кристаллоидов, норадреналин при стойкой гипотонии.",
no:"Не совсем. Задержка с антибиотиками и инфузией每小时 повышает летальность. Все компоненты выполняются в первый час."
}
},
diagnosis:"Септический шок. Внебольничная пневмония нижней доли справа — вероятный источник. Острое почечное повреждение.",
algorithm:[
{s:"Sepsis hour-1 bundle: лактат, забор гемокультур до антибиотиков."},
{s:"Антибиотики широкого спектра в/в в первый час (учесть аллергию на пенициллин — например, цефалоспорин/карбапенем + макролид)."},
{s:"Инфузия кристаллоидов 30 мл/кг за 3 ч при гипотензии или лактате ≥ 4 ммоль/л."},
{s:"При стойкой гипотензии после инфузии — норадреналин титровать до САД ≥ 65 мм рт.ст. Вазопрессином добавить при необходимости."},
{s:"Контроль источника инфекции (дренирование выпота, лечение пневмонии). Мониторинг: лактат, диурез, АД."},
{s:"Поддерживающая терапия: оксигенация/ИВЛ по показаниям, контроль гликемии, профилактика тромбозов."}
],
keypoints:[
{t:"bad",x:"Не ждите посевов, чтобы начать антибиотики — каждая час задержки повышает летальность."},
{t:"good",x:"Норадреналин — препарат первого ряда при септическом шоке после адекватной инфузии."},
{t:"good",x:"Контроль источника инфекции так же важен, как антибиотики — без него шок не купируется."}
]
}
];
/* ===== Рендер карточек ===== */
const grid = document.getElementById("caseGrid");
function sevDots(n){
let s="";for(let i=0;i<3;i++) s+=`<span class="sev-dot ${i<n?'on':''}"></span>`;
return s;
}
function renderCards(filter="all"){
grid.innerHTML="";
CASES.filter(c=>filter==="all"||c.theme===filter).forEach(c=>{
const el=document.createElement("article");
el.className=`case-card t-${c.theme}`;
el.innerHTML=`
<div class="case-head">
<div class="case-avatar">${c.patient.avatar}</div>
<div>
<h3>${c.title}</h3>
<div class="case-sub">${c.patient.name}, ${c.patient.age.split(',')[0]}</div>
</div>
</div>
<p class="case-complaint">«${c.complaint.length>130?c.complaint.slice(0,130)+'…':c.complaint}»</p>
<div class="case-foot">
<span class="playhint">▶ Прослушать и разобрать</span>
<span class="severity" title="Тяжесть">${sevDots(c.severity)}</span>
</div>`;
el.addEventListener("click",()=>openCase(c));
grid.appendChild(el);
});
}
/* ===== Фильтры ===== */
document.getElementById("filters").addEventListener("click",e=>{
const b=e.target.closest(".filter");if(!b)return;
document.querySelectorAll(".filter").forEach(f=>f.classList.remove("is-active"));
b.classList.add("is-active");
renderCards(b.dataset.filter);
});
/* ===== Модальное окно ===== */
const modal=document.getElementById("modal");
const modalBody=document.getElementById("modalBody");
function vitalsHTML(v){
let extras="";
if(v.temp) extras=`<div class="vital v-temp"><b>${v.temp}</b><span>Температура</span></div>`;
return `<div class="vitals">
<div class="vital v-hr"><b>${v.hr}</b><span>ЧСС</span></div>
<div class="vital v-bp"><b>${v.bp}</b><span>АД</span></div>
<div class="vital v-rr"><b>${v.rr}</b><span>ЧДД</span></div>
<div class="vital v-spo"><b>${v.spo2}</b><span>SpO₂</span></div>
${extras}
</div>`;
}
function examHTML(o){
return `
<h4>Жизненные показатели</h4>
${vitalsHTML(o.vitals)}
<h4>Общий осмотр</h4><p>${o.general}</p>
<h4>Сердечно-сосудистая система</h4><p>${o.heart}</p>
<h4>Дыхательная система</h4><p>${o.lungs}</p>
<h4>Прочее</h4><p>${o.other}</p>`;
}
function algoHTML(a){
return `<ol class="algo">${a.map(s=>`<li>${s.s}</li>`).join("")}</ol>`;
}
function kpHTML(k){
return `<div class="keypoints">${k.map(p=>`<div class="kp kp-${p.t==='good'?'good':'bad'}"><span class="kp-ico">${p.t==='good'?'✅':'⚠️'}</span><span>${p.x}</span></div>`).join("")}</div>`;
}
function openCase(c){
const qOpts=c.quiz.options.map((o,i)=>`<button class="quiz-opt" data-i="${i}" data-c="${o.c}">${o.t}</button>`).join("");
modalBody.innerHTML=`
<div class="patient">
<div class="patient-top">
<div class="patient-avatar" id="pAvatar">${c.patient.avatar}</div>
<div class="patient-info">
<h2 id="mTitle">${c.title}</h2>
<div class="meta">${c.patient.name} · ${c.patient.age} · ${c.patient.ctx}</div>
<div class="patient-badges"><span class="badge ${c.badge}">${c.themeLabel}</span></div>
</div>
</div>
<div class="speak-bar">
<span class="speak-ico">🔊</span>
<span class="speak-text"><b>Жалобы пациента</b><small>Нажмите, чтобы услышать голос</small></span>
<button class="speak-btn" id="speakBtn">▶ Прослушать</button>
</div>
</div>
<div class="mbody">
<div class="complaint">«${c.complaint}»</div>
<details class="acc" open>
<summary>📋 Анамнез заболевания и жизни</summary>
<div class="acc-body"><ul>${c.anamnesis.map(a=>`<li>${a}</li>`).join("")}</ul></div>
</details>
<details class="acc" open>
<summary>🩺 Объективный осмотр</summary>
<div class="acc-body">${examHTML(c.objective)}</div>
</details>
<details class="acc">
<summary>🔬 Дополнительные данные</summary>
<div class="acc-body"><p>${c.extra.text}</p></div>
</details>
<div class="quiz">
<h3>🧠 Поставьте диагноз и выберите тактику</h3>
<p class="q-sub">${c.quiz.q}</p>
<div class="quiz-opts">${qOpts}</div>
<div class="quiz-feedback" id="quizFb"></div>
</div>
<details class="acc">
<summary>✅ Правильный диагноз</summary>
<div class="acc-body"><p>${c.diagnosis}</p></div>
</details>
<details class="acc">
<summary>🚑 Алгоритм неотложной помощи</summary>
<div class="acc-body">${algoHTML(c.algorithm)}</div>
</details>
<details class="acc">
<summary>💡 Ключевые моменты и типичные ошибки</summary>
<div class="acc-body">${kpHTML(c.keypoints)}</div>
</details>
</div>`;
// озвучка
const speakBtn=document.getElementById("speakBtn");
const pAvatar=document.getElementById("pAvatar");
speakBtn.addEventListener("click",()=>toggleSpeak(c.complaint,speakBtn,pAvatar));
// квиз
const opts=modalBody.querySelectorAll(".quiz-opt");
const fb=document.getElementById("quizFb");
opts.forEach(o=>{
o.addEventListener("click",()=>{
opts.forEach(x=>x.disabled=true);
const correct=o.dataset.c==="true";
if(correct){o.classList.add("correct")}else{o.classList.add("wrong");
// подсветить верный
opts.forEach(x=>{if(x.dataset.c==="true")x.classList.add("correct")});
}
fb.className=`quiz-feedback show ${correct?'ok':'no'}`;
fb.innerHTML=correct?`<b>Верно!</b>${c.quiz.feedback.ok}`:`<b>Не совсем так</b>${c.quiz.feedback.no}`;
});
});
modal.hidden=false;
document.body.style.overflow="hidden";
}
function closeModal(){
window.speechSynthesis.cancel();
modal.hidden=true;
document.body.style.overflow="";
}
modal.addEventListener("click",e=>{if(e.target.hasAttribute("data-close"))closeModal()});
document.addEventListener("keydown",e=>{if(e.key==="Escape"&&!modal.hidden)closeModal()});
/* ===== Озвучка (Web Speech API) ===== */
let voicesReady=false;
function loadVoices(){
voicesReady=window.speechSynthesis.getVoices().length>0;
}
if("speechSynthesis" in window){
loadVoices();
window.speechSynthesis.onvoiceschanged=loadVoices;
}
function toggleSpeak(text,btn,avatar){
const synth=window.speechSynthesis;
if(!synth){alert("Озвучка не поддерживается этим браузером.");return}
// если уже играет — остановить
if(btn.classList.contains("is-playing")){
synth.cancel();
btn.classList.remove("is-playing");
btn.textContent="▶ Прослушать";
avatar.classList.remove("is-speaking");
return;
}
synth.cancel();
const u=new SpeechSynthesisUtterance(text);
u.lang="ru-RU";u.rate=0.97;u.pitch=1;
const voices=synth.getVoices();
const ru=voices.find(v=>v.lang&&v.lang.toLowerCase().startsWith("ru"));
if(ru)u.voice=ru;
u.onend=()=>{btn.classList.remove("is-playing");btn.textContent="▶ Прослушать";avatar.classList.remove("is-speaking")};
u.onerror=()=>{btn.classList.remove("is-playing");btn.textContent="▶ Прослушать";avatar.classList.remove("is-speaking")};
btn.classList.add("is-playing");
btn.textContent="⏹ Остановить";
avatar.classList.add("is-speaking");
synth.speak(u);
}
/* остановить речь при перерисовке */
window.addEventListener("beforeunload",()=>{if("speechSynthesis" in window)window.speechSynthesis.cancel()});
/* старт */
renderCards();