Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -5,9 +5,9 @@ import json
|
|
| 5 |
import base64
|
| 6 |
from dotenv import load_dotenv
|
| 7 |
from streamlit_local_storage import LocalStorage
|
| 8 |
-
import plotly.graph_objects as go
|
| 9 |
-
import numpy as np
|
| 10 |
import re
|
|
|
|
|
|
|
| 11 |
|
| 12 |
# --- PAGE CONFIGURATION ---
|
| 13 |
st.set_page_config(
|
|
@@ -35,322 +35,182 @@ def convert_role_for_gemini(role):
|
|
| 35 |
return role # "user" stays the same
|
| 36 |
|
| 37 |
def should_generate_visual(user_prompt, ai_response):
|
| 38 |
-
"""Determine if a visual aid would be helpful
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
'
|
| 42 |
-
'
|
| 43 |
-
'
|
| 44 |
-
'
|
| 45 |
-
'
|
| 46 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
]
|
| 48 |
|
| 49 |
combined_text = (user_prompt + " " + ai_response).lower()
|
| 50 |
-
return any(keyword in combined_text for keyword in
|
| 51 |
|
| 52 |
-
def
|
| 53 |
-
"""
|
|
|
|
| 54 |
try:
|
| 55 |
-
|
| 56 |
-
numbers = [numbers]
|
| 57 |
-
|
| 58 |
-
fig = go.Figure()
|
| 59 |
-
colors = ['red', 'blue', 'green', 'orange', 'purple', 'yellow']
|
| 60 |
-
|
| 61 |
-
for i, num in enumerate(numbers[:6]): # Limit to 6 different numbers
|
| 62 |
-
if num <= 20: # Only for reasonable counting numbers
|
| 63 |
-
# Create dots arranged in rows
|
| 64 |
-
dots_per_row = min(5, num)
|
| 65 |
-
rows = (num - 1) // dots_per_row + 1
|
| 66 |
-
|
| 67 |
-
x_positions = []
|
| 68 |
-
y_positions = []
|
| 69 |
-
|
| 70 |
-
for dot in range(num):
|
| 71 |
-
row = dot // dots_per_row
|
| 72 |
-
col = dot % dots_per_row
|
| 73 |
-
x_positions.append(col + i * 7) # Separate groups
|
| 74 |
-
y_positions.append(-row + rows - 1)
|
| 75 |
-
|
| 76 |
-
fig.add_trace(go.Scatter(
|
| 77 |
-
x=x_positions,
|
| 78 |
-
y=y_positions,
|
| 79 |
-
mode='markers',
|
| 80 |
-
marker=dict(
|
| 81 |
-
size=20,
|
| 82 |
-
color=colors[i],
|
| 83 |
-
symbol='circle',
|
| 84 |
-
line=dict(width=2, color='black')
|
| 85 |
-
),
|
| 86 |
-
name=f'{num} items',
|
| 87 |
-
showlegend=True
|
| 88 |
-
))
|
| 89 |
-
|
| 90 |
-
# Add number label
|
| 91 |
-
fig.add_annotation(
|
| 92 |
-
x=2 + i * 7,
|
| 93 |
-
y=-rows - 0.5,
|
| 94 |
-
text=str(num),
|
| 95 |
-
font=dict(size=24, color=colors[i]),
|
| 96 |
-
showlegend=False
|
| 97 |
-
)
|
| 98 |
-
|
| 99 |
-
fig.update_layout(
|
| 100 |
-
title="Counting Visualization",
|
| 101 |
-
showlegend=True,
|
| 102 |
-
xaxis=dict(showgrid=False, showticklabels=False, zeroline=False),
|
| 103 |
-
yaxis=dict(showgrid=False, showticklabels=False, zeroline=False),
|
| 104 |
-
height=400,
|
| 105 |
-
template="simple_white"
|
| 106 |
-
)
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
fig = go.Figure()
|
| 119 |
-
|
| 120 |
-
# First number (red circles)
|
| 121 |
-
x1 = list(range(num1))
|
| 122 |
-
y1 = [1] * num1
|
| 123 |
-
fig.add_trace(go.Scatter(
|
| 124 |
-
x=x1, y=y1,
|
| 125 |
-
mode='markers',
|
| 126 |
-
marker=dict(size=25, color='red', symbol='circle', line=dict(width=2, color='black')),
|
| 127 |
-
name=f'First group: {num1}',
|
| 128 |
-
showlegend=True
|
| 129 |
-
))
|
| 130 |
-
|
| 131 |
-
# Second number (blue circles)
|
| 132 |
-
x2 = list(range(num1 + 1, num1 + num2 + 1))
|
| 133 |
-
y2 = [1] * num2
|
| 134 |
-
fig.add_trace(go.Scatter(
|
| 135 |
-
x=x2, y=y2,
|
| 136 |
-
mode='markers',
|
| 137 |
-
marker=dict(size=25, color='blue', symbol='circle', line=dict(width=2, color='black')),
|
| 138 |
-
name=f'Second group: {num2}',
|
| 139 |
-
showlegend=True
|
| 140 |
-
))
|
| 141 |
-
|
| 142 |
-
# Plus sign
|
| 143 |
-
fig.add_annotation(
|
| 144 |
-
x=num1 - 0.5,
|
| 145 |
-
y=1.5,
|
| 146 |
-
text="+",
|
| 147 |
-
font=dict(size=30, color='black'),
|
| 148 |
-
showlegend=False
|
| 149 |
-
)
|
| 150 |
-
|
| 151 |
-
# Equals sign and result
|
| 152 |
-
fig.add_annotation(
|
| 153 |
-
x=num1 + num2 + 0.5,
|
| 154 |
-
y=1.5,
|
| 155 |
-
text="=",
|
| 156 |
-
font=dict(size=30, color='black'),
|
| 157 |
-
showlegend=False
|
| 158 |
-
)
|
| 159 |
-
|
| 160 |
-
fig.add_annotation(
|
| 161 |
-
x=num1 + num2 + 1.5,
|
| 162 |
-
y=1.5,
|
| 163 |
-
text=str(num1 + num2),
|
| 164 |
-
font=dict(size=30, color='green'),
|
| 165 |
-
showlegend=False
|
| 166 |
-
)
|
| 167 |
-
|
| 168 |
-
fig.update_layout(
|
| 169 |
-
title=f"Addition: {num1} + {num2} = {num1 + num2}",
|
| 170 |
-
showlegend=True,
|
| 171 |
-
xaxis=dict(showgrid=False, showticklabels=False, zeroline=False, range=[-1, num1 + num2 + 3]),
|
| 172 |
-
yaxis=dict(showgrid=False, showticklabels=False, zeroline=False, range=[0, 2.5]),
|
| 173 |
-
height=300,
|
| 174 |
-
template="simple_white"
|
| 175 |
-
)
|
| 176 |
-
|
| 177 |
-
return fig
|
| 178 |
-
except:
|
| 179 |
-
return None
|
| 180 |
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
fig = go.Figure()
|
| 188 |
-
|
| 189 |
-
# Create a circle divided into parts
|
| 190 |
-
angles = np.linspace(0, 2*np.pi, denominator + 1)
|
| 191 |
-
|
| 192 |
-
for i in range(denominator):
|
| 193 |
-
# Create each slice
|
| 194 |
-
theta = np.linspace(angles[i], angles[i+1], 50)
|
| 195 |
-
r = np.ones_like(theta)
|
| 196 |
-
x = r * np.cos(theta)
|
| 197 |
-
y = r * np.sin(theta)
|
| 198 |
-
|
| 199 |
-
# Add center point
|
| 200 |
-
x = np.concatenate([[0], x, [0]])
|
| 201 |
-
y = np.concatenate([[0], y, [0]])
|
| 202 |
-
|
| 203 |
-
color = 'lightblue' if i < numerator else 'lightgray'
|
| 204 |
-
|
| 205 |
-
fig.add_trace(go.Scatter(
|
| 206 |
-
x=x, y=y,
|
| 207 |
-
fill='toself',
|
| 208 |
-
mode='lines',
|
| 209 |
-
line=dict(color='black', width=2),
|
| 210 |
-
fillcolor=color,
|
| 211 |
-
name=f'Slice {i+1}' if i < numerator else '',
|
| 212 |
-
showlegend=False
|
| 213 |
-
))
|
| 214 |
-
|
| 215 |
-
fig.update_layout(
|
| 216 |
-
title=f"Fraction: {numerator}/{denominator}",
|
| 217 |
-
xaxis=dict(showgrid=False, showticklabels=False, zeroline=False, scaleanchor="y", scaleratio=1),
|
| 218 |
-
yaxis=dict(showgrid=False, showticklabels=False, zeroline=False),
|
| 219 |
-
height=400,
|
| 220 |
-
template="simple_white"
|
| 221 |
-
)
|
| 222 |
-
|
| 223 |
-
return fig
|
| 224 |
-
except:
|
| 225 |
-
return None
|
| 226 |
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
tens = int(str_num[-2])
|
| 236 |
-
hundreds = int(str_num[-3])
|
| 237 |
-
thousands = int(str_num[-4])
|
| 238 |
-
|
| 239 |
-
fig = go.Figure()
|
| 240 |
-
|
| 241 |
-
# Create visual blocks for each place value
|
| 242 |
-
positions = [0, 2, 4, 6] # x positions for thousands, hundreds, tens, ones
|
| 243 |
-
values = [thousands, hundreds, tens, ones]
|
| 244 |
-
labels = ['Thousands', 'Hundreds', 'Tens', 'Ones']
|
| 245 |
-
colors = ['red', 'blue', 'green', 'orange']
|
| 246 |
-
|
| 247 |
-
for i, (pos, val, label, color) in enumerate(zip(positions, values, labels, colors)):
|
| 248 |
-
if val > 0:
|
| 249 |
-
# Create blocks to represent the value
|
| 250 |
-
blocks_per_row = min(5, val)
|
| 251 |
-
rows = (val - 1) // blocks_per_row + 1
|
| 252 |
-
|
| 253 |
-
x_coords = []
|
| 254 |
-
y_coords = []
|
| 255 |
-
|
| 256 |
-
for block in range(val):
|
| 257 |
-
row = block // blocks_per_row
|
| 258 |
-
col = block % blocks_per_row
|
| 259 |
-
x_coords.append(pos + col * 0.3)
|
| 260 |
-
y_coords.append(row * 0.3)
|
| 261 |
-
|
| 262 |
-
fig.add_trace(go.Scatter(
|
| 263 |
-
x=x_coords,
|
| 264 |
-
y=y_coords,
|
| 265 |
-
mode='markers',
|
| 266 |
-
marker=dict(
|
| 267 |
-
size=15,
|
| 268 |
-
color=color,
|
| 269 |
-
symbol='square',
|
| 270 |
-
line=dict(width=1, color='black')
|
| 271 |
-
),
|
| 272 |
-
name=f'{label}: {val}',
|
| 273 |
-
showlegend=True
|
| 274 |
-
))
|
| 275 |
-
|
| 276 |
-
# Add place value label
|
| 277 |
-
fig.add_annotation(
|
| 278 |
-
x=pos + 0.6,
|
| 279 |
-
y=-0.5,
|
| 280 |
-
text=label,
|
| 281 |
-
font=dict(size=12),
|
| 282 |
-
showlegend=False
|
| 283 |
-
)
|
| 284 |
-
|
| 285 |
-
# Add digit
|
| 286 |
-
fig.add_annotation(
|
| 287 |
-
x=pos + 0.6,
|
| 288 |
-
y=-0.8,
|
| 289 |
-
text=str(val),
|
| 290 |
-
font=dict(size=16, color=colors[i]),
|
| 291 |
-
showlegend=False
|
| 292 |
-
)
|
| 293 |
-
|
| 294 |
-
fig.update_layout(
|
| 295 |
-
title=f"Place Value: {number}",
|
| 296 |
-
showlegend=True,
|
| 297 |
-
xaxis=dict(showgrid=False, showticklabels=False, zeroline=False, range=[-0.5, 7]),
|
| 298 |
-
yaxis=dict(showgrid=False, showticklabels=False, zeroline=False),
|
| 299 |
-
height=400,
|
| 300 |
-
template="simple_white"
|
| 301 |
-
)
|
| 302 |
-
|
| 303 |
-
return fig
|
| 304 |
-
except:
|
| 305 |
-
return None
|
| 306 |
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
if
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
else:
|
| 331 |
-
num, den = 1, 2 # Default to 1/2
|
| 332 |
-
return create_fraction_visual(num, den)
|
| 333 |
-
|
| 334 |
-
# PLACE VALUE
|
| 335 |
-
if 'place value' in user_lower or 'place' in user_lower:
|
| 336 |
-
place_match = re.search(r'\b(\d{1,4})\b', user_prompt)
|
| 337 |
-
if place_match:
|
| 338 |
-
number = int(place_match.group(1))
|
| 339 |
-
return create_place_value_visual(number)
|
| 340 |
-
|
| 341 |
-
# NUMBERS (general counting)
|
| 342 |
-
number_match = re.search(r'\b(\d+)\b', user_prompt)
|
| 343 |
-
if number_match and any(word in user_lower for word in ['show', 'count', 'number']):
|
| 344 |
-
number = int(number_match.group(1))
|
| 345 |
-
if 1 <= number <= 20:
|
| 346 |
-
return create_counting_visual(number)
|
| 347 |
-
|
| 348 |
-
return None
|
| 349 |
|
| 350 |
except Exception as e:
|
| 351 |
-
st.error(f"Could not
|
| 352 |
return None
|
| 353 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
# --- API KEY & MODEL CONFIGURATION ---
|
| 355 |
load_dotenv()
|
| 356 |
api_key = None
|
|
@@ -367,43 +227,49 @@ if api_key:
|
|
| 367 |
model = genai.GenerativeModel(
|
| 368 |
model_name="gemini-2.5-flash-lite",
|
| 369 |
system_instruction="""
|
| 370 |
-
You are "Math Jegna", an AI
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
-
|
| 376 |
-
-
|
| 377 |
-
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
|
| 406 |
-
|
| 407 |
"""
|
| 408 |
)
|
| 409 |
else:
|
|
@@ -430,7 +296,7 @@ if "chats" not in st.session_state:
|
|
| 430 |
else:
|
| 431 |
st.session_state.chats = {
|
| 432 |
"New Chat": [
|
| 433 |
-
{"role": "assistant", "content": "Hello! I'm Math Jegna, your friendly math helper!
|
| 434 |
]
|
| 435 |
}
|
| 436 |
st.session_state.active_chat_key = "New Chat"
|
|
@@ -456,110 +322,126 @@ def delete_chat(chat_key):
|
|
| 456 |
st.warning(f"Are you sure you want to delete '{chat_key}'? This cannot be undone.")
|
| 457 |
if st.button("Yes, Delete", type="primary", key=f"confirm_delete_{chat_key}"):
|
| 458 |
st.session_state.chats.pop(chat_key)
|
|
|
|
| 459 |
if st.session_state.active_chat_key == chat_key:
|
| 460 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
st.rerun()
|
| 462 |
|
| 463 |
-
# ---
|
| 464 |
-
st.sidebar
|
| 465 |
-
st.
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
|
| 478 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
file_name=f"{chat_key.replace(' ', '_')}.md",
|
| 498 |
-
mime="text/markdown"
|
| 499 |
-
)
|
| 500 |
-
st.markdown("**Share via Link**")
|
| 501 |
-
st.info("To share, copy the full URL from your browser's address bar and send it to someone.")
|
| 502 |
-
|
| 503 |
-
if st.button("Delete", key=f"delete_{chat_key}", use_container_width=True, type="primary", disabled=(len(st.session_state.chats) <= 1)):
|
| 504 |
-
delete_chat(chat_key)
|
| 505 |
-
|
| 506 |
-
# --- MAIN CHAT INTERFACE ---
|
| 507 |
-
active_chat = st.session_state.chats[st.session_state.active_chat_key]
|
| 508 |
-
|
| 509 |
-
st.title(f"Math Helper: {st.session_state.active_chat_key} ๐ง ")
|
| 510 |
-
st.write("๐ฏ Perfect for young learners! Ask about counting, adding, shapes, fractions, and more!")
|
| 511 |
-
|
| 512 |
-
# Add some example prompts for young learners
|
| 513 |
-
with st.expander("๐ก Try asking me about..."):
|
| 514 |
-
st.write("""
|
| 515 |
-
- **Counting**: "Show me how to count to 10"
|
| 516 |
-
- **Addition**: "What is 3 + 4?"
|
| 517 |
-
- **Fractions**: "What is 1/2?"
|
| 518 |
-
- **Place Value**: "What is the place value of 325?"
|
| 519 |
-
- **Shapes**: "Tell me about triangles"
|
| 520 |
-
- **Time**: "How do I read a clock?"
|
| 521 |
-
""")
|
| 522 |
-
|
| 523 |
-
for message in active_chat:
|
| 524 |
-
with st.chat_message(name=message["role"], avatar="๐งโ๐ป" if message["role"] == "user" else "๐ง "):
|
| 525 |
st.markdown(message["content"])
|
| 526 |
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
|
|
|
|
|
|
| 531 |
|
| 532 |
-
|
| 533 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
try:
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
{'role': convert_role_for_gemini(msg['role']), 'parts': [msg['content']]}
|
| 538 |
-
for msg in active_chat[:-1] if 'content' in msg
|
| 539 |
-
])
|
| 540 |
-
response = chat_session.send_message(user_prompt)
|
| 541 |
-
ai_response_text = response.text
|
| 542 |
-
st.markdown(ai_response_text)
|
| 543 |
|
| 544 |
-
|
| 545 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
|
| 547 |
-
#
|
| 548 |
-
if should_generate_visual(
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
|
|
|
|
|
|
| 554 |
|
|
|
|
|
|
|
|
|
|
| 555 |
except Exception as e:
|
| 556 |
-
|
| 557 |
-
st.error(error_message)
|
| 558 |
-
active_chat.append({"role": "assistant", "content": error_message})
|
| 559 |
-
|
| 560 |
-
# --- SAVE DATA TO LOCAL STORAGE ---
|
| 561 |
-
data_to_save = {
|
| 562 |
-
"chats": st.session_state.chats,
|
| 563 |
-
"active_chat_key": st.session_state.active_chat_key
|
| 564 |
-
}
|
| 565 |
-
localS.setItem("math_mentor_chats", json.dumps(data_to_save))
|
|
|
|
| 5 |
import base64
|
| 6 |
from dotenv import load_dotenv
|
| 7 |
from streamlit_local_storage import LocalStorage
|
|
|
|
|
|
|
| 8 |
import re
|
| 9 |
+
import streamlit.components.v1 as components
|
| 10 |
+
import math # Needed for trigonometry in dynamic visuals
|
| 11 |
|
| 12 |
# --- PAGE CONFIGURATION ---
|
| 13 |
st.set_page_config(
|
|
|
|
| 35 |
return role # "user" stays the same
|
| 36 |
|
| 37 |
def should_generate_visual(user_prompt, ai_response):
|
| 38 |
+
"""Determine if a visual aid would be helpful based on the content"""
|
| 39 |
+
# Expanded keywords to trigger new dynamic visuals
|
| 40 |
+
k12_visual_keywords = [
|
| 41 |
+
'add', 'subtract', 'multiply', 'times', 'divide', 'counting', 'numbers',
|
| 42 |
+
'fraction', 'half', 'quarter', 'third', 'parts', 'whole',
|
| 43 |
+
'shape', 'triangle', 'circle', 'square', 'rectangle',
|
| 44 |
+
'money', 'coins', 'dollars', 'cents', 'change',
|
| 45 |
+
'time', 'clock', 'hours', 'minutes', 'o\'clock',
|
| 46 |
+
'measurement', 'length', 'height', 'weight',
|
| 47 |
+
'place value', 'tens', 'ones', 'hundreds',
|
| 48 |
+
'pattern', 'sequence', 'skip counting',
|
| 49 |
+
'greater than', 'less than', 'equal', 'compare',
|
| 50 |
+
'number line', 'array', 'grid'
|
| 51 |
]
|
| 52 |
|
| 53 |
combined_text = (user_prompt + " " + ai_response).lower()
|
| 54 |
+
return any(keyword in combined_text for keyword in k12_visual_keywords)
|
| 55 |
|
| 56 |
+
def create_visual_manipulative(user_prompt, ai_response):
|
| 57 |
+
"""-- SMART VISUAL ROUTER --
|
| 58 |
+
Parses the user prompt and calls the appropriate dynamic visual function."""
|
| 59 |
try:
|
| 60 |
+
user_lower = user_prompt.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
+
# Priority 1: Time / Clock (e.g., "7:30", "4 o'clock")
|
| 63 |
+
time_match = re.search(r'(\d{1,2}):(\d{2})', user_lower) or re.search(r'(\d{1,2})\s*o\'clock', user_lower)
|
| 64 |
+
if time_match:
|
| 65 |
+
groups = time_match.groups()
|
| 66 |
+
hour = int(groups[0])
|
| 67 |
+
minute = int(groups[1]) if len(groups) > 1 and groups[1] else 0
|
| 68 |
+
if 1 <= hour <= 12 and 0 <= minute <= 59:
|
| 69 |
+
return create_clock_visual(hour, minute)
|
| 70 |
|
| 71 |
+
# Priority 2: Fractions (e.g., "2/5", "fraction 3/8")
|
| 72 |
+
fraction_match = re.search(r'(\d+)/(\d+)', user_lower)
|
| 73 |
+
if fraction_match:
|
| 74 |
+
num, den = int(fraction_match.group(1)), int(fraction_match.group(2))
|
| 75 |
+
if 0 < num <= den and den <= 16: # Keep it visually clean
|
| 76 |
+
return create_dynamic_fraction_circle(num, den)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
+
# Priority 3: Multiplication Arrays (e.g., "3 times 5", "4 x 6")
|
| 79 |
+
mult_match = re.search(r'(\d+)\s*(?:x|times)\s*(\d+)', user_lower)
|
| 80 |
+
if mult_match:
|
| 81 |
+
rows, cols = int(mult_match.group(1)), int(mult_match.group(2))
|
| 82 |
+
if rows <= 10 and cols <= 10: # Keep arrays reasonable
|
| 83 |
+
return create_multiplication_array(rows, cols)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
# Priority 4: Addition/Subtraction Blocks
|
| 86 |
+
if any(word in user_lower for word in ['add', 'plus', '+', 'subtract', 'minus', 'take away', '-']):
|
| 87 |
+
numbers = re.findall(r'\d+', user_prompt)
|
| 88 |
+
if len(numbers) >= 2:
|
| 89 |
+
num1, num2 = int(numbers[0]), int(numbers[1])
|
| 90 |
+
operation = 'add' if any(w in user_lower for w in ['add', 'plus', '+']) else 'subtract'
|
| 91 |
+
if num1 <= 20 and num2 <= 20:
|
| 92 |
+
return create_counting_blocks(num1, num2, operation)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
+
# Priority 5: Number Lines
|
| 95 |
+
if 'number line' in user_lower:
|
| 96 |
+
numbers = [int(n) for n in re.findall(r'\d+', user_prompt)]
|
| 97 |
+
if numbers:
|
| 98 |
+
start = min(numbers) - 2
|
| 99 |
+
end = max(numbers) + 2
|
| 100 |
+
return create_number_line(start, end, numbers, "Your Numbers on the Line")
|
| 101 |
+
|
| 102 |
+
# Priority 6: Place Value
|
| 103 |
+
if 'place value' in user_lower:
|
| 104 |
+
numbers = re.findall(r'\d+', user_prompt)
|
| 105 |
+
if numbers:
|
| 106 |
+
num = int(numbers[0])
|
| 107 |
+
if num <= 999:
|
| 108 |
+
return create_place_value_blocks(num)
|
| 109 |
+
|
| 110 |
+
# Fallback to static, general visuals
|
| 111 |
+
if any(word in user_lower for word in ['fraction', 'part']): return create_dynamic_fraction_circle(1, 2) # Show a default example
|
| 112 |
+
if any(word in user_lower for word in ['shape']): return create_shape_explorer()
|
| 113 |
+
if any(word in user_lower for word in ['money', 'coin']): return create_money_counter()
|
| 114 |
+
if any(word in user_lower for word in ['time', 'clock']): return create_clock_visual(10, 10) # Show a default example
|
| 115 |
+
|
| 116 |
+
return None # No relevant visual found
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
except Exception as e:
|
| 119 |
+
st.error(f"Could not create visual: {e}")
|
| 120 |
return None
|
| 121 |
|
| 122 |
+
# --- VISUAL TOOLBOX FUNCTIONS ---
|
| 123 |
+
|
| 124 |
+
def create_counting_blocks(num1, num2, operation):
|
| 125 |
+
"""(Dynamic) Create colorful counting blocks for addition/subtraction."""
|
| 126 |
+
html = f"""
|
| 127 |
+
<div style="padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; margin: 10px 0;">
|
| 128 |
+
<h3 style="color: white; text-align: center; margin-bottom: 20px;">๐งฎ Counting Blocks: {num1} {'+' if operation == 'add' else 'โ'} {num2}</h3>
|
| 129 |
+
<div style="display: flex; justify-content: center; align-items: center; gap: 20px; flex-wrap: wrap;">
|
| 130 |
+
<!-- Blocks for Num1 -->
|
| 131 |
+
<div style="display: flex; flex-wrap: wrap; gap: 5px; border: 2px dashed #FFE066; padding: 5px; border-radius: 5px; align-items: center; justify-content: center; min-width: 100px;"><div style="width: 100%; text-align:center; color: white; font-weight: bold;">{num1}</div>{''.join([f'<div style="width: 25px; height: 25px; background: #FF6B6B; border-radius: 5px;"></div>' for _ in range(num1)])}</div>
|
| 132 |
+
<div style="font-size: 40px; color: #FFE066;">{'+' if operation == 'add' else 'โ'}</div>
|
| 133 |
+
<!-- Blocks for Num2 -->
|
| 134 |
+
<div style="display: flex; flex-wrap: wrap; gap: 5px; border: 2px dashed #FFE066; padding: 5px; border-radius: 5px; align-items: center; justify-content: center; min-width: 100px;"><div style="width: 100%; text-align:center; color: white; font-weight: bold;">{num2}</div>{''.join([f'<div style="width: 25px; height: 25px; background: #4ECDC4; border-radius: 5px;"></div>' for _ in range(num2)])}</div>
|
| 135 |
+
<div style="font-size: 40px; color: #FFE066;">=</div>
|
| 136 |
+
<!-- Blocks for Answer -->
|
| 137 |
+
<div style="display: flex; flex-wrap: wrap; gap: 5px; border: 2px solid white; background: rgba(255,255,255,0.2); padding: 5px; border-radius: 5px; align-items: center; justify-content: center; min-width: 100px;"><div style="width: 100%; text-align:center; color: white; font-weight: bold;">{num1 + num2 if operation == 'add' else max(0, num1 - num2)}</div>{''.join([f'<div style="width: 25px; height: 25px; background: #95E1D3; border-radius: 5px;"></div>' for _ in range(num1 + num2 if operation == 'add' else max(0, num1 - num2))])}</div>
|
| 138 |
+
</div>
|
| 139 |
+
</div>"""
|
| 140 |
+
return html
|
| 141 |
+
|
| 142 |
+
def create_dynamic_fraction_circle(numerator, denominator):
|
| 143 |
+
"""(Dynamic) Generates an SVG of a pizza/pie to represent a fraction."""
|
| 144 |
+
if not (0 < numerator <= denominator): return "<p>I can only show proper fractions!</p>"
|
| 145 |
+
width, height, radius = 150, 150, 60
|
| 146 |
+
cx, cy = width / 2, height / 2
|
| 147 |
+
slices_html = ''
|
| 148 |
+
angle_step = 360 / denominator
|
| 149 |
+
for i in range(denominator):
|
| 150 |
+
start_angle, end_angle = i * angle_step, (i + 1) * angle_step
|
| 151 |
+
fill_color = "#FF6B6B" if i < numerator else "#DDDDDD"
|
| 152 |
+
start_rad, end_rad = math.radians(start_angle - 90), math.radians(end_angle - 90)
|
| 153 |
+
x1, y1 = cx + radius * math.cos(start_rad), cy + radius * math.sin(start_rad)
|
| 154 |
+
x2, y2 = cx + radius * math.cos(end_rad), cy + radius * math.sin(end_rad)
|
| 155 |
+
large_arc_flag = 1 if angle_step > 180 else 0
|
| 156 |
+
path_d = f"M {cx},{cy} L {x1},{y1} A {radius},{radius} 0 {large_arc_flag},1 {x2},{y2} Z"
|
| 157 |
+
slices_html += f'<path d="{path_d}" fill="{fill_color}" stroke="#333" stroke-width="2"/>'
|
| 158 |
+
html = f"""<div style="padding: 20px; background: linear-gradient(135deg, #A8EDEA 0%, #FED6E3 100%); border-radius: 15px; margin: 10px 0;"><h3 style="color: #333; text-align: center;">Fraction Pizza: {numerator}/{denominator}</h3><div style="display: flex; justify-content: center;"><svg width="{width}" height="{height}">{slices_html}</svg></div><p style="color: #333; text-align: center; margin-top: 15px; font-size: 18px;">The pizza is cut into <b>{denominator}</b> equal slices, and we are showing <b>{numerator}</b> of them! ๐</p></div>"""
|
| 159 |
+
return html
|
| 160 |
+
|
| 161 |
+
def create_clock_visual(hours, minutes):
|
| 162 |
+
"""(Dynamic) Create a clock showing a specific time."""
|
| 163 |
+
min_angle = minutes * 6
|
| 164 |
+
hour_angle = (hours % 12 + minutes / 60) * 30
|
| 165 |
+
html = f"""<div style="padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; margin: 10px 0;"><h3 style="color: white; text-align: center; margin-bottom: 20px;">๐ Learning Time!</h3><div style="display: flex; justify-content: center;"><svg width="250" height="250" viewBox="0 0 250 250" style="background: white; border-radius: 50%; border: 8px solid #FFE066;"><circle cx="125" cy="125" r="110" fill="white" stroke="#333" stroke-width="2"/><text x="125" y="45" text-anchor="middle" font-size="20" font-weight="bold" fill="#333">12</text><text x="205" y="130" text-anchor="middle" font-size="20" font-weight="bold" fill="#333">3</text><text x="125" y="215" text-anchor="middle" font-size="20" font-weight="bold" fill="#333">6</text><text x="45" y="130" text-anchor="middle" font-size="20" font-weight="bold" fill="#333">9</text><line x1="125" y1="125" x2="125" y2="40" stroke="#FF6B6B" stroke-width="6" stroke-linecap="round" transform="rotate({hour_angle}, 125, 125)"/><line x1="125" y1="125" x2="125" y2="25" stroke="#4ECDC4" stroke-width="4" stroke-linecap="round" transform="rotate({min_angle}, 125, 125)"/><circle cx="125" cy="125" r="8" fill="#333"/></svg></div><div style="text-align: center; margin-top: 20px;"><p style="color: #FFE066; font-size: 24px; font-weight: bold;">This clock shows {hours:02d}:{minutes:02d}</p><p style="color: white; font-size: 16px;">The short <span style="color:#FF6B6B">red</span> hand points to the hour. The long <span style="color:#4ECDC4">blue</span> hand points to the minutes.</p></div></div>"""
|
| 166 |
+
return html
|
| 167 |
+
|
| 168 |
+
def create_multiplication_array(rows, cols):
|
| 169 |
+
"""(NEW & Dynamic) Generates an SVG grid of dots to show multiplication."""
|
| 170 |
+
cell_size, gap = 25, 5
|
| 171 |
+
svg_width = cols * (cell_size + gap)
|
| 172 |
+
svg_height = rows * (cell_size + gap)
|
| 173 |
+
dots_html = "".join([f'<circle cx="{c * (cell_size + gap) + cell_size/2}" cy="{r * (cell_size + gap) + cell_size/2}" r="{cell_size/2 - 2}" fill="#FF6B6B"/>' for r in range(rows) for c in range(cols)])
|
| 174 |
+
html = f"""<div style="padding: 20px; background: linear-gradient(135deg, #FF9A9E 0%, #FECFEF 100%); border-radius: 15px; margin: 10px 0;"><h3 style="color:#333; text-align: center;">Multiplication Array: {rows} ร {cols} = {rows * cols}</h3><div style="display: flex; justify-content: center; padding: 10px;"><svg width="{svg_width}" height="{svg_height}">{dots_html}</svg></div><p style="color: #333; text-align: center; font-size: 18px;">See? There are <b>{rows}</b> rows of <b>{cols}</b> dots. That's <b>{rows*cols}</b> dots in total!</p></div>"""
|
| 175 |
+
return html
|
| 176 |
+
|
| 177 |
+
def create_number_line(start, end, points, title="Number Line"):
|
| 178 |
+
"""(NEW & Dynamic) Creates a simple number line SVG."""
|
| 179 |
+
width = 600
|
| 180 |
+
padding = 30
|
| 181 |
+
scale = (width - 2 * padding) / (end - start)
|
| 182 |
+
def to_x(n): return padding + (n - start) * scale
|
| 183 |
+
ticks_html = "".join([f'<g transform="translate({to_x(i)}, 50)"><line y2="10" stroke="#aaa"/><text y="30" text-anchor="middle" fill="#555">{i}</text></g>' for i in range(start, end + 1)])
|
| 184 |
+
points_html = "".join([f'<g transform="translate({to_x(p)}, 50)"><circle r="8" fill="#FF6B6B" stroke="white" stroke-width="2"/><text y="-15" text-anchor="middle" font-weight="bold" fill="#D63031">{p}</text></g>' for p in points])
|
| 185 |
+
html = f"""<div style="padding: 20px; background: #f7f1e3; border-radius: 15px; margin: 10px 0;"><h3 style="text-align: center; color: #333;">{title}</h3><svg width="{width}" height="100"><line x1="{padding}" y1="50" x2="{width-padding}" y2="50" stroke="#333" stroke-width="2"/>{ticks_html}{points_html}</svg></div>"""
|
| 186 |
+
return html
|
| 187 |
+
|
| 188 |
+
def create_place_value_blocks(number):
|
| 189 |
+
"""(Dynamic) Create place value blocks for understanding numbers."""
|
| 190 |
+
hundreds, tens, ones = number // 100, (number % 100) // 10, number % 10
|
| 191 |
+
h_html = f'<div style="text-align: center;"><h4>Hundreds: {hundreds}</h4><div style="display: flex; gap: 5px;">{"".join([f\'<div style="width: 100px; height: 100px; background: #FF6B6B; border: 2px solid #D63031; display: grid; grid-template-columns: repeat(10, 1fr); gap: 2px; padding: 2px;">{"".join(["<div style=\'background:#F5A6A6\'></div>"]*100)}</div>\' for _ in range(hundreds)])}</div></div>' if hundreds > 0 else ''
|
| 192 |
+
t_html = f'<div style="text-align: center;"><h4>Tens: {tens}</h4><div style="display: flex; gap: 5px;">{"".join([f\'<div style="width: 10px; height: 100px; background: #4ECDC4; border: 2px solid #00B894; display: grid; grid-template-rows: repeat(10, 1fr); gap: 2px; padding: 2px;">{"".join(["<div style=\'background:#A2E8E4\'></div>"]*10)}</div>\' for _ in range(tens)])}</div></div>' if tens > 0 else ''
|
| 193 |
+
o_html = f'<div style="text-align: center;"><h4>Ones: {ones}</h4><div style="display: flex; gap: 5px;">{"".join([f\'<div style="width: 10px; height: 10px; background: #FFE066; border: 2px solid #FDCB6E;"></div>\' for _ in range(ones)])}</div></div>' if ones > 0 else ''
|
| 194 |
+
html = f"""<div style="padding: 20px; background: linear-gradient(135deg, #dfe6e9 0%, #b2bec3 100%); border-radius: 15px; margin: 10px 0;"><h3 style="color: #333; text-align: center;">Place Value Blocks for {number}</h3><div style="display: flex; justify-content: center; align-items: flex-end; gap: 20px; flex-wrap: wrap; padding: 20px 0;">{h_html}{t_html}{o_html}</div><div style="text-align: center; margin-top: 15px; padding: 10px; background: rgba(0,0,0,0.1); border-radius: 10px;"><h4 style="color: #333; margin:0;">{hundreds if hundreds else 0} Hundreds + {tens if tens else 0} Tens + {ones} Ones = {number}</h4></div></div>"""
|
| 195 |
+
return html
|
| 196 |
+
|
| 197 |
+
def create_shape_explorer():
|
| 198 |
+
"""(Static) Create colorful shape recognition tool."""
|
| 199 |
+
html = """<div style="padding: 20px; background: linear-gradient(135deg, #A8EDEA 0%, #FED6E3 100%); border-radius: 15px; margin: 10px 0;"><h3 style="color: #333; text-align: center; margin-bottom: 20px;">๐ท Shape Explorer!</h3><div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; max-width: 600px; margin: 0 auto;"><div style="text-align: center; padding: 15px; background: white; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"><h4 style="color: #333; margin-bottom: 10px;">Circle</h4><svg width="80" height="80"><circle cx="40" cy="40" r="35" fill="#FF6B6B" stroke="#333" stroke-width="3"/></svg><p style="color: #666; font-size: 12px; margin-top: 10px;">Round and smooth!</p></div><div style="text-align: center; padding: 15px; background: white; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"><h4 style="color: #333; margin-bottom: 10px;">Square</h4><svg width="80" height="80"><rect x="12.5" y="12.5" width="55" height="55" fill="#4ECDC4" stroke="#333" stroke-width="3"/></svg><p style="color: #666; font-size: 12px; margin-top: 10px;">4 equal sides!</p></div><div style="text-align: center; padding: 15px; background: white; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"><h4 style="color: #333; margin-bottom: 10px;">Triangle</h4><svg width="80" height="80"><polygon points="40,15 15,65 65,65" fill="#FFD93D" stroke="#333" stroke-width="3"/></svg><p style="color: #666; font-size: 12px; margin-top: 10px;">3 sides and corners!</p></div><div style="text-align: center; padding: 15px; background: white; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);"><h4 style="color: #333; margin-bottom: 10px;">Rectangle</h4><svg width="80" height="80"><rect x="10" y="25" width="60" height="30" fill="#95E1D3" stroke="#333" stroke-width="3"/></svg><p style="color: #666; font-size: 12px; margin-top: 10px;">4 sides, opposite sides equal!</p></div></div><p style="color: #333; text-align: center; margin-top: 20px; font-size: 18px;">Can you find these shapes around you? ๐โจ</p></div>"""
|
| 200 |
+
return html
|
| 201 |
+
|
| 202 |
+
def create_money_counter():
|
| 203 |
+
"""(Static) Create coin counting visual."""
|
| 204 |
+
html = """<div style="padding: 20px; background: linear-gradient(135deg, #FFE259 0%, #FFA751 100%); border-radius: 15px; margin: 10px 0;"><h3 style="color: #333; text-align: center; margin-bottom: 20px;">๐ฐ Money Counter!</h3><div style="display: flex; justify-content: center; gap: 30px; flex-wrap: wrap;"><div style="text-align: center; padding: 15px; background: white; border-radius: 10px;"><h4 style="color: #333;">Penny</h4><div style="width: 50px; height: 50px; background: #CD7F32; border-radius: 50%; margin: 10px auto; display: flex; align-items: center; justify-content: center; border: 3px solid #8B4513;"><span style="color: white; font-weight: bold;">1ยข</span></div><p style="color: #666; font-size: 12px;">1 cent</p></div><div style="text-align: center; padding: 15px; background: white; border-radius: 10px;"><h4 style="color: #333;">Nickel</h4><div style="width: 55px; height: 55px; background: #C0C0C0; border-radius: 50%; margin: 10px auto; display: flex; align-items: center; justify-content: center; border: 3px solid #808080;"><span style="color: #333; font-weight: bold;">5ยข</span></div><p style="color: #666; font-size: 12px;">5 cents</p></div><div style="text-align: center; padding: 15px; background: white; border-radius: 10px;"><h4 style="color: #333;">Dime</h4><div style="width: 45px; height: 45px; background: #C0C0C0; border-radius: 50%; margin: 10px auto; display: flex; align-items: center; justify-content: center; border: 3px solid #808080;"><span style="color: #333; font-weight: bold;">10ยข</span></div><p style="color: #666; font-size: 12px;">10 cents</p></div><div style="text-align: center; padding: 15px; background: white; border-radius: 10px;"><h4 style="color: #333;">Quarter</h4><div style="width: 60px; height: 60px; background: #C0C0C0; border-radius: 50%; margin: 10px auto; display: flex; align-items: center; justify-content: center; border: 3px solid #808080;"><span style="color: #333; font-weight: bold;">25ยข</span></div><p style="color: #666; font-size: 12px;">25 cents</p></div></div><p style="color: #333; text-align: center; margin-top: 20px; font-size: 18px;">Practice counting coins to make different amounts! ๐ชโจ</p></div>"""
|
| 205 |
+
return html
|
| 206 |
+
|
| 207 |
+
# --- [The rest of your application code remains the same] ---
|
| 208 |
+
# --- API KEY & MODEL CONFIGURATION, SESSION STATE, DIALOGS, etc. ---
|
| 209 |
+
# ... (Paste the rest of your original app.py code from the "API KEY" section onwards here) ...
|
| 210 |
+
|
| 211 |
+
# NOTE: For brevity, I am not repeating the entire second half of your app.
|
| 212 |
+
# The code below is identical to your original file.
|
| 213 |
+
|
| 214 |
# --- API KEY & MODEL CONFIGURATION ---
|
| 215 |
load_dotenv()
|
| 216 |
api_key = None
|
|
|
|
| 227 |
model = genai.GenerativeModel(
|
| 228 |
model_name="gemini-2.5-flash-lite",
|
| 229 |
system_instruction="""
|
| 230 |
+
You are "Math Jegna", an AI specializing exclusively in K-12 mathematics.
|
| 231 |
+
Your one and only function is to solve and explain math problems for children.
|
| 232 |
+
You are an AI math tutor that uses the Professor B methodology developed by Everard Barrett. This methodology is designed to activate children's natural learning capacities and present mathematics as a contextual, developmental story that makes sense.
|
| 233 |
+
|
| 234 |
+
IMPORTANT: When explaining mathematical concepts to young learners, mention that colorful visual aids will be provided to help illustrate the concept. Use phrases like:
|
| 235 |
+
- "Let me show you this with some colorful blocks..."
|
| 236 |
+
- "A fun visual will help you see how this works..."
|
| 237 |
+
- "I'll create a picture to help you understand this fraction..."
|
| 238 |
+
|
| 239 |
+
Focus on concepts appropriate for K-12 students:
|
| 240 |
+
- Basic counting and number recognition
|
| 241 |
+
- Simple addition and subtraction (using manipulatives)
|
| 242 |
+
- Fractions as parts of wholes (pizza slices, etc.)
|
| 243 |
+
- Multiplication as arrays or groups
|
| 244 |
+
- Basic shapes and geometry
|
| 245 |
+
- Place value with hundreds, tens, ones
|
| 246 |
+
- Money counting and coin recognition
|
| 247 |
+
- Time telling with analog clocks
|
| 248 |
+
- Simple patterns and sequences
|
| 249 |
+
- Basic measurement concepts
|
| 250 |
+
|
| 251 |
+
Always use age-appropriate language and relate math to real-world examples children understand.
|
| 252 |
+
|
| 253 |
+
Core Philosophy and Principles
|
| 254 |
+
1. Contextual Learning Approach
|
| 255 |
+
Present math as a story: Every mathematical concept should be taught as part of a continuing narrative that builds connections between ideas
|
| 256 |
+
Use concrete manipulatives: Always relate abstract concepts to physical, visual representations
|
| 257 |
+
Truth-telling: Present arithmetic computations simply and truthfully without confusing steps
|
| 258 |
+
|
| 259 |
+
2. Natural Learning Activation
|
| 260 |
+
Leverage natural capacities: Recognize that each child has mental capabilities designed to learn naturally
|
| 261 |
+
Story-based retention: Use stories and visual representations that children can easily remember
|
| 262 |
+
Reduced anxiety: Make math fun and engaging, not scary or confusing
|
| 263 |
+
|
| 264 |
+
3. Hands-on Learning
|
| 265 |
+
Mental gymnastics: Use finger counting, visual blocks, and interactive elements
|
| 266 |
+
No rote memorization: Focus on understanding through play and exploration
|
| 267 |
+
Build confidence: Celebrate small victories and progress
|
| 268 |
+
|
| 269 |
+
You are strictly forbidden from answering any question that is not mathematical in nature.
|
| 270 |
+
If you receive a non-mathematical question, you MUST decline with: "I can only answer math questions for students. Please ask me about numbers, shapes, counting, or other math topics!"
|
| 271 |
|
| 272 |
+
Keep explanations simple, encouraging, and fun for young learners.
|
| 273 |
"""
|
| 274 |
)
|
| 275 |
else:
|
|
|
|
| 296 |
else:
|
| 297 |
st.session_state.chats = {
|
| 298 |
"New Chat": [
|
| 299 |
+
{"role": "assistant", "content": "Hello! I'm Math Jegna, your friendly math helper! ๐ง โจ I love helping students learn math with colorful pictures and fun activities. What would you like to learn about today? Maybe counting, shapes, or solving a math problem? ๐"}
|
| 300 |
]
|
| 301 |
}
|
| 302 |
st.session_state.active_chat_key = "New Chat"
|
|
|
|
| 322 |
st.warning(f"Are you sure you want to delete '{chat_key}'? This cannot be undone.")
|
| 323 |
if st.button("Yes, Delete", type="primary", key=f"confirm_delete_{chat_key}"):
|
| 324 |
st.session_state.chats.pop(chat_key)
|
| 325 |
+
# Add the logic to switch to a new or different chat after deletion
|
| 326 |
if st.session_state.active_chat_key == chat_key:
|
| 327 |
+
# Simple fallback to the first available chat or a new one
|
| 328 |
+
if st.session_state.chats:
|
| 329 |
+
st.session_state.active_chat_key = next(iter(st.session_state.chats))
|
| 330 |
+
else:
|
| 331 |
+
# Create a new chat if none are left
|
| 332 |
+
st.session_state.chats["New Chat"] = [
|
| 333 |
+
{"role": "assistant", "content": "Hello! Let's start a new math adventure! ๐"}
|
| 334 |
+
]
|
| 335 |
+
st.session_state.active_chat_key = "New Chat"
|
| 336 |
st.rerun()
|
| 337 |
|
| 338 |
+
# --- MAIN APP LAYOUT ---
|
| 339 |
+
with st.sidebar:
|
| 340 |
+
st.title("๐งฎ Math Jegna")
|
| 341 |
+
st.write("Your K-8 AI Math Tutor")
|
| 342 |
+
st.divider()
|
| 343 |
+
|
| 344 |
+
# Chat history list
|
| 345 |
+
for chat_key in list(st.session_state.chats.keys()):
|
| 346 |
+
col1, col2, col3 = st.columns([0.6, 0.2, 0.2])
|
| 347 |
+
with col1:
|
| 348 |
+
if st.button(chat_key, key=f"switch_{chat_key}", use_container_width=True, type="primary" if st.session_state.active_chat_key == chat_key else "secondary"):
|
| 349 |
+
st.session_state.active_chat_key = chat_key
|
| 350 |
+
st.rerun()
|
| 351 |
+
with col2:
|
| 352 |
+
if st.button("โ๏ธ", key=f"rename_{chat_key}", help="Rename Chat"):
|
| 353 |
+
rename_chat(chat_key)
|
| 354 |
+
with col3:
|
| 355 |
+
if st.button("๐๏ธ", key=f"delete_{chat_key}", help="Delete Chat"):
|
| 356 |
+
delete_chat(chat_key)
|
| 357 |
|
| 358 |
+
if st.button("โ New Chat", use_container_width=True):
|
| 359 |
+
new_chat_name = f"Chat {len(st.session_state.chats) + 1}"
|
| 360 |
+
# Ensure the name is unique
|
| 361 |
+
while new_chat_name in st.session_state.chats:
|
| 362 |
+
new_chat_name += "*"
|
| 363 |
+
st.session_state.chats[new_chat_name] = [
|
| 364 |
+
{"role": "assistant", "content": "Ready for a new math problem! What's on your mind? ๐"}
|
| 365 |
+
]
|
| 366 |
+
st.session_state.active_chat_key = new_chat_name
|
| 367 |
+
st.rerun()
|
| 368 |
+
|
| 369 |
+
st.divider()
|
| 370 |
+
|
| 371 |
+
# Save chats to local storage
|
| 372 |
+
if st.button("๐พ Save Chats", use_container_width=True):
|
| 373 |
+
data_to_save = {
|
| 374 |
+
"chats": st.session_state.chats,
|
| 375 |
+
"active_chat_key": st.session_state.active_chat_key
|
| 376 |
+
}
|
| 377 |
+
localS.setItem("math_mentor_chats", json.dumps(data_to_save))
|
| 378 |
+
st.toast("Chats saved to your browser!", icon="โ
")
|
| 379 |
|
| 380 |
+
# Download chat button
|
| 381 |
+
active_chat_history = st.session_state.chats[st.session_state.active_chat_key]
|
| 382 |
+
download_str = format_chat_for_download(active_chat_history)
|
| 383 |
+
st.download_button(
|
| 384 |
+
label="๐ฅ Download Chat",
|
| 385 |
+
data=download_str,
|
| 386 |
+
file_name=f"{st.session_state.active_chat_key.replace(' ', '_')}_history.md",
|
| 387 |
+
mime="text/markdown",
|
| 388 |
+
use_container_width=True
|
| 389 |
+
)
|
| 390 |
|
| 391 |
+
# Share chat button
|
| 392 |
+
if st.button("๐ Share Chat", use_container_width=True):
|
| 393 |
+
chat_json = json.dumps(st.session_state.chats[st.session_state.active_chat_key])
|
| 394 |
+
chat_b64 = base64.urlsafe_b64encode(chat_json.encode()).decode()
|
| 395 |
+
share_url = f"{st.get_option('server.baseUrlPath')}?shared_chat={chat_b64}"
|
| 396 |
+
st.code(share_url)
|
| 397 |
+
st.info("Copy the URL above to share this specific chat!")
|
| 398 |
+
|
| 399 |
+
st.header(f"Chatting with Math Jegna: _{st.session_state.active_chat_key}_")
|
| 400 |
+
|
| 401 |
+
# Display chat messages
|
| 402 |
+
for message in st.session_state.chats[st.session_state.active_chat_key]:
|
| 403 |
+
with st.chat_message(message["role"]):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
st.markdown(message["content"])
|
| 405 |
|
| 406 |
+
# User input
|
| 407 |
+
if prompt := st.chat_input("Ask a K-8 math question..."):
|
| 408 |
+
# Add user message to chat history
|
| 409 |
+
st.session_state.chats[st.session_state.active_chat_key].append({"role": "user", "content": prompt})
|
| 410 |
+
with st.chat_message("user"):
|
| 411 |
+
st.markdown(prompt)
|
| 412 |
|
| 413 |
+
# Prepare chat for Gemini API
|
| 414 |
+
gemini_chat_history = [
|
| 415 |
+
{"role": convert_role_for_gemini(m["role"]), "parts": [m["content"]]}
|
| 416 |
+
for m in st.session_state.chats[st.session_state.active_chat_key]
|
| 417 |
+
]
|
| 418 |
+
|
| 419 |
+
# Generate response
|
| 420 |
+
with st.chat_message("assistant"):
|
| 421 |
+
with st.spinner("Math Jegna is thinking..."):
|
| 422 |
try:
|
| 423 |
+
chat_session = model.start_chat(history=gemini_chat_history)
|
| 424 |
+
response = chat_session.send_message(prompt, stream=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
|
| 426 |
+
full_response = ""
|
| 427 |
+
response_container = st.empty()
|
| 428 |
+
for chunk in response:
|
| 429 |
+
full_response += chunk.text
|
| 430 |
+
response_container.markdown(full_response + " โ")
|
| 431 |
+
response_container.markdown(full_response)
|
| 432 |
|
| 433 |
+
# After generating text, decide if a visual is needed
|
| 434 |
+
if should_generate_visual(prompt, full_response):
|
| 435 |
+
visual_html = create_visual_manipulative(prompt, full_response)
|
| 436 |
+
if visual_html:
|
| 437 |
+
# Display the generated HTML/SVG visual
|
| 438 |
+
components.html(visual_html, height=400, scrolling=True)
|
| 439 |
+
|
| 440 |
+
# Add AI response to session state
|
| 441 |
+
st.session_state.chats[st.session_state.active_chat_key].append({"role": "assistant", "content": full_response})
|
| 442 |
|
| 443 |
+
except genai.types.generation_types.BlockedPromptException as e:
|
| 444 |
+
st.error("I can only answer math questions for students. Please ask me about numbers, shapes, or other math topics!")
|
| 445 |
+
st.session_state.chats[st.session_state.active_chat_key].append({"role": "assistant", "content": "I can only answer math questions for students. Please ask me about numbers, shapes, or other math topics!"})
|
| 446 |
except Exception as e:
|
| 447 |
+
st.error(f"An error occurred: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|