saad-sust commited on
Commit
e20cb5f
ยท
verified ยท
1 Parent(s): 3b55630

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +193 -1
app.py CHANGED
@@ -291,6 +291,57 @@ hr { border-color: #1a1a2e !important; }
291
  ::-webkit-scrollbar-track { background: #0f0f0f; }
292
  ::-webkit-scrollbar-thumb { background: #1f2937; border-radius: 2px; }
293
  ::-webkit-scrollbar-thumb:hover { background: #3b82f6; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  </style>
295
  """, unsafe_allow_html=True)
296
 
@@ -2108,8 +2159,113 @@ def ask_ai(problem: str, sympy_info: dict, history: list) -> str:
2108
 
2109
 
2110
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
2111
- # STREAMING WRAPPER โ€” shows text word by word like ChatGPT
2112
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2113
  def ask_ai_streaming(problem: str, sympy_info: dict, history: list) -> str:
2114
  """Get AI response and stream it word by word using st.write_stream."""
2115
  import time
@@ -2409,6 +2565,42 @@ for i, msg in enumerate(st.session_state.messages):
2409
  with st.expander("๐Ÿ“‹ Copy"):
2410
  st.code(msg["content"], language=None)
2411
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2412
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
2413
  # INPUT โ€” use st.chat_input (cleaner than text_input + button)
2414
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
 
291
  ::-webkit-scrollbar-track { background: #0f0f0f; }
292
  ::-webkit-scrollbar-thumb { background: #1f2937; border-radius: 2px; }
293
  ::-webkit-scrollbar-thumb:hover { background: #3b82f6; }
294
+
295
+ /* โ”€โ”€ File uploader โ€” compact, attached to input bar โ”€โ”€ */
296
+ [data-testid="stFileUploader"] {
297
+ background: #111827 !important;
298
+ border: 1px solid #1f2937 !important;
299
+ border-bottom: none !important;
300
+ border-radius: 14px 14px 0 0 !important;
301
+ padding: 0.35rem 1rem !important;
302
+ transition: all 0.2s ease !important;
303
+ }
304
+ [data-testid="stFileUploader"]:hover {
305
+ border-color: #3b82f6 !important;
306
+ background: #0f172a !important;
307
+ }
308
+ [data-testid="stFileUploader"] section {
309
+ padding: 0 !important;
310
+ border: none !important;
311
+ background: transparent !important;
312
+ }
313
+ [data-testid="stFileUploader"] label {
314
+ font-size: 0.78rem !important;
315
+ color: #4b5563 !important;
316
+ transition: color 0.2s !important;
317
+ }
318
+ [data-testid="stFileUploader"]:hover label {
319
+ color: #60a5fa !important;
320
+ }
321
+ [data-testid="stFileUploaderDropzone"] {
322
+ background: transparent !important;
323
+ border: none !important;
324
+ padding: 0.1rem 0 !important;
325
+ min-height: 0 !important;
326
+ transition: all 0.2s !important;
327
+ }
328
+ [data-testid="stFileUploaderDropzone"]:hover {
329
+ background: rgba(59,130,246,0.05) !important;
330
+ border-radius: 8px !important;
331
+ }
332
+ [data-testid="stFileUploaderDropzoneInstructions"] {
333
+ font-size: 0.75rem !important;
334
+ color: #4b5563 !important;
335
+ padding: 0.2rem 0 !important;
336
+ }
337
+ /* Input bar connects seamlessly below uploader */
338
+ [data-testid="stChatInput"] {
339
+ border-radius: 0 0 14px 14px !important;
340
+ border-top: 1px solid #1f2937 !important;
341
+ }
342
+ [data-testid="stChatInput"]:focus-within {
343
+ border-color: #3b82f6 !important;
344
+ }
345
  </style>
346
  """, unsafe_allow_html=True)
347
 
 
2159
 
2160
 
2161
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
2162
+ # FILE UPLOAD โ€” image/PDF sent to Gemini Vision, then SymPy verified
2163
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
2164
+ def ask_gemini_vision(image_b64: str, mime_type: str, user_note: str) -> str:
2165
+ """
2166
+ Send image or PDF to Gemini 1.5 Flash vision.
2167
+ Returns extracted problem text + solution from Gemini.
2168
+ Only Gemini supports vision โ€” Groq and OpenRouter skipped.
2169
+ """
2170
+ gemini_keys = [
2171
+ os.environ.get("GEMINI_API_KEY_1", ""),
2172
+ os.environ.get("GEMINI_API_KEY_2", ""),
2173
+ ]
2174
+
2175
+ prompt = (
2176
+ "You are Saad.AI, a BSc Mathematics assistant built by Saad.\n"
2177
+ "The student has uploaded an image or document containing a math problem.\n\n"
2178
+ "YOUR TASKS:\n"
2179
+ "1. Read and extract the math problem from the image/document exactly.\n"
2180
+ "2. State what the problem is clearly.\n"
2181
+ "3. Solve it step by step using the full math structure:\n"
2182
+ " ๐Ÿ” **Given:** ...\n"
2183
+ " ๐Ÿ“Œ **Method:** ...\n"
2184
+ " ๐Ÿงฎ **Step 1:** ...\n"
2185
+ " โœ… **Final Answer:** $$\\boxed{answer}$$\n"
2186
+ "4. ALL math must be in LaTeX โ€” never plain text math.\n"
2187
+ "5. If the image is unclear or not math-related, say so politely.\n\n"
2188
+ f"Additional note from student: {user_note if user_note else 'None'}"
2189
+ )
2190
+
2191
+ for key in gemini_keys:
2192
+ if not key:
2193
+ continue
2194
+ try:
2195
+ resp = requests.post(
2196
+ f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={key}",
2197
+ headers={"Content-Type": "application/json"},
2198
+ json={
2199
+ "contents": [{
2200
+ "parts": [
2201
+ {"inline_data": {"mime_type": mime_type, "data": image_b64}},
2202
+ {"text": prompt}
2203
+ ]
2204
+ }],
2205
+ "generationConfig": {"maxOutputTokens": 2048, "temperature": 0.15}
2206
+ },
2207
+ timeout=60
2208
+ )
2209
+ resp.raise_for_status()
2210
+ return resp.json()["candidates"][0]["content"]["parts"][0]["text"]
2211
+ except Exception:
2212
+ continue
2213
+
2214
+ return "โš ๏ธ Could not process the file. Please check your Gemini API keys or try again."
2215
+
2216
+
2217
+ def handle_uploaded_file(uploaded_file, user_note: str) -> str:
2218
+ """
2219
+ Process uploaded image or PDF:
2220
+ 1. Convert to base64
2221
+ 2. Send to Gemini Vision
2222
+ 3. Try SymPy verification on extracted text
2223
+ 4. Return final answer
2224
+ """
2225
+ import base64
2226
+
2227
+ # โ”€โ”€ Validate file โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2228
+ MAX_SIZE = 5 * 1024 * 1024 # 5MB
2229
+ file_bytes = uploaded_file.read()
2230
+
2231
+ if len(file_bytes) > MAX_SIZE:
2232
+ return "โš ๏ธ File too large. Please upload a file under 5MB."
2233
+
2234
+ mime_map = {
2235
+ "jpg": "image/jpeg", "jpeg": "image/jpeg",
2236
+ "png": "image/png", "webp": "image/webp",
2237
+ "pdf": "application/pdf"
2238
+ }
2239
+ ext = uploaded_file.name.split(".")[-1].lower()
2240
+ mime_type = mime_map.get(ext)
2241
+
2242
+ if not mime_type:
2243
+ return "โš ๏ธ Unsupported format. Please upload JPG, PNG, WEBP or PDF."
2244
+
2245
+ # โ”€โ”€ Convert to base64 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2246
+ image_b64 = base64.b64encode(file_bytes).decode("utf-8")
2247
+
2248
+ # โ”€โ”€ Send to Gemini Vision โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2249
+ gemini_response = ask_gemini_vision(image_b64, mime_type, user_note)
2250
+
2251
+ # โ”€โ”€ Try SymPy verification on extracted text โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2252
+ # Gemini response contains the problem โ€” try to run SymPy on it
2253
+ sympy_result = run_sympy(gemini_response)
2254
+
2255
+ if (sympy_result.get("result") and
2256
+ sympy_result["result"] not in (None, "matrix_detected", "mod_detected")):
2257
+ # SymPy verified โ€” strip AI answer, append verified one
2258
+ latex = sympy_result.get("latex", "")
2259
+ cleaned = re.sub(
2260
+ r'(โœ…\s*\*{0,2}Final\s*Answer\*{0,2}.*|โœ…[^\n]*$)',
2261
+ "", gemini_response,
2262
+ flags=re.DOTALL | re.IGNORECASE
2263
+ ).rstrip()
2264
+ verified_note = "\n\n๐Ÿ”’ **SymPy Verified**"
2265
+ return cleaned + verified_note + f"\n\nโœ… **Final Answer:** $$\\boxed{{{latex}}}$$"
2266
+ else:
2267
+ # No SymPy verification โ€” return Gemini answer with warning
2268
+ return gemini_response + "\n\nโš ๏ธ *AI-generated answer โ€” not SymPy verified.*"
2269
  def ask_ai_streaming(problem: str, sympy_info: dict, history: list) -> str:
2270
  """Get AI response and stream it word by word using st.write_stream."""
2271
  import time
 
2565
  with st.expander("๐Ÿ“‹ Copy"):
2566
  st.code(msg["content"], language=None)
2567
 
2568
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
2569
+ # FILE UPLOAD โ€” above chat input
2570
+ # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
2571
+ uploaded_file = st.file_uploader(
2572
+ "๐Ÿ“Ž Upload a math problem (image or PDF)",
2573
+ type=["jpg", "jpeg", "png", "webp", "pdf"],
2574
+ label_visibility="collapsed",
2575
+ help="Upload a photo or PDF of your math problem โ€” Saad.AI will read and solve it"
2576
+ )
2577
+
2578
+ if uploaded_file and uploaded_file.name != st.session_state.get("last_uploaded_file", ""):
2579
+ st.session_state["last_uploaded_file"] = uploaded_file.name
2580
+ user_note = ""
2581
+
2582
+ # Show user message
2583
+ with st.chat_message("user", avatar="๐Ÿง‘โ€๐ŸŽ“"):
2584
+ st.markdown(f"๐Ÿ“Ž **Uploaded:** `{uploaded_file.name}`")
2585
+
2586
+ # Process file
2587
+ with st.chat_message("assistant", avatar="๐Ÿ“"):
2588
+ with st.spinner("๐Ÿ“– Reading your file..."):
2589
+ answer = handle_uploaded_file(uploaded_file, user_note)
2590
+ st.markdown(answer)
2591
+ with st.expander("๐Ÿ“‹ Copy"):
2592
+ st.code(answer, language=None)
2593
+
2594
+ # Save to history
2595
+ if not st.session_state.current_chat_id:
2596
+ new_chat()
2597
+ st.session_state.messages.append({
2598
+ "role": "user",
2599
+ "content": f"๐Ÿ“Ž Uploaded file: `{uploaded_file.name}`"
2600
+ })
2601
+ st.session_state.messages.append({"role": "assistant", "content": answer})
2602
+ save_current_chat()
2603
+
2604
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
2605
  # INPUT โ€” use st.chat_input (cleaner than text_input + button)
2606
  # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•