+page.svelte 12.5 KB
Newer Older
Timothy J. Baek's avatar
Timothy J. Baek committed
1
<script lang="ts">
2
	import { v4 as uuidv4 } from 'uuid';
Timothy J. Baek's avatar
Timothy J. Baek committed
3
4
	import toast from 'svelte-french-toast';

5
	import { OLLAMA_API_BASE_URL } from '$lib/constants';
6
	import { onMount, tick } from 'svelte';
7
8
	import { splitStream } from '$lib/utils';
	import { goto } from '$app/navigation';
Timothy J. Baek's avatar
Timothy J. Baek committed
9

10
11
12
13
14
	import { config, user, settings, db, chats, chatId } from '$lib/stores';

	import MessageInput from '$lib/components/chat/MessageInput.svelte';
	import Messages from '$lib/components/chat/Messages.svelte';
	import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
15
	import Navbar from '$lib/components/layout/Navbar.svelte';
16

17
18
	let stopResponseFlag = false;
	let autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
19

Timothy J. Baek's avatar
Timothy J. Baek committed
20
	let selectedModels = [''];
21

Timothy J. Baek's avatar
Timothy J. Baek committed
22
	let title = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
23
	let prompt = '';
24

25
	let messages = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
	let history = {
		messages: {},
		currentId: null
	};

	$: if (history.currentId !== null) {
		let _messages = [];

		let currentMessage = history.messages[history.currentId];
		while (currentMessage !== null) {
			_messages.unshift({ ...currentMessage });
			currentMessage =
				currentMessage.parentId !== null ? history.messages[currentMessage.parentId] : null;
		}
		messages = _messages;
	}
Timothy J. Baek's avatar
Timothy J. Baek committed
42

43
	onMount(async () => {
44
45
		console.log();
		await initNewChat();
46
47
48
49
50
51
	});

	//////////////////////////
	// Web functions
	//////////////////////////

52
53
	const initNewChat = async () => {
		await chatId.set(uuidv4());
Timothy J. Baek's avatar
Timothy J. Baek committed
54

55
		console.log($chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
56

57
		autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
58

59
60
61
62
63
		title = '';
		messages = [];
		history = {
			messages: {},
			currentId: null
Timothy J. Baek's avatar
Timothy J. Baek committed
64
		};
Timothy J. Baek's avatar
Timothy J. Baek committed
65

66
67
		await settings.set(JSON.parse(localStorage.getItem('settings') ?? JSON.stringify($settings)));
		selectedModels = $settings.models ?? [''];
Timothy J. Baek's avatar
Timothy J. Baek committed
68
69
	};

70
71
72
73
	//////////////////////////
	// Ollama functions
	//////////////////////////

Timothy J. Baek's avatar
Timothy J. Baek committed
74
	const sendPrompt = async (userPrompt, parentId) => {
75
76
		await chats.set(await $db.getAllFromIndex('chats', 'timestamp'));

Timothy J. Baek's avatar
Timothy J. Baek committed
77
78
79
80
81
82
83
84
85
		await Promise.all(
			selectedModels.map(async (model) => {
				if (model.includes('gpt-')) {
					await sendPromptOpenAI(model, userPrompt, parentId);
				} else {
					await sendPromptOllama(model, userPrompt, parentId);
				}
			})
		);
86
87
88

		await chats.set(await $db.getAllFromIndex('chats', 'timestamp'));

Timothy J. Baek's avatar
Timothy J. Baek committed
89
		console.log(history);
90
91
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
92
	const sendPromptOllama = async (model, userPrompt, parentId) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
93
94
		let responseMessageId = uuidv4();

95
		let responseMessage = {
Timothy J. Baek's avatar
Timothy J. Baek committed
96
97
98
			parentId: parentId,
			id: responseMessageId,
			childrenIds: [],
99
			role: 'assistant',
Timothy J. Baek's avatar
Timothy J. Baek committed
100
101
			content: '',
			model: model
102
103
		};

Timothy J. Baek's avatar
Timothy J. Baek committed
104
105
106
107
108
109
110
111
112
		history.messages[responseMessageId] = responseMessage;
		history.currentId = responseMessageId;
		if (parentId !== null) {
			history.messages[parentId].childrenIds = [
				...history.messages[parentId].childrenIds,
				responseMessageId
			];
		}

113
114
		window.scrollTo({ top: document.body.scrollHeight });

115
		const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/generate`, {
116
117
			method: 'POST',
			headers: {
Timothy J. Baek's avatar
Timothy J. Baek committed
118
				'Content-Type': 'text/event-stream',
119
				...($settings.authHeader && { Authorization: $settings.authHeader }),
120
				...($user && { Authorization: `Bearer ${localStorage.token}` })
121
122
			},
			body: JSON.stringify({
Timothy J. Baek's avatar
Timothy J. Baek committed
123
				model: model,
124
				prompt: userPrompt,
125
				system: $settings.system ?? undefined,
126
				options: {
127
128
129
130
131
					seed: $settings.seed ?? undefined,
					temperature: $settings.temperature ?? undefined,
					repeat_penalty: $settings.repeat_penalty ?? undefined,
					top_k: $settings.top_k ?? undefined,
					top_p: $settings.top_p ?? undefined
132
				},
133
				format: $settings.requestFormat ?? undefined,
134
				context:
Timothy J. Baek's avatar
Timothy J. Baek committed
135
136
137
					history.messages[parentId] !== null &&
					history.messages[parentId].parentId in history.messages
						? history.messages[history.messages[parentId].parentId]?.context ?? undefined
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
						: undefined
			})
		});

		const reader = res.body
			.pipeThrough(new TextDecoderStream())
			.pipeThrough(splitStream('\n'))
			.getReader();

		while (true) {
			const { value, done } = await reader.read();
			if (done || stopResponseFlag) {
				if (stopResponseFlag) {
					responseMessage.done = true;
					messages = messages;
				}

				break;
			}

			try {
				let lines = value.split('\n');

				for (const line of lines) {
					if (line !== '') {
						console.log(line);
						let data = JSON.parse(line);
						if (data.done == false) {
							if (responseMessage.content == '' && data.response == '\n') {
								continue;
							} else {
								responseMessage.content += data.response;
								messages = messages;
							}
172
173
						} else if ('detail' in data) {
							throw data;
174
175
176
177
178
179
180
181
182
						} else {
							responseMessage.done = true;
							responseMessage.context = data.context;
							messages = messages;
						}
					}
				}
			} catch (error) {
				console.log(error);
183
184
185
186
				if ('detail' in error) {
					toast.error(error.detail);
				}
				break;
187
188
189
190
191
192
			}

			if (autoScroll) {
				window.scrollTo({ top: document.body.scrollHeight });
			}

193
194
			await $db.put('chats', {
				id: $chatId,
195
				title: title === '' ? 'New Chat' : title,
Timothy J. Baek's avatar
Timothy J. Baek committed
196
				models: selectedModels,
197
				system: $settings.system ?? undefined,
198
				options: {
199
200
201
202
203
					seed: $settings.seed ?? undefined,
					temperature: $settings.temperature ?? undefined,
					repeat_penalty: $settings.repeat_penalty ?? undefined,
					top_k: $settings.top_k ?? undefined,
					top_p: $settings.top_p ?? undefined
204
				},
Timothy J. Baek's avatar
Timothy J. Baek committed
205
				messages: messages,
206
207
				history: history,
				timestamp: Date.now()
208
209
210
211
212
213
214
215
			});
		}

		stopResponseFlag = false;
		await tick();
		if (autoScroll) {
			window.scrollTo({ top: document.body.scrollHeight });
		}
216

217
		if (messages.length == 2 && messages.at(1).content !== '') {
218
219
			window.history.replaceState(history.state, '', `/c/${$chatId}`);
			await generateChatTitle($chatId, userPrompt);
220
221
222
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
223
	const sendPromptOpenAI = async (model, userPrompt, parentId) => {
224
225
		if (settings.OPENAI_API_KEY) {
			if (models) {
226
227
				let responseMessageId = uuidv4();

228
				let responseMessage = {
229
230
231
					parentId: parentId,
					id: responseMessageId,
					childrenIds: [],
232
					role: 'assistant',
Timothy J. Baek's avatar
Timothy J. Baek committed
233
234
					content: '',
					model: model
235
236
				};

237
238
239
240
241
242
243
244
245
				history.messages[responseMessageId] = responseMessage;
				history.currentId = responseMessageId;
				if (parentId !== null) {
					history.messages[parentId].childrenIds = [
						...history.messages[parentId].childrenIds,
						responseMessageId
					];
				}

246
247
248
249
250
251
252
253
254
				window.scrollTo({ top: document.body.scrollHeight });

				const res = await fetch(`https://api.openai.com/v1/chat/completions`, {
					method: 'POST',
					headers: {
						'Content-Type': 'application/json',
						Authorization: `Bearer ${settings.OPENAI_API_KEY}`
					},
					body: JSON.stringify({
Timothy J. Baek's avatar
Timothy J. Baek committed
255
						model: model,
256
						stream: true,
257
						messages: [
258
							$settings.system
259
260
261
262
263
264
265
266
								? {
										role: 'system',
										content: settings.system
								  }
								: undefined,
							...messages
						]
							.filter((message) => message)
267
							.map((message) => ({ role: message.role, content: message.content })),
268
269
270
						temperature: $settings.temperature ?? undefined,
						top_p: $settings.top_p ?? undefined,
						frequency_penalty: $settings.repeat_penalty ?? undefined
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
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
					})
				});

				const reader = res.body
					.pipeThrough(new TextDecoderStream())
					.pipeThrough(splitStream('\n'))
					.getReader();

				while (true) {
					const { value, done } = await reader.read();
					if (done || stopResponseFlag) {
						if (stopResponseFlag) {
							responseMessage.done = true;
							messages = messages;
						}

						break;
					}

					try {
						let lines = value.split('\n');

						for (const line of lines) {
							if (line !== '') {
								console.log(line);
								if (line === 'data: [DONE]') {
									responseMessage.done = true;
									messages = messages;
								} else {
									let data = JSON.parse(line.replace(/^data: /, ''));
									console.log(data);

									if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
										continue;
									} else {
										responseMessage.content += data.choices[0].delta.content ?? '';
										messages = messages;
									}
								}
							}
						}
					} catch (error) {
						console.log(error);
					}

					if (autoScroll) {
						window.scrollTo({ top: document.body.scrollHeight });
					}

320
321
					await $db.put('chats', {
						id: $chatId,
322
						title: title === '' ? 'New Chat' : title,
Timothy J. Baek's avatar
Timothy J. Baek committed
323
324
						models: selectedModels,

325
						system: $settings.system ?? undefined,
326
						options: {
327
328
329
330
331
							seed: $settings.seed ?? undefined,
							temperature: $settings.temperature ?? undefined,
							repeat_penalty: $settings.repeat_penalty ?? undefined,
							top_k: $settings.top_k ?? undefined,
							top_p: $settings.top_p ?? undefined
332
						},
Timothy J. Baek's avatar
Timothy J. Baek committed
333
						messages: messages,
334
335
						history: history,
						timestamp: Date.now()
336
337
338
339
340
341
342
343
344
345
346
					});
				}

				stopResponseFlag = false;

				await tick();
				if (autoScroll) {
					window.scrollTo({ top: document.body.scrollHeight });
				}

				if (messages.length == 2) {
347
348
349
					window.history.replaceState(history.state, '', `/c/${$chatId}`);

					await setChatTitle($chatId, userPrompt);
350
351
352
				}
			}
		}
353
354
355
	};

	const submitPrompt = async (userPrompt) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
356
		console.log('submitPrompt');
357

Timothy J. Baek's avatar
Timothy J. Baek committed
358
		if (selectedModels.includes('')) {
Timothy J. Baek's avatar
Timothy J. Baek committed
359
			toast.error('Model not selected');
360
		} else if (messages.length != 0 && messages.at(-1).done != true) {
Timothy J. Baek's avatar
Timothy J. Baek committed
361
362
			console.log('wait');
		} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
363
364
			document.getElementById('chat-textarea').style.height = '';

Timothy J. Baek's avatar
Timothy J. Baek committed
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
			let userMessageId = uuidv4();

			let userMessage = {
				id: userMessageId,
				parentId: messages.length !== 0 ? messages.at(-1).id : null,
				childrenIds: [],
				role: 'user',
				content: userPrompt
			};

			if (messages.length !== 0) {
				history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
			}

			history.messages[userMessageId] = userMessage;
			history.currentId = userMessageId;
Timothy J. Baek's avatar
Timothy J. Baek committed
381
382
383

			prompt = '';

384
			if (messages.length == 0) {
385
386
387
				await $db.put('chats', {
					id: $chatId,
					title: 'New Chat',
Timothy J. Baek's avatar
Timothy J. Baek committed
388
					models: selectedModels,
389
					system: $settings.system ?? undefined,
390
					options: {
391
392
393
394
395
						seed: $settings.seed ?? undefined,
						temperature: $settings.temperature ?? undefined,
						repeat_penalty: $settings.repeat_penalty ?? undefined,
						top_k: $settings.top_k ?? undefined,
						top_p: $settings.top_p ?? undefined
396
					},
Timothy J. Baek's avatar
Timothy J. Baek committed
397
					messages: messages,
398
399
					history: history,
					timestamp: Date.now()
400
401
				});
			}
402

403
404
405
406
			setTimeout(() => {
				window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
			}, 50);

Timothy J. Baek's avatar
Timothy J. Baek committed
407
			await sendPrompt(userPrompt, userMessageId);
Timothy J. Baek's avatar
Timothy J. Baek committed
408
409
410
		}
	};

411
412
413
414
415
	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

416
417
418
419
420
	const regenerateResponse = async () => {
		console.log('regenerateResponse');
		if (messages.length != 0 && messages.at(-1).done == true) {
			messages.splice(messages.length - 1, 1);
			messages = messages;
Timothy J. Baek's avatar
Timothy J. Baek committed
421

422
423
			let userMessage = messages.at(-1);
			let userPrompt = userMessage.content;
424

Timothy J. Baek's avatar
Timothy J. Baek committed
425
			await sendPrompt(userPrompt, userMessage.id);
Timothy J. Baek's avatar
Timothy J. Baek committed
426
		}
427
	};
428

429
430
	const generateChatTitle = async (_chatId, userPrompt) => {
		console.log('generateChatTitle');
431

432
		const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/generate`, {
433
434
			method: 'POST',
			headers: {
Timothy J. Baek's avatar
Timothy J. Baek committed
435
				'Content-Type': 'text/event-stream',
436
				...($settings.authHeader && { Authorization: $settings.authHeader }),
437
				...($user && { Authorization: `Bearer ${localStorage.token}` })
438
439
			},
			body: JSON.stringify({
Timothy J. Baek's avatar
Timothy J. Baek committed
440
				model: selectedModels[0],
441
				prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${userPrompt}`,
442
443
444
445
446
447
448
449
				stream: false
			})
		})
			.then(async (res) => {
				if (!res.ok) throw await res.json();
				return res.json();
			})
			.catch((error) => {
450
451
452
				if ('detail' in error) {
					toast.error(error.detail);
				}
453
454
455
456
457
				console.log(error);
				return null;
			});

		if (res) {
458
459
460
461
462
			await setChatTitle(_chatId, res.response === '' ? 'New Chat' : res.response);
		}
	};

	const setChatTitle = async (_chatId, _title) => {
463
464
465
		const chat = await $db.get('chats', _chatId);
		await $db.put('chats', { ...chat, title: _title });
		if (chat.id === $chatId) {
466
			title = _title;
467
468
		}
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
469
470
</script>

471
472
<svelte:window
	on:scroll={(e) => {
473
		console.log(e);
Timothy J. Baek's avatar
Timothy J. Baek committed
474
		autoScroll = window.innerHeight + window.scrollY >= document.body.offsetHeight - 40;
475
476
477
	}}
/>

478
479
480
481
482
<Navbar {title} />
<div class="min-h-screen w-full flex justify-center">
	<div class=" py-2.5 flex flex-col justify-between w-full">
		<div class="max-w-2xl mx-auto w-full px-3 md:px-0 mt-10">
			<ModelSelector bind:selectedModels disabled={messages.length > 0} />
Timothy J. Baek's avatar
Timothy J. Baek committed
483
		</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
484

485
486
487
488
		<div class=" h-full mt-10 mb-32 w-full flex flex-col">
			<Messages bind:history bind:messages bind:autoScroll {sendPrompt} {regenerateResponse} />
		</div>
	</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
489

490
491
	<MessageInput bind:prompt bind:autoScroll {messages} {submitPrompt} {stopResponse} />
</div>