mirror of
				https://github.com/ggml-org/llama.cpp.git
				synced 2025-10-31 08:51:55 +00:00 
			
		
		
		
	* Switched web UI to hash-based routing * Added hash to missed goto function call * Removed outdated SPA handling code * Fixed broken sidebar home link
This commit is contained in:
		| @@ -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 | ||||
|     // | ||||
|   | ||||
| @@ -64,13 +64,13 @@ | ||||
| 			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> | ||||
|  | ||||
|   | ||||
| @@ -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" | ||||
| 		> | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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}` } : {}) | ||||
|   | ||||
| @@ -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}` } : {}) | ||||
| 				} | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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}` } : {}) | ||||
| 				} | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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') { | ||||
| @@ -115,7 +115,7 @@ | ||||
| 				headers.Authorization = `Bearer ${apiKey.trim()}`; | ||||
| 			} | ||||
|  | ||||
| 			fetch('/props', { headers }) | ||||
| 			fetch(`./props`, { headers }) | ||||
| 				.then((response) => { | ||||
| 					if (response.status === 401 || response.status === 403) { | ||||
| 						window.location.reload(); | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| export const csr = true; | ||||
| export const prerender = false; | ||||
| export const ssr = false; | ||||
| @@ -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('#/'); | ||||
| 				} | ||||
| 			})(); | ||||
| 		} | ||||
|   | ||||
| @@ -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', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Isaac McFadyen
					Isaac McFadyen