Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,634 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import os
|
| 3 |
+
import requests
|
| 4 |
+
from PIL import Image
|
| 5 |
+
from typing import Optional, Tuple, List, Dict
|
| 6 |
+
import gradio as gr
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
import google.generativeai as genai
|
| 9 |
+
import pdfplumber
|
| 10 |
+
import tempfile
|
| 11 |
+
from fpdf import FPDF
|
| 12 |
+
import re # Import regex for parsing quiz
|
| 13 |
+
|
| 14 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
+
# Load environment variables & Constants
|
| 16 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 17 |
+
load_dotenv()
|
| 18 |
+
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
|
| 19 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 20 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 21 |
+
|
| 22 |
+
MISTRAL_MODEL = "mistralai/mistral-7b-instruct"
|
| 23 |
+
FLUX_MODEL_API = "https://api-inference.huggingface.co/models/black-forest-labs/FLUX.1-schnell"
|
| 24 |
+
OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"
|
| 25 |
+
GEMINI_MODEL_NAME = "gemini-1.5-flash"
|
| 26 |
+
PDF_TEXT_LIMIT = 8000
|
| 27 |
+
|
| 28 |
+
if not all([OPENROUTER_API_KEY, HF_TOKEN, GEMINI_API_KEY]):
|
| 29 |
+
raise ValueError("β Missing one or more required environment variables.")
|
| 30 |
+
|
| 31 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 32 |
+
# Configure Gemini
|
| 33 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
+
genai.configure(api_key=GEMINI_API_KEY)
|
| 35 |
+
gemini_model = genai.GenerativeModel(GEMINI_MODEL_NAME)
|
| 36 |
+
|
| 37 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 38 |
+
# Prompt Templates
|
| 39 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 40 |
+
LEVEL_PROMPTS = {
|
| 41 |
+
"Kid": "Explain like I'm 7 years old: ",
|
| 42 |
+
"Beginner": "Explain in simple terms: ",
|
| 43 |
+
"Advanced": "Explain in technical detail: "
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 47 |
+
# UI HTML & CSS Styling
|
| 48 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 49 |
+
LOTTIE_ANIMATION_HTML = """
|
| 50 |
+
<div style="text-align: center; margin-bottom: 20px;">
|
| 51 |
+
<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>
|
| 52 |
+
<lottie-player src="https://assets5.lottiefiles.com/packages/lf20_M9p23l.json"
|
| 53 |
+
background="transparent" speed="1" style="width: 200px; height: 200px; margin: auto;"
|
| 54 |
+
loop autoplay></lottie-player>
|
| 55 |
+
</div>
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
# Combined CSS for dark mode, hover effect, and accordion styling
|
| 59 |
+
APP_CSS = """
|
| 60 |
+
body.dark {
|
| 61 |
+
--body-background-fill: #121212;
|
| 62 |
+
--background-fill-primary: #1E1E1E;
|
| 63 |
+
--background-fill-secondary: #2C2C2C;
|
| 64 |
+
--text-color-primary: #FFFFFF;
|
| 65 |
+
--text-color-secondary: #E0E0E0;
|
| 66 |
+
--border-color-primary: #333333;
|
| 67 |
+
--input-background-fill: #2C2C2C;
|
| 68 |
+
--button-secondary-background-fill: #333333;
|
| 69 |
+
--button-secondary-text-color: #FFFFFF;
|
| 70 |
+
--button-primary-background-fill: #6d28d9; /* Purple */
|
| 71 |
+
--button-primary-border-color: #6d28d9; /* Purple */
|
| 72 |
+
--button-primary-text-color: #FFFFFF;
|
| 73 |
+
}
|
| 74 |
+
.dark .gradio-container { background: var(--body-background-fill); }
|
| 75 |
+
.dark .gradio-tabs-nav { background: var(--background-fill-secondary); }
|
| 76 |
+
|
| 77 |
+
/* Button Hover Effect */
|
| 78 |
+
button:hover {
|
| 79 |
+
transition: all 0.3s ease;
|
| 80 |
+
transform: scale(1.01);
|
| 81 |
+
box-shadow: 0px 0px 8px rgba(255, 255, 255, 0.1);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* Style for Accordions */
|
| 85 |
+
/* Adjusting background and text color for accordions in both modes */
|
| 86 |
+
.gradio-accordion.secondary > .label {
|
| 87 |
+
background-color: var(--background-fill-primary); /* Use primary background for label */
|
| 88 |
+
color: var(--text-color-primary);
|
| 89 |
+
border-color: var(--border-color-primary);
|
| 90 |
+
}
|
| 91 |
+
.gradio-accordion.secondary > .label:hover {
|
| 92 |
+
background-color: var(--background-fill-secondary); /* Slightly change on hover */
|
| 93 |
+
}
|
| 94 |
+
.gradio-accordion.secondary > .label > .icon {
|
| 95 |
+
color: var(--text-color-primary);
|
| 96 |
+
}
|
| 97 |
+
.gradio-accordion.secondary > .panel {
|
| 98 |
+
background-color: var(--background-fill-secondary); /* Secondary background for panel content */
|
| 99 |
+
border-color: var(--border-color-primary);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/* Style for Dropdowns inside Accordions */
|
| 103 |
+
.gradio-accordion .gr-form { /* Target forms/inputs inside accordion panels */
|
| 104 |
+
background-color: var(--background-fill-secondary); /* Ensure consistent background */
|
| 105 |
+
}
|
| 106 |
+
.gradio-accordion .gr-dropdown-value {
|
| 107 |
+
color: var(--text-color-primary); /* Set text color for dropdown */
|
| 108 |
+
}
|
| 109 |
+
.gradio-accordion .gr-dropdown-options {
|
| 110 |
+
background-color: var(--background-fill-secondary); /* Options background */
|
| 111 |
+
border-color: var(--border-color-primary);
|
| 112 |
+
}
|
| 113 |
+
.gradio-accordion .gr-dropdown-options .gr-dropdown-option {
|
| 114 |
+
color: var(--text-color-primary); /* Options text color */
|
| 115 |
+
}
|
| 116 |
+
.gradio-accordion .gr-dropdown-options .gr-dropdown-option:hover {
|
| 117 |
+
background-color: var(--background-fill-primary); /* Option hover background */
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* Style for feedback text */
|
| 121 |
+
#quiz_feedback_box {
|
| 122 |
+
margin-top: 15px; /* Add space above feedback */
|
| 123 |
+
padding: 10px;
|
| 124 |
+
border: 1px solid var(--border-color-primary);
|
| 125 |
+
border-radius: 5px;
|
| 126 |
+
}
|
| 127 |
+
#quiz_feedback_box.correct {
|
| 128 |
+
border-color: green;
|
| 129 |
+
color: green;
|
| 130 |
+
}
|
| 131 |
+
#quiz_feedback_box.incorrect {
|
| 132 |
+
border-color: red;
|
| 133 |
+
color: red;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
"""
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 141 |
+
# API Calls
|
| 142 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 143 |
+
def generate_mistral_response(prompt: str, system_message: str) -> str:
|
| 144 |
+
headers = {"Authorization": f"Bearer {OPENROUTER_API_KEY}"}
|
| 145 |
+
payload = {
|
| 146 |
+
"model": MISTRAL_MODEL,
|
| 147 |
+
"messages": [{"role": "system", "content": system_message}, {"role": "user", "content": prompt}],
|
| 148 |
+
"temperature": 0.7
|
| 149 |
+
}
|
| 150 |
+
try:
|
| 151 |
+
response = requests.post(OPENROUTER_API_URL, headers=headers, json=payload, timeout=45)
|
| 152 |
+
response.raise_for_status()
|
| 153 |
+
return response.json()["choices"][0]["message"]["content"].strip()
|
| 154 |
+
except requests.exceptions.RequestException as e:
|
| 155 |
+
return f"β Mistral Network Error: {e}"
|
| 156 |
+
except (KeyError, IndexError):
|
| 157 |
+
return "β Mistral API Error: Unexpected response format."
|
| 158 |
+
except Exception as e:
|
| 159 |
+
return f"β Mistral Error: {e}"
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def generate_diagram_image(prompt: str) -> Optional[str]:
|
| 163 |
+
try:
|
| 164 |
+
response = requests.post(
|
| 165 |
+
FLUX_MODEL_API,
|
| 166 |
+
headers={"Authorization": f"Bearer {HF_TOKEN}"},
|
| 167 |
+
json={"inputs": prompt},
|
| 168 |
+
timeout=60
|
| 169 |
+
)
|
| 170 |
+
response.raise_for_status()
|
| 171 |
+
image = Image.open(io.BytesIO(response.content))
|
| 172 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
|
| 173 |
+
image.save(temp_file.name, "PNG")
|
| 174 |
+
return temp_file.name
|
| 175 |
+
except requests.exceptions.RequestException as e:
|
| 176 |
+
print(f"β FLUX Network Error: {e}")
|
| 177 |
+
return None
|
| 178 |
+
except Exception as e:
|
| 179 |
+
print(f"β FLUX Error: {e}")
|
| 180 |
+
return None
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def gemini_explain_file(file, question: Optional[str] = None) -> str:
|
| 184 |
+
if not file: return "β οΈ No file uploaded."
|
| 185 |
+
try:
|
| 186 |
+
file_path = file if isinstance(file, str) else file.name
|
| 187 |
+
|
| 188 |
+
if file_path.lower().endswith((".png", ".jpg", ".jpeg")):
|
| 189 |
+
img = Image.open(file_path)
|
| 190 |
+
prompt = f"Explain the science in this image. If there's a specific question, address it: {question}" if question else "Explain the science in this image."
|
| 191 |
+
response = gemini_model.generate_content([prompt, img])
|
| 192 |
+
return response.text
|
| 193 |
+
elif file_path.lower().endswith(".pdf"):
|
| 194 |
+
with pdfplumber.open(file_path) as pdf:
|
| 195 |
+
text = "\n".join(page.extract_text() or "" for page in pdf.pages)
|
| 196 |
+
prompt = f"Explain the science in this PDF, focusing on this question: {question}\n\nPDF Content:\n{text[:PDF_TEXT_LIMIT]}" if question else f"Summarize and explain the science in this PDF:\n\n{text[:PDF_TEXT_LIMIT]}"
|
| 197 |
+
response = gemini_model.generate_content(prompt)
|
| 198 |
+
return response.text
|
| 199 |
+
else:
|
| 200 |
+
return "β οΈ Unsupported file type."
|
| 201 |
+
except Exception as e:
|
| 202 |
+
return f"β Gemini Error: {e}"
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 206 |
+
# Feature Functions
|
| 207 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 208 |
+
def generate_quiz(explanation_text: str) -> str:
|
| 209 |
+
"""Generates a 2-question quiz based on the explanation."""
|
| 210 |
+
if not explanation_text or "β" in explanation_text or "β οΈ" in explanation_text:
|
| 211 |
+
return "Quiz could not be generated based on the explanation."
|
| 212 |
+
|
| 213 |
+
# Prompt to ensure clear formatting for parsing, including the marker
|
| 214 |
+
system_message = "You are a quiz creator. Based on the provided text, create a simple 2-question multiple-choice quiz. For each question, provide 3-4 options. Clearly indicate the correct answer by putting '(Correct answer)' immediately after it. Format each question as follows: 'Question #: [Question Text]\nA) Option A\nB) Option B\nC) Option C (Correct answer)\nD) Option D'. Put a blank line between questions. Do not include introductory or concluding remarks or explanations for the answers."
|
| 215 |
+
prompt = f"Create a 2-question multiple-choice quiz from this explanation:\n\n{explanation_text[:PDF_TEXT_LIMIT*2]}"
|
| 216 |
+
quiz_result = generate_mistral_response(prompt, system_message)
|
| 217 |
+
|
| 218 |
+
if "β" in quiz_result:
|
| 219 |
+
return f"Could not generate quiz: {quiz_result}"
|
| 220 |
+
return quiz_result
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def parse_quiz_text_for_dropdown(quiz_text: str) -> Tuple[List[Dict[str, any]], List[str]]:
|
| 224 |
+
"""
|
| 225 |
+
Parses AI quiz text. Returns a list of question data (label, choices)
|
| 226 |
+
and a list of *clean* correct answers (without the marker) for state.
|
| 227 |
+
"""
|
| 228 |
+
questions_data = []
|
| 229 |
+
correct_answers_clean = [] # To store correct answers without marker
|
| 230 |
+
|
| 231 |
+
question_blocks = quiz_text.strip().split('\n\n')
|
| 232 |
+
|
| 233 |
+
for raw_block in question_blocks[:2]: # Process up to 2 questions
|
| 234 |
+
if not raw_block.strip(): continue
|
| 235 |
+
|
| 236 |
+
lines = raw_block.strip().split('\n')
|
| 237 |
+
if not lines: continue
|
| 238 |
+
|
| 239 |
+
q_text_line = lines[0].strip()
|
| 240 |
+
q_text_match = re.match(r'^\s*Question\s*\d+:\s*(.*)$', q_text_line, flags=re.IGNORECASE)
|
| 241 |
+
q_text = q_text_match.group(1).strip() if q_text_match else q_text_line
|
| 242 |
+
|
| 243 |
+
options_for_dropdown = [] # Options without the marker
|
| 244 |
+
correct_option_clean = None # Clean correct answer for state
|
| 245 |
+
|
| 246 |
+
for line in lines[1:]:
|
| 247 |
+
line = line.strip()
|
| 248 |
+
if not line: continue
|
| 249 |
+
# Check if it looks like an option line (starts with A), capture text before marker
|
| 250 |
+
option_match = re.match(r'^[A-Z]\)\s*(.*?)(?:\s*\(Correct answer\))?\s*$', line, flags=re.IGNORECASE)
|
| 251 |
+
if option_match:
|
| 252 |
+
option_text_clean = option_match.group(1).strip()
|
| 253 |
+
options_for_dropdown.append(option_text_clean) # Add clean text to dropdown options
|
| 254 |
+
|
| 255 |
+
# Check if this is the correct answer
|
| 256 |
+
if "(Correct answer)" in line:
|
| 257 |
+
correct_option_clean = option_text_clean # Store the clean correct answer
|
| 258 |
+
|
| 259 |
+
if q_text and options_for_dropdown:
|
| 260 |
+
questions_data.append({
|
| 261 |
+
"label": q_text,
|
| 262 |
+
"choices": options_for_dropdown # Use the cleaned options for dropdown
|
| 263 |
+
})
|
| 264 |
+
# Store the clean correct answer, ensure it corresponds to this question
|
| 265 |
+
correct_answers_clean.append(correct_option_clean if correct_option_clean else "No correct answer found")
|
| 266 |
+
else:
|
| 267 |
+
print(f"Warning: Could not fully parse quiz block for dropdowns: {raw_block[:100]}...")
|
| 268 |
+
correct_answers_clean.append("Error parsing question") # Add placeholder
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
# Ensure we always return exactly two correct answers (fill with None if fewer than 2 questions)
|
| 272 |
+
while len(correct_answers_clean) < 2:
|
| 273 |
+
correct_answers_clean.append(None)
|
| 274 |
+
|
| 275 |
+
return questions_data, correct_answers_clean[:2] # Return parsed data and the list of correct answers
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
# CHANGED: Grade quiz function takes user answers and state values
|
| 279 |
+
def grade_quiz(q1_answer_user: str, q2_answer_user: str, correct_q1_stored: str, correct_q2_stored: str) -> Tuple[str, gr.update]:
|
| 280 |
+
"""Grades the user's answers based on the stored correct answers."""
|
| 281 |
+
feedback_lines = ["### Quiz Feedback"]
|
| 282 |
+
total_correct = 0
|
| 283 |
+
graded_count = 0
|
| 284 |
+
|
| 285 |
+
# Grade Question 1
|
| 286 |
+
if correct_q1_stored is not None and "Error" not in str(correct_q1_stored):
|
| 287 |
+
graded_count += 1
|
| 288 |
+
if q1_answer_user is None:
|
| 289 |
+
feedback_lines.append(f"β οΈ Question 1: No answer selected. Correct was: '{correct_q1_stored}'")
|
| 290 |
+
elif q1_answer_user == correct_q1_stored:
|
| 291 |
+
feedback_lines.append(f"β
Question 1: Correct!")
|
| 292 |
+
total_correct += 1
|
| 293 |
+
else:
|
| 294 |
+
feedback_lines.append(f"β Question 1: Incorrect. Your answer: '{q1_answer_user}'. Correct was: '{correct_q1_stored}'")
|
| 295 |
+
elif q1_answer_user: # User selected an answer but question/correct answer wasn't parsed
|
| 296 |
+
feedback_lines.append(f"β οΈ Question 1: Answer selected, but correct answer could not be determined.")
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
# Grade Question 2
|
| 300 |
+
if correct_q2_stored is not None and "Error" not in str(correct_q2_stored):
|
| 301 |
+
graded_count += 1
|
| 302 |
+
if q2_answer_user is None:
|
| 303 |
+
feedback_lines.append(f"β οΈ Question 2: No answer selected. Correct was: '{correct_q2_stored}'")
|
| 304 |
+
elif q2_answer_user == correct_q2_stored:
|
| 305 |
+
feedback_lines.append(f"β
Question 2: Correct!")
|
| 306 |
+
total_correct += 1
|
| 307 |
+
else:
|
| 308 |
+
feedback_lines.append(f"β Question 2: Incorrect. Your answer: '{q2_answer_user}'. Correct was: '{correct_q2_stored}'")
|
| 309 |
+
elif q2_answer_user: # User selected an answer but question/correct answer wasn't parsed
|
| 310 |
+
feedback_lines.append(f"β οΈ Question 2: Answer selected, but correct answer could not be determined.")
|
| 311 |
+
|
| 312 |
+
# Overall score
|
| 313 |
+
if graded_count > 0:
|
| 314 |
+
score_message = f"Overall Score: {total_correct}/{graded_count}."
|
| 315 |
+
feedback_lines.append(score_message)
|
| 316 |
+
|
| 317 |
+
# Determine feedback box class based on score
|
| 318 |
+
if total_correct == graded_count:
|
| 319 |
+
feedback_css_class = "correct"
|
| 320 |
+
elif total_correct > 0:
|
| 321 |
+
feedback_css_class = "partial" # Using 'partial' class if you add one in CSS
|
| 322 |
+
else:
|
| 323 |
+
feedback_css_class = "incorrect"
|
| 324 |
+
else:
|
| 325 |
+
feedback_lines.append("No quiz questions were available to grade.")
|
| 326 |
+
feedback_css_class = "" # No specific class if nothing graded
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
feedback_text = "\n".join(feedback_lines)
|
| 330 |
+
|
| 331 |
+
# Update feedback box visibility and value
|
| 332 |
+
feedback_update = gr.update(value=feedback_text, visible=True, elem_classes=[feedback_css_class])
|
| 333 |
+
|
| 334 |
+
return feedback_update
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def create_report(report_format: str, explanation: str, image_path: Optional[str], raw_quiz_text: str) -> Optional[str]:
|
| 338 |
+
"""Creates a downloadable report in PDF or Markdown format."""
|
| 339 |
+
if not explanation:
|
| 340 |
+
return None
|
| 341 |
+
|
| 342 |
+
if report_format == "PDF":
|
| 343 |
+
pdf = FPDF()
|
| 344 |
+
pdf.add_page()
|
| 345 |
+
pdf.set_font("Helvetica", 'B', 16)
|
| 346 |
+
pdf.cell(0, 10, 'Science Report', 0, 1, 'C')
|
| 347 |
+
pdf.ln(10)
|
| 348 |
+
|
| 349 |
+
pdf.set_font("Helvetica", 'B', 14)
|
| 350 |
+
pdf.cell(0, 10, 'Explanation', 0, 1)
|
| 351 |
+
pdf.set_font("Helvetica", '', 12)
|
| 352 |
+
try:
|
| 353 |
+
pdf.multi_cell(0, 10, explanation.encode('latin-1', 'replace').decode('latin-1'))
|
| 354 |
+
except Exception as e:
|
| 355 |
+
print(f"PDF Explanation Encoding Error: {e}")
|
| 356 |
+
pdf.multi_cell(0, 10, "Error encoding explanation text for PDF.") # Fallback
|
| 357 |
+
pdf.ln(5)
|
| 358 |
+
|
| 359 |
+
if image_path and os.path.exists(image_path):
|
| 360 |
+
pdf.set_font("Helvetica", 'B', 14)
|
| 361 |
+
pdf.cell(0, 10, 'Generated Diagram', 0, 1)
|
| 362 |
+
pdf.ln(5)
|
| 363 |
+
try:
|
| 364 |
+
available_width = pdf.w - pdf.l_margin - pdf.r_margin
|
| 365 |
+
pdf.image(image_path, x=pdf.get_x(), y=pdf.get_y(), w=available_width)
|
| 366 |
+
# Move cursor down past where image would be (approximate height based on width)
|
| 367 |
+
try:
|
| 368 |
+
img_w, img_h = Image.open(image_path).size
|
| 369 |
+
img_display_height = (img_h / img_w) * available_width
|
| 370 |
+
pdf.ln(min(img_display_height, pdf.h - pdf.get_y() - pdf.b_margin) + 5) # Add space after image, don't go past page bottom
|
| 371 |
+
except Exception as img_size_e:
|
| 372 |
+
print(f"Could not get image size for PDF spacing: {img_size_e}")
|
| 373 |
+
pdf.ln(100) # Fallback fixed spacing if size fails
|
| 374 |
+
|
| 375 |
+
except Exception as e:
|
| 376 |
+
print(f"PDF Image Error: {e}")
|
| 377 |
+
pdf.ln(15) # Add some space even if image failed
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
pdf.set_font("Helvetica", 'B', 14)
|
| 381 |
+
pdf.cell(0, 10, 'Quiz', 0, 1)
|
| 382 |
+
pdf.set_font("Helvetica", '', 12)
|
| 383 |
+
# Use the raw quiz text for the report as it contains correct answers
|
| 384 |
+
if raw_quiz_text and "Quiz could not be generated" not in raw_quiz_text and "β" not in raw_quiz_text and "β οΈ" not in raw_quiz_text:
|
| 385 |
+
try:
|
| 386 |
+
pdf.multi_cell(0, 10, raw_quiz_text.encode('latin-1', 'replace').decode('latin-1'))
|
| 387 |
+
except Exception as e:
|
| 388 |
+
print(f"PDF Quiz Encoding Error: {e}")
|
| 389 |
+
pdf.multi_cell(0, 10, "Error encoding quiz text for PDF.")
|
| 390 |
+
else:
|
| 391 |
+
pdf.multi_cell(0, 10, "Quiz was not available.")
|
| 392 |
+
pdf.ln(5)
|
| 393 |
+
|
| 394 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
|
| 395 |
+
pdf.output(temp_file.name)
|
| 396 |
+
return temp_file.name
|
| 397 |
+
|
| 398 |
+
elif report_format == "Markdown":
|
| 399 |
+
content = f"# Science Report\n\n## Explanation\n\n{explanation}\n\n"
|
| 400 |
+
if image_path:
|
| 401 |
+
content += "## Diagram\n\n(Note: The diagram image is a separate file.)\n\n"
|
| 402 |
+
content += f"## Quiz\n\n{raw_quiz_text}\n" # Use raw text for Markdown too
|
| 403 |
+
|
| 404 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".md", mode='w', encoding='utf-8') as temp_file:
|
| 405 |
+
temp_file.write(content)
|
| 406 |
+
return temp_file.name
|
| 407 |
+
return None
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 411 |
+
# Main Handler
|
| 412 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 413 |
+
# This function updates ALL relevant output components and state variables.
|
| 414 |
+
# The order of returned values MUST match the outputs list in submit_btn.click
|
| 415 |
+
def handle_combined_input(question, level, uploaded_file):
|
| 416 |
+
explanation = ""
|
| 417 |
+
image_path = None # This holds the temporary file path
|
| 418 |
+
raw_quiz_text = "Your quiz will appear here..." # Store raw AI output for quiz
|
| 419 |
+
reasoning = ""
|
| 420 |
+
download_box_visible = gr.update(visible=False) # Assume hidden initially
|
| 421 |
+
diagram_visible = gr.update(visible=False) # Hide diagram by default
|
| 422 |
+
|
| 423 |
+
# --- Generation Logic ---
|
| 424 |
+
if uploaded_file:
|
| 425 |
+
explanation = gemini_explain_file(uploaded_file, question)
|
| 426 |
+
reasoning = "π§ Analysis powered by Google Gemini."
|
| 427 |
+
image_path = None # No diagram for file upload
|
| 428 |
+
diagram_visible = gr.update(visible=False)
|
| 429 |
+
elif question and question.strip():
|
| 430 |
+
prompt = LEVEL_PROMPTS.get(level, "") + question
|
| 431 |
+
explanation = generate_mistral_response(prompt, "You are a helpful science explainer.")
|
| 432 |
+
reasoning = "π§ Explanation powered by Mistral via OpenRouter."
|
| 433 |
+
if "β" not in explanation and "β οΈ" not in explanation: # Only try image if explanation worked
|
| 434 |
+
image_path = generate_diagram_image(question)
|
| 435 |
+
if image_path:
|
| 436 |
+
reasoning += "\nπ¨ Diagram generated by FLUX via Hugging Face."
|
| 437 |
+
diagram_visible = gr.update(visible=True) # Show diagram if successful
|
| 438 |
+
else:
|
| 439 |
+
reasoning += "\nβ Diagram generation failed."
|
| 440 |
+
diagram_visible = gr.update(visible=False) # Hide if failed
|
| 441 |
+
else:
|
| 442 |
+
image_path = None # If explanation failed, no image is generated
|
| 443 |
+
diagram_visible = gr.update(visible=False)
|
| 444 |
+
else:
|
| 445 |
+
explanation = "β οΈ Please ask a science question or upload a file."
|
| 446 |
+
reasoning = "" # Clear reasoning if no input
|
| 447 |
+
image_path = None
|
| 448 |
+
diagram_visible = gr.update(visible=False)
|
| 449 |
+
|
| 450 |
+
# Generate quiz only if explanation was successful and not empty/warning
|
| 451 |
+
parsed_quiz_data = [] # Initialize parsed data and correct answers
|
| 452 |
+
correct_answers = [None, None] # Initialize state values
|
| 453 |
+
|
| 454 |
+
if explanation.strip() and "β" not in explanation and "β οΈ" not in explanation:
|
| 455 |
+
raw_quiz_text = generate_quiz(explanation)
|
| 456 |
+
# Parse quiz text for dropdowns and get correct answers for state
|
| 457 |
+
parsed_quiz_data, correct_answers = parse_quiz_text_for_dropdown(raw_quiz_text)
|
| 458 |
+
|
| 459 |
+
# Only show download box if explanation AND quiz generation seem OK
|
| 460 |
+
if "β" not in raw_quiz_text and "β οΈ" not in raw_quiz_text and raw_quiz_text.strip() and "Quiz could not be generated" not in raw_quiz_text:
|
| 461 |
+
download_box_visible = gr.update(visible=True)
|
| 462 |
+
# If quiz generation failed, raw_quiz_text will contain the error message
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
# --- Prepare UI Updates ---
|
| 466 |
+
|
| 467 |
+
# Updates for Quiz Question 1 Dropdown
|
| 468 |
+
if len(parsed_quiz_data) > 0:
|
| 469 |
+
q1_label = parsed_quiz_data[0]["label"]
|
| 470 |
+
q1_choices = parsed_quiz_data[0]["choices"]
|
| 471 |
+
q1_update = gr.update(
|
| 472 |
+
label=f"Question 1: {q1_label}", # Add "Question 1:" prefix
|
| 473 |
+
choices=q1_choices,
|
| 474 |
+
value=None, # Reset value
|
| 475 |
+
visible=True # Make visible
|
| 476 |
+
)
|
| 477 |
+
else:
|
| 478 |
+
# If no Q1, hide the dropdown
|
| 479 |
+
q1_update = gr.update(label="Quiz Question 1", choices=[], value=None, visible=False)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
# Updates for Quiz Question 2 Dropdown
|
| 483 |
+
if len(parsed_quiz_data) > 1:
|
| 484 |
+
q2_label = parsed_quiz_data[1]["label"]
|
| 485 |
+
q2_choices = parsed_quiz_data[1]["choices"]
|
| 486 |
+
q2_update = gr.update(
|
| 487 |
+
label=f"Question 2: {q2_label}", # Add "Question 2:" prefix
|
| 488 |
+
choices=q2_choices,
|
| 489 |
+
value=None, # Reset value
|
| 490 |
+
visible=True # Make visible
|
| 491 |
+
)
|
| 492 |
+
else:
|
| 493 |
+
# If no Q2, hide the dropdown
|
| 494 |
+
q2_update = gr.update(label="Quiz Question 2", choices=[], value=None, visible=False)
|
| 495 |
+
|
| 496 |
+
# Update the raw quiz markdown text display
|
| 497 |
+
# Only show the raw text if quiz generation provided some content
|
| 498 |
+
if raw_quiz_text.strip() and "Your quiz will appear here" not in raw_quiz_text:
|
| 499 |
+
raw_quiz_markdown_update = gr.update(value=f"**Raw Quiz Output:**\n\n{raw_quiz_text}", visible=True)
|
| 500 |
+
else:
|
| 501 |
+
raw_quiz_markdown_update = gr.update(value="", visible=False)
|
| 502 |
+
|
| 503 |
+
# Hide quiz feedback initially or if quiz generation failed
|
| 504 |
+
quiz_feedback_update = gr.update(value="", visible=False, elem_classes="") # Also clear feedback class
|
| 505 |
+
# Show submit button only if at least one question was parsed
|
| 506 |
+
submit_quiz_btn_update = gr.update(visible= True if len(parsed_quiz_data) > 0 else False)
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
# Return all the updated components' values/updates
|
| 510 |
+
# Order MUST match the outputs list in submit_btn.click
|
| 511 |
+
return (explanation, # explanation_out
|
| 512 |
+
image_path, # diagram_out (path to temp file)
|
| 513 |
+
diagram_visible, # diagram_out visibility update
|
| 514 |
+
reasoning, # reasoning_out
|
| 515 |
+
q1_update, # quiz_q1 dropdown update
|
| 516 |
+
q2_update, # quiz_q2 dropdown update
|
| 517 |
+
raw_quiz_markdown_update, # raw_quiz_markdown update
|
| 518 |
+
download_box_visible, # download_box visibility update
|
| 519 |
+
submit_quiz_btn_update, # submit_quiz_btn visibility update
|
| 520 |
+
quiz_feedback_update, # quiz_feedback visibility/value update
|
| 521 |
+
correct_answers[0], # correct_q1_state (value update)
|
| 522 |
+
correct_answers[1] # correct_q2_state (value update)
|
| 523 |
+
)
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 527 |
+
# Launch Gradio App
|
| 528 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 529 |
+
def launch_app():
|
| 530 |
+
# Use the combined CSS
|
| 531 |
+
with gr.Blocks(theme=gr.themes.Soft(), css=APP_CSS) as demo:
|
| 532 |
+
gr.HTML(LOTTIE_ANIMATION_HTML)
|
| 533 |
+
gr.Markdown("# π ExplainAnything.AI\nYour personal AI-powered science tutor.")
|
| 534 |
+
|
| 535 |
+
# Hidden state variables to store correct answers
|
| 536 |
+
correct_q1_state = gr.State(value=None)
|
| 537 |
+
correct_q2_state = gr.State(value=None)
|
| 538 |
+
|
| 539 |
+
with gr.Row():
|
| 540 |
+
with gr.Column(scale=1):
|
| 541 |
+
question = gr.Textbox(label="Ask a science question", placeholder="e.g., How does photosynthesis work?", info="Ask any science-related question.")
|
| 542 |
+
level = gr.Radio(choices=["Kid", "Beginner", "Advanced"], value="Beginner", label="Explanation Level", info="Choose how detailed the explanation should be.")
|
| 543 |
+
uploaded_file = gr.File(label="Upload Image/PDF", file_types=["image", ".pdf"])
|
| 544 |
+
|
| 545 |
+
with gr.Row():
|
| 546 |
+
toggle_theme_btn = gr.Button("Toggle Theme π/βοΈ")
|
| 547 |
+
submit_btn = gr.Button("Generate Explanation", variant="primary")
|
| 548 |
+
|
| 549 |
+
with gr.Column(scale=2):
|
| 550 |
+
# Use Accordions - added elem_classes for styling
|
| 551 |
+
with gr.Accordion("π Explanation", open=True, elem_classes="secondary"):
|
| 552 |
+
explanation_out = gr.Textbox(lines=8, label="Explanation", interactive=False)
|
| 553 |
+
reasoning_out = gr.Textbox(lines=2, label="π§ Processing Details", interactive=False)
|
| 554 |
+
|
| 555 |
+
with gr.Accordion("πΌοΈ Diagram", open=False, elem_classes="secondary"):
|
| 556 |
+
diagram_out = gr.Image(label="Generated Diagram", interactive=False, show_label=True, visible=False)
|
| 557 |
+
|
| 558 |
+
with gr.Accordion("π§ͺ Quiz", open=False, elem_classes="secondary") as quiz_accordion:
|
| 559 |
+
gr.Markdown("Test Your Knowledge:")
|
| 560 |
+
# Pre-defined Dropdown menus for Quiz Questions (up to 2 questions)
|
| 561 |
+
quiz_q1 = gr.Dropdown(label="Quiz Question 1", choices=[], value=None, interactive=True, visible=False)
|
| 562 |
+
quiz_q2 = gr.Dropdown(label="Quiz Question 2", choices=[], value=None, interactive=True, visible=False)
|
| 563 |
+
# Raw quiz text output - changed from Markdown to Textbox as in your provided code
|
| 564 |
+
raw_quiz_markdown = gr.Markdown(value="Your quiz will appear here...", label="Raw Quiz Data", visible=False) # Switched back to Markdown
|
| 565 |
+
submit_quiz_btn = gr.Button("Submit Answers", variant="secondary", visible=False)
|
| 566 |
+
# Feedback textbox - initially hidden, given an ID for CSS styling
|
| 567 |
+
quiz_feedback = gr.Textbox(label="Quiz Feedback", lines=3, interactive=False, visible=False, elem_id="quiz_feedback_box")
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
with gr.Group(visible=False) as download_box:
|
| 571 |
+
gr.Markdown("### π₯ Download Report")
|
| 572 |
+
with gr.Row():
|
| 573 |
+
report_format = gr.Radio(["PDF", "Markdown"], label="Choose Format", value="PDF")
|
| 574 |
+
download_btn = gr.Button("Download")
|
| 575 |
+
download_file = gr.File(label="Your report is ready to download", interactive=False)
|
| 576 |
+
|
| 577 |
+
# --- Event Handlers ---
|
| 578 |
+
# Inputs for handle_combined_input: question, level, uploaded_file
|
| 579 |
+
# Outputs: explanation_out, diagram_out (value), diagram_out (visibility),
|
| 580 |
+
# reasoning_out (value), quiz_q1 (update), quiz_q2 (update),
|
| 581 |
+
# raw_quiz_markdown (update), download_box (visibility),
|
| 582 |
+
# submit_quiz_btn (visibility), quiz_feedback (visibility),
|
| 583 |
+
# correct_q1_state (update), correct_q2_state (update)
|
| 584 |
+
# The order here MUST match the return order in handle_combined_input
|
| 585 |
+
submit_btn.click(
|
| 586 |
+
fn=handle_combined_input,
|
| 587 |
+
inputs=[question, level, uploaded_file],
|
| 588 |
+
outputs=[explanation_out, diagram_out, diagram_out, # Diagram value and visibility
|
| 589 |
+
reasoning_out,
|
| 590 |
+
quiz_q1, quiz_q2, # Dropdown updates
|
| 591 |
+
raw_quiz_markdown, # Raw text update
|
| 592 |
+
download_box, # Download box visibility
|
| 593 |
+
submit_quiz_btn, # Submit quiz button visibility
|
| 594 |
+
quiz_feedback, # Feedback box visibility/value
|
| 595 |
+
correct_q1_state, # State update for correct answer 1
|
| 596 |
+
correct_q2_state # State update for correct answer 2
|
| 597 |
+
]
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
# Handle quiz submission
|
| 601 |
+
# Inputs: Current value of quiz_q1, quiz_q2, and the state variables holding correct answers
|
| 602 |
+
# Output: Update the quiz_feedback textbox
|
| 603 |
+
submit_quiz_btn.click(
|
| 604 |
+
fn=grade_quiz,
|
| 605 |
+
inputs=[quiz_q1, quiz_q2, correct_q1_state, correct_q2_state], # Get selected answers AND stored correct answers
|
| 606 |
+
outputs=[quiz_feedback] # Update the feedback textbox
|
| 607 |
+
)
|
| 608 |
+
|
| 609 |
+
toggle_theme_btn.click(
|
| 610 |
+
fn=None, inputs=None, outputs=None,
|
| 611 |
+
js="() => { document.body.classList.toggle('dark'); }"
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
+
|
| 615 |
+
# Inputs for create_report need the CURRENT values from the outputs AFTER submit is run
|
| 616 |
+
# Inputs: report_format (from radio), explanation (from explanation_out),
|
| 617 |
+
# image_path (from diagram_out), raw_quiz_text (from raw_quiz_markdown)
|
| 618 |
+
# Output: download_file (the downloadable file)
|
| 619 |
+
# Note: We pass the component objects themselves, Gradio gets their current VALUE
|
| 620 |
+
download_btn.click(
|
| 621 |
+
fn=create_report,
|
| 622 |
+
inputs=[report_format, explanation_out, diagram_out, raw_quiz_markdown],
|
| 623 |
+
outputs=[download_file]
|
| 624 |
+
)
|
| 625 |
+
|
| 626 |
+
print("Launching Gradio app with a public link...")
|
| 627 |
+
demo.launch(share=True, debug=True)
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 631 |
+
# Run App
|
| 632 |
+
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 633 |
+
if __name__ == "__main__":
|
| 634 |
+
launch_app()
|