Merge branch 'main' of https://github.com/Alalalallalalalalalalalal/FinED-Front
Browse files- phase/Student_view/chatbot.py +31 -35
- phase/Student_view/games/budgetbuilder.py +69 -0
- phase/Student_view/games/debtdilemma.py +50 -14
- phase/Student_view/games/profitpuzzle.py +53 -12
- utils/api.py +38 -1
phase/Student_view/chatbot.py
CHANGED
|
@@ -17,7 +17,11 @@ TUTOR_PROMPT = (
|
|
| 17 |
"Teach step-by-step with tiny examples. Avoid giving personal financial advice."
|
| 18 |
)
|
| 19 |
|
|
|
|
|
|
|
|
|
|
| 20 |
def _format_history_for_flan(messages: list[dict]) -> str:
|
|
|
|
| 21 |
lines = []
|
| 22 |
for m in messages:
|
| 23 |
txt = (m.get("text") or "").strip()
|
|
@@ -26,14 +30,8 @@ def _format_history_for_flan(messages: list[dict]) -> str:
|
|
| 26 |
lines.append(("Tutor" if m.get("sender") == "assistant" else "User") + f": {txt}")
|
| 27 |
return "\n".join(lines)
|
| 28 |
|
| 29 |
-
def _trim_turn(text: str) -> str:
|
| 30 |
-
for cp in ["\nUser:", "\nTutor:", "\nAssistant:", "\n###"]:
|
| 31 |
-
if cp in text:
|
| 32 |
-
return text.split(cp, 1)[0].strip()
|
| 33 |
-
return text.strip()
|
| 34 |
-
|
| 35 |
def _history_as_chat_messages(messages: list[dict]) -> list[dict]:
|
| 36 |
-
|
| 37 |
msgs = [{"role": "system", "content": TUTOR_PROMPT}]
|
| 38 |
for m in messages:
|
| 39 |
txt = (m.get("text") or "").strip()
|
|
@@ -44,55 +42,53 @@ def _history_as_chat_messages(messages: list[dict]) -> list[dict]:
|
|
| 44 |
return msgs
|
| 45 |
|
| 46 |
def _extract_chat_text(chat_resp) -> str:
|
| 47 |
-
|
| 48 |
try:
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
except Exception:
|
| 53 |
-
# fallback for dict payloads
|
| 54 |
try:
|
| 55 |
return chat_resp["choices"][0]["message"]["content"]
|
| 56 |
except Exception:
|
| 57 |
return str(chat_resp)
|
| 58 |
|
|
|
|
|
|
|
|
|
|
| 59 |
def _reply_with_hf():
|
| 60 |
if "client" not in globals():
|
| 61 |
raise RuntimeError("HF client not initialized")
|
| 62 |
|
| 63 |
-
# Text-generation prompt (for providers that support it)
|
| 64 |
-
convo = _format_history_for_flan(st.session_state.get("messages", []))
|
| 65 |
-
tg_prompt = f"{TUTOR_PROMPT}\n\n{convo}\n\nTutor:"
|
| 66 |
-
|
| 67 |
try:
|
| 68 |
-
# 1)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
| 72 |
temperature=0.2,
|
| 73 |
top_p=0.9,
|
| 74 |
-
repetition_penalty=1.1,
|
| 75 |
-
return_full_text=True,
|
| 76 |
-
stream=False,
|
| 77 |
)
|
| 78 |
-
|
| 79 |
-
return _trim_turn(str(text or "").strip())
|
| 80 |
|
| 81 |
except ValueError as ve:
|
| 82 |
-
# 2)
|
| 83 |
-
if "Supported task:
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
temperature=0.2,
|
| 90 |
top_p=0.9,
|
|
|
|
|
|
|
|
|
|
| 91 |
)
|
| 92 |
-
return
|
| 93 |
|
| 94 |
-
#
|
| 95 |
-
raise
|
| 96 |
|
| 97 |
except Exception as e:
|
| 98 |
err_text = ''.join(traceback.format_exception_only(type(e), e)).strip()
|
|
|
|
| 17 |
"Teach step-by-step with tiny examples. Avoid giving personal financial advice."
|
| 18 |
)
|
| 19 |
|
| 20 |
+
# -------------------------------
|
| 21 |
+
# History helpers
|
| 22 |
+
# -------------------------------
|
| 23 |
def _format_history_for_flan(messages: list[dict]) -> str:
|
| 24 |
+
"""Format history for text-generation style models."""
|
| 25 |
lines = []
|
| 26 |
for m in messages:
|
| 27 |
txt = (m.get("text") or "").strip()
|
|
|
|
| 30 |
lines.append(("Tutor" if m.get("sender") == "assistant" else "User") + f": {txt}")
|
| 31 |
return "\n".join(lines)
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
def _history_as_chat_messages(messages: list[dict]) -> list[dict]:
|
| 34 |
+
"""Convert history to chat-completion style messages."""
|
| 35 |
msgs = [{"role": "system", "content": TUTOR_PROMPT}]
|
| 36 |
for m in messages:
|
| 37 |
txt = (m.get("text") or "").strip()
|
|
|
|
| 42 |
return msgs
|
| 43 |
|
| 44 |
def _extract_chat_text(chat_resp) -> str:
|
| 45 |
+
"""Extract text from HF chat response."""
|
| 46 |
try:
|
| 47 |
+
return chat_resp.choices[0].message["content"] if isinstance(
|
| 48 |
+
chat_resp.choices[0].message, dict
|
| 49 |
+
) else chat_resp.choices[0].message.content
|
| 50 |
except Exception:
|
|
|
|
| 51 |
try:
|
| 52 |
return chat_resp["choices"][0]["message"]["content"]
|
| 53 |
except Exception:
|
| 54 |
return str(chat_resp)
|
| 55 |
|
| 56 |
+
# -------------------------------
|
| 57 |
+
# Reply logic
|
| 58 |
+
# -------------------------------
|
| 59 |
def _reply_with_hf():
|
| 60 |
if "client" not in globals():
|
| 61 |
raise RuntimeError("HF client not initialized")
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
try:
|
| 64 |
+
# 1) Prefer chat API
|
| 65 |
+
msgs = _history_as_chat_messages(st.session_state.get("messages", []))
|
| 66 |
+
chat = client.chat.completions.create(
|
| 67 |
+
model=GEN_MODEL,
|
| 68 |
+
messages=msgs,
|
| 69 |
+
max_tokens=300, # give enough room
|
| 70 |
temperature=0.2,
|
| 71 |
top_p=0.9,
|
|
|
|
|
|
|
|
|
|
| 72 |
)
|
| 73 |
+
return _extract_chat_text(chat).strip()
|
|
|
|
| 74 |
|
| 75 |
except ValueError as ve:
|
| 76 |
+
# 2) Fallback to text-generation if chat unsupported
|
| 77 |
+
if "Supported task: text-generation" in str(ve):
|
| 78 |
+
convo = _format_history_for_flan(st.session_state.get("messages", []))
|
| 79 |
+
tg_prompt = f"{TUTOR_PROMPT}\n\n{convo}\n\nTutor:"
|
| 80 |
+
resp = client.text_generation(
|
| 81 |
+
tg_prompt,
|
| 82 |
+
max_new_tokens=300,
|
| 83 |
temperature=0.2,
|
| 84 |
top_p=0.9,
|
| 85 |
+
repetition_penalty=1.1,
|
| 86 |
+
return_full_text=True,
|
| 87 |
+
stream=False,
|
| 88 |
)
|
| 89 |
+
return (resp.get("generated_text") if isinstance(resp, dict) else resp).strip()
|
| 90 |
|
| 91 |
+
raise # rethrow anything else
|
|
|
|
| 92 |
|
| 93 |
except Exception as e:
|
| 94 |
err_text = ''.join(traceback.format_exception_only(type(e), e)).strip()
|
phase/Student_view/games/budgetbuilder.py
CHANGED
|
@@ -1,10 +1,70 @@
|
|
| 1 |
# phase\Student_view\games\budgetbuilder.py
|
| 2 |
|
| 3 |
import streamlit as st
|
|
|
|
|
|
|
| 4 |
from utils import db as dbapi
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
def show_budget_builder():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
# Add custom CSS for improved styling
|
| 9 |
st.markdown("""
|
| 10 |
<style>
|
|
@@ -392,6 +452,12 @@ def show_budget_builder():
|
|
| 392 |
st.session_state.level_completed = True
|
| 393 |
if level["id"] not in st.session_state.completed_levels:
|
| 394 |
st.session_state.completed_levels.append(level["id"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
else:
|
| 396 |
st.error("❌ Not complete yet. Check the requirements!")
|
| 397 |
for desc, passed in results:
|
|
@@ -416,8 +482,10 @@ def show_budget_builder():
|
|
| 416 |
st.session_state.current_level += 1
|
| 417 |
st.session_state.categories = {cid: 0 for cid in categories_master.keys()}
|
| 418 |
st.session_state.level_completed = False
|
|
|
|
| 419 |
st.experimental_rerun()
|
| 420 |
|
|
|
|
| 421 |
with right_col:
|
| 422 |
criteria_html = ""
|
| 423 |
for desc, fn in level["success"]:
|
|
@@ -486,4 +554,5 @@ def show_budget_builder():
|
|
| 486 |
st.session_state.completed_levels = []
|
| 487 |
st.session_state.categories = {cid: 0 for cid in categories_master.keys()}
|
| 488 |
st.session_state.level_completed = False
|
|
|
|
| 489 |
st.experimental_rerun()
|
|
|
|
| 1 |
# phase\Student_view\games\budgetbuilder.py
|
| 2 |
|
| 3 |
import streamlit as st
|
| 4 |
+
import os, time
|
| 5 |
+
from utils import api as backend
|
| 6 |
from utils import db as dbapi
|
| 7 |
|
| 8 |
+
DISABLE_DB = os.getenv("DISABLE_DB", "1") == "1"
|
| 9 |
+
|
| 10 |
+
def _refresh_global_xp():
|
| 11 |
+
user = st.session_state.get("user")
|
| 12 |
+
if not user:
|
| 13 |
+
return
|
| 14 |
+
try:
|
| 15 |
+
stats = backend.user_stats(user["user_id"]) if DISABLE_DB else dbapi.user_xp_and_level(user["user_id"])
|
| 16 |
+
st.session_state.xp = stats.get("xp", st.session_state.get("xp", 0))
|
| 17 |
+
st.session_state.streak = stats.get("streak", st.session_state.get("streak", 0))
|
| 18 |
+
except Exception as e:
|
| 19 |
+
st.warning(f"XP refresh failed: {e}")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _persist_budget_result(level_cfg: dict, success: bool, gained_xp: int):
|
| 23 |
+
user = st.session_state.get("user")
|
| 24 |
+
if not user:
|
| 25 |
+
st.info("Login to earn and save XP.")
|
| 26 |
+
return
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
elapsed_ms = int((time.time() - st.session_state.get("bb_start_ts", time.time())) * 1000)
|
| 30 |
+
allocations = [{"id": cid, "amount": int(val)} for cid, val in st.session_state.categories.items()]
|
| 31 |
+
budget_score = 100 if success else 0
|
| 32 |
+
|
| 33 |
+
if DISABLE_DB:
|
| 34 |
+
backend.record_budget_builder_play(
|
| 35 |
+
user_id=user["user_id"],
|
| 36 |
+
weekly_allowance=int(level_cfg["income"]),
|
| 37 |
+
budget_score=int(budget_score),
|
| 38 |
+
elapsed_ms=elapsed_ms,
|
| 39 |
+
allocations=allocations,
|
| 40 |
+
gained_xp=int(gained_xp),
|
| 41 |
+
)
|
| 42 |
+
else:
|
| 43 |
+
# Local DB path (if your db layer has one of these)
|
| 44 |
+
if hasattr(dbapi, "record_budget_builder_result"):
|
| 45 |
+
dbapi.record_budget_builder_result(
|
| 46 |
+
user_id=user["user_id"],
|
| 47 |
+
weekly_allowance=int(level_cfg["income"]),
|
| 48 |
+
budget_score=int(budget_score),
|
| 49 |
+
elapsed_ms=elapsed_ms,
|
| 50 |
+
allocations=allocations,
|
| 51 |
+
gained_xp=int(gained_xp),
|
| 52 |
+
)
|
| 53 |
+
elif hasattr(dbapi, "award_xp"):
|
| 54 |
+
dbapi.award_xp(user["user_id"], int(gained_xp), reason="budget_builder")
|
| 55 |
+
|
| 56 |
+
_refresh_global_xp() # <-- this makes the XP bar move immediately
|
| 57 |
+
except Exception as e:
|
| 58 |
+
st.warning(f"Could not save budget result: {e}")
|
| 59 |
+
|
| 60 |
|
| 61 |
def show_budget_builder():
|
| 62 |
+
|
| 63 |
+
# timer for elapsed_ms
|
| 64 |
+
if "bb_start_ts" not in st.session_state:
|
| 65 |
+
st.session_state.bb_start_ts = time.time()
|
| 66 |
+
|
| 67 |
+
|
| 68 |
# Add custom CSS for improved styling
|
| 69 |
st.markdown("""
|
| 70 |
<style>
|
|
|
|
| 452 |
st.session_state.level_completed = True
|
| 453 |
if level["id"] not in st.session_state.completed_levels:
|
| 454 |
st.session_state.completed_levels.append(level["id"])
|
| 455 |
+
|
| 456 |
+
# award exactly once per level
|
| 457 |
+
award_key = f"_bb_xp_awarded_L{level['id']}"
|
| 458 |
+
if not st.session_state.get(award_key):
|
| 459 |
+
_persist_budget_result(level, success=True, gained_xp=int(level["xp"]))
|
| 460 |
+
st.session_state[award_key] = True
|
| 461 |
else:
|
| 462 |
st.error("❌ Not complete yet. Check the requirements!")
|
| 463 |
for desc, passed in results:
|
|
|
|
| 482 |
st.session_state.current_level += 1
|
| 483 |
st.session_state.categories = {cid: 0 for cid in categories_master.keys()}
|
| 484 |
st.session_state.level_completed = False
|
| 485 |
+
st.session_state.bb_start_ts = time.time() # <-- reset timer
|
| 486 |
st.experimental_rerun()
|
| 487 |
|
| 488 |
+
|
| 489 |
with right_col:
|
| 490 |
criteria_html = ""
|
| 491 |
for desc, fn in level["success"]:
|
|
|
|
| 554 |
st.session_state.completed_levels = []
|
| 555 |
st.session_state.categories = {cid: 0 for cid in categories_master.keys()}
|
| 556 |
st.session_state.level_completed = False
|
| 557 |
+
st.session_state.bb_start_ts = time.time() # <-- reset timer
|
| 558 |
st.experimental_rerun()
|
phase/Student_view/games/debtdilemma.py
CHANGED
|
@@ -4,9 +4,11 @@ from dataclasses import dataclass, field
|
|
| 4 |
from typing import List, Optional, Dict, Literal
|
| 5 |
import random
|
| 6 |
import math
|
| 7 |
-
import os
|
|
|
|
| 8 |
from utils import db as dbapi
|
| 9 |
|
|
|
|
| 10 |
|
| 11 |
def load_css(file_name: str):
|
| 12 |
try:
|
|
@@ -16,6 +18,18 @@ def load_css(file_name: str):
|
|
| 16 |
st.warning("⚠️ Stylesheet not found. Please ensure 'assets/styles.css' exists.")
|
| 17 |
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
DD_SCOPE_CLASS = "dd-scope"
|
| 20 |
|
| 21 |
def _ensure_dd_css():
|
|
@@ -278,19 +292,37 @@ def _award_level_completion_if_needed():
|
|
| 278 |
return # already awarded for this level
|
| 279 |
|
| 280 |
try:
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
st.session_state[key] = True
|
|
|
|
| 294 |
st.success("Saved +50 XP for completing this loan")
|
| 295 |
except Exception as e:
|
| 296 |
st.error(f"Could not save completion XP: {e}")
|
|
@@ -346,6 +378,8 @@ def init_state():
|
|
| 346 |
missedPayments=0,
|
| 347 |
creditScore=random.randint(200, 600),
|
| 348 |
)
|
|
|
|
|
|
|
| 349 |
|
| 350 |
# Fortnight helper (every 2 weeks)
|
| 351 |
def current_fortnight() -> int:
|
|
@@ -420,6 +454,7 @@ def gen_random_event() -> Optional[RandomEvent]:
|
|
| 420 |
# Game actions
|
| 421 |
# ===============================
|
| 422 |
def start_loan():
|
|
|
|
| 423 |
st.session_state.gamePhase = "repaying"
|
| 424 |
if DISBURSE_LOAN_TO_WALLET:
|
| 425 |
st.session_state.wallet += st.session_state.loan.principal
|
|
@@ -955,6 +990,7 @@ def reset_game():
|
|
| 955 |
if k not in {"user", "current_page", "xp", "streak", "current_game", "temp_user"}:
|
| 956 |
del st.session_state[k]
|
| 957 |
init_state()
|
|
|
|
| 958 |
st.rerun()
|
| 959 |
|
| 960 |
def hospital_screen():
|
|
@@ -984,8 +1020,8 @@ def level_complete_screen():
|
|
| 984 |
creditScore=st.session_state.loan.creditScore,
|
| 985 |
)
|
| 986 |
st.session_state.monthlyIncome = lvl.startingIncome
|
|
|
|
| 987 |
st.session_state.gamePhase = "setup"
|
| 988 |
-
# use scoped button
|
| 989 |
st.buttondd("➡️ Start next level", on_click=_go_next, key="btn_next_level", variant="success")
|
| 990 |
|
| 991 |
def completed_screen():
|
|
|
|
| 4 |
from typing import List, Optional, Dict, Literal
|
| 5 |
import random
|
| 6 |
import math
|
| 7 |
+
import os, time
|
| 8 |
+
from utils import api as backend
|
| 9 |
from utils import db as dbapi
|
| 10 |
|
| 11 |
+
DISABLE_DB = os.getenv("DISABLE_DB", "1") == "1"
|
| 12 |
|
| 13 |
def load_css(file_name: str):
|
| 14 |
try:
|
|
|
|
| 18 |
st.warning("⚠️ Stylesheet not found. Please ensure 'assets/styles.css' exists.")
|
| 19 |
|
| 20 |
|
| 21 |
+
def _refresh_global_xp():
|
| 22 |
+
user = st.session_state.get("user")
|
| 23 |
+
if not user:
|
| 24 |
+
return
|
| 25 |
+
try:
|
| 26 |
+
stats = backend.user_stats(user["user_id"]) if DISABLE_DB else dbapi.user_xp_and_level(user["user_id"])
|
| 27 |
+
st.session_state.xp = stats.get("xp", st.session_state.get("xp", 0))
|
| 28 |
+
st.session_state.streak = stats.get("streak", st.session_state.get("streak", 0))
|
| 29 |
+
except Exception as e:
|
| 30 |
+
st.warning(f"XP refresh failed: {e}")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
DD_SCOPE_CLASS = "dd-scope"
|
| 34 |
|
| 35 |
def _ensure_dd_css():
|
|
|
|
| 292 |
return # already awarded for this level
|
| 293 |
|
| 294 |
try:
|
| 295 |
+
# compute elapsed time for the level
|
| 296 |
+
start_ts = st.session_state.get("dd_start_ts", time.time())
|
| 297 |
+
elapsed_ms = int(max(0, (time.time() - start_ts) * 1000))
|
| 298 |
+
|
| 299 |
+
if DISABLE_DB:
|
| 300 |
+
# call backend Space
|
| 301 |
+
backend.record_debt_dilemma_play(
|
| 302 |
+
user_id=user["user_id"],
|
| 303 |
+
loans_cleared=1, # you completed the level
|
| 304 |
+
mistakes=int(st.session_state.loan.missedPayments),
|
| 305 |
+
elapsed_ms=elapsed_ms,
|
| 306 |
+
gained_xp=50,
|
| 307 |
+
)
|
| 308 |
+
else:
|
| 309 |
+
# local DB path kept for dev mode
|
| 310 |
+
dbapi.record_debt_dilemma_round(
|
| 311 |
+
user["user_id"],
|
| 312 |
+
level=lvl,
|
| 313 |
+
round_no=0,
|
| 314 |
+
wallet=int(st.session_state.wallet),
|
| 315 |
+
health=int(st.session_state.health),
|
| 316 |
+
happiness=int(st.session_state.happiness),
|
| 317 |
+
credit_score=int(st.session_state.loan.creditScore),
|
| 318 |
+
event_json={"phase": st.session_state.gamePhase},
|
| 319 |
+
outcome=("level_complete" if st.session_state.gamePhase == "level-complete" else "game_complete"),
|
| 320 |
+
gained_xp=50,
|
| 321 |
+
elapsed_ms=elapsed_ms,
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
st.session_state[key] = True
|
| 325 |
+
_refresh_global_xp()
|
| 326 |
st.success("Saved +50 XP for completing this loan")
|
| 327 |
except Exception as e:
|
| 328 |
st.error(f"Could not save completion XP: {e}")
|
|
|
|
| 378 |
missedPayments=0,
|
| 379 |
creditScore=random.randint(200, 600),
|
| 380 |
)
|
| 381 |
+
if "dd_start_ts" not in st.session_state:
|
| 382 |
+
st.session_state.dd_start_ts = time.time()
|
| 383 |
|
| 384 |
# Fortnight helper (every 2 weeks)
|
| 385 |
def current_fortnight() -> int:
|
|
|
|
| 454 |
# Game actions
|
| 455 |
# ===============================
|
| 456 |
def start_loan():
|
| 457 |
+
st.session_state.dd_start_ts = time.time() # start elapsed timer
|
| 458 |
st.session_state.gamePhase = "repaying"
|
| 459 |
if DISBURSE_LOAN_TO_WALLET:
|
| 460 |
st.session_state.wallet += st.session_state.loan.principal
|
|
|
|
| 990 |
if k not in {"user", "current_page", "xp", "streak", "current_game", "temp_user"}:
|
| 991 |
del st.session_state[k]
|
| 992 |
init_state()
|
| 993 |
+
st.session_state.dd_start_ts = time.time() # fresh timer after reset
|
| 994 |
st.rerun()
|
| 995 |
|
| 996 |
def hospital_screen():
|
|
|
|
| 1020 |
creditScore=st.session_state.loan.creditScore,
|
| 1021 |
)
|
| 1022 |
st.session_state.monthlyIncome = lvl.startingIncome
|
| 1023 |
+
st.session_state.dd_start_ts = time.time() # reset timer for new level
|
| 1024 |
st.session_state.gamePhase = "setup"
|
|
|
|
| 1025 |
st.buttondd("➡️ Start next level", on_click=_go_next, key="btn_next_level", variant="success")
|
| 1026 |
|
| 1027 |
def completed_screen():
|
phase/Student_view/games/profitpuzzle.py
CHANGED
|
@@ -1,12 +1,19 @@
|
|
| 1 |
-
|
| 2 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
def _refresh_global_xp():
|
| 5 |
user = st.session_state.get("user")
|
| 6 |
if not user:
|
| 7 |
return
|
| 8 |
try:
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
| 10 |
st.session_state.xp = stats.get("xp", st.session_state.get("xp", 0))
|
| 11 |
st.session_state.streak = stats.get("streak", st.session_state.get("streak", 0))
|
| 12 |
except Exception as e:
|
|
@@ -279,6 +286,7 @@ def reset_game():
|
|
| 279 |
st.session_state.show_solution = False
|
| 280 |
st.session_state.score = 0
|
| 281 |
st.session_state.completed_scenarios = []
|
|
|
|
| 282 |
st.rerun()
|
| 283 |
|
| 284 |
|
|
@@ -288,6 +296,9 @@ def show_profit_puzzle():
|
|
| 288 |
# Load CSS styling
|
| 289 |
load_css()
|
| 290 |
|
|
|
|
|
|
|
|
|
|
| 291 |
st.markdown("""
|
| 292 |
<div class="game-header">
|
| 293 |
<h1>🎯 Profit Puzzle Challenge!</h1>
|
|
@@ -401,28 +412,58 @@ def show_profit_puzzle():
|
|
| 401 |
# Persist to TiDB if logged in
|
| 402 |
user = st.session_state.get("user")
|
| 403 |
if user:
|
|
|
|
|
|
|
| 404 |
try:
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
_refresh_global_xp()
|
| 414 |
except Exception as e:
|
| 415 |
-
st.warning(f"
|
| 416 |
else:
|
| 417 |
st.info("Login to earn and save XP.")
|
| 418 |
|
| 419 |
st.session_state.show_solution = True
|
| 420 |
|
| 421 |
def next_scenario():
|
| 422 |
-
if st.session_state.current_scenario < len(
|
| 423 |
st.session_state.current_scenario += 1
|
| 424 |
st.session_state.user_answer = ""
|
| 425 |
st.session_state.show_solution = False
|
|
|
|
|
|
|
| 426 |
|
| 427 |
def reset_game():
|
| 428 |
st.session_state.current_scenario = 0
|
|
|
|
| 1 |
+
import os, time
|
| 2 |
import streamlit as st
|
| 3 |
+
from utils import api as backend # HTTP to backend Space
|
| 4 |
+
from utils import db as dbapi # direct DB path (only if DISABLE_DB=0)
|
| 5 |
+
|
| 6 |
+
DISABLE_DB = os.getenv("DISABLE_DB", "1") == "1"
|
| 7 |
|
| 8 |
def _refresh_global_xp():
|
| 9 |
user = st.session_state.get("user")
|
| 10 |
if not user:
|
| 11 |
return
|
| 12 |
try:
|
| 13 |
+
if DISABLE_DB:
|
| 14 |
+
stats = backend.user_stats(user["user_id"])
|
| 15 |
+
else:
|
| 16 |
+
stats = dbapi.user_xp_and_level(user["user_id"])
|
| 17 |
st.session_state.xp = stats.get("xp", st.session_state.get("xp", 0))
|
| 18 |
st.session_state.streak = stats.get("streak", st.session_state.get("streak", 0))
|
| 19 |
except Exception as e:
|
|
|
|
| 286 |
st.session_state.show_solution = False
|
| 287 |
st.session_state.score = 0
|
| 288 |
st.session_state.completed_scenarios = []
|
| 289 |
+
st.session_state.pp_start_ts = time.time()
|
| 290 |
st.rerun()
|
| 291 |
|
| 292 |
|
|
|
|
| 296 |
# Load CSS styling
|
| 297 |
load_css()
|
| 298 |
|
| 299 |
+
if "pp_start_ts" not in st.session_state:
|
| 300 |
+
st.session_state.pp_start_ts = time.time()
|
| 301 |
+
|
| 302 |
st.markdown("""
|
| 303 |
<div class="game-header">
|
| 304 |
<h1>🎯 Profit Puzzle Challenge!</h1>
|
|
|
|
| 412 |
# Persist to TiDB if logged in
|
| 413 |
user = st.session_state.get("user")
|
| 414 |
if user:
|
| 415 |
+
elapsed_ms = int((time.time() - st.session_state.get("pp_start_ts", time.time())) * 1000)
|
| 416 |
+
|
| 417 |
try:
|
| 418 |
+
if DISABLE_DB:
|
| 419 |
+
# Route to backend Space
|
| 420 |
+
backend.record_profit_puzzler_play(
|
| 421 |
+
user_id=user["user_id"],
|
| 422 |
+
puzzles_solved=1 if correct else 0,
|
| 423 |
+
mistakes=0 if correct else 1,
|
| 424 |
+
elapsed_ms=elapsed_ms,
|
| 425 |
+
gained_xp=reward # keep UI and server in sync
|
| 426 |
+
)
|
| 427 |
+
else:
|
| 428 |
+
# Direct DB path if you keep it
|
| 429 |
+
if hasattr(dbapi, "record_profit_puzzler_play"):
|
| 430 |
+
dbapi.record_profit_puzzler_play(
|
| 431 |
+
user_id=user["user_id"],
|
| 432 |
+
puzzles_solved=1 if correct else 0,
|
| 433 |
+
mistakes=0 if correct else 1,
|
| 434 |
+
elapsed_ms=elapsed_ms,
|
| 435 |
+
gained_xp=reward
|
| 436 |
+
)
|
| 437 |
+
else:
|
| 438 |
+
# Fallback to your existing detailed writer
|
| 439 |
+
dbapi.record_profit_puzzle_result(
|
| 440 |
+
user_id=user["user_id"],
|
| 441 |
+
scenario_id=scenario.get("id") or f"scenario_{st.session_state.current_scenario}",
|
| 442 |
+
title=scenario.get("title", f"Scenario {st.session_state.current_scenario+1}"),
|
| 443 |
+
units=int(scenario["variables"]["units"]),
|
| 444 |
+
price=int(scenario["variables"]["sellingPrice"]),
|
| 445 |
+
cost=int(scenario["variables"]["costPerUnit"]),
|
| 446 |
+
user_answer=float(st.session_state.user_answer),
|
| 447 |
+
actual_profit=float(actual_profit),
|
| 448 |
+
is_correct=bool(correct),
|
| 449 |
+
gained_xp=int(reward)
|
| 450 |
+
)
|
| 451 |
+
|
| 452 |
_refresh_global_xp()
|
| 453 |
except Exception as e:
|
| 454 |
+
st.warning(f"Save failed: {e}")
|
| 455 |
else:
|
| 456 |
st.info("Login to earn and save XP.")
|
| 457 |
|
| 458 |
st.session_state.show_solution = True
|
| 459 |
|
| 460 |
def next_scenario():
|
| 461 |
+
if st.session_state.get("current_scenario", 0) < len(st.session_state.get("profit_scenarios", [])) - 1:
|
| 462 |
st.session_state.current_scenario += 1
|
| 463 |
st.session_state.user_answer = ""
|
| 464 |
st.session_state.show_solution = False
|
| 465 |
+
st.session_state.pp_start_ts = time.time()
|
| 466 |
+
st.rerun()
|
| 467 |
|
| 468 |
def reset_game():
|
| 469 |
st.session_state.current_scenario = 0
|
utils/api.py
CHANGED
|
@@ -409,4 +409,41 @@ def record_money_match_play(user_id: int, target: int, total: int,
|
|
| 409 |
}
|
| 410 |
return _try_candidates("POST", [
|
| 411 |
("/games/money_match/record", {"json": payload}),
|
| 412 |
-
])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
}
|
| 410 |
return _try_candidates("POST", [
|
| 411 |
("/games/money_match/record", {"json": payload}),
|
| 412 |
+
])
|
| 413 |
+
|
| 414 |
+
def record_budget_builder_play(user_id: int, weekly_allowance: int, budget_score: int,
|
| 415 |
+
elapsed_ms: int, allocations: list[dict], gained_xp: int | None):
|
| 416 |
+
payload = {
|
| 417 |
+
"user_id": user_id,
|
| 418 |
+
"weekly_allowance": weekly_allowance,
|
| 419 |
+
"budget_score": budget_score,
|
| 420 |
+
"elapsed_ms": elapsed_ms,
|
| 421 |
+
"allocations": allocations,
|
| 422 |
+
"gained_xp": gained_xp,
|
| 423 |
+
}
|
| 424 |
+
return _try_candidates("POST", [
|
| 425 |
+
("/games/budget_builder/record", {"json": payload}),
|
| 426 |
+
])
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def record_debt_dilemma_play(user_id: int, loans_cleared: int,
|
| 430 |
+
mistakes: int, elapsed_ms: int, gained_xp: int):
|
| 431 |
+
payload = {
|
| 432 |
+
"user_id": user_id,
|
| 433 |
+
"loans_cleared": loans_cleared,
|
| 434 |
+
"mistakes": mistakes,
|
| 435 |
+
"elapsed_ms": elapsed_ms,
|
| 436 |
+
"gained_xp": gained_xp,
|
| 437 |
+
}
|
| 438 |
+
return _try_candidates("POST", [
|
| 439 |
+
("/games/debt_dilemma/record", {"json": payload}),
|
| 440 |
+
("/api/games/debt_dilemma/record", {"json": payload}),
|
| 441 |
+
("/api/v1/games/debt_dilemma/record", {"json": payload}),
|
| 442 |
+
])
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
def record_profit_puzzler_play(user_id: int, puzzles_solved: int, mistakes: int, elapsed_ms: int, gained_xp: int | None = None):
|
| 446 |
+
payload = {"user_id": user_id, "puzzles_solved": puzzles_solved, "mistakes": mistakes, "elapsed_ms": elapsed_ms}
|
| 447 |
+
if gained_xp is not None:
|
| 448 |
+
payload["gained_xp"] = gained_xp
|
| 449 |
+
return _try_candidates("POST", [("/games/profit_puzzler/record", {"json": payload})])
|