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()