Merge branch 'main' of https://github.com/Alalalallalalalalalalalal/Frontend
Browse files- app.py +0 -12
- phase/Student_view/chatbot.py +39 -9
- phase/Student_view/quiz.py +19 -5
- phase/welcome.py +3 -3
- utils/api.py +18 -12
app.py
CHANGED
|
@@ -16,21 +16,9 @@ from phase import welcome
|
|
| 16 |
|
| 17 |
import os, requests
|
| 18 |
|
| 19 |
-
from utils.api import BACKEND
|
| 20 |
-
st.sidebar.caption(f"Backend URL: {BACKEND}")
|
| 21 |
-
|
| 22 |
|
| 23 |
DISABLE_DB = os.getenv("DISABLE_DB", "1") == "1"
|
| 24 |
|
| 25 |
-
try:
|
| 26 |
-
ok = api.health().get("ok")
|
| 27 |
-
if ok:
|
| 28 |
-
st.sidebar.success("Backend: UP")
|
| 29 |
-
else:
|
| 30 |
-
st.sidebar.warning("Backend: ?")
|
| 31 |
-
except Exception as e:
|
| 32 |
-
st.sidebar.error(f"Backend DOWN: {e}")
|
| 33 |
-
|
| 34 |
|
| 35 |
# --- SESSION STATE INITIALIZATION ---
|
| 36 |
for key, default in [("user", None), ("current_page", "Welcome"),
|
|
|
|
| 16 |
|
| 17 |
import os, requests
|
| 18 |
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
DISABLE_DB = os.getenv("DISABLE_DB", "1") == "1"
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
# --- SESSION STATE INITIALIZATION ---
|
| 24 |
for key, default in [("user", None), ("current_page", "Welcome"),
|
phase/Student_view/chatbot.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
# phase/Student_view/chatbot.py
|
| 2 |
import os
|
|
|
|
| 3 |
import datetime
|
| 4 |
import traceback
|
| 5 |
import streamlit as st
|
|
@@ -17,6 +18,15 @@ except ModuleNotFoundError:
|
|
| 17 |
|
| 18 |
TUTOR_WELCOME = "Hi! I'm your AI Financial Tutor. What would you like to learn today?"
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
# -------------------------------
|
| 21 |
# History helpers
|
| 22 |
# -------------------------------
|
|
@@ -60,32 +70,36 @@ def _normalize_messages():
|
|
| 60 |
st.session_state.messages = normed
|
| 61 |
|
| 62 |
def _history_for_backend():
|
| 63 |
-
"""Convert our local history into [{role, content}] for the backend."""
|
| 64 |
hist = []
|
| 65 |
for m in st.session_state.get("messages", []):
|
| 66 |
text = (m.get("text") or "").strip()
|
| 67 |
-
if not text:
|
| 68 |
continue
|
| 69 |
role = "assistant" if (m.get("sender") == "assistant") else "user"
|
| 70 |
hist.append({"role": role, "content": text})
|
| 71 |
-
return hist
|
| 72 |
|
| 73 |
# -------------------------------
|
| 74 |
# Reply via backend (/chat)
|
| 75 |
# -------------------------------
|
| 76 |
def _reply_via_backend(user_text: str) -> str:
|
| 77 |
-
# Defaults: use selected lesson/level if present
|
| 78 |
lesson_id = st.session_state.get("current_lesson_id") or 0
|
| 79 |
level_slug = (st.session_state.get("user", {}).get("level") or "beginner").strip().lower()
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
try:
|
| 82 |
answer = backend.chat_ai(
|
| 83 |
query=user_text,
|
| 84 |
lesson_id=lesson_id,
|
| 85 |
level_slug=level_slug,
|
| 86 |
-
history=
|
| 87 |
)
|
| 88 |
-
return (answer or "").strip()
|
| 89 |
except Exception as e:
|
| 90 |
err_text = "".join(traceback.format_exception_only(type(e), e)).strip()
|
| 91 |
return f"โ ๏ธ Chat failed: {err_text}"
|
|
@@ -97,6 +111,7 @@ def show_page():
|
|
| 97 |
st.title("๐ค AI Financial Tutor")
|
| 98 |
st.caption("Get personalized help with your financial questions")
|
| 99 |
|
|
|
|
| 100 |
if "messages" not in st.session_state:
|
| 101 |
st.session_state.messages = [{
|
| 102 |
"id": "1",
|
|
@@ -106,9 +121,12 @@ def show_page():
|
|
| 106 |
}]
|
| 107 |
if "is_typing" not in st.session_state:
|
| 108 |
st.session_state.is_typing = False
|
|
|
|
|
|
|
| 109 |
|
| 110 |
_normalize_messages()
|
| 111 |
|
|
|
|
| 112 |
chat_container = st.container()
|
| 113 |
with chat_container:
|
| 114 |
for msg in st.session_state.messages:
|
|
@@ -130,7 +148,16 @@ def show_page():
|
|
| 130 |
if st.session_state.is_typing:
|
| 131 |
st.markdown("๐ค _FinanceBot is typing..._")
|
| 132 |
|
| 133 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
if len(st.session_state.messages) == 1:
|
| 135 |
st.markdown("Try asking about:")
|
| 136 |
cols = st.columns(2)
|
|
@@ -146,19 +173,22 @@ def show_page():
|
|
| 146 |
st.session_state.is_typing = True
|
| 147 |
st.rerun()
|
| 148 |
|
|
|
|
| 149 |
user_input = st.chat_input("Ask me anything about personal finance...")
|
| 150 |
if user_input:
|
| 151 |
add_message(user_input, "user")
|
| 152 |
st.session_state.is_typing = True
|
| 153 |
st.rerun()
|
| 154 |
|
|
|
|
| 155 |
if st.session_state.is_typing:
|
| 156 |
with st.spinner("FinanceBot is thinking..."):
|
| 157 |
-
|
|
|
|
| 158 |
add_message(bot_reply, "assistant")
|
| 159 |
st.session_state.is_typing = False
|
| 160 |
st.rerun()
|
| 161 |
|
| 162 |
if st.button("Back to Dashboard", key="ai_tutor_back_btn"):
|
| 163 |
st.session_state.current_page = "Student Dashboard"
|
| 164 |
-
st.rerun()
|
|
|
|
| 1 |
# phase/Student_view/chatbot.py
|
| 2 |
import os
|
| 3 |
+
import re
|
| 4 |
import datetime
|
| 5 |
import traceback
|
| 6 |
import streamlit as st
|
|
|
|
| 18 |
|
| 19 |
TUTOR_WELCOME = "Hi! I'm your AI Financial Tutor. What would you like to learn today?"
|
| 20 |
|
| 21 |
+
def _clean_bot_text(t: str) -> str:
|
| 22 |
+
# strip xml-ish tags like <user>...</user>, <assistant>...</assistant>
|
| 23 |
+
t = re.sub(r"</?(user|assistant|system)\b[^>]*>", "", t, flags=re.I)
|
| 24 |
+
# strip leading speaker labels (User:, Assistant:, System:)
|
| 25 |
+
t = re.sub(r"(?im)^(user|assistant|system)\s*:\s*", "", t)
|
| 26 |
+
# collapse extra newlines
|
| 27 |
+
t = re.sub(r"\n{3,}", "\n\n", t)
|
| 28 |
+
return t.strip()
|
| 29 |
+
|
| 30 |
# -------------------------------
|
| 31 |
# History helpers
|
| 32 |
# -------------------------------
|
|
|
|
| 70 |
st.session_state.messages = normed
|
| 71 |
|
| 72 |
def _history_for_backend():
|
|
|
|
| 73 |
hist = []
|
| 74 |
for m in st.session_state.get("messages", []):
|
| 75 |
text = (m.get("text") or "").strip()
|
| 76 |
+
if not text or text == TUTOR_WELCOME:
|
| 77 |
continue
|
| 78 |
role = "assistant" if (m.get("sender") == "assistant") else "user"
|
| 79 |
hist.append({"role": role, "content": text})
|
| 80 |
+
return hist[-4:] # <= keep it tiny
|
| 81 |
|
| 82 |
# -------------------------------
|
| 83 |
# Reply via backend (/chat)
|
| 84 |
# -------------------------------
|
| 85 |
def _reply_via_backend(user_text: str) -> str:
|
|
|
|
| 86 |
lesson_id = st.session_state.get("current_lesson_id") or 0
|
| 87 |
level_slug = (st.session_state.get("user", {}).get("level") or "beginner").strip().lower()
|
| 88 |
|
| 89 |
+
# Build history and remove duplicate of the message we are sending as `query`
|
| 90 |
+
hist = _history_for_backend()
|
| 91 |
+
if hist and hist[-1].get("role") == "user" and hist[-1].get("content", "").strip() == (user_text or "").strip():
|
| 92 |
+
hist = hist[:-1]
|
| 93 |
+
hist = hist[-4:]
|
| 94 |
+
|
| 95 |
try:
|
| 96 |
answer = backend.chat_ai(
|
| 97 |
query=user_text,
|
| 98 |
lesson_id=lesson_id,
|
| 99 |
level_slug=level_slug,
|
| 100 |
+
history=hist,
|
| 101 |
)
|
| 102 |
+
return _clean_bot_text((answer or "").strip())
|
| 103 |
except Exception as e:
|
| 104 |
err_text = "".join(traceback.format_exception_only(type(e), e)).strip()
|
| 105 |
return f"โ ๏ธ Chat failed: {err_text}"
|
|
|
|
| 111 |
st.title("๐ค AI Financial Tutor")
|
| 112 |
st.caption("Get personalized help with your financial questions")
|
| 113 |
|
| 114 |
+
# --- session state init ---
|
| 115 |
if "messages" not in st.session_state:
|
| 116 |
st.session_state.messages = [{
|
| 117 |
"id": "1",
|
|
|
|
| 121 |
}]
|
| 122 |
if "is_typing" not in st.session_state:
|
| 123 |
st.session_state.is_typing = False
|
| 124 |
+
if "chatbot_prefill_sent" not in st.session_state:
|
| 125 |
+
st.session_state.chatbot_prefill_sent = False
|
| 126 |
|
| 127 |
_normalize_messages()
|
| 128 |
|
| 129 |
+
# --- render chat bubbles ---
|
| 130 |
chat_container = st.container()
|
| 131 |
with chat_container:
|
| 132 |
for msg in st.session_state.messages:
|
|
|
|
| 148 |
if st.session_state.is_typing:
|
| 149 |
st.markdown("๐ค _FinanceBot is typing..._")
|
| 150 |
|
| 151 |
+
# --- quiz handoff auto-prompt (only once) ---
|
| 152 |
+
prefill = st.session_state.get("chatbot_prefill")
|
| 153 |
+
if prefill and not st.session_state.chatbot_prefill_sent:
|
| 154 |
+
add_message(prefill, "user")
|
| 155 |
+
st.session_state.is_typing = True
|
| 156 |
+
st.session_state.chatbot_prefill_sent = True
|
| 157 |
+
st.session_state.chatbot_prefill = None
|
| 158 |
+
st.rerun()
|
| 159 |
+
|
| 160 |
+
# --- quick suggestions when fresh ---
|
| 161 |
if len(st.session_state.messages) == 1:
|
| 162 |
st.markdown("Try asking about:")
|
| 163 |
cols = st.columns(2)
|
|
|
|
| 173 |
st.session_state.is_typing = True
|
| 174 |
st.rerun()
|
| 175 |
|
| 176 |
+
# --- user input ---
|
| 177 |
user_input = st.chat_input("Ask me anything about personal finance...")
|
| 178 |
if user_input:
|
| 179 |
add_message(user_input, "user")
|
| 180 |
st.session_state.is_typing = True
|
| 181 |
st.rerun()
|
| 182 |
|
| 183 |
+
# --- handle pending bot reply ---
|
| 184 |
if st.session_state.is_typing:
|
| 185 |
with st.spinner("FinanceBot is thinking..."):
|
| 186 |
+
last_user_msg = next((m["text"] for m in reversed(st.session_state.messages) if m["sender"] == "user"), "")
|
| 187 |
+
bot_reply = _reply_via_backend(last_user_msg)
|
| 188 |
add_message(bot_reply, "assistant")
|
| 189 |
st.session_state.is_typing = False
|
| 190 |
st.rerun()
|
| 191 |
|
| 192 |
if st.button("Back to Dashboard", key="ai_tutor_back_btn"):
|
| 193 |
st.session_state.current_page = "Student Dashboard"
|
| 194 |
+
st.rerun()
|
phase/Student_view/quiz.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import os
|
| 2 |
import streamlit as st
|
| 3 |
from utils.quizdata import quizzes_data
|
|
@@ -350,13 +351,26 @@ def show_results(quiz_id):
|
|
| 350 |
st.session_state.answers = {}
|
| 351 |
if "messages" not in st.session_state:
|
| 352 |
st.session_state.messages = []
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
st.session_state.messages.append({
|
| 358 |
"id": str(datetime.datetime.now().timestamp()),
|
| 359 |
-
"text":
|
| 360 |
"sender": "user",
|
| 361 |
"timestamp": datetime.datetime.now()
|
| 362 |
})
|
|
|
|
| 1 |
+
#quiz.py
|
| 2 |
import os
|
| 3 |
import streamlit as st
|
| 4 |
from utils.quizdata import quizzes_data
|
|
|
|
| 351 |
st.session_state.answers = {}
|
| 352 |
if "messages" not in st.session_state:
|
| 353 |
st.session_state.messages = []
|
| 354 |
+
|
| 355 |
+
# keep only first 3 wrong items, and cap text length
|
| 356 |
+
short = wrong_answers[:3]
|
| 357 |
+
rows = []
|
| 358 |
+
for q, ua, ca, ex in short:
|
| 359 |
+
q = (q or "")[:160]
|
| 360 |
+
ua = (", ".join(ua) if isinstance(ua, list) else str(ua or ""))[:120]
|
| 361 |
+
ca = (", ".join(ca) if isinstance(ca, list) else str(ca or ""))[:120]
|
| 362 |
+
ex = (ex or "")[:200]
|
| 363 |
+
rows.append(f"Q: {q}\nYour answer: {ua}\nCorrect answer: {ca}\nExplanation: {ex}")
|
| 364 |
+
|
| 365 |
+
handoff = (
|
| 366 |
+
"I just completed a financial quiz and missed a few. "
|
| 367 |
+
"Explain each briefly and give one easy tip to remember:\n\n" +
|
| 368 |
+
"\n\n".join(rows)
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
st.session_state.messages.append({
|
| 372 |
"id": str(datetime.datetime.now().timestamp()),
|
| 373 |
+
"text": handoff,
|
| 374 |
"sender": "user",
|
| 375 |
"timestamp": datetime.datetime.now()
|
| 376 |
})
|
phase/welcome.py
CHANGED
|
@@ -128,8 +128,8 @@ def welcomeui():
|
|
| 128 |
col1, col2, col3, col4 = st.columns(4)
|
| 129 |
|
| 130 |
features = [
|
| 131 |
-
("๐", "
|
| 132 |
-
("๐ฎ", "
|
| 133 |
("๐", "Progress Tracking", "Monitor learning progress with quizzes, achievements, and detailed analytics!"),
|
| 134 |
("๐ค", "AI Assistant", "Get instant help and personalized guidance from our friendly chatbot!")
|
| 135 |
]
|
|
@@ -165,7 +165,7 @@ def welcomeui():
|
|
| 165 |
<div class="card" style="background: linear-gradient(to right, #30E8BF, #FF8235); color: #fff;">
|
| 166 |
<div class="card-icon">๐ฉโ๐ซ</div>
|
| 167 |
<h3>For Teachers</h3>
|
| 168 |
-
<p>Manage your classroom, track progress, and engage your students
|
| 169 |
</div>
|
| 170 |
""", unsafe_allow_html=True)
|
| 171 |
if st.button("๐ Teacher Dashboard"):
|
|
|
|
| 128 |
col1, col2, col3, col4 = st.columns(4)
|
| 129 |
|
| 130 |
features = [
|
| 131 |
+
("๐", "Lessons", "Engaging content that makes financial literacy fun and accessible for all ages!"),
|
| 132 |
+
("๐ฎ", "Games", "Learn through play with our collection of money management games and challenges!"),
|
| 133 |
("๐", "Progress Tracking", "Monitor learning progress with quizzes, achievements, and detailed analytics!"),
|
| 134 |
("๐ค", "AI Assistant", "Get instant help and personalized guidance from our friendly chatbot!")
|
| 135 |
]
|
|
|
|
| 165 |
<div class="card" style="background: linear-gradient(to right, #30E8BF, #FF8235); color: #fff;">
|
| 166 |
<div class="card-icon">๐ฉโ๐ซ</div>
|
| 167 |
<h3>For Teachers</h3>
|
| 168 |
+
<p>Manage your classroom, track progress, and engage your students!</p>
|
| 169 |
</div>
|
| 170 |
""", unsafe_allow_html=True)
|
| 171 |
if st.button("๐ Teacher Dashboard"):
|
utils/api.py
CHANGED
|
@@ -12,7 +12,7 @@ if not BACKEND:
|
|
| 12 |
raise RuntimeError("BACKEND_URL is not set in Space secrets.")
|
| 13 |
|
| 14 |
TOKEN = (os.getenv("BACKEND_TOKEN") or os.getenv("HF_TOKEN") or "").strip()
|
| 15 |
-
DEFAULT_TIMEOUT = int(os.getenv("BACKEND_TIMEOUT", "
|
| 16 |
|
| 17 |
_session = requests.Session()
|
| 18 |
retry = Retry(
|
|
@@ -65,18 +65,24 @@ def _req(method: str, path: str, **kw):
|
|
| 65 |
# try next prefix on 404
|
| 66 |
if status == 404:
|
| 67 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
body = ""
|
| 69 |
try:
|
| 70 |
body = e.response.text[:500]
|
| 71 |
except Exception:
|
| 72 |
pass
|
| 73 |
-
if status in (401, 403):
|
| 74 |
-
raise RuntimeError(
|
| 75 |
-
f"{method} {url} failed [{status}] โ auth rejected. "
|
| 76 |
-
f"Check BACKEND_TOKEN/HF_TOKEN and backend visibility."
|
| 77 |
-
) from e
|
| 78 |
raise RuntimeError(f"{method} {url} failed [{status}]: {body}") from e
|
| 79 |
-
except requests.RequestException
|
| 80 |
# try next prefix
|
| 81 |
continue
|
| 82 |
raise RuntimeError(f"No matching endpoint for {method} {path} with prefixes {list(_prefixes())}")
|
|
@@ -337,12 +343,12 @@ def chat_ai(query: str, lesson_id: int, level_slug: str, history=None) -> str:
|
|
| 337 |
"history": history or [],
|
| 338 |
}
|
| 339 |
try:
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
|
|
|
| 343 |
r.raise_for_status()
|
| 344 |
-
|
| 345 |
-
return d.get("answer", "")
|
| 346 |
except Exception as e:
|
| 347 |
return f"(chat failed: {e})"
|
| 348 |
|
|
|
|
| 12 |
raise RuntimeError("BACKEND_URL is not set in Space secrets.")
|
| 13 |
|
| 14 |
TOKEN = (os.getenv("BACKEND_TOKEN") or os.getenv("HF_TOKEN") or "").strip()
|
| 15 |
+
DEFAULT_TIMEOUT = int(os.getenv("BACKEND_TIMEOUT", "120"))
|
| 16 |
|
| 17 |
_session = requests.Session()
|
| 18 |
retry = Retry(
|
|
|
|
| 65 |
# try next prefix on 404
|
| 66 |
if status == 404:
|
| 67 |
continue
|
| 68 |
+
|
| 69 |
+
# ---------- friendlier auth errors ----------
|
| 70 |
+
if status in (401, 403):
|
| 71 |
+
# Normal bad credentials on the login endpoint
|
| 72 |
+
if path.endswith("/auth/login"):
|
| 73 |
+
raise RuntimeError("Incorrect email or password.") from e
|
| 74 |
+
# Generic auth issue elsewhere (expired session, etc.)
|
| 75 |
+
raise RuntimeError("Authentication failed. Please sign in again.") from e
|
| 76 |
+
# -------------------------------------------
|
| 77 |
+
|
| 78 |
+
# keep a tiny body for other errors
|
| 79 |
body = ""
|
| 80 |
try:
|
| 81 |
body = e.response.text[:500]
|
| 82 |
except Exception:
|
| 83 |
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
raise RuntimeError(f"{method} {url} failed [{status}]: {body}") from e
|
| 85 |
+
except requests.RequestException:
|
| 86 |
# try next prefix
|
| 87 |
continue
|
| 88 |
raise RuntimeError(f"No matching endpoint for {method} {path} with prefixes {list(_prefixes())}")
|
|
|
|
| 343 |
"history": history or [],
|
| 344 |
}
|
| 345 |
try:
|
| 346 |
+
url = f"{BACKEND}/chat"
|
| 347 |
+
with requests.Session() as s:
|
| 348 |
+
s.headers.update(_session.headers)
|
| 349 |
+
r = s.post(url, json=payload, timeout=DEFAULT_TIMEOUT) # no retry adapter
|
| 350 |
r.raise_for_status()
|
| 351 |
+
return r.json().get("answer", "")
|
|
|
|
| 352 |
except Exception as e:
|
| 353 |
return f"(chat failed: {e})"
|
| 354 |
|