mirror of
https://github.com/mudler/LocalAI.git
synced 2025-05-11 21:12:58 +00:00
feat(webui): ux improvements (#2247)
* ux: change welcome when there are no models installed Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * ux: filter Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * ux: show tags in filter Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * wip Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * make tags clickable Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * allow to delete models from the list Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * ui: display icon of installed models Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * gallery: remove gallery file when removing model Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(gallery): show a re-install button Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * make filter buttons, rename Gallery field Signed-off-by: mudler <mudler@localai.io> * show again buttons at end of operations Signed-off-by: mudler <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Signed-off-by: mudler <mudler@localai.io>
This commit is contained in:
parent
581b894789
commit
fe055d4b36
@ -182,6 +182,12 @@ func (cl *BackendConfigLoader) GetAllBackendConfigs() []BackendConfig {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cl *BackendConfigLoader) RemoveBackendConfig(m string) {
|
||||||
|
cl.Lock()
|
||||||
|
defer cl.Unlock()
|
||||||
|
delete(cl.configs, m)
|
||||||
|
}
|
||||||
|
|
||||||
func (cl *BackendConfigLoader) ListBackendConfigs() []string {
|
func (cl *BackendConfigLoader) ListBackendConfigs() []string {
|
||||||
cl.Lock()
|
cl.Lock()
|
||||||
defer cl.Unlock()
|
defer cl.Unlock()
|
||||||
|
@ -2,6 +2,7 @@ package elements
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/chasefleming/elem-go"
|
"github.com/chasefleming/elem-go"
|
||||||
"github.com/chasefleming/elem-go/attrs"
|
"github.com/chasefleming/elem-go/attrs"
|
||||||
@ -13,7 +14,12 @@ const (
|
|||||||
NoImage = "https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg"
|
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(
|
return elem.Div(
|
||||||
attrs.Props{},
|
attrs.Props{},
|
||||||
elem.H3(
|
elem.H3(
|
||||||
@ -25,10 +31,11 @@ func DoneProgress(uid, text string) string {
|
|||||||
},
|
},
|
||||||
elem.Text(text),
|
elem.Text(text),
|
||||||
),
|
),
|
||||||
|
elem.If(showDelete, deleteButton(galleryID), reInstallButton(galleryID)),
|
||||||
).Render()
|
).Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrorProgress(err string) string {
|
func ErrorProgress(err, galleryName string) string {
|
||||||
return elem.Div(
|
return elem.Div(
|
||||||
attrs.Props{},
|
attrs.Props{},
|
||||||
elem.H3(
|
elem.H3(
|
||||||
@ -40,6 +47,7 @@ func ErrorProgress(err string) string {
|
|||||||
},
|
},
|
||||||
elem.Text("Error "+err),
|
elem.Text("Error "+err),
|
||||||
),
|
),
|
||||||
|
installButton(galleryName),
|
||||||
).Render()
|
).Render()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +75,7 @@ func StartProgressBar(uid, progress, text string) string {
|
|||||||
return elem.Div(attrs.Props{
|
return elem.Div(attrs.Props{
|
||||||
"hx-trigger": "done",
|
"hx-trigger": "done",
|
||||||
"hx-get": "/browse/job/" + uid,
|
"hx-get": "/browse/job/" + uid,
|
||||||
"hx-swap": "outerHTML",
|
"hx-swap": "innerHTML",
|
||||||
"hx-target": "this",
|
"hx-target": "this",
|
||||||
},
|
},
|
||||||
elem.H3(
|
elem.H3(
|
||||||
@ -99,7 +107,119 @@ func cardSpan(text, icon string) elem.Node {
|
|||||||
elem.I(attrs.Props{
|
elem.I(attrs.Props{
|
||||||
"class": icon + " pr-2",
|
"class": icon + " pr-2",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
elem.Text(text),
|
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),
|
// 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 {
|
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 {
|
for _, tag := range m.Tags {
|
||||||
nodes = append(nodes,
|
tagsNodes = append(tagsNodes,
|
||||||
cardSpan(tag, "fas fa-tag"),
|
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 {
|
for i, url := range m.URLs {
|
||||||
nodes = append(nodes,
|
nodes = append(nodes,
|
||||||
elem.A(
|
link("Link #"+fmt.Sprintf("%d", i+1), url),
|
||||||
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)),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return elem.Div(
|
return elem.Div(
|
||||||
@ -224,12 +308,12 @@ func ListModels(models []*gallery.GalleryModel, installing *xsync.SyncedMap[stri
|
|||||||
elem.Raw(StartProgressBar(installing.Get(galleryID), "0", "Installing")),
|
elem.Raw(StartProgressBar(installing.Get(galleryID), "0", "Installing")),
|
||||||
), // Otherwise, show install button (if not installed) or display "Installed"
|
), // Otherwise, show install button (if not installed) or display "Installed"
|
||||||
elem.If(m.Installed,
|
elem.If(m.Installed,
|
||||||
//elem.Node(elem.Div(
|
elem.Node(elem.Div(
|
||||||
// attrs.Props{},
|
attrs.Props{},
|
||||||
// span("Installed"), deleteButton(m),
|
reInstallButton(m.ID()),
|
||||||
// )),
|
deleteButton(m.Name),
|
||||||
deleteButton(m),
|
)),
|
||||||
installButton(m),
|
installButton(m.ID()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -63,7 +63,7 @@ func (mgs *ModelGalleryEndpointService) ApplyModelGalleryEndpoint() func(c *fibe
|
|||||||
mgs.galleryApplier.C <- gallery.GalleryOp{
|
mgs.galleryApplier.C <- gallery.GalleryOp{
|
||||||
Req: input.GalleryModel,
|
Req: input.GalleryModel,
|
||||||
Id: uuid.String(),
|
Id: uuid.String(),
|
||||||
GalleryName: input.ID,
|
GalleryModelName: input.ID,
|
||||||
Galleries: mgs.galleries,
|
Galleries: mgs.galleries,
|
||||||
ConfigURL: input.ConfigURL,
|
ConfigURL: input.ConfigURL,
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ func (mgs *ModelGalleryEndpointService) DeleteModelGalleryEndpoint() func(c *fib
|
|||||||
|
|
||||||
mgs.galleryApplier.C <- gallery.GalleryOp{
|
mgs.galleryApplier.C <- gallery.GalleryOp{
|
||||||
Delete: true,
|
Delete: true,
|
||||||
GalleryName: modelName,
|
GalleryModelName: modelName,
|
||||||
}
|
}
|
||||||
|
|
||||||
uuid, err := uuid.NewUUID()
|
uuid, err := uuid.NewUUID()
|
||||||
|
@ -3,6 +3,7 @@ package localai
|
|||||||
import (
|
import (
|
||||||
"github.com/go-skynet/LocalAI/core/config"
|
"github.com/go-skynet/LocalAI/core/config"
|
||||||
"github.com/go-skynet/LocalAI/internal"
|
"github.com/go-skynet/LocalAI/internal"
|
||||||
|
"github.com/go-skynet/LocalAI/pkg/gallery"
|
||||||
"github.com/go-skynet/LocalAI/pkg/model"
|
"github.com/go-skynet/LocalAI/pkg/model"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
@ -13,11 +14,22 @@ func WelcomeEndpoint(appConfig *config.ApplicationConfig,
|
|||||||
models, _ := ml.ListModels()
|
models, _ := ml.ListModels()
|
||||||
backendConfigs := cl.GetAllBackendConfigs()
|
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{
|
summary := fiber.Map{
|
||||||
"Title": "LocalAI API - " + internal.PrintableVersion(),
|
"Title": "LocalAI API - " + internal.PrintableVersion(),
|
||||||
"Version": internal.PrintableVersion(),
|
"Version": internal.PrintableVersion(),
|
||||||
"Models": models,
|
"Models": models,
|
||||||
"ModelsConfig": backendConfigs,
|
"ModelsConfig": backendConfigs,
|
||||||
|
"GalleryConfig": galleryConfigs,
|
||||||
"ApplicationConfig": appConfig,
|
"ApplicationConfig": appConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package routes
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-skynet/LocalAI/core/config"
|
"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 {
|
app.Get("/browse", auth, func(c *fiber.Ctx) error {
|
||||||
models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath)
|
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{
|
summary := fiber.Map{
|
||||||
"Title": "LocalAI - Models",
|
"Title": "LocalAI - Models",
|
||||||
"Version": internal.PrintableVersion(),
|
"Version": internal.PrintableVersion(),
|
||||||
"Models": template.HTML(elements.ListModels(models, installingModels)),
|
"Models": template.HTML(elements.ListModels(models, installingModels)),
|
||||||
"Repositories": appConfig.Galleries,
|
"Repositories": appConfig.Galleries,
|
||||||
|
"AllTags": tags,
|
||||||
// "ApplicationConfig": appConfig,
|
// "ApplicationConfig": appConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +107,7 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
|
|
||||||
op := gallery.GalleryOp{
|
op := gallery.GalleryOp{
|
||||||
Id: uid,
|
Id: uid,
|
||||||
GalleryName: galleryID,
|
GalleryModelName: galleryID,
|
||||||
Galleries: appConfig.Galleries,
|
Galleries: appConfig.Galleries,
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
@ -120,10 +134,11 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
op := gallery.GalleryOp{
|
op := gallery.GalleryOp{
|
||||||
Id: uid,
|
Id: uid,
|
||||||
Delete: true,
|
Delete: true,
|
||||||
GalleryName: galleryID,
|
GalleryModelName: galleryID,
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
galleryService.C <- op
|
galleryService.C <- op
|
||||||
|
cl.RemoveBackendConfig(galleryID)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return c.SendString(elements.StartProgressBar(uid, "0", "Deletion"))
|
return c.SendString(elements.StartProgressBar(uid, "0", "Deletion"))
|
||||||
@ -146,7 +161,7 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
return c.SendString(elements.ProgressBar("100"))
|
return c.SendString(elements.ProgressBar("100"))
|
||||||
}
|
}
|
||||||
if status.Error != nil {
|
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)))
|
return c.SendString(elements.ProgressBar(fmt.Sprint(status.Progress)))
|
||||||
@ -158,18 +173,22 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
|
|
||||||
status := galleryService.GetStatus(c.Params("uid"))
|
status := galleryService.GetStatus(c.Params("uid"))
|
||||||
|
|
||||||
|
galleryID := ""
|
||||||
for _, k := range installingModels.Keys() {
|
for _, k := range installingModels.Keys() {
|
||||||
if installingModels.Get(k) == c.Params("uid") {
|
if installingModels.Get(k) == c.Params("uid") {
|
||||||
|
galleryID = k
|
||||||
installingModels.Delete(k)
|
installingModels.Delete(k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showDelete := true
|
||||||
displayText := "Installation completed"
|
displayText := "Installation completed"
|
||||||
if status.Deletion {
|
if status.Deletion {
|
||||||
|
showDelete = false
|
||||||
displayText = "Deletion completed"
|
displayText = "Deletion completed"
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.SendString(elements.DoneProgress(c.Params("uid"), displayText))
|
return c.SendString(elements.DoneProgress(galleryID, displayText, showDelete))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Show the Chat page
|
// Show the Chat page
|
||||||
@ -191,7 +210,8 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
backendConfigs := cl.GetAllBackendConfigs()
|
backendConfigs := cl.GetAllBackendConfigs()
|
||||||
|
|
||||||
if len(backendConfigs) == 0 {
|
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{
|
summary := fiber.Map{
|
||||||
@ -224,7 +244,8 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
backendConfigs := cl.GetAllBackendConfigs()
|
backendConfigs := cl.GetAllBackendConfigs()
|
||||||
|
|
||||||
if len(backendConfigs) == 0 {
|
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{
|
summary := fiber.Map{
|
||||||
@ -257,7 +278,8 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
backendConfigs := cl.GetAllBackendConfigs()
|
backendConfigs := cl.GetAllBackendConfigs()
|
||||||
|
|
||||||
if len(backendConfigs) == 0 {
|
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{
|
summary := fiber.Map{
|
||||||
|
@ -20,12 +20,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="models mt-12">
|
<div class="models mt-12">
|
||||||
|
{{ if eq (len .ModelsConfig) 0 }}
|
||||||
|
<h2 class="text-center text-3xl font-semibold text-gray-100"> <i class="text-yellow-200 ml-2 fa-solid fa-triangle-exclamation animate-pulse"></i> Ouch! seems you don't have any models installed!</h2>
|
||||||
|
<p class="text-center mt-4 text-xl">..install something from the <a class="text-gray-400 hover:text-white ml-1 px-3 py-2 rounded" href="/browse">🖼️ Gallery</a> or check the <a href="https://localai.io/basics/getting_started/" class="text-gray-400 hover:text-white ml-1 px-3 py-2 rounded"> <i class="fa-solid fa-book"></i> Getting started documentation </a></p>
|
||||||
|
{{ else }}
|
||||||
<h2 class="text-center text-3xl font-semibold text-gray-100">Installed models</h2>
|
<h2 class="text-center text-3xl font-semibold text-gray-100">Installed models</h2>
|
||||||
<p class="text-center mt-4 text-xl">We have {{len .ModelsConfig}} pre-loaded models available.</p>
|
<p class="text-center mt-4 text-xl">We have {{len .ModelsConfig}} pre-loaded models available.</p>
|
||||||
<ul class="mt-8 space-y-4">
|
<ul class="mt-8 space-y-4">
|
||||||
|
{{$galleryConfig:=.GalleryConfig}}
|
||||||
{{ range .ModelsConfig }}
|
{{ range .ModelsConfig }}
|
||||||
|
{{ $cfg:= index $galleryConfig .Name}}
|
||||||
<li class="bg-gray-800 border border-gray-700 p-4 rounded-lg">
|
<li class="bg-gray-800 border border-gray-700 p-4 rounded-lg">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
|
|
||||||
|
<img {{ if $cfg.Icon }}
|
||||||
|
src="{{$cfg.Icon}}"
|
||||||
|
{{ else }}
|
||||||
|
src="https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg"
|
||||||
|
{{ end }}
|
||||||
|
class="rounded-t-lg max-h-24 max-w-24 object-cover mt-3"
|
||||||
|
>
|
||||||
|
|
||||||
<p class="font-bold text-white flex items-center"><i class="fas fa-brain pr-2"></i>{{.Name}}</p>
|
<p class="font-bold text-white flex items-center"><i class="fas fa-brain pr-2"></i>{{.Name}}</p>
|
||||||
{{ if .Backend }}
|
{{ if .Backend }}
|
||||||
<!-- Badge for Backend -->
|
<!-- Badge for Backend -->
|
||||||
@ -37,11 +52,16 @@
|
|||||||
auto
|
auto
|
||||||
</span>
|
</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
data-twe-ripple-color="light" data-twe-ripple-init="" hx-confirm="Are you sure you wish to delete the model?" hx-post="/browse/delete/model/{{.Name}}" hx-swap="outerHTML"><i class="fa-solid fa-cancel pr-2"></i>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Additional details can go here -->
|
<!-- Additional details can go here -->
|
||||||
</li>
|
</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -14,6 +14,54 @@
|
|||||||
<i class="fas fa-circle-info pr-2"></i>
|
<i class="fas fa-circle-info pr-2"></i>
|
||||||
</a></h2>
|
</a></h2>
|
||||||
|
|
||||||
|
<div class="text-center font-semibold text-gray-100">
|
||||||
|
<h2>Filter by type:</h2>
|
||||||
|
<button hx-post="/browse/search/models"
|
||||||
|
class="text-white-500 inline-block bg-blue-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"
|
||||||
|
hx-target="#search-results"
|
||||||
|
hx-vals='{"search": "tts"}'
|
||||||
|
hx-indicator=".htmx-indicator" >TTS</button>
|
||||||
|
<button hx-post="/browse/search/models"
|
||||||
|
class="text-white-500 inline-block bg-blue-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"
|
||||||
|
hx-target="#search-results"
|
||||||
|
hx-vals='{"search": "stablediffusion"}'
|
||||||
|
hx-indicator=".htmx-indicator" >Image generation</button>
|
||||||
|
<button hx-post="/browse/search/models" \
|
||||||
|
class="text-white-500 inline-block bg-blue-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"
|
||||||
|
hx-target="#search-results"
|
||||||
|
hx-vals='{"search": "llm"}'
|
||||||
|
hx-indicator=".htmx-indicator" >Text generation</button>
|
||||||
|
<button hx-post="/browse/search/models"
|
||||||
|
class="text-white-500 inline-block bg-blue-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"
|
||||||
|
hx-target="#search-results"
|
||||||
|
hx-vals='{"search": "multimodal"}'
|
||||||
|
hx-indicator=".htmx-indicator" >Multimodal</button>
|
||||||
|
<button hx-post="/browse/search/models"
|
||||||
|
class="text-white-500 inline-block bg-blue-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"
|
||||||
|
hx-target="#search-results"
|
||||||
|
hx-vals='{"search": "embedding"}'
|
||||||
|
hx-indicator=".htmx-indicator" >Embeddings</button>
|
||||||
|
<button hx-post="/browse/search/models"
|
||||||
|
class="text-white-500 inline-block bg-blue-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"
|
||||||
|
hx-target="#search-results"
|
||||||
|
hx-vals='{"search": "rerank"}'
|
||||||
|
hx-indicator=".htmx-indicator" >Rerankers</button>
|
||||||
|
<button
|
||||||
|
hx-post="/browse/search/models"
|
||||||
|
class="text-white-500 inline-block bg-blue-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"
|
||||||
|
hx-target="#search-results"
|
||||||
|
hx-vals='{"search": "whisper"}'
|
||||||
|
hx-indicator=".htmx-indicator" >Audio transcription</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center text-xs font-semibold text-gray-100">
|
||||||
|
Filter by tags:
|
||||||
|
{{ range .AllTags }}
|
||||||
|
<button hx-post="/browse/search/models" class="text-blue-500" hx-target="#search-results"
|
||||||
|
hx-vals='{"search": "{{.}}"}'
|
||||||
|
hx-indicator=".htmx-indicator" >{{.}}</button>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<span class="htmx-indicator loader"></span>
|
<span class="htmx-indicator loader"></span>
|
||||||
<input class="form-control appearance-none block w-full px-3 py-2 text-base font-normal text-gray-300 pb-2 mb-5 bg-gray-800 bg-clip-padding border border-solid border-gray-600 rounded transition ease-in-out m-0 focus:text-gray-300 focus:bg-gray-900 focus:border-blue-500 focus:outline-none" type="search"
|
<input class="form-control appearance-none block w-full px-3 py-2 text-base font-normal text-gray-300 pb-2 mb-5 bg-gray-800 bg-clip-padding border border-solid border-gray-600 rounded transition ease-in-out m-0 focus:text-gray-300 focus:bg-gray-900 focus:border-blue-500 focus:outline-none" type="search"
|
||||||
|
@ -90,7 +90,7 @@ func (g *GalleryService) Start(c context.Context, cl *config.BackendConfigLoader
|
|||||||
if op.Delete {
|
if op.Delete {
|
||||||
modelConfig := &config.BackendConfig{}
|
modelConfig := &config.BackendConfig{}
|
||||||
// Galleryname is the name of the model in this case
|
// Galleryname is the name of the model in this case
|
||||||
dat, err := os.ReadFile(filepath.Join(g.modelPath, op.GalleryName+".yaml"))
|
dat, err := os.ReadFile(filepath.Join(g.modelPath, op.GalleryModelName+".yaml"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
updateError(err)
|
updateError(err)
|
||||||
continue
|
continue
|
||||||
@ -111,14 +111,14 @@ func (g *GalleryService) Start(c context.Context, cl *config.BackendConfigLoader
|
|||||||
files = append(files, modelConfig.MMProjFileName())
|
files = append(files, modelConfig.MMProjFileName())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = gallery.DeleteModelFromSystem(g.modelPath, op.GalleryName, files)
|
err = gallery.DeleteModelFromSystem(g.modelPath, op.GalleryModelName, files)
|
||||||
} else {
|
} else {
|
||||||
// if the request contains a gallery name, we apply the gallery from the gallery list
|
// if the request contains a gallery name, we apply the gallery from the gallery list
|
||||||
if op.GalleryName != "" {
|
if op.GalleryModelName != "" {
|
||||||
if strings.Contains(op.GalleryName, "@") {
|
if strings.Contains(op.GalleryModelName, "@") {
|
||||||
err = gallery.InstallModelFromGallery(op.Galleries, op.GalleryName, g.modelPath, op.Req, progressCallback)
|
err = gallery.InstallModelFromGallery(op.Galleries, op.GalleryModelName, g.modelPath, op.Req, progressCallback)
|
||||||
} else {
|
} else {
|
||||||
err = gallery.InstallModelFromGalleryByName(op.Galleries, op.GalleryName, g.modelPath, op.Req, progressCallback)
|
err = gallery.InstallModelFromGalleryByName(op.Galleries, op.GalleryModelName, g.modelPath, op.Req, progressCallback)
|
||||||
}
|
}
|
||||||
} else if op.ConfigURL != "" {
|
} else if op.ConfigURL != "" {
|
||||||
startup.PreloadModelsConfigurations(op.ConfigURL, g.modelPath, op.ConfigURL)
|
startup.PreloadModelsConfigurations(op.ConfigURL, g.modelPath, op.ConfigURL)
|
||||||
@ -150,6 +150,7 @@ func (g *GalleryService) Start(c context.Context, cl *config.BackendConfigLoader
|
|||||||
&gallery.GalleryOpStatus{
|
&gallery.GalleryOpStatus{
|
||||||
Deletion: op.Delete,
|
Deletion: op.Delete,
|
||||||
Processed: true,
|
Processed: true,
|
||||||
|
GalleryModelName: op.GalleryModelName,
|
||||||
Message: "completed",
|
Message: "completed",
|
||||||
Progress: 100})
|
Progress: 100})
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,9 @@ func InstallModelFromGallery(galleries []Gallery, name string, basePath string,
|
|||||||
installName = req.Name
|
installName = req.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy the model configuration from the request schema
|
||||||
|
config.URLs = append(config.URLs, model.URLs...)
|
||||||
|
config.Icon = model.Icon
|
||||||
config.Files = append(config.Files, req.AdditionalFiles...)
|
config.Files = append(config.Files, req.AdditionalFiles...)
|
||||||
config.Files = append(config.Files, model.AdditionalFiles...)
|
config.Files = append(config.Files, model.AdditionalFiles...)
|
||||||
|
|
||||||
@ -186,6 +189,12 @@ func getGalleryModels(gallery Gallery, basePath string) ([]*GalleryModel, error)
|
|||||||
return models, nil
|
return models, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetLocalModelConfiguration(basePath string, name string) (*Config, error) {
|
||||||
|
name = strings.ReplaceAll(name, string(os.PathSeparator), "__")
|
||||||
|
galleryFile := filepath.Join(basePath, galleryFileName(name))
|
||||||
|
return ReadConfigFile(galleryFile)
|
||||||
|
}
|
||||||
|
|
||||||
func DeleteModelFromSystem(basePath string, name string, additionalFiles []string) error {
|
func DeleteModelFromSystem(basePath string, name string, additionalFiles []string) error {
|
||||||
// os.PathSeparator is not allowed in model names. Replace them with "__" to avoid conflicts with file paths.
|
// os.PathSeparator is not allowed in model names. Replace them with "__" to avoid conflicts with file paths.
|
||||||
name = strings.ReplaceAll(name, string(os.PathSeparator), "__")
|
name = strings.ReplaceAll(name, string(os.PathSeparator), "__")
|
||||||
@ -228,5 +237,8 @@ func DeleteModelFromSystem(basePath string, name string, additionalFiles []strin
|
|||||||
err = errors.Join(err, fmt.Errorf("failed to remove file %s: %w", configFile, e))
|
err = errors.Join(err, fmt.Errorf("failed to remove file %s: %w", configFile, e))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete gallery config file
|
||||||
|
os.Remove(galleryFile)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -40,8 +40,10 @@ prompt_templates:
|
|||||||
*/
|
*/
|
||||||
// Config is the model configuration which contains all the model details
|
// Config is the model configuration which contains all the model details
|
||||||
// This configuration is read from the gallery endpoint and is used to download and install the model
|
// This configuration is read from the gallery endpoint and is used to download and install the model
|
||||||
|
// It is the internal structure, separated from the request
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Description string `yaml:"description"`
|
Description string `yaml:"description"`
|
||||||
|
Icon string `yaml:"icon"`
|
||||||
License string `yaml:"license"`
|
License string `yaml:"license"`
|
||||||
URLs []string `yaml:"urls"`
|
URLs []string `yaml:"urls"`
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
|
@ -2,7 +2,7 @@ package gallery
|
|||||||
|
|
||||||
type GalleryOp struct {
|
type GalleryOp struct {
|
||||||
Id string
|
Id string
|
||||||
GalleryName string
|
GalleryModelName string
|
||||||
ConfigURL string
|
ConfigURL string
|
||||||
Delete bool
|
Delete bool
|
||||||
|
|
||||||
@ -19,4 +19,5 @@ type GalleryOpStatus struct {
|
|||||||
Progress float64 `json:"progress"`
|
Progress float64 `json:"progress"`
|
||||||
TotalFileSize string `json:"file_size"`
|
TotalFileSize string `json:"file_size"`
|
||||||
DownloadedFileSize string `json:"downloaded_size"`
|
DownloadedFileSize string `json:"downloaded_size"`
|
||||||
|
GalleryModelName string `json:"gallery_model_name"`
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package gallery
|
package gallery
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
// GalleryModel is the struct used to represent a model in the gallery returned by the endpoint.
|
// GalleryModel is the struct used to represent a model in the gallery returned by the endpoint.
|
||||||
// It is used to install the model by resolving the URL and downloading the files.
|
// It is used to install the model by resolving the URL and downloading the files.
|
||||||
// The other fields are used to override the configuration of the model.
|
// The other fields are used to override the configuration of the model.
|
||||||
@ -22,3 +24,7 @@ type GalleryModel struct {
|
|||||||
// Installed is used to indicate if the model is installed or not
|
// Installed is used to indicate if the model is installed or not
|
||||||
Installed bool `json:"installed,omitempty" yaml:"installed,omitempty"`
|
Installed bool `json:"installed,omitempty" yaml:"installed,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m GalleryModel) ID() string {
|
||||||
|
return fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name)
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user