Initial commit

This commit is contained in:
Niek van der Maas
2023-03-02 22:12:55 +04:00
commit d4e3c40df1
18 changed files with 2036 additions and 0 deletions

102
src/App.svelte Normal file
View File

@@ -0,0 +1,102 @@
<script lang="ts">
import logo from "./assets/logo.svg";
import Chat from "./lib/Chat.svelte";
import {
addChat,
apiKeyStorage,
chatsStorage,
clearChats,
} from "./lib/Storage.svelte";
$: sortedChats = $chatsStorage.sort((a, b) => b.id - a.id);
$: apiKey = $apiKeyStorage;
let activeChatId: number;
</script>
<nav class="navbar" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<img src={logo} alt="ChatGPT-web" width="28" height="28" />
<p class="ml-2 is-size-4 has-text-weight-bold">ChatGPT-web</p>
</a>
</div>
</nav>
<!-- svelte-ignore a11y-missing-attribute a11y-click-events-have-key-events -->
<section class="section">
<div class="container is-fluid">
<div class="columns">
<div class="column is-one-fifth">
<article class="panel is-link">
<p class="panel-heading">Chats</p>
{#if sortedChats.length === 0 || !apiKey}
<a class="panel-block">No chats...</a>
{:else}
{#each sortedChats as chat}
<a class="panel-block" on:click={() => (activeChatId = chat.id)}
>Chat {chat.id}</a
>
{/each}
{/if}
</article>
<article class="panel is-link">
<a
class="panel-block {!apiKey ? 'is-disabled' : ''}"
on:click={() => {
activeChatId = addChat();
}}> New chat</a
>
<a
class="panel-block {!apiKey ? 'is-disabled' : ''}"
on:click={() => {
clearChats();
activeChatId = null;
}}>🗑️ Clear chats</a
>
<a
class="panel-block {!apiKey ? 'is-disabled' : ''}"
on:click={() => {
activeChatId = null;
}}>🔙 Back to home</a
>
</article>
</div>
<div class="column">
{#if activeChatId}
<Chat chatId={activeChatId} />
{:else}
<article class="message {!apiKey ? 'is-danger' : 'is-warning'}">
<div class="message-body">
Set your OpenAI API key below:
<input
type="text"
class="input {!apiKey ? 'is-danger' : ''}"
value={apiKey}
on:change={(event) => {
// @ts-ignore
apiKeyStorage.set(event.target.value);
}}
/>
{#if !apiKey}
<p class="help is-danger">
Please enter your OpenAI API key above to use ChatGPT-web
</p>
{/if}
</div>
</article>
<article class="message is-info">
<div class="message-body">
Select an existing chat on the sidebar, or <a
on:click={() => {
activeChatId = addChat();
}}>create a new chat</a
>
</div>
</article>
{/if}
</div>
</div>
</div>
</section>

18
src/app.css Normal file
View File

@@ -0,0 +1,18 @@
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.rotate {
animation: rotating 10s linear infinite;
}
a.is-disabled {
pointer-events: none;
cursor: default;
opacity: 0.5;
}

1
src/assets/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

168
src/lib/Chat.svelte Normal file
View File

@@ -0,0 +1,168 @@
<script lang="ts">
//import { fetchEventSource } from "@microsoft/fetch-event-source";
import {
apiKeyStorage,
chatsStorage,
addMessage,
clearMessages,
} from "./Storage.svelte";
import type { Message } from "./Types.svelte";
import { marked } from "marked";
import { afterUpdate, onMount } from "svelte";
export let chatId: number;
let updating: boolean = false;
let input: HTMLInputElement;
$: chat = $chatsStorage.find((chat) => chat.id === chatId);
const token_price = 0.000002; // $0.002 per 1000 tokens
// Focus the input on mount
onMount(() => input.focus());
// Scroll to the bottom of the chat on update
afterUpdate(() => {
// Scroll to the bottom of the page after any updates to the messages array
window.scrollTo(0, document.body.scrollHeight);
input.focus();
});
const send = async () => {
// Compose the input message
const inputMessage: Message = { role: "user", content: input.value };
addMessage(chatId, inputMessage);
// Clear the input value
input.value = "";
// Show updating bar
updating = true;
// Send API request
/*
await fetchEventSource("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization:
`Bearer ${$apiKeyStorage}`,
"Content-Type": "text/event-stream",
},
body: JSON.stringify({
model: "gpt-3.5-turbo",
messages, // Provide the previous messages as well for context
// temperature: 1
// top_p: 1
// n: 1
stream: false,
// stop: null
max_tokens: 4096,
}),
onmessage(ev) {
console.log(ev);
},
onerror(err) {
throw err;
},
});
*/
const response = await (
await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${$apiKeyStorage}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "gpt-3.5-turbo",
// Remove the usage property from all messages
messages: chat.messages.map((message): Message => {
const { usage, ...rest } = message;
return rest;
}),
// Provide the previous messages as well for context
// temperature: 1
// top_p: 1
// n: 1
//stream: false,
// stop: null
//max_tokens: 4096,
}),
})
).json();
console.log(response);
// Hide updating bar
updating = false;
if (response.error) {
addMessage(chatId, {
role: "system",
content: `Error: ${response.error.message}`,
});
} else {
response.choices.map((choice) => {
choice.message.usage = response.usage;
addMessage(chatId, choice.message);
});
}
};
</script>
<button
class="button is-danger is-pulled-right"
on:click={() => {
clearMessages(chatId);
}}>Clear messages</button
>
<p class="subtitle">Chat {chatId}</p>
{#each chat.messages as message}
{#if message.role === "user"}
<article class="message is-info has-text-right">
<div class="message-body">{@html marked(message.content)}</div>
</article>
{:else if message.role === "system"}
<article class="message is-danger">
<div class="message-body">{@html marked(message.content)}</div>
</article>
{:else}
<article class="message is-success">
<div class="message-body">
{@html marked(message.content)}
{#if message.usage}
<p class="is-size-7">
This message was generated using <span class="has-text-weight-bold"
>{message.usage.total_tokens}</span
>
tokens ~=
<span class="has-text-weight-bold"
>${(message.usage.total_tokens * token_price).toFixed(6)}</span
>
</p>
{/if}
</div>
</article>
{/if}
{/each}
{#if updating}
<progress class="progress is-small is-dark" max="100" />
{/if}
<form class="field has-addons has-addons-right" on:submit|preventDefault={send}>
<p class="control is-expanded">
<input
class="input is-info is-medium is-focused"
type="text"
placeholder="Type your message here..."
bind:this={input}
/>
</p>
<p class="control">
<button class="button is-info is-medium" type="submit">Send</button>
</p>
</form>

49
src/lib/Storage.svelte Normal file
View File

@@ -0,0 +1,49 @@
<script context="module" lang="ts">
import { persisted } from "svelte-local-storage-store";
import { get } from "svelte/store";
import type { Chat, Message } from "./Types.svelte";
export const chatsStorage = persisted("chats", [] as Chat[]);
export const apiKeyStorage = persisted("apiKey", null as string);
export const addChat = (): number => {
const chats = get(chatsStorage);
// Find the max chatId
const chatId =
chats.reduce((maxId, chat) => Math.max(maxId, chat.id), 0) + 1;
// Add a new chat
chats.push({
id: chatId,
messages: [],
});
chatsStorage.set(chats);
return chatId;
};
export const clearChats = () => {
chatsStorage.set([]);
};
export const addMessage = (chatId: number, message: Message) => {
const chats = get(chatsStorage);
const chat = chats.find((chat) => chat.id === chatId);
chat.messages.push(message);
chatsStorage.set(chats);
};
export const clearMessages = (chatId: number) => {
const chats = get(chatsStorage);
const chat = chats.find((chat) => chat.id === chatId);
chat.messages = [];
chatsStorage.set(chats);
};
export const deleteChat = (chatId: number) => {
const chats = get(chatsStorage);
const chatIndex = chats.findIndex((chat) => chat.id === chatId);
chats.splice(chatIndex, 1);
chatsStorage.set(chats);
};
</script>

18
src/lib/Types.svelte Normal file
View File

@@ -0,0 +1,18 @@
<script context="module" lang="ts">
export type Chat = {
id: number;
messages: Message[];
};
export type Message = {
role: "user" | "assistant" | "system";
content: string;
usage?: Usage;
};
export type Usage = {
completion_tokens: number;
prompt_tokens: number;
total_tokens: number;
};
</script>

9
src/main.ts Normal file
View File

@@ -0,0 +1,9 @@
import "./app.css";
import "../node_modules/bulma/";
import App from "./App.svelte";
const app = new App({
target: document.getElementById("app"),
});
export default app;

2
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />