Update app.py
Browse files
app.py
CHANGED
|
@@ -393,8 +393,6 @@ if "supa_loaded" not in st.session_state:
|
|
| 393 |
st.session_state.supa_loaded = False
|
| 394 |
if "current_chat_id" not in st.session_state:
|
| 395 |
st.session_state.current_chat_id = None
|
| 396 |
-
if "show_uploader" not in st.session_state:
|
| 397 |
-
st.session_state.show_uploader = False
|
| 398 |
if "pending_file_bytes" not in st.session_state:
|
| 399 |
st.session_state.pending_file_bytes = None
|
| 400 |
if "pending_file_name" not in st.session_state:
|
|
@@ -2289,23 +2287,36 @@ def ask_gemini_vision(image_b64: str, mime_type: str, user_note: str) -> str:
|
|
| 2289 |
pass # Conversion failed — fall through to Gemini
|
| 2290 |
|
| 2291 |
prompt = (
|
| 2292 |
-
"You are Saad.AI, a
|
| 2293 |
-
"
|
| 2294 |
-
|
| 2295 |
-
"
|
| 2296 |
-
"
|
| 2297 |
-
|
| 2298 |
-
"
|
| 2299 |
-
"
|
| 2300 |
-
"
|
| 2301 |
-
"
|
| 2302 |
-
"
|
| 2303 |
-
"
|
| 2304 |
-
"
|
| 2305 |
-
"
|
| 2306 |
-
"
|
| 2307 |
-
"
|
| 2308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2309 |
)
|
| 2310 |
|
| 2311 |
errors = []
|
|
@@ -2587,35 +2598,6 @@ with st.sidebar:
|
|
| 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**")
|
|
@@ -2876,30 +2858,93 @@ for i, msg in enumerate(st.session_state.messages):
|
|
| 2876 |
st.code(msg["content"], language=None)
|
| 2877 |
|
| 2878 |
# ════════════════════════════════════════════════════════════════════
|
| 2879 |
-
# FILE ATTACH
|
| 2880 |
-
#
|
| 2881 |
# ════════════════════════════════════════════════════════════════════
|
| 2882 |
|
| 2883 |
-
# ──
|
| 2884 |
-
|
| 2885 |
-
|
| 2886 |
-
|
| 2887 |
-
|
| 2888 |
-
|
| 2889 |
-
|
| 2890 |
-
|
| 2891 |
-
|
| 2892 |
-
|
| 2893 |
-
|
| 2894 |
-
|
| 2895 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2896 |
else:
|
| 2897 |
-
|
| 2898 |
-
|
| 2899 |
-
|
|
|
|
| 2900 |
|
|
|
|
| 2901 |
st.markdown(
|
| 2902 |
f"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2903 |
<div style="
|
| 2904 |
position: fixed;
|
| 2905 |
bottom: 68px;
|
|
@@ -2907,33 +2952,57 @@ st.markdown(
|
|
| 2907 |
transform: translateX(-50%);
|
| 2908 |
width: 720px;
|
| 2909 |
max-width: 88vw;
|
| 2910 |
-
|
| 2911 |
-
|
| 2912 |
-
|
| 2913 |
-
padding: 7px 16px;
|
| 2914 |
z-index: 9999;
|
| 2915 |
-
font-size: 0.80rem;
|
| 2916 |
-
color: #d1d5db;
|
| 2917 |
">
|
| 2918 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2919 |
</div>
|
| 2920 |
""",
|
| 2921 |
unsafe_allow_html=True
|
| 2922 |
)
|
| 2923 |
|
| 2924 |
-
#
|
| 2925 |
-
if
|
| 2926 |
col_sp, col_rm = st.columns([11, 1])
|
| 2927 |
with col_rm:
|
| 2928 |
-
if st.button("✕", key="remove_pending", help="Remove
|
| 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
|
| 2934 |
col_sp, col_rm = st.columns([11, 1])
|
| 2935 |
with col_rm:
|
| 2936 |
-
if st.button("✕", key="clear_memory_file", help="
|
| 2937 |
st.session_state.attached_file_bytes = None
|
| 2938 |
st.session_state.attached_file_name = None
|
| 2939 |
st.session_state.attached_file_mime = None
|
|
@@ -3004,7 +3073,7 @@ if problem and problem != st.session_state.last_submitted:
|
|
| 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,
|
| 3008 |
else:
|
| 3009 |
# PDF — try to show first page
|
| 3010 |
try:
|
|
@@ -3012,7 +3081,7 @@ if problem and problem != st.session_state.last_submitted:
|
|
| 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)",
|
| 3016 |
except Exception:
|
| 3017 |
st.caption(f"📄 {file_name}")
|
| 3018 |
|
|
|
|
| 393 |
st.session_state.supa_loaded = False
|
| 394 |
if "current_chat_id" not in st.session_state:
|
| 395 |
st.session_state.current_chat_id = None
|
|
|
|
|
|
|
| 396 |
if "pending_file_bytes" not in st.session_state:
|
| 397 |
st.session_state.pending_file_bytes = None
|
| 398 |
if "pending_file_name" not in st.session_state:
|
|
|
|
| 2287 |
pass # Conversion failed — fall through to Gemini
|
| 2288 |
|
| 2289 |
prompt = (
|
| 2290 |
+
"You are Saad.AI, a helpful AI assistant built by Saad.\n"
|
| 2291 |
+
"You can read and understand ALL types of images and documents.\n\n"
|
| 2292 |
+
|
| 2293 |
+
"STEP 1 — Look at the image carefully from top to bottom.\n"
|
| 2294 |
+
"STEP 2 — Identify what type of content is in the image:\n\n"
|
| 2295 |
+
|
| 2296 |
+
"━━━ CASE A: IMAGE CONTAINS MATH PROBLEMS ━━━\n"
|
| 2297 |
+
"(Equations, exam paper, homework sheet, math diagrams, numbered questions)\n"
|
| 2298 |
+
"→ Read the ENTIRE document. Extract EVERY question — do NOT skip any.\n"
|
| 2299 |
+
"→ Do NOT invent questions. ONLY solve what is actually written.\n"
|
| 2300 |
+
"→ For EACH problem use this structure:\n"
|
| 2301 |
+
" ---\n"
|
| 2302 |
+
" ### Question [N]: [restate exact question from file]\n"
|
| 2303 |
+
" 🔍 **Given:** ...\n"
|
| 2304 |
+
" 📌 **Method:** ...\n"
|
| 2305 |
+
" 🧮 **Step 1:** ...\n"
|
| 2306 |
+
" ✅ **Final Answer:** $$\\boxed{answer}$$\n"
|
| 2307 |
+
" ---\n"
|
| 2308 |
+
"→ ALL math must be in LaTeX — never plain text math.\n\n"
|
| 2309 |
+
|
| 2310 |
+
"━━━ CASE B: IMAGE IS NOT A MATH PROBLEM ━━━\n"
|
| 2311 |
+
"(Photo, screenshot, diagram, chart, meme, nature, objects, people, text, etc.)\n"
|
| 2312 |
+
"→ Describe the image in detail — what you see, what it shows.\n"
|
| 2313 |
+
"→ Be conversational and helpful like ChatGPT or Claude.\n"
|
| 2314 |
+
"→ Answer the student's specific question about the image.\n"
|
| 2315 |
+
"→ No forced math structure. Just natural, helpful conversation.\n"
|
| 2316 |
+
"→ Point out interesting details, context, or meaning.\n\n"
|
| 2317 |
+
|
| 2318 |
+
f"Student's instruction: {user_note if user_note else 'Look at this image and describe or analyze it.'}\n\n"
|
| 2319 |
+
"Always be helpful, friendly, and clear."
|
| 2320 |
)
|
| 2321 |
|
| 2322 |
errors = []
|
|
|
|
| 2598 |
|
| 2599 |
st.divider()
|
| 2600 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2601 |
# ── Chat History ─────────────────────────────────────────────
|
| 2602 |
if st.session_state.chats:
|
| 2603 |
st.markdown("**💬 Chat History**")
|
|
|
|
| 2858 |
st.code(msg["content"], language=None)
|
| 2859 |
|
| 2860 |
# ════════════════════════════════════════════════════════════════════
|
| 2861 |
+
# FILE ATTACH — ChatGPT-style: 📎 button right next to the send bar
|
| 2862 |
+
# The file uploader is hidden in the DOM; JS click triggers it.
|
| 2863 |
# ════════════════════════════════════════════════════════════════════
|
| 2864 |
|
| 2865 |
+
# ── Hidden uploader — rendered but invisible, triggered by JS ────
|
| 2866 |
+
main_uploader = st.file_uploader(
|
| 2867 |
+
"main_attach",
|
| 2868 |
+
type=["jpg", "jpeg", "png", "webp", "pdf"],
|
| 2869 |
+
label_visibility="collapsed",
|
| 2870 |
+
key="main_uploader"
|
| 2871 |
+
)
|
| 2872 |
+
if main_uploader:
|
| 2873 |
+
_fkey = f"{main_uploader.size}_{main_uploader.type}"
|
| 2874 |
+
if _fkey != st.session_state.get("last_uploaded_file", ""):
|
| 2875 |
+
st.session_state["last_uploaded_file"] = _fkey
|
| 2876 |
+
st.session_state.pending_file_bytes = main_uploader.read()
|
| 2877 |
+
st.session_state.pending_file_name = main_uploader.name
|
| 2878 |
+
st.session_state.pending_file_mime = main_uploader.type or "application/octet-stream"
|
| 2879 |
+
st.rerun()
|
| 2880 |
+
|
| 2881 |
+
# ── Status values ─────────────────────────────────────────────────
|
| 2882 |
+
_pending = st.session_state.pending_file_bytes is not None
|
| 2883 |
+
_memfile = st.session_state.attached_file_name
|
| 2884 |
+
|
| 2885 |
+
if _pending:
|
| 2886 |
+
_fname = st.session_state.pending_file_name
|
| 2887 |
+
_status = f"✅ <b>{_fname}</b> — ready · type your question and press Enter"
|
| 2888 |
+
_bcolor = "#0d2318"
|
| 2889 |
+
_bborder = "#22c55e"
|
| 2890 |
+
_tcolor = "#86efac"
|
| 2891 |
+
elif _memfile:
|
| 2892 |
+
_status = f"📎 <b>{_memfile}</b> in memory — ask a follow-up or click 📎 to attach new"
|
| 2893 |
+
_bcolor = "#0d1a2e"
|
| 2894 |
+
_bborder = "#3b82f6"
|
| 2895 |
+
_tcolor = "#93c5fd"
|
| 2896 |
else:
|
| 2897 |
+
_status = "Click <b>📎</b> below to attach an image or PDF"
|
| 2898 |
+
_bcolor = "#111827"
|
| 2899 |
+
_bborder = "#2d3748"
|
| 2900 |
+
_tcolor = "#6b7280"
|
| 2901 |
|
| 2902 |
+
# ── Fixed bar: [📎 button] [status text] [✕ if file exists] ──────
|
| 2903 |
st.markdown(
|
| 2904 |
f"""
|
| 2905 |
+
<style>
|
| 2906 |
+
/* Hide the Streamlit file-uploader widget — triggered via JS */
|
| 2907 |
+
[data-testid="stFileUploader"] {{
|
| 2908 |
+
visibility: hidden !important;
|
| 2909 |
+
height: 0 !important;
|
| 2910 |
+
min-height: 0 !important;
|
| 2911 |
+
overflow: hidden !important;
|
| 2912 |
+
margin: 0 !important;
|
| 2913 |
+
padding: 0 !important;
|
| 2914 |
+
position: absolute !important;
|
| 2915 |
+
pointer-events: none !important;
|
| 2916 |
+
}}
|
| 2917 |
+
/* Keep the actual <input type=file> reachable by JS */
|
| 2918 |
+
[data-testid="stFileUploaderDropzoneInput"] {{
|
| 2919 |
+
visibility: visible !important;
|
| 2920 |
+
pointer-events: auto !important;
|
| 2921 |
+
}}
|
| 2922 |
+
/* Hide the Streamlit ✕ clear-buttons below — they're fired by JS */
|
| 2923 |
+
div[data-testid="stHorizontalBlock"]:has(button[kind="secondary"]) {{
|
| 2924 |
+
visibility: hidden !important;
|
| 2925 |
+
height: 0 !important;
|
| 2926 |
+
overflow: hidden !important;
|
| 2927 |
+
margin: 0 !important;
|
| 2928 |
+
padding: 0 !important;
|
| 2929 |
+
position: absolute !important;
|
| 2930 |
+
}}
|
| 2931 |
+
</style>
|
| 2932 |
+
|
| 2933 |
+
<script>
|
| 2934 |
+
function saadAttach() {{
|
| 2935 |
+
var inp = document.querySelector('[data-testid="stFileUploaderDropzoneInput"]');
|
| 2936 |
+
if (inp) {{ inp.click(); }}
|
| 2937 |
+
}}
|
| 2938 |
+
function saadClearFile() {{
|
| 2939 |
+
/* Find and click the Streamlit ✕ button by its text content */
|
| 2940 |
+
var btns = document.querySelectorAll('button');
|
| 2941 |
+
for (var b of btns) {{
|
| 2942 |
+
if (b.innerText.trim() === '✕') {{ b.click(); break; }}
|
| 2943 |
+
}}
|
| 2944 |
+
}}
|
| 2945 |
+
</script>
|
| 2946 |
+
|
| 2947 |
+
<!-- Fixed attach bar just above the chat input -->
|
| 2948 |
<div style="
|
| 2949 |
position: fixed;
|
| 2950 |
bottom: 68px;
|
|
|
|
| 2952 |
transform: translateX(-50%);
|
| 2953 |
width: 720px;
|
| 2954 |
max-width: 88vw;
|
| 2955 |
+
display: flex;
|
| 2956 |
+
align-items: center;
|
| 2957 |
+
gap: 8px;
|
|
|
|
| 2958 |
z-index: 9999;
|
|
|
|
|
|
|
| 2959 |
">
|
| 2960 |
+
<!-- 📎 Paperclip button — triggers hidden file input -->
|
| 2961 |
+
<button onclick="saadAttach()" title="Attach image or PDF"
|
| 2962 |
+
style="flex-shrink:0; width:40px; height:36px;
|
| 2963 |
+
background:#1e293b; border:1.5px solid #334155;
|
| 2964 |
+
border-radius:9px; cursor:pointer; font-size:17px;
|
| 2965 |
+
color:#9ca3af; line-height:1; transition:all .15s;"
|
| 2966 |
+
onmouseover="this.style.background='#334155';this.style.borderColor='#60a5fa';this.style.color='#93c5fd';"
|
| 2967 |
+
onmouseout="this.style.background='#1e293b';this.style.borderColor='#334155';this.style.color='#9ca3af';">
|
| 2968 |
+
📎
|
| 2969 |
+
</button>
|
| 2970 |
+
<!-- Status pill -->
|
| 2971 |
+
<div style="flex:1; background:{_bcolor}; border:1px solid {_bborder};
|
| 2972 |
+
border-radius:9px; padding:6px 14px; font-size:0.79rem;
|
| 2973 |
+
color:{_tcolor}; white-space:nowrap; overflow:hidden;
|
| 2974 |
+
text-overflow:ellipsis;">
|
| 2975 |
+
{_status}
|
| 2976 |
+
</div>
|
| 2977 |
+
{"""<!-- ✕ clear button -->
|
| 2978 |
+
<button onclick="saadClearFile()" title="Remove file"
|
| 2979 |
+
style="flex-shrink:0; width:30px; height:30px;
|
| 2980 |
+
background:transparent; border:1px solid #4b5563;
|
| 2981 |
+
border-radius:7px; cursor:pointer; font-size:13px;
|
| 2982 |
+
color:#9ca3af; line-height:1; transition:all .15s;"
|
| 2983 |
+
onmouseover="this.style.borderColor='#f87171';this.style.color='#f87171';"
|
| 2984 |
+
onmouseout="this.style.borderColor='#4b5563';this.style.color='#9ca3af';">
|
| 2985 |
+
✕
|
| 2986 |
+
</button>""" if (_pending or _memfile) else ""}
|
| 2987 |
</div>
|
| 2988 |
""",
|
| 2989 |
unsafe_allow_html=True
|
| 2990 |
)
|
| 2991 |
|
| 2992 |
+
# ── Real Streamlit clear-buttons (hidden by CSS, fired by JS above) ─
|
| 2993 |
+
if _pending:
|
| 2994 |
col_sp, col_rm = st.columns([11, 1])
|
| 2995 |
with col_rm:
|
| 2996 |
+
if st.button("✕", key="remove_pending", help="Remove file"):
|
| 2997 |
st.session_state.pending_file_bytes = None
|
| 2998 |
st.session_state.pending_file_name = None
|
| 2999 |
st.session_state.pending_file_mime = None
|
| 3000 |
+
st.session_state["last_uploaded_file"] = ""
|
| 3001 |
st.rerun()
|
| 3002 |
+
elif _memfile:
|
| 3003 |
col_sp, col_rm = st.columns([11, 1])
|
| 3004 |
with col_rm:
|
| 3005 |
+
if st.button("✕", key="clear_memory_file", help="Clear file memory"):
|
| 3006 |
st.session_state.attached_file_bytes = None
|
| 3007 |
st.session_state.attached_file_name = None
|
| 3008 |
st.session_state.attached_file_mime = None
|
|
|
|
| 3073 |
st.markdown(f"📎 **{file_name}** — {problem}")
|
| 3074 |
# Show file preview so user can see what was attached
|
| 3075 |
if file_mime and file_mime.startswith("image/"):
|
| 3076 |
+
st.image(file_bytes, caption=file_name, use_column_width=True)
|
| 3077 |
else:
|
| 3078 |
# PDF — try to show first page
|
| 3079 |
try:
|
|
|
|
| 3081 |
doc = fitz.open(stream=file_bytes, filetype="pdf")
|
| 3082 |
pix = doc[0].get_pixmap(matrix=fitz.Matrix(1.5, 1.5))
|
| 3083 |
doc.close()
|
| 3084 |
+
st.image(pix.tobytes("png"), caption=f"📄 {file_name} (page 1 preview)", use_column_width=True)
|
| 3085 |
except Exception:
|
| 3086 |
st.caption(f"📄 {file_name}")
|
| 3087 |
|