Initial commit
This commit is contained in:
39
src/app.css
Normal file
39
src/app.css
Normal 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
12
src/app.d.ts
vendored
Normal 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
12
src/app.html
Normal 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
7
src/index.test.ts
Normal 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);
|
||||
});
|
||||
});
|
61
src/lib/components/ChatHistory.svelte
Normal file
61
src/lib/components/ChatHistory.svelte
Normal 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>
|
46
src/lib/components/ChatMessage.svelte
Normal file
46
src/lib/components/ChatMessage.svelte
Normal 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>
|
13
src/lib/components/Icons/Chat.svelte
Normal file
13
src/lib/components/Icons/Chat.svelte
Normal 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 |
13
src/lib/components/Icons/Pencil.svelte
Normal file
13
src/lib/components/Icons/Pencil.svelte
Normal 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 |
13
src/lib/components/Icons/Plus.svelte
Normal file
13
src/lib/components/Icons/Plus.svelte
Normal 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 |
20
src/lib/components/Icons/Trash.svelte
Normal file
20
src/lib/components/Icons/Trash.svelte
Normal 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 |
25
src/lib/components/Input.svelte
Normal file
25
src/lib/components/Input.svelte
Normal 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>
|
53
src/lib/stores/chat-history.ts
Normal file
53
src/lib/stores/chat-history.ts
Normal 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' });
|
||||
};
|
80
src/lib/stores/chat-messages.ts
Normal file
80
src/lib/stores/chat-messages.ts
Normal 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>('');
|
13
src/lib/utils/tokenizer.ts
Normal file
13
src/lib/utils/tokenizer.ts
Normal 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;
|
||||
}
|
7
src/routes/+layout.svelte
Normal file
7
src/routes/+layout.svelte
Normal 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
69
src/routes/+page.svelte
Normal 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>
|
115
src/routes/api/chat/+server.ts
Normal file
115
src/routes/api/chat/+server.ts
Normal 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
3
src/styles/tailwind.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
11
src/styles/vars.css
Normal file
11
src/styles/vars.css
Normal 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
12
src/types/sse.d.ts
vendored
Normal 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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user