rdz-falcon commited on
Commit
8fc0636
·
verified ·
1 Parent(s): 500e161

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +104 -293
src/streamlit_app.py CHANGED
@@ -1,31 +1,26 @@
1
  import streamlit as st
2
 
3
- import altair as alt # Keep if used, otherwise remove
4
- import numpy as np # Keep if used, otherwise remove
5
  import pandas as pd
6
- import rag # Import your rag module
7
  import os
8
 
9
- # --- Environment Variable Setup for Caching (run once at the top) ---
10
  cache_dir_base = "/app/.cache_app"
11
- if not os.path.exists(cache_dir_base): # Ensure base exists, useful if Dockerfile step was missed
12
  try:
13
  os.makedirs(cache_dir_base, exist_ok=True)
14
  except Exception as e:
15
  st.error(f"Failed to create base cache directory {cache_dir_base}: {e}")
16
 
17
- # Set environment variables for Hugging Face libraries if not already set by Dockerfile
18
- # Note: Setting them here is a fallback; best set in Dockerfile or Space secrets for build-time tools.
19
  os.environ['HF_HOME'] = os.environ.get('HF_HOME', os.path.join(cache_dir_base, 'huggingface_hub'))
20
  os.environ['TRANSFORMERS_CACHE'] = os.environ.get('TRANSFORMERS_CACHE', os.path.join(cache_dir_base, 'transformers'))
21
  os.environ['SENTENCE_TRANSFORMERS_HOME'] = os.environ.get('SENTENCE_TRANSFORMERS_HOME', os.path.join(cache_dir_base, 'sentence_transformers'))
22
  os.environ['PIP_CACHE_DIR'] = os.environ.get('PIP_CACHE_DIR', os.path.join(cache_dir_base, 'pip'))
23
 
24
- # --- Page Configuration (should be the first Streamlit command) ---
25
  st.set_page_config(layout="wide", page_title="Communication Board")
26
 
27
- # --- Session State Initialization ---
28
- if 'assistant_initialized_flag' not in st.session_state: # Use a flag to track if full init ran
29
  st.session_state.current_page = "main"
30
  st.session_state.show_custom_words = False
31
  st.session_state.custom_words = []
@@ -34,67 +29,31 @@ if 'assistant_initialized_flag' not in st.session_state: # Use a flag to track i
34
  st.session_state.speech_rate = 1.0
35
  st.session_state.voice_option = "Default Voice"
36
  st.session_state.messages = []
37
- st.session_state.assistant = None # Will be initialized by initialize_assistant
38
  st.session_state.text_output = ""
39
- st.session_state.assistant_initialized_flag = True # Mark that initial session state setup is done
40
 
41
-
42
- # --- Theme Colors ---
43
  theme_colors = {
44
- "Default": {
45
- "bg": "#FFFFFF", "text": "#000000",
46
- "pronoun": "#FFFF99", "verb": "#CCFFCC",
47
- "question": "#CCCCFF", "common": "#FFCC99",
48
- "preposition": "#99CCFF", "descriptive": "#CCFF99",
49
- "misc": "#FFB6C1"
50
- },
51
- "High Contrast": {
52
- "bg": "#FFFFFF", "text": "#000000",
53
- "pronoun": "#FFFF00", "verb": "#00FF00",
54
- "question": "#0000FF", "common": "#FF6600",
55
- "preposition": "#00CCFF", "descriptive": "#66FF33",
56
- "misc": "#FF3366"
57
- },
58
- "Pastel": {
59
- "bg": "#F8F8FF", "text": "#333333",
60
- "pronoun": "#FFEFD5", "verb": "#E0FFFF",
61
- "question": "#D8BFD8", "common": "#FFE4B5",
62
- "preposition": "#B0E0E6", "descriptive": "#F0FFF0",
63
- "misc": "#FFF0F5"
64
- },
65
- "Dark Mode": {
66
- "bg": "#333333", "text": "#FFFFFF",
67
- "pronoun": "#8B8B00", "verb": "#006400",
68
- "question": "#00008B", "common": "#8B4500",
69
- "preposition": "#00688B", "descriptive": "#698B22",
70
- "misc": "#8B1A1A"
71
- }
72
  }
73
  colors = theme_colors[st.session_state.theme]
74
 
75
- # --- Helper Function to Initialize Assistant ---
76
- @st.cache_resource # Cache the assistant object
77
  def initialize_assistant(doc_path):
78
- """Initializes the AACAssistant."""
79
- # Create a dummy document if it doesn't exist for demonstration
80
- # This part is fine, but ensure doc_path is correct relative to where streamlit_app.py is.
81
- # If streamlit_app.py is in src/, and aac_user_experiences.txt is also in src/, then "aac_user_experiences.txt" is enough.
82
- # If streamlit_app.py is at /app and text file is /app/src/aac_user_experiences.txt, then "src/aac_user_experiences.txt" is correct.
83
  if not os.path.exists(doc_path):
84
  st.sidebar.warning(f"Doc '{os.path.basename(doc_path)}' not found at '{doc_path}'. Creating dummy.")
85
  try:
86
- os.makedirs(os.path.dirname(doc_path), exist_ok=True) # Ensure directory exists
 
 
87
  with open(doc_path, "w") as f:
88
- f.write("""
89
- I grew up in Seattle and love the rain.
90
- My favorite hobby is playing chess.
91
- I have a dog named Max.
92
- I studied computer science.
93
- I enjoy sci-fi movies.
94
- """)
95
  except Exception as e:
96
  st.sidebar.error(f"Failed to create dummy doc at '{doc_path}': {e}")
97
- return None
98
  try:
99
  st.sidebar.info("Initializing AAC Assistant... This may take some time.")
100
  assistant = rag.AACAssistant(doc_path)
@@ -103,331 +62,183 @@ def initialize_assistant(doc_path):
103
  except Exception as e:
104
  st.sidebar.error(f"Fatal Error Initializing AAC Assistant: {e}")
105
  st.sidebar.error("The assistant could not be started. Please check the logs for details.")
106
- # import traceback # Optional: show full traceback in sidebar for debugging
107
- # st.sidebar.text(traceback.format_exc())
108
  return None
109
 
110
- DEFAULT_DOCUMENT_PATH = "src/aac_user_experiences.txt" # Make sure this path is correct
111
 
112
- # --- Initialize Assistant (runs only if assistant is None in session_state) ---
113
  if st.session_state.assistant is None:
114
  st.session_state.assistant = initialize_assistant(DEFAULT_DOCUMENT_PATH)
115
 
116
- # --- CSS Styling ---
117
- # Your CSS remains largely the same, ensure variables like st.session_state.text_size and colors are defined before this.
118
  css = f"""
119
- <style>
120
- /* ... your existing CSS using {st.session_state.text_size} and {colors["..."]} ... */
121
- .big-font {{
122
- font-size:{st.session_state.text_size}px !important;
123
- text-align: center;
124
- }}
125
- .output-box {{
126
- border: 2px solid #ddd;
127
- border-radius: 5px;
128
- padding: 15px;
129
- min-height: 100px;
130
- background-color: white;
131
- margin-bottom: 15px;
132
- font-size: {st.session_state.text_size}px;
133
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
134
- }}
135
- div[data-testid="stHorizontalBlock"] {{
136
- gap: 5px;
137
- }}
138
- section[data-testid="stSidebar"] {{
139
- width: 20rem !important; /* Ensure sidebar width is applied */
140
- background-color: {colors["bg"]};
141
- }}
142
- body {{
143
- background-color: {colors["bg"]};
144
- color: {colors["text"]};
145
- }}
146
- .stButton>button {{
147
- width: 100%;
148
- height: 60px;
149
- font-size: {max(16, st.session_state.text_size - 6)}px;
150
- font-weight: bold;
151
- white-space: normal;
152
- word-wrap: break-word; /* Ensure long words in buttons wrap */
153
- padding: 0px;
154
- transition: transform 0.1s ease;
155
- }}
156
- .stButton>button:hover {{
157
- filter: brightness(95%);
158
- transform: scale(1.03);
159
- box-shadow: 0 2px 3px rgba(0,0,0,0.1);
160
- }}
161
- .control-button {{ /* This class was defined but not used on control buttons */
162
- height: 80px !important;
163
- font-size: {max(18, st.session_state.text_size - 4)}px !important;
164
- }}
165
- .btn-pronoun {{ background-color: {colors["pronoun"]} !important; border: 1px solid #888 !important; }}
166
- .btn-verb {{ background-color: {colors["verb"]} !important; border: 1px solid #888 !important; }}
167
- .btn-question {{ background-color: {colors["question"]} !important; border: 1px solid #888 !important; }}
168
- .btn-common {{ background-color: {colors["common"]} !important; border: 1px solid #888 !important; }}
169
- .btn-preposition {{ background-color: {colors["preposition"]} !important; border: 1px solid #888 !important; }}
170
- .btn-descriptive {{ background-color: {colors["descriptive"]} !important; border: 1px solid #888 !important; }}
171
- .btn-misc {{ background-color: {colors["misc"]} !important; border: 1px solid #888 !important; }}
172
- .sidebar .stChatMessage {{
173
- background-color: {colors.get('bg', '#FFFFFF')};
174
- border-radius: 8px;
175
- }}
176
- </style>
177
- """
178
  st.markdown(css, unsafe_allow_html=True)
179
 
180
- # --- JS for Button Coloring ---
181
- # Your JS remains the same
182
  js = """
183
  <script>
184
- function colorButtons() {
185
- const buttons = document.querySelectorAll('button[id^="key_"]');
186
- buttons.forEach(button => {
187
- const id = button.id;
188
- const parts = id.split('_');
189
- if (parts.length >= 4) { // Ensure category is present
190
- const category = parts[3];
191
- // Remove old category classes before adding new one
192
- button.className = button.className.replace(/btn-[a-z]+/g, '').trim();
193
- button.classList.add('btn-' + category);
194
- }
195
- });
196
- }
197
- // Run on initial load
198
- document.addEventListener('DOMContentLoaded', colorButtons);
199
-
200
- // Re-run when Streamlit updates the DOM (e.g., after button clicks that cause re-render)
201
- // Using a more robust approach for Streamlit that re-evaluates when elements are added
202
- const streamlitDoc = window.parent.document;
203
- const observer = new MutationObserver((mutationsList, observer) => {
204
- for(const mutation of mutationsList) {
205
- if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
206
- colorButtons(); // Re-apply colors if new nodes are added
207
- // We could be more specific here to only run if relevant buttons are added
208
- }
209
  }
210
  });
211
- observer.observe(streamlitDoc.body, { childList: true, subtree: true });
212
-
213
- // Initial call in case DOM is already ready from a script rerun
214
- colorButtons();
 
 
 
 
 
 
 
 
215
  </script>
216
  """
217
  st.markdown(js, unsafe_allow_html=True)
218
 
219
-
220
- # --- Main UI Layout ---
221
  st.title("Communication Board")
222
 
223
- # Output Box (moved to top as per your original structure)
224
  st.session_state.text_output = st.text_area(
225
- "Compose Message:", value=st.session_state.text_output, height=100, key="main_text_output_area" # Changed key to avoid conflict
226
  )
227
 
228
- # Keyboard Layout
229
  layout = [
230
- # ... (your existing layout definition - keep as is) ...
231
- [
232
- {"word": "I", "category": "pronoun"}, {"word": "am", "category": "verb"},
233
- {"word": "how", "category": "question"}, {"word": "what", "category": "question"},
234
- {"word": "when", "category": "question"}, {"word": "where", "category": "question"},
235
- {"word": "who", "category": "question"}, {"word": "why", "category": "question"},
236
- {"word": "That", "category": "pronoun"}, {"word": "Please", "category": "common"}
237
- ],
238
- [
239
- {"word": "me", "category": "pronoun"}, {"word": "are", "category": "verb"},
240
- {"word": "is", "category": "verb"}, {"word": "was", "category": "verb"},
241
- {"word": "will", "category": "verb"}, {"word": "help", "category": "verb"},
242
- {"word": "need", "category": "verb"}, {"word": "want", "category": "verb"},
243
- {"word": "thank you", "category": "common"}, {"word": "sorry", "category": "common"}
244
- ],
245
- [
246
- {"word": "my", "category": "pronoun"}, {"word": "can", "category": "verb"},
247
- {"word": "A", "category": "misc"}, {"word": "B", "category": "misc"},
248
- {"word": "C", "category": "misc"}, {"word": "D", "category": "misc"},
249
- {"word": "E", "category": "misc"}, {"word": "F", "category": "misc"},
250
- {"word": "G", "category": "misc"}, {"word": "H", "category": "misc"}
251
- ],
252
- [
253
- {"word": "it", "category": "pronoun"}, {"word": "did", "category": "verb"},
254
- {"word": "letter_I", "category": "misc", "display": "I"}, {"word": "J", "category": "misc"},
255
- {"word": "K", "category": "misc"}, {"word": "L", "category": "misc"},
256
- {"word": "M", "category": "misc"}, {"word": "N", "category": "misc"},
257
- {"word": "O", "category": "misc"}, {"word": "P", "category": "misc"}
258
- ],
259
- [
260
- {"word": "they", "category": "pronoun"}, {"word": "do", "category": "verb"},
261
- {"word": "Q", "category": "misc"}, {"word": "R", "category": "misc"},
262
- {"word": "S", "category": "misc"}, {"word": "T", "category": "misc"},
263
- {"word": "U", "category": "misc"}, {"word": "V", "category": "misc"},
264
- {"word": "W", "category": "misc"}, {"word": "X", "category": "misc"}
265
- ],
266
- [
267
- {"word": "we", "category": "pronoun"}, {"word": "Y", "category": "misc"},
268
- {"word": "Z", "category": "misc"}, {"word": "1", "category": "misc"},
269
- {"word": "2", "category": "misc"}, {"word": "3", "category": "misc"},
270
- {"word": "4", "category": "misc"}, {"word": "5", "category": "misc"}, # Corrected, was 4
271
- {"word": ".", "category": "misc"}, {"word": "?", "category": "misc"}
272
- ],
273
- [
274
- {"word": "you", "category": "pronoun"}, {"word": "6", "category": "misc"},
275
- {"word": "7", "category": "misc"}, {"word": "8", "category": "misc"},
276
- {"word": "9", "category": "misc"}, {"word": "0", "category": "misc"},
277
- {"word": "-", "category": "misc"}, {"word": "!", "category": "misc"},
278
- {"word": ",", "category": "misc"}, {"word": "SPACE", "category": "misc"}
279
- ],
280
- [
281
- {"word": "your", "category": "pronoun"}, {"word": "like", "category": "verb"},
282
- {"word": "to", "category": "preposition"}, {"word": "with", "category": "preposition"},
283
- {"word": "in", "category": "preposition"}, {"word": "the", "category": "misc"},
284
- {"word": "and", "category": "misc"}, {"word": "but", "category": "misc"},
285
- {"word": "not", "category": "descriptive"}, {"word": "yes", "category": "common"}
286
- ]
287
  ]
288
 
289
- # --- Add Custom Words to Layout (if any) ---
 
290
  if st.session_state.custom_words:
291
- # This logic for adding custom words seems okay, but ensure layout is mutable if modified often
292
- # Or rebuild the full layout list including custom words each time
293
  current_custom_row = []
294
  for idx, word_info in enumerate(st.session_state.custom_words):
295
  word = word_info["word"]
296
  category = word_info["category"]
297
  current_custom_row.append({"word": f"custom_{idx}_{word}", "display": word, "category": category})
298
- if len(current_custom_row) == 10: # Assuming 10 columns for keyboard
299
- layout.append(list(current_custom_row)) # Add a copy
300
  current_custom_row = []
301
- if current_custom_row: # Add any remaining custom words
302
  while len(current_custom_row) < 10:
303
- current_custom_row.append({"word": "", "category": "misc"}) # Pad with empty placeholders
304
- layout.append(list(current_custom_row))
305
 
306
- # --- Keyboard Rendering ---
307
  def add_to_output(word_to_add):
308
  current_text = st.session_state.get("text_output", "")
309
  if word_to_add == "SPACE":
310
  current_text += " "
311
  elif word_to_add in [".", "?", "!", ",", "-"]:
312
  current_text += word_to_add
313
- elif word_to_add.isdigit() or (len(word_to_add) == 1 and word_to_add.isalpha()): # For single letters/numbers
314
  current_text += word_to_add
315
- else: # For full words
316
  if current_text and not current_text.endswith(" "):
317
  current_text += " "
318
  current_text += word_to_add
319
  st.session_state.text_output = current_text
320
 
321
  st.markdown("### Communication Keyboard")
322
- for row_idx, row_items in enumerate(layout):
323
  cols = st.columns(len(row_items))
324
  for col_idx, item_info in enumerate(row_items):
325
- # Ensure item_info is a dictionary and has 'word'
326
  if not isinstance(item_info, dict) or "word" not in item_info or item_info["word"] == "":
327
- continue
328
-
329
  word = item_info["word"]
330
- category = item_info.get("category", "misc") # Default category if missing
331
  display_text = item_info.get("display", word)
332
-
333
- button_key = f"key_{row_idx}_{col_idx}_{category}_{word.replace(' ', '_')}" # Make key more robust
334
 
335
- # Use a lambda with default arguments to capture current word for the callback
336
- # This avoids the late binding issue with loops if not using functools.partial or similar
337
  if cols[col_idx].button(
338
- display_text if word != "SPACE" else "␣", # Display text
339
  key=button_key,
340
  use_container_width=True
341
  ):
342
- # Action happens here when button is clicked, then Streamlit reruns
343
- # For complex actions or to avoid issues with default args in loops for callbacks,
344
- # direct action or ensuring unique callbacks is better.
345
- # Here, the action is simple enough that direct modification is fine.
346
- word_to_add_on_click = display_text # Use display_text for 'letter_I' or custom words
347
- if word.startswith("custom_") or word.startswith("letter_"):
348
- add_to_output(display_text)
349
- else:
350
- add_to_output(word)
351
- st.rerun() # Rerun to update the text_area with the new character/word
352
-
353
 
354
- # --- Control Buttons & SEND Action Processing ---
355
  col1, col2, col3, col4 = st.columns(4)
356
 
357
  with col1:
358
- if st.button("CLEAR", key="clear_btn", help="Clear all text", use_container_width=True, type="secondary"): # Changed type
359
  st.session_state.text_output = ""
360
- st.rerun()
361
  with col2:
362
- if st.button("SPEAK", key="speak_btn", help="Speak the current text (feature placeholder)", use_container_width=True, type="secondary"): # Changed type
363
  if st.session_state.text_output:
364
  st.toast(f"Speaking (simulated): {st.session_state.text_output}", icon="🔊")
365
- # Actual text-to-speech integration would go here
366
-
367
- # Moved DEL button here as SPEAK often stands alone or is larger
368
- if st.button("⌫ DEL", key="backspace_btn", help="Delete last character", use_container_width=True): # Changed key
369
  if st.session_state.text_output:
370
  st.session_state.text_output = st.session_state.text_output[:-1]
371
- st.rerun()
372
- with col4:
373
- if st.button("⌫ WORD", key="backspace_word_btn", help="Delete last word", use_container_width=True): # Changed key
374
- if st.session_state.text_output:
375
- words = st.session_state.text_output.rstrip().split()
376
- if words:
377
- words.pop()
378
- st.session_state.text_output = " ".join(words)
379
- if words: # Add a space if there are still words left
380
- st.session_state.text_output += " "
381
- else: # If no words left after split (e.g. only spaces before), clear it
382
- st.session_state.text_output = ""
383
  st.rerun()
 
 
 
 
 
 
 
 
384
 
385
- with col3:
386
  if st.button("SEND 🗣️", key="send_btn", help="Send message to assistant", use_container_width=True, type="primary"):
387
  if 'assistant' not in st.session_state or st.session_state.assistant is None:
388
- st.error("AAC Assistant is not initialized. Please check sidebar for errors.")
389
  elif not st.session_state.text_output.strip():
390
  st.toast("Please compose a message first.", icon="✍️")
391
  else:
392
- user_message_to_send = st.session_state.text_output # Capture message
393
-
394
- # Add user message to chat history
395
  st.session_state.messages.append({"role": "user", "content": user_message_to_send})
 
396
 
397
- # Clear the input text area immediately
398
- st.session_state.text_output = ""
399
-
400
- # Process with AACAssistant and show a spinner
401
  with st.spinner("Elliot is thinking... please wait a moment..."):
402
  try:
403
  print(f"DEBUG [Streamlit UI]: User typed: '{user_message_to_send}'")
404
  print(f"DEBUG [Streamlit UI]: Calling assistant.process_query for '{user_message_to_send}'")
405
-
406
- # CRITICAL: This is where your rag.py's process_query is called
407
- # Ensure it returns ONLY the assistant's clean response
408
  assistant_response = st.session_state.assistant.process_query(user_message_to_send)
409
-
410
  print(f"DEBUG [Streamlit UI]: Received assistant response: '{assistant_response}'")
411
  st.session_state.messages.append({"role": "assistant", "content": assistant_response})
412
  except Exception as e:
413
  error_message_display = f"An error occurred while processing your message: {e}"
414
- st.error(error_message_display)
415
  st.session_state.messages.append({"role": "assistant", "content": f"*Error: Could not get a response. Details: {e}*"})
416
- # For more detailed debugging in the UI itself if needed:
417
- # import traceback
418
- # st.text_area("Error Traceback:", traceback.format_exc(), height=200)
419
-
420
- st.rerun() # Rerun to update the chat display and clear the (now empty) input field visually.
421
 
422
- # --- Settings Sidebar ---
423
  with st.sidebar:
424
- # Your settings code (commented out in your original, kept as is)
425
- # st.title("Settings")
426
- # ...
427
  st.divider()
428
- # --- Chat History Display in Sidebar ---
429
  st.subheader("Conversation Log")
430
- chat_container = st.container(height=400 if st.session_state.messages else 100) # Adjust height if empty
431
  with chat_container:
432
  if not st.session_state.messages:
433
  st.caption("No messages yet...")
 
1
  import streamlit as st
2
 
3
+ import altair as alt
4
+ import numpy as np
5
  import pandas as pd
6
+ import rag
7
  import os
8
 
 
9
  cache_dir_base = "/app/.cache_app"
10
+ if not os.path.exists(cache_dir_base):
11
  try:
12
  os.makedirs(cache_dir_base, exist_ok=True)
13
  except Exception as e:
14
  st.error(f"Failed to create base cache directory {cache_dir_base}: {e}")
15
 
 
 
16
  os.environ['HF_HOME'] = os.environ.get('HF_HOME', os.path.join(cache_dir_base, 'huggingface_hub'))
17
  os.environ['TRANSFORMERS_CACHE'] = os.environ.get('TRANSFORMERS_CACHE', os.path.join(cache_dir_base, 'transformers'))
18
  os.environ['SENTENCE_TRANSFORMERS_HOME'] = os.environ.get('SENTENCE_TRANSFORMERS_HOME', os.path.join(cache_dir_base, 'sentence_transformers'))
19
  os.environ['PIP_CACHE_DIR'] = os.environ.get('PIP_CACHE_DIR', os.path.join(cache_dir_base, 'pip'))
20
 
 
21
  st.set_page_config(layout="wide", page_title="Communication Board")
22
 
23
+ if 'assistant_initialized_flag' not in st.session_state:
 
24
  st.session_state.current_page = "main"
25
  st.session_state.show_custom_words = False
26
  st.session_state.custom_words = []
 
29
  st.session_state.speech_rate = 1.0
30
  st.session_state.voice_option = "Default Voice"
31
  st.session_state.messages = []
32
+ st.session_state.assistant = None
33
  st.session_state.text_output = ""
34
+ st.session_state.assistant_initialized_flag = True
35
 
 
 
36
  theme_colors = {
37
+ "Default": {"bg": "#FFFFFF", "text": "#000000", "pronoun": "#FFFF99", "verb": "#CCFFCC", "question": "#CCCCFF", "common": "#FFCC99", "preposition": "#99CCFF", "descriptive": "#CCFF99", "misc": "#FFB6C1"},
38
+ "High Contrast": {"bg": "#FFFFFF", "text": "#000000", "pronoun": "#FFFF00", "verb": "#00FF00", "question": "#0000FF", "common": "#FF6600", "preposition": "#00CCFF", "descriptive": "#66FF33", "misc": "#FF3366"},
39
+ "Pastel": {"bg": "#F8F8FF", "text": "#333333", "pronoun": "#FFEFD5", "verb": "#E0FFFF", "question": "#D8BFD8", "common": "#FFE4B5", "preposition": "#B0E0E6", "descriptive": "#F0FFF0", "misc": "#FFF0F5"},
40
+ "Dark Mode": {"bg": "#333333", "text": "#FFFFFF", "pronoun": "#8B8B00", "verb": "#006400", "question": "#00008B", "common": "#8B4500", "preposition": "#00688B", "descriptive": "#698B22", "misc": "#8B1A1A"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  }
42
  colors = theme_colors[st.session_state.theme]
43
 
44
+ @st.cache_resource
 
45
  def initialize_assistant(doc_path):
 
 
 
 
 
46
  if not os.path.exists(doc_path):
47
  st.sidebar.warning(f"Doc '{os.path.basename(doc_path)}' not found at '{doc_path}'. Creating dummy.")
48
  try:
49
+ parent_dir = os.path.dirname(doc_path)
50
+ if parent_dir: # Ensure parent_dir is not empty if doc_path is just a filename
51
+ os.makedirs(parent_dir, exist_ok=True)
52
  with open(doc_path, "w") as f:
53
+ f.write("I grew up in Seattle and love the rain.\nMy favorite hobby is playing chess.\nI have a dog named Max.\nI studied computer science.\nI enjoy sci-fi movies.\n")
 
 
 
 
 
 
54
  except Exception as e:
55
  st.sidebar.error(f"Failed to create dummy doc at '{doc_path}': {e}")
56
+ return None
57
  try:
58
  st.sidebar.info("Initializing AAC Assistant... This may take some time.")
59
  assistant = rag.AACAssistant(doc_path)
 
62
  except Exception as e:
63
  st.sidebar.error(f"Fatal Error Initializing AAC Assistant: {e}")
64
  st.sidebar.error("The assistant could not be started. Please check the logs for details.")
 
 
65
  return None
66
 
67
+ DEFAULT_DOCUMENT_PATH = "src/aac_user_experiences.txt"
68
 
 
69
  if st.session_state.assistant is None:
70
  st.session_state.assistant = initialize_assistant(DEFAULT_DOCUMENT_PATH)
71
 
 
 
72
  css = f"""
73
+ <style>
74
+ .big-font {{ font-size:{st.session_state.text_size}px !important; text-align: center; }}
75
+ .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); }}
76
+ div[data-testid="stHorizontalBlock"] {{ gap: 5px; }}
77
+ section[data-testid="stSidebar"] {{ width: 20rem !important; background-color: {colors["bg"]}; }}
78
+ body {{ background-color: {colors["bg"]}; color: {colors["text"]}; }}
79
+ .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; }}
80
+ .stButton>button:hover {{ filter: brightness(95%); transform: scale(1.03); box-shadow: 0 2px 3px rgba(0,0,0,0.1); }}
81
+ .control-button {{ height: 80px !important; font-size: {max(18, st.session_state.text_size - 4)}px !important; }}
82
+ .btn-pronoun {{ background-color: {colors["pronoun"]} !important; border: 1px solid #888 !important; }}
83
+ .btn-verb {{ background-color: {colors["verb"]} !important; border: 1px solid #888 !important; }}
84
+ .btn-question {{ background-color: {colors["question"]} !important; border: 1px solid #888 !important; }}
85
+ .btn-common {{ background-color: {colors["common"]} !important; border: 1px solid #888 !important; }}
86
+ .btn-preposition {{ background-color: {colors["preposition"]} !important; border: 1px solid #888 !important; }}
87
+ .btn-descriptive {{ background-color: {colors["descriptive"]} !important; border: 1px solid #888 !important; }}
88
+ .btn-misc {{ background-color: {colors["misc"]} !important; border: 1px solid #888 !important; }}
89
+ .sidebar .stChatMessage {{ background-color: {colors.get('bg', '#FFFFFF')}; border-radius: 8px; }}
90
+ </style>
91
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  st.markdown(css, unsafe_allow_html=True)
93
 
 
 
94
  js = """
95
  <script>
96
+ function colorButtons() {
97
+ const buttons = document.querySelectorAll('button[id^="key_"]');
98
+ buttons.forEach(button => {
99
+ const id = button.id;
100
+ const parts = id.split('_');
101
+ if (parts.length >= 4) {
102
+ const category = parts[3];
103
+ button.className = button.className.replace(/btn-[a-z]+/g, '').trim();
104
+ button.classList.add('btn-' + category);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  }
106
  });
107
+ }
108
+ document.addEventListener('DOMContentLoaded', colorButtons);
109
+ const streamlitDoc = window.parent.document;
110
+ const observer = new MutationObserver((mutationsList, observer) => {
111
+ for(const mutation of mutationsList) {
112
+ if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
113
+ colorButtons();
114
+ }
115
+ }
116
+ });
117
+ observer.observe(streamlitDoc.body, { childList: true, subtree: true });
118
+ colorButtons();
119
  </script>
120
  """
121
  st.markdown(js, unsafe_allow_html=True)
122
 
 
 
123
  st.title("Communication Board")
124
 
 
125
  st.session_state.text_output = st.text_area(
126
+ "Compose Message:", value=st.session_state.get("text_output", ""), height=100, key="main_text_output_area"
127
  )
128
 
 
129
  layout = [
130
+ [{"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"}],
131
+ [{"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"}],
132
+ [{"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"}],
133
+ [{"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"}],
134
+ [{"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"}],
135
+ [{"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"}],
136
+ [{"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"}],
137
+ [{"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"}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  ]
139
 
140
+ dynamic_layout = list(layout) # Make a mutable copy for adding custom words
141
+
142
  if st.session_state.custom_words:
 
 
143
  current_custom_row = []
144
  for idx, word_info in enumerate(st.session_state.custom_words):
145
  word = word_info["word"]
146
  category = word_info["category"]
147
  current_custom_row.append({"word": f"custom_{idx}_{word}", "display": word, "category": category})
148
+ if len(current_custom_row) == 10:
149
+ dynamic_layout.append(list(current_custom_row))
150
  current_custom_row = []
151
+ if current_custom_row:
152
  while len(current_custom_row) < 10:
153
+ current_custom_row.append({"word": "", "category": "misc"})
154
+ dynamic_layout.append(list(current_custom_row))
155
 
 
156
  def add_to_output(word_to_add):
157
  current_text = st.session_state.get("text_output", "")
158
  if word_to_add == "SPACE":
159
  current_text += " "
160
  elif word_to_add in [".", "?", "!", ",", "-"]:
161
  current_text += word_to_add
162
+ elif word_to_add.isdigit() or (len(word_to_add) == 1 and word_to_add.isalpha()):
163
  current_text += word_to_add
164
+ else:
165
  if current_text and not current_text.endswith(" "):
166
  current_text += " "
167
  current_text += word_to_add
168
  st.session_state.text_output = current_text
169
 
170
  st.markdown("### Communication Keyboard")
171
+ for row_idx, row_items in enumerate(dynamic_layout):
172
  cols = st.columns(len(row_items))
173
  for col_idx, item_info in enumerate(row_items):
 
174
  if not isinstance(item_info, dict) or "word" not in item_info or item_info["word"] == "":
175
+ continue
 
176
  word = item_info["word"]
177
+ category = item_info.get("category", "misc")
178
  display_text = item_info.get("display", word)
179
+ button_key = f"key_{row_idx}_{col_idx}_{category}_{word.replace(' ', '_').replace('.', 'dot').replace('?', 'qmark')}" # Make key even more robust
 
180
 
 
 
181
  if cols[col_idx].button(
182
+ display_text if word != "SPACE" else "␣",
183
  key=button_key,
184
  use_container_width=True
185
  ):
186
+ word_to_add_on_click = display_text if word.startswith("custom_") or word.startswith("letter_") else word
187
+ add_to_output(word_to_add_on_click)
188
+ st.rerun()
 
 
 
 
 
 
 
 
189
 
 
190
  col1, col2, col3, col4 = st.columns(4)
191
 
192
  with col1:
193
+ if st.button("CLEAR", key="clear_btn", help="Clear all text", use_container_width=True, type="secondary"):
194
  st.session_state.text_output = ""
195
+ st.rerun()
196
  with col2:
197
+ if st.button("SPEAK", key="speak_btn", help="Speak the current text (feature placeholder)", use_container_width=True, type="secondary"):
198
  if st.session_state.text_output:
199
  st.toast(f"Speaking (simulated): {st.session_state.text_output}", icon="🔊")
200
+
201
+ if st.button("⌫ DEL", key="backspace_btn", help="Delete last character", use_container_width=True):
 
 
202
  if st.session_state.text_output:
203
  st.session_state.text_output = st.session_state.text_output[:-1]
 
 
 
 
 
 
 
 
 
 
 
 
204
  st.rerun()
205
+ with col4:
206
+ if st.button("⌫ WORD", key="backspace_word_btn", help="Delete last word", use_container_width=True):
207
+ current_text = st.session_state.text_output.rstrip()
208
+ if ' ' in current_text:
209
+ st.session_state.text_output = current_text.rsplit(' ', 1)[0] + " "
210
+ else:
211
+ st.session_state.text_output = ""
212
+ st.rerun()
213
 
214
+ with col3:
215
  if st.button("SEND 🗣️", key="send_btn", help="Send message to assistant", use_container_width=True, type="primary"):
216
  if 'assistant' not in st.session_state or st.session_state.assistant is None:
217
+ st.error("AAC Assistant is not initialized. Please check sidebar for errors or wait for initialization to complete.")
218
  elif not st.session_state.text_output.strip():
219
  st.toast("Please compose a message first.", icon="✍️")
220
  else:
221
+ user_message_to_send = st.session_state.text_output
 
 
222
  st.session_state.messages.append({"role": "user", "content": user_message_to_send})
223
+ st.session_state.text_output = ""
224
 
 
 
 
 
225
  with st.spinner("Elliot is thinking... please wait a moment..."):
226
  try:
227
  print(f"DEBUG [Streamlit UI]: User typed: '{user_message_to_send}'")
228
  print(f"DEBUG [Streamlit UI]: Calling assistant.process_query for '{user_message_to_send}'")
 
 
 
229
  assistant_response = st.session_state.assistant.process_query(user_message_to_send)
 
230
  print(f"DEBUG [Streamlit UI]: Received assistant response: '{assistant_response}'")
231
  st.session_state.messages.append({"role": "assistant", "content": assistant_response})
232
  except Exception as e:
233
  error_message_display = f"An error occurred while processing your message: {e}"
234
+ st.error(error_message_display)
235
  st.session_state.messages.append({"role": "assistant", "content": f"*Error: Could not get a response. Details: {e}*"})
236
+ st.rerun()
 
 
 
 
237
 
 
238
  with st.sidebar:
 
 
 
239
  st.divider()
 
240
  st.subheader("Conversation Log")
241
+ chat_container = st.container(height=400 if st.session_state.messages else 100)
242
  with chat_container:
243
  if not st.session_state.messages:
244
  st.caption("No messages yet...")