From fe055d4b361790478a4862d320574ffdb96a52f6 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 7 May 2024 01:17:07 +0200 Subject: [PATCH] feat(webui): ux improvements (#2247) * ux: change welcome when there are no models installed Signed-off-by: Ettore Di Giacinto * ux: filter Signed-off-by: Ettore Di Giacinto * ux: show tags in filter Signed-off-by: Ettore Di Giacinto * wip Signed-off-by: Ettore Di Giacinto * make tags clickable Signed-off-by: Ettore Di Giacinto * allow to delete models from the list Signed-off-by: Ettore Di Giacinto * ui: display icon of installed models Signed-off-by: Ettore Di Giacinto * gallery: remove gallery file when removing model Signed-off-by: Ettore Di Giacinto * feat(gallery): show a re-install button Signed-off-by: Ettore Di Giacinto * make filter buttons, rename Gallery field Signed-off-by: mudler * show again buttons at end of operations Signed-off-by: mudler --------- Signed-off-by: Ettore Di Giacinto Signed-off-by: mudler --- core/config/backend_config_loader.go | 6 + core/http/elements/gallery.go | 204 +++++++++++++++++-------- core/http/endpoints/localai/gallery.go | 14 +- core/http/endpoints/localai/welcome.go | 12 ++ core/http/routes/ui.go | 44 ++++-- core/http/views/index.html | 22 ++- core/http/views/models.html | 52 ++++++- core/services/gallery.go | 21 +-- pkg/gallery/gallery.go | 12 ++ pkg/gallery/models.go | 2 + pkg/gallery/op.go | 9 +- pkg/gallery/request.go | 6 + 12 files changed, 309 insertions(+), 95 deletions(-) diff --git a/core/config/backend_config_loader.go b/core/config/backend_config_loader.go index 83b66740..cf7238ce 100644 --- a/core/config/backend_config_loader.go +++ b/core/config/backend_config_loader.go @@ -182,6 +182,12 @@ func (cl *BackendConfigLoader) GetAllBackendConfigs() []BackendConfig { return res } +func (cl *BackendConfigLoader) RemoveBackendConfig(m string) { + cl.Lock() + defer cl.Unlock() + delete(cl.configs, m) +} + func (cl *BackendConfigLoader) ListBackendConfigs() []string { cl.Lock() defer cl.Unlock() diff --git a/core/http/elements/gallery.go b/core/http/elements/gallery.go index 8093b042..60c53da2 100644 --- a/core/http/elements/gallery.go +++ b/core/http/elements/gallery.go @@ -2,6 +2,7 @@ package elements import ( "fmt" + "strings" "github.com/chasefleming/elem-go" "github.com/chasefleming/elem-go/attrs" @@ -13,7 +14,12 @@ const ( NoImage = "https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg" ) -func DoneProgress(uid, text string) string { +func DoneProgress(galleryID, text string, showDelete bool) string { + // Split by @ and grab the name + if strings.Contains(galleryID, "@") { + galleryID = strings.Split(galleryID, "@")[1] + } + return elem.Div( attrs.Props{}, elem.H3( @@ -25,10 +31,11 @@ func DoneProgress(uid, text string) string { }, elem.Text(text), ), + elem.If(showDelete, deleteButton(galleryID), reInstallButton(galleryID)), ).Render() } -func ErrorProgress(err string) string { +func ErrorProgress(err, galleryName string) string { return elem.Div( attrs.Props{}, elem.H3( @@ -38,8 +45,9 @@ func ErrorProgress(err string) string { "tabindex": "-1", "autofocus": "", }, - elem.Text("Error"+err), + elem.Text("Error "+err), ), + installButton(galleryName), ).Render() } @@ -67,7 +75,7 @@ func StartProgressBar(uid, progress, text string) string { return elem.Div(attrs.Props{ "hx-trigger": "done", "hx-get": "/browse/job/" + uid, - "hx-swap": "outerHTML", + "hx-swap": "innerHTML", "hx-target": "this", }, elem.H3( @@ -99,7 +107,119 @@ func cardSpan(text, icon string) elem.Node { elem.I(attrs.Props{ "class": icon + " pr-2", }), + elem.Text(text), + + //elem.Text(text), + ) +} + +func searchableElement(text, icon string) elem.Node { + return elem.Form( + attrs.Props{}, + elem.Input( + attrs.Props{ + "type": "hidden", + "name": "search", + "value": text, + }, + ), + elem.Span( + attrs.Props{ + "class": "inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2 hover:bg-gray-300 hover:shadow-gray-2", + }, + + elem.A( + attrs.Props{ + // "name": "search", + // "value": text, + //"class": "inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2", + "href": "#!", + "hx-post": "/browse/search/models", + "hx-target": "#search-results", + // TODO: this doesn't work + // "hx-vals": `{ \"search\": \"` + text + `\" }`, + "hx-indicator": ".htmx-indicator", + }, + elem.I(attrs.Props{ + "class": icon + " pr-2", + }), + elem.Text(text), + ), + ), + + //elem.Text(text), + ) +} + +func link(text, url string) elem.Node { + return elem.A( + attrs.Props{ + "class": "inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2 hover:bg-gray-300 hover:shadow-gray-2", + "href": url, + "target": "_blank", + }, + elem.I(attrs.Props{ + "class": "fas fa-link pr-2", + }), + elem.Text(text), + ) +} +func installButton(galleryName string) elem.Node { + return elem.Button( + attrs.Props{ + "data-twe-ripple-init": "", + "data-twe-ripple-color": "light", + "class": "float-right inline-block rounded bg-primary px-6 pb-2.5 mb-3 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-primary-accent-300 hover:shadow-primary-2 focus:bg-primary-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-primary-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong", + "hx-swap": "outerHTML", + // post the Model ID as param + "hx-post": "/browse/install/model/" + galleryName, + }, + elem.I( + attrs.Props{ + "class": "fa-solid fa-download pr-2", + }, + ), + elem.Text("Install"), + ) +} + +func reInstallButton(galleryName string) elem.Node { + return elem.Button( + attrs.Props{ + "data-twe-ripple-init": "", + "data-twe-ripple-color": "light", + "class": "float-right inline-block rounded bg-primary ml-2 px-6 pb-2.5 mb-3 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-primary-accent-300 hover:shadow-primary-2 focus:bg-primary-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-primary-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong", + "hx-swap": "outerHTML", + // post the Model ID as param + "hx-post": "/browse/install/model/" + galleryName, + }, + elem.I( + attrs.Props{ + "class": "fa-solid fa-arrow-rotate-right pr-2", + }, + ), + elem.Text("Reinstall"), + ) +} + +func deleteButton(modelName string) elem.Node { + return elem.Button( + attrs.Props{ + "data-twe-ripple-init": "", + "data-twe-ripple-color": "light", + "hx-confirm": "Are you sure you wish to delete the model?", + "class": "float-right inline-block rounded bg-red-800 px-6 pb-2.5 mb-3 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-red-accent-300 hover:shadow-red-2 focus:bg-red-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-red-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong", + "hx-swap": "outerHTML", + // post the Model ID as param + "hx-post": "/browse/delete/model/" + modelName, + }, + elem.I( + attrs.Props{ + "class": "fa-solid fa-cancel pr-2", + }, + ), + elem.Text("Delete"), ) } @@ -114,43 +234,6 @@ func ListModels(models []*gallery.GalleryModel, installing *xsync.SyncedMap[stri // elem.Text(s), // ) // } - deleteButton := func(m *gallery.GalleryModel) elem.Node { - return elem.Button( - attrs.Props{ - "data-twe-ripple-init": "", - "data-twe-ripple-color": "light", - "class": "float-right inline-block rounded bg-red-800 px-6 pb-2.5 mb-3 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-red-accent-300 hover:shadow-red-2 focus:bg-red-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-red-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong", - "hx-swap": "outerHTML", - // post the Model ID as param - "hx-post": "/browse/delete/model/" + m.Name, - }, - elem.I( - attrs.Props{ - "class": "fa-solid fa-cancel pr-2", - }, - ), - elem.Text("Delete"), - ) - } - - installButton := func(m *gallery.GalleryModel) elem.Node { - return elem.Button( - attrs.Props{ - "data-twe-ripple-init": "", - "data-twe-ripple-color": "light", - "class": "float-right inline-block rounded bg-primary px-6 pb-2.5 mb-3 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-primary-accent-300 hover:shadow-primary-2 focus:bg-primary-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-primary-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong", - "hx-swap": "outerHTML", - // post the Model ID as param - "hx-post": "/browse/install/model/" + fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name), - }, - elem.I( - attrs.Props{ - "class": "fa-solid fa-download pr-2", - }, - ), - elem.Text("Install"), - ) - } descriptionDiv := func(m *gallery.GalleryModel) elem.Node { @@ -187,25 +270,26 @@ func ListModels(models []*gallery.GalleryModel, installing *xsync.SyncedMap[stri ) } + tagsNodes := []elem.Node{} for _, tag := range m.Tags { - nodes = append(nodes, - cardSpan(tag, "fas fa-tag"), + tagsNodes = append(tagsNodes, + searchableElement(tag, "fas fa-tag"), ) } + nodes = append(nodes, + elem.Div( + attrs.Props{ + "class": "flex flex-row flex-wrap content-center", + }, + tagsNodes..., + ), + ) + for i, url := range m.URLs { nodes = append(nodes, - elem.A( - attrs.Props{ - "class": "inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2", - "href": url, - "target": "_blank", - }, - elem.I(attrs.Props{ - "class": "fas fa-link pr-2", - }), - elem.Text("Link #"+fmt.Sprintf("%d", i+1)), - )) + link("Link #"+fmt.Sprintf("%d", i+1), url), + ) } return elem.Div( @@ -224,12 +308,12 @@ func ListModels(models []*gallery.GalleryModel, installing *xsync.SyncedMap[stri elem.Raw(StartProgressBar(installing.Get(galleryID), "0", "Installing")), ), // Otherwise, show install button (if not installed) or display "Installed" elem.If(m.Installed, - //elem.Node(elem.Div( - // attrs.Props{}, - // span("Installed"), deleteButton(m), - // )), - deleteButton(m), - installButton(m), + elem.Node(elem.Div( + attrs.Props{}, + reInstallButton(m.ID()), + deleteButton(m.Name), + )), + installButton(m.ID()), ), ), ) diff --git a/core/http/endpoints/localai/gallery.go b/core/http/endpoints/localai/gallery.go index a74a2bb9..049dc802 100644 --- a/core/http/endpoints/localai/gallery.go +++ b/core/http/endpoints/localai/gallery.go @@ -61,11 +61,11 @@ func (mgs *ModelGalleryEndpointService) ApplyModelGalleryEndpoint() func(c *fibe return err } mgs.galleryApplier.C <- gallery.GalleryOp{ - Req: input.GalleryModel, - Id: uuid.String(), - GalleryName: input.ID, - Galleries: mgs.galleries, - ConfigURL: input.ConfigURL, + Req: input.GalleryModel, + Id: uuid.String(), + GalleryModelName: input.ID, + Galleries: mgs.galleries, + ConfigURL: input.ConfigURL, } return c.JSON(struct { ID string `json:"uuid"` @@ -79,8 +79,8 @@ func (mgs *ModelGalleryEndpointService) DeleteModelGalleryEndpoint() func(c *fib modelName := c.Params("name") mgs.galleryApplier.C <- gallery.GalleryOp{ - Delete: true, - GalleryName: modelName, + Delete: true, + GalleryModelName: modelName, } uuid, err := uuid.NewUUID() diff --git a/core/http/endpoints/localai/welcome.go b/core/http/endpoints/localai/welcome.go index 291422c6..3b36aaf6 100644 --- a/core/http/endpoints/localai/welcome.go +++ b/core/http/endpoints/localai/welcome.go @@ -3,6 +3,7 @@ package localai import ( "github.com/go-skynet/LocalAI/core/config" "github.com/go-skynet/LocalAI/internal" + "github.com/go-skynet/LocalAI/pkg/gallery" "github.com/go-skynet/LocalAI/pkg/model" "github.com/gofiber/fiber/v2" ) @@ -13,11 +14,22 @@ func WelcomeEndpoint(appConfig *config.ApplicationConfig, models, _ := ml.ListModels() backendConfigs := cl.GetAllBackendConfigs() + galleryConfigs := map[string]*gallery.Config{} + for _, m := range backendConfigs { + + cfg, err := gallery.GetLocalModelConfiguration(ml.ModelPath, m.Name) + if err != nil { + continue + } + galleryConfigs[m.Name] = cfg + } + summary := fiber.Map{ "Title": "LocalAI API - " + internal.PrintableVersion(), "Version": internal.PrintableVersion(), "Models": models, "ModelsConfig": backendConfigs, + "GalleryConfig": galleryConfigs, "ApplicationConfig": appConfig, } diff --git a/core/http/routes/ui.go b/core/http/routes/ui.go index 70715823..455647e4 100644 --- a/core/http/routes/ui.go +++ b/core/http/routes/ui.go @@ -3,6 +3,7 @@ package routes import ( "fmt" "html/template" + "sort" "strings" "github.com/go-skynet/LocalAI/core/config" @@ -34,11 +35,24 @@ func RegisterUIRoutes(app *fiber.App, app.Get("/browse", auth, func(c *fiber.Ctx) error { models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath) + // Get all available tags + allTags := map[string]struct{}{} + tags := []string{} + for _, m := range models { + for _, t := range m.Tags { + allTags[t] = struct{}{} + } + } + for t := range allTags { + tags = append(tags, t) + } + sort.Strings(tags) summary := fiber.Map{ "Title": "LocalAI - Models", "Version": internal.PrintableVersion(), "Models": template.HTML(elements.ListModels(models, installingModels)), "Repositories": appConfig.Galleries, + "AllTags": tags, // "ApplicationConfig": appConfig, } @@ -92,9 +106,9 @@ func RegisterUIRoutes(app *fiber.App, installingModels.Set(galleryID, uid) op := gallery.GalleryOp{ - Id: uid, - GalleryName: galleryID, - Galleries: appConfig.Galleries, + Id: uid, + GalleryModelName: galleryID, + Galleries: appConfig.Galleries, } go func() { galleryService.C <- op @@ -118,12 +132,13 @@ func RegisterUIRoutes(app *fiber.App, installingModels.Set(galleryID, uid) op := gallery.GalleryOp{ - Id: uid, - Delete: true, - GalleryName: galleryID, + Id: uid, + Delete: true, + GalleryModelName: galleryID, } go func() { galleryService.C <- op + cl.RemoveBackendConfig(galleryID) }() return c.SendString(elements.StartProgressBar(uid, "0", "Deletion")) @@ -146,7 +161,7 @@ func RegisterUIRoutes(app *fiber.App, return c.SendString(elements.ProgressBar("100")) } if status.Error != nil { - return c.SendString(elements.ErrorProgress(status.Error.Error())) + return c.SendString(elements.ErrorProgress(status.Error.Error(), status.GalleryModelName)) } return c.SendString(elements.ProgressBar(fmt.Sprint(status.Progress))) @@ -158,18 +173,22 @@ func RegisterUIRoutes(app *fiber.App, status := galleryService.GetStatus(c.Params("uid")) + galleryID := "" for _, k := range installingModels.Keys() { if installingModels.Get(k) == c.Params("uid") { + galleryID = k installingModels.Delete(k) } } + showDelete := true displayText := "Installation completed" if status.Deletion { + showDelete = false displayText = "Deletion completed" } - return c.SendString(elements.DoneProgress(c.Params("uid"), displayText)) + return c.SendString(elements.DoneProgress(galleryID, displayText, showDelete)) }) // Show the Chat page @@ -191,7 +210,8 @@ func RegisterUIRoutes(app *fiber.App, backendConfigs := cl.GetAllBackendConfigs() if len(backendConfigs) == 0 { - return c.SendString("No models available") + // If no model is available redirect to the index which suggests how to install models + return c.Redirect("/") } summary := fiber.Map{ @@ -224,7 +244,8 @@ func RegisterUIRoutes(app *fiber.App, backendConfigs := cl.GetAllBackendConfigs() if len(backendConfigs) == 0 { - return c.SendString("No models available") + // If no model is available redirect to the index which suggests how to install models + return c.Redirect("/") } summary := fiber.Map{ @@ -257,7 +278,8 @@ func RegisterUIRoutes(app *fiber.App, backendConfigs := cl.GetAllBackendConfigs() if len(backendConfigs) == 0 { - return c.SendString("No models available") + // If no model is available redirect to the index which suggests how to install models + return c.Redirect("/") } summary := fiber.Map{ diff --git a/core/http/views/index.html b/core/http/views/index.html index 287ee1ce..f8cae175 100644 --- a/core/http/views/index.html +++ b/core/http/views/index.html @@ -16,16 +16,31 @@

The FOSS alternative to OpenAI, Claude, ...

Documentation - +
+ {{ if eq (len .ModelsConfig) 0 }} +

Ouch! seems you don't have any models installed!

+

..install something from the 🖼️ Gallery or check the Getting started documentation

+ {{ else }}

Installed models

We have {{len .ModelsConfig}} pre-loaded models available.

    + {{$galleryConfig:=.GalleryConfig}} {{ range .ModelsConfig }} + {{ $cfg:= index $galleryConfig .Name}}
  • + + +

    {{.Name}}

    {{ if .Backend }} @@ -37,11 +52,16 @@ auto {{ end }} + +
  • {{ end }}
+ {{ end }}
diff --git a/core/http/views/models.html b/core/http/views/models.html index be3c1bef..17561594 100644 --- a/core/http/views/models.html +++ b/core/http/views/models.html @@ -13,8 +13,56 @@ 🖼️ Available models from {{ len .Repositories }} repositories - - + +
+

Filter by type:

+ + + + + + + +
+ +
+ Filter by tags: + {{ range .AllTags }} + + {{ end }} +
+