Update app.py
Browse files
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 |
-
/* ββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
[data-testid="stFileUploader"] {
|
| 297 |
background: #111827 !important;
|
| 298 |
-
border: 1px solid #
|
| 299 |
-
border-
|
| 300 |
-
|
| 301 |
-
|
| 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:
|
| 324 |
-
|
|
|
|
| 325 |
min-height: 0 !important;
|
| 326 |
transition: all 0.2s !important;
|
| 327 |
}
|
| 328 |
[data-testid="stFileUploaderDropzone"]:hover {
|
| 329 |
-
background: rgba(59,130,246,0.
|
| 330 |
-
border-
|
| 331 |
}
|
| 332 |
[data-testid="stFileUploaderDropzoneInstructions"] {
|
| 333 |
font-size: 0.75rem !important;
|
| 334 |
color: #4b5563 !important;
|
| 335 |
-
padding: 0.
|
| 336 |
}
|
| 337 |
-
/*
|
| 338 |
[data-testid="stChatInput"] {
|
| 339 |
-
border-radius:
|
| 340 |
-
border
|
| 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 = {}
|
| 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 |
-
|
| 1802 |
-
|
| 1803 |
-
|
| 1804 |
-
|
| 1805 |
-
|
| 1806 |
-
|
| 1807 |
-
|
| 1808 |
-
|
| 1809 |
-
|
| 1810 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 2145 |
-
|
| 2146 |
-
|
| 2147 |
-
|
|
|
|
|
|
|
| 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
|
| 2156 |
-
f"Last error: {last_error}\n\n"
|
| 2157 |
-
"
|
| 2158 |
-
"
|
|
|
|
| 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 |
-
|
| 2203 |
-
|
| 2204 |
-
|
| 2205 |
-
|
| 2206 |
-
|
| 2207 |
-
|
| 2208 |
-
"
|
| 2209 |
-
|
| 2210 |
-
|
| 2211 |
-
|
| 2212 |
-
|
| 2213 |
-
|
| 2214 |
-
|
| 2215 |
-
|
| 2216 |
-
|
| 2217 |
-
|
| 2218 |
-
|
| 2219 |
-
|
| 2220 |
-
|
| 2221 |
-
|
| 2222 |
-
|
| 2223 |
-
|
| 2224 |
-
|
| 2225 |
-
|
| 2226 |
-
|
| 2227 |
-
|
| 2228 |
-
|
| 2229 |
-
|
| 2230 |
-
|
| 2231 |
-
|
| 2232 |
-
|
| 2233 |
-
|
| 2234 |
-
|
| 2235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2236 |
|
| 2237 |
if "429" in last_error or "quota" in last_error:
|
| 2238 |
-
return
|
| 2239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 β
|
| 2619 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2620 |
-
|
| 2621 |
-
|
| 2622 |
-
|
| 2623 |
-
|
| 2624 |
-
|
| 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 β
|
| 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 |
)
|