diff --git a/core/http/elements/buttons.go b/core/http/elements/buttons.go
new file mode 100644
index 000000000000..7cfe968ffe8b
--- /dev/null
+++ b/core/http/elements/buttons.go
@@ -0,0 +1,97 @@
+package elements
+
+import (
+ "strings"
+
+ "github.com/chasefleming/elem-go"
+ "github.com/chasefleming/elem-go/attrs"
+ "github.com/mudler/LocalAI/core/gallery"
+)
+
+func installButton(galleryName string) elem.Node {
+ return elem.Button(
+ attrs.Props{
+ "data-twe-ripple-init": "",
+ "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",
+ "hx-swap": "outerHTML",
+ // post the Model ID as param
+ "hx-post": "/browse/install/model/" + galleryName,
+ },
+ elem.I(
+ attrs.Props{
+ "class": "fa-solid fa-download pr-2",
+ },
+ ),
+ elem.Text("Install"),
+ )
+}
+
+func reInstallButton(galleryName string) elem.Node {
+ return elem.Button(
+ attrs.Props{
+ "data-twe-ripple-init": "",
+ "data-twe-ripple-color": "light",
+ "class": "float-right inline-block rounded bg-primary ml-2 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-target": "#action-div-" + dropBadChars(galleryName),
+ "hx-swap": "outerHTML",
+ // post the Model ID as param
+ "hx-post": "/browse/install/model/" + galleryName,
+ },
+ elem.I(
+ attrs.Props{
+ "class": "fa-solid fa-arrow-rotate-right pr-2",
+ },
+ ),
+ elem.Text("Reinstall"),
+ )
+}
+
+func infoButton(m *gallery.GalleryModel) elem.Node {
+ return elem.Button(
+ attrs.Props{
+ "data-twe-ripple-init": "",
+ "data-twe-ripple-color": "light",
+ "class": "float-left 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",
+ "data-modal-target": modalName(m),
+ "data-modal-toggle": modalName(m),
+ },
+ elem.P(
+ attrs.Props{
+ "class": "flex items-center",
+ },
+ elem.I(
+ attrs.Props{
+ "class": "fas fa-info-circle pr-2",
+ },
+ ),
+ elem.Text("Info"),
+ ),
+ )
+}
+
+func deleteButton(galleryID string) elem.Node {
+ return elem.Button(
+ 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-target": "#action-div-" + dropBadChars(galleryID),
+ "hx-swap": "outerHTML",
+ // post the Model ID as param
+ "hx-post": "/browse/delete/model/" + galleryID,
+ },
+ elem.I(
+ attrs.Props{
+ "class": "fa-solid fa-cancel pr-2",
+ },
+ ),
+ elem.Text("Delete"),
+ )
+}
+
+// Javascript/HTMX doesn't like weird IDs
+func dropBadChars(s string) string {
+ return strings.ReplaceAll(s, "@", "__")
+}
diff --git a/core/http/elements/gallery.go b/core/http/elements/gallery.go
index 06076bd9e55f..c9d7a1cb5be2 100644
--- a/core/http/elements/gallery.go
+++ b/core/http/elements/gallery.go
@@ -2,13 +2,11 @@ package elements
import (
"fmt"
- "strings"
"github.com/chasefleming/elem-go"
"github.com/chasefleming/elem-go/attrs"
"github.com/microcosm-cc/bluemonday"
"github.com/mudler/LocalAI/core/gallery"
- "github.com/mudler/LocalAI/core/p2p"
"github.com/mudler/LocalAI/core/services"
)
@@ -16,231 +14,6 @@ const (
noImage = "https://upload.wikimedia.org/wikipedia/commons/6/65/No-Image-Placeholder.svg"
)
-func renderElements(n []elem.Node) string {
- render := ""
- for _, r := range n {
- render += r.Render()
- }
- return render
-}
-
-func DoneProgress(galleryID, text string, showDelete bool) string {
- var modelName = galleryID
- // Split by @ and grab the name
- if strings.Contains(galleryID, "@") {
- modelName = strings.Split(galleryID, "@")[1]
- }
-
- return elem.Div(
- attrs.Props{
- "id": "action-div-" + dropBadChars(galleryID),
- },
- elem.H3(
- attrs.Props{
- "role": "status",
- "id": "pblabel",
- "tabindex": "-1",
- "autofocus": "",
- },
- elem.Text(bluemonday.StrictPolicy().Sanitize(text)),
- ),
- elem.If(showDelete, deleteButton(galleryID, modelName), reInstallButton(galleryID)),
- ).Render()
-}
-
-func ErrorProgress(err, galleryName string) string {
- return elem.Div(
- attrs.Props{},
- elem.H3(
- attrs.Props{
- "role": "status",
- "id": "pblabel",
- "tabindex": "-1",
- "autofocus": "",
- },
- elem.Text("Error "+bluemonday.StrictPolicy().Sanitize(err)),
- ),
- installButton(galleryName),
- ).Render()
-}
-
-func ProgressBar(progress string) string {
- return elem.Div(attrs.Props{
- "class": "progress",
- "role": "progressbar",
- "aria-valuemin": "0",
- "aria-valuemax": "100",
- "aria-valuenow": "0",
- "aria-labelledby": "pblabel",
- },
- elem.Div(attrs.Props{
- "id": "pb",
- "class": "progress-bar",
- "style": "width:" + progress + "%",
- }),
- ).Render()
-}
-
-func P2PNodeStats(nodes []p2p.NodeData) string {
- /*
-
-
Total Workers Detected: {{ len .Nodes }}
- {{ $online := 0 }}
- {{ range .Nodes }}
- {{ if .IsOnline }}
- {{ $online = add $online 1 }}
- {{ end }}
- {{ end }}
-
Total Online Workers: {{$online}}
-
- */
-
- online := 0
- for _, n := range nodes {
- if n.IsOnline() {
- online++
- }
- }
-
- class := "text-green-500"
- if online == 0 {
- class = "text-red-500"
- }
- /*
-
- */
- circle := elem.I(attrs.Props{
- "class": "fas fa-circle animate-pulse " + class + " ml-2 mr-1",
- })
- nodesElements := []elem.Node{
- elem.Span(
- attrs.Props{
- "class": class,
- },
- circle,
- elem.Text(fmt.Sprintf("%d", online)),
- ),
- elem.Span(
- attrs.Props{
- "class": "text-gray-200",
- },
- elem.Text(fmt.Sprintf("/%d", len(nodes))),
- ),
- }
-
- return renderElements(nodesElements)
-}
-
-func P2PNodeBoxes(nodes []p2p.NodeData) string {
- /*
-
-
-
- {{.ID}}
-
-
- Status:
-
-
- {{ if .IsOnline }}Online{{ else }}Offline{{ end }}
-
-
-
- */
-
- nodesElements := []elem.Node{}
-
- for _, n := range nodes {
-
- nodesElements = append(nodesElements,
- elem.Div(
- attrs.Props{
- "class": "bg-gray-700 p-6 rounded-lg shadow-lg text-left",
- },
- elem.P(
- attrs.Props{
- "class": "text-sm text-gray-400 mt-2 flex",
- },
- elem.I(
- attrs.Props{
- "class": "fas fa-desktop text-gray-400 mr-2",
- },
- ),
- elem.Text("Name: "),
- elem.Span(
- attrs.Props{
- "class": "text-gray-200 font-semibold ml-2 mr-1",
- },
- elem.Text(bluemonday.StrictPolicy().Sanitize(n.ID)),
- ),
- elem.Text("Status: "),
- elem.If(
- n.IsOnline(),
- elem.I(
- attrs.Props{
- "class": "fas fa-circle animate-pulse text-green-500 ml-2 mr-1",
- },
- ),
- elem.I(
- attrs.Props{
- "class": "fas fa-circle animate-pulse text-red-500 ml-2 mr-1",
- },
- ),
- ),
- elem.If(
- n.IsOnline(),
- elem.Span(
- attrs.Props{
- "class": "text-green-400",
- },
-
- elem.Text("Online"),
- ),
- elem.Span(
- attrs.Props{
- "class": "text-red-400",
- },
- elem.Text("Offline"),
- ),
- ),
- ),
- ))
- }
-
- return renderElements(nodesElements)
-}
-
-func StartProgressBar(uid, progress, text string) string {
- if progress == "" {
- progress = "0"
- }
- return elem.Div(
- attrs.Props{
- "hx-trigger": "done",
- "hx-get": "/browse/job/" + uid,
- "hx-swap": "outerHTML",
- "hx-target": "this",
- },
- elem.H3(
- attrs.Props{
- "role": "status",
- "id": "pblabel",
- "tabindex": "-1",
- "autofocus": "",
- },
- elem.Text(bluemonday.StrictPolicy().Sanitize(text)), //Perhaps overly defensive
- elem.Div(attrs.Props{
- "hx-get": "/browse/job/progress/" + uid,
- "hx-trigger": "every 600ms",
- "hx-target": "this",
- "hx-swap": "innerHTML",
- },
- elem.Raw(ProgressBar(progress)),
- ),
- ),
- ).Render()
-}
-
func cardSpan(text, icon string) elem.Node {
return elem.Span(
attrs.Props{
@@ -268,7 +41,6 @@ func searchableElement(text, icon string) elem.Node {
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",
@@ -290,7 +62,8 @@ func searchableElement(text, icon string) elem.Node {
)
}
-func link(text, url string) elem.Node {
+/*
+func buttonLink(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",
@@ -303,163 +76,255 @@ func link(text, url string) elem.Node {
elem.Text(bluemonday.StrictPolicy().Sanitize(text)),
)
}
-func installButton(galleryName string) elem.Node {
- return elem.Button(
+*/
+
+func link(text, url string) elem.Node {
+ return elem.A(
attrs.Props{
- "data-twe-ripple-init": "",
- "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",
- "hx-swap": "outerHTML",
- // post the Model ID as param
- "hx-post": "/browse/install/model/" + galleryName,
+ "class": "text-base leading-relaxed text-gray-500 dark:text-gray-400",
+ "href": url,
+ "target": "_blank",
},
- elem.I(
- attrs.Props{
- "class": "fa-solid fa-download pr-2",
- },
- ),
- elem.Text("Install"),
+ elem.I(attrs.Props{
+ "class": "fas fa-link pr-2",
+ }),
+ elem.Text(bluemonday.StrictPolicy().Sanitize(text)),
)
}
-func reInstallButton(galleryName string) elem.Node {
- return elem.Button(
+type ProcessTracker interface {
+ Exists(string) bool
+ Get(string) string
+}
+
+func modalName(m *gallery.GalleryModel) string {
+ return m.Name + "-modal"
+}
+
+func modelDescription(m *gallery.GalleryModel) elem.Node {
+ urls := []elem.Node{}
+ for _, url := range m.URLs {
+ urls = append(urls,
+ elem.Li(attrs.Props{}, link(url, url)),
+ )
+ }
+
+ tagsNodes := []elem.Node{}
+ for _, tag := range m.Tags {
+ tagsNodes = append(tagsNodes,
+ searchableElement(tag, "fas fa-tag"),
+ )
+ }
+
+ return elem.Div(
attrs.Props{
- "data-twe-ripple-init": "",
- "data-twe-ripple-color": "light",
- "class": "float-right inline-block rounded bg-primary ml-2 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-target": "#action-div-" + dropBadChars(galleryName),
- "hx-swap": "outerHTML",
- // post the Model ID as param
- "hx-post": "/browse/install/model/" + galleryName,
+ "class": "p-6 text-surface dark:text-white",
},
- elem.I(
+ elem.H5(
attrs.Props{
- "class": "fa-solid fa-arrow-rotate-right pr-2",
+ "class": "mb-2 text-xl font-bold leading-tight",
},
+ elem.Text(bluemonday.StrictPolicy().Sanitize(m.Name)),
),
- elem.Text("Reinstall"),
- )
-}
-
-func deleteButton(galleryID, modelName string) elem.Node {
- return elem.Button(
- 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-target": "#action-div-" + dropBadChars(galleryID),
- "hx-swap": "outerHTML",
- // post the Model ID as param
- "hx-post": "/browse/delete/model/" + galleryID,
- },
- elem.I(
+ elem.Div( // small description
attrs.Props{
- "class": "fa-solid fa-cancel pr-2",
+ "class": "mb-4 text-sm truncate text-base",
},
+ elem.Text(bluemonday.StrictPolicy().Sanitize(m.Description)),
),
- elem.Text("Delete"),
- )
-}
-
-// Javascript/HTMX doesn't like weird IDs
-func dropBadChars(s string) string {
- return strings.ReplaceAll(s, "@", "__")
-}
-type ProcessTracker interface {
- Exists(string) bool
- Get(string) string
-}
-
-func ListModels(models []*gallery.GalleryModel, processTracker ProcessTracker, galleryService *services.GalleryService) string {
- modelsElements := []elem.Node{}
- descriptionDiv := func(m *gallery.GalleryModel) elem.Node {
- return elem.Div(
+ elem.Div(
attrs.Props{
- "class": "p-6 text-surface dark:text-white",
+ "id": modalName(m),
+ "tabindex": "-1",
+ "aria-hidden": "true",
+ "class": "hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full",
},
- elem.H5(
- attrs.Props{
- "class": "mb-2 text-xl font-bold leading-tight",
- },
- elem.Text(bluemonday.StrictPolicy().Sanitize(m.Name)),
- ),
- elem.P(
+ elem.Div(
attrs.Props{
- "class": "mb-4 text-sm [&:not(:hover)]:truncate text-base",
+ "class": "relative p-4 w-full max-w-2xl max-h-full",
},
- elem.Text(bluemonday.StrictPolicy().Sanitize(m.Description)),
+ elem.Div(
+ attrs.Props{
+ "class": "relative p-4 w-full max-w-2xl max-h-full bg-white rounded-lg shadow dark:bg-gray-700",
+ },
+ // header
+ elem.Div(
+ attrs.Props{
+ "class": "flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600",
+ },
+ elem.H3(
+ attrs.Props{
+ "class": "text-xl font-semibold text-gray-900 dark:text-white",
+ },
+ elem.Text(bluemonday.StrictPolicy().Sanitize(m.Name)),
+ ),
+ elem.Button( // close button
+ attrs.Props{
+ "class": "text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white",
+ "data-modal-hide": modalName(m),
+ },
+ elem.Raw(
+ ``,
+ ),
+ elem.Span(
+ attrs.Props{
+ "class": "sr-only",
+ },
+ elem.Text("Close modal"),
+ ),
+ ),
+ ),
+ // body
+ elem.Div(
+ attrs.Props{
+ "class": "p-4 md:p-5 space-y-4",
+ },
+ elem.Div(
+ attrs.Props{
+ "class": "flex justify-center items-center",
+ },
+ elem.Img(attrs.Props{
+ // "class": "rounded-t-lg object-fit object-center h-96",
+ "class": "lazy rounded-t-lg max-h-48 max-w-96 object-cover mt-3 entered loaded",
+ "src": m.Icon,
+ "loading": "lazy",
+ }),
+ ),
+ elem.P(
+ attrs.Props{
+ "class": "text-base leading-relaxed text-gray-500 dark:text-gray-400",
+ },
+ elem.Text(bluemonday.StrictPolicy().Sanitize(m.Description)),
+ ),
+ elem.Hr(
+ attrs.Props{},
+ ),
+ elem.P(
+ attrs.Props{
+ "class": "text-sm font-semibold text-gray-900 dark:text-white",
+ },
+ elem.Text("Links"),
+ ),
+ elem.Ul(
+ attrs.Props{},
+ urls...,
+ ),
+ elem.If(
+ len(m.Tags) > 0,
+ elem.Div(
+ attrs.Props{},
+ elem.P(
+ attrs.Props{
+ "class": "text-sm mb-5 font-semibold text-gray-900 dark:text-white",
+ },
+ elem.Text("Tags"),
+ ),
+ elem.Div(
+ attrs.Props{
+ "class": "flex flex-row flex-wrap content-center",
+ },
+ tagsNodes...,
+ ),
+ ),
+ elem.Div(attrs.Props{}),
+ ),
+ ),
+ // Footer
+ elem.Div(
+ attrs.Props{
+ "class": "flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600",
+ },
+ elem.Button(
+ attrs.Props{
+ "data-modal-hide": modalName(m),
+ "class": "py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700",
+ },
+ elem.Text("Close"),
+ ),
+ ),
+ ),
),
- )
- }
-
- actionDiv := func(m *gallery.GalleryModel) elem.Node {
- galleryID := fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name)
- currentlyProcessing := processTracker.Exists(galleryID)
- jobID := ""
- isDeletionOp := false
- if currentlyProcessing {
- status := galleryService.GetStatus(galleryID)
- if status != nil && status.Deletion {
- isDeletionOp = true
- }
- jobID = processTracker.Get(galleryID)
- // TODO:
- // case not handled, if status == nil : "Waiting"
- }
-
- nodes := []elem.Node{
- cardSpan("Repository: "+m.Gallery.Name, "fa-brands fa-git-alt"),
- }
+ ),
+ )
+}
- if m.License != "" {
- nodes = append(nodes,
- cardSpan("License: "+m.License, "fas fa-book"),
- )
+func modelActionItems(m *gallery.GalleryModel, processTracker ProcessTracker, galleryService *services.GalleryService) elem.Node {
+ galleryID := fmt.Sprintf("%s@%s", m.Gallery.Name, m.Name)
+ currentlyProcessing := processTracker.Exists(galleryID)
+ jobID := ""
+ isDeletionOp := false
+ if currentlyProcessing {
+ status := galleryService.GetStatus(galleryID)
+ if status != nil && status.Deletion {
+ isDeletionOp = true
}
+ jobID = processTracker.Get(galleryID)
+ // TODO:
+ // case not handled, if status == nil : "Waiting"
+ }
- tagsNodes := []elem.Node{}
- for _, tag := range m.Tags {
- tagsNodes = append(tagsNodes,
- searchableElement(tag, "fas fa-tag"),
- )
- }
+ nodes := []elem.Node{
+ cardSpan("Repository: "+m.Gallery.Name, "fa-brands fa-git-alt"),
+ }
+ if m.License != "" {
nodes = append(nodes,
- elem.Div(
- attrs.Props{
- "class": "flex flex-row flex-wrap content-center",
- },
- tagsNodes...,
- ),
+ cardSpan("License: "+m.License, "fas fa-book"),
)
+ }
+ /*
+ tagsNodes := []elem.Node{}
- for i, url := range m.URLs {
- nodes = append(nodes,
- link("Link #"+fmt.Sprintf("%d", i+1), url),
- )
- }
+ for _, tag := range m.Tags {
+ tagsNodes = append(tagsNodes,
+ searchableElement(tag, "fas fa-tag"),
+ )
+ }
- progressMessage := "Installation"
- if isDeletionOp {
- progressMessage = "Deletion"
- }
- return elem.Div(
+ 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,
+ buttonLink("Link #"+fmt.Sprintf("%d", i+1), url),
+ )
+ }
+ */
+
+ progressMessage := "Installation"
+ if isDeletionOp {
+ progressMessage = "Deletion"
+ }
+
+ return elem.Div(
+ attrs.Props{
+ "class": "px-6 pt-4 pb-2",
+ },
+ elem.P(
attrs.Props{
- "class": "px-6 pt-4 pb-2",
+ "class": "mb-4 text-base",
},
- elem.P(
- attrs.Props{
- "class": "mb-4 text-base",
- },
- nodes...,
- ),
+ nodes...,
+ ),
+ elem.Div(
+ attrs.Props{
+ "id": "action-div-" + dropBadChars(galleryID),
+ "class": "flow-root", // To order buttons left and right
+ },
+ infoButton(m),
elem.Div(
attrs.Props{
- "id": "action-div-" + dropBadChars(galleryID),
+ "class": "float-right",
},
elem.If(
currentlyProcessing,
@@ -470,14 +335,18 @@ func ListModels(models []*gallery.GalleryModel, processTracker ProcessTracker, g
elem.Node(elem.Div(
attrs.Props{},
reInstallButton(m.ID()),
- deleteButton(m.ID(), m.Name),
+ deleteButton(m.ID()),
)),
installButton(m.ID()),
),
),
),
- )
- }
+ ),
+ )
+}
+
+func ListModels(models []*gallery.GalleryModel, processTracker ProcessTracker, galleryService *services.GalleryService) string {
+ modelsElements := []elem.Node{}
for _, m := range models {
elems := []elem.Node{}
@@ -521,7 +390,10 @@ func ListModels(models []*gallery.GalleryModel, processTracker ProcessTracker, g
))
}
- elems = append(elems, descriptionDiv(m), actionDiv(m))
+ elems = append(elems,
+ modelDescription(m),
+ modelActionItems(m, processTracker, galleryService),
+ )
modelsElements = append(modelsElements,
elem.Div(
attrs.Props{
diff --git a/core/http/elements/p2p.go b/core/http/elements/p2p.go
new file mode 100644
index 000000000000..7eb10df51c4a
--- /dev/null
+++ b/core/http/elements/p2p.go
@@ -0,0 +1,147 @@
+package elements
+
+import (
+ "fmt"
+
+ "github.com/chasefleming/elem-go"
+ "github.com/chasefleming/elem-go/attrs"
+ "github.com/microcosm-cc/bluemonday"
+ "github.com/mudler/LocalAI/core/p2p"
+)
+
+func renderElements(n []elem.Node) string {
+ render := ""
+ for _, r := range n {
+ render += r.Render()
+ }
+ return render
+}
+
+func P2PNodeStats(nodes []p2p.NodeData) string {
+ /*
+
+
Total Workers Detected: {{ len .Nodes }}
+ {{ $online := 0 }}
+ {{ range .Nodes }}
+ {{ if .IsOnline }}
+ {{ $online = add $online 1 }}
+ {{ end }}
+ {{ end }}
+
Total Online Workers: {{$online}}
+
+ */
+
+ online := 0
+ for _, n := range nodes {
+ if n.IsOnline() {
+ online++
+ }
+ }
+
+ class := "text-green-500"
+ if online == 0 {
+ class = "text-red-500"
+ }
+ /*
+
+ */
+ circle := elem.I(attrs.Props{
+ "class": "fas fa-circle animate-pulse " + class + " ml-2 mr-1",
+ })
+ nodesElements := []elem.Node{
+ elem.Span(
+ attrs.Props{
+ "class": class,
+ },
+ circle,
+ elem.Text(fmt.Sprintf("%d", online)),
+ ),
+ elem.Span(
+ attrs.Props{
+ "class": "text-gray-200",
+ },
+ elem.Text(fmt.Sprintf("/%d", len(nodes))),
+ ),
+ }
+
+ return renderElements(nodesElements)
+}
+
+func P2PNodeBoxes(nodes []p2p.NodeData) string {
+ /*
+
+
+
+ {{.ID}}
+
+
+ Status:
+
+
+ {{ if .IsOnline }}Online{{ else }}Offline{{ end }}
+
+
+
+ */
+
+ nodesElements := []elem.Node{}
+
+ for _, n := range nodes {
+
+ nodesElements = append(nodesElements,
+ elem.Div(
+ attrs.Props{
+ "class": "bg-gray-700 p-6 rounded-lg shadow-lg text-left",
+ },
+ elem.P(
+ attrs.Props{
+ "class": "text-sm text-gray-400 mt-2 flex",
+ },
+ elem.I(
+ attrs.Props{
+ "class": "fas fa-desktop text-gray-400 mr-2",
+ },
+ ),
+ elem.Text("Name: "),
+ elem.Span(
+ attrs.Props{
+ "class": "text-gray-200 font-semibold ml-2 mr-1",
+ },
+ elem.Text(bluemonday.StrictPolicy().Sanitize(n.ID)),
+ ),
+ elem.Text("Status: "),
+ elem.If(
+ n.IsOnline(),
+ elem.I(
+ attrs.Props{
+ "class": "fas fa-circle animate-pulse text-green-500 ml-2 mr-1",
+ },
+ ),
+ elem.I(
+ attrs.Props{
+ "class": "fas fa-circle animate-pulse text-red-500 ml-2 mr-1",
+ },
+ ),
+ ),
+ elem.If(
+ n.IsOnline(),
+ elem.Span(
+ attrs.Props{
+ "class": "text-green-400",
+ },
+
+ elem.Text("Online"),
+ ),
+ elem.Span(
+ attrs.Props{
+ "class": "text-red-400",
+ },
+ elem.Text("Offline"),
+ ),
+ ),
+ ),
+ ))
+ }
+
+ return renderElements(nodesElements)
+}
diff --git a/core/http/elements/progressbar.go b/core/http/elements/progressbar.go
new file mode 100644
index 000000000000..c9af98d9a5ca
--- /dev/null
+++ b/core/http/elements/progressbar.go
@@ -0,0 +1,89 @@
+package elements
+
+import (
+ "github.com/chasefleming/elem-go"
+ "github.com/chasefleming/elem-go/attrs"
+ "github.com/microcosm-cc/bluemonday"
+)
+
+func DoneProgress(galleryID, text string, showDelete bool) string {
+ return elem.Div(
+ attrs.Props{
+ "id": "action-div-" + dropBadChars(galleryID),
+ },
+ elem.H3(
+ attrs.Props{
+ "role": "status",
+ "id": "pblabel",
+ "tabindex": "-1",
+ "autofocus": "",
+ },
+ elem.Text(bluemonday.StrictPolicy().Sanitize(text)),
+ ),
+ elem.If(showDelete, deleteButton(galleryID), reInstallButton(galleryID)),
+ ).Render()
+}
+
+func ErrorProgress(err, galleryName string) string {
+ return elem.Div(
+ attrs.Props{},
+ elem.H3(
+ attrs.Props{
+ "role": "status",
+ "id": "pblabel",
+ "tabindex": "-1",
+ "autofocus": "",
+ },
+ elem.Text("Error "+bluemonday.StrictPolicy().Sanitize(err)),
+ ),
+ installButton(galleryName),
+ ).Render()
+}
+
+func ProgressBar(progress string) string {
+ return elem.Div(attrs.Props{
+ "class": "progress",
+ "role": "progressbar",
+ "aria-valuemin": "0",
+ "aria-valuemax": "100",
+ "aria-valuenow": "0",
+ "aria-labelledby": "pblabel",
+ },
+ elem.Div(attrs.Props{
+ "id": "pb",
+ "class": "progress-bar",
+ "style": "width:" + progress + "%",
+ }),
+ ).Render()
+}
+
+func StartProgressBar(uid, progress, text string) string {
+ if progress == "" {
+ progress = "0"
+ }
+ return elem.Div(
+ attrs.Props{
+ "hx-trigger": "done",
+ "hx-get": "/browse/job/" + uid,
+ "hx-swap": "outerHTML",
+ "hx-target": "this",
+ },
+ elem.H3(
+ attrs.Props{
+ "role": "status",
+ "id": "pblabel",
+ "tabindex": "-1",
+ "autofocus": "",
+ },
+ elem.Text(bluemonday.StrictPolicy().Sanitize(text)), //Perhaps overly defensive
+ elem.Div(attrs.Props{
+ "hx-get": "/browse/job/progress/" + uid,
+ "hx-trigger": "every 600ms",
+ "hx-target": "this",
+ "hx-swap": "innerHTML",
+ },
+ elem.Raw(ProgressBar(progress)),
+ ),
+ ),
+ ).Render()
+}
diff --git a/core/http/static/assets/flowbite.min.js b/core/http/static/assets/flowbite.min.js
new file mode 100644
index 000000000000..e2c52c2cf3f6
--- /dev/null
+++ b/core/http/static/assets/flowbite.min.js
@@ -0,0 +1,2 @@
+!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("Flowbite",[],e):"object"==typeof exports?exports.Flowbite=e():t.Flowbite=e()}(self,(function(){return function(){"use strict";var t={647:function(t,e,i){i.r(e)},853:function(t,e,i){i.r(e),i.d(e,{afterMain:function(){return w},afterRead:function(){return y},afterWrite:function(){return O},applyStyles:function(){return P},arrow:function(){return Q},auto:function(){return a},basePlacements:function(){return c},beforeMain:function(){return b},beforeRead:function(){return _},beforeWrite:function(){return L},bottom:function(){return o},clippingParents:function(){return u},computeStyles:function(){return it},createPopper:function(){return Pt},createPopperBase:function(){return Ht},createPopperLite:function(){return St},detectOverflow:function(){return mt},end:function(){return l},eventListeners:function(){return ot},flip:function(){return yt},hide:function(){return wt},left:function(){return s},main:function(){return E},modifierPhases:function(){return k},offset:function(){return Lt},placements:function(){return g},popper:function(){return h},popperGenerator:function(){return Tt},popperOffsets:function(){return It},preventOverflow:function(){return Ot},read:function(){return m},reference:function(){return f},right:function(){return r},start:function(){return d},top:function(){return n},variationPlacements:function(){return v},viewport:function(){return p},write:function(){return I}});var n="top",o="bottom",r="right",s="left",a="auto",c=[n,o,r,s],d="start",l="end",u="clippingParents",p="viewport",h="popper",f="reference",v=c.reduce((function(t,e){return t.concat([e+"-"+d,e+"-"+l])}),[]),g=[].concat(c,[a]).reduce((function(t,e){return t.concat([e,e+"-"+d,e+"-"+l])}),[]),_="beforeRead",m="read",y="afterRead",b="beforeMain",E="main",w="afterMain",L="beforeWrite",I="write",O="afterWrite",k=[_,m,y,b,E,w,L,I,O];function x(t){return t?(t.nodeName||"").toLowerCase():null}function A(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function C(t){return t instanceof A(t).Element||t instanceof Element}function T(t){return t instanceof A(t).HTMLElement||t instanceof HTMLElement}function H(t){return"undefined"!=typeof ShadowRoot&&(t instanceof A(t).ShadowRoot||t instanceof ShadowRoot)}var P={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},o=e.elements[t];T(o)&&x(o)&&(Object.assign(o.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?o.removeAttribute(t):o.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],o=e.attributes[t]||{},r=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});T(n)&&x(n)&&(Object.assign(n.style,r),Object.keys(o).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function S(t){return t.split("-")[0]}var j=Math.max,D=Math.min,z=Math.round;function M(){var t=navigator.userAgentData;return null!=t&&t.brands?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function q(){return!/^((?!chrome|android).)*safari/i.test(M())}function V(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),o=1,r=1;e&&T(t)&&(o=t.offsetWidth>0&&z(n.width)/t.offsetWidth||1,r=t.offsetHeight>0&&z(n.height)/t.offsetHeight||1);var s=(C(t)?A(t):window).visualViewport,a=!q()&&i,c=(n.left+(a&&s?s.offsetLeft:0))/o,d=(n.top+(a&&s?s.offsetTop:0))/r,l=n.width/o,u=n.height/r;return{width:l,height:u,top:d,right:c+l,bottom:d+u,left:c,x:c,y:d}}function B(t){var e=V(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function R(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&H(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function W(t){return A(t).getComputedStyle(t)}function F(t){return["table","td","th"].indexOf(x(t))>=0}function K(t){return((C(t)?t.ownerDocument:t.document)||window.document).documentElement}function N(t){return"html"===x(t)?t:t.assignedSlot||t.parentNode||(H(t)?t.host:null)||K(t)}function U(t){return T(t)&&"fixed"!==W(t).position?t.offsetParent:null}function X(t){for(var e=A(t),i=U(t);i&&F(i)&&"static"===W(i).position;)i=U(i);return i&&("html"===x(i)||"body"===x(i)&&"static"===W(i).position)?e:i||function(t){var e=/firefox/i.test(M());if(/Trident/i.test(M())&&T(t)&&"fixed"===W(t).position)return null;var i=N(t);for(H(i)&&(i=i.host);T(i)&&["html","body"].indexOf(x(i))<0;){var n=W(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Y(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function G(t,e,i){return j(t,D(e,i))}function $(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function J(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}var Q={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,a=t.name,d=t.options,l=i.elements.arrow,u=i.modifiersData.popperOffsets,p=S(i.placement),h=Y(p),f=[s,r].indexOf(p)>=0?"height":"width";if(l&&u){var v=function(t,e){return $("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:J(t,c))}(d.padding,i),g=B(l),_="y"===h?n:s,m="y"===h?o:r,y=i.rects.reference[f]+i.rects.reference[h]-u[h]-i.rects.popper[f],b=u[h]-i.rects.reference[h],E=X(l),w=E?"y"===h?E.clientHeight||0:E.clientWidth||0:0,L=y/2-b/2,I=v[_],O=w-g[f]-v[m],k=w/2-g[f]/2+L,x=G(I,k,O),A=h;i.modifiersData[a]=((e={})[A]=x,e.centerOffset=x-k,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&R(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Z(t){return t.split("-")[1]}var tt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function et(t){var e,i=t.popper,a=t.popperRect,c=t.placement,d=t.variation,u=t.offsets,p=t.position,h=t.gpuAcceleration,f=t.adaptive,v=t.roundOffsets,g=t.isFixed,_=u.x,m=void 0===_?0:_,y=u.y,b=void 0===y?0:y,E="function"==typeof v?v({x:m,y:b}):{x:m,y:b};m=E.x,b=E.y;var w=u.hasOwnProperty("x"),L=u.hasOwnProperty("y"),I=s,O=n,k=window;if(f){var x=X(i),C="clientHeight",T="clientWidth";if(x===A(i)&&"static"!==W(x=K(i)).position&&"absolute"===p&&(C="scrollHeight",T="scrollWidth"),c===n||(c===s||c===r)&&d===l)O=o,b-=(g&&x===k&&k.visualViewport?k.visualViewport.height:x[C])-a.height,b*=h?1:-1;if(c===s||(c===n||c===o)&&d===l)I=r,m-=(g&&x===k&&k.visualViewport?k.visualViewport.width:x[T])-a.width,m*=h?1:-1}var H,P=Object.assign({position:p},f&&tt),S=!0===v?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:z(e*n)/n||0,y:z(i*n)/n||0}}({x:m,y:b}):{x:m,y:b};return m=S.x,b=S.y,h?Object.assign({},P,((H={})[O]=L?"0":"",H[I]=w?"0":"",H.transform=(k.devicePixelRatio||1)<=1?"translate("+m+"px, "+b+"px)":"translate3d("+m+"px, "+b+"px, 0)",H)):Object.assign({},P,((e={})[O]=L?b+"px":"",e[I]=w?m+"px":"",e.transform="",e))}var it={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,o=void 0===n||n,r=i.adaptive,s=void 0===r||r,a=i.roundOffsets,c=void 0===a||a,d={placement:S(e.placement),variation:Z(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:o,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,et(Object.assign({},d,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:s,roundOffsets:c})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,et(Object.assign({},d,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:c})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}},nt={passive:!0};var ot={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,o=n.scroll,r=void 0===o||o,s=n.resize,a=void 0===s||s,c=A(e.elements.popper),d=[].concat(e.scrollParents.reference,e.scrollParents.popper);return r&&d.forEach((function(t){t.addEventListener("scroll",i.update,nt)})),a&&c.addEventListener("resize",i.update,nt),function(){r&&d.forEach((function(t){t.removeEventListener("scroll",i.update,nt)})),a&&c.removeEventListener("resize",i.update,nt)}},data:{}},rt={left:"right",right:"left",bottom:"top",top:"bottom"};function st(t){return t.replace(/left|right|bottom|top/g,(function(t){return rt[t]}))}var at={start:"end",end:"start"};function ct(t){return t.replace(/start|end/g,(function(t){return at[t]}))}function dt(t){var e=A(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function lt(t){return V(K(t)).left+dt(t).scrollLeft}function ut(t){var e=W(t),i=e.overflow,n=e.overflowX,o=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+o+n)}function pt(t){return["html","body","#document"].indexOf(x(t))>=0?t.ownerDocument.body:T(t)&&ut(t)?t:pt(N(t))}function ht(t,e){var i;void 0===e&&(e=[]);var n=pt(t),o=n===(null==(i=t.ownerDocument)?void 0:i.body),r=A(n),s=o?[r].concat(r.visualViewport||[],ut(n)?n:[]):n,a=e.concat(s);return o?a:a.concat(ht(N(s)))}function ft(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function vt(t,e,i){return e===p?ft(function(t,e){var i=A(t),n=K(t),o=i.visualViewport,r=n.clientWidth,s=n.clientHeight,a=0,c=0;if(o){r=o.width,s=o.height;var d=q();(d||!d&&"fixed"===e)&&(a=o.offsetLeft,c=o.offsetTop)}return{width:r,height:s,x:a+lt(t),y:c}}(t,i)):C(e)?function(t,e){var i=V(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):ft(function(t){var e,i=K(t),n=dt(t),o=null==(e=t.ownerDocument)?void 0:e.body,r=j(i.scrollWidth,i.clientWidth,o?o.scrollWidth:0,o?o.clientWidth:0),s=j(i.scrollHeight,i.clientHeight,o?o.scrollHeight:0,o?o.clientHeight:0),a=-n.scrollLeft+lt(t),c=-n.scrollTop;return"rtl"===W(o||i).direction&&(a+=j(i.clientWidth,o?o.clientWidth:0)-r),{width:r,height:s,x:a,y:c}}(K(t)))}function gt(t,e,i,n){var o="clippingParents"===e?function(t){var e=ht(N(t)),i=["absolute","fixed"].indexOf(W(t).position)>=0&&T(t)?X(t):t;return C(i)?e.filter((function(t){return C(t)&&R(t,i)&&"body"!==x(t)})):[]}(t):[].concat(e),r=[].concat(o,[i]),s=r[0],a=r.reduce((function(e,i){var o=vt(t,i,n);return e.top=j(o.top,e.top),e.right=D(o.right,e.right),e.bottom=D(o.bottom,e.bottom),e.left=j(o.left,e.left),e}),vt(t,s,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}function _t(t){var e,i=t.reference,a=t.element,c=t.placement,u=c?S(c):null,p=c?Z(c):null,h=i.x+i.width/2-a.width/2,f=i.y+i.height/2-a.height/2;switch(u){case n:e={x:h,y:i.y-a.height};break;case o:e={x:h,y:i.y+i.height};break;case r:e={x:i.x+i.width,y:f};break;case s:e={x:i.x-a.width,y:f};break;default:e={x:i.x,y:i.y}}var v=u?Y(u):null;if(null!=v){var g="y"===v?"height":"width";switch(p){case d:e[v]=e[v]-(i[g]/2-a[g]/2);break;case l:e[v]=e[v]+(i[g]/2-a[g]/2)}}return e}function mt(t,e){void 0===e&&(e={});var i=e,s=i.placement,a=void 0===s?t.placement:s,d=i.strategy,l=void 0===d?t.strategy:d,v=i.boundary,g=void 0===v?u:v,_=i.rootBoundary,m=void 0===_?p:_,y=i.elementContext,b=void 0===y?h:y,E=i.altBoundary,w=void 0!==E&&E,L=i.padding,I=void 0===L?0:L,O=$("number"!=typeof I?I:J(I,c)),k=b===h?f:h,x=t.rects.popper,A=t.elements[w?k:b],T=gt(C(A)?A:A.contextElement||K(t.elements.popper),g,m,l),H=V(t.elements.reference),P=_t({reference:H,element:x,strategy:"absolute",placement:a}),S=ft(Object.assign({},x,P)),j=b===h?S:H,D={top:T.top-j.top+O.top,bottom:j.bottom-T.bottom+O.bottom,left:T.left-j.left+O.left,right:j.right-T.right+O.right},z=t.modifiersData.offset;if(b===h&&z){var M=z[a];Object.keys(D).forEach((function(t){var e=[r,o].indexOf(t)>=0?1:-1,i=[n,o].indexOf(t)>=0?"y":"x";D[t]+=M[i]*e}))}return D}var yt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,l=t.name;if(!e.modifiersData[l]._skip){for(var u=i.mainAxis,p=void 0===u||u,h=i.altAxis,f=void 0===h||h,_=i.fallbackPlacements,m=i.padding,y=i.boundary,b=i.rootBoundary,E=i.altBoundary,w=i.flipVariations,L=void 0===w||w,I=i.allowedAutoPlacements,O=e.options.placement,k=S(O),x=_||(k===O||!L?[st(O)]:function(t){if(S(t)===a)return[];var e=st(t);return[ct(t),e,ct(e)]}(O)),A=[O].concat(x).reduce((function(t,i){return t.concat(S(i)===a?function(t,e){void 0===e&&(e={});var i=e,n=i.placement,o=i.boundary,r=i.rootBoundary,s=i.padding,a=i.flipVariations,d=i.allowedAutoPlacements,l=void 0===d?g:d,u=Z(n),p=u?a?v:v.filter((function(t){return Z(t)===u})):c,h=p.filter((function(t){return l.indexOf(t)>=0}));0===h.length&&(h=p);var f=h.reduce((function(e,i){return e[i]=mt(t,{placement:i,boundary:o,rootBoundary:r,padding:s})[S(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}(e,{placement:i,boundary:y,rootBoundary:b,padding:m,flipVariations:L,allowedAutoPlacements:I}):i)}),[]),C=e.rects.reference,T=e.rects.popper,H=new Map,P=!0,j=A[0],D=0;D=0,B=V?"width":"height",R=mt(e,{placement:z,boundary:y,rootBoundary:b,altBoundary:E,padding:m}),W=V?q?r:s:q?o:n;C[B]>T[B]&&(W=st(W));var F=st(W),K=[];if(p&&K.push(R[M]<=0),f&&K.push(R[W]<=0,R[F]<=0),K.every((function(t){return t}))){j=z,P=!1;break}H.set(z,K)}if(P)for(var N=function(t){var e=A.find((function(e){var i=H.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return j=e,"break"},U=L?3:1;U>0;U--){if("break"===N(U))break}e.placement!==j&&(e.modifiersData[l]._skip=!0,e.placement=j,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function bt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Et(t){return[n,r,o,s].some((function(e){return t[e]>=0}))}var wt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,o=e.rects.popper,r=e.modifiersData.preventOverflow,s=mt(e,{elementContext:"reference"}),a=mt(e,{altBoundary:!0}),c=bt(s,n),d=bt(a,o,r),l=Et(c),u=Et(d);e.modifiersData[i]={referenceClippingOffsets:c,popperEscapeOffsets:d,isReferenceHidden:l,hasPopperEscaped:u},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":l,"data-popper-escaped":u})}};var Lt={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,o=t.name,a=i.offset,c=void 0===a?[0,0]:a,d=g.reduce((function(t,i){return t[i]=function(t,e,i){var o=S(t),a=[s,n].indexOf(o)>=0?-1:1,c="function"==typeof i?i(Object.assign({},e,{placement:t})):i,d=c[0],l=c[1];return d=d||0,l=(l||0)*a,[s,r].indexOf(o)>=0?{x:l,y:d}:{x:d,y:l}}(i,e.rects,c),t}),{}),l=d[e.placement],u=l.x,p=l.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=u,e.modifiersData.popperOffsets.y+=p),e.modifiersData[o]=d}};var It={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=_t({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}};var Ot={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,a=t.name,c=i.mainAxis,l=void 0===c||c,u=i.altAxis,p=void 0!==u&&u,h=i.boundary,f=i.rootBoundary,v=i.altBoundary,g=i.padding,_=i.tether,m=void 0===_||_,y=i.tetherOffset,b=void 0===y?0:y,E=mt(e,{boundary:h,rootBoundary:f,padding:g,altBoundary:v}),w=S(e.placement),L=Z(e.placement),I=!L,O=Y(w),k="x"===O?"y":"x",x=e.modifiersData.popperOffsets,A=e.rects.reference,C=e.rects.popper,T="function"==typeof b?b(Object.assign({},e.rects,{placement:e.placement})):b,H="number"==typeof T?{mainAxis:T,altAxis:T}:Object.assign({mainAxis:0,altAxis:0},T),P=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,z={x:0,y:0};if(x){if(l){var M,q="y"===O?n:s,V="y"===O?o:r,R="y"===O?"height":"width",W=x[O],F=W+E[q],K=W-E[V],N=m?-C[R]/2:0,U=L===d?A[R]:C[R],$=L===d?-C[R]:-A[R],J=e.elements.arrow,Q=m&&J?B(J):{width:0,height:0},tt=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},et=tt[q],it=tt[V],nt=G(0,A[R],Q[R]),ot=I?A[R]/2-N-nt-et-H.mainAxis:U-nt-et-H.mainAxis,rt=I?-A[R]/2+N+nt+it+H.mainAxis:$+nt+it+H.mainAxis,st=e.elements.arrow&&X(e.elements.arrow),at=st?"y"===O?st.clientTop||0:st.clientLeft||0:0,ct=null!=(M=null==P?void 0:P[O])?M:0,dt=W+rt-ct,lt=G(m?D(F,W+ot-ct-at):F,W,m?j(K,dt):K);x[O]=lt,z[O]=lt-W}if(p){var ut,pt="x"===O?n:s,ht="x"===O?o:r,ft=x[k],vt="y"===k?"height":"width",gt=ft+E[pt],_t=ft-E[ht],yt=-1!==[n,s].indexOf(w),bt=null!=(ut=null==P?void 0:P[k])?ut:0,Et=yt?gt:ft-A[vt]-C[vt]-bt+H.altAxis,wt=yt?ft+A[vt]+C[vt]-bt-H.altAxis:_t,Lt=m&&yt?function(t,e,i){var n=G(t,e,i);return n>i?i:n}(Et,ft,wt):G(m?Et:gt,ft,m?wt:_t);x[k]=Lt,z[k]=Lt-ft}e.modifiersData[a]=z}},requiresIfExists:["offset"]};function kt(t,e,i){void 0===i&&(i=!1);var n,o,r=T(e),s=T(e)&&function(t){var e=t.getBoundingClientRect(),i=z(e.width)/t.offsetWidth||1,n=z(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=K(e),c=V(t,s,i),d={scrollLeft:0,scrollTop:0},l={x:0,y:0};return(r||!r&&!i)&&(("body"!==x(e)||ut(a))&&(d=(n=e)!==A(n)&&T(n)?{scrollLeft:(o=n).scrollLeft,scrollTop:o.scrollTop}:dt(n)),T(e)?((l=V(e,!0)).x+=e.clientLeft,l.y+=e.clientTop):a&&(l.x=lt(a))),{x:c.left+d.scrollLeft-l.x,y:c.top+d.scrollTop-l.y,width:c.width,height:c.height}}function xt(t){var e=new Map,i=new Set,n=[];function o(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&o(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||o(t)})),n}var At={placement:"bottom",modifiers:[],strategy:"absolute"};function Ct(){for(var t=arguments.length,e=new Array(t),i=0;it._options.maxValue&&(i.value=t._options.maxValue.toString()),null!==t._options.minValue&&parseInt(i.value)=this._options.maxValue||(this._targetEl.value=(this.getCurrentValue()+1).toString(),this._options.onIncrement(this))},t.prototype.decrement=function(){null!==this._options.minValue&&this.getCurrentValue()<=this._options.minValue||(this._targetEl.value=(this.getCurrentValue()-1).toString(),this._options.onDecrement(this))},t.prototype.updateOnIncrement=function(t){this._options.onIncrement=t},t.prototype.updateOnDecrement=function(t){this._options.onDecrement=t},t}();function c(){document.querySelectorAll("[data-input-counter]").forEach((function(t){var e=t.id,i=document.querySelector('[data-input-counter-increment="'+e+'"]'),n=document.querySelector('[data-input-counter-decrement="'+e+'"]'),r=t.getAttribute("data-input-counter-min"),s=t.getAttribute("data-input-counter-max");t?o.default.instanceExists("InputCounter",t.getAttribute("id"))||new a(t,i||null,n||null,{minValue:r?parseInt(r):null,maxValue:s?parseInt(s):null}):console.error('The target element with id "'.concat(e,'" does not exist. Please check the data-input-counter attribute.'))}))}e.initInputCounters=c,"undefined"!=typeof window&&(window.InputCounter=a,window.initInputCounters=c),e.default=a},16:function(t,e,i){var n=this&&this.__assign||function(){return n=Object.assign||function(t){for(var e,i=1,n=arguments.length;i
+
+
+
+
\ No newline at end of file
diff --git a/embedded/webui_static.yaml b/embedded/webui_static.yaml
index fab448cbf918..8d6912129d1d 100644
--- a/embedded/webui_static.yaml
+++ b/embedded/webui_static.yaml
@@ -56,4 +56,7 @@
sha: "8a9a74f4455f392ec3e7499cfda6097b536bb4b7f1e529a079c3d953c08b54ca"
- filename: "KFOlCnqEu92Fr1MmYUtfBBc9.ttf"
url: "https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmYUtfBBc9.ttf"
- sha: "361a50f8a6c816ba4306c5290b7e487a726e1b4dcc3d8d7e4acf1fc2dae9f551"
\ No newline at end of file
+ sha: "361a50f8a6c816ba4306c5290b7e487a726e1b4dcc3d8d7e4acf1fc2dae9f551"
+- filename: "flowbite.min.js"
+ url: "https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.js"
+ sha: "d2a1a72a4c2399e43c01412b86b9957c4df1845f2e0586607c7e55b9ae949cf8"
\ No newline at end of file