Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 0 additions & 36 deletions tools/server/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()}`
Expand All @@ -77,7 +77,7 @@

// Show success state briefly, then navigate to home
setTimeout(() => {
goto('/');
goto(`#/`);
}, 1000);
} else {
// API key is invalid - User Story A
Expand Down
4 changes: 2 additions & 2 deletions tools/server/webui/src/lib/services/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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}` } : {})
Expand Down
2 changes: 1 addition & 1 deletion tools/server/webui/src/lib/services/slots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}` } : {})
}
Expand Down
4 changes: 2 additions & 2 deletions tools/server/webui/src/lib/stores/chat.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class ChatStore {

this.maxContextError = null;

await goto(`/chat/${conversation.id}`);
await goto(`#/chat/${conversation.id}`);

return conversation.id;
}
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion tools/server/webui/src/lib/stores/server.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}` } : {})
}
Expand Down
2 changes: 1 addition & 1 deletion tools/server/webui/src/lib/utils/api-key-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions tools/server/webui/src/routes/+error.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

function handleRetry() {
// Navigate back to home page after successful API key validation
goto('/');
goto('#/');
}
</script>

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tools/server/webui/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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();
Expand Down
3 changes: 0 additions & 3 deletions tools/server/webui/src/routes/+layout.ts

This file was deleted.

4 changes: 2 additions & 2 deletions tools/server/webui/src/routes/chat/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
});
Expand All @@ -44,7 +44,7 @@
const success = await chatStore.loadConversation(chatId);

if (!success) {
await goto('/');
await goto('#/');
}
})();
}
Expand Down
4 changes: 4 additions & 0 deletions tools/server/webui/svelte.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading