mirror of
https://github.com/mudler/LocalAI.git
synced 2026-03-31 13:15:51 -04:00
fix(ui): fix /app redirect
Do not handle redirect individually, but serve the app directly in / Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
@@ -58,6 +58,9 @@ func API(application *application.Application) (*echo.Echo, error) {
|
||||
e.Use(middleware.BodyLimit(fmt.Sprintf("%dM", application.ApplicationConfig().UploadLimitMB)))
|
||||
}
|
||||
|
||||
// SPA fallback handler, set later when React UI is available
|
||||
var spaFallback func(echo.Context) error
|
||||
|
||||
// Set error handler
|
||||
if !application.ApplicationConfig().OpaqueErrors {
|
||||
e.HTTPErrorHandler = func(err error, c echo.Context) {
|
||||
@@ -67,8 +70,16 @@ func API(application *application.Application) (*echo.Echo, error) {
|
||||
code = he.Code
|
||||
}
|
||||
|
||||
// Handle 404 errors with HTML rendering when appropriate
|
||||
// Handle 404 errors: serve React SPA for HTML requests, JSON otherwise
|
||||
if code == http.StatusNotFound {
|
||||
if spaFallback != nil {
|
||||
accept := c.Request().Header.Get("Accept")
|
||||
contentType := c.Request().Header.Get("Content-Type")
|
||||
if strings.Contains(accept, "text/html") && !strings.Contains(contentType, "application/json") {
|
||||
spaFallback(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
notFoundHandler(c)
|
||||
return
|
||||
}
|
||||
@@ -236,7 +247,7 @@ func API(application *application.Application) (*echo.Echo, error) {
|
||||
routes.RegisterUIAPIRoutes(e, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService(), opcache, application)
|
||||
routes.RegisterUIRoutes(e, application.ModelConfigLoader(), application.ModelLoader(), application.ApplicationConfig(), application.GalleryService())
|
||||
|
||||
// Serve React SPA at /app with SPA fallback
|
||||
// Serve React SPA from / with SPA fallback via 404 handler
|
||||
reactFS, fsErr := fs.Sub(reactUI, "react-ui/dist")
|
||||
if fsErr != nil {
|
||||
xlog.Warn("React UI not available (build with 'make core/http/react-ui/dist')", "error", fsErr)
|
||||
@@ -255,12 +266,15 @@ func API(application *application.Application) (*echo.Echo, error) {
|
||||
return c.HTMLBlob(http.StatusOK, indexHTML)
|
||||
}
|
||||
|
||||
e.GET("/", serveIndex)
|
||||
e.GET("/app", serveIndex)
|
||||
e.GET("/app/*", func(c echo.Context) error {
|
||||
p := c.Param("*")
|
||||
// Enable SPA fallback in the 404 handler for client-side routing
|
||||
spaFallback = serveIndex
|
||||
|
||||
// Try to serve static file from embedded FS
|
||||
// Serve React SPA at /
|
||||
e.GET("/", serveIndex)
|
||||
|
||||
// Serve React static assets (JS, CSS, etc.)
|
||||
serveReactAsset := func(c echo.Context) error {
|
||||
p := "assets/" + c.Param("*")
|
||||
f, err := reactFS.Open(p)
|
||||
if err == nil {
|
||||
defer f.Close()
|
||||
@@ -273,9 +287,17 @@ func API(application *application.Application) (*echo.Echo, error) {
|
||||
return c.Stream(http.StatusOK, contentType, f)
|
||||
}
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
e.GET("/assets/*", serveReactAsset)
|
||||
|
||||
// SPA fallback: serve index.html for client-side routing
|
||||
return serveIndex(c)
|
||||
// Backward compatibility: redirect /app/* to /*
|
||||
e.GET("/app", func(c echo.Context) error {
|
||||
return c.Redirect(http.StatusMovedPermanently, "/")
|
||||
})
|
||||
e.GET("/app/*", func(c echo.Context) error {
|
||||
p := c.Param("*")
|
||||
return c.Redirect(http.StatusMovedPermanently, "/"+p)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
2747
core/http/react-ui/package-lock.json
generated
Normal file
2747
core/http/react-ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -63,4 +63,4 @@ export const router = createBrowserRouter([
|
||||
{ path: '*', element: <NotFound /> },
|
||||
],
|
||||
},
|
||||
], { basename: '/app' })
|
||||
])
|
||||
|
||||
@@ -5,7 +5,7 @@ const backendUrl = process.env.LOCALAI_URL || 'http://localhost:8080'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: '/app',
|
||||
base: '/',
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
|
||||
@@ -15,68 +15,8 @@ func RegisterUIRoutes(app *echo.Echo,
|
||||
appConfig *config.ApplicationConfig,
|
||||
galleryService *services.GalleryService) {
|
||||
|
||||
// Redirect all old UI routes to React SPA at /app
|
||||
redirectToApp := func(path string) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return c.Redirect(302, "/app"+path)
|
||||
}
|
||||
}
|
||||
|
||||
redirectToAppWithParam := func(prefix string) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
param := c.Param("model")
|
||||
if param == "" {
|
||||
param = c.Param("id")
|
||||
}
|
||||
if param != "" {
|
||||
return c.Redirect(302, "/app"+prefix+"/"+param)
|
||||
}
|
||||
return c.Redirect(302, "/app"+prefix)
|
||||
}
|
||||
}
|
||||
|
||||
// "/" is handled in app.go to serve React SPA directly (preserves reverse-proxy headers)
|
||||
app.GET("/manage", redirectToApp("/manage"))
|
||||
|
||||
if !appConfig.DisableRuntimeSettings {
|
||||
app.GET("/settings", redirectToApp("/settings"))
|
||||
}
|
||||
|
||||
// Agent Jobs pages
|
||||
app.GET("/agent-jobs", redirectToApp("/agent-jobs"))
|
||||
app.GET("/agent-jobs/tasks/new", redirectToApp("/agent-jobs/tasks/new"))
|
||||
app.GET("/agent-jobs/tasks/:id/edit", func(c echo.Context) error {
|
||||
return c.Redirect(302, "/app/agent-jobs/tasks/"+c.Param("id")+"/edit")
|
||||
})
|
||||
app.GET("/agent-jobs/tasks/:id", func(c echo.Context) error {
|
||||
return c.Redirect(302, "/app/agent-jobs/tasks/"+c.Param("id"))
|
||||
})
|
||||
app.GET("/agent-jobs/jobs/:id", func(c echo.Context) error {
|
||||
return c.Redirect(302, "/app/agent-jobs/jobs/"+c.Param("id"))
|
||||
})
|
||||
|
||||
// P2P
|
||||
app.GET("/p2p", redirectToApp("/p2p"))
|
||||
|
||||
if !appConfig.DisableGalleryEndpoint {
|
||||
app.GET("/browse", redirectToApp("/browse"))
|
||||
app.GET("/browse/backends", redirectToApp("/backends"))
|
||||
}
|
||||
|
||||
app.GET("/talk", redirectToApp("/talk"))
|
||||
app.GET("/chat", redirectToApp("/chat"))
|
||||
app.GET("/chat/:model", redirectToAppWithParam("/chat"))
|
||||
app.GET("/image", redirectToApp("/image"))
|
||||
app.GET("/image/:model", redirectToAppWithParam("/image"))
|
||||
app.GET("/tts", redirectToApp("/tts"))
|
||||
app.GET("/tts/:model", redirectToAppWithParam("/tts"))
|
||||
app.GET("/sound", redirectToApp("/sound"))
|
||||
app.GET("/sound/:model", redirectToAppWithParam("/sound"))
|
||||
app.GET("/video", redirectToApp("/video"))
|
||||
app.GET("/video/:model", redirectToAppWithParam("/video"))
|
||||
|
||||
// Traces UI
|
||||
app.GET("/traces", redirectToApp("/traces"))
|
||||
// SPA routes are handled by the 404 fallback in app.go which serves
|
||||
// index.html for any unmatched HTML request, enabling client-side routing.
|
||||
|
||||
app.GET("/api/traces", func(c echo.Context) error {
|
||||
return c.JSON(200, middleware.GetTraces())
|
||||
|
||||
Reference in New Issue
Block a user