char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
f1357e499
char/aliceai-t5-moe
1< script lang ="ts "> 2import '../app.css' ; 3import { browser } from '$app/environment' ; 4import { goto } from '$app/navigation' ; 5import { base } from '$app/paths' ; 6import { page } from '$app/state' ; 7import { SidebarNavigation } from '$lib/components/app' ; 8import { PwaMetaTags , PwaRefreshAlert } from '$lib/components/pwa' ; 9import * as Tooltip from '$lib/components/ui/tooltip' ; 10import { 11FAVICON_PATHS , 12FAVICON_SELECTORS , 13HEADERS , 14NEW_CHAT_TAB_ID , 15ROUTES , 16SETTINGS_KEYS , 17TOOLTIP_DELAY_DURATION 18} from '$lib/constants' ; 19import { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte' ; 20import { usePwa } from '$lib/hooks/use-pwa.svelte' ; 21import { RouterService } from '$lib/services/router.service' ; 22import { 23chatStore , 24conversationsStore , 25deviceStore , 26mcpStore , 27modelsStore , 28serverStore , 29settingsStore , 30tabsStore , 31versionStore 32} from '$lib/stores' ; 33import { initStores } from '$lib/stores/init' ; 34import { ModeWatcher } from 'mode-watcher' ; 35import { untrack } from 'svelte' ; 36import { onMount } from 'svelte' ; 37import { Toaster } from 'svelte-sonner' ; 38import { pwaAssetsHead } from 'virtual:pwa-assets/head' ; 39 40let { children} = $props (); 41 42// migrations and store startup, ordered explicitly instead of import side effects 43void initStores (); 44 45let innerHeight = $state < number | undefined >(); 46let innerWidth = $state ( browser ?window . innerWidth :0 ); 47 48let chatSidebar : 49| { 50activateSearchMode ?:() => void ; 51editActiveConversation ?:() => void ; 52} 53| undefined = $state (); 54 55let showBuildVersion = $derived ( 56settingsStore . 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 60const pwa = usePwa (); 61const { needRefresh, updateServiceWorker} = pwa ; 62 63function updateFavicon () { 64const dark = deviceStore . systemTheme . isDark ; 65 66let icoLink = document . querySelector ( FAVICON_SELECTORS . ICO_48X48 ) as HTMLLinkElement | null ; 67 68if ( icoLink ) { 69icoLink . href = dark ?FAVICON_PATHS . ICO_DARK :FAVICON_PATHS . ICO_LIGHT ; 70} 71 72let svgLink = document . querySelector ( FAVICON_SELECTORS . SVG_ANY ) as HTMLLinkElement | null ; 73 74if ( svgLink ) { 75svgLink . href = dark ?FAVICON_PATHS . SVG_DARK :FAVICON_PATHS . SVG_LIGHT ; 76} 77} 78 79function navigateToTab ( direction :- 1 | 1 ) { 80// only makes sense with conversation tabs enabled 81if ( ! settingsStore . config . conversationTabs ) return ; 82 83const openTabs = tabsStore . openTabs ; 84 85if ( openTabs . length === 0 ) return ; 86 87const activeId = page . params . id ?? NEW_CHAT_TAB_ID ; 88const idx = openTabs . indexOf ( activeId ); 89// active tab not in list (e.g. a non-chat route): start from an edge 90const targetIdx = 91idx === - 1 92 ?direction === 1 93 ?0 94 :openTabs . length - 1 95 :( idx + direction + openTabs . length ) % openTabs . length ; 96 97void tabsStore . activate ( openTabs [ targetIdx ]); 98} 99 100function navigateToConversation ( direction :- 1 | 1 ) { 101const allConvs = conversationsStore . conversations ; 102 103if ( allConvs . length === 0 ) return ; 104 105const currentId = page . params . id ; 106 107if ( ! currentId ) { 108goto ( RouterService . chat ( allConvs [ direction === 1 ?0 :allConvs . length - 1 ]. id )); 109 110return ; 111} 112 113const idx = allConvs . findIndex (( c ) => c . id === currentId ); 114 115if ( idx === - 1 ) return ; 116 117const targetIdx = idx + direction ; 118 119if ( targetIdx >= 0 && targetIdx < allConvs . length ) { 120goto ( RouterService . chat ( allConvs [ targetIdx ]. id )); 121} else { 122conversationsStore . 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 128let previousChatId = $state < string | undefined >( undefined ); 129 130$effect (() => { 131const id = page . params . id ?? ( page . route . id === '/(chat)' ?NEW_CHAT_TAB_ID :undefined ); 132const prev = untrack (() => previousChatId ); 133 134previousChatId = id ; 135 136if ( id !== prev && prev && settingsStore . config . conversationTabs && prev === NEW_CHAT_TAB_ID ) { 137untrack (() => tabsStore . removeTabs ([ NEW_CHAT_TAB_ID ])); 138} 139}); 140// Global keyboard shortcuts 141const { handleKeydown} = useKeyboardShortcuts ({ 142editActiveConversation :() => chatSidebar ?. editActiveConversation ?.(), 143navigateToNextConversation :() => navigateToConversation ( 1 ), 144navigateToNextTab :() => navigateToTab ( 1 ), 145navigateToPrevConversation :() => navigateToConversation ( - 1 ), 146navigateToPrevTab :() => navigateToTab ( - 1 ) 147}); 148 149function checkApiKey () { 150const 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. 155if ( ! apiKey || apiKey . trim () === '' ) { 156return ; 157} 158 159untrack (() => { 160if ( 161( page . route . id === '/(chat)' || page . route . id === '/(chat)/chat/[id]' ) && 162page . status !== 401 && 163page . status !== 403 164) { 165const headers :Record < string , string > = { 166'Content-Type' :'application/json' , 167[ HEADERS . AUTHORIZATION ] :` ${ HEADERS . BEARER } ${ apiKey . trim ()} ` 168}; 169 170fetch ( ` ${ base } /props` , { headers}) 171. then (( response ) => { 172if ( response . status === 401 || response . status === 403 ) { 173window . location . reload (); 174} 175}) 176. catch (( e ) => { 177console . error ( 'Error checking API key:' , e ); 178}); 179} 180}); 181} 182 183onMount (() => { 184updateFavicon (); 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 187void 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 192function handleVisibilityChange () { 193if ( document . visibilityState !== 'visible' ) return ; 194 195void chatStore . syncRemoteRunningStreams (); 196} 197 198$effect (() => { 199void deviceStore . systemTheme . isDark ; 200 201updateFavicon (); 202}); 203 204// Initialize server properties on app load (run once) 205$effect (() => { 206// Only fetch if we don't already have props 207if ( ! serverStore . props ) { 208untrack (() => { 209serverStore . fetch (); 210}); 211} 212}); 213 214// Sync settings when server props are loaded 215$effect (() => { 216const serverProps = serverStore . props ; 217 218if ( serverProps ) { 219untrack (() => { 220settingsStore . 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 227function customCss ( node :HTMLStyleElement ) { 228$effect (() => { 229node . 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 235let routerModelsFetched = false ; 236 237$effect (() => { 238const isRouter = serverStore . isRouterMode ; 239const modelsCount = modelsStore . models . length ; 240 241// Only fetch router models once when we have models loaded and in router mode 242if ( isRouter && modelsCount > 0 && ! routerModelsFetched ) { 243routerModelsFetched = true ; 244 245untrack (() => { 246modelsStore . fetchRouterModels (); 247}); 248} 249}); 250 251// Live model status and load progress via the /models/sse feed (router mode) 252$effect (() => { 253if ( ! browser ) return ; 254 255if ( ! serverStore . isRouterMode ) return ; 256 257untrack (() => { 258modelsStore . status . subscribe (); 259}); 260 261return () => { 262modelsStore . 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 (() => { 276if ( ! browser ) return ; 277 278const mcpServers = mcpStore . getServers (); 279const serversWithUrls = mcpServers . filter (( s ) => s . url . trim ()); 280 281if ( serversWithUrls . length > 0 ) { 282untrack (() => { 283// Run health checks in background (don't await) 284mcpStore . runHealthChecksForServers ( serversWithUrls , true ). catch (( error ) => { 285console . 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 (() => { 293checkApiKey (); 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 319onSearchClick ={() => { 320if ( deviceStore . isMobile ) { 321goto ( ROUTES . SEARCH ); 322} else if ( chatSidebar ?. activateSearchMode ) { 323chatSidebar . 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 345forceReload ={ pwa . needRefreshByStorage } 346needRefresh ={ $needRefresh || pwa . needRefreshByStorage } 347{ updateServiceWorker } 348/> 349</ div >