Merge branch 'main' into pr/terryoy/41
This commit is contained in:
commit
0571a79886
|
@ -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: [
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}[];
|
||||
|
|
Loading…
Reference in New Issue