lemonade-sdk 8.1.11__py3-none-any.whl → 8.2.0__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 (32) 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/bench.py +22 -1
  5. lemonade/tools/flm/utils.py +70 -22
  6. lemonade/tools/llamacpp/bench.py +111 -23
  7. lemonade/tools/llamacpp/load.py +30 -2
  8. lemonade/tools/llamacpp/utils.py +234 -15
  9. lemonade/tools/oga/bench.py +0 -26
  10. lemonade/tools/oga/load.py +38 -142
  11. lemonade/tools/oga/migration.py +403 -0
  12. lemonade/tools/report/table.py +6 -0
  13. lemonade/tools/server/flm.py +2 -6
  14. lemonade/tools/server/llamacpp.py +20 -1
  15. lemonade/tools/server/serve.py +335 -17
  16. lemonade/tools/server/static/js/models.js +416 -18
  17. lemonade/tools/server/static/js/shared.js +44 -6
  18. lemonade/tools/server/static/logs.html +29 -19
  19. lemonade/tools/server/static/styles.css +204 -0
  20. lemonade/tools/server/static/webapp.html +32 -0
  21. lemonade/version.py +1 -1
  22. lemonade_install/install.py +33 -579
  23. {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.0.dist-info}/METADATA +5 -3
  24. {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.0.dist-info}/RECORD +32 -31
  25. lemonade_server/cli.py +10 -0
  26. lemonade_server/model_manager.py +172 -11
  27. lemonade_server/server_models.json +102 -66
  28. {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.0.dist-info}/WHEEL +0 -0
  29. {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.0.dist-info}/entry_points.txt +0 -0
  30. {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.0.dist-info}/licenses/LICENSE +0 -0
  31. {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.0.dist-info}/licenses/NOTICE.md +0 -0
  32. {lemonade_sdk-8.1.11.dist-info → lemonade_sdk-8.2.0.dist-info}/top_level.txt +0 -0
@@ -166,6 +166,29 @@ async function unloadModel() {
166
166
 
167
167
  // === Model Browser Management ===
168
168
 
169
+ // Update visibility of categories/subcategories based on available models
170
+ function updateCategoryVisibility() {
171
+ const allModels = window.SERVER_MODELS || {};
172
+
173
+ // Count models for each recipe
174
+ const recipeCounts = {};
175
+ const recipes = ['llamacpp', 'oga-hybrid', 'oga-npu', 'oga-cpu', 'flm'];
176
+ recipes.forEach(recipe => {
177
+ recipeCounts[recipe] = 0;
178
+ Object.entries(allModels).forEach(([modelId, modelData]) => {
179
+ if (modelData.recipe === recipe && (modelData.suggested || installedModels.has(modelId))) {
180
+ recipeCounts[recipe]++;
181
+ }
182
+ });
183
+
184
+ // Show/hide recipe subcategory
185
+ const subcategory = document.querySelector(`[data-recipe="${recipe}"]`);
186
+ if (subcategory) {
187
+ subcategory.style.display = recipeCounts[recipe] > 0 ? 'block' : 'none';
188
+ }
189
+ });
190
+ }
191
+
169
192
  // Toggle category in model browser (only for Hot Models now)
170
193
  function toggleCategory(categoryName) {
171
194
  const header = document.querySelector(`[data-category="${categoryName}"] .category-header`);
@@ -283,7 +306,7 @@ function displayHotModels() {
283
306
  modelList.innerHTML = '';
284
307
 
285
308
  Object.entries(allModels).forEach(([modelId, modelData]) => {
286
- if (modelData.labels && modelData.labels.includes('hot')) {
309
+ if (modelData.labels && modelData.labels.includes('hot') && (modelData.suggested || installedModels.has(modelId))) {
287
310
  createModelItem(modelId, modelData, modelList);
288
311
  }
289
312
  });
@@ -317,7 +340,7 @@ function displayModelsByRecipe(recipe) {
317
340
  }
318
341
 
319
342
  Object.entries(allModels).forEach(([modelId, modelData]) => {
320
- if (modelData.recipe === recipe) {
343
+ if (modelData.recipe === recipe && (modelData.suggested || installedModels.has(modelId))) {
321
344
  createModelItem(modelId, modelData, modelList);
322
345
  }
323
346
  });
@@ -341,7 +364,7 @@ function displayModelsByLabel(label) {
341
364
  if (modelId.startsWith('user.')) {
342
365
  createModelItem(modelId, modelData, modelList);
343
366
  }
344
- } else if (modelData.labels && modelData.labels.includes(label)) {
367
+ } else if (modelData.labels && modelData.labels.includes(label) && (modelData.suggested || installedModels.has(modelId))) {
345
368
  createModelItem(modelId, modelData, modelList);
346
369
  }
347
370
  });
@@ -505,7 +528,11 @@ async function deleteModel(modelId) {
505
528
  headers: { 'Content-Type': 'application/json' },
506
529
  body: JSON.stringify({ model_name: modelId })
507
530
  });
508
-
531
+ installedModels.delete(modelId);
532
+ // Remove custom models from SERVER_MODELS to prevent them from reappearing without having to do a manual refresh
533
+ if (modelId.startsWith('user.')) {
534
+ delete window.SERVER_MODELS[modelId];
535
+ }
509
536
  // Refresh installed models and model status
510
537
  await fetchInstalledModels();
511
538
  await updateModelStatusIndicator();
@@ -618,8 +645,8 @@ document.addEventListener('DOMContentLoaded', async function() {
618
645
  // Initial fetch of model data - this will populate installedModels
619
646
  await updateModelStatusIndicator();
620
647
 
621
- // Set up periodic refresh of model status
622
- setInterval(updateModelStatusIndicator, 1000); // Check every 1 seconds
648
+ // Update category visibility on initial load
649
+ updateCategoryVisibility();
623
650
 
624
651
  // Initialize model browser with hot models
625
652
  displayHotModels();
@@ -635,6 +662,45 @@ document.addEventListener('DOMContentLoaded', async function() {
635
662
 
636
663
  // Set up register model form
637
664
  setupRegisterModelForm();
665
+ setupFolderSelection();
666
+
667
+ // Set up smart periodic refresh to detect external model changes
668
+ // Poll every 15 seconds (much less aggressive than 1 second)
669
+ // Only poll when page is visible to save resources
670
+ let pollInterval = null;
671
+
672
+ function startPolling() {
673
+ if (!pollInterval) {
674
+ pollInterval = setInterval(async () => {
675
+ // Only update if page is visible
676
+ if (document.visibilityState === 'visible') {
677
+ await updateModelStatusIndicator();
678
+ }
679
+ }, 15000); // Check every 15 seconds
680
+ }
681
+ }
682
+
683
+ function stopPolling() {
684
+ if (pollInterval) {
685
+ clearInterval(pollInterval);
686
+ pollInterval = null;
687
+ }
688
+ }
689
+
690
+ // Start polling when page is visible, stop when hidden
691
+ document.addEventListener('visibilitychange', () => {
692
+ if (document.visibilityState === 'visible') {
693
+ // Page became visible - update immediately and resume polling
694
+ updateModelStatusIndicator();
695
+ startPolling();
696
+ } else {
697
+ // Page hidden - stop polling to save resources
698
+ stopPolling();
699
+ }
700
+ });
701
+
702
+ // Start polling initially
703
+ startPolling();
638
704
  });
639
705
 
640
706
  // Toggle Add Model form
@@ -928,7 +994,17 @@ function setupRegisterModelForm() {
928
994
  if (!name.startsWith('user.')) {
929
995
  name = 'user.' + name;
930
996
  }
931
-
997
+
998
+ // Check if model name already exists
999
+ const allModels = window.SERVER_MODELS || {};
1000
+ if (allModels[name] || installedModels.has(name)) {
1001
+ showErrorBanner('Model name already exists. Please enter a different name.');
1002
+ registerStatus.textContent = 'Model name already exists';
1003
+ registerStatus.style.color = '#b10819ff';
1004
+ registerStatus.className = 'register-status error';
1005
+ return;
1006
+ }
1007
+
932
1008
  const checkpoint = document.getElementById('register-checkpoint').value.trim();
933
1009
  const recipe = document.getElementById('register-recipe').value;
934
1010
  const reasoning = document.getElementById('register-reasoning').checked;
@@ -939,24 +1015,70 @@ function setupRegisterModelForm() {
939
1015
  return;
940
1016
  }
941
1017
 
942
- const payload = { model_name: name, recipe, reasoning, vision };
943
- if (checkpoint) payload.checkpoint = checkpoint;
944
- if (mmproj) payload.mmproj = mmproj;
945
-
946
1018
  const btn = document.getElementById('register-submit');
947
1019
  btn.disabled = true;
948
1020
  btn.textContent = 'Installing...';
949
1021
 
950
1022
  try {
951
- await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
952
- method: 'POST',
953
- headers: { 'Content-Type': 'application/json' },
954
- body: JSON.stringify(payload)
955
- });
1023
+ if (isLocalModel && selectedModelFiles) {
1024
+ if (recipe === 'llamacpp' && !Array.from(selectedModelFiles).some(file => file.name.toLowerCase().endsWith('.gguf'))) {
1025
+ throw new Error('No .gguf files found in the selected folder for llamacpp');
1026
+ }
1027
+
1028
+ const formData = new FormData();
1029
+ formData.append('model_name', name);
1030
+ formData.append('checkpoint', checkpoint);
1031
+ formData.append('recipe', recipe);
1032
+ formData.append('reasoning', reasoning);
1033
+ formData.append('vision', vision);
1034
+ if (mmproj) formData.append('mmproj', mmproj);
1035
+ Array.from(selectedModelFiles).forEach(file => {
1036
+ formData.append('model_files', file, file.webkitRelativePath);
1037
+ });
1038
+
1039
+ await httpRequest(getServerBaseUrl() + '/api/v1/add-local-model', {
1040
+ method: 'POST',
1041
+ body: formData
1042
+ });
1043
+ }
1044
+ else {
1045
+ if (!checkpoint) {
1046
+ throw new Error('Checkpoint is required for remote models');
1047
+ }
1048
+ const payload = { model_name: name, recipe, reasoning, vision };
1049
+ if (checkpoint) payload.checkpoint = checkpoint;
1050
+ if (mmproj) payload.mmproj = mmproj;
1051
+
1052
+ await httpRequest(getServerBaseUrl() + '/api/v1/pull', {
1053
+ method: 'POST',
1054
+ headers: { 'Content-Type': 'application/json' },
1055
+ body: JSON.stringify(payload)
1056
+ });
1057
+ }
1058
+
956
1059
  registerStatus.textContent = 'Model installed!';
957
- registerStatus.style.color = '#27ae60';
1060
+ registerStatus.style.color = '#0eaf51ff';
958
1061
  registerStatus.className = 'register-status success';
1062
+
1063
+ // Add custom model to SERVER_MODELS so it appears in the UI without having to do a manual refresh
1064
+ if (name.startsWith('user.')) {
1065
+ const labels = ['custom'];
1066
+ if (vision) labels.push('vision');
1067
+ if (reasoning) labels.push('reasoning');
1068
+
1069
+ window.SERVER_MODELS[name] = {
1070
+ recipe: recipe,
1071
+ labels: labels
1072
+ };
1073
+ if (checkpoint) window.SERVER_MODELS[name].checkpoint = checkpoint;
1074
+ if (mmproj) window.SERVER_MODELS[name].mmproj = mmproj;
1075
+ }
1076
+
959
1077
  registerForm.reset();
1078
+ isLocalModel = false;
1079
+ selectedModelFiles = null;
1080
+ document.getElementById('folder-input').value = '';
1081
+
960
1082
  await refreshModelMgmtUI();
961
1083
  // Update chat dropdown too if loadModels function exists
962
1084
  if (typeof loadModels === 'function') {
@@ -975,6 +1097,278 @@ function setupRegisterModelForm() {
975
1097
  };
976
1098
  }
977
1099
  }
1100
+ let isLocalModel = false;
1101
+ let selectedModelFiles = null;
1102
+ // Helper function to find mmproj file in selected folder
1103
+ function findMmprojFile(files) {
1104
+ for (let i = 0; i < files.length; i++) {
1105
+ const file = files[i];
1106
+ const fileName = file.name.toLowerCase();
1107
+ const relativePath = file.webkitRelativePath;
1108
+
1109
+ // Check if file contains 'mmproj' and has .gguf extension
1110
+ if (fileName.includes('mmproj') && fileName.endsWith('.gguf')) {
1111
+ // Return just the filename (last part of the path)
1112
+ return relativePath.split('/').pop();
1113
+ }
1114
+ }
1115
+ return null;
1116
+ }
1117
+
1118
+ // Helper function to find all non-mmproj GGUF files in selected folder
1119
+ function findGgufFiles(files) {
1120
+ const ggufFiles = [];
1121
+ for (let i = 0; i < files.length; i++) {
1122
+ const file = files[i];
1123
+ const fileName = file.name.toLowerCase();
1124
+ const relativePath = file.webkitRelativePath;
1125
+
1126
+ // Check if file has .gguf extension but is NOT an mmproj file
1127
+ if (fileName.endsWith('.gguf') && !fileName.includes('mmproj')) {
1128
+ // Store just the filename (last part of the path)
1129
+ ggufFiles.push(relativePath.split('/').pop());
1130
+ }
1131
+ }
1132
+ return ggufFiles;
1133
+ }
1134
+
1135
+ // Helper function to check GGUF files and show appropriate banners
1136
+ function checkGgufFilesAndShowBanner(files) {
1137
+ const recipeSelect = document.getElementById('register-recipe');
1138
+
1139
+ // Only check if llamacpp is selected
1140
+ if (!recipeSelect || recipeSelect.value !== 'llamacpp') {
1141
+ return;
1142
+ }
1143
+
1144
+ const mmprojFile = findMmprojFile(files);
1145
+ const ggufFiles = findGgufFiles(files);
1146
+
1147
+ // Hide any existing banners first
1148
+ hideErrorBanner();
1149
+
1150
+ if (ggufFiles.length > 1) {
1151
+ // Multiple GGUF files detected
1152
+ const folderPath = files[0].webkitRelativePath.split('/')[0];
1153
+ 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]}`;
1154
+
1155
+ if (mmprojFile) {
1156
+ bannerMsg += `\n\nDon't forget to enter the mmproj file name and check the 'vision' checkbox if it is a vision model.`;
1157
+ }
1158
+
1159
+ showBanner(bannerMsg, 'warning');
1160
+ } else if (mmprojFile) {
1161
+ // MMproj detected
1162
+ showBanner("MMproj detected and populated. Please validate the file name and check the 'vision' checkbox if it is a vision model.", 'success');
1163
+ }
1164
+ }
1165
+ // Helper function to auto-fill mmproj field if llamacpp is selected
1166
+ function autoFillMmproj() {
1167
+ const recipeSelect = document.getElementById('register-recipe');
1168
+ const mmprojInput = document.getElementById('register-mmproj');
1169
+
1170
+ if (recipeSelect && mmprojInput && isLocalModel && selectedModelFiles) {
1171
+ const selectedRecipe = recipeSelect.value;
1172
+
1173
+ if (selectedRecipe === 'llamacpp') {
1174
+ const mmprojFile = findMmprojFile(selectedModelFiles);
1175
+ if (mmprojFile) {
1176
+ mmprojInput.value = mmprojFile;
1177
+ }
1178
+
1179
+ // Check GGUF files and show appropriate banner
1180
+ checkGgufFilesAndShowBanner(selectedModelFiles);
1181
+ } else {
1182
+ // Hide banners if not llamacpp
1183
+ hideErrorBanner();
1184
+ }
1185
+ }
1186
+ }
1187
+ function setupFolderSelection() {
1188
+ const selectFolderBtn = document.getElementById('select-folder-btn');
1189
+ const folderInput = document.getElementById('folder-input');
1190
+ const checkpointInput = document.getElementById('register-checkpoint');
1191
+ const recipeSelect = document.getElementById('register-recipe');
1192
+
1193
+ if (selectFolderBtn && folderInput && checkpointInput) {
1194
+ selectFolderBtn.addEventListener('click', () => {
1195
+ folderInput.click();
1196
+ });
1197
+
1198
+ folderInput.addEventListener('change', (event) => {
1199
+ const files = event.target.files;
1200
+ if (files.length > 0) {
1201
+ const firstFile = files[0];
1202
+ const folderPath = firstFile.webkitRelativePath.split('/')[0];
1203
+ checkpointInput.value = folderPath;
1204
+ isLocalModel = true;
1205
+ selectedModelFiles = files;
1206
+
1207
+ // Auto-fill mmproj if llamacpp is already selected
1208
+ autoFillMmproj();
1209
+ }
1210
+ else {
1211
+ isLocalModel = false;
1212
+ selectedModelFiles = null;
1213
+ checkpointInput.value = '';
1214
+ hideErrorBanner();
1215
+ }
1216
+ });
1217
+
1218
+ // Add listener to recipe dropdown to auto-fill mmproj when changed to llamacpp
1219
+ if (recipeSelect) {
1220
+ recipeSelect.addEventListener('change', () => {
1221
+ autoFillMmproj();
1222
+ });
1223
+ }
1224
+ }
1225
+ }
1226
+ // === Migration/Cleanup Functions ===
1227
+
1228
+ // Store incompatible models data globally
1229
+ let incompatibleModelsData = null;
1230
+
1231
+ // Check for incompatible models on page load
1232
+ async function checkIncompatibleModels() {
1233
+ try {
1234
+ const response = await httpJson(getServerBaseUrl() + '/api/v1/migration/incompatible-models');
1235
+ incompatibleModelsData = response;
1236
+
1237
+ if (response.count > 0) {
1238
+ showMigrationBanner(response.count, response.total_size);
1239
+ }
1240
+ } catch (error) {
1241
+ console.error('Error checking for incompatible models:', error);
1242
+ }
1243
+ }
1244
+
1245
+ // Show migration banner
1246
+ function showMigrationBanner(count, totalSize) {
1247
+ const banner = document.getElementById('migration-banner');
1248
+ const msg = document.getElementById('migration-banner-msg');
1249
+
1250
+ const sizeGB = (totalSize / (1024 * 1024 * 1024)).toFixed(1);
1251
+ msg.textContent = `Found ${count} incompatible RyzenAI model${count > 1 ? 's' : ''} (${sizeGB} GB). Clean up to free disk space.`;
1252
+ banner.style.display = 'flex';
1253
+ }
1254
+
1255
+ // Hide migration banner
1256
+ function hideMigrationBanner() {
1257
+ const banner = document.getElementById('migration-banner');
1258
+ banner.style.display = 'none';
1259
+ }
1260
+
1261
+ // Show migration modal with model list
1262
+ function showMigrationModal() {
1263
+ if (!incompatibleModelsData || incompatibleModelsData.count === 0) {
1264
+ return;
1265
+ }
1266
+
1267
+ const modal = document.getElementById('migration-modal');
1268
+ const modelList = document.getElementById('migration-model-list');
1269
+ const totalSize = document.getElementById('migration-total-size');
1270
+
1271
+ // Populate model list
1272
+ modelList.innerHTML = '';
1273
+ incompatibleModelsData.models.forEach(model => {
1274
+ const item = document.createElement('div');
1275
+ item.className = 'migration-model-item';
1276
+
1277
+ const nameSpan = document.createElement('span');
1278
+ nameSpan.className = 'migration-model-name';
1279
+ nameSpan.textContent = model.name;
1280
+
1281
+ const sizeSpan = document.createElement('span');
1282
+ sizeSpan.className = 'migration-model-size';
1283
+ sizeSpan.textContent = model.size_formatted;
1284
+
1285
+ item.appendChild(nameSpan);
1286
+ item.appendChild(sizeSpan);
1287
+ modelList.appendChild(item);
1288
+ });
1289
+
1290
+ // Set total size
1291
+ const sizeGB = (incompatibleModelsData.total_size / (1024 * 1024 * 1024)).toFixed(1);
1292
+ totalSize.textContent = `${sizeGB} GB`;
1293
+
1294
+ modal.style.display = 'flex';
1295
+ }
1296
+
1297
+ // Hide migration modal
1298
+ function hideMigrationModal() {
1299
+ const modal = document.getElementById('migration-modal');
1300
+ modal.style.display = 'none';
1301
+ }
1302
+
1303
+ // Delete incompatible models
1304
+ async function deleteIncompatibleModels() {
1305
+ if (!incompatibleModelsData || incompatibleModelsData.count === 0) {
1306
+ return;
1307
+ }
1308
+
1309
+ const modelPaths = incompatibleModelsData.models.map(m => m.path);
1310
+
1311
+ try {
1312
+ // Disable buttons during deletion
1313
+ const deleteBtn = document.querySelector('.delete-btn');
1314
+ const cancelBtn = document.querySelector('.cancel-btn');
1315
+ deleteBtn.disabled = true;
1316
+ cancelBtn.disabled = true;
1317
+ deleteBtn.textContent = 'Deleting...';
1318
+
1319
+ const response = await httpRequest(getServerBaseUrl() + '/api/v1/migration/cleanup', {
1320
+ method: 'POST',
1321
+ headers: { 'Content-Type': 'application/json' },
1322
+ body: JSON.stringify({ model_paths: modelPaths })
1323
+ });
1324
+
1325
+ const result = await response.json();
1326
+
1327
+ // Close modal
1328
+ hideMigrationModal();
1329
+
1330
+ // Hide banner
1331
+ hideMigrationBanner();
1332
+
1333
+ // Show success message
1334
+ showSuccessMessage(`Successfully deleted ${result.success_count} model${result.success_count > 1 ? 's' : ''}, freed ${result.freed_size_formatted}`);
1335
+
1336
+ // Clear cached data
1337
+ incompatibleModelsData = null;
1338
+
1339
+ } catch (error) {
1340
+ console.error('Error deleting incompatible models:', error);
1341
+ showErrorBanner('Failed to delete models: ' + error.message);
1342
+
1343
+ // Re-enable buttons
1344
+ const deleteBtn = document.querySelector('.delete-btn');
1345
+ const cancelBtn = document.querySelector('.cancel-btn');
1346
+ deleteBtn.disabled = false;
1347
+ cancelBtn.disabled = false;
1348
+ deleteBtn.textContent = 'Delete All';
1349
+ }
1350
+ }
1351
+
1352
+ // Show success message (reuse error banner with green color)
1353
+ function showSuccessMessage(message) {
1354
+ const banner = document.getElementById('error-banner');
1355
+ const msg = document.getElementById('error-banner-msg');
1356
+ msg.textContent = message;
1357
+ banner.style.backgroundColor = '#2d7f47';
1358
+ banner.style.display = 'flex';
1359
+
1360
+ // Auto-hide after 5 seconds
1361
+ setTimeout(() => {
1362
+ banner.style.display = 'none';
1363
+ banner.style.backgroundColor = ''; // Reset to default
1364
+ }, 5000);
1365
+ }
1366
+
1367
+ // Check for incompatible models when page loads
1368
+ document.addEventListener('DOMContentLoaded', function() {
1369
+ // Run check after a short delay to let the page load
1370
+ setTimeout(checkIncompatibleModels, 1000);
1371
+ });
978
1372
 
979
1373
  // Make functions globally available for HTML onclick handlers and other components
980
1374
  window.toggleCategory = toggleCategory;
@@ -983,4 +1377,8 @@ window.selectLabel = selectLabel;
983
1377
  window.showAddModelForm = showAddModelForm;
984
1378
  window.unloadModel = unloadModel;
985
1379
  window.installModel = installModel;
986
- window.deleteModel = deleteModel;
1380
+ window.deleteModel = deleteModel;
1381
+ window.showMigrationModal = showMigrationModal;
1382
+ window.hideMigrationModal = hideMigrationModal;
1383
+ window.hideMigrationBanner = hideMigrationBanner;
1384
+ window.deleteIncompatibleModels = deleteIncompatibleModels;
@@ -54,23 +54,53 @@ function renderMarkdown(text) {
54
54
 
55
55
  // Display an error message in the banner
56
56
  function showErrorBanner(msg) {
57
+ showBanner(msg, 'error');
58
+ }
59
+
60
+ // Display a banner with a specific type (error, warning, success)
61
+ function showBanner(msg, type = 'error') {
57
62
  // If DOM isn't ready, wait for it
58
63
  if (document.readyState === 'loading') {
59
64
  document.addEventListener('DOMContentLoaded', () => {
60
- showErrorBanner(msg);
65
+ showBanner(msg, type);
61
66
  });
62
67
  return;
63
68
  }
64
-
69
+
65
70
  const banner = document.getElementById('error-banner');
66
71
  if (!banner) return;
67
72
  const msgEl = document.getElementById('error-banner-msg');
68
- const fullMsg = msg + '\nCheck the Lemonade Server logs via the system tray app for more information.';
73
+ const logsUrl = window.location.origin + '/static/logs.html';
74
+
75
+ // Determine the full message and styling based on type
76
+ let fullMsg = msg;
77
+ let backgroundColor, color;
78
+
79
+ switch(type) {
80
+ case 'success':
81
+ backgroundColor = '#27ae60'; // green
82
+ color = '#ffffff';
83
+ break;
84
+ case 'warning':
85
+ backgroundColor = '#8d5803ff'; // yellow/orange
86
+ color = '#ffffff';
87
+ break;
88
+ case 'error':
89
+ default:
90
+ backgroundColor = '#b10819ff'; // red
91
+ color = '#ffffff';
92
+ fullMsg = `${msg}<br>Check the Lemonade Server logs <a href="${logsUrl}" target="_blank" rel="noopener noreferrer">on the browser</a> or via the system tray app for more information.`;
93
+ break;
94
+ }
95
+
69
96
  if (msgEl) {
70
- msgEl.textContent = fullMsg;
97
+ msgEl.innerHTML = fullMsg;
71
98
  } else {
72
- banner.textContent = fullMsg;
99
+ banner.innerHTML = fullMsg;
73
100
  }
101
+
102
+ banner.style.backgroundColor = backgroundColor;
103
+ banner.style.color = color;
74
104
  banner.style.display = 'flex';
75
105
  }
76
106
 
@@ -222,10 +252,18 @@ async function loadModelStandardized(modelId, options = {}) {
222
252
  }
223
253
 
224
254
  // Make the API call to load the model
255
+ // Include mmproj if the model has it defined
256
+ const loadPayload = { model_name: modelId };
257
+ const allModels = window.SERVER_MODELS || {};
258
+ const modelData = allModels[modelId];
259
+ if (modelData && modelData.mmproj) {
260
+ loadPayload.mmproj = modelData.mmproj;
261
+ }
262
+
225
263
  await httpRequest(getServerBaseUrl() + '/api/v1/load', {
226
264
  method: 'POST',
227
265
  headers: { 'Content-Type': 'application/json' },
228
- body: JSON.stringify({ model_name: modelId })
266
+ body: JSON.stringify(loadPayload)
229
267
  });
230
268
 
231
269
  // Update model status indicator after successful load
@@ -22,26 +22,36 @@
22
22
  <body>
23
23
  <div id="log-container"></div>
24
24
 
25
- <script>
26
- function stripAnsi(str) {
27
- return str.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
28
- }
29
- const logContainer = document.getElementById("log-container");
30
- const ws = new WebSocket(`ws://${location.host}/api/v1/logs/ws`);
25
+ <script>
26
+ function stripAnsi(str) {
27
+ return str.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
28
+ }
29
+
30
+ const logContainer = document.getElementById("log-container");
31
+ const ws = new WebSocket(`ws://${location.host}/api/v1/logs/ws`);
32
+
33
+ function isNearBottom() {
34
+ const threshold = 50; // px from bottom
35
+ return logContainer.scrollTop + logContainer.clientHeight >= logContainer.scrollHeight - threshold;
36
+ }
31
37
 
32
- ws.onmessage = (event) => {
33
- const line = document.createElement("div");
34
- line.textContent = stripAnsi(event.data);
35
- logContainer.appendChild(line);
36
- logContainer.scrollTop = logContainer.scrollHeight; // auto scroll
37
- };
38
+ ws.onmessage = (event) => {
39
+ const line = document.createElement("div");
40
+ line.textContent = stripAnsi(event.data);
41
+ logContainer.appendChild(line);
42
+
43
+ // Only autoscroll if the user is already at (or near) the bottom
44
+ if (isNearBottom()) {
45
+ logContainer.scrollTop = logContainer.scrollHeight;
46
+ }
47
+ };
38
48
 
39
- ws.onclose = () => {
40
- const msg = document.createElement("div");
41
- msg.textContent = "[Disconnected from log stream]";
42
- msg.style.color = "red";
43
- logContainer.appendChild(msg);
44
- };
45
- </script>
49
+ ws.onclose = () => {
50
+ const msg = document.createElement("div");
51
+ msg.textContent = "[Disconnected from log stream]";
52
+ msg.style.color = "red";
53
+ logContainer.appendChild(msg);
54
+ };
55
+ </script>
46
56
  </body>
47
57
  </html>