mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-19 14:19:16 -04:00
* feat(ui): restructure sidebar into Create/Recognition/Build tiers Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(ui): preserve exact sidebar gating for agent items and fine-tune/quantize Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * i18n(ui): add nav tier + console keys to all locales Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ui): add grouped admin console via pathless layout route Wrap the existing admin pages in a pathless AdminConsoleLayout route so they keep their exact flat URLs while gaining a grouped left rail (Inference / Cluster / Observability / Access / System). Rail item gating mirrors the sidebar (adminOnly / authOnly / feature + /api/features). The layout forwards the App-level outlet context (addToast) to the wrapped pages, which read it via useOutletContext(). Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ui): fold Audio Transform into Studio as a tab Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * test(ui): update e2e specs for tiered nav + admin console Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(ui): gate embedded Studio transform view on audio_transform feature Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ui): visual polish + console-ize Build/Recognition tiers Generalize the one-off admin console into a reusable ConsoleLayout driven by a shared consoleConfig (single source of truth for the rail, its gating, and the sidebar entry that opens it — removes the prior rail/sidebar drift). - Promote Install Models to the top menu next to Home. - Build and Operate are now console tiers (secondary rail); Create stays inline. - Fold Recognition (Faces/Voices) into the Build console as a group alongside Automation and Training so it no longer feels split off. - Style the console rail as a panel (header, grouped dividers, rounded active pills) with a hover nudge; sidebar items become inset rounded pills. The rail slide-in plays only when entering a console, not on item-to-item sub-nav (which remounts the layout), so switching no longer flashes the menu. All token-based (light + dark), respects reduced-motion. - Add a delayed RouteFallback loader so lazy routes no longer flash blank; scoped inside ConsoleLayout so the rail stays put while the body loads. - Update e2e specs for the new structure (.console-* classes, console entries). Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ui): persist console layout across sub-nav + add drop-in endpoint section - Keep the page-transition key stable within a console (derived from the shared console config) so the ConsoleLayout and its rail persist across item-to-item navigation instead of remounting — fixes the submenu flash. Cache /api/features across mounts and play the rail entrance animation only when actually entering a console. - Add a "One endpoint, every API" section to Home: leads with LocalAI's own native API (images, video, realtime voice over WebRTC/WS, depth, object detection, rerank, audio/TTS, face & voice recognition) plus a Full API reference link, then the drop-in compatibility layer (OpenAI, Anthropic, Ollama, OpenAI Responses) with the live copyable base URL. All 7 locales. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(ui): revert Middleware nav label rename (keep Middleware in all locales) Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
199 lines
9.7 KiB
JavaScript
199 lines
9.7 KiB
JavaScript
import { lazy } from 'react'
|
|
import { createBrowserRouter, Navigate, useParams } from 'react-router-dom'
|
|
import { routerBasename } from './utils/basePath'
|
|
import App from './App'
|
|
import RequireAdmin from './components/RequireAdmin'
|
|
import RequireAuth from './components/RequireAuth'
|
|
import RequireAuthEnabled from './components/RequireAuthEnabled'
|
|
import RequireFeature from './components/RequireFeature'
|
|
|
|
// Pages are code-split: each becomes its own chunk loaded on demand, so a route
|
|
// no longer drags every other page (and its heavy deps — CodeMirror, the MCP
|
|
// SDK, yaml, marked) into the initial bundle. The <Suspense> boundary in
|
|
// App.jsx (around <Outlet/>) shows nothing while a chunk loads, keeping the
|
|
// sidebar/header mounted.
|
|
//
|
|
// `page(key, loader)` registers the dynamic import under a route-segment key
|
|
// (the first segment after /app/) so a NavLink can warm the chunk on hover via
|
|
// `preloadRoute('/app/chat')`. Dynamic import() is memoised by the module
|
|
// loader, so a preloaded chunk is reused — not re-fetched — when the user
|
|
// actually navigates. Pages with `key: null` aren't sidebar-reachable; they
|
|
// still code-split, they just won't be preloaded from the nav.
|
|
const preloaders = {}
|
|
function page(key, loader) {
|
|
if (key !== null) preloaders[key] = loader
|
|
return lazy(loader)
|
|
}
|
|
|
|
export function preloadRoute(path) {
|
|
if (!path) return
|
|
const m = path.match(/^\/app(?:\/([^/?#]*))?/)
|
|
if (!m) return
|
|
preloaders[m[1] ?? '']?.().catch(() => { /* network blip — real click will retry */ })
|
|
}
|
|
|
|
const Home = page('', () => import('./pages/Home'))
|
|
const Chat = page('chat', () => import('./pages/Chat'))
|
|
const Models = page('models', () => import('./pages/Models'))
|
|
const Manage = page('manage', () => import('./pages/Manage'))
|
|
const ImageGen = page('image', () => import('./pages/ImageGen'))
|
|
const VideoGen = page('video', () => import('./pages/VideoGen'))
|
|
const TTS = page('tts', () => import('./pages/TTS'))
|
|
const Sound = page('sound', () => import('./pages/Sound'))
|
|
const AudioTransform = page('transform', () => import('./pages/AudioTransform'))
|
|
const Talk = page('talk', () => import('./pages/Talk'))
|
|
const Backends = page('backends', () => import('./pages/Backends'))
|
|
const Settings = page('settings', () => import('./pages/Settings'))
|
|
const Traces = page('traces', () => import('./pages/Traces'))
|
|
const P2P = page('p2p', () => import('./pages/P2P'))
|
|
const Agents = page('agents', () => import('./pages/Agents'))
|
|
const AgentCreate = page(null, () => import('./pages/AgentCreate'))
|
|
const AgentChat = page(null, () => import('./pages/AgentChat'))
|
|
const AgentStatus = page(null, () => import('./pages/AgentStatus'))
|
|
const Collections = page('collections', () => import('./pages/Collections'))
|
|
const CollectionDetails = page(null, () => import('./pages/CollectionDetails'))
|
|
const Skills = page('skills', () => import('./pages/Skills'))
|
|
const SkillEdit = page(null, () => import('./pages/SkillEdit'))
|
|
const AgentJobs = page('agent-jobs', () => import('./pages/AgentJobs'))
|
|
const AgentTaskDetails = page(null, () => import('./pages/AgentTaskDetails'))
|
|
const AgentJobDetails = page(null, () => import('./pages/AgentJobDetails'))
|
|
const ModelEditor = page(null, () => import('./pages/ModelEditor'))
|
|
// PipelineEditor removed — the Model Editor with templates handles all model types
|
|
const ImportModel = page(null, () => import('./pages/ImportModel'))
|
|
const BackendLogs = page(null, () => import('./pages/BackendLogs'))
|
|
const Explorer = page(null, () => import('./pages/Explorer'))
|
|
const Login = page(null, () => import('./pages/Login'))
|
|
const FineTune = page('fine-tune', () => import('./pages/FineTune'))
|
|
const Quantize = page('quantize', () => import('./pages/Quantize'))
|
|
const Studio = page('studio', () => import('./pages/Studio'))
|
|
const FaceRecognition = page('face', () => import('./pages/FaceRecognition'))
|
|
const VoiceRecognition = page('voice', () => import('./pages/VoiceRecognition'))
|
|
const Nodes = page('nodes', () => import('./pages/Nodes'))
|
|
const NodeBackendLogs = page(null, () => import('./pages/NodeBackendLogs'))
|
|
const NotFound = page(null, () => import('./pages/NotFound'))
|
|
const Usage = page('usage', () => import('./pages/Usage'))
|
|
const Users = page('users', () => import('./pages/Users'))
|
|
const Middleware = page('middleware', () => import('./pages/Middleware'))
|
|
const Account = page('account', () => import('./pages/Account'))
|
|
|
|
import ConsoleLayout from './components/console/ConsoleLayout'
|
|
import { buildConsole, operateConsole } from './components/console/consoleConfig'
|
|
|
|
function BrowseRedirect() {
|
|
const { '*': splat } = useParams()
|
|
return <Navigate to={`/app/${splat || ''}`} replace />
|
|
}
|
|
|
|
|
|
function Admin({ children }) {
|
|
return <RequireAdmin>{children}</RequireAdmin>
|
|
}
|
|
|
|
function Feature({ feature, children }) {
|
|
return <RequireFeature feature={feature}>{children}</RequireFeature>
|
|
}
|
|
|
|
const appChildren = [
|
|
{ index: true, element: <Home /> },
|
|
{ path: 'chat', element: <Chat /> },
|
|
{ path: 'chat/:model', element: <Chat /> },
|
|
{ path: 'image', element: <ImageGen /> },
|
|
{ path: 'image/:model', element: <ImageGen /> },
|
|
{ path: 'video', element: <VideoGen /> },
|
|
{ path: 'video/:model', element: <VideoGen /> },
|
|
{ path: 'tts', element: <TTS /> },
|
|
{ path: 'tts/:model', element: <TTS /> },
|
|
{ path: 'sound', element: <Sound /> },
|
|
{ path: 'sound/:model', element: <Sound /> },
|
|
{ path: 'transform', element: <Feature feature="audio_transform"><AudioTransform /></Feature> },
|
|
{ path: 'transform/:model', element: <Feature feature="audio_transform"><AudioTransform /></Feature> },
|
|
{ path: 'studio', element: <Studio /> },
|
|
{ path: 'talk', element: <Talk /> },
|
|
{ path: 'account', element: <Account /> },
|
|
|
|
// Build console — Automation, Training, and Recognition groups share one rail.
|
|
// Only the section landing pages live under the rail; deep create/edit/chat
|
|
// flows below render full-width.
|
|
{
|
|
element: <ConsoleLayout config={buildConsole} />,
|
|
children: [
|
|
{ path: 'agents', element: <Feature feature="agents"><Agents /></Feature> },
|
|
{ path: 'skills', element: <Feature feature="skills"><Skills /></Feature> },
|
|
{ path: 'collections', element: <Feature feature="collections"><Collections /></Feature> },
|
|
{ path: 'agent-jobs', element: <Feature feature="mcp_jobs"><AgentJobs /></Feature> },
|
|
{ path: 'fine-tune', element: <Feature feature="fine_tuning"><FineTune /></Feature> },
|
|
{ path: 'quantize', element: <Feature feature="quantization"><Quantize /></Feature> },
|
|
{ path: 'face', element: <Feature feature="face_recognition"><FaceRecognition /></Feature> },
|
|
{ path: 'face/:model', element: <Feature feature="face_recognition"><FaceRecognition /></Feature> },
|
|
{ path: 'voice', element: <Feature feature="voice_recognition"><VoiceRecognition /></Feature> },
|
|
{ path: 'voice/:model', element: <Feature feature="voice_recognition"><VoiceRecognition /></Feature> },
|
|
],
|
|
},
|
|
// Build deep flows — full-width, no rail.
|
|
{ path: 'agents/new', element: <Feature feature="agents"><AgentCreate /></Feature> },
|
|
{ path: 'agents/:name/edit', element: <Feature feature="agents"><AgentCreate /></Feature> },
|
|
{ path: 'agents/:name/chat', element: <Feature feature="agents"><AgentChat /></Feature> },
|
|
{ path: 'agents/:name/status', element: <Feature feature="agents"><AgentStatus /></Feature> },
|
|
{ path: 'collections/:name', element: <Feature feature="collections"><CollectionDetails /></Feature> },
|
|
{ path: 'skills/new', element: <Feature feature="skills"><SkillEdit /></Feature> },
|
|
{ path: 'skills/edit/:name', element: <Feature feature="skills"><SkillEdit /></Feature> },
|
|
{ path: 'agent-jobs/tasks/new', element: <Feature feature="mcp_jobs"><AgentTaskDetails /></Feature> },
|
|
{ path: 'agent-jobs/tasks/:id', element: <Feature feature="mcp_jobs"><AgentTaskDetails /></Feature> },
|
|
{ path: 'agent-jobs/tasks/:id/edit', element: <Feature feature="mcp_jobs"><AgentTaskDetails /></Feature> },
|
|
{ path: 'agent-jobs/jobs/:id', element: <Feature feature="mcp_jobs"><AgentJobDetails /></Feature> },
|
|
|
|
// Operate console (admin).
|
|
{
|
|
element: <ConsoleLayout config={operateConsole} />,
|
|
children: [
|
|
{ path: 'backends', element: <Admin><Backends /></Admin> },
|
|
{ path: 'settings', element: <Admin><Settings /></Admin> },
|
|
{ path: 'traces', element: <Admin><Traces /></Admin> },
|
|
{ path: 'backend-logs/:modelId', element: <Admin><BackendLogs /></Admin> },
|
|
{ path: 'p2p', element: <Admin><P2P /></Admin> },
|
|
{ path: 'nodes', element: <Admin><Nodes /></Admin> },
|
|
{ path: 'node-backend-logs/:nodeId/:modelId', element: <Admin><NodeBackendLogs /></Admin> },
|
|
{ path: 'usage', element: <Usage /> },
|
|
{ path: 'users', element: <RequireAuthEnabled><Admin><Users /></Admin></RequireAuthEnabled> },
|
|
{ path: 'middleware', element: <Admin><Middleware /></Admin> },
|
|
{ path: 'manage', element: <Admin><Manage /></Admin> },
|
|
],
|
|
},
|
|
|
|
// Models management (Install Models) — top-level destination, full-width.
|
|
{ path: 'models', element: <Admin><Models /></Admin> },
|
|
{ path: 'model-editor', element: <Admin><ModelEditor /></Admin> },
|
|
{ path: 'model-editor/:name', element: <Admin><ModelEditor /></Admin> },
|
|
{ path: 'import-model', element: <Admin><ImportModel /></Admin> },
|
|
{ path: '*', element: <NotFound /> },
|
|
]
|
|
|
|
export const router = createBrowserRouter([
|
|
{
|
|
path: '/login',
|
|
element: <Login />,
|
|
},
|
|
{
|
|
path: '/invite/:code',
|
|
element: <Login />,
|
|
},
|
|
{
|
|
path: '/explorer',
|
|
element: <Explorer />,
|
|
},
|
|
{
|
|
path: '/app',
|
|
element: <RequireAuth><App /></RequireAuth>,
|
|
children: appChildren,
|
|
},
|
|
// Backward compatibility: redirect /browse/* to /app/*
|
|
{
|
|
path: '/browse/*',
|
|
element: <BrowseRedirect />,
|
|
},
|
|
{
|
|
path: '/',
|
|
element: <Navigate to="/app" replace />,
|
|
},
|
|
], { basename: routerBasename })
|