rdz-falcon commited on
Commit
3941fef
·
verified ·
1 Parent(s): 19b88dc

Update src/streamlit_app.py

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