Skip to content
This repository has been archived by the owner on Jan 8, 2025. It is now read-only.

Commit

Permalink
Add chat component (#3)
Browse files Browse the repository at this point in the history
* Add a chat component

* Update test to check for correct URL
  • Loading branch information
ReinderVosDeWael authored Feb 21, 2024
1 parent 745b5f8 commit a5983d7
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 27 deletions.
10 changes: 10 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface Prompts {
system: { [key: string]: string };
user: { [key: string]: string };
}

export interface Message {
role: 'user' | 'system' | 'assistant';
content: string;
timestamp: string;
}
5 changes: 5 additions & 0 deletions src/routes/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from '@sveltejs/kit';

export function load() {
redirect(308, '/text-to-speech');
}
15 changes: 0 additions & 15 deletions src/routes/+page.svelte

This file was deleted.

38 changes: 38 additions & 0 deletions src/routes/api/gpt/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import OpenAI from 'openai';
import { OPENAI_API_KEY } from '$lib/server/secrets.js';
import type { Message } from '$lib/types';

const openai = new OpenAI({ apiKey: OPENAI_API_KEY });

export async function POST({ request }) {
const json = await request.json();
const messages = json.messages as Message[];
const messagesOpenai = messages.map((message) => {
return {
role: message.role,
content: message.content
};
});
const model = json.model as string;

if (messages.length === 0) {
return new Response('Missing input.', { status: 422 });
}
if (messages[messages.length - 1].role !== 'user') {
return new Response('Last message must be from user.', { status: 422 });
}

const response = await openai.chat.completions.create({
model,
messages: messagesOpenai
});
const response_message = response.choices[0].message.content;
if (!response_message) {
return new Response('No response from OpenAI.', { status: 500 });
}

return new Response(JSON.stringify({ message: response_message }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
1 change: 1 addition & 0 deletions src/routes/gpt/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from 'fs';
import yaml from 'js-yaml';
import type { Prompts } from '$lib/types';

export function load() {
const promptFile = 'static/prompts.yaml';
Expand Down
34 changes: 27 additions & 7 deletions src/routes/gpt/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import Chat from './Chat.svelte';
import SystemPrompt from './SystemPrompt.svelte';
import { getToastStore, type ToastSettings } from '@skeletonlabs/skeleton';
Expand All @@ -9,21 +10,40 @@
const toastStore = getToastStore();
const noPromptToast: ToastSettings = {
message: 'Please select or write a system prompt.',
background: 'variant-filled-warning'
};
function onClick() {
if (disableSystemPrompt) {
endChat();
} else {
startChat();
}
}
function startChat() {
if (systemPrompt === '') {
const noPromptToast: ToastSettings = {
message: 'Please select or write a system prompt.',
background: 'variant-filled-warning'
};
toastStore.trigger(noPromptToast);
return;
}
disableSystemPrompt = true;
}
function endChat() {
disableSystemPrompt = false;
}
</script>

<SystemPrompt presets={data.systemPrompts} bind:systemPrompt disabled={disableSystemPrompt} />
<button class="btn variant-soft-primary" on:click={startChat} disabled={disableSystemPrompt}
>Start Chat</button
>
<button class="btn variant-soft-primary" on:click={onClick}>
{#if disableSystemPrompt}
End Chat
{:else}
Start Chat
{/if}
</button>

{#if systemPrompt !== '' && disableSystemPrompt}
<Chat {systemPrompt} />
{/if}
123 changes: 123 additions & 0 deletions src/routes/gpt/Chat.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<script lang="ts">
import { Avatar } from '@skeletonlabs/skeleton';
import { onMount } from 'svelte';
export let systemPrompt: string;
let messages = [
{ role: 'system', content: systemPrompt, timestamp: new Date().toLocaleTimeString() }
];
let currentMessage = '';
let elemChat: HTMLElement;
let loading = false;
const model = 'gpt-4';
const user_avatar = 'https://i.pravatar.cc/150?img=31';
const server_avatar = 'https://i.pravatar.cc/150?img=54';
async function addUserMessage(event: KeyboardEvent) {
if (event.key !== 'Enter') return;
if (currentMessage === '') return;
if (loading) return;
event.preventDefault();
addMessage(currentMessage, 'user');
loading = true;
await fetch('/api/gpt', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ messages, model })
})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error('Network response was not ok.');
})
.then((data) => {
addMessage(data.message, 'assistant');
loading = false;
})
.catch((error) => {
console.error('There was a problem with the fetch operation:', error);
});
loading = false;
}
function addMessage(content: string, role: 'assistant' | 'user') {
const timestamp = new Date().toLocaleTimeString();
messages = [
...messages,
{
content,
role,
timestamp
}
];
currentMessage = '';
// Timeout prevents race condition
setTimeout(() => {
scrollChatBottom();
}, 0);
}
function scrollChatBottom(): void {
if (!elemChat) return;
elemChat.scrollTo({ top: elemChat.scrollHeight, behavior: 'smooth' });
}
onMount(() => {
scrollChatBottom();
});
</script>

<div class="chat w-full h-full">
<section
bind:this={elemChat}
id="chat-container"
class="w-full max-h-[600px] min-h-[600px] p-4 overflow-y-auto space-y-4"
>
{#each messages as bubble}
{#if bubble.role === 'assistant'}
<div class="grid grid-cols-[auto_1fr] gap-2">
<Avatar src={server_avatar} width="w-12" />
<div class="card p-4 rounded-tl-none space-y-2 bg-primary-200/30">
<header class="flex justify-between items-center">
<p class="font-bold">Agate</p>
<small class="opacity-50">{bubble.timestamp}</small>
</header>
<p class="whitespace-pre">{bubble.content}</p>
</div>
</div>
{:else if bubble.role === 'user'}
<div class="grid grid-cols-[1fr_auto] gap-2">
<div class="card p-4 rounded-tr-none space-y-2 bg-primary-500/30">
<header class="flex justify-between items-center">
<p class="font-bold">You</p>
<small class="opacity-50">{bubble.timestamp}</small>
</header>
<p class="whitespace-pre">{bubble.content}</p>
</div>
<Avatar src={user_avatar} width="w-12" />
</div>
{/if}
{/each}
{#if loading}
<p>Agate is typing...</p>
{/if}
</section>
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto] rounded-container-token">
<button class="input-group-shim">+</button>
<textarea
bind:value={currentMessage}
class="bg-transparent border-0 ring-0"
name="prompt"
id="prompt"
placeholder="Write a message..."
rows="4"
on:keydown={addUserMessage}
/>
</div>
</div>
5 changes: 1 addition & 4 deletions src/routes/gpt/SystemPrompt.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
<script lang="ts">
interface Prompts {
system: { [key: string]: string };
user: { [key: string]: string };
}
import type { Prompts } from '$lib/types';
export let systemPrompt: string = '';
export let presets: Prompts;
Expand Down
2 changes: 1 addition & 1 deletion tests/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { expect, test } from '@playwright/test';

test('index page has expected h1', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: "Let's get cracking bones!" })).toBeVisible();
expect(page.url()).toContain('/text-to-speech');
});

0 comments on commit a5983d7

Please sign in to comment.