File size: 20,701 Bytes
9180644
6a69810
8fc0636
 
9180644
8fc0636
3941fef
044cef5
3941fef
8fc0636
3941fef
 
 
 
958aee6
3941fef
 
 
 
958aee6
9180644
3941fef
8fc0636
3941fef
 
 
 
 
 
 
 
8fc0636
3941fef
8fc0636
958aee6
9180644
8fc0636
 
 
 
3941fef
9180644
3941fef
8fc0636
9180644
 
3941fef
958aee6
8fc0636
 
 
9180644
dfac76f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
958aee6
3941fef
8fc0636
9180644
3941fef
9180644
3941fef
9180644
 
3941fef
 
 
 
8fc0636
3941fef
 
 
958aee6
9180644
8fc0636
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3941fef
958aee6
9180644
 
8fc0636
 
 
 
 
 
 
 
 
3941fef
 
8fc0636
 
 
 
 
 
 
 
 
 
 
 
9180644
 
 
958aee6
3941fef
 
 
8fc0636
3941fef
 
9180644
8fc0636
 
 
 
 
 
 
 
3941fef
958aee6
8fc0636
 
9180644
3941fef
9180644
 
 
3941fef
8fc0636
 
3941fef
8fc0636
3941fef
8fc0636
 
958aee6
3941fef
 
 
 
 
 
8fc0636
3941fef
8fc0636
3941fef
 
 
 
958aee6
9180644
8fc0636
3941fef
 
 
8fc0636
3941fef
8fc0636
3941fef
8fc0636
3941fef
 
8fc0636
3941fef
 
 
8fc0636
 
 
3941fef
9180644
3941fef
9180644
8fc0636
9180644
8fc0636
9180644
8fc0636
9180644
3941fef
8fc0636
 
9180644
 
3941fef
8fc0636
 
 
 
 
 
 
 
3941fef
8fc0636
3941fef
 
8fc0636
3941fef
 
 
8fc0636
3941fef
8fc0636
3941fef
 
 
 
 
 
 
 
 
 
8fc0636
3941fef
8fc0636
3941fef
9180644
 
3941fef
8fc0636
9180644
3941fef
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
import streamlit as st

import altair as alt
import numpy as np
import pandas as pd
import rag
import os

cache_dir_base = "/app/.cache_app"
if not os.path.exists(cache_dir_base):
    try:
        os.makedirs(cache_dir_base, exist_ok=True)
    except Exception as e:
        st.error(f"Failed to create base cache directory {cache_dir_base}: {e}")

os.environ['HF_HOME'] = os.environ.get('HF_HOME', os.path.join(cache_dir_base, 'huggingface_hub'))
os.environ['TRANSFORMERS_CACHE'] = os.environ.get('TRANSFORMERS_CACHE', os.path.join(cache_dir_base, 'transformers'))
os.environ['SENTENCE_TRANSFORMERS_HOME'] = os.environ.get('SENTENCE_TRANSFORMERS_HOME', os.path.join(cache_dir_base, 'sentence_transformers'))
os.environ['PIP_CACHE_DIR'] = os.environ.get('PIP_CACHE_DIR', os.path.join(cache_dir_base, 'pip'))

st.set_page_config(layout="wide", page_title="Communication Board")

if 'assistant_initialized_flag' not in st.session_state:
    st.session_state.current_page = "main"
    st.session_state.show_custom_words = False
    st.session_state.custom_words = []
    st.session_state.text_size = 22
    st.session_state.theme = "Default"
    st.session_state.speech_rate = 1.0
    st.session_state.voice_option = "Default Voice"
    st.session_state.messages = []
    st.session_state.assistant = None
    st.session_state.text_output = ""
    st.session_state.assistant_initialized_flag = True

theme_colors = {
    "Default": {"bg": "#FFFFFF", "text": "#000000", "pronoun": "#FFFF99", "verb": "#CCFFCC", "question": "#CCCCFF", "common": "#FFCC99", "preposition": "#99CCFF", "descriptive": "#CCFF99", "misc": "#FFB6C1"},
    "High Contrast": {"bg": "#FFFFFF", "text": "#000000", "pronoun": "#FFFF00", "verb": "#00FF00", "question": "#0000FF", "common": "#FF6600", "preposition": "#00CCFF", "descriptive": "#66FF33", "misc": "#FF3366"},
    "Pastel": {"bg": "#F8F8FF", "text": "#333333", "pronoun": "#FFEFD5", "verb": "#E0FFFF", "question": "#D8BFD8", "common": "#FFE4B5", "preposition": "#B0E0E6", "descriptive": "#F0FFF0", "misc": "#FFF0F5"},
    "Dark Mode": {"bg": "#333333", "text": "#FFFFFF", "pronoun": "#8B8B00", "verb": "#006400", "question": "#00008B", "common": "#8B4500", "preposition": "#00688B", "descriptive": "#698B22", "misc": "#8B1A1A"}
}
colors = theme_colors[st.session_state.theme]

@st.cache_resource
def initialize_assistant(doc_path):
    if not os.path.exists(doc_path):
        st.sidebar.warning(f"Doc '{os.path.basename(doc_path)}' not found at '{doc_path}'. Creating dummy.")
        try:
            parent_dir = os.path.dirname(doc_path)
            if parent_dir: # Ensure parent_dir is not empty if doc_path is just a filename
                 os.makedirs(parent_dir, exist_ok=True)
            with open(doc_path, "w") as f:
                f.write("""Elliot Harper is a 29‐year‐old man whose life story is defined by both challenges and triumphs. Born with a congenital motor impairment that necessitates the use of a power wheelchair and an augmentative and alternative communication (AAC) device, Elliot has spent his life navigating a world that often sees only his physical limitations rather than his vibrant mind and resilient spirit.

Early Life and Education
From his earliest school days, Elliot experienced the sting of exclusion. Group projects, team sports, and school social events frequently left him feeling isolated and overlooked. Although his teachers recognized his quick wit and robust intellectual abilities—sometimes marveling at how he effortlessly absorbed new concepts using his AAC device—social stereotypes persisted. The very same classroom that fostered his love for learning also reinforced the painful notion that he did not quite fit the conventional mold. Despite these challenges, Elliot excelled academically and learned to use computers and other assistive technologies with remarkable speed, impressing both peers and educators alike.

Personal Relationships and Social Life
As Elliot grew older, his journey into adulthood brought new hurdles in the realm of personal relationships. His experiences with friendship and love have been as diverse as they have been challenging. He forged a deep bond with a former coworker—a friend who eventually became one of his most cherished confidants. Their relationship started in the workplace but blossomed into a genuine friendship marked by moments of humor, shared struggles, and subtle heartbreak. Though he once nurtured romantic feelings for her, Elliot eventually came to understand that differences in lifestyle—she loved outdoor adventures like running, biking, and exploring the trails—posed significant challenges in the dating arena.

Elliot's life is also enriched by the companionship of his attentive pet cat, whose gentle presence in the quiet moments of his day offers him profound comfort. Amid the long, solitary hours—sometimes marked by the anxiety of a brewing storm or the vulnerability of a cold night—both human connection and the soothing purr of his pet have been lifelines that remind him he is never truly alone.

Emotional Resilience and Coping Strategies
Deeply introspective, Elliot has developed a rich inner world to cope with the weight of societal misconceptions and moments of loneliness. His AAC device not only serves as a bridge to the outside world but also channels his creativity. Elliot finds solace and empowerment in writing—he often spends late nights crafting memoir notes that capture the nuances of his lived experience. Whether he is meticulously typing out thoughts on his computer or air-writing words with his arm in a burst of patient creativity, every communication is a testament to his determination to be heard and understood on his own terms.

Humor is another tool in his resilient arsenal. In moments of vulnerability—such as the awkward challenges of navigating social norms or the struggle to find the right words when discussing personal matters—Elliot turns to humor, sometimes researching witty pickup lines late at night to keep his spirits high. His ability to laugh in the face of hardship underlines the strength of his character.

Aspirations and Future Goals
Looking forward, Elliot dreams of a future where technology bridges the gap between physical challenges and boundless opportunities. He hopes to one day upgrade his mobility device—a power wheelchair, perhaps with advanced track capabilities—that would allow him greater independence and access to experiences that many take for granted. Professionally, his passion for technology and data has led him to master complex tools like spreadsheets, reinforcing his belief that his intellect is as dynamic as it is misunderstood.

Elliot's ultimate aspiration is to write a memoir that tells his true story—a narrative of perseverance and authenticity that not only reclaims his identity but also serves as an inspiration to others living with disabilities. He envisions his book as a beacon for those who have been sidelined by societal biases, proving that a vibrant life transcends physical limitations and that every individual has a unique, invaluable story to tell.

Navigating Love and Intimacy
In the realm of dating, Elliot’s journey has been bittersweet. The superficial judgments that often focus solely on his wheelchair have made it incredibly difficult to find a partner who sees past his disability and cherishes his intellect and kind-hearted nature. Elliot longs for a relationship where he is valued for his genuine self rather than merely his visible differences. Despite numerous setbacks and disappointments, his enduring hope for meaningful love reflects an unwavering belief in his own worth—a belief that continues to propel him forward.

In Summary
Elliot Harper’s life is a rich tapestry of struggle, strength, and quiet defiance. His story—marked by both moments of deep sorrow and bursts of unexpected joy—reminds us all that the measure of a person lies not in their physical limitations, but in the strength of their character, the depth of their compassion, and the courage with which they strive to realize their dreams. Through his journey with his AAC device, his cherished friendships, and his indomitable spirit, Elliot continues to challenge societal misconceptions and advocate for a world where everyone is seen and celebrated for who they truly are.
""")
        except Exception as e:
            st.sidebar.error(f"Failed to create dummy doc at '{doc_path}': {e}")
            return None
    try:
        st.sidebar.info("Initializing AAC Assistant... This may take some time.")
        assistant = rag.AACAssistant(doc_path)
        st.sidebar.success("AAC Assistant Initialized and ready!")
        return assistant
    except Exception as e:
        st.sidebar.error(f"Fatal Error Initializing AAC Assistant: {e}")
        st.sidebar.error("The assistant could not be started. Please check the logs for details.")
        return None

DEFAULT_DOCUMENT_PATH = "src/aac_user_experiences.txt"

if st.session_state.assistant is None:
    st.session_state.assistant = initialize_assistant(DEFAULT_DOCUMENT_PATH)

css = f"""
<style>
.big-font {{ font-size:{st.session_state.text_size}px !important; text-align: center; }}
.output-box {{ border: 2px solid #ddd; border-radius: 5px; padding: 15px; min-height: 100px; background-color: white; margin-bottom: 15px; font-size: {st.session_state.text_size}px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
div[data-testid="stHorizontalBlock"] {{ gap: 5px; }}
section[data-testid="stSidebar"] {{ width: 20rem !important; background-color: {colors["bg"]}; }}
body {{ background-color: {colors["bg"]}; color: {colors["text"]}; }}
.stButton>button {{ width: 100%; height: 60px; font-size: {max(16, st.session_state.text_size - 6)}px; font-weight: bold; white-space: normal; word-wrap: break-word; padding: 0px; transition: transform 0.1s ease; }}
.stButton>button:hover {{ filter: brightness(95%); transform: scale(1.03); box-shadow: 0 2px 3px rgba(0,0,0,0.1); }}
.control-button {{ height: 80px !important; font-size: {max(18, st.session_state.text_size - 4)}px !important; }}
.btn-pronoun {{ background-color: {colors["pronoun"]} !important; border: 1px solid #888 !important; }}
.btn-verb {{ background-color: {colors["verb"]} !important; border: 1px solid #888 !important; }}
.btn-question {{ background-color: {colors["question"]} !important; border: 1px solid #888 !important; }}
.btn-common {{ background-color: {colors["common"]} !important; border: 1px solid #888 !important; }}
.btn-preposition {{ background-color: {colors["preposition"]} !important; border: 1px solid #888 !important; }}
.btn-descriptive {{ background-color: {colors["descriptive"]} !important; border: 1px solid #888 !important; }}
.btn-misc {{ background-color: {colors["misc"]} !important; border: 1px solid #888 !important; }}
.sidebar .stChatMessage {{ background-color: {colors.get('bg', '#FFFFFF')}; border-radius: 8px; }}
</style>
"""
st.markdown(css, unsafe_allow_html=True)

js = """
<script>
function colorButtons() {
    const buttons = document.querySelectorAll('button[id^="key_"]');
    buttons.forEach(button => {
        const id = button.id;
        const parts = id.split('_');
        if (parts.length >= 4) {
            const category = parts[3];
            button.className = button.className.replace(/btn-[a-z]+/g, '').trim();
            button.classList.add('btn-' + category);
        }
    });
}
document.addEventListener('DOMContentLoaded', colorButtons);
const streamlitDoc = window.parent.document;
const observer = new MutationObserver((mutationsList, observer) => {
    for(const mutation of mutationsList) {
        if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
            colorButtons();
        }
    }
});
observer.observe(streamlitDoc.body, { childList: true, subtree: true });
colorButtons();
</script>
"""
st.markdown(js, unsafe_allow_html=True)

st.title("Communication Board")

st.session_state.text_output = st.text_area(
    "Compose Message:", value=st.session_state.get("text_output", ""), height=100, key="main_text_output_area"
)

layout = [
    [{"word": "I", "category": "pronoun"}, {"word": "am", "category": "verb"}, {"word": "how", "category": "question"}, {"word": "what", "category": "question"}, {"word": "when", "category": "question"}, {"word": "where", "category": "question"}, {"word": "who", "category": "question"}, {"word": "why", "category": "question"}, {"word": "That", "category": "pronoun"}, {"word": "Please", "category": "common"}],
    [{"word": "me", "category": "pronoun"}, {"word": "are", "category": "verb"}, {"word": "is", "category": "verb"}, {"word": "was", "category": "verb"}, {"word": "will", "category": "verb"}, {"word": "help", "category": "verb"}, {"word": "need", "category": "verb"}, {"word": "want", "category": "verb"}, {"word": "thank you", "category": "common"}, {"word": "sorry", "category": "common"}],
    [{"word": "my", "category": "pronoun"}, {"word": "can", "category": "verb"}, {"word": "A", "category": "misc"}, {"word": "B", "category": "misc"}, {"word": "C", "category": "misc"}, {"word": "D", "category": "misc"}, {"word": "E", "category": "misc"}, {"word": "F", "category": "misc"}, {"word": "G", "category": "misc"}, {"word": "H", "category": "misc"}],
    [{"word": "it", "category": "pronoun"}, {"word": "did", "category": "verb"}, {"word": "letter_I", "category": "misc", "display": "I"}, {"word": "J", "category": "misc"}, {"word": "K", "category": "misc"}, {"word": "L", "category": "misc"}, {"word": "M", "category": "misc"}, {"word": "N", "category": "misc"}, {"word": "O", "category": "misc"}, {"word": "P", "category": "misc"}],
    [{"word": "they", "category": "pronoun"}, {"word": "do", "category": "verb"}, {"word": "Q", "category": "misc"}, {"word": "R", "category": "misc"}, {"word": "S", "category": "misc"}, {"word": "T", "category": "misc"}, {"word": "U", "category": "misc"}, {"word": "V", "category": "misc"}, {"word": "W", "category": "misc"}, {"word": "X", "category": "misc"}],
    [{"word": "we", "category": "pronoun"}, {"word": "Y", "category": "misc"}, {"word": "Z", "category": "misc"}, {"word": "1", "category": "misc"}, {"word": "2", "category": "misc"}, {"word": "3", "category": "misc"}, {"word": "4", "category": "misc"}, {"word": "5", "category": "misc"}, {"word": ".", "category": "misc"}, {"word": "?", "category": "misc"}],
    [{"word": "you", "category": "pronoun"}, {"word": "6", "category": "misc"}, {"word": "7", "category": "misc"}, {"word": "8", "category": "misc"}, {"word": "9", "category": "misc"}, {"word": "0", "category": "misc"}, {"word": "-", "category": "misc"}, {"word": "!", "category": "misc"}, {"word": ",", "category": "misc"}, {"word": "SPACE", "category": "misc"}],
    [{"word": "your", "category": "pronoun"}, {"word": "like", "category": "verb"}, {"word": "to", "category": "preposition"}, {"word": "with", "category": "preposition"}, {"word": "in", "category": "preposition"}, {"word": "the", "category": "misc"}, {"word": "and", "category": "misc"}, {"word": "but", "category": "misc"}, {"word": "not", "category": "descriptive"}, {"word": "yes", "category": "common"}]
]

dynamic_layout = list(layout) # Make a mutable copy for adding custom words

if st.session_state.custom_words:
    current_custom_row = []
    for idx, word_info in enumerate(st.session_state.custom_words):
        word = word_info["word"]
        category = word_info["category"]
        current_custom_row.append({"word": f"custom_{idx}_{word}", "display": word, "category": category})
        if len(current_custom_row) == 10:
            dynamic_layout.append(list(current_custom_row))
            current_custom_row = []
    if current_custom_row:
        while len(current_custom_row) < 10:
            current_custom_row.append({"word": "", "category": "misc"})
        dynamic_layout.append(list(current_custom_row))

def add_to_output(word_to_add):
    current_text = st.session_state.get("text_output", "")
    if word_to_add == "SPACE":
        current_text += " "
    elif word_to_add in [".", "?", "!", ",", "-"]:
        current_text += word_to_add
    elif word_to_add.isdigit() or (len(word_to_add) == 1 and word_to_add.isalpha()):
        current_text += word_to_add
    else:
        if current_text and not current_text.endswith(" "):
            current_text += " "
        current_text += word_to_add
    st.session_state.text_output = current_text

st.markdown("### Communication Keyboard")
for row_idx, row_items in enumerate(dynamic_layout):
    cols = st.columns(len(row_items))
    for col_idx, item_info in enumerate(row_items):
        if not isinstance(item_info, dict) or "word" not in item_info or item_info["word"] == "":
            continue
        word = item_info["word"]
        category = item_info.get("category", "misc")
        display_text = item_info.get("display", word)
        button_key = f"key_{row_idx}_{col_idx}_{category}_{word.replace(' ', '_').replace('.', 'dot').replace('?', 'qmark')}" # Make key even more robust

        if cols[col_idx].button(
                display_text if word != "SPACE" else "␣",
                key=button_key,
                use_container_width=True
            ):
            word_to_add_on_click = display_text if word.startswith("custom_") or word.startswith("letter_") else word
            add_to_output(word_to_add_on_click)
            st.rerun()

col1, col2, col3, col4 = st.columns(4)

with col1:
    if st.button("CLEAR", key="clear_btn", help="Clear all text", use_container_width=True, type="secondary"):
        st.session_state.text_output = ""
        st.rerun()
with col2:
    if st.button("SPEAK", key="speak_btn", help="Speak the current text (feature placeholder)", use_container_width=True, type="secondary"):
        if st.session_state.text_output:
            st.toast(f"Speaking (simulated): {st.session_state.text_output}", icon="🔊")
    
    if st.button("⌫ DEL", key="backspace_btn", help="Delete last character", use_container_width=True):
        if st.session_state.text_output:
            st.session_state.text_output = st.session_state.text_output[:-1]
            st.rerun()
with col4:
    if st.button("⌫ WORD", key="backspace_word_btn", help="Delete last word", use_container_width=True):
        current_text = st.session_state.text_output.rstrip()
        if ' ' in current_text:
            st.session_state.text_output = current_text.rsplit(' ', 1)[0] + " "
        else:
            st.session_state.text_output = ""
        st.rerun()

with col3:
    if st.button("SEND 🗣️", key="send_btn", help="Send message to assistant", use_container_width=True, type="primary"):
        if 'assistant' not in st.session_state or st.session_state.assistant is None:
            st.error("AAC Assistant is not initialized. Please check sidebar for errors or wait for initialization to complete.")
        elif not st.session_state.text_output.strip():
            st.toast("Please compose a message first.", icon="✍️")
        else:
            user_message_to_send = st.session_state.text_output
            st.session_state.messages.append({"role": "user", "content": user_message_to_send})
            st.session_state.text_output = ""
            
            with st.spinner("Elliot is thinking... please wait a moment..."):
                try:
                    print(f"DEBUG [Streamlit UI]: User typed: '{user_message_to_send}'")
                    print(f"DEBUG [Streamlit UI]: Calling assistant.process_query for '{user_message_to_send}'")
                    assistant_response = st.session_state.assistant.process_query(user_message_to_send)
                    print(f"DEBUG [Streamlit UI]: Received assistant response: '{assistant_response}'")
                    st.session_state.messages.append({"role": "assistant", "content": assistant_response})
                except Exception as e:
                    error_message_display = f"An error occurred while processing your message: {e}"
                    st.error(error_message_display)
                    st.session_state.messages.append({"role": "assistant", "content": f"*Error: Could not get a response. Details: {e}*"})
            st.rerun()

with st.sidebar:
    st.divider()
    st.subheader("Conversation Log")
    chat_container = st.container(height=400 if st.session_state.messages else 100)
    with chat_container:
        if not st.session_state.messages:
            st.caption("No messages yet...")
        else:
            for message in st.session_state.messages:
                with st.chat_message(message["role"]):
                    st.markdown(message["content"])