+page.svelte 16.6 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
		} else {
			if (res !== null) {
				const error = await res.json();
				console.log(error);
				if ('detail' in error) {
					toast.error(error.detail);
279
					responseMessage.content = error.detail;
280
281
				} else {
					toast.error(error.error);
282
					responseMessage.content = error.error;
283
284
285
				}
			} else {
				toast.error(`Uh-oh! There was an issue connecting to Ollama.`);
286
				responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`;
287
			}
288
289
290
291
292

			responseMessage.error = true;
			responseMessage.content = `Uh-oh! There was an issue connecting to Ollama.`;
			responseMessage.done = true;
			messages = messages;
293
294
295
296
297
298
299
		}

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

301
		if (messages.length == 2 && messages.at(1).content !== '') {
Timothy J. Baek's avatar
Timothy J. Baek committed
302
303
			window.history.replaceState(history.state, '', `/c/${_chatId}`);
			await generateChatTitle(_chatId, userPrompt);
304
305
306
		}
	};

Timothy J. Baek's avatar
Timothy J. Baek committed
307
	const sendPromptOpenAI = async (model, userPrompt, parentId, _chatId) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
308
		if ($settings.OPENAI_API_KEY) {
309
			if (models) {
310
311
				let responseMessageId = uuidv4();

312
				let responseMessage = {
313
314
315
					parentId: parentId,
					id: responseMessageId,
					childrenIds: [],
316
					role: 'assistant',
Timothy J. Baek's avatar
Timothy J. Baek committed
317
318
					content: '',
					model: model
319
320
				};

321
322
323
324
325
326
327
328
329
				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
330
331
				await tick();

332
333
334
335
336
337
				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
338
						Authorization: `Bearer ${$settings.OPENAI_API_KEY}`
339
340
					},
					body: JSON.stringify({
Timothy J. Baek's avatar
Timothy J. Baek committed
341
						model: model,
342
						stream: true,
343
						messages: [
344
							$settings.system
345
346
								? {
										role: 'system',
Timothy J. Baek's avatar
Timothy J. Baek committed
347
										content: $settings.system
348
349
350
351
352
								  }
								: undefined,
							...messages
						]
							.filter((message) => message)
353
							.map((message) => ({ role: message.role, content: message.content })),
354
355
						temperature: $settings.temperature ?? undefined,
						top_p: $settings.top_p ?? undefined,
356
						num_ctx: $settings.num_ctx ?? undefined,
357
						frequency_penalty: $settings.repeat_penalty ?? undefined
358
359
360
361
362
363
364
365
366
367
					})
				});

				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
368
369
370
					if (done || stopResponseFlag || _chatId !== $chatId) {
						responseMessage.done = true;
						messages = messages;
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
396
397
398
399
400
401
402
403
						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
404
					await $db.updateChatById(_chatId, {
405
						title: title === '' ? 'New Chat' : title,
Timothy J. Baek's avatar
Timothy J. Baek committed
406
						models: selectedModels,
407
						system: $settings.system ?? undefined,
408
						options: {
409
410
411
412
							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
413
							top_p: $settings.top_p ?? undefined,
414
415
							num_ctx: $settings.num_ctx ?? undefined,
							...($settings.options ?? {})
416
						},
Timothy J. Baek's avatar
Timothy J. Baek committed
417
						messages: messages,
418
						history: history
419
420
421
422
423
424
425
426
427
428
429
					});
				}

				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
430
431
					window.history.replaceState(history.state, '', `/c/${_chatId}`);
					await setChatTitle(_chatId, userPrompt);
432
433
434
				}
			}
		}
435
436
437
	};

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

Timothy J. Baek's avatar
Timothy J. Baek committed
441
		if (selectedModels.includes('')) {
Timothy J. Baek's avatar
Timothy J. Baek committed
442
			toast.error('Model not selected');
443
		} else if (messages.length != 0 && messages.at(-1).done != true) {
Timothy J. Baek's avatar
Timothy J. Baek committed
444
445
			console.log('wait');
		} else {
Timothy J. Baek's avatar
Timothy J. Baek committed
446
447
			document.getElementById('chat-textarea').style.height = '';

Timothy J. Baek's avatar
Timothy J. Baek committed
448
449
450
451
452
453
			let userMessageId = uuidv4();
			let userMessage = {
				id: userMessageId,
				parentId: messages.length !== 0 ? messages.at(-1).id : null,
				childrenIds: [],
				role: 'user',
454
455
				content: userPrompt,
				files: files.length > 0 ? files : undefined
Timothy J. Baek's avatar
Timothy J. Baek committed
456
457
458
459
460
461
462
463
			};

			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
464

Timothy J. Baek's avatar
Timothy J. Baek committed
465
466
			await tick();
			if (messages.length == 1) {
467
				await $db.createNewChat({
Timothy J. Baek's avatar
Timothy J. Baek committed
468
					id: _chatId,
469
					title: 'New Chat',
Timothy J. Baek's avatar
Timothy J. Baek committed
470
					models: selectedModels,
471
					system: $settings.system ?? undefined,
472
					options: {
473
474
475
476
						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
477
						top_p: $settings.top_p ?? undefined,
478
479
						num_ctx: $settings.num_ctx ?? undefined,
						...($settings.options ?? {})
480
					},
Timothy J. Baek's avatar
Timothy J. Baek committed
481
					messages: messages,
482
					history: history
483
484
				});
			}
485

Timothy J. Baek's avatar
Timothy J. Baek committed
486
487
488
			prompt = '';
			files = [];

489
490
491
492
			setTimeout(() => {
				window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
			}, 50);

Timothy J. Baek's avatar
Timothy J. Baek committed
493
			await sendPrompt(userPrompt, userMessageId, _chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
494
495
496
		}
	};

497
498
499
500
501
	const stopResponse = () => {
		stopResponseFlag = true;
		console.log('stopResponse');
	};

502
	const regenerateResponse = async () => {
Timothy J. Baek's avatar
Timothy J. Baek committed
503
504
505
		const _chatId = JSON.parse(JSON.stringify($chatId));
		console.log('regenerateResponse', _chatId);

506
507
508
		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
509

510
511
			let userMessage = messages.at(-1);
			let userPrompt = userMessage.content;
512

Timothy J. Baek's avatar
Timothy J. Baek committed
513
			await sendPrompt(userPrompt, userMessage.id, _chatId);
Timothy J. Baek's avatar
Timothy J. Baek committed
514
		}
515
	};
516

517
	const generateChatTitle = async (_chatId, userPrompt) => {
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
		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
				})
533
			})
534
535
536
537
538
539
540
541
542
543
544
				.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;
				});
545

546
547
548
549
550
			if (res) {
				await setChatTitle(_chatId, res.response === '' ? 'New Chat' : res.response);
			}
		} else {
			await setChatTitle(_chatId, `${userPrompt}`);
551
552
553
554
		}
	};

	const setChatTitle = async (_chatId, _title) => {
555
556
		await $db.updateChatById(_chatId, { title: _title });
		if (_chatId === $chatId) {
557
			title = _title;
558
559
		}
	};
Timothy J. Baek's avatar
Timothy J. Baek committed
560
561
</script>

562
563
<svelte:window
	on:scroll={(e) => {
Timothy J. Baek's avatar
Timothy J. Baek committed
564
		autoScroll = window.innerHeight + window.scrollY >= document.body.offsetHeight - 40;
565
566
567
	}}
/>

568
569
570
571
572
<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
573
		</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
574

575
		<div class=" h-full mt-10 mb-32 w-full flex flex-col">
576
577
578
579
580
581
582
583
584
			<Messages
				{selectedModels}
				{selectedModelfile}
				bind:history
				bind:messages
				bind:autoScroll
				{sendPrompt}
				{regenerateResponse}
			/>
585
586
		</div>
	</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
587

588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
	<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}
	/>
614
</div>