* 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:
parent
5d0a40f390
commit
e0539eb6ae
|
|
@ -5262,42 +5262,6 @@ int main(int argc, char ** argv) {
|
||||||
svr->Get (params.api_prefix + "/slots", handle_slots);
|
svr->Get (params.api_prefix + "/slots", handle_slots);
|
||||||
svr->Post(params.api_prefix + "/slots/:id_slot", handle_slots_action);
|
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
|
// Start the server
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -64,13 +64,13 @@
|
||||||
searchQuery = '';
|
searchQuery = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
await goto(`/chat/${id}`);
|
await goto(`#/chat/${id}`);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ScrollArea class="h-[100vh]">
|
<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">
|
<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>
|
<h1 class="inline-flex items-center gap-1 px-2 text-xl font-semibold">llama.cpp</h1>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<Button
|
<Button
|
||||||
class="w-full justify-between hover:[&>kbd]:opacity-100"
|
class="w-full justify-between hover:[&>kbd]:opacity-100"
|
||||||
href="/?new_chat=true"
|
href="?new_chat=true#/"
|
||||||
onclick={handleMobileSidebarItemClick}
|
onclick={handleMobileSidebarItemClick}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@
|
||||||
updateConfig('apiKey', apiKeyInput.trim());
|
updateConfig('apiKey', apiKeyInput.trim());
|
||||||
|
|
||||||
// Test the API key by making a real request to the server
|
// Test the API key by making a real request to the server
|
||||||
const response = await fetch('/props', {
|
const response = await fetch('./props', {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${apiKeyInput.trim()}`
|
Authorization: `Bearer ${apiKeyInput.trim()}`
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
|
|
||||||
// Show success state briefly, then navigate to home
|
// Show success state briefly, then navigate to home
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
goto('/');
|
goto(`#/`);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
// API key is invalid - User Story A
|
// API key is invalid - User Story A
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ export class ChatService {
|
||||||
const currentConfig = config();
|
const currentConfig = config();
|
||||||
const apiKey = currentConfig.apiKey?.toString().trim();
|
const apiKey = currentConfig.apiKey?.toString().trim();
|
||||||
|
|
||||||
const response = await fetch(`/v1/chat/completions`, {
|
const response = await fetch(`./v1/chat/completions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
@ -533,7 +533,7 @@ export class ChatService {
|
||||||
const currentConfig = config();
|
const currentConfig = config();
|
||||||
const apiKey = currentConfig.apiKey?.toString().trim();
|
const apiKey = currentConfig.apiKey?.toString().trim();
|
||||||
|
|
||||||
const response = await fetch(`/props`, {
|
const response = await fetch(`./props`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ export class SlotsService {
|
||||||
const currentConfig = config();
|
const currentConfig = config();
|
||||||
const apiKey = currentConfig.apiKey?.toString().trim();
|
const apiKey = currentConfig.apiKey?.toString().trim();
|
||||||
|
|
||||||
const response = await fetch('/slots', {
|
const response = await fetch(`./slots`, {
|
||||||
headers: {
|
headers: {
|
||||||
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ class ChatStore {
|
||||||
|
|
||||||
this.maxContextError = null;
|
this.maxContextError = null;
|
||||||
|
|
||||||
await goto(`/chat/${conversation.id}`);
|
await goto(`#/chat/${conversation.id}`);
|
||||||
|
|
||||||
return conversation.id;
|
return conversation.id;
|
||||||
}
|
}
|
||||||
|
|
@ -910,7 +910,7 @@ class ChatStore {
|
||||||
if (this.activeConversation?.id === convId) {
|
if (this.activeConversation?.id === convId) {
|
||||||
this.activeConversation = null;
|
this.activeConversation = null;
|
||||||
this.activeMessages = [];
|
this.activeMessages = [];
|
||||||
await goto('/?new_chat=true');
|
await goto(`?new_chat=true#/`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete conversation:', error);
|
console.error('Failed to delete conversation:', error);
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ class ServerStore {
|
||||||
const currentConfig = config();
|
const currentConfig = config();
|
||||||
const apiKey = currentConfig.apiKey?.toString().trim();
|
const apiKey = currentConfig.apiKey?.toString().trim();
|
||||||
|
|
||||||
const response = await fetch('/slots', {
|
const response = await fetch(`./slots`, {
|
||||||
headers: {
|
headers: {
|
||||||
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export async function validateApiKey(fetch: typeof globalThis.fetch): Promise<vo
|
||||||
headers.Authorization = `Bearer ${apiKey}`;
|
headers.Authorization = `Bearer ${apiKey}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/props', { headers });
|
const response = await fetch(`./props`, { headers });
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
function handleRetry() {
|
function handleRetry() {
|
||||||
// Navigate back to home page after successful API key validation
|
// Navigate back to home page after successful API key validation
|
||||||
goto('/');
|
goto('#/');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onclick={() => goto('/')}
|
onclick={() => goto('#/')}
|
||||||
class="rounded-md bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
|
class="rounded-md bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
Go Home
|
Go Home
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
|
|
||||||
if (isCtrlOrCmd && event.shiftKey && event.key === 'o') {
|
if (isCtrlOrCmd && event.shiftKey && event.key === 'o') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
goto('/?new_chat=true');
|
goto('?new_chat=true#/');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.shiftKey && isCtrlOrCmd && event.key === 'e') {
|
if (event.shiftKey && isCtrlOrCmd && event.key === 'e') {
|
||||||
|
|
@ -115,7 +115,7 @@
|
||||||
headers.Authorization = `Bearer ${apiKey.trim()}`;
|
headers.Authorization = `Bearer ${apiKey.trim()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch('/props', { headers })
|
fetch(`./props`, { headers })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
window.location.reload();
|
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();
|
await gracefulStop();
|
||||||
|
|
||||||
if (to?.url) {
|
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);
|
const success = await chatStore.loadConversation(chatId);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
await goto('/');
|
await goto('#/');
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ const config = {
|
||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: [vitePreprocess(), mdsvex()],
|
preprocess: [vitePreprocess(), mdsvex()],
|
||||||
kit: {
|
kit: {
|
||||||
|
paths: {
|
||||||
|
relative: true
|
||||||
|
},
|
||||||
|
router: { type: 'hash' },
|
||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
pages: '../public',
|
pages: '../public',
|
||||||
assets: '../public',
|
assets: '../public',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue