SHELLAPANDIANGANHUNGING commited on
Commit
614588f
·
verified ·
1 Parent(s): 9277ad9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +184 -139
app.py CHANGED
@@ -1667,216 +1667,261 @@ st.subheader("OBJECTIVE 6: Instant Insights & Recommendations")
1667
  # Membagi tampilan menjadi dua kolom
1668
  col_insights, col_recs = st.columns(2)
1669
 
1670
- # Kolom kiri: Insights by Advanced Analytics
 
 
1671
  with col_insights:
1672
  st.subheader("Insights by Advanced Analytics")
1673
 
1674
- # 1. Critical Hour Analysis (2-5 AM)
1675
  critical_hours = [2, 3, 4, 5]
1676
  critical_alerts = df[df['hour'].isin(critical_hours)]
1677
  critical_pct = (len(critical_alerts) / len(df)) * 100 if len(df) > 0 else 0
1678
 
1679
  st.markdown(f"**Critical Hour Risk (3-6 AM)**")
1680
- bg_color = "#ffcccc" if critical_pct > 50 else "#ffebcc" if critical_pct > 25 else "#ffffcc" if critical_pct > 10 else "#e6ffe6"
1681
- st.markdown(f'<div style="background-color: {bg_color}; padding: 10px; border-radius: 5px;">Critical Hour Alerts: {len(critical_alerts)} ({critical_pct:.1f}% of total alerts)</div>', unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
1682
  if critical_pct > 10:
1683
- st.warning(f"High risk: {critical_pct:.1f}% of fatigue alerts occur during critical hours (3-6 AM). This is a known circadian dip period.")
 
 
 
1684
  else:
1685
- st.info(f"{critical_pct:.1f}% of alerts occur during critical hours. This is within acceptable range.")
 
 
1686
 
1687
- # 2. High-Speed Fatigue Analysis (Environmental Risk)
1688
  if col_speed and col_speed in df.columns:
 
1689
  high_speed_threshold = 20
1690
- high_speed_fatigue = df[df[col_speed] >= high_speed_threshold] if high_speed_threshold > 0 else pd.DataFrame()
1691
  high_speed_pct = (len(high_speed_fatigue) / len(df)) * 100 if len(df) > 0 else 0
1692
-
1693
- st.markdown(f"**High-Speed Fatigue Risk (Speed > {high_speed_threshold:.0f} km/h)**")
1694
- # Ganti st.metric dengan HTML custom
1695
- st.markdown(f"""
1696
- <div style="font-size: 24px; font-weight: bold;">{len(high_speed_fatigue)}</div>
1697
- <div style="color: red; font-size: 14px; margin-top: -5px;">↑ {high_speed_pct:.1f}% of total alerts</div>
1698
- """, unsafe_allow_html=True)
 
 
 
1699
  if high_speed_pct > 20:
1700
- st.warning(f"High risk: {high_speed_pct:.1f}% of fatigue alerts occur at high speeds. This increases accident severity potential.")
 
 
 
1701
  else:
1702
- st.info(f"{high_speed_pct:.1f}% of alerts occur at high speeds. This is within acceptable range.")
 
 
 
1703
  else:
1704
  st.info("Speed data not available for High-Speed Fatigue Analysis.")
1705
- # 3.Objective
1706
- if col_shift and col_shift in df.columns:
1707
- shift_counts = df[col_shift].value_counts()
1708
 
 
 
 
 
1709
  st.markdown(f"**Shift Pattern Risk**")
 
1710
  for shift_val in shift_counts.index:
1711
  shift_pct = (shift_counts[shift_val] / len(df)) * 100
1712
- # Ganti st.metric dengan HTML custom
1713
- st.markdown(f"""<div style="font-size: 24px; font-weight: bold;">{shift_counts[shift_val]}</div><div style="color: red; font-size: 14px; margin-top: -5px;">↑ {shift_pct:.1f}% of total alerts</div>""", unsafe_allow_html=True)
 
 
 
 
 
 
1714
 
1715
  if shift_pct > 50:
1716
- st.warning(f"Shift {shift_val} has disproportionately high alerts ({shift_pct:.1f}%). Review shift scheduling and workload.")
 
 
 
1717
  else:
1718
- st.info(f"Shift {shift_val} alert distribution is acceptable ({shift_pct:.1f}%).")
 
 
 
1719
  else:
1720
  st.info("Shift data not available for Shift Pattern Analysis.")
1721
 
 
 
1722
 
1723
- # 4. Operator Risk Profiling
1724
- # 4. Operator Risk Profiling
1725
- if col_operator and col_operator in df.columns:
1726
- operator_alerts = df[col_operator].value_counts()
1727
- top_risk_operators = operator_alerts.head(5) # Top 5 operators by alerts
1728
-
1729
- st.markdown("**High-Risk Operator Identification**")
1730
 
1731
- # Warna berdasarkan ranking 1–5
1732
- colors = ["#d32f2f", "#e57373", "#ef9a9a", "#ffcdd2", "#ffe1e4"]
1733
 
1734
- for idx, (op_name, count) in enumerate(top_risk_operators.items()):
1735
- op_pct = (count / len(df)) * 100
1736
- color = colors[idx] if idx < len(colors) else colors[-1]
1737
 
1738
- # Teks normal untuk nama dan jumlah alert
1739
- st.markdown(f"**Operator:** {op_name} \n**Alerts:** {count}")
1740
-
1741
- # Hanya 'Share' yang berwarna sesuai ranking
1742
- st.markdown(
1743
- f"<span style='font-weight:600'>Share:</span> "
1744
- f"<span style='color:{color}; font-weight:700'>{op_pct:.1f}% of total alerts</span>",
1745
- unsafe_allow_html=True
1746
- )
1747
-
1748
- # Risk message (tetap gunakan component Streamlit agar konsisten)
1749
- if op_pct > 5:
1750
- st.warning(f"Operator {op_name} has high fatigue risk ({op_pct:.1f}%). Consider coaching or rest plan.")
1751
- else:
1752
- st.info(f"Operator {op_name} fatigue risk is within acceptable range ({op_pct:.1f}%).")
1753
 
1754
- else:
1755
- st.info("Operator data not available for Operator Risk Profiling.")
 
 
 
 
 
 
 
1756
 
 
 
1757
 
1758
 
1759
- # Kolom kanan: AI Recommendations
 
 
1760
  with col_recs:
 
1761
  st.subheader("Recommendations")
1762
  ai_recs = []
1763
  insights_found = []
1764
 
1765
- # Peak hour
1766
  if "hour" in df.columns and not df.empty:
1767
  peak_hour = df["hour"].value_counts().idxmax()
1768
  critical_hours = [2, 3, 4, 5]
 
1769
  if peak_hour in critical_hours:
1770
- insights_found.append(f" Most fatigue risk occurs at **{peak_hour}:00** — during critical circadian low period (3-6 AM). Consider enhanced monitoring.")
 
 
1771
  else:
1772
- insights_found.append(f"Most fatigue risk occurs at **{peak_hour}:00** — likely due to circadian drop.")
 
 
1773
 
1774
- # Risk shift
1775
- if col_shift and not df.empty:
1776
  worst_shift = df[col_shift].value_counts().idxmax()
1777
- insights_found.append(f" Highest fatigue recorded in **Shift {worst_shift}** — review scheduling & workload.")
 
 
1778
 
1779
- # Worst operator
1780
- if col_operator and not df.empty:
1781
  worst_operator = df[col_operator].value_counts().idxmax()
1782
- insights_found.append(f" Operator at highest risk: **{worst_operator}** — suggested coaching or rest plan.")
 
 
1783
 
1784
- # Duration risk
1785
  if "duration_sec" in df.columns and not df.empty:
1786
  avg_duration = df["duration_sec"].mean()
1787
- if not pd.isna(avg_duration) and avg_duration > 10:
1788
- insights_found.append(" Long fatigue event duration suggests slow response — improve alerting training.")
 
 
1789
 
1790
- # Generate recommendations based on found insights
1791
  if insights_found:
1792
- if any("circadian low" in i.lower() for i in insights_found):
 
1793
  ai_recs.append({
1794
- "recommendation": "Deploy enhanced fatigue monitoring systems (e.g., EOR) specifically during 3-6 AM shifts.",
1795
- "data_point": f"Critical Hour Alerts: {len(critical_alerts)} ({critical_pct:.1f}% of total alerts)",
1796
- "reason": "High percentage of alerts occurring during the known circadian low period (3-6 AM) indicates increased risk during these hours."
1797
  })
 
1798
  if any("shift" in i.lower() for i in insights_found):
1799
  ai_recs.append({
1800
- "recommendation": "Review shift rotation schedules to minimize consecutive high-risk shifts.",
1801
- "data_point": f"Shift {worst_shift} Alerts: {df[col_shift].value_counts()[worst_shift]} ({(df[col_shift].value_counts()[worst_shift] / len(df)) * 100:.1f}% of total alerts)",
1802
- "reason": f"The identified high-risk shift ({worst_shift}) has the highest number of fatigue alerts, suggesting scheduling or workload issues."
1803
  })
 
1804
  if any("operator" in i.lower() for i in insights_found):
1805
  ai_recs.append({
1806
- "recommendation": "Initiate individual coaching or mandatory rest periods for high-risk operators.",
1807
- "data_point": f"Operator {worst_operator} Alerts: {df[col_operator].value_counts()[worst_operator]} ({(df[col_operator].value_counts()[worst_operator] / len(df)) * 100:.1f}% of total alerts)",
1808
- "reason": f"The identified high-risk operator ({worst_operator}) has the highest number of fatigue alerts, indicating a need for targeted intervention."
1809
  })
 
1810
  if any("duration" in i.lower() for i in insights_found):
1811
  ai_recs.append({
1812
- "recommendation": "Review and improve alert response protocols and training.",
1813
- "data_point": f"Average Fatigue Event Duration: {avg_duration:.2f} seconds",
1814
- "reason": "Long average duration suggests potential delays in response time or alert acknowledgment, requiring protocol review."
1815
- })
1816
- if any("high-speed" in i.lower() for i in insights_found):
1817
- ai_recs.append({
1818
- "recommendation": "Implement speed management strategies in conjunction with fatigue monitoring.",
1819
- "data_point": f"High-Speed Fatigue Events: {len(high_speed_fatigue)} ({high_speed_pct:.1f}% of total alerts)",
1820
- "reason": "A significant percentage of alerts occur at high speeds, increasing accident severity risk. Speed control is crucial."
1821
- })
1822
- if not ai_recs:
1823
- ai_recs.append({
1824
- "recommendation": "Data quality is sufficient. Focus on implementing recommendations from Objectives 1-5.",
1825
- "data_point": "General Data Quality Check",
1826
- "reason": "No specific high-impact insights were automatically identified from the aggregated data in this section."
1827
  })
1828
 
 
 
 
1829
  for rec in ai_recs:
1830
- # Ambil data_point dan ganti teks persentase di dalamnya menjadi warna merah
1831
- data_point_text = rec['data_point']
1832
- # Ganti pola persentase (X.X%) dengan span warna merah
1833
- import re
1834
- # Cari pola seperti "1.6%", "21.2%", dll.
1835
- data_point_colored = re.sub(r'(\d+\.?\d*%)', r'<span style="color: red;">\1</span>', data_point_text)
1836
-
1837
- # Ambil reason dan lakukan hal yang sama
1838
- reason_text = rec['reason']
1839
- reason_colored = re.sub(r'(\d+\.?\d*%)', r'<span style="color: red;">\1</span>', reason_text)
1840
-
1841
- # Tampilkan rekomendasi dengan teks persentase berwarna merah
1842
- st.markdown(f"""
1843
- <div style="
1844
- background: #f8f9fa;
1845
- border: 1px solid #dee2e6;
1846
- border-radius: 8px;
1847
- padding: 15px;
1848
- margin: 10px 0;
1849
- color: #2c3e50;
1850
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1851
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
1852
- display: flex;
1853
- flex-direction: column;
1854
- justify-content: space-between;
1855
- ">
1856
  <div style="
1857
- font-weight: bold;
1858
- color: #2c3e50;
1859
- margin-bottom: 8px;
1860
- font-size: 14px;
1861
- background: #e9ecef;
1862
- padding: 8px;
1863
- border-radius: 5px;
1864
- border-left: 4px solid #495057;
1865
- ">AI Recommendation</div>
1866
- <div style="padding-top: 8px; font-size: 14px; margin-bottom: 10px;">
1867
- <strong>Action:</strong> {rec['recommendation']}
1868
- </div>
1869
- <div style="font-size: 12px; padding: 8px; background: #e9ecef; border-radius: 5px; margin-top: 5px;">
1870
- <strong>Data Point:</strong> {data_point_colored}
1871
- </div>
1872
- <div style="font-size: 12px; padding: 8px; background: #f1f1f1; border-radius: 5px; margin-top: 5px;">
1873
- <strong>AI Reasoning:</strong> {reason_colored}
1874
  </div>
1875
- </div>
1876
- """, unsafe_allow_html=True)
 
 
1877
  else:
1878
- st.info("No specific data points available for AI recommendations. Ensure relevant columns (hour, shift, operator, duration, speed) are present and populated.")
 
 
 
1879
 
1880
  # ================= FOOTER ===========================
1881
  st.markdown("---")
1882
- st.markdown('<div class="footer">FatigueAnalyzer - Transforming Mining Safety with Intelligent Analytics | Contact: info@bukittechnology.com</div>', unsafe_allow_html=True)
 
 
 
 
1667
  # Membagi tampilan menjadi dua kolom
1668
  col_insights, col_recs = st.columns(2)
1669
 
1670
+ # =====================================================================
1671
+ # 🔹 KOLOM KIRI — INSIGHTS BY ADVANCED ANALYTICS
1672
+ # =====================================================================
1673
  with col_insights:
1674
  st.subheader("Insights by Advanced Analytics")
1675
 
1676
+ # ===================== 1. Critical Hour Analysis =====================
1677
  critical_hours = [2, 3, 4, 5]
1678
  critical_alerts = df[df['hour'].isin(critical_hours)]
1679
  critical_pct = (len(critical_alerts) / len(df)) * 100 if len(df) > 0 else 0
1680
 
1681
  st.markdown(f"**Critical Hour Risk (3-6 AM)**")
1682
+ bg_color = (
1683
+ "#ffcccc" if critical_pct > 50 else
1684
+ "#ffebcc" if critical_pct > 25 else
1685
+ "#ffffcc" if critical_pct > 10 else
1686
+ "#e6ffe6"
1687
+ )
1688
+ st.markdown(
1689
+ f'<div style="background-color: {bg_color}; padding: 10px; border-radius: 5px;">'
1690
+ f'Critical Hour Alerts: {len(critical_alerts)} ({critical_pct:.1f}% of total alerts)</div>',
1691
+ unsafe_allow_html=True
1692
+ )
1693
+
1694
  if critical_pct > 10:
1695
+ st.warning(
1696
+ f"High risk: {critical_pct:.1f}% of fatigue alerts occur during critical hours (3-6 AM). "
1697
+ f"This is a known circadian dip period."
1698
+ )
1699
  else:
1700
+ st.info(
1701
+ f"{critical_pct:.1f}% of alerts occur during critical hours. This is within acceptable range."
1702
+ )
1703
 
1704
+ # ===================== 2. High-Speed Fatigue Analysis =====================
1705
  if col_speed and col_speed in df.columns:
1706
+
1707
  high_speed_threshold = 20
1708
+ high_speed_fatigue = df[df[col_speed] >= high_speed_threshold]
1709
  high_speed_pct = (len(high_speed_fatigue) / len(df)) * 100 if len(df) > 0 else 0
1710
+
1711
+ st.markdown(f"**High-Speed Fatigue Risk (Speed > {high_speed_threshold} km/h)**")
1712
+ st.markdown(
1713
+ f"""
1714
+ <div style="font-size: 24px; font-weight: bold;">{len(high_speed_fatigue)}</div>
1715
+ <div style="color: red; font-size: 14px; margin-top: -5px;">↑ {high_speed_pct:.1f}% of total alerts</div>
1716
+ """,
1717
+ unsafe_allow_html=True
1718
+ )
1719
+
1720
  if high_speed_pct > 20:
1721
+ st.warning(
1722
+ f"High risk: {high_speed_pct:.1f}% of fatigue alerts occur at high speeds. "
1723
+ f"This increases accident severity potential."
1724
+ )
1725
  else:
1726
+ st.info(
1727
+ f"{high_speed_pct:.1f}% of alerts occur at high speeds. This is within acceptable range."
1728
+ )
1729
+
1730
  else:
1731
  st.info("Speed data not available for High-Speed Fatigue Analysis.")
 
 
 
1732
 
1733
+ # ===================== 3. Shift Pattern Analysis =====================
1734
+ if col_shift and col_shift in df.columns:
1735
+
1736
+ shift_counts = df[col_shift].value_counts()
1737
  st.markdown(f"**Shift Pattern Risk**")
1738
+
1739
  for shift_val in shift_counts.index:
1740
  shift_pct = (shift_counts[shift_val] / len(df)) * 100
1741
+
1742
+ st.markdown(
1743
+ f"""
1744
+ <div style="font-size: 24px; font-weight: bold;">{shift_counts[shift_val]}</div>
1745
+ <div style="color: red; font-size: 14px; margin-top: -5px;">↑ {shift_pct:.1f}% of total alerts</div>
1746
+ """,
1747
+ unsafe_allow_html=True
1748
+ )
1749
 
1750
  if shift_pct > 50:
1751
+ st.warning(
1752
+ f"Shift {shift_val} has disproportionately high alerts ({shift_pct:.1f}%). "
1753
+ f"Review shift scheduling and workload."
1754
+ )
1755
  else:
1756
+ st.info(
1757
+ f"Shift {shift_val} alert distribution is acceptable ({shift_pct:.1f}%)."
1758
+ )
1759
+
1760
  else:
1761
  st.info("Shift data not available for Shift Pattern Analysis.")
1762
 
1763
+ # ===================== 4. Operator Risk Profiling =====================
1764
+ if col_operator and col_operator in df.columns:
1765
 
1766
+ operator_alerts = df[col_operator].value_counts()
1767
+ top_risk_operators = operator_alerts.head(5)
 
 
 
 
 
1768
 
1769
+ st.markdown("**High-Risk Operator Identification**")
1770
+ colors = ["#d32f2f", "#e57373", "#ef9a9a", "#ffcdd2", "#ffe1e4"]
1771
 
1772
+ for idx, (op_name, count) in enumerate(top_risk_operators.items()):
1773
+ op_pct = (count / len(df)) * 100
1774
+ color = colors[idx] if idx < len(colors) else colors[-1]
1775
 
1776
+ st.markdown(
1777
+ f"**Operator:** {op_name} \n**Alerts:** {count}"
1778
+ )
1779
+ st.markdown(
1780
+ f"<span style='font-weight:600'>Share:</span> "
1781
+ f"<span style='color:{color}; font-weight:700'>{op_pct:.1f}% of total alerts</span>",
1782
+ unsafe_allow_html=True
1783
+ )
 
 
 
 
 
 
 
1784
 
1785
+ if op_pct > 5:
1786
+ st.warning(
1787
+ f"Operator {op_name} has high fatigue risk ({op_pct:.1f}%). "
1788
+ f"Consider coaching or rest plan."
1789
+ )
1790
+ else:
1791
+ st.info(
1792
+ f"Operator {op_name} fatigue risk is within acceptable range ({op_pct:.1f}%)."
1793
+ )
1794
 
1795
+ else:
1796
+ st.info("Operator data not available for Operator Risk Profiling.")
1797
 
1798
 
1799
+ # =====================================================================
1800
+ # 🔹 KOLOM KANAN — AI RECOMMENDATIONS
1801
+ # =====================================================================
1802
  with col_recs:
1803
+
1804
  st.subheader("Recommendations")
1805
  ai_recs = []
1806
  insights_found = []
1807
 
1808
+ # Peak Hour
1809
  if "hour" in df.columns and not df.empty:
1810
  peak_hour = df["hour"].value_counts().idxmax()
1811
  critical_hours = [2, 3, 4, 5]
1812
+
1813
  if peak_hour in critical_hours:
1814
+ insights_found.append(
1815
+ f"Most fatigue risk occurs at **{peak_hour}:00** — during critical circadian low period (3-6 AM)."
1816
+ )
1817
  else:
1818
+ insights_found.append(
1819
+ f"Most fatigue risk occurs at **{peak_hour}:00** — likely due to circadian drop."
1820
+ )
1821
 
1822
+ # Risk Shift
1823
+ if col_shift and col_shift in df.columns and not df.empty:
1824
  worst_shift = df[col_shift].value_counts().idxmax()
1825
+ insights_found.append(
1826
+ f"Highest fatigue recorded in **Shift {worst_shift}** — review scheduling & workload."
1827
+ )
1828
 
1829
+ # Worst Operator
1830
+ if col_operator and col_operator in df.columns and not df.empty:
1831
  worst_operator = df[col_operator].value_counts().idxmax()
1832
+ insights_found.append(
1833
+ f"Operator at highest risk: **{worst_operator}** — suggested coaching or rest plan."
1834
+ )
1835
 
1836
+ # Duration Risk
1837
  if "duration_sec" in df.columns and not df.empty:
1838
  avg_duration = df["duration_sec"].mean()
1839
+ if avg_duration > 10:
1840
+ insights_found.append(
1841
+ "Long fatigue event duration suggests slow response — improve alerting training."
1842
+ )
1843
 
1844
+ # ===================== AI DECISION ENGINE =====================
1845
  if insights_found:
1846
+
1847
+ if any("circadian" in i.lower() for i in insights_found):
1848
  ai_recs.append({
1849
+ "recommendation": "Deploy enhanced fatigue monitoring systems during 3-6 AM.",
1850
+ "data_point": f"Critical Hour Alerts: {len(critical_alerts)} ({critical_pct:.1f}%)",
1851
+ "reason": "High percentage of alerts during circadian low period."
1852
  })
1853
+
1854
  if any("shift" in i.lower() for i in insights_found):
1855
  ai_recs.append({
1856
+ "recommendation": "Review shift rotation schedules.",
1857
+ "data_point": f"Shift {worst_shift}: {df[col_shift].value_counts()[worst_shift]} alerts",
1858
+ "reason": "This shift shows highest fatigue alerts."
1859
  })
1860
+
1861
  if any("operator" in i.lower() for i in insights_found):
1862
  ai_recs.append({
1863
+ "recommendation": "Coaching or mandatory rest for the identified high-risk operator.",
1864
+ "data_point": f"Operator {worst_operator}: {df[col_operator].value_counts()[worst_operator]} alerts",
1865
+ "reason": "Operator has highest fatigue alerts."
1866
  })
1867
+
1868
  if any("duration" in i.lower() for i in insights_found):
1869
  ai_recs.append({
1870
+ "recommendation": "Improve fatigue alert response training.",
1871
+ "data_point": f"Avg Duration: {avg_duration:.1f} sec",
1872
+ "reason": "Long fatigue event duration indicates slow response."
 
 
 
 
 
 
 
 
 
 
 
 
1873
  })
1874
 
1875
+ # Render all recommendations
1876
+ import re
1877
+
1878
  for rec in ai_recs:
1879
+
1880
+ data_point_colored = re.sub(
1881
+ r'(\d+\.?\d*%)',
1882
+ r'<span style="color: red;">\1</span>',
1883
+ rec['data_point']
1884
+ )
1885
+
1886
+ reason_colored = re.sub(
1887
+ r'(\d+\.?\d*%)',
1888
+ r'<span style="color: red;">\1</span>',
1889
+ rec['reason']
1890
+ )
1891
+
1892
+ st.markdown(
1893
+ f"""
 
 
 
 
 
 
 
 
 
 
 
1894
  <div style="
1895
+ background: #f8f9fa;
1896
+ border: 1px solid #dee2e6;
1897
+ border-radius: 8px;
1898
+ padding: 15px;
1899
+ margin: 10px 0;
1900
+ ">
1901
+ <div style="font-weight: bold; background: #e9ecef; padding: 8px; border-radius: 5px;">
1902
+ AI Recommendation
1903
+ </div>
1904
+ <div style="padding-top: 8px;"><strong>Action:</strong> {rec['recommendation']}</div>
1905
+ <div style="padding: 8px; background: #e9ecef; border-radius: 5px;">
1906
+ <strong>Data Point:</strong> {data_point_colored}
1907
+ </div>
1908
+ <div style="padding: 8px; background: #f1f1f1; border-radius: 5px;">
1909
+ <strong>AI Reasoning:</strong> {reason_colored}
1910
+ </div>
 
1911
  </div>
1912
+ """,
1913
+ unsafe_allow_html=True
1914
+ )
1915
+
1916
  else:
1917
+ st.info(
1918
+ "No specific data points available for AI recommendations. "
1919
+ "Ensure relevant columns are present (hour, shift, operator, duration, speed)."
1920
+ )
1921
 
1922
  # ================= FOOTER ===========================
1923
  st.markdown("---")
1924
+ st.markdown(
1925
+ '<div class="footer">FatigueAnalyzer - Transforming Mining Safety with Intelligent Analytics | Contact: info@bukittechnology.com</div>',
1926
+ unsafe_allow_html=True
1927
+ )