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
//
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>
{:else}
<Button
class="w-full justify-between hover:[&>kbd]:opacity-100"
- href="/?new_chat=true"
+ href="?new_chat=true#/"
onclick={handleMobileSidebarItemClick}
variant="ghost"
>
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()}`
// Show success state briefly, then navigate to home
setTimeout(() => {
- goto('/');
+ goto(`#/`);
}, 1000);
} else {
// API key is invalid - User Story A
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',
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}` } : {})
const currentConfig = config();
const apiKey = currentConfig.apiKey?.toString().trim();
- const response = await fetch('/slots', {
+ const response = await fetch(`./slots`, {
headers: {
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
}
this.maxContextError = null;
- await goto(`/chat/${conversation.id}`);
+ await goto(`#/chat/${conversation.id}`);
return conversation.id;
}
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);
const currentConfig = config();
const apiKey = currentConfig.apiKey?.toString().trim();
- const response = await fetch('/slots', {
+ const response = await fetch(`./slots`, {
headers: {
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
}
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) {
function handleRetry() {
// Navigate back to home page after successful API key validation
- goto('/');
+ goto('#/');
}
</script>
</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
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();
+++ /dev/null
-export const csr = true;
-export const prerender = false;
-export const ssr = false;
await gracefulStop();
if (to?.url) {
- await goto(to.url.pathname + to.url.search);
+ await goto(to.url.pathname + to.url.search + to.url.hash);
}
}
});
const success = await chatStore.loadConversation(chatId);
if (!success) {
- await goto('/');
+ await goto('#/');
}
})();
}
// for more information about preprocessors
preprocess: [vitePreprocess(), mdsvex()],
kit: {
+ paths: {
+ relative: true
+ },
+ router: { type: 'hash' },
adapter: adapter({
pages: '../public',
assets: '../public',