char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aleksander Grygierui: ESLint config updates (#27700)f1357e499

char/aliceai-t5-moe
9.8 KiB349 linesraw
1<script lang="ts">
2	import '../app.css';
3	import { browser } from '$app/environment';
4	import { goto } from '$app/navigation';
5	import { base } from '$app/paths';
6	import { page } from '$app/state';
7	import { SidebarNavigation } from '$lib/components/app';
8	import { PwaMetaTags, PwaRefreshAlert } from '$lib/components/pwa';
9	import * as Tooltip from '$lib/components/ui/tooltip';
10	import {
11		FAVICON_PATHS,
12		FAVICON_SELECTORS,
13		HEADERS,
14		NEW_CHAT_TAB_ID,
15		ROUTES,
16		SETTINGS_KEYS,
17		TOOLTIP_DELAY_DURATION
18	} from '$lib/constants';
19	import { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte';
20	import { usePwa } from '$lib/hooks/use-pwa.svelte';
21	import { RouterService } from '$lib/services/router.service';
22	import {
23		chatStore,
24		conversationsStore,
25		deviceStore,
26		mcpStore,
27		modelsStore,
28		serverStore,
29		settingsStore,
30		tabsStore,
31		versionStore
32	} from '$lib/stores';
33	import { initStores } from '$lib/stores/init';
34	import { ModeWatcher } from 'mode-watcher';
35	import { untrack } from 'svelte';
36	import { onMount } from 'svelte';
37	import { Toaster } from 'svelte-sonner';
38	import { pwaAssetsHead } from 'virtual:pwa-assets/head';
39
40	let { children } = $props();
41
42	// migrations and store startup, ordered explicitly instead of import side effects
43	void initStores();
44
45	let innerHeight = $state<number | undefined>();
46	let innerWidth = $state(browser ? window.innerWidth : 0);
47
48	let chatSidebar:
49		| {
50				activateSearchMode?: () => void;
51				editActiveConversation?: () => void;
52		  }
53		| undefined = $state();
54
55	let showBuildVersion = $derived(
56		settingsStore.config[SETTINGS_KEYS.SHOW_BUILD_VERSION] as boolean
57	);
58
59	// Keep the hook object intact: destructuring needRefreshByStorage reads the getter once and freezes it
60	const pwa = usePwa();
61	const { needRefresh, updateServiceWorker } = pwa;
62
63	function updateFavicon() {
64		const dark = deviceStore.systemTheme.isDark;
65
66		let icoLink = document.querySelector(FAVICON_SELECTORS.ICO_48X48) as HTMLLinkElement | null;
67
68		if (icoLink) {
69			icoLink.href = dark ? FAVICON_PATHS.ICO_DARK : FAVICON_PATHS.ICO_LIGHT;
70		}
71
72		let svgLink = document.querySelector(FAVICON_SELECTORS.SVG_ANY) as HTMLLinkElement | null;
73
74		if (svgLink) {
75			svgLink.href = dark ? FAVICON_PATHS.SVG_DARK : FAVICON_PATHS.SVG_LIGHT;
76		}
77	}
78
79	function navigateToTab(direction: -1 | 1) {
80		// only makes sense with conversation tabs enabled
81		if (!settingsStore.config.conversationTabs) return;
82
83		const openTabs = tabsStore.openTabs;
84
85		if (openTabs.length === 0) return;
86
87		const activeId = page.params.id ?? NEW_CHAT_TAB_ID;
88		const idx = openTabs.indexOf(activeId);
89		// active tab not in list (e.g. a non-chat route): start from an edge
90		const targetIdx =
91			idx === -1
92				? direction === 1
93					? 0
94					: openTabs.length - 1
95				: (idx + direction + openTabs.length) % openTabs.length;
96
97		void tabsStore.activate(openTabs[targetIdx]);
98	}
99
100	function navigateToConversation(direction: -1 | 1) {
101		const allConvs = conversationsStore.conversations;
102
103		if (allConvs.length === 0) return;
104
105		const currentId = page.params.id;
106
107		if (!currentId) {
108			goto(RouterService.chat(allConvs[direction === 1 ? 0 : allConvs.length - 1].id));
109
110			return;
111		}
112
113		const idx = allConvs.findIndex((c) => c.id === currentId);
114
115		if (idx === -1) return;
116
117		const targetIdx = idx + direction;
118
119		if (targetIdx >= 0 && targetIdx < allConvs.length) {
120			goto(RouterService.chat(allConvs[targetIdx].id));
121		} else {
122			conversationsStore.openNewChat();
123		}
124	}
125
126	// navigating away from the new-chat screen drops its tab, so it does not
127	// linger once the user moves to a real conversation or another route
128	let previousChatId = $state<string | undefined>(undefined);
129
130	$effect(() => {
131		const id = page.params.id ?? (page.route.id === '/(chat)' ? NEW_CHAT_TAB_ID : undefined);
132		const prev = untrack(() => previousChatId);
133
134		previousChatId = id;
135
136		if (id !== prev && prev && settingsStore.config.conversationTabs && prev === NEW_CHAT_TAB_ID) {
137			untrack(() => tabsStore.removeTabs([NEW_CHAT_TAB_ID]));
138		}
139	});
140	// Global keyboard shortcuts
141	const { handleKeydown } = useKeyboardShortcuts({
142		editActiveConversation: () => chatSidebar?.editActiveConversation?.(),
143		navigateToNextConversation: () => navigateToConversation(1),
144		navigateToNextTab: () => navigateToTab(1),
145		navigateToPrevConversation: () => navigateToConversation(-1),
146		navigateToPrevTab: () => navigateToTab(-1)
147	});
148
149	function checkApiKey() {
150		const apiKey = settingsStore.config.apiKey;
151
152		// Without a stored key there is nothing to re-validate here; the keyless
153		// 401 case is handled by validateApiKey() at navigation time, and the
154		// reload below must never fire in a keyless loop.
155		if (!apiKey || apiKey.trim() === '') {
156			return;
157		}
158
159		untrack(() => {
160			if (
161				(page.route.id === '/(chat)' || page.route.id === '/(chat)/chat/[id]') &&
162				page.status !== 401 &&
163				page.status !== 403
164			) {
165				const headers: Record<string, string> = {
166					'Content-Type': 'application/json',
167					[HEADERS.AUTHORIZATION]: `${HEADERS.BEARER}${apiKey.trim()}`
168				};
169
170				fetch(`${base}/props`, { headers })
171					.then((response) => {
172						if (response.status === 401 || response.status === 403) {
173							window.location.reload();
174						}
175					})
176					.catch((e) => {
177						console.error('Error checking API key:', e);
178					});
179			}
180		});
181	}
182
183	onMount(() => {
184		updateFavicon();
185		// snapshot of every backend running stream on first load, populates the sidebar spinners
186		// so the user sees each conv that has a live inference, even ones not opened yet
187		void chatStore.syncRemoteRunningStreams();
188	});
189
190	// refresh that snapshot when the tab returns to the foreground, a stream may have advanced
191	// or ended while it was hidden. snapshot only, no polling
192	function handleVisibilityChange() {
193		if (document.visibilityState !== 'visible') return;
194
195		void chatStore.syncRemoteRunningStreams();
196	}
197
198	$effect(() => {
199		void deviceStore.systemTheme.isDark;
200
201		updateFavicon();
202	});
203
204	// Initialize server properties on app load (run once)
205	$effect(() => {
206		// Only fetch if we don't already have props
207		if (!serverStore.props) {
208			untrack(() => {
209				serverStore.fetch();
210			});
211		}
212	});
213
214	// Sync settings when server props are loaded
215	$effect(() => {
216		const serverProps = serverStore.props;
217
218		if (serverProps) {
219			untrack(() => {
220				settingsStore.syncWithServerDefaults();
221			});
222		}
223	});
224
225	// Inject custom CSS at runtime through an action on the head style node
226	// textContent keeps the value as text, never parsed as HTML
227	function customCss(node: HTMLStyleElement) {
228		$effect(() => {
229			node.textContent = (settingsStore.config.customCss as string | undefined) ?? '';
230		});
231	}
232
233	// Fetch router models when in router mode (for status and modalities)
234	// Wait for models to be loaded first, run only once
235	let routerModelsFetched = false;
236
237	$effect(() => {
238		const isRouter = serverStore.isRouterMode;
239		const modelsCount = modelsStore.models.length;
240
241		// Only fetch router models once when we have models loaded and in router mode
242		if (isRouter && modelsCount > 0 && !routerModelsFetched) {
243			routerModelsFetched = true;
244
245			untrack(() => {
246				modelsStore.fetchRouterModels();
247			});
248		}
249	});
250
251	// Live model status and load progress via the /models/sse feed (router mode)
252	$effect(() => {
253		if (!browser) return;
254
255		if (!serverStore.isRouterMode) return;
256
257		untrack(() => {
258			modelsStore.status.subscribe();
259		});
260
261		return () => {
262			modelsStore.status.unsubscribe();
263		};
264	});
265
266	// Background MCP server health checks on app load.
267	// Health-check every configured server with a URL - including disabled ones -
268	// so the /mcp-servers page can display health metadata for servers that are
269	// currently turned off. Disabled servers never get promoted to active
270	// connections (see runHealthCheck), so their tools/prompts/resources stay
271	// out of the chat-side stores.
272	// Only IDLE servers are checked; already-resolved (SUCCESS / ERROR) servers
273	// keep their existing state, so adding or removing a server does not flash
274	// every other card back through skeleton state.
275	$effect(() => {
276		if (!browser) return;
277
278		const mcpServers = mcpStore.getServers();
279		const serversWithUrls = mcpServers.filter((s) => s.url.trim());
280
281		if (serversWithUrls.length > 0) {
282			untrack(() => {
283				// Run health checks in background (don't await)
284				mcpStore.runHealthChecksForServers(serversWithUrls, true).catch((error) => {
285					console.warn('[layout] MCP health checks failed:', error);
286				});
287			});
288		}
289	});
290
291	// Monitor API key changes and redirect to error page if removed or changed when required
292	$effect(() => {
293		checkApiKey();
294	});
295</script>
296
297<svelte:head>
298	{#if pwaAssetsHead.themeColor}
299		<meta content={pwaAssetsHead.themeColor.content} name="theme-color" />
300	{/if}
301
302	{#if settingsStore.config.customCss}
303		<style use:customCss></style>
304	{/if}
305
306	{#each pwaAssetsHead.links as link (link.href)}
307		<link {...link} />
308	{/each}
309
310	<PwaMetaTags />
311</svelte:head>
312
313<svelte:window bind:innerHeight bind:innerWidth onkeydown={handleKeydown} />
314<svelte:document onvisibilitychange={handleVisibilityChange} />
315
316<Tooltip.Provider delayDuration={TOOLTIP_DELAY_DURATION}>
317	<div class="flex flex-col md:flex-row">
318		<SidebarNavigation
319			onSearchClick={() => {
320				if (deviceStore.isMobile) {
321					goto(ROUTES.SEARCH);
322				} else if (chatSidebar?.activateSearchMode) {
323					chatSidebar.activateSearchMode();
324				}
325			}}
326		/>
327
328		<div class="flex-1">
329			{@render children?.()}
330		</div>
331	</div>
332
333	<ModeWatcher />
334
335	<Toaster richColors />
336</Tooltip.Provider>
337
338<!-- PWA update prompt + version -->
339<div class="fixed right-4 bottom-4 z-9999 flex flex-col items-end gap-1">
340	{#if showBuildVersion && versionStore.build}
341		<span class="text-[10px] tabular-nums text-muted-foreground">{versionStore.build}</span>
342	{/if}
343
344	<PwaRefreshAlert
345		forceReload={pwa.needRefreshByStorage}
346		needRefresh={$needRefresh || pwa.needRefreshByStorage}
347		{updateServiceWorker}
348	/>
349</div>