saad-sust commited on
Commit
e459963
·
verified ·
1 Parent(s): 2354601

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +190 -56
app.py CHANGED
@@ -383,7 +383,6 @@ div[data-testid="column"] .stButton > button[kind="secondary"] {
383
  # ── Session state ────────────────────────────────────────────────────
384
  import datetime as _dt
385
 
386
- # ── Session state ────────────────────────────────────────────────
387
  if "messages" not in st.session_state:
388
  st.session_state.messages = []
389
  if "last_submitted" not in st.session_state:
@@ -402,6 +401,13 @@ if "pending_file_name" not in st.session_state:
402
  st.session_state.pending_file_name = None
403
  if "pending_file_mime" not in st.session_state:
404
  st.session_state.pending_file_mime = None
 
 
 
 
 
 
 
405
 
406
  # ════════════════════════════════════════════════════════════════════
407
  # SUPABASE — Persistent chat history
@@ -479,6 +485,10 @@ def new_chat():
479
  st.session_state.current_chat_id = chat_id
480
  st.session_state.messages = []
481
  st.session_state.last_submitted = ""
 
 
 
 
482
 
483
  def save_current_chat():
484
  """Save current messages to session state and Supabase."""
@@ -491,7 +501,7 @@ def save_current_chat():
491
  st.session_state.chats[cid] = {
492
  "title": title,
493
  "messages": list(st.session_state.messages),
494
- "created": _dt.datetime.now().strftime("%d %b %H:%M")
495
  }
496
  # Save to Supabase (persistent)
497
  supa_save_chat(cid, title, list(st.session_state.messages))
@@ -502,6 +512,10 @@ def load_chat(chat_id):
502
  st.session_state.current_chat_id = chat_id
503
  st.session_state.messages = list(st.session_state.chats[chat_id]["messages"])
504
  st.session_state.last_submitted = ""
 
 
 
 
505
 
506
 
507
  # ════════════════════════════════════════════════════════════════════
@@ -2246,8 +2260,8 @@ def ask_gemini_vision(image_b64: str, mime_type: str, user_note: str) -> str:
2246
  doc = fitz.open(stream=pdf_bytes, filetype="pdf")
2247
  # Render all pages (up to 4) as one tall PNG
2248
  imgs = []
2249
- for page_num in range(min(len(doc), 4)):
2250
- pix = doc[page_num].get_pixmap(matrix=fitz.Matrix(2, 2))
2251
  imgs.append(pix.tobytes("png"))
2252
  doc.close()
2253
  # Stack page images vertically using PIL if available, else just use first page
@@ -2276,18 +2290,22 @@ def ask_gemini_vision(image_b64: str, mime_type: str, user_note: str) -> str:
2276
 
2277
  prompt = (
2278
  "You are Saad.AI, a BSc Mathematics assistant built by Saad.\n"
2279
- "The student has uploaded a file containing a math problem.\n\n"
2280
- "YOUR TASKS:\n"
2281
- "1. Read and extract the math problem from the file exactly.\n"
2282
- "2. State what the problem is clearly.\n"
2283
- "3. Solve it step by step:\n"
2284
- " 🔍 **Given:** ...\n"
2285
- " 📌 **Method:** ...\n"
2286
- " 🧮 **Step 1:** ...\n"
2287
- " **Final Answer:** $$\\boxed{answer}$$\n"
2288
- "4. ALL math must be in LaTeX — never plain text math.\n"
2289
- "5. If the file is unclear or not math-related, say so politely.\n\n"
2290
- f"Student's question: {user_note if user_note else 'Solve this problem completely.'}"
 
 
 
 
2291
  )
2292
 
2293
  errors = []
@@ -2569,6 +2587,35 @@ with st.sidebar:
2569
 
2570
  st.divider()
2571
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2572
  # ── Chat History ─────────────────────────────────────────────
2573
  if st.session_state.chats:
2574
  st.markdown("**💬 Chat History**")
@@ -2662,8 +2709,7 @@ with st.sidebar:
2662
 
2663
  st.divider()
2664
  if st.button("🗑️ Clear Chat", use_container_width=True):
2665
- st.session_state.messages = []
2666
- st.session_state.last_submitted = ""
2667
  st.rerun()
2668
 
2669
 
@@ -2830,47 +2876,68 @@ for i, msg in enumerate(st.session_state.messages):
2830
  st.code(msg["content"], language=None)
2831
 
2832
  # ════════════════════════════════════════════════════════════════════
2833
- # FILE UPLOADTrue ChatGPT-style: attach file THEN type question
 
2834
  # ════════════════════════════════════════════════════════════════════
2835
 
2836
- # Toolbar row: paperclip button (left-aligned, compact)
2837
- col_attach, col_spacer = st.columns([1, 8])
2838
- with col_attach:
2839
- attach_label = "📎 Attach" if not st.session_state.show_uploader else "✕ Close"
2840
- if st.button(attach_label, key="attach_toggle", help="Upload image or PDF"):
2841
- st.session_state.show_uploader = not st.session_state.show_uploader
2842
- st.rerun()
2843
-
2844
- # Compact uploaderonly visible when toggled ON
2845
- if st.session_state.show_uploader:
2846
- uploaded_file = st.file_uploader(
2847
- "Upload image or PDF of your math problem",
2848
- type=["jpg", "jpeg", "png", "webp", "pdf"],
2849
- label_visibility="collapsed",
2850
- help="JPG · PNG · WEBP · PDF — max 5 MB"
2851
- )
2852
- if uploaded_file:
2853
- file_key = f"{uploaded_file.size}_{uploaded_file.type}"
2854
- if file_key != st.session_state.get("last_uploaded_file", ""):
2855
- st.session_state["last_uploaded_file"] = file_key
2856
- # Store file bytes in session state — wait for user's typed question
2857
- st.session_state.pending_file_bytes = uploaded_file.read()
2858
- st.session_state.pending_file_name = uploaded_file.name
2859
- st.session_state.pending_file_mime = uploaded_file.type or "application/octet-stream"
2860
- st.session_state.show_uploader = False
2861
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2862
 
2863
- # Show attached file pill above input bar so user knows it's ready
2864
- if st.session_state.pending_file_bytes is not None:
2865
- col_pill, col_x = st.columns([9, 1])
2866
- with col_pill:
2867
- st.info(f"📎 **{st.session_state.pending_file_name}** attached type your question below and press Enter")
2868
- with col_x:
2869
- if st.button("✕", key="remove_pending", help="Remove attached file"):
2870
  st.session_state.pending_file_bytes = None
2871
  st.session_state.pending_file_name = None
2872
  st.session_state.pending_file_mime = None
2873
  st.rerun()
 
 
 
 
 
 
 
 
2874
 
2875
  # ════════════════════════════════════════════════════════════════════
2876
  # INPUT — ChatGPT-style input bar
@@ -2880,7 +2947,7 @@ if st.session_state.pending_file_bytes is not None:
2880
  prefill = examples.get(selected, "") if selected != "-- Select --" else ""
2881
 
2882
  user_input = st.chat_input(
2883
- placeholder="Type a BSc math problem... e.g. 'Solve d²y/dx² + 4y = cos(2x)'",
2884
  )
2885
 
2886
  # Also allow clicking an example to submit it directly
@@ -2897,7 +2964,7 @@ else:
2897
  if problem and problem != st.session_state.last_submitted:
2898
  st.session_state.last_submitted = problem
2899
 
2900
- # ── If a file is attached, send file + question to Gemini Vision ─
2901
  if st.session_state.pending_file_bytes is not None:
2902
  import base64
2903
 
@@ -2905,16 +2972,52 @@ if problem and problem != st.session_state.last_submitted:
2905
  file_name = st.session_state.pending_file_name
2906
  file_mime = st.session_state.pending_file_mime
2907
 
2908
- # Clear pending file immediately so it's not re-sent on rerun
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2909
  st.session_state.pending_file_bytes = None
2910
  st.session_state.pending_file_name = None
2911
  st.session_state.pending_file_mime = None
 
 
 
2912
 
2913
  with st.chat_message("user", avatar="🧑‍🎓"):
2914
  st.markdown(f"📎 **{file_name}** — {problem}")
 
 
 
 
 
 
 
 
 
 
 
 
 
2915
 
2916
  with st.chat_message("assistant", avatar="📐"):
2917
- with st.spinner("📖 Reading your file with Gemini..."):
2918
  image_b64 = base64.b64encode(file_bytes).decode("utf-8")
2919
  answer = ask_gemini_vision(image_b64, file_mime, problem)
2920
  st.markdown(answer)
@@ -2931,6 +3034,37 @@ if problem and problem != st.session_state.last_submitted:
2931
  save_current_chat()
2932
  st.stop()
2933
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2934
  # ── Detect casual / non-math messages ───────────────────────────
2935
  p_lower = problem.lower().strip()
2936
  casual_keywords = [
 
383
  # ── Session state ────────────────────────────────────────────────────
384
  import datetime as _dt
385
 
 
386
  if "messages" not in st.session_state:
387
  st.session_state.messages = []
388
  if "last_submitted" not in st.session_state:
 
401
  st.session_state.pending_file_name = None
402
  if "pending_file_mime" not in st.session_state:
403
  st.session_state.pending_file_mime = None
404
+ # ── Attached file kept in memory for follow-up questions ─────────
405
+ if "attached_file_bytes" not in st.session_state:
406
+ st.session_state.attached_file_bytes = None
407
+ if "attached_file_name" not in st.session_state:
408
+ st.session_state.attached_file_name = None
409
+ if "attached_file_mime" not in st.session_state:
410
+ st.session_state.attached_file_mime = None
411
 
412
  # ════════════════════════════════════════════════════════════════════
413
  # SUPABASE — Persistent chat history
 
485
  st.session_state.current_chat_id = chat_id
486
  st.session_state.messages = []
487
  st.session_state.last_submitted = ""
488
+ # Clear any attached file memory from previous chat
489
+ st.session_state.attached_file_bytes = None
490
+ st.session_state.attached_file_name = None
491
+ st.session_state.attached_file_mime = None
492
 
493
  def save_current_chat():
494
  """Save current messages to session state and Supabase."""
 
501
  st.session_state.chats[cid] = {
502
  "title": title,
503
  "messages": list(st.session_state.messages),
504
+ "created": _dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S") # ISO-sortable
505
  }
506
  # Save to Supabase (persistent)
507
  supa_save_chat(cid, title, list(st.session_state.messages))
 
512
  st.session_state.current_chat_id = chat_id
513
  st.session_state.messages = list(st.session_state.chats[chat_id]["messages"])
514
  st.session_state.last_submitted = ""
515
+ # Clear attached file memory — it belongs to a different chat session
516
+ st.session_state.attached_file_bytes = None
517
+ st.session_state.attached_file_name = None
518
+ st.session_state.attached_file_mime = None
519
 
520
 
521
  # ════════════════════════════════════════════════════════════════════
 
2260
  doc = fitz.open(stream=pdf_bytes, filetype="pdf")
2261
  # Render all pages (up to 4) as one tall PNG
2262
  imgs = []
2263
+ for page_num in range(min(len(doc), 6)):
2264
+ pix = doc[page_num].get_pixmap(matrix=fitz.Matrix(3, 3)) # 3x zoom for crisp text
2265
  imgs.append(pix.tobytes("png"))
2266
  doc.close()
2267
  # Stack page images vertically using PIL if available, else just use first page
 
2290
 
2291
  prompt = (
2292
  "You are Saad.AI, a BSc Mathematics assistant built by Saad.\n"
2293
+ "The student has uploaded a file that may contain MULTIPLE math problems.\n\n"
2294
+ "CRITICAL RULES:\n"
2295
+ "- Read the ENTIRE document carefully from top to bottom.\n"
2296
+ "- Extract EVERY question/problem you see — do NOT skip any.\n"
2297
+ "- Do NOT invent or make up questions. ONLY solve what is written in the file.\n"
2298
+ "- If you cannot read part of the text, say so explicitly.\n\n"
2299
+ "FOR EACH PROBLEM FOUND, use this structure:\n"
2300
+ "---\n"
2301
+ "### Question [N]: [restate the exact question from the file]\n"
2302
+ "🔍 **Given:** ...\n"
2303
+ "📌 **Method:** ...\n"
2304
+ "🧮 **Step 1:** ...\n"
2305
+ "✅ **Final Answer:** $$\\boxed{answer}$$\n"
2306
+ "---\n\n"
2307
+ "ALL math must be in LaTeX — never plain text math.\n\n"
2308
+ f"Student's specific instruction: {user_note if user_note else 'Read the full file and solve ALL problems you find.'}"
2309
  )
2310
 
2311
  errors = []
 
2587
 
2588
  st.divider()
2589
 
2590
+ # ── File Attach — always visible in sidebar ───────────────────
2591
+ st.markdown("**📎 Attach File**")
2592
+ st.caption("Image or PDF with math problems")
2593
+ sidebar_file = st.file_uploader(
2594
+ "sidebar_upload",
2595
+ type=["jpg", "jpeg", "png", "webp", "pdf"],
2596
+ label_visibility="collapsed",
2597
+ help="JPG · PNG · WEBP · PDF — max 5 MB",
2598
+ key="sidebar_uploader"
2599
+ )
2600
+ if sidebar_file:
2601
+ file_key = f"{sidebar_file.size}_{sidebar_file.type}"
2602
+ if file_key != st.session_state.get("last_uploaded_file", ""):
2603
+ st.session_state["last_uploaded_file"] = file_key
2604
+ st.session_state.pending_file_bytes = sidebar_file.read()
2605
+ st.session_state.pending_file_name = sidebar_file.name
2606
+ st.session_state.pending_file_mime = sidebar_file.type or "application/octet-stream"
2607
+ st.rerun()
2608
+ if st.session_state.pending_file_bytes is not None:
2609
+ st.success(f"✅ **{st.session_state.pending_file_name}**\nType your question in chat ↓")
2610
+ if st.button("✕ Remove file", use_container_width=True, key="sidebar_remove"):
2611
+ st.session_state.pending_file_bytes = None
2612
+ st.session_state.pending_file_name = None
2613
+ st.session_state.pending_file_mime = None
2614
+ st.session_state["last_uploaded_file"] = "" # allow re-uploading the same file
2615
+ st.rerun()
2616
+
2617
+ st.divider()
2618
+
2619
  # ── Chat History ─────────────────────────────────────────────
2620
  if st.session_state.chats:
2621
  st.markdown("**💬 Chat History**")
 
2709
 
2710
  st.divider()
2711
  if st.button("🗑️ Clear Chat", use_container_width=True):
2712
+ new_chat()
 
2713
  st.rerun()
2714
 
2715
 
 
2876
  st.code(msg["content"], language=None)
2877
 
2878
  # ════════════════════════════════════════════════════════════════════
2879
+ # FILE ATTACH STATUS fixed bar just above the chat input
2880
+ # (uploader lives in the sidebar — always visible, never scrolls away)
2881
  # ════════════════════════════════════════════════════════════════════
2882
 
2883
+ # ── Always-visible fixed attach bar above the chat input ─────────
2884
+ # Shows three states: no file / file pending / file in memory
2885
+ _pending_name = st.session_state.pending_file_bytes is not None
2886
+ _memory_name = st.session_state.attached_file_name
2887
+
2888
+ if _pending_name:
2889
+ _bar_color = "#1a2e1a"
2890
+ _bar_border = "#22c55e"
2891
+ _bar_text = f"✅ <strong>{st.session_state.pending_file_name}</strong> ready type your question below and press Enter"
2892
+ elif _memory_name:
2893
+ _bar_color = "#1a1f2e"
2894
+ _bar_border = "#3b82f6"
2895
+ _bar_text = f"📎 <strong>{_memory_name}</strong> in memory — ask a follow-up or attach a new file via sidebar"
2896
+ else:
2897
+ _bar_color = "#111"
2898
+ _bar_border = "#374151"
2899
+ _bar_text = "📎 Attach a file: open the <strong>sidebar ←</strong> to upload an image or PDF"
2900
+
2901
+ st.markdown(
2902
+ f"""
2903
+ <div style="
2904
+ position: fixed;
2905
+ bottom: 68px;
2906
+ left: 50%;
2907
+ transform: translateX(-50%);
2908
+ width: 720px;
2909
+ max-width: 88vw;
2910
+ background: {_bar_color};
2911
+ border: 1px solid {_bar_border};
2912
+ border-radius: 10px;
2913
+ padding: 7px 16px;
2914
+ z-index: 9999;
2915
+ font-size: 0.80rem;
2916
+ color: #d1d5db;
2917
+ ">
2918
+ {_bar_text}
2919
+ </div>
2920
+ """,
2921
+ unsafe_allow_html=True
2922
+ )
2923
 
2924
+ # Remove-file buttons (in-page, shown only when file exists)
2925
+ if _pending_name:
2926
+ col_sp, col_rm = st.columns([11, 1])
2927
+ with col_rm:
2928
+ if st.button("✕", key="remove_pending", help="Remove pending file"):
 
 
2929
  st.session_state.pending_file_bytes = None
2930
  st.session_state.pending_file_name = None
2931
  st.session_state.pending_file_mime = None
2932
  st.rerun()
2933
+ elif _memory_name:
2934
+ col_sp, col_rm = st.columns([11, 1])
2935
+ with col_rm:
2936
+ if st.button("✕", key="clear_memory_file", help="Remove file from memory"):
2937
+ st.session_state.attached_file_bytes = None
2938
+ st.session_state.attached_file_name = None
2939
+ st.session_state.attached_file_mime = None
2940
+ st.rerun()
2941
 
2942
  # ════════════════════════════════════════════════════════════════════
2943
  # INPUT — ChatGPT-style input bar
 
2947
  prefill = examples.get(selected, "") if selected != "-- Select --" else ""
2948
 
2949
  user_input = st.chat_input(
2950
+ placeholder="Type a math problem... or attach a file in the sidebar ← then ask here",
2951
  )
2952
 
2953
  # Also allow clicking an example to submit it directly
 
2964
  if problem and problem != st.session_state.last_submitted:
2965
  st.session_state.last_submitted = problem
2966
 
2967
+ # ── If a NEW file is attached, send file + question to Vision ────
2968
  if st.session_state.pending_file_bytes is not None:
2969
  import base64
2970
 
 
2972
  file_name = st.session_state.pending_file_name
2973
  file_mime = st.session_state.pending_file_mime
2974
 
2975
+ # ── Validate size (5 MB limit) ────────────────────────────────
2976
+ MAX_FILE_SIZE = 5 * 1024 * 1024
2977
+ if len(file_bytes) > MAX_FILE_SIZE:
2978
+ st.session_state.pending_file_bytes = None
2979
+ st.session_state.pending_file_name = None
2980
+ st.session_state.pending_file_mime = None
2981
+ st.session_state["last_uploaded_file"] = ""
2982
+ st.error(f"⚠️ File too large ({len(file_bytes)//1024} KB). Please upload under 5 MB.")
2983
+ st.stop()
2984
+
2985
+ # ── Validate MIME type ────────────────────────────────────────
2986
+ _allowed_mimes = {"image/jpeg", "image/png", "image/webp", "application/pdf"}
2987
+ if file_mime not in _allowed_mimes:
2988
+ st.session_state.pending_file_bytes = None
2989
+ st.session_state.pending_file_name = None
2990
+ st.session_state.pending_file_mime = None
2991
+ st.session_state["last_uploaded_file"] = ""
2992
+ st.error("⚠️ Unsupported format. Please upload JPG, PNG, WEBP or PDF.")
2993
+ st.stop()
2994
+
2995
+ # Clear pending (one-time) but keep in attached memory for follow-ups
2996
  st.session_state.pending_file_bytes = None
2997
  st.session_state.pending_file_name = None
2998
  st.session_state.pending_file_mime = None
2999
+ st.session_state.attached_file_bytes = file_bytes
3000
+ st.session_state.attached_file_name = file_name
3001
+ st.session_state.attached_file_mime = file_mime
3002
 
3003
  with st.chat_message("user", avatar="🧑‍🎓"):
3004
  st.markdown(f"📎 **{file_name}** — {problem}")
3005
+ # Show file preview so user can see what was attached
3006
+ if file_mime and file_mime.startswith("image/"):
3007
+ st.image(file_bytes, caption=file_name, use_container_width=True)
3008
+ else:
3009
+ # PDF — try to show first page
3010
+ try:
3011
+ import fitz, io
3012
+ doc = fitz.open(stream=file_bytes, filetype="pdf")
3013
+ pix = doc[0].get_pixmap(matrix=fitz.Matrix(1.5, 1.5))
3014
+ doc.close()
3015
+ st.image(pix.tobytes("png"), caption=f"📄 {file_name} (page 1 preview)", use_container_width=True)
3016
+ except Exception:
3017
+ st.caption(f"📄 {file_name}")
3018
 
3019
  with st.chat_message("assistant", avatar="📐"):
3020
+ with st.spinner("📖 Reading your file..."):
3021
  image_b64 = base64.b64encode(file_bytes).decode("utf-8")
3022
  answer = ask_gemini_vision(image_b64, file_mime, problem)
3023
  st.markdown(answer)
 
3034
  save_current_chat()
3035
  st.stop()
3036
 
3037
+ # ── Follow-up about previously attached file ──────────────────────
3038
+ # Detects "solve q3", "next question", "question 2" etc. and re-sends the file
3039
+ _p = problem.lower()
3040
+ _followup_triggers = [
3041
+ "question", "solve q", "q1","q2","q3","q4","q5","q6","q7","q8","q9","q10",
3042
+ "next one", "next question", "next qus", "next ques",
3043
+ "previous", "solve the next", "solve all", "solve rest",
3044
+ "number ", "no.", "no ", "#", "part ", "part(", "section",
3045
+ ]
3046
+ _is_file_followup = (
3047
+ st.session_state.attached_file_bytes is not None and
3048
+ any(t in _p for t in _followup_triggers)
3049
+ )
3050
+ if _is_file_followup:
3051
+ import base64 as _b64_fu
3052
+ with st.chat_message("user", avatar="🧑‍🎓"):
3053
+ st.markdown(f"📎 *{st.session_state.attached_file_name}* — {problem}")
3054
+ with st.chat_message("assistant", avatar="📐"):
3055
+ with st.spinner("📖 Re-reading your file..."):
3056
+ _fb64 = _b64_fu.b64encode(st.session_state.attached_file_bytes).decode("utf-8")
3057
+ answer = ask_gemini_vision(_fb64, st.session_state.attached_file_mime, problem)
3058
+ st.markdown(answer)
3059
+ with st.expander("📋 Copy"):
3060
+ st.code(answer, language=None)
3061
+ if not st.session_state.current_chat_id:
3062
+ new_chat()
3063
+ st.session_state.messages.append({"role": "user", "content": f"📎 {st.session_state.attached_file_name} — {problem}"})
3064
+ st.session_state.messages.append({"role": "assistant", "content": answer})
3065
+ save_current_chat()
3066
+ st.stop()
3067
+
3068
  # ── Detect casual / non-math messages ───────────────────────────
3069
  p_lower = problem.lower().strip()
3070
  casual_keywords = [