1616from 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
55131def _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
62138def 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