Skip to content
Merged
Binary file modified tools/server/public/index.html.gz
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
ChatMessages,
ChatProcessingInfo,
EmptyFileAlertDialog,
ChatErrorDialog,
ServerErrorSplash,
ServerInfo,
ServerLoadingSplash,
Expand All @@ -22,10 +23,11 @@
activeMessages,
activeConversation,
deleteConversation,
dismissErrorDialog,
errorDialog,
isLoading,
sendMessage,
stopGeneration,
setMaxContextError
stopGeneration
} from '$lib/stores/chat.svelte';
import {
supportsVision,
Expand All @@ -34,7 +36,6 @@
serverWarning,
serverStore
} from '$lib/stores/server.svelte';
import { contextService } from '$lib/services';
import { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra';
import { isFileTypeSupported } from '$lib/utils/file-type';
import { filterFilesByModalities } from '$lib/utils/modality-file-validation';
Expand Down Expand Up @@ -79,6 +80,7 @@
showCenteredEmpty && !activeConversation() && activeMessages().length === 0 && !isLoading()
);

let activeErrorDialog = $derived(errorDialog());
let isServerLoading = $derived(serverLoading());

async function handleDeleteConfirm() {
Expand All @@ -105,6 +107,12 @@
}
}

function handleErrorDialogOpenChange(open: boolean) {
if (!open) {
dismissErrorDialog();
}
}

function handleDragOver(event: DragEvent) {
event.preventDefault();
}
Expand Down Expand Up @@ -183,21 +191,6 @@

const extras = result?.extras;

// Check context limit using real-time slots data
const contextCheck = await contextService.checkContextLimit();

if (contextCheck && contextCheck.wouldExceed) {
const errorMessage = contextService.getContextErrorMessage(contextCheck);

setMaxContextError({
message: errorMessage,
estimatedTokens: contextCheck.currentUsage,
maxContext: contextCheck.maxContext
});

return false;
}

// Enable autoscroll for user-initiated message sending
userScrolledUp = false;
autoScrollEnabled = true;
Expand Down Expand Up @@ -461,6 +454,13 @@
}}
/>

<ChatErrorDialog
message={activeErrorDialog?.message ?? ''}
onOpenChange={handleErrorDialogOpenChange}
open={Boolean(activeErrorDialog)}
type={activeErrorDialog?.type ?? 'server'}
/>

<style>
.conversation-chat-form {
position: relative;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script lang="ts">
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { AlertTriangle, TimerOff } from '@lucide/svelte';

interface Props {
open: boolean;
type: 'timeout' | 'server';
message: string;
onOpenChange?: (open: boolean) => void;
}

let { open = $bindable(), type, message, onOpenChange }: Props = $props();

const isTimeout = $derived(type === 'timeout');
const title = $derived(isTimeout ? 'TCP Timeout' : 'Server Error');
const description = $derived(
isTimeout
? 'The request did not receive a response from the server before timing out.'
: 'The server responded with an error message. Review the details below.'
);
const iconClass = $derived(isTimeout ? 'text-destructive' : 'text-amber-500');
const badgeClass = $derived(
isTimeout
? 'border-destructive/40 bg-destructive/10 text-destructive'
: 'border-amber-500/40 bg-amber-500/10 text-amber-600 dark:text-amber-400'
);

function handleOpenChange(newOpen: boolean) {
open = newOpen;
onOpenChange?.(newOpen);
}
</script>

<AlertDialog.Root {open} onOpenChange={handleOpenChange}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title class="flex items-center gap-2">
{#if isTimeout}
<TimerOff class={`h-5 w-5 ${iconClass}`} />
{:else}
<AlertTriangle class={`h-5 w-5 ${iconClass}`} />
{/if}

{title}
</AlertDialog.Title>

<AlertDialog.Description>
{description}
</AlertDialog.Description>
</AlertDialog.Header>

<div class={`rounded-lg border px-4 py-3 text-sm ${badgeClass}`}>
<p class="font-medium">{message}</p>
</div>

<AlertDialog.Footer>
<AlertDialog.Action onclick={() => handleOpenChange(false)}>Close</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>

This file was deleted.

3 changes: 1 addition & 2 deletions tools/server/webui/src/lib/components/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,11 @@ export { default as ChatSidebar } from './chat/ChatSidebar/ChatSidebar.svelte';
export { default as ChatSidebarConversationItem } from './chat/ChatSidebar/ChatSidebarConversationItem.svelte';
export { default as ChatSidebarSearch } from './chat/ChatSidebar/ChatSidebarSearch.svelte';

export { default as ChatErrorDialog } from './dialogs/ChatErrorDialog.svelte';
export { default as EmptyFileAlertDialog } from './dialogs/EmptyFileAlertDialog.svelte';

export { default as ConversationTitleUpdateDialog } from './dialogs/ConversationTitleUpdateDialog.svelte';

export { default as MaximumContextAlertDialog } from './dialogs/MaximumContextAlertDialog.svelte';

export { default as KeyboardShortcutInfo } from './misc/KeyboardShortcutInfo.svelte';

export { default as MarkdownContent } from './misc/MarkdownContent.svelte';
Expand Down
89 changes: 26 additions & 63 deletions tools/server/webui/src/lib/services/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { slotsService } from './slots';
* - Manages streaming and non-streaming response parsing
* - Provides request abortion capabilities
* - Converts database messages to API format
* - Handles error translation and context detection
* - Handles error translation for server responses
*
* - **ChatStore**: Stateful orchestration and UI state management
* - Uses ChatService for all AI model communication
Expand All @@ -26,7 +26,6 @@ import { slotsService } from './slots';
* - Streaming response handling with real-time callbacks
* - Reasoning content extraction and processing
* - File attachment processing (images, PDFs, audio, text)
* - Context error detection and reporting
* - Request lifecycle management (abort, cleanup)
*/
export class ChatService {
Expand Down Expand Up @@ -209,10 +208,13 @@ export class ChatService {
userFriendlyError = new Error(
'Unable to connect to server - please check if the server is running'
);
userFriendlyError.name = 'NetworkError';
} else if (error.message.includes('ECONNREFUSED')) {
userFriendlyError = new Error('Connection refused - server may be offline');
userFriendlyError.name = 'NetworkError';
} else if (error.message.includes('ETIMEDOUT')) {
userFriendlyError = new Error('Request timeout - server may be overloaded');
userFriendlyError = new Error('Request timed out - the server took too long to respond');
userFriendlyError.name = 'TimeoutError';
} else {
userFriendlyError = error;
}
Expand Down Expand Up @@ -262,6 +264,7 @@ export class ChatService {
let fullReasoningContent = '';
let hasReceivedData = false;
let lastTimings: ChatMessageTimings | undefined;
let streamFinished = false;

try {
let chunk = '';
Expand All @@ -277,18 +280,8 @@ export class ChatService {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
if (!hasReceivedData && aggregatedContent.length === 0) {
const contextError = new Error(
'The request exceeds the available context size. Try increasing the context size or enable context shift.'
);
contextError.name = 'ContextError';
onError?.(contextError);
return;
}

onComplete?.(aggregatedContent, fullReasoningContent || undefined, lastTimings);

return;
streamFinished = true;
continue;
}

try {
Expand Down Expand Up @@ -326,13 +319,13 @@ export class ChatService {
}
}

if (!hasReceivedData && aggregatedContent.length === 0) {
const contextError = new Error(
'The request exceeds the available context size. Try increasing the context size or enable context shift.'
);
contextError.name = 'ContextError';
onError?.(contextError);
return;
if (streamFinished) {
if (!hasReceivedData && aggregatedContent.length === 0) {
const noResponseError = new Error('No response received from server. Please try again.');
throw noResponseError;
}

onComplete?.(aggregatedContent, fullReasoningContent || undefined, lastTimings);
}
} catch (error) {
const err = error instanceof Error ? error : new Error('Stream error');
Expand Down Expand Up @@ -368,12 +361,8 @@ export class ChatService {
const responseText = await response.text();

if (!responseText.trim()) {
const contextError = new Error(
'The request exceeds the available context size. Try increasing the context size or enable context shift.'
);
contextError.name = 'ContextError';
onError?.(contextError);
throw contextError;
const noResponseError = new Error('No response received from server. Please try again.');
throw noResponseError;
}

const data: ApiChatCompletionResponse = JSON.parse(responseText);
Expand All @@ -385,22 +374,14 @@ export class ChatService {
}

if (!content.trim()) {
const contextError = new Error(
'The request exceeds the available context size. Try increasing the context size or enable context shift.'
);
contextError.name = 'ContextError';
onError?.(contextError);
throw contextError;
const noResponseError = new Error('No response received from server. Please try again.');
throw noResponseError;
}

onComplete?.(content, reasoningContent);

return content;
} catch (error) {
if (error instanceof Error && error.name === 'ContextError') {
throw error;
}

const err = error instanceof Error ? error : new Error('Parse error');

onError?.(err);
Expand Down Expand Up @@ -594,37 +575,19 @@ export class ChatService {
const errorText = await response.text();
const errorData: ApiErrorResponse = JSON.parse(errorText);

if (errorData.error?.type === 'exceed_context_size_error') {
const contextError = errorData.error as ApiContextSizeError;
const error = new Error(contextError.message);
error.name = 'ContextError';
// Attach structured context information
(
error as Error & {
contextInfo?: { promptTokens: number; maxContext: number; estimatedTokens: number };
}
).contextInfo = {
promptTokens: contextError.n_prompt_tokens,
maxContext: contextError.n_ctx,
estimatedTokens: contextError.n_prompt_tokens
};
return error;
}

// Fallback for other error types
const message = errorData.error?.message || 'Unknown server error';
return new Error(message);
const error = new Error(message);
error.name = response.status === 400 ? 'ServerError' : 'HttpError';

return error;
} catch {
// If we can't parse the error response, return a generic error
return new Error(`Server error (${response.status}): ${response.statusText}`);
const fallback = new Error(`Server error (${response.status}): ${response.statusText}`);
fallback.name = 'HttpError';
return fallback;
}
}

/**
* Updates the processing state with timing information from the server response
* @param timings - Timing data from the API response
* @param promptProgress - Progress data from the API response
*/
private updateProcessingState(
timings?: ChatMessageTimings,
promptProgress?: ChatMessagePromptProgress
Expand Down
Loading