Initial commit
This commit is contained in:
commit
d4e3c40df1
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["svelte.svelte-vscode"]
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
# ChatGPT-web
|
||||||
|
|
||||||
|
ChatGPT-web is a simple one-page web interface to the OpenAI ChatGPT model. To use it, you need to register for an API key at https://platform.openai.com/account/api-keys first. All messages are stored in your browser's local storage, so you can close the tab and come back later to continue the conversation.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
To run the development server, run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci
|
||||||
|
npm run dev # or: npm run publish
|
||||||
|
```
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/src/assets/logo.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ChatGTP-web</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "chatgpt-web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^2.0.2",
|
||||||
|
"@tsconfig/svelte": "^3.0.0",
|
||||||
|
"@types/marked": "^4.0.8",
|
||||||
|
"bulma": "^0.9.4",
|
||||||
|
"marked": "^4.2.12",
|
||||||
|
"sass": "^1.58.3",
|
||||||
|
"svelte": "^3.55.1",
|
||||||
|
"svelte-check": "^2.10.3",
|
||||||
|
"svelte-local-storage-store": "^0.4.0",
|
||||||
|
"tslib": "^2.5.0",
|
||||||
|
"typescript": "^4.9.3",
|
||||||
|
"vite": "^4.1.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.9 KiB |
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
||||||
|
/// <reference types="svelte" />
|
||||||
|
/// <reference types="vite/client" />
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
/**
|
||||||
|
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||||
|
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||||
|
* Note that setting allowJs false does not prevent the use
|
||||||
|
* of JS in `.svelte` files.
|
||||||
|
*/
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node"
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [svelte()],
|
||||||
|
})
|
Loading…
Reference in New Issue