From f44342158cf7f9cef135b0a2721aadb791ef91ca Mon Sep 17 00:00:00 2001 From: webdevtodayjason Date: Mon, 6 Jul 2026 14:44:53 -0500 Subject: [PATCH] fix(web): wire Server-view LLM load; cancel-download error surfacing; wizard empty-state; drop dead renderModels Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017NziXzqT1L9kj2T1byA3Ak --- ainode/web/static/js/app.js | 124 +++++++++++++++++++++----------- ainode/web/templates/index.html | 7 -- 2 files changed, 82 insertions(+), 49 deletions(-) diff --git a/ainode/web/static/js/app.js b/ainode/web/static/js/app.js index cb11bd1..2f01e7d 100644 --- a/ainode/web/static/js/app.js +++ b/ainode/web/static/js/app.js @@ -343,9 +343,6 @@ const AINode = { this.renderDownloads(); } break; - case 'models': - this.renderModels(); - break; case 'training': this.renderTraining(); break; @@ -2042,9 +2039,17 @@ const AINode = { }).join('') + ''; - // Wire cancel buttons (event delegation) - container.querySelectorAll('.queue-item-cancel').forEach(function (btn) { - btn.addEventListener('click', function (e) { + // Wire cancel buttons via TRUE event delegation on the stable container. + // Rows redrawn in place (renderQueueItemInPlace → existing.outerHTML) mint + // brand-new button nodes; a per-button listener would not survive that + // (e.g. cancelDownload's revert() redraws an enabled, listener-less button + // that would then be a silent no-op). Bind once to the container — which + // innerHTML/outerHTML on its children never replaces — and match on click. + if (!container._cancelDelegated) { + container._cancelDelegated = true; + container.addEventListener('click', function (e) { + var btn = e.target.closest && e.target.closest('.queue-item-cancel'); + if (!btn || !container.contains(btn)) return; e.stopPropagation(); var jobId = btn.getAttribute('data-job-id'); if (!jobId) return; @@ -2052,17 +2057,35 @@ const AINode = { btn.textContent = '…'; self.cancelDownload(jobId); }); - }); + } }, cancelDownload(jobId) { var self = this; + // Revert any rows for this job back to 'downloading' so the poll resumes + // and the cancel button (currently disabled/…) is redrawn fresh. + var revert = function () { + Object.keys(self.state.activeDownloads || {}).forEach(function (repo) { + var dl = self.state.activeDownloads[repo]; + if (dl.jobId === jobId) { + dl.status = 'downloading'; + self.renderQueueItemInPlace(repo); + } + }); + }; fetch('/api/models/download-cancel', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ job_id: jobId }), }) - .then(function (r) { return r.json(); }) + .then(function (r) { + return r.json().catch(function () { return {}; }).then(function (data) { + if (!r.ok) { + throw new Error((data.error && data.error.message) || data.error || ('HTTP ' + r.status)); + } + return data; + }); + }) .then(function (data) { if (data.status === 'cancelling') { // Mark the local state so the UI updates immediately @@ -2073,9 +2096,15 @@ const AINode = { self.renderQueueItemInPlace(repo); } }); + } else { + // Unexpected response shape — don't leave the row stuck. + revert(); } }) - .catch(function () { /* server will clean up */ }); + .catch(function (err) { + self.toast('Cancel failed: ' + err.message, 'error'); + revert(); + }); }, renderQueueItem(dl) { @@ -2864,30 +2893,10 @@ const AINode = { }); }, - // ======================================================================== - // MODELS VIEW (secondary, accessible from downloads or direct) - // ======================================================================== - - renderModels() { - // Reuse downloads rendering for the models view - this.renderDownloads(); - }, - // ======================================================================== // TRAINING VIEW (overview / autodata / datasets / runs / templates) // ======================================================================== - trainingModels: [ - { id: 'meta-llama/Llama-3.2-3B-Instruct', name: 'Llama 3.2 3B Instruct', size: '~6 GB' }, - { id: 'meta-llama/Llama-3.1-8B-Instruct', name: 'Llama 3.1 8B Instruct', size: '~16 GB' }, - { id: 'meta-llama/Llama-3.1-70B-Instruct-AWQ', name: 'Llama 3.1 70B AWQ', size: '~38 GB' }, - { id: 'Qwen/Qwen2.5-72B-Instruct', name: 'Qwen 2.5 72B Instruct', size: '~145 GB' }, - { id: 'deepseek-ai/DeepSeek-R1-Distill-Qwen-7B', name: 'DeepSeek R1 7B', size: '~14 GB' }, - { id: 'mistralai/Mistral-7B-Instruct-v0.3', name: 'Mistral 7B v0.3', size: '~14 GB' }, - { id: 'microsoft/Phi-3-mini-4k-instruct', name: 'Phi-3 Mini 4K', size: '~7.5 GB' }, - { id: 'meta-llama/CodeLlama-34b-Instruct-hf', name: 'CodeLlama 34B', size: '~63 GB' }, - ], - // ---- Sidebar ------------------------------------------------------------ async renderTrainingSidebar() { var mount = document.getElementById('left-panel-training'); @@ -3555,12 +3564,15 @@ const AINode = { : (((await this.fetchJSON('/api/datasets')) || {}).datasets || []); this.state.datasets = dsets; - // Try to get downloaded models from /api/models; fall back to built-in list + // Only offer models that are actually on disk — a run against a + // not-downloaded model would fail. When none are downloaded we leave the + // list empty and show an empty-state hint (the free-text HF-id input in + // step 1 still works). The backend only ever sets `downloaded`. var modelsResp = await this.fetchJSON('/api/models'); var modelList = []; if (modelsResp && Array.isArray(modelsResp.models)) { modelList = modelsResp.models - .filter(function (m) { return m.downloaded || m.is_downloaded || m.status === 'downloaded'; }) + .filter(function (m) { return m.downloaded; }) // base_model = the canonical HF repo id (cards know it), NOT the on-disk // slug — the containerized runner hands base_model to // AutoTokenizer.from_pretrained, which rejects a '--' slug (HFValidationError). @@ -3569,7 +3581,6 @@ const AINode = { return { id: repo, name: m.name || repo, size: m.size || m.size_gb || '' }; }); } - if (!modelList.length) modelList = this.trainingModels.slice(); var initial = Object.assign({ base_model: modelList[0] ? modelList[0].id : '', @@ -3690,16 +3701,23 @@ const AINode = { _renderWizardStep1() { var s = this.state.wizardState; - var opts = (s.models || []).map(function (m) { - var active = m.id === s.base_model ? ' active' : ''; - return '
' + - '
' + AINode.esc(m.name || m.id) + '
' + - '
' + AINode.esc(m.id) + (m.size ? ' · ' + AINode.esc(m.size) : '') + '
' + - '
'; - }).join(''); + var models = s.models || []; + var grid; + if (models.length) { + grid = '
' + models.map(function (m) { + var active = m.id === s.base_model ? ' active' : ''; + return '
' + + '
' + AINode.esc(m.name || m.id) + '
' + + '
' + AINode.esc(m.id) + (m.size ? ' · ' + AINode.esc(m.size) : '') + '
' + + '
'; + }).join('') + '
'; + } else { + grid = '
' + + 'No downloaded models — download one from the Models page, or enter an HF id below.
'; + } return '

Choose a Base Model

' + '

Downloaded models are shown. Your fine-tune will build on top of this one.

' + - '
' + opts + '
' + + grid + '
' + '
'; }, @@ -5562,8 +5580,30 @@ const AINode = { html += ''; body.innerHTML = html; body.querySelectorAll('[data-action="llm-load"]').forEach(function (btn) { - btn.addEventListener('click', function () { - self.toast('LLM load requires a restart with --model ' + btn.dataset.model + ' (coming soon via engine swap)', 'info'); + btn.addEventListener('click', async function () { + var mid = btn.dataset.model; + btn.disabled = true; + btn.textContent = 'Loading…'; + try { + var resp = await fetch('/api/models/load', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model: mid }), + }); + var payload = await resp.json().catch(function () { return {}; }); + if (resp.ok && !payload.error) { + self.toast('Loaded ' + mid, 'success'); + self.renderServer(); + } else { + self.toast((payload.error && payload.error.message) || payload.error || 'Load failed', 'error'); + btn.disabled = false; + btn.textContent = 'Load'; + } + } catch (err) { + self.toast('Load failed: ' + err.message, 'error'); + btn.disabled = false; + btn.textContent = 'Load'; + } }); }); } catch (err) { diff --git a/ainode/web/templates/index.html b/ainode/web/templates/index.html index bf5d39a..3bb5524 100644 --- a/ainode/web/templates/index.html +++ b/ainode/web/templates/index.html @@ -94,13 +94,6 @@

Models

- -