File size: 3,551 Bytes
cd470c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/**

 * TelemetryUI Module

 * Synchronizes scientific metrics with the dashboard.

 */
export class TelemetryUI {
  constructor() {
    this.els = {
      kpi: {
        step: document.getElementById('kpi-step'),
        reward: document.getElementById('kpi-reward'),
        best: document.getElementById('kpi-best'),
        orderIndex: document.getElementById('kpi-order'),
        atoms: document.getElementById('kpi-atoms'),
        cost: document.getElementById('kpi-cost'),
        episodeId: document.getElementById('episode-id')
      },
      log: document.getElementById('log-box'),
      statusText: document.getElementById('status-text'),
      statusDot: document.getElementById('status-dot')
    };
  }

  updateKPIs(obs, reward, bestReward) {
    const { kpi } = this.els;
    if (!kpi.step) return;

    kpi.step.textContent = `${obs.step_number} / ${obs.max_steps}`;
    kpi.reward.textContent = (reward ?? 0).toFixed(4);
    kpi.best.textContent = bestReward.toFixed(4);
    kpi.cost.textContent = `${Math.round(obs.total_cost)} / ${Math.round(obs.cost_budget)}`;
    kpi.atoms.textContent = obs.grid.flat().filter(c => c !== '.').length;
    
    if (kpi.orderIndex) {
        const orderIdx = obs.score_breakdown?.lattice_order_index ?? 0;
        kpi.orderIndex.textContent = orderIdx.toFixed(3);
    }
    
    if (obs.metadata && kpi.episodeId) {
      kpi.episodeId.textContent = `Scenario: ${obs.metadata.scenario_name} | Difficulty: ${obs.metadata.difficulty}`;
    }
  }

  updatePropertyBars(obs) {
    ['hardness', 'conductivity', 'thermal_resistance', 'elasticity'].forEach(p => {
      const valText = document.getElementById(`pv-${p}`);
      const fillBar = document.getElementById(`pb-${p}`);
      const targetMarker = document.getElementById(`pt-${p}`);
      
      if (!valText || !fillBar) return;

      const curr = obs.current_properties[p] ?? 0;
      const tgt  = obs.target[p] ?? 0;
      
      valText.textContent = `${curr.toFixed(0)} / ${tgt.toFixed(0)}`;
      fillBar.style.width = Math.min(curr, 100) + '%';
      if (targetMarker) targetMarker.style.left = Math.min(tgt, 100) + '%';
    });

    const sb = obs.score_breakdown || {};
    const stabText = document.getElementById('sc-stability');
    const orderText = document.getElementById('sc-order');
    if (stabText) stabText.textContent = (sb.structural_stability ?? 0).toFixed(3);
    if (orderText) orderText.textContent = (sb.lattice_order_index ?? 0).toFixed(3);
    
    const stabFill = document.getElementById('sb-stability');
    const orderFill = document.getElementById('sb-order');
    if (stabFill) stabFill.style.width = ((sb.structural_stability ?? 0) * 100) + '%';
    if (orderFill) orderFill.style.width = ((sb.lattice_order_index ?? 0) * 100) + '%';
  }

  setStatus(online) {
    if (this.els.statusText) {
        this.els.statusText.textContent = online ? 'LIVE' : 'OFFLINE';
    }
    if (this.els.statusDot) {
        this.els.statusDot.classList.toggle('offline', !online);
    }
  }

  log(msg, type = 'info') {
    if (!this.els.log) return;
    const entry = document.createElement('div');
    entry.className = `log-line ${type}`; /* changed to match playground.css line 86 */
    const ts = new Date().toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
    entry.innerHTML = `<span style="opacity:0.4">[${ts}]</span> <span>${msg}</span>`;
    this.els.log.prepend(entry);
  }
}