Download app.py from garrec2/mocaTest: direct link, hf CLI and curl.
- Browser
- Download file 25.9 kB
-
https://huggingface.co/spaces/garrec2/mocaTest/resolve/main/app.py
- Command line
-
hf download hf://spaces/garrec2/mocaTest/app.py
-
curl -L -o app.py https://huggingface.co/spaces/garrec2/mocaTest/resolve/main/app.py
25.9 kB
| """ | |
| Interactive Montreal Cognitive Assessment (MoCA) v8.1 | |
| Streamlit App for Hugging Face Spaces Deployment | |
| To deploy: | |
| 1. Create a Hugging Face Space (https://huggingface.co/spaces) | |
| 2. Choose "Streamlit" as SDK | |
| 3. Upload this file as app.py | |
| 4. Create a requirements.txt with: streamlit, pandas, matplotlib, numpy | |
| """ | |
| import streamlit as st | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| from datetime import datetime | |
| import json | |
| # Page config | |
| st.set_page_config( | |
| page_title="MoCA Assessment", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Custom CSS | |
| st.markdown(""" | |
| <style> | |
| .main { | |
| padding: 1rem; | |
| } | |
| .stAlert { | |
| margin: 1rem 0; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| st.title("π§ Interactive Montreal Cognitive Assessment (MoCA) v8.1") | |
| st.markdown(""" | |
| --- | |
| **π About this tool:** | |
| - Digital implementation of the Montreal Cognitive Assessment | |
| - Assessment takes approximately 10-15 minutes | |
| - For research and educational purposes | |
| **β οΈ DISCLAIMER:** | |
| Clinical interpretation must be done by a qualified healthcare professional. | |
| Do NOT use this for diagnosis without professional oversight. | |
| **Normal Score:** β₯26/30 (add 1 point if education β€12 years) | |
| --- | |
| """) | |
| # Initialize session state | |
| if 'test_data' not in st.session_state: | |
| st.session_state.test_data = { | |
| 'demographics': {}, | |
| 'responses': {}, | |
| 'scores': {}, | |
| 'timestamp': datetime.now().isoformat() | |
| } | |
| test_data = st.session_state.test_data | |
| # Sidebar navigation | |
| with st.sidebar: | |
| st.header("π Assessment Steps") | |
| step = st.radio( | |
| "Select Step:", | |
| [ | |
| "1οΈβ£ Demographics", | |
| "2οΈβ£ Visuospatial", | |
| "3οΈβ£ Naming", | |
| "4οΈβ£ Memory (Learning)", | |
| "5οΈβ£ Attention", | |
| "6οΈβ£ Language", | |
| "7οΈβ£ Delayed Recall", | |
| "8οΈβ£ Orientation", | |
| "π Results" | |
| ] | |
| ) | |
| st.divider() | |
| # Progress indicator | |
| completed_sections = len([s for s in test_data['scores'].keys() if s != 'education_bonus' and s != 'mis']) | |
| st.metric("Progress", f"{completed_sections}/8 sections") | |
| # ===== STEP 1: DEMOGRAPHICS ===== | |
| if "1οΈβ£" in step: | |
| st.header("π€ Participant Information") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| name = st.text_input( | |
| "Name (optional):", | |
| value=test_data['demographics'].get('name', ''), | |
| help="For record keeping only" | |
| ) | |
| age = st.slider( | |
| "Age:", | |
| min_value=18, | |
| max_value=100, | |
| value=test_data['demographics'].get('age', 65), | |
| step=1 | |
| ) | |
| with col2: | |
| education = st.slider( | |
| "Years of Education:", | |
| min_value=0, | |
| max_value=20, | |
| value=test_data['demographics'].get('education', 12), | |
| step=1, | |
| help="Total formal education completed" | |
| ) | |
| sex = st.selectbox( | |
| "Sex:", | |
| ["Not Specified", "Male", "Female", "Other"], | |
| index=["Not Specified", "Male", "Female", "Other"].index( | |
| test_data['demographics'].get('sex', "Not Specified") | |
| ) | |
| ) | |
| language = st.selectbox( | |
| "Native Language:", | |
| ["English", "Other"], | |
| index=0 if test_data['demographics'].get('language') == "English" else 1 | |
| ) | |
| if st.button("β Save Demographics & Continue", key="demo_btn", use_container_width=True): | |
| test_data['demographics'] = { | |
| 'name': name, | |
| 'age': age, | |
| 'education': education, | |
| 'sex': sex, | |
| 'language': language | |
| } | |
| st.session_state.test_data = test_data | |
| st.success("β Demographics saved! Proceed to next step.") | |
| # ===== STEP 2: VISUOSPATIAL ===== | |
| elif "2οΈβ£" in step: | |
| st.header("π¨ Visuospatial & Executive Function") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.subheader("1οΈβ£ Trail Making Test") | |
| st.info(""" | |
| **Instructions:** Draw a line from number to letter in ascending order: | |
| 1 β A β 2 β B β 3 β C β 4 β D β 5 β E | |
| **Important:** Lines should not cross. Do not connect E back to 1. | |
| """) | |
| trail = st.radio( | |
| "Result:", | |
| ["β Completed correctly (no crossing lines)", "β Errors or lines crossed"], | |
| key="trail_radio" | |
| ) | |
| with col2: | |
| st.subheader("2οΈβ£ Cube Drawing") | |
| st.info(""" | |
| **Instructions:** Copy this cube drawing as accurately as you can. | |
| **Scoring:** 3-dimensional, all lines drawn, lines meet with little space, | |
| no extra lines, lines relatively parallel. | |
| """) | |
| cube = st.radio( | |
| "Result:", | |
| ["β Correctly executed", "β Errors in drawing"], | |
| key="cube_radio" | |
| ) | |
| st.subheader("3οΈβ£ Clock Drawing") | |
| st.info("**Instructions:** Draw a clock. Put in all numbers and set time to 10 past 11.") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| clock_contour = st.checkbox("β Contour correct (circle/square)") | |
| with col2: | |
| clock_numbers = st.checkbox("β All numbers present & correct order") | |
| with col3: | |
| clock_hands = st.checkbox("β Hands correct (short hour, long minute, centered)") | |
| if st.button("β Save Visuospatial & Continue", key="vis_btn", use_container_width=True): | |
| trail_pts = 1 if "β" in trail else 0 | |
| cube_pts = 1 if "β" in cube else 0 | |
| clock_pts = sum([clock_contour, clock_numbers, clock_hands]) | |
| test_data['responses']['visuospatial'] = { | |
| 'trail': trail, 'cube': cube, | |
| 'clock_contour': clock_contour, | |
| 'clock_numbers': clock_numbers, | |
| 'clock_hands': clock_hands | |
| } | |
| test_data['scores']['visuospatial'] = trail_pts + cube_pts + clock_pts | |
| st.session_state.test_data = test_data | |
| st.success(f"β Visuospatial saved! Points: {trail_pts + cube_pts + clock_pts}/5") | |
| # ===== STEP 3: NAMING ===== | |
| elif "3οΈβ£" in step: | |
| st.header("π¦ Naming Test") | |
| st.info(""" | |
| **Instructions:** The examiner points to three animals. | |
| What name did the participant give for each? | |
| """) | |
| st.markdown("### Animals:") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.markdown("**Animal 1: Lion**") | |
| animal1 = st.text_input("Participant's answer:", key="animal1", placeholder="e.g., lion") | |
| with col2: | |
| st.markdown("**Animal 2: Rhinoceros**") | |
| animal2 = st.text_input("Participant's answer:", key="animal2", placeholder="e.g., rhino") | |
| with col3: | |
| st.markdown("**Animal 3: Camel**") | |
| animal3 = st.text_input("Participant's answer:", key="animal3", placeholder="e.g., camel") | |
| if st.button("β Save Naming & Continue", key="naming_btn", use_container_width=True): | |
| score = 0 | |
| if 'lion' in animal1.lower(): | |
| score += 1 | |
| if any(x in animal2.lower() for x in ['rhino', 'rhinoceros']): | |
| score += 1 | |
| if any(x in animal3.lower() for x in ['camel', 'dromedary']): | |
| score += 1 | |
| test_data['responses']['naming'] = { | |
| 'animal1': animal1, 'animal2': animal2, 'animal3': animal3 | |
| } | |
| test_data['scores']['naming'] = score | |
| st.session_state.test_data = test_data | |
| st.success(f"β Naming saved! Points: {score}/3") | |
| # ===== STEP 4: MEMORY LEARNING ===== | |
| elif "4οΈβ£" in step: | |
| st.header("π§ Memory Test - Learning Phase") | |
| st.warning(""" | |
| β οΈ **IMPORTANT:** The participant must REMEMBER these words. | |
| They will be tested on them later in the assessment. | |
| """) | |
| st.markdown(""" | |
| ### π WORDS TO READ (at rate of 1 word per second): | |
| # **FACE β’ VELVET β’ CHURCH β’ DAISY β’ RED** | |
| """) | |
| st.info(""" | |
| **Examiner Instructions:** | |
| "This is a memory test. I am going to read a list of words that you will | |
| have to remember now and later on. Listen carefully. When I am through, | |
| tell me as many words as you can remember." | |
| """) | |
| if st.button("β Participant Heard Words - Continue to Attention", | |
| key="memory_btn", use_container_width=True): | |
| test_data['responses']['memory_words'] = ['FACE', 'VELVET', 'CHURCH', 'DAISY', 'RED'] | |
| st.session_state.test_data = test_data | |
| st.success("β Memory encoding recorded. Proceed to Attention section.") | |
| # ===== STEP 5: ATTENTION ===== | |
| elif "5οΈβ£" in step: | |
| st.header("ποΈ Attention Tests") | |
| with st.container(border=True): | |
| st.subheader("1οΈβ£ Forward Digit Span") | |
| st.markdown("**Read at 1 digit/second:** 2 1 8 5 4") | |
| digit_forward = st.text_input( | |
| "Participant repeated:", | |
| placeholder="e.g., 2 1 8 5 4 or 2-1-8-5-4", | |
| key="digit_forward" | |
| ) | |
| with st.container(border=True): | |
| st.subheader("2οΈβ£ Backward Digit Span") | |
| st.markdown("**Read at 1 digit/second:** 7 4 2") | |
| st.markdown("*Correct backward answer: 2 4 7*") | |
| digit_backward = st.text_input( | |
| "Participant repeated:", | |
| placeholder="e.g., 2 4 7", | |
| key="digit_backward" | |
| ) | |
| with st.container(border=True): | |
| st.subheader("3οΈβ£ Vigilance (Tap on Letter A)") | |
| st.markdown("**Read letters at 1/second. Participant taps on every 'A':**") | |
| st.markdown("F B A C M N A A J K L B A F A K D E A A A J A M O F A A B") | |
| vig_errors = st.slider("Errors (tapped wrong or missed A):", 0, 10, value=0, key="vig_errors") | |
| with st.container(border=True): | |
| st.subheader("4οΈβ£ Serial 7 Subtraction") | |
| st.markdown(""" | |
| **Instruction:** "Count backward by 7s starting from 100." | |
| Expected sequence: 93 β 86 β 79 β 72 β 65 | |
| """) | |
| serial7_correct = st.slider( | |
| "Number of correct subtractions:", | |
| 0, 5, value=0, key="serial7_correct" | |
| ) | |
| if st.button("β Save Attention & Continue", key="attention_btn", use_container_width=True): | |
| score = 0 | |
| score += 1 if '2 1 8 5 4' in digit_forward.replace('-', ' ').replace(',', ' ') else 0 | |
| score += 1 if '2 4 7' in digit_backward.replace('-', ' ').replace(',', ' ') else 0 | |
| score += 1 if vig_errors <= 1 else 0 | |
| if serial7_correct >= 4: | |
| score += 3 | |
| elif serial7_correct >= 2: | |
| score += 2 | |
| elif serial7_correct >= 1: | |
| score += 1 | |
| test_data['responses']['attention'] = { | |
| 'digit_forward': digit_forward, | |
| 'digit_backward': digit_backward, | |
| 'vigilance_errors': vig_errors, | |
| 'serial7_correct': serial7_correct | |
| } | |
| test_data['scores']['attention'] = score | |
| st.session_state.test_data = test_data | |
| st.success(f"β Attention saved! Points: {score}/8") | |
| # ===== STEP 6: LANGUAGE ===== | |
| elif "6οΈβ£" in step: | |
| st.header("π¬ Language Tests") | |
| with st.container(border=True): | |
| st.subheader("1οΈβ£ Sentence Repetition") | |
| st.markdown(""" | |
| **Instruction:** "I am going to read you a sentence. | |
| Repeat it exactly as I say it." | |
| """) | |
| st.markdown("**Sentence 1:**") | |
| st.markdown('> "I only know that John is the one to help today."') | |
| sent1 = st.checkbox( | |
| "β Repeated exactly (watch for omissions, substitutions, grammar errors)", | |
| key="sent1" | |
| ) | |
| st.markdown("**Sentence 2:**") | |
| st.markdown('> "The cat always hid under the couch when dogs were in the room."') | |
| sent2 = st.checkbox( | |
| "β Repeated exactly", | |
| key="sent2" | |
| ) | |
| with st.container(border=True): | |
| st.subheader("2οΈβ£ Verbal Fluency (Letter F)") | |
| st.markdown(""" | |
| **Instruction:** "Tell me as many words as you can think of | |
| that begin with the letter F. I will tell you to stop after 60 seconds." | |
| *Rules: No proper nouns, numbers, or different verb forms* | |
| """) | |
| fluency = st.slider( | |
| "Number of valid words starting with F:", | |
| 0, 30, value=0, key="fluency" | |
| ) | |
| with st.container(border=True): | |
| st.subheader("3οΈβ£ Abstraction") | |
| st.markdown("**Pair 1: Train and Bicycle**") | |
| st.markdown("*Acceptable answers: means of transportation, means of travel, you take trips*") | |
| st.markdown("*NOT acceptable: they have wheels*") | |
| abstract1 = st.radio( | |
| "Response:", | |
| ["β Correct (transportation/travel)", "β Incorrect or concrete"], | |
| key="abstract1" | |
| ) | |
| st.markdown("**Pair 2: Ruler and Watch**") | |
| st.markdown("*Acceptable answers: measuring instruments, used to measure*") | |
| st.markdown("*NOT acceptable: they have numbers*") | |
| abstract2 = st.radio( | |
| "Response:", | |
| ["β Correct (measuring instruments)", "β Incorrect or concrete"], | |
| key="abstract2" | |
| ) | |
| if st.button("β Save Language & Continue", key="language_btn", use_container_width=True): | |
| score = 0 | |
| score += int(sent1) | |
| score += int(sent2) | |
| score += 1 if fluency >= 11 else 0 | |
| score += 1 if "β" in abstract1 else 0 | |
| score += 1 if "β" in abstract2 else 0 | |
| test_data['responses']['language'] = { | |
| 'sent1': sent1, 'sent2': sent2, 'fluency': fluency, | |
| 'abstract1': abstract1, 'abstract2': abstract2 | |
| } | |
| test_data['scores']['language'] = score | |
| st.session_state.test_data = test_data | |
| st.success(f"β Language saved! Points: {score}/5") | |
| # ===== STEP 7: DELAYED RECALL ===== | |
| elif "7οΈβ£" in step: | |
| st.header("π§ Delayed Recall & Memory Index Score") | |
| st.warning("β οΈ **Recall the 5 words from earlier:**") | |
| st.markdown("# **FACE β’ VELVET β’ CHURCH β’ DAISY β’ RED**") | |
| with st.container(border=True): | |
| st.subheader("Spontaneous Recall (WITHOUT CUES)") | |
| st.markdown("Which words did participant recall without any help?") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| recall_face = st.checkbox("FACE", key="recall_face") | |
| recall_velvet = st.checkbox("VELVET", key="recall_velvet") | |
| with col2: | |
| recall_church = st.checkbox("CHURCH", key="recall_church") | |
| recall_daisy = st.checkbox("DAISY", key="recall_daisy") | |
| with col3: | |
| recall_red = st.checkbox("RED", key="recall_red") | |
| with st.container(border=True): | |
| st.subheader("With Category Cues (if needed)") | |
| st.markdown("For words not recalled, give category hints:") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| cue_face = st.checkbox("FACE (body part)", key="cue_face") | |
| cue_velvet = st.checkbox("VELVET (fabric)", key="cue_velvet") | |
| with col2: | |
| cue_church = st.checkbox("CHURCH (building)", key="cue_church") | |
| cue_daisy = st.checkbox("DAISY (flower)", key="cue_daisy") | |
| with col3: | |
| cue_red = st.checkbox("RED (color)", key="cue_red") | |
| with st.container(border=True): | |
| st.subheader("With Multiple-Choice Only") | |
| st.markdown("Words only recalled when given multiple choice options:") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| mult_face = st.checkbox("FACE (MC only)", key="mult_face") | |
| mult_velvet = st.checkbox("VELVET (MC only)", key="mult_velvet") | |
| with col2: | |
| mult_church = st.checkbox("CHURCH (MC only)", key="mult_church") | |
| mult_daisy = st.checkbox("DAISY (MC only)", key="mult_daisy") | |
| with col3: | |
| mult_red = st.checkbox("RED (MC only)", key="mult_red") | |
| if st.button("β Save Delayed Recall & Continue", key="memory_recall_btn", use_container_width=True): | |
| spontaneous = sum([ | |
| recall_face, recall_velvet, recall_church, recall_daisy, recall_red | |
| ]) | |
| category = sum([ | |
| cue_face and not recall_face, | |
| cue_velvet and not recall_velvet, | |
| cue_church and not recall_church, | |
| cue_daisy and not recall_daisy, | |
| cue_red and not recall_red | |
| ]) | |
| multiple = sum([ | |
| mult_face and not recall_face and not cue_face, | |
| mult_velvet and not recall_velvet and not cue_velvet, | |
| mult_church and not recall_church and not cue_church, | |
| mult_daisy and not recall_daisy and not cue_daisy, | |
| mult_red and not recall_red and not cue_red | |
| ]) | |
| mis = (spontaneous * 3) + (category * 2) + (multiple * 1) | |
| test_data['responses']['delayed_recall'] = { | |
| 'spontaneous': spontaneous, | |
| 'category': category, | |
| 'multiple': multiple | |
| } | |
| test_data['scores']['delayed_recall'] = spontaneous | |
| test_data['scores']['mis'] = mis | |
| st.session_state.test_data = test_data | |
| st.success(f""" | |
| β Delayed Recall Saved! | |
| - Spontaneous: {spontaneous}/5 points | |
| - Memory Index Score (MIS): {mis}/15 points | |
| """) | |
| # ===== STEP 8: ORIENTATION ===== | |
| elif "8οΈβ£" in step: | |
| st.header("π Orientation") | |
| today = datetime.now() | |
| st.info(f"Today's date should be: **{today.strftime('%A, %B %d, %Y')}**") | |
| st.markdown("### Check which orientation items are correct:") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| orient_date = st.checkbox("β Date correct (exact, no Β±1 day error)") | |
| orient_month = st.checkbox("β Month correct") | |
| orient_year = st.checkbox("β Year correct") | |
| with col2: | |
| orient_day = st.checkbox("β Day of week correct") | |
| orient_place = st.checkbox("β Place/Location correct") | |
| with col3: | |
| orient_city = st.checkbox("β City correct") | |
| if st.button("β Complete Assessment", key="orient_btn", use_container_width=True): | |
| score = sum([ | |
| orient_date, orient_month, orient_year, | |
| orient_day, orient_place, orient_city | |
| ]) | |
| test_data['scores']['orientation'] = score | |
| st.session_state.test_data = test_data | |
| st.success(f"β Orientation saved! Points: {score}/6") | |
| st.balloons() | |
| # ===== RESULTS PAGE ===== | |
| elif "π" in step: | |
| st.header("π Assessment Results & Scoring") | |
| def calc_total(): | |
| total = sum([ | |
| test_data['scores'].get('visuospatial', 0), | |
| test_data['scores'].get('naming', 0), | |
| test_data['scores'].get('attention', 0), | |
| test_data['scores'].get('language', 0), | |
| test_data['scores'].get('delayed_recall', 0), | |
| test_data['scores'].get('orientation', 0) | |
| ]) | |
| if test_data['demographics'].get('education', 0) <= 12: | |
| total += 1 | |
| return min(total, 30) | |
| total_score = calc_total() | |
| is_normal = total_score >= 26 | |
| # Summary table | |
| summary_data = { | |
| 'Domain': ['Visuospatial/Executive', 'Naming', 'Attention', 'Language', 'Delayed Recall', 'Orientation'], | |
| 'Points': [ | |
| test_data['scores'].get('visuospatial', 0), | |
| test_data['scores'].get('naming', 0), | |
| test_data['scores'].get('attention', 0), | |
| test_data['scores'].get('language', 0), | |
| test_data['scores'].get('delayed_recall', 0), | |
| test_data['scores'].get('orientation', 0) | |
| ], | |
| 'Max': [5, 3, 8, 5, 5, 6] | |
| } | |
| df = pd.DataFrame(summary_data) | |
| st.subheader("Score Breakdown by Domain") | |
| st.dataframe(df, use_container_width=True, hide_index=True) | |
| # Key metrics | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.metric("π Total Score", f"{total_score}/30", | |
| delta="Normal" if is_normal else "Possible Impairment") | |
| with col2: | |
| st.metric("π Status", "β Normal" if is_normal else "β Below 26") | |
| with col3: | |
| st.metric("π§ MIS Score", f"{test_data['scores'].get('mis', 0)}/15", | |
| help="Memory Index Score - shows type of memory deficit") | |
| with col4: | |
| st.metric("π€ Participant", | |
| test_data['demographics'].get('name', 'Not provided'), | |
| test_data['demographics'].get('age', 'N/A')) | |
| # Visualizations | |
| st.subheader("Score Visualization") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| fig1, ax1 = plt.subplots(figsize=(10, 5)) | |
| scores = df['Points'].tolist() | |
| max_scores = df['Max'].tolist() | |
| x = np.arange(len(df['Domain'])) | |
| ax1.bar(x - 0.2, scores, 0.4, label='Participant Score', color='steelblue', alpha=0.8) | |
| ax1.bar(x + 0.2, max_scores, 0.4, label='Maximum', color='lightgray', alpha=0.5) | |
| ax1.set_ylabel('Points', fontsize=11) | |
| ax1.set_title('MoCA Score by Cognitive Domain', fontsize=12, fontweight='bold') | |
| ax1.set_xticks(x) | |
| ax1.set_xticklabels(df['Domain'], rotation=45, ha='right', fontsize=9) | |
| ax1.legend() | |
| ax1.grid(axis='y', alpha=0.3, linestyle='--') | |
| ax1.set_ylim(0, 10) | |
| plt.tight_layout() | |
| st.pyplot(fig1, use_container_width=True) | |
| with col2: | |
| fig2, ax2 = plt.subplots(figsize=(8, 6)) | |
| colors = ['#2ecc71' if is_normal else '#e74c3c', '#ecf0f1'] | |
| explode = (0.1, 0) | |
| wedges, texts, autotexts = ax2.pie( | |
| [total_score, 30 - total_score], | |
| labels=[f'Scored: {total_score}', f'Remaining: {30 - total_score}'], | |
| autopct='%1.1f%%', | |
| colors=colors, | |
| explode=explode, | |
| startangle=90, | |
| textprops={'fontsize': 11} | |
| ) | |
| status_text = "β NORMAL COGNITION" if is_normal else "β POSSIBLE IMPAIRMENT" | |
| ax2.set_title(f'Overall Score\n{status_text}', fontsize=13, fontweight='bold') | |
| st.pyplot(fig2, use_container_width=True) | |
| # Interpretation | |
| st.divider() | |
| st.subheader("π Clinical Interpretation") | |
| if is_normal: | |
| st.success(f""" | |
| **β NORMAL COGNITION** | |
| Total Score: **{total_score}/30** | |
| The participant's performance falls within the normal range for cognitive function. | |
| A score of 26 or above is considered normal on the MoCA. | |
| """) | |
| else: | |
| st.warning(f""" | |
| **β POSSIBLE COGNITIVE IMPAIRMENT** | |
| Total Score: **{total_score}/30** | |
| The participant's score is below 26, suggesting possible mild cognitive impairment. | |
| **Areas of concern:** | |
| """) | |
| for idx, row in df.iterrows(): | |
| if row['Points'] < (row['Max'] * 0.75): # <75% correct | |
| st.markdown(f"- **{row['Domain']}:** {row['Points']}/{row['Max']} points") | |
| # Memory Index interpretation | |
| st.info(f""" | |
| **Memory Index Score (MIS): {test_data['scores'].get('mis', 0)}/15** | |
| The MIS reveals the nature of memory problems: | |
| - **High spontaneous recall:** Memory retrieval is intact | |
| - **Improved with cues:** Retrieval problem (generally better prognosis) | |
| - **No improvement with cues:** Encoding problem (suggests more severe impairment) | |
| """) | |
| # Export section | |
| st.divider() | |
| st.subheader("πΎ Export Results") | |
| # Prepare export data | |
| export_df = df.copy() | |
| export_df.loc[len(export_df)] = ['TOTAL', total_score, 30] | |
| export_df.loc[len(export_df)] = ['MIS Score', test_data['scores'].get('mis', 0), 15] | |
| csv = export_df.to_csv(index=False) | |
| json_str = json.dumps(test_data, indent=2, default=str) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.download_button( | |
| label="π₯ Download as CSV", | |
| data=csv, | |
| file_name=f"moca_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", | |
| mime="text/csv" | |
| ) | |
| with col2: | |
| st.download_button( | |
| label="π₯ Download as JSON", | |
| data=json_str, | |
| file_name=f"moca_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", | |
| mime="application/json" | |
| ) | |
| # Critical disclaimer | |
| st.divider() | |
| st.error(""" | |
| ### β οΈ CRITICAL DISCLAIMER | |
| **This digital assessment is for RESEARCH AND EDUCATIONAL PURPOSES ONLY.** | |
| Clinical interpretation must ONLY be performed by: | |
| - A qualified healthcare professional (MD, DO, NP, PA) | |
| - With training in cognitive assessment | |
| - Who understands the participant's complete medical history | |
| - Who knows about current medications and their cognitive effects | |
| - Who can assess the overall clinical context and comorbidities | |
| **DO NOT USE THIS FOR DIAGNOSIS WITHOUT PROFESSIONAL OVERSIGHT.** | |
| Results should be reviewed with a healthcare provider before any clinical decisions are made. | |
| """) | |
| # Footer | |
| st.divider() | |
| st.markdown(""" | |
| <div style='text-align: center; color: gray; font-size: 0.85em;'> | |
| <p>Interactive MoCA v8.1 Assessment Tool | Based on the Montreal Cognitive Assessment</p> | |
| <p>Original MoCA Β© Z. Nasreddine MD | www.mocatest.org</p> | |
| <p>This implementation is for research and educational purposes only.</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |