LazyHuman10 commited on
Commit
ff90096
·
1 Parent(s): 9e39bcc

Add optional remembered assistant settings

Browse files
Files changed (3) hide show
  1. example.env +5 -1
  2. pages/Plexi-Assistant.py +191 -0
  3. requirements.txt +2 -1
example.env CHANGED
@@ -2,4 +2,8 @@
2
 
3
  # GitHub repo hosting study materials (owner/repo format)
4
  # Defaults to KunalGupta25/plexi-materials if not set
5
- # MATERIALS_REPO=KunalGupta25/plexi-materials
 
 
 
 
 
2
 
3
  # GitHub repo hosting study materials (owner/repo format)
4
  # Defaults to KunalGupta25/plexi-materials if not set
5
+ # MATERIALS_REPO=KunalGupta25/plexi-materials
6
+
7
+ # Secret used to encrypt browser-saved Plexi assistant settings
8
+ # Required for "remember settings on this device"
9
+ # PLEXI_COOKIE_PASSWORD=replace-with-a-long-random-secret
pages/Plexi-Assistant.py CHANGED
@@ -10,8 +10,19 @@ Flow per user message:
10
  3. Build focused prompt with retrieved chunks -> call user's LLM
11
  """
12
 
 
 
 
13
  import requests
14
  import streamlit as st
 
 
 
 
 
 
 
 
15
  from utils import (
16
  fetch_rag_index,
17
  get_manifest,
@@ -104,6 +115,17 @@ PROVIDER_NAMES = list(PROVIDERS.keys())
104
  PLEXI_GPT_URL = "https://chatgpt.com/g/g-69caa671910481919ce71d19952e34e5-plexi"
105
  PLEXI_MCP_GUIDE_URL = "https://lazyhuman.notion.site/Setting-Up-Plexi-MCP-for-Claude-and-ChatGPT-336e3502f0918090b69fdbed148e8e55"
106
  PLEXI_MCP_ENDPOINT = "https://plexi-mcp.vercel.app/api/mcp"
 
 
 
 
 
 
 
 
 
 
 
107
 
108
 
109
  def _matches_scope(node, semester: str, subject: str) -> bool:
@@ -118,6 +140,77 @@ def queue_prompt(prompt: str):
118
  st.rerun()
119
 
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  def render_external_access():
122
  """Render low-emphasis outbound access actions."""
123
  st.markdown(
@@ -263,9 +356,33 @@ def render_onboarding(manifest):
263
  api_key = st.text_input(
264
  "API Key",
265
  type="password",
 
266
  placeholder="Paste your API key here",
267
  )
268
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  semester_names = sorted(manifest.keys())
270
  default_semester = st.session_state.get("asst_semester")
271
  semester_index = (
@@ -311,8 +428,24 @@ def render_onboarding(manifest):
311
  st.session_state.cfg_model = model_name
312
  st.session_state.asst_semester = selected_semester
313
  st.session_state.asst_subject = selected_subject
 
314
  if api_key:
315
  st.session_state.api_key = api_key
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  st.session_state.pop("messages", None)
317
  st.rerun()
318
 
@@ -346,6 +479,7 @@ def render_onboarding(manifest):
346
  )
347
 
348
 
 
349
  render_sidebar_intro()
350
 
351
  try:
@@ -467,24 +601,70 @@ with st.sidebar:
467
  key="sb_api_key",
468
  )
469
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  changed = (
471
  new_provider != provider_name
472
  or new_base_url != base_url
473
  or new_model != model_name
474
  or new_key != api_key
 
475
  )
476
  if changed and new_model:
477
  if st.button("Apply Changes", use_container_width=True, type="primary"):
478
  st.session_state.cfg_provider = new_provider
479
  st.session_state.cfg_base_url = new_base_url
480
  st.session_state.cfg_model = new_model
 
481
  if new_key:
482
  st.session_state.api_key = new_key
483
  elif "api_key" in st.session_state:
484
  del st.session_state.api_key
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  st.session_state.pop("messages", None)
486
  st.rerun()
487
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
  if st.button("New Chat", use_container_width=True):
489
  st.session_state.pop("messages", None)
490
  st.rerun()
@@ -503,6 +683,15 @@ render_page_header(
503
 
504
  render_external_access()
505
 
 
 
 
 
 
 
 
 
 
506
  render_stat_cards(
507
  [
508
  {
@@ -636,6 +825,8 @@ if prompt:
636
  st.stop()
637
  if "AUTH_ERROR" in err_text:
638
  st.error(err_text.split(": ", 1)[1])
 
 
639
  if "api_key" in st.session_state:
640
  del st.session_state.api_key
641
  st.session_state.messages.pop()
 
10
  3. Build focused prompt with retrieved chunks -> call user's LLM
11
  """
12
 
13
+ import json
14
+ import os
15
+
16
  import requests
17
  import streamlit as st
18
+
19
+ try:
20
+ from streamlit_cookies_manager_ext import EncryptedCookieManager
21
+
22
+ COOKIES_MANAGER_AVAILABLE = True
23
+ except ImportError:
24
+ EncryptedCookieManager = None
25
+ COOKIES_MANAGER_AVAILABLE = False
26
  from utils import (
27
  fetch_rag_index,
28
  get_manifest,
 
115
  PLEXI_GPT_URL = "https://chatgpt.com/g/g-69caa671910481919ce71d19952e34e5-plexi"
116
  PLEXI_MCP_GUIDE_URL = "https://lazyhuman.notion.site/Setting-Up-Plexi-MCP-for-Claude-and-ChatGPT-336e3502f0918090b69fdbed148e8e55"
117
  PLEXI_MCP_ENDPOINT = "https://plexi-mcp.vercel.app/api/mcp"
118
+ SAVED_CONFIG_COOKIE = "assistant_config"
119
+ COOKIE_PASSWORD = os.getenv("PLEXI_COOKIE_PASSWORD") or os.getenv("COOKIES_PASSWORD")
120
+
121
+ cookies = None
122
+ if COOKIE_PASSWORD and COOKIES_MANAGER_AVAILABLE:
123
+ cookies = EncryptedCookieManager(
124
+ prefix="plexi/assistant/",
125
+ password=COOKIE_PASSWORD,
126
+ )
127
+ if not cookies.ready():
128
+ st.stop()
129
 
130
 
131
  def _matches_scope(node, semester: str, subject: str) -> bool:
 
140
  st.rerun()
141
 
142
 
143
+ def _saved_config_available():
144
+ return cookies is not None
145
+
146
+
147
+ def _load_saved_config():
148
+ """Load saved assistant settings from the browser cookie."""
149
+ if not _saved_config_available():
150
+ return None
151
+
152
+ raw_config = cookies.get(SAVED_CONFIG_COOKIE)
153
+ if not raw_config:
154
+ return None
155
+
156
+ try:
157
+ return json.loads(raw_config)
158
+ except (TypeError, json.JSONDecodeError):
159
+ del cookies[SAVED_CONFIG_COOKIE]
160
+ cookies.save()
161
+ return None
162
+
163
+
164
+ def _save_config(config):
165
+ """Persist assistant settings in the browser cookie."""
166
+ if not _saved_config_available():
167
+ return
168
+
169
+ cookies[SAVED_CONFIG_COOKIE] = json.dumps(config)
170
+ cookies.save()
171
+
172
+
173
+ def _clear_saved_config():
174
+ """Remove the saved browser-side assistant settings."""
175
+ if not _saved_config_available():
176
+ return
177
+
178
+ if SAVED_CONFIG_COOKIE in cookies:
179
+ del cookies[SAVED_CONFIG_COOKIE]
180
+ cookies.save()
181
+
182
+
183
+ def _current_config(selected_semester=None, selected_subject=None, api_key=None):
184
+ """Build the current assistant configuration payload."""
185
+ return {
186
+ "cfg_provider": st.session_state.get("cfg_provider"),
187
+ "cfg_base_url": st.session_state.get("cfg_base_url"),
188
+ "cfg_model": st.session_state.get("cfg_model"),
189
+ "api_key": api_key if api_key is not None else st.session_state.get("api_key"),
190
+ "asst_semester": selected_semester
191
+ if selected_semester is not None
192
+ else st.session_state.get("asst_semester"),
193
+ "asst_subject": selected_subject
194
+ if selected_subject is not None
195
+ else st.session_state.get("asst_subject"),
196
+ }
197
+
198
+
199
+ def _hydrate_saved_config():
200
+ """Hydrate session state from a remembered browser config once per load."""
201
+ if st.session_state.get("_saved_config_hydrated"):
202
+ return
203
+
204
+ saved_config = _load_saved_config()
205
+ if saved_config:
206
+ for key, value in saved_config.items():
207
+ if value and key not in st.session_state:
208
+ st.session_state[key] = value
209
+ st.session_state["remember_device"] = True
210
+
211
+ st.session_state["_saved_config_hydrated"] = True
212
+
213
+
214
  def render_external_access():
215
  """Render low-emphasis outbound access actions."""
216
  st.markdown(
 
356
  api_key = st.text_input(
357
  "API Key",
358
  type="password",
359
+ value=st.session_state.get("api_key", ""),
360
  placeholder="Paste your API key here",
361
  )
362
 
363
+ remember_default = bool(
364
+ st.session_state.get("remember_device") or _load_saved_config()
365
+ )
366
+ remember_device = st.checkbox(
367
+ "Remember these settings on this device",
368
+ value=remember_default,
369
+ disabled=not _saved_config_available(),
370
+ help=(
371
+ "Saves your provider settings and API key in this browser only."
372
+ if _saved_config_available()
373
+ else (
374
+ "Install the optional cookie dependency and set "
375
+ "PLEXI_COOKIE_PASSWORD to enable saved browser settings."
376
+ )
377
+ ),
378
+ )
379
+ if not _saved_config_available():
380
+ st.caption(
381
+ "Saved browser settings are disabled until "
382
+ "`streamlit-cookies-manager-ext` is installed and "
383
+ "`PLEXI_COOKIE_PASSWORD` is set."
384
+ )
385
+
386
  semester_names = sorted(manifest.keys())
387
  default_semester = st.session_state.get("asst_semester")
388
  semester_index = (
 
428
  st.session_state.cfg_model = model_name
429
  st.session_state.asst_semester = selected_semester
430
  st.session_state.asst_subject = selected_subject
431
+ st.session_state.remember_device = remember_device
432
  if api_key:
433
  st.session_state.api_key = api_key
434
+ elif "api_key" in st.session_state:
435
+ del st.session_state.api_key
436
+ if remember_device:
437
+ _save_config(
438
+ {
439
+ "cfg_provider": provider_name,
440
+ "cfg_base_url": base_url,
441
+ "cfg_model": model_name,
442
+ "api_key": api_key,
443
+ "asst_semester": selected_semester,
444
+ "asst_subject": selected_subject,
445
+ }
446
+ )
447
+ else:
448
+ _clear_saved_config()
449
  st.session_state.pop("messages", None)
450
  st.rerun()
451
 
 
479
  )
480
 
481
 
482
+ _hydrate_saved_config()
483
  render_sidebar_intro()
484
 
485
  try:
 
601
  key="sb_api_key",
602
  )
603
 
604
+ remember_sidebar = st.checkbox(
605
+ "Remember these settings on this device",
606
+ value=bool(st.session_state.get("remember_device")),
607
+ disabled=not _saved_config_available(),
608
+ key="sb_remember_device",
609
+ help=(
610
+ "Keeps the selected provider, key, and scope in this browser."
611
+ if _saved_config_available()
612
+ else (
613
+ "Install the optional cookie dependency and set "
614
+ "PLEXI_COOKIE_PASSWORD to enable saved browser settings."
615
+ )
616
+ ),
617
+ )
618
+
619
  changed = (
620
  new_provider != provider_name
621
  or new_base_url != base_url
622
  or new_model != model_name
623
  or new_key != api_key
624
+ or remember_sidebar != bool(st.session_state.get("remember_device"))
625
  )
626
  if changed and new_model:
627
  if st.button("Apply Changes", use_container_width=True, type="primary"):
628
  st.session_state.cfg_provider = new_provider
629
  st.session_state.cfg_base_url = new_base_url
630
  st.session_state.cfg_model = new_model
631
+ st.session_state.remember_device = remember_sidebar
632
  if new_key:
633
  st.session_state.api_key = new_key
634
  elif "api_key" in st.session_state:
635
  del st.session_state.api_key
636
+ if remember_sidebar:
637
+ _save_config(
638
+ {
639
+ "cfg_provider": new_provider,
640
+ "cfg_base_url": new_base_url,
641
+ "cfg_model": new_model,
642
+ "api_key": new_key,
643
+ "asst_semester": selected_semester,
644
+ "asst_subject": selected_subject,
645
+ }
646
+ )
647
+ else:
648
+ _clear_saved_config()
649
  st.session_state.pop("messages", None)
650
  st.rerun()
651
 
652
+ if _load_saved_config():
653
+ if st.button("Forget Saved Settings", use_container_width=True):
654
+ _clear_saved_config()
655
+ st.session_state.remember_device = False
656
+ for key in (
657
+ "cfg_provider",
658
+ "cfg_base_url",
659
+ "cfg_model",
660
+ "api_key",
661
+ "asst_semester",
662
+ "asst_subject",
663
+ ):
664
+ st.session_state.pop(key, None)
665
+ st.session_state.pop("messages", None)
666
+ st.rerun()
667
+
668
  if st.button("New Chat", use_container_width=True):
669
  st.session_state.pop("messages", None)
670
  st.rerun()
 
683
 
684
  render_external_access()
685
 
686
+ if st.session_state.get("remember_device") and _saved_config_available():
687
+ desired_config = _current_config(
688
+ selected_semester=selected_semester,
689
+ selected_subject=selected_subject,
690
+ api_key=api_key,
691
+ )
692
+ if desired_config != _load_saved_config():
693
+ _save_config(desired_config)
694
+
695
  render_stat_cards(
696
  [
697
  {
 
825
  st.stop()
826
  if "AUTH_ERROR" in err_text:
827
  st.error(err_text.split(": ", 1)[1])
828
+ _clear_saved_config()
829
+ st.session_state.remember_device = False
830
  if "api_key" in st.session_state:
831
  del st.session_state.api_key
832
  st.session_state.messages.pop()
requirements.txt CHANGED
@@ -5,4 +5,5 @@ PyPDF2
5
  streamlit-pdf-viewer
6
  llama-index-core
7
  llama-index-embeddings-huggingface
8
- sentence-transformers
 
 
5
  streamlit-pdf-viewer
6
  llama-index-core
7
  llama-index-embeddings-huggingface
8
+ sentence-transformers
9
+ streamlit-cookies-manager-ext