lemonade-sdk 8.1.4__py3-none-any.whl → 8.2.2__py3-none-any.whl
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.
Potentially problematic release.
This version of lemonade-sdk might be problematic. Click here for more details.
- lemonade/cache.py +6 -1
- lemonade/cli.py +47 -5
- lemonade/common/inference_engines.py +13 -4
- lemonade/common/status.py +4 -4
- lemonade/common/system_info.py +544 -1
- lemonade/profilers/agt_power.py +437 -0
- lemonade/profilers/hwinfo_power.py +429 -0
- lemonade/tools/accuracy.py +143 -48
- lemonade/tools/adapter.py +6 -1
- lemonade/tools/bench.py +26 -8
- lemonade/tools/flm/__init__.py +1 -0
- lemonade/tools/flm/utils.py +303 -0
- lemonade/tools/huggingface/bench.py +6 -1
- lemonade/tools/llamacpp/bench.py +146 -27
- lemonade/tools/llamacpp/load.py +30 -2
- lemonade/tools/llamacpp/utils.py +393 -33
- lemonade/tools/oga/bench.py +5 -26
- lemonade/tools/oga/load.py +60 -121
- lemonade/tools/oga/migration.py +403 -0
- lemonade/tools/report/table.py +76 -8
- lemonade/tools/server/flm.py +133 -0
- lemonade/tools/server/llamacpp.py +220 -553
- lemonade/tools/server/serve.py +684 -168
- lemonade/tools/server/static/js/chat.js +666 -342
- lemonade/tools/server/static/js/model-settings.js +24 -3
- lemonade/tools/server/static/js/models.js +597 -73
- lemonade/tools/server/static/js/shared.js +79 -14
- lemonade/tools/server/static/logs.html +191 -0
- lemonade/tools/server/static/styles.css +491 -66
- lemonade/tools/server/static/webapp.html +83 -31
- lemonade/tools/server/tray.py +158 -38
- lemonade/tools/server/utils/macos_tray.py +226 -0
- lemonade/tools/server/utils/{system_tray.py → windows_tray.py} +13 -0
- lemonade/tools/server/webapp.py +4 -1
- lemonade/tools/server/wrapped_server.py +559 -0
- lemonade/version.py +1 -1
- lemonade_install/install.py +54 -611
- {lemonade_sdk-8.1.4.dist-info → lemonade_sdk-8.2.2.dist-info}/METADATA +29 -72
- lemonade_sdk-8.2.2.dist-info/RECORD +83 -0
- lemonade_server/cli.py +145 -37
- lemonade_server/model_manager.py +521 -37
- lemonade_server/pydantic_models.py +28 -1
- lemonade_server/server_models.json +246 -92
- lemonade_server/settings.py +39 -39
- lemonade/tools/quark/__init__.py +0 -0
- lemonade/tools/quark/quark_load.py +0 -173
- lemonade/tools/quark/quark_quantize.py +0 -439
- lemonade_sdk-8.1.4.dist-info/RECORD +0 -77
- {lemonade_sdk-8.1.4.dist-info → lemonade_sdk-8.2.2.dist-info}/WHEEL +0 -0
- {lemonade_sdk-8.1.4.dist-info → lemonade_sdk-8.2.2.dist-info}/entry_points.txt +0 -0
- {lemonade_sdk-8.1.4.dist-info → lemonade_sdk-8.2.2.dist-info}/licenses/LICENSE +0 -0
- {lemonade_sdk-8.1.4.dist-info → lemonade_sdk-8.2.2.dist-info}/licenses/NOTICE.md +0 -0
- {lemonade_sdk-8.1.4.dist-info → lemonade_sdk-8.2.2.dist-info}/top_level.txt +0 -0
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// State variables for model management
|
|
4
4
|
let currentLoadedModel = null;
|
|
5
5
|
let installedModels = new Set(); // Track which models are actually installed
|
|
6
|
+
let activeOperations = new Set(); // Track models currently being downloaded or loaded
|
|
6
7
|
|
|
7
|
-
// Make installedModels accessible globally
|
|
8
|
+
// Make installedModels and activeOperations accessible globally
|
|
8
9
|
window.installedModels = installedModels;
|
|
10
|
+
window.activeOperations = activeOperations;
|
|
9
11
|
let currentCategory = 'hot';
|
|
10
12
|
let currentFilter = null;
|
|
11
13
|
|
|
@@ -42,48 +44,95 @@ async function checkModelHealth() {
|
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
// Populate the model dropdown with all installed models
|
|
48
|
+
function populateModelDropdown() {
|
|
49
|
+
const indicator = document.getElementById('model-status-indicator');
|
|
50
|
+
const select = document.getElementById('model-select');
|
|
51
|
+
select.innerHTML = '';
|
|
52
|
+
|
|
53
|
+
// Add the default option
|
|
54
|
+
const defaultOption = document.createElement('option');
|
|
55
|
+
defaultOption.value = '';
|
|
56
|
+
defaultOption.textContent = 'Click to select a model ▼';
|
|
57
|
+
select.appendChild(defaultOption);
|
|
58
|
+
|
|
59
|
+
// Add the hidden 'Server Offline' option
|
|
60
|
+
const hiddenOption = document.createElement('option');
|
|
61
|
+
hiddenOption.value = 'server-offline';
|
|
62
|
+
hiddenOption.textContent = 'Server Offline';
|
|
63
|
+
hiddenOption.hidden = true;
|
|
64
|
+
select.appendChild(hiddenOption);
|
|
65
|
+
|
|
66
|
+
// Get all installed models from the global set
|
|
67
|
+
const sortedModels = Array.from(installedModels).sort();
|
|
68
|
+
|
|
69
|
+
// Add options for each installed model
|
|
70
|
+
sortedModels.forEach(modelId => {
|
|
71
|
+
const option = document.createElement('option');
|
|
72
|
+
option.value = modelId;
|
|
73
|
+
option.textContent = modelId;
|
|
74
|
+
select.appendChild(option);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
45
78
|
// Update model status indicator
|
|
46
79
|
async function updateModelStatusIndicator() {
|
|
47
80
|
const indicator = document.getElementById('model-status-indicator');
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
81
|
+
const select = document.getElementById('model-select');
|
|
82
|
+
const buttonIcons = document.querySelectorAll('button');
|
|
83
|
+
|
|
51
84
|
// Fetch both health and installed models
|
|
52
85
|
const [health] = await Promise.all([
|
|
53
86
|
checkModelHealth(),
|
|
54
87
|
fetchInstalledModels()
|
|
55
88
|
]);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
89
|
+
|
|
90
|
+
// Populate the dropdown with the newly fetched installed models
|
|
91
|
+
populateModelDropdown();
|
|
92
|
+
|
|
62
93
|
// Refresh model management UI if we're on the models tab
|
|
63
94
|
const modelsTab = document.getElementById('content-models');
|
|
64
95
|
if (modelsTab && modelsTab.classList.contains('active')) {
|
|
65
96
|
// Use the display-only version to avoid re-fetching data we just fetched
|
|
66
97
|
refreshModelMgmtUIDisplay();
|
|
98
|
+
|
|
99
|
+
// Also refresh the model browser to show updated button states
|
|
100
|
+
if (currentCategory === 'hot') displayHotModels();
|
|
101
|
+
else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
|
|
102
|
+
else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
|
|
67
103
|
}
|
|
68
|
-
|
|
69
|
-
// Remove any click handlers
|
|
70
|
-
indicator.onclick = null;
|
|
71
|
-
|
|
104
|
+
|
|
72
105
|
if (health && health.model_loaded) {
|
|
73
106
|
// Model is loaded - show model name with online status
|
|
107
|
+
indicator.classList.remove('online', 'offline', 'loading');
|
|
74
108
|
currentLoadedModel = health.model_loaded;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
109
|
+
indicator.classList.add('loaded');
|
|
110
|
+
select.value = currentLoadedModel;
|
|
111
|
+
select.disabled = false;
|
|
112
|
+
buttonIcons.forEach(btn => btn.disabled = false);
|
|
113
|
+
} else if (health !== null) {
|
|
78
114
|
// Server is online but no model loaded
|
|
115
|
+
indicator.classList.remove('loaded', 'offline', 'loading');
|
|
79
116
|
currentLoadedModel = null;
|
|
80
|
-
|
|
81
|
-
|
|
117
|
+
indicator.classList.add('online');
|
|
118
|
+
select.value = ''; // Set to the "Click to select a model ▼" option
|
|
119
|
+
select.disabled = false;
|
|
120
|
+
buttonIcons.forEach(btn => btn.disabled = false);
|
|
82
121
|
} else {
|
|
83
122
|
// Server is offline
|
|
123
|
+
indicator.classList.remove('loaded', 'online', 'loading');
|
|
84
124
|
currentLoadedModel = null;
|
|
85
|
-
|
|
86
|
-
|
|
125
|
+
// Add the hidden 'Server Offline' option
|
|
126
|
+
const hiddenOption = document.createElement('option');
|
|
127
|
+
hiddenOption.value = 'server-offline';
|
|
128
|
+
hiddenOption.textContent = 'Server Offline';
|
|
129
|
+
hiddenOption.hidden = true;
|
|
130
|
+
select.appendChild(hiddenOption);
|
|
131
|
+
indicator.classList.add('offline');
|
|
132
|
+
select.value = 'server-offline';
|
|
133
|
+
select.disabled = true;
|
|
134
|
+
buttonIcons.forEach(btn => btn.disabled = true);
|
|
135
|
+
return;
|
|
87
136
|
}
|
|
88
137
|
}
|
|
89
138
|
|
|
@@ -92,9 +141,18 @@ async function unloadModel() {
|
|
|
92
141
|
if (!currentLoadedModel) return;
|
|
93
142
|
|
|
94
143
|
try {
|
|
144
|
+
// Set loading state
|
|
145
|
+
const indicator = document.getElementById('model-status-indicator');
|
|
146
|
+
const select = document.getElementById('model-select');
|
|
147
|
+
indicator.classList.remove('loaded', 'online', 'offline');
|
|
148
|
+
indicator.classList.add('loading');
|
|
149
|
+
select.disabled = true;
|
|
150
|
+
select.value = currentLoadedModel; // Keep the selected model visible during unload
|
|
151
|
+
|
|
95
152
|
await httpRequest(getServerBaseUrl() + '/api/v1/unload', {
|
|
96
153
|
method: 'POST'
|
|
97
154
|
});
|
|
155
|
+
|
|
98
156
|
await updateModelStatusIndicator();
|
|
99
157
|
|
|
100
158
|
// Refresh model list to show updated button states
|
|
@@ -104,11 +162,35 @@ async function unloadModel() {
|
|
|
104
162
|
} catch (error) {
|
|
105
163
|
console.error('Error unloading model:', error);
|
|
106
164
|
showErrorBanner('Failed to unload model: ' + error.message);
|
|
165
|
+
await updateModelStatusIndicator(); // Revert state on error
|
|
107
166
|
}
|
|
108
167
|
}
|
|
109
168
|
|
|
110
169
|
// === Model Browser Management ===
|
|
111
170
|
|
|
171
|
+
// Update visibility of categories/subcategories based on available models
|
|
172
|
+
function updateCategoryVisibility() {
|
|
173
|
+
const allModels = window.SERVER_MODELS || {};
|
|
174
|
+
|
|
175
|
+
// Count models for each recipe
|
|
176
|
+
const recipeCounts = {};
|
|
177
|
+
const recipes = ['llamacpp', 'oga-hybrid', 'oga-npu', 'oga-cpu', 'flm'];
|
|
178
|
+
recipes.forEach(recipe => {
|
|
179
|
+
recipeCounts[recipe] = 0;
|
|
180
|
+
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
181
|
+
if (modelData.recipe === recipe && (modelData.suggested || installedModels.has(modelId))) {
|
|
182
|
+
recipeCounts[recipe]++;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Show/hide recipe subcategory
|
|
187
|
+
const subcategory = document.querySelector(`[data-recipe="${recipe}"]`);
|
|
188
|
+
if (subcategory) {
|
|
189
|
+
subcategory.style.display = recipeCounts[recipe] > 0 ? 'block' : 'none';
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
112
194
|
// Toggle category in model browser (only for Hot Models now)
|
|
113
195
|
function toggleCategory(categoryName) {
|
|
114
196
|
const header = document.querySelector(`[data-category="${categoryName}"] .category-header`);
|
|
@@ -226,7 +308,7 @@ function displayHotModels() {
|
|
|
226
308
|
modelList.innerHTML = '';
|
|
227
309
|
|
|
228
310
|
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
229
|
-
if (modelData.labels && modelData.labels.includes('hot')) {
|
|
311
|
+
if (modelData.labels && modelData.labels.includes('hot') && (modelData.suggested || installedModels.has(modelId))) {
|
|
230
312
|
createModelItem(modelId, modelData, modelList);
|
|
231
313
|
}
|
|
232
314
|
});
|
|
@@ -244,8 +326,23 @@ function displayModelsByRecipe(recipe) {
|
|
|
244
326
|
|
|
245
327
|
modelList.innerHTML = '';
|
|
246
328
|
|
|
329
|
+
// Add FastFlowLM notice if this is the FLM recipe
|
|
330
|
+
if (recipe === 'flm') {
|
|
331
|
+
const notice = document.createElement('div');
|
|
332
|
+
notice.className = 'flm-notice';
|
|
333
|
+
notice.innerHTML = `
|
|
334
|
+
<div class="flm-notice-content">
|
|
335
|
+
<div class="flm-notice-icon">⚠️</div>
|
|
336
|
+
<div class="flm-notice-text">
|
|
337
|
+
<strong><a href="https://github.com/FastFlowLM/FastFlowLM">FastFlowLM (FLM)</a> support in Lemonade is in Early Access.</strong> FLM is free for non-commercial use, however note that commercial licensing terms apply. Installing an FLM model will automatically launch the FLM installer, which will require you to accept the FLM license terms to continue. Contact <a href="mailto:lemonade@amd.com">lemonade@amd.com</a> for inquiries.
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
`;
|
|
341
|
+
modelList.appendChild(notice);
|
|
342
|
+
}
|
|
343
|
+
|
|
247
344
|
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
248
|
-
if (modelData.recipe === recipe) {
|
|
345
|
+
if (modelData.recipe === recipe && (modelData.suggested || installedModels.has(modelId))) {
|
|
249
346
|
createModelItem(modelId, modelData, modelList);
|
|
250
347
|
}
|
|
251
348
|
});
|
|
@@ -269,7 +366,7 @@ function displayModelsByLabel(label) {
|
|
|
269
366
|
if (modelId.startsWith('user.')) {
|
|
270
367
|
createModelItem(modelId, modelData, modelList);
|
|
271
368
|
}
|
|
272
|
-
} else if (modelData.labels && modelData.labels.includes(label)) {
|
|
369
|
+
} else if (modelData.labels && modelData.labels.includes(label) && (modelData.suggested || installedModels.has(modelId))) {
|
|
273
370
|
createModelItem(modelId, modelData, modelList);
|
|
274
371
|
}
|
|
275
372
|
});
|
|
@@ -321,10 +418,22 @@ function createModelItem(modelId, modelData, container) {
|
|
|
321
418
|
actions.appendChild(unloadBtn);
|
|
322
419
|
} else {
|
|
323
420
|
const loadBtn = document.createElement('button');
|
|
421
|
+
const modelSelect = document.getElementById('model-select');
|
|
324
422
|
loadBtn.className = 'model-item-btn load';
|
|
325
423
|
loadBtn.textContent = '🚀';
|
|
326
424
|
loadBtn.title = 'Load';
|
|
327
|
-
loadBtn.onclick = () =>
|
|
425
|
+
loadBtn.onclick = () => {
|
|
426
|
+
loadModelStandardized(modelId, {
|
|
427
|
+
loadButton: loadBtn,
|
|
428
|
+
onSuccess: (loadedModelId) => {
|
|
429
|
+
console.log(`Model ${loadedModelId} loaded successfully`);
|
|
430
|
+
},
|
|
431
|
+
onError: (error, failedModelId) => {
|
|
432
|
+
console.error(`Failed to load model ${failedModelId}:`, error);
|
|
433
|
+
showErrorBanner('Failed to load model: ' + error.message);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
};
|
|
328
437
|
actions.appendChild(loadBtn);
|
|
329
438
|
}
|
|
330
439
|
|
|
@@ -349,7 +458,7 @@ async function installModel(modelId) {
|
|
|
349
458
|
|
|
350
459
|
modelItems.forEach(item => {
|
|
351
460
|
const nameElement = item.querySelector('.model-item-name .model-labels-container span');
|
|
352
|
-
if (nameElement && nameElement.
|
|
461
|
+
if (nameElement && nameElement.getAttribute('data-model-id') === modelId) {
|
|
353
462
|
installBtn = item.querySelector('.model-item-btn.install');
|
|
354
463
|
}
|
|
355
464
|
});
|
|
@@ -359,6 +468,9 @@ async function installModel(modelId) {
|
|
|
359
468
|
installBtn.textContent = '⏳';
|
|
360
469
|
}
|
|
361
470
|
|
|
471
|
+
// Track this download as active
|
|
472
|
+
activeOperations.add(modelId);
|
|
473
|
+
|
|
362
474
|
try {
|
|
363
475
|
const modelData = window.SERVER_MODELS[modelId];
|
|
364
476
|
await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
|
|
@@ -367,6 +479,9 @@ async function installModel(modelId) {
|
|
|
367
479
|
body: JSON.stringify({ model_name: modelId, ...modelData })
|
|
368
480
|
});
|
|
369
481
|
|
|
482
|
+
// Download complete - remove from active operations
|
|
483
|
+
activeOperations.delete(modelId);
|
|
484
|
+
|
|
370
485
|
// Refresh installed models and model status
|
|
371
486
|
await fetchInstalledModels();
|
|
372
487
|
await updateModelStatusIndicator();
|
|
@@ -384,47 +499,38 @@ async function installModel(modelId) {
|
|
|
384
499
|
console.error('Error installing model:', error);
|
|
385
500
|
showErrorBanner('Failed to install model: ' + error.message);
|
|
386
501
|
|
|
502
|
+
// Remove from active operations on error too
|
|
503
|
+
activeOperations.delete(modelId);
|
|
504
|
+
|
|
387
505
|
// Reset button state on error
|
|
388
506
|
if (installBtn) {
|
|
389
507
|
installBtn.disabled = false;
|
|
390
|
-
installBtn.textContent = '
|
|
508
|
+
installBtn.textContent = '📥';
|
|
391
509
|
}
|
|
392
510
|
}
|
|
393
511
|
}
|
|
394
512
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
513
|
+
|
|
514
|
+
// Delete model
|
|
515
|
+
async function deleteModel(modelId) {
|
|
516
|
+
if (!confirm(`Are you sure you want to delete the model "${modelId}"?`)) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Find the delete button and show loading state
|
|
398
521
|
const modelItems = document.querySelectorAll('.model-item');
|
|
399
|
-
let
|
|
522
|
+
let deleteBtn = null;
|
|
400
523
|
|
|
401
524
|
modelItems.forEach(item => {
|
|
402
525
|
const nameElement = item.querySelector('.model-item-name .model-labels-container span');
|
|
403
|
-
if (nameElement && nameElement.
|
|
404
|
-
|
|
526
|
+
if (nameElement && nameElement.getAttribute('data-model-id') === modelId) {
|
|
527
|
+
deleteBtn = item.querySelector('.model-item-btn.delete');
|
|
405
528
|
}
|
|
406
529
|
});
|
|
407
530
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
onSuccess: (loadedModelId) => {
|
|
412
|
-
console.log(`Model ${loadedModelId} loaded successfully`);
|
|
413
|
-
// Refresh model list after successful load
|
|
414
|
-
if (currentCategory === 'hot') displayHotModels();
|
|
415
|
-
else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
|
|
416
|
-
else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
|
|
417
|
-
},
|
|
418
|
-
onError: (error, failedModelId) => {
|
|
419
|
-
console.error(`Failed to load model ${failedModelId}:`, error);
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Delete model
|
|
425
|
-
async function deleteModel(modelId) {
|
|
426
|
-
if (!confirm(`Are you sure you want to delete the model "${modelId}"?`)) {
|
|
427
|
-
return;
|
|
531
|
+
if (deleteBtn) {
|
|
532
|
+
deleteBtn.disabled = true;
|
|
533
|
+
deleteBtn.textContent = '⏳';
|
|
428
534
|
}
|
|
429
535
|
|
|
430
536
|
try {
|
|
@@ -433,7 +539,11 @@ async function deleteModel(modelId) {
|
|
|
433
539
|
headers: { 'Content-Type': 'application/json' },
|
|
434
540
|
body: JSON.stringify({ model_name: modelId })
|
|
435
541
|
});
|
|
436
|
-
|
|
542
|
+
installedModels.delete(modelId);
|
|
543
|
+
// Remove custom models from SERVER_MODELS to prevent them from reappearing without having to do a manual refresh
|
|
544
|
+
if (modelId.startsWith('user.')) {
|
|
545
|
+
delete window.SERVER_MODELS[modelId];
|
|
546
|
+
}
|
|
437
547
|
// Refresh installed models and model status
|
|
438
548
|
await fetchInstalledModels();
|
|
439
549
|
await updateModelStatusIndicator();
|
|
@@ -450,6 +560,12 @@ async function deleteModel(modelId) {
|
|
|
450
560
|
} catch (error) {
|
|
451
561
|
console.error('Error deleting model:', error);
|
|
452
562
|
showErrorBanner('Failed to delete model: ' + error.message);
|
|
563
|
+
|
|
564
|
+
// Reset button state on error
|
|
565
|
+
if (deleteBtn) {
|
|
566
|
+
deleteBtn.disabled = false;
|
|
567
|
+
deleteBtn.textContent = '🗑️';
|
|
568
|
+
}
|
|
453
569
|
}
|
|
454
570
|
}
|
|
455
571
|
|
|
@@ -457,16 +573,25 @@ async function deleteModel(modelId) {
|
|
|
457
573
|
|
|
458
574
|
// Create model name with labels
|
|
459
575
|
function createModelNameWithLabels(modelId, serverModels) {
|
|
576
|
+
const modelData = serverModels[modelId];
|
|
460
577
|
const container = document.createElement('div');
|
|
461
578
|
container.className = 'model-labels-container';
|
|
462
579
|
|
|
463
580
|
// Model name
|
|
464
581
|
const nameSpan = document.createElement('span');
|
|
465
|
-
|
|
582
|
+
|
|
583
|
+
// Store the original modelId as a data attribute for button finding
|
|
584
|
+
nameSpan.setAttribute('data-model-id', modelId);
|
|
585
|
+
|
|
586
|
+
// Append size if available
|
|
587
|
+
let displayName = modelId;
|
|
588
|
+
if (modelData && typeof modelData.size === 'number') {
|
|
589
|
+
displayName += ` (${modelData.size} GB)`;
|
|
590
|
+
}
|
|
591
|
+
nameSpan.textContent = displayName;
|
|
466
592
|
container.appendChild(nameSpan);
|
|
467
593
|
|
|
468
594
|
// Labels
|
|
469
|
-
const modelData = serverModels[modelId];
|
|
470
595
|
if (modelData && modelData.labels && Array.isArray(modelData.labels)) {
|
|
471
596
|
modelData.labels.forEach(label => {
|
|
472
597
|
const labelLower = label.toLowerCase();
|
|
@@ -488,6 +613,8 @@ function createModelNameWithLabels(modelId, serverModels) {
|
|
|
488
613
|
labelClass = 'reranking';
|
|
489
614
|
} else if (labelLower === 'coding') {
|
|
490
615
|
labelClass = 'coding';
|
|
616
|
+
} else if (labelLower === 'tool-calling') {
|
|
617
|
+
labelClass = 'tool-calling';
|
|
491
618
|
}
|
|
492
619
|
labelSpan.className = `model-label ${labelClass}`;
|
|
493
620
|
labelSpan.textContent = label;
|
|
@@ -511,8 +638,8 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
511
638
|
// Initial fetch of model data - this will populate installedModels
|
|
512
639
|
await updateModelStatusIndicator();
|
|
513
640
|
|
|
514
|
-
//
|
|
515
|
-
|
|
641
|
+
// Update category visibility on initial load
|
|
642
|
+
updateCategoryVisibility();
|
|
516
643
|
|
|
517
644
|
// Initialize model browser with hot models
|
|
518
645
|
displayHotModels();
|
|
@@ -528,6 +655,46 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
528
655
|
|
|
529
656
|
// Set up register model form
|
|
530
657
|
setupRegisterModelForm();
|
|
658
|
+
setupFolderSelection();
|
|
659
|
+
|
|
660
|
+
// Set up smart periodic refresh to detect external model changes
|
|
661
|
+
// Poll every 15 seconds (much less aggressive than 1 second)
|
|
662
|
+
// Only poll when page is visible to save resources
|
|
663
|
+
let pollInterval = null;
|
|
664
|
+
|
|
665
|
+
function startPolling() {
|
|
666
|
+
if (!pollInterval) {
|
|
667
|
+
pollInterval = setInterval(async () => {
|
|
668
|
+
// Only update if page is visible AND no active operations
|
|
669
|
+
// Skip polling during downloads/loads to prevent false positives
|
|
670
|
+
if (document.visibilityState === 'visible' && activeOperations.size === 0) {
|
|
671
|
+
await updateModelStatusIndicator();
|
|
672
|
+
}
|
|
673
|
+
}, 15000); // Check every 15 seconds
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function stopPolling() {
|
|
678
|
+
if (pollInterval) {
|
|
679
|
+
clearInterval(pollInterval);
|
|
680
|
+
pollInterval = null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Start polling when page is visible, stop when hidden
|
|
685
|
+
document.addEventListener('visibilitychange', () => {
|
|
686
|
+
if (document.visibilityState === 'visible') {
|
|
687
|
+
// Page became visible - update immediately and resume polling
|
|
688
|
+
updateModelStatusIndicator();
|
|
689
|
+
startPolling();
|
|
690
|
+
} else {
|
|
691
|
+
// Page hidden - stop polling to save resources
|
|
692
|
+
stopPolling();
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// Start polling initially
|
|
697
|
+
startPolling();
|
|
531
698
|
});
|
|
532
699
|
|
|
533
700
|
// Toggle Add Model form
|
|
@@ -566,14 +733,22 @@ function renderModelTable(tbody, models, allModels, emptyMessage) {
|
|
|
566
733
|
btn.title = 'Install model';
|
|
567
734
|
btn.onclick = async function() {
|
|
568
735
|
btn.disabled = true;
|
|
569
|
-
btn.textContent = '
|
|
736
|
+
btn.textContent = '⏳';
|
|
570
737
|
btn.classList.add('installing-btn');
|
|
738
|
+
|
|
739
|
+
// Track this download as active
|
|
740
|
+
activeOperations.add(mid);
|
|
741
|
+
|
|
571
742
|
try {
|
|
572
743
|
await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
|
|
573
744
|
method: 'POST',
|
|
574
745
|
headers: { 'Content-Type': 'application/json' },
|
|
575
746
|
body: JSON.stringify({ model_name: mid })
|
|
576
747
|
});
|
|
748
|
+
|
|
749
|
+
// Download complete - remove from active operations
|
|
750
|
+
activeOperations.delete(mid);
|
|
751
|
+
|
|
577
752
|
await refreshModelMgmtUI();
|
|
578
753
|
// Update chat dropdown too if loadModels function exists
|
|
579
754
|
if (typeof loadModels === 'function') {
|
|
@@ -581,7 +756,11 @@ function renderModelTable(tbody, models, allModels, emptyMessage) {
|
|
|
581
756
|
}
|
|
582
757
|
} catch (e) {
|
|
583
758
|
btn.textContent = 'Error';
|
|
759
|
+
btn.disabled = false;
|
|
584
760
|
showErrorBanner(`Failed to install model: ${e.message}`);
|
|
761
|
+
|
|
762
|
+
// Remove from active operations on error too
|
|
763
|
+
activeOperations.delete(mid);
|
|
585
764
|
}
|
|
586
765
|
};
|
|
587
766
|
tdBtn.appendChild(btn);
|
|
@@ -657,7 +836,7 @@ async function refreshModelMgmtUI() {
|
|
|
657
836
|
return;
|
|
658
837
|
}
|
|
659
838
|
btn.disabled = true;
|
|
660
|
-
btn.textContent = '
|
|
839
|
+
btn.textContent = '⏳';
|
|
661
840
|
btn.style.backgroundColor = '#888';
|
|
662
841
|
try {
|
|
663
842
|
await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
|
|
@@ -673,11 +852,11 @@ async function refreshModelMgmtUI() {
|
|
|
673
852
|
} catch (e) {
|
|
674
853
|
btn.textContent = 'Error';
|
|
675
854
|
btn.disabled = false;
|
|
855
|
+
btn.style.backgroundColor = '';
|
|
676
856
|
showErrorBanner(`Failed to delete model: ${e.message}`);
|
|
677
857
|
}
|
|
678
858
|
};
|
|
679
859
|
tdBtn.appendChild(btn);
|
|
680
|
-
|
|
681
860
|
tr.appendChild(tdName);
|
|
682
861
|
tr.appendChild(tdBtn);
|
|
683
862
|
installedTbody.appendChild(tr);
|
|
@@ -699,6 +878,11 @@ async function refreshModelMgmtUI() {
|
|
|
699
878
|
if (window.initializeModelDropdown) {
|
|
700
879
|
window.initializeModelDropdown();
|
|
701
880
|
}
|
|
881
|
+
|
|
882
|
+
// Update system message when installed models change
|
|
883
|
+
if (window.displaySystemMessage) {
|
|
884
|
+
window.displaySystemMessage();
|
|
885
|
+
}
|
|
702
886
|
}
|
|
703
887
|
|
|
704
888
|
// Make refreshModelMgmtUI globally accessible
|
|
@@ -755,6 +939,10 @@ function refreshModelMgmtUIDisplay() {
|
|
|
755
939
|
btn.title = 'Remove this model';
|
|
756
940
|
btn.onclick = async function() {
|
|
757
941
|
if (confirm(`Are you sure you want to remove the model "${mid}"?`)) {
|
|
942
|
+
btn.disabled = true;
|
|
943
|
+
btn.textContent = '⏳';
|
|
944
|
+
const originalBgColor = btn.style.backgroundColor;
|
|
945
|
+
btn.style.backgroundColor = '#888';
|
|
758
946
|
try {
|
|
759
947
|
await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
|
|
760
948
|
method: 'POST',
|
|
@@ -765,6 +953,10 @@ function refreshModelMgmtUIDisplay() {
|
|
|
765
953
|
} catch (error) {
|
|
766
954
|
console.error('Error removing model:', error);
|
|
767
955
|
showErrorBanner('Failed to remove model: ' + error.message);
|
|
956
|
+
// Reset button state on error
|
|
957
|
+
btn.disabled = false;
|
|
958
|
+
btn.textContent = '−';
|
|
959
|
+
btn.style.backgroundColor = originalBgColor;
|
|
768
960
|
}
|
|
769
961
|
}
|
|
770
962
|
};
|
|
@@ -807,34 +999,91 @@ function setupRegisterModelForm() {
|
|
|
807
999
|
if (!name.startsWith('user.')) {
|
|
808
1000
|
name = 'user.' + name;
|
|
809
1001
|
}
|
|
810
|
-
|
|
1002
|
+
|
|
1003
|
+
// Check if model name already exists
|
|
1004
|
+
const allModels = window.SERVER_MODELS || {};
|
|
1005
|
+
if (allModels[name] || installedModels.has(name)) {
|
|
1006
|
+
showErrorBanner('Model name already exists. Please enter a different name.');
|
|
1007
|
+
registerStatus.textContent = 'Model name already exists';
|
|
1008
|
+
registerStatus.style.color = '#b10819ff';
|
|
1009
|
+
registerStatus.className = 'register-status error';
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
811
1013
|
const checkpoint = document.getElementById('register-checkpoint').value.trim();
|
|
812
1014
|
const recipe = document.getElementById('register-recipe').value;
|
|
813
1015
|
const reasoning = document.getElementById('register-reasoning').checked;
|
|
1016
|
+
const vision = document.getElementById('register-vision').checked;
|
|
814
1017
|
const mmproj = document.getElementById('register-mmproj').value.trim();
|
|
815
1018
|
|
|
816
1019
|
if (!name || !recipe) {
|
|
817
1020
|
return;
|
|
818
1021
|
}
|
|
819
1022
|
|
|
820
|
-
const payload = { model_name: name, recipe, reasoning };
|
|
821
|
-
if (checkpoint) payload.checkpoint = checkpoint;
|
|
822
|
-
if (mmproj) payload.mmproj = mmproj;
|
|
823
|
-
|
|
824
1023
|
const btn = document.getElementById('register-submit');
|
|
825
1024
|
btn.disabled = true;
|
|
826
1025
|
btn.textContent = 'Installing...';
|
|
827
1026
|
|
|
828
1027
|
try {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1028
|
+
if (isLocalModel && selectedModelFiles) {
|
|
1029
|
+
if (recipe === 'llamacpp' && !Array.from(selectedModelFiles).some(file => file.name.toLowerCase().endsWith('.gguf'))) {
|
|
1030
|
+
throw new Error('No .gguf files found in the selected folder for llamacpp');
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const formData = new FormData();
|
|
1034
|
+
formData.append('model_name', name);
|
|
1035
|
+
formData.append('checkpoint', checkpoint);
|
|
1036
|
+
formData.append('recipe', recipe);
|
|
1037
|
+
formData.append('reasoning', reasoning);
|
|
1038
|
+
formData.append('vision', vision);
|
|
1039
|
+
if (mmproj) formData.append('mmproj', mmproj);
|
|
1040
|
+
Array.from(selectedModelFiles).forEach(file => {
|
|
1041
|
+
formData.append('model_files', file, file.webkitRelativePath);
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/add-local-model', {
|
|
1045
|
+
method: 'POST',
|
|
1046
|
+
body: formData
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
else {
|
|
1050
|
+
if (!checkpoint) {
|
|
1051
|
+
throw new Error('Checkpoint is required for remote models');
|
|
1052
|
+
}
|
|
1053
|
+
const payload = { model_name: name, recipe, reasoning, vision };
|
|
1054
|
+
if (checkpoint) payload.checkpoint = checkpoint;
|
|
1055
|
+
if (mmproj) payload.mmproj = mmproj;
|
|
1056
|
+
|
|
1057
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
|
|
1058
|
+
method: 'POST',
|
|
1059
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1060
|
+
body: JSON.stringify(payload)
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
834
1064
|
registerStatus.textContent = 'Model installed!';
|
|
835
|
-
registerStatus.style.color = '#
|
|
1065
|
+
registerStatus.style.color = '#0eaf51ff';
|
|
836
1066
|
registerStatus.className = 'register-status success';
|
|
1067
|
+
|
|
1068
|
+
// Add custom model to SERVER_MODELS so it appears in the UI without having to do a manual refresh
|
|
1069
|
+
if (name.startsWith('user.')) {
|
|
1070
|
+
const labels = ['custom'];
|
|
1071
|
+
if (vision) labels.push('vision');
|
|
1072
|
+
if (reasoning) labels.push('reasoning');
|
|
1073
|
+
|
|
1074
|
+
window.SERVER_MODELS[name] = {
|
|
1075
|
+
recipe: recipe,
|
|
1076
|
+
labels: labels
|
|
1077
|
+
};
|
|
1078
|
+
if (checkpoint) window.SERVER_MODELS[name].checkpoint = checkpoint;
|
|
1079
|
+
if (mmproj) window.SERVER_MODELS[name].mmproj = mmproj;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
837
1082
|
registerForm.reset();
|
|
1083
|
+
isLocalModel = false;
|
|
1084
|
+
selectedModelFiles = null;
|
|
1085
|
+
document.getElementById('folder-input').value = '';
|
|
1086
|
+
|
|
838
1087
|
await refreshModelMgmtUI();
|
|
839
1088
|
// Update chat dropdown too if loadModels function exists
|
|
840
1089
|
if (typeof loadModels === 'function') {
|
|
@@ -853,6 +1102,278 @@ function setupRegisterModelForm() {
|
|
|
853
1102
|
};
|
|
854
1103
|
}
|
|
855
1104
|
}
|
|
1105
|
+
let isLocalModel = false;
|
|
1106
|
+
let selectedModelFiles = null;
|
|
1107
|
+
// Helper function to find mmproj file in selected folder
|
|
1108
|
+
function findMmprojFile(files) {
|
|
1109
|
+
for (let i = 0; i < files.length; i++) {
|
|
1110
|
+
const file = files[i];
|
|
1111
|
+
const fileName = file.name.toLowerCase();
|
|
1112
|
+
const relativePath = file.webkitRelativePath;
|
|
1113
|
+
|
|
1114
|
+
// Check if file contains 'mmproj' and has .gguf extension
|
|
1115
|
+
if (fileName.includes('mmproj') && fileName.endsWith('.gguf')) {
|
|
1116
|
+
// Return just the filename (last part of the path)
|
|
1117
|
+
return relativePath.split('/').pop();
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Helper function to find all non-mmproj GGUF files in selected folder
|
|
1124
|
+
function findGgufFiles(files) {
|
|
1125
|
+
const ggufFiles = [];
|
|
1126
|
+
for (let i = 0; i < files.length; i++) {
|
|
1127
|
+
const file = files[i];
|
|
1128
|
+
const fileName = file.name.toLowerCase();
|
|
1129
|
+
const relativePath = file.webkitRelativePath;
|
|
1130
|
+
|
|
1131
|
+
// Check if file has .gguf extension but is NOT an mmproj file
|
|
1132
|
+
if (fileName.endsWith('.gguf') && !fileName.includes('mmproj')) {
|
|
1133
|
+
// Store just the filename (last part of the path)
|
|
1134
|
+
ggufFiles.push(relativePath.split('/').pop());
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return ggufFiles;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Helper function to check GGUF files and show appropriate banners
|
|
1141
|
+
function checkGgufFilesAndShowBanner(files) {
|
|
1142
|
+
const recipeSelect = document.getElementById('register-recipe');
|
|
1143
|
+
|
|
1144
|
+
// Only check if llamacpp is selected
|
|
1145
|
+
if (!recipeSelect || recipeSelect.value !== 'llamacpp') {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const mmprojFile = findMmprojFile(files);
|
|
1150
|
+
const ggufFiles = findGgufFiles(files);
|
|
1151
|
+
|
|
1152
|
+
// Hide any existing banners first
|
|
1153
|
+
hideErrorBanner();
|
|
1154
|
+
|
|
1155
|
+
if (ggufFiles.length > 1) {
|
|
1156
|
+
// Multiple GGUF files detected
|
|
1157
|
+
const folderPath = files[0].webkitRelativePath.split('/')[0];
|
|
1158
|
+
let bannerMsg = `More than one variant detected. Please clarify them at the end of the checkpoint name like:\n<folder_name>:<variant>\nExample: ${folderPath}:${ggufFiles[0]}`;
|
|
1159
|
+
|
|
1160
|
+
if (mmprojFile) {
|
|
1161
|
+
bannerMsg += `\n\nDon't forget to enter the mmproj file name and check the 'vision' checkbox if it is a vision model.`;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
showBanner(bannerMsg, 'warning');
|
|
1165
|
+
} else if (mmprojFile) {
|
|
1166
|
+
// MMproj detected
|
|
1167
|
+
showBanner("MMproj detected and populated. Please validate the file name and check the 'vision' checkbox if it is a vision model.", 'success');
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
// Helper function to auto-fill mmproj field if llamacpp is selected
|
|
1171
|
+
function autoFillMmproj() {
|
|
1172
|
+
const recipeSelect = document.getElementById('register-recipe');
|
|
1173
|
+
const mmprojInput = document.getElementById('register-mmproj');
|
|
1174
|
+
|
|
1175
|
+
if (recipeSelect && mmprojInput && isLocalModel && selectedModelFiles) {
|
|
1176
|
+
const selectedRecipe = recipeSelect.value;
|
|
1177
|
+
|
|
1178
|
+
if (selectedRecipe === 'llamacpp') {
|
|
1179
|
+
const mmprojFile = findMmprojFile(selectedModelFiles);
|
|
1180
|
+
if (mmprojFile) {
|
|
1181
|
+
mmprojInput.value = mmprojFile;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Check GGUF files and show appropriate banner
|
|
1185
|
+
checkGgufFilesAndShowBanner(selectedModelFiles);
|
|
1186
|
+
} else {
|
|
1187
|
+
// Hide banners if not llamacpp
|
|
1188
|
+
hideErrorBanner();
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
function setupFolderSelection() {
|
|
1193
|
+
const selectFolderBtn = document.getElementById('select-folder-btn');
|
|
1194
|
+
const folderInput = document.getElementById('folder-input');
|
|
1195
|
+
const checkpointInput = document.getElementById('register-checkpoint');
|
|
1196
|
+
const recipeSelect = document.getElementById('register-recipe');
|
|
1197
|
+
|
|
1198
|
+
if (selectFolderBtn && folderInput && checkpointInput) {
|
|
1199
|
+
selectFolderBtn.addEventListener('click', () => {
|
|
1200
|
+
folderInput.click();
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
folderInput.addEventListener('change', (event) => {
|
|
1204
|
+
const files = event.target.files;
|
|
1205
|
+
if (files.length > 0) {
|
|
1206
|
+
const firstFile = files[0];
|
|
1207
|
+
const folderPath = firstFile.webkitRelativePath.split('/')[0];
|
|
1208
|
+
checkpointInput.value = folderPath;
|
|
1209
|
+
isLocalModel = true;
|
|
1210
|
+
selectedModelFiles = files;
|
|
1211
|
+
|
|
1212
|
+
// Auto-fill mmproj if llamacpp is already selected
|
|
1213
|
+
autoFillMmproj();
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
isLocalModel = false;
|
|
1217
|
+
selectedModelFiles = null;
|
|
1218
|
+
checkpointInput.value = '';
|
|
1219
|
+
hideErrorBanner();
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
// Add listener to recipe dropdown to auto-fill mmproj when changed to llamacpp
|
|
1224
|
+
if (recipeSelect) {
|
|
1225
|
+
recipeSelect.addEventListener('change', () => {
|
|
1226
|
+
autoFillMmproj();
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
// === Migration/Cleanup Functions ===
|
|
1232
|
+
|
|
1233
|
+
// Store incompatible models data globally
|
|
1234
|
+
let incompatibleModelsData = null;
|
|
1235
|
+
|
|
1236
|
+
// Check for incompatible models on page load
|
|
1237
|
+
async function checkIncompatibleModels() {
|
|
1238
|
+
try {
|
|
1239
|
+
const response = await httpJson(getServerBaseUrl() + '/api/v1/migration/incompatible-models');
|
|
1240
|
+
incompatibleModelsData = response;
|
|
1241
|
+
|
|
1242
|
+
if (response.count > 0) {
|
|
1243
|
+
showMigrationBanner(response.count, response.total_size);
|
|
1244
|
+
}
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
console.error('Error checking for incompatible models:', error);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Show migration banner
|
|
1251
|
+
function showMigrationBanner(count, totalSize) {
|
|
1252
|
+
const banner = document.getElementById('migration-banner');
|
|
1253
|
+
const msg = document.getElementById('migration-banner-msg');
|
|
1254
|
+
|
|
1255
|
+
const sizeGB = (totalSize / (1024 * 1024 * 1024)).toFixed(1);
|
|
1256
|
+
msg.textContent = `Found ${count} incompatible RyzenAI model${count > 1 ? 's' : ''} (${sizeGB} GB). Clean up to free disk space.`;
|
|
1257
|
+
banner.style.display = 'flex';
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// Hide migration banner
|
|
1261
|
+
function hideMigrationBanner() {
|
|
1262
|
+
const banner = document.getElementById('migration-banner');
|
|
1263
|
+
banner.style.display = 'none';
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Show migration modal with model list
|
|
1267
|
+
function showMigrationModal() {
|
|
1268
|
+
if (!incompatibleModelsData || incompatibleModelsData.count === 0) {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const modal = document.getElementById('migration-modal');
|
|
1273
|
+
const modelList = document.getElementById('migration-model-list');
|
|
1274
|
+
const totalSize = document.getElementById('migration-total-size');
|
|
1275
|
+
|
|
1276
|
+
// Populate model list
|
|
1277
|
+
modelList.innerHTML = '';
|
|
1278
|
+
incompatibleModelsData.models.forEach(model => {
|
|
1279
|
+
const item = document.createElement('div');
|
|
1280
|
+
item.className = 'migration-model-item';
|
|
1281
|
+
|
|
1282
|
+
const nameSpan = document.createElement('span');
|
|
1283
|
+
nameSpan.className = 'migration-model-name';
|
|
1284
|
+
nameSpan.textContent = model.name;
|
|
1285
|
+
|
|
1286
|
+
const sizeSpan = document.createElement('span');
|
|
1287
|
+
sizeSpan.className = 'migration-model-size';
|
|
1288
|
+
sizeSpan.textContent = model.size_formatted;
|
|
1289
|
+
|
|
1290
|
+
item.appendChild(nameSpan);
|
|
1291
|
+
item.appendChild(sizeSpan);
|
|
1292
|
+
modelList.appendChild(item);
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
// Set total size
|
|
1296
|
+
const sizeGB = (incompatibleModelsData.total_size / (1024 * 1024 * 1024)).toFixed(1);
|
|
1297
|
+
totalSize.textContent = `${sizeGB} GB`;
|
|
1298
|
+
|
|
1299
|
+
modal.style.display = 'flex';
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Hide migration modal
|
|
1303
|
+
function hideMigrationModal() {
|
|
1304
|
+
const modal = document.getElementById('migration-modal');
|
|
1305
|
+
modal.style.display = 'none';
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Delete incompatible models
|
|
1309
|
+
async function deleteIncompatibleModels() {
|
|
1310
|
+
if (!incompatibleModelsData || incompatibleModelsData.count === 0) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const modelPaths = incompatibleModelsData.models.map(m => m.path);
|
|
1315
|
+
|
|
1316
|
+
try {
|
|
1317
|
+
// Disable buttons during deletion
|
|
1318
|
+
const deleteBtn = document.querySelector('.delete-btn');
|
|
1319
|
+
const cancelBtn = document.querySelector('.cancel-btn');
|
|
1320
|
+
deleteBtn.disabled = true;
|
|
1321
|
+
cancelBtn.disabled = true;
|
|
1322
|
+
deleteBtn.textContent = 'Deleting...';
|
|
1323
|
+
|
|
1324
|
+
const response = await httpRequest(getServerBaseUrl() + '/api/v1/migration/cleanup', {
|
|
1325
|
+
method: 'POST',
|
|
1326
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1327
|
+
body: JSON.stringify({ model_paths: modelPaths })
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
const result = await response.json();
|
|
1331
|
+
|
|
1332
|
+
// Close modal
|
|
1333
|
+
hideMigrationModal();
|
|
1334
|
+
|
|
1335
|
+
// Hide banner
|
|
1336
|
+
hideMigrationBanner();
|
|
1337
|
+
|
|
1338
|
+
// Show success message
|
|
1339
|
+
showSuccessMessage(`Successfully deleted ${result.success_count} model${result.success_count > 1 ? 's' : ''}, freed ${result.freed_size_formatted}`);
|
|
1340
|
+
|
|
1341
|
+
// Clear cached data
|
|
1342
|
+
incompatibleModelsData = null;
|
|
1343
|
+
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
console.error('Error deleting incompatible models:', error);
|
|
1346
|
+
showErrorBanner('Failed to delete models: ' + error.message);
|
|
1347
|
+
|
|
1348
|
+
// Re-enable buttons
|
|
1349
|
+
const deleteBtn = document.querySelector('.delete-btn');
|
|
1350
|
+
const cancelBtn = document.querySelector('.cancel-btn');
|
|
1351
|
+
deleteBtn.disabled = false;
|
|
1352
|
+
cancelBtn.disabled = false;
|
|
1353
|
+
deleteBtn.textContent = 'Delete All';
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Show success message (reuse error banner with green color)
|
|
1358
|
+
function showSuccessMessage(message) {
|
|
1359
|
+
const banner = document.getElementById('error-banner');
|
|
1360
|
+
const msg = document.getElementById('error-banner-msg');
|
|
1361
|
+
msg.textContent = message;
|
|
1362
|
+
banner.style.backgroundColor = '#2d7f47';
|
|
1363
|
+
banner.style.display = 'flex';
|
|
1364
|
+
|
|
1365
|
+
// Auto-hide after 5 seconds
|
|
1366
|
+
setTimeout(() => {
|
|
1367
|
+
banner.style.display = 'none';
|
|
1368
|
+
banner.style.backgroundColor = ''; // Reset to default
|
|
1369
|
+
}, 5000);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// Check for incompatible models when page loads
|
|
1373
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1374
|
+
// Run check after a short delay to let the page load
|
|
1375
|
+
setTimeout(checkIncompatibleModels, 1000);
|
|
1376
|
+
});
|
|
856
1377
|
|
|
857
1378
|
// Make functions globally available for HTML onclick handlers and other components
|
|
858
1379
|
window.toggleCategory = toggleCategory;
|
|
@@ -861,5 +1382,8 @@ window.selectLabel = selectLabel;
|
|
|
861
1382
|
window.showAddModelForm = showAddModelForm;
|
|
862
1383
|
window.unloadModel = unloadModel;
|
|
863
1384
|
window.installModel = installModel;
|
|
864
|
-
window.loadModel = loadModel;
|
|
865
1385
|
window.deleteModel = deleteModel;
|
|
1386
|
+
window.showMigrationModal = showMigrationModal;
|
|
1387
|
+
window.hideMigrationModal = hideMigrationModal;
|
|
1388
|
+
window.hideMigrationBanner = hideMigrationBanner;
|
|
1389
|
+
window.deleteIncompatibleModels = deleteIncompatibleModels;
|