Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- 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 |
-
-
|
| 7 |
-
-
|
| 8 |
-
- Agent
|
| 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:
|
| 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
|
| 118 |
-
.
|
| 119 |
-
.
|
| 120 |
-
.
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
.
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
.run
|
| 128 |
-
.run
|
| 129 |
-
.run
|
| 130 |
-
.run .
|
| 131 |
-
.run .
|
|
|
|
|
|
|
| 132 |
|
| 133 |
/* ================= Empty & Error States ================= */
|
| 134 |
-
.empty{padding:12px;border:1px dashed var(--line);border-radius:12px;text-align:center;color:
|
| 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 |
-
|
| 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:
|
| 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([
|
| 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
|
| 592 |
-
|
| 593 |
-
|
| 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"]),
|
|
|
|
| 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)
|