Spaces:
Configuration error
Configuration error
| package routes | |
| import ( | |
| "fmt" | |
| "html/template" | |
| "sort" | |
| "strings" | |
| "github.com/microcosm-cc/bluemonday" | |
| "github.com/mudler/LocalAI/core/config" | |
| "github.com/mudler/LocalAI/core/gallery" | |
| "github.com/mudler/LocalAI/core/http/elements" | |
| "github.com/mudler/LocalAI/core/http/endpoints/localai" | |
| "github.com/mudler/LocalAI/core/p2p" | |
| "github.com/mudler/LocalAI/core/services" | |
| "github.com/mudler/LocalAI/internal" | |
| "github.com/mudler/LocalAI/pkg/model" | |
| "github.com/mudler/LocalAI/pkg/xsync" | |
| "github.com/rs/zerolog/log" | |
| "github.com/gofiber/fiber/v2" | |
| "github.com/google/uuid" | |
| ) | |
| type modelOpCache struct { | |
| status *xsync.SyncedMap[string, string] | |
| } | |
| func NewModelOpCache() *modelOpCache { | |
| return &modelOpCache{ | |
| status: xsync.NewSyncedMap[string, string](), | |
| } | |
| } | |
| func (m *modelOpCache) Set(key string, value string) { | |
| m.status.Set(key, value) | |
| } | |
| func (m *modelOpCache) Get(key string) string { | |
| return m.status.Get(key) | |
| } | |
| func (m *modelOpCache) DeleteUUID(uuid string) { | |
| for _, k := range m.status.Keys() { | |
| if m.status.Get(k) == uuid { | |
| m.status.Delete(k) | |
| } | |
| } | |
| } | |
| func (m *modelOpCache) Map() map[string]string { | |
| return m.status.Map() | |
| } | |
| func (m *modelOpCache) Exists(key string) bool { | |
| return m.status.Exists(key) | |
| } | |
| func RegisterUIRoutes(app *fiber.App, | |
| cl *config.BackendConfigLoader, | |
| ml *model.ModelLoader, | |
| appConfig *config.ApplicationConfig, | |
| galleryService *services.GalleryService) { | |
| // keeps the state of models that are being installed from the UI | |
| var processingModels = NewModelOpCache() | |
| // modelStatus returns the current status of the models being processed (installation or deletion) | |
| // it is called asynchonously from the UI | |
| modelStatus := func() (map[string]string, map[string]string) { | |
| processingModelsData := processingModels.Map() | |
| taskTypes := map[string]string{} | |
| for k, v := range processingModelsData { | |
| status := galleryService.GetStatus(v) | |
| taskTypes[k] = "Installation" | |
| if status != nil && status.Deletion { | |
| taskTypes[k] = "Deletion" | |
| } else if status == nil { | |
| taskTypes[k] = "Waiting" | |
| } | |
| } | |
| return processingModelsData, taskTypes | |
| } | |
| app.Get("/", localai.WelcomeEndpoint(appConfig, cl, ml, modelStatus)) | |
| if p2p.IsP2PEnabled() { | |
| app.Get("/p2p", func(c *fiber.Ctx) error { | |
| summary := fiber.Map{ | |
| "Title": "LocalAI - P2P dashboard", | |
| "Version": internal.PrintableVersion(), | |
| //"Nodes": p2p.GetAvailableNodes(""), | |
| //"FederatedNodes": p2p.GetAvailableNodes(p2p.FederatedID), | |
| "IsP2PEnabled": p2p.IsP2PEnabled(), | |
| "P2PToken": appConfig.P2PToken, | |
| "NetworkID": appConfig.P2PNetworkID, | |
| } | |
| // Render index | |
| return c.Render("views/p2p", summary) | |
| }) | |
| /* show nodes live! */ | |
| app.Get("/p2p/ui/workers", func(c *fiber.Ctx) error { | |
| return c.SendString(elements.P2PNodeBoxes(p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, p2p.WorkerID)))) | |
| }) | |
| app.Get("/p2p/ui/workers-federation", func(c *fiber.Ctx) error { | |
| return c.SendString(elements.P2PNodeBoxes(p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, p2p.FederatedID)))) | |
| }) | |
| app.Get("/p2p/ui/workers-stats", func(c *fiber.Ctx) error { | |
| return c.SendString(elements.P2PNodeStats(p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, p2p.WorkerID)))) | |
| }) | |
| app.Get("/p2p/ui/workers-federation-stats", func(c *fiber.Ctx) error { | |
| return c.SendString(elements.P2PNodeStats(p2p.GetAvailableNodes(p2p.NetworkID(appConfig.P2PNetworkID, p2p.FederatedID)))) | |
| }) | |
| } | |
| if !appConfig.DisableGalleryEndpoint { | |
| // Show the Models page (all models) | |
| app.Get("/browse", func(c *fiber.Ctx) error { | |
| term := c.Query("term") | |
| 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) | |
| if term != "" { | |
| models = gallery.GalleryModels(models).Search(term) | |
| } | |
| // Get model statuses | |
| processingModelsData, taskTypes := modelStatus() | |
| summary := fiber.Map{ | |
| "Title": "LocalAI - Models", | |
| "Version": internal.PrintableVersion(), | |
| "Models": template.HTML(elements.ListModels(models, processingModels, galleryService)), | |
| "Repositories": appConfig.Galleries, | |
| "AllTags": tags, | |
| "ProcessingModels": processingModelsData, | |
| "AvailableModels": len(models), | |
| "IsP2PEnabled": p2p.IsP2PEnabled(), | |
| "TaskTypes": taskTypes, | |
| // "ApplicationConfig": appConfig, | |
| } | |
| // Render index | |
| return c.Render("views/models", summary) | |
| }) | |
| // Show the models, filtered from the user input | |
| // https://htmx.org/examples/active-search/ | |
| app.Post("/browse/search/models", func(c *fiber.Ctx) error { | |
| form := struct { | |
| Search string `form:"search"` | |
| }{} | |
| if err := c.BodyParser(&form); err != nil { | |
| return c.Status(fiber.StatusBadRequest).SendString(bluemonday.StrictPolicy().Sanitize(err.Error())) | |
| } | |
| models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath) | |
| return c.SendString(elements.ListModels(gallery.GalleryModels(models).Search(form.Search), processingModels, galleryService)) | |
| }) | |
| /* | |
| Install routes | |
| */ | |
| // 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/ | |
| app.Post("/browse/install/model/:id", func(c *fiber.Ctx) error { | |
| galleryID := strings.Clone(c.Params("id")) // note: strings.Clone is required for multiple requests! | |
| log.Debug().Msgf("UI job submitted to install : %+v\n", galleryID) | |
| id, err := uuid.NewUUID() | |
| if err != nil { | |
| return err | |
| } | |
| uid := id.String() | |
| processingModels.Set(galleryID, uid) | |
| op := gallery.GalleryOp{ | |
| Id: uid, | |
| GalleryModelName: galleryID, | |
| Galleries: appConfig.Galleries, | |
| } | |
| go func() { | |
| galleryService.C <- op | |
| }() | |
| return c.SendString(elements.StartProgressBar(uid, "0", "Installation")) | |
| }) | |
| // 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/ | |
| app.Post("/browse/delete/model/:id", func(c *fiber.Ctx) error { | |
| galleryID := strings.Clone(c.Params("id")) // note: strings.Clone is required for multiple requests! | |
| log.Debug().Msgf("UI job submitted to delete : %+v\n", galleryID) | |
| var galleryName = galleryID | |
| if strings.Contains(galleryID, "@") { | |
| // if the galleryID contains a @ it means that it's a model from a gallery | |
| // but we want to delete it from the local models which does not need | |
| // a repository ID | |
| galleryName = strings.Split(galleryID, "@")[1] | |
| } | |
| id, err := uuid.NewUUID() | |
| if err != nil { | |
| return err | |
| } | |
| uid := id.String() | |
| // Track the deletion job by galleryID and galleryName | |
| // The GalleryID contains information about the repository, | |
| // while the GalleryName is ONLY the name of the model | |
| processingModels.Set(galleryName, uid) | |
| processingModels.Set(galleryID, uid) | |
| op := gallery.GalleryOp{ | |
| Id: uid, | |
| Delete: true, | |
| GalleryModelName: galleryName, | |
| } | |
| go func() { | |
| galleryService.C <- op | |
| cl.RemoveBackendConfig(galleryName) | |
| }() | |
| return c.SendString(elements.StartProgressBar(uid, "0", "Deletion")) | |
| }) | |
| // Display the job current progress status | |
| // If the job is done, we trigger the /browse/job/:uid route | |
| // https://htmx.org/examples/progress-bar/ | |
| app.Get("/browse/job/progress/:uid", func(c *fiber.Ctx) error { | |
| jobUID := strings.Clone(c.Params("uid")) // note: strings.Clone is required for multiple requests! | |
| status := galleryService.GetStatus(jobUID) | |
| if status == nil { | |
| //fmt.Errorf("could not find any status for ID") | |
| return c.SendString(elements.ProgressBar("0")) | |
| } | |
| if status.Progress == 100 { | |
| c.Set("HX-Trigger", "done") // this triggers /browse/job/:uid (which is when the job is done) | |
| return c.SendString(elements.ProgressBar("100")) | |
| } | |
| if status.Error != nil { | |
| // TODO: instead of deleting the job, we should keep it in the cache and make it dismissable by the user | |
| processingModels.DeleteUUID(jobUID) | |
| return c.SendString(elements.ErrorProgress(status.Error.Error(), status.GalleryModelName)) | |
| } | |
| 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", func(c *fiber.Ctx) error { | |
| jobUID := strings.Clone(c.Params("uid")) // note: strings.Clone is required for multiple requests! | |
| status := galleryService.GetStatus(jobUID) | |
| galleryID := "" | |
| processingModels.DeleteUUID(jobUID) | |
| if galleryID == "" { | |
| log.Debug().Msgf("no processing model found for job : %+v\n", jobUID) | |
| } | |
| log.Debug().Msgf("JOB finished : %+v\n", status) | |
| showDelete := true | |
| displayText := "Installation completed" | |
| if status.Deletion { | |
| showDelete = false | |
| displayText = "Deletion completed" | |
| } | |
| return c.SendString(elements.DoneProgress(galleryID, displayText, showDelete)) | |
| }) | |
| } | |
| // Show the Chat page | |
| app.Get("/chat/:model", func(c *fiber.Ctx) error { | |
| backendConfigs, _ := services.ListModels(cl, ml, config.NoFilterFn, services.SKIP_IF_CONFIGURED) | |
| summary := fiber.Map{ | |
| "Title": "LocalAI - Chat with " + c.Params("model"), | |
| "ModelsConfig": backendConfigs, | |
| "Model": c.Params("model"), | |
| "Version": internal.PrintableVersion(), | |
| "IsP2PEnabled": p2p.IsP2PEnabled(), | |
| } | |
| // Render index | |
| return c.Render("views/chat", summary) | |
| }) | |
| app.Get("/talk/", func(c *fiber.Ctx) error { | |
| backendConfigs, _ := services.ListModels(cl, ml, config.NoFilterFn, services.SKIP_IF_CONFIGURED) | |
| if len(backendConfigs) == 0 { | |
| // If no model is available redirect to the index which suggests how to install models | |
| return c.Redirect("/") | |
| } | |
| summary := fiber.Map{ | |
| "Title": "LocalAI - Talk", | |
| "ModelsConfig": backendConfigs, | |
| "Model": backendConfigs[0], | |
| "IsP2PEnabled": p2p.IsP2PEnabled(), | |
| "Version": internal.PrintableVersion(), | |
| } | |
| // Render index | |
| return c.Render("views/talk", summary) | |
| }) | |
| app.Get("/chat/", func(c *fiber.Ctx) error { | |
| backendConfigs, _ := services.ListModels(cl, ml, config.NoFilterFn, services.SKIP_IF_CONFIGURED) | |
| if len(backendConfigs) == 0 { | |
| // If no model is available redirect to the index which suggests how to install models | |
| return c.Redirect("/") | |
| } | |
| summary := fiber.Map{ | |
| "Title": "LocalAI - Chat with " + backendConfigs[0], | |
| "ModelsConfig": backendConfigs, | |
| "Model": backendConfigs[0], | |
| "Version": internal.PrintableVersion(), | |
| "IsP2PEnabled": p2p.IsP2PEnabled(), | |
| } | |
| // Render index | |
| return c.Render("views/chat", summary) | |
| }) | |
| app.Get("/text2image/:model", func(c *fiber.Ctx) error { | |
| backendConfigs := cl.GetAllBackendConfigs() | |
| summary := fiber.Map{ | |
| "Title": "LocalAI - Generate images with " + c.Params("model"), | |
| "ModelsConfig": backendConfigs, | |
| "Model": c.Params("model"), | |
| "Version": internal.PrintableVersion(), | |
| "IsP2PEnabled": p2p.IsP2PEnabled(), | |
| } | |
| // Render index | |
| return c.Render("views/text2image", summary) | |
| }) | |
| app.Get("/text2image/", func(c *fiber.Ctx) error { | |
| backendConfigs := cl.GetAllBackendConfigs() | |
| if len(backendConfigs) == 0 { | |
| // If no model is available redirect to the index which suggests how to install models | |
| return c.Redirect("/") | |
| } | |
| summary := fiber.Map{ | |
| "Title": "LocalAI - Generate images with " + backendConfigs[0].Name, | |
| "ModelsConfig": backendConfigs, | |
| "Model": backendConfigs[0].Name, | |
| "Version": internal.PrintableVersion(), | |
| "IsP2PEnabled": p2p.IsP2PEnabled(), | |
| } | |
| // Render index | |
| return c.Render("views/text2image", summary) | |
| }) | |
| app.Get("/tts/:model", func(c *fiber.Ctx) error { | |
| backendConfigs := cl.GetAllBackendConfigs() | |
| summary := fiber.Map{ | |
| "Title": "LocalAI - Generate images with " + c.Params("model"), | |
| "ModelsConfig": backendConfigs, | |
| "Model": c.Params("model"), | |
| "Version": internal.PrintableVersion(), | |
| "IsP2PEnabled": p2p.IsP2PEnabled(), | |
| } | |
| // Render index | |
| return c.Render("views/tts", summary) | |
| }) | |
| app.Get("/tts/", func(c *fiber.Ctx) error { | |
| backendConfigs := cl.GetAllBackendConfigs() | |
| if len(backendConfigs) == 0 { | |
| // If no model is available redirect to the index which suggests how to install models | |
| return c.Redirect("/") | |
| } | |
| summary := fiber.Map{ | |
| "Title": "LocalAI - Generate audio with " + backendConfigs[0].Name, | |
| "ModelsConfig": backendConfigs, | |
| "Model": backendConfigs[0].Name, | |
| "IsP2PEnabled": p2p.IsP2PEnabled(), | |
| "Version": internal.PrintableVersion(), | |
| } | |
| // Render index | |
| return c.Render("views/tts", summary) | |
| }) | |
| } | |