Update app.py
Browse files
app.py
CHANGED
|
@@ -6,13 +6,16 @@ import os
|
|
| 6 |
import io
|
| 7 |
import json
|
| 8 |
import requests
|
|
|
|
| 9 |
from PIL import Image
|
| 10 |
from gtts import gTTS
|
| 11 |
from duckduckgo_search import DDGS
|
| 12 |
import warnings
|
| 13 |
warnings.filterwarnings('ignore')
|
| 14 |
|
| 15 |
-
#
|
|
|
|
|
|
|
| 16 |
st.set_page_config(
|
| 17 |
page_title="H2 Physics Feynman Bot",
|
| 18 |
page_icon="⚛️",
|
|
@@ -20,10 +23,13 @@ st.set_page_config(
|
|
| 20 |
initial_sidebar_state="expanded"
|
| 21 |
)
|
| 22 |
|
| 23 |
-
#
|
|
|
|
|
|
|
|
|
|
| 24 |
@st.cache_data(show_spinner=False, ttl=3600)
|
| 25 |
def generate_audio(text):
|
| 26 |
-
"""Generates MP3 audio from text
|
| 27 |
clean_text = re.sub(r'```.*?```', 'I have generated a graph.', text, flags=re.DOTALL)
|
| 28 |
clean_text = re.sub(r'\[IMAGE:.*?\]', 'Here is a diagram.', clean_text)
|
| 29 |
|
|
@@ -37,12 +43,10 @@ def generate_audio(text):
|
|
| 37 |
audio_fp.seek(0)
|
| 38 |
return audio_fp
|
| 39 |
except Exception as e:
|
| 40 |
-
st.error(f"Audio generation error: {e}")
|
| 41 |
return None
|
| 42 |
|
| 43 |
-
# Image search functions
|
| 44 |
def google_search_api(query, api_key, cx):
|
| 45 |
-
"""
|
| 46 |
try:
|
| 47 |
url = "https://www.googleapis.com/customsearch/v1"
|
| 48 |
params = {
|
|
@@ -69,180 +73,219 @@ def google_search_api(query, api_key, cx):
|
|
| 69 |
return data["items"][0]["link"]
|
| 70 |
|
| 71 |
except Exception as e:
|
| 72 |
-
print(f"Google API Exception: {e}")
|
| 73 |
return None
|
| 74 |
return None
|
| 75 |
|
| 76 |
def duckduckgo_search_api(query):
|
| 77 |
-
"""
|
| 78 |
try:
|
| 79 |
with DDGS() as ddgs:
|
| 80 |
results = list(ddgs.images(query, max_results=1))
|
| 81 |
if results:
|
| 82 |
return results[0]['image']
|
| 83 |
except Exception as e:
|
| 84 |
-
return
|
| 85 |
return None
|
| 86 |
|
| 87 |
@st.cache_data(show_spinner=False, ttl=300)
|
| 88 |
def search_image(query):
|
| 89 |
-
"""
|
| 90 |
try:
|
| 91 |
-
#
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
if key2 and cx:
|
| 106 |
-
url = google_search_api(query, key2, cx)
|
| 107 |
-
if url:
|
| 108 |
-
return url
|
| 109 |
-
except:
|
| 110 |
-
pass
|
| 111 |
|
| 112 |
-
#
|
| 113 |
-
|
| 114 |
-
key1 = os.environ.get("GOOGLE_SEARCH_KEY", "")
|
| 115 |
-
key2 = os.environ.get("GOOGLE_SEARCH_KEY_2", "")
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
if url:
|
| 120 |
-
return url
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
return url
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
except Exception as e:
|
| 131 |
-
return f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
-
# Graph plotting function - REORDERED AND FIXED
|
| 134 |
def execute_plotting_code(code_snippet):
|
| 135 |
-
"""Execute plotting code
|
| 136 |
try:
|
| 137 |
-
# Clear any existing plots FIRST
|
| 138 |
plt.close('all')
|
|
|
|
| 139 |
|
| 140 |
-
# Create a fresh figure
|
| 141 |
-
fig = plt.figure(figsize=(10, 6))
|
| 142 |
-
|
| 143 |
-
# Create execution namespace with ALL required imports
|
| 144 |
namespace = {
|
| 145 |
'plt': plt,
|
| 146 |
'np': np,
|
|
|
|
| 147 |
'fig': fig,
|
| 148 |
-
'ax': None,
|
| 149 |
'math': __import__('math')
|
| 150 |
}
|
| 151 |
|
| 152 |
-
# Clean the code
|
| 153 |
cleaned_code = code_snippet.strip()
|
| 154 |
|
| 155 |
-
#
|
| 156 |
if 'import matplotlib' not in cleaned_code:
|
| 157 |
cleaned_code = 'import matplotlib.pyplot as plt\nimport numpy as np\n' + cleaned_code
|
| 158 |
|
| 159 |
-
#
|
| 160 |
-
|
| 161 |
-
cleaned_code = cleaned_code.replace('import matplotlib.pyplot as plt',
|
| 162 |
-
'import matplotlib.pyplot as plt\nplt.figure(figsize=(10, 6))')
|
| 163 |
-
|
| 164 |
-
# CRITICAL FIX: Check if there's actual plotting
|
| 165 |
-
plot_keywords = ['plt.plot(', 'ax.plot(', 'plt.scatter(', 'ax.scatter(',
|
| 166 |
-
'plt.bar(', 'ax.bar(', 'plt.hist(', 'ax.hist(']
|
| 167 |
-
|
| 168 |
has_plot = any(keyword in cleaned_code for keyword in plot_keywords)
|
| 169 |
|
| 170 |
if not has_plot:
|
| 171 |
-
#
|
| 172 |
-
cleaned_code += '\n\n# Adding sample plot since no plotting command was found\n'
|
| 173 |
-
cleaned_code += 'x = np.linspace(0, 10, 100)\n'
|
| 174 |
-
cleaned_code += 'y = np.sin(x)\n'
|
| 175 |
-
cleaned_code += 'plt.plot(x, y, "b-", linewidth=2, label="sin(x)")\n'
|
| 176 |
|
| 177 |
-
#
|
| 178 |
cleaned_code = cleaned_code.replace('plt.show()', '')
|
| 179 |
-
cleaned_code = cleaned_code.replace('plt.show', '')
|
| 180 |
|
| 181 |
# Add labels if missing
|
| 182 |
if 'plt.xlabel' not in cleaned_code:
|
| 183 |
cleaned_code += '\nplt.xlabel("X-axis", fontsize=12)'
|
| 184 |
if 'plt.ylabel' not in cleaned_code:
|
| 185 |
cleaned_code += '\nplt.ylabel("Y-axis", fontsize=12)'
|
| 186 |
-
if 'plt.title' not in cleaned_code:
|
| 187 |
-
cleaned_code += '\nplt.title("Physics Graph", fontsize=14)'
|
| 188 |
if 'plt.grid' not in cleaned_code:
|
| 189 |
cleaned_code += '\nplt.grid(True, linestyle="--", alpha=0.6)'
|
| 190 |
|
| 191 |
-
# DEBUG: Print the code being executed
|
| 192 |
-
# print("DEBUG - Executing code:\n", cleaned_code[:200])
|
| 193 |
-
|
| 194 |
-
# EXECUTE THE CODE
|
| 195 |
exec(cleaned_code, namespace)
|
| 196 |
|
| 197 |
-
|
| 198 |
-
|
|
|
|
| 199 |
|
| 200 |
-
# Display the plot IMMEDIATELY
|
| 201 |
st.pyplot(fig)
|
| 202 |
-
|
| 203 |
-
# Close the figure
|
| 204 |
plt.close(fig)
|
| 205 |
|
| 206 |
-
return True
|
| 207 |
-
|
| 208 |
except Exception as e:
|
| 209 |
-
st.error(f"
|
| 210 |
|
| 211 |
-
#
|
| 212 |
try:
|
| 213 |
fig, ax = plt.subplots(figsize=(10, 6))
|
| 214 |
x = np.linspace(0, 10, 100)
|
| 215 |
-
y = x**2
|
| 216 |
ax.plot(x, y, 'b-', linewidth=2, label='y = x²')
|
| 217 |
ax.set_xlabel('X', fontsize=12)
|
| 218 |
ax.set_ylabel('Y', fontsize=12)
|
| 219 |
-
ax.set_title('Sample Graph
|
| 220 |
ax.grid(True, linestyle='--', alpha=0.6)
|
| 221 |
ax.legend()
|
| 222 |
st.pyplot(fig)
|
| 223 |
plt.close(fig)
|
| 224 |
-
st.warning("Displayed sample graph. The AI's code may need adjustment.")
|
| 225 |
except:
|
| 226 |
-
st.
|
| 227 |
-
|
| 228 |
-
return False
|
| 229 |
-
# Display message function
|
| 230 |
-
# UPDATED Display message function - FIXED DEPRECATED PARAMETER
|
| 231 |
def display_message(role, content, enable_voice=False):
|
| 232 |
-
"""Display chat message
|
| 233 |
with st.chat_message(role):
|
| 234 |
|
| 235 |
text_to_display = content
|
| 236 |
|
| 237 |
-
#
|
| 238 |
code_pattern = r'```python\s*(.*?)```'
|
| 239 |
code_matches = list(re.finditer(code_pattern, content, re.DOTALL))
|
| 240 |
|
| 241 |
-
# Remove code blocks from displayed text
|
| 242 |
for match in reversed(code_matches):
|
| 243 |
text_to_display = text_to_display.replace(match.group(0), "")
|
| 244 |
|
| 245 |
-
#
|
| 246 |
image_match = re.search(r'\[IMAGE:\s*(.*?)\]', text_to_display, re.IGNORECASE)
|
| 247 |
image_result = None
|
| 248 |
|
|
@@ -252,48 +295,43 @@ def display_message(role, content, enable_voice=False):
|
|
| 252 |
with st.spinner(f"Searching for '{search_query}'..."):
|
| 253 |
image_result = search_image(search_query)
|
| 254 |
|
| 255 |
-
#
|
| 256 |
st.markdown(text_to_display)
|
| 257 |
-
|
| 258 |
-
#
|
| 259 |
if code_matches and role == "assistant":
|
| 260 |
for match in code_matches:
|
| 261 |
code_content = match.group(1).strip()
|
| 262 |
if code_content:
|
| 263 |
-
# Show code FIRST
|
| 264 |
with st.expander("📝 View Python Code", expanded=False):
|
| 265 |
st.code(code_content, language='python')
|
| 266 |
|
| 267 |
-
# Then show graph BELOW the code
|
| 268 |
with st.expander("📊 Generated Graph", expanded=True):
|
| 269 |
-
# Execute and display graph
|
| 270 |
execute_plotting_code(code_content)
|
| 271 |
|
| 272 |
-
#
|
| 273 |
if image_match and role == "assistant":
|
| 274 |
-
if image_result and
|
| 275 |
try:
|
| 276 |
-
|
| 277 |
-
st.image(image_result, caption=f"📷 {image_match.group(1)}", width="auto")
|
| 278 |
st.markdown(f"🔗 [Open Image]({image_result})")
|
| 279 |
-
except
|
| 280 |
-
|
| 281 |
-
try:
|
| 282 |
-
st.image(image_result, caption=f"📷 {image_match.group(1)}", width=500)
|
| 283 |
-
except:
|
| 284 |
-
st.warning(f"⚠️ Could not display image from URL: {image_result[:100]}")
|
| 285 |
else:
|
| 286 |
-
st.warning(f"
|
| 287 |
|
| 288 |
-
|
| 289 |
-
# --- AUDIO OUTPUT ---
|
| 290 |
if enable_voice and role == "assistant" and len(text_to_display.strip()) > 10:
|
| 291 |
audio_bytes = generate_audio(text_to_display)
|
| 292 |
if audio_bytes:
|
| 293 |
st.audio(audio_bytes, format='audio/mp3')
|
| 294 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
def call_groq_api(api_key, messages, max_tokens=2000):
|
| 296 |
-
"""Call Groq API
|
| 297 |
url = "https://api.groq.com/openai/v1/chat/completions"
|
| 298 |
|
| 299 |
headers = {
|
|
@@ -301,7 +339,6 @@ def call_groq_api(api_key, messages, max_tokens=2000):
|
|
| 301 |
"Content-Type": "application/json"
|
| 302 |
}
|
| 303 |
|
| 304 |
-
# Format messages for Groq API
|
| 305 |
formatted_messages = []
|
| 306 |
for msg in messages:
|
| 307 |
if msg["role"] == "system":
|
|
@@ -315,11 +352,9 @@ def call_groq_api(api_key, messages, max_tokens=2000):
|
|
| 315 |
"content": msg["content"]
|
| 316 |
})
|
| 317 |
|
| 318 |
-
# Try multiple models
|
| 319 |
models_to_try = [
|
| 320 |
"llama-3.1-8b-instant",
|
| 321 |
"llama-3.2-3b-preview",
|
| 322 |
-
"mixtral-8x7b-32768",
|
| 323 |
]
|
| 324 |
|
| 325 |
for model in models_to_try:
|
|
@@ -339,18 +374,12 @@ def call_groq_api(api_key, messages, max_tokens=2000):
|
|
| 339 |
return result["choices"][0]["message"]["content"]
|
| 340 |
elif response.status_code == 429:
|
| 341 |
continue
|
| 342 |
-
elif response.status_code == 401:
|
| 343 |
-
return "❌ Invalid API Key. Please check your Groq API key."
|
| 344 |
-
elif response.status_code == 402:
|
| 345 |
-
return "❌ Payment required. The free model might be unavailable. Try again later."
|
| 346 |
|
| 347 |
-
except
|
| 348 |
-
continue
|
| 349 |
-
except Exception as e:
|
| 350 |
continue
|
| 351 |
|
| 352 |
-
return "
|
| 353 |
-
|
| 354 |
# System Instructions for the AI - Part 5a (Start)
|
| 355 |
SYSTEM_INSTRUCTIONS = """
|
| 356 |
**Identity:** Richard Feynman. Tutor for Singapore H2 Physics (9478).
|
|
@@ -467,38 +496,124 @@ with st.sidebar:
|
|
| 467 |
)
|
| 468 |
|
| 469 |
enable_voice = st.toggle("🗣️ Text-to-Speech", value=False)
|
| 470 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
|
| 472 |
#st.divider()
|
| 473 |
|
| 474 |
-
# Media Input
|
| 475 |
-
st.header("📤
|
| 476 |
|
| 477 |
visual_content = None
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
|
|
|
|
|
|
|
|
|
| 482 |
)
|
| 483 |
|
| 484 |
-
if
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
|
| 493 |
#st.divider()
|
| 494 |
|
| 495 |
-
#
|
| 496 |
if st.button("🧹 Clear Chat History", use_container_width=True):
|
| 497 |
st.session_state.messages = []
|
| 498 |
st.rerun()
|
| 499 |
|
| 500 |
st.divider()
|
| 501 |
-
|
| 502 |
st.caption("Made with ❤️ for JPJC H2 Physics students | Powered by Groq AI")
|
| 503 |
|
| 504 |
# 7 Main Chat Interface
|
|
@@ -526,97 +641,77 @@ if user_input or visual_content:
|
|
| 526 |
user_message = ""
|
| 527 |
if user_input:
|
| 528 |
user_message += user_input + " "
|
|
|
|
| 529 |
if visual_content:
|
| 530 |
-
user_message += "[I
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
if topic != "General / Any":
|
| 532 |
user_message += f"(Focus on: {topic})"
|
| 533 |
|
| 534 |
-
# Add
|
| 535 |
st.session_state.messages.append({"role": "user", "content": user_message})
|
| 536 |
|
| 537 |
# Display user message
|
| 538 |
with st.chat_message("user"):
|
| 539 |
if user_input:
|
| 540 |
st.markdown(user_input)
|
|
|
|
| 541 |
if visual_content:
|
| 542 |
-
st.image(visual_content, caption="
|
| 543 |
-
|
|
|
|
|
|
|
|
|
|
| 544 |
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
st.error(
|
| 549 |
-
"
|
| 550 |
-
"
|
| 551 |
-
"Please contact the administrator or check if the API keys "
|
| 552 |
-
"are properly configured in the deployment settings."
|
| 553 |
)
|
| 554 |
st.stop()
|
| 555 |
|
| 556 |
# Prepare API call
|
| 557 |
try:
|
| 558 |
-
# Build conversation context
|
| 559 |
conversation_context = []
|
| 560 |
-
|
| 561 |
-
# Add system message
|
| 562 |
conversation_context.append({"role": "system", "content": SYSTEM_INSTRUCTIONS})
|
| 563 |
|
| 564 |
-
# Add recent conversation (last 8 messages for context)
|
| 565 |
recent_messages = st.session_state.messages[-8:]
|
| 566 |
for msg in recent_messages:
|
| 567 |
-
if msg["role"] != "system":
|
| 568 |
conversation_context.append(msg)
|
| 569 |
|
| 570 |
# Call Groq API
|
| 571 |
with st.spinner("Feynman is thinking... ⚛️"):
|
| 572 |
-
response_text = call_groq_api(
|
| 573 |
|
| 574 |
-
# Handle response
|
| 575 |
if response_text:
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
"3. Make sure you're using the correct key\n"
|
| 585 |
-
"4. Keys start with: `gsk_`"
|
| 586 |
-
)
|
| 587 |
-
else:
|
| 588 |
-
# Add assistant response to history
|
| 589 |
-
st.session_state.messages.append({"role": "assistant", "content": response_text})
|
| 590 |
-
|
| 591 |
-
# Display response
|
| 592 |
-
display_message("assistant", response_text, enable_voice)
|
| 593 |
|
| 594 |
-
# Show token estimate
|
| 595 |
-
estimated_tokens = len(response_text) // 4
|
| 596 |
-
st.caption(f"📝 Response length: ~{estimated_tokens} tokens | ⚡ Powered by Groq")
|
| 597 |
else:
|
| 598 |
-
st.error(
|
| 599 |
-
"**Failed to get response from Groq API.**\n\n"
|
| 600 |
-
"Possible reasons:\n"
|
| 601 |
-
"1. **Rate Limit** - Free tier has limits, wait a moment\n"
|
| 602 |
-
"2. **Service Down** - Groq might be experiencing issues\n"
|
| 603 |
-
"3. **Network Issue** - Check your connection\n\n"
|
| 604 |
-
"Try again in a moment."
|
| 605 |
-
)
|
| 606 |
|
| 607 |
except Exception as e:
|
| 608 |
-
st.error(
|
| 609 |
-
f"**❌ Unexpected Error**\n\n"
|
| 610 |
-
f"```python\n{str(e)[:200]}\n```\n\n"
|
| 611 |
-
f"**Troubleshooting:**\n"
|
| 612 |
-
f"1. Refresh the page\n"
|
| 613 |
-
f"2. Check your API key\n"
|
| 614 |
-
f"3. Clear chat and try again\n"
|
| 615 |
-
f"4. Visit [status.groq.com](https://status.groq.com) for service status"
|
| 616 |
-
)
|
| 617 |
|
| 618 |
# Footer - SIMPLE VERSION
|
| 619 |
st.divider()
|
| 620 |
-
st.markdown("
|
| 621 |
-
st.markdown("Powered by
|
| 622 |
-
st.markdown("
|
|
|
|
| 6 |
import io
|
| 7 |
import json
|
| 8 |
import requests
|
| 9 |
+
import base64
|
| 10 |
from PIL import Image
|
| 11 |
from gtts import gTTS
|
| 12 |
from duckduckgo_search import DDGS
|
| 13 |
import warnings
|
| 14 |
warnings.filterwarnings('ignore')
|
| 15 |
|
| 16 |
+
# -----------------------------------------------------------------------------
|
| 17 |
+
# 1. PAGE CONFIGURATION
|
| 18 |
+
# -----------------------------------------------------------------------------
|
| 19 |
st.set_page_config(
|
| 20 |
page_title="H2 Physics Feynman Bot",
|
| 21 |
page_icon="⚛️",
|
|
|
|
| 23 |
initial_sidebar_state="expanded"
|
| 24 |
)
|
| 25 |
|
| 26 |
+
# -----------------------------------------------------------------------------
|
| 27 |
+
# 2. HELPER FUNCTIONS
|
| 28 |
+
# -----------------------------------------------------------------------------
|
| 29 |
+
|
| 30 |
@st.cache_data(show_spinner=False, ttl=3600)
|
| 31 |
def generate_audio(text):
|
| 32 |
+
"""Generates MP3 audio from text."""
|
| 33 |
clean_text = re.sub(r'```.*?```', 'I have generated a graph.', text, flags=re.DOTALL)
|
| 34 |
clean_text = re.sub(r'\[IMAGE:.*?\]', 'Here is a diagram.', clean_text)
|
| 35 |
|
|
|
|
| 43 |
audio_fp.seek(0)
|
| 44 |
return audio_fp
|
| 45 |
except Exception as e:
|
|
|
|
| 46 |
return None
|
| 47 |
|
|
|
|
| 48 |
def google_search_api(query, api_key, cx):
|
| 49 |
+
"""Google Custom Search API."""
|
| 50 |
try:
|
| 51 |
url = "https://www.googleapis.com/customsearch/v1"
|
| 52 |
params = {
|
|
|
|
| 73 |
return data["items"][0]["link"]
|
| 74 |
|
| 75 |
except Exception as e:
|
|
|
|
| 76 |
return None
|
| 77 |
return None
|
| 78 |
|
| 79 |
def duckduckgo_search_api(query):
|
| 80 |
+
"""DuckDuckGo image search."""
|
| 81 |
try:
|
| 82 |
with DDGS() as ddgs:
|
| 83 |
results = list(ddgs.images(query, max_results=1))
|
| 84 |
if results:
|
| 85 |
return results[0]['image']
|
| 86 |
except Exception as e:
|
| 87 |
+
return None
|
| 88 |
return None
|
| 89 |
|
| 90 |
@st.cache_data(show_spinner=False, ttl=300)
|
| 91 |
def search_image(query):
|
| 92 |
+
"""Image search with fallback."""
|
| 93 |
try:
|
| 94 |
+
# Check secrets
|
| 95 |
+
if hasattr(st, 'secrets'):
|
| 96 |
+
cx = st.secrets.get("GOOGLE_CX", "")
|
| 97 |
+
key1 = st.secrets.get("GOOGLE_SEARCH_KEY", "")
|
| 98 |
+
key2 = st.secrets.get("GOOGLE_SEARCH_KEY_2", "")
|
| 99 |
+
|
| 100 |
+
if key1 and cx:
|
| 101 |
+
url = google_search_api(query, key1, cx)
|
| 102 |
+
if url: return url
|
| 103 |
+
|
| 104 |
+
if key2 and cx:
|
| 105 |
+
url = google_search_api(query, key2, cx)
|
| 106 |
+
if url: return url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
+
# Fallback to DuckDuckGo
|
| 109 |
+
return duckduckgo_search_api(query)
|
|
|
|
|
|
|
| 110 |
|
| 111 |
+
except Exception as e:
|
| 112 |
+
return None
|
|
|
|
|
|
|
| 113 |
|
| 114 |
+
# -----------------------------------------------------------------------------
|
| 115 |
+
# 3. QWEN-VL IMAGE ANALYSIS FUNCTIONS
|
| 116 |
+
# -----------------------------------------------------------------------------
|
|
|
|
| 117 |
|
| 118 |
+
def analyze_image_with_qwen_api(image, query="What physics concepts are shown in this image?", api_key=None):
|
| 119 |
+
"""Analyze image using DashScope Qwen-VL API."""
|
| 120 |
+
try:
|
| 121 |
+
# Convert image to base64
|
| 122 |
+
buffered = io.BytesIO()
|
| 123 |
+
|
| 124 |
+
# Convert to RGB if necessary
|
| 125 |
+
if image.mode != 'RGB':
|
| 126 |
+
image = image.convert('RGB')
|
| 127 |
+
|
| 128 |
+
image.save(buffered, format="JPEG", quality=85)
|
| 129 |
+
img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
|
| 130 |
+
|
| 131 |
+
# Prepare API request
|
| 132 |
+
url = "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
|
| 133 |
+
|
| 134 |
+
headers = {
|
| 135 |
+
"Authorization": f"Bearer {api_key}",
|
| 136 |
+
"Content-Type": "application/json"
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
# Build message
|
| 140 |
+
messages = [
|
| 141 |
+
{
|
| 142 |
+
"role": "system",
|
| 143 |
+
"content": [
|
| 144 |
+
{
|
| 145 |
+
"text": "You are a physics expert analyzing images for educational purposes. Focus on identifying physics concepts, diagrams, equations, and explaining them clearly."
|
| 146 |
+
}
|
| 147 |
+
]
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"role": "user",
|
| 151 |
+
"content": [
|
| 152 |
+
{"image": f"data:image/jpeg;base64,{img_base64}"},
|
| 153 |
+
{"text": query}
|
| 154 |
+
]
|
| 155 |
+
}
|
| 156 |
+
]
|
| 157 |
+
|
| 158 |
+
payload = {
|
| 159 |
+
"model": "qwen-vl-plus",
|
| 160 |
+
"input": {"messages": messages},
|
| 161 |
+
"parameters": {
|
| 162 |
+
"max_tokens": 1000,
|
| 163 |
+
"temperature": 0.7
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
|
| 167 |
+
# Make API call
|
| 168 |
+
response = requests.post(url, json=payload, headers=headers, timeout=30)
|
| 169 |
+
|
| 170 |
+
if response.status_code == 200:
|
| 171 |
+
result = response.json()
|
| 172 |
+
if "output" in result and "choices" in result["output"]:
|
| 173 |
+
return result["output"]["choices"][0]["message"]["content"]
|
| 174 |
+
else:
|
| 175 |
+
return "Analysis completed but no detailed response."
|
| 176 |
+
else:
|
| 177 |
+
return f"API Error {response.status_code}: {response.text[:200]}"
|
| 178 |
+
|
| 179 |
except Exception as e:
|
| 180 |
+
return f"Image analysis error: {str(e)[:100]}"
|
| 181 |
+
|
| 182 |
+
def analyze_image_local_fallback(image, query):
|
| 183 |
+
"""Simple local image analysis fallback."""
|
| 184 |
+
try:
|
| 185 |
+
# Get basic image info
|
| 186 |
+
width, height = image.size
|
| 187 |
+
format_info = image.format if image.format else "Unknown"
|
| 188 |
+
mode = image.mode
|
| 189 |
+
|
| 190 |
+
analysis = f"Image analysis: {width}x{height} pixels, format: {format_info}, mode: {mode}. "
|
| 191 |
+
|
| 192 |
+
# Try to detect if it's a physics-related image
|
| 193 |
+
if width > height:
|
| 194 |
+
analysis += "The image appears to be landscape orientation. "
|
| 195 |
+
else:
|
| 196 |
+
analysis += "The image appears to be portrait orientation. "
|
| 197 |
+
|
| 198 |
+
# Add physics context
|
| 199 |
+
analysis += "For physics analysis, please describe what you see in the image, and I'll help explain the physics concepts."
|
| 200 |
+
|
| 201 |
+
return analysis
|
| 202 |
+
except:
|
| 203 |
+
return "Image received. Please describe what you see in the image for physics analysis."
|
| 204 |
+
|
| 205 |
+
# -----------------------------------------------------------------------------
|
| 206 |
+
# 4. GRAPH FUNCTIONS
|
| 207 |
+
# -----------------------------------------------------------------------------
|
| 208 |
|
|
|
|
| 209 |
def execute_plotting_code(code_snippet):
|
| 210 |
+
"""Execute Python plotting code."""
|
| 211 |
try:
|
|
|
|
| 212 |
plt.close('all')
|
| 213 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
namespace = {
|
| 216 |
'plt': plt,
|
| 217 |
'np': np,
|
| 218 |
+
'ax': ax,
|
| 219 |
'fig': fig,
|
|
|
|
| 220 |
'math': __import__('math')
|
| 221 |
}
|
| 222 |
|
|
|
|
| 223 |
cleaned_code = code_snippet.strip()
|
| 224 |
|
| 225 |
+
# Ensure imports
|
| 226 |
if 'import matplotlib' not in cleaned_code:
|
| 227 |
cleaned_code = 'import matplotlib.pyplot as plt\nimport numpy as np\n' + cleaned_code
|
| 228 |
|
| 229 |
+
# Ensure plotting
|
| 230 |
+
plot_keywords = ['plt.plot(', 'ax.plot(', 'plt.scatter(', 'ax.scatter(']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
has_plot = any(keyword in cleaned_code for keyword in plot_keywords)
|
| 232 |
|
| 233 |
if not has_plot:
|
| 234 |
+
cleaned_code += '\n\n# Sample plot\nx = np.linspace(0, 10, 100)\ny = np.sin(x)\nplt.plot(x, y, "b-", linewidth=2, label="sin(x)")\n'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
|
| 236 |
+
# Remove plt.show
|
| 237 |
cleaned_code = cleaned_code.replace('plt.show()', '')
|
|
|
|
| 238 |
|
| 239 |
# Add labels if missing
|
| 240 |
if 'plt.xlabel' not in cleaned_code:
|
| 241 |
cleaned_code += '\nplt.xlabel("X-axis", fontsize=12)'
|
| 242 |
if 'plt.ylabel' not in cleaned_code:
|
| 243 |
cleaned_code += '\nplt.ylabel("Y-axis", fontsize=12)'
|
|
|
|
|
|
|
| 244 |
if 'plt.grid' not in cleaned_code:
|
| 245 |
cleaned_code += '\nplt.grid(True, linestyle="--", alpha=0.6)'
|
| 246 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
exec(cleaned_code, namespace)
|
| 248 |
|
| 249 |
+
ax = plt.gca()
|
| 250 |
+
if not ax.get_title():
|
| 251 |
+
ax.set_title('Physics Graph', fontsize=14)
|
| 252 |
|
|
|
|
| 253 |
st.pyplot(fig)
|
|
|
|
|
|
|
| 254 |
plt.close(fig)
|
| 255 |
|
|
|
|
|
|
|
| 256 |
except Exception as e:
|
| 257 |
+
st.error(f"Graph Error: {str(e)[:100]}")
|
| 258 |
|
| 259 |
+
# Fallback plot
|
| 260 |
try:
|
| 261 |
fig, ax = plt.subplots(figsize=(10, 6))
|
| 262 |
x = np.linspace(0, 10, 100)
|
| 263 |
+
y = x**2
|
| 264 |
ax.plot(x, y, 'b-', linewidth=2, label='y = x²')
|
| 265 |
ax.set_xlabel('X', fontsize=12)
|
| 266 |
ax.set_ylabel('Y', fontsize=12)
|
| 267 |
+
ax.set_title('Sample Graph', fontsize=14)
|
| 268 |
ax.grid(True, linestyle='--', alpha=0.6)
|
| 269 |
ax.legend()
|
| 270 |
st.pyplot(fig)
|
| 271 |
plt.close(fig)
|
|
|
|
| 272 |
except:
|
| 273 |
+
st.warning("Could not generate graph.")
|
| 274 |
+
|
|
|
|
|
|
|
|
|
|
| 275 |
def display_message(role, content, enable_voice=False):
|
| 276 |
+
"""Display chat message."""
|
| 277 |
with st.chat_message(role):
|
| 278 |
|
| 279 |
text_to_display = content
|
| 280 |
|
| 281 |
+
# Extract code blocks
|
| 282 |
code_pattern = r'```python\s*(.*?)```'
|
| 283 |
code_matches = list(re.finditer(code_pattern, content, re.DOTALL))
|
| 284 |
|
|
|
|
| 285 |
for match in reversed(code_matches):
|
| 286 |
text_to_display = text_to_display.replace(match.group(0), "")
|
| 287 |
|
| 288 |
+
# Check for image tags
|
| 289 |
image_match = re.search(r'\[IMAGE:\s*(.*?)\]', text_to_display, re.IGNORECASE)
|
| 290 |
image_result = None
|
| 291 |
|
|
|
|
| 295 |
with st.spinner(f"Searching for '{search_query}'..."):
|
| 296 |
image_result = search_image(search_query)
|
| 297 |
|
| 298 |
+
# Display text
|
| 299 |
st.markdown(text_to_display)
|
| 300 |
+
|
| 301 |
+
# Show code and graph
|
| 302 |
if code_matches and role == "assistant":
|
| 303 |
for match in code_matches:
|
| 304 |
code_content = match.group(1).strip()
|
| 305 |
if code_content:
|
|
|
|
| 306 |
with st.expander("📝 View Python Code", expanded=False):
|
| 307 |
st.code(code_content, language='python')
|
| 308 |
|
|
|
|
| 309 |
with st.expander("📊 Generated Graph", expanded=True):
|
|
|
|
| 310 |
execute_plotting_code(code_content)
|
| 311 |
|
| 312 |
+
# Show image
|
| 313 |
if image_match and role == "assistant":
|
| 314 |
+
if image_result and "http" in str(image_result):
|
| 315 |
try:
|
| 316 |
+
st.image(image_result, caption=f"📷 {image_match.group(1)}", width=500)
|
|
|
|
| 317 |
st.markdown(f"🔗 [Open Image]({image_result})")
|
| 318 |
+
except:
|
| 319 |
+
st.warning("Could not display image.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
else:
|
| 321 |
+
st.warning(f"Image search failed.")
|
| 322 |
|
| 323 |
+
# Audio output
|
|
|
|
| 324 |
if enable_voice and role == "assistant" and len(text_to_display.strip()) > 10:
|
| 325 |
audio_bytes = generate_audio(text_to_display)
|
| 326 |
if audio_bytes:
|
| 327 |
st.audio(audio_bytes, format='audio/mp3')
|
| 328 |
+
|
| 329 |
+
# -----------------------------------------------------------------------------
|
| 330 |
+
# 5. GROQ API FUNCTION
|
| 331 |
+
# -----------------------------------------------------------------------------
|
| 332 |
+
|
| 333 |
def call_groq_api(api_key, messages, max_tokens=2000):
|
| 334 |
+
"""Call Groq API."""
|
| 335 |
url = "https://api.groq.com/openai/v1/chat/completions"
|
| 336 |
|
| 337 |
headers = {
|
|
|
|
| 339 |
"Content-Type": "application/json"
|
| 340 |
}
|
| 341 |
|
|
|
|
| 342 |
formatted_messages = []
|
| 343 |
for msg in messages:
|
| 344 |
if msg["role"] == "system":
|
|
|
|
| 352 |
"content": msg["content"]
|
| 353 |
})
|
| 354 |
|
|
|
|
| 355 |
models_to_try = [
|
| 356 |
"llama-3.1-8b-instant",
|
| 357 |
"llama-3.2-3b-preview",
|
|
|
|
| 358 |
]
|
| 359 |
|
| 360 |
for model in models_to_try:
|
|
|
|
| 374 |
return result["choices"][0]["message"]["content"]
|
| 375 |
elif response.status_code == 429:
|
| 376 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
|
| 378 |
+
except:
|
|
|
|
|
|
|
| 379 |
continue
|
| 380 |
|
| 381 |
+
return "Service temporarily unavailable. Please try again."
|
| 382 |
+
|
| 383 |
# System Instructions for the AI - Part 5a (Start)
|
| 384 |
SYSTEM_INSTRUCTIONS = """
|
| 385 |
**Identity:** Richard Feynman. Tutor for Singapore H2 Physics (9478).
|
|
|
|
| 496 |
)
|
| 497 |
|
| 498 |
enable_voice = st.toggle("🗣️ Text-to-Speech", value=False)
|
| 499 |
+
|
| 500 |
+
st.divider()
|
| 501 |
+
|
| 502 |
+
# Image Analysis Settings
|
| 503 |
+
st.header("🖼️ Image Analysis")
|
| 504 |
+
|
| 505 |
+
image_analysis_mode = st.radio(
|
| 506 |
+
"Analysis Method:",
|
| 507 |
+
["DashScope API", "Basic Analysis"],
|
| 508 |
+
index=0,
|
| 509 |
+
help="DashScope: Advanced AI analysis. Basic: Simple image info."
|
| 510 |
+
)
|
| 511 |
+
|
| 512 |
+
if image_analysis_mode == "DashScope API":
|
| 513 |
+
# Get API key from secrets or env
|
| 514 |
+
dashscope_key = None
|
| 515 |
+
if hasattr(st, 'secrets') and "DASHSCOPE_API_KEY" in st.secrets:
|
| 516 |
+
dashscope_key = st.secrets["DASHSCOPE_API_KEY"]
|
| 517 |
+
st.success("✓ DashScope API configured")
|
| 518 |
+
elif "DASHSCOPE_API_KEY" in os.environ:
|
| 519 |
+
dashscope_key = os.environ["DASHSCOPE_API_KEY"]
|
| 520 |
+
st.success("✓ DashScope API configured")
|
| 521 |
+
else:
|
| 522 |
+
st.warning("Add DASHSCOPE_API_KEY to secrets")
|
| 523 |
+
st.info("Get free credits: dashscope.aliyun.com")
|
| 524 |
+
else:
|
| 525 |
+
st.info("Basic image analysis mode")
|
| 526 |
|
| 527 |
#st.divider()
|
| 528 |
|
| 529 |
+
# Media Input
|
| 530 |
+
st.header("📤 Input Methods")
|
| 531 |
|
| 532 |
visual_content = None
|
| 533 |
+
image_analysis = None
|
| 534 |
+
|
| 535 |
+
# Method selection
|
| 536 |
+
input_method = st.radio(
|
| 537 |
+
"Choose input method:",
|
| 538 |
+
["Camera", "Upload"],
|
| 539 |
+
horizontal=True
|
| 540 |
)
|
| 541 |
|
| 542 |
+
if input_method == "Camera":
|
| 543 |
+
st.subheader("📷 Camera")
|
| 544 |
+
camera_photo = st.camera_input("Take a photo of physics problem")
|
| 545 |
+
|
| 546 |
+
if camera_photo:
|
| 547 |
+
try:
|
| 548 |
+
image = Image.open(camera_photo)
|
| 549 |
+
visual_content = image
|
| 550 |
+
st.image(image, caption="Camera Capture", width=200)
|
| 551 |
+
st.success("✓ Photo captured")
|
| 552 |
+
|
| 553 |
+
# Analyze immediately
|
| 554 |
+
with st.spinner("Analyzing image..."):
|
| 555 |
+
if image_analysis_mode == "DashScope API" and dashscope_key:
|
| 556 |
+
image_analysis = analyze_image_with_qwen_api(
|
| 557 |
+
image,
|
| 558 |
+
"Analyze this physics image. What concepts, diagrams, or equations do you see?",
|
| 559 |
+
dashscope_key
|
| 560 |
+
)
|
| 561 |
+
else:
|
| 562 |
+
image_analysis = analyze_image_local_fallback(
|
| 563 |
+
image,
|
| 564 |
+
"Analyze this physics image"
|
| 565 |
+
)
|
| 566 |
+
|
| 567 |
+
if image_analysis:
|
| 568 |
+
st.info(f"📋 Analysis: {image_analysis[:150]}...")
|
| 569 |
+
|
| 570 |
+
except Exception as e:
|
| 571 |
+
st.error(f"Camera error: {e}")
|
| 572 |
+
|
| 573 |
+
else: # Upload
|
| 574 |
+
st.subheader("📁 Upload")
|
| 575 |
+
uploaded_file = st.file_uploader(
|
| 576 |
+
"Choose image",
|
| 577 |
+
type=["jpg", "jpeg", "png"],
|
| 578 |
+
help="Upload physics diagrams or problems"
|
| 579 |
+
)
|
| 580 |
+
|
| 581 |
+
if uploaded_file:
|
| 582 |
+
try:
|
| 583 |
+
image = Image.open(uploaded_file)
|
| 584 |
+
visual_content = image
|
| 585 |
+
st.image(image, caption="Uploaded Image", width=200)
|
| 586 |
+
st.success("✓ Image loaded")
|
| 587 |
+
|
| 588 |
+
# Analyze immediately
|
| 589 |
+
with st.spinner("Analyzing image..."):
|
| 590 |
+
if image_analysis_mode == "DashScope API" and dashscope_key:
|
| 591 |
+
image_analysis = analyze_image_with_qwen_api(
|
| 592 |
+
image,
|
| 593 |
+
"Analyze this physics image. What concepts, diagrams, or equations do you see?",
|
| 594 |
+
dashscope_key
|
| 595 |
+
)
|
| 596 |
+
else:
|
| 597 |
+
image_analysis = analyze_image_local_fallback(
|
| 598 |
+
image,
|
| 599 |
+
"Analyze this physics image"
|
| 600 |
+
)
|
| 601 |
+
|
| 602 |
+
if image_analysis:
|
| 603 |
+
st.info(f"📋 Analysis: {image_analysis[:150]}...")
|
| 604 |
+
|
| 605 |
+
except Exception as e:
|
| 606 |
+
st.error(f"Upload error: {e}")
|
| 607 |
|
| 608 |
#st.divider()
|
| 609 |
|
| 610 |
+
# Controls
|
| 611 |
if st.button("🧹 Clear Chat History", use_container_width=True):
|
| 612 |
st.session_state.messages = []
|
| 613 |
st.rerun()
|
| 614 |
|
| 615 |
st.divider()
|
| 616 |
+
st.caption("H2 Physics 9478 | Qwen-VL Image Analysis")
|
| 617 |
st.caption("Made with ❤️ for JPJC H2 Physics students | Powered by Groq AI")
|
| 618 |
|
| 619 |
# 7 Main Chat Interface
|
|
|
|
| 641 |
user_message = ""
|
| 642 |
if user_input:
|
| 643 |
user_message += user_input + " "
|
| 644 |
+
|
| 645 |
if visual_content:
|
| 646 |
+
user_message += "[I have uploaded/taken a photo of a physics problem] "
|
| 647 |
+
|
| 648 |
+
if image_analysis:
|
| 649 |
+
user_message += f"[Image Analysis: {image_analysis}] "
|
| 650 |
+
|
| 651 |
if topic != "General / Any":
|
| 652 |
user_message += f"(Focus on: {topic})"
|
| 653 |
|
| 654 |
+
# Add to history
|
| 655 |
st.session_state.messages.append({"role": "user", "content": user_message})
|
| 656 |
|
| 657 |
# Display user message
|
| 658 |
with st.chat_message("user"):
|
| 659 |
if user_input:
|
| 660 |
st.markdown(user_input)
|
| 661 |
+
|
| 662 |
if visual_content:
|
| 663 |
+
st.image(visual_content, caption="Your Image", width=300)
|
| 664 |
+
|
| 665 |
+
if image_analysis:
|
| 666 |
+
with st.expander("📋 Image Analysis Details"):
|
| 667 |
+
st.write(image_analysis)
|
| 668 |
|
| 669 |
+
# Check for Groq API key
|
| 670 |
+
groq_key = None
|
| 671 |
+
if hasattr(st, 'secrets') and "GROQ_API_KEY" in st.secrets:
|
| 672 |
+
groq_key = st.secrets["GROQ_API_KEY"]
|
| 673 |
+
elif "GROQ_API_KEY" in os.environ:
|
| 674 |
+
groq_key = os.environ["GROQ_API_KEY"]
|
| 675 |
+
|
| 676 |
+
if not groq_key:
|
| 677 |
st.error(
|
| 678 |
+
"**API Configuration Required**\n\n"
|
| 679 |
+
"Groq API key not found. Please ensure GROQ_API_KEY is set in secrets."
|
|
|
|
|
|
|
| 680 |
)
|
| 681 |
st.stop()
|
| 682 |
|
| 683 |
# Prepare API call
|
| 684 |
try:
|
|
|
|
| 685 |
conversation_context = []
|
|
|
|
|
|
|
| 686 |
conversation_context.append({"role": "system", "content": SYSTEM_INSTRUCTIONS})
|
| 687 |
|
|
|
|
| 688 |
recent_messages = st.session_state.messages[-8:]
|
| 689 |
for msg in recent_messages:
|
| 690 |
+
if msg["role"] != "system":
|
| 691 |
conversation_context.append(msg)
|
| 692 |
|
| 693 |
# Call Groq API
|
| 694 |
with st.spinner("Feynman is thinking... ⚛️"):
|
| 695 |
+
response_text = call_groq_api(groq_key, conversation_context)
|
| 696 |
|
|
|
|
| 697 |
if response_text:
|
| 698 |
+
st.session_state.messages.append({"role": "assistant", "content": response_text})
|
| 699 |
+
display_message("assistant", response_text, enable_voice)
|
| 700 |
+
|
| 701 |
+
# Clear image content after processing
|
| 702 |
+
if 'visual_content' in locals():
|
| 703 |
+
visual_content = None
|
| 704 |
+
if 'image_analysis' in locals():
|
| 705 |
+
image_analysis = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
|
|
|
|
|
|
|
|
|
|
| 707 |
else:
|
| 708 |
+
st.error("Failed to get response. Please try again.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
|
| 710 |
except Exception as e:
|
| 711 |
+
st.error(f"Error: {str(e)[:200]}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
|
| 713 |
# Footer - SIMPLE VERSION
|
| 714 |
st.divider()
|
| 715 |
+
st.markdown("H2 Physics Feynman Tutor | Singapore H2 Physics (9478) Syllabus")
|
| 716 |
+
st.markdown("Powered by Groq AI + Qwen-VL Image Analysis")
|
| 717 |
+
st.markdown("AI tutoring assistant. Verify with official syllabus.")
|