lemonade-sdk 8.1.11__py3-none-any.whl → 8.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of lemonade-sdk might be problematic. Click here for more details.

Files changed (38) hide show
  1. lemonade/cache.py +6 -1
  2. lemonade/common/status.py +4 -4
  3. lemonade/common/system_info.py +0 -26
  4. lemonade/tools/accuracy.py +143 -48
  5. lemonade/tools/adapter.py +6 -1
  6. lemonade/tools/bench.py +26 -8
  7. lemonade/tools/flm/utils.py +70 -22
  8. lemonade/tools/huggingface/bench.py +6 -1
  9. lemonade/tools/llamacpp/bench.py +146 -27
  10. lemonade/tools/llamacpp/load.py +30 -2
  11. lemonade/tools/llamacpp/utils.py +317 -21
  12. lemonade/tools/oga/bench.py +5 -26
  13. lemonade/tools/oga/load.py +49 -123
  14. lemonade/tools/oga/migration.py +403 -0
  15. lemonade/tools/report/table.py +76 -8
  16. lemonade/tools/server/flm.py +2 -6
  17. lemonade/tools/server/llamacpp.py +43 -2
  18. lemonade/tools/server/serve.py +354 -18
  19. lemonade/tools/server/static/js/chat.js +15 -77
  20. lemonade/tools/server/static/js/model-settings.js +24 -3
  21. lemonade/tools/server/static/js/models.js +440 -37
  22. lemonade/tools/server/static/js/shared.js +61 -8
  23. lemonade/tools/server/static/logs.html +157 -13
  24. lemonade/tools/server/static/styles.css +204 -0
  25. lemonade/tools/server/static/webapp.html +39 -1
  26. lemonade/version.py +1 -1
  27. lemonade_install/install.py +33 -579
  28. {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/METADATA +6 -4
  29. {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/RECORD +38 -37
  30. lemonade_server/cli.py +10 -0
  31. lemonade_server/model_manager.py +172 -11
  32. lemonade_server/pydantic_models.py +3 -0
  33. lemonade_server/server_models.json +102 -66
  34. {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/WHEEL +0 -0
  35. {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/entry_points.txt +0 -0
  36. {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/licenses/LICENSE +0 -0
  37. {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/licenses/NOTICE.md +0 -0
  38. {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.2.dist-info}/top_level.txt +0 -0
@@ -28,9 +28,11 @@ function loadModelSettings() {
28
28
  const topKInput = document.getElementById('setting-top-k');
29
29
  const topPInput = document.getElementById('setting-top-p');
30
30
  const repeatInput = document.getElementById('setting-repeat-penalty');
31
+ const thinkingCheckbox = document.getElementById('enable-thinking');
32
+
31
33
 
32
34
  // Check if DOM elements exist
33
- if (!tempInput || !topKInput || !topPInput || !repeatInput) {
35
+ if (!tempInput || !topKInput || !topPInput || !repeatInput || !thinkingCheckbox) {
34
36
  return;
35
37
  }
36
38
 
@@ -47,11 +49,18 @@ function loadModelSettings() {
47
49
  if (modelSettings.repeat_penalty !== undefined) {
48
50
  repeatInput.value = modelSettings.repeat_penalty;
49
51
  }
52
+ if (modelSettings.enable_thinking !== undefined) {
53
+ thinkingCheckbox.checked = modelSettings.enable_thinking;
54
+
55
+ } else {
56
+ thinkingCheckbox.checked = true; // default to enabled
57
+
58
+ }
50
59
  }
51
60
 
52
61
  // Auto-save model settings whenever inputs change
53
62
  function setupAutoSaveSettings() {
54
- const inputs = ['setting-temperature', 'setting-top-k', 'setting-top-p', 'setting-repeat-penalty'];
63
+ const inputs = ['setting-temperature', 'setting-top-k', 'setting-top-p', 'setting-repeat-penalty', 'enable-thinking'];
55
64
 
56
65
  inputs.forEach(inputId => {
57
66
  const input = document.getElementById(inputId);
@@ -63,6 +72,12 @@ function setupAutoSaveSettings() {
63
72
  updateModelSettings();
64
73
  });
65
74
  }
75
+ const thinkingCheckbox = document.getElementById('enable-thinking');
76
+ if (thinkingCheckbox) {
77
+ thinkingCheckbox.addEventListener('change', function() {
78
+ updateModelSettings();
79
+ });
80
+ }
66
81
  });
67
82
  }
68
83
 
@@ -72,9 +87,10 @@ function updateModelSettings() {
72
87
  const topKInput = document.getElementById('setting-top-k');
73
88
  const topPInput = document.getElementById('setting-top-p');
74
89
  const repeatInput = document.getElementById('setting-repeat-penalty');
90
+ const thinkingCheckbox = document.getElementById('enable-thinking');
75
91
 
76
92
  // Check if DOM elements exist (might not be available if DOM isn't ready)
77
- if (!tempInput || !topKInput || !topPInput || !repeatInput) {
93
+ if (!tempInput || !topKInput || !topPInput || !repeatInput || !thinkingCheckbox) {
78
94
  return;
79
95
  }
80
96
 
@@ -93,6 +109,7 @@ function updateModelSettings() {
93
109
  if (repeatInput.value && repeatInput.value.trim() !== '') {
94
110
  modelSettings.repeat_penalty = parseFloat(repeatInput.value);
95
111
  }
112
+ modelSettings.enable_thinking = thinkingCheckbox.checked;
96
113
 
97
114
  // Save to localStorage
98
115
  localStorage.setItem('lemonade_model_settings', JSON.stringify(modelSettings));
@@ -107,6 +124,7 @@ function resetModelSettings() {
107
124
  document.getElementById('setting-top-k').value = '';
108
125
  document.getElementById('setting-top-p').value = '';
109
126
  document.getElementById('setting-repeat-penalty').value = '';
127
+ document.getElementById('enable-thinking').checked = true;
110
128
 
111
129
  localStorage.removeItem('lemonade_model_settings');
112
130
  }
@@ -135,6 +153,9 @@ function getCurrentModelSettings() {
135
153
  if (modelSettings.repeat_penalty !== undefined) {
136
154
  currentSettings.repeat_penalty = modelSettings.repeat_penalty;
137
155
  }
156
+ if (modelSettings.enable_thinking !== undefined) {
157
+ currentSettings.enable_thinking = modelSettings.enable_thinking;
158
+ }
138
159
 
139
160
  console.log('getCurrentModelSettings returning:', currentSettings);
140
161
  return currentSettings;
@@ -3,9 +3,11 @@
3
3
  // State variables for model management
4
4
  let currentLoadedModel = null;
5
5
  let installedModels = new Set(); // Track which models are actually installed
6
+ let activeOperations = new Set(); // Track models currently being downloaded or loaded
6
7
 
7
- // Make installedModels accessible globally for the chat dropdown
8
+ // Make installedModels and activeOperations accessible globally
8
9
  window.installedModels = installedModels;
10
+ window.activeOperations = activeOperations;
9
11
  let currentCategory = 'hot';
10
12
  let currentFilter = null;
11
13
 
@@ -166,6 +168,29 @@ async function unloadModel() {
166
168
 
167
169
  // === Model Browser Management ===
168
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
+
169
194
  // Toggle category in model browser (only for Hot Models now)
170
195
  function toggleCategory(categoryName) {
171
196
  const header = document.querySelector(`[data-category="${categoryName}"] .category-header`);
@@ -283,7 +308,7 @@ function displayHotModels() {
283
308
  modelList.innerHTML = '';
284
309
 
285
310
  Object.entries(allModels).forEach(([modelId, modelData]) => {
286
- if (modelData.labels && modelData.labels.includes('hot')) {
311
+ if (modelData.labels && modelData.labels.includes('hot') && (modelData.suggested || installedModels.has(modelId))) {
287
312
  createModelItem(modelId, modelData, modelList);
288
313
  }
289
314
  });
@@ -317,7 +342,7 @@ function displayModelsByRecipe(recipe) {
317
342
  }
318
343
 
319
344
  Object.entries(allModels).forEach(([modelId, modelData]) => {
320
- if (modelData.recipe === recipe) {
345
+ if (modelData.recipe === recipe && (modelData.suggested || installedModels.has(modelId))) {
321
346
  createModelItem(modelId, modelData, modelList);
322
347
  }
323
348
  });
@@ -341,7 +366,7 @@ function displayModelsByLabel(label) {
341
366
  if (modelId.startsWith('user.')) {
342
367
  createModelItem(modelId, modelData, modelList);
343
368
  }
344
- } else if (modelData.labels && modelData.labels.includes(label)) {
369
+ } else if (modelData.labels && modelData.labels.includes(label) && (modelData.suggested || installedModels.has(modelId))) {
345
370
  createModelItem(modelId, modelData, modelList);
346
371
  }
347
372
  });
@@ -443,6 +468,9 @@ async function installModel(modelId) {
443
468
  installBtn.textContent = '⏳';
444
469
  }
445
470
 
471
+ // Track this download as active
472
+ activeOperations.add(modelId);
473
+
446
474
  try {
447
475
  const modelData = window.SERVER_MODELS[modelId];
448
476
  await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
@@ -451,6 +479,9 @@ async function installModel(modelId) {
451
479
  body: JSON.stringify({ model_name: modelId, ...modelData })
452
480
  });
453
481
 
482
+ // Download complete - remove from active operations
483
+ activeOperations.delete(modelId);
484
+
454
485
  // Refresh installed models and model status
455
486
  await fetchInstalledModels();
456
487
  await updateModelStatusIndicator();
@@ -468,6 +499,9 @@ async function installModel(modelId) {
468
499
  console.error('Error installing model:', error);
469
500
  showErrorBanner('Failed to install model: ' + error.message);
470
501
 
502
+ // Remove from active operations on error too
503
+ activeOperations.delete(modelId);
504
+
471
505
  // Reset button state on error
472
506
  if (installBtn) {
473
507
  installBtn.disabled = false;
@@ -505,7 +539,11 @@ async function deleteModel(modelId) {
505
539
  headers: { 'Content-Type': 'application/json' },
506
540
  body: JSON.stringify({ model_name: modelId })
507
541
  });
508
-
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
+ }
509
547
  // Refresh installed models and model status
510
548
  await fetchInstalledModels();
511
549
  await updateModelStatusIndicator();
@@ -597,29 +635,11 @@ document.addEventListener('DOMContentLoaded', async function() {
597
635
  unloadBtn.onclick = unloadModel;
598
636
  }
599
637
 
600
- const modelSelect = document.getElementById('model-select');
601
- if (modelSelect) {
602
- modelSelect.addEventListener('change', async function() {
603
- const modelId = this.value;
604
- if (modelId) {
605
- await loadModelStandardized(modelId, {
606
- onSuccess: (loadedModelId) => {
607
- console.log(`Model ${loadedModelId} loaded successfully`);
608
- },
609
- onError: (error, failedModelId) => {
610
- console.error(`Failed to load model ${failedModelId}:`, error);
611
- showErrorBanner('Failed to load model: ' + error.message);
612
- }
613
- });
614
- }
615
- });
616
- }
617
-
618
638
  // Initial fetch of model data - this will populate installedModels
619
639
  await updateModelStatusIndicator();
620
640
 
621
- // Set up periodic refresh of model status
622
- setInterval(updateModelStatusIndicator, 1000); // Check every 1 seconds
641
+ // Update category visibility on initial load
642
+ updateCategoryVisibility();
623
643
 
624
644
  // Initialize model browser with hot models
625
645
  displayHotModels();
@@ -635,6 +655,46 @@ document.addEventListener('DOMContentLoaded', async function() {
635
655
 
636
656
  // Set up register model form
637
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();
638
698
  });
639
699
 
640
700
  // Toggle Add Model form
@@ -675,12 +735,20 @@ function renderModelTable(tbody, models, allModels, emptyMessage) {
675
735
  btn.disabled = true;
676
736
  btn.textContent = '⏳';
677
737
  btn.classList.add('installing-btn');
738
+
739
+ // Track this download as active
740
+ activeOperations.add(mid);
741
+
678
742
  try {
679
743
  await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
680
744
  method: 'POST',
681
745
  headers: { 'Content-Type': 'application/json' },
682
746
  body: JSON.stringify({ model_name: mid })
683
747
  });
748
+
749
+ // Download complete - remove from active operations
750
+ activeOperations.delete(mid);
751
+
684
752
  await refreshModelMgmtUI();
685
753
  // Update chat dropdown too if loadModels function exists
686
754
  if (typeof loadModels === 'function') {
@@ -690,6 +758,9 @@ function renderModelTable(tbody, models, allModels, emptyMessage) {
690
758
  btn.textContent = 'Error';
691
759
  btn.disabled = false;
692
760
  showErrorBanner(`Failed to install model: ${e.message}`);
761
+
762
+ // Remove from active operations on error too
763
+ activeOperations.delete(mid);
693
764
  }
694
765
  };
695
766
  tdBtn.appendChild(btn);
@@ -928,7 +999,17 @@ function setupRegisterModelForm() {
928
999
  if (!name.startsWith('user.')) {
929
1000
  name = 'user.' + name;
930
1001
  }
931
-
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
+
932
1013
  const checkpoint = document.getElementById('register-checkpoint').value.trim();
933
1014
  const recipe = document.getElementById('register-recipe').value;
934
1015
  const reasoning = document.getElementById('register-reasoning').checked;
@@ -939,24 +1020,70 @@ function setupRegisterModelForm() {
939
1020
  return;
940
1021
  }
941
1022
 
942
- const payload = { model_name: name, recipe, reasoning, vision };
943
- if (checkpoint) payload.checkpoint = checkpoint;
944
- if (mmproj) payload.mmproj = mmproj;
945
-
946
1023
  const btn = document.getElementById('register-submit');
947
1024
  btn.disabled = true;
948
1025
  btn.textContent = 'Installing...';
949
1026
 
950
1027
  try {
951
- await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
952
- method: 'POST',
953
- headers: { 'Content-Type': 'application/json' },
954
- body: JSON.stringify(payload)
955
- });
1028
+ if (isLocalModel && selectedModelFiles) {
1029
+ if (recipe === 'llamacpp' && !Array.from(selectedModelFiles).some(file => file.name.toLowerCase().endsWith('.gguf'))) {
1030
+ throw new Error('No .gguf files found in the selected folder for llamacpp');
1031
+ }
1032
+
1033
+ const formData = new FormData();
1034
+ formData.append('model_name', name);
1035
+ formData.append('checkpoint', checkpoint);
1036
+ formData.append('recipe', recipe);
1037
+ formData.append('reasoning', reasoning);
1038
+ formData.append('vision', vision);
1039
+ if (mmproj) formData.append('mmproj', mmproj);
1040
+ Array.from(selectedModelFiles).forEach(file => {
1041
+ formData.append('model_files', file, file.webkitRelativePath);
1042
+ });
1043
+
1044
+ await httpRequest(getServerBaseUrl() + '/api/v1/add-local-model', {
1045
+ method: 'POST',
1046
+ body: formData
1047
+ });
1048
+ }
1049
+ else {
1050
+ if (!checkpoint) {
1051
+ throw new Error('Checkpoint is required for remote models');
1052
+ }
1053
+ const payload = { model_name: name, recipe, reasoning, vision };
1054
+ if (checkpoint) payload.checkpoint = checkpoint;
1055
+ if (mmproj) payload.mmproj = mmproj;
1056
+
1057
+ await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
1058
+ method: 'POST',
1059
+ headers: { 'Content-Type': 'application/json' },
1060
+ body: JSON.stringify(payload)
1061
+ });
1062
+ }
1063
+
956
1064
  registerStatus.textContent = 'Model installed!';
957
- registerStatus.style.color = '#27ae60';
1065
+ registerStatus.style.color = '#0eaf51ff';
958
1066
  registerStatus.className = 'register-status success';
1067
+
1068
+ // Add custom model to SERVER_MODELS so it appears in the UI without having to do a manual refresh
1069
+ if (name.startsWith('user.')) {
1070
+ const labels = ['custom'];
1071
+ if (vision) labels.push('vision');
1072
+ if (reasoning) labels.push('reasoning');
1073
+
1074
+ window.SERVER_MODELS[name] = {
1075
+ recipe: recipe,
1076
+ labels: labels
1077
+ };
1078
+ if (checkpoint) window.SERVER_MODELS[name].checkpoint = checkpoint;
1079
+ if (mmproj) window.SERVER_MODELS[name].mmproj = mmproj;
1080
+ }
1081
+
959
1082
  registerForm.reset();
1083
+ isLocalModel = false;
1084
+ selectedModelFiles = null;
1085
+ document.getElementById('folder-input').value = '';
1086
+
960
1087
  await refreshModelMgmtUI();
961
1088
  // Update chat dropdown too if loadModels function exists
962
1089
  if (typeof loadModels === 'function') {
@@ -975,6 +1102,278 @@ function setupRegisterModelForm() {
975
1102
  };
976
1103
  }
977
1104
  }
1105
+ let isLocalModel = false;
1106
+ let selectedModelFiles = null;
1107
+ // Helper function to find mmproj file in selected folder
1108
+ function findMmprojFile(files) {
1109
+ for (let i = 0; i < files.length; i++) {
1110
+ const file = files[i];
1111
+ const fileName = file.name.toLowerCase();
1112
+ const relativePath = file.webkitRelativePath;
1113
+
1114
+ // Check if file contains 'mmproj' and has .gguf extension
1115
+ if (fileName.includes('mmproj') && fileName.endsWith('.gguf')) {
1116
+ // Return just the filename (last part of the path)
1117
+ return relativePath.split('/').pop();
1118
+ }
1119
+ }
1120
+ return null;
1121
+ }
1122
+
1123
+ // Helper function to find all non-mmproj GGUF files in selected folder
1124
+ function findGgufFiles(files) {
1125
+ const ggufFiles = [];
1126
+ for (let i = 0; i < files.length; i++) {
1127
+ const file = files[i];
1128
+ const fileName = file.name.toLowerCase();
1129
+ const relativePath = file.webkitRelativePath;
1130
+
1131
+ // Check if file has .gguf extension but is NOT an mmproj file
1132
+ if (fileName.endsWith('.gguf') && !fileName.includes('mmproj')) {
1133
+ // Store just the filename (last part of the path)
1134
+ ggufFiles.push(relativePath.split('/').pop());
1135
+ }
1136
+ }
1137
+ return ggufFiles;
1138
+ }
1139
+
1140
+ // Helper function to check GGUF files and show appropriate banners
1141
+ function checkGgufFilesAndShowBanner(files) {
1142
+ const recipeSelect = document.getElementById('register-recipe');
1143
+
1144
+ // Only check if llamacpp is selected
1145
+ if (!recipeSelect || recipeSelect.value !== 'llamacpp') {
1146
+ return;
1147
+ }
1148
+
1149
+ const mmprojFile = findMmprojFile(files);
1150
+ const ggufFiles = findGgufFiles(files);
1151
+
1152
+ // Hide any existing banners first
1153
+ hideErrorBanner();
1154
+
1155
+ if (ggufFiles.length > 1) {
1156
+ // Multiple GGUF files detected
1157
+ const folderPath = files[0].webkitRelativePath.split('/')[0];
1158
+ let bannerMsg = `More than one variant detected. Please clarify them at the end of the checkpoint name like:\n<folder_name>:<variant>\nExample: ${folderPath}:${ggufFiles[0]}`;
1159
+
1160
+ if (mmprojFile) {
1161
+ bannerMsg += `\n\nDon't forget to enter the mmproj file name and check the 'vision' checkbox if it is a vision model.`;
1162
+ }
1163
+
1164
+ showBanner(bannerMsg, 'warning');
1165
+ } else if (mmprojFile) {
1166
+ // MMproj detected
1167
+ showBanner("MMproj detected and populated. Please validate the file name and check the 'vision' checkbox if it is a vision model.", 'success');
1168
+ }
1169
+ }
1170
+ // Helper function to auto-fill mmproj field if llamacpp is selected
1171
+ function autoFillMmproj() {
1172
+ const recipeSelect = document.getElementById('register-recipe');
1173
+ const mmprojInput = document.getElementById('register-mmproj');
1174
+
1175
+ if (recipeSelect && mmprojInput && isLocalModel && selectedModelFiles) {
1176
+ const selectedRecipe = recipeSelect.value;
1177
+
1178
+ if (selectedRecipe === 'llamacpp') {
1179
+ const mmprojFile = findMmprojFile(selectedModelFiles);
1180
+ if (mmprojFile) {
1181
+ mmprojInput.value = mmprojFile;
1182
+ }
1183
+
1184
+ // Check GGUF files and show appropriate banner
1185
+ checkGgufFilesAndShowBanner(selectedModelFiles);
1186
+ } else {
1187
+ // Hide banners if not llamacpp
1188
+ hideErrorBanner();
1189
+ }
1190
+ }
1191
+ }
1192
+ function setupFolderSelection() {
1193
+ const selectFolderBtn = document.getElementById('select-folder-btn');
1194
+ const folderInput = document.getElementById('folder-input');
1195
+ const checkpointInput = document.getElementById('register-checkpoint');
1196
+ const recipeSelect = document.getElementById('register-recipe');
1197
+
1198
+ if (selectFolderBtn && folderInput && checkpointInput) {
1199
+ selectFolderBtn.addEventListener('click', () => {
1200
+ folderInput.click();
1201
+ });
1202
+
1203
+ folderInput.addEventListener('change', (event) => {
1204
+ const files = event.target.files;
1205
+ if (files.length > 0) {
1206
+ const firstFile = files[0];
1207
+ const folderPath = firstFile.webkitRelativePath.split('/')[0];
1208
+ checkpointInput.value = folderPath;
1209
+ isLocalModel = true;
1210
+ selectedModelFiles = files;
1211
+
1212
+ // Auto-fill mmproj if llamacpp is already selected
1213
+ autoFillMmproj();
1214
+ }
1215
+ else {
1216
+ isLocalModel = false;
1217
+ selectedModelFiles = null;
1218
+ checkpointInput.value = '';
1219
+ hideErrorBanner();
1220
+ }
1221
+ });
1222
+
1223
+ // Add listener to recipe dropdown to auto-fill mmproj when changed to llamacpp
1224
+ if (recipeSelect) {
1225
+ recipeSelect.addEventListener('change', () => {
1226
+ autoFillMmproj();
1227
+ });
1228
+ }
1229
+ }
1230
+ }
1231
+ // === Migration/Cleanup Functions ===
1232
+
1233
+ // Store incompatible models data globally
1234
+ let incompatibleModelsData = null;
1235
+
1236
+ // Check for incompatible models on page load
1237
+ async function checkIncompatibleModels() {
1238
+ try {
1239
+ const response = await httpJson(getServerBaseUrl() + '/api/v1/migration/incompatible-models');
1240
+ incompatibleModelsData = response;
1241
+
1242
+ if (response.count > 0) {
1243
+ showMigrationBanner(response.count, response.total_size);
1244
+ }
1245
+ } catch (error) {
1246
+ console.error('Error checking for incompatible models:', error);
1247
+ }
1248
+ }
1249
+
1250
+ // Show migration banner
1251
+ function showMigrationBanner(count, totalSize) {
1252
+ const banner = document.getElementById('migration-banner');
1253
+ const msg = document.getElementById('migration-banner-msg');
1254
+
1255
+ const sizeGB = (totalSize / (1024 * 1024 * 1024)).toFixed(1);
1256
+ msg.textContent = `Found ${count} incompatible RyzenAI model${count > 1 ? 's' : ''} (${sizeGB} GB). Clean up to free disk space.`;
1257
+ banner.style.display = 'flex';
1258
+ }
1259
+
1260
+ // Hide migration banner
1261
+ function hideMigrationBanner() {
1262
+ const banner = document.getElementById('migration-banner');
1263
+ banner.style.display = 'none';
1264
+ }
1265
+
1266
+ // Show migration modal with model list
1267
+ function showMigrationModal() {
1268
+ if (!incompatibleModelsData || incompatibleModelsData.count === 0) {
1269
+ return;
1270
+ }
1271
+
1272
+ const modal = document.getElementById('migration-modal');
1273
+ const modelList = document.getElementById('migration-model-list');
1274
+ const totalSize = document.getElementById('migration-total-size');
1275
+
1276
+ // Populate model list
1277
+ modelList.innerHTML = '';
1278
+ incompatibleModelsData.models.forEach(model => {
1279
+ const item = document.createElement('div');
1280
+ item.className = 'migration-model-item';
1281
+
1282
+ const nameSpan = document.createElement('span');
1283
+ nameSpan.className = 'migration-model-name';
1284
+ nameSpan.textContent = model.name;
1285
+
1286
+ const sizeSpan = document.createElement('span');
1287
+ sizeSpan.className = 'migration-model-size';
1288
+ sizeSpan.textContent = model.size_formatted;
1289
+
1290
+ item.appendChild(nameSpan);
1291
+ item.appendChild(sizeSpan);
1292
+ modelList.appendChild(item);
1293
+ });
1294
+
1295
+ // Set total size
1296
+ const sizeGB = (incompatibleModelsData.total_size / (1024 * 1024 * 1024)).toFixed(1);
1297
+ totalSize.textContent = `${sizeGB} GB`;
1298
+
1299
+ modal.style.display = 'flex';
1300
+ }
1301
+
1302
+ // Hide migration modal
1303
+ function hideMigrationModal() {
1304
+ const modal = document.getElementById('migration-modal');
1305
+ modal.style.display = 'none';
1306
+ }
1307
+
1308
+ // Delete incompatible models
1309
+ async function deleteIncompatibleModels() {
1310
+ if (!incompatibleModelsData || incompatibleModelsData.count === 0) {
1311
+ return;
1312
+ }
1313
+
1314
+ const modelPaths = incompatibleModelsData.models.map(m => m.path);
1315
+
1316
+ try {
1317
+ // Disable buttons during deletion
1318
+ const deleteBtn = document.querySelector('.delete-btn');
1319
+ const cancelBtn = document.querySelector('.cancel-btn');
1320
+ deleteBtn.disabled = true;
1321
+ cancelBtn.disabled = true;
1322
+ deleteBtn.textContent = 'Deleting...';
1323
+
1324
+ const response = await httpRequest(getServerBaseUrl() + '/api/v1/migration/cleanup', {
1325
+ method: 'POST',
1326
+ headers: { 'Content-Type': 'application/json' },
1327
+ body: JSON.stringify({ model_paths: modelPaths })
1328
+ });
1329
+
1330
+ const result = await response.json();
1331
+
1332
+ // Close modal
1333
+ hideMigrationModal();
1334
+
1335
+ // Hide banner
1336
+ hideMigrationBanner();
1337
+
1338
+ // Show success message
1339
+ showSuccessMessage(`Successfully deleted ${result.success_count} model${result.success_count > 1 ? 's' : ''}, freed ${result.freed_size_formatted}`);
1340
+
1341
+ // Clear cached data
1342
+ incompatibleModelsData = null;
1343
+
1344
+ } catch (error) {
1345
+ console.error('Error deleting incompatible models:', error);
1346
+ showErrorBanner('Failed to delete models: ' + error.message);
1347
+
1348
+ // Re-enable buttons
1349
+ const deleteBtn = document.querySelector('.delete-btn');
1350
+ const cancelBtn = document.querySelector('.cancel-btn');
1351
+ deleteBtn.disabled = false;
1352
+ cancelBtn.disabled = false;
1353
+ deleteBtn.textContent = 'Delete All';
1354
+ }
1355
+ }
1356
+
1357
+ // Show success message (reuse error banner with green color)
1358
+ function showSuccessMessage(message) {
1359
+ const banner = document.getElementById('error-banner');
1360
+ const msg = document.getElementById('error-banner-msg');
1361
+ msg.textContent = message;
1362
+ banner.style.backgroundColor = '#2d7f47';
1363
+ banner.style.display = 'flex';
1364
+
1365
+ // Auto-hide after 5 seconds
1366
+ setTimeout(() => {
1367
+ banner.style.display = 'none';
1368
+ banner.style.backgroundColor = ''; // Reset to default
1369
+ }, 5000);
1370
+ }
1371
+
1372
+ // Check for incompatible models when page loads
1373
+ document.addEventListener('DOMContentLoaded', function() {
1374
+ // Run check after a short delay to let the page load
1375
+ setTimeout(checkIncompatibleModels, 1000);
1376
+ });
978
1377
 
979
1378
  // Make functions globally available for HTML onclick handlers and other components
980
1379
  window.toggleCategory = toggleCategory;
@@ -983,4 +1382,8 @@ window.selectLabel = selectLabel;
983
1382
  window.showAddModelForm = showAddModelForm;
984
1383
  window.unloadModel = unloadModel;
985
1384
  window.installModel = installModel;
986
- window.deleteModel = deleteModel;
1385
+ window.deleteModel = deleteModel;
1386
+ window.showMigrationModal = showMigrationModal;
1387
+ window.hideMigrationModal = hideMigrationModal;
1388
+ window.hideMigrationBanner = hideMigrationBanner;
1389
+ window.deleteIncompatibleModels = deleteIncompatibleModels;