Fedir-Ilina commited on
Commit
3bedbae
·
verified ·
1 Parent(s): aa20325

Upload index-6.html

Browse files
Files changed (1) hide show
  1. templates/index-6.html +303 -73
templates/index-6.html CHANGED
@@ -49,29 +49,26 @@
49
  .ok{color:#7ee787}.warn{color:#ffd479}.err{color:#ff7b72}
50
  .footer{padding:12px 16px;color:var(--muted);border-top:1px solid var(--border);font-size:12px}
51
 
 
52
  .resizer-v {
53
- height: 15px; /* Mais alto para facilitar o toque */
54
  cursor: ns-resize;
55
- background: #21262d;
56
- border-top: 1px solid #30363d;
57
- border-bottom: 1px solid #30363d;
58
  display: flex;
59
  align-items: center;
60
  justify-content: center;
61
- user-select: none; /* Impede que seleciones texto ao arrastar */
62
- z-index: 50;
63
- }
64
- .resizer-v:hover {
65
- background: #1f6feb; /* Fica azul brilhante ao passar o rato */
66
  }
 
67
  .resizer-v::after {
68
  content: "";
69
  width: 40px;
70
- height: 4px;
71
- background: #8b949e;
72
  border-radius: 2px;
73
  }
74
-
75
  </style>
76
 
77
  </head>
@@ -207,7 +204,7 @@
207
 
208
  <section class="card">
209
  <header>
210
- <h2>Treinar IA com Ficheiros</h2>
211
  <div class="chips">
212
  <span class="pill" id="trainerState">estado: inativo</span>
213
  </div>
@@ -215,7 +212,7 @@
215
  <div class="content">
216
  <div class="row">
217
  <div class="col-12">
218
- <label for="model_path">Caminho do Modelo Base</label>
219
  <input type="text" id="model_path" placeholder="/caminho/para/modelo" />
220
  </div>
221
  <div class="col-12">
@@ -243,22 +240,81 @@
243
 
244
  </div>
245
 
246
- <div class="mt-2 w-full bg-gray-200 rounded-full h-3">
247
- <div id="progressBar" class="bg-indigo-500 h-3 rounded-full transition-all duration-300" style="width: 0%;"></div>
 
 
 
248
  </div>
249
-
250
- <h3>Status do Treino</h3>
251
- <div id="trainingBox" class="bg-gray-800" style="height: 500px; overflow-y: auto; white-space: pre-wrap; border: 1px solid #ccc; padding: 10px;">
252
- À espera do início do treino…
253
- </div>
254
-
255
- <!-- INSERE ISTO AQUI -->
256
- <div id="resizer" class="resizer-v"></div>
257
-
258
- <h3>Conteúdo do Diretório do Modelo</h3>
259
- <div id="modelDirBox" class="bg-gray-900" style="height:200px;overflow-y:auto;border:1px solid #444;padding:10px;font-family:monospace;">
260
- A carregar conteúdo do diretório...
 
 
 
 
261
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
 
263
  </div>
264
  </section>
@@ -432,6 +488,130 @@
432
  trainingBox.scrollTop = trainingBox.scrollHeight;
433
  }
434
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  function startPolling() {
436
  if (pollTimer) return;
437
 
@@ -441,33 +621,74 @@
441
  if (!r.ok) throw new Error('HTTP ' + r.status);
442
  const data = await r.json();
443
 
444
- let content = '🚀 Treino iniciado\n';
445
-
446
- // Progresso e estado
447
  if (data.progress !== undefined) {
448
  const pct = typeof data.progress === 'object' ? data.progress.percent : data.progress;
449
- content += `Progresso: ${pct}%\n`;
450
  const progressBar = $('progressBar');
 
451
  if (progressBar) progressBar.style.width = `${pct}%`;
 
452
  }
453
 
454
-
455
- if (data.status) content += `Estado: ${data.status}\n`;
 
 
 
 
 
 
 
 
456
 
457
- // Epoch losses
458
- if (data.epoch_losses && data.epoch_losses.length > 0) {
459
- content += `Loss por época: ${data.epoch_losses.join(', ')}\n`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  }
461
 
462
- // Logs
463
- if (data.logs && data.logs.length > 0) {
464
- content += 'Logs recentes:\n' + data.logs.join('\n');
465
- } else {
466
- content += 'Logs ainda não recebidos...\n';
 
 
 
 
467
  }
468
 
469
- trainingBox.textContent = content;
470
- trainingBox.scrollTop = trainingBox.scrollHeight;
 
 
 
 
 
 
 
 
 
 
 
 
471
 
472
  } catch (e) {
473
  console.error('Erro ao atualizar treinamento:', e);
@@ -546,6 +767,11 @@
546
  }
547
  }
548
 
 
 
 
 
 
549
  // =========================
550
  // EVENT LISTENERS
551
  // =========================
@@ -563,43 +789,47 @@
563
  case 'tensorboardBtn': el.addEventListener('click',openTensorBoard); break;
564
  case 'reduceBtn': el.addEventListener('click',()=>manualAdjustAccumulation(-1)); break;
565
  case 'increaseBtn': el.addEventListener('click',()=>manualAdjustAccumulation(1)); break;
566
- case 'modelDirBtn': el.addEventListener('click',loadModelDir); break;
 
 
 
 
 
 
 
 
 
 
 
567
  }
568
  });
569
  fetchConfigFromServer();
570
  });
571
 
572
- // Lógica de Redimensionamento Vertical (Versão HF)
573
- const resizer = document.getElementById('resizer');
574
- const trainingBox = document.getElementById('trainingBox');
575
-
576
- if (resizer && trainingBox) {
577
- resizer.addEventListener('mousedown', function(e) {
578
- e.preventDefault();
579
- // Adiciona uma classe ao body para o cursor não "fugir" durante o arrasto
580
- document.body.style.cursor = 'ns-resize';
581
-
582
- window.addEventListener('mousemove', startResizing);
583
- window.addEventListener('mouseup', stopResizing);
584
- });
585
- }
586
-
587
- function startResizing(e) {
588
- // Calculamos a posição relativa ao topo da caixa de treino
589
- const rect = trainingBox.getBoundingClientRect();
590
- const offset = e.clientY - rect.top;
591
-
592
- // Aplicamos a nova altura (mínimo 100px, máximo 800px)
593
- if (offset > 100 && offset < 800) {
594
- trainingBox.style.height = offset + 'px';
595
  }
 
 
 
596
  }
597
 
598
- function stopResizing() {
599
- document.body.style.cursor = 'default';
600
- window.removeEventListener('mousemove', startResizing);
601
- window.removeEventListener('mouseup', stopResizing);
602
- }
603
 
604
  </script>
605
  </body>
 
49
  .ok{color:#7ee787}.warn{color:#ffd479}.err{color:#ff7b72}
50
  .footer{padding:12px 16px;color:var(--muted);border-top:1px solid var(--border);font-size:12px}
51
 
52
+ /* NOVO: Barra de redimensionamento vertical */
53
  .resizer-v {
54
+ height: 10px;
55
  cursor: ns-resize;
56
+ background: var(--border);
57
+ margin: 6px 0;
58
+ border-radius: 4px;
59
  display: flex;
60
  align-items: center;
61
  justify-content: center;
62
+ transition: background 0.2s;
 
 
 
 
63
  }
64
+ .resizer-v:hover { background: var(--accent2); }
65
  .resizer-v::after {
66
  content: "";
67
  width: 40px;
68
+ height: 2px;
69
+ background: var(--muted);
70
  border-radius: 2px;
71
  }
 
72
  </style>
73
 
74
  </head>
 
204
 
205
  <section class="card">
206
  <header>
207
+ <h2>Treinar IA</h2>
208
  <div class="chips">
209
  <span class="pill" id="trainerState">estado: inativo</span>
210
  </div>
 
212
  <div class="content">
213
  <div class="row">
214
  <div class="col-12">
215
+ <label for="model_path">Caminho do Modelo Base (exemple: Fedir-Ilina/meta-llamaLlama-3.2-1B)</label>
216
  <input type="text" id="model_path" placeholder="/caminho/para/modelo" />
217
  </div>
218
  <div class="col-12">
 
240
 
241
  </div>
242
 
243
+ <!-- BARRA DE PROGRESSO -->
244
+ <div style="margin:12px 0 6px;">
245
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
246
+ <span style="font-size:12px;color:var(--muted)">Progresso do Treino</span>
247
+ <span id="progressPct" style="font-size:13px;font-family:var(--mono);color:#7ee787;font-weight:600">0%</span>
248
  </div>
249
+ <div style="background:#0a0e14;border:1px solid var(--border);border-radius:999px;height:10px;overflow:hidden;">
250
+ <div id="progressBar" style="width:0%;height:100%;background:linear-gradient(90deg,#1f6feb,#2ea043);border-radius:999px;transition:width 0.5s ease;"></div>
251
+ </div>
252
+ </div>
253
+
254
+ <!-- MÉTRICAS EM TEMPO REAL -->
255
+ <h3>📊 Métricas em Tempo Real</h3>
256
+ <div style="display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-bottom:12px;">
257
+ <div style="background:#0c1117;border:1px solid var(--border);border-radius:8px;padding:10px 12px;">
258
+ <div style="font-size:11px;color:var(--muted);margin-bottom:4px;text-transform:uppercase;letter-spacing:.05em">Estado</div>
259
+ <div id="mv-status" style="font-size:16px;font-weight:700;font-family:var(--mono);color:#79c0ff">—</div>
260
+ </div>
261
+ <div style="background:#0c1117;border:1px solid var(--border);border-radius:8px;padding:10px 12px;">
262
+ <div style="font-size:11px;color:var(--muted);margin-bottom:4px;text-transform:uppercase;letter-spacing:.05em">Gradient Accum.</div>
263
+ <div id="mv-accum" style="font-size:16px;font-weight:700;font-family:var(--mono);color:#ffd479">—</div>
264
+ <div id="mv-accum-origin" style="font-size:10px;color:#7aa2d6;margin-top:2px;"></div>
265
  </div>
266
+ <div style="background:#0c1117;border:1px solid var(--border);border-radius:8px;padding:10px 12px;">
267
+ <div style="font-size:11px;color:var(--muted);margin-bottom:4px;text-transform:uppercase;letter-spacing:.05em">Effective Batch</div>
268
+ <div id="mv-effbatch" style="font-size:16px;font-weight:700;font-family:var(--mono);color:#7ee787">—</div>
269
+ </div>
270
+ <div style="background:#0c1117;border:1px solid var(--border);border-radius:8px;padding:10px 12px;">
271
+ <div style="font-size:11px;color:var(--muted);margin-bottom:4px;text-transform:uppercase;letter-spacing:.05em">Total Steps</div>
272
+ <div id="mv-steps" style="font-size:16px;font-weight:700;font-family:var(--mono);color:#d2a8ff">—</div>
273
+ </div>
274
+ <div style="background:#0c1117;border:1px solid var(--border);border-radius:8px;padding:10px 12px;position:relative;overflow:hidden;">
275
+ <div style="font-size:11px;color:var(--muted);margin-bottom:4px;text-transform:uppercase;letter-spacing:.05em">RAM Usada</div>
276
+ <div id="mv-ram" style="font-size:16px;font-weight:700;font-family:var(--mono);color:#ff7b72">—</div>
277
+ <div style="position:absolute;bottom:0;left:0;right:0;height:3px;background:#1a1f26;">
278
+ <div id="mv-ram-bar" style="height:100%;width:0%;background:#f85149;transition:width .5s,background .5s;border-radius:0 2px 0 0;"></div>
279
+ </div>
280
+ </div>
281
+ </div>
282
+
283
+ <!-- HISTÓRICO DE AJUSTES DO CALLBACK -->
284
+ <div id="accumHistorySection" style="display:none;margin-bottom:12px;">
285
+ <h3>⚙️ Histórico de Ajustes (DynamicAccumulationCallback)</h3>
286
+ <div id="accumHistoryList" style="background:#0a0e14;border:1px solid var(--border);border-radius:8px;padding:8px;max-height:130px;overflow-y:auto;display:flex;flex-direction:column;gap:4px;"></div>
287
+ </div>
288
+
289
+ <!-- PASSO ATUAL & ETA -->
290
+ <div id="currentStepSection" style="display:none;margin-bottom:10px;">
291
+ <div style="background:#0c1117;border:1px solid var(--border);border-radius:8px;padding:10px 14px;font-family:var(--mono);font-size:13px;display:flex;gap:20px;flex-wrap:wrap;">
292
+ <span><span style="color:var(--muted)">Último passo: </span><span id="mv-laststep" style="color:#ffd479;font-weight:600"></span></span>
293
+ <span><span style="color:var(--muted)">Tempo restante (est.): </span><span id="mv-eta" style="color:#79c0ff;font-weight:600"></span></span>
294
+ <span><span style="color:var(--muted)">Último tempo/passo: </span><span id="mv-steptime" style="color:#7ee787;font-weight:600"></span></span>
295
+ </div>
296
+ </div>
297
+
298
+ <!-- LOSS POR ÉPOCA -->
299
+ <div id="epochLossSection" style="display:none;margin-bottom:12px;">
300
+ <h3>📉 Loss por Época</h3>
301
+ <div id="epochLossBar" style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end;"></div>
302
+ </div>
303
+
304
+ <!-- LOGS -->
305
+ <h3>Status do Treino / Logs</h3>
306
+ <div id="trainingBox" style="height:340px;overflow-y:auto;white-space:pre-wrap;border:1px solid var(--border);padding:10px;font-family:var(--mono);font-size:14px;line-height:1.6;background:#0a0e14;border-radius:8px;color:#c9d1d9;">
307
+ À espera do início do treino…
308
+ </div>
309
+
310
+ <!-- INSERE ISTO AQUI -->
311
+ <div id="resizer" class="resizer-v"></div>
312
+
313
+ <h3>Diretório Output</h3>
314
+ <div id="modelDirBox" style="height:200px;overflow-y:auto;border:1px solid var(--border);padding:10px;font-family:var(--mono);font-size:14px;line-height:1.6;background:#0a0e14;border-radius:8px;color:#c9d1d9;">
315
+ A carregar conteúdo do diretório...
316
+ </div>
317
+ <div id="resizer2" class="resizer-v"></div>
318
 
319
  </div>
320
  </section>
 
488
  trainingBox.scrollTop = trainingBox.scrollHeight;
489
  }
490
 
491
+ // Guarda histórico de ajustes do callback (apenas em memória, por sessão)
492
+ const accumAdjustHistory = [];
493
+
494
+ function parseMetricsFromLogs(logs) {
495
+ const metrics = {
496
+ accum: null, accumOrigin: null, effbatch: null, steps: null,
497
+ lastStep: null, eta: null, stepTime: null,
498
+ ram: null, newAccumEvents: []
499
+ };
500
+
501
+ for (const line of logs) {
502
+ // ── Padrão do backend: gradient_accumulation_steps atual: N (origem) ──
503
+ const mAccumInfo = line.match(/gradient_accumulation_steps atual:\s*(\d+)\s*\(([^)]+)\)/);
504
+ if (mAccumInfo) { metrics.accum = mAccumInfo[1]; metrics.accumOrigin = mAccumInfo[2]; }
505
+
506
+ // ── Effective Batch via backend ──
507
+ const mEff = line.match(/Effective Batch[^=]+=\s*(\d+)/);
508
+ if (mEff) metrics.effbatch = mEff[1];
509
+
510
+ // ── Total train steps ──
511
+ const mSteps = line.match(/total_train_steps[^:]*:\s*(\d+)/);
512
+ if (mSteps) metrics.steps = mSteps[1];
513
+
514
+ // ── Ajuste MANUAL: 🧭 Ajuste manual aplicado → gradient_accumulation_steps = N (Effective Batch: Y) ──
515
+ const mManual = line.match(/Ajuste manual aplicado.*gradient_accumulation_steps\s*=\s*(\d+).*Effective Batch[:\s]+(\d+)/);
516
+ if (mManual) {
517
+ metrics.accum = mManual[1];
518
+ metrics.effbatch = mManual[2];
519
+ metrics.accumOrigin = 'manual';
520
+ metrics.newAccumEvents.push({ type: 'manual', accum: mManual[1], effbatch: mManual[2], ram: null });
521
+ }
522
+
523
+ // ── RAM ALTA: ⚠️ RAM alta (X%) → diminuindo accumulation_steps de A para B (Effective Batch: Y) ──
524
+ const mHighRam = line.match(/RAM alta \(([0-9.]+)%\).*diminuindo accumulation_steps de (\d+) para (\d+).*Effective Batch[:\s]+(\d+)/);
525
+ if (mHighRam) {
526
+ metrics.accum = mHighRam[3];
527
+ metrics.effbatch = mHighRam[4];
528
+ metrics.accumOrigin = 'auto↓RAM alta';
529
+ metrics.ram = mHighRam[1];
530
+ metrics.newAccumEvents.push({ type: 'high', ram: mHighRam[1], from: mHighRam[2], accum: mHighRam[3], effbatch: mHighRam[4] });
531
+ }
532
+
533
+ // ── RAM OCIOSA: 💡 RAM ociosa (X%) → aumentando accumulation_steps de A para B (Effective Batch: Y) ──
534
+ const mLowRam = line.match(/RAM ociosa \(([0-9.]+)%\).*aumentando accumulation_steps de (\d+) para (\d+).*Effective Batch[:\s]+(\d+)/);
535
+ if (mLowRam) {
536
+ metrics.accum = mLowRam[3];
537
+ metrics.effbatch = mLowRam[4];
538
+ metrics.accumOrigin = 'auto↑RAM ociosa';
539
+ metrics.ram = mLowRam[1];
540
+ metrics.newAccumEvents.push({ type: 'low', ram: mLowRam[1], from: mLowRam[2], accum: mLowRam[3], effbatch: mLowRam[4] });
541
+ }
542
+
543
+ // ── RAM % de qualquer linha de aviso (fallback) ──
544
+ const mRamLine = line.match(/RAM[^(]*\(([0-9.]+)%\)/);
545
+ if (mRamLine && !metrics.ram) metrics.ram = mRamLine[1];
546
+
547
+ // ── Passo: -- | N/M | ... Tempo: | Xs | ──
548
+ const mStep = line.match(/PASSO:\s*-\s*-\s*\|\s*(\d+)\/(\d+)\s*\|[^|]*Tempo:\s*\|\s*([\d.]+)s/);
549
+ if (mStep) {
550
+ metrics.lastStep = `${mStep[1]} / ${mStep[2]}`;
551
+ metrics.stepTime = `${parseFloat(mStep[3]).toFixed(1)}s`;
552
+ const remaining = (parseInt(mStep[2]) - parseInt(mStep[1])) * parseFloat(mStep[3]);
553
+ if (remaining > 0) {
554
+ const h = Math.floor(remaining / 3600), m = Math.floor((remaining % 3600) / 60), s = Math.floor(remaining % 60);
555
+ metrics.eta = h > 0 ? `${h}h ${m}m` : m > 0 ? `${m}m ${s}s` : `${s}s`;
556
+ }
557
+ }
558
+ }
559
+ return metrics;
560
+ }
561
+
562
+ function updateAccumHistory(events) {
563
+ if (!events || events.length === 0) return;
564
+ const section = $('accumHistorySection');
565
+ const list = $('accumHistoryList');
566
+ if (!section || !list) return;
567
+
568
+ // Deduplica: só adiciona eventos realmente novos
569
+ events.forEach(ev => {
570
+ const key = `${ev.type}-${ev.accum}-${ev.effbatch}-${ev.ram}`;
571
+ if (accumAdjustHistory.find(h => h.key === key)) return;
572
+ accumAdjustHistory.push({ key, ...ev, ts: new Date().toLocaleTimeString() });
573
+
574
+ const row = document.createElement('div');
575
+ const isHigh = ev.type === 'high', isManual = ev.type === 'manual';
576
+ const icon = isManual ? '🧭' : isHigh ? '⚠️' : '💡';
577
+ const color = isManual ? '#79c0ff' : isHigh ? '#ff7b72' : '#7ee787';
578
+ const label = isManual
579
+ ? `Ajuste manual → accum=${ev.accum}, batch=${ev.effbatch}`
580
+ : `RAM ${ev.ram}% → accum ${ev.from}→${ev.accum} (batch ${ev.effbatch})`;
581
+ row.style = `display:flex;align-items:center;gap:8px;font-family:var(--mono);font-size:11px;padding:3px 6px;border-radius:4px;background:#0c1117;`;
582
+ row.innerHTML = `<span>${icon}</span><span style="color:${color};flex:1">${label}</span><span style="color:var(--muted)">${new Date().toLocaleTimeString()}</span>`;
583
+ list.appendChild(row);
584
+ list.scrollTop = list.scrollHeight;
585
+ });
586
+
587
+ if (accumAdjustHistory.length > 0) section.style.display = '';
588
+ }
589
+
590
+ function updateMetricCard(id, value, fallback='—') {
591
+ const el = $(id); if(el) el.textContent = value ?? fallback;
592
+ }
593
+
594
+ function updateEpochLosses(epoch_losses) {
595
+ const section = $('epochLossSection');
596
+ const bar = $('epochLossBar');
597
+ if (!section || !bar || !epoch_losses || epoch_losses.length === 0) return;
598
+ section.style.display = '';
599
+ bar.innerHTML = '';
600
+ const maxLoss = Math.max(...epoch_losses.map(Number).filter(isFinite));
601
+ epoch_losses.forEach((loss, i) => {
602
+ const val = parseFloat(loss);
603
+ const heightPct = isFinite(val) && maxLoss > 0 ? Math.max(10, (val / maxLoss) * 60) : 30;
604
+ const chip = document.createElement('div');
605
+ chip.style = `display:flex;flex-direction:column;align-items:center;gap:4px;`;
606
+ chip.innerHTML = `
607
+ <div style="font-size:11px;font-family:var(--mono);color:#ffd479;font-weight:600">${isFinite(val)?val.toFixed(4):loss}</div>
608
+ <div style="width:28px;height:${heightPct}px;background:linear-gradient(180deg,#1f6feb,#2ea043);border-radius:4px 4px 0 0;min-height:6px;transition:height .4s;"></div>
609
+ <div style="font-size:10px;color:var(--muted)">É${i+1}</div>
610
+ `;
611
+ bar.appendChild(chip);
612
+ });
613
+ }
614
+
615
  function startPolling() {
616
  if (pollTimer) return;
617
 
 
621
  if (!r.ok) throw new Error('HTTP ' + r.status);
622
  const data = await r.json();
623
 
624
+ // Progresso
 
 
625
  if (data.progress !== undefined) {
626
  const pct = typeof data.progress === 'object' ? data.progress.percent : data.progress;
 
627
  const progressBar = $('progressBar');
628
+ const progressPct = $('progressPct');
629
  if (progressBar) progressBar.style.width = `${pct}%`;
630
+ if (progressPct) progressPct.textContent = `${pct}%`;
631
  }
632
 
633
+ // — Estado —
634
+ const statusColors = { training:'#79c0ff', idle:'var(--muted)', done:'#7ee787', error:'#ff7b72' };
635
+ if (data.status) {
636
+ const el = $('mv-status');
637
+ if (el) { el.textContent = data.status; el.style.color = statusColors[data.status] || '#79c0ff'; }
638
+ }
639
+
640
+ // — Métricas extraídas dos logs (fallback) + campos directos do JSON (prioritários) —
641
+ const allLogs = data.logs || [];
642
+ const m = parseMetricsFromLogs(allLogs);
643
 
644
+ // Gradient Accumulation: JSON > logs
645
+ const accumVal = data.current_accum_steps ?? m.accum;
646
+ if (accumVal != null) updateMetricCard('mv-accum', String(accumVal));
647
+
648
+ // Effective Batch: JSON > logs
649
+ const effVal = data.effective_batch ?? m.effbatch;
650
+ if (effVal != null) updateMetricCard('mv-effbatch', String(effVal));
651
+
652
+ // Total Steps: JSON > logs
653
+ const stepsVal = data.total_train_steps ?? m.steps;
654
+ if (stepsVal != null) updateMetricCard('mv-steps', String(stepsVal));
655
+
656
+ // Origem do accum (só via logs)
657
+ if (m.accumOrigin) { const el=$('mv-accum-origin'); if(el) el.textContent=m.accumOrigin; }
658
+
659
+ // Passo actual & ETA
660
+ if (m.lastStep || m.eta || m.stepTime) {
661
+ const sec = $('currentStepSection'); if(sec) sec.style.display='';
662
+ if (m.lastStep) updateMetricCard('mv-laststep', m.lastStep);
663
+ if (m.eta) updateMetricCard('mv-eta', m.eta);
664
+ if (m.stepTime) updateMetricCard('mv-steptime', m.stepTime);
665
  }
666
 
667
+ // — RAM: JSON > logs —
668
+ const ramVal = data.ram_percent ?? (m.ram ? parseFloat(m.ram) : null);
669
+ if (ramVal != null) {
670
+ updateMetricCard('mv-ram', `${parseFloat(ramVal).toFixed(1)}%`);
671
+ const bar = $('mv-ram-bar');
672
+ if (bar) {
673
+ bar.style.width = `${ramVal}%`;
674
+ bar.style.background = ramVal > 85 ? '#f85149' : ramVal > 70 ? '#ffd479' : '#7ee787';
675
+ }
676
  }
677
 
678
+ // Histórico de ajustes do callback —
679
+ updateAccumHistory(m.newAccumEvents);
680
+
681
+ // — Epoch losses —
682
+ updateEpochLosses(data.epoch_losses);
683
+
684
+ // — Logs (só texto limpo) —
685
+ const logsText = allLogs.join('\n');
686
+ const trainingBox = $('trainingBox');
687
+ if (trainingBox) {
688
+ const wasAtBottom = trainingBox.scrollHeight - trainingBox.scrollTop <= trainingBox.clientHeight + 40;
689
+ trainingBox.textContent = logsText;
690
+ if (wasAtBottom) trainingBox.scrollTop = trainingBox.scrollHeight;
691
+ }
692
 
693
  } catch (e) {
694
  console.error('Erro ao atualizar treinamento:', e);
 
767
  }
768
  }
769
 
770
+
771
+
772
+
773
+
774
+
775
  // =========================
776
  // EVENT LISTENERS
777
  // =========================
 
789
  case 'tensorboardBtn': el.addEventListener('click',openTensorBoard); break;
790
  case 'reduceBtn': el.addEventListener('click',()=>manualAdjustAccumulation(-1)); break;
791
  case 'increaseBtn': el.addEventListener('click',()=>manualAdjustAccumulation(1)); break;
792
+ case 'modelDirBtn': el.addEventListener('click', async () => {
793
+ await loadModelDir();
794
+ // Autoscroll para a janela "Diretório Output"
795
+ const box = $('modelDirBox');
796
+ if (box) {
797
+ box.scrollIntoView({ behavior: 'smooth', block: 'start' });
798
+ // Flash visual para indicar que a janela foi atualizada
799
+ box.style.transition = 'box-shadow 0.3s';
800
+ box.style.boxShadow = '0 0 0 2px #1f6feb';
801
+ setTimeout(() => { box.style.boxShadow = 'none'; }, 1200);
802
+ }
803
+ }); break;
804
  }
805
  });
806
  fetchConfigFromServer();
807
  });
808
 
809
+ // Lógica para redimensionar: trainingBox (resizer) e modelDirBox (resizer2)
810
+ function makeResizer(resizerId, targetBoxId) {
811
+ const resizer = document.getElementById(resizerId);
812
+ const box = document.getElementById(targetBoxId);
813
+ if (!resizer || !box) return;
814
+
815
+ resizer.addEventListener('mousedown', function(e) {
816
+ e.preventDefault();
817
+ function onMove(e) {
818
+ const rect = box.getBoundingClientRect();
819
+ const newH = e.clientY - rect.top;
820
+ if (newH > 80 && newH < 1000) box.style.height = newH + 'px';
821
+ }
822
+ function onUp() {
823
+ window.removeEventListener('mousemove', onMove);
824
+ window.removeEventListener('mouseup', onUp);
 
 
 
 
 
 
 
825
  }
826
+ window.addEventListener('mousemove', onMove);
827
+ window.addEventListener('mouseup', onUp);
828
+ });
829
  }
830
 
831
+ makeResizer('resizer', 'trainingBox');
832
+ makeResizer('resizer2', 'modelDirBox');
 
 
 
833
 
834
  </script>
835
  </body>