Dhom1 commited on
Commit
98bb40e
ยท
verified ยท
1 Parent(s): 1824090

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +73 -46
src/streamlit_app.py CHANGED
@@ -1,11 +1,11 @@
1
  """
2
- Health Matrix โ€“ AI Command Center (Enhanced UI/UX)
3
  Single-file Streamlit app with embedded CSS + UKG helpers.
4
 
5
  Front-end upgrades (layout/styling ONLY):
6
- - Responsive Hero + Business Case
7
- - Accessible Status Bar (ARIA live)
8
- - Agent Runline (vertical, scrollable, animated)
9
  - KPI cards with micro-interactions
10
  - Readable tables: sticky headers, zebra rows; horizontal scroll support
11
  - Loading skeletons, friendly empty/error states
@@ -17,7 +17,6 @@ Usage: streamlit run streamlit_app.py
17
 
18
  import os
19
  import json
20
- import pathlib
21
  import time
22
  from typing import List, Optional, Iterable, Dict, Any
23
  from io import StringIO
@@ -43,6 +42,10 @@ _EMBEDDED_CSS = """
43
  --t1:#0F172A; --t2:#1F2937; --t3:#334155; --muted:#64748B;
44
  --blue:#004C97; --green:#36BA01; --ok:#16a34a; --warn:#D97706; --fail:#DC2626;
45
  }
 
 
 
 
46
  html,body{margin:0;padding:0;height:100%;background:var(--bg);color:var(--t1);
47
  font-family:'Segoe UI','Cairo','Tajawal','Open Sans',sans-serif;}
48
  .container{max-width:1200px;margin:0 auto;padding:24px}
@@ -70,7 +73,7 @@ p,li{color:var(--t2);font-size:1.05em}
70
  -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;
71
  background-size:200% 100%;animation:aiTitle 8s ease-in-out infinite;
72
  }
73
- .hero p{font-size:1rem;line-height:1.6;color:var(--t2);max-width:760px;margin:0 auto}
74
  .hero::before,.hero::after{content:"";position:absolute;inset:-20%;pointer-events:none;filter:blur(42px);opacity:.18}
75
  .hero::before{
76
  background:
@@ -114,24 +117,26 @@ p,li{color:var(--t2);font-size:1.05em}
114
  .stat-label{color:#334155;font-size:0.95rem}
115
  .stat-value{font-size:1.7rem;font-weight:900;color:var(--t1)}
116
 
117
- /* === Agent Runline (vertical execution log) === */
118
- .run-wrap{margin:10px 0 20px;max-height:360px;overflow-y:auto;padding-right:6px}
119
- .run-legend{display:flex;gap:8px;flex-wrap:wrap;margin:6px 0 10px}
120
- .run-legend .chip{font-size:12px;padding:6px 10px;border:1px solid var(--line);border-radius:999px;background:#fff}
121
- .run{position:relative;margin:0;padding:0 0 0 18px;list-style:none}
122
- .run::before{content:"";position:absolute;left:6px;top:0;bottom:0;width:2px;background:#E2E8F0}
123
- .run li{position:relative;margin:0 0 12px;padding:8px 12px;background:#fff;border:1px solid var(--line);
124
- border-radius:12px;box-shadow:0 6px 14px rgba(0,0,0,.05);opacity:0;animation:fadeIn .45s forwards}
125
- .run li::before{content:"";position:absolute;left:-13px;top:12px;width:12px;height:12px;border-radius:50%;
126
- background:#94A3B8;border:2px solid #fff;box-shadow:0 0 0 2px #E2E8F0}
127
- .run li.done::before{background:var(--green)}
128
- .run li.current::before{background:var(--blue)}
129
- .run h5{margin:0 0 4px;font-size:14px}
130
- .run .meta{display:flex;gap:10px;font-size:12px;color:#64748B}
131
- .run .desc{margin-top:6px;color:#334155;font-size:13px}
 
 
132
 
133
  /* ================= Empty & Error States ================= */
134
- .empty{padding:12px;border:1px dashed var(--line);border-radius:12px;text-align:center;color:var(--muted)}
135
  .error{padding:12px;border:1px solid #fecaca;background:#fff1f2;color:#7f1d1d;border-radius:12px}
136
 
137
  /* ================= Loading Skeleton ================= */
@@ -379,21 +384,7 @@ def gpt_decide(shift: pd.Series, eligible_df: pd.DataFrame) -> Dict[str, str]:
379
  st.error(f"โŒ GPT Error: {e}")
380
  return {"action": "skip"}
381
 
382
- def render_mini_timeline(events: List[Dict[str, str]]) -> str:
383
- # kept in case you want the compact view
384
- steps = []
385
- for i, ev in enumerate(events):
386
- title = ev.get("title","")
387
- done = "done" if i < len(events)-1 else "" # last is current
388
- label = (title.replace("๐Ÿ”","").replace("โœ…","").replace("๐Ÿ“‹","")
389
- .replace("๐Ÿค–","").replace("๐Ÿ“ฌ","").strip())
390
- steps.append(f'<div class="step {done}">{label}</div>')
391
- html = f'''
392
- <div class="ai-timeline-light">{"".join(steps)}</div>
393
- <div class="ai-next">Next: {events[-1]["title"] if events else "Ready"}</div>
394
- '''
395
- return html
396
-
397
  def render_runline(events: List[Dict[str, Any]]) -> str:
398
  def li(ev):
399
  cls = ev.get("status","done")
@@ -410,6 +401,28 @@ def render_runline(events: List[Dict[str, Any]]) -> str:
410
  '<ul class="run">{items}</ul></div>'
411
  return html.replace("{items}", "".join(li(e) for e in events))
412
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  def run_agent(employee_ids: Iterable[int], df_shifts: pd.DataFrame):
414
  events: List[Dict[str, Any]] = []
415
  shift_assignment_results: List[tuple] = []
@@ -505,7 +518,7 @@ def main() -> None:
505
 
506
  # --- Business Case (top) ---
507
  st.markdown("""
508
- <div class="card" style="max-width:980px;margin:0 auto 10px;" dir="auto">
509
  <h4 style="margin:0 0 6px;">Business Case โ€” Open Shifts Auto-Fulfillment</h4>
510
  <p style="margin:0;">
511
  Health Matrix AI Agent ูŠุฑุงู‚ุจ ุงู„ุดูุชุงุช ุงู„ู…ูุชูˆุญุฉ ูˆูŠุทุงุจู‚ู‡ุง ุชู„ู‚ุงุฆูŠู‹ุง ู…ุน ุงู„ู…ูˆุงุฑุฏ ุงู„ู…ุคู‡ู‘ู„ุฉ ูˆูู‚ ุงู„ู…ุนุงูŠูŠุฑ:
@@ -540,7 +553,7 @@ S101,ICU,Registered Nurse,ICU-trained,BLS/ALS certified,Night,12h,A&E,Arabic,202
540
  df_shifts_default = pd.read_csv(StringIO(shift_data))
541
 
542
  # --- CTA ---
543
- col_left, col_center, col_right = st.columns([2,3,2])
544
  with col_center:
545
  if st.button("โ–ถ๏ธ Start AI Agent", key="start_agent", help="Let the AI handle the work"):
546
  # Loading state
@@ -588,14 +601,28 @@ S101,ICU,Registered Nurse,ICU-trained,BLS/ALS certified,Night,12h,A&E,Arabic,202
588
  """, unsafe_allow_html=True
589
  )
590
 
591
- # --- Agent Runline (vertical, styled here so global CSS applies) ---
592
- st.markdown("### ๐Ÿ•’ Agent Runline")
593
- st.markdown(render_runline(events), unsafe_allow_html=True)
594
-
595
- # --- Stat row (quick KPIs) ---
596
  assigned = sum(1 for e in events if e["title"].startswith("Assigned"))
597
  notified = sum(1 for e in events if e["title"].startswith("Notify"))
598
  skipped = sum(1 for e in events if e["title"].startswith("Skipped"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
599
  st.markdown('<div class="stat-row">', unsafe_allow_html=True)
600
  st.markdown(f'''
601
  <div class="stat-card" role="group" aria-label="Assigned"><div class="stat-icon">โœ…</div><div class="stat-label">Assigned</div><div class="stat-value">{assigned}</div></div>
@@ -608,9 +635,9 @@ S101,ICU,Registered Nurse,ICU-trained,BLS/ALS certified,Night,12h,A&E,Arabic,202
608
  st.markdown("### ๐Ÿ“Š Results")
609
  with st.expander("Shift Fulfillment Summary", expanded=True):
610
  if shift_assignment_results:
611
- # Wrapper for horizontal scroll on smaller screens
612
  st.markdown('<div class="table-wrap">', unsafe_allow_html=True)
613
- st.dataframe(pd.DataFrame(shift_assignment_results, columns=["Employee", "ShiftID", "Status"]), use_container_width=True)
 
614
  st.markdown('</div>', unsafe_allow_html=True)
615
  else:
616
  st.markdown('<div class="empty">No assignments to display.</div>', unsafe_allow_html=True)
 
1
  """
2
+ Health Matrix โ€“ AI Command Center (Enhanced UI/UX โ€ข Wide Timeline)
3
  Single-file Streamlit app with embedded CSS + UKG helpers.
4
 
5
  Front-end upgrades (layout/styling ONLY):
6
+ - Page uses full width on web
7
+ - Horizontal/GRID Agent Runline (wide cards) + legend
8
+ - Agent Overview strip (steps, total duration, decision mix)
9
  - KPI cards with micro-interactions
10
  - Readable tables: sticky headers, zebra rows; horizontal scroll support
11
  - Loading skeletons, friendly empty/error states
 
17
 
18
  import os
19
  import json
 
20
  import time
21
  from typing import List, Optional, Iterable, Dict, Any
22
  from io import StringIO
 
42
  --t1:#0F172A; --t2:#1F2937; --t3:#334155; --muted:#64748B;
43
  --blue:#004C97; --green:#36BA01; --ok:#16a34a; --warn:#D97706; --fail:#DC2626;
44
  }
45
+
46
+ /* ====== Make the main page wider on web ====== */
47
+ .main .block-container{max-width:1520px !important; padding-left:1rem; padding-right:1rem}
48
+
49
  html,body{margin:0;padding:0;height:100%;background:var(--bg);color:var(--t1);
50
  font-family:'Segoe UI','Cairo','Tajawal','Open Sans',sans-serif;}
51
  .container{max-width:1200px;margin:0 auto;padding:24px}
 
73
  -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;
74
  background-size:200% 100%;animation:aiTitle 8s ease-in-out infinite;
75
  }
76
+ .hero p{font-size:1rem;line-height:1.6;color:var(--t2);max-width:900px;margin:0 auto}
77
  .hero::before,.hero::after{content:"";position:absolute;inset:-20%;pointer-events:none;filter:blur(42px);opacity:.18}
78
  .hero::before{
79
  background:
 
117
  .stat-label{color:#334155;font-size:0.95rem}
118
  .stat-value{font-size:1.7rem;font-weight:900;color:var(--t1)}
119
 
120
+ /* === Agent Overview strip (chips) === */
121
+ .overview{display:flex;gap:8px;flex-wrap:wrap;margin:4px 0 10px}
122
+ .overview .chip{font-size:12.5px;padding:6px 10px;border:1px solid var(--line);border-radius:999px;background:#fff}
123
+ .overview .chip b{color:var(--t1)}
124
+
125
+ /* === Agent Runline - WIDE GRID (cards) === */
126
+ .runH{display:grid;grid-template-columns:repeat(12, 1fr);gap:12px;margin:8px 0}
127
+ .run-card{grid-column:span 4;background:#fff;border:1px solid var(--line);border-radius:14px;
128
+ box-shadow:0 6px 14px rgba(0,0,0,.06);padding:12px;position:relative;min-height:96px;opacity:0;animation:fadeIn .45s forwards}
129
+ .run-card .dot{position:absolute;left:10px;top:12px;width:10px;height:10px;border-radius:50%}
130
+ .run-card .title{margin:0 0 6px 0;padding-left:22px;font-weight:800}
131
+ .run-card .meta{color:#64748B;font-size:12.5px;margin-bottom:4px;padding-left:22px}
132
+ .run-card .desc{color:#334155;font-size:13px;padding-left:22px}
133
+ .run-card.done .dot{background:var(--green)}
134
+ .run-card.current .dot{background:var(--blue)}
135
+ @media (max-width:1200px){.run-card{grid-column:span 6}}
136
+ @media (max-width:640px){.run-card{grid-column:span 12}}
137
 
138
  /* ================= Empty & Error States ================= */
139
+ .empty{padding:12px;border:1px dashed var(--line);border-radius:12px;text-align:center;color:#64748B}
140
  .error{padding:12px;border:1px solid #fecaca;background:#fff1f2;color:#7f1d1d;border-radius:12px}
141
 
142
  /* ================= Loading Skeleton ================= */
 
384
  st.error(f"โŒ GPT Error: {e}")
385
  return {"action": "skip"}
386
 
387
+ # -------- EXISTING (vertical) kept if you want to switch back later ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  def render_runline(events: List[Dict[str, Any]]) -> str:
389
  def li(ev):
390
  cls = ev.get("status","done")
 
401
  '<ul class="run">{items}</ul></div>'
402
  return html.replace("{items}", "".join(li(e) for e in events))
403
 
404
+ # -------- NEW: WIDE GRID timeline (uses the full horizontal space) ----------
405
+ def render_runline_wide(events: List[Dict[str, Any]]) -> str:
406
+ def card(ev):
407
+ cls = ev.get("status","done")
408
+ dur = ev.get("dur_ms")
409
+ dur_txt = f"{int(dur)} ms" if isinstance(dur,(int,float)) else "โ€”"
410
+ title = ev.get("title","")
411
+ desc = ev.get("desc","")
412
+ return f"""
413
+ <div class="run-card {cls}">
414
+ <span class="dot"></span>
415
+ <div class="title">{title}</div>
416
+ <div class="meta">Duration: {dur_txt}</div>
417
+ <div class="desc">{desc}</div>
418
+ </div>
419
+ """
420
+ legend = '<div class="overview" dir="auto"><span class="chip">โ— <b>Current</b></span><span class="chip">โ— <b>Done</b></span></div>'
421
+ return legend + '<div class="runH" dir="auto">' + "".join(card(e) for e in events) + "</div>"
422
+
423
+ # =============================================================================
424
+ # Main agent runner (unchanged)
425
+ # =============================================================================
426
  def run_agent(employee_ids: Iterable[int], df_shifts: pd.DataFrame):
427
  events: List[Dict[str, Any]] = []
428
  shift_assignment_results: List[tuple] = []
 
518
 
519
  # --- Business Case (top) ---
520
  st.markdown("""
521
+ <div class="card" style="max-width:1200px;margin:0 auto 10px;" dir="auto">
522
  <h4 style="margin:0 0 6px;">Business Case โ€” Open Shifts Auto-Fulfillment</h4>
523
  <p style="margin:0;">
524
  Health Matrix AI Agent ูŠุฑุงู‚ุจ ุงู„ุดูุชุงุช ุงู„ู…ูุชูˆุญุฉ ูˆูŠุทุงุจู‚ู‡ุง ุชู„ู‚ุงุฆูŠู‹ุง ู…ุน ุงู„ู…ูˆุงุฑุฏ ุงู„ู…ุคู‡ู‘ู„ุฉ ูˆูู‚ ุงู„ู…ุนุงูŠูŠุฑ:
 
553
  df_shifts_default = pd.read_csv(StringIO(shift_data))
554
 
555
  # --- CTA ---
556
+ col_left, col_center, col_right = st.columns([1,2,1])
557
  with col_center:
558
  if st.button("โ–ถ๏ธ Start AI Agent", key="start_agent", help="Let the AI handle the work"):
559
  # Loading state
 
601
  """, unsafe_allow_html=True
602
  )
603
 
604
+ # ---------------- Agent Overview (clarify agent function) ----------------
605
+ total_steps = len(events)
606
+ total_ms = sum(int(e.get("dur_ms", 0) or 0) for e in events)
 
 
607
  assigned = sum(1 for e in events if e["title"].startswith("Assigned"))
608
  notified = sum(1 for e in events if e["title"].startswith("Notify"))
609
  skipped = sum(1 for e in events if e["title"].startswith("Skipped"))
610
+ st.markdown('<div class="overview">', unsafe_allow_html=True)
611
+ st.markdown(
612
+ f'<span class="chip">๐Ÿง  <b>Agent Steps:</b> {total_steps}</span>'
613
+ f'<span class="chip">โฑ๏ธ <b>Total Duration:</b> {total_ms} ms</span>'
614
+ f'<span class="chip">โœ… <b>Assigned:</b> {assigned}</span>'
615
+ f'<span class="chip">๐Ÿ“ฌ <b>Notify:</b> {notified}</span>'
616
+ f'<span class="chip">โš ๏ธ <b>Skipped:</b> {skipped}</span>',
617
+ unsafe_allow_html=True
618
+ )
619
+ st.markdown('</div>', unsafe_allow_html=True)
620
+
621
+ # --- Agent Runline (WIDE GRID) ---
622
+ st.markdown("### ๐Ÿ•’ Agent Runline")
623
+ st.markdown(render_runline_wide(events), unsafe_allow_html=True)
624
+
625
+ # --- Stat row (quick KPIs) ---
626
  st.markdown('<div class="stat-row">', unsafe_allow_html=True)
627
  st.markdown(f'''
628
  <div class="stat-card" role="group" aria-label="Assigned"><div class="stat-icon">โœ…</div><div class="stat-label">Assigned</div><div class="stat-value">{assigned}</div></div>
 
635
  st.markdown("### ๐Ÿ“Š Results")
636
  with st.expander("Shift Fulfillment Summary", expanded=True):
637
  if shift_assignment_results:
 
638
  st.markdown('<div class="table-wrap">', unsafe_allow_html=True)
639
+ st.dataframe(pd.DataFrame(shift_assignment_results, columns=["Employee", "ShiftID", "Status"]),
640
+ use_container_width=True)
641
  st.markdown('</div>', unsafe_allow_html=True)
642
  else:
643
  st.markdown('<div class="empty">No assignments to display.</div>', unsafe_allow_html=True)