Skip to content

Commit a5277b0

Browse files
committed
feat: Improves error handling and UI feedback
Enhances error handling in the chat service with user-friendly messages for various server errors and network issues. Implements loading and error splash screens to provide better UI feedback during server connection and error states, improving user experience. Gracefully degrades slots monitoring if the server does not support the slots endpoint, preventing application crashes.
1 parent 5ff1575 commit a5277b0

File tree

8 files changed

+294
-21
lines changed

8 files changed

+294
-21
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<script lang="ts">
2+
import { Button } from '$lib/components/ui/button';
3+
import { AlertTriangle, RefreshCw, Server } from '@lucide/svelte';
4+
import { ServerStatus } from '$lib/components/app';
5+
import { serverStore, serverLoading } from '$lib/stores/server.svelte';
6+
import { fade, fly } from 'svelte/transition';
7+
8+
interface Props {
9+
/**
10+
* The error message to display
11+
*/
12+
error: string;
13+
/**
14+
* Whether to show the retry button
15+
*/
16+
showRetry?: boolean;
17+
/**
18+
* Whether to show troubleshooting information
19+
*/
20+
showTroubleshooting?: boolean;
21+
/**
22+
* Custom retry handler - if not provided, will use default server retry
23+
*/
24+
onRetry?: () => void;
25+
/**
26+
* Additional CSS classes
27+
*/
28+
class?: string;
29+
}
30+
31+
let {
32+
error,
33+
showRetry = true,
34+
showTroubleshooting = true,
35+
onRetry,
36+
class: className = ''
37+
}: Props = $props();
38+
39+
const isServerLoading = $derived(serverLoading());
40+
41+
function handleRetryConnection() {
42+
if (onRetry) {
43+
onRetry();
44+
} else {
45+
serverStore.fetchServerProps();
46+
}
47+
}
48+
</script>
49+
50+
<div class="flex h-full items-center justify-center {className}">
51+
<div class="w-full max-w-md px-4 text-center">
52+
<div class="mb-6" in:fade={{ duration: 300 }}>
53+
<div class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-destructive/10">
54+
<AlertTriangle class="h-8 w-8 text-destructive" />
55+
</div>
56+
<h2 class="mb-2 text-xl font-semibold">Server Connection Error</h2>
57+
<p class="text-muted-foreground mb-4 text-sm">
58+
{error}
59+
</p>
60+
<div class="mb-4">
61+
<ServerStatus showActions={true} class="justify-center" />
62+
</div>
63+
</div>
64+
65+
{#if showRetry}
66+
<div in:fly={{ y: 10, duration: 300, delay: 200 }}>
67+
<Button
68+
onclick={handleRetryConnection}
69+
disabled={isServerLoading}
70+
class="w-full"
71+
>
72+
{#if isServerLoading}
73+
<RefreshCw class="mr-2 h-4 w-4 animate-spin" />
74+
Connecting...
75+
{:else}
76+
<RefreshCw class="mr-2 h-4 w-4" />
77+
Retry Connection
78+
{/if}
79+
</Button>
80+
</div>
81+
{/if}
82+
83+
{#if showTroubleshooting}
84+
<div class="mt-4 text-left" in:fly={{ y: 10, duration: 300, delay: 400 }}>
85+
<details class="text-sm">
86+
<summary class="cursor-pointer text-muted-foreground hover:text-foreground">
87+
Troubleshooting
88+
</summary>
89+
<div class="mt-2 space-y-3 text-muted-foreground text-xs">
90+
<div class="space-y-2">
91+
<p class="font-medium mb-4">Start the llama-server:</p>
92+
93+
<div class="bg-muted/50 rounded px-2 py-1 font-mono text-xs">
94+
<p>llama-server -hf ggml-org/gemma-3-4b-it-GGUF</p>
95+
</div>
96+
97+
<p>or</p>
98+
99+
<div class="bg-muted/50 rounded px-2 py-1 font-mono text-xs">
100+
<p class="mt-1">llama-server -m locally-stored-model.gguf</p>
101+
</div>
102+
</div>
103+
<ul class="space-y-1 list-disc pl-4">
104+
<li>Check that the server is accessible at the correct URL</li>
105+
<li>Verify your network connection</li>
106+
<li>Check server logs for any error messages</li>
107+
</ul>
108+
</div>
109+
</details>
110+
</div>
111+
{/if}
112+
</div>
113+
</div>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script lang="ts">
2+
import { Server } from '@lucide/svelte';
3+
import { ServerStatus } from '$lib/components/app';
4+
import { fade } from 'svelte/transition';
5+
6+
interface Props {
7+
/**
8+
* Custom loading message
9+
*/
10+
message?: string;
11+
/**
12+
* Additional CSS classes
13+
*/
14+
class?: string;
15+
}
16+
17+
let {
18+
message = 'Initializing connection to llama.cpp server...',
19+
class: className = ''
20+
}: Props = $props();
21+
</script>
22+
23+
<div class="flex h-full items-center justify-center {className}">
24+
<div class="text-center">
25+
<div class="mb-4" in:fade={{ duration: 300 }}>
26+
<div class="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-muted">
27+
<Server class="h-8 w-8 text-muted-foreground animate-pulse" />
28+
</div>
29+
<h2 class="mb-2 text-xl font-semibold">Connecting to Server</h2>
30+
<p class="text-muted-foreground text-sm">
31+
{message}
32+
</p>
33+
</div>
34+
<div class="mt-4">
35+
<ServerStatus class="justify-center" />
36+
</div>
37+
</div>
38+
</div>

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
filterFilesByModalities,
88
generateModalityErrorMessage
99
} from '$lib/utils/modality-file-validation';
10-
import { supportsVision, supportsAudio } from '$lib/stores/server.svelte';
11-
import { ChatForm, ChatScreenHeader, ChatMessages, ServerInfo } from '$lib/components/app';
10+
import { supportsVision, supportsAudio, serverError, serverLoading } from '$lib/stores/server.svelte';
11+
import { ChatForm, ChatScreenHeader, ChatMessages, ServerInfo, ServerErrorSplash, ServerLoadingSplash } from '$lib/components/app';
1212
import {
1313
activeMessages,
1414
activeConversation,
@@ -24,6 +24,7 @@
2424
import ChatScreenDragOverlay from './ChatScreenDragOverlay.svelte';
2525
import * as AlertDialog from '$lib/components/ui/alert-dialog';
2626
27+
2728
let { showCenteredEmpty = false } = $props();
2829
let chatScrollContainer: HTMLDivElement | undefined = $state();
2930
let scrollInterval: ReturnType<typeof setInterval> | undefined;
@@ -50,6 +51,9 @@
5051
showCenteredEmpty && !activeConversation() && activeMessages().length === 0 && !isLoading()
5152
);
5253
54+
const hasServerError = $derived(serverError());
55+
const isServerLoading = $derived(serverLoading());
56+
5357
function handleDragEnter(event: DragEvent) {
5458
event.preventDefault();
5559
dragCounter++;
@@ -233,6 +237,12 @@
233237
</div>
234238
</div>
235239
</div>
240+
{:else if hasServerError}
241+
<!-- Server Error State -->
242+
<ServerErrorSplash error={hasServerError} />
243+
{:else if isServerLoading}
244+
<!-- Server Loading State -->
245+
<ServerLoadingSplash />
236246
{:else if serverStore.modelName}
237247
<div
238248
aria-label="Welcome screen with file drop zone"

tools/server/webui/src/lib/components/app/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,6 @@ export { default as ChatSidebarSearch } from './chat/ChatSidebar/ChatSidebarSear
2828
export { default as MarkdownContent } from './MarkdownContent.svelte';
2929

3030
export { default as ServerStatus } from './ServerStatus.svelte';
31+
export { default as ServerErrorSplash } from './ServerErrorSplash.svelte';
32+
export { default as ServerLoadingSplash } from './ServerLoadingSplash.svelte';
3133
export { default as ServerInfo } from './ServerInfo.svelte';

tools/server/webui/src/lib/hooks/use-processing-state.svelte.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export function useProcessingState() {
66
let isPolling = $state(false);
77
let unsubscribe: (() => void) | null = null;
88

9-
function startMonitoring(): void {
9+
async function startMonitoring(): Promise<void> {
1010
if (isPolling) return;
1111

1212
isPolling = true;
@@ -15,7 +15,12 @@ export function useProcessingState() {
1515
processingState = state;
1616
});
1717

18-
slotsService.startPolling();
18+
try {
19+
await slotsService.startPolling();
20+
} catch (error) {
21+
console.warn('Failed to start slots polling:', error);
22+
// Continue without slots monitoring - graceful degradation
23+
}
1924
}
2025

2126
function stopMonitoring(): void {

tools/server/webui/src/lib/services/chat.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,29 @@ export class ChatService {
9898
});
9999

100100
if (!response.ok) {
101-
throw new Error(`HTTP error! status: ${response.status}`);
101+
let errorMessage = `Server error (${response.status})`;
102+
103+
switch (response.status) {
104+
case 400:
105+
errorMessage = 'Invalid request - check your message format';
106+
break;
107+
case 401:
108+
errorMessage = 'Unauthorized - check server authentication';
109+
break;
110+
case 404:
111+
errorMessage = 'Chat endpoint not found - server may not support chat completions';
112+
break;
113+
case 500:
114+
errorMessage = 'Server internal error - check server logs';
115+
break;
116+
case 503:
117+
errorMessage = 'Server unavailable - try again later';
118+
break;
119+
default:
120+
errorMessage = `Server error (${response.status}): ${response.statusText}`;
121+
}
122+
123+
throw new Error(errorMessage);
102124
}
103125

104126
if (stream) {
@@ -108,14 +130,31 @@ export class ChatService {
108130
}
109131
} catch (error) {
110132
if (error instanceof Error && error.name === 'AbortError') {
133+
console.log('Chat completion request was aborted');
111134
return;
112135
}
113136

114-
const err = error instanceof Error ? error : new Error('Unknown error');
115-
116-
onError?.(err);
137+
// Handle network errors with user-friendly messages
138+
let friendlyError: Error;
139+
if (error instanceof Error) {
140+
if (error.name === 'TypeError' && error.message.includes('fetch')) {
141+
friendlyError = new Error('Unable to connect to server - please check if the server is running');
142+
} else if (error.message.includes('ECONNREFUSED')) {
143+
friendlyError = new Error('Connection refused - server may be offline');
144+
} else if (error.message.includes('ETIMEDOUT')) {
145+
friendlyError = new Error('Request timeout - server may be overloaded');
146+
} else {
147+
friendlyError = error;
148+
}
149+
} else {
150+
friendlyError = new Error('Unknown error occurred while sending message');
151+
}
117152

118-
throw err;
153+
console.error('Error in sendMessage:', error);
154+
if (onError) {
155+
onError(friendlyError);
156+
}
157+
throw friendlyError;
119158
}
120159
}
121160

0 commit comments

Comments
 (0)