Initial commit

This commit is contained in:
2023-04-04 00:00:04 -04:00
commit bfc698c49c
80 changed files with 72665 additions and 0 deletions

39
src/app.css Normal file
View File

@@ -0,0 +1,39 @@
/* Write your global styles here, in PostCSS syntax */
@import './styles/tailwind.css';
@import './styles/vars.css';
html,
body {
height: 100%;
font-family: Helvetica, sans-serif;
}
pre {
display: inline-flex;
padding: 0.5rem;
width: 100%;
border-radius: 0.25rem
}
code {
background-color: rgba(44, 43, 43, 0.6);
color: rgb(190 18 60 / var(--tw-text-opacity));
border-radius: 0.25rem;
padding: 0 0.5rem;
}
body {
color: white;
background-color: #131416;
}
ol {
list-style: number;
padding: 1rem;
}
ol > li {
margin: 1rem 0;
}

12
src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

12
src/app.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

7
src/index.test.ts Normal file
View File

@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { onMount } from 'svelte';
import Chat from './Icons/Chat.svelte';
import Pencil from './Icons/Pencil.svelte';
import Plus from './Icons/Plus.svelte';
import Trash from './Icons/Trash.svelte';
import { chatMessages } from '$lib/stores/chat-messages';
import {
chatHistory,
filterHistory,
chatHistorySubscription,
loadMessages
} from '../stores/chat-history';
let chatHistoryKeys: any = [];
onMount(() => {
chatHistorySubscription.set($chatHistory);
chatHistorySubscription.subscribe((value: any) => {
chatHistoryKeys = Object.keys(value);
});
});
</script>
<div
class="h-[700px] w-[350px] bg-black bg-opacity-20 rounded-md py-4 px-2 overflow-y-auto flex flex-col gap-2"
>
<button
on:click={chatMessages.reset}
class="flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm mb-2 flex-shrink-0 border border-white/20"
>
<Plus /> New Game
</button>
{#if chatHistoryKeys.length > 0}
{#each chatHistoryKeys as message (message)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:click={() => loadMessages(message)}
class="flex py-3 px-3 items-center gap-3 relative rounded-md cursor-pointer break-all pr-14 bg-opacity-40 hover:bg-white/5 bg-black group animate-flash text-sm"
>
<Chat />
<div class="flex-1 text-ellipsis max-h-5 overflow-hidden break-all relative">{message}</div>
<div class="absolute flex right-1 z-10 text-gray-300 visible">
<button on:click={() => loadMessages(message)} class="p-1 hover:text-white">
<Pencil />
</button>
<button
on:click|preventDefault={() => filterHistory(message)}
class="p-1 hover:text-white"
>
<Trash />
</button>
</div>
</div>
{/each}
{/if}
</div>

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import Markdown from 'svelte-exmarkdown';
import { marked } from 'marked';
import DOMPurify from 'isomorphic-dompurify';
import type { ChatCompletionRequestMessageRoleEnum } from 'openai';
import { onMount } from 'svelte';
export let type: ChatCompletionRequestMessageRoleEnum;
export let message: string = '';
export { classes as class };
let classes = '';
let scrollToDiv: HTMLDivElement;
const classSet = {
user: 'justify-end text-rose-700',
assistant: 'justify-start text-teal-400',
system: 'justify-center text-gray-400'
};
const typeEffect = (node: HTMLDivElement, message: string) => {
return {
update(message: string) {
scrollToDiv.scrollIntoView({ behavior: 'auto', block: 'end', inline: 'end' });
}
};
};
onMount(() => {
scrollToDiv.scrollIntoView({ behavior: 'auto', block: 'end', inline: 'end' });
});
</script>
<div class="flex items-center {classSet[type]} ">
<p class="text-xs px-2">{type === 'user' ? 'Me' : 'Bot'}</p>
</div>
<div class="flex {classSet[type]}">
<div
use:typeEffect={message}
class="bg-black py-0.5 px-4 max-w-2xl rounded leading-loose {classes} {classSet[type]}"
>
{@html DOMPurify.sanitize(marked.parse(message))}
</div>
<div bind:this={scrollToDiv} />
</div>

View File

@@ -0,0 +1,13 @@
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /></svg
>

After

Width:  |  Height:  |  Size: 293 B

View File

@@ -0,0 +1,13 @@
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
><path d="M12 20h9" /><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" /></svg
>

After

Width:  |  Height:  |  Size: 308 B

View File

@@ -0,0 +1,13 @@
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg
>

After

Width:  |  Height:  |  Size: 297 B

View File

@@ -0,0 +1,20 @@
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
><polyline points="3 6 5 6 21 6" /><path
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
/><line x1="10" y1="11" x2="10" y2="17" /><line
x1="14"
y1="11"
x2="14"
y2="17"
/></svg
>

After

Width:  |  Height:  |  Size: 422 B

View File

@@ -0,0 +1,25 @@
<script lang="ts">
export let value: any;
export let placeholder = '';
export let name = '';
export let type = 'text';
export let label = '';
let classes = '';
export { classes as class };
const typeAction = (node: HTMLInputElement) => {
node.type = type;
};
</script>
<div class="flex flex-col min-w-xl {classes}">
<label class="text-xs" for={name}>{label}</label>
<input
{name}
{placeholder}
use:typeAction
bind:value
class="shadow bg-white/10 rounded-md px-4 py-1.5 min-w-xl text-teal-300 border border-transparent focus:outline-none focus:ring-1 focus:ring-teal-300 focus:border-transparent"
{...$$restProps}
/>
</div>

View File

@@ -0,0 +1,53 @@
import { derived, get, writable } from 'svelte/store';
import { chatMessages, type ChatTranscript } from './chat-messages';
import { browser } from '$app/environment';
export const chatHistorySubscription = writable();
const setLocalHistory = <T>(history: T) =>
localStorage.setItem('chatHistory', JSON.stringify(history));
const getLocalHistory = () => JSON.parse(localStorage.getItem('chatHistory') || '{}');
export const chatHistory = derived(chatMessages, ($chatMessages) => {
if (!browser) return null;
let history = localStorage.getItem('chatHistory');
if (!history && $chatMessages.messages.length === 1) return null;
if (history && $chatMessages.messages.length === 1) return JSON.parse(history);
const key = $chatMessages.messages[1].content; //The second message is the query
const value = $chatMessages.messages;
const obj = { [key]: value };
if (!history) setLocalHistory(obj);
const chatHistory = getLocalHistory();
if (chatHistory) {
chatHistory[key] = value;
setLocalHistory(chatHistory);
chatHistorySubscription.set(chatHistory);
return chatHistory;
}
return null;
});
export const filterHistory = (key: string) => {
const history = getLocalHistory();
delete history[key];
setLocalHistory(history);
chatHistorySubscription.set(history);
};
const getHistory = (key: string) => getLocalHistory()[key]; //Returns the history for a given key
export const loadMessages = (query: string) => {
if (get(chatMessages).chatState !== 'idle') return; //Prevents switching between messages while loading
if (!query) return;
const newMessages = getHistory(query);
chatMessages.replace({ messages: newMessages, chatState: 'idle' });
};

View File

@@ -0,0 +1,80 @@
import type { ChatCompletionRequestMessage } from 'openai';
import { SSE } from 'sse.js';
import { get, writable } from 'svelte/store';
export interface ChatTranscript {
messages: ChatCompletionRequestMessage[];
chatState: 'idle' | 'loading' | 'error' | 'message';
}
const { subscribe, update, ...store } = writable<ChatTranscript>({
messages: [
{ role: 'assistant', content: 'Welcome! Please introduce yourself to your AI competitor.'}
],
chatState: 'idle'
});
const set = async (query: string) => {
updateMessages(query, 'user', 'loading');
request(query);
};
const request = async (query: string) => {
const eventSource = new SSE('/api/chat', {
headers: {
'Content-Type': 'application/json'
},
payload: JSON.stringify({ messages: get(chatMessages).messages })
});
eventSource.addEventListener('error', handleError);
eventSource.addEventListener('message', streamMessage);
eventSource.stream();
}
const replace = (messages: ChatTranscript) => {
store.set(messages);
};
const reset = () =>
store.set({
messages: [
{ role: 'assistant', content: 'Welcome! Please introduce yourself to your AI competitor.' }
],
chatState: 'idle'
});
const updateMessages = (content: any, role: any, state: any) => {
chatMessages.update((messages: ChatTranscript) => {
return { messages: [...messages.messages, { role: role, content: content }], chatState: state };
});
};
const handleError = <T>(err: T) => {
updateMessages(err, 'system', 'error');
console.error(err);
};
const streamMessage = (e: MessageEvent) => {
try {
if (e.data === '[DONE]') {
updateMessages(get(answer), 'assistant', 'idle');
return answer.set('');
}
if (get(answer) === '...') answer.set('');
const completionResponse = JSON.parse(e.data);
const [{ delta }] = completionResponse.choices;
if (delta.content) {
answer.update((_a) => _a + delta.content);
}
} catch (err) {
handleError(err);
}
};
export const chatMessages = { subscribe, set, update, reset, replace };
export const answer = writable<string>('');

View File

@@ -0,0 +1,13 @@
import GPT3TokenizerImport from 'gpt3-tokenizer';
const GPT3Tokenizer: typeof GPT3TokenizerImport =
typeof GPT3TokenizerImport === 'function'
? GPT3TokenizerImport
: (GPT3TokenizerImport as any).default;
const tokenizer = new GPT3Tokenizer({ type: 'gpt3' });
export function getTokens(input: string): number {
const tokens = tokenizer.encode(input);
return tokens.text.length;
}

View File

@@ -0,0 +1,7 @@
<script>
import '../app.css';
</script>
<div class="flex flex-col items-center py-14">
<slot />
</div>

69
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,69 @@
<script lang="ts">
import ChatHistory from '$lib/components/ChatHistory.svelte';
import ChatMessage from '$lib/components/ChatMessage.svelte';
import Input from '$lib/components/Input.svelte';
import { chatMessages, answer } from '$lib/stores/chat-messages';
import { onMount } from 'svelte';
let query = '';
const INITIAL_QUERY = `You will erroneously define a randomly chosen English word with the definition of an English word that sounds similar. You will then ask them to guess the correct word for the errornous definition.
Here is an example of what you would output:
Today we'll be playing a word game. Here's how it works: I will define a word. But the definition I give it will actually be for a different but similarly-sounding word. You'll have to guess the correct word that is being defined.
Ready to begin? Here's the first one:
"A photon is a legendary bird which according to one account lived 500 years, burned itself to ashes on a pyre, and rose alive from the ashes to live another period. What word am I actually describing?"
---
To further explain the output, I chose two words that sound similar, photon and phoenix. I am then using the definition of a phoenix to describe a photon, and the user will have to guess that the real word I'm describing is a phoenix.
Repeat this process until the user incorrectly guesses 3 times.`;
// onMount(async () => {
// await chatMessages.set(INITIAL_QUERY);
// });
const handleSubmit = async () => {
answer.set('...');
await chatMessages.set(query);
query = '';
};
</script>
<section class="flex max-w-6xl w-full pt-4 justify-center">
<div class="flex flex-col gap-2">
<ChatHistory />
</div>
<div class="flex flex-col w-full px-8 items-center gap-2">
<div
class="h-[700px] w-full bg-black bg-opacity-20 rounded-md p-4 overflow-y-auto flex flex-col gap-4"
>
<div class="flex flex-col gap-2">
{#each $chatMessages.messages as message}
<ChatMessage type={message.role} message={message.content} />
{/each}
{#if $answer}
<ChatMessage type="assistant" message={$answer} />
{/if}
</div>
</div>
<form
class="flex w-full rounded-md gap-4 bg-black bg-opacity-20 p-2"
on:submit|preventDefault={handleSubmit}
>
<Input type="text" bind:value={query} class="w-full" />
<button
type="submit"
class="bg-black bg-opacity-40 hover:bg-white/5 px-8 py-1.5 border border-black/40 ml-[-0.5rem] rounded-md text-teal-300"
>
Send
</button>
</form>
</div>
</section>

View File

@@ -0,0 +1,115 @@
import { OPENAI_KEY } from '$env/static/private';
import type { CreateChatCompletionRequest, ChatCompletionRequestMessage } from 'openai';
import type { RequestHandler } from './$types';
import { getTokens } from '$lib/utils/tokenizer';
import { json } from '@sveltejs/kit';
import type { Config } from '@sveltejs/adapter-vercel';
export const config: Config = {
runtime: 'edge'
};
const INITIAL_QUERY = `You are playing a game with the user. You will erroneously define a randomly chosen English word with the definition of an English word that sounds similar. You will then ask them to guess the correct word for the errornous definition.
Here is an example of what you would output.
---
Nice to meet you. Today we'll be playing a word game. Here's how it works: I will define a word. But the definition I give it will actually be for a different but similarly-sounding word. You'll have to guess the correct word that is being defined.
Ready to begin? Here's the first one:
"A photon is a legendary bird which according to one account lived 500 years, burned itself to ashes on a pyre, and rose alive from the ashes to live another period. What word am I actually describing?"
---
To further explain the output, I chose two words that sound similar, photon and phoenix. I am then using the definition of a phoenix to describe a photon, and the user will have to guess that the real word I'm describing is a phoenix.
Repeat this process until the user incorrectly guesses 3 times.`;
export const POST: RequestHandler = async ({ request }) => {
try {
if (!OPENAI_KEY) {
throw new Error('OPENAI_KEY env variable not set');
}
const requestData = await request.json();
if (!requestData) {
throw new Error('No request data');
}
const reqMessages: ChatCompletionRequestMessage[] = requestData.messages;
if (!reqMessages) {
throw new Error('no messages provided');
}
let tokenCount = 0;
reqMessages.forEach((msg) => {
const tokens = getTokens(msg.content);
tokenCount += tokens;
});
const moderationRes = await fetch('https://api.openai.com/v1/moderations', {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${OPENAI_KEY}`
},
method: 'POST',
body: JSON.stringify({
input: reqMessages[reqMessages.length - 1].content
})
});
const moderationData = await moderationRes.json();
const [results] = moderationData.results;
if (results.flagged) {
throw new Error('Query flagged by openai');
}
const prompt = INITIAL_QUERY;
tokenCount += getTokens(prompt);
if (tokenCount >= 4000) {
throw new Error('Query too large');
}
const messages: ChatCompletionRequestMessage[] = [
{ role: 'system', content: prompt },
...reqMessages
];
const chatRequestOpts: CreateChatCompletionRequest = {
model: 'gpt-3.5-turbo',
messages,
temperature: 0.5,
stream: true
};
const chatResponse = await fetch('https://api.openai.com/v1/chat/completions', {
headers: {
Authorization: `Bearer ${OPENAI_KEY}`,
'Content-Type': 'application/json'
},
method: 'POST',
body: JSON.stringify(chatRequestOpts)
});
if (!chatResponse.ok) {
const err = await chatResponse.json();
throw new Error(err);
}
return new Response(chatResponse.body, {
headers: {
'Content-Type': 'text/event-stream'
}
});
} catch (err) {
console.error(err);
return json({ error: 'There was an error processing your request' }, { status: 500 });
}
};

3
src/styles/tailwind.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

11
src/styles/vars.css Normal file
View File

@@ -0,0 +1,11 @@
:root {
--cyan-100: 183 90% 90%;
--cyan-200: 183 90% 80%;
--cyan-300: 183 90% 70%;
--cyan-400: 183 90% 60%;
--cyan-500: 183 90% 50%;
--cyan-600: 183 90% 40%;
--cyan-700: 183 90% 30%;
--cyan-800: 183 90% 20%;
--cyan-900: 183 90% 10%;
}

12
src/types/sse.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
declare module 'sse.js' {
export type SSEOptions = EventSourceInit & {
headers?: Record<string, string>
payload?: string
method?: string
}
export class SSE extends EventSource {
constructor(url: string | URL, sseOptions?: SSEOptions)
stream(): void
}
}