Spaces:
Sleeping
Sleeping
| import sys | |
| try: | |
| import audioop | |
| except ImportError: | |
| # Mocking audioop for Python 3.13 compatibility | |
| class MockAudioop: | |
| def getsample(self, *args, **kwargs): | |
| return 0 | |
| def max(self, *args, **kwargs): | |
| return 0 | |
| def minmax(self, *args, **kwargs): | |
| return (0, 0) | |
| def avg(self, *args, **kwargs): | |
| return 0 | |
| sys.modules["audioop"] = MockAudioop() | |
| # Fix for Hugging Face Hub HfFolder disappearance in Gradio | |
| import huggingface_hub | |
| if not hasattr(huggingface_hub, "HfFolder"): | |
| class MockHfFolder: | |
| def get_token(): | |
| return None | |
| def save_token(token): | |
| pass | |
| def delete_token(): | |
| pass | |
| huggingface_hub.HfFolder = MockHfFolder | |
| import gradio as gr | |
| import os | |
| import json | |
| import re | |
| # Ensure the current directory is in the path | |
| current_dir = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.append(current_dir) | |
| from smart_tutor_core import crew | |
| # ---------------------------------------------------------------------- | |
| # Helper: Parse Output | |
| # ---------------------------------------------------------------------- | |
| def parse_agent_output(raw_output: str): | |
| """ | |
| Tries to parse JSON from the raw string output. | |
| Returns (data_dict, is_json). | |
| """ | |
| data = None | |
| try: | |
| data = json.loads(raw_output) | |
| return data, True | |
| except json.JSONDecodeError: | |
| # Try finding JSON block | |
| match = re.search(r"(\{.*\})", raw_output, re.DOTALL) | |
| if match: | |
| try: | |
| data = json.loads(match.group(1)) | |
| return data, True | |
| except: | |
| pass | |
| return raw_output, False | |
| def clean_text(text: str) -> str: | |
| """ | |
| Aggressively removes markdown formatting to ensure clean text display. | |
| Removes: **bold**, __bold__, *italic*, _italic_, `code` | |
| """ | |
| if not text: | |
| return "" | |
| text = str(text) | |
| # Remove bold/italic markers | |
| text = re.sub(r"\*\*|__|`", "", text) | |
| text = re.sub(r"^\s*\*\s+", "", text) # Remove leading list asterisks if any | |
| return text.strip() | |
| # ---------------------------------------------------------------------- | |
| # Helper: Format Text Output for Display | |
| # ---------------------------------------------------------------------- | |
| def format_text_output(raw_text): | |
| """ | |
| Converts raw agent text (markdown-ish) into | |
| beautifully styled HTML inside a summary-box. | |
| """ | |
| if not raw_text: | |
| return "" | |
| text = str(raw_text).strip() | |
| # Convert markdown headings to HTML | |
| text = re.sub(r"^### (.+)$", r"<h3>\1</h3>", text, flags=re.MULTILINE) | |
| text = re.sub(r"^## (.+)$", r"<h2>\1</h2>", text, flags=re.MULTILINE) | |
| text = re.sub(r"^# (.+)$", r"<h2>\1</h2>", text, flags=re.MULTILINE) | |
| # Convert **bold** to <strong> | |
| text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text) | |
| # Convert bullet lists (- item or * item) | |
| lines = text.split("\n") | |
| result = [] | |
| in_list = False | |
| for line in lines: | |
| stripped = line.strip() | |
| is_bullet = ( | |
| stripped.startswith("- ") | |
| or stripped.startswith("* ") | |
| or re.match(r"^\d+\.\s", stripped) | |
| ) | |
| if is_bullet: | |
| if not in_list: | |
| tag = "ul" # Always use bullets as requested | |
| result.append(f"<{tag}>") | |
| in_list = tag | |
| # Remove both -/* and 1. from the start of the line | |
| content = re.sub(r"^[-*]\s+|^\d+\.\s+", "", stripped) | |
| result.append(f"<li>{content}</li>") | |
| else: | |
| if in_list: | |
| result.append(f"</{in_list}>") | |
| in_list = False | |
| if stripped.startswith("<h"): | |
| result.append(stripped) | |
| elif stripped: | |
| result.append(f"<p>{stripped}</p>") | |
| if in_list: | |
| result.append(f"</{in_list}>") | |
| html = "\n".join(result) | |
| return f"<div class='summary-box'>{html}</div>" | |
| # ---------------------------------------------------------------------- | |
| # Logic: Run Agent | |
| # ---------------------------------------------------------------------- | |
| def run_agent(file, user_text): | |
| if not user_text and not file: | |
| return ( | |
| gr.update( | |
| visible=True, | |
| value="<div class='error-box'>β οΈ Please enter a request or upload a file.</div>", | |
| ), | |
| gr.update(visible=False), # Quiz Group | |
| None, # State | |
| ) | |
| full_request = user_text | |
| # Check if user wants a quiz but didn't upload a file (common error) | |
| if "quiz" in user_text.lower() and not file: | |
| return ( | |
| gr.update( | |
| visible=True, | |
| value="<div class='error-box'>β οΈ To generate a quiz, please upload a document first.</div>", | |
| ), | |
| gr.update(visible=False), | |
| None, | |
| ) | |
| if file: | |
| # file is a filepath string because type='filepath' | |
| full_request = f"""USER REQUEST: {user_text} | |
| IMPORTANT: The file to process is located at this EXACT path: | |
| {file} | |
| You MUST use this exact path when calling tools (process_file, store_quiz, etc.).""" | |
| # SYSTEM PROMPT INJECTION to force JSON format from the agent | |
| system_instruction = "\n\n(SYSTEM NOTE: If generating a quiz, you MUST call the store_quiz tool and return its VALID JSON output including 'quiz_id'. Do NOT return just the questions text.)" | |
| try: | |
| inputs = {"user_request": full_request + system_instruction} | |
| result = crew.kickoff(inputs=inputs) | |
| raw_output = str(result) | |
| print(f"\n{'='*60}") | |
| print(f"[DEBUG] raw_output (first 500 chars):") | |
| print(raw_output[:500]) | |
| print(f"{'='*60}") | |
| data, is_json = parse_agent_output(raw_output) | |
| print(f"[DEBUG] is_json={is_json}") | |
| if is_json: | |
| print( | |
| f"[DEBUG] keys={list(data.keys()) if isinstance(data, dict) else 'not a dict'}" | |
| ) | |
| if isinstance(data, dict) and "questions" in data: | |
| print(f"[DEBUG] num questions={len(data['questions'])}") | |
| # Case 1: Quiz Output (Success) | |
| if is_json and "questions" in data: | |
| # We accept it even if quiz_id is missing, but grading might fail. | |
| return ( | |
| gr.update(visible=False), # Hide Summary | |
| gr.update(visible=True), # Show Quiz | |
| data, # Store Data | |
| ) | |
| # Case 2: Grade Result (Standard JSON from grade_quiz) - Handled nicely | |
| if is_json and "score" in data: | |
| markdown = format_grade_result(data) | |
| return ( | |
| gr.update(visible=True, value=markdown), | |
| gr.update(visible=False), | |
| None, | |
| ) | |
| # Case 3: Normal Text / Summary / Explanation | |
| html_content = format_text_output(raw_output) | |
| return ( | |
| gr.update(visible=True, value=html_content), | |
| gr.update(visible=False), | |
| None, | |
| ) | |
| except Exception as e: | |
| error_msg = f"<div class='error-box'>β Error: {str(e)}</div>" | |
| return ( | |
| gr.update(visible=True, value=error_msg), | |
| gr.update(visible=False), | |
| None, | |
| ) | |
| # ---------------------------------------------------------------------- | |
| # Logic: Quiz Render & Grading | |
| # ---------------------------------------------------------------------- | |
| def render_quiz(quiz_data): | |
| """ | |
| Renders the quiz questions dynamically. | |
| Returns updates for: [Radios x10] + [Feedbacks x10] + [CheckBtn] (Total 21) | |
| """ | |
| updates = [] | |
| if not quiz_data: | |
| # Hide everything | |
| return [gr.update(visible=False)] * 21 | |
| questions = quiz_data.get("questions", []) | |
| # 1. Update Radios (10 slots) | |
| for i in range(10): | |
| if i < len(questions): | |
| q = questions[i] | |
| q_txt = clean_text(q.get("question", "Question text missing")) | |
| question_text = f"{i+1}. {q_txt}" | |
| # Ensure options are a dict and sorted | |
| raw_options = q.get("options", {}) | |
| if not isinstance(raw_options, dict): | |
| # Fallback if options came as a list or string | |
| raw_options = {"A": "Error loading options"} | |
| # Sort by key A, B, C, D... | |
| # We strictly enforce the "Key. Value" format | |
| choices = [] | |
| for key in sorted(raw_options.keys()): | |
| val = clean_text(raw_options[key]) | |
| choices.append(f"{key}. {val}") | |
| updates.append( | |
| gr.update( | |
| visible=True, | |
| label=question_text, | |
| choices=choices, | |
| value=None, | |
| interactive=True, | |
| ) | |
| ) | |
| else: | |
| updates.append(gr.update(visible=False, choices=[], value=None)) | |
| # 2. Update Feedbacks (10 slots) - Hide them initially | |
| for i in range(10): | |
| updates.append(gr.update(visible=False, value="")) | |
| # 3. Show Grid/Check Button | |
| updates.append(gr.update(visible=True)) | |
| return updates | |
| def grade_quiz_ui(quiz_data, *args): | |
| """ | |
| Collects answers, calls agent (or tool), and returns graded results designed for UI. | |
| Input args: [Radio1_Val, Radio2_Val, ..., Radio10_Val] (Length 10) | |
| Output: [Radios x10] + [Feedbacks x10] + [ResultMsg] (Total 21) | |
| """ | |
| # args tuple contains the values of the 10 radios | |
| answers_list = args[0:10] | |
| updates = [] | |
| # Validation | |
| if not quiz_data or "quiz_id" not in quiz_data: | |
| # Fallback if ID is missing | |
| error_updates = [gr.update(visible=True)] * 10 + [gr.update(visible=False)] * 10 | |
| error_updates.append( | |
| gr.update( | |
| visible=True, | |
| value="<div class='error-box'>β οΈ Error: Quiz ID not found. Cannot grade this quiz.</div>", | |
| ) | |
| ) | |
| return error_updates | |
| quiz_id = quiz_data["quiz_id"] | |
| # Construct answer map {"1": "A", ...} | |
| user_answers = {} | |
| for i, ans in enumerate(answers_list): | |
| if ans: | |
| # ans is like "A. Option Text" -> extract "A" | |
| selected_opt = ans.split(".")[0] | |
| # Use qid from data if available, else i+1 | |
| qid = str(i + 1) | |
| # Try to match qid from quiz_data if possible | |
| if i < len(quiz_data.get("questions", [])): | |
| q = quiz_data["questions"][i] | |
| qid = str(q.get("qid", i + 1)) | |
| user_answers[qid] = selected_opt | |
| # Construct the JSON for the agent | |
| answers_json = json.dumps(user_answers) | |
| grading_request = f"Grade quiz {quiz_id} with answers {answers_json}\n(SYSTEM: Return valid JSON matching GradeQuizResult schema.)" | |
| try: | |
| inputs = {"user_request": grading_request} | |
| result = crew.kickoff(inputs=inputs) | |
| raw_output = str(result) | |
| data, is_json = parse_agent_output(raw_output) | |
| if is_json and "score" in data: | |
| return format_grade_result_interactive(data, answers_list) | |
| else: | |
| # Fallback error in result box | |
| error_updates = [gr.update(visible=True)] * 10 + [ | |
| gr.update(visible=False) | |
| ] * 10 | |
| error_updates.append( | |
| gr.update( | |
| visible=True, | |
| value=f"<div class='error-box'>Error parsing grading result: {raw_output}</div>", | |
| ) | |
| ) | |
| return error_updates | |
| except Exception as e: | |
| error_updates = [gr.update(visible=True)] * 10 + [gr.update(visible=False)] * 10 | |
| error_updates.append( | |
| gr.update( | |
| visible=True, value=f"<div class='error-box'>Error: {str(e)}</div>" | |
| ) | |
| ) | |
| return error_updates | |
| def format_grade_result_interactive(data, user_answers_list): | |
| """ | |
| Updates the UI with colors and correctness. | |
| Returns 21 updates. | |
| """ | |
| details = data.get("details", []) | |
| # Map details by QID or index for safety | |
| details_map = {} | |
| for det in details: | |
| details_map[str(det.get("qid"))] = det | |
| radio_updates = [] | |
| feedback_updates = [] | |
| # Iterate 10 slots | |
| for i in range(10): | |
| # Find corresponding detail | |
| # We assume strict ordering i=0 -> Q1 | |
| # But let's try to be smart with QID if possible | |
| qid = ( | |
| str(data.get("details", [])[i].get("qid")) | |
| if i < len(data.get("details", [])) | |
| else str(i + 1) | |
| ) | |
| det = details_map.get(qid) | |
| if det: | |
| # Clean feedback text | |
| correct_raw = det.get("correct_answer", "?") | |
| correct = clean_text(correct_raw) | |
| explanation_raw = det.get("explanation", "") | |
| explanation = clean_text(explanation_raw) | |
| is_correct = det.get("is_correct", False) | |
| # 1. Lock Radio | |
| radio_updates.append(gr.update(interactive=False)) | |
| # 2. Show Feedback Box | |
| css_class = ( | |
| "feedback-box-correct" if is_correct else "feedback-box-incorrect" | |
| ) | |
| # Title | |
| title_text = "Correct Answer!" if is_correct else "Incorrect Answer." | |
| title_icon = "β " if is_correct else "β" | |
| html_content = f""" | |
| <div class='{css_class}'> | |
| <div class='feedback-header'> | |
| <span class='feedback-icon'>{title_icon}</span> | |
| <span class='feedback-title'>{title_text}</span> | |
| </div> | |
| <div class='feedback-body'> | |
| <div class='feedback-correct-answer'><strong>Correct Answer:</strong> {correct}</div> | |
| {'<div class="feedback-explanation"><strong>Explanation:</strong> ' + explanation + '</div>' if explanation else ''} | |
| </div> | |
| </div> | |
| """ | |
| feedback_updates.append(gr.update(visible=True, value=html_content)) | |
| else: | |
| # No detail (maybe question didn't exist) | |
| radio_updates.append(gr.update(visible=False)) | |
| feedback_updates.append(gr.update(visible=False)) | |
| # 3. Final Score Msg | |
| percentage = data.get("percentage", 0) | |
| emoji = "π" if percentage >= 80 else "π" | |
| # Create a nice result card | |
| score_html = f""" | |
| <div class='result-card'> | |
| <div class='result-header'>{emoji} Quiz Completed!</div> | |
| <div class='result-score'>Your Score: {data.get('score')} / {data.get('total')}</div> | |
| <div class='result-percentage'>({percentage}%)</div> | |
| </div> | |
| """ | |
| return ( | |
| radio_updates + feedback_updates + [gr.update(visible=True, value=score_html)] | |
| ) | |
| def format_grade_result(data): | |
| """Standard markdown formatter for standalone grade result""" | |
| score = data.get("percentage", 0) | |
| emoji = "π" if score > 70 else "π" | |
| md = f"# {emoji} Score: {data.get('score')}/{data.get('total')}\n\n" | |
| for cx in data.get("details", []): | |
| md += f"- **Q{cx['qid']}**: {cx['is_correct'] and 'β ' or 'β'} (Correct: {cx.get('correct_answer')})\n" | |
| return md | |
| # ---------------------------------------------------------------------- | |
| # CSS Styling | |
| # ---------------------------------------------------------------------- | |
| custom_css = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); | |
| body { | |
| font-family: 'Poppins', sans-serif !important; | |
| background: #f8fafc; /* Lighter background */ | |
| color: #334155; | |
| font-weight: 400; /* Regular weight by default */ | |
| } | |
| .gradio-container { | |
| max-width: 900px !important; | |
| margin: 40px auto !important; | |
| background: #ffffff; | |
| border-radius: 24px; | |
| box-shadow: 0 20px 40px -10px rgba(0,0,0,0.1); | |
| padding: 0 !important; | |
| overflow: hidden; | |
| border: 1px solid rgba(255,255,255,0.8); | |
| } | |
| /* ================= HEADER ================= */ | |
| .header-box { | |
| background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); | |
| color: white; | |
| padding: 60px 40px; | |
| text-align: center; | |
| position: relative; | |
| overflow: hidden; | |
| margin-bottom: 30px; | |
| } | |
| .header-box::before { | |
| content: ''; | |
| position: absolute; | |
| top: -50%; | |
| left: -50%; | |
| width: 200%; | |
| height: 200%; | |
| background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%); | |
| animation: rotate 20s linear infinite; | |
| } | |
| .header-box h1 { | |
| color: white !important; | |
| margin: 0; | |
| font-size: 3em !important; | |
| font-weight: 700; | |
| letter-spacing: -1px; | |
| text-shadow: 0 4px 10px rgba(0,0,0,0.2); | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .header-box p { | |
| color: #e0e7ff !important; | |
| font-size: 1.25em !important; | |
| margin-top: 15px; | |
| font-weight: 300; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| @keyframes rotate { | |
| from { transform: rotate(0deg); } | |
| to { transform: rotate(360deg); } | |
| } | |
| /* ================= INPUT PANEL ================= */ | |
| .gradio-row { | |
| gap: 30px !important; | |
| padding: 0 40px 40px 40px; | |
| } | |
| /* Logic to remove padding from internal rows if needed, simplified here */ | |
| /* Buttons */ | |
| button.primary { | |
| background: linear-gradient(90deg, #4f46e5 0%, #6366f1 100%) !important; | |
| border: none !important; | |
| color: white !important; | |
| font-weight: 600 !important; | |
| padding: 12px 24px !important; | |
| border-radius: 12px !important; | |
| box-shadow: 0 4px 15px rgba(79, 70, 229, 0.4) !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| button.primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 25px rgba(79, 70, 229, 0.5) !important; | |
| } | |
| button.secondary { | |
| background: #f3f4f6 !important; | |
| color: #4b5563 !important; | |
| border: 1px solid #e5e7eb !important; | |
| border-radius: 12px !important; | |
| } | |
| button.secondary:hover { | |
| background: #e5e7eb !important; | |
| } | |
| /* ================= QUIZ CARDS ================= */ | |
| .quiz-question { | |
| background: #ffffff; | |
| border-radius: 16px; | |
| padding: 25px; | |
| margin-bottom: 30px !important; | |
| border: 1px solid #e5e7eb; | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.03), 0 4px 6px -2px rgba(0, 0, 0, 0.02); | |
| transition: transform 0.2s ease, box-shadow 0.2s ease; | |
| } | |
| .quiz-question:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 10px 10px -5px rgba(0, 0, 0, 0.02); | |
| } | |
| .quiz-question span { /* Label/Title */ | |
| font-size: 1.15em !important; | |
| font-weight: 600 !important; | |
| color: #111827; | |
| margin-bottom: 20px; | |
| display: block; | |
| line-height: 1.5; | |
| } | |
| /* Options Wrapper (The Radio Group) */ | |
| .quiz-question .wrap { | |
| display: flex !important; | |
| flex-direction: column !important; | |
| gap: 12px !important; | |
| } | |
| /* Individual Option Label */ | |
| .quiz-question .wrap label { | |
| display: flex !important; | |
| align-items: center !important; | |
| background: #f9fafb; | |
| border: 2px solid #e5e7eb !important; /* Thick border */ | |
| padding: 15px 20px !important; | |
| border-radius: 12px !important; | |
| cursor: pointer; | |
| transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| font-size: 1.05em; | |
| color: #4b5563; | |
| } | |
| .quiz-question .wrap label:hover { | |
| background: #f3f4f6; | |
| border-color: #6366f1 !important; | |
| color: #4f46e5; | |
| } | |
| .quiz-question .wrap label.selected { | |
| background: #eef2ff !important; | |
| border-color: #4f46e5 !important; | |
| color: #4338ca !important; | |
| font-weight: 600; | |
| box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.1); | |
| } | |
| /* Hide default circle if possible, or style it. | |
| Gradio's radio inputs are tricky to hide fully without breaking accessibility, | |
| but we can style the container enough. */ | |
| /* ================= RESULTS & FEEDBACK ================= */ | |
| /* Success/Error Cards */ | |
| .feedback-box-correct, .feedback-box-incorrect { | |
| margin-top: 20px; | |
| padding: 20px; | |
| border-radius: 12px; | |
| animation: popIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .feedback-box-correct { | |
| background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%); | |
| border: 1px solid #10b981; | |
| color: #065f46; | |
| } | |
| .feedback-box-incorrect { | |
| background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%); | |
| border: 1px solid #ef4444; | |
| color: #991b1b; | |
| } | |
| .feedback-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 12px; | |
| font-size: 1.2em; | |
| font-weight: 700; | |
| } | |
| .feedback-icon { | |
| font-size: 1.4em; | |
| background: rgba(255,255,255,0.5); | |
| border-radius: 50%; | |
| width: 32px; | |
| height: 32px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.05); | |
| } | |
| .feedback-body { | |
| background: rgba(255,255,255,0.4); | |
| padding: 15px; | |
| border-radius: 8px; | |
| font-size: 1em; | |
| line-height: 1.6; | |
| } | |
| .feedback-correct-answer { | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| color: #064e3b; /* darker green */ | |
| } | |
| .feedback-box-incorrect .feedback-correct-answer { | |
| color: #7f1d1d; /* darker red */ | |
| } | |
| /* Summary / Explanation Box */ | |
| .summary-box { | |
| background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%); | |
| border-radius: 20px; | |
| padding: 35px 40px; | |
| border: 1px solid #e0e7ff; | |
| box-shadow: 0 8px 30px rgba(79, 70, 229, 0.06); | |
| font-size: 1.05em; | |
| line-height: 1.9; | |
| color: #374151; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .summary-box::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 4px; | |
| background: linear-gradient(90deg, #4f46e5, #7c3aed, #a78bfa); | |
| } | |
| .summary-box h2 { | |
| font-size: 1.4em; | |
| font-weight: 700; | |
| color: #312e81; | |
| margin: 0 0 18px 0; | |
| padding-bottom: 12px; | |
| border-bottom: 2px solid #e0e7ff; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .summary-box h3 { | |
| font-size: 1.15em; | |
| font-weight: 600; | |
| color: #4338ca; | |
| margin: 20px 0 10px 0; | |
| } | |
| .summary-box p { | |
| margin: 0 0 14px 0; | |
| text-align: justify; | |
| } | |
| .summary-box ul, .summary-box ol { | |
| margin: 10px 0 16px 0; | |
| padding-left: 24px; | |
| } | |
| .summary-box li { | |
| margin-bottom: 8px; | |
| position: relative; | |
| } | |
| .summary-box strong { | |
| color: #312e81; | |
| font-weight: 600; | |
| } | |
| .summary-box .summary-footer { | |
| margin-top: 20px; | |
| padding-top: 14px; | |
| border-top: 1px solid #e0e7ff; | |
| font-size: 0.85em; | |
| color: #9ca3af; | |
| text-align: left; | |
| } | |
| /* Example Buttons */ | |
| #examples-container { | |
| margin: 15px 0; | |
| padding: 10px; | |
| background: #f3f4f6; | |
| border-radius: 12px; | |
| } | |
| .example-btn { | |
| background: #ffffff !important; | |
| border: 1px solid #e5e7eb !important; | |
| color: #6366f1 !important; /* Indigo text */ | |
| font-size: 0.85em !important; | |
| padding: 2px 10px !important; | |
| border-radius: 20px !important; /* Pill shape */ | |
| transition: all 0.2s ease !important; | |
| font-weight: 500 !important; | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important; | |
| } | |
| .example-btn:hover { | |
| background: #f5f7ff !important; | |
| border-color: #6366f1 !important; | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 6px -1px rgba(99, 102, 241, 0.1) !important; | |
| } | |
| /* Result Card */ | |
| .result-card { | |
| background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); | |
| border-radius: 20px; | |
| padding: 40px; | |
| text-align: center; | |
| color: white; | |
| box-shadow: 0 20px 25px -5px rgba(79, 70, 229, 0.3); | |
| margin-top: 40px; | |
| animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1); | |
| } | |
| .result-header { | |
| font-size: 2em; | |
| font-weight: 800; | |
| margin-bottom: 15px; | |
| text-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .result-score { | |
| font-size: 3.5em; | |
| font-weight: 800; | |
| margin: 10px 0; | |
| background: -webkit-linear-gradient(#ffffff, #e0e7ff); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .result-percentage { | |
| font-size: 1.5em; | |
| opacity: 0.9; | |
| font-weight: 500; | |
| } | |
| /* Keyframes */ | |
| @keyframes popIn { | |
| from { opacity: 0; transform: scale(0.95) translateY(-5px); } | |
| to { opacity: 1; transform: scale(1) translateY(0); } | |
| } | |
| @keyframes slideUp { | |
| from { opacity: 0; transform: translateY(40px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| /* Hide Gradio Footer */ | |
| footer { display: none !important; } | |
| .gradio-container .prose.footer-content { display: none !important; } | |
| """ | |
| # ---------------------------------------------------------------------- | |
| # Main App | |
| # ---------------------------------------------------------------------- | |
| with gr.Blocks(css=custom_css, title="SmartTutor AI") as demo: | |
| # State | |
| quiz_state = gr.State(None) | |
| with gr.Column(elem_classes="header-box"): | |
| gr.HTML( | |
| """ | |
| <div style='color: white;'> | |
| <h1 style='color: white; font-size: 3em; margin: 0;'>π§ SmartTutor AI</h1> | |
| <p style='color: #e0e7ff; font-size: 1.25em;'>Your intelligent companion for learning and assessment</p> | |
| </div> | |
| """ | |
| ) | |
| with gr.Row(): | |
| # Left Panel: Controls | |
| with gr.Column(scale=1, variant="panel"): | |
| file_input = gr.File( | |
| label="π Upload Document", file_types=[".pdf", ".txt"], type="filepath" | |
| ) | |
| user_input = gr.Textbox( | |
| label="βοΈ Request", | |
| placeholder="e.g. 'Summarize this' or 'Create a quiz'", | |
| lines=3, | |
| ) | |
| # Quick Examples | |
| with gr.Column(elem_id="examples-container"): | |
| gr.Markdown("β¨ **Quick Actions:**") | |
| with gr.Row(): | |
| ex_summarize = gr.Button( | |
| "π Summary (3 lines)", size="sm", elem_classes="example-btn" | |
| ) | |
| ex_quiz = gr.Button( | |
| "π§ͺ 3 Questions", size="sm", elem_classes="example-btn" | |
| ) | |
| with gr.Row(): | |
| ex_explain = gr.Button( | |
| "π‘ Main Concepts", size="sm", elem_classes="example-btn" | |
| ) | |
| with gr.Row(): | |
| submit_btn = gr.Button("π Run", variant="primary") | |
| clear_btn = gr.Button("π§Ή Clear") | |
| # Right Panel: Results | |
| with gr.Column(scale=2): | |
| # 1. Summary / Text Output | |
| summary_output = gr.HTML(visible=True) | |
| # 2. Quiz Group (Hidden initially) | |
| with gr.Group(visible=False) as quiz_group: | |
| gr.Markdown("## π Quiz Time") | |
| gr.Markdown("Select the correct answer for each question.") | |
| # Create 10 Questions + Feedback slots | |
| q_radios = [] | |
| q_feedbacks = [] | |
| for i in range(10): | |
| # Radio | |
| r = gr.Radio( | |
| label=f"Question {i+1}", | |
| visible=False, | |
| elem_classes="quiz-question", | |
| ) | |
| q_radios.append(r) | |
| # Feedback (Markdown/HTML) | |
| fb = gr.HTML(visible=False) | |
| q_feedbacks.append(fb) | |
| check_btn = gr.Button( | |
| "β Check Answers", variant="primary", visible=False | |
| ) | |
| # Final Result Message | |
| quiz_result_msg = gr.Markdown(visible=False) | |
| # ------------------------------------------------------------------ | |
| # Events | |
| # ------------------------------------------------------------------ | |
| # 1. Run Agent | |
| # Returns: [Summary, QuizGroup, QuizState] | |
| submit_btn.click( | |
| fn=run_agent, | |
| inputs=[file_input, user_input], | |
| outputs=[summary_output, quiz_group, quiz_state], | |
| ).success( | |
| # On success, update the quiz UI components (21 items) | |
| fn=render_quiz, | |
| inputs=[quiz_state], | |
| outputs=q_radios + q_feedbacks + [check_btn], | |
| ) | |
| # Example Buttons Handling | |
| # These will ONLY fill the text box. User must click 'Run' manually. | |
| ex_summarize.click( | |
| fn=lambda: "Summarize this document strictly in exactly 3 lines.", | |
| outputs=[user_input], | |
| ) | |
| ex_quiz.click( | |
| fn=lambda: "Generate a quiz with exactly 3 multiple-choice questions.", | |
| outputs=[user_input], | |
| ) | |
| ex_explain.click( | |
| fn=lambda: "Explain the 5 most important core concepts in this document clearly.", | |
| outputs=[user_input], | |
| ) | |
| # 2. Check Answers | |
| # Inputs: State + 10 Radios | |
| # Outputs: 10 Radios (Lock) + 10 Feedbacks (Show) + ResultMsg | |
| check_btn.click( | |
| fn=grade_quiz_ui, | |
| inputs=[quiz_state] + q_radios, | |
| outputs=q_radios + q_feedbacks + [quiz_result_msg], | |
| ) | |
| # 3. Clear | |
| def reset_ui(): | |
| # Reset everything to default | |
| updates = [ | |
| gr.update(value=None, interactive=True, visible=False) | |
| ] * 10 # Radios | |
| fb_updates = [gr.update(value="", visible=False)] * 10 # Feedbacks | |
| return ( | |
| None, | |
| "", # Inputs | |
| gr.update(value="", visible=True), # Summary | |
| gr.update(visible=False), # Quiz Group | |
| None, # State | |
| *updates, | |
| *fb_updates, # Radios + Feedbacks | |
| gr.update(visible=False), # CheckBtn | |
| gr.update(visible=False), # ResultMsg | |
| ) | |
| clear_btn.click( | |
| fn=reset_ui, | |
| inputs=[], | |
| outputs=[file_input, user_input, summary_output, quiz_group, quiz_state] | |
| + q_radios | |
| + q_feedbacks | |
| + [check_btn, quiz_result_msg], | |
| ) | |
| if __name__ == "__main__": | |
| print("Starting SmartTutor AI on Hugging Face...") | |
| demo.queue() | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False, | |
| show_api=False, | |
| ) | |