Merge branch 'main' into pr/terryoy/41

This commit is contained in:
Niek van der Maas
2023-03-20 14:34:46 +01:00
23 changed files with 4411 additions and 758 deletions

View File

@@ -1,8 +1,9 @@
<script lang="ts">
import Router, { location } from "svelte-spa-router";
import routes from "./routes";
import Navbar from "./lib/Navbar.svelte";
import Sidebar from "./lib/Sidebar.svelte";
import Home from "./lib/Home.svelte";
import Chat from "./lib/Chat.svelte";
import Footer from "./lib/Footer.svelte";
import { apiKeyStorage, chatsStorage } from "./lib/Storage.svelte";
@@ -11,12 +12,10 @@
$: apiKey = $apiKeyStorage;
// Check if the API key is passed in as a "key" query parameter - if so, save it
const urlParams = new URLSearchParams(window.location.search);
const urlParams: URLSearchParams = new URLSearchParams(window.location.search);
if (urlParams.has("key")) {
apiKeyStorage.set(urlParams.get("key")!);
apiKeyStorage.set(urlParams.get("key") as string);
}
let activeChatId: number;
</script>
<Navbar />
@@ -25,14 +24,12 @@
<div class="container is-fullhd">
<div class="columns">
<div class="column is-one-fifth">
<Sidebar bind:apiKey bind:sortedChats bind:activeChatId />
<Sidebar bind:apiKey bind:sortedChats />
</div>
<div class="column is-four-fifths">
{#if activeChatId}
<Chat bind:chatId={activeChatId} />
{:else}
<Home bind:activeChatId />
{/if}
{#key $location}
<Router {routes} />
{/key}
</div>
</div>
</div>

View File

@@ -83,4 +83,17 @@ $modal-background-background-color-dark: rgba($dark, 0.86) !default; // remove t
@import "/node_modules/bulma-prefers-dark/build/bulma-prefers-dark.sass";
.modal-card-body { // remove this once https: //github.com/jloh/bulma-prefers-dark/pull/90 is merged and released
background-color: $background-dark;
}
/* Support for copy code button */
.code-block>button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
/* Make menu stick on the left side */
.menu {
position: sticky;
top: 1rem;
}

View File

@@ -7,13 +7,23 @@
addMessage,
clearMessages,
} from "./Storage.svelte";
import type { Request, Response, Message, Settings } from "./Types.svelte";
import {
type Request,
type Response,
type Message,
type Settings,
supportedModels,
type ResponseModels,
type SettingsSelect,
} from "./Types.svelte";
import Code from "./Code.svelte";
import { afterUpdate, onMount } from "svelte";
import { replace } from "svelte-spa-router";
import SvelteMarkdown from "svelte-markdown";
export let chatId: number;
export let params = { chatId: undefined };
let chatId: number = parseInt(params.chatId);
let updating: boolean = false;
let input: HTMLTextAreaElement;
@@ -23,40 +33,65 @@
let recording = false;
const settingsMap: Settings[] = [
{
key: "model",
name: "Model",
default: "gpt-3.5-turbo",
options: supportedModels,
type: "select",
},
{
key: "temperature",
name: "Sampling Temperature",
default: 1,
min: 0,
max: 2,
step: 0.1,
type: "number",
},
{
key: "top_p",
name: "Nucleus Sampling",
default: 1,
min: 0,
max: 1,
step: 0.1,
type: "number",
},
{
key: "n",
name: "Number of Messages",
default: 1,
min: 1,
max: 10,
step: 1,
type: "number",
},
{
key: "max_tokens",
name: "Max Tokens",
default: 0,
min: 0,
max: 32768,
step: 1024,
type: "number",
},
{
key: "presence_penalty",
name: "Presence Penalty",
default: 0,
min: -2,
max: 2,
step: 0.2,
type: "number",
},
{
key: "frequency_penalty",
name: "Frequency Penalty",
default: 0,
min: -2,
max: 2,
step: 0.2,
type: "number",
},
];
@@ -65,7 +100,7 @@
const token_price = 0.000002; // $0.002 per 1000 tokens
// Focus the input on mount
onMount(() => {
onMount(async () => {
input.focus();
// Try to detect speech recognition support
@@ -105,8 +140,9 @@
// Marked options
const markedownOptions = {
gfm: true,
breaks: true,
gfm: true, // Use GitHub Flavored Markdown
breaks: true, // Enable line breaks in markdown
mangle: false, // Do not mangle email addresses
};
const sendRequest = async (messages: Message[]): Promise<Response> => {
@@ -135,7 +171,6 @@
let response: Response;
try {
const request: Request = {
model: "gpt-3.5-turbo",
// Submit only the role and content of the messages, provide the previous messages as well for context
messages: messages
.map((message): Message => {
@@ -241,10 +276,11 @@
const deleteChat = () => {
if (confirm("Are you sure you want to delete this chat?")) {
chatsStorage.update((chats) =>
chats.filter((chat) => chat.id !== chatId)
);
chatId = null;
replace("/").then(() => {
chatsStorage.update((chats) =>
chats.filter((chat) => chat.id !== chatId)
);
});
}
};
@@ -266,8 +302,23 @@
chatNameSettings.classList.remove("is-active");
};
const showSettings = () => {
const showSettings = async () => {
settings.classList.add("is-active");
// Load available models from OpenAI
const allModels = (await (
await fetch("https://api.openai.com/v1/models", {
method: "GET",
headers: {
Authorization: `Bearer ${$apiKeyStorage}`,
"Content-Type": "application/json",
},
})
).json()) as ResponseModels;
const filteredModels = supportedModels.filter((model) => allModels.data.find((m) => m.id === model));
// Update the models in the settings
(settingsMap[0] as SettingsSelect).options = filteredModels;
};
const closeSettings = () => {
@@ -472,18 +523,32 @@
{#each settingsMap as setting}
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label" for="settings-temperature"
<label class="label" for="settings-{setting.key}"
>{setting.name}</label
>
</div>
<div class="field-body">
<div class="field">
<input
class="input"
type={setting.type}
id="settings-{setting.key}"
placeholder={String(setting.default)}
/>
{#if setting.type === "number"}
<input
class="input"
inputmode="decimal"
type={setting.type}
id="settings-{setting.key}"
min={setting.min}
max={setting.max}
step={setting.step}
placeholder={String(setting.default)}
/>
{:else if setting.type === "select"}
<div class="select">
<select id="settings-{setting.key}">
{#each setting.options as option}
<option value={option} selected={option == setting.default}>{option}</option>
{/each}
</select>
</div>
{/if}
</div>
</div>
</div>

View File

@@ -7,6 +7,9 @@
// Style depends on system theme
const style = window.matchMedia("(prefers-color-scheme: dark)").matches ? githubDark : github;
// Copy function for the code block
import copy from "copy-to-clipboard";
// Import all supported languages
import {
javascript,
@@ -68,10 +71,38 @@
default:
language = plaintext;
}
// For copying code - reference: https://vyacheslavbasharov.com/blog/adding-click-to-copy-code-markdown-blog
const copyFunction = (event) => {
// Get the button the user clicked on
const clickedElement = event.target as HTMLButtonElement;
// Get the next element
const nextElement = clickedElement.nextElementSibling;
// Modify the appearance of the button
const originalButtonContent = clickedElement.innerHTML;
clickedElement.classList.add("is-success");
clickedElement.innerHTML = "Copied!";
// Retrieve the code in the code block
const codeBlock = (nextElement.querySelector("pre > code") as HTMLPreElement).innerText;
copy(codeBlock);
// Restored the button after copying the text in 1 second.
setTimeout(() => {
clickedElement.innerHTML = originalButtonContent;
clickedElement.classList.remove("is-success");
clickedElement.blur();
}, 1000);
};
</script>
<svelte:head>
{@html style}
</svelte:head>
<Highlight code={text} {language} />
<div class="code-block is-relative">
<button class="button is-light is-outlined is-small p-2" on:click={copyFunction}>Copy</button>
<Highlight code={text} {language} />
</div>

View File

@@ -1,16 +1,14 @@
<script lang="ts">
import { addChat, apiKeyStorage } from "./Storage.svelte";
import { apiKeyStorage } from './Storage.svelte'
$: apiKey = $apiKeyStorage;
export let activeChatId: number;
$: apiKey = $apiKeyStorage
</script>
<article class="message">
<div class="message-body">
<strong><a href="https://github.com/Niek/chatgpt-web">ChatGPT-web</a></strong>
is a simple one-page web interface to the OpenAI ChatGPT API. To use it, you need to register for
<a href="https://platform.openai.com/account/api-key" target="_blank" rel="noreferrer">an OpenAI API key</a>
<a href="https://platform.openai.com/account/api-keys" target="_blank" rel="noreferrer">an OpenAI API key</a>
first. OpenAI bills per token (usage-based), which means it is a lot cheaper than
<a href="https://openai.com/blog/chatgpt-plus" target="_blank" rel="noreferrer">ChatGPT Plus</a>, unless you use
more than 10 million tokens per month. All messages are stored in your browser's local storage, so everything is
@@ -24,7 +22,7 @@
<form
class="field has-addons has-addons-right"
on:submit|preventDefault={(event) => {
apiKeyStorage.set(event.target[0].value);
apiKeyStorage.set(event.target[0].value)
}}
>
<p class="control is-expanded">
@@ -44,7 +42,7 @@
{#if !apiKey}
<p class="help is-danger">
Please enter your <a href="https://platform.openai.com/account/api-key">OpenAI API key</a> above to use ChatGPT-web.
Please enter your <a href="https://platform.openai.com/account/api-keys">OpenAI API key</a> above to use ChatGPT-web.
It is required to use ChatGPT-web.
</p>
{/if}
@@ -54,12 +52,7 @@
<article class="message is-info">
<div class="message-body">
Select an existing chat on the sidebar, or
<a
href={"#"}
on:click|preventDefault={() => {
activeChatId = addChat();
}}>create a new chat</a
>
<a href={'#/chat/new'}>create a new chat</a>
</div>
</article>
{/if}

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import logo from "../assets/logo.svg";
import logo from '../assets/logo.svg'
</script>
<nav class="navbar" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href={"#"} on:click|preventDefault={() => (window.location = window.location)}>
<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>

8
src/lib/NewChat.svelte Normal file
View File

@@ -0,0 +1,8 @@
<script lang="ts">
import { addChat } from './Storage.svelte'
import { replace } from 'svelte-spa-router'
// Create the new chat instance then redirect to it
const chatId = addChat()
replace(`/chat/${chatId}`)
</script>

View File

@@ -1,11 +1,14 @@
<script lang="ts">
import { addChat, clearChats } from "./Storage.svelte";
import { params, replace } from "svelte-spa-router";
import { clearChats } from "./Storage.svelte";
import { exportAsMarkdown } from "./Export.svelte";
import type { Chat } from "./Types.svelte";
export let activeChatId: number;
export let sortedChats: Chat[];
export let apiKey: string;
$: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined;
</script>
<aside class="menu">
@@ -18,11 +21,8 @@
<ul>
{#each sortedChats as chat}
<li>
<a
href={"#"}
class:is-disabled={!apiKey}
class:is-active={activeChatId === chat.id}
on:click|preventDefault={() => (activeChatId = chat.id)}>{chat.name || `Chat ${chat.id}`}</a
<a href={`#/chat/${chat.id}`} class:is-disabled={!apiKey} class:is-active={activeChatId === chat.id}
>{chat.name || `Chat ${chat.id}`}</a
>
</li>
{/each}
@@ -33,41 +33,31 @@
<p class="menu-label">Actions</p>
<ul class="menu-list">
<li>
<a
href={"#"}
class="panel-block"
class:is-disabled={!apiKey}
class:is-active={!activeChatId}
on:click|preventDefault={() => {
activeChatId = null;
}}><span class="greyscale mr-2">🔑</span> API key</a
<a href={"#/"} class="panel-block" class:is-disabled={!apiKey} class:is-active={!activeChatId}
><span class="greyscale mr-2">🔑</span> API key</a
>
</li>
<li>
<a href={"#/chat/new"} class="panel-block" class:is-disabled={!apiKey}
><span class="greyscale mr-2"></span> New chat</a
>
</li>
<li>
<a
href={"#"}
href={"#/"}
class="panel-block"
class:is-disabled={!apiKey}
on:click|preventDefault={() => {
activeChatId = addChat();
}}><span class="greyscale mr-2"></span> New chat</a
>
</li>
<li>
<a
href={"#"}
class="panel-block"
class:is-disabled={!apiKey}
on:click|preventDefault={() => {
clearChats();
activeChatId = null;
on:click={() => {
replace("#/").then(() => {
clearChats();
});
}}><span class="greyscale mr-2">🗑️</span> Clear chats</a
>
</li>
{#if activeChatId}
<li>
<a
href={"#"}
href={"#/"}
class="panel-block"
class:is-disabled={!apiKey}
on:click|preventDefault={() => {

View File

@@ -18,7 +18,7 @@
};
export type Request = {
model: "gpt-3.5-turbo" | "gpt-3.5-turbo-0301";
model?: Model;
messages: Message[];
temperature?: number;
top_p?: number;
@@ -32,12 +32,35 @@
user?: string;
};
// See: https://platform.openai.com/docs/models/model-endpoint-compatibility
export const supportedModels = [
"gpt-4",
"gpt-4-0314",
"gpt-4-32k",
"gpt-4-32k-0314",
"gpt-3.5-turbo",
"gpt-3.5-turbo-0301",
];
type Model = typeof supportedModels[number];
type SettingsNumber = {
type: "number";
default: number;
min: number;
max: number;
step: number;
};
export type SettingsSelect = {
type: "select";
default: Model;
options: Model[];
};
export type Settings = {
key: string;
name: string;
default: number;
type: "number";
};
} & (SettingsNumber | SettingsSelect);
type ResponseOK = {
id: string;
@@ -61,4 +84,11 @@
};
export type Response = ResponseOK & ResponseError;
export type ResponseModels = {
object: "list";
data: {
id: string;
}[];
};
</script>

View File

@@ -1,11 +1,11 @@
// This can be false if you're using a fallback (i.e. SPA mode)
export const prerender = false;
import './app.scss'
import App from './App.svelte'
import "./app.scss";
import App from "./App.svelte";
export const prerender = false
const app = new App({
target: document.getElementById("app"),
});
target: document.getElementById('app') as HTMLElement
})
export default app;
export default app

12
src/routes.ts Normal file
View File

@@ -0,0 +1,12 @@
import Home from './lib/Home.svelte'
import Chat from './lib/Chat.svelte'
import NewChat from './lib/NewChat.svelte'
export default {
'/': Home,
'/chat/new': NewChat,
'/chat/:chatId': Chat,
'*': Home
}