hesham commited on
Commit
d522c85
·
1 Parent(s): a559d80

v1.5.2: Replace localStorage with server-side file persistence - bulletproof on HF Spaces

Browse files
Files changed (2) hide show
  1. app.py +54 -50
  2. requirements.txt +0 -1
app.py CHANGED
@@ -4,7 +4,6 @@ An AI-powered chatbot for designing R/R TCL clinical trials.
4
  """
5
 
6
  import streamlit as st
7
- from streamlit_javascript import st_javascript
8
  import anthropic
9
  import os
10
  import time
@@ -185,19 +184,13 @@ def get_theme_css(dark_mode: bool = True) -> str:
185
  """
186
 
187
 
188
- SESSIONS_LS_KEY = "clinical_trial_sessions"
189
- CURRENT_SESSION_LS_KEY = "clinical_trial_current_session"
190
 
191
 
192
  def _new_session_id() -> str:
193
  return datetime.now().strftime("%Y%m%d_%H%M%S_%f")
194
 
195
 
196
- def _js_key(base: str) -> str:
197
- """Generate a unique key for each st_javascript call to avoid duplicate element ID errors."""
198
- return f"{base}_{time.time_ns()}"
199
-
200
-
201
  def _session_title(messages: list) -> str:
202
  """Auto-generate a session title from the first user message."""
203
  for m in messages:
@@ -207,53 +200,65 @@ def _session_title(messages: list) -> str:
207
  return "New conversation"
208
 
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  def save_all_sessions() -> None:
211
- """Persist all sessions + current session ID to localStorage."""
212
- # Sync current messages into sessions dict before saving
213
  sid = st.session_state.current_session_id
214
  if sid in st.session_state.sessions:
215
- st.session_state.sessions[sid]["messages"] = st.session_state.messages
216
  st.session_state.sessions[sid]["title"] = _session_title(st.session_state.messages)
217
-
218
- sessions_json = json.dumps(st.session_state.sessions, ensure_ascii=False)
219
- encoded = sessions_json.replace('\\', '\\\\').replace('`', '\\`')
220
- # Unique keys prevent StreamlitDuplicateElementId when called multiple times per render
221
- st_javascript(f"localStorage.setItem('{SESSIONS_LS_KEY}', `{encoded}`); 1;", key=_js_key("save_sess"))
222
- st_javascript(f"localStorage.setItem('{CURRENT_SESSION_LS_KEY}', '{sid}'); 1;", key=_js_key("save_cur"))
223
 
224
 
225
  def load_all_sessions() -> None:
226
- """
227
- Restore all sessions from localStorage on first page load.
228
- st_javascript returns JS values directly to Python — no URL tricks.
229
- """
230
  if st.session_state.get("_sessions_restored"):
231
  return
232
  st.session_state._sessions_restored = True
233
 
234
- raw_sessions = st_javascript(f"localStorage.getItem('{SESSIONS_LS_KEY}');", key="load_sessions_once")
235
- raw_current = st_javascript(f"localStorage.getItem('{CURRENT_SESSION_LS_KEY}');", key="load_current_once")
 
236
 
237
- if raw_sessions and raw_sessions not in ("null", "undefined", "", None):
238
- try:
239
- sessions = json.loads(raw_sessions)
240
- if isinstance(sessions, dict) and sessions:
241
- st.session_state.sessions = sessions
242
- # Restore last active session
243
- if raw_current and raw_current in sessions:
244
- st.session_state.current_session_id = raw_current
245
- else:
246
- # Fall back to most recent
247
- st.session_state.current_session_id = sorted(sessions.keys())[-1]
248
- st.session_state.messages = sessions[
249
- st.session_state.current_session_id
250
- ]["messages"]
251
- except Exception:
252
- pass # Corrupted — start fresh
253
 
254
 
255
  def create_new_session() -> None:
256
  """Create a brand-new empty session and switch to it."""
 
257
  sid = _new_session_id()
258
  st.session_state.sessions[sid] = {
259
  "title": "New conversation",
@@ -268,10 +273,10 @@ def create_new_session() -> None:
268
 
269
  def switch_session(sid: str) -> None:
270
  """Switch to an existing session."""
271
- # Save current session first
272
- save_all_sessions()
273
  st.session_state.current_session_id = sid
274
- st.session_state.messages = st.session_state.sessions[sid]["messages"]
 
275
  st.rerun()
276
 
277
 
@@ -280,11 +285,10 @@ def delete_session(sid: str) -> None:
280
  st.session_state.sessions.pop(sid, None)
281
  if st.session_state.sessions:
282
  st.session_state.current_session_id = sorted(st.session_state.sessions.keys())[-1]
283
- st.session_state.messages = st.session_state.sessions[
284
- st.session_state.current_session_id
285
- ]["messages"]
286
  else:
287
- # No sessions left — create fresh one
288
  new_sid = _new_session_id()
289
  st.session_state.sessions[new_sid] = {
290
  "title": "New conversation",
@@ -321,11 +325,11 @@ def initialize_session_state() -> None:
321
  st.session_state.current_session_id = first_sid
322
 
323
  if "messages" not in st.session_state:
324
- st.session_state.messages = st.session_state.sessions[
325
- st.session_state.current_session_id
326
- ]["messages"]
327
 
328
- # Restore persisted sessions from localStorage (once per page load)
329
  load_all_sessions()
330
 
331
 
 
4
  """
5
 
6
  import streamlit as st
 
7
  import anthropic
8
  import os
9
  import time
 
184
  """
185
 
186
 
187
+ SESSIONS_FILE = os.path.join(os.environ.get("HOME", "/tmp"), ".clinical_trial_sessions.json")
 
188
 
189
 
190
  def _new_session_id() -> str:
191
  return datetime.now().strftime("%Y%m%d_%H%M%S_%f")
192
 
193
 
 
 
 
 
 
194
  def _session_title(messages: list) -> str:
195
  """Auto-generate a session title from the first user message."""
196
  for m in messages:
 
200
  return "New conversation"
201
 
202
 
203
+ def _read_sessions_file() -> dict:
204
+ """Read sessions from the JSON file on disk. Returns empty dict on any error."""
205
+ try:
206
+ if os.path.exists(SESSIONS_FILE):
207
+ with open(SESSIONS_FILE, "r", encoding="utf-8") as f:
208
+ data = json.load(f)
209
+ if isinstance(data, dict) and "sessions" in data:
210
+ return data
211
+ except Exception:
212
+ pass
213
+ return {}
214
+
215
+
216
+ def _write_sessions_file(sessions: dict, current_sid: str) -> None:
217
+ """Write sessions and current session ID to the JSON file on disk."""
218
+ try:
219
+ data = {"sessions": sessions, "current_session_id": current_sid}
220
+ with open(SESSIONS_FILE, "w", encoding="utf-8") as f:
221
+ json.dump(data, f, ensure_ascii=False, indent=2)
222
+ except Exception:
223
+ pass # Non-critical — session state in memory still works
224
+
225
+
226
  def save_all_sessions() -> None:
227
+ """Sync current messages into sessions dict and persist to disk."""
 
228
  sid = st.session_state.current_session_id
229
  if sid in st.session_state.sessions:
230
+ st.session_state.sessions[sid]["messages"] = list(st.session_state.messages)
231
  st.session_state.sessions[sid]["title"] = _session_title(st.session_state.messages)
232
+ _write_sessions_file(st.session_state.sessions, sid)
 
 
 
 
 
233
 
234
 
235
  def load_all_sessions() -> None:
236
+ """Restore sessions from disk into session state. Runs once per page load."""
 
 
 
237
  if st.session_state.get("_sessions_restored"):
238
  return
239
  st.session_state._sessions_restored = True
240
 
241
+ data = _read_sessions_file()
242
+ if not data:
243
+ return
244
 
245
+ sessions = data.get("sessions", {})
246
+ current_sid = data.get("current_session_id", "")
247
+
248
+ if sessions:
249
+ st.session_state.sessions = sessions
250
+ if current_sid and current_sid in sessions:
251
+ st.session_state.current_session_id = current_sid
252
+ else:
253
+ st.session_state.current_session_id = sorted(sessions.keys())[-1]
254
+ st.session_state.messages = list(
255
+ st.session_state.sessions[st.session_state.current_session_id]["messages"]
256
+ )
 
 
 
 
257
 
258
 
259
  def create_new_session() -> None:
260
  """Create a brand-new empty session and switch to it."""
261
+ save_all_sessions() # Save the current session first
262
  sid = _new_session_id()
263
  st.session_state.sessions[sid] = {
264
  "title": "New conversation",
 
273
 
274
  def switch_session(sid: str) -> None:
275
  """Switch to an existing session."""
276
+ save_all_sessions() # Save current session first
 
277
  st.session_state.current_session_id = sid
278
+ st.session_state.messages = list(st.session_state.sessions[sid]["messages"])
279
+ save_all_sessions()
280
  st.rerun()
281
 
282
 
 
285
  st.session_state.sessions.pop(sid, None)
286
  if st.session_state.sessions:
287
  st.session_state.current_session_id = sorted(st.session_state.sessions.keys())[-1]
288
+ st.session_state.messages = list(
289
+ st.session_state.sessions[st.session_state.current_session_id]["messages"]
290
+ )
291
  else:
 
292
  new_sid = _new_session_id()
293
  st.session_state.sessions[new_sid] = {
294
  "title": "New conversation",
 
325
  st.session_state.current_session_id = first_sid
326
 
327
  if "messages" not in st.session_state:
328
+ st.session_state.messages = list(
329
+ st.session_state.sessions[st.session_state.current_session_id]["messages"]
330
+ )
331
 
332
+ # Restore persisted sessions from disk (once per page load)
333
  load_all_sessions()
334
 
335
 
requirements.txt CHANGED
@@ -1,4 +1,3 @@
1
  # Core dependencies
2
  streamlit>=1.28.0
3
  anthropic>=0.45.0
4
- streamlit-javascript>=0.1.5
 
1
  # Core dependencies
2
  streamlit>=1.28.0
3
  anthropic>=0.45.0