LocalAI/core/http/render.go

90 lines
2.5 KiB
Go
Raw Normal View History

package http
import (
"embed"
"fmt"
"html/template"
"io"
"io/fs"
"net/http"
"strings"
"github.com/Masterminds/sprig/v3"
"github.com/labstack/echo/v4"
"github.com/microcosm-cc/bluemonday"
"github.com/mudler/LocalAI/core/http/middleware"
"github.com/mudler/LocalAI/core/schema"
"github.com/russross/blackfriday"
)
//go:embed views/*
var viewsfs embed.FS
// TemplateRenderer is a custom template renderer for Echo
type TemplateRenderer struct {
templates *template.Template
}
// Render renders a template document
func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
func notFoundHandler(c echo.Context) error {
// Check if the request accepts JSON
contentType := c.Request().Header.Get("Content-Type")
accept := c.Request().Header.Get("Accept")
if strings.Contains(contentType, "application/json") || !strings.Contains(accept, "text/html") {
// The client expects a JSON response
return c.JSON(http.StatusNotFound, schema.ErrorResponse{
Error: &schema.APIError{Message: "Resource not found", Code: http.StatusNotFound},
})
} else {
// The client expects an HTML response
return c.Render(http.StatusNotFound, "views/404", map[string]interface{}{
"BaseURL": middleware.BaseURL(c),
feat(ui): path prefix support via HTTP header (#4497) Makes the web app honour the `X-Forwarded-Prefix` HTTP request header that may be sent by a reverse-proxy in order to inform the app that its public routes contain a path prefix. For instance this allows to serve the webapp via a reverse-proxy/ingress controller under a path prefix/sub path such as e.g. `/localai/` while still being able to use the regular LocalAI routes/paths without prefix when directly connecting to the LocalAI server. Changes: * Add new `StripPathPrefix` middleware to strip the path prefix (provided with the `X-Forwarded-Prefix` HTTP request header) from the request path prior to matching the HTTP route. * Add a `BaseURL` utility function to build the base URL, honouring the `X-Forwarded-Prefix` HTTP request header. * Generate the derived base URL into the HTML (`head.html` template) as `<base/>` tag. * Make all webapp-internal URLs (within HTML+JS) relative in order to make the browser resolve them against the `<base/>` URL specified within each HTML page's header. * Make font URLs within the CSS files relative to the CSS file. * Generate redirect location URLs using the new `BaseURL` function. * Use the new `BaseURL` function to generate absolute URLs within gallery JSON responses. Closes #3095 TL;DR: The header-based approach allows to move the path prefix configuration concern completely to the reverse-proxy/ingress as opposed to having to align the path prefix configuration between LocalAI, the reverse-proxy and potentially other internal LocalAI clients. The gofiber swagger handler already supports path prefixes this way, see https://github.com/gofiber/swagger/blob/e2d9e9916d8809e8b23c4365f8acfbbd8a71c4cd/swagger.go#L79 Signed-off-by: Max Goltzsche <max.goltzsche@gmail.com>
2025-01-07 16:18:21 +00:00
})
}
}
func renderEngine() *TemplateRenderer {
// Parse all templates from embedded filesystem
tmpl := template.New("").Funcs(sprig.FuncMap())
tmpl = tmpl.Funcs(template.FuncMap{
"MDToHTML": markDowner,
})
// Recursively walk through embedded filesystem and parse all HTML templates
err := fs.WalkDir(viewsfs, "views", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && strings.HasSuffix(path, ".html") {
data, err := viewsfs.ReadFile(path)
if err == nil {
// Remove .html extension to get template name (e.g., "views/index.html" -> "views/index")
templateName := strings.TrimSuffix(path, ".html")
_, err := tmpl.New(templateName).Parse(string(data))
if err != nil {
// If parsing fails, try parsing without explicit name (for templates with {{define}})
tmpl.Parse(string(data))
}
}
}
return nil
})
if err != nil {
// Log error but continue - templates might still work
fmt.Printf("Error walking views directory: %v\n", err)
}
return &TemplateRenderer{
templates: tmpl,
}
}
func markDowner(args ...interface{}) template.HTML {
s := blackfriday.MarkdownCommon([]byte(fmt.Sprintf("%s", args...)))
return template.HTML(bluemonday.UGCPolicy().Sanitize(string(s)))
}