+layout.svelte 8.45 KB
Newer Older
1
<script lang="ts">
2
	import { v4 as uuidv4 } from 'uuid';
3
	import { openDB, deleteDB } from 'idb';
Timothy J. Baek's avatar
Timothy J. Baek committed
4
	import { onMount, tick } from 'svelte';
5
6
	import { goto } from '$app/navigation';

7
8
	import {
		config,
Timothy J. Baek's avatar
Timothy J. Baek committed
9
		info,
10
11
12
13
14
15
16
17
18
		user,
		showSettings,
		settings,
		models,
		db,
		chats,
		chatId,
		modelfiles
	} from '$lib/stores';
19
20
21
22

	import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
	import Sidebar from '$lib/components/layout/Sidebar.svelte';
	import toast from 'svelte-french-toast';
23
	import { OLLAMA_API_BASE_URL, WEBUI_API_BASE_URL } from '$lib/constants';
24

Timothy J. Baek's avatar
Timothy J. Baek committed
25
	let requiredOllamaVersion = '0.1.16';
Timothy J. Baek's avatar
Timothy J. Baek committed
26
	let loaded = false;
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

	const getModels = async () => {
		let models = [];
		const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/tags`, {
			method: 'GET',
			headers: {
				Accept: 'application/json',
				'Content-Type': 'application/json',
				...($settings.authHeader && { Authorization: $settings.authHeader }),
				...($user && { Authorization: `Bearer ${localStorage.token}` })
			}
		})
			.then(async (res) => {
				if (!res.ok) throw await res.json();
				return res.json();
			})
			.catch((error) => {
				console.log(error);
				if ('detail' in error) {
					toast.error(error.detail);
				} else {
					toast.error('Server connection failed');
				}
				return null;
			});
		console.log(res);
		models.push(...(res?.models ?? []));

		// If OpenAI API Key exists
		if ($settings.OPENAI_API_KEY) {
			// Validate OPENAI_API_KEY
58
59
60

			const API_BASE_URL = $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1';
			const openaiModelRes = await fetch(`${API_BASE_URL}/models`, {
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
				method: 'GET',
				headers: {
					'Content-Type': 'application/json',
					Authorization: `Bearer ${$settings.OPENAI_API_KEY}`
				}
			})
				.then(async (res) => {
					if (!res.ok) throw await res.json();
					return res.json();
				})
				.catch((error) => {
					console.log(error);
					toast.error(`OpenAI: ${error?.error?.message ?? 'Network Problem'}`);
					return null;
				});

77
78
79
			const openAIModels = Array.isArray(openaiModelRes)
				? openaiModelRes
				: openaiModelRes?.data ?? null;
80
81
82
83
84
85

			models.push(
				...(openAIModels
					? [
							{ name: 'hr' },
							...openAIModels
86
87
88
89
								.map((model) => ({ name: model.id, external: true }))
								.filter((model) =>
									API_BASE_URL.includes('openai') ? model.name.includes('gpt') : true
								)
90
91
92
93
94
95
96
97
98
					  ]
					: [])
			);
		}

		return models;
	};

	const getDB = async () => {
99
		const DB = await openDB('Chats', 1, {
100
101
102
103
104
105
106
107
			upgrade(db) {
				const store = db.createObjectStore('chats', {
					keyPath: 'id',
					autoIncrement: true
				});
				store.createIndex('timestamp', 'timestamp');
			}
		});
108
109

		return {
110
			db: DB,
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
			getChatById: async function (id) {
				return await this.db.get('chats', id);
			},
			getChats: async function () {
				let chats = await this.db.getAllFromIndex('chats', 'timestamp');
				chats = chats.map((item, idx) => ({
					title: chats[chats.length - 1 - idx].title,
					id: chats[chats.length - 1 - idx].id
				}));
				return chats;
			},
			exportChats: async function () {
				let chats = await this.db.getAllFromIndex('chats', 'timestamp');
				chats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
				return chats;
			},
			addChats: async function (_chats) {
				for (const chat of _chats) {
					console.log(chat);
					await this.addChat(chat);
				}
Timothy J. Baek's avatar
Timothy J. Baek committed
132
				await chats.set(await this.getChats());
133
134
135
136
137
138
139
140
			},
			addChat: async function (chat) {
				await this.db.put('chats', {
					...chat
				});
			},
			createNewChat: async function (chat) {
				await this.addChat({ ...chat, timestamp: Date.now() });
Timothy J. Baek's avatar
Timothy J. Baek committed
141
				await chats.set(await this.getChats());
142
143
144
145
146
147
148
149
150
151
			},
			updateChatById: async function (id, updated) {
				const chat = await this.getChatById(id);

				await this.db.put('chats', {
					...chat,
					...updated,
					timestamp: Date.now()
				});

Timothy J. Baek's avatar
Timothy J. Baek committed
152
				await chats.set(await this.getChats());
153
154
			},
			deleteChatById: async function (id) {
155
156
157
158
				if ($chatId === id) {
					goto('/');
					await chatId.set(uuidv4());
				}
159
				await this.db.delete('chats', id);
Timothy J. Baek's avatar
Timothy J. Baek committed
160
				await chats.set(await this.getChats());
161
162
163
164
165
			},
			deleteAllChat: async function () {
				const tx = this.db.transaction('chats', 'readwrite');
				await Promise.all([tx.store.clear(), tx.done]);

Timothy J. Baek's avatar
Timothy J. Baek committed
166
				await chats.set(await this.getChats());
167
168
			}
		};
169
170
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
	const getOllamaVersion = async () => {
		const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/version`, {
			method: 'GET',
			headers: {
				Accept: 'application/json',
				'Content-Type': 'application/json',
				...($settings.authHeader && { Authorization: $settings.authHeader }),
				...($user && { Authorization: `Bearer ${localStorage.token}` })
			}
		})
			.then(async (res) => {
				if (!res.ok) throw await res.json();
				return res.json();
			})
			.catch((error) => {
				console.log(error);
				if ('detail' in error) {
					toast.error(error.detail);
				} else {
					toast.error('Server connection failed');
				}
				return null;
			});

		console.log(res);

		return res?.version ?? '0';
	};

	const setOllamaVersion = async (ollamaVersion) => {
		await info.set({ ...$info, ollama: { version: ollamaVersion } });

		if (
			ollamaVersion.localeCompare(requiredOllamaVersion, undefined, {
				numeric: true,
				sensitivity: 'case',
				caseFirst: 'upper'
			}) < 0
		) {
			toast.error(`Ollama Version: ${ollamaVersion}`);
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
214
215
216
217
218
	onMount(async () => {
		if ($config && $config.auth && $user === undefined) {
			await goto('/auth');
		}

Timothy J. Baek's avatar
Timothy J. Baek committed
219
		await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
220

Timothy J. Baek's avatar
Timothy J. Baek committed
221
222
		await models.set(await getModels());
		await modelfiles.set(JSON.parse(localStorage.getItem('modelfiles') ?? '[]'));
223
224
225
226
227

		modelfiles.subscribe(async () => {
			await models.set(await getModels());
		});

Timothy J. Baek's avatar
Timothy J. Baek committed
228
229
230
231
232
		let _db = await getDB();
		await db.set(_db);

		await setOllamaVersion(await getOllamaVersion());

Timothy J. Baek's avatar
Timothy J. Baek committed
233
234
235
		await tick();
		loaded = true;
	});
236
237
</script>

Timothy J. Baek's avatar
Timothy J. Baek committed
238
{#if loaded}
Timothy J. Baek's avatar
Timothy J. Baek committed
239
240
241
242
243
244
	<div class="app relative">
		{#if ($info?.ollama?.version ?? '0').localeCompare( requiredOllamaVersion, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper' } ) < 0}
			<div class="absolute w-full h-full flex z-50">
				<div
					class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-900/60 flex justify-center"
				>
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
					<div class="m-auto pb-44 flex flex-col justify-center">
						<div class="max-w-md">
							<div class="text-center dark:text-white text-2xl font-medium z-50">
								Connection Issue or Update Needed
							</div>

							<div class=" mt-4 text-center text-sm dark:text-gray-200 w-full">
								Oops! It seems like your Ollama needs a little attention. <br
									class=" hidden sm:flex"
								/>We've detected either a connection hiccup or observed that you're using an older
								version. Ensure you're on the latest Ollama version
								<br class=" hidden sm:flex" />(version
								<span class=" dark:text-white font-medium">{requiredOllamaVersion} or higher</span>)
								or check your connection.
							</div>

							<div class=" mt-6 mx-auto relative group w-fit">
								<button
									class="relative z-20 flex px-5 py-2 rounded-full bg-gray-100 hover:bg-gray-200 transition font-medium text-sm"
									on:click={async () => {
										await setOllamaVersion(await getOllamaVersion());
									}}
								>
									Check Again
								</button>

								<button
									class="text-xs text-center w-full mt-2 text-gray-400 underline"
									on:click={async () => {
										await setOllamaVersion(requiredOllamaVersion);
									}}>Close</button
								>
							</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
278
279
280
281
282
283
						</div>
					</div>
				</div>
			</div>
		{/if}

284
285
286
287
288
289
290
291
292
293
		<div
			class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-800 min-h-screen overflow-auto flex flex-row"
		>
			<Sidebar />

			<SettingsModal bind:show={$showSettings} />

			<slot />
		</div>
	</div>
294
{/if}
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338

<style>
	.loading {
		display: inline-block;
		clip-path: inset(0 1ch 0 0);
		animation: l 1s steps(3) infinite;
		letter-spacing: -0.5px;
	}

	@keyframes l {
		to {
			clip-path: inset(0 -1ch 0 0);
		}
	}

	pre[class*='language-'] {
		position: relative;
		overflow: auto;

		/* make space  */
		margin: 5px 0;
		padding: 1.75rem 0 1.75rem 1rem;
		border-radius: 10px;
	}

	pre[class*='language-'] button {
		position: absolute;
		top: 5px;
		right: 5px;

		font-size: 0.9rem;
		padding: 0.15rem;
		background-color: #828282;

		border: ridge 1px #7b7b7c;
		border-radius: 5px;
		text-shadow: #c4c4c4 0 0 2px;
	}

	pre[class*='language-'] button:hover {
		cursor: pointer;
		background-color: #bcbabb;
	}
</style>