Merge branch 'main' into pr/terryoy/41

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

View File

@ -29,7 +29,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/8343178?v=4", "avatar_url": "https://avatars.githubusercontent.com/u/8343178?v=4",
"profile": "https://danb.me", "profile": "https://danb.me",
"contributions": [ "contributions": [
"ideas" "ideas",
"code"
] ]
}, },
{ {
@ -68,6 +69,24 @@
"contributions": [ "contributions": [
"ideas" "ideas"
] ]
},
{
"login": "fuegovic",
"name": "fuegovic",
"avatar_url": "https://avatars.githubusercontent.com/u/32828263?v=4",
"profile": "https://github.com/fuegovic",
"contributions": [
"ideas"
]
},
{
"login": "Sixzeroo",
"name": "Sixzeroo",
"avatar_url": "https://avatars.githubusercontent.com/u/20949383?v=4",
"profile": "https://www.liuin.cn",
"contributions": [
"code"
]
} }
], ],
"files": [ "files": [

23
.eslintrc.cjs Normal file
View File

@ -0,0 +1,23 @@
module.exports = {
extends: 'standard-with-typescript',
parser: '@typescript-eslint/parser',
parserOptions: { // add these parser options
project: ['./tsconfig.json']
},
plugins: [
'svelte3',
'@typescript-eslint'
],
overrides: [
{
files: [
'**/*.svelte'
],
processor: 'svelte3/svelte3'
}
],
settings: {
'svelte3/typescript': true
},
ignorePatterns: ['node_modules/*', 'dist/*', 'vite-env.d.ts']
}

21
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,21 @@
name: Linting
# Run on PRs only
on:
pull_request:
types: [opened, reopened]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Lint
run: npm ci && npm run lint

View File

@ -23,7 +23,7 @@ jobs:
node-version: '18' node-version: '18'
- name: Build - name: Build
run: npm ci && npm run build:github run: npm ci && npm run lint && npm run build:github
- name: Deploy - name: Deploy
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v3

8
Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM node:18-alpine
ADD . /work
WORKDIR /work
RUN npm ci
CMD ["npm", "run", "dev:public"]

View File

@ -21,6 +21,12 @@ npm ci
npm run dev # or: npm run build npm run dev # or: npm run build
``` ```
## Use with Docker compose
```bash
docker compose up -d
```
## Contributors ## Contributors
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
@ -31,12 +37,16 @@ npm run dev # or: npm run build
<tr> <tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Michael-Tanzer"><img src="https://avatars.githubusercontent.com/u/23483071?v=4?s=100" width="100px;" alt="Michael Tanzer"/><br /><sub><b>Michael Tanzer</b></sub></a><br /><a href="#ideas-Michael-Tanzer" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/Niek/chatgpt-web/commits?author=Michael-Tanzer" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Michael-Tanzer"><img src="https://avatars.githubusercontent.com/u/23483071?v=4?s=100" width="100px;" alt="Michael Tanzer"/><br /><sub><b>Michael Tanzer</b></sub></a><br /><a href="#ideas-Michael-Tanzer" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/Niek/chatgpt-web/commits?author=Michael-Tanzer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/petergeneric"><img src="https://avatars.githubusercontent.com/u/870655?v=4?s=100" width="100px;" alt="Peter"/><br /><sub><b>Peter</b></sub></a><br /><a href="#ideas-petergeneric" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/petergeneric"><img src="https://avatars.githubusercontent.com/u/870655?v=4?s=100" width="100px;" alt="Peter"/><br /><sub><b>Peter</b></sub></a><br /><a href="#ideas-petergeneric" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://danb.me"><img src="https://avatars.githubusercontent.com/u/8343178?v=4?s=100" width="100px;" alt="Dan Brown"/><br /><sub><b>Dan Brown</b></sub></a><br /><a href="#ideas-ssddanbrown" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://danb.me"><img src="https://avatars.githubusercontent.com/u/8343178?v=4?s=100" width="100px;" alt="Dan Brown"/><br /><sub><b>Dan Brown</b></sub></a><br /><a href="#ideas-ssddanbrown" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/Niek/chatgpt-web/commits?author=ssddanbrown" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlemoonstones"><img src="https://avatars.githubusercontent.com/u/32943414?v=4?s=100" width="100px;" alt="littlemoonstones"/><br /><sub><b>littlemoonstones</b></sub></a><br /><a href="https://github.com/Niek/chatgpt-web/commits?author=littlemoonstones" title="Code">💻</a> <a href="#ideas-littlemoonstones" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/littlemoonstones"><img src="https://avatars.githubusercontent.com/u/32943414?v=4?s=100" width="100px;" alt="littlemoonstones"/><br /><sub><b>littlemoonstones</b></sub></a><br /><a href="https://github.com/Niek/chatgpt-web/commits?author=littlemoonstones" title="Code">💻</a> <a href="#ideas-littlemoonstones" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/maxrye1996"><img src="https://avatars.githubusercontent.com/u/28844671?v=4?s=100" width="100px;" alt="maxrye1996"/><br /><sub><b>maxrye1996</b></sub></a><br /><a href="https://github.com/Niek/chatgpt-web/issues?q=author%3Amaxrye1996" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/maxrye1996"><img src="https://avatars.githubusercontent.com/u/28844671?v=4?s=100" width="100px;" alt="maxrye1996"/><br /><sub><b>maxrye1996</b></sub></a><br /><a href="https://github.com/Niek/chatgpt-web/issues?q=author%3Amaxrye1996" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Mikemansour"><img src="https://avatars.githubusercontent.com/u/50986937?v=4?s=100" width="100px;" alt="Mikemansour"/><br /><sub><b>Mikemansour</b></sub></a><br /><a href="#ideas-Mikemansour" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Mikemansour"><img src="https://avatars.githubusercontent.com/u/50986937?v=4?s=100" width="100px;" alt="Mikemansour"/><br /><sub><b>Mikemansour</b></sub></a><br /><a href="#ideas-Mikemansour" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abc91199"><img src="https://avatars.githubusercontent.com/u/16594734?v=4?s=100" width="100px;" alt="abc91199"/><br /><sub><b>abc91199</b></sub></a><br /><a href="#ideas-abc91199" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/abc91199"><img src="https://avatars.githubusercontent.com/u/16594734?v=4?s=100" width="100px;" alt="abc91199"/><br /><sub><b>abc91199</b></sub></a><br /><a href="#ideas-abc91199" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr> </tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fuegovic"><img src="https://avatars.githubusercontent.com/u/32828263?v=4?s=100" width="100px;" alt="fuegovic"/><br /><sub><b>fuegovic</b></sub></a><br /><a href="#ideas-fuegovic" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.liuin.cn"><img src="https://avatars.githubusercontent.com/u/20949383?v=4?s=100" width="100px;" alt="Sixzeroo"/><br /><sub><b>Sixzeroo</b></sub></a><br /><a href="https://github.com/Niek/chatgpt-web/commits?author=Sixzeroo" title="Code">💻</a></td>
</tr>
</tbody> </tbody>
</table> </table>

11
docker-compose.yml Normal file
View File

@ -0,0 +1,11 @@
version: "3"
services:
chatgpt_web:
container_name: chatgpt_web
restart: always
ports:
- 5173:5173
build:
context: "."
dockerfile: Dockerfile

4665
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,11 +5,13 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"dev:public": "vite --host 0.0.0.0",
"build": "vite build", "build": "vite build",
"build:github": "vite build --base=/chatgpt-web/", "build:github": "vite build --base=/chatgpt-web/",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json", "check": "svelte-check --tsconfig ./tsconfig.json",
"tauri": "tauri" "tauri": "tauri",
"lint": "eslint . --fix"
}, },
"devDependencies": { "devDependencies": {
"@fullhuman/postcss-purgecss": "^5.0.0", "@fullhuman/postcss-purgecss": "^5.0.0",
@ -21,15 +23,19 @@
"@types/marked": "^4.0.8", "@types/marked": "^4.0.8",
"bulma": "^0.9.4", "bulma": "^0.9.4",
"bulma-prefers-dark": "^0.1.0-beta.1", "bulma-prefers-dark": "^0.1.0-beta.1",
"copy-to-clipboard": "^3.3.3",
"eslint-config-standard-with-typescript": "^34.0.1",
"eslint-plugin-svelte3": "^4.0.0",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"sass": "^1.58.3", "sass": "^1.59.2",
"svelte": "^3.55.1", "svelte": "^3.55.1",
"svelte-check": "^3.0.4", "svelte-check": "^3.1.2",
"svelte-highlight": "^7.2.0", "svelte-highlight": "^7.2.0",
"svelte-local-storage-store": "^0.4.0", "svelte-local-storage-store": "^0.4.0",
"svelte-markdown": "^0.2.3", "svelte-markdown": "^0.2.3",
"svelte-spa-router": "^3.3.0",
"tslib": "^2.5.0", "tslib": "^2.5.0",
"typescript": "^4.9.3", "typescript": "^5.0.2",
"vite": "^4.1.0" "vite": "^4.1.0"
} }
} }

View File

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

View File

@ -84,3 +84,16 @@ $modal-background-background-color-dark: rgba($dark, 0.86) !default; // remove t
.modal-card-body { // remove this once https: //github.com/jloh/bulma-prefers-dark/pull/90 is merged and released .modal-card-body { // remove this once https: //github.com/jloh/bulma-prefers-dark/pull/90 is merged and released
background-color: $background-dark; 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, addMessage,
clearMessages, clearMessages,
} from "./Storage.svelte"; } 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 Code from "./Code.svelte";
import { afterUpdate, onMount } from "svelte"; import { afterUpdate, onMount } from "svelte";
import { replace } from "svelte-spa-router";
import SvelteMarkdown from "svelte-markdown"; 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 updating: boolean = false;
let input: HTMLTextAreaElement; let input: HTMLTextAreaElement;
@ -23,40 +33,65 @@
let recording = false; let recording = false;
const settingsMap: Settings[] = [ const settingsMap: Settings[] = [
{
key: "model",
name: "Model",
default: "gpt-3.5-turbo",
options: supportedModels,
type: "select",
},
{ {
key: "temperature", key: "temperature",
name: "Sampling Temperature", name: "Sampling Temperature",
default: 1, default: 1,
min: 0,
max: 2,
step: 0.1,
type: "number", type: "number",
}, },
{ {
key: "top_p", key: "top_p",
name: "Nucleus Sampling", name: "Nucleus Sampling",
default: 1, default: 1,
min: 0,
max: 1,
step: 0.1,
type: "number", type: "number",
}, },
{ {
key: "n", key: "n",
name: "Number of Messages", name: "Number of Messages",
default: 1, default: 1,
min: 1,
max: 10,
step: 1,
type: "number", type: "number",
}, },
{ {
key: "max_tokens", key: "max_tokens",
name: "Max Tokens", name: "Max Tokens",
default: 0, default: 0,
min: 0,
max: 32768,
step: 1024,
type: "number", type: "number",
}, },
{ {
key: "presence_penalty", key: "presence_penalty",
name: "Presence Penalty", name: "Presence Penalty",
default: 0, default: 0,
min: -2,
max: 2,
step: 0.2,
type: "number", type: "number",
}, },
{ {
key: "frequency_penalty", key: "frequency_penalty",
name: "Frequency Penalty", name: "Frequency Penalty",
default: 0, default: 0,
min: -2,
max: 2,
step: 0.2,
type: "number", type: "number",
}, },
]; ];
@ -65,7 +100,7 @@
const token_price = 0.000002; // $0.002 per 1000 tokens const token_price = 0.000002; // $0.002 per 1000 tokens
// Focus the input on mount // Focus the input on mount
onMount(() => { onMount(async () => {
input.focus(); input.focus();
// Try to detect speech recognition support // Try to detect speech recognition support
@ -105,8 +140,9 @@
// Marked options // Marked options
const markedownOptions = { const markedownOptions = {
gfm: true, gfm: true, // Use GitHub Flavored Markdown
breaks: true, breaks: true, // Enable line breaks in markdown
mangle: false, // Do not mangle email addresses
}; };
const sendRequest = async (messages: Message[]): Promise<Response> => { const sendRequest = async (messages: Message[]): Promise<Response> => {
@ -135,7 +171,6 @@
let response: Response; let response: Response;
try { try {
const request: Request = { 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 // Submit only the role and content of the messages, provide the previous messages as well for context
messages: messages messages: messages
.map((message): Message => { .map((message): Message => {
@ -241,10 +276,11 @@
const deleteChat = () => { const deleteChat = () => {
if (confirm("Are you sure you want to delete this chat?")) { if (confirm("Are you sure you want to delete this chat?")) {
replace("/").then(() => {
chatsStorage.update((chats) => chatsStorage.update((chats) =>
chats.filter((chat) => chat.id !== chatId) chats.filter((chat) => chat.id !== chatId)
); );
chatId = null; });
} }
}; };
@ -266,8 +302,23 @@
chatNameSettings.classList.remove("is-active"); chatNameSettings.classList.remove("is-active");
}; };
const showSettings = () => { const showSettings = async () => {
settings.classList.add("is-active"); 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 = () => { const closeSettings = () => {
@ -472,18 +523,32 @@
{#each settingsMap as setting} {#each settingsMap as setting}
<div class="field is-horizontal"> <div class="field is-horizontal">
<div class="field-label is-normal"> <div class="field-label is-normal">
<label class="label" for="settings-temperature" <label class="label" for="settings-{setting.key}"
>{setting.name}</label >{setting.name}</label
> >
</div> </div>
<div class="field-body"> <div class="field-body">
<div class="field"> <div class="field">
{#if setting.type === "number"}
<input <input
class="input" class="input"
inputmode="decimal"
type={setting.type} type={setting.type}
id="settings-{setting.key}" id="settings-{setting.key}"
min={setting.min}
max={setting.max}
step={setting.step}
placeholder={String(setting.default)} 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> </div>
</div> </div>

View File

@ -7,6 +7,9 @@
// Style depends on system theme // Style depends on system theme
const style = window.matchMedia("(prefers-color-scheme: dark)").matches ? githubDark : github; 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 all supported languages
import { import {
javascript, javascript,
@ -68,10 +71,38 @@
default: default:
language = plaintext; 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> </script>
<svelte:head> <svelte:head>
{@html style} {@html style}
</svelte:head> </svelte:head>
<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} /> <Highlight code={text} {language} />
</div>

View File

@ -1,16 +1,14 @@
<script lang="ts"> <script lang="ts">
import { addChat, apiKeyStorage } from "./Storage.svelte"; import { apiKeyStorage } from './Storage.svelte'
$: apiKey = $apiKeyStorage; $: apiKey = $apiKeyStorage
export let activeChatId: number;
</script> </script>
<article class="message"> <article class="message">
<div class="message-body"> <div class="message-body">
<strong><a href="https://github.com/Niek/chatgpt-web">ChatGPT-web</a></strong> <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 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 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 <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 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 <form
class="field has-addons has-addons-right" class="field has-addons has-addons-right"
on:submit|preventDefault={(event) => { on:submit|preventDefault={(event) => {
apiKeyStorage.set(event.target[0].value); apiKeyStorage.set(event.target[0].value)
}} }}
> >
<p class="control is-expanded"> <p class="control is-expanded">
@ -44,7 +42,7 @@
{#if !apiKey} {#if !apiKey}
<p class="help is-danger"> <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. It is required to use ChatGPT-web.
</p> </p>
{/if} {/if}
@ -54,12 +52,7 @@
<article class="message is-info"> <article class="message is-info">
<div class="message-body"> <div class="message-body">
Select an existing chat on the sidebar, or Select an existing chat on the sidebar, or
<a <a href={'#/chat/new'}>create a new chat</a>
href={"#"}
on:click|preventDefault={() => {
activeChatId = addChat();
}}>create a new chat</a
>
</div> </div>
</article> </article>
{/if} {/if}

View File

@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import logo from "../assets/logo.svg"; import logo from '../assets/logo.svg'
</script> </script>
<nav class="navbar" aria-label="main navigation"> <nav class="navbar" aria-label="main navigation">
<div class="navbar-brand"> <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" /> <img src={logo} alt="ChatGPT-web" width="28" height="28" />
<p class="ml-2 is-size-4 has-text-weight-bold">ChatGPT-web</p> <p class="ml-2 is-size-4 has-text-weight-bold">ChatGPT-web</p>
</a> </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"> <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 { exportAsMarkdown } from "./Export.svelte";
import type { Chat } from "./Types.svelte"; import type { Chat } from "./Types.svelte";
export let activeChatId: number;
export let sortedChats: Chat[]; export let sortedChats: Chat[];
export let apiKey: string; export let apiKey: string;
$: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined;
</script> </script>
<aside class="menu"> <aside class="menu">
@ -18,11 +21,8 @@
<ul> <ul>
{#each sortedChats as chat} {#each sortedChats as chat}
<li> <li>
<a <a href={`#/chat/${chat.id}`} class:is-disabled={!apiKey} class:is-active={activeChatId === chat.id}
href={"#"} >{chat.name || `Chat ${chat.id}`}</a
class:is-disabled={!apiKey}
class:is-active={activeChatId === chat.id}
on:click|preventDefault={() => (activeChatId = chat.id)}>{chat.name || `Chat ${chat.id}`}</a
> >
</li> </li>
{/each} {/each}
@ -33,41 +33,31 @@
<p class="menu-label">Actions</p> <p class="menu-label">Actions</p>
<ul class="menu-list"> <ul class="menu-list">
<li> <li>
<a <a href={"#/"} class="panel-block" class:is-disabled={!apiKey} class:is-active={!activeChatId}
href={"#"} ><span class="greyscale mr-2">🔑</span> API key</a
class="panel-block" >
class:is-disabled={!apiKey} </li>
class:is-active={!activeChatId} <li>
on:click|preventDefault={() => { <a href={"#/chat/new"} class="panel-block" class:is-disabled={!apiKey}
activeChatId = null; ><span class="greyscale mr-2"></span> New chat</a
}}><span class="greyscale mr-2">🔑</span> API key</a
> >
</li> </li>
<li> <li>
<a <a
href={"#"} href={"#/"}
class="panel-block" class="panel-block"
class:is-disabled={!apiKey} class:is-disabled={!apiKey}
on:click|preventDefault={() => { on:click={() => {
activeChatId = addChat(); replace("#/").then(() => {
}}><span class="greyscale mr-2"></span> New chat</a
>
</li>
<li>
<a
href={"#"}
class="panel-block"
class:is-disabled={!apiKey}
on:click|preventDefault={() => {
clearChats(); clearChats();
activeChatId = null; });
}}><span class="greyscale mr-2">🗑️</span> Clear chats</a }}><span class="greyscale mr-2">🗑️</span> Clear chats</a
> >
</li> </li>
{#if activeChatId} {#if activeChatId}
<li> <li>
<a <a
href={"#"} href={"#/"}
class="panel-block" class="panel-block"
class:is-disabled={!apiKey} class:is-disabled={!apiKey}
on:click|preventDefault={() => { on:click|preventDefault={() => {

View File

@ -18,7 +18,7 @@
}; };
export type Request = { export type Request = {
model: "gpt-3.5-turbo" | "gpt-3.5-turbo-0301"; model?: Model;
messages: Message[]; messages: Message[];
temperature?: number; temperature?: number;
top_p?: number; top_p?: number;
@ -32,12 +32,35 @@
user?: string; 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 = { export type Settings = {
key: string; key: string;
name: string; name: string;
default: number; } & (SettingsNumber | SettingsSelect);
type: "number";
};
type ResponseOK = { type ResponseOK = {
id: string; id: string;
@ -61,4 +84,11 @@
}; };
export type Response = ResponseOK & ResponseError; export type Response = ResponseOK & ResponseError;
export type ResponseModels = {
object: "list";
data: {
id: string;
}[];
};
</script> </script>

View File

@ -1,11 +1,11 @@
// This can be false if you're using a fallback (i.e. SPA mode) // 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"; export const prerender = false
import App from "./App.svelte";
const app = new App({ 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
}

View File

@ -1,5 +1,5 @@
import adapter from "@sveltejs/adapter-static"; // This was changed from adapter-auto import adapter from '@sveltejs/adapter-static' // This was changed from adapter-auto
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
@ -8,6 +8,6 @@ export default {
// for more information about preprocessors // for more information about preprocessors
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
adapter: adapter(), adapter: adapter()
}, }
}; }

View File

@ -5,6 +5,7 @@
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "ESNext", "module": "ESNext",
"resolveJsonModule": true, "resolveJsonModule": true,
"strictNullChecks": true,
/** /**
* Typecheck JS in `.svelte` and `.js` files by default. * Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS. * Disable checkJs if you'd like to use dynamic types in JS.
@ -15,6 +16,6 @@
"checkJs": true, "checkJs": true,
"isolatedModules": true "isolatedModules": true
}, },
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "vite.config.ts", "svelte.config.js"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@ -1,28 +1,28 @@
import { defineConfig } from "vite"; import { defineConfig } from 'vite'
import { svelte } from "@sveltejs/vite-plugin-svelte"; import { svelte } from '@sveltejs/vite-plugin-svelte'
import purgecss from "@fullhuman/postcss-purgecss"; import purgecss from '@fullhuman/postcss-purgecss'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ command, mode, ssrBuild }) => { export default defineConfig(({ command, mode, ssrBuild }) => {
// Only run PurgeCSS in production builds // Only run PurgeCSS in production builds
if (command === "build") { if (command === 'build') {
return { return {
plugins: [svelte()], plugins: [svelte()],
css: { css: {
postcss: { postcss: {
plugins: [ plugins: [
purgecss({ purgecss({
content: ["./**/*.html", "./**/*.svelte"], content: ['./**/*.html', './**/*.svelte'],
safelist: ["pre", "code"], safelist: ['pre', 'code']
}), })
], ]
}, }
}, }
}; }
} else { } else {
return { return {
plugins: [svelte()], plugins: [svelte()]
};
} }
}); }
})