from typing import Dict def get_color(value: float, thresholds: tuple = (50, 25)) -> str: high, low = thresholds if value >= high: return "#4ade80" elif value >= low: return "#fb923c" else: return "#f87171" def get_diff_color(value: float) -> str: return "#4ade80" if value >= 0 else "#f87171" # Card Components def metric_card(title: str, value: str, color: str, subtitle: str = "") -> str: subtitle_html = ( f'
{subtitle}
' if subtitle else "" ) return f"""
{title}
{value}
{subtitle_html}
""" def info_row(label: str, value: str, color: str = "#fff", border: bool = True) -> str: border_style = "border-bottom: 1px solid #334155;" if border else "" return f"""
{label} {value}
""" def info_card(title: str, rows: list) -> str: rows_html = "".join(rows) return f"""

{title}

{rows_html}
""" # Summary Templates def build_validation_summary(data: Dict) -> str: """Build summary HTML for validation mode (when actual data exists).""" year = data["year"] semester_name = data["semester_name"] class_capacity = data["class_capacity"] data_source = data.get("data_source", "kalkulasi") # Metrics class_accuracy_pct = data.get("class_accuracy_pct", 0) class_within_one_pct = data.get("class_within_one_pct", 0) total_classes = data.get("total_classes", 0) comparison_mae = data.get("comparison_mae", 0) comparison_rmse = data.get("comparison_rmse", 0) total_for_class_accuracy = data.get("total_for_class_accuracy", 0) # Enrollment metrics total_actual = data.get("total_actual", 0) total_predicted = data.get("total_predicted", 0) accuracy_pct = data.get("accuracy_pct", 0) class_matches = data.get("class_matches", 0) class_within_one = data.get("class_within_one", 0) # Colors class_accuracy_color = get_color(class_accuracy_pct) diff_color = get_diff_color(total_predicted - total_actual) return f"""

{ year } Semester {semester_name}

Validasi prediksi terhadap data aktual | Kapasitas per kelas: { class_capacity } mahasiswa | Sumber kelas aktual: {data_source}

{ metric_card( "Akurasi Kelas", f"{class_accuracy_pct:.1f}%", class_accuracy_color, f"±1 kelas: {class_within_one_pct:.1f}%", ) } {metric_card("Total Kelas Prediksi", str(total_classes), "#60a5fa")} { metric_card( "MAE / RMSE", f"{comparison_mae:.1f} / {comparison_rmse:.1f}", "#a78bfa" ) } {metric_card("MK Divalidasi", str(total_for_class_accuracy), "#fb923c")}
{ info_card( "Ringkasan Enrollment", [ info_row("Total Aktual", str(int(total_actual))), info_row("Total Prediksi", str(int(total_predicted))), info_row( "Selisih", f"{int(total_predicted - total_actual):+d}", diff_color, border=False, ), ], ) } { info_card( f"Akurasi Prediksi Kelas (dari {data_source})", [ info_row( "Kelas Tepat", f"{class_matches}/{total_for_class_accuracy}", "#4ade80", ), info_row( "Selisih ±1 Kelas", f"{class_within_one}/{total_for_class_accuracy}", "#60a5fa", ), info_row("Akurasi Enrollment", f"{accuracy_pct:.1f}%", border=False), ], ) }
""" def build_no_match_summary(data: Dict) -> str: year = data["year"] semester_name = data["semester_name"] metrics = data.get("metrics", {"mae": 0, "rmse": 0}) total_to_open = data.get("total_to_open", 0) total_classes = data.get("total_classes", 0) return f"""

{year} Semester {semester_name}

Data semester ada, tetapi tidak ditemukan MK pilihan yang cocok

{metric_card("MAE (Backtest)", f"{metrics['mae']:.2f}", "#60a5fa")} {metric_card("RMSE (Backtest)", f"{metrics['rmse']:.2f}", "#a78bfa")} {metric_card("MK Dibuka", str(total_to_open), "#4ade80")} {metric_card("Total Kelas", str(total_classes), "#fb923c")}
""" def build_future_prediction_summary(data: Dict) -> str: year = data["year"] semester_name = data["semester_name"] class_capacity = data["class_capacity"] metrics = data.get("metrics", {"mae": 0, "rmse": 0}) total_to_open = data.get("total_to_open", 0) total_classes = data.get("total_classes", 0) total_predicted_students = data.get("total_predicted_students", 0) total_capacity = data.get("total_capacity", 0) avg_utilization = ( (total_predicted_students / total_capacity * 100) if total_capacity > 0 else 0 ) return f"""

{ year } Semester {semester_name}

Prediksi masa depan berdasarkan tren historis | Kapasitas per kelas: { class_capacity } mahasiswa

{metric_card("MK Dibuka", str(total_to_open), "#4ade80")} {metric_card("Total Kelas Dibuka", str(total_classes), "#60a5fa")} {metric_card("Prediksi Mahasiswa", str(total_predicted_students), "#a78bfa")} {metric_card("Total Kuota", str(total_capacity), "#fb923c")}
{ info_card( "Backtest Metrics", [ info_row("MAE", f"{metrics['mae']:.2f}"), info_row("RMSE", f"{metrics['rmse']:.2f}", border=False), ], ) } { info_card( "Kapasitas Info", [ info_row("Kapasitas/Kelas", f"{class_capacity} mhs"), info_row("Avg Utilization", f"{avg_utilization:.1f}%", border=False), ], ) }
""" def build_prediction_summary(data: Dict) -> str: has_actual_data = data.get("has_actual_data", False) if has_actual_data: if "comparison_mae" in data: return build_validation_summary(data) else: return build_no_match_summary(data) else: return build_future_prediction_summary(data) def build_multi_year_summary(data: Dict) -> str: year = data["year"] years_ahead = data["years_ahead"] semester_name = data["semester_name"] class_capacity = data["class_capacity"] first_year_classes = data["first_year_classes"] last_year_classes = data["last_year_classes"] growth_classes = data["growth_classes"] growth_students = data["growth_students"] growth_class_color = get_diff_color(growth_classes) growth_student_color = get_diff_color(growth_students) return f"""

Proyeksi {years_ahead} Tahun ke Depan - Semester {semester_name}

Forecasting kebutuhan kelas {year} - {year + years_ahead} | Kapasitas per kelas: {class_capacity} mahasiswa

{metric_card(f"Kelas ({year})", str(first_year_classes), "#4ade80")} {metric_card(f"Kelas ({year + years_ahead})", str(last_year_classes), "#60a5fa")} {metric_card("Pertumbuhan Kelas", f"{growth_classes:+d}", growth_class_color)} {metric_card("Pertumbuhan Mhs", f"{growth_students:+d}", growth_student_color)}
""" # Placeholder Templates def placeholder_card(title: str, subtitle: str) -> str: return f"""

{title}

{subtitle}

""" def get_prediction_placeholder() -> str: return placeholder_card( "Pilih tahun dan semester", "Klik Generate Predictions untuk melihat rekomendasi jumlah kelas", ) def get_forecast_placeholder() -> str: return placeholder_card( "Proyeksi Multi-Tahun", "Lihat tren kebutuhan kelas beberapa tahun ke depan" ) # Data Info Component def build_data_info(data: Dict) -> str: if "error" in data: return f"

{data['error']}

" return f"""
Total MK
{data["total_courses"]}
MK Pilihan
{data["elective_courses"]}
Kapasitas/Kelas
{data["class_capacity"]}
Tahun Data
{data["year_min"]}-{data["year_max"]}
"""