diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..0b5a4c0
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,4 @@
+.venv
+.uv
+__pycache__
+*.pyc
\ No newline at end of file
diff --git a/.streamlit/config.toml b/.streamlit/config.toml
new file mode 100644
index 0000000..74812cd
--- /dev/null
+++ b/.streamlit/config.toml
@@ -0,0 +1,2 @@
+[client]
+showSidebarNavigation = false
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..ea8a0b8
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,36 @@
+# Use the official uv image for the binary
+FROM ghcr.io/astral-sh/uv:latest AS uv_bin
+
+# Use the official Python 3.13 slim image
+FROM python:3.13-slim
+
+# Declare build-time arguments
+ARG GIT_BRANCH
+ARG GIT_COMMIT
+
+# Set them as environment variables so the app can see them
+ENV APP_GIT_BRANCH=$GIT_BRANCH
+ENV APP_GIT_COMMIT=$GIT_COMMIT
+
+# Copy uv binaries
+COPY --from=uv_bin /uv /uvx /bin/
+
+WORKDIR /app
+
+# Enable bytecode compilation for faster startup in Python 3.13
+ENV UV_COMPILE_BYTECODE=1
+
+# Copy lockfiles
+COPY pyproject.toml uv.lock ./
+
+# Install dependencies
+# We use --system because we are inside a dedicated container
+RUN uv pip install --system --no-cache -r pyproject.toml
+
+# Copy the rest of your router app
+COPY . .
+
+EXPOSE 8501
+
+# Entrypoint remains the same
+ENTRYPOINT ["streamlit", "run", "main.py", "--server.port=8501", "--server.address=0.0.0.0"]
\ No newline at end of file
diff --git a/README.md b/README.md
index d9eab2b..93165ef 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,28 @@
# Arwen
-DSM-5 Diagnostic Criteria aggregator and formatter for EHRs
+
+## About
+
+**Arwen DCA** is designed to help clinicians aggregate diagnostic criteria based on the structure found in the DSM-5-TR.
+
+> [!CAUTION]
+> This is **not** a diagnostic tool and should not be used as such. Clinicians should always use their own judgement and verify that criteria and codes are correct.
+
+Clincians can select all diagnostic criteria that apply to their patient/client and the tool will output a list of criteria met formatted in a way that can be easily copied into an EHR or note (including systems which parse criteria to :sparkles: automagically* :sparkles: create a note)
+
+The tool does **not store any data** that is submitted and **does not allow the input of any PHI**. When a user refreshes a page or navigates away, all data is irrevocably lost.
+
+This project is open source and available on [GitHub](https://github.com/hermaplusplus/Arwen). It is provided under the [MIT License](https://github.com/hermaplusplus/Arwen/blob/main/LICENSE). Contributions, issue reports, feedback, and suggestions are welcome.
+
+Why is the tool called 'Arwen'? I watched Lord of the Rings recently. That's it.
+
+* Automagically, meaning using a Large Language Model.
+
+## Development
+
+Arwen uses [uv](https://docs.astral.sh/uv/) as a package manager and [Docker Compose](https://docs.docker.com/compose/) for containerisation. To run the project, make sure you have these installed, as well as Python 3.11+.
+
+To activate the virtual environment, run the appropriate (for your system) script under `./.venv/Scripts/`.
+
+To run the project during development, run `streamlit run main.py`.
+
+To deploy the project in production, run `./start.sh`.
diff --git a/data/adhd.yaml b/data/adhd.yaml
new file mode 100644
index 0000000..39357b0
--- /dev/null
+++ b/data/adhd.yaml
@@ -0,0 +1,36 @@
+A: "A persistent pattern of inattention and/or hyperactivityimpulsivity that interferes with functioning or development, as characterized by (1) and/or (2):"
+A1: "Inattention: Six (or more) of the following symptoms have persisted for at least 6 months to a degree that is inconsistent with developmental level and that negatively impacts directly on social and academic/occupational activities:"
+A1note: "Note: The symptoms are not solely a manifestation of oppositional behavior, defiance, hostility, or failure to understand tasks or instructions. For older adolescents ad adults (age 17 and older), at least five symptoms are required."
+A1a: "Fails to give close attention to detail, makes careless mistakes, overlooks or misses details, work is inaccurate."
+A1b: "Difficulty focusing."
+A1c: "Does not seem to listen when spoken to directly, even in the absence of any obvious distraction."
+A1d: "Struggles to follow through on instructions and fails to finish tasks, starts tasks but quickly loses focus and is easily sidetracked."
+A1e: "Difficulty organizing tasks and activities, difficulty managing sequential tasks, difficulty keeping materials and belongings in order, messy, disorganized work, has poor time management, fails to meet deadlines."
+A1f: "Avoids, dislikes, or is reluctant to engage in tasks that require sustained mental effort."
+A1g: "Loses things necessary for tasks or activities."
+A1h: "Easily distracted by extraneous stimuli."
+A1i: "Forgetful in daily activities."
+A2: "Hyperactivity and Impulsivity: Six (or more) of the following symptoms have persisted for at least 6 months to a degree that is inconsistent with developmental level and that negatively impacts directly on social and academic/occupational activities:"
+A2note: "Note: The symptoms are not solely a manifestation of oppositional behavior, defiance, hostility, or failure to understand tasks or instructions. For older adolescents ad adults (age 17 and older), at least five symptoms are required."
+A2a: "Fidgets with or taps hands or feet or squirms in seat."
+A2b: "Leaves seat in situations when remaining seated is expected."
+A2c: "Feeling restless."
+A2d: "Unable to play or engage in leisure activities quietly."
+A2e: "Is often 'on the go,' acting as if 'driven by a motor'."
+A2f: "Talks excessively."
+A2g: "Blurts out an answer before a question has been completed, completes people's sentences, cannot wait for turn in conversation."
+A2h: "Difficulty waiting his or her turn."
+A2i: "Interrupts or intrudes on others, butts into conversations, games, or activities, may start using other people's things without asking or receiving permission, may intrude into or take over what others are doing."
+B: "Several inattentive or hyperactive-impulsive symptoms were present prior to age 12 years."
+C: "Several inattentive or hyperactive-impulsive symptoms are present in two or more settings (e.g., at home, school, or work; with friends or relatives; in other activities)."
+D: "There is clear evidence that the symptoms interfere with, or reduce the quality of, social, academic, or occupational functioning."
+E: "The symptoms do not occur exclusively during the course of schizophrenia or another psychotic disorder and are not better explained by another mental disorder (e.g., mood disorder, anxiety disorder, dissociative disorder, personality disorder, substance intoxication or withdrawal)."
+S1a: "Both inattentive and hyperactive-impulsive presentation: If both Criterion A1 and Criterion A2 are met for the past 6 months."
+S1b: "Predominantly inattentive presentation: If Criterion A1 is met but Criterion A2 is not met for the past 6 months."
+S1c: "Predominantly hyperactive-impulsive presentation: If Criterion A2 is met but Criterion A1 is not met for the past 6 months."
+S2: "In partial remission: Full criteria were previously met, but symptoms currently do not meet full criteria, and the symptoms that are present cause clinically significant impairment in social, academic, or occupational functioning."
+S3a: "Mild: Few, if any, symptoms in excess of those required to make the diagnosis are present, and symptoms result in no more than mild impairment in social or occupational functioning."
+S3b: "Moderate: Symptoms or functional impairment between 'mild' and 'severe' are present."
+S3c: "Severe: Many symptoms in excess of those required to make the diagnosis, or several symptoms that are particularly severe, are present, and symptoms result in marked impairment in social or occupational functioning."
+S1: none # key only
+S3: none # key only
\ No newline at end of file
diff --git a/data/ptsd-o6.yaml b/data/ptsd-o6.yaml
new file mode 100644
index 0000000..5e4c057
--- /dev/null
+++ b/data/ptsd-o6.yaml
@@ -0,0 +1,42 @@
+Note: "The following criteria apply to adults, adolescents, and children older than 6 years. For children 6 years and younger, return to the homepage and select :green[Post-Traumatic Stress Disorder in Children 6 Years and Younger]."
+A: "Exposure to actual or threatened death, serious injury, or sexual violence in one (or more) of the following ways:"
+A1: "Directly experiencing the traumatic event(s)."
+A2: "Witnessing, in person, the event(s) as it occurred to others."
+A3: "Learning that the traumatic event(s) occurred to a close family member or close friend. In cases of actual or threatened death of a family member or friend, the event(s) must have been violent or accidental."
+A4: "Experiencing repeated or extreme exposure to aversive details of the traumatic event(s) (e.g., first responders collecting human remains; police officers repeatedlyexposed to details of child abuse)."
+A4note: "Note: Criterion A4 does not apply to exposure through electronic media, television, movies, or pictures, unless this exposure is work related."
+B: "Presence of one (or more) of the following intrusion symptoms associated with the traumatic event(s), beginning after the traumatic event(s) occurred:"
+B1: "Recurrent, involuntary, and intrusive distressing memories of the traumatic event(s)."
+B1note: "Note: In children older than 6 years, repetitive play may occur in which themes or aspects of the traumatic event(s) are expressed."
+B2: "Recurrent distressing dreams in which the content and/or affect of the dream are related to the traumatic event(s)."
+B2note: "Note: In children, there may be frightening dreams without recognizable content."
+B3: "Dissociative reactions (e.g., flashbacks) in which the individual feels or acts as if the traumatic event(s) were recurring. (Such reactions may occur on a continuum, with the most extreme expression being a complete loss of awareness of present surroundings.)"
+B3note: "Note: In children, trauma-specific reenactment may occur in play."
+B4: "Intense or prolonged psychological distress at exposure to internal or external cues that symbolize or resemble an aspect of the traumatic event(s)."
+B5: "Marked physiological reactions to internal or external cues that symbolize or resemble an aspect of the traumatic event(s)."
+C: "Persistent avoidance of stimuli associated with the traumatic event(s), beginning after the traumatic event(s) occurred, as evidenced by one or both of the following:"
+C1: "Avoidance of or efforts to avoid distressing memories, thoughts, or feelings about or closely associated with the traumatic event(s)."
+C2: "Avoidance of or efforts to avoid external reminders (people, places, conversations, activities, objects, situations) that arouse distressing memories, thoughts, or feelings about or closely associated with the traumatic event(s)."
+D: "Negative alterations in cognitions and mood associated with the traumatic event(s), beginning or worsening after the traumatic event(s) occurred, as evidenced by two (or more) of the following:"
+D1: "Inability to remember an important aspect of the traumatic event(s) (typically due to dissociative amnesia and not to other factors such as head injury, alcohol, or drugs)."
+D2: "Persistent and exaggerated negative beliefs or expectations about oneself, others, or the world (e.g., 'I am bad,' 'No one can be trusted,' 'The world is completely dangerous', 'My whole nervous system is permanently ruined')."
+D3: "Persistent, distorted cognitions about the cause or consequences of the traumatic event(s) that lead the individual to blame himself/herself or others."
+D4: "Persistent negative emotional state (e.g., fear, horror, anger, guilt, or shame)."
+D5: "Markedly diminished interest or participation in significant activities."
+D6: "Feelings of detachment or estrangement from others."
+D7: "Persistent inability to experience positive emotions (e.g., inability to experience happiness, satisfaction, or loving feelings)."
+E: "Marked alterations in arousal and reactivity associated with the traumatic event(s), beginning or worsening after the traumatic event(s) occurred, as evidenced by two (or more) of the following:"
+E1: "Irritable behavior and angry outbursts (with little or no provocation) typically expressed as verbal or physical aggression toward people or objects."
+E2: "Reckless or self-destructive behavior."
+E3: "Hypervigilance."
+E4: "Exaggerated startle response."
+E5: "Problems with concentration."
+E6: "Sleep disturbance (e.g., difficulty falling or staying asleep or restless sleep)."
+F: "Duration of the disturbance (Criteria B, C, D, and E) is more than 1 month."
+G: "The disturbance causes clinically significant distress or impairment in social, occupational, or other important areas of functioning."
+H: "The disturbance is not attributable to the physiological effects of a substance (e.g., medication, alcohol) or another medical condition."
+S1: "With dissociative symptoms: The individual's symptoms meet criteria for post-traumatic stress disorder, and in addition, in response to the stressor, the individual experiences persistent or recurrent symptoms of either of the following:"
+S11: "Depersonalization: Persistent or recurrent experiences of feeling detached from, and as if one were an outside observer of, one's mental processes or body (e.g., feeling as though one were in a dream; feeling a sense of unreality of self or body or of time moving slowly)."
+S12: "Derealization: Persistent or recurrent experiences of unreality of surroundings (e.g., the world around the individual is experienced as unreal, dreamlike, distant, or distorted)."
+S1note: "Note: To use this subtype, the dissociative symptoms must not be attributable to the physiological effects of a substance (e.g., blacking out from alcohol intoxication) or another medical condition (e.g., complex partial seizures)."
+S2: "With delayed expression: If the full diagnostic criteria are not met until at least 6 months after the event (although the onset and expression of some symptoms may be immediate)."
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..569aba2
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,14 @@
+services:
+ app:
+ build:
+ context: .
+ args:
+ - GIT_BRANCH=${GIT_BRANCH:-unknown}
+ - GIT_COMMIT=${GIT_COMMIT:-unknown}
+ container_name: arwen
+ ports:
+ - "8501:8501"
+ environment:
+ - PYTHONUNBUFFERED=1
+ - UV_COMPILE_BYTECODE=1
+ restart: always
\ No newline at end of file
diff --git a/dsm.yaml b/dsm.yaml
deleted file mode 100644
index e69de29..0000000
diff --git a/main.py b/main.py
index 14ca0b8..ac2692d 100644
--- a/main.py
+++ b/main.py
@@ -1,6 +1,43 @@
import streamlit as st
-import pyyaml
+import yaml
+from utils import add_clean_footer
-st.set_page_config(page_title="Arwen DSM", layout="wide")
+# Load data
+with open("router.yaml", "r") as f:
+ data = yaml.safe_load(f)
-st.title("Arwen DSM-5 Diagnostic Criteria Aggregator")
+st.set_page_config(page_title="Arwen DCA", layout="centered")
+
+st.title("Arwen Diagnostic Criteria Aggregator 📋🧝♀️🩺")
+
+st.error("This is **not** a diagnostic tool and should not be used as such. Please read the **About** section below for more information.", icon="🚨")
+
+st.error("This tool is work-in-progress. Some pages may not be fully functional yet or may contain errors.", icon="🚧")
+
+st.markdown("Select a category and disorder, then press the button below to get started:")
+
+cat = st.selectbox("Category", list(data.keys()))
+dis = st.selectbox("Disorder", list(data[cat].keys()))
+
+if st.button(":green[Open Diagnostic Criteria ->]"):
+ st.session_state.current_cat = cat
+ st.session_state.current_dis = dis
+ st.session_state.current_dis_page = data[cat][dis]["page_path"]
+ st.session_state.current_dis_data = data[cat][dis]["data_path"]
+
+ logic_type = data[cat][dis].get("logic", "simple")
+
+ if logic_type == "complex":
+ st.switch_page(st.session_state.current_dis_page)
+ else:
+ st.switch_page("pages/simple.py")
+
+st.markdown("## About")
+st.markdown("**Arwen DCA** is designed to help clinicians aggregate diagnostic criteria based on the structure found in the DSM-5-TR. :red[This is **not** a diagnostic tool and should not be used as such]. Clinicians should always use their own judgement and verify that criteria and codes are correct.")
+st.markdown("Clincians can select all diagnostic criteria that apply to their patient/client and the tool will output a list of criteria met formatted in a way that can be easily copied into an EHR or note (including systems which parse criteria to :sparkles: :rainbow[automagically*] :sparkles: create a note)")
+st.markdown("This website does :green[**not store any data**] that is submitted and :green[**does not allow the input of any PHI**]. When a user refreshes a page or navigates away, all data is irrevocably lost.")
+st.markdown("This project is open source and available on [GitHub](https://github.com/hermaplusplus/Arwen). It is provided under the [MIT License](https://github.com/hermaplusplus/Arwen/blob/main/LICENSE). Contributions, issue reports, feedback, and suggestions are welcome.")
+st.markdown(":grey[Why is the tool called 'Arwen'? I watched Lord of the Rings recently. That's it.]")
+st.markdown(":grey[:small[* Automagically, meaning using a Large Language Model.]]")
+
+add_clean_footer()
\ No newline at end of file
diff --git a/pages/adhd.py b/pages/adhd.py
new file mode 100644
index 0000000..8c53034
--- /dev/null
+++ b/pages/adhd.py
@@ -0,0 +1,164 @@
+import streamlit as st
+import yaml
+from utils import add_clean_footer
+
+# Safety check
+if "current_dis" not in st.session_state:
+ st.switch_page("main.py")
+
+st.title(f"{st.session_state.current_dis}")
+
+# Back button
+if st.button("<- Return to Homepage", key="backtop"):
+ st.switch_page("main.py")
+
+with open(st.session_state.current_dis_data, "r") as f:
+ data = yaml.safe_load(f)
+
+for key in data.keys():
+ if key not in st.session_state:
+ st.session_state[key] = False
+
+st.markdown("## Client Metadata")
+
+st.info("This is a :rainbow[reminder] that no data on this site is stored or shared. Specific age under 90 is not considered PHI regardless! This age selection determines the threshold for the number of subcriteria required to satisfy criteria A1 and A2.", icon="ℹ️")
+
+age = st.radio("Client Age", ["17 or older", "Under 17"], key="age")
+threshold = 5 if age == "17 or older" else 6
+
+a1_score = sum(st.session_state.get(f"A1{char}", False) for char in "abcdefghi")
+a2_score = sum(st.session_state.get(f"A2{char}", False) for char in "abcdefghi")
+a1_score_badge = f":red-badge[{a1_score}/{threshold}]" if a1_score < threshold else f":green-badge[{a1_score}/{threshold}]"
+a2_score_badge = f":red-badge[{a2_score}/{threshold}]" if a2_score < threshold else f":green-badge[{a2_score}/{threshold}]"
+
+st.session_state.A1 = a1_score >= threshold
+st.session_state.A2 = a2_score >= threshold
+st.session_state.A = st.session_state.A1 or st.session_state.A2
+
+if st.session_state.A1 and st.session_state.A2:
+ S1text = [f":blue[**F90.2**] Combined presentation :green-badge[RECOMMENDED]\n\n{data['S1a']}", f":blue[**F90.0**] Predominantly inattentive presentation\n\n{data['S1b']}", f":blue[**F90.1**] Predominantly hyperactive/impulsive presentation\n\n{data['S1c']}"]
+ recommended = ":blue[**F90.2**] Combined presentation"
+elif st.session_state.A1:
+ S1text = [f":blue[**F90.2**] Combined presentation\n\n{data['S1a']}", f":blue[**F90.0**] Predominantly inattentive presentation :green-badge[RECOMMENDED]\n\n{data['S1b']}", f":blue[**F90.1**] Predominantly hyperactive/impulsive presentation\n\n{data['S1c']}"]
+ recommended = ":blue[**F90.0**] Predominantly inattentive presentation"
+elif st.session_state.A2:
+ S1text = [f":blue[**F90.2**] Combined presentation\n\n{data['S1a']}", f":blue[**F90.0**] Predominantly inattentive presentation\n\n{data['S1b']}", f":blue[**F90.1**] Predominantly hyperactive/impulsive presentation :green-badge[RECOMMENDED]\n\n{data['S1c']}"]
+ recommended = ":blue[**F90.1**] Predominantly hyperactive/impulsive presentation"
+else:
+ S1text = [f":blue[**F90.2**] Combined presentation\n\n{data['S1a']}", f":blue[**F90.0**] Predominantly inattentive presentation\n\n{data['S1b']}", f":blue[**F90.1**] Predominantly hyperactive/impulsive presentation\n\n{data['S1c']}"]
+ recommended = None
+
+st.markdown("## Diagnostic Criteria")
+st.info("Criteria A, A1, and A2 will check automatically based on sub-criteria.", icon="ℹ️")
+
+A = st.checkbox(f"A. {data['A']}", key="A", disabled=True)
+col1, col2 = st.columns([0.05, 0.95])
+with col2:
+ A1 = st.checkbox(f"1\\. {data['A1'].replace('Inattention', '**Inattention**', 1)}\n\n:orange[{data['A1note']}]\n\n{a1_score_badge}", key="A1", disabled=True)
+ subcol1, subcol2 = st.columns([0.05, 0.95])
+ with subcol2:
+ A1a = st.checkbox(f"a. {data['A1a']}", key="A1a")
+ A1b = st.checkbox(f"b. {data['A1b']}", key="A1b")
+ A1c = st.checkbox(f"c. {data['A1c']}", key="A1c")
+ A1d = st.checkbox(f"d. {data['A1d']}", key="A1d")
+ A1e = st.checkbox(f"e. {data['A1e']}", key="A1e")
+ A1f = st.checkbox(f"f. {data['A1f']}", key="A1f")
+ A1g = st.checkbox(f"g. {data['A1g']}", key="A1g")
+ A1h = st.checkbox(f"h. {data['A1h']}", key="A1h")
+ A1i = st.checkbox(f"i. {data['A1i']}", key="A1i")
+ A2 = st.checkbox(f"2\\. {data['A2'].replace('Hyperactivity and Impulsivity', '**Hyperactivity and Impulsivity**', 1)}\n\n:orange[{data['A2note']}]\n\n{a2_score_badge}", key="A2", disabled=True)
+ subcol3, subcol4 = st.columns([0.05, 0.95])
+ with subcol4:
+ A2a = st.checkbox(f"a. {data['A2a']}", key="A2a")
+ A2b = st.checkbox(f"b. {data['A2b']}", key="A2b")
+ A2c = st.checkbox(f"c. {data['A2c']}", key="A2c")
+ A2d = st.checkbox(f"d. {data['A2d']}", key="A2d")
+ A2e = st.checkbox(f"e. {data['A2e']}", key="A2e")
+ A2f = st.checkbox(f"f. {data['A2f']}", key="A2f")
+ A2g = st.checkbox(f"g. {data['A2g']}", key="A2g")
+ A2h = st.checkbox(f"h. {data['A2h']}", key="A2h")
+ A2i = st.checkbox(f"i. {data['A2i']}", key="A2i")
+B = st.checkbox(f"B. {data['B']}", key="B")
+C = st.checkbox(f"C. {data['C']}", key="C")
+D = st.checkbox(f"D. {data['D']}", key="D")
+E = st.checkbox(f"E. {data['E']}", key="E")
+
+st.markdown("## Specifiers")
+st.info("Presentation recommendation is based on criteria A1 and A2, but you can select otherwise.", icon="ℹ️")
+
+S1 = st.radio("Specify whether:", S1text, key="S1")
+st.markdown(":small[Specify if:]")
+S2 = st.checkbox(f"{data['S2']}", key="S2")
+S3 = st.radio("Specify current severity:", [f":green[**Mild**]\n\n{data['S3a'].replace("Mild: ", "")}", f":orange[**Moderate**]\n\n{data['S3b'].replace("Moderate: ", "")}", f":red[**Severe**]\n\n{data['S3c'].replace("Severe: ", "")}"], key="S3")
+
+st.markdown("## Output")
+
+omitBCDE = st.toggle("Ignore criteria B, C, D, and E", key="omitBCDE", value=True)
+omitS1 = st.toggle("Ignore presentation specifier", key="omitS1", value=False)
+omitS2 = st.toggle("Ignore remission specifier", key="omitS2", value=False)
+omitS3 = st.toggle("Ignore severity specifier", key="omitS3", value=False)
+
+if omitBCDE and not any([omitS1, omitS2, omitS3]):
+ st.info("Output will not include criteria B, C, D, and E.", icon="ℹ️")
+elif omitBCDE and all([omitS1, omitS2, omitS3]):
+ st.info("Output will not include criteria B, C, D, and E, nor presentation, remission, and severity specifiers.", icon="ℹ️")
+elif omitBCDE and any([omitS1, omitS2, omitS3]):
+ omitted = [o for o in [omitS1, omitS2, omitS3] if o]
+ omittext = ["presentation", "remission", "severity"]
+ if len(omitted) == 1:
+ st.info(f"Output will not include criteria B, C, D, and E, nor {omittext[[omitS1, omitS2, omitS3].index(True)]} specifier.", icon="ℹ️")
+ else:
+ st.info(f"Output will not include criteria B, C, D, and E, nor {omittext[[omitS1, omitS2, omitS3].index(True)]} and {omittext[[omitS1, omitS2, omitS3].index(True, [omitS1, omitS2, omitS3].index(True)+1)]} specifiers.", icon="ℹ️")
+elif all([omitS1, omitS2, omitS3]):
+ st.info("Output will not include presentation, remission, and severity specifiers.", icon="ℹ️")
+elif any([omitS1, omitS2, omitS3]):
+ omitted = [o for o in [omitS1, omitS2, omitS3] if o]
+ omittext = ["presentation", "remission", "severity"]
+ if len(omitted) == 1:
+ st.info(f"Output will not include {omittext[[omitS1, omitS2, omitS3].index(True)]} specifier.", icon="ℹ️")
+ else:
+ st.info(f"Output will not include {omittext[[omitS1, omitS2, omitS3].index(True)]} and {omittext[[omitS1, omitS2, omitS3].index(True, [omitS1, omitS2, omitS3].index(True)+1)]} specifiers.", icon="ℹ️")
+
+if not omitBCDE:
+ criteria_met = all([st.session_state.get(key, False) for key in ["A", "B", "C", "D", "E"]])
+ if not criteria_met:
+ unmet = [key for key in ["A", "B", "C", "D", "E"] if not st.session_state.get(key, False)]
+ st.error(f"Criteria not met: **{'**, **'.join(unmet)}**\n\n{st.session_state.current_dis} cannot be diagnosed unless all criteria A-E are met.\n\nConsider diagnosing :blue[F90.8] Other Specified Attention Deficit/Hyperactivity Disorder or :blue[F90.9] Unspecified Attention-Deficit/Hyperactivity Disorder", icon="🚨")
+else:
+ criteria_met = st.session_state.A
+ if not criteria_met:
+ st.error(f"Criteria not met: **A**\n\n{st.session_state.current_dis} cannot be diagnosed unless all criteria A-E are met.\n\nConsider diagnosing :blue[F90.8] Other Specified Attention Deficit/Hyperactivity Disorder or :blue[F90.9] Unspecified Attention-Deficit/Hyperactivity Disorder", icon="🚨")
+
+if ":green-badge[RECOMMENDED]" not in S1 and st.session_state.A:
+ st.warning(f"Presentation specifier does not match recommendation. **{S1.split('\n\n')[0]}** was selected instead of **{recommended}**.", icon="⚠️")
+
+st.markdown(":blue-badge[Triple click anywhere in the text below to select all of it at once!]")
+with st.container(border=True):
+ data_out = data.copy()
+ output = []
+ pop = ["A", "A1", "A1note", "A2", "A2note", "S1a", "S1b", "S1c", "S2", "S3a", "S3b", "S3c", "S1", "S3"]
+ if omitBCDE:
+ pop.extend(["B", "C", "D", "E"])
+ for p in pop:
+ data_out.pop(p, None)
+ for key in data_out.keys():
+ if st.session_state.get(key, False):
+ if key == "A2c":
+ output.append(data_out[key].replace("(Note:", "(note:"))
+ else:
+ output.append(data_out[key])
+ if not omitS1:
+ output.append([data["S1a"], data["S1b"], data["S1c"]][S1text.index(S1)].replace(" If ", " if ", 1).replace(" Criterion ", " criterion "))
+ if st.session_state.S2 and not omitS2:
+ output.append(data["S2"].replace(" Full ", " full ", 1))
+ if not omitS3:
+ output.append([data["S3a"].replace("Few", "few", 1), data["S3b"].replace("Symptoms", "symptoms", 1), data["S3c"].replace("Many", "many", 1)][[f":green[**Mild**]\n\n{data['S3a'].replace("Mild: ", "")}", f":orange[**Moderate**]\n\n{data['S3b'].replace("Moderate: ", "")}", f":red[**Severe**]\n\n{data['S3c'].replace("Severe: ", "")}"].index(S3)])
+ for i in range(len(output)):
+ output[i] = output[i][0].lower() + output[i][1:].rstrip(".")
+ st.markdown(", ".join(output))
+
+# Back button
+if st.button("<- Return to Homepage", key="backbottom"):
+ st.switch_page("main.py")
+
+add_clean_footer()
\ No newline at end of file
diff --git a/pages/ptsd-o6.py b/pages/ptsd-o6.py
new file mode 100644
index 0000000..2e18d12
--- /dev/null
+++ b/pages/ptsd-o6.py
@@ -0,0 +1,167 @@
+import streamlit as st
+import yaml
+from utils import add_clean_footer
+
+# Safety check
+if "current_dis" not in st.session_state:
+ st.switch_page("main.py")
+
+st.title(f"{st.session_state.current_dis}")
+
+# Back button
+if st.button("<- Return to Homepage", key="backtop"):
+ st.switch_page("main.py")
+
+with open(st.session_state.current_dis_data, "r") as f:
+ data = yaml.safe_load(f)
+
+for key in data.keys():
+ if key not in st.session_state:
+ st.session_state[key] = False
+
+a_score = sum(st.session_state.get(f"A{char}", False) for char in "1234")
+b_score = sum(st.session_state.get(f"B{char}", False) for char in "12345")
+c_score = sum(st.session_state.get(f"C{char}", False) for char in "12")
+d_score = sum(st.session_state.get(f"D{char}", False) for char in "1234567")
+e_score = sum(st.session_state.get(f"E{char}", False) for char in "123456")
+s1_score = sum(st.session_state.get(f"S1{char}", False) for char in "12")
+a_score_badge = f":red-badge[{a_score}/1]" if a_score < 1 else f":green-badge[{a_score}/1]"
+b_score_badge = f":red-badge[{b_score}/1]" if b_score < 1 else f":green-badge[{b_score}/1]"
+c_score_badge = f":red-badge[{c_score}/1]" if c_score < 1 else f":green-badge[{c_score}/1]"
+d_score_badge = f":red-badge[{d_score}/2]" if d_score < 2 else f":green-badge[{d_score}/2]"
+e_score_badge = f":red-badge[{e_score}/2]" if e_score < 2 else f":green-badge[{e_score}/2]"
+
+st.session_state.A = a_score >= 1
+st.session_state.B = b_score >= 1
+st.session_state.C = c_score >= 1
+st.session_state.D = d_score >= 2
+st.session_state.E = e_score >= 2
+st.session_state.S1 = s1_score >= 1
+
+st.markdown("## Diagnostic Criteria")
+st.info("Criteria A, B, C, D, and E will check automatically based on sub-criteria.", icon="ℹ️")
+
+A = st.checkbox(f"A. {data['A']}\n\n{a_score_badge}", key="A", disabled=True)
+col1, col2 = st.columns([0.05, 0.95])
+with col2:
+ A1 = st.checkbox(f"1\\. {data['A1']}", key="A1")
+ A2 = st.checkbox(f"2\\. {data['A2']}", key="A2")
+ A3 = st.checkbox(f"3\\. {data['A3']}", key="A3")
+ A4 = st.checkbox(f"4\\. {data['A4']}\n\n:orange[{data['A4note']}]", key="A4")
+B = st.checkbox(f"B. {data['B']}\n\n{b_score_badge}", key="B", disabled=True)
+col1, col2 = st.columns([0.05, 0.95])
+with col2:
+ B1 = st.checkbox(f"1\\. {data['B1']}\n\n:orange[{data['B1note']}]", key="B1")
+ B2 = st.checkbox(f"2\\. {data['B2']}\n\n:orange[{data['B2note']}]", key="B2")
+ B3 = st.checkbox(f"3\\. {data['B3']}\n\n:orange[{data['B3note']}]", key="B3")
+ B4 = st.checkbox(f"4\\. {data['B4']}", key="B4")
+ B5 = st.checkbox(f"5\\. {data['B5']}", key="B5")
+C = st.checkbox(f"C. {data['C']}\n\n{c_score_badge}", key="C", disabled=True)
+col1, col2 = st.columns([0.05, 0.95])
+with col2:
+ C1 = st.checkbox(f"1\\. {data['C1']}", key="C1")
+ C2 = st.checkbox(f"2\\. {data['C2']}", key="C2")
+D = st.checkbox(f"D. {data['D']}\n\n{d_score_badge}", key="D", disabled=True)
+col1, col2 = st.columns([0.05, 0.95])
+with col2:
+ D1 = st.checkbox(f"1\\. {data['D1']}", key="D1")
+ D2 = st.checkbox(f"2\\. {data['D2']}", key="D2")
+ D3 = st.checkbox(f"3\\. {data['D3']}", key="D3")
+ D4 = st.checkbox(f"4\\. {data['D4']}", key="D4")
+ D5 = st.checkbox(f"5\\. {data['D5']}", key="D5")
+ D6 = st.checkbox(f"6\\. {data['D6']}", key="D6")
+ D7 = st.checkbox(f"7\\. {data['D7']}", key="D7")
+E = st.checkbox(f"E. {data['E']}\n\n{e_score_badge}", key="E", disabled=True)
+col1, col2 = st.columns([0.05, 0.95])
+with col2:
+ E1 = st.checkbox(f"1\\. {data['E1']}", key="E1")
+ E2 = st.checkbox(f"2\\. {data['E2']}", key="E2")
+ E3 = st.checkbox(f"3\\. {data['E3']}", key="E3")
+ E4 = st.checkbox(f"4\\. {data['E4']}", key="E4")
+ E5 = st.checkbox(f"5\\. {data['E5']}", key="E5")
+ E6 = st.checkbox(f"6\\. {data['E6']}", key="E6")
+F = st.checkbox(f"F. {data['F']}", key="F")
+G = st.checkbox(f"G. {data['G']}", key="G")
+H = st.checkbox(f"H. {data['H']}", key="H")
+
+st.markdown("## Specifiers")
+
+S1text = data["S1"].replace("With dissociative symptoms:", "**With dissociative symptoms:**")
+S11text = data["S11"].replace("Depersonalization:", "**Depersonalization:**")
+S12text = data["S12"].replace("Derealization:", "**Derealization:**")
+
+st.markdown(":small[Specify whether:]")
+S1 = st.checkbox(f"{S1text}", key="S1", disabled=True)
+col1, col2 = st.columns([0.05, 0.95])
+with col2:
+ S11 = st.checkbox(f"{S11text}", key="S11")
+ S12 = st.checkbox(f"{S12text}", key="S12")
+ st.markdown(f":orange[{data['S1note']}]")
+st.markdown(":small[Specify if:]")
+S2 = st.checkbox(f"{data['S2']}", key="S2")
+
+st.markdown("## Output")
+
+omitFGH = st.toggle("Ignore criteria F, G, and H", key="omitFGH", value=True)
+omitS1 = st.toggle("Ignore dissociative symptoms specifier", key="omitS1", value=False)
+omitS2 = st.toggle("Ignore delayed expression specifier", key="omitS2", value=False)
+
+if omitFGH and not any([omitS1, omitS2]):
+ st.info("Output will not include criteria F, G, and H.", icon="ℹ️")
+elif omitFGH and all([omitS1, omitS2]):
+ st.info("Output will not include criteria F, G, and H, nor dissociative symptoms and delayed expression specifiers.", icon="ℹ️")
+elif omitFGH and any([omitS1, omitS2]):
+ omitted = [o for o in [omitS1, omitS2] if o]
+ omittext = ["dissociative symptoms", "delayed expression"]
+ if len(omitted) == 1:
+ st.info(f"Output will not include criteria F, G, and H, nor {omittext[[omitS1, omitS2].index(True)]} specifier.", icon="ℹ️")
+ else:
+ st.info(f"Output will not include criteria F, G, and H, nor {omittext[[omitS1, omitS2].index(True)]} and {omittext[[omitS1, omitS2].index(True, [omitS1, omitS2].index(True)+1)]} specifiers.", icon="ℹ️")
+elif all([omitS1, omitS2]):
+ st.info("Output will not include dissociative symptoms and delayed expression specifiers.", icon="ℹ️")
+elif any([omitS1, omitS2]):
+ omitted = [o for o in [omitS1, omitS2] if o]
+ omittext = ["dissociative symptoms", "delayed expression"]
+ if len(omitted) == 1:
+ st.info(f"Output will not include {omittext[[omitS1, omitS2].index(True)]} specifier.", icon="ℹ️")
+ else:
+ st.info(f"Output will not include {omittext[[omitS1, omitS2].index(True)]} and {omittext[[omitS1, omitS2].index(True, [omitS1, omitS2].index(True)+1)]} specifiers.", icon="ℹ️")
+
+if not omitFGH:
+ criteria_met = all([st.session_state.get(key, False) for key in ["A", "B", "C", "D", "E", "F", "G", "H"]])
+ if not criteria_met:
+ unmet = [key for key in ["A", "B", "C", "D", "E", "F", "G", "H"] if not st.session_state.get(key, False)]
+ st.error(f"Criteria not met: **{'**, **'.join(unmet)}**\n\n{st.session_state.current_dis} cannot be diagnosed unless all criteria A-H are met.\n\nConsider diagnosing :blue[F43.8] Other Specified Trauma- and Stressor-Related Disorder or :blue[F43.9] Unspecified Trauma- and Stressor-Related Disorder", icon="🚨")
+else:
+ criteria_met = all([st.session_state.get(key, False) for key in ["A", "B", "C", "D", "E"]])
+ if not criteria_met:
+ unmet = [key for key in ["A", "B", "C", "D", "E"] if not st.session_state.get(key, False)]
+ st.error(f"Criteria not met: **{'**, **'.join(unmet)}**\n\n{st.session_state.current_dis} cannot be diagnosed unless criteria A-E are met.\n\nConsider diagnosing :blue[F43.8] Other Specified Trauma- and Stressor-Related Disorder or :blue[F43.9] Unspecified Trauma- and Stressor-Related Disorder", icon="🚨")
+
+st.markdown(":blue-badge[Triple click anywhere in the text below to select all of it at once!]")
+with st.container(border=True):
+ data_out = data.copy()
+ output = []
+ pop = ["A", "A4note", "B", "B1note", "B2note", "B3note", "C", "D", "E", "S1", "S11", "S12", "S1note", "S2"]
+ if omitFGH:
+ pop.extend(["F", "G", "H"])
+ for p in pop:
+ data_out.pop(p, None)
+ for key in data_out.keys():
+ if st.session_state.get(key, False):
+ output.append(data_out[key])
+ if st.session_state.get("S11", False) and not omitS1:
+ output.append(data["S11"].replace("Depersonalization: ", ""))
+ if st.session_state.get("S12", False) and not omitS1:
+ output.append(data["S12"].replace("Derealization: ", ""))
+ if st.session_state.get("S2", False) and not omitS2:
+ output.append(data["S2"].split(": ")[0])
+ for i in range(len(output)):
+ output[i] = output[i][0].lower() + output[i][1:].rstrip(".")
+ st.markdown(", ".join(output))
+
+# Back button
+if st.button("<- Return to Homepage", key="backbottom"):
+ st.switch_page("main.py")
+
+add_clean_footer()
\ No newline at end of file
diff --git a/pages/simple.py b/pages/simple.py
new file mode 100644
index 0000000..757ce87
--- /dev/null
+++ b/pages/simple.py
@@ -0,0 +1,21 @@
+import streamlit as st
+import yaml
+from utils import add_clean_footer
+
+# Safety check
+if "current_dis" not in st.session_state:
+ st.switch_page("main.py")
+
+st.title(f"{st.session_state.current_dis}")
+
+# Back button
+if st.button("<- Return to Homepage", key="backtop"):
+ st.switch_page("main.py")
+
+
+
+# Back button
+if st.button("<- Return to Homepage", key="backbottom"):
+ st.switch_page("main.py")
+
+add_clean_footer()
\ No newline at end of file
diff --git a/router.yaml b/router.yaml
new file mode 100644
index 0000000..925fad4
--- /dev/null
+++ b/router.yaml
@@ -0,0 +1,71 @@
+Neurodevelopmental Disorders:
+ Attention-Deficit/Hyperactivity Disorder:
+ logic: complex
+ display_name: "Attention-Deficit/Hyperactivity Disorder"
+ icd_10: F90.2, F90.0, F90.1
+ page_path: "pages/adhd.py"
+ data_path: "data/adhd.yaml"
+ Autism Spectrum Disorder:
+ logic: simple
+ display_name: "Autism Spectrum Disorder"
+ icd_10: F84.0, F84.5, F84.8, F84.9
+ page_path: "pages/simple.py"
+ data_path: "data/asd.yaml"
+Depressive Disorders:
+ Major Depressive Disorder:
+ logic: complex
+ display_name: "Major Depressive Disorder"
+ icd_10: F32.0-F32.9, F33.0-F33.9
+ page_path: "pages/mdd.py"
+ data_path: "data/mdd.yaml"
+ Persistent Depressive Disorder:
+ logic: complex
+ display_name: "Persistent Depressive Disorder (Dysthymia)"
+ icd_10: F34.1
+ page_path: "pages/pdd.py"
+ data_path: "data/pdd.yaml"
+Anxiety Disorders:
+ Generalized Anxiety Disorder:
+ logic: complex
+ display_name: "Generalized Anxiety Disorder"
+ icd_10: F41.1
+ page_path: "pages/gad.py"
+ data_path: "data/gad.yaml"
+Trauma- and Stressor-Related Disorders:
+ Post-Traumatic Stress Disorder in Individuals Older than 6 Years:
+ logic: complex
+ display_name: "Post-Traumatic Stress Disorder in Individuals Older than 6 Years"
+ icd_10: F43.1
+ page_path: "pages/ptsd-o6.py"
+ data_path: "data/ptsd-o6.yaml"
+ Post-Traumatic Stress Disorder in Children 6 Years and Younger:
+ logic: complex
+ display_name: "Post-Traumatic Stress Disorder in Children 6 Years and Younger"
+ icd_10: F43.1
+ page_path: "pages/ptsd-u6.py"
+ data_path: "data/ptsd-u6.yaml"
+Personality Disorders:
+ Antisocial Personality Disorder:
+ logic: complex
+ display_name: "Antisocial Personality Disorder"
+ icd_10: F60.2
+ page_path: "pages/aspd.py"
+ data_path: "data/aspd.yaml"
+ Borderline Personality Disorder:
+ logic: simple
+ display_name: "Borderline Personality Disorder"
+ icd_10: F60.3
+ page_path: "pages/simple.py"
+ data_path: none
+ Histrionic Personality Disorder:
+ logic: simple
+ display_name: "Histrionic Personality Disorder"
+ icd_10: F60.4
+ page_path: "pages/simple.py"
+ data_path: none
+ Narcissistic Personality Disorder:
+ logic: simple
+ display_name: "Narcissistic Personality Disorder"
+ icd_10: F60.81
+ page_path: "pages/simple.py"
+ data_path: none
\ No newline at end of file
diff --git a/start.sh b/start.sh
new file mode 100644
index 0000000..470a79b
--- /dev/null
+++ b/start.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+export GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
+export GIT_COMMIT=$(git rev-parse --short HEAD)
+
+echo "GIT_BRANCH=$GIT_BRANCH" > .env
+echo "GIT_COMMIT=$GIT_COMMIT" >> .env
+
+docker compose up -d --build
\ No newline at end of file
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..445a2dc
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,35 @@
+import streamlit as st
+import subprocess
+import platform
+import os
+
+def add_clean_footer():
+
+ branch = os.environ.get("APP_GIT_BRANCH", "UNKNOWN")
+ branch_url = f"https://github.com/hermaplusplus/Arwen/tree/{branch}"
+ commit = os.environ.get("APP_GIT_COMMIT", "UNKNOWN")
+ commit_url = f"https://github.com/hermaplusplus/Arwen/commit/{commit}"
+
+ if branch != "UNKNOWN":
+ branch = f":green-badge[**[{branch}]({branch_url})**]" if branch == "main" else f":orange-badge[**[{branch}]({branch_url})**]"
+ else:
+ try:
+ branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).decode().strip()
+ branch = f":green-badge[**[{branch}]({branch_url})**]" if branch == "main" else f":orange-badge[**[{branch}]({branch_url})**]"
+ except:
+ branch = ":red-badge[UNKNOWN]"
+
+ if commit != "UNKNOWN":
+ commit = f":blue-badge[**[{commit}]({commit_url})**]"
+ else:
+ try:
+ commit = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).decode().strip()
+ commit = f":blue-badge[**[{commit}]({commit_url})**]"
+ except:
+ commit = ":red-badge[UNKNOWN]"
+
+ is_docker = ":green-badge[ACTIVE]" if os.path.exists('/.dockerenv') else ":red-badge[INACTIVE]"
+
+ st.markdown("---")
+ st.markdown(f"Made with ❤️ by [Herma](https://github.com/hermaplusplus/Arwen) | Provided under the [MIT License](https://github.com/hermaplusplus/Arwen/blob/main/LICENSE)", text_alignment="center")
+ st.markdown(f"Branch: {branch}@{commit} | Docker: {is_docker} | Python: :blue-badge[{platform.python_version()}]", text_alignment="center")
\ No newline at end of file