mirror of
https://github.com/mudler/LocalAI.git
synced 2024-12-20 21:23:10 +00:00
feat(gallery): display job status also during navigation (#2151)
* feat(gallery): keep showing progress also when refreshing Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(intel-gpu): better defaults Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat: make it thread-safe 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
030d555995
commit
fb2a05ff43
@ -238,7 +238,13 @@ func (cfg *BackendConfig) SetDefaults(opts ...ConfigLoaderOption) {
|
|||||||
|
|
||||||
if cfg.MMap == nil {
|
if cfg.MMap == nil {
|
||||||
// MMap is enabled by default
|
// MMap is enabled by default
|
||||||
cfg.MMap = &trueV
|
|
||||||
|
// Only exception is for Intel GPUs
|
||||||
|
if os.Getenv("XPU") != "" {
|
||||||
|
cfg.MMap = &falseV
|
||||||
|
} else {
|
||||||
|
cfg.MMap = &trueV
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.MMlock == nil {
|
if cfg.MMlock == nil {
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/chasefleming/elem-go"
|
"github.com/chasefleming/elem-go"
|
||||||
"github.com/chasefleming/elem-go/attrs"
|
"github.com/chasefleming/elem-go/attrs"
|
||||||
"github.com/go-skynet/LocalAI/pkg/gallery"
|
"github.com/go-skynet/LocalAI/pkg/gallery"
|
||||||
|
"github.com/go-skynet/LocalAI/pkg/xsync"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -102,7 +103,8 @@ func cardSpan(text, icon string) elem.Node {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListModels(models []*gallery.GalleryModel) string {
|
func ListModels(models []*gallery.GalleryModel, installing *xsync.SyncedMap[string, string]) string {
|
||||||
|
//StartProgressBar(uid, "0")
|
||||||
modelsElements := []elem.Node{}
|
modelsElements := []elem.Node{}
|
||||||
span := func(s string) elem.Node {
|
span := func(s string) elem.Node {
|
||||||
return elem.Span(
|
return elem.Span(
|
||||||
@ -118,6 +120,7 @@ func ListModels(models []*gallery.GalleryModel) string {
|
|||||||
"data-twe-ripple-init": "",
|
"data-twe-ripple-init": "",
|
||||||
"data-twe-ripple-color": "light",
|
"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",
|
"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
|
// post the Model ID as param
|
||||||
"hx-post": "/browse/install/model/" + fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name),
|
"hx-post": "/browse/install/model/" + fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name),
|
||||||
},
|
},
|
||||||
@ -152,6 +155,9 @@ func ListModels(models []*gallery.GalleryModel) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actionDiv := func(m *gallery.GalleryModel) elem.Node {
|
actionDiv := func(m *gallery.GalleryModel) elem.Node {
|
||||||
|
galleryID := fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name)
|
||||||
|
currentlyInstalling := installing.Exists(galleryID)
|
||||||
|
|
||||||
nodes := []elem.Node{
|
nodes := []elem.Node{
|
||||||
cardSpan("Repository: "+m.Gallery.Name, "fa-brands fa-git-alt"),
|
cardSpan("Repository: "+m.Gallery.Name, "fa-brands fa-git-alt"),
|
||||||
}
|
}
|
||||||
@ -193,7 +199,16 @@ func ListModels(models []*gallery.GalleryModel) string {
|
|||||||
},
|
},
|
||||||
nodes...,
|
nodes...,
|
||||||
),
|
),
|
||||||
elem.If(m.Installed, span("Installed"), installButton(m)),
|
elem.If(
|
||||||
|
currentlyInstalling,
|
||||||
|
elem.Node( // If currently installing, show progress bar
|
||||||
|
elem.Raw(StartProgressBar(installing.Get(galleryID), "0")),
|
||||||
|
), // Otherwise, show install button (if not installed) or display "Installed"
|
||||||
|
elem.If(m.Installed,
|
||||||
|
span("Installed"),
|
||||||
|
installButton(m),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ import (
|
|||||||
"github.com/go-skynet/LocalAI/core/services"
|
"github.com/go-skynet/LocalAI/core/services"
|
||||||
"github.com/go-skynet/LocalAI/pkg/gallery"
|
"github.com/go-skynet/LocalAI/pkg/gallery"
|
||||||
"github.com/go-skynet/LocalAI/pkg/model"
|
"github.com/go-skynet/LocalAI/pkg/model"
|
||||||
|
"github.com/go-skynet/LocalAI/pkg/xsync"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
@ -21,13 +23,16 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
galleryService *services.GalleryService,
|
galleryService *services.GalleryService,
|
||||||
auth func(*fiber.Ctx) error) {
|
auth func(*fiber.Ctx) error) {
|
||||||
|
|
||||||
// Show the Models page
|
// keeps the state of models that are being installed from the UI
|
||||||
|
var installingModels = xsync.NewSyncedMap[string, string]()
|
||||||
|
|
||||||
|
// Show the Models page (all models)
|
||||||
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)
|
||||||
|
|
||||||
summary := fiber.Map{
|
summary := fiber.Map{
|
||||||
"Title": "LocalAI - Models",
|
"Title": "LocalAI - Models",
|
||||||
"Models": template.HTML(elements.ListModels(models)),
|
"Models": template.HTML(elements.ListModels(models, installingModels)),
|
||||||
"Repositories": appConfig.Galleries,
|
"Repositories": appConfig.Galleries,
|
||||||
// "ApplicationConfig": appConfig,
|
// "ApplicationConfig": appConfig,
|
||||||
}
|
}
|
||||||
@ -36,7 +41,7 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
return c.Render("views/models", summary)
|
return c.Render("views/models", summary)
|
||||||
})
|
})
|
||||||
|
|
||||||
// HTMX: return the model details
|
// Show the models, filtered from the user input
|
||||||
// https://htmx.org/examples/active-search/
|
// https://htmx.org/examples/active-search/
|
||||||
app.Post("/browse/search/models", auth, func(c *fiber.Ctx) error {
|
app.Post("/browse/search/models", auth, func(c *fiber.Ctx) error {
|
||||||
form := struct {
|
form := struct {
|
||||||
@ -58,12 +63,13 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.SendString(elements.ListModels(filteredModels))
|
return c.SendString(elements.ListModels(filteredModels, installingModels))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// This route is used when the "Install" button is pressed, we submit here a new job to the gallery service
|
||||||
// https://htmx.org/examples/progress-bar/
|
// https://htmx.org/examples/progress-bar/
|
||||||
app.Post("/browse/install/model/:id", auth, func(c *fiber.Ctx) error {
|
app.Post("/browse/install/model/:id", auth, func(c *fiber.Ctx) error {
|
||||||
galleryID := strings.Clone(c.Params("id")) // strings.Clone is required!
|
galleryID := strings.Clone(c.Params("id")) // note: strings.Clone is required for multiple requests!
|
||||||
|
|
||||||
id, err := uuid.NewUUID()
|
id, err := uuid.NewUUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -72,6 +78,8 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
|
|
||||||
uid := id.String()
|
uid := id.String()
|
||||||
|
|
||||||
|
installingModels.Set(galleryID, uid)
|
||||||
|
|
||||||
op := gallery.GalleryOp{
|
op := gallery.GalleryOp{
|
||||||
Id: uid,
|
Id: uid,
|
||||||
GalleryName: galleryID,
|
GalleryName: galleryID,
|
||||||
@ -84,6 +92,8 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
return c.SendString(elements.StartProgressBar(uid, "0"))
|
return c.SendString(elements.StartProgressBar(uid, "0"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Display the job current progress status
|
||||||
|
// If the job is done, we trigger the /browse/job/:uid route
|
||||||
// https://htmx.org/examples/progress-bar/
|
// https://htmx.org/examples/progress-bar/
|
||||||
app.Get("/browse/job/progress/:uid", auth, func(c *fiber.Ctx) error {
|
app.Get("/browse/job/progress/:uid", auth, func(c *fiber.Ctx) error {
|
||||||
jobUID := c.Params("uid")
|
jobUID := c.Params("uid")
|
||||||
@ -95,7 +105,7 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if status.Progress == 100 {
|
if status.Progress == 100 {
|
||||||
c.Set("HX-Trigger", "done")
|
c.Set("HX-Trigger", "done") // this triggers /browse/job/:uid (which is when the job is done)
|
||||||
return c.SendString(elements.ProgressBar("100"))
|
return c.SendString(elements.ProgressBar("100"))
|
||||||
}
|
}
|
||||||
if status.Error != nil {
|
if status.Error != nil {
|
||||||
@ -105,7 +115,15 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
return c.SendString(elements.ProgressBar(fmt.Sprint(status.Progress)))
|
return c.SendString(elements.ProgressBar(fmt.Sprint(status.Progress)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// this route is hit when the job is done, and we display the
|
||||||
|
// final state (for now just displays "Installation completed")
|
||||||
app.Get("/browse/job/:uid", auth, func(c *fiber.Ctx) error {
|
app.Get("/browse/job/:uid", auth, func(c *fiber.Ctx) error {
|
||||||
|
for _, k := range installingModels.Keys() {
|
||||||
|
if installingModels.Get(k) == c.Params("uid") {
|
||||||
|
installingModels.Delete(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.SendString(elements.DoneProgress(c.Params("uid")))
|
return c.SendString(elements.DoneProgress(c.Params("uid")))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
77
pkg/xsync/map.go
Normal file
77
pkg/xsync/map.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package xsync
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SyncedMap[K comparable, V any] struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
m map[K]V
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSyncedMap[K comparable, V any]() *SyncedMap[K, V] {
|
||||||
|
return &SyncedMap[K, V]{
|
||||||
|
m: make(map[K]V),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SyncedMap[K, V]) Get(key K) V {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
return m.m[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SyncedMap[K, V]) Keys() []K {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
keys := make([]K, 0, len(m.m))
|
||||||
|
for k := range m.m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SyncedMap[K, V]) Values() []V {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
values := make([]V, 0, len(m.m))
|
||||||
|
for _, v := range m.m {
|
||||||
|
values = append(values, v)
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SyncedMap[K, V]) Len() int {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
return len(m.m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SyncedMap[K, V]) Iterate(f func(key K, value V) bool) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
for k, v := range m.m {
|
||||||
|
if !f(k, v) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SyncedMap[K, V]) Set(key K, value V) {
|
||||||
|
m.mu.Lock()
|
||||||
|
m.m[key] = value
|
||||||
|
m.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SyncedMap[K, V]) Delete(key K) {
|
||||||
|
m.mu.Lock()
|
||||||
|
delete(m.m, key)
|
||||||
|
m.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SyncedMap[K, V]) Exists(key K) bool {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
_, ok := m.m[key]
|
||||||
|
return ok
|
||||||
|
}
|
26
pkg/xsync/map_test.go
Normal file
26
pkg/xsync/map_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package xsync_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/go-skynet/LocalAI/pkg/xsync"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("SyncMap", func() {
|
||||||
|
|
||||||
|
Context("Syncmap", func() {
|
||||||
|
It("sets and gets", func() {
|
||||||
|
m := NewSyncedMap[string, string]()
|
||||||
|
m.Set("foo", "bar")
|
||||||
|
Expect(m.Get("foo")).To(Equal("bar"))
|
||||||
|
})
|
||||||
|
It("deletes", func() {
|
||||||
|
m := NewSyncedMap[string, string]()
|
||||||
|
m.Set("foo", "bar")
|
||||||
|
m.Delete("foo")
|
||||||
|
Expect(m.Get("foo")).To(Equal(""))
|
||||||
|
Expect(m.Exists("foo")).To(Equal(false))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
13
pkg/xsync/sync_suite_test.go
Normal file
13
pkg/xsync/sync_suite_test.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package xsync_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSync(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "LocalAI sync test")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user