Skip to content

Commit a935fbf

Browse files
authored
server: remove loading.html (#25500)
* server: remove loading.html * apply ui changes
1 parent 0badc06 commit a935fbf

9 files changed

Lines changed: 99 additions & 60 deletions

File tree

.github/workflows/ui-publish.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,3 @@ jobs:
7373
hf buckets rm ggml-org/${{ env.HF_BUCKET_NAME }}/index.html --yes 2>/dev/null || true
7474
hf buckets rm ggml-org/${{ env.HF_BUCKET_NAME }}/bundle.js --yes 2>/dev/null || true
7575
hf buckets rm ggml-org/${{ env.HF_BUCKET_NAME }}/bundle.css --yes 2>/dev/null || true
76-
hf buckets rm ggml-org/${{ env.HF_BUCKET_NAME }}/loading.html --yes 2>/dev/null || true

tools/server/server-http.cpp

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -175,18 +175,24 @@ bool server_http_context::init(const common_params & params) {
175175
// Middlewares
176176
//
177177

178+
// Frontend paths - all embedded UI assets
179+
static const std::unordered_set<std::string> frontend_paths = []() {
180+
std::unordered_set<std::string> paths { "/" };
181+
for (const llama_ui_asset & a : llama_ui_get_assets()) {
182+
paths.insert("/" + a.name);
183+
}
184+
return paths;
185+
}();
186+
178187
// Public endpoints - API routes plus all embedded UI assets
179188
static const std::unordered_set<std::string> get_public_endpoints = []() {
180189
std::unordered_set<std::string> endpoints {
181190
"/health",
182191
"/v1/health",
183192
"/models",
184193
"/v1/models",
185-
"/",
186194
};
187-
for (const llama_ui_asset & a : llama_ui_get_assets()) {
188-
endpoints.insert("/" + a.name);
189-
}
195+
endpoints.insert(frontend_paths.begin(), frontend_paths.end());
190196
return endpoints;
191197
}();
192198

@@ -239,18 +245,9 @@ bool server_http_context::init(const common_params & params) {
239245

240246
auto middleware_server_state = [this](const httplib::Request & req, httplib::Response & res) {
241247
if (!is_ready.load()) {
242-
#if defined(LLAMA_UI_HAS_ASSETS)
243-
if (const auto tmp = string_split<std::string>(req.path, '.');
244-
req.path == "/" || (!tmp.empty() && tmp.back() == "html")) {
245-
if (const llama_ui_asset * a = llama_ui_find_asset("loading.html")) {
246-
res.status = 503;
247-
res.set_content(reinterpret_cast<const char*>(a->data), a->size, "text/html; charset=utf-8");
248-
return false;
249-
}
248+
if (frontend_paths.count(req.path)) {
249+
return true; // frontend asset, allow it to load and show "loading"
250250
}
251-
#else
252-
(void)req;
253-
#endif
254251
// no endpoints are allowed to be accessed when the server is not ready
255252
// this is to prevent any data races or inconsistent states
256253
res.status = 503;

tools/ui/embed.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@ int main(int argc, char ** argv) {
187187
struct required_check { const char * label; match_fn match; bool found; };
188188
required_check checks[] = {
189189
{ "index.html", exact("index.html"), false },
190-
{ "loading.html", exact("loading.html"), false },
191190
{ "manifest.webmanifest", exact("manifest.webmanifest"), false },
192191
{ "sw.js", exact("sw.js"), false },
193192
{ "build.json", exact("build.json"), false },
Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,43 @@
11
<script lang="ts">
2-
import { AlertTriangle, RefreshCw } from '@lucide/svelte';
2+
import { AlertTriangle, Loader2, RefreshCw } from '@lucide/svelte';
33
import { fadeInView } from '$lib/actions/fade-in-view.svelte';
44
import * as Alert from '$lib/components/ui/alert';
5-
import { serverError, serverLoading, serverStore } from '$lib/stores/server.svelte';
5+
import { serverError, serverLoading, serverStatus, serverStore } from '$lib/stores/server.svelte';
66
77
let hasError = $derived(!!serverError());
8+
let isLoadingModel = $derived(serverStatus() === 503);
89
</script>
910

1011
{#if hasError}
1112
<div
1213
class="pointer-events-auto mx-auto mb-4 max-w-[48rem] px-1"
1314
use:fadeInView={{ y: 10, duration: 250 }}
1415
>
15-
<Alert.Root variant="destructive">
16-
<AlertTriangle class="h-4 w-4" />
16+
<Alert.Root variant={isLoadingModel ? 'default' : 'destructive'}>
17+
{#if isLoadingModel}
18+
<Loader2 class="h-4 w-4 animate-spin" />
19+
{:else}
20+
<AlertTriangle class="h-4 w-4" />
21+
{/if}
1722

1823
<Alert.Title class="flex items-center justify-between">
19-
<span>Server unavailable</span>
24+
<span>{isLoadingModel ? 'Loading model' : 'Server unavailable'}</span>
2025

21-
<button
22-
onclick={() => serverStore.fetch()}
23-
disabled={serverLoading()}
24-
class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-2 py-1 text-xs font-medium hover:bg-destructive/30 disabled:opacity-50"
25-
>
26-
<RefreshCw class="h-3 w-3 {serverLoading() ? 'animate-spin' : ''}" />
27-
{serverLoading() ? 'Retrying...' : 'Retry'}
28-
</button>
26+
{#if !isLoadingModel}
27+
<button
28+
onclick={() => serverStore.fetch()}
29+
disabled={serverLoading()}
30+
class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-2 py-1 text-xs font-medium hover:bg-destructive/30 disabled:opacity-50"
31+
>
32+
<RefreshCw class="h-3 w-3 {serverLoading() ? 'animate-spin' : ''}" />
33+
{serverLoading() ? 'Retrying...' : 'Retry'}
34+
</button>
35+
{/if}
2936
</Alert.Title>
3037

31-
<Alert.Description>{serverError()}</Alert.Description>
38+
{#if !isLoadingModel}
39+
<Alert.Description>{serverError()}</Alert.Description>
40+
{/if}
3241
</Alert.Root>
3342
</div>
3443
{/if}

tools/ui/src/lib/constants/pwa.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -258,12 +258,6 @@ export const GLOB_PATTERNS: string[] = [
258258
'**/*.{js,css,html,ico,svg,png,webp,woff,woff2,json,webmanifest}'
259259
];
260260

261-
// loading.html is the model loading page served by llama-server itself.
262-
// The SvelteKit PWA manifest transform strips the html extension from every
263-
// precache entry to match clean URLs, but loading.html is a plain static asset
264-
// with no clean URL, so static servers answer 404 and the SW install fails.
265-
export const GLOB_IGNORES: string[] = ['**/loading.html'];
266-
267261
export const SW_CONFIG = {
268262
CHECK_INTERVAL_MS: 60000,
269263
UPDATE_FETCH_OPTIONS: {
@@ -317,7 +311,6 @@ export const SVELTEKIT_PWA_OPTIONS: SvelteKitPWAOptions = {
317311
// Uses '**/' because SvelteKit outputs files under _app/immutable/
318312
// subdirectories.
319313
globPatterns: GLOB_PATTERNS,
320-
globIgnores: GLOB_IGNORES,
321314
maximumFileSizeToCacheInBytes: CACHE_SETTINGS.MAX_FILE_SIZE_BYTES,
322315

323316
// Prevent @vite-pwa/sveltekit from auto-adding a NavigationRoute by

tools/ui/src/lib/stores/server.svelte.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { PropsService } from '$lib/services/props.service';
22
import { ServerRole } from '$lib/enums';
3+
import { ApiError } from '$lib/utils/api-fetch';
4+
5+
const LOADING_RETRY_INTERVAL_MS = 1000;
36

47
/**
58
* serverStore - Server connection state, configuration, and role detection
@@ -29,8 +32,10 @@ class ServerStore {
2932
props = $state<ApiLlamaCppServerProps | null>(null);
3033
loading = $state(false);
3134
error = $state<string | null>(null);
35+
status = $state<number | null>(null);
3236
role = $state<ServerRole | null>(null);
3337
private fetchPromise: Promise<void> | null = null;
38+
private retryTimer: ReturnType<typeof setTimeout> | null = null;
3439

3540
/**
3641
*
@@ -70,23 +75,43 @@ class ServerStore {
7075
*
7176
*/
7277

73-
async fetch(): Promise<void> {
78+
/**
79+
* @param background - Set by the automatic "still loading" poll. Skips the
80+
* `loading` flag flip so the UI doesn't bounce between the full loading
81+
* splash and the chat screen every retry tick.
82+
*/
83+
async fetch({ background = false }: { background?: boolean } = {}): Promise<void> {
7484
if (this.fetchPromise) return this.fetchPromise;
7585

76-
this.loading = true;
77-
this.error = null;
86+
this.clearRetryTimer();
87+
if (!background) {
88+
this.loading = true;
89+
}
90+
// Don't clear an existing "still loading" error before a retry -
91+
// doing so would unmount/remount the error banner every second.
92+
if (this.status !== 503) {
93+
this.error = null;
94+
}
7895

7996
const fetchPromise = (async () => {
8097
try {
8198
const props = await PropsService.fetch();
8299
this.props = props;
83100
this.error = null;
101+
this.status = null;
84102
this.detectRole(props);
85103
} catch (error: unknown) {
86104
this.error = error instanceof Error ? error.message : String(error);
105+
this.status = error instanceof ApiError ? error.status : null;
87106
console.error('Error fetching server properties:', error);
107+
108+
if (this.status === 503) {
109+
this.scheduleRetry();
110+
}
88111
} finally {
89-
this.loading = false;
112+
if (!background) {
113+
this.loading = false;
114+
}
90115
this.fetchPromise = null;
91116
}
92117
})();
@@ -96,13 +121,30 @@ class ServerStore {
96121
}
97122

98123
clear(): void {
124+
this.clearRetryTimer();
99125
this.props = null;
100126
this.error = null;
127+
this.status = null;
101128
this.loading = false;
102129
this.role = null;
103130
this.fetchPromise = null;
104131
}
105132

133+
private scheduleRetry(): void {
134+
if (this.retryTimer) return;
135+
this.retryTimer = setTimeout(() => {
136+
this.retryTimer = null;
137+
this.fetch({ background: true });
138+
}, LOADING_RETRY_INTERVAL_MS);
139+
}
140+
141+
private clearRetryTimer(): void {
142+
if (this.retryTimer) {
143+
clearTimeout(this.retryTimer);
144+
this.retryTimer = null;
145+
}
146+
}
147+
106148
/**
107149
*
108150
*
@@ -125,6 +167,7 @@ export const serverStore = new ServerStore();
125167
export const serverProps = () => serverStore.props;
126168
export const serverLoading = () => serverStore.loading;
127169
export const serverError = () => serverStore.error;
170+
export const serverStatus = () => serverStore.status;
128171
export const serverRole = () => serverStore.role;
129172
export const defaultParams = () => serverStore.defaultParams;
130173
export const contextSize = () => serverStore.contextSize;

tools/ui/src/lib/utils/api-fetch.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@ import { ERROR_MESSAGES, HTTP_CODE_TO_STRING } from '$lib/constants/error';
1212
* - Base path resolution
1313
*/
1414

15+
/**
16+
* Error thrown when an API request fails, carrying the HTTP status code
17+
* so callers can distinguish e.g. a 503 "still loading" response from a
18+
* genuine failure.
19+
*/
20+
export class ApiError extends Error {
21+
status: number;
22+
23+
constructor(message: string, status: number) {
24+
super(message);
25+
this.name = 'ApiError';
26+
this.status = status;
27+
}
28+
}
29+
1530
export interface ApiFetchOptions extends Omit<RequestInit, 'headers'> {
1631
/**
1732
* Use auth-only headers (no Content-Type).
@@ -67,7 +82,7 @@ export async function apiFetch<T>(path: string, options: ApiFetchOptions = {}):
6782

6883
if (!response.ok) {
6984
const errorMessage = await parseErrorMessage(response);
70-
throw new Error(errorMessage);
85+
throw new ApiError(errorMessage, response.status);
7186
}
7287

7388
return response.json() as Promise<T>;
@@ -119,7 +134,7 @@ export async function apiFetchWithParams<T>(
119134

120135
if (!response.ok) {
121136
const errorMessage = await parseErrorMessage(response);
122-
throw new Error(errorMessage);
137+
throw new ApiError(errorMessage, response.status);
123138
}
124139

125140
return response.json() as Promise<T>;

tools/ui/static/loading.html

Lines changed: 0 additions & 12 deletions
This file was deleted.

tools/ui/tests/unit/pwa.spec.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,5 @@ describe('PWA Build Output', () => {
189189
expect(existsSync(resolve(DIST_DIR, 'pwa-192x192.png'))).toBeTruthy();
190190
expect(existsSync(resolve(DIST_DIR, 'pwa-512x512.png'))).toBeTruthy();
191191
});
192-
193-
it('has loading.html fallback page', () => {
194-
expect(existsSync(resolve(DIST_DIR, 'loading.html'))).toBeTruthy();
195-
});
196192
});
197193
});

0 commit comments

Comments
 (0)