@@ -7,15 +7,21 @@ class TrainingDashboard {
77 this . metricsGrid = document . querySelector ( '.metrics-grid' ) ;
88 this . lossChart = document . getElementById ( 'lossChart' ) ;
99 this . accChart = document . getElementById ( 'accuracyChart' ) ;
10+ this . testChart = document . getElementById ( 'testAccuracyChart' ) ;
1011 this . predictionsGrid = document . getElementById ( 'predictionsGrid' ) ;
1112 this . imagesGrid = document . getElementById ( 'imagesGrid' ) ;
1213 this . statusBadge = document . getElementById ( 'trainingStatus' ) ;
14+ this . lastUpdateElem = document . getElementById ( 'lastUpdate' ) ;
1315 this . state = {
1416 testAccuracy : null ,
1517 testLoss : null ,
1618 totalEpochs : null ,
1719 finalTrainAccuracy : null ,
1820 } ;
21+ this . predictionData = [ ] ;
22+ this . maxSampleImages = 5 ;
23+ this . refreshTimer = null ;
24+ this . refreshIntervalMs = 300000 ; // 5 minutes
1925 this . init ( ) ;
2026 }
2127
@@ -64,9 +70,11 @@ class TrainingDashboard {
6470 this . statusBadge . textContent = dataLoaded ? 'Data Loaded' : 'No Data' ;
6571 this . statusBadge . classList . toggle ( 'error' , ! dataLoaded ) ;
6672 }
73+ this . updateLastUpdateTime ( ) ;
6774 } catch ( err ) {
6875 console . error ( 'Init error:' , err ) ;
6976 this . failBadge ( 'Data Load Error' ) ;
77+ this . setLastUpdateFallback ( ) ;
7078 }
7179 // Always hide preloader after 3 seconds, regardless of data loading
7280 setTimeout ( ( ) => {
@@ -106,15 +114,20 @@ class TrainingDashboard {
106114
107115 // predictions table
108116 const lines = md . split ( '\n' ) ;
109- const predRows = lines . filter ( line => / ^ \| \s * \d + \s * \| \s * \d + \s * \| \s * \d + \s * \| / i. test ( line ) ) . slice ( 0 , 8 ) ;
110- const preds = predRows . map ( r => {
111- const cells = r . split ( '|' ) . map ( c => c . trim ( ) ) . filter ( Boolean ) ;
112- const idx = parseInt ( cells [ 0 ] , 10 ) ;
113- const t = parseInt ( cells [ 1 ] , 10 ) ;
114- const p = parseInt ( cells [ 2 ] , 10 ) ;
115- return { index : idx , true_label : t , predicted_label : p , correct : t === p } ;
117+ const preds = [ ] ;
118+ lines . forEach ( line => {
119+ const match = line . match ( / ^ \| \s * ( \d + ) \s * \| \s * ( \d + ) \s * \| \s * ( \d + ) \s * \| / ) ;
120+ if ( match ) {
121+ const idx = parseInt ( match [ 1 ] , 10 ) ;
122+ const t = parseInt ( match [ 2 ] , 10 ) ;
123+ const p = parseInt ( match [ 3 ] , 10 ) ;
124+ preds . push ( { index : idx , true_label : t , predicted_label : p , correct : t === p } ) ;
125+ }
116126 } ) ;
117- this . renderPredictions ( preds ) ;
127+ if ( preds . length ) {
128+ this . predictionData = preds ;
129+ this . renderPredictions ( preds ) ;
130+ }
118131 }
119132
120133 renderMetrics ( ) {
@@ -154,19 +167,10 @@ class TrainingDashboard {
154167 } ;
155168 const lossSrc = outputs ?. train_loss_image || 'images/train_loss.png' ;
156169 const accSrc = outputs ?. train_accuracy_image || 'images/train_accuracy.png' ;
170+ const testSrc = outputs ?. test_accuracy_image || 'images/test_accuracy.png' ;
157171 ensureImg ( this . lossChart , lossSrc , 'Training Loss' ) ;
158172 ensureImg ( this . accChart , accSrc , 'Training Accuracy' ) ;
159- // Optional: render test accuracy image above images grid
160- const testAccImg = document . createElement ( 'img' ) ;
161- testAccImg . src = outputs ?. test_accuracy_image || 'images/train_accuracy.png' ;
162- testAccImg . alt = 'Test Accuracy' ;
163- testAccImg . className = 'chart-image' ;
164- testAccImg . style . maxWidth = '320px' ;
165- testAccImg . onerror = ( ) => { testAccImg . remove ( ) ; } ;
166- const section = document . querySelector ( '.sample-images h2' ) ;
167- if ( section && section . parentElement ) {
168- section . parentElement . insertBefore ( testAccImg , section . nextSibling ) ;
169- }
173+ ensureImg ( this . testChart , testSrc , 'Test Accuracy' ) ;
170174 }
171175
172176 parseKV ( text ) {
@@ -201,7 +205,10 @@ class TrainingDashboard {
201205 preds . push ( { index : i , true_label : ti , predicted_label : pi , correct : ti === pi } ) ;
202206 }
203207 }
204- if ( preds . length ) this . renderPredictions ( preds ) ;
208+ if ( preds . length ) {
209+ this . predictionData = preds ;
210+ this . renderPredictions ( preds ) ;
211+ }
205212
206213 // Render sample images directly if provided
207214 const grid = this . imagesGrid ;
@@ -220,17 +227,24 @@ class TrainingDashboard {
220227 renderPredictions ( preds ) {
221228 const grid = this . predictionsGrid ;
222229 if ( ! grid ) return ;
230+ const data = preds && preds . length ? preds : this . predictionData ;
223231 grid . innerHTML = '' ;
224- if ( ! preds || preds . length === 0 ) {
232+ if ( ! data || data . length === 0 ) {
225233 grid . innerHTML = '<div class="no-data">No prediction data available</div>' ;
226234 return ;
227235 }
228- preds . forEach ( pred => {
236+ data . slice ( 0 , 8 ) . forEach ( pred => {
229237 const div = document . createElement ( 'div' ) ;
230238 div . className = `prediction-item ${ pred . correct ? 'correct' : 'incorrect' } ` ;
231239 div . innerHTML = `
232- <div class="prediction-digit">${ pred . predicted_label } </div>
233- <div class="prediction-labels">True: ${ pred . true_label } <br>${ pred . correct ? 'Correct' : 'Wrong' } </div>
240+ <div class="prediction-header">
241+ <span class="prediction-id">#${ pred . index } </span>
242+ <span class="prediction-status">${ pred . correct ? 'Correct' : 'Wrong' } </span>
243+ </div>
244+ <div class="prediction-body">
245+ <span class="prediction-true">True: ${ pred . true_label } </span>
246+ <span class="prediction-pred">Pred: ${ pred . predicted_label } </span>
247+ </div>
234248 ` ;
235249 grid . appendChild ( div ) ;
236250 } ) ;
@@ -245,23 +259,54 @@ class TrainingDashboard {
245259 grid . innerHTML = '<div class="no-data">No images listed</div>' ;
246260 return ;
247261 }
248- imgLines . forEach ( line => {
249- const match = line . match ( / ! \[ [ ^ \] ] * \] \( ( i m a g e s \/ [ ^ ) ] + ) \) .* T r u e : \s * ( \d ) [ ^ \d ] + P r e d : \s * ( \d ) / i) ;
262+ imgLines . slice ( 0 , this . maxSampleImages ) . forEach ( line => {
263+ const match = line . match ( / ! \[ [ ^ \] ] * \] \( ( i m a g e s \/ [ ^ ) ] + ) \) .* T r u e : \s * ( \d + ) \D + P r e d : \s * ( \d + ) / i) ;
250264 const src = match ? match [ 1 ] : null ;
251265 const t = match ? parseInt ( match [ 2 ] , 10 ) : null ;
252266 const p = match ? parseInt ( match [ 3 ] , 10 ) : null ;
267+ const trueLabel = Number . isInteger ( t ) ? t : '—' ;
268+ const predLabel = Number . isInteger ( p ) ? p : '—' ;
253269 const card = document . createElement ( 'div' ) ;
254270 card . className = 'image-card' ;
255271 card . innerHTML = src ? `
256272 <img src="${ src } " alt="sample" onerror="this.replaceWith(document.createTextNode('Image missing'))">
257- <div class="image-caption">True: ${ t } · Pred: ${ p } </div>
273+ <div class="image-caption">True: ${ trueLabel } · Pred: ${ predLabel } </div>
258274 ` : '<div class="image-caption">Image reference invalid</div>' ;
259275 grid . appendChild ( card ) ;
260276 } ) ;
261277 }
262278
263279 startAutoRefresh ( ) {
264- setInterval ( ( ) => this . init ( ) , 300000 ) ; // 5 minutes
280+ if ( this . refreshTimer ) return ;
281+ this . refreshTimer = setInterval ( ( ) => this . init ( ) , this . refreshIntervalMs ) ;
282+ }
283+
284+ async updateLastUpdateTime ( ) {
285+ if ( ! this . lastUpdateElem ) return ;
286+ try {
287+ const resp = await fetch ( 'train_output.md' , { method : 'HEAD' , cache : 'no-store' } ) ;
288+ if ( ! resp . ok ) throw new Error ( 'Failed to fetch headers' ) ;
289+ const lastMod = resp . headers . get ( 'last-modified' ) ;
290+ if ( lastMod ) {
291+ const date = new Date ( lastMod ) ;
292+ if ( ! Number . isNaN ( date . getTime ( ) ) ) {
293+ this . lastUpdateElem . textContent = date . toLocaleString ( undefined , {
294+ dateStyle : 'medium' ,
295+ timeStyle : 'short' ,
296+ } ) ;
297+ return ;
298+ }
299+ }
300+ this . setLastUpdateFallback ( ) ;
301+ } catch ( err ) {
302+ console . warn ( 'Last update fetch failed' , err ) ;
303+ this . setLastUpdateFallback ( ) ;
304+ }
305+ }
306+
307+ setLastUpdateFallback ( ) {
308+ if ( ! this . lastUpdateElem ) return ;
309+ this . lastUpdateElem . textContent = 'Unavailable' ;
265310 }
266311}
267312
0 commit comments