видео-аватар пациента (волны/мимика/субтитры), бренд ПУЛЬС, PRD.md

This commit is contained in:
Dauren777 2026-06-26 06:52:01 +00:00
parent 803cabbde7
commit 38bee301c3
4 changed files with 225 additions and 14 deletions

166
PRD.md Normal file
View File

@ -0,0 +1,166 @@
# PRD · ПУЛЬС — Неотложка
**Игровой тренажёр клинических кейсов по неотложной медицинской помощи**
Версия документа: 1.0
Статус: реализовано (MVP), в итеративной доработке
Платформа: статический сайт (HTML + CSS + vanilla JS), публикуется через Git Pages
---
## 1. Концепция
**ПУЛЬС** — образовательный игровой тренажёр для студентов-медиков 6 курса. Студент работает с «говорящими» видео-пациентами: выслушивает жалобы голосом, самостоятельно назначает обследования и лечение, ставит диагноз и оказывает экстренную помощь на время. За точность начисляется опыт (XP), открываются достижения и уровни, результаты попадают в рейтинг. Алгоритмы решения основаны на клинических протоколах диагностики и лечения МЗ РК.
**Ценность:** превращает сухую зубрёжку протоколов в тренировку клинического мышления в условиях, близких к приёмному отделению, с мгновенной обратной связью и игровой мотивацией.
---
## 2. Целевая аудитория
- **Основная:** студенты 6 курса медицинских вузов (интерны, ординаторы) — отработка неотложных состояний.
- **Дополнительная:** врачи СМП и приёмного покоя для повторения алгоритмов; преподаватели — как материал для семинаров.
- **Контекст применения:** Казахстан (протоколы МЗ РК, русскоязычный интерфейс).
---
## 3. Цели и задачи
### Цели продукта
1. Дать практику клинического мышления по неотложным состояниям в безопасной среде.
2. Закрепить алгоритмы клинических протоколов МЗ РК через повторение.
3. Повысить мотивацию к обучению за счёт геймификации.
### Метрики успеха (MVP)
- Завершение полного экзамена (6 кейсов) за один заход — основная конверсия.
- Доля повторных прохождений кейсов (replay) — вовлечённость.
- Средний балл по кейсу/экзамену — уровень усвоения.
- Количество разблокированных достижений — глубина погружения.
---
## 4. Функциональные требования
### 4.1. Профиль игрока и уровни
- Создание профиля: имя + аватар (emoji).
- Система опыта (XP): начисляется за результаты кейсов (пропорционально приросту лучшего балла).
- 8 уровней: Стажёр → Студент → Интерн → Ординатор → Врач → Специалист → Эксперт → Профессор.
- Шапка профиля: аватар, имя, текущий уровень, XP-бар до следующего уровня.
- Сохранение профиля и прогресса в браузере (localStorage).
### 4.2. Карта прохождения тем
- 3 темы: «ОКС и инфаркт», «ОДН · астма · ХОБЛ», «Шок и травма».
- По каждой теме: прогресс-бар (кейсов пройдено), заработанные звёзды.
- Клик по теме фильтрует кейсы и переносит к списку.
### 4.3. Кейсы
- 6 клинических кейсов (по 2 на тему).
- Карточка кейса: портрет-аватар (фото), тема, звёзды (13), лучший результат, счётчик прохождений, статус «Пройден», кнопка «Играть/Повторить».
### 4.4. Видео-пациент (говорящий аватар)
- В шаге «Пациент» — видеоплеер с крупным портретом пациента.
- При нажатии «Прослушать»: синтез речи (Web Speech API), портрет «оживает» — звуковые волны, лёгкая анимация говорения, индикатор LIVE.
- **Субтитры** синхронно с речью (по фразам).
- Голос подбирается под пациента: пол, возраст, состояние (pitch/rate/громкость), паузы-«дыхание» между фразами (у астматика — рваная речь с длинными паузами, у шокового — тихая и замедленная).
### 4.5. Пошаговое решение кейса (экзамен)
Каждый кейс = 4 шага + результат:
1. **Обследования** — множественный выбор из перечня (нужные vs лишние).
2. **Лечение** — множественный выбор препаратов (нужные vs противопоказанные).
3. **Диагноз** — выбор одного верного варианта.
4. **Экстренная помощь ⏱** — за 60 секунд выбрать правильные действия; таймер с обратным отсчётом, автозавершение при истечении.
Требования к выбору вариантов:
- Варианты кликабельны и доступны для нажатия на всех шагах.
- После «Подтвердить» варианты блокируются и подсвечиваются: верно / лишнее / пропущено / верный диагноз.
### 4.6. Оценка и обратная связь
- Балл за шаг множественного выбора: `(TP + TN) / всего × 100%`.
- Диагноз: 100% / 0%.
- Вес шагов в кейсе: обследования 20%, лечение 20%, диагноз 30%, экстренная помощь 30%.
- Экран результата кейса: кольцевой индикатор %, грейд (Отлично/Хорошо/Удовл./Неуд), разбивка по шагам, награды (звёзды, XP, новые достижения), разбор: правильный диагноз, **протокол МЗ РК**, ключевые данные, типичные ошибки.
### 4.7. Геймификация
- **Звёзды за кейс:** ≥85% → 3, ≥70% → 2, ≥55% → 1.
- **Достижения (11 шт.):** «Первый шаг», «Точный диагноз», «Безупречный кейс», «Чистая работа», «Молниеносная реакция», «Мастер ОКС/ОДН/шока», «Великий диагност», «Полное прохождение», «Профессор».
- **Тосты** при разблокировке достижений и повышении уровня.
### 4.8. Рейтинг игроков
- Топ-10 результатов полного экзамена (имя, аватар, %, дата).
- Сохраняется локально в браузере; результат текущего игрока подсвечен.
### 4.9. Полный экзамен
- Последовательное прохождение всех 6 кейсов.
- Итоговый экран: средний %, грейд, разбивка по темам и по кейсам.
- Результат добавляется в рейтинг.
---
## 5. Содержание кейсов (по протоколам МЗ РК)
| # | Тема | Кейс | Протокол МЗ РК |
|---|------|------|----------------|
| 1 | ОКС | Острый инфаркт миокарда с подъёмом ST (STEMI) | «Острый коронарный синдром» |
| 2 | ОКС | ОКС без подъёма ST (NSTEMI) | «Острый коронарный синдром» |
| 3 | ОДН | Острый тяжёлый приступ бронхиальной астмы | «Бронхиальная астма» |
| 4 | ОДН | Обострение ХОБЛ с ОДН 2 типа | «Хроническая обструктивная болезнь лёгких» |
| 5 | Шок | Травматический геморрагический шок | «Шок», «Острое кровотечение» |
| 6 | Шок | Септический шок | «Сепсис и септический шок» |
Ключевые положения протоколов, отражённые в кейсах:
- **STEMI:** первичное ЧКВ (door-to-balloon ≤ 90 мин); при недоступности ≤ 120 мин — фибринолизис, в т.ч. догоспитальный; двойная антитромбоцитарная терапия + антикоагулянт.
- **NSTEMI:** тромболизис НЕ показан; стратификация по GRACE; ранняя инвазивная стратегия.
- **Астма:** β2-агонисты + ипратропия бромид + системные ГКС + магний; седативные/опиоиды противопоказаны.
- **ХОБЛ:** контролируемая О₂-терапия (целевой SpO₂ 8892%); NIV при гиперкапнии.
- **Геморрагический шок:** damage control, остановка кровотечения, гемотрансфузия 1:1:1, транексамовая кислота; борьба с триадой смерти.
- **Сепсис:** hour-1 bundle (лактат, гемокультуры, антибиотики в 1-й час, инфузия 30 мл/кг, норадреналин).
---
## 6. Дизайн
- **Стиль:** выдержанная классическая палитра — графит-navy, кремовый «бумажный» фон, приглушённое золото (акцент), бордо (ОКС), тёмно-бирюзовый (ОДН), кирпичный (шок), хирургический зелёный (верно). Строгий медицинский вид, без неон-яркости.
- **Типографика:** Manrope.
- **Адаптив:** desktop / планшет / мобильный.
- **Иконки:** emoji и inline-элементы.
---
## 7. Технологии и ограничения
- **Стек:** статические `index.html` + `style.css` + `app.js` (vanilla JS). Без бэкенда, БД, сборщиков, аутентификации.
- **Озвучка:** Web Speech API (SpeechSynthesis), русские голоса браузера.
- **Хранение:** localStorage (профиль, прогресс, рейтинг).
- **Публикация:** Git Pages, ветка `pages`.
- **Ограничения песочницы:** только статика; реальные фото/видео пациентов не используются (этика) — применяется анимированный видео-аватар.
---
## 8. Пользовательский путь
1. Открывает сайт → видит шапку профиля и hero с уровнем/XP.
2. Создаёт профиль (имя + аватар) через ⚙️.
3. Заходит в «Карту прохождения» или сразу в «Кейсы».
4. Выбирает кейс → видеопациент рассказывает жалобы голосом.
5. Проходит 4 шага (обследования → лечение → диагноз → экстренная помощь на 60 с).
6. Получает результат: %, звёзды, XP, достижения, разбор с протоколом МЗ РК.
7. Идёт к следующему кейсу или запускает полный экзамен → попадает в рейтинг.
8. Растёт в уровне от Стажёра до Профессора.
---
## 9. Roadmap (возможные доработки)
- Расширение тем: кома и неврология, острый живот, отравления, анафилаксия.
- Режим «Экзамен с таймером на весь кейс».
- Экспорт результата / сертификат прохождения.
- Импорт своих медиа (фото/видео пациентов) для преподавателя.
- Поддержка казахского языка интерфейса.
- Синхронизация прогресса между устройствами (требует бэкенда — вне песочницы).
---
## 10. Ссылки
- Сайт: https://pages.git.vibe42.kz/saniya_akpanova_91/emergency-cases/
- Репозиторий: https://git.vibe42.kz/saniya_akpanova_91/emergency-cases

44
app.js
View File

@ -469,22 +469,24 @@ function pickVoice(preferMale){
return ru[0];
}
let speakState={playing:false, queue:[], idx:0, timer:null, btn:null, avatar:null, opts:null};
let speakState={playing:false, queue:[], idx:0, timer:null, btn:null, avatar:null, opts:null, player:null, subsEl:null, onPhrase:null, onEnd: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.btn){ speakState.btn.classList.remove("is-playing"); speakState.btn.textContent="▶ Прослушать жалобы"; }
if(speakState.avatar) speakState.avatar.classList.remove("is-speaking");
speakState.queue=[]; speakState.idx=0;
if(speakState.player) speakState.player.classList.remove("is-speaking");
if(speakState.onEnd){ const cb=speakState.onEnd; speakState.onEnd=null; cb(); }
speakState.queue=[]; speakState.idx=0; speakState.player=null; speakState.subsEl=null; speakState.onPhrase=null;
}
function splitPhrases(text){
return text.split(/(?<=[.!?…])\s+|(?<=…)\s*/).map(s=>s.trim()).filter(Boolean);
}
function speakPatient(text, opts, btn, avatar){
function speakPatient(text, opts, btn, avatar, ext){
const synth=window.speechSynthesis;
if(!synth){ return; }
if(speakState.playing){ stopSpeak(); return; }
@ -492,9 +494,14 @@ function speakPatient(text, opts, btn, avatar){
const phrases=splitPhrases(text);
if(!phrases.length) return;
speakState.queue=phrases; speakState.idx=0; speakState.btn=btn; speakState.avatar=avatar; speakState.opts=opts;
speakState.player=ext&&ext.player||null;
speakState.subsEl=ext&&ext.subsEl||null;
speakState.onPhrase=ext&&ext.onPhrase||null;
speakState.onEnd=ext&&ext.onEnd||null;
speakState.playing=true;
btn.classList.add("is-playing"); btn.textContent="⏹ Остановить";
if(avatar) avatar.classList.add("is-speaking");
if(speakState.player) speakState.player.classList.add("is-speaking");
playNext();
}
@ -502,6 +509,8 @@ function playNext(){
const synth=window.speechSynthesis;
if(!speakState.playing || speakState.idx>=speakState.queue.length){ stopSpeak(); return; }
const phrase=speakState.queue[speakState.idx];
if(speakState.subsEl) speakState.subsEl.textContent=phrase;
if(speakState.onPhrase) speakState.onPhrase(phrase, speakState.idx);
const opts=speakState.opts||{};
const u=new SpeechSynthesisUtterance(phrase);
u.lang="ru-RU";
@ -869,10 +878,18 @@ function patientBlock(c){
<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 class="vplayer" id="vplayer">
<div class="vp-screen">
<div class="vp-waves"><i></i><i></i><i></i></div>
<div class="vp-avatar">${c.patient.avatar}</div>
<span class="vp-live"> ВИДЕО · LIVE</span>
<span class="vp-photo">📷 Фото пациента</span>
</div>
<div class="vp-subs" id="vpSubs">Нажмите «Прослушать», чтобы пациент рассказал жалобы</div>
<div class="vp-ctrl">
<button class="speak-btn" id="speakBtn"> Прослушать жалобы</button>
<span class="vp-hint">🔊 Включите звук на устройстве</span>
</div>
</div>
<div class="complaint">«${c.complaint}»</div>
</div>`;
@ -888,8 +905,15 @@ function vitalsHTML(v){
${extra}</div>`;
}
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));
const btn=root.querySelector("#speakBtn"),
av=root.querySelector(".vp-avatar"),
player=root.querySelector("#vplayer"),
subs=root.querySelector("#vpSubs");
if(!btn) return;
btn.addEventListener("click",()=>speakPatient(c.complaint, c.patient.voice, btn, av, {
player, subsEl:subs,
onEnd:()=>{ if(subs) subs.textContent="Нажмите «Прослушать», чтобы пациент рассказал жалобы"; }
}));
}
function renderPatient(){

View File

@ -3,8 +3,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>МедИгра · Неотложка — тренажёр для медиков</title>
<meta name="description" content="Игровой тренажёр клинических кейсов по неотложной помощи: говорящие пациенты, уровни, опыт, достижения и рейтинг. По протоколам МЗ РК.">
<title>ПУЛЬС · Неотложка — игровой тренажёр для медиков</title>
<meta name="description" content="ПУЛЬС — игровой тренажёр клинических кейсов по неотложной помощи: говорящие видео-пациенты, уровни, опыт, достижения и рейтинг. По протоколам МЗ РК.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700;800&display=swap" rel="stylesheet">
@ -15,7 +15,7 @@
<!-- ИГРОВАЯ ШАПКА -->
<header class="topbar">
<div class="container topbar-in">
<a class="brand" href="#top">🩺 МедИгра<span>Неотложка</span></a>
<a class="brand" href="#top">🫀 ПУЛЬС<span>Неотложка</span></a>
<div class="player" id="playerBar"></div>
<div class="topbar-actions">
<button class="tbtn" id="btnAchv" title="Достижения">🏅</button>
@ -92,7 +92,7 @@
<!-- FOOTER -->
<footer class="footer">
<div class="container">
<p><b>МедИгра · Неотложка</b> — игровой тренажёр для студентов-медиков. Алгоритмы основаны на клинических протоколах диагностики и лечения МЗ РК. Материалы носят обучающий характер.</p>
<p><b>ПУЛЬС · Неотложка</b> — игровой тренажёр для студентов-медиков. Алгоритмы основаны на клинических протоколах диагностики и лечения МЗ РК. Материалы носят обучающий характер.</p>
</div>
</footer>
</main>

View File

@ -280,6 +280,26 @@ h1,h2,h3{letter-spacing:-.4px}
.speak-btn.is-playing{background:var(--red)}
.complaint{background:rgba(176,141,87,.12);border-left:4px solid var(--gold);border-radius:0 12px 12px 0;padding:16px 18px;font-size:16.5px;line-height:1.7;margin-top:16px;font-style:italic;color:#dfe5ec}
/* видео-аватар пациента */
.vplayer{background:#0f1828;border-radius:var(--radius-lg);overflow:hidden;margin-bottom:18px;box-shadow:var(--shadow)}
.vp-screen{position:relative;aspect-ratio:16/10;background:radial-gradient(circle at 50% 38%,#2a3a52,#101a2c);display:grid;place-items:center;overflow:hidden}
.vp-waves{position:absolute;inset:0;display:grid;place-items:center;pointer-events:none}
.vp-waves i{position:absolute;width:130px;height:130px;border-radius:50%;border:2px solid rgba(176,141,87,.5);opacity:0}
.vplayer.is-speaking .vp-waves i{animation:vpwave 1.7s ease-out infinite}
.vplayer.is-speaking .vp-waves i:nth-child(2){animation-delay:.55s}
.vplayer.is-speaking .vp-waves i:nth-child(3){animation-delay:1.1s}
@keyframes vpwave{0%{transform:scale(.55);opacity:.7}100%{transform:scale(2.3);opacity:0}}
.vp-avatar{font-size:104px;line-height:1;position:relative;z-index:2;filter:drop-shadow(0 10px 18px rgba(0,0,0,.45))}
.vplayer.is-speaking .vp-avatar{animation:vptalk .45s ease-in-out infinite alternate}
@keyframes vptalk{0%{transform:scale(1) translateY(0)}100%{transform:scale(1.05) translateY(-4px)}}
.vp-live{position:absolute;top:12px;left:12px;background:rgba(0,0,0,.45);color:#fff;font-size:11px;font-weight:800;letter-spacing:.5px;padding:4px 10px;border-radius:6px;z-index:3}
.vplayer.is-speaking .vp-live{color:#ff6b6b}
.vp-photo{position:absolute;top:12px;right:12px;background:rgba(0,0,0,.45);color:#cfd6e0;font-size:11px;font-weight:700;padding:4px 10px;border-radius:6px;z-index:3}
.vp-subs{background:rgba(0,0,0,.6);color:#fff;padding:12px 16px;font-size:14.5px;line-height:1.45;min-height:50px;text-align:center;display:flex;align-items:center;justify-content:center}
.vp-ctrl{padding:12px 16px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;background:#0c1422}
.vp-hint{color:#9ba6b4;font-size:13px}
.vp-ctrl .speak-btn{margin-left:auto}
/* аккордеон */
.acc{border:1px solid var(--line);border-radius:var(--radius);margin-bottom:12px;overflow:hidden;background:var(--white)}
.acc summary{list-style:none;cursor:pointer;padding:15px 18px;font-weight:800;font-size:15.5px;display:flex;align-items:center;gap:10px;background:var(--paper);user-select:none}
@ -430,4 +450,5 @@ h1,h2,h3{letter-spacing:-.4px}
.hero-stats{gap:20px}
.exam-bar,.exam-stage-track,.exam-body{padding-left:16px;padding-right:16px}
.toasts{right:12px;left:12px;max-width:none}
.vp-avatar{font-size:72px}
}