// app.jsx — мини-апп «мера.»
// IOSDevice + home/test/result flow for Beck, PSS-10, Clarity
const { useState, useEffect, useRef, useCallback } = React;
const { IOSDevice } = window;
const { BECK, PSS, PSS_OPTS, PSS_REVERSE, interpBeck, interpPss, interpClarity } = window.MERA_DATA;
const EMBED = /[?&]embed=1/.test(location.search) || (window.self !== window.top);
// ─── HOME ─────────────────────────────────────────────────
function Home({ onPick, results }){
const tests = [
{ id:'beck', code:'A', title:<>шкала бека >, info:'21 пункт · ~7 мин', n:21, done:!!results.beck },
{ id:'pss', code:'B', title:<>стресс · pss -10>, info:'10 вопросов · ~3 мин', n:10, done:!!results.pss },
{ id:'clarity', code:'C', title:<>ясность · vas >, info:'1 шкала · ~1 мин', n:1, done:!!results.clarity },
];
const doneCount = Object.values(results).filter(Boolean).length;
const last = results.last;
return (
сегодня · {new Date().toLocaleDateString('ru-RU',{day:'numeric',month:'long'})}
тихая проверка самочувствия.
Три шкалы — депрессия, стресс, ясность мышления. Без советов, без баллов в красном. Просто точка отсчёта.
{doneCount}/3 сегодня
14d стрик
v0.1 версия
шкалы 3 · готовые
{tests.map(t => (
onPick(t.id)}>
{t.code}{t.done?'✓ done':'start'}
{t.title}
{t.info} {t.n} ↗
))}
{last && (
<>
последний {last.label}
{last.kicker} {last.when}
{last.title}
{last.bars.map((b,i) => (
))}
>
)}
);
}
// ─── TEST COVER (intro for a single test) ─────────────────
function Cover({ test, onStart, onBack }){
const meta = {
beck: { code:'A', title:<>шкала бека >, n:'21', kind:'bdi', desc:'В каждой группе — четыре утверждения (0–3). Выберите то, что точнее всего описывает Вас на этой неделе и сегодня.', m:'~7 мин', score:'0–63', q:'21 группа', opt:'4 пункта' },
pss: { code:'B', title:<>воспринимаемый стресс >, n:'10', kind:'pss-10', desc:'Десять вопросов о прошедшем месяце. Пятибалльная шкала: никогда — почти никогда — иногда — довольно часто — часто.', m:'~3 мин', score:'0–40', q:'10 вопр.', opt:'5 опций' },
clarity: { code:'C', title:<>ясность мышления >, n:'1', kind:'vas', desc:'Визуальная аналоговая шкала. Передвиньте бегунок: от полного тумана (0) до исключительной ясности (10).', m:'~1 мин', score:'0–10', q:'1 шкала', opt:'0–10' },
}[test];
return (
← назад
{meta.code} · {meta.kind}
{meta.m}
{meta.code} · шкала · {meta.kind}
{meta.title}
{meta.desc}
время
{meta.m.replace('~','')}
// прежде чем начать:
Это self-report . Это не диагноз и не замена консультации специалиста — это точка отсчёта, которую полезно сравнить через неделю.
начать →
);
}
// ─── BECK & PSS multiple-choice test ──────────────────────
function ChoiceTest({ test, items, opts, onDone, onBack }){
const [idx, setIdx] = useState(0);
const [answers, setAnswers] = useState({}); // {id: opt index}
const cur = items[idx];
const sel = answers[cur.id];
const isLast = idx === items.length - 1;
function pick(o){
setAnswers(a => ({...a, [cur.id]: o}));
}
function next(){
if (isLast){
// compute score
let score = 0;
if (test === 'beck'){
for (const it of items){
const v = answers[it.id] ?? 0;
score += v;
}
} else {
// PSS — reverse positive items
for (const it of items){
const raw = answers[it.id] ?? 0;
score += PSS_REVERSE.has(it.id) ? (4 - raw) : raw;
}
}
onDone(score, answers);
} else {
setIdx(idx + 1);
}
}
function prev(){
if (idx > 0) setIdx(idx - 1);
}
const code = test === 'beck' ? 'A · бек' : 'B · pss-10';
const total = items.length;
return (
← назад
{code}
{idx+1}/{total}
прогресс
{idx+1} / {total}
{test === 'beck' ? `группа ${String(cur.id).padStart(2,'0')} · ${cur.title}` : `вопрос ${String(cur.id).padStart(2,'0')}`}
{test === 'beck' ? <>выберите одно утверждение, которое лучше всего описывает эту неделю и сегодня> : cur.text}
{test === 'beck' &&
группа {cur.id} — «{cur.short}»
}
{(test === 'beck' ? cur.opts : opts).map((o, i) => {
const txt = test === 'beck'
? o.replace(/^\d+ · /,'')
: o.replace(/^\d+ · /,'');
return (
pick(i)}>
{i}
{txt}
);
})}
{idx > 0 && ← пред }
{isLast ? 'готово →' : 'дальше →'}
);
}
// ─── CLARITY VAS ──────────────────────────────────────────
function ClarityTest({ onDone, onBack }){
const [val, setVal] = useState(5.0);
const trackRef = useRef(null);
const onPointer = useCallback((e) => {
const track = trackRef.current;
if (!track) return;
const rect = track.getBoundingClientRect();
const x = (e.clientX ?? e.touches?.[0]?.clientX) - rect.left;
const pct = Math.max(0, Math.min(1, x / rect.width));
setVal(Math.round(pct * 100) / 10); // 0.0 – 10.0
}, []);
function onDown(e){
onPointer(e);
const move = (ev) => onPointer(ev);
const up = () => {
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', up);
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', up);
}
return (
← назад
C · ясность · vas
1 / 1
насколько ясно вы мыслите прямо сейчас ?
{val.toFixed(1)}из 10
{[0,1,2,3,4,5,6,7,8,9,10].map(n => {n} )}
0 · туман, спутанность
чёткость · 10
onDone(val)}>сохранить →
);
}
// ─── RESULT ───────────────────────────────────────────────
function Result({ test, score, answers, onBack }){
if (test === 'beck'){
const i = interpBeck(score);
// Compute subscale breakdown (cognitive vs somatic): simplified
const cognitive = [1,2,3,5,7,8,9,12,13,14].reduce((s,id) => s + (answers[id] ?? 0), 0);
const somatic = [10,11,15,16,17,18,19,20,21].reduce((s,id) => s + (answers[id] ?? 0), 0);
return (
← в начало
A · бек · результат
21/21
результат · шкала бека
тон — {i.lab}
{score}
/ 63
{i.lab} уровень симптомов
{i.desc}
когнитивная
{cognitive} /30
self-report {new Date().toLocaleDateString('ru-RU')}
← на главную
);
}
if (test === 'pss'){
const i = interpPss(score);
return (
← в начало
B · pss-10 · результат
10/10
результат · воспринимаемый стресс
уровень — {i.lab}
{score}
/ 40
{i.lab} стресс за месяц
{i.desc}
self-report {new Date().toLocaleDateString('ru-RU')}
← на главную
);
}
// clarity
const i = interpClarity(score);
return (
← в начало
C · ясность · результат
1/1
результат · ясность мышления
сейчас — {i.lab}
{score.toFixed(1)}
/ 10
{i.lab} чёткость мыслей
{i.desc}
visual-analog сейчас
← на главную
);
}
// ─── ROOT ─────────────────────────────────────────────────
function App({ initialTest }){
// screen: 'home' | 'cover' | 'test' | 'result'
const [screen, setScreen] = useState(initialTest && initialTest !== 'home' ? 'cover' : 'home');
const [curTest, setCurTest] = useState(initialTest && initialTest !== 'home' ? initialTest : null);
const [results, setResults] = useState({});
const [lastScore, setLastScore] = useState(null);
const [lastAnswers, setLastAnswers] = useState(null);
// react to external tab change
useEffect(() => {
if (!initialTest) return;
if (initialTest === 'home') {
setScreen('home');
setCurTest(null);
} else {
setCurTest(initialTest);
setScreen('cover');
}
}, [initialTest]);
const goHome = useCallback(() => { setScreen('home'); setCurTest(null); }, []);
const startTest = useCallback(() => setScreen('test'), []);
const pickTest = useCallback((id) => { setCurTest(id); setScreen('cover'); }, []);
function finishTest(score, answers){
setLastScore(score);
setLastAnswers(answers);
// save into results
const now = new Date().toLocaleDateString('ru-RU',{day:'numeric',month:'short'});
const codeMap = { beck:'бек', pss:'pss', clarity:'ясность' };
let bars, label, title;
if (curTest === 'beck'){
const items = BECK.map(b => ({h: ((answers[b.id] ?? 0)/3) || 0.05}));
bars = items.slice(0, 12).map((x,i) => ({...x, cls: i===6?'acc':''}));
label = interpBeck(score).lab; title = `бек · ${score}/63`;
} else if (curTest === 'pss'){
const items = PSS.map(p => {
const raw = answers[p.id] ?? 0;
const v = PSS_REVERSE.has(p.id) ? (4 - raw) : raw;
return {h: v/4 || 0.05, cls: PSS_REVERSE.has(p.id) ? 'm' : (v>=3?'acc':'')};
});
bars = items;
label = interpPss(score).lab; title = `pss-10 · ${score}/40`;
} else {
bars = Array.from({length:10}, (_,i) => ({h: i < Math.round(score) ? 0.9 : 0.15, cls: i < Math.round(score) ? 'acc' : ''}));
label = interpClarity(score).lab; title = `ясность · ${score.toFixed(1)}/10`;
}
setResults(r => ({
...r,
[curTest]: { score, when: now },
last: { kicker:`${codeMap[curTest]} · ${now}`, when: now, title, label, bars }
}));
setScreen('result');
}
let content;
if (screen === 'home') content = ;
else if (screen === 'cover') content = ;
else if (screen === 'test'){
if (curTest === 'beck') content = ;
else if (curTest === 'pss') content = ;
else content = ;
}
else content = ;
return (
{content}
);
}
// ─── MOUNT + tab wiring ───────────────────────────────────
const mount = document.getElementById('ios-mount');
const root = ReactDOM.createRoot(mount);
let curTab = 'home';
function render(){ root.render( ); }
render();
// Tab clicks
document.querySelectorAll('#tabs .pg-tab').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('#tabs .pg-tab').forEach(x => x.classList.remove('active'));
b.classList.add('active');
curTab = b.dataset.test;
updateNotes(curTab);
render();
});
});
// Notes panel
const NOTES = {
home: [
{ tag:'A · home', title:<>главный экран >, p:<>Три шкалы карточками. Точка входа, статус-карта, последний замер для контекста. Без советов до результата — только название, время, число пунктов.> },
{ tag:'паттерн', title:<>тихая подача >, p:<>Большой заголовок в Instrument Serif italic, мелкие мета-метки в JetBrains Mono. Все цифры — antomic display (Anton).> },
],
beck: [
{ tag:'A · бдi', title:<>21 группа >, p:<>В каждой группе — четыре утверждения (0–3). Выбирается одно, лучше всего описывающее прошедшую неделю и сегодня .> },
{ tag:'подсчёт', title:<>сумма баллов >, p:<>Сумма по всем группам. Диапазон 0–63 : ≤9 — минимально, 10–15 — лёгкая, 16–19 — умеренная, 20–29 — выраженная, 30+ — тяжёлая.> },
{ tag:'примечание', p:<>В девятой группе — суицидальные мысли. Если ответ ≥1, в продакшене показываем горячую линию. В прототипе это пропущено.>, title:<>9-я группа > }
],
pss: [
{ tag:'B · pss-10', title:<>10 вопросов >, p:<>Шкала воспринимаемого стресса. Спрашиваем о последнем месяце: пятибалльная сетка никогда → часто (0–4).> },
{ tag:'reverse', title:<>обратные пункты >, p:<>Вопросы 4, 5, 7, 8 — о чувстве контроля. Считаются в обратном направлении: 4 − ответ.> },
{ tag:'подсчёт', title:<>сумма 0–40 >, p:<>≤13 — низкий стресс, 14–26 — умеренный, 27+ — высокий.> },
],
clarity: [
{ tag:'C · vas', title:<>визуальная шкала >, p:<>Аналоговая шкала 0–10. От «полный туман» до «исключительная ясность». Делается ежедневно — чтобы поймать тренд.> },
{ tag:'паттерн', title:<>один жест >, p:<>Драг или клик по треку — значение обновляется с шагом 0.1 . Гигантская цифра в Anton как обратная связь.> },
],
};
const NOTES_SCORING = {
beck: [
{b:'≤9', t:'минимально'},
{b:'10–15', t:'лёгкая'},
{b:'16–19', t:'умеренная', acc:true},
{b:'20+', t:'выраж/тяж'},
],
pss: [
{b:'0–13', t:'низкий'},
{b:'14–20', t:'умерен'},
{b:'21–26', t:'умерен+', acc:true},
{b:'27+', t:'высокий'},
],
clarity: [
{b:'0–2', t:'туман'},
{b:'3–4', t:'муть'},
{b:'5–7', t:'средне', acc:true},
{b:'8–10', t:'ясно'},
],
};
function updateNotes(tab){
const body = document.getElementById('notesBody');
const tag = document.getElementById('notesTag');
tag.textContent = tab;
const items = NOTES[tab] || [];
body.innerHTML = '';
const tmp = document.createElement('div');
tmp.style.cssText = 'display:flex;flex-direction:column;gap:14px';
// Render via React mini for JSX content
const cont = document.createElement('div');
cont.style.cssText = 'display:flex;flex-direction:column;gap:14px';
body.appendChild(cont);
const r = ReactDOM.createRoot(cont);
function NotesView({tab}){
const items = NOTES[tab] || [];
const sc = NOTES_SCORING[tab];
return (
<>
{items.map((it,i) => (
{it.tag} #{String(i+1).padStart(2,'0')}
{it.title}
{it.p}
))}
{sc && (
<>
пороги scoring
{sc.map((c,i) => (
{c.b} {c.t}
))}
>
)}
{tab === 'home' && (
стек type
три шрифта
Instrument Serif italic — заголовки, тёплый «бумажный» голос. Anton — большие цифры. JetBrains Mono — мета-метки, теги, пороги.
)}
>
);
}
r.render();
}
setTimeout(() => updateNotes('home'), 0);