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 />