SMART_TUTOR / app.py
YazeedBinShihah's picture
Update app.py
5e0d041 verified
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:
@staticmethod
def get_token():
return None
@staticmethod
def save_token(token):
pass
@staticmethod
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,
)