Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(webui): ux improvements #2247

Merged
merged 11 commits into from
May 6, 2024
6 changes: 6 additions & 0 deletions core/config/backend_config_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
83 changes: 70 additions & 13 deletions core/http/elements/gallery.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,61 @@ 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),
)
}
Expand All @@ -119,6 +174,7 @@ func ListModels(models []*gallery.GalleryModel, installing *xsync.SyncedMap[stri
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
Expand Down Expand Up @@ -187,25 +243,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(
Expand Down
12 changes: 12 additions & 0 deletions core/http/endpoints/localai/welcome.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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,
}

Expand Down
24 changes: 21 additions & 3 deletions core/http/routes/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package routes
import (
"fmt"
"html/template"
"sort"
"strings"

"github.com/go-skynet/LocalAI/core/config"
Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -124,6 +138,7 @@ func RegisterUIRoutes(app *fiber.App,
}
go func() {
galleryService.C <- op
cl.RemoveBackendConfig(galleryID)
}()

return c.SendString(elements.StartProgressBar(uid, "0", "Deletion"))
Expand Down Expand Up @@ -191,7 +206,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{
Expand Down Expand Up @@ -224,7 +240,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{
Expand Down Expand Up @@ -257,7 +274,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{
Expand Down
22 changes: 21 additions & 1 deletion core/http/views/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,31 @@ <h1 class="text-5xl font-bold text-gray-100">Welcome to <i>your</i> LocalAI inst
<p class="mt-4 text-lg">The FOSS alternative to OpenAI, Claude, ...</p>
<a href="https://localai.io" target="_blank" class="mt-4 inline-block bg-blue-500 text-white py-2 px-4 rounded-lg shadow transition duration-300 ease-in-out hover:bg-blue-700 hover:shadow-lg">
<i class="fas fa-book-reader pr-2"></i>Documentation
</a>
</a>
</div>

<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>
<p class="text-center mt-4 text-xl">We have {{len .ModelsConfig}} pre-loaded models available.</p>
<ul class="mt-8 space-y-4">
{{$galleryConfig:=.GalleryConfig}}
{{ range .ModelsConfig }}
{{ $cfg:= index $galleryConfig .Name}}
<li class="bg-gray-800 border border-gray-700 p-4 rounded-lg">
<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>
{{ if .Backend }}
<!-- Badge for Backend -->
Expand All @@ -37,11 +52,16 @@ <h2 class="text-center text-3xl font-semibold text-gray-100">Installed models</h
auto
</span>
{{ 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>
<!-- Additional details can go here -->
</li>
{{ end }}
</ul>
{{ end }}
</div>
</div>

Expand Down
19 changes: 17 additions & 2 deletions core/http/views/models.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,23 @@ <h2 class="text-center text-3xl font-semibold text-gray-100">
🖼️ Available models from <i>{{ len .Repositories }}</i> repositories <a href="https://localai.io/models/" target="_blank" >
<i class="fas fa-circle-info pr-2"></i>
</a></h2>



<div class="text-center font-semibold text-gray-100">
Filter by type:
<button hx-post="/browse/search/models" class="text-blue-500" hx-target="#search-results"
hx-vals='{"search": "tts"}'
hx-indicator=".htmx-indicator" >TTS</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>
<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"
name="search" placeholder="Begin Typing To Search models..."
Expand Down
12 changes: 12 additions & 0 deletions pkg/gallery/gallery.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
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, model.AdditionalFiles...)

Expand Down Expand Up @@ -186,6 +189,12 @@
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 {
// os.PathSeparator is not allowed in model names. Replace them with "__" to avoid conflicts with file paths.
name = strings.ReplaceAll(name, string(os.PathSeparator), "__")
Expand Down Expand Up @@ -228,5 +237,8 @@
err = errors.Join(err, fmt.Errorf("failed to remove file %s: %w", configFile, e))
}

// Delete gallery config file
os.Remove(galleryFile)
Dismissed Show dismissed Hide dismissed

return err
}
2 changes: 2 additions & 0 deletions pkg/gallery/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ prompt_templates:
*/
// 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
// It is the internal structure, separated from the request
type Config struct {
Description string `yaml:"description"`
Icon string `yaml:"icon"`
License string `yaml:"license"`
URLs []string `yaml:"urls"`
Name string `yaml:"name"`
Expand Down
Loading