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- app.py +54 -50
- 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 |
-
|
| 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 |
-
"""
|
| 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 |
-
|
| 235 |
-
|
|
|
|
| 236 |
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 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 =
|
| 284 |
-
st.session_state.current_session_id
|
| 285 |
-
|
| 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 =
|
| 325 |
-
st.session_state.current_session_id
|
| 326 |
-
|
| 327 |
|
| 328 |
-
# Restore persisted sessions from
|
| 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
|
|
|