saad-sust commited on
Commit
984fba2
Β·
verified Β·
1 Parent(s): edd2a88

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +190 -85
app.py CHANGED
@@ -292,13 +292,39 @@ hr { border-color: #1a1a2e !important; }
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 {
@@ -310,38 +336,47 @@ hr { border-color: #1a1a2e !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
 
@@ -354,11 +389,13 @@ if "messages" not in st.session_state:
354
  if "last_submitted" not in st.session_state:
355
  st.session_state.last_submitted = ""
356
  if "chats" not in st.session_state:
357
- st.session_state.chats = {} # Will load from Supabase after functions defined
358
  if "supa_loaded" not in st.session_state:
359
  st.session_state.supa_loaded = False
360
  if "current_chat_id" not in st.session_state:
361
  st.session_state.current_chat_id = None
 
 
362
 
363
  # ════════════════════════════════════════════════════════════════════
364
  # SUPABASE β€” Persistent chat history
@@ -1798,16 +1835,32 @@ def ask_ai(problem: str, sympy_info: dict, history: list) -> str:
1798
  for m in user_msgs:
1799
  role = "user" if m["role"]=="user" else "model"
1800
  contents.append({"role": role, "parts": [{"text": m["content"]}]})
1801
- resp = requests.post(
1802
- f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={key}",
1803
- headers={"Content-Type": "application/json"},
1804
- json={"system_instruction": {"parts": [{"text": system_msg}]},
1805
- "contents": contents,
1806
- "generationConfig": {"maxOutputTokens": 2048, "temperature": 0.15}},
1807
- timeout=60
1808
- )
1809
- resp.raise_for_status()
1810
- return resp.json()["candidates"][0]["content"]["parts"][0]["text"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1811
 
1812
  def try_openrouter(key, messages):
1813
  resp = requests.post(
@@ -2141,10 +2194,12 @@ def ask_ai(problem: str, sympy_info: dict, history: list) -> str:
2141
  continue
2142
  except requests.exceptions.HTTPError as e:
2143
  code = e.response.status_code if e.response else 0
2144
- # Skip to next provider for ALL these codes:
2145
- # 429 = rate limit, 401 = invalid key, 402 = no credits
2146
- # 403 = forbidden, 503 = service down, 500 = server error
2147
- last_error = f"⚠️ {provider_name} HTTP {code}"
 
 
2148
  continue # always try next provider
2149
  except Exception as e:
2150
  last_error = f"⚠️ {provider_name} error: {str(e)}"
@@ -2152,10 +2207,11 @@ def ask_ai(problem: str, sympy_info: dict, history: list) -> str:
2152
 
2153
  # All providers exhausted
2154
  return (
2155
- f"⚠️ **All API limits reached.**\n\n"
2156
- f"Last error: {last_error}\n\n"
2157
- "All 6 providers (3Γ—Groq, 2Γ—Gemini, OpenRouter) are at their daily limit.\n"
2158
- "Limits reset every 24 hours. Please try again tomorrow! πŸ”„"
 
2159
  )
2160
 
2161
 
@@ -2195,48 +2251,85 @@ def ask_gemini_vision(image_b64: str, mime_type: str, user_note: str) -> str:
2195
  f"Additional note from student: {user_note if user_note else 'None'}"
2196
  )
2197
 
 
 
 
 
2198
  last_error = ""
2199
  for i, key in enumerate(gemini_keys):
2200
  if not key.strip():
2201
  continue
2202
- try:
2203
- resp = requests.post(
2204
- f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={key}",
2205
- headers={"Content-Type": "application/json"},
2206
- json={
2207
- "contents": [{
2208
- "parts": [
2209
- {"inline_data": {"mime_type": mime_type, "data": image_b64}},
2210
- {"text": prompt}
2211
- ]
2212
- }],
2213
- "generationConfig": {"maxOutputTokens": 2048, "temperature": 0.15}
2214
- },
2215
- timeout=60
2216
- )
2217
- if resp.status_code == 200:
2218
- data = resp.json()
2219
- candidates = data.get("candidates", [])
2220
- if not candidates:
2221
- last_error = f"Key {i+1}: blocked by safety filter"
2222
- continue
2223
- return candidates[0]["content"]["parts"][0]["text"]
2224
- elif resp.status_code == 429:
2225
- last_error = f"Key {i+1}: quota exceeded (429)"
2226
- continue
2227
- else:
2228
- last_error = f"Key {i+1}: HTTP {resp.status_code}"
2229
- continue
2230
- except requests.exceptions.Timeout:
2231
- last_error = f"Key {i+1}: timed out"
2232
- continue
2233
- except Exception as e:
2234
- last_error = f"Key {i+1}: {str(e)}"
2235
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2236
 
2237
  if "429" in last_error or "quota" in last_error:
2238
- return "⚠️ **All Gemini keys rate-limited.** Please wait a minute and try again."
2239
- return f"⚠️ **Could not process image.** ({last_error})"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2240
 
2241
 
2242
  def handle_uploaded_file(uploaded_file, user_note: str) -> str:
@@ -2615,20 +2708,33 @@ for i, msg in enumerate(st.session_state.messages):
2615
  st.code(msg["content"], language=None)
2616
 
2617
  # ════════════════════════════════════════════════════════════════════
2618
- # FILE UPLOAD β€” above chat input
2619
  # ════════════════════════════════════════════════════════════════════
2620
- uploaded_file = st.file_uploader(
2621
- "πŸ“Ž Upload a math problem (image or PDF)",
2622
- type=["jpg", "jpeg", "png", "webp", "pdf"],
2623
- label_visibility="collapsed",
2624
- help="Upload a photo or PDF of your math problem β€” Saad.AI will read and solve it"
2625
- )
 
 
 
 
 
 
 
 
 
 
 
 
2626
 
2627
  if uploaded_file:
2628
  # Use size+type as key β€” works with ANY filename including spaces/brackets
2629
  file_key = f"{uploaded_file.size}_{uploaded_file.type}"
2630
  if file_key != st.session_state.get("last_uploaded_file", ""):
2631
  st.session_state["last_uploaded_file"] = file_key
 
2632
  user_note = ""
2633
 
2634
  with st.chat_message("user", avatar="πŸ§‘β€πŸŽ“"):
@@ -2651,13 +2757,12 @@ if uploaded_file:
2651
  save_current_chat()
2652
 
2653
  # ════════════════════════════════════════════════════════════════════
2654
- # INPUT β€” use st.chat_input (cleaner than text_input + button)
2655
  # ════════════════════════════════════════════════════════════════════
2656
 
2657
  # Pre-fill from example selector
2658
  prefill = examples.get(selected, "") if selected != "-- Select --" else ""
2659
 
2660
- # st.chat_input is the clean ChatGPT-style input bar
2661
  user_input = st.chat_input(
2662
  placeholder="Type a BSc math problem... e.g. 'Solve dΒ²y/dxΒ² + 4y = cos(2x)'",
2663
  )
 
292
  ::-webkit-scrollbar-thumb { background: #1f2937; border-radius: 2px; }
293
  ::-webkit-scrollbar-thumb:hover { background: #3b82f6; }
294
 
295
+ /* ── Attachment toolbar (paperclip row above chat input) ── */
296
+ .attach-toolbar {
297
+ display: flex;
298
+ align-items: center;
299
+ gap: 0.5rem;
300
+ padding: 0.3rem 0.2rem 0.2rem 0.2rem;
301
+ }
302
+ .attach-btn {
303
+ background: transparent;
304
+ border: 1px solid #1f2937;
305
+ color: #4b5563;
306
+ border-radius: 8px;
307
+ font-size: 0.78rem;
308
+ padding: 4px 10px;
309
+ cursor: pointer;
310
+ transition: all 0.15s;
311
+ display: inline-flex;
312
+ align-items: center;
313
+ gap: 4px;
314
+ }
315
+ .attach-btn:hover {
316
+ border-color: #3b82f6;
317
+ color: #60a5fa;
318
+ background: #0f172a;
319
+ }
320
+
321
+ /* ── File uploader β€” compact popup style ── */
322
  [data-testid="stFileUploader"] {
323
  background: #111827 !important;
324
+ border: 1px solid #2a3a5e !important;
325
+ border-radius: 12px !important;
326
+ padding: 0.5rem 1rem 0.4rem 1rem !important;
327
+ margin-bottom: 0.4rem !important;
328
  transition: all 0.2s ease !important;
329
  }
330
  [data-testid="stFileUploader"]:hover {
 
336
  border: none !important;
337
  background: transparent !important;
338
  }
 
 
 
 
 
 
 
 
339
  [data-testid="stFileUploaderDropzone"] {
340
  background: transparent !important;
341
+ border: 1px dashed #2a3a5e !important;
342
+ border-radius: 8px !important;
343
+ padding: 0.5rem 0.5rem !important;
344
  min-height: 0 !important;
345
  transition: all 0.2s !important;
346
  }
347
  [data-testid="stFileUploaderDropzone"]:hover {
348
+ background: rgba(59,130,246,0.06) !important;
349
+ border-color: #3b82f6 !important;
350
  }
351
  [data-testid="stFileUploaderDropzoneInstructions"] {
352
  font-size: 0.75rem !important;
353
  color: #4b5563 !important;
354
+ padding: 0.15rem 0 !important;
355
  }
356
+ /* ── Chat input β€” always rounded ── */
357
  [data-testid="stChatInput"] {
358
+ border-radius: 14px !important;
359
+ border: 1px solid #1f2937 !important;
360
  }
361
  [data-testid="stChatInput"]:focus-within {
362
  border-color: #3b82f6 !important;
363
  }
364
+ /* Hide "Browse files" button text, keep icon feel */
365
+ [data-testid="stFileUploaderDropzone"] button {
366
+ font-size: 0.72rem !important;
367
+ padding: 3px 10px !important;
368
+ border-radius: 6px !important;
369
+ background: #0f172a !important;
370
+ border: 1px solid #2a3a5e !important;
371
+ color: #60a5fa !important;
372
+ }
373
+ /* Compact upload section toggle button */
374
+ div[data-testid="column"] .stButton > button[kind="secondary"] {
375
+ padding: 4px 10px !important;
376
+ font-size: 0.78rem !important;
377
+ width: auto !important;
378
+ border-radius: 8px !important;
379
+ }
380
  </style>
381
  """, unsafe_allow_html=True)
382
 
 
389
  if "last_submitted" not in st.session_state:
390
  st.session_state.last_submitted = ""
391
  if "chats" not in st.session_state:
392
+ st.session_state.chats = {}
393
  if "supa_loaded" not in st.session_state:
394
  st.session_state.supa_loaded = False
395
  if "current_chat_id" not in st.session_state:
396
  st.session_state.current_chat_id = None
397
+ if "show_uploader" not in st.session_state:
398
+ st.session_state.show_uploader = False
399
 
400
  # ════════════════════════════════════════════════════════════════════
401
  # SUPABASE β€” Persistent chat history
 
1835
  for m in user_msgs:
1836
  role = "user" if m["role"]=="user" else "model"
1837
  contents.append({"role": role, "parts": [{"text": m["content"]}]})
1838
+ payload = {
1839
+ "system_instruction": {"parts": [{"text": system_msg}]},
1840
+ "contents": contents,
1841
+ "generationConfig": {"maxOutputTokens": 2048, "temperature": 0.15}
1842
+ }
1843
+ # Try 2.0-flash first, fall back to 1.5-flash if model not available
1844
+ for model in ["gemini-2.0-flash", "gemini-1.5-flash"]:
1845
+ resp = requests.post(
1846
+ f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}",
1847
+ headers={"Content-Type": "application/json"},
1848
+ json=payload,
1849
+ timeout=60
1850
+ )
1851
+ if resp.status_code == 404:
1852
+ continue # model not found β€” try next model
1853
+ if not resp.ok:
1854
+ # Attach real error body to the exception so the caller can log it
1855
+ try:
1856
+ err_msg = resp.json().get("error", {}).get("message", resp.text[:100])
1857
+ except Exception:
1858
+ err_msg = resp.text[:100]
1859
+ resp._content = f"{resp.status_code}: {err_msg}".encode()
1860
+ resp.raise_for_status()
1861
+ return resp.json()["candidates"][0]["content"]["parts"][0]["text"]
1862
+ # Both models failed with 404
1863
+ raise requests.exceptions.HTTPError("Both gemini-2.0-flash and gemini-1.5-flash returned 404")
1864
 
1865
  def try_openrouter(key, messages):
1866
  resp = requests.post(
 
2194
  continue
2195
  except requests.exceptions.HTTPError as e:
2196
  code = e.response.status_code if e.response else 0
2197
+ # Include the real error body if available (set by try_gemini)
2198
+ try:
2199
+ body = e.response.text[:120] if e.response else str(e)
2200
+ except Exception:
2201
+ body = str(e)[:120]
2202
+ last_error = f"⚠️ {provider_name} HTTP {code}: {body}"
2203
  continue # always try next provider
2204
  except Exception as e:
2205
  last_error = f"⚠️ {provider_name} error: {str(e)}"
 
2207
 
2208
  # All providers exhausted
2209
  return (
2210
+ f"⚠️ **All providers failed.**\n\n"
2211
+ f"Last error: `{last_error}`\n\n"
2212
+ "Tried: 3Γ—Groq β†’ 4Γ—Gemini β†’ OpenRouter. All failed or rate-limited.\n"
2213
+ "If you see `API not enabled` β€” visit [aistudio.google.com](https://aistudio.google.com/app/apikey) and enable the Generative Language API for your account.\n"
2214
+ "If you see `quota exceeded` β€” wait 60 seconds (rate limit) or until midnight Pacific (daily limit)."
2215
  )
2216
 
2217
 
 
2251
  f"Additional note from student: {user_note if user_note else 'None'}"
2252
  )
2253
 
2254
+ # Try gemini-2.0-flash first, fall back to gemini-1.5-flash per key
2255
+ # (new Google accounts sometimes can't access 2.0 yet)
2256
+ models_to_try = ["gemini-2.0-flash", "gemini-1.5-flash"]
2257
+
2258
  last_error = ""
2259
  for i, key in enumerate(gemini_keys):
2260
  if not key.strip():
2261
  continue
2262
+ for model in models_to_try:
2263
+ try:
2264
+ resp = requests.post(
2265
+ f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}",
2266
+ headers={"Content-Type": "application/json"},
2267
+ json={
2268
+ "contents": [{
2269
+ "parts": [
2270
+ {"inline_data": {"mime_type": mime_type, "data": image_b64}},
2271
+ {"text": prompt}
2272
+ ]
2273
+ }],
2274
+ "generationConfig": {"maxOutputTokens": 2048, "temperature": 0.15}
2275
+ },
2276
+ timeout=60
2277
+ )
2278
+ if resp.status_code == 200:
2279
+ data = resp.json()
2280
+ candidates = data.get("candidates", [])
2281
+ if not candidates:
2282
+ last_error = f"Key {i+1}/{model}: blocked by safety filter"
2283
+ break # try next key
2284
+ return candidates[0]["content"]["parts"][0]["text"]
2285
+ elif resp.status_code == 429:
2286
+ last_error = f"Key {i+1}/{model}: quota exceeded (429)"
2287
+ break # rate-limited on this key β€” try next key
2288
+ elif resp.status_code in (400, 403, 404):
2289
+ # Capture the actual error body so user can diagnose
2290
+ try:
2291
+ err_detail = resp.json().get("error", {}).get("message", resp.text[:120])
2292
+ except Exception:
2293
+ err_detail = resp.text[:120]
2294
+ last_error = f"Key {i+1}/{model}: HTTP {resp.status_code} β€” {err_detail}"
2295
+ # 404 = model not found, try next model in list
2296
+ if resp.status_code == 404:
2297
+ continue # try next model
2298
+ break # 400/403 = key issue β€” try next key
2299
+ else:
2300
+ try:
2301
+ err_detail = resp.json().get("error", {}).get("message", "")[:80]
2302
+ except Exception:
2303
+ err_detail = ""
2304
+ last_error = f"Key {i+1}/{model}: HTTP {resp.status_code} {err_detail}"
2305
+ break
2306
+ except requests.exceptions.Timeout:
2307
+ last_error = f"Key {i+1}/{model}: timed out"
2308
+ break
2309
+ except Exception as e:
2310
+ last_error = f"Key {i+1}/{model}: {str(e)}"
2311
+ break
2312
 
2313
  if "429" in last_error or "quota" in last_error:
2314
+ return (
2315
+ "⚠️ **All Gemini keys are rate-limited.**\n\n"
2316
+ "Free tier resets every minute (15 req/min) and daily at midnight Pacific.\n"
2317
+ "Wait 60 seconds and try again."
2318
+ )
2319
+ if "API_KEY_INVALID" in last_error or "403" in last_error:
2320
+ return (
2321
+ f"⚠️ **Gemini key rejected.**\n\n"
2322
+ f"Error: `{last_error}`\n\n"
2323
+ "**How to fix:** Go to [Google AI Studio](https://aistudio.google.com/app/apikey), "
2324
+ "create a key, and make sure the **Generative Language API** is enabled in your "
2325
+ "Google Cloud project."
2326
+ )
2327
+ return (
2328
+ f"⚠️ **Could not process image.**\n\n"
2329
+ f"Last error: `{last_error}`\n\n"
2330
+ "If you see `API not enabled` or `permission denied`: go to "
2331
+ "[aistudio.google.com](https://aistudio.google.com/app/apikey) and enable the API for your account."
2332
+ )
2333
 
2334
 
2335
  def handle_uploaded_file(uploaded_file, user_note: str) -> str:
 
2708
  st.code(msg["content"], language=None)
2709
 
2710
  # ════════════════════════════════════════════════════════════════════
2711
+ # FILE UPLOAD β€” ChatGPT-style: paperclip toggle above input bar
2712
  # ════════════════════════════════════════════════════════════════════
2713
+
2714
+ # Toolbar row: paperclip button (left-aligned, compact)
2715
+ col_attach, col_spacer = st.columns([1, 8])
2716
+ with col_attach:
2717
+ attach_label = "πŸ“Ž Attach" if not st.session_state.show_uploader else "βœ• Close"
2718
+ if st.button(attach_label, key="attach_toggle", help="Upload image or PDF"):
2719
+ st.session_state.show_uploader = not st.session_state.show_uploader
2720
+ st.rerun()
2721
+
2722
+ # Compact uploader β€” only visible when toggled ON
2723
+ uploaded_file = None
2724
+ if st.session_state.show_uploader:
2725
+ uploaded_file = st.file_uploader(
2726
+ "Upload image or PDF of your math problem",
2727
+ type=["jpg", "jpeg", "png", "webp", "pdf"],
2728
+ label_visibility="collapsed",
2729
+ help="JPG Β· PNG Β· WEBP Β· PDF β€” max 5 MB"
2730
+ )
2731
 
2732
  if uploaded_file:
2733
  # Use size+type as key β€” works with ANY filename including spaces/brackets
2734
  file_key = f"{uploaded_file.size}_{uploaded_file.type}"
2735
  if file_key != st.session_state.get("last_uploaded_file", ""):
2736
  st.session_state["last_uploaded_file"] = file_key
2737
+ st.session_state.show_uploader = False # auto-close after upload
2738
  user_note = ""
2739
 
2740
  with st.chat_message("user", avatar="πŸ§‘β€πŸŽ“"):
 
2757
  save_current_chat()
2758
 
2759
  # ════════════════════════════════════════════════════════════════════
2760
+ # INPUT β€” ChatGPT-style input bar
2761
  # ════════════════════════════════════════════════════════════════════
2762
 
2763
  # Pre-fill from example selector
2764
  prefill = examples.get(selected, "") if selected != "-- Select --" else ""
2765
 
 
2766
  user_input = st.chat_input(
2767
  placeholder="Type a BSc math problem... e.g. 'Solve dΒ²y/dxΒ² + 4y = cos(2x)'",
2768
  )