1<script lang="ts">
  2	import '../app.css';
  3	import { base } from '$app/paths';
  4	import { page } from '$app/state';
  5	import { untrack } from 'svelte';
  6	import { ChatSidebar, DialogConversationTitleUpdate } from '$lib/components/app';
  7	import { isLoading } from '$lib/stores/chat.svelte';
  8	import { conversationsStore, activeMessages } from '$lib/stores/conversations.svelte';
  9	import * as Sidebar from '$lib/components/ui/sidebar/index.js';
 10	import * as Tooltip from '$lib/components/ui/tooltip';
 11	import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
 12	import { config, settingsStore } from '$lib/stores/settings.svelte';
 13	import { ModeWatcher } from 'mode-watcher';
 14	import { Toaster } from 'svelte-sonner';
 15	import { goto } from '$app/navigation';
 16	import { modelsStore } from '$lib/stores/models.svelte';
 17	import { TOOLTIP_DELAY_DURATION } from '$lib/constants/tooltip-config';
 18	import { IsMobile } from '$lib/hooks/is-mobile.svelte';
 19
 20	let { children } = $props();
 21
 22	let isChatRoute = $derived(page.route.id === '/chat/[id]');
 23	let isHomeRoute = $derived(page.route.id === '/');
 24	let isNewChatMode = $derived(page.url.searchParams.get('new_chat') === 'true');
 25	let showSidebarByDefault = $derived(activeMessages().length > 0 || isLoading());
 26	let alwaysShowSidebarOnDesktop = $derived(config().alwaysShowSidebarOnDesktop);
 27	let autoShowSidebarOnNewChat = $derived(config().autoShowSidebarOnNewChat);
 28	let isMobile = new IsMobile();
 29	let isDesktop = $derived(!isMobile.current);
 30	let sidebarOpen = $state(false);
 31	let innerHeight = $state<number | undefined>();
 32	let chatSidebar:
 33		| { activateSearchMode?: () => void; editActiveConversation?: () => void }
 34		| undefined = $state();
 35
 36	// Conversation title update dialog state
 37	let titleUpdateDialogOpen = $state(false);
 38	let titleUpdateCurrentTitle = $state('');
 39	let titleUpdateNewTitle = $state('');
 40	let titleUpdateResolve: ((value: boolean) => void) | null = null;
 41
 42	// Global keyboard shortcuts
 43	function handleKeydown(event: KeyboardEvent) {
 44		const isCtrlOrCmd = event.ctrlKey || event.metaKey;
 45
 46		if (isCtrlOrCmd && event.key === 'k') {
 47			event.preventDefault();
 48			if (chatSidebar?.activateSearchMode) {
 49				chatSidebar.activateSearchMode();
 50				sidebarOpen = true;
 51			}
 52		}
 53
 54		if (isCtrlOrCmd && event.shiftKey && event.key === 'O') {
 55			event.preventDefault();
 56			goto('?new_chat=true#/');
 57		}
 58
 59		if (event.shiftKey && isCtrlOrCmd && event.key === 'E') {
 60			event.preventDefault();
 61
 62			if (chatSidebar?.editActiveConversation) {
 63				chatSidebar.editActiveConversation();
 64			}
 65		}
 66	}
 67
 68	function handleTitleUpdateCancel() {
 69		titleUpdateDialogOpen = false;
 70		if (titleUpdateResolve) {
 71			titleUpdateResolve(false);
 72			titleUpdateResolve = null;
 73		}
 74	}
 75
 76	function handleTitleUpdateConfirm() {
 77		titleUpdateDialogOpen = false;
 78		if (titleUpdateResolve) {
 79			titleUpdateResolve(true);
 80			titleUpdateResolve = null;
 81		}
 82	}
 83
 84	$effect(() => {
 85		if (alwaysShowSidebarOnDesktop && isDesktop) {
 86			sidebarOpen = true;
 87			return;
 88		}
 89
 90		if (isHomeRoute && !isNewChatMode) {
 91			// Auto-collapse sidebar when navigating to home route (but not in new chat mode)
 92			sidebarOpen = false;
 93		} else if (isHomeRoute && isNewChatMode) {
 94			// Keep sidebar open in new chat mode
 95			sidebarOpen = true;
 96		} else if (isChatRoute) {
 97			// On chat routes, only auto-show sidebar if setting is enabled
 98			if (autoShowSidebarOnNewChat) {
 99				sidebarOpen = true;
100			}
101			// If setting is disabled, don't change sidebar state - let user control it manually
102		} else {
103			// Other routes follow default behavior
104			sidebarOpen = showSidebarByDefault;
105		}
106	});
107
108	// Initialize server properties on app load (run once)
109	$effect(() => {
110		// Only fetch if we don't already have props
111		if (!serverStore.props) {
112			untrack(() => {
113				serverStore.fetch();
114			});
115		}
116	});
117
118	// Sync settings when server props are loaded
119	$effect(() => {
120		const serverProps = serverStore.props;
121
122		if (serverProps) {
123			settingsStore.syncWithServerDefaults();
124		}
125	});
126
127	// Fetch router models when in router mode (for status and modalities)
128	// Wait for models to be loaded first, run only once
129	let routerModelsFetched = false;
130
131	$effect(() => {
132		const isRouter = isRouterMode();
133		const modelsCount = modelsStore.models.length;
134
135		// Only fetch router models once when we have models loaded and in router mode
136		if (isRouter && modelsCount > 0 && !routerModelsFetched) {
137			routerModelsFetched = true;
138			untrack(() => {
139				modelsStore.fetchRouterModels();
140			});
141		}
142	});
143
144	// Monitor API key changes and redirect to error page if removed or changed when required
145	$effect(() => {
146		const apiKey = config().apiKey;
147
148		if (
149			(page.route.id === '/' || page.route.id === '/chat/[id]') &&
150			page.status !== 401 &&
151			page.status !== 403
152		) {
153			const headers: Record<string, string> = {
154				'Content-Type': 'application/json'
155			};
156
157			if (apiKey && apiKey.trim() !== '') {
158				headers.Authorization = `Bearer ${apiKey.trim()}`;
159			}
160
161			fetch(`${base}/props`, { headers })
162				.then((response) => {
163					if (response.status === 401 || response.status === 403) {
164						window.location.reload();
165					}
166				})
167				.catch((e) => {
168					console.error('Error checking API key:', e);
169				});
170		}
171	});
172
173	// Set up title update confirmation callback
174	$effect(() => {
175		conversationsStore.setTitleUpdateConfirmationCallback(
176			async (currentTitle: string, newTitle: string) => {
177				return new Promise<boolean>((resolve) => {
178					titleUpdateCurrentTitle = currentTitle;
179					titleUpdateNewTitle = newTitle;
180					titleUpdateResolve = resolve;
181					titleUpdateDialogOpen = true;
182				});
183			}
184		);
185	});
186</script>
187
188<Tooltip.Provider delayDuration={TOOLTIP_DELAY_DURATION}>
189	<ModeWatcher />
190
191	<Toaster richColors />
192
193	<DialogConversationTitleUpdate
194		bind:open={titleUpdateDialogOpen}
195		currentTitle={titleUpdateCurrentTitle}
196		newTitle={titleUpdateNewTitle}
197		onConfirm={handleTitleUpdateConfirm}
198		onCancel={handleTitleUpdateCancel}
199	/>
200
201	<Sidebar.Provider bind:open={sidebarOpen}>
202		<div class="flex h-screen w-full" style:height="{innerHeight}px">
203			<Sidebar.Root class="h-full">
204				<ChatSidebar bind:this={chatSidebar} />
205			</Sidebar.Root>
206
207			{#if !(alwaysShowSidebarOnDesktop && isDesktop)}
208				<Sidebar.Trigger
209					class="transition-left absolute left-0 z-[900] h-8 w-8 duration-200 ease-linear {sidebarOpen
210						? 'md:left-[var(--sidebar-width)]'
211						: ''}"
212					style="translate: 1rem 1rem;"
213				/>
214			{/if}
215
216			<Sidebar.Inset class="flex flex-1 flex-col overflow-hidden">
217				{@render children?.()}
218			</Sidebar.Inset>
219		</div>
220	</Sidebar.Provider>
221</Tooltip.Provider>
222
223<svelte:window onkeydown={handleKeydown} bind:innerHeight />