lemonade-sdk 9.1.1__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.
- lemonade/__init__.py +5 -0
- lemonade/api.py +180 -0
- lemonade/cache.py +92 -0
- lemonade/cli.py +173 -0
- lemonade/common/__init__.py +0 -0
- lemonade/common/build.py +176 -0
- lemonade/common/cli_helpers.py +139 -0
- lemonade/common/exceptions.py +98 -0
- lemonade/common/filesystem.py +368 -0
- lemonade/common/inference_engines.py +408 -0
- lemonade/common/network.py +93 -0
- lemonade/common/printing.py +110 -0
- lemonade/common/status.py +471 -0
- lemonade/common/system_info.py +1411 -0
- lemonade/common/test_helpers.py +28 -0
- lemonade/profilers/__init__.py +1 -0
- lemonade/profilers/agt_power.py +437 -0
- lemonade/profilers/hwinfo_power.py +429 -0
- lemonade/profilers/memory_tracker.py +259 -0
- lemonade/profilers/profiler.py +58 -0
- lemonade/sequence.py +363 -0
- lemonade/state.py +159 -0
- lemonade/tools/__init__.py +1 -0
- lemonade/tools/accuracy.py +432 -0
- lemonade/tools/adapter.py +114 -0
- lemonade/tools/bench.py +302 -0
- lemonade/tools/flm/__init__.py +1 -0
- lemonade/tools/flm/utils.py +305 -0
- lemonade/tools/huggingface/bench.py +187 -0
- lemonade/tools/huggingface/load.py +235 -0
- lemonade/tools/huggingface/utils.py +359 -0
- lemonade/tools/humaneval.py +264 -0
- lemonade/tools/llamacpp/bench.py +255 -0
- lemonade/tools/llamacpp/load.py +222 -0
- lemonade/tools/llamacpp/utils.py +1260 -0
- lemonade/tools/management_tools.py +319 -0
- lemonade/tools/mmlu.py +319 -0
- lemonade/tools/oga/__init__.py +0 -0
- lemonade/tools/oga/bench.py +120 -0
- lemonade/tools/oga/load.py +804 -0
- lemonade/tools/oga/migration.py +403 -0
- lemonade/tools/oga/utils.py +462 -0
- lemonade/tools/perplexity.py +147 -0
- lemonade/tools/prompt.py +263 -0
- lemonade/tools/report/__init__.py +0 -0
- lemonade/tools/report/llm_report.py +203 -0
- lemonade/tools/report/table.py +899 -0
- lemonade/tools/server/__init__.py +0 -0
- lemonade/tools/server/flm.py +133 -0
- lemonade/tools/server/llamacpp.py +320 -0
- lemonade/tools/server/serve.py +2123 -0
- lemonade/tools/server/static/favicon.ico +0 -0
- lemonade/tools/server/static/index.html +279 -0
- lemonade/tools/server/static/js/chat.js +1059 -0
- lemonade/tools/server/static/js/model-settings.js +183 -0
- lemonade/tools/server/static/js/models.js +1395 -0
- lemonade/tools/server/static/js/shared.js +556 -0
- lemonade/tools/server/static/logs.html +191 -0
- lemonade/tools/server/static/styles.css +2654 -0
- lemonade/tools/server/static/webapp.html +321 -0
- lemonade/tools/server/tool_calls.py +153 -0
- lemonade/tools/server/tray.py +664 -0
- lemonade/tools/server/utils/macos_tray.py +226 -0
- lemonade/tools/server/utils/port.py +77 -0
- lemonade/tools/server/utils/thread.py +85 -0
- lemonade/tools/server/utils/windows_tray.py +408 -0
- lemonade/tools/server/webapp.py +34 -0
- lemonade/tools/server/wrapped_server.py +559 -0
- lemonade/tools/tool.py +374 -0
- lemonade/version.py +1 -0
- lemonade_install/__init__.py +1 -0
- lemonade_install/install.py +239 -0
- lemonade_sdk-9.1.1.dist-info/METADATA +276 -0
- lemonade_sdk-9.1.1.dist-info/RECORD +84 -0
- lemonade_sdk-9.1.1.dist-info/WHEEL +5 -0
- lemonade_sdk-9.1.1.dist-info/entry_points.txt +5 -0
- lemonade_sdk-9.1.1.dist-info/licenses/LICENSE +201 -0
- lemonade_sdk-9.1.1.dist-info/licenses/NOTICE.md +47 -0
- lemonade_sdk-9.1.1.dist-info/top_level.txt +3 -0
- lemonade_server/cli.py +805 -0
- lemonade_server/model_manager.py +758 -0
- lemonade_server/pydantic_models.py +159 -0
- lemonade_server/server_models.json +643 -0
- lemonade_server/settings.py +39 -0
|
@@ -0,0 +1,1395 @@
|
|
|
1
|
+
// Model Management functionality
|
|
2
|
+
|
|
3
|
+
// State variables for model management
|
|
4
|
+
let currentLoadedModel = null;
|
|
5
|
+
let installedModels = new Set(); // Track which models are actually installed
|
|
6
|
+
let activeOperations = new Set(); // Track models currently being downloaded or loaded
|
|
7
|
+
|
|
8
|
+
// Make installedModels and activeOperations accessible globally
|
|
9
|
+
window.installedModels = installedModels;
|
|
10
|
+
window.activeOperations = activeOperations;
|
|
11
|
+
let currentCategory = 'hot';
|
|
12
|
+
let currentFilter = null;
|
|
13
|
+
|
|
14
|
+
// === Model Status Management ===
|
|
15
|
+
|
|
16
|
+
// Fetch installed models from the server
|
|
17
|
+
async function fetchInstalledModels() {
|
|
18
|
+
try {
|
|
19
|
+
const response = await httpJson(getServerBaseUrl() + '/api/v1/models');
|
|
20
|
+
installedModels.clear();
|
|
21
|
+
if (response && response.data) {
|
|
22
|
+
response.data.forEach(model => {
|
|
23
|
+
installedModels.add(model.id);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('Error fetching installed models:', error);
|
|
28
|
+
// If we can't fetch, assume all are installed to maintain current functionality
|
|
29
|
+
const allModels = window.SERVER_MODELS || {};
|
|
30
|
+
Object.keys(allModels).forEach(modelId => {
|
|
31
|
+
installedModels.add(modelId);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check health endpoint to get current model status
|
|
37
|
+
async function checkModelHealth() {
|
|
38
|
+
try {
|
|
39
|
+
const response = await httpJson(getServerBaseUrl() + '/api/v1/health');
|
|
40
|
+
return response;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Error checking model health:', error);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
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
|
+
|
|
78
|
+
// Update model status indicator
|
|
79
|
+
async function updateModelStatusIndicator() {
|
|
80
|
+
const indicator = document.getElementById('model-status-indicator');
|
|
81
|
+
const select = document.getElementById('model-select');
|
|
82
|
+
const buttonIcons = document.querySelectorAll('button');
|
|
83
|
+
|
|
84
|
+
// Fetch both health and installed models
|
|
85
|
+
const [health] = await Promise.all([
|
|
86
|
+
checkModelHealth(),
|
|
87
|
+
fetchInstalledModels()
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
// Populate the dropdown with the newly fetched installed models
|
|
91
|
+
populateModelDropdown();
|
|
92
|
+
|
|
93
|
+
// Refresh model management UI if we're on the models tab
|
|
94
|
+
const modelsTab = document.getElementById('content-models');
|
|
95
|
+
if (modelsTab && modelsTab.classList.contains('active')) {
|
|
96
|
+
// Use the display-only version to avoid re-fetching data we just fetched
|
|
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);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (health && health.model_loaded) {
|
|
106
|
+
// Model is loaded - show model name with online status
|
|
107
|
+
indicator.classList.remove('online', 'offline', 'loading');
|
|
108
|
+
currentLoadedModel = health.model_loaded;
|
|
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) {
|
|
114
|
+
// Server is online but no model loaded
|
|
115
|
+
indicator.classList.remove('loaded', 'offline', 'loading');
|
|
116
|
+
currentLoadedModel = null;
|
|
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);
|
|
121
|
+
} else {
|
|
122
|
+
// Server is offline
|
|
123
|
+
indicator.classList.remove('loaded', 'online', 'loading');
|
|
124
|
+
currentLoadedModel = null;
|
|
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;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Unload current model
|
|
140
|
+
async function unloadModel() {
|
|
141
|
+
if (!currentLoadedModel) return;
|
|
142
|
+
|
|
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
|
+
|
|
152
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/unload', {
|
|
153
|
+
method: 'POST'
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await updateModelStatusIndicator();
|
|
157
|
+
|
|
158
|
+
// Refresh model list to show updated button states
|
|
159
|
+
if (currentCategory === 'hot') displayHotModels();
|
|
160
|
+
else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
|
|
161
|
+
else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('Error unloading model:', error);
|
|
164
|
+
showErrorBanner('Failed to unload model: ' + error.message);
|
|
165
|
+
await updateModelStatusIndicator(); // Revert state on error
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// === Model Browser Management ===
|
|
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
|
+
|
|
194
|
+
// Toggle category in model browser (only for Hot Models now)
|
|
195
|
+
function toggleCategory(categoryName) {
|
|
196
|
+
const header = document.querySelector(`[data-category="${categoryName}"] .category-header`);
|
|
197
|
+
const content = document.getElementById(`category-${categoryName}`);
|
|
198
|
+
|
|
199
|
+
if (categoryName === 'hot') {
|
|
200
|
+
// Check if hot models is already selected
|
|
201
|
+
const isCurrentlyActive = header.classList.contains('active');
|
|
202
|
+
|
|
203
|
+
// Clear all other active states
|
|
204
|
+
document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
|
|
205
|
+
|
|
206
|
+
if (!isCurrentlyActive) {
|
|
207
|
+
// Show hot models
|
|
208
|
+
header.classList.add('active');
|
|
209
|
+
content.classList.add('expanded');
|
|
210
|
+
currentCategory = categoryName;
|
|
211
|
+
currentFilter = null;
|
|
212
|
+
displayHotModels();
|
|
213
|
+
}
|
|
214
|
+
// If already active, keep it active (don't toggle off)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Show add model form in main area
|
|
219
|
+
function showAddModelForm() {
|
|
220
|
+
// Clear all sidebar active states
|
|
221
|
+
document.querySelectorAll('.category-header').forEach(h => h.classList.remove('active'));
|
|
222
|
+
document.querySelectorAll('.category-content').forEach(c => c.classList.remove('expanded'));
|
|
223
|
+
document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
|
|
224
|
+
|
|
225
|
+
// Highlight "Add a Model" as selected
|
|
226
|
+
const addModelHeader = document.querySelector('[data-category="add"] .category-header');
|
|
227
|
+
if (addModelHeader) {
|
|
228
|
+
addModelHeader.classList.add('active');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Hide model list and show form
|
|
232
|
+
document.getElementById('model-list').style.display = 'none';
|
|
233
|
+
document.getElementById('add-model-form-main').style.display = 'block';
|
|
234
|
+
|
|
235
|
+
// Set current state
|
|
236
|
+
currentCategory = 'add';
|
|
237
|
+
currentFilter = null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Select recipe filter
|
|
241
|
+
function selectRecipe(recipe) {
|
|
242
|
+
// Clear hot models active state
|
|
243
|
+
document.querySelectorAll('.category-header').forEach(h => h.classList.remove('active'));
|
|
244
|
+
document.querySelectorAll('.category-content').forEach(c => c.classList.remove('expanded'));
|
|
245
|
+
|
|
246
|
+
// Clear all subcategory selections
|
|
247
|
+
document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
|
|
248
|
+
|
|
249
|
+
// Set this recipe as active
|
|
250
|
+
document.querySelector(`[data-recipe="${recipe}"]`).classList.add('active');
|
|
251
|
+
|
|
252
|
+
currentCategory = 'recipes';
|
|
253
|
+
currentFilter = recipe;
|
|
254
|
+
displayModelsByRecipe(recipe);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Select label filter
|
|
258
|
+
function selectLabel(label) {
|
|
259
|
+
// Clear hot models active state
|
|
260
|
+
document.querySelectorAll('.category-header').forEach(h => h.classList.remove('active'));
|
|
261
|
+
document.querySelectorAll('.category-content').forEach(c => c.classList.remove('expanded'));
|
|
262
|
+
|
|
263
|
+
// Clear all subcategory selections
|
|
264
|
+
document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
|
|
265
|
+
|
|
266
|
+
// Set this label as active
|
|
267
|
+
document.querySelector(`[data-label="${label}"]`).classList.add('active');
|
|
268
|
+
|
|
269
|
+
currentCategory = 'labels';
|
|
270
|
+
currentFilter = label;
|
|
271
|
+
displayModelsByLabel(label);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Display suggested models (Qwen3-0.6B-GGUF as default)
|
|
275
|
+
function displaySuggestedModels() {
|
|
276
|
+
const modelList = document.getElementById('model-list');
|
|
277
|
+
const allModels = window.SERVER_MODELS || {};
|
|
278
|
+
|
|
279
|
+
modelList.innerHTML = '';
|
|
280
|
+
|
|
281
|
+
// First show Qwen3-0.6B-GGUF as the default suggested model
|
|
282
|
+
if (allModels['Qwen3-0.6B-GGUF']) {
|
|
283
|
+
createModelItem('Qwen3-0.6B-GGUF', allModels['Qwen3-0.6B-GGUF'], modelList);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Then show other suggested models (excluding the one already shown)
|
|
287
|
+
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
288
|
+
if (modelData.suggested && modelId !== 'Qwen3-0.6B-GGUF') {
|
|
289
|
+
createModelItem(modelId, modelData, modelList);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (modelList.innerHTML === '') {
|
|
294
|
+
modelList.innerHTML = '<p>No suggested models available</p>';
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Display hot models
|
|
299
|
+
function displayHotModels() {
|
|
300
|
+
const modelList = document.getElementById('model-list');
|
|
301
|
+
const addModelForm = document.getElementById('add-model-form-main');
|
|
302
|
+
const allModels = window.SERVER_MODELS || {};
|
|
303
|
+
|
|
304
|
+
// Show model list, hide form
|
|
305
|
+
modelList.style.display = 'block';
|
|
306
|
+
addModelForm.style.display = 'none';
|
|
307
|
+
|
|
308
|
+
modelList.innerHTML = '';
|
|
309
|
+
|
|
310
|
+
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
311
|
+
if (modelData.labels && modelData.labels.includes('hot') && (modelData.suggested || installedModels.has(modelId))) {
|
|
312
|
+
createModelItem(modelId, modelData, modelList);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Display models by recipe
|
|
318
|
+
function displayModelsByRecipe(recipe) {
|
|
319
|
+
const modelList = document.getElementById('model-list');
|
|
320
|
+
const addModelForm = document.getElementById('add-model-form-main');
|
|
321
|
+
const allModels = window.SERVER_MODELS || {};
|
|
322
|
+
|
|
323
|
+
// Show model list, hide form
|
|
324
|
+
modelList.style.display = 'block';
|
|
325
|
+
addModelForm.style.display = 'none';
|
|
326
|
+
|
|
327
|
+
modelList.innerHTML = '';
|
|
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
|
+
|
|
344
|
+
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
345
|
+
if (modelData.recipe === recipe && (modelData.suggested || installedModels.has(modelId))) {
|
|
346
|
+
createModelItem(modelId, modelData, modelList);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Display models by label
|
|
352
|
+
function displayModelsByLabel(label) {
|
|
353
|
+
const modelList = document.getElementById('model-list');
|
|
354
|
+
const addModelForm = document.getElementById('add-model-form-main');
|
|
355
|
+
const allModels = window.SERVER_MODELS || {};
|
|
356
|
+
|
|
357
|
+
// Show model list, hide form
|
|
358
|
+
modelList.style.display = 'block';
|
|
359
|
+
addModelForm.style.display = 'none';
|
|
360
|
+
|
|
361
|
+
modelList.innerHTML = '';
|
|
362
|
+
|
|
363
|
+
Object.entries(allModels).forEach(([modelId, modelData]) => {
|
|
364
|
+
if (label === 'custom') {
|
|
365
|
+
// Show user-added models (those starting with 'user.')
|
|
366
|
+
if (modelId.startsWith('user.')) {
|
|
367
|
+
createModelItem(modelId, modelData, modelList);
|
|
368
|
+
}
|
|
369
|
+
} else if (modelData.labels && modelData.labels.includes(label) && (modelData.suggested || installedModels.has(modelId))) {
|
|
370
|
+
createModelItem(modelId, modelData, modelList);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Create model item element
|
|
376
|
+
function createModelItem(modelId, modelData, container) {
|
|
377
|
+
const item = document.createElement('div');
|
|
378
|
+
item.className = 'model-item';
|
|
379
|
+
|
|
380
|
+
const info = document.createElement('div');
|
|
381
|
+
info.className = 'model-item-info';
|
|
382
|
+
|
|
383
|
+
const name = document.createElement('div');
|
|
384
|
+
name.className = 'model-item-name';
|
|
385
|
+
name.appendChild(createModelNameWithLabels(modelId, window.SERVER_MODELS || {}));
|
|
386
|
+
|
|
387
|
+
info.appendChild(name);
|
|
388
|
+
|
|
389
|
+
// Only add description if it exists and is not empty
|
|
390
|
+
if (modelData.description && modelData.description.trim()) {
|
|
391
|
+
const description = document.createElement('div');
|
|
392
|
+
description.className = 'model-item-description';
|
|
393
|
+
description.textContent = modelData.description;
|
|
394
|
+
info.appendChild(description);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const actions = document.createElement('div');
|
|
398
|
+
actions.className = 'model-item-actions';
|
|
399
|
+
|
|
400
|
+
// Check if model is actually installed by looking at the installedModels set
|
|
401
|
+
const isInstalled = installedModels.has(modelId);
|
|
402
|
+
const isLoaded = currentLoadedModel === modelId;
|
|
403
|
+
|
|
404
|
+
if (!isInstalled) {
|
|
405
|
+
const installBtn = document.createElement('button');
|
|
406
|
+
installBtn.className = 'model-item-btn install';
|
|
407
|
+
installBtn.textContent = '📥';
|
|
408
|
+
installBtn.title = 'Install';
|
|
409
|
+
installBtn.onclick = () => installModel(modelId);
|
|
410
|
+
actions.appendChild(installBtn);
|
|
411
|
+
} else {
|
|
412
|
+
if (isLoaded) {
|
|
413
|
+
const unloadBtn = document.createElement('button');
|
|
414
|
+
unloadBtn.className = 'model-item-btn unload';
|
|
415
|
+
unloadBtn.textContent = '⏏️';
|
|
416
|
+
unloadBtn.title = 'Unload';
|
|
417
|
+
unloadBtn.onclick = () => unloadModel();
|
|
418
|
+
actions.appendChild(unloadBtn);
|
|
419
|
+
} else {
|
|
420
|
+
const loadBtn = document.createElement('button');
|
|
421
|
+
const modelSelect = document.getElementById('model-select');
|
|
422
|
+
loadBtn.className = 'model-item-btn load';
|
|
423
|
+
loadBtn.textContent = '🚀';
|
|
424
|
+
loadBtn.title = 'Load';
|
|
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
|
+
};
|
|
437
|
+
actions.appendChild(loadBtn);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const deleteBtn = document.createElement('button');
|
|
441
|
+
deleteBtn.className = 'model-item-btn delete';
|
|
442
|
+
deleteBtn.textContent = '🗑️';
|
|
443
|
+
deleteBtn.title = 'Delete';
|
|
444
|
+
deleteBtn.onclick = () => deleteModel(modelId);
|
|
445
|
+
actions.appendChild(deleteBtn);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
item.appendChild(info);
|
|
449
|
+
item.appendChild(actions);
|
|
450
|
+
container.appendChild(item);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Install model
|
|
454
|
+
async function installModel(modelId) {
|
|
455
|
+
// Find the install button and show loading state
|
|
456
|
+
const modelItems = document.querySelectorAll('.model-item');
|
|
457
|
+
let installBtn = null;
|
|
458
|
+
|
|
459
|
+
modelItems.forEach(item => {
|
|
460
|
+
const nameElement = item.querySelector('.model-item-name .model-labels-container span');
|
|
461
|
+
if (nameElement && nameElement.getAttribute('data-model-id') === modelId) {
|
|
462
|
+
installBtn = item.querySelector('.model-item-btn.install');
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
if (installBtn) {
|
|
467
|
+
installBtn.disabled = true;
|
|
468
|
+
installBtn.textContent = '⏳';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Track this download as active
|
|
472
|
+
activeOperations.add(modelId);
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
// For registered models, only send model_name (per API spec)
|
|
476
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
|
|
477
|
+
method: 'POST',
|
|
478
|
+
headers: { 'Content-Type': 'application/json' },
|
|
479
|
+
body: JSON.stringify({ model_name: modelId })
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Download complete - remove from active operations
|
|
483
|
+
activeOperations.delete(modelId);
|
|
484
|
+
|
|
485
|
+
// Refresh installed models and model status
|
|
486
|
+
await fetchInstalledModels();
|
|
487
|
+
await updateModelStatusIndicator();
|
|
488
|
+
|
|
489
|
+
// Refresh model dropdown in chat
|
|
490
|
+
if (window.initializeModelDropdown) {
|
|
491
|
+
window.initializeModelDropdown();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Refresh model list
|
|
495
|
+
if (currentCategory === 'hot') displayHotModels();
|
|
496
|
+
else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
|
|
497
|
+
else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
console.error('Error installing model:', error);
|
|
500
|
+
showErrorBanner('Failed to install model: ' + error.message);
|
|
501
|
+
|
|
502
|
+
// Remove from active operations on error too
|
|
503
|
+
activeOperations.delete(modelId);
|
|
504
|
+
|
|
505
|
+
// Reset button state on error
|
|
506
|
+
if (installBtn) {
|
|
507
|
+
installBtn.disabled = false;
|
|
508
|
+
installBtn.textContent = '📥';
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
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
|
|
521
|
+
const modelItems = document.querySelectorAll('.model-item');
|
|
522
|
+
let deleteBtn = null;
|
|
523
|
+
|
|
524
|
+
modelItems.forEach(item => {
|
|
525
|
+
const nameElement = item.querySelector('.model-item-name .model-labels-container span');
|
|
526
|
+
if (nameElement && nameElement.getAttribute('data-model-id') === modelId) {
|
|
527
|
+
deleteBtn = item.querySelector('.model-item-btn.delete');
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
if (deleteBtn) {
|
|
532
|
+
deleteBtn.disabled = true;
|
|
533
|
+
deleteBtn.textContent = '⏳';
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
|
|
538
|
+
method: 'POST',
|
|
539
|
+
headers: { 'Content-Type': 'application/json' },
|
|
540
|
+
body: JSON.stringify({ model_name: modelId })
|
|
541
|
+
});
|
|
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
|
+
}
|
|
547
|
+
// Refresh installed models and model status
|
|
548
|
+
await fetchInstalledModels();
|
|
549
|
+
await updateModelStatusIndicator();
|
|
550
|
+
|
|
551
|
+
// Refresh model dropdown in chat
|
|
552
|
+
if (window.initializeModelDropdown) {
|
|
553
|
+
window.initializeModelDropdown();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Refresh model list
|
|
557
|
+
if (currentCategory === 'hot') displayHotModels();
|
|
558
|
+
else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
|
|
559
|
+
else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
console.error('Error deleting model:', error);
|
|
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
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// === Model Name Display ===
|
|
573
|
+
|
|
574
|
+
// Create model name with labels
|
|
575
|
+
function createModelNameWithLabels(modelId, serverModels) {
|
|
576
|
+
const modelData = serverModels[modelId];
|
|
577
|
+
const container = document.createElement('div');
|
|
578
|
+
container.className = 'model-labels-container';
|
|
579
|
+
|
|
580
|
+
// Model name
|
|
581
|
+
const nameSpan = document.createElement('span');
|
|
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;
|
|
592
|
+
container.appendChild(nameSpan);
|
|
593
|
+
|
|
594
|
+
// Labels
|
|
595
|
+
if (modelData && modelData.labels && Array.isArray(modelData.labels)) {
|
|
596
|
+
modelData.labels.forEach(label => {
|
|
597
|
+
const labelLower = label.toLowerCase();
|
|
598
|
+
|
|
599
|
+
// Skip "hot" labels since they have their own section
|
|
600
|
+
if (labelLower === 'hot') {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const labelSpan = document.createElement('span');
|
|
605
|
+
let labelClass = 'other';
|
|
606
|
+
if (labelLower === 'vision') {
|
|
607
|
+
labelClass = 'vision';
|
|
608
|
+
} else if (labelLower === 'embeddings') {
|
|
609
|
+
labelClass = 'embeddings';
|
|
610
|
+
} else if (labelLower === 'reasoning') {
|
|
611
|
+
labelClass = 'reasoning';
|
|
612
|
+
} else if (labelLower === 'reranking') {
|
|
613
|
+
labelClass = 'reranking';
|
|
614
|
+
} else if (labelLower === 'coding') {
|
|
615
|
+
labelClass = 'coding';
|
|
616
|
+
} else if (labelLower === 'tool-calling') {
|
|
617
|
+
labelClass = 'tool-calling';
|
|
618
|
+
}
|
|
619
|
+
labelSpan.className = `model-label ${labelClass}`;
|
|
620
|
+
labelSpan.textContent = label;
|
|
621
|
+
container.appendChild(labelSpan);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return container;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// === Model Management Table (for models tab) ===
|
|
629
|
+
|
|
630
|
+
// Initialize model management functionality when DOM is loaded
|
|
631
|
+
document.addEventListener('DOMContentLoaded', async function() {
|
|
632
|
+
// Set up model status controls
|
|
633
|
+
const unloadBtn = document.getElementById('model-unload-btn');
|
|
634
|
+
if (unloadBtn) {
|
|
635
|
+
unloadBtn.onclick = unloadModel;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Initial fetch of model data - this will populate installedModels
|
|
639
|
+
await updateModelStatusIndicator();
|
|
640
|
+
|
|
641
|
+
// Update category visibility on initial load
|
|
642
|
+
updateCategoryVisibility();
|
|
643
|
+
|
|
644
|
+
// Initialize model browser with hot models
|
|
645
|
+
displayHotModels();
|
|
646
|
+
|
|
647
|
+
// Initial load of model management UI - this will use the populated installedModels
|
|
648
|
+
await refreshModelMgmtUI();
|
|
649
|
+
|
|
650
|
+
// Refresh when switching to the models tab
|
|
651
|
+
const modelsTab = document.getElementById('tab-models');
|
|
652
|
+
if (modelsTab) {
|
|
653
|
+
modelsTab.addEventListener('click', refreshModelMgmtUI);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Set up register model form
|
|
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();
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// Toggle Add Model form
|
|
701
|
+
function toggleAddModelForm() {
|
|
702
|
+
const form = document.querySelector('.model-mgmt-register-form');
|
|
703
|
+
form.classList.toggle('collapsed');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Helper function to render a model table section
|
|
707
|
+
function renderModelTable(tbody, models, allModels, emptyMessage) {
|
|
708
|
+
tbody.innerHTML = '';
|
|
709
|
+
if (models.length === 0) {
|
|
710
|
+
const tr = document.createElement('tr');
|
|
711
|
+
const td = document.createElement('td');
|
|
712
|
+
td.colSpan = 2;
|
|
713
|
+
td.textContent = emptyMessage;
|
|
714
|
+
td.style.textAlign = 'center';
|
|
715
|
+
td.style.fontStyle = 'italic';
|
|
716
|
+
td.style.color = '#666';
|
|
717
|
+
td.style.padding = '1em';
|
|
718
|
+
tr.appendChild(td);
|
|
719
|
+
tbody.appendChild(tr);
|
|
720
|
+
} else {
|
|
721
|
+
models.forEach(mid => {
|
|
722
|
+
const tr = document.createElement('tr');
|
|
723
|
+
const tdName = document.createElement('td');
|
|
724
|
+
|
|
725
|
+
tdName.appendChild(createModelNameWithLabels(mid, allModels));
|
|
726
|
+
tdName.style.paddingRight = '1em';
|
|
727
|
+
tdName.style.verticalAlign = 'middle';
|
|
728
|
+
const tdBtn = document.createElement('td');
|
|
729
|
+
tdBtn.style.width = '1%';
|
|
730
|
+
tdBtn.style.verticalAlign = 'middle';
|
|
731
|
+
const btn = document.createElement('button');
|
|
732
|
+
btn.textContent = '+';
|
|
733
|
+
btn.title = 'Install model';
|
|
734
|
+
btn.onclick = async function() {
|
|
735
|
+
btn.disabled = true;
|
|
736
|
+
btn.textContent = '⏳';
|
|
737
|
+
btn.classList.add('installing-btn');
|
|
738
|
+
|
|
739
|
+
// Track this download as active
|
|
740
|
+
activeOperations.add(mid);
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
|
|
744
|
+
method: 'POST',
|
|
745
|
+
headers: { 'Content-Type': 'application/json' },
|
|
746
|
+
body: JSON.stringify({ model_name: mid })
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// Download complete - remove from active operations
|
|
750
|
+
activeOperations.delete(mid);
|
|
751
|
+
|
|
752
|
+
await refreshModelMgmtUI();
|
|
753
|
+
// Update chat dropdown too if loadModels function exists
|
|
754
|
+
if (typeof loadModels === 'function') {
|
|
755
|
+
await loadModels();
|
|
756
|
+
}
|
|
757
|
+
} catch (e) {
|
|
758
|
+
btn.textContent = 'Error';
|
|
759
|
+
btn.disabled = false;
|
|
760
|
+
showErrorBanner(`Failed to install model: ${e.message}`);
|
|
761
|
+
|
|
762
|
+
// Remove from active operations on error too
|
|
763
|
+
activeOperations.delete(mid);
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
tdBtn.appendChild(btn);
|
|
767
|
+
tr.appendChild(tdName);
|
|
768
|
+
tr.appendChild(tdBtn);
|
|
769
|
+
tbody.appendChild(tr);
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Model Management Tab Logic
|
|
775
|
+
async function refreshModelMgmtUI() {
|
|
776
|
+
// Get installed models from /api/v1/models
|
|
777
|
+
let installed = [];
|
|
778
|
+
try {
|
|
779
|
+
const data = await httpJson(getServerBaseUrl() + '/api/v1/models');
|
|
780
|
+
if (data.data && Array.isArray(data.data)) {
|
|
781
|
+
installed = data.data.map(m => m.id || m.name || m);
|
|
782
|
+
}
|
|
783
|
+
} catch (e) {
|
|
784
|
+
showErrorBanner(`Error loading models: ${e.message}`);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Update the global installedModels set
|
|
788
|
+
installedModels.clear();
|
|
789
|
+
installed.forEach(modelId => {
|
|
790
|
+
installedModels.add(modelId);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// All models from server_models.json (window.SERVER_MODELS)
|
|
794
|
+
const allModels = window.SERVER_MODELS || {};
|
|
795
|
+
|
|
796
|
+
// Separate hot models and regular suggested models not installed
|
|
797
|
+
const hotModels = [];
|
|
798
|
+
const regularSuggested = [];
|
|
799
|
+
|
|
800
|
+
Object.keys(allModels).forEach(k => {
|
|
801
|
+
if (allModels[k].suggested && !installed.includes(k)) {
|
|
802
|
+
const modelData = allModels[k];
|
|
803
|
+
const hasHotLabel = modelData.labels && modelData.labels.some(label =>
|
|
804
|
+
label.toLowerCase() === 'hot'
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
if (hasHotLabel) {
|
|
808
|
+
hotModels.push(k);
|
|
809
|
+
} else {
|
|
810
|
+
regularSuggested.push(k);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// Render installed models as a table (two columns, second is invisible)
|
|
816
|
+
const installedTbody = document.getElementById('installed-models-tbody');
|
|
817
|
+
if (installedTbody) {
|
|
818
|
+
installedTbody.innerHTML = '';
|
|
819
|
+
installed.forEach(function(mid) {
|
|
820
|
+
var tr = document.createElement('tr');
|
|
821
|
+
var tdName = document.createElement('td');
|
|
822
|
+
|
|
823
|
+
tdName.appendChild(createModelNameWithLabels(mid, allModels));
|
|
824
|
+
tdName.style.paddingRight = '1em';
|
|
825
|
+
tdName.style.verticalAlign = 'middle';
|
|
826
|
+
|
|
827
|
+
var tdBtn = document.createElement('td');
|
|
828
|
+
tdBtn.style.width = '1%';
|
|
829
|
+
tdBtn.style.verticalAlign = 'middle';
|
|
830
|
+
const btn = document.createElement('button');
|
|
831
|
+
btn.textContent = '−';
|
|
832
|
+
btn.title = 'Delete model';
|
|
833
|
+
btn.style.cursor = 'pointer';
|
|
834
|
+
btn.onclick = async function() {
|
|
835
|
+
if (!confirm(`Are you sure you want to delete the model "${mid}"?`)) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
btn.disabled = true;
|
|
839
|
+
btn.textContent = '⏳';
|
|
840
|
+
btn.style.backgroundColor = '#888';
|
|
841
|
+
try {
|
|
842
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
|
|
843
|
+
method: 'POST',
|
|
844
|
+
headers: { 'Content-Type': 'application/json' },
|
|
845
|
+
body: JSON.stringify({ model_name: mid })
|
|
846
|
+
});
|
|
847
|
+
await refreshModelMgmtUI();
|
|
848
|
+
// Update chat dropdown too if loadModels function exists
|
|
849
|
+
if (typeof loadModels === 'function') {
|
|
850
|
+
await loadModels();
|
|
851
|
+
}
|
|
852
|
+
} catch (e) {
|
|
853
|
+
btn.textContent = 'Error';
|
|
854
|
+
btn.disabled = false;
|
|
855
|
+
btn.style.backgroundColor = '';
|
|
856
|
+
showErrorBanner(`Failed to delete model: ${e.message}`);
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
tdBtn.appendChild(btn);
|
|
860
|
+
tr.appendChild(tdName);
|
|
861
|
+
tr.appendChild(tdBtn);
|
|
862
|
+
installedTbody.appendChild(tr);
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Render hot models and suggested models using the helper function
|
|
867
|
+
const hotTbody = document.getElementById('hot-models-tbody');
|
|
868
|
+
const suggestedTbody = document.getElementById('suggested-models-tbody');
|
|
869
|
+
|
|
870
|
+
if (hotTbody) {
|
|
871
|
+
renderModelTable(hotTbody, hotModels, allModels, "Nice, you've already installed all these models!");
|
|
872
|
+
}
|
|
873
|
+
if (suggestedTbody) {
|
|
874
|
+
renderModelTable(suggestedTbody, regularSuggested, allModels, "Nice, you've already installed all these models!");
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Refresh model dropdown in chat after updating installed models
|
|
878
|
+
if (window.initializeModelDropdown) {
|
|
879
|
+
window.initializeModelDropdown();
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Update system message when installed models change
|
|
883
|
+
if (window.displaySystemMessage) {
|
|
884
|
+
window.displaySystemMessage();
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Make refreshModelMgmtUI globally accessible
|
|
889
|
+
window.refreshModelMgmtUI = refreshModelMgmtUI;
|
|
890
|
+
|
|
891
|
+
// Display-only version that uses already-fetched installedModels data
|
|
892
|
+
function refreshModelMgmtUIDisplay() {
|
|
893
|
+
// Use the already-populated installedModels set
|
|
894
|
+
const installed = Array.from(installedModels);
|
|
895
|
+
|
|
896
|
+
// All models from server_models.json (window.SERVER_MODELS)
|
|
897
|
+
const allModels = window.SERVER_MODELS || {};
|
|
898
|
+
|
|
899
|
+
// Separate hot models and regular suggested models not installed
|
|
900
|
+
const hotModels = [];
|
|
901
|
+
const regularSuggested = [];
|
|
902
|
+
|
|
903
|
+
Object.keys(allModels).forEach(k => {
|
|
904
|
+
if (allModels[k].suggested && !installed.includes(k)) {
|
|
905
|
+
if (allModels[k].labels && allModels[k].labels.some(label => label.toLowerCase() === 'hot')) {
|
|
906
|
+
hotModels.push(k);
|
|
907
|
+
} else {
|
|
908
|
+
regularSuggested.push(k);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
// Render installed models as a table (two columns, second is invisible)
|
|
914
|
+
const installedTbody = document.getElementById('installed-models-tbody');
|
|
915
|
+
if (installedTbody) {
|
|
916
|
+
installedTbody.innerHTML = '';
|
|
917
|
+
installed.forEach(function(mid) {
|
|
918
|
+
var tr = document.createElement('tr');
|
|
919
|
+
var tdName = document.createElement('td');
|
|
920
|
+
|
|
921
|
+
tdName.appendChild(createModelNameWithLabels(mid, allModels));
|
|
922
|
+
tdName.style.paddingRight = '1em';
|
|
923
|
+
tdName.style.verticalAlign = 'middle';
|
|
924
|
+
|
|
925
|
+
var tdBtn = document.createElement('td');
|
|
926
|
+
tdBtn.style.width = '1%';
|
|
927
|
+
tdBtn.style.verticalAlign = 'middle';
|
|
928
|
+
const btn = document.createElement('button');
|
|
929
|
+
btn.textContent = '−';
|
|
930
|
+
btn.className = 'btn-remove-model';
|
|
931
|
+
btn.style.minWidth = '24px';
|
|
932
|
+
btn.style.padding = '2px 8px';
|
|
933
|
+
btn.style.fontSize = '16px';
|
|
934
|
+
btn.style.lineHeight = '1';
|
|
935
|
+
btn.style.border = '1px solid #ddd';
|
|
936
|
+
btn.style.backgroundColor = '#f8f9fa';
|
|
937
|
+
btn.style.cursor = 'pointer';
|
|
938
|
+
btn.style.borderRadius = '4px';
|
|
939
|
+
btn.title = 'Remove this model';
|
|
940
|
+
btn.onclick = async function() {
|
|
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';
|
|
946
|
+
try {
|
|
947
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
|
|
948
|
+
method: 'POST',
|
|
949
|
+
headers: { 'Content-Type': 'application/json' },
|
|
950
|
+
body: JSON.stringify({ model_name: mid })
|
|
951
|
+
});
|
|
952
|
+
await refreshModelMgmtUI();
|
|
953
|
+
} catch (error) {
|
|
954
|
+
console.error('Error removing model:', error);
|
|
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;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
tdBtn.appendChild(btn);
|
|
964
|
+
tr.appendChild(tdName);
|
|
965
|
+
tr.appendChild(tdBtn);
|
|
966
|
+
installedTbody.appendChild(tr);
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Render hot models and suggested models using the helper function
|
|
971
|
+
const hotTbody = document.getElementById('hot-models-tbody');
|
|
972
|
+
const suggestedTbody = document.getElementById('suggested-models-tbody');
|
|
973
|
+
|
|
974
|
+
if (hotTbody) {
|
|
975
|
+
renderModelTable(hotTbody, hotModels, allModels, "Nice, you've already installed all these models!");
|
|
976
|
+
}
|
|
977
|
+
if (suggestedTbody) {
|
|
978
|
+
renderModelTable(suggestedTbody, regularSuggested, allModels, "Nice, you've already installed all these models!");
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Refresh model dropdown in chat after updating installed models
|
|
982
|
+
if (window.initializeModelDropdown) {
|
|
983
|
+
window.initializeModelDropdown();
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Set up the register model form
|
|
988
|
+
function setupRegisterModelForm() {
|
|
989
|
+
const registerForm = document.getElementById('register-model-form');
|
|
990
|
+
const registerStatus = document.getElementById('register-model-status');
|
|
991
|
+
|
|
992
|
+
if (registerForm && registerStatus) {
|
|
993
|
+
registerForm.onsubmit = async function(e) {
|
|
994
|
+
e.preventDefault();
|
|
995
|
+
registerStatus.textContent = '';
|
|
996
|
+
let name = document.getElementById('register-model-name').value.trim();
|
|
997
|
+
|
|
998
|
+
// Always prepend 'user.' if not already present
|
|
999
|
+
if (!name.startsWith('user.')) {
|
|
1000
|
+
name = 'user.' + name;
|
|
1001
|
+
}
|
|
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
|
+
|
|
1013
|
+
const checkpoint = document.getElementById('register-checkpoint').value.trim();
|
|
1014
|
+
const recipe = document.getElementById('register-recipe').value;
|
|
1015
|
+
const reasoning = document.getElementById('register-reasoning').checked;
|
|
1016
|
+
const vision = document.getElementById('register-vision').checked;
|
|
1017
|
+
const embedding = document.getElementById('register-embedding').checked;
|
|
1018
|
+
const reranking = document.getElementById('register-reranking').checked;
|
|
1019
|
+
const mmproj = document.getElementById('register-mmproj').value.trim();
|
|
1020
|
+
|
|
1021
|
+
if (!name || !recipe) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const btn = document.getElementById('register-submit');
|
|
1026
|
+
btn.disabled = true;
|
|
1027
|
+
btn.textContent = 'Installing...';
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
1030
|
+
if (isLocalModel && selectedModelFiles) {
|
|
1031
|
+
if (recipe === 'llamacpp' && !Array.from(selectedModelFiles).some(file => file.name.toLowerCase().endsWith('.gguf'))) {
|
|
1032
|
+
throw new Error('No .gguf files found in the selected folder for llamacpp');
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const formData = new FormData();
|
|
1036
|
+
formData.append('model_name', name);
|
|
1037
|
+
formData.append('checkpoint', checkpoint);
|
|
1038
|
+
formData.append('recipe', recipe);
|
|
1039
|
+
formData.append('reasoning', reasoning);
|
|
1040
|
+
formData.append('vision', vision);
|
|
1041
|
+
formData.append('embedding', embedding);
|
|
1042
|
+
formData.append('reranking', reranking);
|
|
1043
|
+
if (mmproj) formData.append('mmproj', mmproj);
|
|
1044
|
+
Array.from(selectedModelFiles).forEach(file => {
|
|
1045
|
+
formData.append('model_files', file, file.webkitRelativePath);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/add-local-model', {
|
|
1049
|
+
method: 'POST',
|
|
1050
|
+
body: formData
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
else {
|
|
1054
|
+
if (!checkpoint) {
|
|
1055
|
+
throw new Error('Checkpoint is required for remote models');
|
|
1056
|
+
}
|
|
1057
|
+
const payload = { model_name: name, recipe, reasoning, vision, embedding, reranking };
|
|
1058
|
+
if (checkpoint) payload.checkpoint = checkpoint;
|
|
1059
|
+
if (mmproj) payload.mmproj = mmproj;
|
|
1060
|
+
|
|
1061
|
+
await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
|
|
1062
|
+
method: 'POST',
|
|
1063
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1064
|
+
body: JSON.stringify(payload)
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
registerStatus.textContent = 'Model installed!';
|
|
1069
|
+
registerStatus.style.color = '#0eaf51ff';
|
|
1070
|
+
registerStatus.className = 'register-status success';
|
|
1071
|
+
|
|
1072
|
+
// Add custom model to SERVER_MODELS so it appears in the UI without having to do a manual refresh
|
|
1073
|
+
if (name.startsWith('user.')) {
|
|
1074
|
+
const labels = ['custom'];
|
|
1075
|
+
if (vision) labels.push('vision');
|
|
1076
|
+
if (reasoning) labels.push('reasoning');
|
|
1077
|
+
if (embedding) labels.push('embeddings');
|
|
1078
|
+
if (reranking) labels.push('reranking');
|
|
1079
|
+
|
|
1080
|
+
window.SERVER_MODELS[name] = {
|
|
1081
|
+
recipe: recipe,
|
|
1082
|
+
labels: labels
|
|
1083
|
+
};
|
|
1084
|
+
if (checkpoint) window.SERVER_MODELS[name].checkpoint = checkpoint;
|
|
1085
|
+
if (mmproj) window.SERVER_MODELS[name].mmproj = mmproj;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
registerForm.reset();
|
|
1089
|
+
isLocalModel = false;
|
|
1090
|
+
selectedModelFiles = null;
|
|
1091
|
+
document.getElementById('folder-input').value = '';
|
|
1092
|
+
|
|
1093
|
+
await refreshModelMgmtUI();
|
|
1094
|
+
// Update chat dropdown too if loadModels function exists
|
|
1095
|
+
if (typeof loadModels === 'function') {
|
|
1096
|
+
await loadModels();
|
|
1097
|
+
}
|
|
1098
|
+
} catch (e) {
|
|
1099
|
+
registerStatus.textContent = e.message + ' See the Lemonade Server log for details.';
|
|
1100
|
+
registerStatus.style.color = '#dc3545';
|
|
1101
|
+
registerStatus.className = 'register-status error';
|
|
1102
|
+
showErrorBanner(`Model install failed: ${e.message}`);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
btn.disabled = false;
|
|
1106
|
+
btn.textContent = 'Install';
|
|
1107
|
+
refreshModelMgmtUI();
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
let isLocalModel = false;
|
|
1112
|
+
let selectedModelFiles = null;
|
|
1113
|
+
// Helper function to find mmproj file in selected folder
|
|
1114
|
+
function findMmprojFile(files) {
|
|
1115
|
+
for (let i = 0; i < files.length; i++) {
|
|
1116
|
+
const file = files[i];
|
|
1117
|
+
const fileName = file.name.toLowerCase();
|
|
1118
|
+
const relativePath = file.webkitRelativePath;
|
|
1119
|
+
|
|
1120
|
+
// Check if file contains 'mmproj' and has .gguf extension
|
|
1121
|
+
if (fileName.includes('mmproj') && fileName.endsWith('.gguf')) {
|
|
1122
|
+
// Return just the filename (last part of the path)
|
|
1123
|
+
return relativePath.split('/').pop();
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Helper function to find all non-mmproj GGUF files in selected folder
|
|
1130
|
+
function findGgufFiles(files) {
|
|
1131
|
+
const ggufFiles = [];
|
|
1132
|
+
for (let i = 0; i < files.length; i++) {
|
|
1133
|
+
const file = files[i];
|
|
1134
|
+
const fileName = file.name.toLowerCase();
|
|
1135
|
+
const relativePath = file.webkitRelativePath;
|
|
1136
|
+
|
|
1137
|
+
// Check if file has .gguf extension but is NOT an mmproj file
|
|
1138
|
+
if (fileName.endsWith('.gguf') && !fileName.includes('mmproj')) {
|
|
1139
|
+
// Store just the filename (last part of the path)
|
|
1140
|
+
ggufFiles.push(relativePath.split('/').pop());
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return ggufFiles;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Helper function to check GGUF files and show appropriate banners
|
|
1147
|
+
function checkGgufFilesAndShowBanner(files) {
|
|
1148
|
+
const recipeSelect = document.getElementById('register-recipe');
|
|
1149
|
+
|
|
1150
|
+
// Only check if llamacpp is selected
|
|
1151
|
+
if (!recipeSelect || recipeSelect.value !== 'llamacpp') {
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const mmprojFile = findMmprojFile(files);
|
|
1156
|
+
const ggufFiles = findGgufFiles(files);
|
|
1157
|
+
|
|
1158
|
+
// Hide any existing banners first
|
|
1159
|
+
hideErrorBanner();
|
|
1160
|
+
|
|
1161
|
+
if (ggufFiles.length > 1) {
|
|
1162
|
+
// Multiple GGUF files detected
|
|
1163
|
+
const folderPath = files[0].webkitRelativePath.split('/')[0];
|
|
1164
|
+
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]}`;
|
|
1165
|
+
|
|
1166
|
+
if (mmprojFile) {
|
|
1167
|
+
bannerMsg += `\n\nDon't forget to enter the mmproj file name and check the 'vision' checkbox if it is a vision model.`;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
showBanner(bannerMsg, 'warning');
|
|
1171
|
+
} else if (mmprojFile) {
|
|
1172
|
+
// MMproj detected
|
|
1173
|
+
showBanner("MMproj detected and populated. Please validate the file name and check the 'vision' checkbox if it is a vision model.", 'success');
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
// Helper function to auto-fill mmproj field if llamacpp is selected
|
|
1177
|
+
function autoFillMmproj() {
|
|
1178
|
+
const recipeSelect = document.getElementById('register-recipe');
|
|
1179
|
+
const mmprojInput = document.getElementById('register-mmproj');
|
|
1180
|
+
|
|
1181
|
+
if (recipeSelect && mmprojInput && isLocalModel && selectedModelFiles) {
|
|
1182
|
+
const selectedRecipe = recipeSelect.value;
|
|
1183
|
+
|
|
1184
|
+
if (selectedRecipe === 'llamacpp') {
|
|
1185
|
+
const mmprojFile = findMmprojFile(selectedModelFiles);
|
|
1186
|
+
if (mmprojFile) {
|
|
1187
|
+
mmprojInput.value = mmprojFile;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Check GGUF files and show appropriate banner
|
|
1191
|
+
checkGgufFilesAndShowBanner(selectedModelFiles);
|
|
1192
|
+
} else {
|
|
1193
|
+
// Hide banners if not llamacpp
|
|
1194
|
+
hideErrorBanner();
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
function setupFolderSelection() {
|
|
1199
|
+
const selectFolderBtn = document.getElementById('select-folder-btn');
|
|
1200
|
+
const folderInput = document.getElementById('folder-input');
|
|
1201
|
+
const checkpointInput = document.getElementById('register-checkpoint');
|
|
1202
|
+
const recipeSelect = document.getElementById('register-recipe');
|
|
1203
|
+
|
|
1204
|
+
if (selectFolderBtn && folderInput && checkpointInput) {
|
|
1205
|
+
selectFolderBtn.addEventListener('click', () => {
|
|
1206
|
+
folderInput.click();
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
folderInput.addEventListener('change', (event) => {
|
|
1210
|
+
const files = event.target.files;
|
|
1211
|
+
if (files.length > 0) {
|
|
1212
|
+
const firstFile = files[0];
|
|
1213
|
+
const folderPath = firstFile.webkitRelativePath.split('/')[0];
|
|
1214
|
+
checkpointInput.value = folderPath;
|
|
1215
|
+
isLocalModel = true;
|
|
1216
|
+
selectedModelFiles = files;
|
|
1217
|
+
|
|
1218
|
+
// Auto-fill mmproj if llamacpp is already selected
|
|
1219
|
+
autoFillMmproj();
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
isLocalModel = false;
|
|
1223
|
+
selectedModelFiles = null;
|
|
1224
|
+
checkpointInput.value = '';
|
|
1225
|
+
hideErrorBanner();
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
// Add listener to recipe dropdown to auto-fill mmproj when changed to llamacpp
|
|
1230
|
+
if (recipeSelect) {
|
|
1231
|
+
recipeSelect.addEventListener('change', () => {
|
|
1232
|
+
autoFillMmproj();
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
// === Migration/Cleanup Functions ===
|
|
1238
|
+
|
|
1239
|
+
// Store incompatible models data globally
|
|
1240
|
+
let incompatibleModelsData = null;
|
|
1241
|
+
|
|
1242
|
+
// Check for incompatible models on page load
|
|
1243
|
+
async function checkIncompatibleModels() {
|
|
1244
|
+
try {
|
|
1245
|
+
const response = await httpJson(getServerBaseUrl() + '/api/v1/migration/incompatible-models');
|
|
1246
|
+
incompatibleModelsData = response;
|
|
1247
|
+
|
|
1248
|
+
if (response.count > 0) {
|
|
1249
|
+
showMigrationBanner(response.count, response.total_size);
|
|
1250
|
+
}
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
console.error('Error checking for incompatible models:', error);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Show migration banner
|
|
1257
|
+
function showMigrationBanner(count, totalSize) {
|
|
1258
|
+
const banner = document.getElementById('migration-banner');
|
|
1259
|
+
const msg = document.getElementById('migration-banner-msg');
|
|
1260
|
+
|
|
1261
|
+
const sizeGB = (totalSize / (1024 * 1024 * 1024)).toFixed(1);
|
|
1262
|
+
msg.textContent = `Found ${count} incompatible RyzenAI model${count > 1 ? 's' : ''} (${sizeGB} GB). Clean up to free disk space.`;
|
|
1263
|
+
banner.style.display = 'flex';
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Hide migration banner
|
|
1267
|
+
function hideMigrationBanner() {
|
|
1268
|
+
const banner = document.getElementById('migration-banner');
|
|
1269
|
+
banner.style.display = 'none';
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Show migration modal with model list
|
|
1273
|
+
function showMigrationModal() {
|
|
1274
|
+
if (!incompatibleModelsData || incompatibleModelsData.count === 0) {
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const modal = document.getElementById('migration-modal');
|
|
1279
|
+
const modelList = document.getElementById('migration-model-list');
|
|
1280
|
+
const totalSize = document.getElementById('migration-total-size');
|
|
1281
|
+
|
|
1282
|
+
// Populate model list
|
|
1283
|
+
modelList.innerHTML = '';
|
|
1284
|
+
incompatibleModelsData.models.forEach(model => {
|
|
1285
|
+
const item = document.createElement('div');
|
|
1286
|
+
item.className = 'migration-model-item';
|
|
1287
|
+
|
|
1288
|
+
const nameSpan = document.createElement('span');
|
|
1289
|
+
nameSpan.className = 'migration-model-name';
|
|
1290
|
+
nameSpan.textContent = model.name;
|
|
1291
|
+
|
|
1292
|
+
const sizeSpan = document.createElement('span');
|
|
1293
|
+
sizeSpan.className = 'migration-model-size';
|
|
1294
|
+
sizeSpan.textContent = model.size_formatted;
|
|
1295
|
+
|
|
1296
|
+
item.appendChild(nameSpan);
|
|
1297
|
+
item.appendChild(sizeSpan);
|
|
1298
|
+
modelList.appendChild(item);
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
// Set total size
|
|
1302
|
+
const sizeGB = (incompatibleModelsData.total_size / (1024 * 1024 * 1024)).toFixed(1);
|
|
1303
|
+
totalSize.textContent = `${sizeGB} GB`;
|
|
1304
|
+
|
|
1305
|
+
modal.style.display = 'flex';
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Hide migration modal
|
|
1309
|
+
function hideMigrationModal() {
|
|
1310
|
+
const modal = document.getElementById('migration-modal');
|
|
1311
|
+
modal.style.display = 'none';
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Delete incompatible models
|
|
1315
|
+
async function deleteIncompatibleModels() {
|
|
1316
|
+
if (!incompatibleModelsData || incompatibleModelsData.count === 0) {
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const modelPaths = incompatibleModelsData.models.map(m => m.path);
|
|
1321
|
+
|
|
1322
|
+
try {
|
|
1323
|
+
// Disable buttons during deletion
|
|
1324
|
+
const deleteBtn = document.querySelector('.delete-btn');
|
|
1325
|
+
const cancelBtn = document.querySelector('.cancel-btn');
|
|
1326
|
+
deleteBtn.disabled = true;
|
|
1327
|
+
cancelBtn.disabled = true;
|
|
1328
|
+
deleteBtn.textContent = 'Deleting...';
|
|
1329
|
+
|
|
1330
|
+
const response = await httpRequest(getServerBaseUrl() + '/api/v1/migration/cleanup', {
|
|
1331
|
+
method: 'POST',
|
|
1332
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1333
|
+
body: JSON.stringify({ model_paths: modelPaths })
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
const result = await response.json();
|
|
1337
|
+
|
|
1338
|
+
// Close modal
|
|
1339
|
+
hideMigrationModal();
|
|
1340
|
+
|
|
1341
|
+
// Hide banner
|
|
1342
|
+
hideMigrationBanner();
|
|
1343
|
+
|
|
1344
|
+
// Show success message
|
|
1345
|
+
showSuccessMessage(`Successfully deleted ${result.success_count} model${result.success_count > 1 ? 's' : ''}, freed ${result.freed_size_formatted}`);
|
|
1346
|
+
|
|
1347
|
+
// Clear cached data
|
|
1348
|
+
incompatibleModelsData = null;
|
|
1349
|
+
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
console.error('Error deleting incompatible models:', error);
|
|
1352
|
+
showErrorBanner('Failed to delete models: ' + error.message);
|
|
1353
|
+
|
|
1354
|
+
// Re-enable buttons
|
|
1355
|
+
const deleteBtn = document.querySelector('.delete-btn');
|
|
1356
|
+
const cancelBtn = document.querySelector('.cancel-btn');
|
|
1357
|
+
deleteBtn.disabled = false;
|
|
1358
|
+
cancelBtn.disabled = false;
|
|
1359
|
+
deleteBtn.textContent = 'Delete All';
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Show success message (reuse error banner with green color)
|
|
1364
|
+
function showSuccessMessage(message) {
|
|
1365
|
+
const banner = document.getElementById('error-banner');
|
|
1366
|
+
const msg = document.getElementById('error-banner-msg');
|
|
1367
|
+
msg.textContent = message;
|
|
1368
|
+
banner.style.backgroundColor = '#2d7f47';
|
|
1369
|
+
banner.style.display = 'flex';
|
|
1370
|
+
|
|
1371
|
+
// Auto-hide after 5 seconds
|
|
1372
|
+
setTimeout(() => {
|
|
1373
|
+
banner.style.display = 'none';
|
|
1374
|
+
banner.style.backgroundColor = ''; // Reset to default
|
|
1375
|
+
}, 5000);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// Check for incompatible models when page loads
|
|
1379
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1380
|
+
// Run check after a short delay to let the page load
|
|
1381
|
+
setTimeout(checkIncompatibleModels, 1000);
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
// Make functions globally available for HTML onclick handlers and other components
|
|
1385
|
+
window.toggleCategory = toggleCategory;
|
|
1386
|
+
window.selectRecipe = selectRecipe;
|
|
1387
|
+
window.selectLabel = selectLabel;
|
|
1388
|
+
window.showAddModelForm = showAddModelForm;
|
|
1389
|
+
window.unloadModel = unloadModel;
|
|
1390
|
+
window.installModel = installModel;
|
|
1391
|
+
window.deleteModel = deleteModel;
|
|
1392
|
+
window.showMigrationModal = showMigrationModal;
|
|
1393
|
+
window.hideMigrationModal = hideMigrationModal;
|
|
1394
|
+
window.hideMigrationBanner = hideMigrationBanner;
|
|
1395
|
+
window.deleteIncompatibleModels = deleteIncompatibleModels;
|