llm-tuner 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -202,6 +202,7 @@ async function chooseEngine(e) {
202
202
  step('model');
203
203
  $('#model').innerHTML = `<option>${esc(t('web.readingModels'))}</option>`;
204
204
  const { models } = await api(`/api/models?engine=${e.id}`);
205
+ models.sort((a, b) => (a.sizeBytes ?? 0) - (b.sizeBytes ?? 0) || (a.name || '').localeCompare(b.name || ''));
205
206
  $('#model').innerHTML = models.length
206
207
  ? models.map((m) => `<option value="${esc(m.key)}">${esc(m.name)} · ${gb(m.sizeBytes)}${m.quant ? ' · ' + esc(m.quant) : ''}</option>`).join('')
207
208
  : `<option value="">${esc(t('web.noModels'))}</option>`;
@@ -386,6 +387,59 @@ function onCtxChange() {
386
387
  }
387
388
  $('#ctx').oninput = onCtxChange;
388
389
 
390
+ function updateTelemetry(e) {
391
+ const panel = $('#telemetry-panel');
392
+ if (!panel || !e) return;
393
+ panel.hidden = false;
394
+
395
+ if (e.cpu) {
396
+ const cpuVal = $('#telemetry-cpu-val');
397
+ const cpuFill = $('#telemetry-cpu-fill');
398
+ if (cpuVal) cpuVal.textContent = `${e.cpu.percent}%`;
399
+ if (cpuFill) cpuFill.style.width = `${Math.min(100, Math.max(0, e.cpu.percent))}%`;
400
+ }
401
+
402
+ if (e.ram) {
403
+ const ramVal = $('#telemetry-ram-val');
404
+ const ramFill = $('#telemetry-ram-fill');
405
+ if (ramVal) ramVal.textContent = `${gb(e.ram.usedBytes)} / ${gb(e.ram.totalBytes)} (${e.ram.percent}%)`;
406
+ if (ramFill) ramFill.style.width = `${Math.min(100, Math.max(0, e.ram.percent))}%`;
407
+ }
408
+
409
+ if (e.gpus) {
410
+ const box = $('#telemetry-gpus');
411
+ if (box) {
412
+ box.innerHTML = e.gpus.map((g) => {
413
+ const name = g.name.replace(/^NVIDIA GeForce /, '');
414
+ return `<div class="telemetry-card">
415
+ <div class="telemetry-header"><span>GPU ${g.index} (${esc(name)})</span> <strong>${gb(g.usedBytes)} / ${gb(g.totalBytes)} (${g.percent}%)</strong></div>
416
+ <div class="meter-bar"><div class="meter-fill" style="width: ${Math.min(100, Math.max(0, g.percent))}%"></div></div>
417
+ </div>`;
418
+ }).join('');
419
+ }
420
+ }
421
+ }
422
+
423
+ function updateProgress(p, label) {
424
+ const wrap = $('#load-progress-wrap');
425
+ if (!wrap || !p) return;
426
+ wrap.hidden = false;
427
+
428
+ const bar = $('#load-progress-bar');
429
+ const pct = $('#load-progress-pct');
430
+ const lbl = $('#load-progress-label');
431
+
432
+ if (pct) pct.textContent = `${p.pct}%`;
433
+ if (bar) bar.style.width = `${p.pct}%`;
434
+ if (lbl && label !== undefined) {
435
+ let text = label || '';
436
+ if (p.phase === 'bench' && p.current && p.total) {
437
+ text = `[${p.current}/${p.total}] ${text}`.trim();
438
+ }
439
+ lbl.textContent = text;
440
+ }
441
+ }
442
+
389
443
  // ---------- Load ----------
390
444
  $('#load-btn').onclick = async () => {
391
445
  const model = $('#model').value;
@@ -407,6 +461,17 @@ $('#load-btn').onclick = async () => {
407
461
  $('#load-result').innerHTML = '';
408
462
  const log = $('#load-log');
409
463
  log.textContent = '';
464
+ // Reset progress and telemetry
465
+ $('#load-progress-wrap').hidden = false;
466
+ $('#load-progress-bar').style.width = '0%';
467
+ $('#load-progress-pct').textContent = '0%';
468
+ $('#load-progress-label').textContent = '';
469
+ $('#telemetry-panel').hidden = false;
470
+ $('#telemetry-cpu-val').textContent = '0%';
471
+ $('#telemetry-cpu-fill').style.width = '0%';
472
+ $('#telemetry-ram-val').textContent = '0 / 0 GB (0%)';
473
+ $('#telemetry-ram-fill').style.width = '0%';
474
+ $('#telemetry-gpus').innerHTML = '';
410
475
  // One element per line, so a warning can be styled on its own.
411
476
  const addLog = (s, level = 'note') => {
412
477
  const line = document.createElement('span');
@@ -424,6 +489,17 @@ $('#load-btn').onclick = async () => {
424
489
  // Progress event before the current one: a fallback that repeats its reason is not logged twice.
425
490
  let previous = null;
426
491
  const done = await follow(jobId, (e) => {
492
+ if (e.type === 'telemetry') {
493
+ updateTelemetry(e);
494
+ return;
495
+ }
496
+ if (e.progress) {
497
+ let label = '';
498
+ if (e.type === 'status' && e.code) label = t(e.code, e.params);
499
+ else if (e.type === 'candidate-start') label = describe(e.candidate);
500
+ else if (e.type === 'bench-progress' && e.phase) label = e.phase === 'deep' ? t('common.longPrompt', { tokens: e.promptTokens }) : '';
501
+ updateProgress(e.progress, label);
502
+ }
427
503
  if (e.type === 'variant-picked' || e.type === 'profile-fallback') {
428
504
  for (const line of profileEventLines(e, t, { engineName: state.engine.name, previous })) addLog(line.text, line.level);
429
505
  }
@@ -456,6 +532,8 @@ $('#load-btn').onclick = async () => {
456
532
  const loaded = done.result?.loaded;
457
533
  throw new Error(done.code || done.error ? errText(done) : loaded && !loaded.ok ? benchErr(loaded) : t('bench.loadFailed'));
458
534
  }
535
+ updateProgress({ pct: 100, phase: 'done' });
536
+
459
537
  const r = done.result;
460
538
  if (r.report?.best) {
461
539
  const idx = r.report.results.findIndex((x) => x.candidate.id === r.report.best.candidate.id);
@@ -75,6 +75,26 @@
75
75
 
76
76
  <section id="sec-load" class="panel" hidden>
77
77
  <header><h2 id="load-title" data-i18n="web.loading">Loading</h2><span class="pill" id="load-status" data-i18n="web.inProgress">in progress</span></header>
78
+ <div id="load-progress-wrap" class="progress-wrap" hidden>
79
+ <div class="progress-bar-track">
80
+ <div id="load-progress-bar" class="progress-bar-fill" style="width: 0%"></div>
81
+ </div>
82
+ <div class="progress-meta">
83
+ <span id="load-progress-label"></span>
84
+ <span id="load-progress-pct">0%</span>
85
+ </div>
86
+ </div>
87
+ <div id="telemetry-panel" class="telemetry-panel" hidden>
88
+ <div class="telemetry-card">
89
+ <div class="telemetry-header"><span data-i18n="web.telemetryCpu">CPU</span> <strong id="telemetry-cpu-val">0%</strong></div>
90
+ <div class="meter-bar"><div id="telemetry-cpu-fill" class="meter-fill" style="width: 0%"></div></div>
91
+ </div>
92
+ <div class="telemetry-card">
93
+ <div class="telemetry-header"><span data-i18n="web.telemetryRam">RAM</span> <strong id="telemetry-ram-val">0 / 0 GB (0%)</strong></div>
94
+ <div class="meter-bar"><div id="telemetry-ram-fill" class="meter-fill" style="width: 0%"></div></div>
95
+ </div>
96
+ <div id="telemetry-gpus" class="telemetry-gpus"></div>
97
+ </div>
78
98
  <div id="bench" hidden>
79
99
  <p class="muted small" data-i18n="web.benchIntro">There is no preset for this context and profile: several configurations are tested on your hardware. It takes a few minutes and only happens once.</p>
80
100
  <div class="table-wrap">
@@ -328,3 +328,19 @@ td .err { color: var(--bad); white-space: normal; }
328
328
  .chat-form input { flex: 1; min-width: 0; }
329
329
  .chat-form button { flex-shrink: 0; }
330
330
 
331
+ /* Progress bar and real-time telemetry */
332
+ .progress-wrap { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
333
+ .progress-bar-track { width: 100%; height: 8px; background: var(--code-bg); border: 1px solid var(--line); border-radius: 4px; overflow: hidden; }
334
+ .progress-bar-fill { height: 100%; background: var(--accent); transition: width .2s ease; }
335
+ .progress-meta { display: flex; justify-content: space-between; align-items: center; font: 500 .8rem var(--mono); color: var(--muted); }
336
+ .progress-meta strong { color: var(--ink); }
337
+
338
+ .telemetry-panel { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin-top: 10px; }
339
+ .telemetry-card { background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; display: grid; gap: 6px; }
340
+ .telemetry-header { display: flex; justify-content: space-between; align-items: center; font: 500 .8rem var(--mono); color: var(--muted); }
341
+ .telemetry-header strong { color: var(--ink); }
342
+ .telemetry-gpus { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
343
+ .meter-bar { width: 100%; height: 8px; background: var(--code-bg); border: 1px solid var(--line); border-radius: 4px; overflow: hidden; }
344
+ .meter-fill { height: 100%; background: var(--gpu); transition: width .2s ease; }
345
+ #telemetry-cpu-fill { background: var(--accent); }
346
+