Update app.py
Browse files
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 %
|
| 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),
|
| 2250 |
-
pix = doc[page_num].get_pixmap(matrix=fitz.Matrix(
|
| 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
|
| 2280 |
-
"
|
| 2281 |
-
"
|
| 2282 |
-
"
|
| 2283 |
-
"
|
| 2284 |
-
"
|
| 2285 |
-
"
|
| 2286 |
-
"
|
| 2287 |
-
"
|
| 2288 |
-
"
|
| 2289 |
-
"
|
| 2290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
|
|
|
| 2834 |
# ════════════════════════════════════════════════════════════════════
|
| 2835 |
|
| 2836 |
-
#
|
| 2837 |
-
|
| 2838 |
-
|
| 2839 |
-
|
| 2840 |
-
|
| 2841 |
-
|
| 2842 |
-
|
| 2843 |
-
|
| 2844 |
-
|
| 2845 |
-
|
| 2846 |
-
|
| 2847 |
-
|
| 2848 |
-
|
| 2849 |
-
|
| 2850 |
-
|
| 2851 |
-
|
| 2852 |
-
|
| 2853 |
-
|
| 2854 |
-
|
| 2855 |
-
|
| 2856 |
-
|
| 2857 |
-
|
| 2858 |
-
|
| 2859 |
-
|
| 2860 |
-
|
| 2861 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2862 |
|
| 2863 |
-
#
|
| 2864 |
-
if
|
| 2865 |
-
|
| 2866 |
-
with
|
| 2867 |
-
st.
|
| 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
|
| 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
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 = [
|