// ============================================================ // SSHADESS — psychosocial screening (age 12+). // Faithful port of public/js/shadess.js (@be14578) — same 8 domains, // same questions, same concern_if flags, same skip toggle. // // Flow: // 1. Clinician fills Yes/No + free-text + comments per domain // (or hits "Listen in" and dictates the whole thing) // 2. Generate → POST /api/well-visit/shadess with {patientAge, // patientGender, domains, dictationText} // 3. Result auto-fills sessionStorage so the Visit Note tab can // carry it into the well-visit note (matches the vanilla // wv-shadess-text auto-fill behaviour). // ============================================================ import { useState } from 'react'; import { useMutation } from '@tanstack/react-query'; import { api, ApiError } from '@/lib/api'; import Recorder from '@/components/Recorder'; import OutputActions from '@/components/OutputActions'; interface YnQ { id: string; text: string; type: 'yn'; concern_if?: boolean } interface TxtQ { id: string; text: string; type: 'text'; placeholder?: string } type Q = YnQ | TxtQ; interface Domain { key: string; label: string; icon: string; // emoji stand-in color: string; // border tint intro: string; questions: Q[]; } // Verbatim from public/js/shadess.js — 8 domains, exact question wording. const SHADESS_DOMAINS: Domain[] = [ { key: 'strengths', label: 'Strengths', icon: '⭐', color: '#f59e0b', intro: 'Starting with what you do well helps us get to know you better.', questions: [ { id: 'str1', text: 'Has something they are proud of or enjoy', type: 'yn' }, { id: 'str2', text: 'Describes self positively when asked', type: 'yn' }, { id: 'str3', text: 'Has at least one trusted adult they can talk to', type: 'yn' }, ] }, { key: 'school', label: 'School', icon: '🎓', color: '#3b82f6', intro: 'Ask about school performance, attendance, and future plans.', questions: [ { id: 'sch1', text: 'Grades are satisfactory / doing their best', type: 'yn' }, { id: 'sch2', text: 'Likes school or finds something enjoyable about it', type: 'yn' }, { id: 'sch3', text: 'Regular attendance (no truancy concerns)', type: 'yn' }, { id: 'sch4', text: 'Has plans or goals for the future', type: 'yn' }, ] }, { key: 'home', label: 'Home', icon: '🏠', color: '#10b981', intro: 'Ask about living situation and family relationships.', questions: [ { id: 'hom1', text: 'Stable living situation', type: 'yn' }, { id: 'hom2', text: 'Gets along with people at home', type: 'yn' }, { id: 'hom3', text: 'Would talk to family member if stressed', type: 'yn' }, { id: 'hom4', text: 'Has experienced household violence or instability (concern if YES)', type: 'yn', concern_if: true }, ] }, { key: 'activities', label: 'Activities', icon: '👥', color: '#8b5cf6', intro: 'Ask about friends, hobbies, and peer relationships.', questions: [ { id: 'act1', text: 'Has friends and spends time with them', type: 'yn' }, { id: 'act2', text: 'Involved in sports, clubs, or hobbies', type: 'yn' }, { id: 'act3', text: 'Social media/screen use within healthy limits', type: 'yn' }, { id: 'act4', text: 'Has experienced bullying (concern if YES)', type: 'yn', concern_if: true }, ] }, { key: 'drugs', label: 'Drugs / Substances', icon: '💊', color: '#ef4444', intro: 'This is confidential. Ask in private.', questions: [ { id: 'drg1', text: 'Has tried cigarettes / vaping / tobacco', type: 'yn', concern_if: true }, { id: 'drg2', text: 'Has tried alcohol', type: 'yn', concern_if: true }, { id: 'drg3', text: 'Has tried marijuana or other drugs', type: 'yn', concern_if: true }, { id: 'drg4', text: 'Friends use substances', type: 'yn' }, { id: 'drg5', text: 'CRAFFT screen result (if done)', type: 'text', placeholder: 'e.g., Score 0 — low risk' }, ] }, { key: 'emotions', label: 'Emotions / Eating', icon: '❤️', color: '#ec4899', intro: 'Screen for depression, anxiety, and disordered eating.', questions: [ { id: 'emo1', text: 'Feeling down, sad, or hopeless recently', type: 'yn', concern_if: true }, { id: 'emo2', text: 'Feeling unusually stressed or anxious', type: 'yn', concern_if: true }, { id: 'emo3', text: 'Trouble sleeping', type: 'yn' }, { id: 'emo4', text: 'PHQ-A / depression screen result (if done)', type: 'text', placeholder: 'e.g., PHQ-A score 3 — minimal' }, { id: 'emo5', text: 'Happy with eating habits and body image', type: 'yn' }, { id: 'emo6', text: 'Restricting food / purging / using diet pills (concern if YES)', type: 'yn', concern_if: true }, ] }, { key: 'sexuality', label: 'Sexuality', icon: '🛡️', color: '#f97316', intro: 'Ask in private. Normalize the questions.', questions: [ { id: 'sex1', text: 'Comfortable discussing attraction/identity', type: 'yn' }, { id: 'sex2', text: 'Sexually active', type: 'yn' }, { id: 'sex3', text: 'Uses protection consistently if sexually active', type: 'yn' }, { id: 'sex4', text: 'History of unwanted sexual contact (concern if YES)', type: 'yn', concern_if: true }, { id: 'sex5', text: 'STI screening indicated/done', type: 'yn' }, ] }, { key: 'safety', label: 'Safety', icon: '🛡', color: '#6366f1', intro: 'Ask about violence, weapons, and suicidal ideation.', questions: [ { id: 'saf1', text: 'Feels safe at school and home', type: 'yn' }, { id: 'saf2', text: 'Carries a weapon (concern if YES)', type: 'yn', concern_if: true }, { id: 'saf3', text: 'Has been in physical fights recently', type: 'yn', concern_if: true }, { id: 'saf4', text: 'Wears seatbelt; safe driving practices', type: 'yn' }, { id: 'saf5', text: 'Thoughts of hurting self or suicide (concern if YES — STAT eval)', type: 'yn', concern_if: true }, { id: 'saf6', text: 'Columbia/ASQ suicide screen result (if done)', type: 'text', placeholder: 'e.g., ASQ: negative' }, ] } ]; interface DomainAnswers { questions: Record; // qid -> 'yes'|'no'|free text comment: string; concern: boolean; skipped: boolean; } type AnswersMap = Record; interface ShadessOk { success: true; assessment: string; model: string } const input = 'rounded-md border border-input bg-background px-3 py-2 text-sm'; const btn = 'inline-flex items-center gap-1 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium hover:bg-muted disabled:opacity-50'; const btnPrimary = 'inline-flex items-center gap-1 rounded-md bg-primary text-primary-foreground px-3 py-2 text-sm font-semibold disabled:opacity-50'; function emptyAnswers(): AnswersMap { const out: AnswersMap = {}; SHADESS_DOMAINS.forEach((d) => { out[d.key] = { questions: {}, comment: '', concern: false, skipped: false }; }); return out; } export default function Shadess() { const [patientAge, setPatientAge] = useState(''); const [patientGender, setPatientGender] = useState(''); const [answers, setAnswers] = useState(emptyAnswers); const [dictationText, setDictationText] = useState(''); const [recError, setRecError] = useState(null); const [result, setResult] = useState(null); const [msg, setMsg] = useState<{ kind: 'ok' | 'err'; text: string } | null>(null); const generate = useMutation({ mutationFn: async () => { const domains: Record = {}; SHADESS_DOMAINS.forEach((d) => { const a = answers[d.key]; const qs = d.questions .map((q) => ({ id: q.id, text: q.text, answer: a.questions[q.id] || '' })) .filter((q) => q.answer !== ''); domains[d.key] = { skipped: a.skipped, comment: a.comment, concern: a.concern, questions: qs }; }); const hasData = Object.values(domains).some((d) => !d.skipped && (d.questions.length > 0 || d.comment)); if (!hasData && !dictationText.trim()) { throw new Error('Fill in at least one domain or dictate something'); } return api.post('/api/well-visit/shadess', { patientAge, patientGender, domains, dictationText: dictationText.trim() || null, }); }, onSuccess: (d) => { setResult(d.assessment); setMsg({ kind: 'ok', text: 'Generated' }); try { sessionStorage.setItem('wv-shadess-assessment', d.assessment); } catch { /* ignore */ } }, onError: (e) => setMsg({ kind: 'err', text: (e as ApiError).message || 'Generation failed' }), }); function setQ(domainKey: string, qid: string, value: string, concernIf?: boolean) { setAnswers((m) => { const cur = m[domainKey]; const newQuestions = { ...cur.questions, [qid]: value }; // Auto-flag concern when the answer matches the concern_if rule. let concern = cur.concern; if (concernIf !== undefined && (value === 'yes') === concernIf) { concern = true; } return { ...m, [domainKey]: { ...cur, questions: newQuestions, concern } }; }); } function setComment(domainKey: string, value: string) { setAnswers((m) => ({ ...m, [domainKey]: { ...m[domainKey], comment: value } })); } function toggleSkip(domainKey: string) { setAnswers((m) => ({ ...m, [domainKey]: { ...m[domainKey], skipped: !m[domainKey].skipped } })); } function toggleConcern(domainKey: string) { setAnswers((m) => ({ ...m, [domainKey]: { ...m[domainKey], concern: !m[domainKey].concern } })); } function clearAll() { setAnswers(emptyAnswers()); setDictationText(''); setResult(null); setMsg(null); } return (
Recommended age 12 and older. Ask in private.
Listen in (optional dictation) { setDictationText((prev) => meta.appended ? (prev ? prev + ' ' + text : text) : text); setRecError(null); }} onError={(m) => setRecError(m)} /> {recError &&
{recError}
}