diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz index f12ff3e62e4aa..53c6a9b5cfb52 100644 Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ diff --git a/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte b/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte index 17215b713a5cb..666febf0d28d6 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte @@ -3,9 +3,11 @@ import { ChatForm, ChatScreenHeader, + ChatScreenWarning, ChatMessages, ChatProcessingInfo, EmptyFileAlertDialog, + ServerErrorSplash, ServerInfo, ServerLoadingSplash, ConfirmationDialog @@ -29,6 +31,7 @@ supportsVision, supportsAudio, serverLoading, + serverWarning, serverStore } from '$lib/stores/server.svelte'; import { contextService } from '$lib/services'; @@ -303,6 +306,10 @@ > + {#if serverWarning()} + + {/if} +
+{:else if serverStore.error && !serverStore.modelName} + {:else if serverStore.modelName}
+ {#if serverWarning()} + + {/if} +
+ import { AlertTriangle, RefreshCw } from '@lucide/svelte'; + import { serverLoading, serverStore } from '$lib/stores/server.svelte'; + import { fly } from 'svelte/transition'; + + interface Props { + class?: string; + } + + let { class: className = '' }: Props = $props(); + + function handleRefreshServer() { + serverStore.fetchServerProps(); + } + + +
+
+
+
+ +

+ Server `/props` endpoint not available - using cached data +

+
+ +
+
+
diff --git a/tools/server/webui/src/lib/components/app/index.ts b/tools/server/webui/src/lib/components/app/index.ts index 2f559bd623a51..7c1af27ecd3fd 100644 --- a/tools/server/webui/src/lib/components/app/index.ts +++ b/tools/server/webui/src/lib/components/app/index.ts @@ -19,6 +19,7 @@ export { default as MessageBranchingControls } from './chat/ChatMessages/ChatMes export { default as ChatProcessingInfo } from './chat/ChatProcessingInfo.svelte'; export { default as ChatScreenHeader } from './chat/ChatScreen/ChatScreenHeader.svelte'; +export { default as ChatScreenWarning } from './chat/ChatScreen/ChatScreenWarning.svelte'; export { default as ChatScreen } from './chat/ChatScreen/ChatScreen.svelte'; export { default as ChatSettingsDialog } from './chat/ChatSettings/ChatSettingsDialog.svelte'; diff --git a/tools/server/webui/src/lib/constants/localstorage-keys.ts b/tools/server/webui/src/lib/constants/localstorage-keys.ts new file mode 100644 index 0000000000000..9fcc7bab93d1d --- /dev/null +++ b/tools/server/webui/src/lib/constants/localstorage-keys.ts @@ -0,0 +1 @@ +export const SERVER_PROPS_LOCALSTORAGE_KEY = 'LlamaCppWebui.serverProps'; diff --git a/tools/server/webui/src/lib/stores/server.svelte.ts b/tools/server/webui/src/lib/stores/server.svelte.ts index 7abaa2bdf512b..8007102222cb9 100644 --- a/tools/server/webui/src/lib/stores/server.svelte.ts +++ b/tools/server/webui/src/lib/stores/server.svelte.ts @@ -1,3 +1,5 @@ +import { browser } from '$app/environment'; +import { SERVER_PROPS_LOCALSTORAGE_KEY } from '$lib/constants/localstorage-keys'; import { ChatService } from '$lib/services/chat'; import { config } from '$lib/stores/settings.svelte'; @@ -34,12 +36,51 @@ import { config } from '$lib/stores/settings.svelte'; * - Slots endpoint availability (for processing state monitoring) * - Context window size and token limits */ + class ServerStore { + constructor() { + if (!browser) return; + + const cachedProps = this.readCachedServerProps(); + if (cachedProps) { + this._serverProps = cachedProps; + } + } + private _serverProps = $state(null); private _loading = $state(false); private _error = $state(null); + private _serverWarning = $state(null); private _slotsEndpointAvailable = $state(null); + private readCachedServerProps(): ApiLlamaCppServerProps | null { + if (!browser) return null; + + try { + const raw = localStorage.getItem(SERVER_PROPS_LOCALSTORAGE_KEY); + if (!raw) return null; + + return JSON.parse(raw) as ApiLlamaCppServerProps; + } catch (error) { + console.warn('Failed to read cached server props from localStorage:', error); + return null; + } + } + + private persistServerProps(props: ApiLlamaCppServerProps | null): void { + if (!browser) return; + + try { + if (props) { + localStorage.setItem(SERVER_PROPS_LOCALSTORAGE_KEY, JSON.stringify(props)); + } else { + localStorage.removeItem(SERVER_PROPS_LOCALSTORAGE_KEY); + } + } catch (error) { + console.warn('Failed to persist server props to localStorage:', error); + } + } + get serverProps(): ApiLlamaCppServerProps | null { return this._serverProps; } @@ -52,6 +93,10 @@ class ServerStore { return this._error; } + get serverWarning(): string | null { + return this._serverWarning; + } + get modelName(): string | null { if (!this._serverProps?.model_path) return null; return this._serverProps.model_path.split(/(\\|\/)/).pop() || null; @@ -123,30 +168,43 @@ class ServerStore { async fetchServerProps(): Promise { this._loading = true; this._error = null; + this._serverWarning = null; try { console.log('Fetching server properties...'); const props = await ChatService.getServerProps(); this._serverProps = props; + this.persistServerProps(props); console.log('Server properties loaded:', props); // Check slots endpoint availability after server props are loaded await this.checkSlotsEndpointAvailability(); } catch (error) { + const hadCachedProps = this._serverProps !== null; let errorMessage = 'Failed to connect to server'; + let isOfflineLikeError = false; + let isServerSideError = false; if (error instanceof Error) { // Handle specific error types with user-friendly messages if (error.name === 'TypeError' && error.message.includes('fetch')) { errorMessage = 'Server is not running or unreachable'; + isOfflineLikeError = true; } else if (error.message.includes('ECONNREFUSED')) { errorMessage = 'Connection refused - server may be offline'; + isOfflineLikeError = true; } else if (error.message.includes('ENOTFOUND')) { errorMessage = 'Server not found - check server address'; + isOfflineLikeError = true; } else if (error.message.includes('ETIMEDOUT')) { errorMessage = 'Connection timeout - server may be overloaded'; + isOfflineLikeError = true; + } else if (error.message.includes('503')) { + errorMessage = 'Server temporarily unavailable - try again shortly'; + isServerSideError = true; } else if (error.message.includes('500')) { errorMessage = 'Server error - check server logs'; + isServerSideError = true; } else if (error.message.includes('404')) { errorMessage = 'Server endpoint not found'; } else if (error.message.includes('403') || error.message.includes('401')) { @@ -154,7 +212,37 @@ class ServerStore { } } - this._error = errorMessage; + let cachedProps: ApiLlamaCppServerProps | null = null; + + if (!hadCachedProps) { + cachedProps = this.readCachedServerProps(); + if (cachedProps) { + this._serverProps = cachedProps; + this._error = null; + + if (isOfflineLikeError || isServerSideError) { + this._serverWarning = errorMessage; + } + + console.warn( + 'Failed to refresh server properties, using cached values from localStorage:', + errorMessage + ); + } else { + this._error = errorMessage; + } + } else { + this._error = null; + + if (isOfflineLikeError || isServerSideError) { + this._serverWarning = errorMessage; + } + + console.warn( + 'Failed to refresh server properties, continuing with cached values:', + errorMessage + ); + } console.error('Error fetching server properties:', error); } finally { this._loading = false; @@ -167,8 +255,10 @@ class ServerStore { clear(): void { this._serverProps = null; this._error = null; + this._serverWarning = null; this._loading = false; this._slotsEndpointAvailable = null; + this.persistServerProps(null); } } @@ -177,6 +267,7 @@ export const serverStore = new ServerStore(); export const serverProps = () => serverStore.serverProps; export const serverLoading = () => serverStore.loading; export const serverError = () => serverStore.error; +export const serverWarning = () => serverStore.serverWarning; export const modelName = () => serverStore.modelName; export const supportedModalities = () => serverStore.supportedModalities; export const supportsVision = () => serverStore.supportsVision; diff --git a/tools/server/webui/src/lib/utils/api-key-validation.ts b/tools/server/webui/src/lib/utils/api-key-validation.ts index d7bbf9c2ebdc9..b8be21568f786 100644 --- a/tools/server/webui/src/lib/utils/api-key-validation.ts +++ b/tools/server/webui/src/lib/utils/api-key-validation.ts @@ -27,11 +27,10 @@ export async function validateApiKey(fetch: typeof globalThis.fetch): Promise= 500) { - throw error(response.status, 'Server error - check if llama.cpp server is running'); - } else { - throw error(response.status, `Server responded with status ${response.status}`); } + + console.warn(`Server responded with status ${response.status} during API key validation`); + return; } } catch (err) { // If it's already a SvelteKit error, re-throw it @@ -40,6 +39,6 @@ export async function validateApiKey(fetch: typeof globalThis.fetch): Promise