From bef4c1062920f27ddaaf39e0f8fd22697b1d0e6a Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sat, 16 Aug 2025 07:44:50 +0200 Subject: [PATCH] feat(ui): General improvements (#6072) * wip * Simplify stop Signed-off-by: Ettore Di Giacinto * Improve UI Signed-off-by: Ettore Di Giacinto * Show installed backends at the index Signed-off-by: Ettore Di Giacinto * Imporve UI Signed-off-by: Ettore Di Giacinto --------- Signed-off-by: Ettore Di Giacinto --- core/http/endpoints/localai/system.go | 2 +- core/http/endpoints/localai/welcome.go | 17 + core/http/static/chat.js | 18 + core/http/views/backends.html | 353 +++++++++++++++----- core/http/views/index.html | 345 ++++++++++++------- core/http/views/model-editor.html | 433 ++++++++++++++++-------- core/http/views/models.html | 439 ++++++++++++++++++------- core/services/backend_monitor.go | 37 +-- pkg/model/loader.go | 2 +- pkg/model/loader_test.go | 2 +- 10 files changed, 1160 insertions(+), 488 deletions(-) diff --git a/core/http/endpoints/localai/system.go b/core/http/endpoints/localai/system.go index 64b1d111b..349f97cf8 100644 --- a/core/http/endpoints/localai/system.go +++ b/core/http/endpoints/localai/system.go @@ -14,7 +14,7 @@ import ( func SystemInformations(ml *model.ModelLoader, appConfig *config.ApplicationConfig) func(*fiber.Ctx) error { return func(c *fiber.Ctx) error { availableBackends := []string{} - loadedModels := ml.ListModels() + loadedModels := ml.ListLoadedModels() for b := range appConfig.ExternalGRPCBackends { availableBackends = append(availableBackends, b) } diff --git a/core/http/endpoints/localai/welcome.go b/core/http/endpoints/localai/welcome.go index 7f5e0076c..23efd0788 100644 --- a/core/http/endpoints/localai/welcome.go +++ b/core/http/endpoints/localai/welcome.go @@ -16,6 +16,15 @@ func WelcomeEndpoint(appConfig *config.ApplicationConfig, modelConfigs := cl.GetAllModelsConfigs() galleryConfigs := map[string]*gallery.ModelConfig{} + backends, _ := gallery.AvailableBackends(appConfig.BackendGalleries, appConfig.SystemState) + + installedBackends := gallery.GalleryElements[*gallery.GalleryBackend]{} + for _, b := range backends { + if b.Installed { + installedBackends = append(installedBackends, b) + } + } + for _, m := range modelConfigs { cfg, err := gallery.GetLocalModelConfiguration(ml.ModelPath, m.Name) if err != nil { @@ -24,6 +33,12 @@ func WelcomeEndpoint(appConfig *config.ApplicationConfig, galleryConfigs[m.Name] = cfg } + loadedModels := ml.ListLoadedModels() + loadedModelsMap := map[string]bool{} + for _, m := range loadedModels { + loadedModelsMap[m.ID] = true + } + modelsWithoutConfig, _ := services.ListModels(cl, ml, config.NoFilterFn, services.LOOSE_ONLY) // Get model statuses to display in the UI the operation in progress @@ -39,6 +54,8 @@ func WelcomeEndpoint(appConfig *config.ApplicationConfig, "ApplicationConfig": appConfig, "ProcessingModels": processingModels, "TaskTypes": taskTypes, + "LoadedModels": loadedModelsMap, + "InstalledBackends": installedBackends, } if string(c.Context().Request.Header.ContentType()) == "application/json" || len(c.Accepts("html")) == 0 { diff --git a/core/http/static/chat.js b/core/http/static/chat.js index db5c884e2..3de1e0724 100644 --- a/core/http/static/chat.js +++ b/core/http/static/chat.js @@ -48,6 +48,24 @@ function submitSystemPrompt(event) { document.getElementById("systemPrompt").blur(); } +function handleShutdownResponse(event, modelName) { + // Check if the request was successful + if (event.detail.successful) { + // Show a success message (optional) + console.log(`Model ${modelName} stopped successfully`); + + // Refresh the page to update the UI + window.location.reload(); + } else { + // Show an error message (optional) + console.error(`Failed to stop model ${modelName}`); + + // You could also show a user-friendly error message here + // For now, we'll still refresh to show the current state + window.location.reload(); + } +} + var images = []; var audios = []; var fileContents = []; diff --git a/core/http/views/backends.html b/core/http/views/backends.html index 23f7d43de..25d7535cb 100644 --- a/core/http/views/backends.html +++ b/core/http/views/backends.html @@ -2,7 +2,7 @@ {{template "views/partials/head" .}} - +
{{template "views/partials/navbar" .}} @@ -10,94 +10,128 @@
-
-
-

- +
+ +
+
+
+
+ +
+

+ Backend Management

-

- {{.AvailableBackends}} backends available - - - +

+ Discover and install AI backends to power your models

+
+
+
+ {{.AvailableBackends}} + backends available +
+ + + Documentation + + +
{{template "views/partials/inprogress" .}} -
- -
-
- -
- - - - - - - -
+
+
- -
-

Filter by type:

-
- - - - - +
+ +
+

+ + Find Backend Components +

+
+
+ +
+ + + + + + + +
+
+ + +
+

+ + Filter by Backend Type +

+
+ + + + + +
@@ -109,21 +143,24 @@ {{ if gt .AvailableBackends $numBackendsPerPage }} -
-
+
+
{{ if .PrevPage }} {{ end }} -
- Page {{.CurrentPage}} of {{.TotalPages}} +
+ Page + {{.CurrentPage}} + of + {{.TotalPages}}
{{ if .NextPage }} {{ end }}
@@ -134,6 +171,135 @@ {{template "views/partials/footer" .}}
+ + diff --git a/core/http/views/index.html b/core/http/views/index.html index d90c899e3..fcafdfcd4 100644 --- a/core/http/views/index.html +++ b/core/http/views/index.html @@ -2,39 +2,51 @@ {{template "views/partials/head" .}} - +
{{template "views/partials/navbar" .}}
-
-
-

- - Welcome to your LocalAI instance! +
+ +
+
+
+
+ + @@ -45,22 +57,41 @@ {{template "views/partials/inprogress" .}} {{ if eq (len .ModelsConfig) 0 }} -
-
-
- + +
+
+
+
+ +
+

No models installed yet

+

Get started by installing models from the gallery or check our documentation for guidance

+ + -

No models installed from the LocalAI gallery

-

Install models from the 🖼️ Gallery or check the Getting started documentation

{{ if ne (len .Models) 0 }} -
-

Models installed without a configuration file:

+
+

Detected Model Files

+

These models were found but don't have configuration files yet

{{ range .Models }} -
- -

{{if .Name}}{{.Name}}{{else}}{{.}}{{end}}

+
+
+ +
+
+

{{if .Name}}{{.Name}}{{else}}{{.}}{{end}}

+

No configuration

+
{{end}}
@@ -69,140 +100,226 @@
{{ else }} + {{ $modelsN := len .ModelsConfig}} {{ $modelsN = add $modelsN (len .Models)}} -
-

- {{$modelsN}} Installed Model -

-
-
+
{{$galleryConfig:=.GalleryConfig}} + {{ $loadedModels := .LoadedModels }} {{$noicon:="https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg"}} {{ range .ModelsConfig }} {{ $backendCfg := . }} {{ $cfg:= index $galleryConfig .Name}} -
-
-
- + +
+
+
+ {{.Name}} icon + {{ if index $loadedModels .Name }} +
{{ end }} - class="w-full h-full object-contain" - alt="{{.Name}} icon" - > -
-
-
-

{{.Name}}

- - -
-
- {{ if .Backend }} - - {{.Backend}} - - {{ else }} - - auto - - {{ end }} +
+
+

{{.Name}}

+ + + +
+ +
+ {{ if .Backend }} + + {{.Backend}} + + {{ else }} + + Auto + + {{ end }} + + {{ if index $loadedModels .Name }} + + Running + + {{ end }} +
-
-
+ +
+
{{ range .KnownUsecaseStrings }} {{ if eq . "FLAG_CHAT" }} - - Chat + + + Chat {{ end }} {{ if eq . "FLAG_IMAGE" }} - - Image + + + Image {{ end }} {{ if eq . "FLAG_TTS" }} - - TTS + + + TTS {{ end }} {{ end }}
-
- - Edit - - + +
+
+ {{ if index $loadedModels .Name }} + + {{ end }} +
+ +
+ + Edit + + +
{{ end }} + {{ range .Models }} -
-
-
- Model icon -
-
-
-

{{.}}

+
+
+
+
+
- -
- - auto - - - No Configuration - -
- -
- - Cannot edit (no config) - +
+

{{.}}

+ +
+ + Auto Backend + + + No Config + +
+ +
+ + + Configuration required for full functionality + +
{{end}} -
+
{{ end }}
+ +
+
+

+ Installed Backends +

+

+ {{len .InstalledBackends}} backend{{if gt (len .InstalledBackends) 1}}s{{end}} ready to use +

+
+
+
+ + + {{ if ne (len .InstalledBackends) 0 }} + + {{ else }} +
+
+
+ +
+

No backends installed yet

+

Get started by installing backends from the gallery or check our documentation for guidance

+
+
+ {{ end }} + + + {{ range .InstalledBackends }} +
+
+
+
+ +
+
+

{{.Name}}

+
+
+
+
+ {{end}} +
{{template "views/partials/footer" .}}
+ + \ No newline at end of file diff --git a/core/http/views/model-editor.html b/core/http/views/model-editor.html index ba4c4befd..6ef937b17 100644 --- a/core/http/views/model-editor.html +++ b/core/http/views/model-editor.html @@ -2,30 +2,42 @@ {{template "views/partials/head" .}} - +
{{template "views/partials/navbar" .}}
- -
-
-
-

- {{if .ModelName}}Edit Model: {{.ModelName}}{{else}}Import New Model{{end}} -

-

Configure your model settings using the form or YAML editor

-
-
- - + +
+ +
+
+
+
+ +
+
+
+

+ + {{if .ModelName}}Edit Model: {{.ModelName}}{{else}}Import New Model{{end}} + +

+

Configure your model settings using the form or YAML editor

+
+
+ + +
@@ -37,45 +49,53 @@
-
-
-

- +
+
+ +
+

+
+ +
Configuration Form

-
- -
-
+
-
+
-
-
-

- +
+
+ +
+

+
+ +
YAML Editor

-
- - +
-
+
@@ -95,90 +115,238 @@ @@ -398,20 +566,22 @@ class ModelEditor { createFormSection(section) { const sectionDiv = document.createElement('div'); - sectionDiv.className = 'bg-gray-700/30 rounded-lg border border-gray-600/50'; + sectionDiv.className = 'form-section'; const isCollapsible = section.collapsible; const sectionId = section.title.toLowerCase().replace(/\s+/g, '-'); sectionDiv.innerHTML = ` -
-

- +
+

+
+ +
${section.title} - ${isCollapsible ? '' : ''} + ${isCollapsible ? '' : ''}

-
+
${section.fields.map(field => this.createFormField(field)).join('')}
`; @@ -428,33 +598,33 @@ class ModelEditor { switch (field.type) { case 'text': const readonlyAttr = field.readonly ? 'readonly' : ''; - const readonlyClass = field.readonly ? 'bg-gray-700 cursor-not-allowed' : 'bg-gray-800'; - inputHtml = ``; + const readonlyClass = field.readonly ? 'bg-gray-700 cursor-not-allowed opacity-60' : ''; + inputHtml = ``; break; case 'textarea': - inputHtml = ``; + inputHtml = ``; break; case 'number': const step = field.step || '1'; const min = field.min !== undefined ? `min="${field.min}"` : ''; const max = field.max !== undefined ? `max="${field.max}"` : ''; - inputHtml = ``; + inputHtml = ``; break; case 'checkbox': const checked = value === true || value === 'true' ? 'checked' : ''; inputHtml = `
- - + +
`; break; case 'select': const options = field.options.map(opt => ``).join(''); - inputHtml = ``; + inputHtml = ``; break; case 'multiselect': @@ -462,30 +632,30 @@ class ModelEditor { const checkboxes = field.options.map(opt => { const isChecked = currentValues.includes(opt); return ` -
- - +
+ +
`; }).join(''); - inputHtml = `
${checkboxes}
`; + inputHtml = `
${checkboxes}
`; break; case 'array': const arrayValues = Array.isArray(value) ? value : []; inputHtml = ` -
+
${arrayValues.map((item, index) => `
- -
`).join('')}
-
`; break; @@ -494,33 +664,33 @@ class ModelEditor { const kvPairs = typeof value === 'object' && value !== null ? value : {}; const kvEntries = Object.entries(kvPairs); inputHtml = ` -
+
${kvEntries.map(([key, val], index) => `
- - -
`).join('')}
-
`; break; } return ` -
-