import streamlit as st
import urllib.parse
from streamlit_cookies_controller import CookieController
import db
import importlib
importlib.reload(db)
import srs_logic
import llm_reviewer
import gdrive_sync
st.set_page_config(page_title="AlgoSpaced", page_icon="π§ ", layout="centered")
# Initialize Cookie Controller
controller = CookieController()
def trigger_delayed_reload(delay_ms=500):
st.components.v1.html(
f"""
""",
height=0,
width=0
)
JOURNEY_SEQUENCE = [
217, 242, 1, 49, 347, 271, 238, 36, 128, 125, 167, 15, 11, 42, 121, 3, 424, 567, 76, 239, 20,
155, 150, 739, 853, 84, 704, 74, 875, 153, 33, 981, 4, 206, 21, 141, 143, 19, 138, 2, 287,
146, 23, 25, 226, 104, 543, 110, 100, 572, 235, 102, 199, 1448, 98, 230, 105, 124, 297, 703,
1046, 973, 215, 621, 355, 295, 78, 39, 40, 46, 90, 22, 79, 131, 17, 51, 208, 211, 212, 200,
695, 133, 286, 994, 417, 130, 207, 210, 261, 323, 684, 127, 743, 332, 1584, 778, 269, 787,
70, 746, 198, 213, 5, 647, 91, 322, 152, 139, 300, 416, 62, 1143, 309, 518, 494, 97, 329,
115, 72, 312, 10, 53, 55, 45, 134, 846, 1899, 763, 678, 57, 56, 435, 252, 253, 1851, 48,
54, 73, 202, 66, 50, 43, 2013, 136, 191, 338, 190, 268, 371, 7
]
@st.dialog("Problem Details")
def show_problem_details_dialog(problem_number):
completions = db.get_user_journey_progress()
is_completed = problem_number in completions
problems = db.get_problems_by_number(problem_number)
if problems:
st.markdown(f"### LeetCode {problem_number}")
# Display each method
for i, p in enumerate(problems):
with st.expander(f"Method: {p['pattern']} ({p['difficulty']})", expanded=(i == 0)):
st.markdown(f"**Complexity**: Time `{p['optimal_time']}` | Space `{p['optimal_space']}`")
st.markdown(f"[View Google Doc]({p['google_doc_url']})")
# Review button
if st.button("π― Review This Problem", key=f"rev_{p['id']}", use_container_width=True):
st.session_state.active_review_problem_id = p['id']
st.session_state.active_tab = "π― Review Queue"
st.rerun()
else:
st.warning("No local notes found for this problem yet.")
st.write("You can search for this problem on LeetCode or Google:")
leetcode_url = f"https://leetcode.com/problemset/?search={problem_number}"
google_url = f"https://www.google.com/search?q=leetcode+{problem_number}"
st.markdown(f"- [Search on LeetCode π]({leetcode_url})")
st.markdown(f"- [Search on Google π]({google_url})")
st.markdown("---")
# Toggle viewed status
if is_completed:
st.write("Status: **Completed / Viewed** β
")
if st.button("π΄ Remove from Viewed", use_container_width=True):
db.toggle_journey_progress(problem_number)
st.rerun()
else:
st.write("Status: **Not Completed / Viewed** β")
if st.button("π’ Mark as Completed / Viewed", type="primary", use_container_width=True):
db.toggle_journey_progress(problem_number)
st.rerun()
def show_journey_page():
import math
if "show_dialog_num" in st.session_state:
show_problem_details_dialog(st.session_state.show_dialog_num)
del st.session_state.show_dialog_num
st.subheader("πΊοΈ Your Journey Path")
st.write("Visually track your algorithmic progress. Synced problems and manual completions are highlighted in **green/gold**.")
completions = db.get_user_journey_progress()
titles = db.get_problem_titles_by_number()
total = len(JOURNEY_SEQUENCE)
completed_count = len(completions.intersection(JOURNEY_SEQUENCE))
pct = int((completed_count / total) * 100) if total else 0
st.progress(pct / 100.0)
st.write(f"**Progress**: {completed_count} / {total} Problems Completed ({pct}%)")
css = """
"""
html = f"{css}
"
for i, num in enumerate(JOURNEY_SEQUENCE):
offset = int(math.sin(i * 0.8) * 65)
is_completed = num in completions
status_class = "completed" if is_completed else "uncompleted"
title = titles.get(num)
if title:
tooltip_text = f"{num}. {title}"
else:
tooltip_text = f"Problem {num} (Not Synced)"
html += f'
{num}'
html += "
"
st.markdown(html, unsafe_allow_html=True)
# Check and restore session from cookies if present and not already restored
client = db.get_supabase_client()
if "session_restored" not in st.session_state and "signed_out" not in st.session_state:
cookies = st.context.cookies
access_token = cookies.get('sb_access_token')
refresh_token = cookies.get('sb_refresh_token')
if access_token and refresh_token:
try:
client.auth.set_session(access_token, refresh_token)
st.session_state.session_restored = True
st.rerun()
except Exception:
# Token might be invalid/expired, remove them
controller.remove('sb_access_token')
controller.remove('sb_refresh_token')
def dump_cookies_diagnostics():
import os
try:
log_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "debug_cookies.txt")
cookies = st.context.cookies
session = client.auth.get_session()
user_id = session.user.id if session and session.user else None
with open(log_path, "w", encoding="utf-8") as f:
f.write(f"Cookies keys: {list(cookies.keys())}\n")
f.write(f"sb_access_token: {cookies.get('sb_access_token')[:20] if cookies.get('sb_access_token') else None}...\n")
f.write(f"sb_refresh_token: {cookies.get('sb_refresh_token')[:20] if cookies.get('sb_refresh_token') else None}...\n")
f.write(f"Session State keys: {list(st.session_state.keys())}\n")
f.write(f"Supabase Client User ID: {user_id}\n")
if "session_restored" in st.session_state:
f.write(f"session_restored: {st.session_state.session_restored}\n")
if "signed_out" in st.session_state:
f.write(f"signed_out: {st.session_state.signed_out}\n")
except Exception as e:
import traceback
with open("debug_cookies_error.txt", "w") as err_f:
err_f.write(f"Error: {e}\n")
err_f.write(traceback.format_exc())
dump_cookies_diagnostics()
def handle_review_completion(current_review, streak_data, rating):
new_box = srs_logic.evaluate_new_box(current_review['box_level'], rating)
next_review_date = srs_logic.calculate_next_review(new_box).isoformat()
times_correct = current_review['times_correct'] + (1 if rating == "Correct" else 0)
total_attempts = current_review['total_attempts'] + 1
db.update_review(current_review['id'], {
"box_level": new_box,
"next_review": next_review_date,
"times_correct": times_correct,
"total_attempts": total_attempts
})
new_streak = srs_logic.increment_streak(streak_data)
db.update_streak(new_streak)
st.toast(f"Review saved! Moved to Box {new_box}. Next review: {next_review_date[:10]}")
def show_auth_screen():
st.markdown("Welcome to AlgoSpaced π§
", unsafe_allow_html=True)
st.markdown("A cloud-syncing Spaced Repetition system for mastering LeetCode algorithmic paradigms.
", unsafe_allow_html=True)
# Sign in with Google (placed prominently at the top)
client = db.get_supabase_client()
if st.button("π΄ Sign in with Google", use_container_width=True):
with st.spinner("Redirecting to Google..."):
try:
# 1. Trigger oauth to generate the code challenge and verifier internally
res = client.auth.sign_in_with_oauth({
"provider": "google",
"options": {
"redirect_to": "http://localhost:8501"
}
})
# 2. Extract code_verifier from the client's internal storage
code_verifier = client.auth._storage.get_item("supabase.auth.token-code-verifier")
if res.url and code_verifier:
# 3. Parse res.url and safely append verifier as a query parameter inside redirect_to URL.
# This avoids overriding the 'state' parameter which Supabase reserves internally.
parsed_url = urllib.parse.urlparse(res.url)
query_params = urllib.parse.parse_qs(parsed_url.query)
if "redirect_to" in query_params:
base_redirect = query_params["redirect_to"][0]
separator = "&" if "?" in base_redirect else "?"
query_params["redirect_to"] = [f"{base_redirect}{separator}verifier={code_verifier}"]
# Rebuild the final authorize URL
new_query = urllib.parse.urlencode(query_params, doseq=True)
oauth_url = urllib.parse.urlunparse(parsed_url._replace(query=new_query))
# Redirect client browser
st.markdown(f'', unsafe_allow_html=True)
except Exception as e:
st.error(f"Google login failed: {e}")
st.markdown("β OR β
", unsafe_allow_html=True)
tab1, tab2 = st.tabs(["Log In", "Sign Up"])
with tab1:
email = st.text_input("Email Address", key="login_email")
password = st.text_input("Password", type="password", key="login_password")
if st.button("Log In", type="primary", use_container_width=True):
if not email or not password:
st.error("Please fill in all fields.")
return
with st.spinner("Logging in..."):
try:
res = client.auth.sign_in_with_password({"email": email, "password": password})
if res.user:
st.success("Successfully logged in! Redirecting...")
session = client.auth.get_session()
if session:
controller.set('sb_access_token', session.access_token)
controller.set('sb_refresh_token', session.refresh_token)
if "signed_out" in st.session_state:
del st.session_state.signed_out
trigger_delayed_reload(500)
except Exception as e:
st.error(f"Login failed: {e}")
with tab2:
new_email = st.text_input("Email Address", key="signup_email")
new_password = st.text_input("Password", type="password", key="signup_password")
confirm_password = st.text_input("Confirm Password", type="password", key="signup_confirm_password")
if st.button("Create Account", use_container_width=True):
if not new_email or not new_password or not confirm_password:
st.error("Please fill in all fields.")
return
if new_password != confirm_password:
st.error("Passwords do not match!")
return
with st.spinner("Signing up..."):
try:
res = client.auth.sign_up({"email": new_email, "password": new_password})
if res.user:
st.success("Sign up successful! If email confirmation is enabled in Supabase, check your inbox. Otherwise, you can Log In now.")
session = client.auth.get_session()
if session:
controller.set('sb_access_token', session.access_token)
controller.set('sb_refresh_token', session.refresh_token)
if "signed_out" in st.session_state:
del st.session_state.signed_out
trigger_delayed_reload(500)
except Exception as e:
st.error(f"Sign up failed: {e}")
def dump_user_data_to_file():
import os
try:
user_id = db.get_current_user_id()
if user_id:
client = db.get_supabase_client()
problems = client.table('problems').select('*').eq('user_id', user_id).execute().data
reviews = client.table('user_reviews').select('*').eq('user_id', user_id).execute().data
log_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "debug_db.txt")
with open(log_path, "w", encoding="utf-8") as f:
f.write(f"User ID: {user_id}\n")
f.write(f"Problems Count: {len(problems)}\n")
f.write("Problems:\n")
for p in problems:
code_val = p.get('reference_code')
code_repr = repr(code_val[:50]) if code_val else str(code_val)
f.write(f" - ID: {p['id']}\n")
f.write(f" Name: {p['name']}\n")
f.write(f" Pattern: {p['pattern']}\n")
f.write(f" Code: {code_repr}\n")
f.write(f" Doc ID: {p.get('google_doc_id')}\n")
f.write(f" Created At: {p.get('created_at')}\n")
f.write(f"\nReviews Count: {len(reviews)}\n")
for r in reviews:
f.write(f" - ID: {r['id']}, Problem ID: {r['problem_id']}, Box: {r['box_level']}\n")
except Exception as e:
log_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "debug_error.txt")
with open(log_path, "w", encoding="utf-8") as f:
f.write(str(e))
def main():
# Check for Journey Path view query parameter
query_params = st.query_params
if "view_num" in query_params:
try:
view_num = int(query_params["view_num"])
st.session_state.show_dialog_num = view_num
st.session_state.active_tab = "πΊοΈ Journey Path"
st.query_params.clear()
st.rerun()
except Exception as e:
st.error(f"Error opening details modal: {e}")
# 0. Check for OAuth redirect code and verifier query parameter
if "code" in query_params and "verifier" in query_params:
auth_code = query_params["code"]
code_verifier = query_params["verifier"]
client = db.get_supabase_client()
with st.spinner("Completing Google authentication..."):
try:
# Seed the verifier back into the storage of the new client instance
client.auth._storage.set_item("supabase.auth.token-code-verifier", code_verifier)
# Exchange the authorization code for an active session
client.auth.exchange_code_for_session({
"auth_code": auth_code,
"code_verifier": code_verifier
})
session = client.auth.get_session()
if session:
controller.set('sb_access_token', session.access_token)
controller.set('sb_refresh_token', session.refresh_token)
if "signed_out" in st.session_state:
del st.session_state.signed_out
# Clear URL params
st.query_params.clear()
st.success("Successfully logged in with Google! Redirecting...")
trigger_delayed_reload(500)
except Exception as e:
st.error(f"Failed to exchange Google OAuth code: {e}")
# Authenticate User
client = db.get_supabase_client()
user = None
try:
session = client.auth.get_session()
if session and session.user:
user = session.user
except Exception:
pass
if not user:
show_auth_screen()
return
dump_user_data_to_file()
st.title("AlgoSpaced π§ ")
# 1. Lazy Check & Streak Banner
streak_data = db.get_active_streak()
if streak_data:
streak_data = srs_logic.lazy_check_streak(streak_data)
db.update_streak(streak_data)
# High visibility fluid CSS banner for streak
st.markdown(
f"""
π₯ Current Streak: {streak_data['current_streak']} Days | Longest: {streak_data['longest_streak']} Days
""",
unsafe_allow_html=True
)
# Navigation in Sidebar
if "active_tab" not in st.session_state:
st.session_state.active_tab = "π― Review Queue"
with st.sidebar:
st.header("Navigation")
app_mode = st.radio(
"Go to",
["π― Review Queue", "πΊοΈ Journey Path"],
index=0 if st.session_state.active_tab == "π― Review Queue" else 1
)
st.session_state.active_tab = app_mode
st.markdown("---")
st.header("Settings & Sync")
st.write(f"Logged in as: **{user.email}**")
if st.button("Sync Google Drive Folder", use_container_width=True):
with st.spinner("Syncing..."):
import importlib
importlib.reload(db)
importlib.reload(gdrive_sync)
res = gdrive_sync.sync_gdrive()
st.info(res)
st.markdown("---")
# Collapsible Guide and Tips Expander
with st.sidebar.expander("π‘ Review Tips & Guide"):
st.markdown("""
### π The Core Cycle
AlgoSpaced uses a customized **Leitner System** to optimize retention:
- **Easy / Smashed It**: Moves up 1 Box (Max Box 5).
- **Medium / Struggles**: Stays in the current Box (resets interval).
- **Hard / Forgot**: Resets to **Box 1** immediately.
**Intervals**:
- **Box 1**: 1 day
- **Box 2**: 3 days
- **Box 3**: 7 days
- **Box 4**: 14 days
- **Box 5**: 30 days (Mastered!)
### π Review Modes
- **AI Review**: Write code from memory. Gemini AI reviews logic, bugs, and time/space constraints.
- **Self-Grade (Offline)**: Quickly self-grade yourself if you have spotty connection or want a fast session.
### π Note Formatting (Google Doc)
Ensure your shared Google Doc follows this format to sync properly:
```markdown
121. Best Time to Buy and Sell Stock
Easy
Description of the problem...
Method 1 (One Pass):
Time Complexity: O(N)
Space Complexity: O(1)
Code:
```python
def maxProfit(prices):
# your code here
```
Method 2 (Brute Force):
Time Complexity: O(N^2)
...
```
""")
st.markdown("---")
if st.button("Sign Out", use_container_width=True):
client.auth.sign_out()
controller.remove('sb_access_token')
controller.remove('sb_refresh_token')
if "session_restored" in st.session_state:
del st.session_state.session_restored
st.session_state.signed_out = True
trigger_delayed_reload(500)
if st.session_state.active_tab == "πΊοΈ Journey Path":
show_journey_page()
return
# Fetch current review card (handling custom reviews from Journey Path)
current_review = None
is_custom_review = False
if "active_review_problem_id" in st.session_state:
prob_id = st.session_state.active_review_problem_id
reviews = db.get_reviews_by_problem_id(prob_id)
if reviews:
current_review = reviews[0]
is_custom_review = True
else:
del st.session_state.active_review_problem_id
if not current_review:
due_reviews = db.get_due_reviews()
if not due_reviews:
st.info("π You're all caught up for today! Come back tomorrow.")
return
current_review = due_reviews[0]
problem = current_review['problems']
st.subheader(f"Review: {problem['name']}")
if is_custom_review:
if st.button("π Return to Daily Queue", use_container_width=True):
del st.session_state.active_review_problem_id
st.rerun()
st.write(f"**Pattern**: {problem['pattern']} | **Difficulty**: {problem['difficulty']}")
st.write(f"**Optimal Time**: {problem['optimal_time']} | **Optimal Space**: {problem['optimal_space']}")
if problem['google_doc_url']:
st.markdown(f"[View Google Doc]({problem['google_doc_url']})")
st.write(f"**Box Level**: {current_review['box_level']} (Next review was scheduled for: {current_review['next_review'][:10]})")
# Check if we have evaluation results for this card in the session state
eval_state_key = f"eval_{current_review['id']}"
if eval_state_key in st.session_state:
eval_res = st.session_state[eval_state_key]
st.markdown("---")
st.subheader("AI Evaluation Feedback")
is_correct = eval_res.get('is_correct', False)
if is_correct:
st.success("β
Solution Approved! You parsed the constraints and code structure correctly.")
else:
st.error("β Solution struggled. Keep reviewing the paradigm.")
st.write(f"**Detected Complexity**: Time: `{eval_res.get('detected_complexity', {}).get('time', 'N/A')}` | Space: `{eval_res.get('detected_complexity', {}).get('space', 'N/A')}`")
if eval_res.get('bugs'):
st.warning("**Bugs / Code Issues Identified**:")
for bug in eval_res['bugs']:
st.markdown(f"- {bug}")
else:
st.write("β¨ No logic bugs detected!")
if eval_res.get('key_suggestion'):
st.info(f"π‘ **Key Suggestion**: {eval_res['key_suggestion']}")
if st.button("Next Card β‘οΈ", type="primary", use_container_width=True):
rating = "Correct" if is_correct else "Incorrect"
handle_review_completion(current_review, streak_data, rating)
del st.session_state[eval_state_key]
st.rerun()
if st.button("π Try Again (Clear Feedback)", use_container_width=True):
del st.session_state[eval_state_key]
st.rerun()
return
mode = st.radio("Review Mode", ["AI Review", "Self-Grade (Offline)"], horizontal=True)
if mode == "AI Review":
code = st.text_area("Write your code here from memory:", height=300)
if st.button("Submit Code for Review", type="primary", use_container_width=True):
if not code.strip():
st.warning("Please type your solution before submitting.")
return
with st.spinner("Evaluating with Gemini AI..."):
eval_res = llm_reviewer.evaluate_code(
problem['name'],
problem['pattern'],
problem['optimal_time'],
problem['optimal_space'],
code
)
if "error" in eval_res:
st.error(f"Error calling AI: {eval_res['error']}")
else:
st.session_state[eval_state_key] = eval_res
st.rerun()
else:
st.warning("Self-Grade Mode Active. Evaluate yourself truthfully.")
with st.expander("Show Reference Code"):
st.code(problem.get('reference_code', 'No reference code available.'), language="python")
col1, col2, col3 = st.columns(3)
if col1.button("Forgot (Box 1)", use_container_width=True):
handle_review_completion(current_review, streak_data, "Incorrect")
st.rerun()
if col2.button("Mixed (Same Box)", use_container_width=True):
handle_review_completion(current_review, streak_data, "Mixed")
st.rerun()
if col3.button("Smashed It (+1 Box)", type="primary", use_container_width=True):
handle_review_completion(current_review, streak_data, "Correct")
st.rerun()
if __name__ == "__main__":
main()