Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -419,14 +419,7 @@ init_session_state()
|
|
| 419 |
|
| 420 |
def extract_and_fix_json(text, subject="General"):
|
| 421 |
"""
|
| 422 |
-
Extract JSON from text and fix common issues
|
| 423 |
-
|
| 424 |
-
Args:
|
| 425 |
-
text (str): Raw text containing JSON
|
| 426 |
-
subject (str): Subject name for fallback
|
| 427 |
-
|
| 428 |
-
Returns:
|
| 429 |
-
dict: Parsed JSON or generic plan
|
| 430 |
"""
|
| 431 |
if not text or not isinstance(text, str):
|
| 432 |
if st.session_state.show_debug:
|
|
@@ -436,18 +429,32 @@ def extract_and_fix_json(text, subject="General"):
|
|
| 436 |
# Store raw response for debug
|
| 437 |
st.session_state.raw_response = text
|
| 438 |
|
| 439 |
-
# Clean the text
|
| 440 |
text = text.strip()
|
| 441 |
|
| 442 |
-
#
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
|
| 448 |
# Show cleaned text in debug
|
| 449 |
if st.session_state.show_debug:
|
| 450 |
-
with st.expander("π§Ή Cleaned Text
|
| 451 |
st.code(text[:500] + "..." if len(text) > 500 else text)
|
| 452 |
|
| 453 |
# Find JSON boundaries
|
|
@@ -457,6 +464,7 @@ def extract_and_fix_json(text, subject="General"):
|
|
| 457 |
if start_idx == -1 or end_idx == -1 or end_idx <= start_idx:
|
| 458 |
if st.session_state.show_debug:
|
| 459 |
st.error(f"β No JSON found. Start: {start_idx}, End: {end_idx}")
|
|
|
|
| 460 |
return None
|
| 461 |
|
| 462 |
json_str = text[start_idx:end_idx+1]
|
|
@@ -464,7 +472,7 @@ def extract_and_fix_json(text, subject="General"):
|
|
| 464 |
# Show JSON string in debug
|
| 465 |
if st.session_state.show_debug:
|
| 466 |
with st.expander("π JSON String Being Parsed", expanded=False):
|
| 467 |
-
st.code(json_str[:
|
| 468 |
|
| 469 |
try:
|
| 470 |
# First try: Direct parse
|
|
@@ -662,7 +670,7 @@ def create_generic_plan(subject, days_available):
|
|
| 662 |
|
| 663 |
def generate_study_plan(api_key, **kwargs):
|
| 664 |
"""
|
| 665 |
-
Generate study plan using Gemini
|
| 666 |
"""
|
| 667 |
if not api_key:
|
| 668 |
st.error("β No API key provided")
|
|
@@ -674,17 +682,19 @@ def generate_study_plan(api_key, **kwargs):
|
|
| 674 |
days_available = kwargs.get('days_available', 60)
|
| 675 |
hours_per_day = kwargs.get('hours_per_day', 2)
|
| 676 |
current_level = kwargs.get('current_level', 'Beginner')
|
|
|
|
| 677 |
study_days = kwargs.get('study_days', ["Mon", "Tue", "Wed", "Thu", "Fri"])
|
| 678 |
|
| 679 |
weeks = max(1, days_available // 7)
|
| 680 |
|
| 681 |
-
#
|
| 682 |
-
prompt = f"""Create a detailed {weeks}-week study plan for: {subject}
|
| 683 |
|
| 684 |
Study schedule: {hours_per_day} hours/day, {len(study_days)} days/week ({', '.join(study_days)})
|
| 685 |
Current level: {current_level}
|
|
|
|
| 686 |
|
| 687 |
-
Return valid JSON with this exact structure:
|
| 688 |
|
| 689 |
{{
|
| 690 |
"subject": "{subject}",
|
|
@@ -698,54 +708,73 @@ Return valid JSON with this exact structure:
|
|
| 698 |
"focus": "Week focus title",
|
| 699 |
"objectives": ["Objective 1", "Objective 2"],
|
| 700 |
"daily_tasks": [
|
| 701 |
-
["Task 1 Monday", "Task 2 Monday"],
|
| 702 |
-
["Task 1 Tuesday", "Task 2 Tuesday"],
|
| 703 |
-
["Task 1 Wednesday", "Task 2 Wednesday"],
|
| 704 |
-
["Task 1 Thursday", "Task 2 Thursday"],
|
| 705 |
-
["Task 1 Friday", "Task 2 Friday"]
|
| 706 |
],
|
| 707 |
-
"day_focus": ["Monday:
|
| 708 |
-
"week_summary": "
|
| 709 |
"resources": ["Resource 1", "Resource 2"],
|
| 710 |
-
"milestone": "
|
| 711 |
}}
|
| 712 |
],
|
| 713 |
"study_tips": ["Tip 1", "Tip 2"],
|
| 714 |
"success_metrics": ["Metric 1", "Metric 2"]
|
| 715 |
}}
|
| 716 |
|
| 717 |
-
IMPORTANT
|
| 718 |
-
1.
|
| 719 |
-
2.
|
| 720 |
-
3.
|
| 721 |
-
4. Make tasks specific and
|
|
|
|
|
|
|
|
|
|
| 722 |
|
| 723 |
try:
|
| 724 |
-
# Configure API
|
| 725 |
genai.configure(api_key=api_key)
|
| 726 |
model = genai.GenerativeModel('gemini-2.5-flash')
|
| 727 |
|
| 728 |
# Show prompt in debug mode
|
| 729 |
if st.session_state.show_debug:
|
| 730 |
with st.expander("π Prompt Sent to AI", expanded=False):
|
| 731 |
-
st.code(prompt[:
|
| 732 |
|
| 733 |
-
# Generate response
|
| 734 |
response = model.generate_content(
|
| 735 |
prompt,
|
| 736 |
generation_config=genai.GenerationConfig(
|
| 737 |
-
max_output_tokens=
|
| 738 |
-
temperature=0.7
|
| 739 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 740 |
)
|
| 741 |
|
| 742 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 743 |
st.session_state.raw_response = raw_text
|
| 744 |
|
| 745 |
# Show raw response in debug mode
|
| 746 |
if st.session_state.show_debug:
|
| 747 |
with st.expander("π Raw AI Response", expanded=False):
|
| 748 |
-
|
|
|
|
|
|
|
|
|
|
| 749 |
|
| 750 |
# Extract JSON
|
| 751 |
plan = extract_and_fix_json(raw_text, subject)
|
|
@@ -766,6 +795,7 @@ IMPORTANT:
|
|
| 766 |
plan['generated_at'] = datetime.now().isoformat()
|
| 767 |
plan['total_days'] = days_available
|
| 768 |
plan['user_level'] = current_level
|
|
|
|
| 769 |
|
| 770 |
# Initialize week tracking
|
| 771 |
for week in plan.get('weekly_schedule', []):
|
|
@@ -783,16 +813,26 @@ IMPORTANT:
|
|
| 783 |
|
| 784 |
return plan
|
| 785 |
else:
|
| 786 |
-
st.warning("β οΈ Could not extract valid JSON from AI response")
|
| 787 |
if st.session_state.show_debug:
|
| 788 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 789 |
return create_generic_plan(subject, days_available)
|
| 790 |
|
| 791 |
except Exception as e:
|
| 792 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 793 |
if st.session_state.show_debug:
|
| 794 |
with st.expander("π Error Details", expanded=False):
|
| 795 |
st.exception(e)
|
|
|
|
| 796 |
return create_generic_plan(subject, days_available)
|
| 797 |
|
| 798 |
def generate_weekly_test(api_key, week_number, weekly_tasks, subject):
|
|
@@ -1188,7 +1228,7 @@ tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([
|
|
| 1188 |
|
| 1189 |
|
| 1190 |
# ============================================
|
| 1191 |
-
# TAB 1: DEFINE GOAL
|
| 1192 |
# ============================================
|
| 1193 |
|
| 1194 |
with tab1:
|
|
@@ -1206,26 +1246,38 @@ with tab1:
|
|
| 1206 |
else:
|
| 1207 |
st.success("β
**API Key Loaded**")
|
| 1208 |
|
| 1209 |
-
# Test API Button
|
| 1210 |
if st.session_state.api_key and st.session_state.show_debug:
|
| 1211 |
-
if st.button("Test API Connection", type="secondary", key="
|
| 1212 |
try:
|
| 1213 |
genai.configure(api_key=st.session_state.api_key)
|
| 1214 |
model = genai.GenerativeModel('gemini-2.5-flash')
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1218 |
st.success(f"β
**API Connected:** {test_response.text}")
|
| 1219 |
else:
|
| 1220 |
-
st.error("β No response from API")
|
|
|
|
| 1221 |
except Exception as e:
|
| 1222 |
st.error(f"β **API Error:** {str(e)}")
|
|
|
|
|
|
|
| 1223 |
|
| 1224 |
st.markdown("---")
|
| 1225 |
|
| 1226 |
# Option 1: Upload Existing Plan
|
| 1227 |
st.info("π€ **Option 1: Upload Existing Plan**")
|
| 1228 |
-
uploaded_file = st.file_uploader("Upload saved study plan (JSON)", type=['json'], key="
|
| 1229 |
|
| 1230 |
if uploaded_file is not None and not st.session_state.plan_loaded:
|
| 1231 |
try:
|
|
@@ -1239,19 +1291,46 @@ with tab1:
|
|
| 1239 |
|
| 1240 |
st.markdown("---")
|
| 1241 |
|
| 1242 |
-
# Option 2: Create New Plan -
|
| 1243 |
st.info("π― **Option 2: Create New Plan**")
|
| 1244 |
|
| 1245 |
col1, col2 = st.columns([2, 1])
|
| 1246 |
|
| 1247 |
with col1:
|
| 1248 |
-
|
| 1249 |
-
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1253 |
)
|
| 1254 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1255 |
col1a, col2a = st.columns(2)
|
| 1256 |
with col1a:
|
| 1257 |
hours_per_day = st.slider(
|
|
@@ -1259,7 +1338,7 @@ with tab1:
|
|
| 1259 |
min_value=1,
|
| 1260 |
max_value=8,
|
| 1261 |
value=2,
|
| 1262 |
-
key="
|
| 1263 |
)
|
| 1264 |
|
| 1265 |
with col2a:
|
|
@@ -1268,14 +1347,14 @@ with tab1:
|
|
| 1268 |
min_value=7,
|
| 1269 |
max_value=365,
|
| 1270 |
value=60,
|
| 1271 |
-
key="
|
| 1272 |
)
|
| 1273 |
|
| 1274 |
study_days = st.multiselect(
|
| 1275 |
"Study days:",
|
| 1276 |
["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
| 1277 |
default=["Mon", "Tue", "Wed", "Thu", "Fri"],
|
| 1278 |
-
key="
|
| 1279 |
)
|
| 1280 |
|
| 1281 |
with col2:
|
|
@@ -1283,17 +1362,25 @@ with tab1:
|
|
| 1283 |
"Your level:",
|
| 1284 |
["Beginner", "Intermediate", "Advanced"],
|
| 1285 |
index=0,
|
| 1286 |
-
key="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1287 |
)
|
| 1288 |
|
| 1289 |
st.markdown("---")
|
| 1290 |
|
| 1291 |
# Generate Plan Button
|
| 1292 |
-
if st.button("π Generate AI Study Plan", type="primary", use_container_width=True, key="
|
| 1293 |
if not st.session_state.api_key:
|
| 1294 |
st.error("β οΈ **API Key Required:** Please set GEMINI_API_KEY in Hugging Face Secrets.")
|
| 1295 |
-
elif not subject:
|
| 1296 |
-
st.error("β οΈ Please enter a subject
|
| 1297 |
elif not study_days:
|
| 1298 |
st.error("β οΈ Please select at least one study day")
|
| 1299 |
else:
|
|
@@ -1306,12 +1393,13 @@ with tab1:
|
|
| 1306 |
len(study_days)
|
| 1307 |
)
|
| 1308 |
|
| 1309 |
-
# Create plan data
|
| 1310 |
plan_data = {
|
| 1311 |
'subject': subject,
|
| 1312 |
'days_available': days_available,
|
| 1313 |
'hours_per_day': hours_per_day,
|
| 1314 |
'current_level': current_level,
|
|
|
|
| 1315 |
'study_days': study_days
|
| 1316 |
}
|
| 1317 |
|
|
@@ -1371,7 +1459,7 @@ with tab1:
|
|
| 1371 |
st.metric("Plan Type", plan_type)
|
| 1372 |
|
| 1373 |
with st.expander("π Raw AI Response", expanded=False):
|
| 1374 |
-
st.text_area("Full Response", st.session_state.raw_response, height=300, key="
|
| 1375 |
|
| 1376 |
if st.session_state.study_plan:
|
| 1377 |
with st.expander("π Parsed Plan Structure", expanded=False):
|
|
@@ -2157,7 +2245,7 @@ with tab4:
|
|
| 2157 |
st.info("π **Generate a study plan first to see analytics!**")
|
| 2158 |
|
| 2159 |
# ============================================
|
| 2160 |
-
# TAB 5: TESTS
|
| 2161 |
# ============================================
|
| 2162 |
|
| 2163 |
with tab5:
|
|
@@ -2188,44 +2276,54 @@ with tab5:
|
|
| 2188 |
completed_week_tasks += 1
|
| 2189 |
|
| 2190 |
completion_percentage = (completed_week_tasks / week_tasks * 100) if week_tasks > 0 else 0
|
| 2191 |
-
test_available = completion_percentage >= 50
|
| 2192 |
-
|
| 2193 |
-
col1, col2, col3 = st.columns([3, 1, 1])
|
| 2194 |
-
|
| 2195 |
-
with col1:
|
| 2196 |
-
st.markdown(f"### Week {week_num}: {week.get('focus', '')}")
|
| 2197 |
-
st.caption(f"Completion: {completion_percentage:.0f}%")
|
| 2198 |
|
| 2199 |
-
with
|
| 2200 |
-
|
| 2201 |
-
test_taken = test_key in st.session_state.test_results
|
| 2202 |
|
| 2203 |
-
|
| 2204 |
-
|
| 2205 |
-
st.
|
| 2206 |
-
|
| 2207 |
-
|
| 2208 |
-
|
| 2209 |
-
|
| 2210 |
-
|
| 2211 |
-
|
| 2212 |
-
|
| 2213 |
-
|
| 2214 |
-
|
| 2215 |
-
|
| 2216 |
-
|
| 2217 |
-
|
| 2218 |
-
|
| 2219 |
-
|
| 2220 |
-
|
| 2221 |
-
|
| 2222 |
-
|
| 2223 |
-
|
| 2224 |
-
|
| 2225 |
-
|
| 2226 |
-
|
| 2227 |
-
|
| 2228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2229 |
|
| 2230 |
# Test Taking View
|
| 2231 |
elif st.session_state.current_test and not st.session_state.test_completed:
|
|
@@ -2233,27 +2331,58 @@ with tab5:
|
|
| 2233 |
test_questions = st.session_state.test_questions
|
| 2234 |
|
| 2235 |
st.markdown(f"### π Week {week_num} Test")
|
|
|
|
| 2236 |
|
| 2237 |
for i, question in enumerate(test_questions):
|
| 2238 |
-
st.
|
| 2239 |
-
|
| 2240 |
-
|
| 2241 |
-
|
| 2242 |
-
|
| 2243 |
-
|
| 2244 |
-
|
| 2245 |
-
|
| 2246 |
-
|
| 2247 |
-
|
| 2248 |
-
|
| 2249 |
-
|
| 2250 |
-
|
| 2251 |
-
|
| 2252 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2253 |
|
| 2254 |
-
|
| 2255 |
-
|
| 2256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2257 |
|
| 2258 |
# Test Results View
|
| 2259 |
elif st.session_state.test_completed:
|
|
@@ -2281,20 +2410,50 @@ with tab5:
|
|
| 2281 |
st.success(f"## π― Test Score: {score_percentage:.1f}%")
|
| 2282 |
st.info(f"**{correct_count} out of {len(test_questions)} correct**")
|
| 2283 |
|
| 2284 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2285 |
for i, question in enumerate(test_questions):
|
| 2286 |
user_answer = user_answers.get(str(i), "Not answered")
|
| 2287 |
is_correct = user_answer == question['correct_answer']
|
| 2288 |
|
| 2289 |
-
st.markdown(f"**Q{i+1}
|
| 2290 |
-
|
| 2291 |
-
|
| 2292 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2293 |
st.markdown("---")
|
| 2294 |
|
| 2295 |
-
|
|
|
|
|
|
|
|
|
|
| 2296 |
st.session_state.current_test = None
|
| 2297 |
st.session_state.test_completed = False
|
|
|
|
|
|
|
| 2298 |
st.rerun()
|
| 2299 |
|
| 2300 |
else:
|
|
|
|
| 419 |
|
| 420 |
def extract_and_fix_json(text, subject="General"):
|
| 421 |
"""
|
| 422 |
+
Extract JSON from text and fix common issues
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
"""
|
| 424 |
if not text or not isinstance(text, str):
|
| 425 |
if st.session_state.show_debug:
|
|
|
|
| 429 |
# Store raw response for debug
|
| 430 |
st.session_state.raw_response = text
|
| 431 |
|
| 432 |
+
# Clean the text - more aggressive cleaning
|
| 433 |
text = text.strip()
|
| 434 |
|
| 435 |
+
# Check if response indicates safety blocking
|
| 436 |
+
if "safety" in text.lower() or "blocked" in text.lower() or "not allowed" in text.lower():
|
| 437 |
+
if st.session_state.show_debug:
|
| 438 |
+
st.error("β Response indicates safety blocking")
|
| 439 |
+
return None
|
| 440 |
+
|
| 441 |
+
# Remove ALL markdown code blocks and any surrounding text
|
| 442 |
+
text = re.sub(r'.*?```(?:json)?\s*', '', text, flags=re.IGNORECASE | re.DOTALL)
|
| 443 |
+
text = re.sub(r'```.*', '', text, flags=re.IGNORECASE | re.DOTALL)
|
| 444 |
+
|
| 445 |
+
# Remove any non-JSON text before first {
|
| 446 |
+
first_brace = text.find('{')
|
| 447 |
+
if first_brace > 0:
|
| 448 |
+
text = text[first_brace:]
|
| 449 |
+
|
| 450 |
+
# Remove any non-JSON text after last }
|
| 451 |
+
last_brace = text.rfind('}')
|
| 452 |
+
if last_brace != -1:
|
| 453 |
+
text = text[:last_brace+1]
|
| 454 |
|
| 455 |
# Show cleaned text in debug
|
| 456 |
if st.session_state.show_debug:
|
| 457 |
+
with st.expander("π§Ή Cleaned Text", expanded=False):
|
| 458 |
st.code(text[:500] + "..." if len(text) > 500 else text)
|
| 459 |
|
| 460 |
# Find JSON boundaries
|
|
|
|
| 464 |
if start_idx == -1 or end_idx == -1 or end_idx <= start_idx:
|
| 465 |
if st.session_state.show_debug:
|
| 466 |
st.error(f"β No JSON found. Start: {start_idx}, End: {end_idx}")
|
| 467 |
+
st.info(f"Text preview: {text[:200]}...")
|
| 468 |
return None
|
| 469 |
|
| 470 |
json_str = text[start_idx:end_idx+1]
|
|
|
|
| 472 |
# Show JSON string in debug
|
| 473 |
if st.session_state.show_debug:
|
| 474 |
with st.expander("π JSON String Being Parsed", expanded=False):
|
| 475 |
+
st.code(json_str[:800] + "..." if len(json_str) > 800 else json_str)
|
| 476 |
|
| 477 |
try:
|
| 478 |
# First try: Direct parse
|
|
|
|
| 670 |
|
| 671 |
def generate_study_plan(api_key, **kwargs):
|
| 672 |
"""
|
| 673 |
+
Generate study plan using Gemini with safety handling
|
| 674 |
"""
|
| 675 |
if not api_key:
|
| 676 |
st.error("β No API key provided")
|
|
|
|
| 682 |
days_available = kwargs.get('days_available', 60)
|
| 683 |
hours_per_day = kwargs.get('hours_per_day', 2)
|
| 684 |
current_level = kwargs.get('current_level', 'Beginner')
|
| 685 |
+
intensity = kwargs.get('intensity', 'Moderate')
|
| 686 |
study_days = kwargs.get('study_days', ["Mon", "Tue", "Wed", "Thu", "Fri"])
|
| 687 |
|
| 688 |
weeks = max(1, days_available // 7)
|
| 689 |
|
| 690 |
+
# IMPROVED PROMPT with safety considerations
|
| 691 |
+
prompt = f"""Create a detailed {weeks}-week study plan for learning: {subject}
|
| 692 |
|
| 693 |
Study schedule: {hours_per_day} hours/day, {len(study_days)} days/week ({', '.join(study_days)})
|
| 694 |
Current level: {current_level}
|
| 695 |
+
Intensity: {intensity}
|
| 696 |
|
| 697 |
+
Return ONLY valid JSON with this exact structure:
|
| 698 |
|
| 699 |
{{
|
| 700 |
"subject": "{subject}",
|
|
|
|
| 708 |
"focus": "Week focus title",
|
| 709 |
"objectives": ["Objective 1", "Objective 2"],
|
| 710 |
"daily_tasks": [
|
| 711 |
+
["Task 1 for Monday", "Task 2 for Monday"],
|
| 712 |
+
["Task 1 for Tuesday", "Task 2 for Tuesday"],
|
| 713 |
+
["Task 1 for Wednesday", "Task 2 for Wednesday"],
|
| 714 |
+
["Task 1 for Thursday", "Task 2 for Thursday"],
|
| 715 |
+
["Task 1 for Friday", "Task 2 for Friday"]
|
| 716 |
],
|
| 717 |
+
"day_focus": ["Monday: Introduction", "Tuesday: Practice", "Wednesday: Deep Dive", "Thursday: Review", "Friday: Project"],
|
| 718 |
+
"week_summary": "Brief summary of week's learning objectives",
|
| 719 |
"resources": ["Resource 1", "Resource 2"],
|
| 720 |
+
"milestone": "What to achieve by week's end"
|
| 721 |
}}
|
| 722 |
],
|
| 723 |
"study_tips": ["Tip 1", "Tip 2"],
|
| 724 |
"success_metrics": ["Metric 1", "Metric 2"]
|
| 725 |
}}
|
| 726 |
|
| 727 |
+
IMPORTANT RULES:
|
| 728 |
+
1. Return ONLY JSON, no additional text
|
| 729 |
+
2. topics_allocation values must be NUMBERS (e.g., 10, not "10 hours")
|
| 730 |
+
3. Include ALL {weeks} weeks in weekly_schedule array
|
| 731 |
+
4. Make tasks specific, practical, and educational
|
| 732 |
+
5. Keep content appropriate for all ages
|
| 733 |
+
6. Focus on learning and skill development
|
| 734 |
+
7. Ensure JSON is valid and complete"""
|
| 735 |
|
| 736 |
try:
|
| 737 |
+
# Configure API with safety settings
|
| 738 |
genai.configure(api_key=api_key)
|
| 739 |
model = genai.GenerativeModel('gemini-2.5-flash')
|
| 740 |
|
| 741 |
# Show prompt in debug mode
|
| 742 |
if st.session_state.show_debug:
|
| 743 |
with st.expander("π Prompt Sent to AI", expanded=False):
|
| 744 |
+
st.code(prompt[:800] + "..." if len(prompt) > 800 else prompt)
|
| 745 |
|
| 746 |
+
# Generate response with safety settings
|
| 747 |
response = model.generate_content(
|
| 748 |
prompt,
|
| 749 |
generation_config=genai.GenerationConfig(
|
| 750 |
+
max_output_tokens=3000, # Reduced for safety
|
| 751 |
+
temperature=0.7,
|
| 752 |
+
top_p=0.95,
|
| 753 |
+
top_k=40
|
| 754 |
+
),
|
| 755 |
+
safety_settings=[
|
| 756 |
+
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
| 757 |
+
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
| 758 |
+
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
| 759 |
+
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}
|
| 760 |
+
]
|
| 761 |
)
|
| 762 |
|
| 763 |
+
# SAFE way to get response text
|
| 764 |
+
if response and hasattr(response, 'text'):
|
| 765 |
+
raw_text = response.text
|
| 766 |
+
else:
|
| 767 |
+
raw_text = ""
|
| 768 |
+
|
| 769 |
st.session_state.raw_response = raw_text
|
| 770 |
|
| 771 |
# Show raw response in debug mode
|
| 772 |
if st.session_state.show_debug:
|
| 773 |
with st.expander("π Raw AI Response", expanded=False):
|
| 774 |
+
if raw_text:
|
| 775 |
+
st.text_area("AI Response", raw_text, height=200, key="ai_response_raw_gen")
|
| 776 |
+
else:
|
| 777 |
+
st.warning("No response text received")
|
| 778 |
|
| 779 |
# Extract JSON
|
| 780 |
plan = extract_and_fix_json(raw_text, subject)
|
|
|
|
| 795 |
plan['generated_at'] = datetime.now().isoformat()
|
| 796 |
plan['total_days'] = days_available
|
| 797 |
plan['user_level'] = current_level
|
| 798 |
+
plan['intensity'] = intensity
|
| 799 |
|
| 800 |
# Initialize week tracking
|
| 801 |
for week in plan.get('weekly_schedule', []):
|
|
|
|
| 813 |
|
| 814 |
return plan
|
| 815 |
else:
|
|
|
|
| 816 |
if st.session_state.show_debug:
|
| 817 |
+
st.warning("β οΈ Could not extract valid JSON from AI response")
|
| 818 |
+
if not raw_text:
|
| 819 |
+
st.error("β Empty response from AI")
|
| 820 |
+
else:
|
| 821 |
+
st.info("Falling back to generic plan")
|
| 822 |
return create_generic_plan(subject, days_available)
|
| 823 |
|
| 824 |
except Exception as e:
|
| 825 |
+
error_msg = str(e)
|
| 826 |
+
st.error(f"β AI Generation Error: {error_msg}")
|
| 827 |
+
|
| 828 |
+
# Handle safety errors
|
| 829 |
+
if "safety" in error_msg.lower() or "finish_reason" in error_msg.lower():
|
| 830 |
+
st.warning("π **Safety restriction triggered.** Try a different subject or simpler request.")
|
| 831 |
+
|
| 832 |
if st.session_state.show_debug:
|
| 833 |
with st.expander("π Error Details", expanded=False):
|
| 834 |
st.exception(e)
|
| 835 |
+
|
| 836 |
return create_generic_plan(subject, days_available)
|
| 837 |
|
| 838 |
def generate_weekly_test(api_key, week_number, weekly_tasks, subject):
|
|
|
|
| 1228 |
|
| 1229 |
|
| 1230 |
# ============================================
|
| 1231 |
+
# TAB 1: DEFINE GOAL - WITH DROPDOWN OPTIONS
|
| 1232 |
# ============================================
|
| 1233 |
|
| 1234 |
with tab1:
|
|
|
|
| 1246 |
else:
|
| 1247 |
st.success("β
**API Key Loaded**")
|
| 1248 |
|
| 1249 |
+
# Test API Button with SAFETY FIX
|
| 1250 |
if st.session_state.api_key and st.session_state.show_debug:
|
| 1251 |
+
if st.button("Test API Connection", type="secondary", key="test_api_tab1"):
|
| 1252 |
try:
|
| 1253 |
genai.configure(api_key=st.session_state.api_key)
|
| 1254 |
model = genai.GenerativeModel('gemini-2.5-flash')
|
| 1255 |
+
|
| 1256 |
+
# Use a safer, more neutral prompt
|
| 1257 |
+
test_response = model.generate_content(
|
| 1258 |
+
"Please respond with just the word 'Connected'",
|
| 1259 |
+
generation_config=genai.GenerationConfig(
|
| 1260 |
+
max_output_tokens=10,
|
| 1261 |
+
temperature=0.1
|
| 1262 |
+
)
|
| 1263 |
+
)
|
| 1264 |
+
|
| 1265 |
+
# Safely get response text
|
| 1266 |
+
if test_response and hasattr(test_response, 'text') and test_response.text:
|
| 1267 |
st.success(f"β
**API Connected:** {test_response.text}")
|
| 1268 |
else:
|
| 1269 |
+
st.error("β No valid response from API")
|
| 1270 |
+
|
| 1271 |
except Exception as e:
|
| 1272 |
st.error(f"β **API Error:** {str(e)}")
|
| 1273 |
+
if "safety" in str(e).lower():
|
| 1274 |
+
st.info("π **Safety restriction detected.** Try a different subject or rephrase your request.")
|
| 1275 |
|
| 1276 |
st.markdown("---")
|
| 1277 |
|
| 1278 |
# Option 1: Upload Existing Plan
|
| 1279 |
st.info("π€ **Option 1: Upload Existing Plan**")
|
| 1280 |
+
uploaded_file = st.file_uploader("Upload saved study plan (JSON)", type=['json'], key="upload_plan_tab1")
|
| 1281 |
|
| 1282 |
if uploaded_file is not None and not st.session_state.plan_loaded:
|
| 1283 |
try:
|
|
|
|
| 1291 |
|
| 1292 |
st.markdown("---")
|
| 1293 |
|
| 1294 |
+
# Option 2: Create New Plan - WITH DROPDOWN + CUSTOM
|
| 1295 |
st.info("π― **Option 2: Create New Plan**")
|
| 1296 |
|
| 1297 |
col1, col2 = st.columns([2, 1])
|
| 1298 |
|
| 1299 |
with col1:
|
| 1300 |
+
# Subject selection: Dropdown OR Custom input
|
| 1301 |
+
subject_options = [
|
| 1302 |
+
"Select a subject or enter custom...",
|
| 1303 |
+
"Data Science & Machine Learning",
|
| 1304 |
+
"Python Programming",
|
| 1305 |
+
"Web Development (Full Stack)",
|
| 1306 |
+
"Artificial Intelligence",
|
| 1307 |
+
"Cloud Computing (AWS/Azure/GCP)",
|
| 1308 |
+
"Cybersecurity",
|
| 1309 |
+
"Mobile App Development",
|
| 1310 |
+
"Digital Marketing",
|
| 1311 |
+
"Business Analytics",
|
| 1312 |
+
"Language Learning (Spanish/French/English)",
|
| 1313 |
+
"Project Management",
|
| 1314 |
+
"Graphic Design",
|
| 1315 |
+
"Game Development"
|
| 1316 |
+
]
|
| 1317 |
+
|
| 1318 |
+
selected_option = st.selectbox(
|
| 1319 |
+
"Choose a subject or enter custom:",
|
| 1320 |
+
options=subject_options,
|
| 1321 |
+
index=0,
|
| 1322 |
+
key="subject_select_tab1"
|
| 1323 |
)
|
| 1324 |
|
| 1325 |
+
if selected_option == "Select a subject or enter custom...":
|
| 1326 |
+
subject = st.text_input(
|
| 1327 |
+
"Or enter your own subject:",
|
| 1328 |
+
placeholder="e.g., Quantum Computing, Ethical Hacking, Music Production...",
|
| 1329 |
+
key="custom_subject_tab1"
|
| 1330 |
+
)
|
| 1331 |
+
else:
|
| 1332 |
+
subject = selected_option
|
| 1333 |
+
|
| 1334 |
col1a, col2a = st.columns(2)
|
| 1335 |
with col1a:
|
| 1336 |
hours_per_day = st.slider(
|
|
|
|
| 1338 |
min_value=1,
|
| 1339 |
max_value=8,
|
| 1340 |
value=2,
|
| 1341 |
+
key="hours_slider_tab1"
|
| 1342 |
)
|
| 1343 |
|
| 1344 |
with col2a:
|
|
|
|
| 1347 |
min_value=7,
|
| 1348 |
max_value=365,
|
| 1349 |
value=60,
|
| 1350 |
+
key="days_slider_tab1"
|
| 1351 |
)
|
| 1352 |
|
| 1353 |
study_days = st.multiselect(
|
| 1354 |
"Study days:",
|
| 1355 |
["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
| 1356 |
default=["Mon", "Tue", "Wed", "Thu", "Fri"],
|
| 1357 |
+
key="study_days_select_tab1"
|
| 1358 |
)
|
| 1359 |
|
| 1360 |
with col2:
|
|
|
|
| 1362 |
"Your level:",
|
| 1363 |
["Beginner", "Intermediate", "Advanced"],
|
| 1364 |
index=0,
|
| 1365 |
+
key="level_select_tab1"
|
| 1366 |
+
)
|
| 1367 |
+
|
| 1368 |
+
# Add study intensity
|
| 1369 |
+
intensity = st.select_slider(
|
| 1370 |
+
"Study intensity:",
|
| 1371 |
+
options=["Casual", "Moderate", "Intensive"],
|
| 1372 |
+
value="Moderate",
|
| 1373 |
+
key="intensity_slider_tab1"
|
| 1374 |
)
|
| 1375 |
|
| 1376 |
st.markdown("---")
|
| 1377 |
|
| 1378 |
# Generate Plan Button
|
| 1379 |
+
if st.button("π Generate AI Study Plan", type="primary", use_container_width=True, key="generate_plan_btn_tab1"):
|
| 1380 |
if not st.session_state.api_key:
|
| 1381 |
st.error("β οΈ **API Key Required:** Please set GEMINI_API_KEY in Hugging Face Secrets.")
|
| 1382 |
+
elif not subject or subject == "Select a subject or enter custom...":
|
| 1383 |
+
st.error("β οΈ Please enter or select a subject")
|
| 1384 |
elif not study_days:
|
| 1385 |
st.error("β οΈ Please select at least one study day")
|
| 1386 |
else:
|
|
|
|
| 1393 |
len(study_days)
|
| 1394 |
)
|
| 1395 |
|
| 1396 |
+
# Create plan data with intensity
|
| 1397 |
plan_data = {
|
| 1398 |
'subject': subject,
|
| 1399 |
'days_available': days_available,
|
| 1400 |
'hours_per_day': hours_per_day,
|
| 1401 |
'current_level': current_level,
|
| 1402 |
+
'intensity': intensity,
|
| 1403 |
'study_days': study_days
|
| 1404 |
}
|
| 1405 |
|
|
|
|
| 1459 |
st.metric("Plan Type", plan_type)
|
| 1460 |
|
| 1461 |
with st.expander("π Raw AI Response", expanded=False):
|
| 1462 |
+
st.text_area("Full Response", st.session_state.raw_response, height=300, key="raw_response_display_tab1")
|
| 1463 |
|
| 1464 |
if st.session_state.study_plan:
|
| 1465 |
with st.expander("π Parsed Plan Structure", expanded=False):
|
|
|
|
| 2245 |
st.info("π **Generate a study plan first to see analytics!**")
|
| 2246 |
|
| 2247 |
# ============================================
|
| 2248 |
+
# TAB 5: TESTS - FIXED DUPLICATE BUTTON ERROR
|
| 2249 |
# ============================================
|
| 2250 |
|
| 2251 |
with tab5:
|
|
|
|
| 2276 |
completed_week_tasks += 1
|
| 2277 |
|
| 2278 |
completion_percentage = (completed_week_tasks / week_tasks * 100) if week_tasks > 0 else 0
|
| 2279 |
+
test_available = completion_percentage >= 50
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2280 |
|
| 2281 |
+
with st.container():
|
| 2282 |
+
col1, col2, col3 = st.columns([3, 1, 1])
|
|
|
|
| 2283 |
|
| 2284 |
+
with col1:
|
| 2285 |
+
st.markdown(f"**Week {week_num}:** {week.get('focus', '')[:50]}...")
|
| 2286 |
+
st.caption(f"Completion: {completion_percentage:.0f}% β’ Tasks: {completed_week_tasks}/{week_tasks}")
|
| 2287 |
+
|
| 2288 |
+
with col2:
|
| 2289 |
+
test_key = f"week_{week_num}_test"
|
| 2290 |
+
test_taken = test_key in st.session_state.test_results
|
| 2291 |
+
|
| 2292 |
+
if test_taken:
|
| 2293 |
+
score = st.session_state.test_results[test_key]['percentage']
|
| 2294 |
+
st.metric("", f"{score:.0f}%", label_visibility="collapsed")
|
| 2295 |
+
|
| 2296 |
+
with col3:
|
| 2297 |
+
if test_available:
|
| 2298 |
+
if st.button(f"π Start Test",
|
| 2299 |
+
key=f"start_test_week_{week_num}_{subject.replace(' ', '_')}",
|
| 2300 |
+
use_container_width=True):
|
| 2301 |
+
with st.spinner(f"Generating test for Week {week_num}..."):
|
| 2302 |
+
test_data = generate_weekly_test(
|
| 2303 |
+
st.session_state.api_key,
|
| 2304 |
+
week_num,
|
| 2305 |
+
week.get('daily_tasks', []),
|
| 2306 |
+
subject
|
| 2307 |
+
)
|
| 2308 |
+
|
| 2309 |
+
if test_data and 'questions' in test_data:
|
| 2310 |
+
st.session_state.current_test = week_num
|
| 2311 |
+
st.session_state.test_questions = test_data['questions']
|
| 2312 |
+
st.session_state.test_completed = False
|
| 2313 |
+
st.session_state.user_test_answers = {}
|
| 2314 |
+
st.rerun()
|
| 2315 |
+
else:
|
| 2316 |
+
st.error("Failed to generate test. Try again.")
|
| 2317 |
+
else:
|
| 2318 |
+
# FIXED: Added unique key and disabled properly
|
| 2319 |
+
st.button(f"π Locked",
|
| 2320 |
+
key=f"locked_test_week_{week_num}_{subject.replace(' ', '_')}",
|
| 2321 |
+
disabled=True,
|
| 2322 |
+
use_container_width=True)
|
| 2323 |
+
if completion_percentage < 50:
|
| 2324 |
+
st.caption(f"Complete 50% to unlock")
|
| 2325 |
+
|
| 2326 |
+
st.markdown("---")
|
| 2327 |
|
| 2328 |
# Test Taking View
|
| 2329 |
elif st.session_state.current_test and not st.session_state.test_completed:
|
|
|
|
| 2331 |
test_questions = st.session_state.test_questions
|
| 2332 |
|
| 2333 |
st.markdown(f"### π Week {week_num} Test")
|
| 2334 |
+
st.info(f"Answer all {len(test_questions)} questions. Click on your chosen answer.")
|
| 2335 |
|
| 2336 |
for i, question in enumerate(test_questions):
|
| 2337 |
+
with st.container():
|
| 2338 |
+
st.markdown(f"**Q{i+1}. {question['question']}**")
|
| 2339 |
+
|
| 2340 |
+
current_answer = st.session_state.user_test_answers.get(str(i), "")
|
| 2341 |
+
|
| 2342 |
+
# Create 2 columns for options
|
| 2343 |
+
col_a, col_b = st.columns(2)
|
| 2344 |
+
|
| 2345 |
+
with col_a:
|
| 2346 |
+
# Option A and B
|
| 2347 |
+
for option in ['A', 'B']:
|
| 2348 |
+
option_text = question['options'][option]
|
| 2349 |
+
is_selected = (current_answer == option)
|
| 2350 |
+
|
| 2351 |
+
if st.button(
|
| 2352 |
+
f"{option}. {option_text[:80]}..." if len(option_text) > 80 else f"{option}. {option_text}",
|
| 2353 |
+
key=f"test_q{week_num}_{i}_opt{option}",
|
| 2354 |
+
use_container_width=True,
|
| 2355 |
+
type="primary" if is_selected else "secondary"
|
| 2356 |
+
):
|
| 2357 |
+
st.session_state.user_test_answers[str(i)] = option
|
| 2358 |
+
st.rerun()
|
| 2359 |
+
|
| 2360 |
+
with col_b:
|
| 2361 |
+
# Option C and D
|
| 2362 |
+
for option in ['C', 'D']:
|
| 2363 |
+
option_text = question['options'][option]
|
| 2364 |
+
is_selected = (current_answer == option)
|
| 2365 |
+
|
| 2366 |
+
if st.button(
|
| 2367 |
+
f"{option}. {option_text[:80]}..." if len(option_text) > 80 else f"{option}. {option_text}",
|
| 2368 |
+
key=f"test_q{week_num}_{i}_opt{option}",
|
| 2369 |
+
use_container_width=True,
|
| 2370 |
+
type="primary" if is_selected else "secondary"
|
| 2371 |
+
):
|
| 2372 |
+
st.session_state.user_test_answers[str(i)] = option
|
| 2373 |
+
st.rerun()
|
| 2374 |
+
|
| 2375 |
+
st.markdown("---")
|
| 2376 |
|
| 2377 |
+
# Submit button
|
| 2378 |
+
col_submit1, col_submit2, col_submit3 = st.columns([1, 2, 1])
|
| 2379 |
+
with col_submit2:
|
| 2380 |
+
if st.button("π€ Submit Test",
|
| 2381 |
+
type="primary",
|
| 2382 |
+
key=f"submit_test_week_{week_num}",
|
| 2383 |
+
use_container_width=True):
|
| 2384 |
+
st.session_state.test_completed = True
|
| 2385 |
+
st.rerun()
|
| 2386 |
|
| 2387 |
# Test Results View
|
| 2388 |
elif st.session_state.test_completed:
|
|
|
|
| 2410 |
st.success(f"## π― Test Score: {score_percentage:.1f}%")
|
| 2411 |
st.info(f"**{correct_count} out of {len(test_questions)} correct**")
|
| 2412 |
|
| 2413 |
+
# Performance feedback
|
| 2414 |
+
if score_percentage >= 80:
|
| 2415 |
+
st.balloons()
|
| 2416 |
+
st.success("**Excellent!** You have a strong understanding of this week's material.")
|
| 2417 |
+
elif score_percentage >= 60:
|
| 2418 |
+
st.success("**Good job!** You understand the main concepts.")
|
| 2419 |
+
else:
|
| 2420 |
+
st.warning("**Review needed.** Consider revisiting this week's material.")
|
| 2421 |
+
|
| 2422 |
+
with st.expander("π Review Answers", expanded=False):
|
| 2423 |
for i, question in enumerate(test_questions):
|
| 2424 |
user_answer = user_answers.get(str(i), "Not answered")
|
| 2425 |
is_correct = user_answer == question['correct_answer']
|
| 2426 |
|
| 2427 |
+
st.markdown(f"**Q{i+1}. {question['question']}**")
|
| 2428 |
+
|
| 2429 |
+
# Show user's answer with color
|
| 2430 |
+
if user_answer in question['options']:
|
| 2431 |
+
user_answer_text = f"{user_answer}. {question['options'][user_answer]}"
|
| 2432 |
+
if is_correct:
|
| 2433 |
+
st.success(f"β
**Your answer:** {user_answer_text}")
|
| 2434 |
+
else:
|
| 2435 |
+
st.error(f"β **Your answer:** {user_answer_text}")
|
| 2436 |
+
else:
|
| 2437 |
+
st.warning("βΈοΈ **Your answer:** Not answered")
|
| 2438 |
+
|
| 2439 |
+
# Show correct answer
|
| 2440 |
+
correct_answer_text = f"{question['correct_answer']}. {question['options'][question['correct_answer']]}"
|
| 2441 |
+
st.info(f"π **Correct answer:** {correct_answer_text}")
|
| 2442 |
+
|
| 2443 |
+
# Show explanation
|
| 2444 |
+
if question.get('explanation'):
|
| 2445 |
+
st.markdown(f"π‘ **Explanation:** {question['explanation']}")
|
| 2446 |
+
|
| 2447 |
st.markdown("---")
|
| 2448 |
|
| 2449 |
+
# Back button
|
| 2450 |
+
if st.button("β Back to Test List",
|
| 2451 |
+
key=f"back_from_test_{week_num}",
|
| 2452 |
+
use_container_width=True):
|
| 2453 |
st.session_state.current_test = None
|
| 2454 |
st.session_state.test_completed = False
|
| 2455 |
+
st.session_state.test_questions = []
|
| 2456 |
+
st.session_state.user_test_answers = {}
|
| 2457 |
st.rerun()
|
| 2458 |
|
| 2459 |
else:
|