Upload app.py
Browse files
app.py
CHANGED
|
@@ -291,6 +291,57 @@ hr { border-color: #1a1a2e !important; }
|
|
| 291 |
::-webkit-scrollbar-track { background: #0f0f0f; }
|
| 292 |
::-webkit-scrollbar-thumb { background: #1f2937; border-radius: 2px; }
|
| 293 |
::-webkit-scrollbar-thumb:hover { background: #3b82f6; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
</style>
|
| 295 |
""", unsafe_allow_html=True)
|
| 296 |
|
|
@@ -2108,8 +2159,113 @@ def ask_ai(problem: str, sympy_info: dict, history: list) -> str:
|
|
| 2108 |
|
| 2109 |
|
| 2110 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 2111 |
-
#
|
| 2112 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2113 |
def ask_ai_streaming(problem: str, sympy_info: dict, history: list) -> str:
|
| 2114 |
"""Get AI response and stream it word by word using st.write_stream."""
|
| 2115 |
import time
|
|
@@ -2409,6 +2565,42 @@ for i, msg in enumerate(st.session_state.messages):
|
|
| 2409 |
with st.expander("๐ Copy"):
|
| 2410 |
st.code(msg["content"], language=None)
|
| 2411 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2412 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 2413 |
# INPUT โ use st.chat_input (cleaner than text_input + button)
|
| 2414 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
|
|
| 291 |
::-webkit-scrollbar-track { background: #0f0f0f; }
|
| 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 {
|
| 305 |
+
border-color: #3b82f6 !important;
|
| 306 |
+
background: #0f172a !important;
|
| 307 |
+
}
|
| 308 |
+
[data-testid="stFileUploader"] section {
|
| 309 |
+
padding: 0 !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 |
|
|
|
|
| 2159 |
|
| 2160 |
|
| 2161 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 2162 |
+
# FILE UPLOAD โ image/PDF sent to Gemini Vision, then SymPy verified
|
| 2163 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 2164 |
+
def ask_gemini_vision(image_b64: str, mime_type: str, user_note: str) -> str:
|
| 2165 |
+
"""
|
| 2166 |
+
Send image or PDF to Gemini 1.5 Flash vision.
|
| 2167 |
+
Returns extracted problem text + solution from Gemini.
|
| 2168 |
+
Only Gemini supports vision โ Groq and OpenRouter skipped.
|
| 2169 |
+
"""
|
| 2170 |
+
gemini_keys = [
|
| 2171 |
+
os.environ.get("GEMINI_API_KEY_1", ""),
|
| 2172 |
+
os.environ.get("GEMINI_API_KEY_2", ""),
|
| 2173 |
+
]
|
| 2174 |
+
|
| 2175 |
+
prompt = (
|
| 2176 |
+
"You are Saad.AI, a BSc Mathematics assistant built by Saad.\n"
|
| 2177 |
+
"The student has uploaded an image or document containing a math problem.\n\n"
|
| 2178 |
+
"YOUR TASKS:\n"
|
| 2179 |
+
"1. Read and extract the math problem from the image/document exactly.\n"
|
| 2180 |
+
"2. State what the problem is clearly.\n"
|
| 2181 |
+
"3. Solve it step by step using the full math structure:\n"
|
| 2182 |
+
" ๐ **Given:** ...\n"
|
| 2183 |
+
" ๐ **Method:** ...\n"
|
| 2184 |
+
" ๐งฎ **Step 1:** ...\n"
|
| 2185 |
+
" โ
**Final Answer:** $$\\boxed{answer}$$\n"
|
| 2186 |
+
"4. ALL math must be in LaTeX โ never plain text math.\n"
|
| 2187 |
+
"5. If the image is unclear or not math-related, say so politely.\n\n"
|
| 2188 |
+
f"Additional note from student: {user_note if user_note else 'None'}"
|
| 2189 |
+
)
|
| 2190 |
+
|
| 2191 |
+
for key in gemini_keys:
|
| 2192 |
+
if not key:
|
| 2193 |
+
continue
|
| 2194 |
+
try:
|
| 2195 |
+
resp = requests.post(
|
| 2196 |
+
f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={key}",
|
| 2197 |
+
headers={"Content-Type": "application/json"},
|
| 2198 |
+
json={
|
| 2199 |
+
"contents": [{
|
| 2200 |
+
"parts": [
|
| 2201 |
+
{"inline_data": {"mime_type": mime_type, "data": image_b64}},
|
| 2202 |
+
{"text": prompt}
|
| 2203 |
+
]
|
| 2204 |
+
}],
|
| 2205 |
+
"generationConfig": {"maxOutputTokens": 2048, "temperature": 0.15}
|
| 2206 |
+
},
|
| 2207 |
+
timeout=60
|
| 2208 |
+
)
|
| 2209 |
+
resp.raise_for_status()
|
| 2210 |
+
return resp.json()["candidates"][0]["content"]["parts"][0]["text"]
|
| 2211 |
+
except Exception:
|
| 2212 |
+
continue
|
| 2213 |
+
|
| 2214 |
+
return "โ ๏ธ Could not process the file. Please check your Gemini API keys or try again."
|
| 2215 |
+
|
| 2216 |
+
|
| 2217 |
+
def handle_uploaded_file(uploaded_file, user_note: str) -> str:
|
| 2218 |
+
"""
|
| 2219 |
+
Process uploaded image or PDF:
|
| 2220 |
+
1. Convert to base64
|
| 2221 |
+
2. Send to Gemini Vision
|
| 2222 |
+
3. Try SymPy verification on extracted text
|
| 2223 |
+
4. Return final answer
|
| 2224 |
+
"""
|
| 2225 |
+
import base64
|
| 2226 |
+
|
| 2227 |
+
# โโ Validate file โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 2228 |
+
MAX_SIZE = 5 * 1024 * 1024 # 5MB
|
| 2229 |
+
file_bytes = uploaded_file.read()
|
| 2230 |
+
|
| 2231 |
+
if len(file_bytes) > MAX_SIZE:
|
| 2232 |
+
return "โ ๏ธ File too large. Please upload a file under 5MB."
|
| 2233 |
+
|
| 2234 |
+
mime_map = {
|
| 2235 |
+
"jpg": "image/jpeg", "jpeg": "image/jpeg",
|
| 2236 |
+
"png": "image/png", "webp": "image/webp",
|
| 2237 |
+
"pdf": "application/pdf"
|
| 2238 |
+
}
|
| 2239 |
+
ext = uploaded_file.name.split(".")[-1].lower()
|
| 2240 |
+
mime_type = mime_map.get(ext)
|
| 2241 |
+
|
| 2242 |
+
if not mime_type:
|
| 2243 |
+
return "โ ๏ธ Unsupported format. Please upload JPG, PNG, WEBP or PDF."
|
| 2244 |
+
|
| 2245 |
+
# โโ Convert to base64 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 2246 |
+
image_b64 = base64.b64encode(file_bytes).decode("utf-8")
|
| 2247 |
+
|
| 2248 |
+
# โโ Send to Gemini Vision โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 2249 |
+
gemini_response = ask_gemini_vision(image_b64, mime_type, user_note)
|
| 2250 |
+
|
| 2251 |
+
# โโ Try SymPy verification on extracted text โโโโโโโโโโโโโโโโโโโโโ
|
| 2252 |
+
# Gemini response contains the problem โ try to run SymPy on it
|
| 2253 |
+
sympy_result = run_sympy(gemini_response)
|
| 2254 |
+
|
| 2255 |
+
if (sympy_result.get("result") and
|
| 2256 |
+
sympy_result["result"] not in (None, "matrix_detected", "mod_detected")):
|
| 2257 |
+
# SymPy verified โ strip AI answer, append verified one
|
| 2258 |
+
latex = sympy_result.get("latex", "")
|
| 2259 |
+
cleaned = re.sub(
|
| 2260 |
+
r'(โ
\s*\*{0,2}Final\s*Answer\*{0,2}.*|โ
[^\n]*$)',
|
| 2261 |
+
"", gemini_response,
|
| 2262 |
+
flags=re.DOTALL | re.IGNORECASE
|
| 2263 |
+
).rstrip()
|
| 2264 |
+
verified_note = "\n\n๐ **SymPy Verified**"
|
| 2265 |
+
return cleaned + verified_note + f"\n\nโ
**Final Answer:** $$\\boxed{{{latex}}}$$"
|
| 2266 |
+
else:
|
| 2267 |
+
# No SymPy verification โ return Gemini answer with warning
|
| 2268 |
+
return gemini_response + "\n\nโ ๏ธ *AI-generated answer โ not SymPy verified.*"
|
| 2269 |
def ask_ai_streaming(problem: str, sympy_info: dict, history: list) -> str:
|
| 2270 |
"""Get AI response and stream it word by word using st.write_stream."""
|
| 2271 |
import time
|
|
|
|
| 2565 |
with st.expander("๐ Copy"):
|
| 2566 |
st.code(msg["content"], language=None)
|
| 2567 |
|
| 2568 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 2569 |
+
# FILE UPLOAD โ above chat input
|
| 2570 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 2571 |
+
uploaded_file = st.file_uploader(
|
| 2572 |
+
"๐ Upload a math problem (image or PDF)",
|
| 2573 |
+
type=["jpg", "jpeg", "png", "webp", "pdf"],
|
| 2574 |
+
label_visibility="collapsed",
|
| 2575 |
+
help="Upload a photo or PDF of your math problem โ Saad.AI will read and solve it"
|
| 2576 |
+
)
|
| 2577 |
+
|
| 2578 |
+
if uploaded_file and uploaded_file.name != st.session_state.get("last_uploaded_file", ""):
|
| 2579 |
+
st.session_state["last_uploaded_file"] = uploaded_file.name
|
| 2580 |
+
user_note = ""
|
| 2581 |
+
|
| 2582 |
+
# Show user message
|
| 2583 |
+
with st.chat_message("user", avatar="๐งโ๐"):
|
| 2584 |
+
st.markdown(f"๐ **Uploaded:** `{uploaded_file.name}`")
|
| 2585 |
+
|
| 2586 |
+
# Process file
|
| 2587 |
+
with st.chat_message("assistant", avatar="๐"):
|
| 2588 |
+
with st.spinner("๐ Reading your file..."):
|
| 2589 |
+
answer = handle_uploaded_file(uploaded_file, user_note)
|
| 2590 |
+
st.markdown(answer)
|
| 2591 |
+
with st.expander("๐ Copy"):
|
| 2592 |
+
st.code(answer, language=None)
|
| 2593 |
+
|
| 2594 |
+
# Save to history
|
| 2595 |
+
if not st.session_state.current_chat_id:
|
| 2596 |
+
new_chat()
|
| 2597 |
+
st.session_state.messages.append({
|
| 2598 |
+
"role": "user",
|
| 2599 |
+
"content": f"๐ Uploaded file: `{uploaded_file.name}`"
|
| 2600 |
+
})
|
| 2601 |
+
st.session_state.messages.append({"role": "assistant", "content": answer})
|
| 2602 |
+
save_current_chat()
|
| 2603 |
+
|
| 2604 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 2605 |
# INPUT โ use st.chat_input (cleaner than text_input + button)
|
| 2606 |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|