mindai / app.py
Sameer1234-prog
Enhanced UI with better text formatting, fixed FFmpeg detection, improved lesson display with black text and bold formatting
8fb661f
import streamlit as st
import os
import json
# import whisper # Commented out - will load on demand
from gtts import gTTS
from io import BytesIO
import base64
from audio_recorder_streamlit import audio_recorder
from rag_engine import RAGEngine
from database import init_db, save_score, get_weak_topics, get_performance_history, save_achievement, get_achievements
import pytesseract
from PIL import Image
# ────────────────────────────────────────────────
# Page config & styling
# ────────────────────────────────────────────────
st.set_page_config(
page_title="MindGap AI • Voice + Text Learning",
page_icon="🧠",
layout="wide",
initial_sidebar_state="expanded"
)
st.markdown("""
<style>
/* Import Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700;800&display=swap');
/* Global Styles */
* {
font-family: 'Poppins', sans-serif;
}
/* Main Background with Animated Gradient - Darker for better contrast */
.stApp {
background: linear-gradient(-45deg, #1a1a2e, #16213e, #0f3460, #533483);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Gradient Text - Brighter for better visibility */
.gradient-text {
background: linear-gradient(135deg, #a8c0ff 0%, #c084fc 50%, #fbbf24 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 800;
font-size: 3rem;
text-align: center;
animation: shimmer 3s ease-in-out infinite;
}
@keyframes shimmer {
0%, 100% { opacity: 1; }
50% { opacity: 0.9; }
}
/* Glass Morphism Cards - Better contrast */
.glass-card {
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 2px solid rgba(255, 255, 255, 0.25);
border-radius: 24px;
padding: 2rem;
margin-bottom: 1.5rem;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
transition: all 0.3s ease;
}
.glass-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 40px 0 rgba(168, 192, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
background: rgba(255, 255, 255, 0.18);
}
/* Sidebar Styling - Darker with better contrast */
[data-testid="stSidebar"] {
background: linear-gradient(180deg, rgba(26, 26, 46, 0.95) 0%, rgba(22, 33, 62, 0.95) 100%);
backdrop-filter: blur(10px);
border-right: 2px solid rgba(168, 192, 255, 0.2);
}
[data-testid="stSidebar"] .element-container {
color: #ffffff !important;
}
[data-testid="stSidebar"] label {
color: #ffffff !important;
font-weight: 600;
}
/* Button Styles with Glow Effect - Brighter and more visible */
.stButton > button {
border-radius: 16px;
background: linear-gradient(135deg, #667eea 0%, #a855f7 100%);
color: #ffffff;
font-weight: 700;
border: 2px solid rgba(255, 255, 255, 0.2);
padding: 0.75rem 2rem;
font-size: 1.05rem;
transition: all 0.3s ease;
box-shadow: 0 4px 20px 0 rgba(168, 85, 247, 0.5);
position: relative;
overflow: hidden;
}
.stButton > button:before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
transition: left 0.5s;
}
.stButton > button:hover:before {
left: 100%;
}
.stButton > button:hover {
transform: translateY(-3px) scale(1.05);
box-shadow: 0 8px 30px 0 rgba(168, 85, 247, 0.8);
background: linear-gradient(135deg, #7c3aed 0%, #c084fc 100%);
}
/* Input Fields - Better visibility */
.stTextInput > div > div > input,
.stTextArea > div > div > textarea {
border-radius: 12px;
border: 2px solid rgba(168, 192, 255, 0.4);
background: rgba(255, 255, 255, 0.95);
color: #1a1a2e;
padding: 0.75rem;
font-weight: 500;
transition: all 0.3s ease;
}
.stTextInput > div > div > input:focus,
.stTextArea > div > div > textarea:focus {
border-color: #a855f7;
box-shadow: 0 0 25px rgba(168, 85, 247, 0.6);
transform: scale(1.02);
background: #ffffff;
}
.stTextInput label,
.stTextArea label {
color: #ffffff !important;
font-weight: 600;
font-size: 1.05rem;
}
/* Chat Messages - Better contrast */
.stChatMessage {
background: rgba(255, 255, 255, 0.98);
border-radius: 20px;
padding: 1.5rem;
margin: 1rem 0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(168, 192, 255, 0.3);
animation: slideIn 0.5s ease;
color: #1a1a2e;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Progress Bars */
.stProgress > div > div > div {
background: linear-gradient(90deg, #667eea, #764ba2, #f093fb);
border-radius: 10px;
}
/* Tabs */
.stTabs [data-baseweb="tab-list"] {
gap: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 0.5rem;
}
.stTabs [data-baseweb="tab"] {
border-radius: 12px;
color: white;
font-weight: 600;
padding: 0.75rem 1.5rem;
transition: all 0.3s ease;
}
.stTabs [aria-selected="true"] {
background: linear-gradient(135deg, #667eea, #764ba2);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
/* File Uploader */
[data-testid="stFileUploader"] {
background: rgba(255, 255, 255, 0.1);
border: 2px dashed rgba(255, 255, 255, 0.4);
border-radius: 16px;
padding: 2rem;
transition: all 0.3s ease;
}
[data-testid="stFileUploader"]:hover {
border-color: #667eea;
background: rgba(255, 255, 255, 0.2);
transform: scale(1.02);
}
/* Success/Error/Warning Messages */
.stSuccess, .stError, .stWarning, .stInfo {
border-radius: 12px;
padding: 1rem;
animation: pulse 0.5s ease;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
/* Metrics */
[data-testid="stMetricValue"] {
font-size: 2rem;
font-weight: 800;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Expander */
.streamlit-expanderHeader {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
font-weight: 600;
transition: all 0.3s ease;
}
.streamlit-expanderHeader:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateX(5px);
}
/* Spinner */
.stSpinner > div {
border-top-color: #667eea !important;
border-right-color: #764ba2 !important;
}
/* Headers - Brighter and more readable */
h1, h2, h3 {
color: #ffffff;
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.5);
font-weight: 700;
}
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.5rem;
}
p {
color: rgba(255, 255, 255, 0.95);
line-height: 1.6;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #764ba2, #f093fb);
}
/* Audio Player */
audio {
border-radius: 12px;
filter: drop-shadow(0 4px 10px rgba(102, 126, 234, 0.3));
}
/* Floating Animation for Icons */
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.floating {
animation: float 3s ease-in-out infinite;
}
/* Glow Effect */
.glow {
box-shadow: 0 0 20px rgba(102, 126, 234, 0.6),
0 0 40px rgba(118, 75, 162, 0.4),
0 0 60px rgba(240, 147, 251, 0.2);
}
/* Particle Background Effect - More visible */
.stApp::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
radial-gradient(circle at 20% 50%, rgba(168, 192, 255, 0.15) 2px, transparent 2px),
radial-gradient(circle at 80% 80%, rgba(192, 132, 252, 0.15) 2px, transparent 2px),
radial-gradient(circle at 40% 20%, rgba(251, 191, 36, 0.15) 2px, transparent 2px);
background-size: 50px 50px, 80px 80px, 100px 100px;
animation: particleFloat 20s linear infinite;
pointer-events: none;
z-index: 0;
}
@keyframes particleFloat {
0% { transform: translateY(0); }
100% { transform: translateY(-100px); }
}
/* Loading Animation */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Neon Glow Text - Brighter and more visible */
.neon-text {
color: #ffffff;
text-shadow:
0 0 10px rgba(168, 192, 255, 1),
0 0 20px rgba(168, 192, 255, 0.8),
0 0 30px rgba(192, 132, 252, 0.6),
0 0 40px rgba(192, 132, 252, 0.4);
animation: neonPulse 2s ease-in-out infinite;
font-weight: 600;
}
@keyframes neonPulse {
0%, 100% {
text-shadow: 0 0 10px rgba(168, 192, 255, 1),
0 0 20px rgba(168, 192, 255, 0.8),
0 0 30px rgba(192, 132, 252, 0.6);
}
50% {
text-shadow: 0 0 20px rgba(168, 192, 255, 1),
0 0 30px rgba(192, 132, 252, 1),
0 0 40px rgba(192, 132, 252, 0.8);
}
}
/* Radio Button Styling */
.stRadio > div {
background: rgba(255, 255, 255, 0.12);
border-radius: 12px;
padding: 1rem;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.stRadio label {
color: #ffffff !important;
font-weight: 600;
}
.stRadio > div > label > div {
color: #ffffff !important;
}
/* Select Box Styling */
.stSelectbox > div > div {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
border: 2px solid rgba(168, 192, 255, 0.4);
transition: all 0.3s ease;
color: #1a1a2e;
}
.stSelectbox > div > div:hover {
border-color: #a855f7;
box-shadow: 0 0 20px rgba(168, 85, 247, 0.5);
background: #ffffff;
}
.stSelectbox label {
color: #ffffff !important;
font-weight: 600;
font-size: 1.05rem;
}
/* Divider Styling */
hr {
border: none;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.5), transparent);
margin: 2rem 0;
}
/* Caption Styling */
.stCaption {
color: rgba(255, 255, 255, 0.9) !important;
font-size: 0.95rem;
font-weight: 500;
}
/* Markdown Links */
a {
color: #a8c0ff;
text-decoration: none;
transition: all 0.3s ease;
font-weight: 600;
}
a:hover {
color: #fbbf24;
text-shadow: 0 0 15px rgba(251, 191, 36, 0.8);
}
/* Container Borders */
.element-container {
transition: all 0.3s ease;
}
/* Hover effect for interactive elements */
.stButton, .stTextInput, .stTextArea, .stSelectbox, .stRadio {
transition: all 0.3s ease;
}
/* Audio Player Custom Styling */
audio::-webkit-media-controls-panel {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.8), rgba(118, 75, 162, 0.8));
}
/* Chat Input Styling */
.stChatInputContainer {
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 1rem;
}
/* Success/Info/Warning/Error Box Enhancements */
.stSuccess {
background: linear-gradient(135deg, rgba(46, 213, 115, 0.2), rgba(46, 213, 115, 0.1));
border-left: 4px solid #2ed573;
}
.stInfo {
background: linear-gradient(135deg, rgba(79, 172, 254, 0.2), rgba(79, 172, 254, 0.1));
border-left: 4px solid #4facfe;
}
.stWarning {
background: linear-gradient(135deg, rgba(251, 191, 36, 0.2), rgba(251, 191, 36, 0.1));
border-left: 4px solid #fbbf24;
}
.stError {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(239, 68, 68, 0.1));
border-left: 4px solid #ef4444;
}
</style>
""", unsafe_allow_html=True)
# ────────────────────────────────────────────────
# Session state initialization
# ────────────────────────────────────────────────
if 'rag' not in st.session_state:
init_db()
st.session_state.rag = RAGEngine()
os.makedirs("uploads", exist_ok=True)
if 'student_profile' not in st.session_state:
st.session_state.student_profile = {
"difficulty": "beginner",
"language": "English",
"weak_topics": []
}
if 'conversation_history' not in st.session_state:
st.session_state.conversation_history = []
# Whisper model will be loaded on-demand when voice input is used
# if 'whisper_model' not in st.session_state:
# with st.spinner("Loading Whisper model (first time only)..."):
# st.session_state.whisper_model = whisper.load_model("base")
# ────────────────────────────────────────────────
# Sidebar
# ────────────────────────────────────────────────
with st.sidebar:
st.markdown("""
<div style='text-align: center; padding: 1rem 0;'>
<div class='floating' style='font-size: 4rem; margin-bottom: 0.5rem;'>🧠</div>
<h1 class='gradient-text' style='font-size: 2rem; margin: 0;'>MindGap AI</h1>
<p style='color: rgba(255,255,255,0.9); font-size: 0.9rem; margin-top: 0.5rem;'>✨ Voice + Text Adaptive Learning ✨</p>
</div>
""", unsafe_allow_html=True)
menu = st.radio("🎯 Navigation", [
"🏠 Home & Lessons",
"📤 Upload Materials",
"📊 Dashboard",
"🗣️ Voice Conversation"
], label_visibility="visible")
st.divider()
st.markdown("### ⚙️ Settings")
diff = st.selectbox("🎓 Learning level", ["beginner", "intermediate", "advanced"],
index=["beginner","intermediate","advanced"].index(st.session_state.student_profile["difficulty"]))
lang = st.selectbox("🌍 Response language", ["English", "Spanish", "French"],
index=["English","Spanish","French"].index(st.session_state.student_profile["language"]))
st.session_state.student_profile["difficulty"] = diff
st.session_state.student_profile["language"] = lang
# ────────────────────────────────────────────────
# Pages
# ────────────────────────────────────────────────
if menu == "🏠 Home & Lessons":
st.markdown("""
<div style='text-align: center; padding: 2rem 0;'>
<div class='floating' style='font-size: 5rem; margin-bottom: 1rem;'>🎓</div>
<h1 class='gradient-text' style='font-size: 3.5rem;'>Bridge Your Knowledge Gaps</h1>
<p class='neon-text' style='font-size: 1.2rem; margin-top: 1rem;'>Personalized AI-powered learning at your fingertips</p>
</div>
""", unsafe_allow_html=True)
st.markdown("<div style='height: 2rem;'></div>", unsafe_allow_html=True)
topic = st.text_input("💡 What would you like to learn today?",
placeholder="e.g. Python decorators, Photosynthesis, Blockchain...",
label_visibility="visible")
if st.button("✨ Generate Lesson + Quiz", use_container_width=True) and topic.strip():
with st.spinner("🔮 Creating personalized lesson..."):
chunks = st.session_state.rag.search(topic)
context = "\n".join(chunks)
lesson_text = st.session_state.rag.generate_response(
topic, context, st.session_state.student_profile, st.session_state.conversation_history
)
quiz_data = st.session_state.rag.generate_quiz(
topic, context, st.session_state.student_profile
)
st.session_state.current_topic = topic
st.session_state.current_lesson = lesson_text
st.session_state.current_quiz = quiz_data
if 'current_lesson' in st.session_state:
st.markdown("<div style='height: 2rem;'></div>", unsafe_allow_html=True)
with st.container():
st.markdown(f"""
<div class='glass-card'>
<h2 style='color: white; margin-bottom: 1rem;'>📚 Lesson: <span class='neon-text'>{st.session_state.current_topic}</span></h2>
</div>
""", unsafe_allow_html=True)
st.markdown(f"""
<div class='glass-card' style='background: white; color: #000000; padding: 2.5rem; line-height: 1.8; font-size: 1.05rem;'>
<style>
.glass-card h1, .glass-card h2, .glass-card h3 {{
color: #000000 !important;
font-weight: 700 !important;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}}
.glass-card h1 {{
font-size: 2rem !important;
border-bottom: 3px solid #8b5cf6;
padding-bottom: 0.5rem;
}}
.glass-card h2 {{
font-size: 1.5rem !important;
}}
.glass-card h3 {{
font-size: 1.25rem !important;
}}
.glass-card p {{
color: #000000 !important;
margin-bottom: 1rem;
font-weight: 400;
}}
.glass-card strong, .glass-card b {{
color: #000000 !important;
font-weight: 700 !important;
}}
.glass-card ul {{
list-style: none;
padding-left: 0;
margin: 1rem 0;
}}
.glass-card ul li {{
position: relative;
padding-left: 2rem;
margin-bottom: 0.75rem;
color: #000000 !important;
font-weight: 500;
}}
.glass-card ul li::before {{
content: '●';
position: absolute;
left: 0.5rem;
color: #8b5cf6;
font-size: 1.2rem;
font-weight: bold;
}}
.glass-card ol {{
padding-left: 2rem;
margin: 1rem 0;
}}
.glass-card ol li {{
margin-bottom: 0.75rem;
color: #000000 !important;
font-weight: 500;
}}
.glass-card code {{
background: #f3f4f6;
padding: 0.2rem 0.5rem;
border-radius: 4px;
color: #6366f1;
font-family: 'Courier New', monospace;
font-weight: 600;
}}
</style>
{st.session_state.current_lesson}
</div>
""", unsafe_allow_html=True)
if st.session_state.current_quiz:
st.markdown("<div style='height: 1rem;'></div>", unsafe_allow_html=True)
st.markdown("""
<div class='glass-card'>
<h3 style='color: white;'>🎯 Quick Knowledge Check</h3>
</div>
""", unsafe_allow_html=True)
user_answers = []
for i, q in enumerate(st.session_state.current_quiz):
st.markdown(f"""
<div class='glass-card' style='background: rgba(255, 255, 255, 0.2);'>
<p style='color: white; font-weight: 600; font-size: 1.1rem;'>❓ Q{i+1}. {q['question']}</p>
</div>
""", unsafe_allow_html=True)
choice = st.radio(" ", q['options'], key=f"q_{i}_{st.session_state.current_topic}", horizontal=True, label_visibility="collapsed")
user_answers.append(choice)
if st.button("🚀 Submit Answers", type="primary", use_container_width=True):
score = sum(1 for a, q in zip(user_answers, st.session_state.current_quiz) if a == q['correct_answer'])
total = len(st.session_state.current_quiz)
percentage = score/total * 100
if percentage == 100:
st.markdown(f"""
<div class='glass-card glow' style='text-align: center; background: rgba(46, 213, 115, 0.3);'>
<h2 class='neon-text' style='font-size: 2.5rem;'>🎉 Perfect Score! 🎉</h2>
<p style='color: white; font-size: 1.5rem;'>{score}/{total} ({percentage:.0f}%)</p>
</div>
""", unsafe_allow_html=True)
st.balloons()
elif percentage >= 70:
st.markdown(f"""
<div class='glass-card' style='text-align: center; background: rgba(52, 211, 153, 0.3);'>
<h2 style='color: white; font-size: 2rem;'>✅ Great Job!</h2>
<p style='color: white; font-size: 1.3rem;'>{score}/{total} ({percentage:.0f}%)</p>
</div>
""", unsafe_allow_html=True)
else:
st.markdown(f"""
<div class='glass-card' style='text-align: center; background: rgba(251, 191, 36, 0.3);'>
<h2 style='color: white; font-size: 2rem;'>📖 Keep Learning!</h2>
<p style='color: white; font-size: 1.3rem;'>{score}/{total} ({percentage:.0f}%)</p>
</div>
""", unsafe_allow_html=True)
save_score(st.session_state.current_topic, score, total, st.session_state.student_profile["difficulty"])
if score == total:
save_achievement("Perfect Score Master")
for i, (q, ans) in enumerate(zip(st.session_state.current_quiz, user_answers)):
if ans != q['correct_answer']:
st.error(f"❌ Q{i+1}: Wrong → Correct answer: **{q['correct_answer']}**")
st.info(f"💡 {q['explanation']}")
# ────────────────────────────────────────────────
# Upload Materials
# ────────────────────────────────────────────────
elif menu == "📤 Upload Materials":
st.markdown("""
<div style='text-align: center; padding: 2rem 0;'>
<div class='floating' style='font-size: 5rem; margin-bottom: 1rem;'>📚</div>
<h1 class='gradient-text' style='font-size: 3rem;'>Upload Study Materials</h1>
<p class='neon-text' style='font-size: 1.1rem; margin-top: 1rem;'>PDF • TXT • PNG • JPG • JPEG</p>
</div>
""", unsafe_allow_html=True)
st.markdown("<div style='height: 2rem;'></div>", unsafe_allow_html=True)
file = st.file_uploader("📎 Select file to upload", type=["pdf","txt","png","jpg","jpeg"])
if file and st.button("🚀 Process File", use_container_width=True):
path = os.path.join("uploads", file.name)
with open(path, "wb") as f:
f.write(file.getbuffer())
ocr_text = ""
if file.type.startswith("image/"):
try:
img = Image.open(path)
ocr_text = pytesseract.image_to_string(img)
except:
st.warning("⚠️ Could not perform OCR on image.")
with st.spinner("🔄 Indexing content..."):
count = st.session_state.rag.process_file(path, ocr_text)
st.markdown(f"""
<div class='glass-card glow' style='text-align: center; background: rgba(46, 213, 115, 0.3);'>
<h2 class='neon-text'>✅ Success!</h2>
<p style='color: white; font-size: 1.2rem;'>Added <strong>{count}</strong> chunks to knowledge base</p>
</div>
""", unsafe_allow_html=True)
# ────────────────────────────────────────────────
# Dashboard
# ────────────────────────────────────────────────
elif menu == "📊 Dashboard":
st.markdown("""
<div style='text-align: center; padding: 2rem 0;'>
<div class='floating' style='font-size: 5rem; margin-bottom: 1rem;'>📈</div>
<h1 class='gradient-text' style='font-size: 3rem;'>Your Learning Dashboard</h1>
<p class='neon-text' style='font-size: 1.1rem; margin-top: 1rem;'>Track your progress and achievements</p>
</div>
""", unsafe_allow_html=True)
st.markdown("<div style='height: 2rem;'></div>", unsafe_allow_html=True)
col1, col2 = st.columns(2)
with col1:
st.markdown("""
<div class='glass-card'>
<h3 style='color: white;'>⚠️ Knowledge Gaps</h3>
</div>
""", unsafe_allow_html=True)
weak = get_weak_topics()
if weak:
for w in weak:
st.markdown(f"""
<div class='glass-card' style='background: rgba(239, 68, 68, 0.3); border-color: rgba(239, 68, 68, 0.5);'>
<p style='color: white; margin: 0;'>🔴 <strong>{w['topic']}</strong> (failed {w['frequency']}×)</p>
</div>
""", unsafe_allow_html=True)
else:
st.markdown("""
<div class='glass-card glow' style='background: rgba(46, 213, 115, 0.3);'>
<p style='color: white; text-align: center; margin: 0;'>✅ No weak topics detected — great job!</p>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown("""
<div class='glass-card'>
<h3 style='color: white;'>📊 Recent Performance</h3>
</div>
""", unsafe_allow_html=True)
hist = get_performance_history()
if hist:
for r in hist[:8]:
percentage = (r['score'] / r['total']) * 100
color = "rgba(46, 213, 115, 0.3)" if percentage >= 70 else "rgba(251, 191, 36, 0.3)"
st.markdown(f"""
<div class='glass-card' style='background: {color};'>
<p style='color: white; margin: 0;'><strong>{r['topic']}</strong> • {r['score']}/{r['total']} ({percentage:.0f}%) • {r['date'][:10]}</p>
</div>
""", unsafe_allow_html=True)
else:
st.markdown("""
<div class='glass-card' style='background: rgba(255, 255, 255, 0.1);'>
<p style='color: white; text-align: center; margin: 0;'>No quiz data yet. Start learning!</p>
</div>
""", unsafe_allow_html=True)
st.markdown("<div style='height: 2rem;'></div>", unsafe_allow_html=True)
st.markdown("""
<div class='glass-card'>
<h3 style='color: white;'>🏆 Achievements</h3>
</div>
""", unsafe_allow_html=True)
ach = get_achievements()
if ach:
cols = st.columns(3)
for idx, a in enumerate(ach):
with cols[idx % 3]:
st.markdown(f"""
<div class='glass-card glow' style='text-align: center; background: rgba(251, 191, 36, 0.3);'>
<div style='font-size: 3rem;'>🏆</div>
<p style='color: white; font-weight: 600; margin: 0.5rem 0;'>{a['name']}</p>
<p style='color: rgba(255,255,255,0.8); font-size: 0.9rem; margin: 0;'>{a['date'][:10]}</p>
</div>
""", unsafe_allow_html=True)
else:
st.markdown("""
<div class='glass-card' style='background: rgba(255, 255, 255, 0.1); text-align: center;'>
<p style='color: white; margin: 0;'>Keep learning to unlock achievements! 🌟</p>
</div>
""", unsafe_allow_html=True)
# ────────────────────────────────────────────────
# Voice Conversation
# ────────────────────────────────────────────────
elif menu == "🗣️ Voice Conversation":
st.markdown("""
<div style='text-align: center; padding: 2rem 0;'>
<div class='floating' style='font-size: 5rem; margin-bottom: 1rem;'>🎤</div>
<h1 class='gradient-text' style='font-size: 3rem;'>Voice Chat with MindGap AI</h1>
<p class='neon-text' style='font-size: 1.1rem; margin-top: 1rem;'>Speak → AI listens → AI speaks back</p>
</div>
""", unsafe_allow_html=True)
st.markdown("<div style='height: 2rem;'></div>", unsafe_allow_html=True)
# Show history
for msg in st.session_state.conversation_history:
with st.chat_message("user"):
st.write(msg["user"])
with st.chat_message("assistant"):
st.write(msg["ai"])
if "audio_data" in msg:
st.audio(msg["audio_data"], format="audio/mp3")
# Live microphone recording
st.markdown("""
<div class='glass-card' style='text-align: center;'>
<h3 style='color: white; margin-bottom: 1rem;'>🎤 Voice Input</h3>
<p style='color: rgba(255,255,255,0.9);'>Click the microphone button below to start recording</p>
</div>
""", unsafe_allow_html=True)
# Check if FFmpeg is available
import shutil
import os
# Try to add FFmpeg to PATH if not already there
ffmpeg_dir = r"C:\ffmpeg\ffmpeg-8.0.1-essentials_build\bin"
if os.path.exists(ffmpeg_dir) and ffmpeg_dir not in os.environ.get('PATH', ''):
os.environ['PATH'] = ffmpeg_dir + os.pathsep + os.environ.get('PATH', '')
ffmpeg_path = shutil.which('ffmpeg')
if ffmpeg_path:
ffmpeg_available = True
st.success("✅ Voice recording enabled")
else:
ffmpeg_available = False
st.warning("⚠️ FFmpeg not found. Voice recording requires FFmpeg. Please install FFmpeg and restart the app.")
if ffmpeg_available:
audio_bytes = audio_recorder(
text="Click to record",
recording_color="#e74c3c",
neutral_color="#3498db",
icon_name="microphone",
icon_size="2x"
)
if audio_bytes:
with st.spinner("Transcribing your voice..."):
try:
# Save temp file
tmp_audio = "temp_voice_input.wav"
with open(tmp_audio, "wb") as f:
f.write(audio_bytes)
# Load whisper model if not loaded
if 'whisper_model' not in st.session_state:
import whisper
with st.spinner("Loading speech recognition model (first time only)..."):
st.session_state.whisper_model = whisper.load_model("base")
# Whisper → text
result = st.session_state.whisper_model.transcribe(tmp_audio, language="en")
user_speech = result["text"].strip()
if not user_speech:
st.warning("Could not understand speech. Try speaking more clearly.")
else:
st.chat_message("user").write(user_speech)
# Get context + generate answer
with st.spinner("Thinking..."):
ctx_chunks = st.session_state.rag.search(user_speech)
context_str = "\n".join(ctx_chunks)
ai_text = st.session_state.rag.generate_response(
user_speech,
context_str,
st.session_state.student_profile,
st.session_state.conversation_history
)
st.chat_message("assistant").write(ai_text)
# Text → Speech
try:
lang_code = {"English":"en", "Spanish":"es", "French":"fr"}.get(
st.session_state.student_profile["language"], "en"
)
tts = gTTS(ai_text, lang=lang_code, slow=False)
audio_io = BytesIO()
tts.write_to_fp(audio_io)
audio_io.seek(0)
st.audio(audio_io, format="audio/mp3")
# Save to history
st.session_state.conversation_history.append({
"user": user_speech,
"ai": ai_text,
"audio_data": audio_io.getvalue()
})
except Exception as e:
st.error(f"TTS failed: {e}")
# Save to history without audio
st.session_state.conversation_history.append({
"user": user_speech,
"ai": ai_text
})
except Exception as e:
st.error(f"Voice transcription failed: {e}")
finally:
# Cleanup
if os.path.exists(tmp_audio):
os.remove(tmp_audio)