Spaces:
Sleeping
Sleeping
| 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""" | |
| <script> | |
| setTimeout(function() {{ | |
| window.parent.location.reload(); | |
| }}, {delay_ms}); | |
| </script> | |
| """, | |
| 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 | |
| ] | |
| 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 = """ | |
| <style> | |
| .journey-wrapper { | |
| display: flex; | |
| justify-content: center; | |
| width: 100%; | |
| padding: 20px 0; | |
| } | |
| .journey-container { | |
| position: relative; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| width: 100%; | |
| max-width: 450px; | |
| padding: 40px 0; | |
| } | |
| .journey-line { | |
| position: absolute; | |
| top: 0; | |
| bottom: 0; | |
| width: 6px; | |
| background: rgba(128, 128, 128, 0.2); | |
| z-index: 1; | |
| border-radius: 3px; | |
| } | |
| .journey-node { | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 50%; | |
| font-weight: bold; | |
| font-size: 14px; | |
| text-decoration: none !important; | |
| z-index: 2; | |
| margin: 15px 0; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15); | |
| transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); | |
| border: 3px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .journey-node.completed { | |
| background: linear-gradient(135deg, #10b981, #f59e0b); | |
| color: #ffffff !important; | |
| box-shadow: 0 0 15px rgba(245, 158, 11, 0.4), 0 4px 6px rgba(0, 0, 0, 0.2); | |
| } | |
| .journey-node.uncompleted { | |
| background: rgba(128, 128, 128, 0.15); | |
| color: rgba(255, 255, 255, 0.6) !important; | |
| } | |
| .journey-node:hover { | |
| transform: scale(1.15); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3); | |
| z-index: 10; | |
| } | |
| .journey-node.completed:hover { | |
| background: linear-gradient(135deg, #059669, #d97706); | |
| box-shadow: 0 0 20px rgba(217, 119, 6, 0.6); | |
| } | |
| .journey-node.uncompleted:hover { | |
| background: rgba(128, 128, 128, 0.3); | |
| color: #ffffff !important; | |
| } | |
| .journey-node::after { | |
| content: attr(data-tooltip); | |
| position: absolute; | |
| bottom: 125%; | |
| left: 50%; | |
| transform: translateX(-50%) scale(0.9); | |
| background: #1e293b; | |
| color: #f8fafc; | |
| padding: 8px 12px; | |
| border-radius: 8px; | |
| font-size: 12px; | |
| font-weight: 500; | |
| width: 220px; | |
| text-align: center; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: all 0.18s cubic-bezier(0.4, 0, 0.2, 1); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3); | |
| z-index: 100; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .journey-node:hover::after { | |
| opacity: 1; | |
| transform: translateX(-50%) scale(1); | |
| } | |
| </style> | |
| """ | |
| html = f"{css}<div class='journey-wrapper'><div class='journey-container'><div class='journey-line'></div>" | |
| 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'<a href="?view_num={num}" target="_self" class="journey-node {status_class}" data-tooltip="{tooltip_text}" style="position: relative; left: {offset}px;">{num}</a>' | |
| html += "</div></div>" | |
| 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("<h2 style='text-align: center;'>Welcome to AlgoSpaced π§ </h2>", unsafe_allow_html=True) | |
| st.markdown("<p style='text-align: center; color: #888;'>A cloud-syncing Spaced Repetition system for mastering LeetCode algorithmic paradigms.</p>", 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'<meta http-equiv="refresh" content="0; url={oauth_url}">', unsafe_allow_html=True) | |
| except Exception as e: | |
| st.error(f"Google login failed: {e}") | |
| st.markdown("<div style='text-align: center; margin: 15px 0; color: #666;'>β OR β</div>", 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""" | |
| <div style="background-color: #2e7b32; padding: 10px; border-radius: 5px; text-align: center; color: white; margin-bottom: 20px;"> | |
| <strong>π₯ Current Streak: {streak_data['current_streak']} Days | Longest: {streak_data['longest_streak']} Days</strong> | |
| </div> | |
| """, | |
| 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() | |