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("""
Step-by-step guidance ยท Grade 1 to High School ยท Upload a worksheet or type any problem