import streamlit as st
import google.generativeai as genai
import os
import json
import base64
from dotenv import load_dotenv
from streamlit_local_storage import LocalStorage
import re
import streamlit.components.v1 as components
import math # Needed for trigonometry in dynamic visuals
# --- PAGE CONFIGURATION ---
st.set_page_config(
page_title="Math Jegna - Your AI Math Tutor",
page_icon="๐ง ",
layout="wide"
)
# Create an instance of the LocalStorage class
localS = LocalStorage()
# --- HELPER FUNCTIONS ---
def format_chat_for_download(chat_history):
"""Formats the chat history into a human-readable string for download."""
formatted_text = f"# Math Mentor Chat\n\n"
for message in chat_history:
role = "You" if message["role"] == "user" else "Math Mentor"
formatted_text += f"**{role}:**\n{message['content']}\n\n---\n\n"
return formatted_text
def convert_role_for_gemini(role):
"""Convert Streamlit chat roles to Gemini API roles"""
if role == "assistant":
return "model"
return role # "user" stays the same
def should_generate_visual(user_prompt, ai_response):
"""Determine if a visual aid would be helpful based on the content"""
# Expanded keywords to trigger new dynamic visuals
k12_visual_keywords = [
'add', 'subtract', 'multiply', 'times', 'divide', 'counting', 'numbers',
'fraction', 'half', 'quarter', 'third', 'parts', 'whole',
'shape', 'triangle', 'circle', 'square', 'rectangle',
'money', 'coins', 'dollars', 'cents', 'change',
'time', 'clock', 'hours', 'minutes', 'o\'clock',
'measurement', 'length', 'height', 'weight',
'place value', 'tens', 'ones', 'hundreds',
'pattern', 'sequence', 'skip counting',
'greater than', 'less than', 'equal', 'compare',
'number line', 'array', 'grid'
]
combined_text = (user_prompt + " " + ai_response).lower()
return any(keyword in combined_text for keyword in k12_visual_keywords)
def create_visual_manipulative(user_prompt, ai_response):
"""-- SMART VISUAL ROUTER --
Parses the user prompt and calls the appropriate dynamic visual function."""
try:
user_lower = user_prompt.lower()
# Priority 1: Time / Clock (e.g., "7:30", "4 o'clock")
time_match = re.search(r'(\d{1,2}):(\d{2})', user_lower) or re.search(r'(\d{1,2})\s*o\'clock', user_lower)
if time_match:
groups = time_match.groups()
hour = int(groups[0])
minute = int(groups[1]) if len(groups) > 1 and groups[1] else 0
if 1 <= hour <= 12 and 0 <= minute <= 59:
return create_clock_visual(hour, minute)
# Priority 2: Fractions (e.g., "2/5", "fraction 3/8")
fraction_match = re.search(r'(\d+)/(\d+)', user_lower)
if fraction_match:
num, den = int(fraction_match.group(1)), int(fraction_match.group(2))
if 0 < num <= den and den <= 16: # Keep it visually clean
return create_dynamic_fraction_circle(num, den)
# Priority 3: Multiplication Arrays (e.g., "3 times 5", "4 x 6")
mult_match = re.search(r'(\d+)\s*(?:x|times)\s*(\d+)', user_lower)
if mult_match:
rows, cols = int(mult_match.group(1)), int(mult_match.group(2))
if rows <= 10 and cols <= 10: # Keep arrays reasonable
return create_multiplication_array(rows, cols)
# Priority 4: Addition/Subtraction Blocks
if any(word in user_lower for word in ['add', 'plus', '+', 'subtract', 'minus', 'take away', '-']):
numbers = re.findall(r'\d+', user_prompt)
if len(numbers) >= 2:
num1, num2 = int(numbers[0]), int(numbers[1])
operation = 'add' if any(w in user_lower for w in ['add', 'plus', '+']) else 'subtract'
if num1 <= 20 and num2 <= 20:
return create_counting_blocks(num1, num2, operation)
# Priority 5: Number Lines
if 'number line' in user_lower:
numbers = [int(n) for n in re.findall(r'\d+', user_prompt)]
if numbers:
start = min(numbers) - 2
end = max(numbers) + 2
return create_number_line(start, end, numbers, "Your Numbers on the Line")
# Priority 6: Place Value
if 'place value' in user_lower:
numbers = re.findall(r'\d+', user_prompt)
if numbers:
num = int(numbers[0])
if num <= 999:
return create_place_value_blocks(num)
# Fallback to static, general visuals
if any(word in user_lower for word in ['fraction', 'part']): return create_dynamic_fraction_circle(1, 2) # Show a default example
if any(word in user_lower for word in ['shape']): return create_shape_explorer()
if any(word in user_lower for word in ['money', 'coin']): return create_money_counter()
if any(word in user_lower for word in ['time', 'clock']): return create_clock_visual(10, 10) # Show a default example
return None # No relevant visual found
except Exception as e:
st.error(f"Could not create visual: {e}")
return None
# --- VISUAL TOOLBOX FUNCTIONS ---
def create_counting_blocks(num1, num2, operation):
"""(Dynamic) Create colorful counting blocks for addition/subtraction."""
html = f"""
{''.join([f'' for _ in range(num1 + num2 if operation == 'add' else max(0, num1 - num2))])}
"""
return html
def create_dynamic_fraction_circle(numerator, denominator):
"""(Dynamic) Generates an SVG of a pizza/pie to represent a fraction."""
if not (0 < numerator <= denominator): return "
The pizza is cut into {denominator} equal slices, and we are showing {numerator} of them! ๐
"""
return html
def create_clock_visual(hours, minutes):
"""(Dynamic) Create a clock showing a specific time."""
min_angle = minutes * 6
hour_angle = (hours % 12 + minutes / 60) * 30
html = f"""
๐ Learning Time!
This clock shows {hours:02d}:{minutes:02d}
The short red hand points to the hour. The long blue hand points to the minutes.
"""
return html
def create_multiplication_array(rows, cols):
"""(NEW & Dynamic) Generates an SVG grid of dots to show multiplication."""
cell_size, gap = 25, 5
svg_width = cols * (cell_size + gap)
svg_height = rows * (cell_size + gap)
dots_html = "".join([f'' for r in range(rows) for c in range(cols)])
html = f"""
See? There are {rows} rows of {cols} dots. That's {rows*cols} dots in total!
"""
return html
def create_number_line(start, end, points, title="Number Line"):
"""(NEW & Dynamic) Creates a simple number line SVG."""
width = 600
padding = 30
scale = (width - 2 * padding) / (end - start)
def to_x(n): return padding + (n - start) * scale
ticks_html = "".join([f'{i}' for i in range(start, end + 1)])
points_html = "".join([f'{p}' for p in points])
html = f"""
{title}
"""
return html
def create_place_value_blocks(number):
"""(Dynamic) Create place value blocks for understanding numbers."""
hundreds, tens, ones = number // 100, (number % 100) // 10, number % 10
h_html = f'
Hundreds: {hundreds}
{"".join([f\'
{"".join([""]*100)}
\' for _ in range(hundreds)])}
' if hundreds > 0 else ''
t_html = f'
Tens: {tens}
{"".join([f\'
{"".join([""]*10)}
\' for _ in range(tens)])}
' if tens > 0 else ''
o_html = f'
Ones: {ones}
{"".join([f\'\' for _ in range(ones)])}
' if ones > 0 else ''
html = f"""
Place Value Blocks for {number}
{h_html}{t_html}{o_html}
{hundreds if hundreds else 0} Hundreds + {tens if tens else 0} Tens + {ones} Ones = {number}
"""
return html
def create_shape_explorer():
"""(Static) Create colorful shape recognition tool."""
html = """
๐ท Shape Explorer!
Circle
Round and smooth!
Square
4 equal sides!
Triangle
3 sides and corners!
Rectangle
4 sides, opposite sides equal!
Can you find these shapes around you? ๐โจ
"""
return html
def create_money_counter():
"""(Static) Create coin counting visual."""
html = """
๐ฐ Money Counter!
Penny
1ยข
1 cent
Nickel
5ยข
5 cents
Dime
10ยข
10 cents
Quarter
25ยข
25 cents
Practice counting coins to make different amounts! ๐ชโจ
"""
return html
# --- [The rest of your application code remains the same] ---
# --- API KEY & MODEL CONFIGURATION, SESSION STATE, DIALOGS, etc. ---
# ... (Paste the rest of your original app.py code from the "API KEY" section onwards here) ...
# NOTE: For brevity, I am not repeating the entire second half of your app.
# The code below is identical to your original file.
# --- API KEY & MODEL CONFIGURATION ---
load_dotenv()
api_key = None
try:
api_key = st.secrets["GOOGLE_API_KEY"]
except (KeyError, FileNotFoundError):
api_key = os.getenv("GOOGLE_API_KEY")
if api_key:
genai.configure(api_key=api_key)
# Main text model
model = genai.GenerativeModel(
model_name="gemini-2.5-flash-lite",
system_instruction="""
You are "Math Jegna", an AI specializing exclusively in K-12 mathematics.
Your one and only function is to solve and explain math problems for children.
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.
IMPORTANT: When explaining mathematical concepts to young learners, mention that colorful visual aids will be provided to help illustrate the concept. Use phrases like:
- "Let me show you this with some colorful blocks..."
- "A fun visual will help you see how this works..."
- "I'll create a picture to help you understand this fraction..."
Focus on concepts appropriate for K-12 students:
- Basic counting and number recognition
- Simple addition and subtraction (using manipulatives)
- Fractions as parts of wholes (pizza slices, etc.)
- Multiplication as arrays or groups
- Basic shapes and geometry
- Place value with hundreds, tens, ones
- Money counting and coin recognition
- Time telling with analog clocks
- Simple patterns and sequences
- Basic measurement concepts
Always use age-appropriate language and relate math to real-world examples children understand.
Core Philosophy and Principles
1. Contextual Learning Approach
Present math as a story: Every mathematical concept should be taught as part of a continuing narrative that builds connections between ideas
Use concrete manipulatives: Always relate abstract concepts to physical, visual representations
Truth-telling: Present arithmetic computations simply and truthfully without confusing steps
2. Natural Learning Activation
Leverage natural capacities: Recognize that each child has mental capabilities designed to learn naturally
Story-based retention: Use stories and visual representations that children can easily remember
Reduced anxiety: Make math fun and engaging, not scary or confusing
3. Hands-on Learning
Mental gymnastics: Use finger counting, visual blocks, and interactive elements
No rote memorization: Focus on understanding through play and exploration
Build confidence: Celebrate small victories and progress
You are strictly forbidden from answering any question that is not mathematical in nature.
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!"
Keep explanations simple, encouraging, and fun for young learners.
"""
)
else:
st.error("๐จ Google API Key not found! Please add it to your secrets or a local .env file.")
st.stop()
# --- SESSION STATE & LOCAL STORAGE INITIALIZATION ---
if "chats" not in st.session_state:
try:
shared_chat_b64 = st.query_params.get("shared_chat")
if shared_chat_b64:
decoded_chat_json = base64.urlsafe_b64decode(shared_chat_b64).decode()
st.session_state.chats = {"Shared Chat": json.loads(decoded_chat_json)}
st.session_state.active_chat_key = "Shared Chat"
st.query_params.clear()
else:
raise ValueError("No shared chat")
except (TypeError, ValueError, Exception):
saved_data_json = localS.getItem("math_mentor_chats")
if saved_data_json:
saved_data = json.loads(saved_data_json)
st.session_state.chats = saved_data.get("chats", {})
st.session_state.active_chat_key = saved_data.get("active_chat_key", "New Chat")
else:
st.session_state.chats = {
"New Chat": [
{"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? ๐"}
]
}
st.session_state.active_chat_key = "New Chat"
# --- RENAME DIALOG ---
@st.dialog("Rename Chat")
def rename_chat(chat_key):
st.write(f"Enter a new name for '{chat_key}':")
new_name = st.text_input("New Name", key=f"rename_input_{chat_key}")
if st.button("Save", key=f"save_rename_{chat_key}"):
if new_name and new_name not in st.session_state.chats:
st.session_state.chats[new_name] = st.session_state.chats.pop(chat_key)
st.session_state.active_chat_key = new_name
st.rerun()
elif not new_name:
st.error("Name cannot be empty.")
else:
st.error("A chat with this name already exists.")
# --- DELETE CONFIRMATION DIALOG ---
@st.dialog("Delete Chat")
def delete_chat(chat_key):
st.warning(f"Are you sure you want to delete '{chat_key}'? This cannot be undone.")
if st.button("Yes, Delete", type="primary", key=f"confirm_delete_{chat_key}"):
st.session_state.chats.pop(chat_key)
# Add the logic to switch to a new or different chat after deletion
if st.session_state.active_chat_key == chat_key:
# Simple fallback to the first available chat or a new one
if st.session_state.chats:
st.session_state.active_chat_key = next(iter(st.session_state.chats))
else:
# Create a new chat if none are left
st.session_state.chats["New Chat"] = [
{"role": "assistant", "content": "Hello! Let's start a new math adventure! ๐"}
]
st.session_state.active_chat_key = "New Chat"
st.rerun()
# --- MAIN APP LAYOUT ---
with st.sidebar:
st.title("๐งฎ Math Jegna")
st.write("Your K-8 AI Math Tutor")
st.divider()
# Chat history list
for chat_key in list(st.session_state.chats.keys()):
col1, col2, col3 = st.columns([0.6, 0.2, 0.2])
with col1:
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"):
st.session_state.active_chat_key = chat_key
st.rerun()
with col2:
if st.button("โ๏ธ", key=f"rename_{chat_key}", help="Rename Chat"):
rename_chat(chat_key)
with col3:
if st.button("๐๏ธ", key=f"delete_{chat_key}", help="Delete Chat"):
delete_chat(chat_key)
if st.button("โ New Chat", use_container_width=True):
new_chat_name = f"Chat {len(st.session_state.chats) + 1}"
# Ensure the name is unique
while new_chat_name in st.session_state.chats:
new_chat_name += "*"
st.session_state.chats[new_chat_name] = [
{"role": "assistant", "content": "Ready for a new math problem! What's on your mind? ๐"}
]
st.session_state.active_chat_key = new_chat_name
st.rerun()
st.divider()
# Save chats to local storage
if st.button("๐พ Save Chats", use_container_width=True):
data_to_save = {
"chats": st.session_state.chats,
"active_chat_key": st.session_state.active_chat_key
}
localS.setItem("math_mentor_chats", json.dumps(data_to_save))
st.toast("Chats saved to your browser!", icon="โ ")
# Download chat button
active_chat_history = st.session_state.chats[st.session_state.active_chat_key]
download_str = format_chat_for_download(active_chat_history)
st.download_button(
label="๐ฅ Download Chat",
data=download_str,
file_name=f"{st.session_state.active_chat_key.replace(' ', '_')}_history.md",
mime="text/markdown",
use_container_width=True
)
# Share chat button
if st.button("๐ Share Chat", use_container_width=True):
chat_json = json.dumps(st.session_state.chats[st.session_state.active_chat_key])
chat_b64 = base64.urlsafe_b64encode(chat_json.encode()).decode()
share_url = f"{st.get_option('server.baseUrlPath')}?shared_chat={chat_b64}"
st.code(share_url)
st.info("Copy the URL above to share this specific chat!")
st.header(f"Chatting with Math Jegna: _{st.session_state.active_chat_key}_")
# Display chat messages
for message in st.session_state.chats[st.session_state.active_chat_key]:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# User input
if prompt := st.chat_input("Ask a K-8 math question..."):
# Add user message to chat history
st.session_state.chats[st.session_state.active_chat_key].append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
# Prepare chat for Gemini API
gemini_chat_history = [
{"role": convert_role_for_gemini(m["role"]), "parts": [m["content"]]}
for m in st.session_state.chats[st.session_state.active_chat_key]
]
# Generate response
with st.chat_message("assistant"):
with st.spinner("Math Jegna is thinking..."):
try:
chat_session = model.start_chat(history=gemini_chat_history)
response = chat_session.send_message(prompt, stream=True)
full_response = ""
response_container = st.empty()
for chunk in response:
full_response += chunk.text
response_container.markdown(full_response + " โ")
response_container.markdown(full_response)
# After generating text, decide if a visual is needed
if should_generate_visual(prompt, full_response):
visual_html = create_visual_manipulative(prompt, full_response)
if visual_html:
# Display the generated HTML/SVG visual
components.html(visual_html, height=400, scrolling=True)
# Add AI response to session state
st.session_state.chats[st.session_state.active_chat_key].append({"role": "assistant", "content": full_response})
except genai.types.generation_types.BlockedPromptException as e:
st.error("I can only answer math questions for students. Please ask me about numbers, shapes, or other math topics!")
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!"})
except Exception as e:
st.error(f"An error occurred: {e}")