mirror of
https://github.com/mudler/LocalAI.git
synced 2025-05-09 12:03:15 +00:00
feat(ui): prompt for chat, support vision, enhancements (#2259)
* feat(ui): allow to set system prompt for chat Make also the models in the index clickable, and display as table Fixes #2257 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(vision): support also png with base64 input Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ui): support vision and upload of files Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * display the processed image Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * make trust remote code stand out Signed-off-by: mudler <mudler@localai.io> * feat(ui): track in progress job across index/model gallery Signed-off-by: mudler <mudler@localai.io> * minor fixups 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
02ec546dd6
commit
6559ac11b1
@ -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/core/services"
|
||||||
"github.com/go-skynet/LocalAI/pkg/gallery"
|
"github.com/go-skynet/LocalAI/pkg/gallery"
|
||||||
"github.com/go-skynet/LocalAI/pkg/xsync"
|
"github.com/go-skynet/LocalAI/pkg/xsync"
|
||||||
)
|
)
|
||||||
@ -72,12 +73,13 @@ func StartProgressBar(uid, progress, text string) string {
|
|||||||
if progress == "" {
|
if progress == "" {
|
||||||
progress = "0"
|
progress = "0"
|
||||||
}
|
}
|
||||||
return elem.Div(attrs.Props{
|
return elem.Div(
|
||||||
"hx-trigger": "done",
|
attrs.Props{
|
||||||
"hx-get": "/browse/job/" + uid,
|
"hx-trigger": "done",
|
||||||
"hx-swap": "innerHTML",
|
"hx-get": "/browse/job/" + uid,
|
||||||
"hx-target": "this",
|
"hx-swap": "innerHTML",
|
||||||
},
|
"hx-target": "this",
|
||||||
|
},
|
||||||
elem.H3(
|
elem.H3(
|
||||||
attrs.Props{
|
attrs.Props{
|
||||||
"role": "status",
|
"role": "status",
|
||||||
@ -223,7 +225,7 @@ func deleteButton(modelName string) elem.Node {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListModels(models []*gallery.GalleryModel, installing *xsync.SyncedMap[string, string]) string {
|
func ListModels(models []*gallery.GalleryModel, processing *xsync.SyncedMap[string, string], galleryService *services.GalleryService) string {
|
||||||
//StartProgressBar(uid, "0")
|
//StartProgressBar(uid, "0")
|
||||||
modelsElements := []elem.Node{}
|
modelsElements := []elem.Node{}
|
||||||
// span := func(s string) elem.Node {
|
// span := func(s string) elem.Node {
|
||||||
@ -258,7 +260,15 @@ func ListModels(models []*gallery.GalleryModel, installing *xsync.SyncedMap[stri
|
|||||||
|
|
||||||
actionDiv := func(m *gallery.GalleryModel) elem.Node {
|
actionDiv := func(m *gallery.GalleryModel) elem.Node {
|
||||||
galleryID := fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name)
|
galleryID := fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name)
|
||||||
currentlyInstalling := installing.Exists(galleryID)
|
currentlyProcessing := processing.Exists(galleryID)
|
||||||
|
isDeletionOp := false
|
||||||
|
if currentlyProcessing {
|
||||||
|
status := galleryService.GetStatus(galleryID)
|
||||||
|
if status != nil && status.Deletion {
|
||||||
|
isDeletionOp = true
|
||||||
|
}
|
||||||
|
// if status == nil : "Waiting"
|
||||||
|
}
|
||||||
|
|
||||||
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"),
|
||||||
@ -292,6 +302,11 @@ func ListModels(models []*gallery.GalleryModel, installing *xsync.SyncedMap[stri
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
progressMessage := "Installation"
|
||||||
|
if isDeletionOp {
|
||||||
|
progressMessage = "Deletion"
|
||||||
|
}
|
||||||
|
|
||||||
return elem.Div(
|
return elem.Div(
|
||||||
attrs.Props{
|
attrs.Props{
|
||||||
"class": "px-6 pt-4 pb-2",
|
"class": "px-6 pt-4 pb-2",
|
||||||
@ -303,9 +318,9 @@ func ListModels(models []*gallery.GalleryModel, installing *xsync.SyncedMap[stri
|
|||||||
nodes...,
|
nodes...,
|
||||||
),
|
),
|
||||||
elem.If(
|
elem.If(
|
||||||
currentlyInstalling,
|
currentlyProcessing,
|
||||||
elem.Node( // If currently installing, show progress bar
|
elem.Node( // If currently installing, show progress bar
|
||||||
elem.Raw(StartProgressBar(installing.Get(galleryID), "0", "Installing")),
|
elem.Raw(StartProgressBar(processing.Get(galleryID), "0", progressMessage)),
|
||||||
), // 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(
|
||||||
@ -331,12 +346,6 @@ func ListModels(models []*gallery.GalleryModel, installing *xsync.SyncedMap[stri
|
|||||||
"class": "flex justify-center items-center",
|
"class": "flex justify-center items-center",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, trustRemoteCodeExists := m.Overrides["trust_remote_code"]
|
|
||||||
if trustRemoteCodeExists {
|
|
||||||
// should this be checking for trust_remote_code: false? I don't think we ever use that value.
|
|
||||||
divProperties["class"] = divProperties["class"] + " remote-code"
|
|
||||||
}
|
|
||||||
|
|
||||||
elems = append(elems,
|
elems = append(elems,
|
||||||
|
|
||||||
elem.Div(divProperties,
|
elem.Div(divProperties,
|
||||||
@ -352,6 +361,19 @@ func ListModels(models []*gallery.GalleryModel, installing *xsync.SyncedMap[stri
|
|||||||
),
|
),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
_, trustRemoteCodeExists := m.Overrides["trust_remote_code"]
|
||||||
|
if trustRemoteCodeExists {
|
||||||
|
elems = append(elems, elem.Div(
|
||||||
|
attrs.Props{
|
||||||
|
"class": "flex justify-center items-center bg-red-500 text-white p-2 rounded-lg mt-2",
|
||||||
|
},
|
||||||
|
elem.I(attrs.Props{
|
||||||
|
"class": "fa-solid fa-circle-exclamation pr-2",
|
||||||
|
}),
|
||||||
|
elem.Text("Attention: Trust Remote Code is required for this model"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
elems = append(elems, descriptionDiv(m), actionDiv(m))
|
elems = append(elems, descriptionDiv(m), actionDiv(m))
|
||||||
modelsElements = append(modelsElements,
|
modelsElements = append(modelsElements,
|
||||||
elem.Div(
|
elem.Div(
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func WelcomeEndpoint(appConfig *config.ApplicationConfig,
|
func WelcomeEndpoint(appConfig *config.ApplicationConfig,
|
||||||
cl *config.BackendConfigLoader, ml *model.ModelLoader) func(*fiber.Ctx) error {
|
cl *config.BackendConfigLoader, ml *model.ModelLoader, modelStatus func() (map[string]string, map[string]string)) func(*fiber.Ctx) error {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
models, _ := ml.ListModels()
|
models, _ := ml.ListModels()
|
||||||
backendConfigs := cl.GetAllBackendConfigs()
|
backendConfigs := cl.GetAllBackendConfigs()
|
||||||
@ -24,6 +24,9 @@ func WelcomeEndpoint(appConfig *config.ApplicationConfig,
|
|||||||
galleryConfigs[m.Name] = cfg
|
galleryConfigs[m.Name] = cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get model statuses to display in the UI the operation in progress
|
||||||
|
processingModels, taskTypes := modelStatus()
|
||||||
|
|
||||||
summary := fiber.Map{
|
summary := fiber.Map{
|
||||||
"Title": "LocalAI API - " + internal.PrintableVersion(),
|
"Title": "LocalAI API - " + internal.PrintableVersion(),
|
||||||
"Version": internal.PrintableVersion(),
|
"Version": internal.PrintableVersion(),
|
||||||
@ -31,6 +34,8 @@ func WelcomeEndpoint(appConfig *config.ApplicationConfig,
|
|||||||
"ModelsConfig": backendConfigs,
|
"ModelsConfig": backendConfigs,
|
||||||
"GalleryConfig": galleryConfigs,
|
"GalleryConfig": galleryConfigs,
|
||||||
"ApplicationConfig": appConfig,
|
"ApplicationConfig": appConfig,
|
||||||
|
"ProcessingModels": processingModels,
|
||||||
|
"TaskTypes": taskTypes,
|
||||||
}
|
}
|
||||||
|
|
||||||
if string(c.Context().Request.Header.ContentType()) == "application/json" || len(c.Accepts("html")) == 0 {
|
if string(c.Context().Request.Header.ContentType()) == "application/json" || len(c.Accepts("html")) == 0 {
|
||||||
|
@ -63,10 +63,14 @@ func getBase64Image(s string) (string, error) {
|
|||||||
return encoded, nil
|
return encoded, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the string instead is prefixed with "data:image/jpeg;base64,", drop it
|
// if the string instead is prefixed with "data:image/...;base64,", drop it
|
||||||
if strings.HasPrefix(s, "data:image/jpeg;base64,") {
|
dropPrefix := []string{"data:image/jpeg;base64,", "data:image/png;base64,"}
|
||||||
return strings.ReplaceAll(s, "data:image/jpeg;base64,", ""), nil
|
for _, prefix := range dropPrefix {
|
||||||
|
if strings.HasPrefix(s, prefix) {
|
||||||
|
return strings.ReplaceAll(s, prefix, ""), nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("not valid string")
|
return "", fmt.Errorf("not valid string")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,7 +185,7 @@ func updateRequestConfig(config *config.BackendConfig, input *schema.OpenAIReque
|
|||||||
input.Messages[i].StringContent = fmt.Sprintf("[img-%d]", index) + input.Messages[i].StringContent
|
input.Messages[i].StringContent = fmt.Sprintf("[img-%d]", index) + input.Messages[i].StringContent
|
||||||
index++
|
index++
|
||||||
} else {
|
} else {
|
||||||
fmt.Print("Failed encoding image", err)
|
log.Error().Msgf("Failed encoding image: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,13 +26,35 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
galleryService *services.GalleryService,
|
galleryService *services.GalleryService,
|
||||||
auth func(*fiber.Ctx) error) {
|
auth func(*fiber.Ctx) error) {
|
||||||
|
|
||||||
app.Get("/", auth, localai.WelcomeEndpoint(appConfig, cl, ml))
|
|
||||||
|
|
||||||
// keeps the state of models that are being installed from the UI
|
// keeps the state of models that are being installed from the UI
|
||||||
var installingModels = xsync.NewSyncedMap[string, string]()
|
var processingModels = xsync.NewSyncedMap[string, string]()
|
||||||
|
|
||||||
|
// 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("/", auth, localai.WelcomeEndpoint(appConfig, cl, ml, modelStatus))
|
||||||
|
|
||||||
// Show the Models page (all models)
|
// Show the Models page (all models)
|
||||||
app.Get("/browse", auth, func(c *fiber.Ctx) error {
|
app.Get("/browse", auth, func(c *fiber.Ctx) error {
|
||||||
|
term := c.Query("term")
|
||||||
|
|
||||||
models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath)
|
models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath)
|
||||||
|
|
||||||
// Get all available tags
|
// Get all available tags
|
||||||
@ -47,12 +69,22 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
tags = append(tags, t)
|
tags = append(tags, t)
|
||||||
}
|
}
|
||||||
sort.Strings(tags)
|
sort.Strings(tags)
|
||||||
|
|
||||||
|
if term != "" {
|
||||||
|
models = gallery.GalleryModels(models).Search(term)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get model statuses
|
||||||
|
processingModelsData, taskTypes := modelStatus()
|
||||||
|
|
||||||
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, processingModels, galleryService)),
|
||||||
"Repositories": appConfig.Galleries,
|
"Repositories": appConfig.Galleries,
|
||||||
"AllTags": tags,
|
"AllTags": tags,
|
||||||
|
"ProcessingModels": processingModelsData,
|
||||||
|
"TaskTypes": taskTypes,
|
||||||
// "ApplicationConfig": appConfig,
|
// "ApplicationConfig": appConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,17 +104,7 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
|
|
||||||
models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath)
|
models, _ := gallery.AvailableGalleryModels(appConfig.Galleries, appConfig.ModelPath)
|
||||||
|
|
||||||
filteredModels := []*gallery.GalleryModel{}
|
return c.SendString(elements.ListModels(gallery.GalleryModels(models).Search(form.Search), processingModels, galleryService))
|
||||||
for _, m := range models {
|
|
||||||
if strings.Contains(m.Name, form.Search) ||
|
|
||||||
strings.Contains(m.Description, form.Search) ||
|
|
||||||
strings.Contains(m.Gallery.Name, form.Search) ||
|
|
||||||
strings.Contains(strings.Join(m.Tags, ","), form.Search) {
|
|
||||||
filteredModels = append(filteredModels, m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.SendString(elements.ListModels(filteredModels, installingModels))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -103,7 +125,7 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
|
|
||||||
uid := id.String()
|
uid := id.String()
|
||||||
|
|
||||||
installingModels.Set(galleryID, uid)
|
processingModels.Set(galleryID, uid)
|
||||||
|
|
||||||
op := gallery.GalleryOp{
|
op := gallery.GalleryOp{
|
||||||
Id: uid,
|
Id: uid,
|
||||||
@ -129,7 +151,7 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
|
|
||||||
uid := id.String()
|
uid := id.String()
|
||||||
|
|
||||||
installingModels.Set(galleryID, uid)
|
processingModels.Set(galleryID, uid)
|
||||||
|
|
||||||
op := gallery.GalleryOp{
|
op := gallery.GalleryOp{
|
||||||
Id: uid,
|
Id: uid,
|
||||||
@ -174,10 +196,10 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
status := galleryService.GetStatus(c.Params("uid"))
|
status := galleryService.GetStatus(c.Params("uid"))
|
||||||
|
|
||||||
galleryID := ""
|
galleryID := ""
|
||||||
for _, k := range installingModels.Keys() {
|
for _, k := range processingModels.Keys() {
|
||||||
if installingModels.Get(k) == c.Params("uid") {
|
if processingModels.Get(k) == c.Params("uid") {
|
||||||
galleryID = k
|
galleryID = k
|
||||||
installingModels.Delete(k)
|
processingModels.Delete(k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,25 +26,48 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function submitKey(event) {
|
function submitKey(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
localStorage.setItem("key", document.getElementById("apiKey").value);
|
localStorage.setItem("key", document.getElementById("apiKey").value);
|
||||||
document.getElementById("apiKey").blur();
|
document.getElementById("apiKey").blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function submitSystemPrompt(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
localStorage.setItem("system_prompt", document.getElementById("systemPrompt").value);
|
||||||
|
document.getElementById("systemPrompt").blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
var image = "";
|
||||||
|
|
||||||
function submitPrompt(event) {
|
function submitPrompt(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const input = document.getElementById("input").value;
|
const input = document.getElementById("input").value;
|
||||||
Alpine.store("chat").add("user", input);
|
Alpine.store("chat").add("user", input, image);
|
||||||
document.getElementById("input").value = "";
|
document.getElementById("input").value = "";
|
||||||
const key = localStorage.getItem("key");
|
const key = localStorage.getItem("key");
|
||||||
|
const systemPrompt = localStorage.getItem("system_prompt");
|
||||||
|
|
||||||
promptGPT(key, input);
|
promptGPT(systemPrompt, key, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readInputImage() {
|
||||||
|
|
||||||
|
if (!this.files || !this.files[0]) return;
|
||||||
|
|
||||||
|
const FR = new FileReader();
|
||||||
|
|
||||||
|
FR.addEventListener("load", function(evt) {
|
||||||
|
image = evt.target.result;
|
||||||
|
});
|
||||||
|
|
||||||
|
FR.readAsDataURL(this.files[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function promptGPT(key, input) {
|
async function promptGPT(systemPrompt, key, input) {
|
||||||
const model = document.getElementById("chat-model").value;
|
const model = document.getElementById("chat-model").value;
|
||||||
// Set class "loader" to the element with "loader" id
|
// Set class "loader" to the element with "loader" id
|
||||||
//document.getElementById("loader").classList.add("loader");
|
//document.getElementById("loader").classList.add("loader");
|
||||||
@ -53,6 +76,72 @@ function submitPrompt(event) {
|
|||||||
document.getElementById("input").disabled = true;
|
document.getElementById("input").disabled = true;
|
||||||
document.getElementById('messages').scrollIntoView(false)
|
document.getElementById('messages').scrollIntoView(false)
|
||||||
|
|
||||||
|
messages = Alpine.store("chat").messages();
|
||||||
|
|
||||||
|
// if systemPrompt isn't empty, push it at the start of messages
|
||||||
|
if (systemPrompt) {
|
||||||
|
messages.unshift({
|
||||||
|
role: "system",
|
||||||
|
content: systemPrompt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop all messages, and check if there are images. If there are, we need to change the content field
|
||||||
|
messages.forEach((message) => {
|
||||||
|
if (message.image) {
|
||||||
|
// The content field now becomes an array
|
||||||
|
message.content = [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": message.content
|
||||||
|
}
|
||||||
|
]
|
||||||
|
message.content.push(
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": message.image,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// remove the image field
|
||||||
|
delete message.image;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// reset the form and the image
|
||||||
|
image = "";
|
||||||
|
document.getElementById("input_image").value = null;
|
||||||
|
document.getElementById("fileName").innerHTML = "";
|
||||||
|
|
||||||
|
// if (image) {
|
||||||
|
// // take the last element content's and add the image
|
||||||
|
// last_message = messages[messages.length - 1]
|
||||||
|
// // The content field now becomes an array
|
||||||
|
// last_message.content = [
|
||||||
|
// {
|
||||||
|
// "type": "text",
|
||||||
|
// "text": last_message.content
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// last_message.content.push(
|
||||||
|
// {
|
||||||
|
// "type": "image_url",
|
||||||
|
// "image_url": {
|
||||||
|
// "url": image,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// // and we replace it in the messages array
|
||||||
|
// messages[messages.length - 1] = last_message
|
||||||
|
|
||||||
|
// // reset the form and the image
|
||||||
|
// image = "";
|
||||||
|
// document.getElementById("input_image").value = null;
|
||||||
|
// document.getElementById("fileName").innerHTML = "";
|
||||||
|
// }
|
||||||
|
|
||||||
// Source: https://stackoverflow.com/a/75751803/11386095
|
// Source: https://stackoverflow.com/a/75751803/11386095
|
||||||
const response = await fetch("/v1/chat/completions", {
|
const response = await fetch("/v1/chat/completions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -62,7 +151,7 @@ function submitPrompt(event) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: model,
|
model: model,
|
||||||
messages: Alpine.store("chat").messages(),
|
messages: messages,
|
||||||
stream: true,
|
stream: true,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -122,12 +211,24 @@ function submitPrompt(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("key").addEventListener("submit", submitKey);
|
document.getElementById("key").addEventListener("submit", submitKey);
|
||||||
|
document.getElementById("system_prompt").addEventListener("submit", submitSystemPrompt);
|
||||||
|
|
||||||
document.getElementById("prompt").addEventListener("submit", submitPrompt);
|
document.getElementById("prompt").addEventListener("submit", submitPrompt);
|
||||||
document.getElementById("input").focus();
|
document.getElementById("input").focus();
|
||||||
|
document.getElementById("input_image").addEventListener("change", readInputImage);
|
||||||
|
|
||||||
const storeKey = localStorage.getItem("key");
|
storeKey = localStorage.getItem("key");
|
||||||
if (storeKey) {
|
if (storeKey) {
|
||||||
document.getElementById("apiKey").value = storeKey;
|
document.getElementById("apiKey").value = storeKey;
|
||||||
|
} else {
|
||||||
|
document.getElementById("apiKey").value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
storesystemPrompt = localStorage.getItem("system_prompt");
|
||||||
|
if (storesystemPrompt) {
|
||||||
|
document.getElementById("systemPrompt").value = storesystemPrompt;
|
||||||
|
} else {
|
||||||
|
document.getElementById("systemPrompt").value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
|
@ -72,16 +72,6 @@ body {
|
|||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remote-code { /* Attempt to make this stand out */
|
|
||||||
outline-style: solid;
|
|
||||||
outline-color: red;
|
|
||||||
outline-width: 0.33rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remote-code::after {
|
|
||||||
content: "\0026A0 Trust Remote Code Required \0026A0"
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style-type: disc; /* Adds bullet points */
|
list-style-type: disc; /* Adds bullet points */
|
||||||
padding-left: 1.25rem; /* Indents the list from the left margin */
|
padding-left: 1.25rem; /* Indents the list from the left margin */
|
||||||
|
@ -62,17 +62,34 @@ SOFTWARE.
|
|||||||
<button @click="component = 'key'" title="Update API key"
|
<button @click="component = 'key'" title="Update API key"
|
||||||
class="m-2 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="m-2 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"
|
||||||
>Set API Key🔑</button>
|
>Set API Key🔑</button>
|
||||||
|
<button @click="component = 'system_prompt'" title="System Prompt"
|
||||||
|
class="m-2 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"
|
||||||
|
>Set system prompt</button>
|
||||||
</div>
|
</div>
|
||||||
<form x-show="component === 'key'" id="key">
|
<form x-show="component === 'key'" id="key">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="apiKey"
|
id="apiKey"
|
||||||
name="apiKey"
|
name="apiKey"
|
||||||
|
class="bg-gray-800 text-white border border-gray-600 focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 rounded-md shadow-sm p-2 appearance-none"
|
||||||
placeholder="OpenAI API Key"
|
placeholder="OpenAI API Key"
|
||||||
x-model.lazy="key"
|
x-model.lazy="key"
|
||||||
/>
|
/>
|
||||||
<button @click="component = 'menu'" type="submit" title="Save API key">
|
<button @click="component = 'menu'" type="submit" title="Save API key">
|
||||||
🔒
|
<i class="fa-solid fa-arrow-right"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form x-show="component === 'system_prompt'" id="system_prompt">
|
||||||
|
<textarea
|
||||||
|
type="text"
|
||||||
|
id="systemPrompt"
|
||||||
|
name="systemPrompt"
|
||||||
|
class="bg-gray-800 text-white border border-gray-600 focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50 rounded-md shadow-sm p-2 appearance-none"
|
||||||
|
placeholder="System prompt"
|
||||||
|
x-model.lazy="system_prompt"
|
||||||
|
></textarea>
|
||||||
|
<button @click="component = 'menu'" type="submit" title="Save Prompt">
|
||||||
|
<i class="fa-solid fa-arrow-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -111,15 +128,19 @@ SOFTWARE.
|
|||||||
<template x-if="message.role === 'assistant'">
|
<template x-if="message.role === 'assistant'">
|
||||||
<div class="p-2 flex-1 rounded" :class="message.role" x-html="message.html"></div>
|
<div class="p-2 flex-1 rounded" :class="message.role" x-html="message.html"></div>
|
||||||
</template>
|
</template>
|
||||||
|
<template x-if="message.image">
|
||||||
|
<img :src="message.image" alt="Image" class="rounded-lg mt-2 h-36 w-36">
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 border-t border-gray-700" x-data="{ inputValue: '', shiftPressed: false }">
|
<div class="p-4 border-t border-gray-700" x-data="{ inputValue: '', shiftPressed: false, fileName: '' }">
|
||||||
<div id="loader" class="my-2 loader" style="display: none;"></div>
|
<div id="loader" class="my-2 loader" style="display: none;"></div>
|
||||||
<input id="chat-model" type="hidden" value="{{.Model}}">
|
<input id="chat-model" type="hidden" value="{{.Model}}">
|
||||||
|
<input id="input_image" type="file" style="display: none;" @change="fileName = $event.target.files[0].name">
|
||||||
<form id="prompt" action="/chat/{{.Model}}" method="get" @submit.prevent="submitPrompt">
|
<form id="prompt" action="/chat/{{.Model}}" method="get" @submit.prevent="submitPrompt">
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<textarea
|
<textarea
|
||||||
@ -134,7 +155,10 @@ SOFTWARE.
|
|||||||
@keydown.enter="if (!shiftPressed) { submitPrompt($event); }"
|
@keydown.enter="if (!shiftPressed) { submitPrompt($event); }"
|
||||||
style="padding-right: 4rem;"
|
style="padding-right: 4rem;"
|
||||||
></textarea>
|
></textarea>
|
||||||
<button type=submit><i class="fa-solid fa-circle-up text-gray-300 absolute right-2 top-3 text-lg p-2 ml-2"></i></button>
|
<span x-text="fileName" id="fileName" class="absolute right-16 top-5 text-gray-300 text-sm mr-2"></span>
|
||||||
|
<button type="button" onclick="document.getElementById('input_image').click()" class="fa-solid fa-paperclip text-gray-300 ml-2 absolute right-10 top-3 text-lg p-2">
|
||||||
|
</button>
|
||||||
|
<button type=submit><i class="fa-solid fa-circle-up text-gray-300 absolute right-2 top-3 text-lg p-2"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -146,7 +170,7 @@ SOFTWARE.
|
|||||||
clear() {
|
clear() {
|
||||||
this.history.length = 0;
|
this.history.length = 0;
|
||||||
},
|
},
|
||||||
add(role, content) {
|
add(role, content, image) {
|
||||||
const N = this.history.length - 1;
|
const N = this.history.length - 1;
|
||||||
if (this.history.length && this.history[N].role === role) {
|
if (this.history.length && this.history[N].role === role) {
|
||||||
this.history[N].content += content;
|
this.history[N].content += content;
|
||||||
@ -167,6 +191,7 @@ SOFTWARE.
|
|||||||
role: role,
|
role: role,
|
||||||
content: content,
|
content: content,
|
||||||
html: c,
|
html: c,
|
||||||
|
image: image,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,6 +216,7 @@ SOFTWARE.
|
|||||||
return {
|
return {
|
||||||
role: message.role,
|
role: message.role,
|
||||||
content: message.content,
|
content: message.content,
|
||||||
|
image: message.image,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -10,38 +10,76 @@
|
|||||||
<div class="container mx-auto px-4 flex-grow">
|
<div class="container mx-auto px-4 flex-grow">
|
||||||
<div class="header text-center py-12">
|
<div class="header text-center py-12">
|
||||||
<h1 class="text-5xl font-bold text-gray-100">Welcome to <i>your</i> LocalAI instance!</h1>
|
<h1 class="text-5xl font-bold text-gray-100">Welcome to <i>your</i> LocalAI instance!</h1>
|
||||||
<div class="mt-6">
|
|
||||||
<!-- Logo can be uncommented and updated with a valid URL -->
|
|
||||||
</div>
|
|
||||||
<p class="mt-4 text-lg">The FOSS alternative to OpenAI, Claude, ...</p>
|
<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">
|
<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
|
<i class="fas fa-book-reader pr-2"></i>Documentation
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="models mt-12">
|
<div class="models mt-4">
|
||||||
|
|
||||||
|
<!-- Show in progress operations-->
|
||||||
|
{{ if .ProcessingModels }}
|
||||||
|
<h3
|
||||||
|
class="mt-4 mb-4 text-center text-3xl font-semibold text-gray-100">Operations in progress</h2>
|
||||||
|
{{end}}
|
||||||
|
{{$taskType:=.TaskTypes}}
|
||||||
|
{{ range $key,$value:=.ProcessingModels }}
|
||||||
|
{{ $op := index $taskType $key}}
|
||||||
|
{{$parts := split "@" $key}}
|
||||||
|
<div class="flex items-center justify-between bg-slate-600 p-2 mb-2 rounded-md">
|
||||||
|
<div class="flex items center">
|
||||||
|
<span class="text-gray-300"><a href="/browse?term={{$parts._1}}"
|
||||||
|
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"
|
||||||
|
>{{$parts._1}}</a> (from the '{{$parts._0}}' repository)</span>
|
||||||
|
</div>
|
||||||
|
<div hx-get="/browse/job/{{$value}}" hx-swap="innerHTML" hx-target="this" hx-trigger="done">
|
||||||
|
<h3 role="status" id="pblabel" >{{$op}}
|
||||||
|
<div hx-get="/browse/job/progress/{{$value}}" hx-trigger="every 600ms" hx-target="this"
|
||||||
|
hx-swap= "innerHTML" ></div></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
<!-- END Show in progress operations-->
|
||||||
|
|
||||||
{{ if eq (len .ModelsConfig) 0 }}
|
{{ 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>
|
<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>
|
<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 }}
|
{{ 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">
|
<table class="table-auto mt-4 w-full text-left text-gray-200">
|
||||||
|
<thead class="text-xs text-gray-400 uppercase bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-2"></th>
|
||||||
|
<th class="px-4 py-2">Model Name</th>
|
||||||
|
<th class="px-4 py-2">Backend</th>
|
||||||
|
<th class="px-4 py-2 float-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{{$galleryConfig:=.GalleryConfig}}
|
{{$galleryConfig:=.GalleryConfig}}
|
||||||
|
{{$noicon:="https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg"}}
|
||||||
{{ range .ModelsConfig }}
|
{{ range .ModelsConfig }}
|
||||||
{{ $cfg:= index $galleryConfig .Name}}
|
{{ $cfg:= index $galleryConfig .Name}}
|
||||||
<li class="bg-gray-800 border border-gray-700 p-4 rounded-lg">
|
<tr class="bg-gray-800 border-b border-gray-700">
|
||||||
<div class="flex justify-between items-center">
|
<td class="px-4 py-3">
|
||||||
|
{{ with $cfg }}
|
||||||
<img {{ if $cfg.Icon }}
|
<img {{ if $cfg.Icon }}
|
||||||
src="{{$cfg.Icon}}"
|
src="{{$cfg.Icon}}"
|
||||||
{{ else }}
|
{{ else }}
|
||||||
src="https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg"
|
src="{{$noicon}}"
|
||||||
{{ end }}
|
{{ end }}
|
||||||
class="rounded-t-lg max-h-24 max-w-24 object-cover mt-3"
|
class="rounded-t-lg max-h-24 max-w-24 object-cover mt-3"
|
||||||
>
|
>
|
||||||
|
{{ else}}
|
||||||
<p class="font-bold text-white flex items-center"><i class="fas fa-brain pr-2"></i>{{.Name}}</p>
|
<img src="{{$noicon}}" class="rounded-t-lg max-h-24 max-w-24 object-cover mt-3">
|
||||||
|
{{ end }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 font-bold">
|
||||||
|
<p class="font-bold text-white flex items-center"><i class="fas fa-brain pr-2"></i><a href="/browse?term={{.Name}}">{{.Name}}</a></p>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 font-bold">
|
||||||
{{ if .Backend }}
|
{{ if .Backend }}
|
||||||
<!-- Badge for Backend -->
|
<!-- Badge for Backend -->
|
||||||
<span class="inline-block bg-blue-500 text-white py-1 px-3 rounded-full text-xs">
|
<span class="inline-block bg-blue-500 text-white py-1 px-3 rounded-full text-xs">
|
||||||
@ -52,16 +90,20 @@
|
|||||||
auto
|
auto
|
||||||
</span>
|
</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="px-4 py-3">
|
||||||
<button
|
<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"
|
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>
|
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>
|
</td>
|
||||||
<!-- Additional details can go here -->
|
|
||||||
</li>
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</tbody>
|
||||||
|
</table>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -63,8 +63,33 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</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"
|
<!-- Show in progress operations-->
|
||||||
|
{{ if .ProcessingModels }}
|
||||||
|
<h2
|
||||||
|
class="mt-4 mb-4 text-center text-3xl font-semibold text-gray-100">Operations in progress</h2>
|
||||||
|
{{end}}
|
||||||
|
{{$taskType:=.TaskTypes}}
|
||||||
|
{{ range $key,$value:=.ProcessingModels }}
|
||||||
|
{{ $op := index $taskType $key}}
|
||||||
|
{{$parts := split "@" $key}}
|
||||||
|
<div class="flex items-center justify-between bg-slate-600 p-2 mb-2 rounded-md">
|
||||||
|
<div class="flex items center">
|
||||||
|
<span class="text-gray-300"><a href="/browse?term={{$parts._1}}"
|
||||||
|
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"
|
||||||
|
>{{$parts._1}}</a> (from the '{{$parts._0}}' repository)</span>
|
||||||
|
</div>
|
||||||
|
<div hx-get="/browse/job/{{$value}}" hx-swap="innerHTML" hx-target="this" hx-trigger="done">
|
||||||
|
<h3 role="status" id="pblabel" >{{$op}}
|
||||||
|
<div hx-get="/browse/job/progress/{{$value}}" hx-trigger="every 600ms" hx-target="this"
|
||||||
|
hx-swap= "innerHTML" ></div></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
<!-- END Show in progress operations-->
|
||||||
|
|
||||||
|
<input class="form-control appearance-none block w-full mt-5 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..."
|
name="search" placeholder="Begin Typing To Search models..."
|
||||||
hx-post="/browse/search/models"
|
hx-post="/browse/search/models"
|
||||||
hx-trigger="input changed delay:500ms, search"
|
hx-trigger="input changed delay:500ms, search"
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package gallery
|
package gallery
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// 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.
|
||||||
@ -28,3 +31,19 @@ type GalleryModel struct {
|
|||||||
func (m GalleryModel) ID() string {
|
func (m GalleryModel) ID() string {
|
||||||
return fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name)
|
return fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GalleryModels []*GalleryModel
|
||||||
|
|
||||||
|
func (gm GalleryModels) Search(term string) GalleryModels {
|
||||||
|
var filteredModels GalleryModels
|
||||||
|
|
||||||
|
for _, m := range gm {
|
||||||
|
if strings.Contains(m.Name, term) ||
|
||||||
|
strings.Contains(m.Description, term) ||
|
||||||
|
strings.Contains(m.Gallery.Name, term) ||
|
||||||
|
strings.Contains(strings.Join(m.Tags, ","), term) {
|
||||||
|
filteredModels = append(filteredModels, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredModels
|
||||||
|
}
|
||||||
|
@ -15,6 +15,12 @@ func NewSyncedMap[K comparable, V any]() *SyncedMap[K, V] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *SyncedMap[K, V]) Map() map[K]V {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
return m.m
|
||||||
|
}
|
||||||
|
|
||||||
func (m *SyncedMap[K, V]) Get(key K) V {
|
func (m *SyncedMap[K, V]) Get(key K) V {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user