import gradio as gr import os import requests import json import base64 import re from PIL import Image, ImageOps from io import BytesIO GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions" SPACE_GROQ_KEY = os.environ.get("GROQ_API_KEY", "") TEXT_MODEL = "llama-3.3-70b-versatile" VISION_MODEL = "meta-llama/llama-4-scout-17b-16e-instruct" SYSTEM_PROMPT = """You are a warm, patient math tutor for students from Grade 1 through High School. Rules you ALWAYS follow: 1. Never give just the answer. Guide the student step by step. 2. Before each step, explain WHAT you are doing and WHY in simple language. 3. Match your language to the grade level given. 4. Use proper math notation (fractions, exponents, equations). 5. End with a clearly marked Final Answer and one sentence of encouragement. 6. For any calculation, write out every arithmetic operation explicitly before evaluating it. Never skip steps or compute mentally. 7. For trigonometry, always state the formula, substitute values, then compute. Show sin/cos/tan values to 4 decimal places. 8. Never use LaTeX notation. Use plain text only. Write fractions as a/b, exponents as x^2, and put each equation on its own line. Always format your response exactly like this: ๐Ÿ“˜ **Problem:** [restate the problem clearly] --- **Step 1 โ€” [Short title]** *Why:* [reason in one sentence] [Show the calculation or concept] **Step 2 โ€” [Short title]** *Why:* [reason in one sentence] [Show the calculation or concept] (continue for all steps needed) --- โœ… **Final Answer:** [clear result] ๐Ÿ’ก [One encouraging sentence]""" def strip_latex(text): text = re.sub(r'\$\$(.+?)\$\$', r'\1', text, flags=re.DOTALL) text = re.sub(r'\$(.+?)\$', r'\1', text) text = re.sub(r'\\frac\{(.+?)\}\{(.+?)\}', r'\1/\2', text) text = re.sub(r'\\text\{(.+?)\}', r'\1', text) text = text.replace(r'\cos', 'cos') text = text.replace(r'\sin', 'sin') text = text.replace(r'\tan', 'tan') text = text.replace(r'\times', 'ร—') text = text.replace(r'\div', 'รท') text = text.replace(r'\approx', 'โ‰ˆ') text = text.replace(r'\cdot', 'ยท') text = text.replace(r'\left', '').replace(r'\right', '') text = text.replace(r'\sqrt', 'sqrt') text = text.replace(r'\pi', 'ฯ€') text = re.sub(r'\{|\}', '', text) return text def image_to_base64(pil_image): # Flip horizontally to correct webcam mirror effect pil_image = ImageOps.mirror(pil_image) buffered = BytesIO() pil_image.save(buffered, format="PNG") return base64.b64encode(buffered.getvalue()).decode("utf-8") def solve_math(image_input, text_input, grade_level, user_key): key = user_key.strip() if user_key and user_key.strip() else SPACE_GROQ_KEY if not key: yield ( "๐Ÿ”‘ **A free Groq API key is required.**\n\n" "Get one in 60 seconds โ€” no credit card needed:\n" "1. Go to [console.groq.com/keys](https://console.groq.com/keys)\n" "2. Sign up free โ†’ click **Create API Key** โ†’ copy it\n" "3. Paste it in the **Groq API Key** box on the left\n\n" "Your key is never stored โ€” session only." ) return has_image = image_input is not None has_text = text_input and text_input.strip() if not has_image and not has_text: yield "โš ๏ธ Please upload a worksheet image **or** type a math problem." return grade_note = f"Grade level: {grade_level}." yield "โšก Solving step by step..." try: if has_image: img_b64 = image_to_base64(image_input) text_note = f"\nStudent's additional note: {text_input.strip()}" if has_text else "" user_content = [ { "type": "image_url", "image_url": { "url": f"data:image/png;base64,{img_b64}" } }, { "type": "text", "text": f"{grade_note}\n\nPlease read this math problem from the image carefully, identify all numbers and labels, then teach me how to solve it step by step.{text_note}" } ] model = VISION_MODEL else: user_content = f"{grade_note}\n\nProblem: {text_input.strip()}\n\nPlease teach me how to solve this step by step." model = TEXT_MODEL response = requests.post( GROQ_API_URL, headers={ "Authorization": f"Bearer {key}", "Content-Type": "application/json", }, json={ "model": model, "messages": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_content}, ], "max_tokens": 1200, "temperature": 0, "stream": True, }, stream=True, timeout=60, ) if response.status_code == 401: yield "โŒ **Invalid API key.** Please check your Groq key and try again." return if response.status_code == 429: yield "โš ๏ธ Rate limit hit. Please wait a few seconds and try again." return if response.status_code != 200: yield f"โŒ API error {response.status_code}: {response.text[:200]}" return output = "" for raw in response.iter_lines(): if not raw: continue line = raw.decode("utf-8", errors="ignore") if line.startswith("data: "): line = line[6:].strip() if line == "[DONE]": break try: token = json.loads(line)["choices"][0]["delta"].get("content", "") if token: output += token yield strip_latex(output) except Exception: continue if not output: yield "โš ๏ธ No response received. Please try again." except requests.exceptions.Timeout: yield "โŒ Request timed out. Please try again." except Exception as e: yield f"โŒ Error: {str(e)}" CSS = """ @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&family=JetBrains+Mono:wght@400;600&display=swap'); :root { --bg:#0d1117; --surface:#161b22; --surface2:#1f2937; --accent:#58a6ff; --accent2:#bc8cff; --green:#3fb950; --text:#e6edf3; --muted:#7d8590; --border:#30363d; --radius:14px; } body, .gradio-container { background:var(--bg) !important; font-family:'Nunito',sans-serif !important; color:var(--text) !important; } .math-header { text-align:center; padding:32px 20px 18px; background:linear-gradient(160deg,#161b22,#0d1117); border-bottom:1px solid var(--border); border-radius:var(--radius); margin-bottom:20px; } .math-header h1 { font-size:2.2rem; font-weight:900; background:linear-gradient(90deg,var(--accent),var(--accent2),var(--green)); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; margin:0 0 6px; } .math-header p { color:var(--muted); font-size:.95rem; margin:0; } .badge-row { display:flex; gap:8px; flex-wrap:wrap; justify-content:center; margin-top:12px; } .badge { background:var(--surface2); border:1px solid var(--border); border-radius:20px; padding:3px 12px; font-size:.76rem; color:var(--muted); } .panel-label { font-size:.72rem; font-weight:800; letter-spacing:.12em; text-transform:uppercase; color:var(--muted); margin-bottom:6px; } .token-box textarea, .token-box input { background:var(--surface2) !important; border:1px solid var(--border) !important; border-radius:10px !important; color:var(--text) !important; font-family:'JetBrains Mono',monospace !important; font-size:.85rem !important; } .gr-textbox textarea, .gr-textbox input { background:var(--surface2) !important; border:1px solid var(--border) !important; border-radius:10px !important; color:var(--text) !important; font-family:'Nunito',sans-serif !important; font-size:.95rem !important; } .solve-btn { background:linear-gradient(135deg,var(--accent),var(--accent2)) !important; color:#fff !important; font-family:'Nunito',sans-serif !important; font-weight:900 !important; font-size:1.05rem !important; border:none !important; border-radius:12px !important; padding:14px 0 !important; width:100% !important; cursor:pointer !important; transition:all .2s !important; } .solve-btn:hover { transform:translateY(-2px) !important; box-shadow:0 8px 24px rgba(88,166,255,.35) !important; } .clear-btn { background:var(--surface2) !important; color:var(--muted) !important; border:1px solid var(--border) !important; border-radius:12px !important; font-family:'Nunito',sans-serif !important; font-weight:700 !important; transition:all .2s !important; } .clear-btn:hover { border-color:var(--accent) !important; color:var(--text) !important; } .output-box { background:var(--surface) !important; border:1px solid var(--border) !important; border-radius:var(--radius) !important; padding:24px !important; min-height:420px !important; font-family:'Nunito',sans-serif !important; font-size:.97rem !important; line-height:1.8 !important; color:var(--text) !important; } .output-box code, .output-box pre { font-family:'JetBrains Mono',monospace !important; background:var(--surface2) !important; border-radius:6px !important; padding:2px 8px !important; } .key-info { background:var(--surface2); border:1px solid var(--border); border-left:3px solid var(--accent); border-radius:10px; padding:12px 16px; margin-top:8px; font-size:.82rem; color:var(--muted); line-height:1.7; } .key-info a { color:var(--accent); text-decoration:none; } .key-info strong { color:var(--text); } .info-box { background:var(--surface2); border:1px solid var(--border); border-left:3px solid var(--green); border-radius:10px; padding:12px 16px; margin-top:8px; font-size:.82rem; color:var(--muted); line-height:1.6; } .info-box strong { color:var(--green); } footer { display:none !important; } """ GRADE_CHOICES = [ "Auto-detect from problem", "Grade 1โ€“2 (Basic Addition & Subtraction)", "Grade 3โ€“4 (Multiplication, Division, Fractions Intro)", "Grade 5โ€“6 (Fractions, Decimals, Geometry Basics)", "Grade 7โ€“8 (Algebra, Ratios, Percentages)", "Grade 9โ€“10 (Algebra II, Geometry, Trigonometry)", "Grade 11โ€“12 (Pre-Calculus, Statistics, Calculus)", ] EXAMPLES = [ [None, "What is 24 รท 6 + 3 ร— 2?", "Grade 3โ€“4 (Multiplication, Division, Fractions Intro)", ""], [None, "If I have 3/4 of a pizza and eat 1/3, how much is left?", "Grade 5โ€“6 (Fractions, Decimals, Geometry Basics)", ""], [None, "Solve for x: 2x + 5 = 17", "Grade 7โ€“8 (Algebra, Ratios, Percentages)", ""], [None, "Find the area of a circle with radius 7 cm", "Grade 9โ€“10 (Algebra II, Geometry, Trigonometry)", ""], [None, "Differentiate f(x) = 3xยณ โˆ’ 2x + 7", "Grade 11โ€“12 (Pre-Calculus, Statistics, Calculus)", ""], ] OWNER_KEY_SET = bool(SPACE_GROQ_KEY) with gr.Blocks(css=CSS, title="Interactive Math Tutor") as demo: gr.HTML("""

๐Ÿ“ Interactive Math Tutor

Step-by-step guidance ยท Grade 1 to High School ยท Upload a worksheet or type any problem

โœ๏ธ Arithmeticยฝ Fractions ๐Ÿ“Š Algebra๐Ÿ“ Geometry ๐Ÿ“ˆ Trigonometryโˆซ Calculus
""") with gr.Row(equal_height=False): with gr.Column(scale=4, min_width=300): if not OWNER_KEY_SET: gr.HTML('
๐Ÿ”‘ Groq API Key (free, no card)
') user_key = gr.Textbox( label="", placeholder="gsk_xxxxxxxxxxxxxxxxxxxx", type="password", lines=1, elem_classes=["token-box"], ) gr.HTML("""
Get your free key in 60 seconds:
1. Go to console.groq.com/keys
2. Sign up free (no credit card) โ†’ Create API Key โ†’ Copy
3. Paste above ยท Never stored, session-only
""") else: user_key = gr.Textbox(value="", visible=False) gr.HTML('
โœ… Ready to use โ€” no setup needed!
') gr.HTML('
๐Ÿ“Ž Upload Worksheet
') image_input = gr.Image(type="pil", label="", height=180) gr.HTML('
๐Ÿ“ธ Images are read directly by AI โ€” diagrams, triangles, and graphs all work!
') gr.HTML('
โœ๏ธ Or Type Your Problem
') text_input = gr.Textbox(label="", placeholder="e.g. Solve for x: 5x โˆ’ 3 = 22", lines=3) gr.HTML('
๐ŸŽ“ Grade Level
') grade_dropdown = gr.Dropdown(choices=GRADE_CHOICES, value="Auto-detect from problem", label="") with gr.Row(): solve_btn = gr.Button("๐Ÿš€ Solve & Explain", elem_classes=["solve-btn"]) clear_btn = gr.Button("๐Ÿ—‘ Clear", elem_classes=["clear-btn"]) gr.HTML('
๐Ÿ’ก Try an example
') gr.Examples(examples=EXAMPLES, inputs=[image_input, text_input, grade_dropdown, user_key], label="") with gr.Column(scale=6, min_width=360): gr.HTML('
๐ŸŽ“ Step-by-Step Solution
') output_display = gr.Markdown( value="*Your solution will appear here. Upload a worksheet or type a problem, then click **Solve & Explain**.*", elem_classes=["output-box"], ) solve_btn.click( fn=solve_math, inputs=[image_input, text_input, grade_dropdown, user_key], outputs=output_display, api_name=False, ) clear_btn.click( fn=lambda: (None, "", "Auto-detect from problem", "", "*Your solution will appear here.*"), inputs=[], outputs=[image_input, text_input, grade_dropdown, user_key, output_display], api_name=False, ) if __name__ == "__main__": demo.queue().launch()