Files
llama.cpp/tools/server/webui/src/routes/+layout.svelte
T
Pascal 81d54bbfd5 webui: remove client-side context pre-check and rely on backend for limits (#16506)
* fix: make SSE client robust to premature [DONE] in agentic proxy chains

* webui: remove client-side context pre-check and rely on backend for limits

Removed the client-side context window pre-check and now simply sends messages
while keeping the dialog imports limited to core components, eliminating the
maximum context alert path

Simplified streaming and non-streaming chat error handling to surface a generic
'No response received from server' error whenever the backend returns no content

Removed the obsolete maxContextError plumbing from the chat store so state
management now focuses on the core message flow without special context-limit cases

* webui: cosmetic rename of error messages

* Update tools/server/webui/src/lib/stores/chat.svelte.ts

Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com>

* Update tools/server/webui/src/lib/stores/chat.svelte.ts

Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com>

* Update tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte

Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com>

* Update tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte

Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com>

* chore: update webui build output

---------

Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com>
2025-10-12 18:06:41 +02:00

172 lines
4.7 KiB
Svelte

<script lang="ts">
import '../app.css';
import { page } from '$app/state';
import { ChatSidebar, ConversationTitleUpdateDialog } from '$lib/components/app';
import {
activeMessages,
isLoading,
setTitleUpdateConfirmationCallback
} from '$lib/stores/chat.svelte';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import { serverStore } from '$lib/stores/server.svelte';
import { config } from '$lib/stores/settings.svelte';
import { ModeWatcher } from 'mode-watcher';
import { Toaster } from 'svelte-sonner';
import { goto } from '$app/navigation';
let { children } = $props();
let isChatRoute = $derived(page.route.id === '/chat/[id]');
let isHomeRoute = $derived(page.route.id === '/');
let isNewChatMode = $derived(page.url.searchParams.get('new_chat') === 'true');
let showSidebarByDefault = $derived(activeMessages().length > 0 || isLoading());
let sidebarOpen = $state(false);
let innerHeight = $state<number | undefined>();
let chatSidebar:
| { activateSearchMode?: () => void; editActiveConversation?: () => void }
| undefined = $state();
// Conversation title update dialog state
let titleUpdateDialogOpen = $state(false);
let titleUpdateCurrentTitle = $state('');
let titleUpdateNewTitle = $state('');
let titleUpdateResolve: ((value: boolean) => void) | null = null;
// Global keyboard shortcuts
function handleKeydown(event: KeyboardEvent) {
const isCtrlOrCmd = event.ctrlKey || event.metaKey;
if (isCtrlOrCmd && event.key === 'k') {
event.preventDefault();
if (chatSidebar?.activateSearchMode) {
chatSidebar.activateSearchMode();
sidebarOpen = true;
}
}
if (isCtrlOrCmd && event.shiftKey && event.key === 'o') {
event.preventDefault();
goto('?new_chat=true#/');
}
if (event.shiftKey && isCtrlOrCmd && event.key === 'e') {
event.preventDefault();
if (chatSidebar?.editActiveConversation) {
chatSidebar.editActiveConversation();
}
}
}
function handleTitleUpdateCancel() {
titleUpdateDialogOpen = false;
if (titleUpdateResolve) {
titleUpdateResolve(false);
titleUpdateResolve = null;
}
}
function handleTitleUpdateConfirm() {
titleUpdateDialogOpen = false;
if (titleUpdateResolve) {
titleUpdateResolve(true);
titleUpdateResolve = null;
}
}
$effect(() => {
if (isHomeRoute && !isNewChatMode) {
// Auto-collapse sidebar when navigating to home route (but not in new chat mode)
sidebarOpen = false;
} else if (isHomeRoute && isNewChatMode) {
// Keep sidebar open in new chat mode
sidebarOpen = true;
} else if (isChatRoute) {
// On chat routes, show sidebar by default
sidebarOpen = true;
} else {
// Other routes follow default behavior
sidebarOpen = showSidebarByDefault;
}
});
// Initialize server properties on app load
$effect(() => {
serverStore.fetchServerProps();
});
// Monitor API key changes and redirect to error page if removed or changed when required
$effect(() => {
const apiKey = config().apiKey;
if (
(page.route.id === '/' || page.route.id === '/chat/[id]') &&
page.status !== 401 &&
page.status !== 403
) {
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
if (apiKey && apiKey.trim() !== '') {
headers.Authorization = `Bearer ${apiKey.trim()}`;
}
fetch(`./props`, { headers })
.then((response) => {
if (response.status === 401 || response.status === 403) {
window.location.reload();
}
})
.catch((e) => {
console.error('Error checking API key:', e);
});
}
});
// Set up title update confirmation callback
$effect(() => {
setTitleUpdateConfirmationCallback(async (currentTitle: string, newTitle: string) => {
return new Promise<boolean>((resolve) => {
titleUpdateCurrentTitle = currentTitle;
titleUpdateNewTitle = newTitle;
titleUpdateResolve = resolve;
titleUpdateDialogOpen = true;
});
});
});
</script>
<ModeWatcher />
<Toaster richColors />
<ConversationTitleUpdateDialog
bind:open={titleUpdateDialogOpen}
currentTitle={titleUpdateCurrentTitle}
newTitle={titleUpdateNewTitle}
onConfirm={handleTitleUpdateConfirm}
onCancel={handleTitleUpdateCancel}
/>
<Sidebar.Provider bind:open={sidebarOpen}>
<div class="flex h-screen w-full" style:height="{innerHeight}px">
<Sidebar.Root class="h-full">
<ChatSidebar bind:this={chatSidebar} />
</Sidebar.Root>
<Sidebar.Trigger
class="transition-left absolute h-8 w-8 duration-200 ease-linear {sidebarOpen
? 'md:left-[var(--sidebar-width)]'
: 'left-0'}"
style="translate: 1rem 1rem; z-index: 99999;"
/>
<Sidebar.Inset class="flex flex-1 flex-col overflow-hidden">
{@render children?.()}
</Sidebar.Inset>
</div>
</Sidebar.Provider>
<svelte:window onkeydown={handleKeydown} bind:innerHeight />