@@ -26,6 +26,24 @@ import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
2626import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore' ;
2727import { txProgress , type TxProgressEvent , type PhaseTiming } from '../tx-progress' ;
2828
29+ // ─── Live phase support ───────────────────────────────────────────────────────
30+
31+ interface LivePhaseTiming extends PhaseTiming {
32+ isLive ?: boolean ;
33+ }
34+
35+ const ACTIVE_PHASE_COLORS : Record < string , string > = {
36+ simulating : '#ce93d8' ,
37+ proving : '#f48fb1' ,
38+ sending : '#2196f3' ,
39+ mining : '#4caf50' ,
40+ } ;
41+
42+ const shimmer = keyframes `
43+ 0% { background-position: -400px 0; }
44+ 100% { background-position: 400px 0; }
45+ ` ;
46+
2947// ─── Helpers ─────────────────────────────────────────────────────────────────
3048
3149const formatDuration = ( ms : number ) : string => {
@@ -60,22 +78,29 @@ const pulse = keyframes`
6078
6179// ─── PhaseTimeline (inline, simplified from demo-wallet) ─────────────────────
6280
63- function PhaseTimelineBar ( { phases } : { phases : PhaseTiming [ ] } ) {
64- const totalDuration = useMemo ( ( ) => phases . reduce ( ( sum , p ) => sum + p . duration , 0 ) , [ phases ] ) ;
81+ function PhaseTimelineBar ( { phases } : { phases : LivePhaseTiming [ ] } ) {
82+ const completedPhases = useMemo ( ( ) => phases . filter ( p => ! p . isLive ) , [ phases ] ) ;
83+ const livePhase = useMemo ( ( ) => phases . find ( p => p . isLive ) , [ phases ] ) ;
84+
85+ const completedDuration = useMemo ( ( ) => completedPhases . reduce ( ( sum , p ) => sum + p . duration , 0 ) , [ completedPhases ] ) ;
86+ const liveDuration = livePhase ?. duration ?? 0 ;
87+ const totalDuration = completedDuration + liveDuration ;
88+
6589 const miningDuration = useMemo (
66- ( ) => phases . filter ( p => p . name === 'Mining' ) . reduce ( ( sum , p ) => sum + p . duration , 0 ) ,
67- [ phases ] ,
90+ ( ) => completedPhases . filter ( p => p . name === 'Mining' ) . reduce ( ( sum , p ) => sum + p . duration , 0 ) ,
91+ [ completedPhases ] ,
6892 ) ;
6993
7094 if ( phases . length === 0 || totalDuration === 0 ) return null ;
7195
7296 const preparingDuration = totalDuration - miningDuration ;
7397 const hasMining = miningDuration > 0 ;
98+ const hasLive = ! ! livePhase ;
7499
75100 return (
76101 < Box sx = { { width : '100%' , mt : 1.5 } } >
77102 { /* Summary chips */ }
78- < Box sx = { { display : 'flex' , gap : 0.5 , mb : 0.5 , flexWrap : 'wrap' } } >
103+ < Box sx = { { display : 'flex' , gap : 0.5 , mb : 0.5 , flexWrap : 'wrap' , alignItems : 'center' } } >
79104 { hasMining ? (
80105 < >
81106 < Chip
@@ -96,7 +121,7 @@ function PhaseTimelineBar({ phases }: { phases: PhaseTiming[] }) {
96121 </ >
97122 ) : (
98123 < Chip
99- label = { `Total: ${ formatDuration ( totalDuration ) } ` }
124+ label = { hasLive ? `Elapsed: ${ formatDuration ( totalDuration ) } ` : `Total: ${ formatDuration ( totalDuration ) } ` }
100125 size = "small"
101126 sx = { { height : 18 , fontSize : '0.6rem' , fontWeight : 600 } }
102127 />
@@ -114,7 +139,8 @@ function PhaseTimelineBar({ phases }: { phases: PhaseTiming[] }) {
114139 bgcolor : 'action.hover' ,
115140 } }
116141 >
117- { phases . map ( ( phase , index ) => {
142+ { /* Completed segments (proportional width based on total) */ }
143+ { completedPhases . map ( ( phase , index ) => {
118144 const percentage = ( phase . duration / totalDuration ) * 100 ;
119145 return (
120146 < Tooltip
@@ -165,7 +191,7 @@ function PhaseTimelineBar({ phases }: { phases: PhaseTiming[] }) {
165191 minWidth : percentage > 0 ? 2 : 0 ,
166192 height : '100%' ,
167193 bgcolor : phase . color ,
168- borderRight : index < phases . length - 1 ? '1px solid rgba(255,255,255,0.3)' : undefined ,
194+ borderRight : ( index < completedPhases . length - 1 || hasLive ) ? '1px solid rgba(255,255,255,0.3)' : undefined ,
169195 transition : 'filter 0.2s ease' ,
170196 cursor : 'pointer' ,
171197 '&:hover' : { filter : 'brightness(1.2)' } ,
@@ -174,15 +200,43 @@ function PhaseTimelineBar({ phases }: { phases: PhaseTiming[] }) {
174200 </ Tooltip >
175201 ) ;
176202 } ) }
203+
204+ { /* Live (shimmer) segment — flex: 1 to fill remaining space */ }
205+ { livePhase && (
206+ < Tooltip
207+ title = {
208+ < Box sx = { { p : 0.5 } } >
209+ < Typography variant = "subtitle2" sx = { { fontWeight : 600 } } >
210+ { livePhase . name }
211+ </ Typography >
212+ < Typography variant = "body2" > { formatDurationLong ( livePhase . duration ) } (in progress)</ Typography >
213+ </ Box >
214+ }
215+ arrow
216+ placement = "top"
217+ >
218+ < Box
219+ sx = { {
220+ flex : 1 ,
221+ minWidth : 40 ,
222+ height : '100%' ,
223+ background : `linear-gradient(90deg, ${ livePhase . color } 88 0%, ${ livePhase . color } 50%, ${ livePhase . color } 88 100%)` ,
224+ backgroundSize : '400px 100%' ,
225+ animation : `${ shimmer } 1.5s infinite linear` ,
226+ cursor : 'pointer' ,
227+ } }
228+ />
229+ </ Tooltip >
230+ ) }
177231 </ Box >
178232
179233 { /* Legend */ }
180234 < Box sx = { { display : 'flex' , gap : 1 , mt : 0.5 , flexWrap : 'wrap' } } >
181235 { phases . map ( phase => (
182236 < Box key = { phase . name } sx = { { display : 'flex' , alignItems : 'center' , gap : 0.3 } } >
183- < Box sx = { { width : 6 , height : 6 , borderRadius : '50%' , bgcolor : phase . color } } />
237+ < Box sx = { { width : 6 , height : 6 , borderRadius : '50%' , bgcolor : phase . color , ... ( phase . isLive && { animation : ` ${ pulse } 1.2s ease-in-out infinite` } ) } } />
184238 < Typography variant = "caption" color = "text.secondary" sx = { { fontSize : '0.6rem' } } >
185- { phase . name }
239+ { phase . name } { phase . isLive ? ' ●' : '' }
186240 </ Typography >
187241 </ Box >
188242 ) ) }
@@ -201,14 +255,16 @@ interface TxToastProps {
201255function TxToast ( { event, onDismiss } : TxToastProps ) {
202256 const isActive = event . phase !== 'complete' && event . phase !== 'error' ;
203257
204- // For completed events, compute total from recorded phase timings (stable across refreshes)
258+ // For completed events, compute total from recorded phases (stable across refreshes)
205259 const computeFinalElapsed = ( ) => {
206- const t = event . phaseTimings ;
207- const fromTimings = ( t . simulation ?? 0 ) + ( t . proving ?? 0 ) + ( t . sending ?? 0 ) + ( t . mining ?? 0 ) ;
208- return fromTimings > 0 ? fromTimings : Date . now ( ) - event . startTime ;
260+ const fromPhases = event . phases . reduce ( ( sum , p ) => sum + p . duration , 0 ) ;
261+ return fromPhases > 0 ? fromPhases : Date . now ( ) - event . startTime ;
209262 } ;
210263
264+ // Total wall-clock elapsed since tx start (for header display)
211265 const [ elapsed , setElapsed ] = useState ( ( ) => isActive ? Date . now ( ) - event . startTime : computeFinalElapsed ( ) ) ;
266+ // Live elapsed within the *current* phase (resets when phase changes)
267+ const [ phaseElapsed , setPhaseElapsed ] = useState ( ( ) => isActive ? Date . now ( ) - event . phaseStartTime : 0 ) ;
212268 const [ expanded , setExpanded ] = useState ( true ) ;
213269 const frozen = useRef ( ! isActive ) ;
214270
@@ -218,17 +274,44 @@ function TxToast({ event, onDismiss }: TxToastProps) {
218274 if ( ! frozen . current ) {
219275 frozen . current = true ;
220276 setElapsed ( computeFinalElapsed ( ) ) ;
277+ setPhaseElapsed ( 0 ) ;
221278 }
222279 return ;
223280 }
224281 frozen . current = false ;
225- const interval = setInterval ( ( ) => setElapsed ( Date . now ( ) - event . startTime ) , 200 ) ;
282+ const interval = setInterval ( ( ) => {
283+ setElapsed ( Date . now ( ) - event . startTime ) ;
284+ setPhaseElapsed ( Date . now ( ) - event . phaseStartTime ) ;
285+ } , 200 ) ;
226286 return ( ) => clearInterval ( interval ) ;
227- } , [ isActive , event . startTime ] ) ;
287+ } , [ isActive , event . startTime , event . phaseStartTime ] ) ;
288+
289+ // Re-initialize elapsed when txId changes (new transaction)
290+ const prevTxIdRef = useRef ( event . txId ) ;
291+ useEffect ( ( ) => {
292+ if ( event . txId !== prevTxIdRef . current ) {
293+ prevTxIdRef . current = event . txId ;
294+ setElapsed ( isActive ? Date . now ( ) - event . startTime : computeFinalElapsed ( ) ) ;
295+ setPhaseElapsed ( isActive ? Date . now ( ) - event . phaseStartTime : 0 ) ;
296+ frozen . current = ! isActive ;
297+ }
298+ } , [ event . txId ] ) ;
228299
229300 const isComplete = event . phase === 'complete' ;
230301 const isError = event . phase === 'error' ;
231302
303+ // Build display phases: completed phases + live shimmer phase when active
304+ const displayPhases : LivePhaseTiming [ ] = useMemo ( ( ) => {
305+ if ( ! isActive ) return event . phases ;
306+ if ( phaseElapsed <= 0 && event . phases . length === 0 ) return [ ] ;
307+ const liveColor = ACTIVE_PHASE_COLORS [ event . phase ] ?? '#90caf9' ;
308+ const liveName = PHASE_LABELS [ event . phase ] ?? event . phase ;
309+ return [
310+ ...event . phases ,
311+ { name : liveName , duration : phaseElapsed > 0 ? phaseElapsed : 100 , color : liveColor , isLive : true } ,
312+ ] ;
313+ } , [ isActive , event . phases , event . phase , phaseElapsed ] ) ;
314+
232315 return (
233316 < Paper
234317 elevation = { 8 }
@@ -304,7 +387,7 @@ function TxToast({ event, onDismiss }: TxToastProps) {
304387 </ Typography >
305388
306389 { /* Expand/collapse */ }
307- { isComplete && event . phases . length > 0 && (
390+ { displayPhases . length > 0 && (
308391 < IconButton size = "small" onClick = { ( ) => setExpanded ( prev => ! prev ) } sx = { { p : 0.25 } } >
309392 { expanded ? < ExpandLessIcon sx = { { fontSize : 16 } } /> : < ExpandMoreIcon sx = { { fontSize : 16 } } /> }
310393 </ IconButton >
@@ -316,10 +399,10 @@ function TxToast({ event, onDismiss }: TxToastProps) {
316399 </ IconButton >
317400 </ Box >
318401
319- { /* Phase timeline breakdown (shown when complete) */ }
320- < Collapse in = { isComplete && expanded && event . phases . length > 0 } >
402+ { /* Phase timeline breakdown (shown during execution and when complete) */ }
403+ < Collapse in = { expanded && displayPhases . length > 0 } >
321404 < Box sx = { { px : 1.5 , pb : 1.5 } } >
322- < PhaseTimelineBar phases = { event . phases } />
405+ < PhaseTimelineBar phases = { displayPhases } />
323406 </ Box >
324407 </ Collapse >
325408
0 commit comments