@@ -192,14 +192,19 @@ export function App(): JSX.Element {
192192 const [ tabs , setTabs ] = useState < WorkspaceTab [ ] > ( getSavedTabs ) ;
193193 const [ activeTabId , setActiveTabId ] = useState < string > ( getSavedActiveTabId ) ;
194194 const isSwitchingRef = useRef < boolean > ( false ) ;
195- const debugState = useNeuralDebugger ( ) ;
196195
197196 const [ nodes , setNodes ] = useState < Node [ ] > ( ( ) => {
198197 const savedTabs = getSavedTabs ( ) ;
199198 const savedActiveId = getSavedActiveTabId ( ) ;
200199 const active = savedTabs . find ( ( t ) => t . id === savedActiveId ) || savedTabs [ 0 ] ;
201200 return active . nodes ;
202201 } ) ;
202+
203+ const modelNodesCount = useMemo ( ( ) => {
204+ return nodes . filter ( n => n . type === "brick" || n . type === "adapter" ) . length ;
205+ } , [ nodes ] ) ;
206+
207+ const maxStep = modelNodesCount + 1 ;
203208 const [ edges , setEdges ] = useState < Edge [ ] > ( ( ) => {
204209 const savedTabs = getSavedTabs ( ) ;
205210 const savedActiveId = getSavedActiveTabId ( ) ;
@@ -303,6 +308,7 @@ export function App(): JSX.Element {
303308 // hardcoding 65536 — so dropping a GPT-2 / SmolLM / cppmega tokenizer
304309 // gives the BrickContextPanel the right embedding_table param count.
305310 const tokenizerVocabSize = useTokenizerVocab ( rpc , trainTokenizerPath ) ;
311+ const debugState = useNeuralDebugger ( rpc , trainTokenizerPath ) ;
306312
307313 // V7-I07: poll the JsonRPC LRU cache hit-rate so the BottomStrip
308314 // chip surfaces hit_rate / size / evictions live.
@@ -1001,6 +1007,7 @@ export function App(): JSX.Element {
10011007 ...( firstGroup . schedule ?? { } ) ,
10021008 kind : scheduleKind ,
10031009 warmup_steps : d . warmup_steps ,
1010+ total_steps : firstGroup . schedule ?. total_steps ?? 100000 ,
10041011 } ,
10051012 } ,
10061013 ...spec . optim . groups . slice ( 1 ) ,
@@ -1088,6 +1095,7 @@ export function App(): JSX.Element {
10881095 ...( firstGroup . schedule ?? { } ) ,
10891096 kind : scheduleKind ,
10901097 warmup_steps : d . warmup_steps ,
1098+ total_steps : firstGroup . schedule ?. total_steps ?? 100000 ,
10911099 } ,
10921100 } ,
10931101 ...spec . optim . groups . slice ( 1 ) ,
@@ -1520,6 +1528,46 @@ export function App(): JSX.Element {
15201528 if ( trainInFlight && trainRunId ) liveTrainResetRef . current ( ) ;
15211529 } , [ trainInFlight , trainRunId ] ) ;
15221530
1531+ // V7-F03-ANIMATE: Trigger forward-backward canvas stepping animation on actual WebSocket step updates
1532+ const lastAnimatedStepRef = useRef < number > ( - 1 ) ;
1533+ const animateStepRef = useRef ( debugState . animateFullTrainStep ) ;
1534+ useEffect ( ( ) => {
1535+ animateStepRef . current = debugState . animateFullTrainStep ;
1536+ } , [ debugState . animateFullTrainStep ] ) ;
1537+
1538+ useEffect ( ( ) => {
1539+ if ( ! trainInFlight ) {
1540+ lastAnimatedStepRef . current = - 1 ;
1541+ return ;
1542+ }
1543+ const lastEvent = liveTrain . events . length > 0 ? liveTrain . events [ liveTrain . events . length - 1 ] : null ;
1544+ if ( lastEvent && lastEvent . step !== lastAnimatedStepRef . current ) {
1545+ lastAnimatedStepRef . current = lastEvent . step ;
1546+ // Trigger full forward-backward canvas pass
1547+ animateStepRef . current ( maxStep ) ;
1548+ }
1549+ } , [ liveTrain . events , trainInFlight , maxStep ] ) ;
1550+
1551+ // V7-F04-SYNC: Sync real Loss and LR values from active WebSocket stream to the debugger
1552+ const setLrRef = useRef ( debugState . setLr ) ;
1553+ const setLossValRef = useRef ( debugState . setLossVal ) ;
1554+ useEffect ( ( ) => {
1555+ setLrRef . current = debugState . setLr ;
1556+ setLossValRef . current = debugState . setLossVal ;
1557+ } , [ debugState . setLr , debugState . setLossVal ] ) ;
1558+
1559+ useEffect ( ( ) => {
1560+ const lastEvent = liveTrain . events . length > 0 ? liveTrain . events [ liveTrain . events . length - 1 ] : null ;
1561+ if ( lastEvent ) {
1562+ if ( typeof lastEvent . lr === "number" ) {
1563+ setLrRef . current ( lastEvent . lr ) ;
1564+ }
1565+ if ( typeof lastEvent . loss === "number" ) {
1566+ setLossValRef . current ( lastEvent . loss ) ;
1567+ }
1568+ }
1569+ } , [ liveTrain . events ] ) ;
1570+
15231571 const handleShardingAccept = useCallback ( ( idx : number ) => {
15241572 const chosen = proposals [ idx ] ;
15251573 if ( ! chosen ) return ;
@@ -1538,11 +1586,6 @@ export function App(): JSX.Element {
15381586 setTimeout ( ( ) => setRunError ( null ) , 2000 ) ;
15391587 } , [ proposals , scheduleVerify , spec . sharding ] ) ;
15401588
1541- const modelNodesCount = useMemo ( ( ) => {
1542- return nodes . filter ( n => n . type === "brick" || n . type === "adapter" ) . length ;
1543- } , [ nodes ] ) ;
1544-
1545- const maxStep = modelNodesCount + 1 ;
15461589
15471590 const mappedNodes = useMemo ( ( ) => {
15481591 const head = spec . loss . head_outputs ?. [ 0 ] ;
@@ -1555,6 +1598,8 @@ export function App(): JSX.Element {
15551598
15561599 const K = modelNodes . length ;
15571600
1601+ const lastEvent = liveTrain . events . length > 0 ? liveTrain . events [ liveTrain . events . length - 1 ] : null ;
1602+
15581603 let enriched = nodes . map ( ( n ) => {
15591604 const isModel = n . type === "brick" || n . type === "adapter" ;
15601605 const idx = isModel ? modelNodes . findIndex ( ( m ) => m . id === n . id ) : - 1 ;
@@ -1566,6 +1611,31 @@ export function App(): JSX.Element {
15661611 ( n . id === "__loss_ghost__" && debugState . activeStep === K ) ;
15671612 }
15681613
1614+ let gradNorm : number | undefined ;
1615+ if ( lastEvent ?. grad_norms && isModel ) {
1616+ const normKeys = Object . keys ( lastEvent . grad_norms ) ;
1617+ const nameVal = typeof ( n . data as any ) ?. name === "string" ? ( n . data as any ) . name . toLowerCase ( ) : "" ;
1618+ const idVal = n . id . toLowerCase ( ) ;
1619+
1620+ // Find best key match in grad_norms map
1621+ const matchedKey = normKeys . find ( ( k ) => {
1622+ const lk = k . toLowerCase ( ) ;
1623+ if ( nameVal && ( lk . includes ( nameVal ) || nameVal . includes ( lk ) ) ) return true ;
1624+ if ( lk . includes ( idVal ) || idVal . includes ( lk ) ) return true ;
1625+ // check if index matches e.g. key contains "layers.3." or "layer_3" etc.
1626+ const idxPattern1 = `layers.${ idx } .` ;
1627+ const idxPattern2 = `layers.${ idx } ` ;
1628+ const idxPattern3 = `layer_${ idx } ` ;
1629+ const idxPattern4 = `.${ idx } .` ;
1630+ const idxPattern5 = `.${ idx } ` ;
1631+ if ( lk . includes ( idxPattern1 ) || lk . includes ( idxPattern2 ) || lk . includes ( idxPattern3 ) || lk . includes ( idxPattern4 ) || lk . endsWith ( idxPattern5 ) ) return true ;
1632+ return false ;
1633+ } ) ;
1634+ if ( matchedKey !== undefined ) {
1635+ gradNorm = lastEvent . grad_norms [ matchedKey ] ;
1636+ }
1637+ }
1638+
15691639 let additionalData : any = { } ;
15701640 if ( n . type === "tokenizer_virtual" ) {
15711641 isActiveNode = debugState . debuggerMode && debugState . activeStep === - 1 ;
@@ -1581,6 +1651,10 @@ export function App(): JSX.Element {
15811651 direction : debugState . direction ,
15821652 isActiveNode,
15831653 } ;
1654+ } else if ( isModel && gradNorm !== undefined ) {
1655+ additionalData = {
1656+ gradNorm,
1657+ } ;
15841658 }
15851659
15861660 return {
@@ -1659,7 +1733,7 @@ export function App(): JSX.Element {
16591733 result . push ( detokenizerNode ) ;
16601734 }
16611735 return result ;
1662- } , [ nodes , debugState . debuggerMode , debugState . activeStep , debugState . direction , debugState . prompt , debugState . tokens , debugState . isWeightUpdated , spec . loss . head_outputs , spec . loss . kind , spec . loss . params ] ) ;
1736+ } , [ nodes , debugState . debuggerMode , debugState . activeStep , debugState . direction , debugState . prompt , debugState . tokens , debugState . isWeightUpdated , spec . loss . head_outputs , spec . loss . kind , spec . loss . params , liveTrain . events ] ) ;
16631737
16641738 const mappedEdges = useMemo ( ( ) => {
16651739 const head = spec . loss . head_outputs ?. [ 0 ] ;
0 commit comments