From 5ecda78be4a57a5207e0e489da414310ffd73be7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 11 Jan 2026 08:34:47 +0000 Subject: [PATCH] Fix: Move Alpine.js router store registration inline The spa-router.js was loaded with defer but registered the Alpine.js store using the 'alpine:init' event. Since Alpine.js also loads with defer, there was a race condition where Alpine could initialize before the event listener was registered, causing $store.router to be undefined. Moved the entire router store definition and registration inline in spa.html so it's guaranteed to be registered before Alpine.js initializes. Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> --- core/http/views/spa.html | 119 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/core/http/views/spa.html b/core/http/views/spa.html index b78013213..94e7c7362 100644 --- a/core/http/views/spa.html +++ b/core/http/views/spa.html @@ -351,6 +351,125 @@ window.startChatSPA = startChatSPA; window.stopModel = stopModel; window.stopAllModels = stopAllModels; + + // ======================================== + // SPA Router - Alpine.js Store Definition + // Must be defined before Alpine.js initializes + // ======================================== + + // Define routes and their corresponding view IDs + const SPA_ROUTES = { + 'home': { title: 'LocalAI', viewId: 'view-home', paths: ['/', ''] }, + 'chat': { title: 'LocalAI - Chat', viewId: 'view-chat', paths: ['/chat'] }, + 'text2image': { title: 'LocalAI - Images', viewId: 'view-text2image', paths: ['/text2image'] }, + 'tts': { title: 'LocalAI - TTS', viewId: 'view-tts', paths: ['/tts'] }, + 'talk': { title: 'LocalAI - Talk', viewId: 'view-talk', paths: ['/talk'] }, + 'manage': { title: 'LocalAI - System', viewId: 'view-manage', paths: ['/manage'] }, + 'browse': { title: 'LocalAI - Model Gallery', viewId: 'view-browse', paths: ['/browse'] } + }; + + // Parse URL path to determine route + function parseUrlPath(pathname) { + pathname = pathname.replace(/\/$/, '') || '/'; + + // Check for hash-based routes first + const hash = window.location.hash.slice(1); + if (hash) { + const hashParts = hash.split('/'); + const route = hashParts[0]; + const model = hashParts[1] || null; + if (SPA_ROUTES[route]) { + return { route, params: model ? { model } : {} }; + } + } + + // Check path-based routes + for (const [route, config] of Object.entries(SPA_ROUTES)) { + for (const path of config.paths) { + if (pathname === path) { + return { route, params: {} }; + } + if (pathname.startsWith(path + '/')) { + const param = pathname.slice(path.length + 1); + if (param) { + return { route, params: { model: param } }; + } + } + } + } + + return { route: 'home', params: {} }; + } + + // Register the router store with Alpine.js on init event + document.addEventListener('alpine:init', () => { + const initialRoute = parseUrlPath(window.location.pathname); + + Alpine.store('router', { + currentRoute: initialRoute.route, + routeParams: initialRoute.params, + previousRoute: null, + + navigate(route, params = {}) { + if (!SPA_ROUTES[route]) { + console.warn('Unknown route:', route); + return; + } + + this.previousRoute = this.currentRoute; + this.currentRoute = route; + this.routeParams = params; + + document.title = SPA_ROUTES[route].title; + + const url = route === 'home' ? '/' : '/#' + route; + if (params.model) { + window.history.pushState({ route, params }, '', '/#' + route + '/' + params.model); + } else { + window.history.pushState({ route, params }, '', url); + } + + window.scrollTo(0, 0); + window.dispatchEvent(new CustomEvent('spa:navigate', { + detail: { route, params, previousRoute: this.previousRoute } + })); + }, + + isRoute(route) { + return this.currentRoute === route; + }, + + navigateToChat(model) { + this.navigate('chat', { model }); + }, + + navigateToText2Image(model) { + this.navigate('text2image', { model }); + }, + + navigateToTTS(model) { + this.navigate('tts', { model }); + } + }); + }); + + // Handle browser back/forward buttons + window.addEventListener('popstate', (event) => { + if (window.Alpine && Alpine.store('router')) { + if (event.state && event.state.route) { + Alpine.store('router').currentRoute = event.state.route; + Alpine.store('router').routeParams = event.state.params || {}; + } else { + const parsed = parseUrlPath(window.location.pathname); + Alpine.store('router').currentRoute = parsed.route; + Alpine.store('router').routeParams = parsed.params; + } + } + }); + + // Export for use in other scripts + window.SPA_ROUTES = SPA_ROUTES; + window.parseUrlPath = parseUrlPath;