mirror of
https://github.com/mudler/LocalAI.git
synced 2025-02-21 09:41:45 +00:00
feat(ux): Add chat, tts, and image-gen pages to the WebUI (#2222)
* feat(webui): Add chat page Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(webui): Add image-gen page Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(webui): Add tts page Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
parent
f7f8b4804b
commit
2c5a46bc34
@ -42,7 +42,7 @@ type RunCMD struct {
|
|||||||
CORSAllowOrigins string `env:"LOCALAI_CORS_ALLOW_ORIGINS,CORS_ALLOW_ORIGINS" group:"api"`
|
CORSAllowOrigins string `env:"LOCALAI_CORS_ALLOW_ORIGINS,CORS_ALLOW_ORIGINS" group:"api"`
|
||||||
UploadLimit int `env:"LOCALAI_UPLOAD_LIMIT,UPLOAD_LIMIT" default:"15" help:"Default upload-limit in MB" group:"api"`
|
UploadLimit int `env:"LOCALAI_UPLOAD_LIMIT,UPLOAD_LIMIT" default:"15" help:"Default upload-limit in MB" group:"api"`
|
||||||
APIKeys []string `env:"LOCALAI_API_KEY,API_KEY" help:"List of API Keys to enable API authentication. When this is set, all the requests must be authenticated with one of these API keys" group:"api"`
|
APIKeys []string `env:"LOCALAI_API_KEY,API_KEY" help:"List of API Keys to enable API authentication. When this is set, all the requests must be authenticated with one of these API keys" group:"api"`
|
||||||
DisableWelcome bool `env:"LOCALAI_DISABLE_WELCOME,DISABLE_WELCOME" default:"false" help:"Disable welcome pages" group:"api"`
|
DisableWebUI bool `env:"LOCALAI_DISABLE_WEBUI,DISABLE_WEBUI" default:"false" help:"Disable webui" group:"api"`
|
||||||
|
|
||||||
ParallelRequests bool `env:"LOCALAI_PARALLEL_REQUESTS,PARALLEL_REQUESTS" help:"Enable backends to handle multiple requests in parallel if they support it (e.g.: llama.cpp or vllm)" group:"backends"`
|
ParallelRequests bool `env:"LOCALAI_PARALLEL_REQUESTS,PARALLEL_REQUESTS" help:"Enable backends to handle multiple requests in parallel if they support it (e.g.: llama.cpp or vllm)" group:"backends"`
|
||||||
SingleActiveBackend bool `env:"LOCALAI_SINGLE_ACTIVE_BACKEND,SINGLE_ACTIVE_BACKEND" help:"Allow only one backend to be run at a time" group:"backends"`
|
SingleActiveBackend bool `env:"LOCALAI_SINGLE_ACTIVE_BACKEND,SINGLE_ACTIVE_BACKEND" help:"Allow only one backend to be run at a time" group:"backends"`
|
||||||
@ -84,8 +84,8 @@ func (r *RunCMD) Run(ctx *Context) error {
|
|||||||
idleWatchDog := r.EnableWatchdogIdle
|
idleWatchDog := r.EnableWatchdogIdle
|
||||||
busyWatchDog := r.EnableWatchdogBusy
|
busyWatchDog := r.EnableWatchdogBusy
|
||||||
|
|
||||||
if r.DisableWelcome {
|
if r.DisableWebUI {
|
||||||
opts = append(opts, config.DisableWelcomePage)
|
opts = append(opts, config.DisableWebUI)
|
||||||
}
|
}
|
||||||
|
|
||||||
if idleWatchDog || busyWatchDog {
|
if idleWatchDog || busyWatchDog {
|
||||||
|
@ -15,7 +15,7 @@ type ApplicationConfig struct {
|
|||||||
ConfigFile string
|
ConfigFile string
|
||||||
ModelPath string
|
ModelPath string
|
||||||
UploadLimitMB, Threads, ContextSize int
|
UploadLimitMB, Threads, ContextSize int
|
||||||
DisableWelcomePage bool
|
DisableWebUI bool
|
||||||
F16 bool
|
F16 bool
|
||||||
Debug bool
|
Debug bool
|
||||||
ImageDir string
|
ImageDir string
|
||||||
@ -107,8 +107,8 @@ var EnableWatchDogBusyCheck = func(o *ApplicationConfig) {
|
|||||||
o.WatchDogBusy = true
|
o.WatchDogBusy = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var DisableWelcomePage = func(o *ApplicationConfig) {
|
var DisableWebUI = func(o *ApplicationConfig) {
|
||||||
o.DisableWelcomePage = true
|
o.DisableWebUI = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetWatchDogBusyTimeout(t time.Duration) AppOption {
|
func SetWatchDogBusyTimeout(t time.Duration) AppOption {
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"embed"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-skynet/LocalAI/pkg/utils"
|
"github.com/go-skynet/LocalAI/pkg/utils"
|
||||||
@ -18,6 +20,7 @@ import (
|
|||||||
"github.com/gofiber/contrib/fiberzerolog"
|
"github.com/gofiber/contrib/fiberzerolog"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||||
|
|
||||||
// swagger handler
|
// swagger handler
|
||||||
@ -42,6 +45,11 @@ func readAuthHeader(c *fiber.Ctx) string {
|
|||||||
return authHeader
|
return authHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Embed a directory
|
||||||
|
//
|
||||||
|
//go:embed static/*
|
||||||
|
var embedDirStatic embed.FS
|
||||||
|
|
||||||
// @title LocalAI API
|
// @title LocalAI API
|
||||||
// @version 2.0.0
|
// @version 2.0.0
|
||||||
// @description The LocalAI Rest API.
|
// @description The LocalAI Rest API.
|
||||||
@ -169,10 +177,17 @@ func App(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *confi
|
|||||||
routes.RegisterElevenLabsRoutes(app, cl, ml, appConfig, auth)
|
routes.RegisterElevenLabsRoutes(app, cl, ml, appConfig, auth)
|
||||||
routes.RegisterLocalAIRoutes(app, cl, ml, appConfig, galleryService, auth)
|
routes.RegisterLocalAIRoutes(app, cl, ml, appConfig, galleryService, auth)
|
||||||
routes.RegisterOpenAIRoutes(app, cl, ml, appConfig, auth)
|
routes.RegisterOpenAIRoutes(app, cl, ml, appConfig, auth)
|
||||||
routes.RegisterPagesRoutes(app, cl, ml, appConfig, auth)
|
if !appConfig.DisableWebUI {
|
||||||
routes.RegisterUIRoutes(app, cl, ml, appConfig, galleryService, auth)
|
routes.RegisterUIRoutes(app, cl, ml, appConfig, galleryService, auth)
|
||||||
|
}
|
||||||
routes.RegisterJINARoutes(app, cl, ml, appConfig, auth)
|
routes.RegisterJINARoutes(app, cl, ml, appConfig, auth)
|
||||||
|
|
||||||
|
app.Use("/static", filesystem.New(filesystem.Config{
|
||||||
|
Root: http.FS(embedDirStatic),
|
||||||
|
PathPrefix: "static",
|
||||||
|
Browse: true,
|
||||||
|
}))
|
||||||
|
|
||||||
// Define a custom 404 handler
|
// Define a custom 404 handler
|
||||||
// Note: keep this at the bottom!
|
// Note: keep this at the bottom!
|
||||||
app.Use(notFoundHandler)
|
app.Use(notFoundHandler)
|
||||||
|
@ -7,7 +7,9 @@ import (
|
|||||||
|
|
||||||
"github.com/go-skynet/LocalAI/core/config"
|
"github.com/go-skynet/LocalAI/core/config"
|
||||||
"github.com/go-skynet/LocalAI/core/http/elements"
|
"github.com/go-skynet/LocalAI/core/http/elements"
|
||||||
|
"github.com/go-skynet/LocalAI/core/http/endpoints/localai"
|
||||||
"github.com/go-skynet/LocalAI/core/services"
|
"github.com/go-skynet/LocalAI/core/services"
|
||||||
|
"github.com/go-skynet/LocalAI/internal"
|
||||||
"github.com/go-skynet/LocalAI/pkg/gallery"
|
"github.com/go-skynet/LocalAI/pkg/gallery"
|
||||||
"github.com/go-skynet/LocalAI/pkg/model"
|
"github.com/go-skynet/LocalAI/pkg/model"
|
||||||
"github.com/go-skynet/LocalAI/pkg/xsync"
|
"github.com/go-skynet/LocalAI/pkg/xsync"
|
||||||
@ -23,6 +25,8 @@ 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 installingModels = xsync.NewSyncedMap[string, string]()
|
||||||
|
|
||||||
@ -32,6 +36,7 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
|
|
||||||
summary := fiber.Map{
|
summary := fiber.Map{
|
||||||
"Title": "LocalAI - Models",
|
"Title": "LocalAI - Models",
|
||||||
|
"Version": internal.PrintableVersion(),
|
||||||
"Models": template.HTML(elements.ListModels(models, installingModels)),
|
"Models": template.HTML(elements.ListModels(models, installingModels)),
|
||||||
"Repositories": appConfig.Galleries,
|
"Repositories": appConfig.Galleries,
|
||||||
// "ApplicationConfig": appConfig,
|
// "ApplicationConfig": appConfig,
|
||||||
@ -166,4 +171,103 @@ func RegisterUIRoutes(app *fiber.App,
|
|||||||
|
|
||||||
return c.SendString(elements.DoneProgress(c.Params("uid"), displayText))
|
return c.SendString(elements.DoneProgress(c.Params("uid"), displayText))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Show the Chat page
|
||||||
|
app.Get("/chat/:model", auth, func(c *fiber.Ctx) error {
|
||||||
|
backendConfigs := cl.GetAllBackendConfigs()
|
||||||
|
|
||||||
|
summary := fiber.Map{
|
||||||
|
"Title": "LocalAI - Chat with " + c.Params("model"),
|
||||||
|
"ModelsConfig": backendConfigs,
|
||||||
|
"Model": c.Params("model"),
|
||||||
|
"Version": internal.PrintableVersion(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render index
|
||||||
|
return c.Render("views/chat", summary)
|
||||||
|
})
|
||||||
|
app.Get("/chat/", auth, func(c *fiber.Ctx) error {
|
||||||
|
|
||||||
|
backendConfigs := cl.GetAllBackendConfigs()
|
||||||
|
|
||||||
|
if len(backendConfigs) == 0 {
|
||||||
|
return c.SendString("No models available")
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := fiber.Map{
|
||||||
|
"Title": "LocalAI - Chat with " + backendConfigs[0].Name,
|
||||||
|
"ModelsConfig": backendConfigs,
|
||||||
|
"Model": backendConfigs[0].Name,
|
||||||
|
"Version": internal.PrintableVersion(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render index
|
||||||
|
return c.Render("views/chat", summary)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/text2image/:model", auth, 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(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render index
|
||||||
|
return c.Render("views/text2image", summary)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/text2image/", auth, func(c *fiber.Ctx) error {
|
||||||
|
|
||||||
|
backendConfigs := cl.GetAllBackendConfigs()
|
||||||
|
|
||||||
|
if len(backendConfigs) == 0 {
|
||||||
|
return c.SendString("No models available")
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := fiber.Map{
|
||||||
|
"Title": "LocalAI - Generate images with " + backendConfigs[0].Name,
|
||||||
|
"ModelsConfig": backendConfigs,
|
||||||
|
"Model": backendConfigs[0].Name,
|
||||||
|
"Version": internal.PrintableVersion(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render index
|
||||||
|
return c.Render("views/text2image", summary)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/tts/:model", auth, 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(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render index
|
||||||
|
return c.Render("views/tts", summary)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Get("/tts/", auth, func(c *fiber.Ctx) error {
|
||||||
|
|
||||||
|
backendConfigs := cl.GetAllBackendConfigs()
|
||||||
|
|
||||||
|
if len(backendConfigs) == 0 {
|
||||||
|
return c.SendString("No models available")
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := fiber.Map{
|
||||||
|
"Title": "LocalAI - Generate audio with " + backendConfigs[0].Name,
|
||||||
|
"ModelsConfig": backendConfigs,
|
||||||
|
"Model": backendConfigs[0].Name,
|
||||||
|
"Version": internal.PrintableVersion(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render index
|
||||||
|
return c.Render("views/tts", summary)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/go-skynet/LocalAI/core/config"
|
|
||||||
"github.com/go-skynet/LocalAI/core/http/endpoints/localai"
|
|
||||||
"github.com/go-skynet/LocalAI/pkg/model"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RegisterPagesRoutes(app *fiber.App,
|
|
||||||
cl *config.BackendConfigLoader,
|
|
||||||
ml *model.ModelLoader,
|
|
||||||
appConfig *config.ApplicationConfig,
|
|
||||||
auth func(*fiber.Ctx) error) {
|
|
||||||
|
|
||||||
if !appConfig.DisableWelcomePage {
|
|
||||||
app.Get("/", auth, localai.WelcomeEndpoint(appConfig, cl, ml))
|
|
||||||
}
|
|
||||||
}
|
|
141
core/http/static/chat.js
Normal file
141
core/http/static/chat.js
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
https://github.com/david-haerer/chatapi
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 David Härer
|
||||||
|
Copyright (c) 2024 Ettore Di Giacinto
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
function submitKey(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
localStorage.setItem("key", document.getElementById("apiKey").value);
|
||||||
|
document.getElementById("apiKey").blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitPrompt(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const input = document.getElementById("input").value;
|
||||||
|
Alpine.store("chat").add("user", input);
|
||||||
|
document.getElementById("input").value = "";
|
||||||
|
const key = localStorage.getItem("key");
|
||||||
|
|
||||||
|
if (input.startsWith("!img")) {
|
||||||
|
promptDallE(key, input.slice(4));
|
||||||
|
} else {
|
||||||
|
promptGPT(key, input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function promptGPT(key, input) {
|
||||||
|
const model = document.getElementById("chat-model").value;
|
||||||
|
// Set class "loader" to the element with "loader" id
|
||||||
|
//document.getElementById("loader").classList.add("loader");
|
||||||
|
// Make the "loader" visible
|
||||||
|
document.getElementById("loader").style.display = "block";
|
||||||
|
document.getElementById("input").disabled = true;
|
||||||
|
document.getElementById('messages').scrollIntoView(false)
|
||||||
|
|
||||||
|
// Source: https://stackoverflow.com/a/75751803/11386095
|
||||||
|
const response = await fetch("/v1/chat/completions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${key}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model,
|
||||||
|
messages: Alpine.store("chat").messages(),
|
||||||
|
stream: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
Alpine.store("chat").add(
|
||||||
|
"assistant",
|
||||||
|
`<span class='error'>Error: POST /v1/chat/completions ${response.status}</span>`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body
|
||||||
|
?.pipeThrough(new TextDecoderStream())
|
||||||
|
.getReader();
|
||||||
|
|
||||||
|
if (!reader) {
|
||||||
|
Alpine.store("chat").add(
|
||||||
|
"assistant",
|
||||||
|
`<span class='error'>Error: Failed to decode API response</span>`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
let dataDone = false;
|
||||||
|
const arr = value.split("\n");
|
||||||
|
arr.forEach((data) => {
|
||||||
|
if (data.length === 0) return;
|
||||||
|
if (data.startsWith(":")) return;
|
||||||
|
if (data === "data: [DONE]") {
|
||||||
|
dataDone = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const token = JSON.parse(data.substring(6)).choices[0].delta.content;
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hljs.highlightAll();
|
||||||
|
Alpine.store("chat").add("assistant", token);
|
||||||
|
document.getElementById('messages').scrollIntoView(false)
|
||||||
|
});
|
||||||
|
hljs.highlightAll();
|
||||||
|
if (dataDone) break;
|
||||||
|
}
|
||||||
|
// Remove class "loader" from the element with "loader" id
|
||||||
|
//document.getElementById("loader").classList.remove("loader");
|
||||||
|
document.getElementById("loader").style.display = "none";
|
||||||
|
// enable input
|
||||||
|
document.getElementById("input").disabled = false;
|
||||||
|
// scroll to the bottom of the chat
|
||||||
|
document.getElementById('messages').scrollIntoView(false)
|
||||||
|
// set focus to the input
|
||||||
|
document.getElementById("input").focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("key").addEventListener("submit", submitKey);
|
||||||
|
document.getElementById("prompt").addEventListener("submit", submitPrompt);
|
||||||
|
document.getElementById("input").focus();
|
||||||
|
|
||||||
|
const storeKey = localStorage.getItem("key");
|
||||||
|
if (storeKey) {
|
||||||
|
document.getElementById("apiKey").value = storeKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
marked.setOptions({
|
||||||
|
highlight: function (code) {
|
||||||
|
return hljs.highlightAuto(code).value;
|
||||||
|
},
|
||||||
|
});
|
73
core/http/static/general.css
Normal file
73
core/http/static/general.css
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
.chat-container { height: 90vh; display: flex; flex-direction: column; }
|
||||||
|
.chat-messages { overflow-y: auto; flex-grow: 1; }
|
||||||
|
.htmx-indicator{
|
||||||
|
opacity:0;
|
||||||
|
transition: opacity 10ms ease-in;
|
||||||
|
}
|
||||||
|
.htmx-request .htmx-indicator{
|
||||||
|
opacity:1
|
||||||
|
}
|
||||||
|
/* Loader (https://cssloaders.github.io/) */
|
||||||
|
.loader {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: block;
|
||||||
|
margin:15px auto;
|
||||||
|
position: relative;
|
||||||
|
color: #FFF;
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: animloader 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animloader {
|
||||||
|
0% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||||
|
25% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 2px; }
|
||||||
|
50% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 2px, -38px 0 0 -2px; }
|
||||||
|
75% { box-shadow: 14px 0 0 2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||||
|
100% { box-shadow: 14px 0 0 -2px, 38px 0 0 2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||||
|
}
|
||||||
|
.progress {
|
||||||
|
height: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
float: left;
|
||||||
|
width: 0%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #337ab7;
|
||||||
|
-webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
|
||||||
|
box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
|
||||||
|
-webkit-transition: width .6s ease;
|
||||||
|
-o-transition: width .6s ease;
|
||||||
|
transition: width .6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
background-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant {
|
||||||
|
background-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user, .assistant {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
96
core/http/static/image.js
Normal file
96
core/http/static/image.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
https://github.com/david-haerer/chatapi
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 David Härer
|
||||||
|
Copyright (c) 2024 Ettore Di Giacinto
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
function submitKey(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
localStorage.setItem("key", document.getElementById("apiKey").value);
|
||||||
|
document.getElementById("apiKey").blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function genImage(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const input = document.getElementById("input").value;
|
||||||
|
const key = localStorage.getItem("key");
|
||||||
|
|
||||||
|
promptDallE(key, input);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptDallE(key, input) {
|
||||||
|
document.getElementById("loader").style.display = "block";
|
||||||
|
document.getElementById("input").value = "";
|
||||||
|
document.getElementById("input").disabled = true;
|
||||||
|
|
||||||
|
const model = document.getElementById("image-model").value;
|
||||||
|
const response = await fetch("/v1/images/generations", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${key}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model,
|
||||||
|
steps: 10,
|
||||||
|
prompt: input,
|
||||||
|
n: 1,
|
||||||
|
size: "512x512",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = await response.json();
|
||||||
|
if (json.error) {
|
||||||
|
// Display error if there is one
|
||||||
|
var div = document.getElementById('result'); // Get the div by its ID
|
||||||
|
div.innerHTML = '<p style="color:red;">' + json.error.message + '</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = json.data[0].url;
|
||||||
|
|
||||||
|
var div = document.getElementById('result'); // Get the div by its ID
|
||||||
|
var img = document.createElement('img'); // Create a new img element
|
||||||
|
img.src = url; // Set the source of the image
|
||||||
|
img.alt = 'Generated image'; // Set the alt text of the image
|
||||||
|
|
||||||
|
div.innerHTML = ''; // Clear the existing content of the div
|
||||||
|
div.appendChild(img); // Add the new img element to the div
|
||||||
|
|
||||||
|
document.getElementById("loader").style.display = "none";
|
||||||
|
document.getElementById("input").disabled = false;
|
||||||
|
document.getElementById("input").focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("key").addEventListener("submit", submitKey);
|
||||||
|
document.getElementById("input").focus();
|
||||||
|
document.getElementById("genimage").addEventListener("submit", genImage);
|
||||||
|
document.getElementById("loader").style.display = "none";
|
||||||
|
|
||||||
|
const storeKey = localStorage.getItem("key");
|
||||||
|
if (storeKey) {
|
||||||
|
document.getElementById("apiKey").value = storeKey;
|
||||||
|
}
|
||||||
|
|
64
core/http/static/tts.js
Normal file
64
core/http/static/tts.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
function submitKey(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
localStorage.setItem("key", document.getElementById("apiKey").value);
|
||||||
|
document.getElementById("apiKey").blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function genAudio(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const input = document.getElementById("input").value;
|
||||||
|
const key = localStorage.getItem("key");
|
||||||
|
|
||||||
|
tts(key, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tts(key, input) {
|
||||||
|
document.getElementById("loader").style.display = "block";
|
||||||
|
document.getElementById("input").value = "";
|
||||||
|
document.getElementById("input").disabled = true;
|
||||||
|
|
||||||
|
const model = document.getElementById("tts-model").value;
|
||||||
|
const response = await fetch("/tts", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${key}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model,
|
||||||
|
input: input,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const jsonData = await response.json(); // Now safely parse JSON
|
||||||
|
var div = document.getElementById('result');
|
||||||
|
div.innerHTML = '<p style="color:red;">Error: ' +jsonData.error.message + '</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var div = document.getElementById('result'); // Get the div by its ID
|
||||||
|
var link=document.createElement('a');
|
||||||
|
link.className = "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";
|
||||||
|
link.innerHTML = "<i class='fa-solid fa-download'></i> Download result";
|
||||||
|
const blob = await response.blob();
|
||||||
|
link.href=window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
div.innerHTML = ''; // Clear the existing content of the div
|
||||||
|
div.appendChild(link); // Add the new img element to the div
|
||||||
|
console.log(link)
|
||||||
|
document.getElementById("loader").style.display = "none";
|
||||||
|
document.getElementById("input").disabled = false;
|
||||||
|
document.getElementById("input").focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("key").addEventListener("submit", submitKey);
|
||||||
|
document.getElementById("input").focus();
|
||||||
|
document.getElementById("tts").addEventListener("submit", genAudio);
|
||||||
|
document.getElementById("loader").style.display = "none";
|
||||||
|
|
||||||
|
const storeKey = localStorage.getItem("key");
|
||||||
|
if (storeKey) {
|
||||||
|
document.getElementById("apiKey").value = storeKey;
|
||||||
|
}
|
||||||
|
|
189
core/http/views/chat.html
Normal file
189
core/http/views/chat.html
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
<!--
|
||||||
|
|
||||||
|
Part of this page is based on the OpenAI Chatbot example by David Härer:
|
||||||
|
https://github.com/david-haerer/chatapi
|
||||||
|
|
||||||
|
MIT License Copyright (c) 2023 David Härer
|
||||||
|
Copyright (c) 2024 Ettore Di Giacinto
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
{{template "views/partials/head" .}}
|
||||||
|
<script defer src="/static/chat.js"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<body class="bg-gray-900 text-gray-200" x-data="{ key: $store.chat.key }">
|
||||||
|
<div class="flex flex-col min-h-screen">
|
||||||
|
|
||||||
|
{{template "views/partials/navbar"}}
|
||||||
|
<div class="chat-container mt-2 mr-2 ml-2 mb-2 bg-gray-800 shadow-lg rounded-lg" >
|
||||||
|
<!-- Chat Header -->
|
||||||
|
<div class="border-b border-gray-700 p-4" x-data="{ component: 'menu' }">
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
|
||||||
|
<h1 class="text-lg font-semibold"> <i class="fa-solid fa-comments"></i> Chat with {{.Model}} <a href="https://localai.io/features/text-generation/" target="_blank" >
|
||||||
|
<i class="fas fa-circle-info pr-2"></i>
|
||||||
|
</a></h1>
|
||||||
|
<div x-show="component === 'menu'" id="menu">
|
||||||
|
<button
|
||||||
|
@click="$store.chat.clear()"
|
||||||
|
id="clear"
|
||||||
|
title="Clear chat history"
|
||||||
|
|
||||||
|
data-twe-ripple-init
|
||||||
|
data-twe-ripple-color="light"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
Clear chat 🔥
|
||||||
|
</button>
|
||||||
|
<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"
|
||||||
|
>Set API Key🔑</button>
|
||||||
|
</div>
|
||||||
|
<form x-show="component === 'key'" id="key">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="apiKey"
|
||||||
|
name="apiKey"
|
||||||
|
placeholder="OpenAI API Key"
|
||||||
|
x-model.lazy="key"
|
||||||
|
/>
|
||||||
|
<button @click="component = 'menu'" type="submit" title="Save API key">
|
||||||
|
🔒
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<select x-data="{ link : '' }" x-model="link" x-init="$watch('link', value => window.location = link)"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<!-- Options -->
|
||||||
|
<option value="" disabled class="text-gray-400" >Select a model</option>
|
||||||
|
{{ $model:=.Model}}
|
||||||
|
{{ range .ModelsConfig }}
|
||||||
|
{{ if eq .Name $model }}
|
||||||
|
<option value="/chat/{{.Name}}" selected class="bg-gray-700 text-white">{{.Name}}</option>
|
||||||
|
{{ else }}
|
||||||
|
<option value="/chat/{{.Name}}" class="bg-gray-700 text-white">{{.Name}}</option>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-messages p-4" id="chat" x-data="{history: $store.chat.history}">
|
||||||
|
<p id="usage" x-show="history.length === 0">
|
||||||
|
Start chatting with the AI by typing a prompt in the input field below.
|
||||||
|
</p>
|
||||||
|
<div id="messages">
|
||||||
|
<template x-for="message in history">
|
||||||
|
<div class="message flex items-start space-x-2 my-2" >
|
||||||
|
<!--<img :src="message.role === 'user' ? '/path/to/user-icon.png' : '/path/to/bot-icon.png'" alt="" class="h-6 w-6">-->
|
||||||
|
<i class="fa-solid h-8 w-8" :class="message.role === 'user' ? 'fa-user' : 'fa-robot'" ></i>
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<span class="text-xs font-semibold text-gray-600" x-text="message.role === 'user' ? 'User' : 'Assistant ({{.Model}})'"></span>
|
||||||
|
<template x-if="message.role === 'user'">
|
||||||
|
<div class="p-2 flex-1 rounded" :class="message.role" x-text="message.content"></div>
|
||||||
|
</template>
|
||||||
|
<template x-if="message.role === 'assistant'">
|
||||||
|
<div class="p-2 flex-1 rounded" :class="message.role" x-html="message.html"></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 border-t border-gray-700">
|
||||||
|
<div id="loader" class="my-2 loader" style="display: none;" ></div>
|
||||||
|
<input id="chat-model" type="hidden" value="{{.Model}}">
|
||||||
|
<form id="prompt" action="/chat/{{.Model}}" method="get">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="input"
|
||||||
|
name="input"
|
||||||
|
placeholder="Prompt…"
|
||||||
|
autocomplete="off"
|
||||||
|
class="p-2 border rounded w-full bg-gray-600 text-white placeholder-gray-300"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("alpine:init", () => {
|
||||||
|
Alpine.store("chat", {
|
||||||
|
history: [],
|
||||||
|
languages: [undefined],
|
||||||
|
clear() {
|
||||||
|
this.history.length = 0;
|
||||||
|
},
|
||||||
|
add(role, content) {
|
||||||
|
const N = this.history.length - 1;
|
||||||
|
if (this.history.length && this.history[N].role === role) {
|
||||||
|
this.history[N].content += content;
|
||||||
|
str = this.history[N].content;
|
||||||
|
this.history[N].html = DOMPurify.sanitize(
|
||||||
|
marked.parse(this.history[N].content),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.history.push({
|
||||||
|
role: role,
|
||||||
|
content: content,
|
||||||
|
html: DOMPurify.sanitize(marked.parse(content)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const html = parser.parseFromString(
|
||||||
|
this.history[this.history.length - 1].html,
|
||||||
|
"text/html",
|
||||||
|
);
|
||||||
|
const code = html.querySelectorAll("pre code");
|
||||||
|
if (!code.length) return;
|
||||||
|
code.forEach((el) => {
|
||||||
|
const language = el.className.split("language-")[1];
|
||||||
|
if (this.languages.includes(language)) return;
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = `https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build/languages/${language}.min.js`;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
this.languages.push(language);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
messages() {
|
||||||
|
return this.history.map((message) => {
|
||||||
|
return {
|
||||||
|
role: message.role,
|
||||||
|
content: message.content,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -2,6 +2,28 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build/styles/default.min.css"
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build/highlight.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"
|
||||||
|
></script>
|
||||||
|
|
||||||
|
<link href="/static/general.css" rel="stylesheet" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Roboto:wght@400;500&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Roboto:wght@400;500&display=swap" rel="stylesheet">
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900&display=swap"
|
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900&display=swap"
|
||||||
@ -27,52 +49,4 @@
|
|||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.12" integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2" crossorigin="anonymous"></script>
|
<script src="https://unpkg.com/htmx.org@1.9.12" integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2" crossorigin="anonymous"></script>
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
}
|
|
||||||
/* Loader (https://cssloaders.github.io/) */
|
|
||||||
.loader {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: block;
|
|
||||||
margin:15px auto;
|
|
||||||
position: relative;
|
|
||||||
color: #FFF;
|
|
||||||
box-sizing: border-box;
|
|
||||||
animation: animloader 2s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes animloader {
|
|
||||||
0% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
|
||||||
25% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 2px; }
|
|
||||||
50% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 2px, -38px 0 0 -2px; }
|
|
||||||
75% { box-shadow: 14px 0 0 2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
|
||||||
100% { box-shadow: 14px 0 0 -2px, 38px 0 0 2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
|
||||||
}
|
|
||||||
.progress {
|
|
||||||
height: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
|
|
||||||
}
|
|
||||||
.progress-bar {
|
|
||||||
float: left;
|
|
||||||
width: 0%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 20px;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
background-color: #337ab7;
|
|
||||||
-webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
|
|
||||||
box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
|
|
||||||
-webkit-transition: width .6s ease;
|
|
||||||
-o-transition: width .6s ease;
|
|
||||||
transition: width .6s ease;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
@ -10,6 +10,9 @@
|
|||||||
<a href="/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-home pr-2"></i>Home</a>
|
<a href="/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-home pr-2"></i>Home</a>
|
||||||
<a href="https://localai.io" class="text-gray-400 hover:text-white px-3 py-2 rounded" target="_blank" ><i class="fas fa-book-reader pr-2"></i> Documentation</a>
|
<a href="https://localai.io" class="text-gray-400 hover:text-white px-3 py-2 rounded" target="_blank" ><i class="fas fa-book-reader pr-2"></i> Documentation</a>
|
||||||
<a href="/browse/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-brain pr-2"></i> Models</a>
|
<a href="/browse/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-brain pr-2"></i> Models</a>
|
||||||
|
<a href="/chat/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fa-solid fa-comments pr-2"></i> Chat</a>
|
||||||
|
<a href="/text2image/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-image pr-2"></i> Generate images</a>
|
||||||
|
<a href="/tts/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fa-solid fa-music pr-2"></i> TTS </a>
|
||||||
<a href="/swagger/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-code pr-2"></i> API</a>
|
<a href="/swagger/" class="text-gray-400 hover:text-white px-3 py-2 rounded"><i class="fas fa-code pr-2"></i> API</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
89
core/http/views/text2image.html
Normal file
89
core/http/views/text2image.html
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{template "views/partials/head" .}}
|
||||||
|
<script defer src="/static/image.js"></script>
|
||||||
|
|
||||||
|
<body class="bg-gray-900 text-gray-200">
|
||||||
|
<div class="flex flex-col min-h-screen">
|
||||||
|
|
||||||
|
{{template "views/partials/navbar" .}}
|
||||||
|
<div class="container mx-auto px-4 flex-grow " x-data="{ component: 'menu' }">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mt-12">
|
||||||
|
<div class="flex items-center justify-center text-center pb-2">
|
||||||
|
<span class="text-3xl font-semibold text-gray-100">
|
||||||
|
🖼️ Text to Image
|
||||||
|
<a href="https://localai.io/models/" target="_blank" >
|
||||||
|
<i class="fas fa-circle-info pr-2"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center font-semibold text-gray-100">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
|
||||||
|
<div x-show="component === 'menu'" id="menu">
|
||||||
|
<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"
|
||||||
|
>Set API Key🔑</button>
|
||||||
|
</div>
|
||||||
|
<form x-show="component === 'key'" id="key">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="apiKey"
|
||||||
|
name="apiKey"
|
||||||
|
placeholder="OpenAI API Key"
|
||||||
|
x-model.lazy="key"
|
||||||
|
/>
|
||||||
|
<button @click="component = 'menu'" type="submit" title="Save API key">
|
||||||
|
🔒
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<select x-data="{ link : '' }" x-model="link" x-init="$watch('link', value => window.location = link)"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<!-- Options -->
|
||||||
|
<option value="" disabled class="text-gray-400" >Select a model</option>
|
||||||
|
{{ $model:=.Model}}
|
||||||
|
{{ range .ModelsConfig }}
|
||||||
|
{{ if eq .Name $model }}
|
||||||
|
<option value="/text2image/{{.Name}}" selected class="bg-gray-700 text-white">{{.Name}}</option>
|
||||||
|
{{ else }}
|
||||||
|
<option value="/text2image/{{.Name}}" class="bg-gray-700 text-white">{{.Name}}</option>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-12">
|
||||||
|
<input id="image-model" type="hidden" value="{{.Model}}">
|
||||||
|
<form id="genimage" action="/text2image/{{.Model}}" method="get">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="input"
|
||||||
|
name="input"
|
||||||
|
placeholder="Prompt…"
|
||||||
|
autocomplete="off"
|
||||||
|
class="p-2 border rounded w-full bg-gray-600 text-white placeholder-gray-300"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<div class="container max-w-screen-lg mx-auto mt-4 pb-10 flex justify-center">
|
||||||
|
<div id="loader" class="my-2 loader" ></div>
|
||||||
|
</div>
|
||||||
|
<div class="container max-w-screen-lg mx-auto mt-4 pb-10 flex justify-center">
|
||||||
|
<div id="result" class="mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "views/partials/footer" .}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
86
core/http/views/tts.html
Normal file
86
core/http/views/tts.html
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{template "views/partials/head" .}}
|
||||||
|
<script defer src="/static/tts.js"></script>
|
||||||
|
|
||||||
|
<body class="bg-gray-900 text-gray-200">
|
||||||
|
<div class="flex flex-col min-h-screen">
|
||||||
|
|
||||||
|
{{template "views/partials/navbar" .}}
|
||||||
|
<div class="container mx-auto px-4 flex-grow " x-data="{ component: 'menu' }">
|
||||||
|
<div class="mt-12">
|
||||||
|
<div class="flex items-center justify-center text-center pb-2">
|
||||||
|
<span class="text-3xl font-semibold text-gray-100">
|
||||||
|
<i class="fa-solid fa-music"></i> Text to speech/audio
|
||||||
|
<a href="https://localai.io/features/text-to-audio/" target="_blank" >
|
||||||
|
<i class="fas fa-circle-info pr-2"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="text-center font-semibold text-gray-100">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
|
||||||
|
<div x-show="component === 'menu'" id="menu">
|
||||||
|
<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"
|
||||||
|
>Set API Key🔑</button>
|
||||||
|
</div>
|
||||||
|
<form x-show="component === 'key'" id="key">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="apiKey"
|
||||||
|
name="apiKey"
|
||||||
|
placeholder="OpenAI API Key"
|
||||||
|
x-model.lazy="key"
|
||||||
|
/>
|
||||||
|
<button @click="component = 'menu'" type="submit" title="Save API key">
|
||||||
|
🔒
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<select x-data="{ link : '' }" x-model="link" x-init="$watch('link', value => window.location = link)"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<!-- Options -->
|
||||||
|
<option value="" disabled class="text-gray-400" >Select a model</option>
|
||||||
|
{{ $model:=.Model}}
|
||||||
|
{{ range .ModelsConfig }}
|
||||||
|
{{ if eq .Name $model }}
|
||||||
|
<option value="/tts/{{.Name}}" selected class="bg-gray-700 text-white">{{.Name}}</option>
|
||||||
|
{{ else }}
|
||||||
|
<option value="/tts/{{.Name}}" class="bg-gray-700 text-white">{{.Name}}</option>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-12">
|
||||||
|
<input id="tts-model" type="hidden" value="{{.Model}}">
|
||||||
|
<form id="tts" action="/tts/{{.Model}}" method="get">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="input"
|
||||||
|
name="input"
|
||||||
|
placeholder="Prompt…"
|
||||||
|
autocomplete="off"
|
||||||
|
class="p-2 border rounded w-full bg-gray-600 text-white placeholder-gray-300"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<div class="container max-w-screen-lg mx-auto mt-4 pb-10 flex justify-center">
|
||||||
|
<div id="loader" class="my-2 loader" ></div>
|
||||||
|
</div>
|
||||||
|
<div class="container max-w-screen-lg mx-auto mt-4 pb-10 flex justify-center">
|
||||||
|
<div id="result" class="mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "views/partials/footer" .}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user