LazyHuman10 commited on
Commit ·
2c111dc
1
Parent(s): defc54d
Add Plexi assistant onboarding and sidebar updates
Browse files- README.md +8 -0
- pages/Plexi-Assistant.py +77 -15
- utils.py +17 -6
README.md
CHANGED
|
@@ -46,6 +46,14 @@ Chat with an AI that **only answers using actual study materials from Database**
|
|
| 46 |
- Every answer includes **source citations** so you know exactly where the information came from
|
| 47 |
- Bring your own API key (free tiers available from most providers)
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
### Contribute Materials
|
| 50 |
|
| 51 |
Have notes that could help others? Submit them through a simple form — they'll be reviewed and added for everyone automatically.
|
|
|
|
| 46 |
- Every answer includes **source citations** so you know exactly where the information came from
|
| 47 |
- Bring your own API key (free tiers available from most providers)
|
| 48 |
|
| 49 |
+
### Use Plexi Anywhere
|
| 50 |
+
|
| 51 |
+
Plexi is also available outside this app:
|
| 52 |
+
|
| 53 |
+
- ChatGPT GPT: [Plexi on ChatGPT](https://chatgpt.com/g/g-69caa671910481919ce71d19952e34e5-plexi)
|
| 54 |
+
- MCP Server: [https://plexi-mcp.vercel.app/](https://plexi-mcp.vercel.app/)
|
| 55 |
+
- MCP Endpoint: `https://plexi-mcp.vercel.app/api/mcp`
|
| 56 |
+
|
| 57 |
### Contribute Materials
|
| 58 |
|
| 59 |
Have notes that could help others? Submit them through a simple form — they'll be reviewed and added for everyone automatically.
|
pages/Plexi-Assistant.py
CHANGED
|
@@ -19,7 +19,8 @@ from utils import (
|
|
| 19 |
load_subject_context,
|
| 20 |
render_page_header,
|
| 21 |
render_panel,
|
| 22 |
-
|
|
|
|
| 23 |
render_stat_cards,
|
| 24 |
summarize_subject_catalog,
|
| 25 |
)
|
|
@@ -100,6 +101,9 @@ PROVIDERS = {
|
|
| 100 |
},
|
| 101 |
}
|
| 102 |
PROVIDER_NAMES = list(PROVIDERS.keys())
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
|
| 105 |
def _matches_scope(node, semester: str, subject: str) -> bool:
|
|
@@ -114,6 +118,24 @@ def queue_prompt(prompt: str):
|
|
| 114 |
st.rerun()
|
| 115 |
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
def local_retrieve(index, query: str, semester: str, subject: str, top_k: int = TOP_K):
|
| 118 |
"""Retrieve top-k relevant chunks scoped to the active semester + subject."""
|
| 119 |
if index is None:
|
|
@@ -189,7 +211,7 @@ def _is_configured():
|
|
| 189 |
)
|
| 190 |
|
| 191 |
|
| 192 |
-
def render_onboarding():
|
| 193 |
"""Render the setup flow before chat becomes available."""
|
| 194 |
render_page_header(
|
| 195 |
"Plexi assistant",
|
|
@@ -244,7 +266,40 @@ def render_onboarding():
|
|
| 244 |
placeholder="Paste your API key here",
|
| 245 |
)
|
| 246 |
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
if st.button(
|
| 249 |
"Start Chatting",
|
| 250 |
type="primary",
|
|
@@ -254,12 +309,15 @@ def render_onboarding():
|
|
| 254 |
st.session_state.cfg_provider = provider_name
|
| 255 |
st.session_state.cfg_base_url = base_url
|
| 256 |
st.session_state.cfg_model = model_name
|
|
|
|
|
|
|
| 257 |
if api_key:
|
| 258 |
st.session_state.api_key = api_key
|
| 259 |
st.session_state.pop("messages", None)
|
| 260 |
st.rerun()
|
| 261 |
|
| 262 |
with right_col:
|
|
|
|
| 263 |
render_panel(
|
| 264 |
"What Plexi does",
|
| 265 |
"Keeps answers grounded in the currently loaded course materials instead of drifting into generic knowledge.",
|
|
@@ -288,10 +346,20 @@ def render_onboarding():
|
|
| 288 |
)
|
| 289 |
|
| 290 |
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
|
| 293 |
if not _is_configured():
|
| 294 |
-
render_onboarding()
|
| 295 |
st.stop()
|
| 296 |
|
| 297 |
provider_name = st.session_state.cfg_provider
|
|
@@ -303,16 +371,6 @@ rag_index, rag_error = fetch_rag_index()
|
|
| 303 |
rag_active = rag_index is not None
|
| 304 |
mode_label = "RAG retrieval" if rag_active else "Full-context fallback"
|
| 305 |
|
| 306 |
-
try:
|
| 307 |
-
manifest = get_manifest()
|
| 308 |
-
except Exception as err:
|
| 309 |
-
st.error(f"Failed to load materials catalog: {err}")
|
| 310 |
-
st.stop()
|
| 311 |
-
|
| 312 |
-
if not manifest:
|
| 313 |
-
st.info("No study materials are available yet.")
|
| 314 |
-
st.stop()
|
| 315 |
-
|
| 316 |
with st.sidebar:
|
| 317 |
st.markdown(
|
| 318 |
'<div class="plexi-section-label">Study Scope</div>',
|
|
@@ -431,6 +489,8 @@ with st.sidebar:
|
|
| 431 |
st.session_state.pop("messages", None)
|
| 432 |
st.rerun()
|
| 433 |
|
|
|
|
|
|
|
| 434 |
render_page_header(
|
| 435 |
"Plexi assistant",
|
| 436 |
f"Ask anything from {selected_subject}",
|
|
@@ -441,6 +501,8 @@ render_page_header(
|
|
| 441 |
badges=[selected_semester, selected_subject, provider_name, mode_label],
|
| 442 |
)
|
| 443 |
|
|
|
|
|
|
|
| 444 |
render_stat_cards(
|
| 445 |
[
|
| 446 |
{
|
|
|
|
| 19 |
load_subject_context,
|
| 20 |
render_page_header,
|
| 21 |
render_panel,
|
| 22 |
+
render_sidebar_footer,
|
| 23 |
+
render_sidebar_intro,
|
| 24 |
render_stat_cards,
|
| 25 |
summarize_subject_catalog,
|
| 26 |
)
|
|
|
|
| 101 |
},
|
| 102 |
}
|
| 103 |
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 |
st.rerun()
|
| 119 |
|
| 120 |
|
| 121 |
+
def render_external_access():
|
| 122 |
+
"""Render low-emphasis outbound access actions."""
|
| 123 |
+
st.markdown(
|
| 124 |
+
'<div class="plexi-section-label">Use Plexi Elsewhere</div>',
|
| 125 |
+
unsafe_allow_html=True,
|
| 126 |
+
)
|
| 127 |
+
st.caption(
|
| 128 |
+
"Open the GPT directly or use the MCP endpoint in any compatible client."
|
| 129 |
+
)
|
| 130 |
+
button_cols = st.columns([1, 1], gap="small")
|
| 131 |
+
with button_cols[0]:
|
| 132 |
+
st.link_button("Open Plexi GPT", PLEXI_GPT_URL, use_container_width=True)
|
| 133 |
+
with button_cols[1]:
|
| 134 |
+
st.link_button("Open MCP Guide", PLEXI_MCP_GUIDE_URL, use_container_width=True)
|
| 135 |
+
with st.expander("MCP endpoint", expanded=False):
|
| 136 |
+
st.code(PLEXI_MCP_ENDPOINT, language=None)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
def local_retrieve(index, query: str, semester: str, subject: str, top_k: int = TOP_K):
|
| 140 |
"""Retrieve top-k relevant chunks scoped to the active semester + subject."""
|
| 141 |
if index is None:
|
|
|
|
| 211 |
)
|
| 212 |
|
| 213 |
|
| 214 |
+
def render_onboarding(manifest):
|
| 215 |
"""Render the setup flow before chat becomes available."""
|
| 216 |
render_page_header(
|
| 217 |
"Plexi assistant",
|
|
|
|
| 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 = (
|
| 272 |
+
semester_names.index(default_semester)
|
| 273 |
+
if default_semester in semester_names
|
| 274 |
+
else 0
|
| 275 |
+
)
|
| 276 |
+
selected_semester = st.selectbox(
|
| 277 |
+
"Semester",
|
| 278 |
+
semester_names,
|
| 279 |
+
index=semester_index,
|
| 280 |
+
key="ob_semester",
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
subject_names = sorted(manifest[selected_semester].keys())
|
| 284 |
+
default_subject = st.session_state.get("asst_subject")
|
| 285 |
+
subject_index = (
|
| 286 |
+
subject_names.index(default_subject)
|
| 287 |
+
if default_subject in subject_names
|
| 288 |
+
else 0
|
| 289 |
+
)
|
| 290 |
+
selected_subject = st.selectbox(
|
| 291 |
+
"Subject",
|
| 292 |
+
subject_names,
|
| 293 |
+
index=subject_index,
|
| 294 |
+
key="ob_subject",
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
can_start = bool(
|
| 298 |
+
model_name
|
| 299 |
+
and selected_semester
|
| 300 |
+
and selected_subject
|
| 301 |
+
and (not needs_key or api_key)
|
| 302 |
+
)
|
| 303 |
if st.button(
|
| 304 |
"Start Chatting",
|
| 305 |
type="primary",
|
|
|
|
| 309 |
st.session_state.cfg_provider = provider_name
|
| 310 |
st.session_state.cfg_base_url = base_url
|
| 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 |
|
| 319 |
with right_col:
|
| 320 |
+
render_external_access()
|
| 321 |
render_panel(
|
| 322 |
"What Plexi does",
|
| 323 |
"Keeps answers grounded in the currently loaded course materials instead of drifting into generic knowledge.",
|
|
|
|
| 346 |
)
|
| 347 |
|
| 348 |
|
| 349 |
+
render_sidebar_intro()
|
| 350 |
+
|
| 351 |
+
try:
|
| 352 |
+
manifest = get_manifest()
|
| 353 |
+
except Exception as err:
|
| 354 |
+
st.error(f"Failed to load materials catalog: {err}")
|
| 355 |
+
st.stop()
|
| 356 |
+
|
| 357 |
+
if not manifest:
|
| 358 |
+
st.info("No study materials are available yet.")
|
| 359 |
+
st.stop()
|
| 360 |
|
| 361 |
if not _is_configured():
|
| 362 |
+
render_onboarding(manifest)
|
| 363 |
st.stop()
|
| 364 |
|
| 365 |
provider_name = st.session_state.cfg_provider
|
|
|
|
| 371 |
rag_active = rag_index is not None
|
| 372 |
mode_label = "RAG retrieval" if rag_active else "Full-context fallback"
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
with st.sidebar:
|
| 375 |
st.markdown(
|
| 376 |
'<div class="plexi-section-label">Study Scope</div>',
|
|
|
|
| 489 |
st.session_state.pop("messages", None)
|
| 490 |
st.rerun()
|
| 491 |
|
| 492 |
+
render_sidebar_footer()
|
| 493 |
+
|
| 494 |
render_page_header(
|
| 495 |
"Plexi assistant",
|
| 496 |
f"Ask anything from {selected_subject}",
|
|
|
|
| 501 |
badges=[selected_semester, selected_subject, provider_name, mode_label],
|
| 502 |
)
|
| 503 |
|
| 504 |
+
render_external_access()
|
| 505 |
+
|
| 506 |
render_stat_cards(
|
| 507 |
[
|
| 508 |
{
|
utils.py
CHANGED
|
@@ -787,13 +787,9 @@ def get_mime_type(filename):
|
|
| 787 |
return mime or "application/octet-stream"
|
| 788 |
|
| 789 |
|
| 790 |
-
def
|
| 791 |
-
"""Render the shared sidebar
|
| 792 |
with st.sidebar:
|
| 793 |
-
current_mode = get_theme_mode()
|
| 794 |
-
widget_value = current_mode.capitalize()
|
| 795 |
-
if st.session_state.get(THEME_MODE_WIDGET_KEY) != widget_value:
|
| 796 |
-
st.session_state[THEME_MODE_WIDGET_KEY] = widget_value
|
| 797 |
st.markdown(
|
| 798 |
"""
|
| 799 |
<section class="plexi-sidecard">
|
|
@@ -807,6 +803,15 @@ def render_sidebar():
|
|
| 807 |
""",
|
| 808 |
unsafe_allow_html=True,
|
| 809 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 810 |
st.markdown(
|
| 811 |
'<div class="plexi-section-label">Appearance</div>',
|
| 812 |
unsafe_allow_html=True,
|
|
@@ -833,6 +838,12 @@ def render_sidebar():
|
|
| 833 |
st.markdown('<div class="plexi-divider"></div>', unsafe_allow_html=True)
|
| 834 |
|
| 835 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 836 |
def read_pdf_text(pdf_bytes):
|
| 837 |
"""Extract text from PDF bytes with error handling."""
|
| 838 |
text = []
|
|
|
|
| 787 |
return mime or "application/octet-stream"
|
| 788 |
|
| 789 |
|
| 790 |
+
def render_sidebar_intro():
|
| 791 |
+
"""Render the shared sidebar intro card."""
|
| 792 |
with st.sidebar:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 793 |
st.markdown(
|
| 794 |
"""
|
| 795 |
<section class="plexi-sidecard">
|
|
|
|
| 803 |
""",
|
| 804 |
unsafe_allow_html=True,
|
| 805 |
)
|
| 806 |
+
|
| 807 |
+
|
| 808 |
+
def render_sidebar_footer():
|
| 809 |
+
"""Render shared appearance controls and outbound links at the end of the sidebar."""
|
| 810 |
+
with st.sidebar:
|
| 811 |
+
current_mode = get_theme_mode()
|
| 812 |
+
widget_value = current_mode.capitalize()
|
| 813 |
+
if st.session_state.get(THEME_MODE_WIDGET_KEY) != widget_value:
|
| 814 |
+
st.session_state[THEME_MODE_WIDGET_KEY] = widget_value
|
| 815 |
st.markdown(
|
| 816 |
'<div class="plexi-section-label">Appearance</div>',
|
| 817 |
unsafe_allow_html=True,
|
|
|
|
| 838 |
st.markdown('<div class="plexi-divider"></div>', unsafe_allow_html=True)
|
| 839 |
|
| 840 |
|
| 841 |
+
def render_sidebar():
|
| 842 |
+
"""Render the shared sidebar for pages without extra sidebar sections."""
|
| 843 |
+
render_sidebar_intro()
|
| 844 |
+
render_sidebar_footer()
|
| 845 |
+
|
| 846 |
+
|
| 847 |
def read_pdf_text(pdf_bytes):
|
| 848 |
"""Extract text from PDF bytes with error handling."""
|
| 849 |
text = []
|