char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
8144f3192
char/aliceai-t5-moe
1/** 2* tabsStore - Reactive State Store for Browser-Style Conversation Tabs 3* 4* Tracks which conversations and the new-chat screen are open as tabs in 5* the chat layout, in order. Real conversation tabs are `#/chat/<id>` 6* routes; the new-chat tab is the bare `#/` route, represented here by the 7* `NEW_CHAT_TAB_ID` sentinel (see {@link NEW_CHAT_TAB_ID}). 8* 9* **Architecture & Relationships:** 10* - **conversationsStore**: owns conversation data; calls `removeTabs()` / 11* `close()` when conversations are deleted. This store never imports it, 12* so there is no circular dependency - tab names are resolved by the 13* ChatTabs component from conversationsStore. 14* - Tab order persists to localStorage and is pruned against the loaded 15* conversation list on init. The new-chat tab is kept across reloads. 16*/ 17 18import { browser } from '$app/environment' ; 19import { goto } from '$app/navigation' ; 20import { CONVERSATION_TABS_LOCALSTORAGE_KEY , NEW_CHAT_TAB_ID , ROUTES } from '$lib/constants' ; 21import { RouterService } from '$lib/services/router.service' ; 22import { untrack } from 'svelte' ; 23 24class TabsStore { 25/** Ordered tab ids: conversation ids and the `NEW_CHAT_TAB_ID` sentinel */ 26openTabs = $state < string []>([]); 27 28/** False until init() has read the persisted tabs; save() is a no-op before that */ 29private initialized = false ; 30 31/** Navigate to a tab (the new-chat sentinel maps to the bare `#/` route) */ 32async activate ( id :string ) :Promise < void > { 33await goto ( id === NEW_CHAT_TAB_ID ?ROUTES . START :RouterService . chat ( id )); 34} 35 36/** Remove all tabs (e.g. after deleting all conversations) */ 37clear () :void { 38this . openTabs = []; 39this . save (); 40} 41 42/** 43* Close a tab. When it belongs to the active route, navigate to the left 44* neighbor (or the right one when the closed tab was leftmost), falling 45* back to the new-chat screen when no tabs remain. 46* @param id - Tab id to close 47* @param activeTabId - Tab id of the current route, if any 48*/ 49async close ( id :string , activeTabId :string | null ) :Promise < void > { 50const idx = this . openTabs . indexOf ( id ); 51 52if ( idx === - 1 ) { 53// tab not tracked (e.g. Conversation tabs are off); still fall back to 54// the new-chat screen when closing the active conversation 55if ( id === activeTabId ) { 56await goto ( ROUTES . START ); 57} 58 59return ; 60} 61 62this . openTabs = this . openTabs . filter (( tabId ) => tabId !== id ); 63this . save (); 64 65if ( id !== activeTabId ) return ; 66 67const target = ( idx > 0 ?this . openTabs [ idx - 1 ] :this . openTabs [ 0 ]) ?? null ; 68 69if ( target ) { 70await goto ( target === NEW_CHAT_TAB_ID ?ROUTES . START :RouterService . chat ( target )); 71} else { 72await goto ( ROUTES . START ); 73} 74} 75 76/** 77* Load persisted tabs, dropping conversation ids that no longer exist. 78* Called once from initStores() after conversations are loaded. 79* Merges with (rather than replaces) current openTabs: the chat layout 80* syncs the route's tab before this async init completes, and replacing 81* here would drop it. 82* @param validIds - Ids of conversations present in the database 83*/ 84init ( validIds :string []) :void { 85if ( ! browser ) return ; 86 87// the new-chat sentinel is a pseudo-tab, not a conversation, but it is 88// still kept so a reload on `#/` does not drop the tab the user is on 89const isLive = ( id :string ) => validIds . includes ( id ) || id === NEW_CHAT_TAB_ID ; 90const persisted = this . load (). filter ( isLive ); 91// tabs already in openTabs come from the live route, so they stay as they 92// are: `validIds` is a snapshot and a conversation created while the list 93// was loading is not in it 94const extras = this . openTabs . filter (( id ) => ! persisted . includes ( id )); 95 96this . openTabs = [ ...persisted , ...extras ]; 97this . initialized = true ; 98this . save (); 99} 100 101/** 102* Remove tabs without navigating. Used when conversations are deleted 103* while some other conversation stays open. 104* @param ids - Tab ids to drop 105*/ 106removeTabs ( ids :string []) :void { 107const removed = new Set ( ids ); 108const next = this . openTabs . filter (( id ) => ! removed . has ( id )); 109 110if ( next . length !== this . openTabs . length ) { 111this . openTabs = next ; 112this . save (); 113} 114} 115 116/** 117* Sync the tab strip with the route. Called from the chat layout on every 118* navigation, so any way of reaching a conversation or new-chat tab opens 119* a tab for it. 120* @param id - The conversation (or temporary new-chat) id of the route 121*/ 122syncWithRoute ( id :string ) :void { 123// untrack: callers invoke this from an effect keyed on the route, and 124// reading openTabs here would subscribe that effect to openTabs too - 125// closing the active tab would then re-run the effect and re-add the tab 126untrack (() => { 127if ( ! this . openTabs . includes ( id )) { 128this . openTabs = [ ...this . openTabs , id ]; 129this . save (); 130} 131}); 132} 133 134private load () :string [] { 135try { 136const raw = localStorage . getItem ( CONVERSATION_TABS_LOCALSTORAGE_KEY ); 137const parsed :unknown = raw ?JSON . parse ( raw ) :[]; 138 139return Array . isArray ( parsed ) ?parsed . filter (( id ) => typeof id === 'string' ) :[]; 140} catch { 141return []; 142} 143} 144 145private save () :void { 146// never write before init has read the persisted tabs, or an early 147// route sync (layout effect runs before async init) would clobber them 148if ( ! browser || ! this . initialized ) return ; 149 150localStorage . setItem ( CONVERSATION_TABS_LOCALSTORAGE_KEY , JSON . stringify ( this . openTabs )); 151} 152} 153 154export const tabsStore = new TabsStore ();