Skip to content

Commit 137f163

Browse files
authored
feat(ui): small refinements (#7285)
* feat(ui): show loaded models in the index Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore(ui): re-organize navbar Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent d7f9f3a commit 137f163

File tree

3 files changed

+488
-347
lines changed

3 files changed

+488
-347
lines changed

core/http/views/index.html

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ <h3 class="text-lg font-bold text-[#E5E7EB] mb-4 flex items-center">
220220
<a href="/manage"
221221
class="inline-flex items-center text-sm text-[#94A3B8] hover:text-[#E5E7EB] px-4 py-2 rounded-lg hover:bg-[#1E293B] transition-colors">
222222
<i class="fas fa-cog mr-2"></i>
223-
Manage Models
223+
System
224224
</a>
225225
<a href="/import-model" class="inline-flex items-center text-sm text-[#94A3B8] hover:text-[#E5E7EB] px-4 py-2 rounded-lg hover:bg-[#1E293B] transition-colors">
226226
<i class="fas fa-upload mr-2"></i>
@@ -237,6 +237,43 @@ <h3 class="text-lg font-bold text-[#E5E7EB] mb-4 flex items-center">
237237
Documentation
238238
</a>
239239
</div>
240+
241+
<!-- Model Status Summary - Subtle -->
242+
{{ $loadedModels := .LoadedModels }}
243+
<div class="mb-8 flex items-center justify-center gap-2 text-xs text-[#94A3B8]"
244+
x-data="{ stoppingAll: false, stopAllModels() { window.stopAllModels(this); }, stopModel(name) { window.stopModel(name); }, getLoadedCount() { return document.querySelectorAll('[data-loaded-model]').length; } }"
245+
x-show="getLoadedCount() > 0"
246+
style="display: none;">
247+
<span class="flex items-center gap-1.5">
248+
<i class="fas fa-circle text-green-500 text-[10px]"></i>
249+
<span x-text="`${getLoadedCount()} model(s) loaded`"></span>
250+
</span>
251+
<span class="text-[#38BDF8]/40"></span>
252+
{{ range .ModelsConfig }}
253+
{{ if index $loadedModels .Name }}
254+
<span class="inline-flex items-center gap-1 text-[#94A3B8] hover:text-[#E5E7EB] transition-colors" data-loaded-model>
255+
<span class="truncate max-w-[100px]">{{.Name}}</span>
256+
<button
257+
@click="stopModel('{{.Name}}')"
258+
class="text-red-400/60 hover:text-red-400 transition-colors ml-0.5"
259+
title="Stop {{.Name}}"
260+
>
261+
<i class="fas fa-times text-[10px]"></i>
262+
</button>
263+
</span>
264+
{{ end }}
265+
{{ end }}
266+
<span class="text-[#38BDF8]/40"></span>
267+
<button
268+
@click="stopAllModels()"
269+
:disabled="stoppingAll"
270+
:class="stoppingAll ? 'opacity-50 cursor-not-allowed' : ''"
271+
class="text-red-400/60 hover:text-red-400 transition-colors text-xs"
272+
title="Stop all loaded models"
273+
>
274+
<span x-text="stoppingAll ? 'Stopping...' : 'Stop all'"></span>
275+
</button>
276+
</div>
240277
{{ end }}
241278
</div>
242279
</div>
@@ -334,6 +371,84 @@ <h3 class="text-lg font-bold text-[#E5E7EB] mb-4 flex items-center">
334371

335372
// Make startChat available globally
336373
window.startChat = startChat;
374+
375+
// Stop individual model
376+
async function stopModel(modelName) {
377+
if (!confirm(`Are you sure you want to stop "${modelName}"?`)) {
378+
return;
379+
}
380+
381+
try {
382+
const response = await fetch('/backend/shutdown', {
383+
method: 'POST',
384+
headers: {
385+
'Content-Type': 'application/json',
386+
},
387+
body: JSON.stringify({ model: modelName })
388+
});
389+
390+
if (response.ok) {
391+
// Reload page after short delay to reflect changes
392+
setTimeout(() => {
393+
window.location.reload();
394+
}, 500);
395+
} else {
396+
alert('Failed to stop model');
397+
}
398+
} catch (error) {
399+
console.error('Error stopping model:', error);
400+
alert('Failed to stop model');
401+
}
402+
}
403+
404+
// Stop all loaded models
405+
async function stopAllModels(component) {
406+
const loadedModelNamesStr = '{{ $loadedModels := .LoadedModels }}{{ range .ModelsConfig }}{{ if index $loadedModels .Name }}{{.Name}},{{ end }}{{ end }}';
407+
const loadedModelNames = loadedModelNamesStr.split(',').filter(name => name.length > 0);
408+
409+
if (loadedModelNames.length === 0) {
410+
return;
411+
}
412+
413+
if (!confirm(`Are you sure you want to stop all ${loadedModelNames.length} loaded model(s)?`)) {
414+
return;
415+
}
416+
417+
// Set loading state
418+
if (component) {
419+
component.stoppingAll = true;
420+
}
421+
422+
try {
423+
// Stop all models in parallel
424+
const stopPromises = loadedModelNames.map(modelName =>
425+
fetch('/backend/shutdown', {
426+
method: 'POST',
427+
headers: {
428+
'Content-Type': 'application/json',
429+
},
430+
body: JSON.stringify({ model: modelName })
431+
})
432+
);
433+
434+
await Promise.all(stopPromises);
435+
436+
// Reload page after short delay to reflect changes
437+
setTimeout(() => {
438+
window.location.reload();
439+
}, 1000);
440+
} catch (error) {
441+
console.error('Error stopping models:', error);
442+
alert('Failed to stop some models');
443+
if (component) {
444+
component.stoppingAll = false;
445+
}
446+
}
447+
}
448+
449+
// Make functions available globally for Alpine.js
450+
window.stopModel = stopModel;
451+
window.stopAllModels = stopAllModels;
337452
</script>
338453

339454
</body>

0 commit comments

Comments
 (0)