File size: 14,941 Bytes
0f02a72
4c1fbe0
d52747d
 
7eeafa4
58a9162
e2f0f0e
7eeafa4
7dcdf66
d52747d
 
7eeafa4
fe59eaf
4c1fbe0
 
 
 
 
 
 
 
b3c62a5
 
fd6c129
58a9162
4c1fbe0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58a9162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7eeafa4
e2f0f0e
 
7eeafa4
 
 
6367dee
 
d52747d
 
 
c6be2bd
d52747d
 
 
 
 
6367dee
c6be2bd
 
87fa1e2
4c1fbe0
499ead8
4c1fbe0
 
 
618b692
4c1fbe0
 
6367dee
7eeafa4
87fa1e2
c6be2bd
7eeafa4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d52747d
 
 
 
 
 
 
7eeafa4
d52747d
 
7eeafa4
d52747d
 
b3c62a5
d52747d
 
 
 
c6be2bd
 
d52747d
 
 
 
 
 
 
 
 
 
7eeafa4
d52747d
 
 
 
 
 
 
 
 
 
 
 
58a9162
d52747d
 
c6be2bd
7eeafa4
 
87fa1e2
d52747d
 
c6be2bd
d52747d
4c1fbe0
 
 
 
 
d52747d
 
 
4c1fbe0
d52747d
4c1fbe0
 
d52747d
4c1fbe0
 
 
c6be2bd
 
4c1fbe0
 
 
 
 
6367dee
d52747d
 
 
 
4c1fbe0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bfa49d6
 
 
 
 
4c1fbe0
 
d52747d
c6be2bd
4c1fbe0
 
 
 
6367dee
4c1fbe0
d52747d
 
 
4c1fbe0
 
 
 
 
 
d52747d
 
 
 
bfa49d6
c6be2bd
 
d52747d
 
 
 
bfa49d6
c6be2bd
 
 
d52747d
bfa49d6
c6be2bd
6367dee
c6be2bd
7eeafa4
4c1fbe0
 
d52747d
4c1fbe0
 
d52747d
4c1fbe0
 
 
 
 
 
d52747d
4c1fbe0
 
 
 
d52747d
4c1fbe0
 
 
cd3d0b1
 
 
 
 
 
4c1fbe0
d52747d
cd3d0b1
 
 
1b39435
7dcdf66
 
499ead8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
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("""
    <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()