| 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): |
| |
| 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(""" |
| <div class="math-header"> |
| <h1>π Interactive Math Tutor</h1> |
| <p>Step-by-step guidance Β· Grade 1 to High School Β· Upload a worksheet or type any problem</p> |
| <div class="badge-row"> |
| <span class="badge">βοΈ Arithmetic</span><span class="badge">Β½ Fractions</span> |
| <span class="badge">π Algebra</span><span class="badge">π Geometry</span> |
| <span class="badge">π Trigonometry</span><span class="badge">β« Calculus</span> |
| </div> |
| </div> |
| """) |
|
|
| with gr.Row(equal_height=False): |
| with gr.Column(scale=4, min_width=300): |
| if not OWNER_KEY_SET: |
| gr.HTML('<div class="panel-label">π Groq API Key (free, no card)</div>') |
| user_key = gr.Textbox( |
| label="", placeholder="gsk_xxxxxxxxxxxxxxxxxxxx", |
| type="password", lines=1, elem_classes=["token-box"], |
| ) |
| gr.HTML(""" |
| <div class="key-info"> |
| <strong>Get your free key in 60 seconds:</strong><br> |
| 1. Go to <a href="https://console.groq.com/keys" target="_blank">console.groq.com/keys</a><br> |
| 2. Sign up free (no credit card) β <strong>Create API Key</strong> β Copy<br> |
| 3. Paste above Β· Never stored, session-only |
| </div> |
| """) |
| else: |
| user_key = gr.Textbox(value="", visible=False) |
| gr.HTML('<div class="info-box"><strong>β
Ready to use β no setup needed!</strong></div>') |
|
|
| gr.HTML('<div class="panel-label" style="margin-top:16px">π Upload Worksheet</div>') |
| image_input = gr.Image(type="pil", label="", height=180) |
| gr.HTML('<div style="font-size:.78rem;color:var(--muted);margin-top:4px">πΈ Images are read directly by AI β diagrams, triangles, and graphs all work!</div>') |
|
|
| gr.HTML('<div class="panel-label" style="margin-top:14px">βοΈ Or Type Your Problem</div>') |
| text_input = gr.Textbox(label="", placeholder="e.g. Solve for x: 5x β 3 = 22", lines=3) |
|
|
| gr.HTML('<div class="panel-label" style="margin-top:14px">π Grade Level</div>') |
| 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('<div class="panel-label" style="margin-top:16px">π‘ Try an example</div>') |
| gr.Examples(examples=EXAMPLES, inputs=[image_input, text_input, grade_dropdown, user_key], label="") |
|
|
| with gr.Column(scale=6, min_width=360): |
| gr.HTML('<div class="panel-label">π Step-by-Step Solution</div>') |
| 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() |