lanna_lalala;- commited on
Commit
f674711
·
2 Parent(s): e70081c2e34c05

Merge branch 'main' of https://github.com/Alalalallalalalalalalalal/FinED-Front

Browse files
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
- # Convert your session history to HF chat format
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
- # Works for both dict-like and object-like responses
48
  try:
49
- # huggingface_hub >= 0.23 style
50
- return chat_resp.choices[0].message["content"] if isinstance(chat_resp.choices[0].message, dict) \
51
- else chat_resp.choices[0].message.content
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) Try text-generation first (many backends support this)
69
- resp = client.text_generation(
70
- tg_prompt,
71
- max_new_tokens=220,
 
 
72
  temperature=0.2,
73
  top_p=0.9,
74
- repetition_penalty=1.1,
75
- return_full_text=True,
76
- stream=False,
77
  )
78
- text = resp.get("generated_text") if isinstance(resp, dict) else resp
79
- return _trim_turn(str(text or "").strip())
80
 
81
  except ValueError as ve:
82
- # 2) If the provider says "Supported task: conversational", use HF chat
83
- if "Supported task: conversational" in str(ve):
84
- msgs = _history_as_chat_messages(st.session_state.get("messages", []))
85
- chat = client.chat.completions.create(
86
- model=GEN_MODEL, # still HF, not OpenAI
87
- messages=msgs,
88
- max_tokens=220,
89
  temperature=0.2,
90
  top_p=0.9,
 
 
 
91
  )
92
- return _trim_turn(_extract_chat_text(chat))
93
 
94
- # Some other ValueError — rethrow with the original message
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
- dbapi.record_debt_dilemma_round(
282
- user["user_id"],
283
- level=lvl,
284
- round_no=0,
285
- wallet=int(st.session_state.wallet),
286
- health=int(st.session_state.health),
287
- happiness=int(st.session_state.happiness),
288
- credit_score=int(st.session_state.loan.creditScore),
289
- event_json={"phase": st.session_state.gamePhase},
290
- outcome="level_complete" if st.session_state.gamePhase == "level-complete" else "game_complete",
291
- gained_xp=50
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
- from utils import db as dbapi
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
- stats = dbapi.user_xp_and_level(user["user_id"])
 
 
 
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
- dbapi.record_profit_puzzle_result(
406
- user_id=user["user_id"],
407
- scenario_id=scenario.get("id") or f"scenario_{st.session_state.current_scenario}",
408
- title=scenario.get("title", f"Scenario {st.session_state.current_scenario+1}"),
409
- units=units, price=price, cost=cost,
410
- user_answer=user_val, actual_profit=float(actual_profit),
411
- is_correct=bool(correct), gained_xp=int(reward)
412
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  _refresh_global_xp()
414
  except Exception as e:
415
- st.warning(f"Could not save result: {e}")
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(scenarios) - 1:
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})])