Skip to content

Commit 6235bf8

Browse files
committed
Add icons
1 parent 2928e00 commit 6235bf8

1 file changed

Lines changed: 99 additions & 20 deletions

File tree

streamlit_app/components/step_viewer.py

Lines changed: 99 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,40 +16,116 @@
1616
from job_state import STEPS, STEP_LABELS, STATUS_PENDING, STATUS_RUNNING, STATUS_DONE, STATUS_ERROR, STATUS_SKIPPED, JobState
1717

1818

19-
_STATUS_ICONS = {
20-
STATUS_PENDING: "⬜",
21-
STATUS_RUNNING: "🔄",
22-
STATUS_DONE: "✅",
23-
STATUS_ERROR: "❌",
24-
STATUS_SKIPPED: "⏭️",
19+
# ---------------------------------------------------------------------------
20+
# Inline SVG icon helpers (replace emoji for cross-platform consistency)
21+
# ---------------------------------------------------------------------------
22+
23+
_STEPPER_CSS = """
24+
<style>
25+
@keyframes spg-spin {
26+
0% { transform: rotate(0deg); }
27+
100% { transform: rotate(360deg); }
28+
}
29+
.spg-icon { display:inline-block; vertical-align:middle; }
30+
.spg-icon-spin svg { animation: spg-spin 1s linear infinite; }
31+
</style>
32+
"""
33+
34+
35+
def _svg_icon(body: str, *, size: int = 28, cls: str = "", color: str = "currentColor") -> str:
36+
"""Wrap an SVG body in a sized <span> container."""
37+
extra = f" {cls}" if cls else ""
38+
return (
39+
f'<span class="spg-icon{extra}">'
40+
f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}" '
41+
f'viewBox="0 0 24 24" fill="none" stroke="{color}" '
42+
f'stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'
43+
f'{body}</svg></span>'
44+
)
45+
46+
47+
def _icon_pending(size: int = 28) -> str:
48+
return _svg_icon('<circle cx="12" cy="12" r="9"/>', size=size, color="#9ca3af")
49+
50+
51+
def _icon_running(size: int = 28) -> str:
52+
# Partial arc that spins via CSS
53+
return _svg_icon(
54+
'<circle cx="12" cy="12" r="9" stroke-dasharray="42" stroke-dashoffset="14"/>',
55+
size=size, cls="spg-icon-spin", color="#2563eb",
56+
)
57+
58+
59+
def _icon_done(size: int = 28) -> str:
60+
return _svg_icon(
61+
'<circle cx="12" cy="12" r="9" fill="#16a34a" stroke="#16a34a"/>'
62+
'<path d="M8 12.5l2.5 2.5 5-5" stroke="#fff"/>',
63+
size=size, color="#16a34a",
64+
)
65+
66+
67+
def _icon_error(size: int = 28) -> str:
68+
return _svg_icon(
69+
'<circle cx="12" cy="12" r="9" fill="#dc2626" stroke="#dc2626"/>'
70+
'<path d="M9 9l6 6M15 9l-6 6" stroke="#fff"/>',
71+
size=size, color="#dc2626",
72+
)
73+
74+
75+
def _icon_skipped(size: int = 28) -> str:
76+
return _svg_icon(
77+
'<circle cx="12" cy="12" r="9" fill="#9ca3af" stroke="#9ca3af"/>'
78+
'<path d="M10 8l4 4-4 4" stroke="#fff" fill="none"/>'
79+
'<line x1="15" y1="8" x2="15" y2="16" stroke="#fff"/>',
80+
size=size, color="#9ca3af",
81+
)
82+
83+
84+
_STATUS_ICON_FN = {
85+
STATUS_PENDING: _icon_pending,
86+
STATUS_RUNNING: _icon_running,
87+
STATUS_DONE: _icon_done,
88+
STATUS_ERROR: _icon_error,
89+
STATUS_SKIPPED: _icon_skipped,
2590
}
2691

2792

93+
def _status_icon(status: str, size: int = 28) -> str:
94+
"""Return an inline-SVG icon string for a step status."""
95+
fn = _STATUS_ICON_FN.get(status, _icon_pending)
96+
return fn(size=size)
97+
98+
99+
def _inject_stepper_css():
100+
"""Inject the stepper CSS once per render cycle."""
101+
if not st.session_state.get("_spg_stepper_css_injected"):
102+
st.markdown(_STEPPER_CSS, unsafe_allow_html=True)
103+
st.session_state["_spg_stepper_css_injected"] = True
104+
105+
28106
# NWChem sub-phase definitions (order matters)
29107
_NWCHEM_SUBPHASES = [
30108
("vacuum_opt", "3a. Vacuum Optimisation"),
31109
("cosmo_opt", "3b. COSMO Optimisation"),
32110
]
33111

34112

35-
def _subphase_icon(subphase_key: str, current_phase: str) -> str:
36-
"""Return a status icon for a NWChem sub-phase given the current live phase."""
113+
def _subphase_icon(subphase_key: str, current_phase: str, size: int = 18) -> str:
114+
"""Return an inline-SVG status icon for a NWChem sub-phase."""
37115
order = ["not_started", "vacuum_opt", "cosmo_opt", "finished"]
38116
try:
39117
current_idx = order.index(current_phase)
40118
except ValueError:
41-
# error or unknown
42119
current_idx = -1
43120
sub_idx = order.index(subphase_key)
44121

45122
if current_phase == "error":
46-
# Mark the phase that was active as errored, earlier ones as done
47-
return "❌" # simplified — shown in the sub-stepper
123+
return _icon_error(size=size)
48124
if current_idx > sub_idx:
49-
return "✅"
125+
return _icon_done(size=size)
50126
if current_idx == sub_idx:
51-
return "🔄"
52-
return "⬜"
127+
return _icon_running(size=size)
128+
return _icon_pending(size=size)
53129

54130

55131
def _resolve_nwchem_phase(state: JobState, progress: dict | None = None) -> str:
@@ -60,11 +136,13 @@ def _resolve_nwchem_phase(state: JobState, progress: dict | None = None) -> str:
60136

61137

62138
def render_step_progress(state: JobState):
63-
"""Render a horizontal step-progress bar."""
139+
"""Render a horizontal step-progress bar with SVG icons."""
140+
_inject_stepper_css()
141+
64142
cols = st.columns(len(STEPS))
65143
for col, step in zip(cols, STEPS):
66144
status = state.step_status.get(step, STATUS_PENDING)
67-
icon = _STATUS_ICONS.get(status, "⬜")
145+
icon = _status_icon(status, size=28)
68146
label = STEP_LABELS[step]
69147

70148
# Build optional sub-phase annotation for the NWChem step
@@ -73,14 +151,14 @@ def render_step_progress(state: JobState):
73151
phase = _resolve_nwchem_phase(state)
74152
parts = []
75153
for key, short_label in _NWCHEM_SUBPHASES:
76-
si = _subphase_icon(key, phase)
154+
si = _subphase_icon(key, phase, size=16)
77155
parts.append(f"{si} {short_label}")
78156
sub_html = "<br>".join(parts)
79157
sub_html = f"<div style='margin-top:2px; font-size:0.75em; line-height:1.4'>{sub_html}</div>"
80158

81159
col.markdown(
82160
f"<div style='text-align:center'>"
83-
f"<span style='font-size:1.6em'>{icon}</span><br>"
161+
f"{icon}<br>"
84162
f"<small>{label}</small>"
85163
f"{sub_html}"
86164
f"</div>",
@@ -95,14 +173,15 @@ def render_nwchem_substeps(state: JobState, progress: dict | None = None):
95173
Displayed inside the NWChem expander to disclose the two sequential
96174
DFT optimisation phases up front.
97175
"""
176+
_inject_stepper_css()
98177
phase = _resolve_nwchem_phase(state, progress)
99178

100179
cols = st.columns(2)
101180
for col, (key, label) in zip(cols, _NWCHEM_SUBPHASES):
102-
icon = _subphase_icon(key, phase)
181+
icon = _subphase_icon(key, phase, size=22)
103182
col.markdown(
104183
f"<div style='text-align:center; padding:6px 0;'>"
105-
f"<span style='font-size:1.3em'>{icon}</span><br>"
184+
f"{icon}<br>"
106185
f"<small><b>{label}</b></small>"
107186
f"</div>",
108187
unsafe_allow_html=True,

0 commit comments

Comments
 (0)