Skip to content

Commit 0d8bf91

Browse files
authored
feat: Galleries UI (#2104)
* WIP: add models to webui Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Register routes Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix: don't cache models Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * small fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix: fixup multiple installs (strings.Clone) Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent bd50767 commit 0d8bf91

File tree

20 files changed

+431
-23
lines changed

20 files changed

+431
-23
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
4545
[![tests](https://github.com/go-skynet/LocalAI/actions/workflows/test.yml/badge.svg)](https://github.com/go-skynet/LocalAI/actions/workflows/test.yml)[![Build and Release](https://github.com/go-skynet/LocalAI/actions/workflows/release.yaml/badge.svg)](https://github.com/go-skynet/LocalAI/actions/workflows/release.yaml)[![build container images](https://github.com/go-skynet/LocalAI/actions/workflows/image.yml/badge.svg)](https://github.com/go-skynet/LocalAI/actions/workflows/image.yml)[![Bump dependencies](https://github.com/go-skynet/LocalAI/actions/workflows/bump_deps.yaml/badge.svg)](https://github.com/go-skynet/LocalAI/actions/workflows/bump_deps.yaml)[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/localai)](https://artifacthub.io/packages/search?repo=localai)
4646

47-
**LocalAI** is the free, Open Source OpenAI alternative. LocalAI act as a drop-in replacement REST API that’s compatible with OpenAI (Elevenlabs, Anthropic... ) API specifications for local AI inferencing. It allows you to run LLMs, generate images, audio (and not only) locally or on-prem with consumer grade hardware, supporting multiple model families. Does not require GPU.
47+
**LocalAI** is the free, Open Source OpenAI alternative. LocalAI act as a drop-in replacement REST API that’s compatible with OpenAI (Elevenlabs, Anthropic... ) API specifications for local AI inferencing. It allows you to run LLMs, generate images, audio (and not only) locally or on-prem with consumer grade hardware, supporting multiple model families. Does not require GPU. It is created and maintained by [Ettore Di Giacinto](https://github.com/mudler).
4848

4949
## 🔥🔥 Hot topics / Roadmap
5050

core/config/backend_config.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ func (cl *BackendConfigLoader) Preload(modelPath string) error {
512512
for i, config := range cl.configs {
513513

514514
// Download files and verify their SHA
515-
for _, file := range config.DownloadFiles {
515+
for i, file := range config.DownloadFiles {
516516
log.Debug().Msgf("Checking %q exists and matches SHA", file.Filename)
517517

518518
if err := utils.VerifyPath(file.Filename, modelPath); err != nil {
@@ -521,7 +521,7 @@ func (cl *BackendConfigLoader) Preload(modelPath string) error {
521521
// Create file path
522522
filePath := filepath.Join(modelPath, file.Filename)
523523

524-
if err := downloader.DownloadFile(file.URI, filePath, file.SHA256, status); err != nil {
524+
if err := downloader.DownloadFile(file.URI, filePath, file.SHA256, i, len(config.DownloadFiles), status); err != nil {
525525
return err
526526
}
527527
}
@@ -535,7 +535,7 @@ func (cl *BackendConfigLoader) Preload(modelPath string) error {
535535

536536
// check if file exists
537537
if _, err := os.Stat(filepath.Join(modelPath, md5Name)); errors.Is(err, os.ErrNotExist) {
538-
err := downloader.DownloadFile(modelURL, filepath.Join(modelPath, md5Name), "", status)
538+
err := downloader.DownloadFile(modelURL, filepath.Join(modelPath, md5Name), "", 0, 0, status)
539539
if err != nil {
540540
return err
541541
}

core/http/app.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,14 @@ func App(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *confi
186186
utils.LoadConfig(appConfig.ConfigsDir, openai.AssistantsConfigFile, &openai.Assistants)
187187
utils.LoadConfig(appConfig.ConfigsDir, openai.AssistantsFileConfigFile, &openai.AssistantFiles)
188188

189+
galleryService := services.NewGalleryService(appConfig.ModelPath)
190+
galleryService.Start(appConfig.Context, cl)
191+
189192
routes.RegisterElevenLabsRoutes(app, cl, ml, appConfig, auth)
190-
routes.RegisterLocalAIRoutes(app, cl, ml, appConfig, auth)
193+
routes.RegisterLocalAIRoutes(app, cl, ml, appConfig, galleryService, auth)
191194
routes.RegisterOpenAIRoutes(app, cl, ml, appConfig, auth)
192195
routes.RegisterPagesRoutes(app, cl, ml, appConfig, auth)
196+
routes.RegisterUIRoutes(app, cl, ml, appConfig, galleryService, auth)
193197

194198
// Define a custom 404 handler
195199
// Note: keep this at the bottom!

core/http/elements/gallery.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package elements
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/chasefleming/elem-go"
7+
"github.com/chasefleming/elem-go/attrs"
8+
"github.com/go-skynet/LocalAI/pkg/gallery"
9+
)
10+
11+
func DoneProgress(uid string) string {
12+
return elem.Div(
13+
attrs.Props{},
14+
elem.H3(
15+
attrs.Props{
16+
"role": "status",
17+
"id": "pblabel",
18+
"tabindex": "-1",
19+
"autofocus": "",
20+
},
21+
elem.Text("Installation completed"),
22+
),
23+
).Render()
24+
}
25+
26+
func ErrorProgress(err string) string {
27+
return elem.Div(
28+
attrs.Props{},
29+
elem.H3(
30+
attrs.Props{
31+
"role": "status",
32+
"id": "pblabel",
33+
"tabindex": "-1",
34+
"autofocus": "",
35+
},
36+
elem.Text("Error"+err),
37+
),
38+
).Render()
39+
}
40+
41+
func ProgressBar(progress string) string {
42+
return elem.Div(attrs.Props{
43+
"class": "progress",
44+
"role": "progressbar",
45+
"aria-valuemin": "0",
46+
"aria-valuemax": "100",
47+
"aria-valuenow": "0",
48+
"aria-labelledby": "pblabel",
49+
},
50+
elem.Div(attrs.Props{
51+
"id": "pb",
52+
"class": "progress-bar",
53+
"style": "width:" + progress + "%",
54+
}),
55+
).Render()
56+
}
57+
58+
func StartProgressBar(uid, progress string) string {
59+
if progress == "" {
60+
progress = "0"
61+
}
62+
return elem.Div(attrs.Props{
63+
"hx-trigger": "done",
64+
"hx-get": "/browse/job/" + uid,
65+
"hx-swap": "outerHTML",
66+
"hx-target": "this",
67+
},
68+
elem.H3(
69+
attrs.Props{
70+
"role": "status",
71+
"id": "pblabel",
72+
"tabindex": "-1",
73+
"autofocus": "",
74+
},
75+
elem.Text("Installing"),
76+
// This is a simple example of how to use the HTMLX library to create a progress bar that updates every 600ms.
77+
elem.Div(attrs.Props{
78+
"hx-get": "/browse/job/progress/" + uid,
79+
"hx-trigger": "every 600ms",
80+
"hx-target": "this",
81+
"hx-swap": "innerHTML",
82+
},
83+
elem.Raw(ProgressBar(progress)),
84+
),
85+
),
86+
).Render()
87+
}
88+
89+
func ListModels(models []*gallery.GalleryModel) string {
90+
modelsElements := []elem.Node{}
91+
span := func(s string) elem.Node {
92+
return elem.Span(
93+
attrs.Props{
94+
"class": "float-right inline-block bg-green-500 text-white py-1 px-3 rounded-full text-xs",
95+
},
96+
elem.Text(s),
97+
)
98+
}
99+
installButton := func(m *gallery.GalleryModel) elem.Node {
100+
return elem.Button(
101+
attrs.Props{
102+
"class": "float-right inline-block rounded bg-primary px-6 pb-2 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",
103+
// post the Model ID as param
104+
"hx-post": "/browse/install/model/" + fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name),
105+
},
106+
elem.Text("Install"),
107+
)
108+
}
109+
110+
descriptionDiv := func(m *gallery.GalleryModel) elem.Node {
111+
112+
return elem.Div(
113+
attrs.Props{
114+
"class": "p-6",
115+
},
116+
elem.H5(
117+
attrs.Props{
118+
"class": "mb-2 text-xl font-medium leading-tight",
119+
},
120+
elem.Text(m.Name),
121+
),
122+
elem.P(
123+
attrs.Props{
124+
"class": "mb-4 text-base",
125+
},
126+
elem.Text(m.Description),
127+
),
128+
)
129+
}
130+
131+
actionDiv := func(m *gallery.GalleryModel) elem.Node {
132+
return elem.Div(
133+
attrs.Props{
134+
"class": "px-6 pt-4 pb-2",
135+
},
136+
elem.Span(
137+
attrs.Props{
138+
"class": "inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2",
139+
},
140+
elem.Text("Repository: "+m.Gallery.Name),
141+
),
142+
elem.If(m.Installed, span("Installed"), installButton(m)),
143+
)
144+
}
145+
146+
for _, m := range models {
147+
modelsElements = append(modelsElements,
148+
elem.Div(
149+
attrs.Props{
150+
"class": "me-4 mb-2 block rounded-lg bg-white shadow-secondary-1 dark:bg-gray-800 dark:bg-surface-dark dark:text-white text-surface p-2",
151+
},
152+
elem.Div(
153+
attrs.Props{
154+
"class": "p-6",
155+
},
156+
descriptionDiv(m),
157+
actionDiv(m),
158+
// elem.If(m.Installed, span("Installed"), installButton(m)),
159+
160+
// elem.If(m.Installed, span("Installed"), span("Not Installed")),
161+
),
162+
),
163+
)
164+
}
165+
166+
wrapper := elem.Div(attrs.Props{
167+
"class": "dark grid grid-cols-1 grid-rows-1 md:grid-cols-2 ",
168+
}, modelsElements...)
169+
170+
return wrapper.Render()
171+
}

core/http/endpoints/localai/welcome.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ package localai
33
import (
44
"github.com/go-skynet/LocalAI/core/config"
55
"github.com/go-skynet/LocalAI/internal"
6+
"github.com/go-skynet/LocalAI/pkg/model"
67
"github.com/gofiber/fiber/v2"
78
)
89

910
func WelcomeEndpoint(appConfig *config.ApplicationConfig,
10-
models []string, backendConfigs []config.BackendConfig) func(*fiber.Ctx) error {
11+
cl *config.BackendConfigLoader, ml *model.ModelLoader) func(*fiber.Ctx) error {
1112
return func(c *fiber.Ctx) error {
13+
models, _ := ml.ListModels()
14+
backendConfigs := cl.GetAllBackendConfigs()
15+
1216
summary := fiber.Map{
1317
"Title": "LocalAI API - " + internal.PrintableVersion(),
1418
"Version": internal.PrintableVersion(),

core/http/routes/localai.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@ func RegisterLocalAIRoutes(app *fiber.App,
1414
cl *config.BackendConfigLoader,
1515
ml *model.ModelLoader,
1616
appConfig *config.ApplicationConfig,
17+
galleryService *services.GalleryService,
1718
auth func(*fiber.Ctx) error) {
1819

1920
app.Get("/swagger/*", swagger.HandlerDefault) // default
2021

2122
// LocalAI API endpoints
22-
galleryService := services.NewGalleryService(appConfig.ModelPath)
23-
galleryService.Start(appConfig.Context, cl)
2423

2524
modelGalleryEndpointService := localai.CreateModelGalleryEndpointService(appConfig.Galleries, appConfig.ModelPath, galleryService)
2625
app.Post("/models/apply", auth, modelGalleryEndpointService.ApplyModelGalleryEndpoint())

core/http/routes/ui.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package routes
2+
3+
import (
4+
"fmt"
5+
"html/template"
6+
"strings"
7+
8+
"github.com/go-skynet/LocalAI/core/config"
9+
"github.com/go-skynet/LocalAI/core/http/elements"
10+
"github.com/go-skynet/LocalAI/core/services"
11+
"github.com/go-skynet/LocalAI/pkg/gallery"
12+
"github.com/go-skynet/LocalAI/pkg/model"
13+
"github.com/gofiber/fiber/v2"
14+
"github.com/google/uuid"
15+
)
16+
17+
func RegisterUIRoutes(app *fiber.App,
18+
cl *config.BackendConfigLoader,
19+
ml *model.ModelLoader,
20+
appConfig *config.ApplicationConfig,
21+
galleryService *services.GalleryService,
22+
auth func(*fiber.Ctx) error) {
23+
24+
// Show the Models page
25+
app.Get("/browse", auth, func(c *fiber.Ctx) error {
26+
models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath)
27+
28+
summary := fiber.Map{
29+
"Title": "LocalAI API - Models",
30+
"Models": template.HTML(elements.ListModels(models)),
31+
// "ApplicationConfig": appConfig,
32+
}
33+
34+
// Render index
35+
return c.Render("views/models", summary)
36+
})
37+
38+
// HTMX: return the model details
39+
// https://htmx.org/examples/active-search/
40+
app.Post("/browse/search/models", auth, func(c *fiber.Ctx) error {
41+
form := struct {
42+
Search string `form:"search"`
43+
}{}
44+
if err := c.BodyParser(&form); err != nil {
45+
return c.Status(fiber.StatusBadRequest).SendString(err.Error())
46+
}
47+
48+
models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath)
49+
50+
filteredModels := []*gallery.GalleryModel{}
51+
for _, m := range models {
52+
if strings.Contains(m.Name, form.Search) {
53+
filteredModels = append(filteredModels, m)
54+
}
55+
}
56+
57+
return c.SendString(elements.ListModels(filteredModels))
58+
})
59+
60+
// https://htmx.org/examples/progress-bar/
61+
app.Post("/browse/install/model/:id", auth, func(c *fiber.Ctx) error {
62+
galleryID := strings.Clone(c.Params("id")) // strings.Clone is required!
63+
64+
id, err := uuid.NewUUID()
65+
if err != nil {
66+
return err
67+
}
68+
69+
uid := id.String()
70+
71+
op := gallery.GalleryOp{
72+
Id: uid,
73+
GalleryName: galleryID,
74+
Galleries: appConfig.Galleries,
75+
}
76+
go func() {
77+
galleryService.C <- op
78+
}()
79+
80+
return c.SendString(elements.StartProgressBar(uid, "0"))
81+
})
82+
83+
// https://htmx.org/examples/progress-bar/
84+
app.Get("/browse/job/progress/:uid", auth, func(c *fiber.Ctx) error {
85+
jobUID := c.Params("uid")
86+
87+
status := galleryService.GetStatus(jobUID)
88+
if status == nil {
89+
//fmt.Errorf("could not find any status for ID")
90+
return c.SendString(elements.ProgressBar("0"))
91+
}
92+
93+
if status.Progress == 100 {
94+
c.Set("HX-Trigger", "done")
95+
return c.SendString(elements.ProgressBar("100"))
96+
}
97+
if status.Error != nil {
98+
return c.SendString(elements.ErrorProgress(status.Error.Error()))
99+
}
100+
101+
return c.SendString(elements.ProgressBar(fmt.Sprint(status.Progress)))
102+
})
103+
104+
app.Get("/browse/job/:uid", auth, func(c *fiber.Ctx) error {
105+
return c.SendString(elements.DoneProgress(c.Params("uid")))
106+
})
107+
}

core/http/routes/welcome.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,7 @@ func RegisterPagesRoutes(app *fiber.App,
1313
appConfig *config.ApplicationConfig,
1414
auth func(*fiber.Ctx) error) {
1515

16-
models, _ := ml.ListModels()
17-
backendConfigs := cl.GetAllBackendConfigs()
18-
1916
if !appConfig.DisableWelcomePage {
20-
app.Get("/", auth, localai.WelcomeEndpoint(appConfig, models, backendConfigs))
17+
app.Get("/", auth, localai.WelcomeEndpoint(appConfig, cl, ml))
2118
}
22-
2319
}

0 commit comments

Comments
 (0)