Spaces:
Paused
Paused
Upload 7 files
Browse filescore AQUA THISTLE files v4
- analytics.py +413 -0
- app.py +133 -0
- components.py +700 -0
- data.py +23 -0
- llm_agent.py +377 -0
- profile_editor.py +309 -0
- styles.py +214 -0
analytics.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
analytics.py
|
| 3 |
+
Analytics helper functions for Aqua Thistle prototype
|
| 4 |
+
Handles exam/test score analysis, progress tracking, insights generation
|
| 5 |
+
Added:
|
| 6 |
+
- Scenario comparison
|
| 7 |
+
- Query parsing for what-if analysis
|
| 8 |
+
- Proactive insights generation
|
| 9 |
+
- Interactive chart support
|
| 10 |
+
"""
|
| 11 |
+
import re
|
| 12 |
+
|
| 13 |
+
def analyze_exam_performance(profile):
|
| 14 |
+
"""Analyze user's last exam performance and return insights"""
|
| 15 |
+
scores = profile.get("last_scores", {})
|
| 16 |
+
recent_exam = profile.get("recent_exam", 0)
|
| 17 |
+
goal = profile.get("goal", 1600)
|
| 18 |
+
|
| 19 |
+
total_points = sum(scores.values())
|
| 20 |
+
score_percentages = {k: round((v/total_points)*100, 1) if total_points > 0 else 0 for k, v in scores.items()}
|
| 21 |
+
|
| 22 |
+
weakest_subject = min(scores, key=scores.get) if scores else None
|
| 23 |
+
strongest_subject = max(scores, key=scores.get) if scores else None
|
| 24 |
+
|
| 25 |
+
progress_pct = round((recent_exam/goal)*100, 1) if goal > 0 else 0
|
| 26 |
+
points_needed = max(0, goal - recent_exam)
|
| 27 |
+
|
| 28 |
+
insights = []
|
| 29 |
+
if profile.get("rushing"):
|
| 30 |
+
insights.append("You're rushing questions near the end. Practice fundamentals to save time.")
|
| 31 |
+
if profile.get("math_weakness"):
|
| 32 |
+
insights.append("Math scores have most room for growth. Focus practice here.")
|
| 33 |
+
if recent_exam >= 1100:
|
| 34 |
+
insights.append(f"Reading score improved 15% over last 3 months! 🎉")
|
| 35 |
+
|
| 36 |
+
return {
|
| 37 |
+
"total_score": recent_exam,
|
| 38 |
+
"goal_score": goal,
|
| 39 |
+
"progress_pct": progress_pct,
|
| 40 |
+
"points_needed": points_needed,
|
| 41 |
+
"weakest_subject": weakest_subject,
|
| 42 |
+
"strongest_subject": strongest_subject,
|
| 43 |
+
"score_breakdown": scores,
|
| 44 |
+
"score_percentages": score_percentages,
|
| 45 |
+
"insights": insights,
|
| 46 |
+
"expected_range": (max(1150, recent_exam-50), min(1320, recent_exam+120))
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def calculate_budget_metrics(profile):
|
| 51 |
+
"""Calculate financial/budget metrics for dashboard"""
|
| 52 |
+
budget = profile.get("budget", 2000)
|
| 53 |
+
spend = profile.get("spend", 0)
|
| 54 |
+
|
| 55 |
+
remaining = budget - spend
|
| 56 |
+
spend_pct = round((spend/budget)*100, 1) if budget > 0 else 0
|
| 57 |
+
|
| 58 |
+
categories = {
|
| 59 |
+
"Savings": 1000,
|
| 60 |
+
"Food": 400,
|
| 61 |
+
"Leisure": 176
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
potential_savings = []
|
| 65 |
+
if categories["Food"] > 300:
|
| 66 |
+
potential_savings.append("Cut 15% on food costs (meal prep)")
|
| 67 |
+
if categories["Leisure"] > 150:
|
| 68 |
+
potential_savings.append("Reduce leisure spending by $50/month")
|
| 69 |
+
|
| 70 |
+
return {
|
| 71 |
+
"budget": budget,
|
| 72 |
+
"spent": spend,
|
| 73 |
+
"remaining": remaining,
|
| 74 |
+
"spend_percentage": spend_pct,
|
| 75 |
+
"categories": categories,
|
| 76 |
+
"potential_savings": potential_savings,
|
| 77 |
+
"on_track": spend_pct <= 75
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def parse_budget_query(query):
|
| 82 |
+
"""
|
| 83 |
+
Parse user queries for budget changes.
|
| 84 |
+
Returns dict with changes or None if no budget query detected.
|
| 85 |
+
|
| 86 |
+
Examples:
|
| 87 |
+
- "What if I save $200 more?" → {"savings": 200}
|
| 88 |
+
- "Show me if I spend $500 less on food" → {"food": -500}
|
| 89 |
+
- "How about $300 extra income?" → {"budget": 300}
|
| 90 |
+
"""
|
| 91 |
+
query_lower = query.lower()
|
| 92 |
+
|
| 93 |
+
# Check if it's a what-if/scenario query
|
| 94 |
+
scenario_keywords = ["what if", "show me if", "how about", "suppose", "imagine", "if i"]
|
| 95 |
+
if not any(keyword in query_lower for keyword in scenario_keywords):
|
| 96 |
+
return None
|
| 97 |
+
|
| 98 |
+
changes = {}
|
| 99 |
+
|
| 100 |
+
# Parse amounts (e.g., $200, 200 dollars, $1,000)
|
| 101 |
+
amount_patterns = [
|
| 102 |
+
r'\$(\d{1,3}(?:,\d{3})*(?:\.\d{2})?)', # $200 or $1,000.00
|
| 103 |
+
r'(\d{1,3}(?:,\d{3})*)\s*dollars?', # 200 dollars
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
amounts = []
|
| 107 |
+
for pattern in amount_patterns:
|
| 108 |
+
matches = re.findall(pattern, query_lower)
|
| 109 |
+
amounts.extend([float(m.replace(',', '')) for m in matches])
|
| 110 |
+
|
| 111 |
+
if not amounts:
|
| 112 |
+
return None
|
| 113 |
+
|
| 114 |
+
# Determine direction (increase/decrease)
|
| 115 |
+
is_increase = any(word in query_lower for word in ["more", "extra", "increase", "add", "save"])
|
| 116 |
+
is_decrease = any(word in query_lower for word in ["less", "reduce", "cut", "decrease", "lower"])
|
| 117 |
+
|
| 118 |
+
multiplier = 1 if is_increase else -1 if is_decrease else 0
|
| 119 |
+
|
| 120 |
+
# Determine category
|
| 121 |
+
if any(word in query_lower for word in ["food", "eating", "meal", "grocery"]):
|
| 122 |
+
changes["food"] = amounts[0] * multiplier
|
| 123 |
+
elif any(word in query_lower for word in ["leisure", "entertainment", "fun", "hobby"]):
|
| 124 |
+
changes["leisure"] = amounts[0] * multiplier
|
| 125 |
+
elif any(word in query_lower for word in ["save", "saving"]):
|
| 126 |
+
changes["savings"] = amounts[0] * multiplier
|
| 127 |
+
elif any(word in query_lower for word in ["budget", "income", "earn"]):
|
| 128 |
+
changes["budget"] = amounts[0] * multiplier
|
| 129 |
+
elif any(word in query_lower for word in ["spend", "spending"]):
|
| 130 |
+
changes["total_spend"] = amounts[0] * multiplier
|
| 131 |
+
else:
|
| 132 |
+
# Generic change
|
| 133 |
+
changes["general"] = amounts[0] * multiplier
|
| 134 |
+
|
| 135 |
+
return changes if changes else None
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def apply_scenario_changes(profile, changes):
|
| 139 |
+
"""
|
| 140 |
+
Apply scenario changes to profile and return modified copy.
|
| 141 |
+
Original profile is NOT modified.
|
| 142 |
+
"""
|
| 143 |
+
scenario_profile = profile.copy()
|
| 144 |
+
|
| 145 |
+
# Deep copy nested dicts
|
| 146 |
+
if "last_scores" in scenario_profile:
|
| 147 |
+
scenario_profile["last_scores"] = profile["last_scores"].copy()
|
| 148 |
+
if "goals_today" in scenario_profile:
|
| 149 |
+
scenario_profile["goals_today"] = profile["goals_today"].copy()
|
| 150 |
+
|
| 151 |
+
if "budget" in changes:
|
| 152 |
+
scenario_profile["budget"] = profile["budget"] + changes["budget"]
|
| 153 |
+
|
| 154 |
+
if "total_spend" in changes:
|
| 155 |
+
scenario_profile["spend"] = profile["spend"] + changes["total_spend"]
|
| 156 |
+
|
| 157 |
+
if "savings" in changes:
|
| 158 |
+
# More savings = less spending
|
| 159 |
+
scenario_profile["spend"] = profile["spend"] - changes["savings"]
|
| 160 |
+
|
| 161 |
+
if "food" in changes:
|
| 162 |
+
scenario_profile["spend"] = profile["spend"] + changes["food"]
|
| 163 |
+
|
| 164 |
+
if "leisure" in changes:
|
| 165 |
+
scenario_profile["spend"] = profile["spend"] + changes["leisure"]
|
| 166 |
+
|
| 167 |
+
# Ensure spend doesn't go negative
|
| 168 |
+
scenario_profile["spend"] = max(0, scenario_profile["spend"])
|
| 169 |
+
|
| 170 |
+
return scenario_profile
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def compare_scenarios(current_profile, scenario_profile):
|
| 174 |
+
"""
|
| 175 |
+
Compare current vs scenario profiles.
|
| 176 |
+
Returns dict with differences and insights.
|
| 177 |
+
"""
|
| 178 |
+
current_metrics = calculate_budget_metrics(current_profile)
|
| 179 |
+
scenario_metrics = calculate_budget_metrics(scenario_profile)
|
| 180 |
+
|
| 181 |
+
diff_spend = scenario_profile["spend"] - current_profile["spend"]
|
| 182 |
+
diff_budget = scenario_profile["budget"] - current_profile["budget"]
|
| 183 |
+
diff_remaining = scenario_metrics["remaining"] - current_metrics["remaining"]
|
| 184 |
+
|
| 185 |
+
insights = []
|
| 186 |
+
|
| 187 |
+
if diff_spend < 0:
|
| 188 |
+
insights.append(f"✅ You'd save {abs(diff_spend):.0f} dollars monthly")
|
| 189 |
+
elif diff_spend > 0:
|
| 190 |
+
insights.append(f"⚠️ Spending would increase by {diff_spend:.0f} dollars")
|
| 191 |
+
|
| 192 |
+
if diff_remaining > 0:
|
| 193 |
+
insights.append(f"💰 {diff_remaining:.0f} dollars more remaining at month end")
|
| 194 |
+
elif diff_remaining < 0:
|
| 195 |
+
insights.append(f"📉 {abs(diff_remaining):.0f} dollars less remaining")
|
| 196 |
+
|
| 197 |
+
if scenario_metrics["on_track"] and not current_metrics["on_track"]:
|
| 198 |
+
insights.append("🎯 This would put you back on track!")
|
| 199 |
+
elif not scenario_metrics["on_track"] and current_metrics["on_track"]:
|
| 200 |
+
insights.append("⚠️ This would put you over budget")
|
| 201 |
+
|
| 202 |
+
return {
|
| 203 |
+
"current": current_metrics,
|
| 204 |
+
"scenario": scenario_metrics,
|
| 205 |
+
"differences": {
|
| 206 |
+
"spend": diff_spend,
|
| 207 |
+
"budget": diff_budget,
|
| 208 |
+
"remaining": diff_remaining
|
| 209 |
+
},
|
| 210 |
+
"insights": insights
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def generate_proactive_insights(profile, category):
|
| 215 |
+
"""
|
| 216 |
+
Generate proactive insights based on profile analysis.
|
| 217 |
+
These appear automatically, not just in response to queries.
|
| 218 |
+
"""
|
| 219 |
+
insights = []
|
| 220 |
+
|
| 221 |
+
if category == "Finance":
|
| 222 |
+
budget_metrics = calculate_budget_metrics(profile)
|
| 223 |
+
spend_pct = budget_metrics["spend_percentage"]
|
| 224 |
+
|
| 225 |
+
# Critical threshold
|
| 226 |
+
if spend_pct > 90:
|
| 227 |
+
insights.append({
|
| 228 |
+
"type": "critical",
|
| 229 |
+
"icon": "🚨",
|
| 230 |
+
"text": f"You've spent {spend_pct}% of budget - urgent action needed",
|
| 231 |
+
"action": "What can I cut immediately?"
|
| 232 |
+
})
|
| 233 |
+
# Warning threshold
|
| 234 |
+
elif spend_pct > 75:
|
| 235 |
+
insights.append({
|
| 236 |
+
"type": "warning",
|
| 237 |
+
"icon": "⚠️",
|
| 238 |
+
"text": f"At {spend_pct}% of budget - pace yourself",
|
| 239 |
+
"action": "Show me where I can save"
|
| 240 |
+
})
|
| 241 |
+
# Good standing
|
| 242 |
+
elif spend_pct < 60:
|
| 243 |
+
insights.append({
|
| 244 |
+
"type": "success",
|
| 245 |
+
"icon": "✅",
|
| 246 |
+
"text": f"Great job! Only {spend_pct}% spent - you're ahead",
|
| 247 |
+
"action": "How much could I save this month?"
|
| 248 |
+
})
|
| 249 |
+
|
| 250 |
+
# Savings opportunity
|
| 251 |
+
remaining = budget_metrics["remaining"]
|
| 252 |
+
if remaining > 500:
|
| 253 |
+
insights.append({
|
| 254 |
+
"type": "opportunity",
|
| 255 |
+
"icon": "💡",
|
| 256 |
+
"text": f"You have {remaining:.0f} dollars left - invest in savings?",
|
| 257 |
+
"action": "Best high-yield accounts?"
|
| 258 |
+
})
|
| 259 |
+
|
| 260 |
+
elif category == "Education":
|
| 261 |
+
exam_data = analyze_exam_performance(profile)
|
| 262 |
+
progress = exam_data["progress_pct"]
|
| 263 |
+
|
| 264 |
+
if progress < 70:
|
| 265 |
+
insights.append({
|
| 266 |
+
"type": "warning",
|
| 267 |
+
"icon": "📚",
|
| 268 |
+
"text": f"At {progress}% of goal - need {exam_data['points_needed']} more points",
|
| 269 |
+
"action": "How can I improve fastest?"
|
| 270 |
+
})
|
| 271 |
+
elif progress >= 90:
|
| 272 |
+
insights.append({
|
| 273 |
+
"type": "success",
|
| 274 |
+
"icon": "🎯",
|
| 275 |
+
"text": f"Almost there! {progress}% to goal",
|
| 276 |
+
"action": "Final push strategies?"
|
| 277 |
+
})
|
| 278 |
+
|
| 279 |
+
if exam_data["weakest_subject"]:
|
| 280 |
+
weak_score = exam_data["score_breakdown"][exam_data["weakest_subject"]]
|
| 281 |
+
if weak_score < 120:
|
| 282 |
+
insights.append({
|
| 283 |
+
"type": "opportunity",
|
| 284 |
+
"icon": "💪",
|
| 285 |
+
"text": f"{exam_data['weakest_subject']} is weakest - most room for growth",
|
| 286 |
+
"action": f"Practice {exam_data['weakest_subject']} questions"
|
| 287 |
+
})
|
| 288 |
+
|
| 289 |
+
return insights
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def get_daily_goals_summary(profile):
|
| 293 |
+
"""Summarize daily goal progress"""
|
| 294 |
+
goals = profile.get("goals_today", {})
|
| 295 |
+
|
| 296 |
+
completed = sum(1 for v in goals.values() if v >= 100)
|
| 297 |
+
in_progress = sum(1 for v in goals.values() if 50 <= v < 100)
|
| 298 |
+
needs_attention = sum(1 for v in goals.values() if v < 50)
|
| 299 |
+
|
| 300 |
+
avg_completion = round(sum(goals.values()) / len(goals), 1) if goals else 0
|
| 301 |
+
|
| 302 |
+
return {
|
| 303 |
+
"total_goals": len(goals),
|
| 304 |
+
"completed": completed,
|
| 305 |
+
"in_progress": in_progress,
|
| 306 |
+
"needs_attention": needs_attention,
|
| 307 |
+
"avg_completion": avg_completion,
|
| 308 |
+
"goals": goals
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def generate_practice_questions(subject, difficulty="medium"):
|
| 313 |
+
"""Generate practice questions based on weak subject areas"""
|
| 314 |
+
question_bank = {
|
| 315 |
+
"Reading": [
|
| 316 |
+
"Read passage about climate change and identify main argument",
|
| 317 |
+
"Analyze author's tone in historical narrative",
|
| 318 |
+
"Compare two texts on same topic, find common themes"
|
| 319 |
+
],
|
| 320 |
+
"Writing": [
|
| 321 |
+
"Fix grammar errors in paragraph about economics",
|
| 322 |
+
"Improve sentence structure for clarity",
|
| 323 |
+
"Choose best transition word for essay flow"
|
| 324 |
+
],
|
| 325 |
+
"Reasoning": [
|
| 326 |
+
"Solve logic puzzle with 3 variables",
|
| 327 |
+
"Identify pattern in number sequence",
|
| 328 |
+
"Draw conclusion from given premises"
|
| 329 |
+
],
|
| 330 |
+
"Algebra": [
|
| 331 |
+
"Solve quadratic equation: x² + 5x - 14 = 0",
|
| 332 |
+
"Find slope of line through points (2,3) and (5,9)",
|
| 333 |
+
"Simplify expression: (3x² - 2x + 1) - (x² + 4x - 3)"
|
| 334 |
+
],
|
| 335 |
+
"Geometry": [
|
| 336 |
+
"Calculate area of triangle given base=8, height=6",
|
| 337 |
+
"Find angle measure in complementary angle pair",
|
| 338 |
+
"Determine volume of cylinder with r=3, h=10"
|
| 339 |
+
]
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
return question_bank.get(subject, ["No questions available for this subject"])
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def get_time_improvement_suggestions(profile):
|
| 346 |
+
"""Generate time management suggestions based on performance"""
|
| 347 |
+
suggestions = []
|
| 348 |
+
|
| 349 |
+
if profile.get("rushing"):
|
| 350 |
+
suggestions.append("Practice timed sections: 15 min sprints")
|
| 351 |
+
suggestions.append("Skip hard questions first, return later")
|
| 352 |
+
suggestions.append("Use process of elimination to save time")
|
| 353 |
+
|
| 354 |
+
if profile.get("math_weakness"):
|
| 355 |
+
suggestions.append("Memorize common formulas before test")
|
| 356 |
+
suggestions.append("Do 10 quick math drills daily")
|
| 357 |
+
|
| 358 |
+
suggestions.append("Take full-length practice test this weekend")
|
| 359 |
+
suggestions.append("Review mistakes immediately after practice")
|
| 360 |
+
|
| 361 |
+
return suggestions
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
def calculate_projected_improvement(current_score, weeks_until_test, study_hours_per_week):
|
| 365 |
+
"""Project score improvement based on study plan"""
|
| 366 |
+
total_study_hours = weeks_until_test * study_hours_per_week
|
| 367 |
+
projected_gain = (total_study_hours / 10) * 10
|
| 368 |
+
|
| 369 |
+
if current_score > 1400:
|
| 370 |
+
projected_gain *= 0.7
|
| 371 |
+
elif current_score > 1200:
|
| 372 |
+
projected_gain *= 0.85
|
| 373 |
+
|
| 374 |
+
projected_score = min(1600, current_score + int(projected_gain))
|
| 375 |
+
|
| 376 |
+
return {
|
| 377 |
+
"current": current_score,
|
| 378 |
+
"projected": projected_score,
|
| 379 |
+
"gain": projected_score - current_score,
|
| 380 |
+
"study_hours_needed": total_study_hours,
|
| 381 |
+
"confidence": "high" if study_hours_per_week >= 10 else "medium"
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def get_category_insights(category, profile):
|
| 386 |
+
"""Generate category-specific insights"""
|
| 387 |
+
insights = []
|
| 388 |
+
|
| 389 |
+
if category == "Finance":
|
| 390 |
+
budget_data = calculate_budget_metrics(profile)
|
| 391 |
+
if not budget_data["on_track"]:
|
| 392 |
+
insights.append("⚠️ Spending at {}%, consider budget review".format(budget_data["spend_percentage"]))
|
| 393 |
+
insights.extend(budget_data["potential_savings"])
|
| 394 |
+
|
| 395 |
+
elif category == "Education":
|
| 396 |
+
exam_data = analyze_exam_performance(profile)
|
| 397 |
+
insights.extend(exam_data["insights"])
|
| 398 |
+
if exam_data["points_needed"] > 0:
|
| 399 |
+
insights.append(f"Need {exam_data['points_needed']} more points to reach goal")
|
| 400 |
+
|
| 401 |
+
elif category == "Family":
|
| 402 |
+
insights.append("Schedule weekly check-in calls")
|
| 403 |
+
insights.append("Send updates about school progress")
|
| 404 |
+
|
| 405 |
+
elif category == "Friends":
|
| 406 |
+
insights.append("Plan low-cost hangouts to stay connected")
|
| 407 |
+
insights.append("Join study groups for accountability")
|
| 408 |
+
|
| 409 |
+
elif category == "Weekend/Vacation":
|
| 410 |
+
insights.append("Research free local events")
|
| 411 |
+
insights.append("Budget $150-200 for weekend activities")
|
| 412 |
+
|
| 413 |
+
return insights
|
app.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
app.py - COMPLETE WORKING VERSION
|
| 3 |
+
No duplicate headers, clean structure
|
| 4 |
+
app.py - A-GRADE VERSION
|
| 5 |
+
Added scenario state management for interactive visualizations
|
| 6 |
+
PRODUCTION READY VERSION:
|
| 7 |
+
1. Fixed: Suggestions NEVER recycle (even after 7+ messages)
|
| 8 |
+
2. Fixed: Proper state tracking with message history analysis
|
| 9 |
+
3. Fixed: Detects NEW topics vs continuation
|
| 10 |
+
4. Added: Gap reduction CSS class for .st-emotion-cache-wfksow
|
| 11 |
+
5. Fixed: All edge cases (bitcoin, options trading, etc)
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import streamlit as st
|
| 15 |
+
from llm_agent import aqua_response, get_default_profile
|
| 16 |
+
from components import render_left_panel, render_chat_panel, check_query_for_scenario
|
| 17 |
+
from styles import get_custom_css
|
| 18 |
+
|
| 19 |
+
st.set_page_config(layout="wide", page_title="Aqua Thistle", page_icon="🌊")
|
| 20 |
+
|
| 21 |
+
st.markdown(get_custom_css(), unsafe_allow_html=True)
|
| 22 |
+
|
| 23 |
+
# Main header
|
| 24 |
+
st.markdown("""
|
| 25 |
+
<div style='text-align: center; padding: 1rem 0 1.5rem 0;'>
|
| 26 |
+
<h1 style='font-size: 2.5rem; margin: 0 0 0.5rem 0;'>🌊 Aqua Thistle</h1>
|
| 27 |
+
<p style='color: #4a4a4a; font-size: 1rem; margin: 0;'>Your Personal GenZ Mentor for Life, Finance & Education</p>
|
| 28 |
+
</div>
|
| 29 |
+
""", unsafe_allow_html=True)
|
| 30 |
+
|
| 31 |
+
# Initialize session state
|
| 32 |
+
if "profile" not in st.session_state:
|
| 33 |
+
st.session_state.profile = get_default_profile()
|
| 34 |
+
|
| 35 |
+
if "seed_chat" not in st.session_state:
|
| 36 |
+
st.session_state.seed_chat = None
|
| 37 |
+
|
| 38 |
+
if "current_category" not in st.session_state:
|
| 39 |
+
st.session_state.current_category = "Finance"
|
| 40 |
+
|
| 41 |
+
# Scenario state for interactive visualizations
|
| 42 |
+
if "scenario_mode" not in st.session_state:
|
| 43 |
+
st.session_state.scenario_mode = False
|
| 44 |
+
|
| 45 |
+
if "scenario_profile" not in st.session_state:
|
| 46 |
+
st.session_state.scenario_profile = None
|
| 47 |
+
|
| 48 |
+
if "whatif_mode" not in st.session_state:
|
| 49 |
+
st.session_state.whatif_mode = False
|
| 50 |
+
|
| 51 |
+
# Initialize chat histories for each category
|
| 52 |
+
categories_list = ["Finance", "Education", "Family", "Friends", "Weekend/Vacation"]
|
| 53 |
+
for cat in categories_list:
|
| 54 |
+
if f"chat_{cat}" not in st.session_state:
|
| 55 |
+
st.session_state[f"chat_{cat}"] = []
|
| 56 |
+
|
| 57 |
+
# Category icons
|
| 58 |
+
category_icons = {
|
| 59 |
+
"Finance": "💰",
|
| 60 |
+
"Education": "📚",
|
| 61 |
+
"Family": "👨👩👧",
|
| 62 |
+
"Friends": "👥",
|
| 63 |
+
"Weekend/Vacation": "🏖️"
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
# Category selector
|
| 67 |
+
st.markdown("### 🎯 Focus Area")
|
| 68 |
+
selected = st.selectbox(
|
| 69 |
+
"cat",
|
| 70 |
+
categories_list,
|
| 71 |
+
index=categories_list.index(st.session_state.current_category),
|
| 72 |
+
format_func=lambda x: f"{category_icons[x]} {x}",
|
| 73 |
+
key="cat_select",
|
| 74 |
+
label_visibility="collapsed"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
if selected != st.session_state.current_category:
|
| 78 |
+
st.session_state.current_category = selected
|
| 79 |
+
# Reset scenario mode when switching categories
|
| 80 |
+
st.session_state.scenario_mode = False
|
| 81 |
+
st.session_state.whatif_mode = False
|
| 82 |
+
st.session_state.scenario_profile = None
|
| 83 |
+
st.rerun()
|
| 84 |
+
|
| 85 |
+
category = st.session_state.current_category
|
| 86 |
+
|
| 87 |
+
st.markdown("---")
|
| 88 |
+
|
| 89 |
+
# Two-column layout
|
| 90 |
+
left_col, right_col = st.columns([2, 3])
|
| 91 |
+
|
| 92 |
+
with left_col:
|
| 93 |
+
render_left_panel(st.session_state, category)
|
| 94 |
+
|
| 95 |
+
with right_col:
|
| 96 |
+
chat_key = f"chat_{category}"
|
| 97 |
+
|
| 98 |
+
# Render chat panel (includes header, reset, messages, quick actions)
|
| 99 |
+
render_chat_panel(chat_key, category)
|
| 100 |
+
|
| 101 |
+
# Chat input
|
| 102 |
+
prompt = st.chat_input(f"Ask about {category.lower()}...")
|
| 103 |
+
|
| 104 |
+
# Handle seed chat (from Quick Actions or Ask buttons)
|
| 105 |
+
if st.session_state.seed_chat:
|
| 106 |
+
prompt = st.session_state.seed_chat
|
| 107 |
+
st.session_state.seed_chat = None
|
| 108 |
+
|
| 109 |
+
# Process user message
|
| 110 |
+
if prompt:
|
| 111 |
+
# Check if query contains a scenario (what-if question)
|
| 112 |
+
scenario_profile = check_query_for_scenario(prompt, st.session_state.profile)
|
| 113 |
+
|
| 114 |
+
if scenario_profile:
|
| 115 |
+
# Enter scenario mode
|
| 116 |
+
st.session_state.scenario_mode = True
|
| 117 |
+
st.session_state.scenario_profile = scenario_profile
|
| 118 |
+
|
| 119 |
+
# Add to chat history
|
| 120 |
+
st.session_state[chat_key].append({"role": "user", "content": prompt})
|
| 121 |
+
|
| 122 |
+
with st.spinner("🌊 Thinking..."):
|
| 123 |
+
ai_response = aqua_response(prompt, st.session_state.profile, category)
|
| 124 |
+
st.session_state[chat_key].append({"role": "assistant", "content": ai_response})
|
| 125 |
+
|
| 126 |
+
st.rerun()
|
| 127 |
+
|
| 128 |
+
# Footer
|
| 129 |
+
st.markdown("---")
|
| 130 |
+
st.markdown("<div style='text-align: center; color: #4a4a4a; font-size: 0.875rem;'><p>Built with 💙 for GenZ</p></div>", unsafe_allow_html=True)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
|
components.py
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
components.py - A-GRADE VERSION
|
| 3 |
+
COMPLETE AND COHERENT with analytics.py, app.py, llm_agent_A_GRADE.py
|
| 4 |
+
|
| 5 |
+
NEW FEATURES:
|
| 6 |
+
1. Proactive AI insights display (auto-generated at top)
|
| 7 |
+
2. Scenario comparison mode (side-by-side current vs scenario)
|
| 8 |
+
3. What-if mode (interactive sliders for testing)
|
| 9 |
+
4. Query detection for chart updates
|
| 10 |
+
5. All existing features preserved
|
| 11 |
+
|
| 12 |
+
PRESERVED FEATURES:
|
| 13 |
+
- Dynamic suggestions (never recycle)
|
| 14 |
+
- Topic tracking across conversation
|
| 15 |
+
- Category-specific visuals
|
| 16 |
+
- Profile editing integration
|
| 17 |
+
- All existing chart types
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import streamlit as st
|
| 21 |
+
import plotly.graph_objects as go
|
| 22 |
+
from analytics import (
|
| 23 |
+
analyze_exam_performance,
|
| 24 |
+
get_category_insights,
|
| 25 |
+
calculate_budget_metrics,
|
| 26 |
+
parse_budget_query,
|
| 27 |
+
apply_scenario_changes,
|
| 28 |
+
compare_scenarios,
|
| 29 |
+
generate_proactive_insights
|
| 30 |
+
)
|
| 31 |
+
from styles import get_custom_css, get_metric_card_html, get_insight_html
|
| 32 |
+
from profile_editor import render_profile_editor
|
| 33 |
+
|
| 34 |
+
def set_seed_chat(prompt_text):
|
| 35 |
+
st.session_state.seed_chat = prompt_text
|
| 36 |
+
|
| 37 |
+
def create_animated_donut_chart(values, labels, colors, title=""):
|
| 38 |
+
fig = go.Figure(data=[go.Pie(
|
| 39 |
+
labels=labels, values=values, hole=0.6,
|
| 40 |
+
marker=dict(colors=colors, line=dict(color='white', width=2)),
|
| 41 |
+
textinfo='label+percent', textposition='outside'
|
| 42 |
+
)])
|
| 43 |
+
|
| 44 |
+
fig.update_layout(
|
| 45 |
+
title=dict(text=title, font=dict(size=16, color='#1a1a1a', family='Inter')),
|
| 46 |
+
showlegend=False, height=300,
|
| 47 |
+
margin=dict(t=40, b=20, l=20, r=20),
|
| 48 |
+
paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)'
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
return fig
|
| 52 |
+
|
| 53 |
+
def create_gauge_chart(value, max_value, title="Progress"):
|
| 54 |
+
fig = go.Figure(go.Indicator(
|
| 55 |
+
mode="gauge+number", value=value,
|
| 56 |
+
title={'text': title, 'font': {'size': 14}},
|
| 57 |
+
number={'font': {'size': 24}},
|
| 58 |
+
gauge={
|
| 59 |
+
'axis': {'range': [None, max_value]},
|
| 60 |
+
'bar': {'color': "#bf6dbf"},
|
| 61 |
+
'steps': [
|
| 62 |
+
{'range': [0, max_value * 0.5], 'color': '#fee2e2'},
|
| 63 |
+
{'range': [max_value * 0.5, max_value * 0.75], 'color': '#fef3c7'},
|
| 64 |
+
{'range': [max_value * 0.75, max_value], 'color': '#d1fae5'}
|
| 65 |
+
]
|
| 66 |
+
}
|
| 67 |
+
))
|
| 68 |
+
|
| 69 |
+
fig.update_layout(
|
| 70 |
+
height=250,
|
| 71 |
+
margin=dict(t=60, b=20, l=20, r=20),
|
| 72 |
+
paper_bgcolor='rgba(0,0,0,0)'
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
return fig
|
| 76 |
+
|
| 77 |
+
# ===== NEW: QUERY DETECTION FOR SCENARIOS =====
|
| 78 |
+
def check_query_for_scenario(query, profile):
|
| 79 |
+
"""
|
| 80 |
+
Check if user query is asking a what-if question.
|
| 81 |
+
Returns scenario_profile if detected, None otherwise.
|
| 82 |
+
"""
|
| 83 |
+
changes = parse_budget_query(query)
|
| 84 |
+
|
| 85 |
+
if changes:
|
| 86 |
+
scenario_profile = apply_scenario_changes(profile, changes)
|
| 87 |
+
return scenario_profile
|
| 88 |
+
|
| 89 |
+
return None
|
| 90 |
+
|
| 91 |
+
# ===== NEW: PROACTIVE INSIGHTS RENDERING =====
|
| 92 |
+
def render_proactive_insights(profile, category):
|
| 93 |
+
"""
|
| 94 |
+
Render auto-generated proactive insights at top of dashboard.
|
| 95 |
+
These appear automatically based on user's data.
|
| 96 |
+
"""
|
| 97 |
+
insights = generate_proactive_insights(profile, category)
|
| 98 |
+
|
| 99 |
+
if not insights:
|
| 100 |
+
return
|
| 101 |
+
|
| 102 |
+
st.markdown("### 🎯 AI Insights for You")
|
| 103 |
+
|
| 104 |
+
# Show top 2 most relevant insights
|
| 105 |
+
for insight in insights[:2]:
|
| 106 |
+
insight_type = insight.get("type", "insight")
|
| 107 |
+
icon = insight.get("icon", "💡")
|
| 108 |
+
text = insight.get("text", "")
|
| 109 |
+
action = insight.get("action", "")
|
| 110 |
+
|
| 111 |
+
# Color coding based on insight type
|
| 112 |
+
if insight_type == "critical":
|
| 113 |
+
bg_color = "#fee2e2"
|
| 114 |
+
border_color = "#ef4444"
|
| 115 |
+
elif insight_type == "warning":
|
| 116 |
+
bg_color = "#fef3c7"
|
| 117 |
+
border_color = "#f59e0b"
|
| 118 |
+
elif insight_type == "success":
|
| 119 |
+
bg_color = "#d1fae5"
|
| 120 |
+
border_color = "#10b981"
|
| 121 |
+
else: # opportunity
|
| 122 |
+
bg_color = "#e0e7ff"
|
| 123 |
+
border_color = "#6366f1"
|
| 124 |
+
|
| 125 |
+
st.markdown(f"""
|
| 126 |
+
<div style='background: {bg_color}; border-left: 4px solid {border_color};
|
| 127 |
+
padding: 1rem; margin-bottom: 0.75rem; border-radius: 0.5rem;'>
|
| 128 |
+
<div style='display: flex; align-items: flex-start; gap: 0.75rem;'>
|
| 129 |
+
<span style='font-size: 1.5rem;'>{icon}</span>
|
| 130 |
+
<div style='flex: 1;'>
|
| 131 |
+
<p style='margin: 0; font-size: 0.95rem; color: #1f2937; font-weight: 500;'>{text}</p>
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
""", unsafe_allow_html=True)
|
| 136 |
+
|
| 137 |
+
if action:
|
| 138 |
+
if st.button(f"💬 {action}", key=f"insight_action_{hash(action)}", use_container_width=True):
|
| 139 |
+
set_seed_chat(action)
|
| 140 |
+
|
| 141 |
+
st.markdown("---")
|
| 142 |
+
|
| 143 |
+
# ===== NEW: SCENARIO COMPARISON RENDERING =====
|
| 144 |
+
def render_scenario_comparison(current_profile, scenario_profile, category):
|
| 145 |
+
"""
|
| 146 |
+
Render side-by-side comparison of current vs scenario.
|
| 147 |
+
Shows visual diff in charts.
|
| 148 |
+
"""
|
| 149 |
+
st.markdown("### 📊 Scenario Comparison")
|
| 150 |
+
|
| 151 |
+
if category == "Finance":
|
| 152 |
+
current_data = calculate_budget_metrics(current_profile)
|
| 153 |
+
scenario_data = calculate_budget_metrics(scenario_profile)
|
| 154 |
+
comparison = compare_scenarios(current_profile, scenario_profile)
|
| 155 |
+
|
| 156 |
+
col1, col2 = st.columns(2)
|
| 157 |
+
|
| 158 |
+
with col1:
|
| 159 |
+
st.markdown("#### Current Budget")
|
| 160 |
+
st.metric("Spent", f"${current_profile['spend']:,}")
|
| 161 |
+
st.metric("Remaining", f"${current_data['remaining']:,}")
|
| 162 |
+
|
| 163 |
+
# Current chart
|
| 164 |
+
fig_current = create_animated_donut_chart(
|
| 165 |
+
list(current_data["categories"].values()),
|
| 166 |
+
list(current_data["categories"].keys()),
|
| 167 |
+
['#10b981', '#f59e0b', '#ef4444']
|
| 168 |
+
)
|
| 169 |
+
st.plotly_chart(fig_current, use_container_width=True, key="scenario_current_chart")
|
| 170 |
+
|
| 171 |
+
with col2:
|
| 172 |
+
st.markdown("#### Scenario Budget")
|
| 173 |
+
st.metric("Spent", f"${scenario_profile['spend']:,}",
|
| 174 |
+
delta=f"{comparison['differences']['spend']:+,.0f}")
|
| 175 |
+
st.metric("Remaining", f"${scenario_data['remaining']:,}",
|
| 176 |
+
delta=f"{comparison['differences']['remaining']:+,.0f}")
|
| 177 |
+
|
| 178 |
+
# Scenario chart
|
| 179 |
+
fig_scenario = create_animated_donut_chart(
|
| 180 |
+
list(scenario_data["categories"].values()),
|
| 181 |
+
list(scenario_data["categories"].keys()),
|
| 182 |
+
['#10b981', '#f59e0b', '#ef4444']
|
| 183 |
+
)
|
| 184 |
+
st.plotly_chart(fig_scenario, use_container_width=True, key="scenario_new_chart")
|
| 185 |
+
|
| 186 |
+
# Show insights
|
| 187 |
+
st.markdown("#### 💡 What This Means")
|
| 188 |
+
for insight in comparison['insights']:
|
| 189 |
+
st.info(insight)
|
| 190 |
+
|
| 191 |
+
elif category == "Education":
|
| 192 |
+
current_exam = analyze_exam_performance(current_profile)
|
| 193 |
+
scenario_exam = analyze_exam_performance(scenario_profile)
|
| 194 |
+
|
| 195 |
+
col1, col2 = st.columns(2)
|
| 196 |
+
|
| 197 |
+
with col1:
|
| 198 |
+
st.markdown("#### Current Performance")
|
| 199 |
+
st.metric("Score", current_profile['recent_exam'])
|
| 200 |
+
st.metric("To Goal", f"{current_exam['points_needed']} pts")
|
| 201 |
+
|
| 202 |
+
with col2:
|
| 203 |
+
st.markdown("#### Scenario Performance")
|
| 204 |
+
st.metric("Score", scenario_profile['recent_exam'],
|
| 205 |
+
delta=f"{scenario_profile['recent_exam'] - current_profile['recent_exam']:+}")
|
| 206 |
+
st.metric("To Goal", f"{scenario_exam['points_needed']} pts",
|
| 207 |
+
delta=f"{scenario_exam['points_needed'] - current_exam['points_needed']:+}")
|
| 208 |
+
|
| 209 |
+
# ===== NEW: WHAT-IF MODE =====
|
| 210 |
+
def render_whatif_mode(profile, category):
|
| 211 |
+
"""
|
| 212 |
+
Interactive what-if mode with sliders.
|
| 213 |
+
User can adjust values and see instant impact.
|
| 214 |
+
"""
|
| 215 |
+
st.markdown("### 🔮 What-If Mode")
|
| 216 |
+
st.markdown("*Adjust values to see instant impact*")
|
| 217 |
+
|
| 218 |
+
if category == "Finance":
|
| 219 |
+
col1, col2 = st.columns(2)
|
| 220 |
+
|
| 221 |
+
with col1:
|
| 222 |
+
new_spend = st.slider(
|
| 223 |
+
"Monthly Spending",
|
| 224 |
+
0, 5000,
|
| 225 |
+
profile['spend'],
|
| 226 |
+
50,
|
| 227 |
+
key="whatif_spend"
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
with col2:
|
| 231 |
+
new_budget = st.slider(
|
| 232 |
+
"Monthly Budget",
|
| 233 |
+
1000, 10000,
|
| 234 |
+
profile['budget'],
|
| 235 |
+
100,
|
| 236 |
+
key="whatif_budget"
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
# Create scenario
|
| 240 |
+
scenario = profile.copy()
|
| 241 |
+
scenario['spend'] = new_spend
|
| 242 |
+
scenario['budget'] = new_budget
|
| 243 |
+
|
| 244 |
+
# Show impact
|
| 245 |
+
st.markdown("#### Impact Analysis")
|
| 246 |
+
|
| 247 |
+
col1, col2, col3 = st.columns(3)
|
| 248 |
+
|
| 249 |
+
current_remaining = profile['budget'] - profile['spend']
|
| 250 |
+
scenario_remaining = new_budget - new_spend
|
| 251 |
+
|
| 252 |
+
with col1:
|
| 253 |
+
st.metric("Current Remaining", f"${current_remaining:,}")
|
| 254 |
+
|
| 255 |
+
with col2:
|
| 256 |
+
st.metric("Scenario Remaining", f"${scenario_remaining:,}",
|
| 257 |
+
delta=f"{scenario_remaining - current_remaining:+,}")
|
| 258 |
+
|
| 259 |
+
with col3:
|
| 260 |
+
spend_pct = round((new_spend / new_budget) * 100, 1)
|
| 261 |
+
status = "✅ On Track" if spend_pct <= 75 else "⚠️ Over Target"
|
| 262 |
+
st.metric("Status", status)
|
| 263 |
+
|
| 264 |
+
# Visual comparison
|
| 265 |
+
render_scenario_comparison(profile, scenario, category)
|
| 266 |
+
|
| 267 |
+
elif category == "Education":
|
| 268 |
+
col1, col2 = st.columns(2)
|
| 269 |
+
|
| 270 |
+
with col1:
|
| 271 |
+
new_score = st.slider(
|
| 272 |
+
"New Exam Score",
|
| 273 |
+
400, 1600,
|
| 274 |
+
profile['recent_exam'],
|
| 275 |
+
10,
|
| 276 |
+
key="whatif_score"
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
with col2:
|
| 280 |
+
study_hours = st.slider(
|
| 281 |
+
"Study Hours/Week",
|
| 282 |
+
0, 40,
|
| 283 |
+
10,
|
| 284 |
+
1,
|
| 285 |
+
key="whatif_study"
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
# Create scenario
|
| 289 |
+
scenario = profile.copy()
|
| 290 |
+
scenario['recent_exam'] = new_score
|
| 291 |
+
|
| 292 |
+
# Show impact
|
| 293 |
+
st.markdown("#### Impact Analysis")
|
| 294 |
+
|
| 295 |
+
col1, col2, col3 = st.columns(3)
|
| 296 |
+
|
| 297 |
+
current_to_goal = profile['goal'] - profile['recent_exam']
|
| 298 |
+
scenario_to_goal = profile['goal'] - new_score
|
| 299 |
+
|
| 300 |
+
with col1:
|
| 301 |
+
st.metric("Current Score", profile['recent_exam'])
|
| 302 |
+
|
| 303 |
+
with col2:
|
| 304 |
+
st.metric("Scenario Score", new_score,
|
| 305 |
+
delta=f"{new_score - profile['recent_exam']:+}")
|
| 306 |
+
|
| 307 |
+
with col3:
|
| 308 |
+
st.metric("Points to Goal", scenario_to_goal,
|
| 309 |
+
delta=f"{scenario_to_goal - current_to_goal:+}")
|
| 310 |
+
|
| 311 |
+
# ===== EXISTING: DASHBOARD SUMMARY =====
|
| 312 |
+
def render_dashboard_summary(profile, category):
|
| 313 |
+
st.markdown("### 📊 Your Dashboard")
|
| 314 |
+
render_profile_editor(profile, category)
|
| 315 |
+
|
| 316 |
+
col1, col2, col3 = st.columns(3)
|
| 317 |
+
|
| 318 |
+
if category == "Finance":
|
| 319 |
+
with col1:
|
| 320 |
+
spend_pct = round((profile['spend'] / profile['budget']) * 100, 1)
|
| 321 |
+
delta = f"🔴 {spend_pct}% of ${profile['budget']:,}" if spend_pct > 75 else f"🟢 {spend_pct}% of ${profile['budget']:,}"
|
| 322 |
+
st.markdown(get_metric_card_html("This Month's Budget Used", f"${profile['spend']:,}", delta, "💰"), unsafe_allow_html=True)
|
| 323 |
+
|
| 324 |
+
with col2:
|
| 325 |
+
remaining = profile['budget'] - profile['spend']
|
| 326 |
+
st.markdown(get_metric_card_html("Remaining Budget", f"${remaining:,}", f"💡 {round((remaining/profile['budget'])*100, 1)}% left", "💳"), unsafe_allow_html=True)
|
| 327 |
+
|
| 328 |
+
with col3:
|
| 329 |
+
st.markdown(get_metric_card_html("Savings Goal", "1,200 dollars", "✨ +300 dollars from last month", "🎯"), unsafe_allow_html=True)
|
| 330 |
+
|
| 331 |
+
elif category == "Education":
|
| 332 |
+
with col1:
|
| 333 |
+
exam_data = analyze_exam_performance(profile)
|
| 334 |
+
st.markdown(get_metric_card_html("Recent Exam Score", f"{profile['recent_exam']}", f"🎯 Goal: {profile['goal']} ({exam_data['points_needed']} needed)", "📚"), unsafe_allow_html=True)
|
| 335 |
+
|
| 336 |
+
with col2:
|
| 337 |
+
progress_pct = round((profile['recent_exam'] / profile['goal']) * 100, 1)
|
| 338 |
+
st.markdown(get_metric_card_html("Goal Progress", f"{progress_pct}%", f"📈 {profile['recent_exam']}/{profile['goal']}", "🎯"), unsafe_allow_html=True)
|
| 339 |
+
|
| 340 |
+
with col3:
|
| 341 |
+
st.markdown(get_metric_card_html("Study Time Today", "-12%", "⚠️ Below average", "⏰"), unsafe_allow_html=True)
|
| 342 |
+
|
| 343 |
+
# ===== ENHANCED: LEFT PANEL WITH PROACTIVE INSIGHTS =====
|
| 344 |
+
def render_left_panel(state, category):
|
| 345 |
+
profile = state.profile
|
| 346 |
+
|
| 347 |
+
# NEW: Show proactive AI insights at top
|
| 348 |
+
render_proactive_insights(profile, category)
|
| 349 |
+
|
| 350 |
+
render_dashboard_summary(profile, category)
|
| 351 |
+
|
| 352 |
+
st.markdown("---")
|
| 353 |
+
|
| 354 |
+
# Existing visuals
|
| 355 |
+
if category == "Education":
|
| 356 |
+
render_education_visuals(profile)
|
| 357 |
+
elif category == "Finance":
|
| 358 |
+
render_finance_visuals(profile)
|
| 359 |
+
|
| 360 |
+
st.markdown("---")
|
| 361 |
+
|
| 362 |
+
# NEW: What-If Mode button
|
| 363 |
+
if category in ["Finance", "Education"]:
|
| 364 |
+
with st.expander("🔮 Try What-If Mode", expanded=False):
|
| 365 |
+
render_whatif_mode(profile, category)
|
| 366 |
+
|
| 367 |
+
st.markdown("---")
|
| 368 |
+
|
| 369 |
+
# Existing smart insights (kept for compatibility)
|
| 370 |
+
st.markdown("### 💡 Smart Insights")
|
| 371 |
+
insights = get_category_insights(category, profile)
|
| 372 |
+
for i, insight in enumerate(insights[:3]):
|
| 373 |
+
card_type = "warning" if "⚠️" in insight else "insight"
|
| 374 |
+
st.markdown(get_insight_html(insight, card_type), unsafe_allow_html=True)
|
| 375 |
+
if st.button(f"Ask about this", key=f"insight_{category}_{i}", use_container_width=True):
|
| 376 |
+
set_seed_chat(f"Tell me more about: {insight}")
|
| 377 |
+
|
| 378 |
+
# ===== EXISTING: EDUCATION VISUALS =====
|
| 379 |
+
def render_education_visuals(profile):
|
| 380 |
+
exam_data = analyze_exam_performance(profile)
|
| 381 |
+
col1, col2 = st.columns(2)
|
| 382 |
+
|
| 383 |
+
with col1:
|
| 384 |
+
st.markdown("#### 📈 Score Breakdown")
|
| 385 |
+
fig = create_animated_donut_chart(
|
| 386 |
+
list(exam_data["score_breakdown"].values()),
|
| 387 |
+
list(exam_data["score_breakdown"].keys()),
|
| 388 |
+
['#bf6dbf', '#5f5fba', '#8b5cf6', '#a78bfa', '#c4b5fd']
|
| 389 |
+
)
|
| 390 |
+
st.plotly_chart(fig, use_container_width=True, key="main_score_chart")
|
| 391 |
+
|
| 392 |
+
with col2:
|
| 393 |
+
st.markdown("#### 🎯 Goal Progress")
|
| 394 |
+
fig = create_gauge_chart(profile['recent_exam'], profile['goal'],
|
| 395 |
+
f"{profile['recent_exam']}/{profile['goal']}")
|
| 396 |
+
st.plotly_chart(fig, use_container_width=True, key="main_goal_gauge")
|
| 397 |
+
|
| 398 |
+
# ===== EXISTING: FINANCE VISUALS =====
|
| 399 |
+
def render_finance_visuals(profile):
|
| 400 |
+
budget_data = calculate_budget_metrics(profile)
|
| 401 |
+
col1, col2 = st.columns(2)
|
| 402 |
+
|
| 403 |
+
with col1:
|
| 404 |
+
st.markdown("#### 💸 Spending Breakdown")
|
| 405 |
+
fig = create_animated_donut_chart(
|
| 406 |
+
list(budget_data["categories"].values()),
|
| 407 |
+
list(budget_data["categories"].keys()),
|
| 408 |
+
['#10b981', '#f59e0b', '#ef4444']
|
| 409 |
+
)
|
| 410 |
+
st.plotly_chart(fig, use_container_width=True, key="main_spending_chart")
|
| 411 |
+
|
| 412 |
+
with col2:
|
| 413 |
+
st.markdown("#### 🎯 Budget Status")
|
| 414 |
+
fig = create_gauge_chart(profile['spend'], profile['budget'],
|
| 415 |
+
f"${profile['spend']}/${profile['budget']}")
|
| 416 |
+
st.plotly_chart(fig, use_container_width=True, key="main_budget_gauge")
|
| 417 |
+
|
| 418 |
+
# ===== EXISTING: TOPIC EXTRACTION (unchanged) =====
|
| 419 |
+
def extract_topics_from_history(chat_history):
|
| 420 |
+
"""Extract ALL topics AND subtopics ever discussed"""
|
| 421 |
+
all_user_text = " ".join([m['content'].lower() for m in chat_history if m['role'] == 'user'])
|
| 422 |
+
topics_discussed = set()
|
| 423 |
+
|
| 424 |
+
# Finance topics
|
| 425 |
+
if any(w in all_user_text for w in ['save', 'savings', 'emergency', 'automate']):
|
| 426 |
+
topics_discussed.add('savings')
|
| 427 |
+
if any(w in all_user_text for w in ['invest', 'etf', 'stock', 'crypto', 'bitcoin', 'trading', 'options']):
|
| 428 |
+
topics_discussed.add('investing')
|
| 429 |
+
if any(w in all_user_text for w in ['budget', 'spend', 'expense', 'cost', 'cut', '50/30/20']):
|
| 430 |
+
topics_discussed.add('budgeting')
|
| 431 |
+
if any(w in all_user_text for w in ['debt', 'loan', 'credit', 'repay']):
|
| 432 |
+
topics_discussed.add('debt')
|
| 433 |
+
|
| 434 |
+
# Finance subtopics (deeper tracking)
|
| 435 |
+
if any(w in all_user_text for w in ['etf', 'exchange traded']):
|
| 436 |
+
topics_discussed.add('etf_basics')
|
| 437 |
+
if any(w in all_user_text for w in ['index fund', 'index', 'mutual fund']):
|
| 438 |
+
topics_discussed.add('index_funds')
|
| 439 |
+
if any(w in all_user_text for w in ['bitcoin', 'crypto', 'cryptocurrency']):
|
| 440 |
+
topics_discussed.add('crypto')
|
| 441 |
+
if any(w in all_user_text for w in ['best etf', 'which etf', 'etf for']):
|
| 442 |
+
topics_discussed.add('etf_selection')
|
| 443 |
+
if any(w in all_user_text for w in ['long-term', 'long term', 'strategy']):
|
| 444 |
+
topics_discussed.add('long_term')
|
| 445 |
+
if any(w in all_user_text for w in ['dividend', 'yield', 'income']):
|
| 446 |
+
topics_discussed.add('dividends')
|
| 447 |
+
if any(w in all_user_text for w in ['rebalance', 'allocation', 'portfolio']):
|
| 448 |
+
topics_discussed.add('portfolio')
|
| 449 |
+
|
| 450 |
+
# Savings subtopics
|
| 451 |
+
if any(w in all_user_text for w in ['high-yield', 'high yield', 'best savings']):
|
| 452 |
+
topics_discussed.add('high_yield')
|
| 453 |
+
if any(w in all_user_text for w in ['automate', 'automatic', 'auto-save']):
|
| 454 |
+
topics_discussed.add('automate_savings')
|
| 455 |
+
if any(w in all_user_text for w in ['emergency fund', 'emergency']):
|
| 456 |
+
topics_discussed.add('emergency_fund')
|
| 457 |
+
|
| 458 |
+
# Budgeting subtopics
|
| 459 |
+
if any(w in all_user_text for w in ['50/30/20', 'budget rule']):
|
| 460 |
+
topics_discussed.add('budget_rules')
|
| 461 |
+
if any(w in all_user_text for w in ['track spending', 'budget app']):
|
| 462 |
+
topics_discussed.add('tracking_tools')
|
| 463 |
+
if any(w in all_user_text for w in ['cut expenses', 'reduce spending']):
|
| 464 |
+
topics_discussed.add('cutting_expenses')
|
| 465 |
+
|
| 466 |
+
# Education topics
|
| 467 |
+
if any(w in all_user_text for w in ['study', 'exam', 'test', 'score', 'improve', 'focus']):
|
| 468 |
+
topics_discussed.add('studying')
|
| 469 |
+
if any(w in all_user_text for w in ['practice', 'question', 'weak', 'subject']):
|
| 470 |
+
topics_discussed.add('practice')
|
| 471 |
+
|
| 472 |
+
return topics_discussed
|
| 473 |
+
|
| 474 |
+
def detect_current_topic(user_message_text):
|
| 475 |
+
"""Detect which topic the user is asking about RIGHT NOW"""
|
| 476 |
+
text = user_message_text.lower()
|
| 477 |
+
|
| 478 |
+
# Finance
|
| 479 |
+
if any(w in text for w in ['save', 'savings', 'automate', 'emergency fund']):
|
| 480 |
+
return 'savings'
|
| 481 |
+
if any(w in text for w in ['invest', 'etf', 'stock', 'bitcoin', 'crypto', 'options', 'trading']):
|
| 482 |
+
return 'investing'
|
| 483 |
+
if any(w in text for w in ['budget', 'spend', 'expense', '50/30/20', 'cut', 'stick', 'track']):
|
| 484 |
+
return 'budgeting'
|
| 485 |
+
if any(w in text for w in ['debt', 'loan', 'credit', 'repay']):
|
| 486 |
+
return 'debt'
|
| 487 |
+
|
| 488 |
+
# Education
|
| 489 |
+
if any(w in text for w in ['study', 'focus', 'rush', 'manage time']):
|
| 490 |
+
return 'studying'
|
| 491 |
+
if any(w in text for w in ['exam', 'test', 'score', 'weak', 'improve']):
|
| 492 |
+
return 'exams'
|
| 493 |
+
if any(w in text for w in ['practice', 'question', 'quiz']):
|
| 494 |
+
return 'practice'
|
| 495 |
+
|
| 496 |
+
return None
|
| 497 |
+
|
| 498 |
+
# ===== EXISTING: FINANCE SUGGESTIONS (unchanged) =====
|
| 499 |
+
def get_suggestions_for_finance(chat_history, current_topic, topics_discussed):
|
| 500 |
+
"""FINANCE SUGGESTIONS - Progressive deepening"""
|
| 501 |
+
if current_topic == 'savings':
|
| 502 |
+
if 'savings' not in topics_discussed:
|
| 503 |
+
return [
|
| 504 |
+
("📈 High-yield accounts?", "What are the best high-yield savings accounts for Gen Z?"),
|
| 505 |
+
("🎯 Automate savings", "How can I automate my savings?"),
|
| 506 |
+
("💡 Cut biggest expenses", "What are my biggest expenses to cut?")
|
| 507 |
+
]
|
| 508 |
+
else:
|
| 509 |
+
return [
|
| 510 |
+
("💳 Which account best for me?", "Which high-yield savings account would be best for my situation?"),
|
| 511 |
+
("🔄 Emergency fund sizing", "How much should I keep in an emergency fund?"),
|
| 512 |
+
("📊 Track savings progress", "How do I track and measure my automated savings?")
|
| 513 |
+
]
|
| 514 |
+
|
| 515 |
+
elif current_topic == 'investing':
|
| 516 |
+
# Level 1: Complete beginner
|
| 517 |
+
if 'investing' not in topics_discussed:
|
| 518 |
+
return [
|
| 519 |
+
("📊 Start investing small", "How do I start investing with little money?"),
|
| 520 |
+
("🔍 ETFs vs stocks", "Should I invest in ETFs or individual stocks?"),
|
| 521 |
+
("📈 Best beginner ETFs", "What are beginner-friendly investment tips for Gen Z?")
|
| 522 |
+
]
|
| 523 |
+
# Level 2: Asked about ETFs/basics, now go deeper
|
| 524 |
+
elif 'etf_selection' not in topics_discussed and 'index_funds' not in topics_discussed:
|
| 525 |
+
return [
|
| 526 |
+
("💎 Best ETFs for my age", "What are the best ETFs for my age and budget?"),
|
| 527 |
+
("📈 Index funds?", "What's the difference between ETFs and index funds?"),
|
| 528 |
+
("🔐 Safe investments", "What are the safest investment options for beginners?")
|
| 529 |
+
]
|
| 530 |
+
# Level 3: Asked about specific ETFs/index funds, now strategy
|
| 531 |
+
elif 'long_term' not in topics_discussed and 'portfolio' not in topics_discussed:
|
| 532 |
+
return [
|
| 533 |
+
("⏰ Long-term strategy", "What's a good long-term investment strategy?"),
|
| 534 |
+
("📊 Portfolio allocation", "How should I allocate my investment portfolio?"),
|
| 535 |
+
("💰 Dividend investing", "Should I focus on dividend-paying investments?")
|
| 536 |
+
]
|
| 537 |
+
# Level 4: Advanced investor questions
|
| 538 |
+
else:
|
| 539 |
+
return [
|
| 540 |
+
("🔄 Rebalancing", "When and how should I rebalance my portfolio?"),
|
| 541 |
+
("🌍 International ETFs", "Should I invest in international ETFs?"),
|
| 542 |
+
("📈 Growth vs value", "What's the difference between growth and value investing?")
|
| 543 |
+
]
|
| 544 |
+
|
| 545 |
+
elif current_topic == 'budgeting':
|
| 546 |
+
if 'budgeting' not in topics_discussed:
|
| 547 |
+
return [
|
| 548 |
+
("✂️ Cut expenses smartly", "Where can I cut expenses without sacrificing quality?"),
|
| 549 |
+
("📊 Track spending", "Best way to track my spending automatically?"),
|
| 550 |
+
("🎯 Create monthly budget", "Help me create a realistic monthly budget")
|
| 551 |
+
]
|
| 552 |
+
else:
|
| 553 |
+
return [
|
| 554 |
+
("50/30/20 rule?", "Should I use the 50/30/20 budgeting rule?"),
|
| 555 |
+
("💳 Best budget app", "What budget tracking app should I use?"),
|
| 556 |
+
("🎯 Stick to budget", "How do I actually stick to my budget?")
|
| 557 |
+
]
|
| 558 |
+
|
| 559 |
+
elif current_topic == 'debt':
|
| 560 |
+
if 'debt' not in topics_discussed:
|
| 561 |
+
return [
|
| 562 |
+
("💳 Pay off faster", "Best strategy to pay off debt faster?"),
|
| 563 |
+
("🔍 Debt vs savings", "Should I focus on debt or savings first?"),
|
| 564 |
+
("📉 Lower interest", "How to reduce credit card interest rates?")
|
| 565 |
+
]
|
| 566 |
+
else:
|
| 567 |
+
return [
|
| 568 |
+
("📊 Snowball vs avalanche", "Is snowball or avalanche better for me?"),
|
| 569 |
+
("⏰ Payoff timeline", "How long would it take to pay off my debt?"),
|
| 570 |
+
("🆘 Negotiate rates", "Can I negotiate lower interest rates?")
|
| 571 |
+
]
|
| 572 |
+
|
| 573 |
+
else:
|
| 574 |
+
return [
|
| 575 |
+
("💰 Save more?", "How can I save more money this month?"),
|
| 576 |
+
("📊 Break down spending", "Break down my spending and show where to cut costs"),
|
| 577 |
+
("📈 Investment tips", "What are beginner-friendly investment tips for Gen Z?")
|
| 578 |
+
]
|
| 579 |
+
|
| 580 |
+
# ===== EXISTING: EDUCATION SUGGESTIONS (unchanged) =====
|
| 581 |
+
def get_suggestions_for_education(chat_history, current_topic, topics_discussed):
|
| 582 |
+
"""EDUCATION SUGGESTIONS - Progressive deepening"""
|
| 583 |
+
if current_topic == 'studying':
|
| 584 |
+
if 'studying' not in topics_discussed:
|
| 585 |
+
return [
|
| 586 |
+
("⏰ Study schedule", "Help me create an effective study schedule"),
|
| 587 |
+
("🎯 Stay focused", "Best techniques to stay focused while studying?"),
|
| 588 |
+
("📝 Take notes better", "What's the most effective way to take notes?")
|
| 589 |
+
]
|
| 590 |
+
else:
|
| 591 |
+
return [
|
| 592 |
+
("🚀 Pomodoro technique", "How does the Pomodoro technique work for studying?"),
|
| 593 |
+
("📍 Study environment", "How do I create a good study environment?"),
|
| 594 |
+
("⚡ Study groups", "How can I make study groups effective?")
|
| 595 |
+
]
|
| 596 |
+
|
| 597 |
+
elif current_topic == 'exams':
|
| 598 |
+
if 'exams' not in topics_discussed:
|
| 599 |
+
return [
|
| 600 |
+
("📊 Boost scores fast", "What's the fastest way to improve exam scores?"),
|
| 601 |
+
("✏️ Test strategies", "Give me strategies for better test performance"),
|
| 602 |
+
("🎯 Fix weak subjects", "How do I tackle my weakest subjects?")
|
| 603 |
+
]
|
| 604 |
+
else:
|
| 605 |
+
return [
|
| 606 |
+
("🧠 Active recall", "How does active recall help with exam prep?"),
|
| 607 |
+
("📅 Study calendar", "How should I plan my study calendar for exams?"),
|
| 608 |
+
("😰 Test anxiety", "How do I handle test anxiety?")
|
| 609 |
+
]
|
| 610 |
+
|
| 611 |
+
elif current_topic == 'practice':
|
| 612 |
+
if 'practice' not in topics_discussed:
|
| 613 |
+
return [
|
| 614 |
+
("✏️ Practice questions", "Give me practice questions for my weakest subject"),
|
| 615 |
+
("🎯 Target weak areas", "How do I focus practice on weak areas?"),
|
| 616 |
+
("📊 Quiz myself", "What's the best way to quiz myself?")
|
| 617 |
+
]
|
| 618 |
+
else:
|
| 619 |
+
return [
|
| 620 |
+
("🔄 Spaced repetition", "How does spaced repetition work?"),
|
| 621 |
+
("📈 Track progress", "How do I measure improvement from practice?"),
|
| 622 |
+
("💡 Learn from mistakes", "How should I analyze my practice mistakes?")
|
| 623 |
+
]
|
| 624 |
+
|
| 625 |
+
else:
|
| 626 |
+
return [
|
| 627 |
+
("📚 Analyze exam", "Analyze my last exam and tell me where to improve"),
|
| 628 |
+
("✏️ Practice questions", "Give me practice questions for my weakest subject"),
|
| 629 |
+
("🎯 Improve scores", "What's the fastest way to improve my exam scores?")
|
| 630 |
+
]
|
| 631 |
+
|
| 632 |
+
# ===== EXISTING: DYNAMIC SUGGESTIONS (unchanged) =====
|
| 633 |
+
def get_dynamic_suggestions_v4(chat_history, category):
|
| 634 |
+
"""PRODUCTION VERSION - Never recycles suggestions"""
|
| 635 |
+
if len(chat_history) <= 1:
|
| 636 |
+
if category == "Finance":
|
| 637 |
+
return [
|
| 638 |
+
("💰 Save more?", "How can I save more money this month?"),
|
| 639 |
+
("📊 Break down spending", "Break down my spending and show where to cut costs"),
|
| 640 |
+
("📈 Investment tips", "What are beginner-friendly investment tips for Gen Z?")
|
| 641 |
+
]
|
| 642 |
+
elif category == "Education":
|
| 643 |
+
return [
|
| 644 |
+
("📚 Analyze exam", "Analyze my last exam and tell me where to improve"),
|
| 645 |
+
("✏️ Practice questions", "Give me practice questions for my weakest subject"),
|
| 646 |
+
("🎯 Improve scores", "What's the fastest way to improve my exam scores?")
|
| 647 |
+
]
|
| 648 |
+
|
| 649 |
+
topics_discussed = extract_topics_from_history(chat_history)
|
| 650 |
+
|
| 651 |
+
latest_user_msg = ""
|
| 652 |
+
for msg in reversed(chat_history):
|
| 653 |
+
if msg['role'] == 'user':
|
| 654 |
+
latest_user_msg = msg['content']
|
| 655 |
+
break
|
| 656 |
+
|
| 657 |
+
current_topic = detect_current_topic(latest_user_msg)
|
| 658 |
+
|
| 659 |
+
if category == "Finance":
|
| 660 |
+
return get_suggestions_for_finance(chat_history, current_topic, topics_discussed)
|
| 661 |
+
elif category == "Education":
|
| 662 |
+
return get_suggestions_for_education(chat_history, current_topic, topics_discussed)
|
| 663 |
+
|
| 664 |
+
return [("💭 Ask something", "What's on your mind?")]
|
| 665 |
+
|
| 666 |
+
# ===== EXISTING: CHAT PANEL (unchanged) =====
|
| 667 |
+
def render_chat_panel(chat_history_key, category):
|
| 668 |
+
"""Chat panel with PRODUCTION-READY suggestion engine"""
|
| 669 |
+
icon_map = {"Finance": "💰", "Education": "📚", "Family": "👨👩👧", "Friends": "👥", "Weekend/Vacation": "🏖"}
|
| 670 |
+
category_icon = icon_map.get(category, "🎯")
|
| 671 |
+
|
| 672 |
+
col1, col2 = st.columns([4, 1])
|
| 673 |
+
with col1:
|
| 674 |
+
st.markdown(f"### 💬 Chat with Aqua about {category_icon} {category}")
|
| 675 |
+
with col2:
|
| 676 |
+
if st.button("🔄 Reset", key=f"reset_chat_{category}", use_container_width=True, type="secondary"):
|
| 677 |
+
st.session_state[chat_history_key] = []
|
| 678 |
+
st.rerun()
|
| 679 |
+
|
| 680 |
+
if not st.session_state[chat_history_key]:
|
| 681 |
+
welcome_msg = f"Hey! 👋 I'm Aqua, your {category.lower()} mentor. What's on your mind?"
|
| 682 |
+
st.session_state[chat_history_key].append({"role": "assistant", "content": welcome_msg})
|
| 683 |
+
|
| 684 |
+
# Display all messages
|
| 685 |
+
for msg in st.session_state[chat_history_key]:
|
| 686 |
+
with st.chat_message(msg["role"], avatar="🌊" if msg["role"] == "assistant" else "👤"):
|
| 687 |
+
st.markdown(msg["content"])
|
| 688 |
+
|
| 689 |
+
st.markdown("---")
|
| 690 |
+
st.markdown("### ⚡ Quick Actions")
|
| 691 |
+
|
| 692 |
+
suggestions = get_dynamic_suggestions_v4(st.session_state[chat_history_key], category)
|
| 693 |
+
|
| 694 |
+
if suggestions:
|
| 695 |
+
cols = st.columns(min(len(suggestions), 3))
|
| 696 |
+
for i, (display_text, full_prompt) in enumerate(suggestions):
|
| 697 |
+
col_idx = i % len(cols)
|
| 698 |
+
button_key = f"quick_{category}_{i}_{len(st.session_state[chat_history_key])}_prod"
|
| 699 |
+
if cols[col_idx].button(display_text, key=button_key, use_container_width=True):
|
| 700 |
+
set_seed_chat(full_prompt)
|
data.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Provide core cards, viz categories, and mock data utilities
|
| 2 |
+
|
| 3 |
+
CATEGORIES = [
|
| 4 |
+
"Finance", "Education", "Family", "Friends", "Weekend/Vacation"
|
| 5 |
+
]
|
| 6 |
+
|
| 7 |
+
def get_dashboard_cards(profile):
|
| 8 |
+
# Simulate dashboard cards data for given profile (expand as needed)
|
| 9 |
+
return [
|
| 10 |
+
{"title": "This Month's Budget", "progress": profile["spend"]/profile["budget"], "details": f"{profile['spend']}/{profile['budget']}"},
|
| 11 |
+
{"title": "12% less study time today", "details": "Stay focused! Important milestone for PM program."},
|
| 12 |
+
{"title": "My Goals For Today", "goals": profile["goals_today"]},
|
| 13 |
+
{"title": "Today's Tip", "details": "Registering for Selective Service may get you aid."},
|
| 14 |
+
{"title": "Savings Goal", "progress": 0.68, "details": "+$300 from last month"},
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
def get_category_data(category, profile):
|
| 18 |
+
# Simulate breakdowns for each category, return chart data
|
| 19 |
+
if category == "Finance":
|
| 20 |
+
return {"labels": ["Savings", "Food", "Leisure"], "values": [1000, 400, 176]}
|
| 21 |
+
elif category == "Education":
|
| 22 |
+
return {"labels": list(profile["last_scores"].keys()), "values": list(profile["last_scores"].values())}
|
| 23 |
+
return {"labels": [], "values": []}
|
llm_agent.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
llm_agent.py
|
| 3 |
+
|
| 4 |
+
FIXED VERSION:
|
| 5 |
+
1. Mistral 7B for Finance (NOT specialized finance model - good for conversations)
|
| 6 |
+
2. Phi-2 2.7B for Education (better than specialized - data quality > size)
|
| 7 |
+
3. 7B max per model (no huge models that fail)
|
| 8 |
+
4. Groq fallback with ONLY available models (mixtral deprecated, using llama)
|
| 9 |
+
5. No falling back immediately - models are ACTUALLY available
|
| 10 |
+
ENHANCEMENTS:
|
| 11 |
+
1. Scenario-aware prompts for what-if queries
|
| 12 |
+
2. Proactive insights integration
|
| 13 |
+
3. Better formatting for comparisons
|
| 14 |
+
4. Unchanged: Multi-LLM strategy (HF + Groq)
|
| 15 |
+
5. Unchanged: All existing analytics integration
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from groq import Groq
|
| 19 |
+
from analytics import (
|
| 20 |
+
analyze_exam_performance,
|
| 21 |
+
calculate_budget_metrics,
|
| 22 |
+
get_time_improvement_suggestions,
|
| 23 |
+
generate_practice_questions,
|
| 24 |
+
calculate_projected_improvement,
|
| 25 |
+
parse_budget_query,
|
| 26 |
+
apply_scenario_changes,
|
| 27 |
+
compare_scenarios,
|
| 28 |
+
generate_proactive_insights
|
| 29 |
+
)
|
| 30 |
+
import json
|
| 31 |
+
import os
|
| 32 |
+
from huggingface_hub import InferenceClient
|
| 33 |
+
|
| 34 |
+
# HF Models - All 7B or smaller
|
| 35 |
+
HF_MODELS = {
|
| 36 |
+
"Finance": "mistralai/Mistral-7B-Instruct-v0.1",
|
| 37 |
+
"Education": "microsoft/phi-2",
|
| 38 |
+
"Family": "HuggingFaceH4/zephyr-7b-beta",
|
| 39 |
+
"Friends": "HuggingFaceH4/zephyr-7b-beta",
|
| 40 |
+
"Weekend/Vacation": "HuggingFaceH4/zephyr-7b-beta"
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# Groq models - Available models only
|
| 44 |
+
GROQ_MODELS = {
|
| 45 |
+
"Finance": "llama-3.1-8b-instant",
|
| 46 |
+
"Education": "llama-3.3-70b-versatile",
|
| 47 |
+
"Family": "llama-3.1-8b-instant",
|
| 48 |
+
"Friends": "llama-3.1-8b-instant",
|
| 49 |
+
"Weekend/Vacation": "llama-3.1-8b-instant"
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "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")
|
| 53 |
+
HF_API_KEY = os.getenv("HF_API_KEY", "hf_kuaUsin8tkP44NrlwmsUWGdyb3FYb8ziZyHAU4")
|
| 54 |
+
|
| 55 |
+
GENZ_PERSONA = r"""You are Aqua, a GenZ AI mentor who's like that super helpful older friend who's been through it all.
|
| 56 |
+
|
| 57 |
+
PERSONALITY TRAITS:
|
| 58 |
+
- Warm, friendly, and supportive (but not fake or overly enthusiastic)
|
| 59 |
+
- Direct and honest (no sugarcoating, but always constructive)
|
| 60 |
+
- Uses casual language naturally (occasional "fr", "ngl", "lowkey" - but don't overdo it)
|
| 61 |
+
- Encouraging but realistic about challenges
|
| 62 |
+
- Uses emojis purposefully (1-2 per response max)
|
| 63 |
+
- Breaks down complex stuff into digestible chunks
|
| 64 |
+
|
| 65 |
+
COMMUNICATION STYLE:
|
| 66 |
+
- Keep responses concise (2-4 sentences for simple questions, more for complex ones)
|
| 67 |
+
- Lead with the most actionable insight
|
| 68 |
+
- Use bullet points for steps or lists (but format naturally)
|
| 69 |
+
- Reference specific data from the user's profile when relevant
|
| 70 |
+
- Ask follow-up questions when you need more context
|
| 71 |
+
- Celebrate wins genuinely
|
| 72 |
+
|
| 73 |
+
CRITICAL: CURRENCY FORMATTING
|
| 74 |
+
- NEVER use bare dollar signs like $750
|
| 75 |
+
- ALWAYS write currency as: "750 dollars" or "USD 750"
|
| 76 |
+
- Example: "You've spent 750 dollars out of 2000 dollars"
|
| 77 |
+
|
| 78 |
+
WHAT TO AVOID:
|
| 79 |
+
- Being overly formal or corporate
|
| 80 |
+
- Using too many emojis or exclamation marks
|
| 81 |
+
- Giving vague advice like "try your best"
|
| 82 |
+
- Long walls of text without structure
|
| 83 |
+
- Ignoring the user's actual situation/data
|
| 84 |
+
|
| 85 |
+
YOUR GOAL:
|
| 86 |
+
Help this GenZ user make smart decisions about their life, finances, and education. Be the mentor they wish they had."""
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def get_system_prompt(profile, category, tool_results="", scenario_data=None):
|
| 90 |
+
"""Enhanced system prompt with scenario awareness"""
|
| 91 |
+
exam_status = f"Recent exam: {profile['recent_exam']}/{profile['goal']}"
|
| 92 |
+
budget_status = f"Spending: {profile['spend']} dollars/{profile['budget']} dollars"
|
| 93 |
+
|
| 94 |
+
weak_areas = []
|
| 95 |
+
if profile.get('math_weakness'):
|
| 96 |
+
weak_areas.append("math")
|
| 97 |
+
if profile.get('rushing'):
|
| 98 |
+
weak_areas.append("time management")
|
| 99 |
+
|
| 100 |
+
# Add scenario context if present
|
| 101 |
+
scenario_context = ""
|
| 102 |
+
if scenario_data:
|
| 103 |
+
scenario_context = f"""
|
| 104 |
+
SCENARIO ANALYSIS:
|
| 105 |
+
The user is asking a 'what-if' question. Here's the comparison:
|
| 106 |
+
|
| 107 |
+
Current Situation:
|
| 108 |
+
- Budget: {profile['budget']} dollars
|
| 109 |
+
- Spent: {profile['spend']} dollars
|
| 110 |
+
- Remaining: {profile['budget'] - profile['spend']} dollars
|
| 111 |
+
|
| 112 |
+
Scenario Result:
|
| 113 |
+
- Budget: {scenario_data['scenario']['budget']} dollars
|
| 114 |
+
- Spent: {scenario_data['scenario']['spent']} dollars
|
| 115 |
+
- Remaining: {scenario_data['scenario']['remaining']} dollars
|
| 116 |
+
|
| 117 |
+
Key Differences:
|
| 118 |
+
{chr(10).join(f"• {insight}" for insight in scenario_data['insights'])}
|
| 119 |
+
|
| 120 |
+
IMPORTANT: Frame your response around this comparison. Be specific about the trade-offs.
|
| 121 |
+
"""
|
| 122 |
+
|
| 123 |
+
context = f"""
|
| 124 |
+
{GENZ_PERSONA}
|
| 125 |
+
|
| 126 |
+
CURRENT USER CONTEXT:
|
| 127 |
+
Name: {profile.get('name', 'User')}
|
| 128 |
+
Category Focus: {category}
|
| 129 |
+
{exam_status if category == 'Education' else ''}
|
| 130 |
+
{budget_status if category == 'Finance' else ''}
|
| 131 |
+
{'Areas needing work: ' + ', '.join(weak_areas) if weak_areas else ''}
|
| 132 |
+
Today's goals progress: {profile.get('goals_today', {})}
|
| 133 |
+
|
| 134 |
+
{scenario_context}
|
| 135 |
+
|
| 136 |
+
{tool_results}
|
| 137 |
+
|
| 138 |
+
IMPORTANT:
|
| 139 |
+
1. Reference specific numbers and data from the user's profile in your responses.
|
| 140 |
+
2. ALWAYS format currency as "X dollars" or "USD X" - NEVER use bare dollar signs like $X
|
| 141 |
+
3. If this is a scenario/what-if question, focus on the comparison and trade-offs.
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
return context
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def should_use_analytics(query, category):
|
| 148 |
+
"""Detect if analytics tools are needed"""
|
| 149 |
+
analysis_keywords = [
|
| 150 |
+
"analyze", "analysis", "break down", "breakdown", "how am i doing",
|
| 151 |
+
"performance", "progress", "score", "exam", "test", "spending",
|
| 152 |
+
"budget", "where", "what", "show me", "tell me about", "compare",
|
| 153 |
+
"what if", "if i", "suppose", "imagine"
|
| 154 |
+
]
|
| 155 |
+
|
| 156 |
+
return any(keyword in query.lower() for keyword in analysis_keywords)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def get_tool_results(query, profile, category, scenario_profile=None):
|
| 160 |
+
"""Get analytics results, with scenario comparison if applicable"""
|
| 161 |
+
results = ""
|
| 162 |
+
|
| 163 |
+
# Check if this is a scenario query
|
| 164 |
+
if scenario_profile:
|
| 165 |
+
comparison = compare_scenarios(profile, scenario_profile)
|
| 166 |
+
|
| 167 |
+
results += "\n\n📊 SCENARIO COMPARISON ANALYSIS\n\n"
|
| 168 |
+
results += "Current vs Scenario:\n"
|
| 169 |
+
results += f"• Spending: {profile['spend']} dollars → {scenario_profile['spend']} dollars\n"
|
| 170 |
+
results += f"• Change: {comparison['differences']['spend']:+.0f} dollars\n"
|
| 171 |
+
results += f"• Remaining: {comparison['current']['remaining']} dollars → {comparison['scenario']['remaining']} dollars\n"
|
| 172 |
+
results += f"• Change: {comparison['differences']['remaining']:+.0f} dollars\n\n"
|
| 173 |
+
|
| 174 |
+
results += "Key Insights:\n"
|
| 175 |
+
for insight in comparison['insights']:
|
| 176 |
+
results += f"• {insight}\n"
|
| 177 |
+
results += "\n"
|
| 178 |
+
|
| 179 |
+
return results
|
| 180 |
+
|
| 181 |
+
# Regular analytics (existing code)
|
| 182 |
+
if category == "Education" and should_use_analytics(query, category):
|
| 183 |
+
exam_data = analyze_exam_performance(profile)
|
| 184 |
+
results += f"\n\n📚 EDUCATION ANALYSIS\n\n"
|
| 185 |
+
results += f"Total Score: {exam_data['total_score']}/{exam_data['goal_score']}\n"
|
| 186 |
+
results += f"Progress: {exam_data['progress_pct']}%\n"
|
| 187 |
+
results += f"Points Needed: {exam_data['points_needed']}\n"
|
| 188 |
+
results += f"Weakest Subject: {exam_data['weakest_subject']} ({exam_data['score_breakdown'][exam_data['weakest_subject']]} points)\n"
|
| 189 |
+
results += f"Strongest Subject: {exam_data['strongest_subject']} ({exam_data['score_breakdown'][exam_data['strongest_subject']]} points)\n"
|
| 190 |
+
results += f"\nScore Breakdown:\n"
|
| 191 |
+
for subject, score in exam_data['score_breakdown'].items():
|
| 192 |
+
results += f" - {subject}: {score} ({exam_data['score_percentages'][subject]}%)\n"
|
| 193 |
+
results += f"\nKey Insights:\n"
|
| 194 |
+
for insight in exam_data['insights']:
|
| 195 |
+
results += f" • {insight}\n"
|
| 196 |
+
results += f"\n"
|
| 197 |
+
|
| 198 |
+
if any(word in query.lower() for word in ['improve', 'better', 'increase', 'boost']):
|
| 199 |
+
suggestions = get_time_improvement_suggestions(profile)
|
| 200 |
+
results += f"\n\n💡 IMPROVEMENT SUGGESTIONS\n\n"
|
| 201 |
+
for suggestion in suggestions:
|
| 202 |
+
results += f" • {suggestion}\n"
|
| 203 |
+
results += f"\n"
|
| 204 |
+
|
| 205 |
+
if any(word in query.lower() for word in ['practice', 'questions', 'quiz', 'drill']):
|
| 206 |
+
practice_qs = generate_practice_questions(exam_data['weakest_subject'])
|
| 207 |
+
results += f"\n\n📝 PRACTICE QUESTIONS\n\n"
|
| 208 |
+
for i, q in enumerate(practice_qs[:3], 1):
|
| 209 |
+
results += f" {i}. {q}\n"
|
| 210 |
+
results += f"\n"
|
| 211 |
+
|
| 212 |
+
elif category == "Finance" and should_use_analytics(query, category):
|
| 213 |
+
budget_data = calculate_budget_metrics(profile)
|
| 214 |
+
results += f"\n\n💰 FINANCE ANALYSIS\n\n"
|
| 215 |
+
results += f"Budget: {budget_data['budget']} dollars\n"
|
| 216 |
+
results += f"Spent: {budget_data['spent']} dollars ({budget_data['spend_percentage']}%)\n"
|
| 217 |
+
results += f"Remaining: {budget_data['remaining']} dollars\n"
|
| 218 |
+
results += f"Status: {'✅ On track' if budget_data['on_track'] else '⚠️ Over target'}\n"
|
| 219 |
+
results += f"\nSpending Breakdown:\n"
|
| 220 |
+
for cat_name, amount in budget_data['categories'].items():
|
| 221 |
+
pct = round((amount / budget_data['budget']) * 100, 1)
|
| 222 |
+
results += f" - {cat_name}: {amount} dollars ({pct}%)\n"
|
| 223 |
+
if budget_data['potential_savings']:
|
| 224 |
+
results += f"\nPotential Savings:\n"
|
| 225 |
+
for saving in budget_data['potential_savings']:
|
| 226 |
+
results += f" • {saving}\n"
|
| 227 |
+
results += f"\n"
|
| 228 |
+
|
| 229 |
+
return results
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def aqua_response_hf(query, profile, category, scenario_profile=None):
|
| 233 |
+
"""Try Hugging Face inference first"""
|
| 234 |
+
|
| 235 |
+
try:
|
| 236 |
+
hf_client = InferenceClient(api_key=HF_API_KEY)
|
| 237 |
+
|
| 238 |
+
model_name = HF_MODELS.get(category, "mistralai/Mistral-7B-Instruct-v0.1")
|
| 239 |
+
tool_results = get_tool_results(query, profile, category, scenario_profile)
|
| 240 |
+
|
| 241 |
+
# Build scenario data if present
|
| 242 |
+
scenario_data = None
|
| 243 |
+
if scenario_profile:
|
| 244 |
+
scenario_data = compare_scenarios(profile, scenario_profile)
|
| 245 |
+
|
| 246 |
+
system_message = get_system_prompt(profile, category, tool_results, scenario_data)
|
| 247 |
+
|
| 248 |
+
full_prompt = f"{system_message}\n\nUser Query: {query}"
|
| 249 |
+
|
| 250 |
+
response = hf_client.text_generation(
|
| 251 |
+
model=model_name,
|
| 252 |
+
prompt=full_prompt,
|
| 253 |
+
max_new_tokens=350,
|
| 254 |
+
temperature=0.75,
|
| 255 |
+
top_p=0.9
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
return response.strip()
|
| 259 |
+
|
| 260 |
+
except Exception as e:
|
| 261 |
+
print(f"HF Error: {str(e)[:100]}")
|
| 262 |
+
return None
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def aqua_response_groq(query, profile, category, scenario_profile=None):
|
| 266 |
+
"""Fallback to Groq with available models only"""
|
| 267 |
+
|
| 268 |
+
if not GROQ_API_KEY or GROQ_API_KEY == "gsk_xxx":
|
| 269 |
+
return "⚠️ Groq API Key Missing! Add it to your environment."
|
| 270 |
+
|
| 271 |
+
client = Groq(api_key=GROQ_API_KEY)
|
| 272 |
+
|
| 273 |
+
tool_results = get_tool_results(query, profile, category, scenario_profile)
|
| 274 |
+
|
| 275 |
+
# Build scenario data if present
|
| 276 |
+
scenario_data = None
|
| 277 |
+
if scenario_profile:
|
| 278 |
+
scenario_data = compare_scenarios(profile, scenario_profile)
|
| 279 |
+
|
| 280 |
+
system_message = get_system_prompt(profile, category, tool_results, scenario_data)
|
| 281 |
+
|
| 282 |
+
model = GROQ_MODELS.get(category, "llama-3.3-70b-versatile")
|
| 283 |
+
|
| 284 |
+
messages = [
|
| 285 |
+
{"role": "system", "content": system_message},
|
| 286 |
+
{"role": "user", "content": query}
|
| 287 |
+
]
|
| 288 |
+
|
| 289 |
+
try:
|
| 290 |
+
chat_completion = client.chat.completions.create(
|
| 291 |
+
messages=messages,
|
| 292 |
+
model=model,
|
| 293 |
+
temperature=0.75,
|
| 294 |
+
max_tokens=350,
|
| 295 |
+
top_p=0.9,
|
| 296 |
+
stop=None
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
response_content = chat_completion.choices[0].message.content
|
| 300 |
+
response_content = response_content.replace('$', '\\$')
|
| 301 |
+
return response_content.strip()
|
| 302 |
+
|
| 303 |
+
except Exception as e:
|
| 304 |
+
error_msg = str(e)[:100]
|
| 305 |
+
print(f"Groq Error: {error_msg}")
|
| 306 |
+
return f"😬 Something went wrong: {error_msg}... Try again?"
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def aqua_response(query, profile, category, scenario_profile=None):
|
| 310 |
+
"""
|
| 311 |
+
Main response function with scenario awareness:
|
| 312 |
+
1. Try HF models first (area-specific, <= 7B)
|
| 313 |
+
2. Fallback to Groq (area-specific, available models)
|
| 314 |
+
3. Return error if both fail
|
| 315 |
+
|
| 316 |
+
NEW: Accepts optional scenario_profile for what-if queries
|
| 317 |
+
"""
|
| 318 |
+
|
| 319 |
+
# Try HF first
|
| 320 |
+
print(f"[DEBUG] Using HF model for {category}: {HF_MODELS.get(category)}")
|
| 321 |
+
hf_response = aqua_response_hf(query, profile, category, scenario_profile)
|
| 322 |
+
|
| 323 |
+
if hf_response:
|
| 324 |
+
return hf_response
|
| 325 |
+
|
| 326 |
+
# If HF fails, use Groq with same focus-area model
|
| 327 |
+
print(f"[DEBUG] HF failed, falling back to Groq: {GROQ_MODELS.get(category)}")
|
| 328 |
+
groq_response = aqua_response_groq(query, profile, category, scenario_profile)
|
| 329 |
+
|
| 330 |
+
return groq_response
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def get_contextual_welcome_message(category, profile):
|
| 334 |
+
"""Generate proactive welcome with insights"""
|
| 335 |
+
# Get proactive insights
|
| 336 |
+
insights = generate_proactive_insights(profile, category)
|
| 337 |
+
|
| 338 |
+
base_messages = {
|
| 339 |
+
"Finance": f"Hey! 💰 I see you're at {profile['spend']} dollars/{profile['budget']} dollars this month.",
|
| 340 |
+
"Education": f"Hey! 📚 Last score was {profile['recent_exam']} - you need {profile['goal'] - profile['recent_exam']} more points to hit your goal.",
|
| 341 |
+
"Family": f"Hey! 👨👩👧 Family stuff can be tricky to balance. I'm here to help you stay connected while crushing your own goals.",
|
| 342 |
+
"Friends": f"Hey! 👥 Balancing friends and responsibilities is an art. Let's figure out how to stay social without breaking the bank.",
|
| 343 |
+
"Weekend/Vacation": f"Hey! 🏖️ Everyone needs a break. Let's plan something fun that won't wreck your budget."
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
base = base_messages.get(category, "Hey! 👋 I'm Aqua, your personal mentor. What's on your mind?")
|
| 347 |
+
|
| 348 |
+
# Add top insight if critical
|
| 349 |
+
if insights and insights[0].get('type') == 'critical':
|
| 350 |
+
base += f" {insights[0]['icon']} {insights[0]['text']}"
|
| 351 |
+
|
| 352 |
+
return base
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def get_default_profile():
|
| 356 |
+
return {
|
| 357 |
+
"name": "Suzy",
|
| 358 |
+
"recent_exam": 1200,
|
| 359 |
+
"goal": 1600,
|
| 360 |
+
"math_weakness": True,
|
| 361 |
+
"rushing": True,
|
| 362 |
+
"budget": 2000,
|
| 363 |
+
"spend": 1576,
|
| 364 |
+
"goals_today": {
|
| 365 |
+
"Calories": 85,
|
| 366 |
+
"Money": 75,
|
| 367 |
+
"Steps": 54
|
| 368 |
+
},
|
| 369 |
+
"last_scores": {
|
| 370 |
+
"Reading": 240,
|
| 371 |
+
"Writing": 220,
|
| 372 |
+
"Reasoning": 140,
|
| 373 |
+
"Algebra": 100,
|
| 374 |
+
"Geometry": 100
|
| 375 |
+
},
|
| 376 |
+
}
|
| 377 |
+
|
profile_editor.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
profile_editor.py
|
| 3 |
+
Profile editing component for Aqua Thistle
|
| 4 |
+
Allows users to update their profile data in real-time
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import streamlit as st
|
| 8 |
+
|
| 9 |
+
def render_profile_editor(profile, category):
|
| 10 |
+
"""
|
| 11 |
+
Render category-specific profile editor
|
| 12 |
+
Allows users to update their data in real-time
|
| 13 |
+
"""
|
| 14 |
+
with st.expander("✏️ Edit Your Profile", expanded=False):
|
| 15 |
+
st.markdown("Update your info below - changes apply instantly")
|
| 16 |
+
|
| 17 |
+
# Finance fields
|
| 18 |
+
if category == "Finance":
|
| 19 |
+
st.markdown("##### 💰 Financial Data")
|
| 20 |
+
col1, col2 = st.columns(2)
|
| 21 |
+
|
| 22 |
+
with col1:
|
| 23 |
+
new_budget = st.number_input(
|
| 24 |
+
"Monthly Budget ($)",
|
| 25 |
+
min_value=500,
|
| 26 |
+
max_value=10000,
|
| 27 |
+
value=profile['budget'],
|
| 28 |
+
step=100,
|
| 29 |
+
key="edit_budget",
|
| 30 |
+
help="Your total monthly budget"
|
| 31 |
+
)
|
| 32 |
+
if new_budget != profile['budget']:
|
| 33 |
+
profile['budget'] = new_budget
|
| 34 |
+
|
| 35 |
+
with col2:
|
| 36 |
+
new_spend = st.number_input(
|
| 37 |
+
"Current Spending ($)",
|
| 38 |
+
min_value=0,
|
| 39 |
+
max_value=20000, # High ceiling to allow any value
|
| 40 |
+
value=profile['spend'],
|
| 41 |
+
step=50,
|
| 42 |
+
key="edit_spend",
|
| 43 |
+
help="How much you've spent this month"
|
| 44 |
+
)
|
| 45 |
+
if new_spend != profile['spend']:
|
| 46 |
+
profile['spend'] = new_spend
|
| 47 |
+
|
| 48 |
+
# Warning if over budget
|
| 49 |
+
if profile['spend'] > profile['budget']:
|
| 50 |
+
over_amount = profile['spend'] - profile['budget']
|
| 51 |
+
st.warning(f"⚠️ You're ${over_amount} over budget! Consider adjusting your spending or budget.")
|
| 52 |
+
|
| 53 |
+
# Education fields
|
| 54 |
+
elif category == "Education":
|
| 55 |
+
st.markdown("##### 📚 Academic Data")
|
| 56 |
+
|
| 57 |
+
col1, col2 = st.columns(2)
|
| 58 |
+
with col1:
|
| 59 |
+
new_exam_score = st.number_input(
|
| 60 |
+
"Recent Exam Score",
|
| 61 |
+
min_value=400,
|
| 62 |
+
max_value=1600,
|
| 63 |
+
value=profile['recent_exam'],
|
| 64 |
+
step=10,
|
| 65 |
+
key="edit_exam",
|
| 66 |
+
help="Your most recent test score"
|
| 67 |
+
)
|
| 68 |
+
if new_exam_score != profile['recent_exam']:
|
| 69 |
+
profile['recent_exam'] = new_exam_score
|
| 70 |
+
|
| 71 |
+
with col2:
|
| 72 |
+
new_goal = st.number_input(
|
| 73 |
+
"Target Score",
|
| 74 |
+
min_value=400,
|
| 75 |
+
max_value=1600,
|
| 76 |
+
value=profile['goal'],
|
| 77 |
+
step=50,
|
| 78 |
+
key="edit_goal",
|
| 79 |
+
help="Your goal score"
|
| 80 |
+
)
|
| 81 |
+
if new_goal != profile['goal']:
|
| 82 |
+
profile['goal'] = new_goal
|
| 83 |
+
|
| 84 |
+
st.markdown("##### 📊 Subject Scores")
|
| 85 |
+
cols = st.columns(5)
|
| 86 |
+
|
| 87 |
+
subjects = ["Reading", "Writing", "Reasoning", "Algebra", "Geometry"]
|
| 88 |
+
for i, subject in enumerate(subjects):
|
| 89 |
+
with cols[i]:
|
| 90 |
+
new_score = st.number_input(
|
| 91 |
+
subject,
|
| 92 |
+
min_value=0,
|
| 93 |
+
max_value=400,
|
| 94 |
+
value=profile['last_scores'].get(subject, 100),
|
| 95 |
+
step=10,
|
| 96 |
+
key=f"edit_score_{subject}",
|
| 97 |
+
help=f"{subject} score"
|
| 98 |
+
)
|
| 99 |
+
if new_score != profile['last_scores'].get(subject, 100):
|
| 100 |
+
profile['last_scores'][subject] = new_score
|
| 101 |
+
|
| 102 |
+
# Checkboxes for weaknesses
|
| 103 |
+
st.markdown("##### ⚠️ Areas Needing Work")
|
| 104 |
+
col1, col2 = st.columns(2)
|
| 105 |
+
with col1:
|
| 106 |
+
math_weak = st.checkbox(
|
| 107 |
+
"Math weakness",
|
| 108 |
+
value=profile.get('math_weakness', False),
|
| 109 |
+
key="edit_math_weak"
|
| 110 |
+
)
|
| 111 |
+
profile['math_weakness'] = math_weak
|
| 112 |
+
|
| 113 |
+
with col2:
|
| 114 |
+
rushing = st.checkbox(
|
| 115 |
+
"Rushing on tests",
|
| 116 |
+
value=profile.get('rushing', False),
|
| 117 |
+
key="edit_rushing"
|
| 118 |
+
)
|
| 119 |
+
profile['rushing'] = rushing
|
| 120 |
+
|
| 121 |
+
# Feedback
|
| 122 |
+
if profile['recent_exam'] >= profile['goal']:
|
| 123 |
+
st.success(f"🎉 You've reached your goal! Consider setting a higher target.")
|
| 124 |
+
|
| 125 |
+
# Show sum of subject scores for reference
|
| 126 |
+
subject_total = sum(profile['last_scores'].values())
|
| 127 |
+
if abs(subject_total - profile['recent_exam']) > 50:
|
| 128 |
+
st.info(f"ℹ️ Subject scores total: {subject_total} (Recent exam: {profile['recent_exam']})")
|
| 129 |
+
|
| 130 |
+
# Family fields
|
| 131 |
+
elif category == "Family":
|
| 132 |
+
st.markdown("##### 👨👩👧 Family Tracking")
|
| 133 |
+
col1, col2 = st.columns(2)
|
| 134 |
+
|
| 135 |
+
# Initialize family metrics if not present
|
| 136 |
+
if 'family_checkins' not in profile:
|
| 137 |
+
profile['family_checkins'] = 2
|
| 138 |
+
if 'family_checkins_goal' not in profile:
|
| 139 |
+
profile['family_checkins_goal'] = 3
|
| 140 |
+
if 'last_contact_days' not in profile:
|
| 141 |
+
profile['last_contact_days'] = 3
|
| 142 |
+
if 'family_time_hours' not in profile:
|
| 143 |
+
profile['family_time_hours'] = 4.5
|
| 144 |
+
|
| 145 |
+
with col1:
|
| 146 |
+
checkin_goal = st.number_input(
|
| 147 |
+
"Weekly Check-ins Goal",
|
| 148 |
+
min_value=1,
|
| 149 |
+
max_value=10,
|
| 150 |
+
value=profile['family_checkins_goal'],
|
| 151 |
+
step=1,
|
| 152 |
+
key="edit_checkin_goal"
|
| 153 |
+
)
|
| 154 |
+
profile['family_checkins_goal'] = checkin_goal
|
| 155 |
+
|
| 156 |
+
checkins = st.number_input(
|
| 157 |
+
"Weekly Check-ins Completed",
|
| 158 |
+
min_value=0,
|
| 159 |
+
max_value=20, # High ceiling to allow exceeding goal
|
| 160 |
+
value=profile['family_checkins'],
|
| 161 |
+
step=1,
|
| 162 |
+
key="edit_checkins"
|
| 163 |
+
)
|
| 164 |
+
profile['family_checkins'] = checkins
|
| 165 |
+
|
| 166 |
+
with col2:
|
| 167 |
+
last_contact = st.number_input(
|
| 168 |
+
"Days Since Last Contact",
|
| 169 |
+
min_value=0,
|
| 170 |
+
max_value=365,
|
| 171 |
+
value=profile['last_contact_days'],
|
| 172 |
+
step=1,
|
| 173 |
+
key="edit_last_contact"
|
| 174 |
+
)
|
| 175 |
+
profile['last_contact_days'] = last_contact
|
| 176 |
+
|
| 177 |
+
family_time = st.number_input(
|
| 178 |
+
"Family Time (hrs/week)",
|
| 179 |
+
min_value=0.0,
|
| 180 |
+
max_value=40.0,
|
| 181 |
+
value=profile['family_time_hours'],
|
| 182 |
+
step=0.5,
|
| 183 |
+
key="edit_family_time"
|
| 184 |
+
)
|
| 185 |
+
profile['family_time_hours'] = family_time
|
| 186 |
+
|
| 187 |
+
# Positive feedback if over goal
|
| 188 |
+
if profile['family_checkins'] > profile['family_checkins_goal']:
|
| 189 |
+
extra = profile['family_checkins'] - profile['family_checkins_goal']
|
| 190 |
+
st.success(f"🎉 You did {extra} extra check-in(s) this week!")
|
| 191 |
+
|
| 192 |
+
# Friends fields
|
| 193 |
+
elif category == "Friends":
|
| 194 |
+
st.markdown("##### 👥 Social Tracking")
|
| 195 |
+
|
| 196 |
+
if 'social_budget' not in profile:
|
| 197 |
+
profile['social_budget'] = 150
|
| 198 |
+
if 'social_spent' not in profile:
|
| 199 |
+
profile['social_spent'] = 75
|
| 200 |
+
if 'hangouts_count' not in profile:
|
| 201 |
+
profile['hangouts_count'] = 4
|
| 202 |
+
|
| 203 |
+
col1, col2 = st.columns(2)
|
| 204 |
+
with col1:
|
| 205 |
+
social_budget = st.number_input(
|
| 206 |
+
"Social Budget ($)",
|
| 207 |
+
min_value=0,
|
| 208 |
+
max_value=1000,
|
| 209 |
+
value=profile['social_budget'],
|
| 210 |
+
step=10,
|
| 211 |
+
key="edit_social_budget"
|
| 212 |
+
)
|
| 213 |
+
profile['social_budget'] = social_budget
|
| 214 |
+
|
| 215 |
+
social_spent = st.number_input(
|
| 216 |
+
"Social Spending ($)",
|
| 217 |
+
min_value=0,
|
| 218 |
+
max_value=5000, # High ceiling
|
| 219 |
+
value=profile['social_spent'],
|
| 220 |
+
step=5,
|
| 221 |
+
key="edit_social_spent"
|
| 222 |
+
)
|
| 223 |
+
profile['social_spent'] = social_spent
|
| 224 |
+
|
| 225 |
+
with col2:
|
| 226 |
+
hangouts = st.number_input(
|
| 227 |
+
"Hangouts This Month",
|
| 228 |
+
min_value=0,
|
| 229 |
+
max_value=30,
|
| 230 |
+
value=profile['hangouts_count'],
|
| 231 |
+
step=1,
|
| 232 |
+
key="edit_hangouts"
|
| 233 |
+
)
|
| 234 |
+
profile['hangouts_count'] = hangouts
|
| 235 |
+
|
| 236 |
+
# Warning if over social budget
|
| 237 |
+
if profile['social_spent'] > profile['social_budget']:
|
| 238 |
+
over_amount = profile['social_spent'] - profile['social_budget']
|
| 239 |
+
st.warning(f"⚠️ You're ${over_amount} over your social budget!")
|
| 240 |
+
|
| 241 |
+
# Weekend/Vacation fields
|
| 242 |
+
elif category == "Weekend/Vacation":
|
| 243 |
+
st.markdown("##### ✈️ Vacation Planning")
|
| 244 |
+
|
| 245 |
+
if 'vacation_fund' not in profile:
|
| 246 |
+
profile['vacation_fund'] = 450
|
| 247 |
+
if 'vacation_goal' not in profile:
|
| 248 |
+
profile['vacation_goal'] = 1000
|
| 249 |
+
|
| 250 |
+
col1, col2 = st.columns(2)
|
| 251 |
+
with col1:
|
| 252 |
+
vac_fund = st.number_input(
|
| 253 |
+
"Vacation Fund ($)",
|
| 254 |
+
min_value=0,
|
| 255 |
+
max_value=5000,
|
| 256 |
+
value=profile['vacation_fund'],
|
| 257 |
+
step=50,
|
| 258 |
+
key="edit_vac_fund"
|
| 259 |
+
)
|
| 260 |
+
profile['vacation_fund'] = vac_fund
|
| 261 |
+
|
| 262 |
+
with col2:
|
| 263 |
+
vac_goal = st.number_input(
|
| 264 |
+
"Vacation Goal ($)",
|
| 265 |
+
min_value=500,
|
| 266 |
+
max_value=10000,
|
| 267 |
+
value=profile['vacation_goal'],
|
| 268 |
+
step=100,
|
| 269 |
+
key="edit_vac_goal"
|
| 270 |
+
)
|
| 271 |
+
profile['vacation_goal'] = vac_goal
|
| 272 |
+
|
| 273 |
+
# Global daily goals (shown in all categories)
|
| 274 |
+
st.markdown("---")
|
| 275 |
+
st.markdown("##### 🎯 Today's Goals Progress (%)")
|
| 276 |
+
cols = st.columns(3)
|
| 277 |
+
|
| 278 |
+
goal_types = ["Calories", "Money", "Steps"]
|
| 279 |
+
for i, goal_type in enumerate(goal_types):
|
| 280 |
+
with cols[i]:
|
| 281 |
+
new_progress = st.slider(
|
| 282 |
+
goal_type,
|
| 283 |
+
0, 100,
|
| 284 |
+
profile['goals_today'].get(goal_type, 50),
|
| 285 |
+
5,
|
| 286 |
+
key=f"edit_goal_{goal_type}"
|
| 287 |
+
)
|
| 288 |
+
profile['goals_today'][goal_type] = new_progress
|
| 289 |
+
|
| 290 |
+
# Contextual success message
|
| 291 |
+
has_warnings = False
|
| 292 |
+
if category == "Finance" and profile['spend'] > profile['budget']:
|
| 293 |
+
has_warnings = True
|
| 294 |
+
if category == "Friends" and profile['social_spent'] > profile['social_budget']:
|
| 295 |
+
has_warnings = True
|
| 296 |
+
|
| 297 |
+
if has_warnings:
|
| 298 |
+
st.info("💡 Changes saved! Check warnings above.")
|
| 299 |
+
else:
|
| 300 |
+
st.success("✅ All changes saved - looking good!")
|
| 301 |
+
|
| 302 |
+
# Reset button
|
| 303 |
+
if st.button("🔄 Reset to Defaults", type="secondary", use_container_width=True):
|
| 304 |
+
# Reset to default profile
|
| 305 |
+
from llm_agent import get_default_profile
|
| 306 |
+
default = get_default_profile()
|
| 307 |
+
for key, value in default.items():
|
| 308 |
+
profile[key] = value
|
| 309 |
+
st.rerun()
|
styles.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
styles.py - UPDATED
|
| 3 |
+
Clean black/white design with Thistle/Aqua accents for the name AQUA THISTLE
|
| 4 |
+
Improved chat container styling
|
| 5 |
+
UPDATED WITH CSS GAP FIX:
|
| 6 |
+
- Reduces .st-emotion-cache-wfksow gap (spacing between chart/buttons)
|
| 7 |
+
- Cleans up Quick Actions section layout
|
| 8 |
+
- Removes excess spacing
|
| 9 |
+
NEW FEATURES:
|
| 10 |
+
1. Proactive AI insights display (auto-generated at top)
|
| 11 |
+
2. Scenario comparison mode (side-by-side current vs scenario)
|
| 12 |
+
3. What-if mode (interactive sliders for testing)
|
| 13 |
+
4. Query detection for chart updates
|
| 14 |
+
5. All existing features preserved
|
| 15 |
+
|
| 16 |
+
PRESERVED FEATURES:
|
| 17 |
+
- Dynamic suggestions (never recycle)
|
| 18 |
+
- Topic tracking across conversation
|
| 19 |
+
- Category-specific visuals
|
| 20 |
+
- Profile editing integration
|
| 21 |
+
- All existing chart types
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def get_custom_css():
|
| 25 |
+
"""Modern black/white with purple/blue accents + gap fixes"""
|
| 26 |
+
return """
|
| 27 |
+
<style>
|
| 28 |
+
/* Better: Use CSS attribute selector to target flex containers */
|
| 29 |
+
/* UNIVERSAL: Target ANY div with gap: 1rem (regardless of class name) */
|
| 30 |
+
div[style*="gap: 1rem"],
|
| 31 |
+
[class*="st-emotion-cache"][style*="gap"],
|
| 32 |
+
[class*="st-emotion"][style*="flex"] {
|
| 33 |
+
gap: 0.5rem !important;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* Better: Use CSS attribute selector to target flex containers */
|
| 37 |
+
[style*="display: flex"][style*="gap: 1rem"] {
|
| 38 |
+
gap: 0.5rem !important;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/* Best: Override Streamlit's default container gaps */
|
| 42 |
+
[class*="st-emotion-cache"] {
|
| 43 |
+
gap: 0.5rem !important;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/* Override inline styles for flex containers */
|
| 47 |
+
[style*="display: flex"] {
|
| 48 |
+
gap: 0.5rem !important;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* Reduce column container gaps */
|
| 52 |
+
[class*="column"] {
|
| 53 |
+
gap: 0.5rem !important;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Button styling */
|
| 57 |
+
button {
|
| 58 |
+
font-size: 0.9rem;
|
| 59 |
+
padding: 0.5rem 1rem;
|
| 60 |
+
border-radius: 0.5rem;
|
| 61 |
+
border: 1px solid #e5e7eb;
|
| 62 |
+
background-color: #f9fafb;
|
| 63 |
+
color: #1f2937;
|
| 64 |
+
font-weight: 500;
|
| 65 |
+
transition: all 0.2s ease;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
button:hover {
|
| 69 |
+
background-color: #f3f4f6;
|
| 70 |
+
border-color: #d1d5db;
|
| 71 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
button:active {
|
| 75 |
+
background-color: #e5e7eb;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/* Chat messages */
|
| 79 |
+
.stChatMessage {
|
| 80 |
+
padding: 1rem;
|
| 81 |
+
margin-bottom: 0.5rem;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* Divider spacing */
|
| 85 |
+
hr {
|
| 86 |
+
margin-top: 1rem;
|
| 87 |
+
margin-bottom: 1rem;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
/* Metric cards */
|
| 91 |
+
.metric-card {
|
| 92 |
+
border-radius: 0.75rem;
|
| 93 |
+
padding: 1.5rem;
|
| 94 |
+
background-color: #f9fafb;
|
| 95 |
+
border: 1px solid #e5e7eb;
|
| 96 |
+
margin-bottom: 0.5rem;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/* Insight cards */
|
| 100 |
+
.insight-card {
|
| 101 |
+
border-radius: 0.5rem;
|
| 102 |
+
padding: 1rem;
|
| 103 |
+
margin-bottom: 0.5rem;
|
| 104 |
+
border-left: 4px solid #bf6dbf;
|
| 105 |
+
background-color: #faf9fc;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.insight-card.warning {
|
| 109 |
+
border-left-color: #ef4444;
|
| 110 |
+
background-color: #fef2f2;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/* Dashboard title */
|
| 114 |
+
h3 {
|
| 115 |
+
color: #1f2937;
|
| 116 |
+
font-weight: 600;
|
| 117 |
+
margin-bottom: 1rem;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* Main heading */
|
| 121 |
+
h1, h2 {
|
| 122 |
+
color: #0f172a;
|
| 123 |
+
font-weight: 700;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/* Text styling */
|
| 127 |
+
p, span {
|
| 128 |
+
color: #4b5563;
|
| 129 |
+
line-height: 1.6;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/* Links */
|
| 133 |
+
a {
|
| 134 |
+
color: #bf6dbf;
|
| 135 |
+
text-decoration: none;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
a:hover {
|
| 139 |
+
text-decoration: underline;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
/* Streamlit defaults override */
|
| 143 |
+
.stMarkdown {
|
| 144 |
+
color: #1f2937;
|
| 145 |
+
}
|
| 146 |
+
</style>
|
| 147 |
+
"""
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def get_metric_card_html(title, value, delta=None, icon="📊"):
|
| 151 |
+
"""Metric card with clean styling"""
|
| 152 |
+
delta_html = ""
|
| 153 |
+
if delta:
|
| 154 |
+
if "+" in str(delta) or "✅" in str(delta) or "🟢" in str(delta):
|
| 155 |
+
delta_color = "#10b981"
|
| 156 |
+
elif "⚠️" in str(delta) or "🔴" in str(delta):
|
| 157 |
+
delta_color = "#ef4444"
|
| 158 |
+
else:
|
| 159 |
+
delta_color = "#4a4a4a"
|
| 160 |
+
delta_html = f'<div style="font-size: 0.875rem; color: {delta_color}; margin-top: 0.25rem;">{delta}</div>'
|
| 161 |
+
|
| 162 |
+
return f'''
|
| 163 |
+
<div class="metric-card">
|
| 164 |
+
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem;">
|
| 165 |
+
<span style="font-size: 1.5rem;">{icon}</span>
|
| 166 |
+
<span style="font-size: 0.875rem; color: #6b7280; font-weight: 500;">{title}</span>
|
| 167 |
+
</div>
|
| 168 |
+
<div style="font-size: 1.75rem; font-weight: 700; color: #1f2937; margin-bottom: 0.25rem;">{value}</div>
|
| 169 |
+
{delta_html}
|
| 170 |
+
</div>
|
| 171 |
+
'''
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def get_insight_html(insight_text, card_type="insight"):
|
| 175 |
+
"""Insight card styling"""
|
| 176 |
+
icon = "💡" if card_type == "insight" else "⚠️"
|
| 177 |
+
card_class = f"insight-card {card_type}"
|
| 178 |
+
|
| 179 |
+
return f'''
|
| 180 |
+
<div class="{card_class}">
|
| 181 |
+
<div style="display: flex; align-items: flex-start; gap: 0.75rem;">
|
| 182 |
+
<span style="font-size: 1.25rem;">{icon}</span>
|
| 183 |
+
<div style="flex: 1;">
|
| 184 |
+
<p style="margin: 0; font-size: 0.9rem; color: #374151;">{insight_text}</p>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
'''
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def get_dashboard_header_css():
|
| 192 |
+
"""Additional CSS for dashboard header"""
|
| 193 |
+
return """
|
| 194 |
+
<style>
|
| 195 |
+
.dashboard-header {
|
| 196 |
+
padding: 1.5rem;
|
| 197 |
+
background: linear-gradient(135deg, #bf6dbf 0%, #5f5fba 100%);
|
| 198 |
+
border-radius: 1rem;
|
| 199 |
+
color: white;
|
| 200 |
+
margin-bottom: 2rem;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.dashboard-header h1 {
|
| 204 |
+
margin: 0;
|
| 205 |
+
color: white;
|
| 206 |
+
font-size: 2rem;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.dashboard-header p {
|
| 210 |
+
margin: 0.5rem 0 0 0;
|
| 211 |
+
opacity: 0.9;
|
| 212 |
+
}
|
| 213 |
+
</style>
|
| 214 |
+
"""
|