mirror of
https://github.com/morgan9e/chatgpt-web
synced 2026-04-14 00:14:04 +09:00
Initial commit
This commit is contained in:
102
src/App.svelte
Normal file
102
src/App.svelte
Normal 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
18
src/app.css
Normal 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
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
168
src/lib/Chat.svelte
Normal 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
49
src/lib/Storage.svelte
Normal 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
18
src/lib/Types.svelte
Normal 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
9
src/main.ts
Normal 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
2
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user