Merge branch 'main' into pr/terryoy/41

This commit is contained in:
Niek van der Maas 2023-03-20 15:51:32 +01:00
commit 0571a79886
9 changed files with 362 additions and 344 deletions

View File

@ -1,13 +1,25 @@
module.exports = {
extends: 'standard-with-typescript',
parser: '@typescript-eslint/parser',
parserOptions: { // add these parser options
project: ['./tsconfig.json']
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
extraFileExtensions: ['.svelte']
},
extends: ['standard-with-typescript'],
plugins: [
'svelte3',
'@typescript-eslint'
],
// Disable these rules: import/first, import/no-duplicates, import/no-mutable-exports, import/no-unresolved, import/prefer-default-export
// Reference: https://github.com/sveltejs/eslint-plugin-svelte3/blob/master/OTHER_PLUGINS.md#eslint-plugin-import
rules: {
'import/first': 'off',
'import/no-duplicates': 'off',
'import/no-mutable-exports': 'off',
'import/no-unresolved': 'off',
'import/prefer-default-export': 'off',
'no-multiple-empty-lines': ['error', { max: 2, maxBOF: 2, maxEOF: 0 }] // See: https://github.com/sveltejs/eslint-plugin-svelte3/issues/41
},
overrides: [
{
files: [

View File

@ -1,20 +1,20 @@
<script lang="ts">
import Router, { location } from "svelte-spa-router";
import routes from "./routes";
import Router, { location } from 'svelte-spa-router'
import routes from './routes'
import Navbar from "./lib/Navbar.svelte";
import Sidebar from "./lib/Sidebar.svelte";
import Footer from "./lib/Footer.svelte";
import Navbar from './lib/Navbar.svelte'
import Sidebar from './lib/Sidebar.svelte'
import Footer from './lib/Footer.svelte'
import { apiKeyStorage, chatsStorage } from "./lib/Storage.svelte";
import { apiKeyStorage, chatsStorage } from './lib/Storage.svelte'
$: sortedChats = $chatsStorage.sort((a, b) => b.id - a.id);
$: apiKey = $apiKeyStorage;
$: sortedChats = $chatsStorage.sort((a, b) => b.id - a.id)
$: apiKey = $apiKeyStorage
// Check if the API key is passed in as a "key" query parameter - if so, save it
const urlParams: URLSearchParams = new URLSearchParams(window.location.search);
if (urlParams.has("key")) {
apiKeyStorage.set(urlParams.get("key") as string);
const urlParams: URLSearchParams = new URLSearchParams(window.location.search)
if (urlParams.has('key')) {
apiKeyStorage.set(urlParams.get('key') as string)
}
</script>

View File

@ -1,12 +1,12 @@
<script lang="ts">
//import { fetchEventSource } from "@microsoft/fetch-event-source";
// import { fetchEventSource } from "@microsoft/fetch-event-source";
import {
apiKeyStorage,
chatsStorage,
addMessage,
clearMessages,
} from "./Storage.svelte";
clearMessages
} from './Storage.svelte'
import {
type Request,
type Response,
@ -15,135 +15,136 @@
supportedModels,
type ResponseModels,
type SettingsSelect,
} from "./Types.svelte";
import Code from "./Code.svelte";
type Chat
} from './Types.svelte'
import Code from './Code.svelte'
import { afterUpdate, onMount } from "svelte";
import { replace } from "svelte-spa-router";
import SvelteMarkdown from "svelte-markdown";
import { afterUpdate, onMount } from 'svelte'
import { replace } from 'svelte-spa-router'
import SvelteMarkdown from 'svelte-markdown'
export let params = { chatId: undefined };
let chatId: number = parseInt(params.chatId);
let updating: boolean = false;
export let params = { chatId: '' }
const chatId: number = parseInt(params.chatId)
let updating: boolean = false
let input: HTMLTextAreaElement;
let settings: HTMLDivElement;
let chatNameSettings: HTMLDivElement;
let recognition: any = null;
let recording = false;
let input: HTMLTextAreaElement
let settings: HTMLDivElement
let chatNameSettings: HTMLDivElement
let recognition: any = null
let recording = false
const settingsMap: Settings[] = [
{
key: "model",
name: "Model",
default: "gpt-3.5-turbo",
key: 'model',
name: 'Model',
default: 'gpt-3.5-turbo',
options: supportedModels,
type: "select",
type: 'select'
},
{
key: "temperature",
name: "Sampling Temperature",
key: 'temperature',
name: 'Sampling Temperature',
default: 1,
min: 0,
max: 2,
step: 0.1,
type: "number",
type: 'number'
},
{
key: "top_p",
name: "Nucleus Sampling",
key: 'top_p',
name: 'Nucleus Sampling',
default: 1,
min: 0,
max: 1,
step: 0.1,
type: "number",
type: 'number'
},
{
key: "n",
name: "Number of Messages",
key: 'n',
name: 'Number of Messages',
default: 1,
min: 1,
max: 10,
step: 1,
type: "number",
type: 'number'
},
{
key: "max_tokens",
name: "Max Tokens",
key: 'max_tokens',
name: 'Max Tokens',
default: 0,
min: 0,
max: 32768,
step: 1024,
type: "number",
type: 'number'
},
{
key: "presence_penalty",
name: "Presence Penalty",
key: 'presence_penalty',
name: 'Presence Penalty',
default: 0,
min: -2,
max: 2,
step: 0.2,
type: "number",
type: 'number'
},
{
key: "frequency_penalty",
name: "Frequency Penalty",
key: 'frequency_penalty',
name: 'Frequency Penalty',
default: 0,
min: -2,
max: 2,
step: 0.2,
type: "number",
},
];
type: 'number'
}
]
$: chat = $chatsStorage.find((chat) => chat.id === chatId);
const token_price = 0.000002; // $0.002 per 1000 tokens
$: chat = $chatsStorage.find((chat) => chat.id === chatId) as Chat
const tokenPrice = 0.000002 // $0.002 per 1000 tokens
// Focus the input on mount
onMount(async () => {
input.focus();
input.focus()
// Try to detect speech recognition support
if ("SpeechRecognition" in window) {
if ('SpeechRecognition' in window) {
// @ts-ignore
recognition = new SpeechRecognition();
} else if ("webkitSpeechRecognition" in window) {
recognition = new window.SpeechRecognition()
} else if ('webkitSpeechRecognition' in window) {
// @ts-ignore
recognition = new webkitSpeechRecognition();
recognition = new window.webkitSpeechRecognition() // eslint-disable-line new-cap
}
if (recognition) {
recognition.interimResults = false;
recognition.interimResults = false
recognition.onstart = () => {
recording = true;
};
recording = true
}
recognition.onresult = (event) => {
// Stop speech recognition, submit the form and remove the pulse
const last = event.results.length - 1;
const text = event.results[last][0].transcript;
input.value = text;
recognition.stop();
recording = false;
submitForm(true);
};
} else {
console.log("Speech recognition not supported");
const last = event.results.length - 1
const text = event.results[last][0].transcript
input.value = text
recognition.stop()
recording = false
submitForm(true)
}
});
} else {
console.log('Speech recognition not supported')
}
})
// 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();
});
window.scrollTo(0, document.body.scrollHeight)
input.focus()
})
// Marked options
const markedownOptions = {
gfm: true, // Use GitHub Flavored Markdown
breaks: true, // Enable line breaks in markdown
mangle: false, // Do not mangle email addresses
};
mangle: false // Do not mangle email addresses
}
const sendRequest = async (messages: Message[]): Promise<Response> => {
// Send API request
@ -166,19 +167,19 @@
});
*/
// Show updating bar
updating = true;
updating = true
let response: Response;
let response: Response
try {
const request: Request = {
// Submit only the role and content of the messages, provide the previous messages as well for context
messages: messages
.map((message): Message => {
const { role, content } = message;
return { role, content };
const { role, content } = message
return { role, content }
})
// Skip error messages
.filter((message) => message.role !== "error"),
.filter((message) => message.role !== 'error'),
// Provide the settings by mapping the settingsMap to key/value pairs
...settingsMap.reduce((acc, setting) => {
@ -186,163 +187,163 @@
settings.querySelector(
`#settings-${setting.key}`
) as HTMLInputElement
).value;
).value
if (value) {
acc[setting.key] =
setting.type === "number" ? parseFloat(value) : value;
setting.type === 'number' ? parseFloat(value) : value
}
return acc
}, {})
}
return acc;
}, {}),
};
response = await (
await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${$apiKeyStorage}`,
"Content-Type": "application/json",
'Content-Type': 'application/json'
},
body: JSON.stringify(request),
body: JSON.stringify(request)
})
).json();
).json()
} catch (e) {
response = { error: { message: e.message } } as Response;
response = { error: { message: e.message } } as Response
}
// Hide updating bar
updating = false;
updating = false
return response;
};
return response
}
const submitForm = async (recorded: boolean = false): Promise<void> => {
// Compose the input message
const inputMessage: Message = { role: "user", content: input.value };
addMessage(chatId, inputMessage);
const inputMessage: Message = { role: 'user', content: input.value }
addMessage(chatId, inputMessage)
// Clear the input value
input.value = "";
input.blur();
input.value = ''
input.blur()
// Resize back to single line height
input.style.height = "auto";
input.style.height = 'auto'
const response = await sendRequest(chat.messages);
const response = await sendRequest(chat.messages)
if (response.error) {
addMessage(chatId, {
role: "error",
content: `Error: ${response.error.message}`,
});
role: 'error',
content: `Error: ${response.error.message}`
})
} else {
response.choices.map((choice) => {
choice.message.usage = response.usage;
response.choices.forEach((choice) => {
choice.message.usage = response.usage
// Remove whitespace around the message that the OpenAI API sometimes returns
choice.message.content = choice.message.content.trim();
addMessage(chatId, choice.message);
choice.message.content = choice.message.content.trim()
addMessage(chatId, choice.message)
// Use TTS to read the response, if query was recorded
if (recorded && "SpeechSynthesisUtterance" in window) {
if (recorded && 'SpeechSynthesisUtterance' in window) {
const utterance = new SpeechSynthesisUtterance(
choice.message.content
);
speechSynthesis.speak(utterance);
)
window.speechSynthesis.speak(utterance)
}
})
}
});
}
};
const suggestName = async (): Promise<void> => {
const suggestMessage: Message = {
role: "user",
content: "Can you give me a 5 word summary of this conversation's topic?",
};
addMessage(chatId, suggestMessage);
role: 'user',
content: "Can you give me a 5 word summary of this conversation's topic?"
}
addMessage(chatId, suggestMessage)
const response = await sendRequest(chat.messages);
const response = await sendRequest(chat.messages)
if (response.error) {
addMessage(chatId, {
role: "error",
content: `Error: ${response.error.message}`,
});
role: 'error',
content: `Error: ${response.error.message}`
})
} else {
response.choices.map((choice) => {
choice.message.usage = response.usage;
addMessage(chatId, choice.message);
chat.name = choice.message.content;
chatsStorage.set($chatsStorage);
});
response.choices.forEach((choice) => {
choice.message.usage = response.usage
addMessage(chatId, choice.message)
chat.name = choice.message.content
chatsStorage.set($chatsStorage)
})
}
}
};
const deleteChat = () => {
if (confirm("Are you sure you want to delete this chat?")) {
replace("/").then(() => {
if (window.confirm('Are you sure you want to delete this chat?')) {
replace('/').then(() => {
chatsStorage.update((chats) =>
chats.filter((chat) => chat.id !== chatId)
);
});
)
})
}
}
};
const showChatNameSettings = () => {
chatNameSettings.classList.add("is-active");
};
chatNameSettings.classList.add('is-active')
}
const saveChatNameSettings = () => {
const newChatName = (
chatNameSettings.querySelector("#settings-chat-name") as HTMLInputElement
).value;
chatNameSettings.querySelector('#settings-chat-name') as HTMLInputElement
).value
// save if changed
if (newChatName && newChatName !== chat.name) {
chat.name = newChatName;
chatsStorage.set($chatsStorage);
chat.name = newChatName
chatsStorage.set($chatsStorage)
}
closeChatNameSettings()
}
closeChatNameSettings();
};
const closeChatNameSettings = () => {
chatNameSettings.classList.remove("is-active");
};
chatNameSettings.classList.remove('is-active')
}
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",
await fetch('https://api.openai.com/v1/models', {
method: 'GET',
headers: {
Authorization: `Bearer ${$apiKeyStorage}`,
"Content-Type": "application/json",
},
'Content-Type': 'application/json'
}
})
).json()) as ResponseModels;
).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;
};
(settingsMap[0] as SettingsSelect).options = filteredModels
}
const closeSettings = () => {
settings.classList.remove("is-active");
};
settings.classList.remove('is-active')
}
const clearSettings = () => {
settingsMap.forEach((setting) => {
const input = settings.querySelector(
`#settings-${setting.key}`
) as HTMLInputElement;
input.value = "";
});
};
) as HTMLInputElement
input.value = ''
})
}
const recordToggle = () => {
// Check if already recording - if so, stop - else start
if (recording) {
recognition?.stop();
recording = false;
recognition?.stop()
recording = false
} else {
recognition?.start();
recognition?.start()
}
}
};
</script>
<nav class="level chat-header">
@ -351,17 +352,17 @@
<p class="subtitle is-5">
{chat.name || `Chat ${chat.id}`}
<a
href={"#"}
href={'#'}
class="greyscale ml-2 is-hidden has-text-weight-bold editbutton"
title="Rename chat"
on:click|preventDefault={() => {
showChatNameSettings();
showChatNameSettings()
}}
>
✏️
</a>
<a
href={"#"}
href={'#'}
class="greyscale ml-2 is-hidden has-text-weight-bold editbutton"
title="Suggest a chat name"
on:click|preventDefault={suggestName}
@ -369,7 +370,7 @@
💡
</a>
<a
href={"#"}
href={'#'}
class="greyscale ml-2 is-hidden editbutton"
title="Delete this chat"
on:click|preventDefault={deleteChat}
@ -385,7 +386,7 @@
<button
class="button is-warning"
on:click={() => {
clearMessages(chatId);
clearMessages(chatId)
}}><span class="greyscale mr-2">🗑️</span> Clear messages</button
>
</p>
@ -393,20 +394,20 @@
</nav>
{#each chat.messages as message}
{#if message.role === "user"}
{#if message.role === 'user'}
<article
class="message is-info user-message"
class:has-text-right={message.content
.split("\n")
.split('\n')
.filter((line) => line.trim()).length === 1}
>
<div class="message-body content">
<a
href={"#"}
href={'#'}
class="greyscale is-pulled-right ml-2 is-hidden editbutton"
on:click={() => {
input.value = message.content;
input.focus();
input.value = message.content
input.focus()
}}
>
✏️
@ -415,19 +416,19 @@
source={message.content}
options={markedownOptions}
renderers={{
code: Code,
code: Code
}}
/>
</div>
</article>
{:else if message.role === "system" || message.role === "error"}
{:else if message.role === 'system' || message.role === 'error'}
<article class="message is-danger">
<div class="message-body content">
<SvelteMarkdown
source={message.content}
options={markedownOptions}
renderers={{
code: Code,
code: Code
}}
/>
</div>
@ -439,7 +440,7 @@
source={message.content}
options={markedownOptions}
renderers={{
code: Code,
code: Code
}}
/>
{#if message.usage}
@ -449,7 +450,7 @@
>
tokens ~=
<span class="has-text-weight-bold"
>${(message.usage.total_tokens * token_price).toFixed(6)}</span
>${(message.usage.total_tokens * tokenPrice).toFixed(6)}</span
>
</p>
{/if}
@ -473,15 +474,15 @@
rows="1"
on:keydown={(e) => {
// Only send if Enter is pressed, not Shift+Enter
if (e.key === "Enter" && !e.shiftKey) {
submitForm();
e.preventDefault();
if (e.key === 'Enter' && !e.shiftKey) {
submitForm()
e.preventDefault()
}
}}
on:input={(e) => {
// Resize the textarea to fit the content - auto is important to reset the height after deleting content
input.style.height = "auto";
input.style.height = input.scrollHeight + "px";
input.style.height = 'auto'
input.style.height = input.scrollHeight + 'px'
}}
bind:this={input}
/>
@ -506,8 +507,8 @@
<svelte:window
on:keydown={(event) => {
if (event.key === "Escape") {
closeSettings();
if (event.key === 'Escape') {
closeSettings()
}
}}
/>
@ -529,7 +530,7 @@
</div>
<div class="field-body">
<div class="field">
{#if setting.type === "number"}
{#if setting.type === 'number'}
<input
class="input"
inputmode="decimal"
@ -540,11 +541,11 @@
step={setting.step}
placeholder={String(setting.default)}
/>
{:else if setting.type === "select"}
{: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>
<option value={option} selected={option === setting.default}>{option}</option>
{/each}
</select>
</div>

View File

@ -1,14 +1,14 @@
<script lang="ts">
import { Highlight } from "svelte-highlight";
import { Highlight } from 'svelte-highlight'
// Import both dark and light styles
import { github, githubDark } from "svelte-highlight/styles";
import { github, githubDark } from 'svelte-highlight/styles'
// 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 copy from 'copy-to-clipboard'
// Import all supported languages
import {
@ -22,80 +22,80 @@
shell,
php,
plaintext,
type LanguageType,
} from "svelte-highlight/languages";
type LanguageType
} from 'svelte-highlight/languages'
export const type: "code" = "code";
export const raw: string = "";
export const codeBlockStyle: "indented" | undefined = undefined;
export let lang: string | undefined = undefined;
export let text: string;
export const type: 'code' = 'code'
export const raw: string = ''
export const codeBlockStyle: 'indented' | undefined = undefined
export let lang: string | undefined
export let text: string
// Map lang string to LanguageType
let language: LanguageType<string>;
let language: LanguageType<string>
switch (lang) {
case "js":
case "javascript":
language = javascript;
break;
case "py":
case "python":
language = python;
break;
case "ts":
case "typescript":
language = typescript;
break;
case "rb":
case "ruby":
language = ruby;
break;
case "go":
case "golang":
language = go;
break;
case "java":
language = java;
break;
case "sql":
language = sql;
break;
case "sh":
case "shell":
case "bash":
language = shell;
break;
case "php":
language = php;
break;
case 'js':
case 'javascript':
language = javascript
break
case 'py':
case 'python':
language = python
break
case 'ts':
case 'typescript':
language = typescript
break
case 'rb':
case 'ruby':
language = ruby
break
case 'go':
case 'golang':
language = go
break
case 'java':
language = java
break
case 'sql':
language = sql
break
case 'sh':
case 'shell':
case 'bash':
language = shell
break
case 'php':
language = php
break
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;
const clickedElement = event.target as HTMLButtonElement
// Get the next element
const nextElement = clickedElement.nextElementSibling;
const nextElement = clickedElement.nextElementSibling as HTMLElement
// Modify the appearance of the button
const originalButtonContent = clickedElement.innerHTML;
clickedElement.classList.add("is-success");
clickedElement.innerHTML = "Copied!";
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);
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);
};
clickedElement.innerHTML = originalButtonContent
clickedElement.classList.remove('is-success')
clickedElement.blur()
}, 1000)
}
</script>
<svelte:head>

View File

@ -1,28 +1,29 @@
<script context="module" lang="ts">
import { get } from "svelte/store";
import { chatsStorage } from "./Storage.svelte";
import { get } from 'svelte/store'
import type { Chat } from './Types.svelte'
import { chatsStorage } from './Storage.svelte'
export const exportAsMarkdown = (chatId: number) => {
const chats = get(chatsStorage);
const chat = chats.find((chat) => chat.id === chatId);
const messages = chat.messages;
console.log(chat);
let markdownContent = `# ${chat.name}\n`;
const chats = get(chatsStorage)
const chat = chats.find((chat) => chat.id === chatId) as Chat
const messages = chat.messages
console.log(chat)
let markdownContent = `# ${chat.name}\n`
messages.forEach((message) => {
const author = message.role;
const content = message.content;
const messageMarkdown = `## ${author}\n${content}\n\n`;
const author = message.role
const content = message.content
const messageMarkdown = `## ${author}\n${content}\n\n`
markdownContent += messageMarkdown;
});
const blob = new Blob([markdownContent], { type: "text/markdown" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.download = `${chat.name}.md`;
a.href = url;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
markdownContent += messageMarkdown
})
const blob = new Blob([markdownContent], { type: 'text/markdown' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.download = `${chat.name}.md`
a.href = url
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
</script>

View File

@ -22,7 +22,9 @@
<form
class="field has-addons has-addons-right"
on:submit|preventDefault={(event) => {
if (event.target && event.target[0].value) {
apiKeyStorage.set(event.target[0].value)
}
}}
>
<p class="control is-expanded">

View File

@ -1,21 +1,21 @@
<script lang="ts">
import { params, replace } from "svelte-spa-router";
import { params, replace } from 'svelte-spa-router'
import { clearChats } from "./Storage.svelte";
import { exportAsMarkdown } from "./Export.svelte";
import type { Chat } from "./Types.svelte";
import { clearChats } from './Storage.svelte'
import { exportAsMarkdown } from './Export.svelte'
import type { Chat } from './Types.svelte'
export let sortedChats: Chat[];
export let apiKey: string;
export let sortedChats: Chat[]
export let apiKey: string
$: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined;
$: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined
</script>
<aside class="menu">
<p class="menu-label">Chats</p>
<ul class="menu-list">
{#if sortedChats.length === 0}
<li><a href={"#"}>No chats yet...</a></li>
<li><a href={'#'}>No chats yet...</a></li>
{:else}
<li>
<ul>
@ -33,35 +33,37 @@
<p class="menu-label">Actions</p>
<ul class="menu-list">
<li>
<a href={"#/"} class="panel-block" class:is-disabled={!apiKey} class:is-active={!activeChatId}
<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}
<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={() => {
replace("#/").then(() => {
clearChats();
});
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={() => {
exportAsMarkdown(activeChatId);
if (activeChatId) {
exportAsMarkdown(activeChatId)
}
}}><span class="greyscale mr-2">📥</span> Export chat</a
>
</li>

View File

@ -1,57 +1,57 @@
<script context="module" lang="ts">
import { persisted } from "svelte-local-storage-store";
import { get } from "svelte/store";
import type { Chat, Message } from "./Types.svelte";
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 chatsStorage = persisted('chats', [] as Chat[])
export const apiKeyStorage = persisted('apiKey', '' as string)
export const addChat = (): number => {
const chats = get(chatsStorage);
const chats = get(chatsStorage)
// Find the max chatId
const chatId = chats.reduce((maxId, chat) => Math.max(maxId, chat.id), 0) + 1;
const chatId = chats.reduce((maxId, chat) => Math.max(maxId, chat.id), 0) + 1
// Add a new chat
chats.push({
id: chatId,
name: `Chat ${chatId}`,
messages: [],
});
chatsStorage.set(chats);
return chatId;
};
messages: []
})
chatsStorage.set(chats)
return chatId
}
export const clearChats = () => {
chatsStorage.set([]);
};
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);
};
const chats = get(chatsStorage)
const chat = chats.find((chat) => chat.id === chatId) as Chat
chat.messages.push(message)
chatsStorage.set(chats)
}
export const editMessage = (chatId: number, index: number, newMessage: Message) => {
const chats = get(chatsStorage);
const chat = chats.find((chat) => chat.id === chatId);
chat.messages[index] = newMessage;
chat.messages.splice(index + 1); // remove the rest of the messages
chatsStorage.set(chats);
};
const chats = get(chatsStorage)
const chat = chats.find((chat) => chat.id === chatId) as Chat
chat.messages[index] = newMessage
chat.messages.splice(index + 1) // remove the rest of the messages
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);
};
const chats = get(chatsStorage)
const chat = chats.find((chat) => chat.id === chatId) as Chat
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);
};
const chats = get(chatsStorage)
const chatIndex = chats.findIndex((chat) => chat.id === chatId)
chats.splice(chatIndex, 1)
chatsStorage.set(chats)
}
</script>

View File

@ -1,21 +1,32 @@
<script context="module" lang="ts">
export type Usage = {
completion_tokens: number;
prompt_tokens: number;
total_tokens: number;
};
export type Message = {
role: 'user' | 'assistant' | 'system' | 'error';
content: string;
usage?: Usage;
};
export type Chat = {
id: number;
name: string;
messages: Message[];
};
export type Message = {
role: "user" | "assistant" | "system" | "error";
content: string;
usage?: Usage;
};
export type Usage = {
completion_tokens: number;
prompt_tokens: number;
total_tokens: number;
};
// 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];
export type Request = {
model?: Model;
@ -32,19 +43,8 @@
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";
type: 'number';
default: number;
min: number;
max: number;
@ -52,7 +52,7 @@
};
export type SettingsSelect = {
type: "select";
type: 'select';
default: Model;
options: Model[];
};
@ -86,7 +86,7 @@
export type Response = ResponseOK & ResponseError;
export type ResponseModels = {
object: "list";
object: 'list';
data: {
id: string;
}[];