+layout.svelte 8.39 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
77
78
79
80
81
82
83
				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;
				});

			const openAIModels = openaiModelRes?.data ?? null;

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

		return models;
	};

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

		return {
108
			db: DB,
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
			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
130
				await chats.set(await this.getChats());
131
132
133
134
135
136
137
138
			},
			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
139
				await chats.set(await this.getChats());
140
141
142
143
144
145
146
147
148
149
			},
			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
150
				await chats.set(await this.getChats());
151
152
			},
			deleteChatById: async function (id) {
153
154
155
156
				if ($chatId === id) {
					goto('/');
					await chatId.set(uuidv4());
				}
157
				await this.db.delete('chats', id);
Timothy J. Baek's avatar
Timothy J. Baek committed
158
				await chats.set(await this.getChats());
159
160
161
162
163
			},
			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
164
				await chats.set(await this.getChats());
165
166
			}
		};
167
168
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
169
170
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
	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
212
213
214
215
216
	onMount(async () => {
		if ($config && $config.auth && $user === undefined) {
			await goto('/auth');
		}

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

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

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

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

		await setOllamaVersion(await getOllamaVersion());

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

Timothy J. Baek's avatar
Timothy J. Baek committed
236
{#if loaded}
Timothy J. Baek's avatar
Timothy J. Baek committed
237
238
239
240
241
242
	<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"
				>
243
244
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
					<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
276
277
278
279
280
281
						</div>
					</div>
				</div>
			</div>
		{/if}

282
283
284
285
286
287
288
289
290
291
		<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>
292
{/if}
293
294
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

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