File size: 10,377 Bytes
bb59211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/** ๆจกๅž‹ๆŽจ็†่ง†ๅ›พ โ€”โ€” ้€‰ๆจกๅž‹/ๆฃ€ๆŸฅ็‚นใ€็œ‹ๅฎƒ็š„่ฎญ็ปƒๅ‚ๆ•ฐใ€้€‰ๆต‹่ฏ•้›†,็„ถๅŽ่ท‘่ฏ„ไผฐใ€‚
 *
 * ไธŽ `static/js/train/` ๅŒๆž„:ไธ€ไธชๅ…ฅๅฃ + ่‹ฅๅนฒๆจกๅ—,ๅ…ฑ็”จ `static/js/shared/` ไธ‹็š„
 * ็บฏๅทฅๅ…ทใ€‚**ไธๆ–ฐๅผ€ไธ€ๅฅ—** โ€”โ€” ๆŽจ็†ๅ’Œ่ฎญ็ปƒๆœฌๆฅๅฐฑๆ˜ฏๅŒไธ€ๆ‰นไบง็‰ฉ็š„ไธŠไธ‹ๆธธใ€‚
 *
 * **ๅŽ็ซฏๆ‰ง่กŒๆœชๆŽฅ** โ€”โ€” ใ€Œ่ฟ่กŒๆŽจ็†ใ€ๆŒ‰้’ฎๅ’Œ่ฎญ็ปƒ้กตไธ€ๆ ทๆ˜ฏ**ๆ•…ๆ„็ฆ็”จ**็š„,ๆ—่พนๅ†™ๆ˜ŽๅŽŸๅ› ใ€‚
 * ่ฟ™ไธ€้กต็œŸๆญฃ่ƒฝ็”จ็š„ๆ˜ฏๅ‰้ขไธ‰ๆฎต(้€‰ๆจกๅž‹ / ็œ‹ๅ‚ๆ•ฐ / ้€‰ๆต‹่ฏ•้›†),ๆ•ฐๆฎ้ƒฝๆ˜ฏ็œŸ็š„:
 *   ๆจกๅž‹ไธŽๆฃ€ๆŸฅ็‚น โ†’ GET /api/v1/training/runs
 *   ่ฎญ็ปƒๅ‚ๆ•ฐ     โ†’ ้‚ฃไบ› checkpoint ้‡Œ็š„ train_config.json
 *   ๆต‹่ฏ•้›†       โ†’ GET /api/v1/training/datasets
 *
 * ๆฒกๆœ‰ใ€Œๅฎžๆ—ถๆ—ฅๅฟ—ใ€ใ€ŒๆŒ‡ๆ ‡ๆ›ฒ็บฟใ€ไน‹็ฑป็š„็ฉบ้กต็ญพ โ€”โ€” ๆฒกๆœ‰ๆ•ฐๆฎ็š„้กต็ญพไธๆ‘†,็†็”ฑๅŒ่ฎญ็ปƒ้กตใ€‚
 *
 * โ˜… ๆ•ฐๆฎๆบไธบไป€ไนˆ็›ดๆŽฅๅผ• `../train/api.js`:runs / datasets ่ฟ™ไธคไธช็ซฏ็‚นๅฑžไบŽ**่ฎญ็ปƒๅŸŸ**
 *   (ๆŽจ็†ๆถˆ่ดน็š„ๆ˜ฏ่ฎญ็ปƒไบง็‰ฉ)ใ€‚็ญ‰ๅฐ†ๆฅๆœ‰็ฌฌไธ‰ไธช้กต้ขไนŸ็”จๅˆฐๅฎƒไปฌ,ๅ†ๆŠฝๅˆฐ shared/ ๅŽปใ€‚
 */

import { el, clear } from '../shared/dom.js';
import { tr } from '../shared/text.js';
import { listDatasets, listRuns } from '../train/api.js';

const INPUT_CLASS = 'bg-gray-800 text-xs rounded px-2 py-1 border border-gray-700 '
  + 'focus:border-blue-500 focus:outline-none font-mono';
const BTN = 'bg-gray-800 hover:bg-gray-700 text-gray-300 text-xs px-2.5 py-1 '
  + 'rounded border border-gray-700';

const LIVE = 'bg-green-900/50 text-green-300';
const PENDING = 'bg-amber-900/50 text-amber-300';

function section(titleKey, fallback, badge, children) {
  return el('div', { class: 'bg-gray-900 rounded-lg border border-gray-800 p-4 mb-4' }, [
    el('div', { class: 'flex items-center gap-3 mb-3' }, [
      el('div', { class: 'text-sm font-semibold text-gray-200', text: tr(titleKey, fallback) }),
      badge ? el('span', { class: `text-[10px] px-1.5 py-0.5 rounded ${badge.cls}`, text: badge.text }) : null,
    ]),
    ...children,
  ]);
}

function labeled(label, control, labelClass = 'text-gray-500 shrink-0 w-32') {
  return el('div', { class: 'flex items-center gap-3 text-xs' }, [
    el('span', { class: labelClass, text: label }),
    control,
  ]);
}

function hint(key, fallback) {
  return el('div', { class: 'text-xs text-gray-600 mt-3', text: tr(key, fallback) });
}

/** ไธ€่กŒใ€Œๅ‚ๆ•ฐๅ โ†’ ๅ€ผใ€ใ€‚ๆŽจ็†ๅ‰ๆœ€่ฏฅ็กฎ่ฎค็š„ๅฐฑๆ˜ฏ**่ฎญ็ปƒๆ—ถ็”จ็š„ chunk_size /
 *  n_action_steps** ่ฟ™็ฑปไผš็›ดๆŽฅๅฝฑๅ“ๆŽจ็†่พ“ๅ‡บ็š„ๅ‚ๆ•ฐ,ๆ‰€ไปฅ่ฟ™้‡ŒๅชๆŒ‘่ฟ™ๅ‡ ไธชๅ…ณ้”ฎ็š„,
 *  ๅฎŒๆ•ด็š„่ฎญ็ปƒๅ‚ๆ•ฐๅœจ่ฎญ็ปƒ้กต้‚ฃไธชใ€Œ่ฎญ็ปƒๅ‚ๆ•ฐใ€้กต็ญพ้‡Œๆœ‰ใ€‚ */
function paramRow(name, value) {
  return el('div', { class: 'flex items-baseline gap-3 text-xs py-0.5' }, [
    el('span', { class: 'text-gray-500 shrink-0 w-32 font-mono', text: name }),
    el('span', { class: 'text-gray-200 font-mono break-all', text: value ?? 'โ€”' }),
  ]);
}

const KEY_PARAMS = [
  'policy.type', 'policy.chunk_size', 'policy.n_action_steps',
  'policy.dim_model', 'policy.vision_backbone',
];

function pick(obj, path) {
  return path.split('.').reduce((a, k) => (a === null || a === undefined ? undefined : a[k]), obj);
}

/**
 * @param {HTMLElement} container
 * @param {{runName?: string, checkpointName?: string}} [preselect]
 *        ไปŽใ€Œๆจกๅž‹่ฎญ็ปƒใ€้กต็ญพ็‚นๆŸไธชๆฃ€ๆŸฅ็‚น็š„ใ€ŒๆŽจ็†ใ€่ทณ่ฟ‡ๆฅๆ—ถๅธฆไธŠ โ€”โ€” ็›ดๆŽฅ้€‰ไธญๅฎƒ,
 *        ไธ็”จๅ†ๆ‰‹ๅŠจๅœจไธ‹ๆ‹‰้‡Œๆ‰พไธ€้ใ€‚
 */
export function renderInference(container, preselect) {
  clear(container);

  const state = { run: null, checkpoint: null, dataset: null };

  // โ”€โ”€ ้€‰ๆ‹ฉๆจกๅž‹ / ๆฃ€ๆŸฅ็‚น โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  const runSelect = el('select', { class: INPUT_CLASS + ' w-64' });
  const stepSelect = el('select', { class: INPUT_CLASS + ' w-40', disabled: true });
  const paramHost = el('div', { class: 'mt-3' });
  let runs = [];

  function renderCheckpointParams() {
    clear(paramHost);
    const config = state.checkpoint?.train_config;
    if (!config || !Object.keys(config).length) {
      paramHost.appendChild(el('div', {
        class: 'text-xs text-gray-600 py-2',
        text: tr('inf_no_params', 'This checkpoint has no train_config.json; parameters unavailable.'),
      }));
      return;
    }
    paramHost.appendChild(el('div', {
      class: 'text-[10px] text-gray-600 mb-2',
      text: `${tr('inf_params_from', 'From')} checkpoints/${state.checkpoint.name}/pretrained_model/train_config.json`,
    }));
    paramHost.appendChild(el('div', { class: 'border border-gray-800 rounded p-3' },
      KEY_PARAMS.map((p) => paramRow(p, pick(config, p)))));
  }

  function fillSteps(wantedStep) {
    clear(stepSelect);
    const run = runs.find((r) => r.name === runSelect.value);
    if (!run) {
      stepSelect.disabled = true;
      stepSelect.appendChild(el('option', { value: '', text: 'โ€”' }));
      state.checkpoint = null;
      renderCheckpointParams();
      return;
    }
    stepSelect.disabled = false;
    for (const c of run.checkpoints) {
      stepSelect.appendChild(el('option', { value: c.name, text: `${c.name} (${c.step})` }));
    }
    // ๆœ‰้ข„้€‰ๅฐฑ็”จ้ข„้€‰,ๅฆๅˆ™็”จๆœ€ๆ–ฐ็š„้‚ฃไธชใ€‚้ข„้€‰ๅๅฏ่ƒฝๅฏนไธไธŠ(ๆฏ”ๅฆ‚้‚ฃไธช checkpoint
    // ๅˆš่ขซๅˆ ),ๅฏนไธไธŠๅฐฑ้€€ๅ›žๆœ€ๆ–ฐ โ€”โ€” ไธ่ฆๅ› ไธบไธ€ไธช่ฟ‡ๆœŸ็š„ๅ‚ๆ•ฐ่ฎฉๆ•ด้กต็ฉบ็€ใ€‚
    const pick = (wantedStep && run.checkpoints.find((c) => c.name === wantedStep))
      || run.checkpoints[0] || null;
    state.checkpoint = pick;
    if (pick) stepSelect.value = pick.name;
    renderCheckpointParams();
  }

  runSelect.addEventListener('change', () => {
    state.run = runSelect.value || null;
    fillSteps();
  });
  stepSelect.addEventListener('change', () => {
    const run = runs.find((r) => r.name === runSelect.value);
    state.checkpoint = (run?.checkpoints || []).find((c) => c.name === stepSelect.value) || null;
    renderCheckpointParams();
  });

  const sectionModel = section('inf_sec_model', '1. Model', null, [
    el('div', { class: 'flex gap-6 flex-wrap' }, [
      labeled(tr('inf_label_run', 'Run'), runSelect),
      labeled(tr('inf_label_step', 'Checkpoint'), stepSelect),
    ]),
    paramHost,
  ]);
  paramHost.appendChild(el('div', { class: 'text-xs text-gray-600 py-2', text: tr('loading', 'Loading...') }));

  listRuns()
    .then((payload) => {
      runs = payload.runs || [];
      clear(runSelect);
      if (!runs.length) {
        runSelect.appendChild(el('option', { value: '', text: tr('inf_no_models', 'No trained model yet') }));
        renderCheckpointParams();
        return;
      }
      for (const r of runs) {
        runSelect.appendChild(el('option', { value: r.name, text: `${r.name} (${r.checkpoint_count})` }));
      }
      const chosen = runs.find((r) => r.name === preselect?.runName) || runs[0];
      runSelect.value = chosen.name;
      state.run = chosen.name;
      fillSteps(preselect?.checkpointName);
    })
    .catch((err) => {
      clear(paramHost);
      paramHost.appendChild(el('div', {
        class: 'text-xs text-red-400 py-2',
        text: `${tr('train_load_failed', 'Failed to load')}: ${err.message}`,
      }));
    });

  // โ”€โ”€ ๆต‹่ฏ•้›† โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  const datasetSelect = el('select', { class: INPUT_CLASS + ' w-80' }, [
    el('option', { value: '', text: tr('inf_dataset_none', 'โ€” Select a test dataset โ€”') }),
  ]);
  datasetSelect.addEventListener('change', () => {
    const value = datasetSelect.value;
    state.dataset = value ? { name: value } : null;
  });
  const datasetNote = el('div', { class: 'text-xs text-gray-600 mt-2' });

  const sectionData = section('inf_sec_data', '2. Test Data',
    { cls: LIVE, text: tr('inf_live', 'live') }, [
      labeled(tr('inf_label_dataset', 'Test dataset'), datasetSelect),
      datasetNote,
    ]);
  datasetNote.textContent = tr('loading', 'Loading...');

  listDatasets()
    .then((payload) => {
      const items = payload.datasets || [];
      datasetNote.textContent = items.length
        ? `${items.length} ${tr('inf_datasets_available', 'dataset(s) available')}`
        : tr('inf_no_datasets', 'No exported dataset yet โ€” export one from the review page first.');
      for (const d of items) {
        datasetSelect.appendChild(el('option', {
          value: d.name,
          text: `${d.name} โ€” ${d.total_episodes} ${tr('train_episodes', 'episodes')}`,
        }));
      }
    })
    .catch(() => { datasetNote.textContent = ''; });

  // โ”€โ”€ ่ฟ่กŒ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  const sectionRun = section('inf_sec_run', '3. Run', { cls: PENDING, text: tr('inf_pending', 'not wired') }, [
    el('div', { class: 'flex items-center gap-3' }, [
      el('button', {
        class: 'bg-blue-600/40 text-blue-200/60 text-sm px-5 py-2 rounded cursor-not-allowed',
        text: tr('inf_run_btn', 'Run inference'),
        disabled: true,
        title: tr('inf_run_disabled', 'Backend execution is not wired up yet'),
      }),
      el('span', {
        class: 'text-xs text-amber-400',
        text: tr('inf_run_note', 'Backend execution is not connected yet โ€” this button is intentionally disabled.'),
      }),
    ]),
    hint('inf_run_hint', 'Once wired, this runs the checkpoint over the chosen test set and reports direction cosine, amplitude ratio, gripper MAE and the error curve.'),
  ]);

  // โ”€โ”€ ็ป“ๆžœ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  const sectionResult = section('inf_sec_result', '4. Results', null, [
    el('div', { class: 'text-center py-8' }, [
      el('div', { class: 'text-xs text-gray-600', text: tr('inf_no_result', 'No evaluation result yet.') }),
    ]),
  ]);

  for (const node of [sectionModel, sectionData, sectionRun, sectionResult]) {
    container.appendChild(node);
  }
}

// ่ฟ™้‡Œ**ๆฒกๆœ‰** boot/่‡ชๅฏๅŠจ โ€”โ€” ๆœฌๆจกๅ—ไธๅ†ๆ˜ฏ้กต้ขๅ…ฅๅฃ,่€Œๆ˜ฏ่ขซ train/main.js
// ๅฝ“ไฝœ็ฌฌไธ‰ไธช้กต็ญพ import ่ฟ›ๅŽป(`render: renderInference`)ใ€‚
// ้กต็ญพ็š„ render ็ญพๅๆ˜ฏ (panel, run),ๆœฌๅ‡ฝๆ•ฐๅช็”จๅˆฐ panel,ๅคšๆ”ถไธ€ไธชๅ‚ๆ•ฐๆ— ๅฆจใ€‚