lemonade-sdk 8.1.1__py3-none-any.whl → 8.1.3__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.

@@ -0,0 +1,865 @@
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
+
7
+ // Make installedModels accessible globally for the chat dropdown
8
+ window.installedModels = installedModels;
9
+ let currentCategory = 'hot';
10
+ let currentFilter = null;
11
+
12
+ // === Model Status Management ===
13
+
14
+ // Fetch installed models from the server
15
+ async function fetchInstalledModels() {
16
+ try {
17
+ const response = await httpJson(getServerBaseUrl() + '/api/v1/models');
18
+ installedModels.clear();
19
+ if (response && response.data) {
20
+ response.data.forEach(model => {
21
+ installedModels.add(model.id);
22
+ });
23
+ }
24
+ } catch (error) {
25
+ console.error('Error fetching installed models:', error);
26
+ // If we can't fetch, assume all are installed to maintain current functionality
27
+ const allModels = window.SERVER_MODELS || {};
28
+ Object.keys(allModels).forEach(modelId => {
29
+ installedModels.add(modelId);
30
+ });
31
+ }
32
+ }
33
+
34
+ // Check health endpoint to get current model status
35
+ async function checkModelHealth() {
36
+ try {
37
+ const response = await httpJson(getServerBaseUrl() + '/api/v1/health');
38
+ return response;
39
+ } catch (error) {
40
+ console.error('Error checking model health:', error);
41
+ return null;
42
+ }
43
+ }
44
+
45
+ // Update model status indicator
46
+ async function updateModelStatusIndicator() {
47
+ const indicator = document.getElementById('model-status-indicator');
48
+ const statusText = document.getElementById('model-status-text');
49
+ const unloadBtn = document.getElementById('model-unload-btn');
50
+
51
+ // Fetch both health and installed models
52
+ const [health] = await Promise.all([
53
+ checkModelHealth(),
54
+ fetchInstalledModels()
55
+ ]);
56
+
57
+ // Refresh model dropdown in chat after fetching installed models
58
+ if (window.initializeModelDropdown) {
59
+ window.initializeModelDropdown();
60
+ }
61
+
62
+ // Refresh model management UI if we're on the models tab
63
+ const modelsTab = document.getElementById('content-models');
64
+ if (modelsTab && modelsTab.classList.contains('active')) {
65
+ // Use the display-only version to avoid re-fetching data we just fetched
66
+ refreshModelMgmtUIDisplay();
67
+ }
68
+
69
+ // Remove any click handlers
70
+ indicator.onclick = null;
71
+
72
+ if (health && health.model_loaded) {
73
+ // Model is loaded - show model name with online status
74
+ currentLoadedModel = health.model_loaded;
75
+ updateStatusIndicator(health.model_loaded, 'loaded');
76
+ unloadBtn.style.display = 'block';
77
+ } else if (health) {
78
+ // Server is online but no model loaded
79
+ currentLoadedModel = null;
80
+ updateStatusIndicator('Server Online', 'online');
81
+ unloadBtn.style.display = 'none';
82
+ } else {
83
+ // Server is offline
84
+ currentLoadedModel = null;
85
+ updateStatusIndicator('Server Offline', 'offline');
86
+ unloadBtn.style.display = 'none';
87
+ }
88
+ }
89
+
90
+ // Unload current model
91
+ async function unloadModel() {
92
+ if (!currentLoadedModel) return;
93
+
94
+ try {
95
+ await httpRequest(getServerBaseUrl() + '/api/v1/unload', {
96
+ method: 'POST'
97
+ });
98
+ await updateModelStatusIndicator();
99
+
100
+ // Refresh model list to show updated button states
101
+ if (currentCategory === 'hot') displayHotModels();
102
+ else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
103
+ else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
104
+ } catch (error) {
105
+ console.error('Error unloading model:', error);
106
+ showErrorBanner('Failed to unload model: ' + error.message);
107
+ }
108
+ }
109
+
110
+ // === Model Browser Management ===
111
+
112
+ // Toggle category in model browser (only for Hot Models now)
113
+ function toggleCategory(categoryName) {
114
+ const header = document.querySelector(`[data-category="${categoryName}"] .category-header`);
115
+ const content = document.getElementById(`category-${categoryName}`);
116
+
117
+ if (categoryName === 'hot') {
118
+ // Check if hot models is already selected
119
+ const isCurrentlyActive = header.classList.contains('active');
120
+
121
+ // Clear all other active states
122
+ document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
123
+
124
+ if (!isCurrentlyActive) {
125
+ // Show hot models
126
+ header.classList.add('active');
127
+ content.classList.add('expanded');
128
+ currentCategory = categoryName;
129
+ currentFilter = null;
130
+ displayHotModels();
131
+ }
132
+ // If already active, keep it active (don't toggle off)
133
+ }
134
+ }
135
+
136
+ // Show add model form in main area
137
+ function showAddModelForm() {
138
+ // Clear all sidebar active states
139
+ document.querySelectorAll('.category-header').forEach(h => h.classList.remove('active'));
140
+ document.querySelectorAll('.category-content').forEach(c => c.classList.remove('expanded'));
141
+ document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
142
+
143
+ // Highlight "Add a Model" as selected
144
+ const addModelHeader = document.querySelector('[data-category="add"] .category-header');
145
+ if (addModelHeader) {
146
+ addModelHeader.classList.add('active');
147
+ }
148
+
149
+ // Hide model list and show form
150
+ document.getElementById('model-list').style.display = 'none';
151
+ document.getElementById('add-model-form-main').style.display = 'block';
152
+
153
+ // Set current state
154
+ currentCategory = 'add';
155
+ currentFilter = null;
156
+ }
157
+
158
+ // Select recipe filter
159
+ function selectRecipe(recipe) {
160
+ // Clear hot models active state
161
+ document.querySelectorAll('.category-header').forEach(h => h.classList.remove('active'));
162
+ document.querySelectorAll('.category-content').forEach(c => c.classList.remove('expanded'));
163
+
164
+ // Clear all subcategory selections
165
+ document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
166
+
167
+ // Set this recipe as active
168
+ document.querySelector(`[data-recipe="${recipe}"]`).classList.add('active');
169
+
170
+ currentCategory = 'recipes';
171
+ currentFilter = recipe;
172
+ displayModelsByRecipe(recipe);
173
+ }
174
+
175
+ // Select label filter
176
+ function selectLabel(label) {
177
+ // Clear hot models active state
178
+ document.querySelectorAll('.category-header').forEach(h => h.classList.remove('active'));
179
+ document.querySelectorAll('.category-content').forEach(c => c.classList.remove('expanded'));
180
+
181
+ // Clear all subcategory selections
182
+ document.querySelectorAll('.subcategory').forEach(s => s.classList.remove('active'));
183
+
184
+ // Set this label as active
185
+ document.querySelector(`[data-label="${label}"]`).classList.add('active');
186
+
187
+ currentCategory = 'labels';
188
+ currentFilter = label;
189
+ displayModelsByLabel(label);
190
+ }
191
+
192
+ // Display suggested models (Qwen3-0.6B-GGUF as default)
193
+ function displaySuggestedModels() {
194
+ const modelList = document.getElementById('model-list');
195
+ const allModels = window.SERVER_MODELS || {};
196
+
197
+ modelList.innerHTML = '';
198
+
199
+ // First show Qwen3-0.6B-GGUF as the default suggested model
200
+ if (allModels['Qwen3-0.6B-GGUF']) {
201
+ createModelItem('Qwen3-0.6B-GGUF', allModels['Qwen3-0.6B-GGUF'], modelList);
202
+ }
203
+
204
+ // Then show other suggested models (excluding the one already shown)
205
+ Object.entries(allModels).forEach(([modelId, modelData]) => {
206
+ if (modelData.suggested && modelId !== 'Qwen3-0.6B-GGUF') {
207
+ createModelItem(modelId, modelData, modelList);
208
+ }
209
+ });
210
+
211
+ if (modelList.innerHTML === '') {
212
+ modelList.innerHTML = '<p>No suggested models available</p>';
213
+ }
214
+ }
215
+
216
+ // Display hot models
217
+ function displayHotModels() {
218
+ const modelList = document.getElementById('model-list');
219
+ const addModelForm = document.getElementById('add-model-form-main');
220
+ const allModels = window.SERVER_MODELS || {};
221
+
222
+ // Show model list, hide form
223
+ modelList.style.display = 'block';
224
+ addModelForm.style.display = 'none';
225
+
226
+ modelList.innerHTML = '';
227
+
228
+ Object.entries(allModels).forEach(([modelId, modelData]) => {
229
+ if (modelData.labels && modelData.labels.includes('hot')) {
230
+ createModelItem(modelId, modelData, modelList);
231
+ }
232
+ });
233
+ }
234
+
235
+ // Display models by recipe
236
+ function displayModelsByRecipe(recipe) {
237
+ const modelList = document.getElementById('model-list');
238
+ const addModelForm = document.getElementById('add-model-form-main');
239
+ const allModels = window.SERVER_MODELS || {};
240
+
241
+ // Show model list, hide form
242
+ modelList.style.display = 'block';
243
+ addModelForm.style.display = 'none';
244
+
245
+ modelList.innerHTML = '';
246
+
247
+ Object.entries(allModels).forEach(([modelId, modelData]) => {
248
+ if (modelData.recipe === recipe) {
249
+ createModelItem(modelId, modelData, modelList);
250
+ }
251
+ });
252
+ }
253
+
254
+ // Display models by label
255
+ function displayModelsByLabel(label) {
256
+ const modelList = document.getElementById('model-list');
257
+ const addModelForm = document.getElementById('add-model-form-main');
258
+ const allModels = window.SERVER_MODELS || {};
259
+
260
+ // Show model list, hide form
261
+ modelList.style.display = 'block';
262
+ addModelForm.style.display = 'none';
263
+
264
+ modelList.innerHTML = '';
265
+
266
+ Object.entries(allModels).forEach(([modelId, modelData]) => {
267
+ if (label === 'custom') {
268
+ // Show user-added models (those starting with 'user.')
269
+ if (modelId.startsWith('user.')) {
270
+ createModelItem(modelId, modelData, modelList);
271
+ }
272
+ } else if (modelData.labels && modelData.labels.includes(label)) {
273
+ createModelItem(modelId, modelData, modelList);
274
+ }
275
+ });
276
+ }
277
+
278
+ // Create model item element
279
+ function createModelItem(modelId, modelData, container) {
280
+ const item = document.createElement('div');
281
+ item.className = 'model-item';
282
+
283
+ const info = document.createElement('div');
284
+ info.className = 'model-item-info';
285
+
286
+ const name = document.createElement('div');
287
+ name.className = 'model-item-name';
288
+ name.appendChild(createModelNameWithLabels(modelId, window.SERVER_MODELS || {}));
289
+
290
+ info.appendChild(name);
291
+
292
+ // Only add description if it exists and is not empty
293
+ if (modelData.description && modelData.description.trim()) {
294
+ const description = document.createElement('div');
295
+ description.className = 'model-item-description';
296
+ description.textContent = modelData.description;
297
+ info.appendChild(description);
298
+ }
299
+
300
+ const actions = document.createElement('div');
301
+ actions.className = 'model-item-actions';
302
+
303
+ // Check if model is actually installed by looking at the installedModels set
304
+ const isInstalled = installedModels.has(modelId);
305
+ const isLoaded = currentLoadedModel === modelId;
306
+
307
+ if (!isInstalled) {
308
+ const installBtn = document.createElement('button');
309
+ installBtn.className = 'model-item-btn install';
310
+ installBtn.textContent = '📥';
311
+ installBtn.title = 'Install';
312
+ installBtn.onclick = () => installModel(modelId);
313
+ actions.appendChild(installBtn);
314
+ } else {
315
+ if (isLoaded) {
316
+ const unloadBtn = document.createElement('button');
317
+ unloadBtn.className = 'model-item-btn unload';
318
+ unloadBtn.textContent = '⏏️';
319
+ unloadBtn.title = 'Unload';
320
+ unloadBtn.onclick = () => unloadModel();
321
+ actions.appendChild(unloadBtn);
322
+ } else {
323
+ const loadBtn = document.createElement('button');
324
+ loadBtn.className = 'model-item-btn load';
325
+ loadBtn.textContent = '🚀';
326
+ loadBtn.title = 'Load';
327
+ loadBtn.onclick = () => loadModel(modelId);
328
+ actions.appendChild(loadBtn);
329
+ }
330
+
331
+ const deleteBtn = document.createElement('button');
332
+ deleteBtn.className = 'model-item-btn delete';
333
+ deleteBtn.textContent = '🗑️';
334
+ deleteBtn.title = 'Delete';
335
+ deleteBtn.onclick = () => deleteModel(modelId);
336
+ actions.appendChild(deleteBtn);
337
+ }
338
+
339
+ item.appendChild(info);
340
+ item.appendChild(actions);
341
+ container.appendChild(item);
342
+ }
343
+
344
+ // Install model
345
+ async function installModel(modelId) {
346
+ // Find the install button and show loading state
347
+ const modelItems = document.querySelectorAll('.model-item');
348
+ let installBtn = null;
349
+
350
+ modelItems.forEach(item => {
351
+ const nameElement = item.querySelector('.model-item-name .model-labels-container span');
352
+ if (nameElement && nameElement.textContent === modelId) {
353
+ installBtn = item.querySelector('.model-item-btn.install');
354
+ }
355
+ });
356
+
357
+ if (installBtn) {
358
+ installBtn.disabled = true;
359
+ installBtn.textContent = '⏳';
360
+ }
361
+
362
+ try {
363
+ const modelData = window.SERVER_MODELS[modelId];
364
+ await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
365
+ method: 'POST',
366
+ headers: { 'Content-Type': 'application/json' },
367
+ body: JSON.stringify({ model_name: modelId, ...modelData })
368
+ });
369
+
370
+ // Refresh installed models and model status
371
+ await fetchInstalledModels();
372
+ await updateModelStatusIndicator();
373
+
374
+ // Refresh model dropdown in chat
375
+ if (window.initializeModelDropdown) {
376
+ window.initializeModelDropdown();
377
+ }
378
+
379
+ // Refresh model list
380
+ if (currentCategory === 'hot') displayHotModels();
381
+ else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
382
+ else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
383
+ } catch (error) {
384
+ console.error('Error installing model:', error);
385
+ showErrorBanner('Failed to install model: ' + error.message);
386
+
387
+ // Reset button state on error
388
+ if (installBtn) {
389
+ installBtn.disabled = false;
390
+ installBtn.textContent = 'Install';
391
+ }
392
+ }
393
+ }
394
+
395
+ // Load model
396
+ async function loadModel(modelId) {
397
+ // Find the load button and show loading state
398
+ const modelItems = document.querySelectorAll('.model-item');
399
+ let loadBtn = null;
400
+
401
+ modelItems.forEach(item => {
402
+ const nameElement = item.querySelector('.model-item-name .model-labels-container span');
403
+ if (nameElement && nameElement.textContent === modelId) {
404
+ loadBtn = item.querySelector('.model-item-btn.load');
405
+ }
406
+ });
407
+
408
+ // Use the standardized load function
409
+ const success = await loadModelStandardized(modelId, {
410
+ loadButton: loadBtn,
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;
428
+ }
429
+
430
+ try {
431
+ await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
432
+ method: 'POST',
433
+ headers: { 'Content-Type': 'application/json' },
434
+ body: JSON.stringify({ model_name: modelId })
435
+ });
436
+
437
+ // Refresh installed models and model status
438
+ await fetchInstalledModels();
439
+ await updateModelStatusIndicator();
440
+
441
+ // Refresh model dropdown in chat
442
+ if (window.initializeModelDropdown) {
443
+ window.initializeModelDropdown();
444
+ }
445
+
446
+ // Refresh model list
447
+ if (currentCategory === 'hot') displayHotModels();
448
+ else if (currentCategory === 'recipes') displayModelsByRecipe(currentFilter);
449
+ else if (currentCategory === 'labels') displayModelsByLabel(currentFilter);
450
+ } catch (error) {
451
+ console.error('Error deleting model:', error);
452
+ showErrorBanner('Failed to delete model: ' + error.message);
453
+ }
454
+ }
455
+
456
+ // === Model Name Display ===
457
+
458
+ // Create model name with labels
459
+ function createModelNameWithLabels(modelId, serverModels) {
460
+ const container = document.createElement('div');
461
+ container.className = 'model-labels-container';
462
+
463
+ // Model name
464
+ const nameSpan = document.createElement('span');
465
+ nameSpan.textContent = modelId;
466
+ container.appendChild(nameSpan);
467
+
468
+ // Labels
469
+ const modelData = serverModels[modelId];
470
+ if (modelData && modelData.labels && Array.isArray(modelData.labels)) {
471
+ modelData.labels.forEach(label => {
472
+ const labelLower = label.toLowerCase();
473
+
474
+ // Skip "hot" labels since they have their own section
475
+ if (labelLower === 'hot') {
476
+ return;
477
+ }
478
+
479
+ const labelSpan = document.createElement('span');
480
+ let labelClass = 'other';
481
+ if (labelLower === 'vision') {
482
+ labelClass = 'vision';
483
+ } else if (labelLower === 'embeddings') {
484
+ labelClass = 'embeddings';
485
+ } else if (labelLower === 'reasoning') {
486
+ labelClass = 'reasoning';
487
+ } else if (labelLower === 'reranking') {
488
+ labelClass = 'reranking';
489
+ } else if (labelLower === 'coding') {
490
+ labelClass = 'coding';
491
+ }
492
+ labelSpan.className = `model-label ${labelClass}`;
493
+ labelSpan.textContent = label;
494
+ container.appendChild(labelSpan);
495
+ });
496
+ }
497
+
498
+ return container;
499
+ }
500
+
501
+ // === Model Management Table (for models tab) ===
502
+
503
+ // Initialize model management functionality when DOM is loaded
504
+ document.addEventListener('DOMContentLoaded', async function() {
505
+ // Set up model status controls
506
+ const unloadBtn = document.getElementById('model-unload-btn');
507
+ if (unloadBtn) {
508
+ unloadBtn.onclick = unloadModel;
509
+ }
510
+
511
+ // Initial fetch of model data - this will populate installedModels
512
+ await updateModelStatusIndicator();
513
+
514
+ // Set up periodic refresh of model status
515
+ setInterval(updateModelStatusIndicator, 5000); // Check every 5 seconds
516
+
517
+ // Initialize model browser with hot models
518
+ displayHotModels();
519
+
520
+ // Initial load of model management UI - this will use the populated installedModels
521
+ await refreshModelMgmtUI();
522
+
523
+ // Refresh when switching to the models tab
524
+ const modelsTab = document.getElementById('tab-models');
525
+ if (modelsTab) {
526
+ modelsTab.addEventListener('click', refreshModelMgmtUI);
527
+ }
528
+
529
+ // Set up register model form
530
+ setupRegisterModelForm();
531
+ });
532
+
533
+ // Toggle Add Model form
534
+ function toggleAddModelForm() {
535
+ const form = document.querySelector('.model-mgmt-register-form');
536
+ form.classList.toggle('collapsed');
537
+ }
538
+
539
+ // Helper function to render a model table section
540
+ function renderModelTable(tbody, models, allModels, emptyMessage) {
541
+ tbody.innerHTML = '';
542
+ if (models.length === 0) {
543
+ const tr = document.createElement('tr');
544
+ const td = document.createElement('td');
545
+ td.colSpan = 2;
546
+ td.textContent = emptyMessage;
547
+ td.style.textAlign = 'center';
548
+ td.style.fontStyle = 'italic';
549
+ td.style.color = '#666';
550
+ td.style.padding = '1em';
551
+ tr.appendChild(td);
552
+ tbody.appendChild(tr);
553
+ } else {
554
+ models.forEach(mid => {
555
+ const tr = document.createElement('tr');
556
+ const tdName = document.createElement('td');
557
+
558
+ tdName.appendChild(createModelNameWithLabels(mid, allModels));
559
+ tdName.style.paddingRight = '1em';
560
+ tdName.style.verticalAlign = 'middle';
561
+ const tdBtn = document.createElement('td');
562
+ tdBtn.style.width = '1%';
563
+ tdBtn.style.verticalAlign = 'middle';
564
+ const btn = document.createElement('button');
565
+ btn.textContent = '+';
566
+ btn.title = 'Install model';
567
+ btn.onclick = async function() {
568
+ btn.disabled = true;
569
+ btn.textContent = 'Installing...';
570
+ btn.classList.add('installing-btn');
571
+ try {
572
+ await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
573
+ method: 'POST',
574
+ headers: { 'Content-Type': 'application/json' },
575
+ body: JSON.stringify({ model_name: mid })
576
+ });
577
+ await refreshModelMgmtUI();
578
+ // Update chat dropdown too if loadModels function exists
579
+ if (typeof loadModels === 'function') {
580
+ await loadModels();
581
+ }
582
+ } catch (e) {
583
+ btn.textContent = 'Error';
584
+ showErrorBanner(`Failed to install model: ${e.message}`);
585
+ }
586
+ };
587
+ tdBtn.appendChild(btn);
588
+ tr.appendChild(tdName);
589
+ tr.appendChild(tdBtn);
590
+ tbody.appendChild(tr);
591
+ });
592
+ }
593
+ }
594
+
595
+ // Model Management Tab Logic
596
+ async function refreshModelMgmtUI() {
597
+ // Get installed models from /api/v1/models
598
+ let installed = [];
599
+ try {
600
+ const data = await httpJson(getServerBaseUrl() + '/api/v1/models');
601
+ if (data.data && Array.isArray(data.data)) {
602
+ installed = data.data.map(m => m.id || m.name || m);
603
+ }
604
+ } catch (e) {
605
+ showErrorBanner(`Error loading models: ${e.message}`);
606
+ }
607
+
608
+ // Update the global installedModels set
609
+ installedModels.clear();
610
+ installed.forEach(modelId => {
611
+ installedModels.add(modelId);
612
+ });
613
+
614
+ // All models from server_models.json (window.SERVER_MODELS)
615
+ const allModels = window.SERVER_MODELS || {};
616
+
617
+ // Separate hot models and regular suggested models not installed
618
+ const hotModels = [];
619
+ const regularSuggested = [];
620
+
621
+ Object.keys(allModels).forEach(k => {
622
+ if (allModels[k].suggested && !installed.includes(k)) {
623
+ const modelData = allModels[k];
624
+ const hasHotLabel = modelData.labels && modelData.labels.some(label =>
625
+ label.toLowerCase() === 'hot'
626
+ );
627
+
628
+ if (hasHotLabel) {
629
+ hotModels.push(k);
630
+ } else {
631
+ regularSuggested.push(k);
632
+ }
633
+ }
634
+ });
635
+
636
+ // Render installed models as a table (two columns, second is invisible)
637
+ const installedTbody = document.getElementById('installed-models-tbody');
638
+ if (installedTbody) {
639
+ installedTbody.innerHTML = '';
640
+ installed.forEach(function(mid) {
641
+ var tr = document.createElement('tr');
642
+ var tdName = document.createElement('td');
643
+
644
+ tdName.appendChild(createModelNameWithLabels(mid, allModels));
645
+ tdName.style.paddingRight = '1em';
646
+ tdName.style.verticalAlign = 'middle';
647
+
648
+ var tdBtn = document.createElement('td');
649
+ tdBtn.style.width = '1%';
650
+ tdBtn.style.verticalAlign = 'middle';
651
+ const btn = document.createElement('button');
652
+ btn.textContent = '−';
653
+ btn.title = 'Delete model';
654
+ btn.style.cursor = 'pointer';
655
+ btn.onclick = async function() {
656
+ if (!confirm(`Are you sure you want to delete the model "${mid}"?`)) {
657
+ return;
658
+ }
659
+ btn.disabled = true;
660
+ btn.textContent = 'Deleting...';
661
+ btn.style.backgroundColor = '#888';
662
+ try {
663
+ await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
664
+ method: 'POST',
665
+ headers: { 'Content-Type': 'application/json' },
666
+ body: JSON.stringify({ model_name: mid })
667
+ });
668
+ await refreshModelMgmtUI();
669
+ // Update chat dropdown too if loadModels function exists
670
+ if (typeof loadModels === 'function') {
671
+ await loadModels();
672
+ }
673
+ } catch (e) {
674
+ btn.textContent = 'Error';
675
+ btn.disabled = false;
676
+ showErrorBanner(`Failed to delete model: ${e.message}`);
677
+ }
678
+ };
679
+ tdBtn.appendChild(btn);
680
+
681
+ tr.appendChild(tdName);
682
+ tr.appendChild(tdBtn);
683
+ installedTbody.appendChild(tr);
684
+ });
685
+ }
686
+
687
+ // Render hot models and suggested models using the helper function
688
+ const hotTbody = document.getElementById('hot-models-tbody');
689
+ const suggestedTbody = document.getElementById('suggested-models-tbody');
690
+
691
+ if (hotTbody) {
692
+ renderModelTable(hotTbody, hotModels, allModels, "Nice, you've already installed all these models!");
693
+ }
694
+ if (suggestedTbody) {
695
+ renderModelTable(suggestedTbody, regularSuggested, allModels, "Nice, you've already installed all these models!");
696
+ }
697
+
698
+ // Refresh model dropdown in chat after updating installed models
699
+ if (window.initializeModelDropdown) {
700
+ window.initializeModelDropdown();
701
+ }
702
+ }
703
+
704
+ // Make refreshModelMgmtUI globally accessible
705
+ window.refreshModelMgmtUI = refreshModelMgmtUI;
706
+
707
+ // Display-only version that uses already-fetched installedModels data
708
+ function refreshModelMgmtUIDisplay() {
709
+ // Use the already-populated installedModels set
710
+ const installed = Array.from(installedModels);
711
+
712
+ // All models from server_models.json (window.SERVER_MODELS)
713
+ const allModels = window.SERVER_MODELS || {};
714
+
715
+ // Separate hot models and regular suggested models not installed
716
+ const hotModels = [];
717
+ const regularSuggested = [];
718
+
719
+ Object.keys(allModels).forEach(k => {
720
+ if (allModels[k].suggested && !installed.includes(k)) {
721
+ if (allModels[k].labels && allModels[k].labels.some(label => label.toLowerCase() === 'hot')) {
722
+ hotModels.push(k);
723
+ } else {
724
+ regularSuggested.push(k);
725
+ }
726
+ }
727
+ });
728
+
729
+ // Render installed models as a table (two columns, second is invisible)
730
+ const installedTbody = document.getElementById('installed-models-tbody');
731
+ if (installedTbody) {
732
+ installedTbody.innerHTML = '';
733
+ installed.forEach(function(mid) {
734
+ var tr = document.createElement('tr');
735
+ var tdName = document.createElement('td');
736
+
737
+ tdName.appendChild(createModelNameWithLabels(mid, allModels));
738
+ tdName.style.paddingRight = '1em';
739
+ tdName.style.verticalAlign = 'middle';
740
+
741
+ var tdBtn = document.createElement('td');
742
+ tdBtn.style.width = '1%';
743
+ tdBtn.style.verticalAlign = 'middle';
744
+ const btn = document.createElement('button');
745
+ btn.textContent = '−';
746
+ btn.className = 'btn-remove-model';
747
+ btn.style.minWidth = '24px';
748
+ btn.style.padding = '2px 8px';
749
+ btn.style.fontSize = '16px';
750
+ btn.style.lineHeight = '1';
751
+ btn.style.border = '1px solid #ddd';
752
+ btn.style.backgroundColor = '#f8f9fa';
753
+ btn.style.cursor = 'pointer';
754
+ btn.style.borderRadius = '4px';
755
+ btn.title = 'Remove this model';
756
+ btn.onclick = async function() {
757
+ if (confirm(`Are you sure you want to remove the model "${mid}"?`)) {
758
+ try {
759
+ await httpRequest(getServerBaseUrl() + '/api/v1/delete', {
760
+ method: 'POST',
761
+ headers: { 'Content-Type': 'application/json' },
762
+ body: JSON.stringify({ model_name: mid })
763
+ });
764
+ await refreshModelMgmtUI();
765
+ } catch (error) {
766
+ console.error('Error removing model:', error);
767
+ showErrorBanner('Failed to remove model: ' + error.message);
768
+ }
769
+ }
770
+ };
771
+ tdBtn.appendChild(btn);
772
+ tr.appendChild(tdName);
773
+ tr.appendChild(tdBtn);
774
+ installedTbody.appendChild(tr);
775
+ });
776
+ }
777
+
778
+ // Render hot models and suggested models using the helper function
779
+ const hotTbody = document.getElementById('hot-models-tbody');
780
+ const suggestedTbody = document.getElementById('suggested-models-tbody');
781
+
782
+ if (hotTbody) {
783
+ renderModelTable(hotTbody, hotModels, allModels, "Nice, you've already installed all these models!");
784
+ }
785
+ if (suggestedTbody) {
786
+ renderModelTable(suggestedTbody, regularSuggested, allModels, "Nice, you've already installed all these models!");
787
+ }
788
+
789
+ // Refresh model dropdown in chat after updating installed models
790
+ if (window.initializeModelDropdown) {
791
+ window.initializeModelDropdown();
792
+ }
793
+ }
794
+
795
+ // Set up the register model form
796
+ function setupRegisterModelForm() {
797
+ const registerForm = document.getElementById('register-model-form');
798
+ const registerStatus = document.getElementById('register-model-status');
799
+
800
+ if (registerForm && registerStatus) {
801
+ registerForm.onsubmit = async function(e) {
802
+ e.preventDefault();
803
+ registerStatus.textContent = '';
804
+ let name = document.getElementById('register-model-name').value.trim();
805
+
806
+ // Always prepend 'user.' if not already present
807
+ if (!name.startsWith('user.')) {
808
+ name = 'user.' + name;
809
+ }
810
+
811
+ const checkpoint = document.getElementById('register-checkpoint').value.trim();
812
+ const recipe = document.getElementById('register-recipe').value;
813
+ const reasoning = document.getElementById('register-reasoning').checked;
814
+ const mmproj = document.getElementById('register-mmproj').value.trim();
815
+
816
+ if (!name || !recipe) {
817
+ return;
818
+ }
819
+
820
+ const payload = { model_name: name, recipe, reasoning };
821
+ if (checkpoint) payload.checkpoint = checkpoint;
822
+ if (mmproj) payload.mmproj = mmproj;
823
+
824
+ const btn = document.getElementById('register-submit');
825
+ btn.disabled = true;
826
+ btn.textContent = 'Installing...';
827
+
828
+ try {
829
+ await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
830
+ method: 'POST',
831
+ headers: { 'Content-Type': 'application/json' },
832
+ body: JSON.stringify(payload)
833
+ });
834
+ registerStatus.textContent = 'Model installed!';
835
+ registerStatus.style.color = '#27ae60';
836
+ registerStatus.className = 'register-status success';
837
+ registerForm.reset();
838
+ await refreshModelMgmtUI();
839
+ // Update chat dropdown too if loadModels function exists
840
+ if (typeof loadModels === 'function') {
841
+ await loadModels();
842
+ }
843
+ } catch (e) {
844
+ registerStatus.textContent = e.message + ' See the Lemonade Server log for details.';
845
+ registerStatus.style.color = '#dc3545';
846
+ registerStatus.className = 'register-status error';
847
+ showErrorBanner(`Model install failed: ${e.message}`);
848
+ }
849
+
850
+ btn.disabled = false;
851
+ btn.textContent = 'Install';
852
+ refreshModelMgmtUI();
853
+ };
854
+ }
855
+ }
856
+
857
+ // Make functions globally available for HTML onclick handlers and other components
858
+ window.toggleCategory = toggleCategory;
859
+ window.selectRecipe = selectRecipe;
860
+ window.selectLabel = selectLabel;
861
+ window.showAddModelForm = showAddModelForm;
862
+ window.unloadModel = unloadModel;
863
+ window.installModel = installModel;
864
+ window.loadModel = loadModel;
865
+ window.deleteModel = deleteModel;