81d54bbfd5
* 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>
172 lines
4.7 KiB
Svelte
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 />
|