]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
webui: switch to hash-based routing (alternative of #16079) (#16157)
authorIsaac McFadyen <redacted>
Fri, 26 Sep 2025 15:36:48 +0000 (11:36 -0400)
committerGitHub <redacted>
Fri, 26 Sep 2025 15:36:48 +0000 (18:36 +0300)
* Switched web UI to hash-based routing

* Added hash to missed goto function call

* Removed outdated SPA handling code

* Fixed broken sidebar home link

14 files changed:
tools/server/server.cpp
tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebar.svelte
tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebarActions.svelte
tools/server/webui/src/lib/components/app/server/ServerErrorSplash.svelte
tools/server/webui/src/lib/services/chat.ts
tools/server/webui/src/lib/services/slots.ts
tools/server/webui/src/lib/stores/chat.svelte.ts
tools/server/webui/src/lib/stores/server.svelte.ts
tools/server/webui/src/lib/utils/api-key-validation.ts
tools/server/webui/src/routes/+error.svelte
tools/server/webui/src/routes/+layout.svelte
tools/server/webui/src/routes/+layout.ts [deleted file]
tools/server/webui/src/routes/chat/[id]/+page.svelte
tools/server/webui/svelte.config.js

index 129801fe06daacf0722fef81e8e618c71bc07a99..6062904a8c7c00df5ab5133348a3465dfe1247b0 100644 (file)
@@ -5262,42 +5262,6 @@ int main(int argc, char ** argv) {
     svr->Get (params.api_prefix + "/slots",               handle_slots);
     svr->Post(params.api_prefix + "/slots/:id_slot",      handle_slots_action);
 
-    // SPA fallback route - serve index.html for any route that doesn't match API endpoints
-    // This enables client-side routing for dynamic routes like /chat/[id]
-    if (params.webui && params.public_path.empty()) {
-        // Only add fallback when using embedded static files
-        svr->Get(".*", [](const httplib::Request & req, httplib::Response & res) {
-            // Skip API routes - they should have been handled above
-            if (req.path.find("/v1/") != std::string::npos ||
-                req.path.find("/health") != std::string::npos ||
-                req.path.find("/metrics") != std::string::npos ||
-                req.path.find("/props") != std::string::npos ||
-                req.path.find("/models") != std::string::npos ||
-                req.path.find("/api/tags") != std::string::npos ||
-                req.path.find("/completions") != std::string::npos ||
-                req.path.find("/chat/completions") != std::string::npos ||
-                req.path.find("/embeddings") != std::string::npos ||
-                req.path.find("/tokenize") != std::string::npos ||
-                req.path.find("/detokenize") != std::string::npos ||
-                req.path.find("/lora-adapters") != std::string::npos ||
-                req.path.find("/slots") != std::string::npos) {
-                return false; // Let other handlers process API routes
-            }
-
-            // Serve index.html for all other routes (SPA fallback)
-            if (req.get_header_value("Accept-Encoding").find("gzip") == std::string::npos) {
-                res.set_content("Error: gzip is not supported by this browser", "text/plain");
-            } else {
-                res.set_header("Content-Encoding", "gzip");
-                // COEP and COOP headers, required by pyodide (python interpreter)
-                res.set_header("Cross-Origin-Embedder-Policy", "require-corp");
-                res.set_header("Cross-Origin-Opener-Policy", "same-origin");
-                res.set_content(reinterpret_cast<const char*>(index_html_gz), index_html_gz_len, "text/html; charset=utf-8");
-            }
-            return false;
-        });
-    }
-
     //
     // Start the server
     //
index 05c775f2f5a0309518ed1829579008770e044d3e..6af348e696da72370cc3d889b05e920ff13f7744 100644 (file)
                        searchQuery = '';
                }
 
-               await goto(`/chat/${id}`);
+               await goto(`#/chat/${id}`);
        }
 </script>
 
 <ScrollArea class="h-[100vh]">
        <Sidebar.Header class=" top-0 z-10 gap-6 bg-sidebar/50 px-4 pt-4 pb-2 backdrop-blur-lg md:sticky">
-               <a href="/" onclick={handleMobileSidebarItemClick}>
+               <a href="#/" onclick={handleMobileSidebarItemClick}>
                        <h1 class="inline-flex items-center gap-1 px-2 text-xl font-semibold">llama.cpp</h1>
                </a>
 
index b946ef07497cc9fe64b95d925c9ead5a74136a06..30d1f9d4b7e98a3f8cf846d8d73bb3e25660e9ae 100644 (file)
@@ -51,7 +51,7 @@
        {:else}
                <Button
                        class="w-full justify-between hover:[&>kbd]:opacity-100"
-                       href="/?new_chat=true"
+                       href="?new_chat=true#/"
                        onclick={handleMobileSidebarItemClick}
                        variant="ghost"
                >
index cd52e75490520559b85ea9b3a5894f28987ac053..af142e32aa1839455cd1135470fbf3188be8c925 100644 (file)
@@ -64,7 +64,7 @@
                        updateConfig('apiKey', apiKeyInput.trim());
 
                        // Test the API key by making a real request to the server
-                       const response = await fetch('/props', {
+                       const response = await fetch('./props', {
                                headers: {
                                        'Content-Type': 'application/json',
                                        Authorization: `Bearer ${apiKeyInput.trim()}`
@@ -77,7 +77,7 @@
 
                                // Show success state briefly, then navigate to home
                                setTimeout(() => {
-                                       goto('/');
+                                       goto(`#/`);
                                }, 1000);
                        } else {
                                // API key is invalid - User Story A
index 91573a86640781012f630c2a07ded5c74ddd2f70..369cdf4e8b9359e09c6aea7cac6aa338ecec67d9 100644 (file)
@@ -164,7 +164,7 @@ export class ChatService {
                        const currentConfig = config();
                        const apiKey = currentConfig.apiKey?.toString().trim();
 
-                       const response = await fetch(`/v1/chat/completions`, {
+                       const response = await fetch(`./v1/chat/completions`, {
                                method: 'POST',
                                headers: {
                                        'Content-Type': 'application/json',
@@ -533,7 +533,7 @@ export class ChatService {
                        const currentConfig = config();
                        const apiKey = currentConfig.apiKey?.toString().trim();
 
-                       const response = await fetch(`/props`, {
+                       const response = await fetch(`./props`, {
                                headers: {
                                        'Content-Type': 'application/json',
                                        ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
index b93a5a90e706a9a844722adc81634c9a1fda87bf..06c0a77de91387018f229836ef45db98d5e9ff33 100644 (file)
@@ -138,7 +138,7 @@ export class SlotsService {
                        const currentConfig = config();
                        const apiKey = currentConfig.apiKey?.toString().trim();
 
-                       const response = await fetch('/slots', {
+                       const response = await fetch(`./slots`, {
                                headers: {
                                        ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
                                }
index ac5e986428ee38cf9e3920276c6210880a4b3010..57b9c33e5a6bdf23e45ab31a49360ca6857c4f8f 100644 (file)
@@ -100,7 +100,7 @@ class ChatStore {
 
                this.maxContextError = null;
 
-               await goto(`/chat/${conversation.id}`);
+               await goto(`#/chat/${conversation.id}`);
 
                return conversation.id;
        }
@@ -910,7 +910,7 @@ class ChatStore {
                        if (this.activeConversation?.id === convId) {
                                this.activeConversation = null;
                                this.activeMessages = [];
-                               await goto('/?new_chat=true');
+                               await goto(`?new_chat=true#/`);
                        }
                } catch (error) {
                        console.error('Failed to delete conversation:', error);
index 7abaa2bdf512b2f171fdf4e7890c72c15fdcdcfe..1b587c064425c1f3b64b230be1ae24cfa953b0f1 100644 (file)
@@ -98,7 +98,7 @@ class ServerStore {
                        const currentConfig = config();
                        const apiKey = currentConfig.apiKey?.toString().trim();
 
-                       const response = await fetch('/slots', {
+                       const response = await fetch(`./slots`, {
                                headers: {
                                        ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
                                }
index d7bbf9c2ebdc9659f3778aca1d40b7baff0ae870..a08e21b3938ae6d7dbd7d4ea0f9fb2fed806dd0d 100644 (file)
@@ -22,7 +22,7 @@ export async function validateApiKey(fetch: typeof globalThis.fetch): Promise<vo
                        headers.Authorization = `Bearer ${apiKey}`;
                }
 
-               const response = await fetch('/props', { headers });
+               const response = await fetch(`./props`, { headers });
 
                if (!response.ok) {
                        if (response.status === 401 || response.status === 403) {
index 8ec48dc01f9aaf4e02a876561dea7800c9636ecb..faddf0bcaa207b9d0c74fb8f5733c6daaeee4af6 100644 (file)
@@ -17,7 +17,7 @@
 
        function handleRetry() {
                // Navigate back to home page after successful API key validation
-               goto('/');
+               goto('#/');
        }
 </script>
 
@@ -60,7 +60,7 @@
                                </p>
                        </div>
                        <button
-                               onclick={() => goto('/')}
+                               onclick={() => goto('#/')}
                                class="rounded-md bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
                        >
                                Go Home
index 58749b9fe5d79e8fe32fd4c9a1f98d2ffdd075f8..6fbee0fe355006bbf3d1b26bd9e010e1560234b2 100644 (file)
@@ -49,7 +49,7 @@
 
                if (isCtrlOrCmd && event.shiftKey && event.key === 'o') {
                        event.preventDefault();
-                       goto('/?new_chat=true');
+                       goto('?new_chat=true#/');
                }
 
                if (event.shiftKey && isCtrlOrCmd && event.key === 'e') {
                                headers.Authorization = `Bearer ${apiKey.trim()}`;
                        }
 
-                       fetch('/props', { headers })
+                       fetch(`./props`, { headers })
                                .then((response) => {
                                        if (response.status === 401 || response.status === 403) {
                                                window.location.reload();
diff --git a/tools/server/webui/src/routes/+layout.ts b/tools/server/webui/src/routes/+layout.ts
deleted file mode 100644 (file)
index 2d3c0a0..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-export const csr = true;
-export const prerender = false;
-export const ssr = false;
index 705c0bd9278ebd069dd14e20bfa8df047c2be2b2..5b6c73d6d47968a2cb710d8dae75e6926c0c3d7c 100644 (file)
@@ -26,7 +26,7 @@
                        await gracefulStop();
 
                        if (to?.url) {
-                               await goto(to.url.pathname + to.url.search);
+                               await goto(to.url.pathname + to.url.search + to.url.hash);
                        }
                }
        });
@@ -44,7 +44,7 @@
                                const success = await chatStore.loadConversation(chatId);
 
                                if (!success) {
-                                       await goto('/');
+                                       await goto('#/');
                                }
                        })();
                }
index 722d40837953821374f75d7629a2e6a81c246f4d..c24f879ddaf42110a0acca5ed59b074bc0e64661 100644 (file)
@@ -8,6 +8,10 @@ const config = {
        // for more information about preprocessors
        preprocess: [vitePreprocess(), mdsvex()],
        kit: {
+               paths: {
+                       relative: true
+               },
+               router: { type: 'hash' },
                adapter: adapter({
                        pages: '../public',
                        assets: '../public',