+page.svelte 16.3 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
	import { config, modelfiles, user, settings, db, chats, chatId } from '$lib/stores';
11
12
13
14

	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
	import { page } from '$app/stores';
17

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

Timothy J. Baek's avatar
Timothy J. Baek committed
21
	let selectedModels = [''];
22
23
24
25
26
27
	let selectedModelfile = null;
	$: selectedModelfile =
		selectedModels.length === 1 &&
		$modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0]).length > 0
			? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0]
			: null;
28

Timothy J. Baek's avatar
Timothy J. Baek committed
29
	let title = '';
Timothy J. Baek's avatar
Timothy J. Baek committed
30
	let prompt = '';
31
	let files = [];
32

33
	let messages = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
	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
49
50
	} else {
		messages = [];
Timothy J. Baek's avatar
Timothy J. Baek committed
51
	}
Timothy J. Baek's avatar
Timothy J. Baek committed
52

53
	onMount(async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
54
55
56
57
58
		await chatId.set(uuidv4());

		chatId.subscribe(async () => {
			await initNewChat();
		});
59
60
61
62
63
64
	});

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

65
66
	const initNewChat = async () => {
		console.log($chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
67

68
		autoScroll = true;
Timothy J. Baek's avatar
Timothy J. Baek committed
69

70
71
72
73
74
		title = '';
		messages = [];
		history = {
			messages: {},
			currentId: null
Timothy J. Baek's avatar
Timothy J. Baek committed
75
		};
76
77
78
		selectedModels = $page.url.searchParams.get('models')
			? $page.url.searchParams.get('models')?.split(',')
			: $settings.models ?? [''];
79
80
81
82
83
84
85

		let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
		console.log(_settings);
		settings.set({
			...$settings,
			..._settings
		});
Timothy J. Baek's avatar
Timothy J. Baek committed
86
87
	};

88
89
90
91
	//////////////////////////
	// Ollama functions
	//////////////////////////

Timothy J. Baek's avatar
Timothy J. Baek committed
92
	const sendPrompt = async (userPrompt, parentId, _chatId) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
93
94
95
		await Promise.all(
			selectedModels.map(async (model) => {
				if (model.includes('gpt-')) {
Timothy J. Baek's avatar
Timothy J. Baek committed
96
					await sendPromptOpenAI(model, userPrompt, parentId, _chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
97
				} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
98
					await sendPromptOllama(model, userPrompt, parentId, _chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
99
100
101
				}
			})
		);
102

103
		await chats.set(await $db.getChats());
104
105
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
106
	const sendPromptOllama = async (model, userPrompt, parentId, _chatId) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
107
		console.log('sendPromptOllama');
Timothy J. Baek's avatar
Timothy J. Baek committed
108
109
		let responseMessageId = uuidv4();

110
		let responseMessage = {
Timothy J. Baek's avatar
Timothy J. Baek committed
111
112
113
			parentId: parentId,
			id: responseMessageId,
			childrenIds: [],
114
			role: 'assistant',
Timothy J. Baek's avatar
Timothy J. Baek committed
115
116
			content: '',
			model: model
117
118
		};

Timothy J. Baek's avatar
Timothy J. Baek committed
119
120
121
122
123
124
125
126
127
		history.messages[responseMessageId] = responseMessage;
		history.currentId = responseMessageId;
		if (parentId !== null) {
			history.messages[parentId].childrenIds = [
				...history.messages[parentId].childrenIds,
				responseMessageId
			];
		}

Timothy J. Baek's avatar
Timothy J. Baek committed
128
		await tick();
129

Timothy J. Baek's avatar
Timothy J. Baek committed
130
		window.scrollTo({ top: document.body.scrollHeight });
131

Timothy J. Baek's avatar
Timothy J. Baek committed
132
		// const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/generate`, {
Timothy J. Baek's avatar
Timothy J. Baek committed
133
134
135
136
137
138
139
140
		// 	method: 'POST',
		// 	headers: {
		// 		'Content-Type': 'text/event-stream',
		// 		...($settings.authHeader && { Authorization: $settings.authHeader }),
		// 		...($user && { Authorization: `Bearer ${localStorage.token}` })
		// 	},
		// 	body: JSON.stringify({
		// 		model: model,
Timothy J. Baek's avatar
Timothy J. Baek committed
141
142
		// 		prompt: userPrompt,
		// 		system: $settings.system ?? undefined,
Timothy J. Baek's avatar
Timothy J. Baek committed
143
144
145
146
147
148
149
150
151
		// 		options: {
		// 			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,
		// 			num_ctx: $settings.num_ctx ?? undefined,
		// 			...($settings.options ?? {})
		// 		},
Timothy J. Baek's avatar
Timothy J. Baek committed
152
153
154
155
156
157
		// 		format: $settings.requestFormat ?? undefined,
		// 		context:
		// 			history.messages[parentId] !== null &&
		// 			history.messages[parentId].parentId in history.messages
		// 				? history.messages[history.messages[parentId].parentId]?.context ?? undefined
		// 				: undefined
Timothy J. Baek's avatar
Timothy J. Baek committed
158
159
160
		// 	})
		// });

Timothy J. Baek's avatar
Timothy J. Baek committed
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
		const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/chat`, {
			method: 'POST',
			headers: {
				'Content-Type': 'text/event-stream',
				...($settings.authHeader && { Authorization: $settings.authHeader }),
				...($user && { Authorization: `Bearer ${localStorage.token}` })
			},
			body: JSON.stringify({
				model: model,
				messages: [
					$settings.system
						? {
								role: 'system',
								content: $settings.system
						  }
						: undefined,
					...messages
				]
					.filter((message) => message)
					.map((message) => ({ role: message.role, content: message.content })),
				options: {
					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,
					num_ctx: $settings.num_ctx ?? undefined,
					...($settings.options ?? {})
				},
				format: $settings.requestFormat ?? undefined
			})
192
193
194
		}).catch((err) => {
			console.log(err);
			return null;
Timothy J. Baek's avatar
Timothy J. Baek committed
195
196
		});

197
		if (res && res.ok) {
Rohit Das's avatar
Rohit Das committed
198
199
200
201
202
203
204
205
206
207
208
209
			const reader = res.body
				.pipeThrough(new TextDecoderStream())
				.pipeThrough(splitStream('\n'))
				.getReader();

			while (true) {
				const { value, done } = await reader.read();
				if (done || stopResponseFlag || _chatId !== $chatId) {
					responseMessage.done = true;
					messages = messages;
					break;
				}
210

Rohit Das's avatar
Rohit Das committed
211
212
				try {
					let lines = value.split('\n');
213

Rohit Das's avatar
Rohit Das committed
214
215
216
217
					for (const line of lines) {
						if (line !== '') {
							console.log(line);
							let data = JSON.parse(line);
Timothy J. Baek's avatar
Timothy J. Baek committed
218

Rohit Das's avatar
Rohit Das committed
219
220
221
							if ('detail' in data) {
								throw data;
							}
Timothy J. Baek's avatar
Timothy J. Baek committed
222

Rohit Das's avatar
Rohit Das committed
223
224
225
226
227
228
229
							if (data.done == false) {
								if (responseMessage.content == '' && data.message.content == '\n') {
									continue;
								} else {
									responseMessage.content += data.message.content;
									messages = messages;
								}
230
							} else {
Rohit Das's avatar
Rohit Das committed
231
232
233
234
235
236
237
238
239
								responseMessage.done = true;
								responseMessage.context = data.context ?? null;
								responseMessage.info = {
									total_duration: data.total_duration,
									prompt_eval_count: data.prompt_eval_count,
									prompt_eval_duration: data.prompt_eval_duration,
									eval_count: data.eval_count,
									eval_duration: data.eval_duration
								};
240
241
242
243
								messages = messages;
							}
						}
					}
Rohit Das's avatar
Rohit Das committed
244
245
246
247
248
249
				} catch (error) {
					console.log(error);
					if ('detail' in error) {
						toast.error(error.detail);
					}
					break;
250
				}
Rohit Das's avatar
Rohit Das committed
251
252
253

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

Rohit Das's avatar
Rohit Das committed
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
				await $db.updateChatById(_chatId, {
					title: title === '' ? 'New Chat' : title,
					models: selectedModels,
					system: $settings.system ?? undefined,
					options: {
						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,
						num_ctx: $settings.num_ctx ?? undefined,
						...($settings.options ?? {})
					},
					messages: messages,
					history: history
				});
272
			}
273
274
275
276
277
278
279
280
281
282
283
284
		} else {
			if (res !== null) {
				const error = await res.json();
				console.log(error);
				if ('detail' in error) {
					toast.error(error.detail);
				} else {
					toast.error(error.error);
				}
			} else {
				toast.error(`Uh-oh! There was an issue connecting to Ollama.`);
			}
285
286
287
288
289
290
291
		}

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

293
		if (messages.length == 2 && messages.at(1).content !== '') {
Timothy J. Baek's avatar
Timothy J. Baek committed
294
295
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
			await generateChatTitle(_chatId, userPrompt);
296
297
298
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
299
	const sendPromptOpenAI = async (model, userPrompt, parentId, _chatId) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
300
		if ($settings.OPENAI_API_KEY) {
301
			if (models) {
302
303
				let responseMessageId = uuidv4();

304
				let responseMessage = {
305
306
307
					parentId: parentId,
					id: responseMessageId,
					childrenIds: [],
308
					role: 'assistant',
Timothy J. Baek's avatar
Timothy J. Baek committed
309
310
					content: '',
					model: model
311
312
				};

313
314
315
316
317
318
319
320
321
				history.messages[responseMessageId] = responseMessage;
				history.currentId = responseMessageId;
				if (parentId !== null) {
					history.messages[parentId].childrenIds = [
						...history.messages[parentId].childrenIds,
						responseMessageId
					];
				}

Timothy J. Baek's avatar
Timothy J. Baek committed
322
323
				await tick();

324
325
326
327
328
329
				window.scrollTo({ top: document.body.scrollHeight });

				const res = await fetch(`https://api.openai.com/v1/chat/completions`, {
					method: 'POST',
					headers: {
						'Content-Type': 'application/json',
Timothy J. Baek's avatar
Timothy J. Baek committed
330
						Authorization: `Bearer ${$settings.OPENAI_API_KEY}`
331
332
					},
					body: JSON.stringify({
Timothy J. Baek's avatar
Timothy J. Baek committed
333
						model: model,
334
						stream: true,
335
						messages: [
336
							$settings.system
337
338
								? {
										role: 'system',
Timothy J. Baek's avatar
Timothy J. Baek committed
339
										content: $settings.system
340
341
342
343
344
								  }
								: undefined,
							...messages
						]
							.filter((message) => message)
345
							.map((message) => ({ role: message.role, content: message.content })),
346
347
						temperature: $settings.temperature ?? undefined,
						top_p: $settings.top_p ?? undefined,
348
						num_ctx: $settings.num_ctx ?? undefined,
349
						frequency_penalty: $settings.repeat_penalty ?? undefined
350
351
352
353
354
355
356
357
358
359
					})
				});

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

				while (true) {
					const { value, done } = await reader.read();
Timothy J. Baek's avatar
Timothy J. Baek committed
360
361
362
					if (done || stopResponseFlag || _chatId !== $chatId) {
						responseMessage.done = true;
						messages = messages;
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
						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 });
					}

Timothy J. Baek's avatar
Timothy J. Baek committed
396
					await $db.updateChatById(_chatId, {
397
						title: title === '' ? 'New Chat' : title,
Timothy J. Baek's avatar
Timothy J. Baek committed
398
						models: selectedModels,
399
						system: $settings.system ?? undefined,
400
						options: {
401
402
403
404
							seed: $settings.seed ?? undefined,
							temperature: $settings.temperature ?? undefined,
							repeat_penalty: $settings.repeat_penalty ?? undefined,
							top_k: $settings.top_k ?? undefined,
Anthony Cucci's avatar
Anthony Cucci committed
405
							top_p: $settings.top_p ?? undefined,
406
407
							num_ctx: $settings.num_ctx ?? undefined,
							...($settings.options ?? {})
408
						},
Timothy J. Baek's avatar
Timothy J. Baek committed
409
						messages: messages,
410
						history: history
411
412
413
414
415
416
417
418
419
420
421
					});
				}

				stopResponseFlag = false;

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

				if (messages.length == 2) {
Timothy J. Baek's avatar
Timothy J. Baek committed
422
423
					window.history.replaceState(history.state, '', `/c/${_chatId}`);
					await setChatTitle(_chatId, userPrompt);
424
425
426
				}
			}
		}
427
428
429
	};

	const submitPrompt = async (userPrompt) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
430
431
		const _chatId = JSON.parse(JSON.stringify($chatId));
		console.log('submitPrompt', _chatId);
432

Timothy J. Baek's avatar
Timothy J. Baek committed
433
		if (selectedModels.includes('')) {
Timothy J. Baek's avatar
Timothy J. Baek committed
434
			toast.error('Model not selected');
435
		} else if (messages.length != 0 && messages.at(-1).done != true) {
Timothy J. Baek's avatar
Timothy J. Baek committed
436
437
			console.log('wait');
		} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
438
439
			document.getElementById('chat-textarea').style.height = '';

Timothy J. Baek's avatar
Timothy J. Baek committed
440
441
442
443
444
445
			let userMessageId = uuidv4();
			let userMessage = {
				id: userMessageId,
				parentId: messages.length !== 0 ? messages.at(-1).id : null,
				childrenIds: [],
				role: 'user',
446
447
				content: userPrompt,
				files: files.length > 0 ? files : undefined
Timothy J. Baek's avatar
Timothy J. Baek committed
448
449
450
451
452
453
454
455
			};

			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
456

Timothy J. Baek's avatar
Timothy J. Baek committed
457
458
			await tick();
			if (messages.length == 1) {
459
				await $db.createNewChat({
Timothy J. Baek's avatar
Timothy J. Baek committed
460
					id: _chatId,
461
					title: 'New Chat',
Timothy J. Baek's avatar
Timothy J. Baek committed
462
					models: selectedModels,
463
					system: $settings.system ?? undefined,
464
					options: {
465
466
467
468
						seed: $settings.seed ?? undefined,
						temperature: $settings.temperature ?? undefined,
						repeat_penalty: $settings.repeat_penalty ?? undefined,
						top_k: $settings.top_k ?? undefined,
Anthony Cucci's avatar
Anthony Cucci committed
469
						top_p: $settings.top_p ?? undefined,
470
471
						num_ctx: $settings.num_ctx ?? undefined,
						...($settings.options ?? {})
472
					},
Timothy J. Baek's avatar
Timothy J. Baek committed
473
					messages: messages,
474
					history: history
475
476
				});
			}
477

Timothy J. Baek's avatar
Timothy J. Baek committed
478
479
480
			prompt = '';
			files = [];

481
482
483
484
			setTimeout(() => {
				window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
			}, 50);

Timothy J. Baek's avatar
Timothy J. Baek committed
485
			await sendPrompt(userPrompt, userMessageId, _chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
486
487
488
		}
	};

489
490
491
492
493
	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

494
	const regenerateResponse = async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
495
496
497
		const _chatId = JSON.parse(JSON.stringify($chatId));
		console.log('regenerateResponse', _chatId);

498
499
500
		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
501

502
503
			let userMessage = messages.at(-1);
			let userPrompt = userMessage.content;
504

Timothy J. Baek's avatar
Timothy J. Baek committed
505
			await sendPrompt(userPrompt, userMessage.id, _chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
506
		}
507
	};
508

509
	const generateChatTitle = async (_chatId, userPrompt) => {
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
		if ($settings.titleAutoGenerate ?? true) {
			console.log('generateChatTitle');

			const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/generate`, {
				method: 'POST',
				headers: {
					'Content-Type': 'text/event-stream',
					...($settings.authHeader && { Authorization: $settings.authHeader }),
					...($user && { Authorization: `Bearer ${localStorage.token}` })
				},
				body: JSON.stringify({
					model: selectedModels[0],
					prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${userPrompt}`,
					stream: false
				})
525
			})
526
527
528
529
530
531
532
533
534
535
536
				.then(async (res) => {
					if (!res.ok) throw await res.json();
					return res.json();
				})
				.catch((error) => {
					if ('detail' in error) {
						toast.error(error.detail);
					}
					console.log(error);
					return null;
				});
537

538
539
540
541
542
			if (res) {
				await setChatTitle(_chatId, res.response === '' ? 'New Chat' : res.response);
			}
		} else {
			await setChatTitle(_chatId, `${userPrompt}`);
543
544
545
546
		}
	};

	const setChatTitle = async (_chatId, _title) => {
547
548
		await $db.updateChatById(_chatId, { title: _title });
		if (_chatId === $chatId) {
549
			title = _title;
550
551
		}
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
552
553
</script>

554
555
<svelte:window
	on:scroll={(e) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
556
		autoScroll = window.innerHeight + window.scrollY >= document.body.offsetHeight - 40;
557
558
559
	}}
/>

560
561
562
563
564
<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
565
		</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
566

567
		<div class=" h-full mt-10 mb-32 w-full flex flex-col">
568
569
570
571
572
573
574
575
576
			<Messages
				{selectedModels}
				{selectedModelfile}
				bind:history
				bind:messages
				bind:autoScroll
				{sendPrompt}
				{regenerateResponse}
			/>
577
578
		</div>
	</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
579

580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
	<MessageInput
		bind:prompt
		bind:files
		bind:autoScroll
		suggestionPrompts={selectedModelfile?.suggestionPrompts ?? [
			{
				title: ['Help me study', 'vocabulary for a college entrance exam'],
				content: `Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.`
			},
			{
				title: ['Give me ideas', `for what to do with my kids' art`],
				content: `What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.`
			},
			{
				title: ['Tell me a fun fact', 'about the Roman Empire'],
				content: 'Tell me a random fun fact about the Roman Empire'
			},
			{
				title: ['Show me a code snippet', `of a website's sticky header`],
				content: `Show me a code snippet of a website's sticky header in CSS and JavaScript.`
			}
		]}
		{messages}
		{submitPrompt}
		{stopResponse}
	/>
606
</div>