Linting
This commit is contained in:
parent
271a88e3a0
commit
5fb12c1f41
|
@ -1,20 +1,20 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Router, { location } from "svelte-spa-router";
|
import Router, { location } from 'svelte-spa-router'
|
||||||
import routes from "./routes";
|
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 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'
|
||||||
|
|
||||||
$: sortedChats = $chatsStorage.sort((a, b) => b.id - a.id);
|
$: sortedChats = $chatsStorage.sort((a, b) => b.id - a.id)
|
||||||
$: 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: URLSearchParams = 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") as string);
|
apiKeyStorage.set(urlParams.get('key') as string)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<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";
|
import { apiKeyStorage, chatsStorage, addMessage, clearMessages } from './Storage.svelte'
|
||||||
import {
|
import {
|
||||||
type Request,
|
type Request,
|
||||||
type Response,
|
type Response,
|
||||||
|
@ -9,135 +9,135 @@
|
||||||
type Settings,
|
type Settings,
|
||||||
supportedModels,
|
supportedModels,
|
||||||
type ResponseModels,
|
type ResponseModels,
|
||||||
type SettingsSelect,
|
type SettingsSelect
|
||||||
} from "./Types.svelte";
|
} 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 { replace } from 'svelte-spa-router'
|
||||||
import SvelteMarkdown from "svelte-markdown";
|
import SvelteMarkdown from 'svelte-markdown'
|
||||||
|
|
||||||
export let params = { chatId: undefined };
|
export let params = { chatId: undefined }
|
||||||
let chatId: number = parseInt(params.chatId);
|
const chatId: number = parseInt(params.chatId)
|
||||||
let updating: boolean = false;
|
let updating: boolean = false
|
||||||
|
|
||||||
let input: HTMLTextAreaElement;
|
let input: HTMLTextAreaElement
|
||||||
let settings: HTMLDivElement;
|
let settings: HTMLDivElement
|
||||||
let recognition: any = null;
|
let recognition: any = null
|
||||||
let recording = false;
|
let recording = false
|
||||||
|
|
||||||
const settingsMap: Settings[] = [
|
const settingsMap: Settings[] = [
|
||||||
{
|
{
|
||||||
key: "model",
|
key: 'model',
|
||||||
name: "Model",
|
name: 'Model',
|
||||||
default: "gpt-3.5-turbo",
|
default: 'gpt-3.5-turbo',
|
||||||
options: supportedModels,
|
options: supportedModels,
|
||||||
type: "select",
|
type: 'select'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "temperature",
|
key: 'temperature',
|
||||||
name: "Sampling Temperature",
|
name: 'Sampling Temperature',
|
||||||
default: 1,
|
default: 1,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 2,
|
max: 2,
|
||||||
step: 0.1,
|
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,
|
min: 0,
|
||||||
max: 1,
|
max: 1,
|
||||||
step: 0.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,
|
min: 1,
|
||||||
max: 10,
|
max: 10,
|
||||||
step: 1,
|
step: 1,
|
||||||
type: "number",
|
type: 'number'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "max_tokens",
|
key: 'max_tokens',
|
||||||
name: "Max Tokens",
|
name: 'Max Tokens',
|
||||||
default: 0,
|
default: 0,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 32768,
|
max: 32768,
|
||||||
step: 1024,
|
step: 1024,
|
||||||
type: "number",
|
type: 'number'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "presence_penalty",
|
key: 'presence_penalty',
|
||||||
name: "Presence Penalty",
|
name: 'Presence Penalty',
|
||||||
default: 0,
|
default: 0,
|
||||||
min: -2,
|
min: -2,
|
||||||
max: 2,
|
max: 2,
|
||||||
step: 0.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,
|
min: -2,
|
||||||
max: 2,
|
max: 2,
|
||||||
step: 0.2,
|
step: 0.2,
|
||||||
type: "number",
|
type: 'number'
|
||||||
},
|
}
|
||||||
];
|
]
|
||||||
|
|
||||||
$: chat = $chatsStorage.find((chat) => chat.id === chatId);
|
$: chat = $chatsStorage.find((chat) => chat.id === chatId)
|
||||||
const token_price = 0.000002; // $0.002 per 1000 tokens
|
const tokenPrice = 0.000002 // $0.002 per 1000 tokens
|
||||||
|
|
||||||
// Focus the input on mount
|
// Focus the input on mount
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
input.focus();
|
input.focus()
|
||||||
|
|
||||||
// Try to detect speech recognition support
|
// Try to detect speech recognition support
|
||||||
if ("SpeechRecognition" in window) {
|
if ('SpeechRecognition' in window) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
recognition = new SpeechRecognition();
|
recognition = new window.SpeechRecognition()
|
||||||
} else if ("webkitSpeechRecognition" in window) {
|
} else if ('webkitSpeechRecognition' in window) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
recognition = new webkitSpeechRecognition();
|
recognition = new window.webkitSpeechRecognition() // eslint-disable-line new-cap
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recognition) {
|
if (recognition) {
|
||||||
recognition.interimResults = false;
|
recognition.interimResults = false
|
||||||
recognition.onstart = () => {
|
recognition.onstart = () => {
|
||||||
recording = true;
|
recording = true
|
||||||
};
|
}
|
||||||
recognition.onresult = (event) => {
|
recognition.onresult = (event) => {
|
||||||
// Stop speech recognition, submit the form and remove the pulse
|
// Stop speech recognition, submit the form and remove the pulse
|
||||||
const last = event.results.length - 1;
|
const last = event.results.length - 1
|
||||||
const text = event.results[last][0].transcript;
|
const text = event.results[last][0].transcript
|
||||||
input.value = text;
|
input.value = text
|
||||||
recognition.stop();
|
recognition.stop()
|
||||||
recording = false;
|
recording = false
|
||||||
submitForm(true);
|
submitForm(true)
|
||||||
};
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("Speech recognition not supported");
|
console.log('Speech recognition not supported')
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// Scroll to the bottom of the chat on update
|
// Scroll to the bottom of the chat on update
|
||||||
afterUpdate(() => {
|
afterUpdate(() => {
|
||||||
// Scroll to the bottom of the page after any updates to the messages array
|
// Scroll to the bottom of the page after any updates to the messages array
|
||||||
window.scrollTo(0, document.body.scrollHeight);
|
window.scrollTo(0, document.body.scrollHeight)
|
||||||
input.focus();
|
input.focus()
|
||||||
});
|
})
|
||||||
|
|
||||||
// Marked options
|
// Marked options
|
||||||
const markedownOptions = {
|
const markedownOptions = {
|
||||||
gfm: true, // Use GitHub Flavored Markdown
|
gfm: true, // Use GitHub Flavored Markdown
|
||||||
breaks: true, // Enable line breaks in 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> => {
|
const sendRequest = async (messages: Message[]): Promise<Response> => {
|
||||||
// Send API request
|
// Send API request
|
||||||
|
@ -160,154 +160,154 @@
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
// Show updating bar
|
// Show updating bar
|
||||||
updating = true;
|
updating = true
|
||||||
|
|
||||||
let response: Response;
|
let response: Response
|
||||||
try {
|
try {
|
||||||
const request: Request = {
|
const request: Request = {
|
||||||
// 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 => {
|
||||||
const { role, content } = message;
|
const { role, content } = message
|
||||||
return { role, content };
|
return { role, content }
|
||||||
})
|
})
|
||||||
// Skip error messages
|
// Skip error messages
|
||||||
.filter((message) => message.role !== "error"),
|
.filter((message) => message.role !== 'error'),
|
||||||
|
|
||||||
// Provide the settings by mapping the settingsMap to key/value pairs
|
// Provide the settings by mapping the settingsMap to key/value pairs
|
||||||
...settingsMap.reduce((acc, setting) => {
|
...settingsMap.reduce((acc, setting) => {
|
||||||
const value = (settings.querySelector(`#settings-${setting.key}`) as HTMLInputElement).value;
|
const value = (settings.querySelector(`#settings-${setting.key}`) as HTMLInputElement).value
|
||||||
if (value) {
|
if (value) {
|
||||||
acc[setting.key] = setting.type === "number" ? parseFloat(value) : value;
|
acc[setting.key] = setting.type === 'number' ? parseFloat(value) : value
|
||||||
}
|
}
|
||||||
return acc;
|
return acc
|
||||||
}, {}),
|
}, {})
|
||||||
};
|
}
|
||||||
response = await (
|
response = await (
|
||||||
await fetch("https://api.openai.com/v1/chat/completions", {
|
await fetch('https://api.openai.com/v1/chat/completions', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${$apiKeyStorage}`,
|
Authorization: `Bearer ${$apiKeyStorage}`,
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(request),
|
body: JSON.stringify(request)
|
||||||
})
|
})
|
||||||
).json();
|
).json()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
response = { error: { message: e.message } } as Response;
|
response = { error: { message: e.message } } as Response
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide updating bar
|
// Hide updating bar
|
||||||
updating = false;
|
updating = false
|
||||||
|
|
||||||
return response;
|
return response
|
||||||
};
|
}
|
||||||
|
|
||||||
const submitForm = async (recorded: boolean = false): Promise<void> => {
|
const submitForm = async (recorded: boolean = false): Promise<void> => {
|
||||||
// Compose the input message
|
// Compose the input message
|
||||||
const inputMessage: Message = { role: "user", content: input.value };
|
const inputMessage: Message = { role: 'user', content: input.value }
|
||||||
addMessage(chatId, inputMessage);
|
addMessage(chatId, inputMessage)
|
||||||
|
|
||||||
// Clear the input value
|
// Clear the input value
|
||||||
input.value = "";
|
input.value = ''
|
||||||
input.blur();
|
input.blur()
|
||||||
|
|
||||||
// Resize back to single line height
|
// 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) {
|
if (response.error) {
|
||||||
addMessage(chatId, {
|
addMessage(chatId, {
|
||||||
role: "error",
|
role: 'error',
|
||||||
content: `Error: ${response.error.message}`,
|
content: `Error: ${response.error.message}`
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
response.choices.map((choice) => {
|
response.choices.forEach((choice) => {
|
||||||
choice.message.usage = response.usage;
|
choice.message.usage = response.usage
|
||||||
// Remove whitespace around the message that the OpenAI API sometimes returns
|
// Remove whitespace around the message that the OpenAI API sometimes returns
|
||||||
choice.message.content = choice.message.content.trim();
|
choice.message.content = choice.message.content.trim()
|
||||||
addMessage(chatId, choice.message);
|
addMessage(chatId, choice.message)
|
||||||
// Use TTS to read the response, if query was recorded
|
// 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);
|
const utterance = new SpeechSynthesisUtterance(choice.message.content)
|
||||||
speechSynthesis.speak(utterance);
|
window.speechSynthesis.speak(utterance)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const suggestName = async (): Promise<void> => {
|
const suggestName = async (): Promise<void> => {
|
||||||
const suggestMessage: Message = {
|
const suggestMessage: Message = {
|
||||||
role: "user",
|
role: 'user',
|
||||||
content: "Can you give me a 5 word summary of this conversation's topic?",
|
content: "Can you give me a 5 word summary of this conversation's topic?"
|
||||||
};
|
}
|
||||||
addMessage(chatId, suggestMessage);
|
addMessage(chatId, suggestMessage)
|
||||||
|
|
||||||
const response = await sendRequest(chat.messages);
|
const response = await sendRequest(chat.messages)
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
addMessage(chatId, {
|
addMessage(chatId, {
|
||||||
role: "error",
|
role: 'error',
|
||||||
content: `Error: ${response.error.message}`,
|
content: `Error: ${response.error.message}`
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
response.choices.map((choice) => {
|
response.choices.forEach((choice) => {
|
||||||
choice.message.usage = response.usage;
|
choice.message.usage = response.usage
|
||||||
addMessage(chatId, choice.message);
|
addMessage(chatId, choice.message)
|
||||||
chat.name = choice.message.content;
|
chat.name = choice.message.content
|
||||||
chatsStorage.set($chatsStorage);
|
chatsStorage.set($chatsStorage)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const deleteChat = () => {
|
const deleteChat = () => {
|
||||||
if (confirm("Are you sure you want to delete this chat?")) {
|
if (window.confirm('Are you sure you want to delete this chat?')) {
|
||||||
replace("/").then(() => {
|
replace('/').then(() => {
|
||||||
chatsStorage.update((chats) => chats.filter((chat) => chat.id !== chatId));
|
chatsStorage.update((chats) => chats.filter((chat) => chat.id !== chatId))
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const showSettings = async () => {
|
const showSettings = async () => {
|
||||||
settings.classList.add("is-active");
|
settings.classList.add('is-active')
|
||||||
|
|
||||||
// Load available models from OpenAI
|
// Load available models from OpenAI
|
||||||
const allModels = (await (
|
const allModels = (await (
|
||||||
await fetch("https://api.openai.com/v1/models", {
|
await fetch('https://api.openai.com/v1/models', {
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${$apiKeyStorage}`,
|
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));
|
const filteredModels = supportedModels.filter((model) => allModels.data.find((m) => m.id === model));
|
||||||
|
|
||||||
// Update the models in the settings
|
// Update the models in the settings
|
||||||
(settingsMap[0] as SettingsSelect).options = filteredModels;
|
(settingsMap[0] as SettingsSelect).options = filteredModels
|
||||||
};
|
}
|
||||||
|
|
||||||
const closeSettings = () => {
|
const closeSettings = () => {
|
||||||
settings.classList.remove("is-active");
|
settings.classList.remove('is-active')
|
||||||
};
|
}
|
||||||
|
|
||||||
const clearSettings = () => {
|
const clearSettings = () => {
|
||||||
settingsMap.forEach((setting) => {
|
settingsMap.forEach((setting) => {
|
||||||
const input = settings.querySelector(`#settings-${setting.key}`) as HTMLInputElement;
|
const input = settings.querySelector(`#settings-${setting.key}`) as HTMLInputElement
|
||||||
input.value = "";
|
input.value = ''
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const recordToggle = () => {
|
const recordToggle = () => {
|
||||||
// Check if already recording - if so, stop - else start
|
// Check if already recording - if so, stop - else start
|
||||||
if (recording) {
|
if (recording) {
|
||||||
recognition?.stop();
|
recognition?.stop()
|
||||||
recording = false;
|
recording = false
|
||||||
} else {
|
} else {
|
||||||
recognition?.start();
|
recognition?.start()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="level chat-header">
|
<nav class="level chat-header">
|
||||||
|
@ -316,21 +316,21 @@
|
||||||
<p class="subtitle is-5">
|
<p class="subtitle is-5">
|
||||||
{chat.name || `Chat ${chat.id}`}
|
{chat.name || `Chat ${chat.id}`}
|
||||||
<a
|
<a
|
||||||
href={"#"}
|
href={'#'}
|
||||||
class="greyscale ml-2 is-hidden has-text-weight-bold editbutton"
|
class="greyscale ml-2 is-hidden has-text-weight-bold editbutton"
|
||||||
title="Rename chat"
|
title="Rename chat"
|
||||||
on:click|preventDefault={() => {
|
on:click|preventDefault={() => {
|
||||||
let newChatName = prompt("Enter a new name for this chat", chat.name);
|
const newChatName = window.prompt('Enter a new name for this chat', chat.name)
|
||||||
if (newChatName) {
|
if (newChatName) {
|
||||||
chat.name = newChatName;
|
chat.name = newChatName
|
||||||
chatsStorage.set($chatsStorage);
|
chatsStorage.set($chatsStorage)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
✏️
|
✏️
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={"#"}
|
href={'#'}
|
||||||
class="greyscale ml-2 is-hidden has-text-weight-bold editbutton"
|
class="greyscale ml-2 is-hidden has-text-weight-bold editbutton"
|
||||||
title="Suggest a chat name"
|
title="Suggest a chat name"
|
||||||
on:click|preventDefault={suggestName}
|
on:click|preventDefault={suggestName}
|
||||||
|
@ -338,7 +338,7 @@
|
||||||
💡
|
💡
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={"#"}
|
href={'#'}
|
||||||
class="greyscale ml-2 is-hidden editbutton"
|
class="greyscale ml-2 is-hidden editbutton"
|
||||||
title="Delete this chat"
|
title="Delete this chat"
|
||||||
on:click|preventDefault={deleteChat}
|
on:click|preventDefault={deleteChat}
|
||||||
|
@ -354,7 +354,7 @@
|
||||||
<button
|
<button
|
||||||
class="button is-warning"
|
class="button is-warning"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
clearMessages(chatId);
|
clearMessages(chatId)
|
||||||
}}><span class="greyscale mr-2">🗑️</span> Clear messages</button
|
}}><span class="greyscale mr-2">🗑️</span> Clear messages</button
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
@ -362,18 +362,18 @@
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{#each chat.messages as message}
|
{#each chat.messages as message}
|
||||||
{#if message.role === "user"}
|
{#if message.role === 'user'}
|
||||||
<article
|
<article
|
||||||
class="message is-info user-message"
|
class="message is-info user-message"
|
||||||
class:has-text-right={message.content.split("\n").filter((line) => line.trim()).length === 1}
|
class:has-text-right={message.content.split('\n').filter((line) => line.trim()).length === 1}
|
||||||
>
|
>
|
||||||
<div class="message-body content">
|
<div class="message-body content">
|
||||||
<a
|
<a
|
||||||
href={"#"}
|
href={'#'}
|
||||||
class="greyscale is-pulled-right ml-2 is-hidden editbutton"
|
class="greyscale is-pulled-right ml-2 is-hidden editbutton"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
input.value = message.content;
|
input.value = message.content
|
||||||
input.focus();
|
input.focus()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
✏️
|
✏️
|
||||||
|
@ -382,19 +382,19 @@
|
||||||
source={message.content}
|
source={message.content}
|
||||||
options={markedownOptions}
|
options={markedownOptions}
|
||||||
renderers={{
|
renderers={{
|
||||||
code: Code,
|
code: Code
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{:else if message.role === "system" || message.role === "error"}
|
{:else if message.role === 'system' || message.role === 'error'}
|
||||||
<article class="message is-danger">
|
<article class="message is-danger">
|
||||||
<div class="message-body content">
|
<div class="message-body content">
|
||||||
<SvelteMarkdown
|
<SvelteMarkdown
|
||||||
source={message.content}
|
source={message.content}
|
||||||
options={markedownOptions}
|
options={markedownOptions}
|
||||||
renderers={{
|
renderers={{
|
||||||
code: Code,
|
code: Code
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -406,14 +406,14 @@
|
||||||
source={message.content}
|
source={message.content}
|
||||||
options={markedownOptions}
|
options={markedownOptions}
|
||||||
renderers={{
|
renderers={{
|
||||||
code: Code,
|
code: Code
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{#if message.usage}
|
{#if message.usage}
|
||||||
<p class="is-size-7">
|
<p class="is-size-7">
|
||||||
This message was generated using <span class="has-text-weight-bold">{message.usage.total_tokens}</span>
|
This message was generated using <span class="has-text-weight-bold">{message.usage.total_tokens}</span>
|
||||||
tokens ~=
|
tokens ~=
|
||||||
<span class="has-text-weight-bold">${(message.usage.total_tokens * token_price).toFixed(6)}</span>
|
<span class="has-text-weight-bold">${(message.usage.total_tokens * tokenPrice).toFixed(6)}</span>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -433,15 +433,15 @@
|
||||||
rows="1"
|
rows="1"
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
// Only send if Enter is pressed, not Shift+Enter
|
// Only send if Enter is pressed, not Shift+Enter
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
submitForm();
|
submitForm()
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
on:input={(e) => {
|
on:input={(e) => {
|
||||||
// Resize the textarea to fit the content - auto is important to reset the height after deleting content
|
// Resize the textarea to fit the content - auto is important to reset the height after deleting content
|
||||||
input.style.height = "auto";
|
input.style.height = 'auto'
|
||||||
input.style.height = input.scrollHeight + "px";
|
input.style.height = input.scrollHeight + 'px'
|
||||||
}}
|
}}
|
||||||
bind:this={input}
|
bind:this={input}
|
||||||
/>
|
/>
|
||||||
|
@ -461,8 +461,8 @@
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
on:keydown={(event) => {
|
on:keydown={(event) => {
|
||||||
if (event.key === "Escape") {
|
if (event.key === 'Escape') {
|
||||||
closeSettings();
|
closeSettings()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -482,7 +482,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{#if setting.type === "number"}
|
{#if setting.type === 'number'}
|
||||||
<input
|
<input
|
||||||
class="input"
|
class="input"
|
||||||
inputmode="decimal"
|
inputmode="decimal"
|
||||||
|
@ -493,11 +493,11 @@
|
||||||
step={setting.step}
|
step={setting.step}
|
||||||
placeholder={String(setting.default)}
|
placeholder={String(setting.default)}
|
||||||
/>
|
/>
|
||||||
{:else if setting.type === "select"}
|
{:else if setting.type === 'select'}
|
||||||
<div class="select">
|
<div class="select">
|
||||||
<select id="settings-{setting.key}">
|
<select id="settings-{setting.key}">
|
||||||
{#each setting.options as option}
|
{#each setting.options as option}
|
||||||
<option value={option} selected={option == setting.default}>{option}</option>
|
<option value={option} selected={option === setting.default}>{option}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Highlight } from "svelte-highlight";
|
import { Highlight } from 'svelte-highlight'
|
||||||
|
|
||||||
// Import both dark and light styles
|
// 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
|
// 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
|
// Copy function for the code block
|
||||||
import copy from "copy-to-clipboard";
|
import copy from 'copy-to-clipboard'
|
||||||
|
|
||||||
// Import all supported languages
|
// Import all supported languages
|
||||||
import {
|
import {
|
||||||
|
@ -22,80 +22,80 @@
|
||||||
shell,
|
shell,
|
||||||
php,
|
php,
|
||||||
plaintext,
|
plaintext,
|
||||||
type LanguageType,
|
type LanguageType
|
||||||
} from "svelte-highlight/languages";
|
} from 'svelte-highlight/languages'
|
||||||
|
|
||||||
export const type: "code" = "code";
|
export const type: 'code' = 'code'
|
||||||
export const raw: string = "";
|
export const raw: string = ''
|
||||||
export const codeBlockStyle: "indented" | undefined = undefined;
|
export const codeBlockStyle: 'indented' | undefined = undefined
|
||||||
export let lang: string | undefined = undefined;
|
export let lang: string | undefined
|
||||||
export let text: string;
|
export let text: string
|
||||||
|
|
||||||
// Map lang string to LanguageType
|
// Map lang string to LanguageType
|
||||||
let language: LanguageType<string>;
|
let language: LanguageType<string>
|
||||||
switch (lang) {
|
switch (lang) {
|
||||||
case "js":
|
case 'js':
|
||||||
case "javascript":
|
case 'javascript':
|
||||||
language = javascript;
|
language = javascript
|
||||||
break;
|
break
|
||||||
case "py":
|
case 'py':
|
||||||
case "python":
|
case 'python':
|
||||||
language = python;
|
language = python
|
||||||
break;
|
break
|
||||||
case "ts":
|
case 'ts':
|
||||||
case "typescript":
|
case 'typescript':
|
||||||
language = typescript;
|
language = typescript
|
||||||
break;
|
break
|
||||||
case "rb":
|
case 'rb':
|
||||||
case "ruby":
|
case 'ruby':
|
||||||
language = ruby;
|
language = ruby
|
||||||
break;
|
break
|
||||||
case "go":
|
case 'go':
|
||||||
case "golang":
|
case 'golang':
|
||||||
language = go;
|
language = go
|
||||||
break;
|
break
|
||||||
case "java":
|
case 'java':
|
||||||
language = java;
|
language = java
|
||||||
break;
|
break
|
||||||
case "sql":
|
case 'sql':
|
||||||
language = sql;
|
language = sql
|
||||||
break;
|
break
|
||||||
case "sh":
|
case 'sh':
|
||||||
case "shell":
|
case 'shell':
|
||||||
case "bash":
|
case 'bash':
|
||||||
language = shell;
|
language = shell
|
||||||
break;
|
break
|
||||||
case "php":
|
case 'php':
|
||||||
language = php;
|
language = php
|
||||||
break;
|
break
|
||||||
default:
|
default:
|
||||||
language = plaintext;
|
language = plaintext
|
||||||
}
|
}
|
||||||
|
|
||||||
// For copying code - reference: https://vyacheslavbasharov.com/blog/adding-click-to-copy-code-markdown-blog
|
// For copying code - reference: https://vyacheslavbasharov.com/blog/adding-click-to-copy-code-markdown-blog
|
||||||
const copyFunction = (event) => {
|
const copyFunction = (event) => {
|
||||||
// Get the button the user clicked on
|
// Get the button the user clicked on
|
||||||
const clickedElement = event.target as HTMLButtonElement;
|
const clickedElement = event.target as HTMLButtonElement
|
||||||
|
|
||||||
// Get the next element
|
// Get the next element
|
||||||
const nextElement = clickedElement.nextElementSibling;
|
const nextElement = clickedElement.nextElementSibling
|
||||||
|
|
||||||
// Modify the appearance of the button
|
// Modify the appearance of the button
|
||||||
const originalButtonContent = clickedElement.innerHTML;
|
const originalButtonContent = clickedElement.innerHTML
|
||||||
clickedElement.classList.add("is-success");
|
clickedElement.classList.add('is-success')
|
||||||
clickedElement.innerHTML = "Copied!";
|
clickedElement.innerHTML = 'Copied!'
|
||||||
|
|
||||||
// Retrieve the code in the code block
|
// Retrieve the code in the code block
|
||||||
const codeBlock = (nextElement.querySelector("pre > code") as HTMLPreElement).innerText;
|
const codeBlock = (nextElement.querySelector('pre > code') as HTMLPreElement).innerText
|
||||||
copy(codeBlock);
|
copy(codeBlock)
|
||||||
|
|
||||||
// Restored the button after copying the text in 1 second.
|
// Restored the button after copying the text in 1 second.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
clickedElement.innerHTML = originalButtonContent;
|
clickedElement.innerHTML = originalButtonContent
|
||||||
clickedElement.classList.remove("is-success");
|
clickedElement.classList.remove('is-success')
|
||||||
clickedElement.blur();
|
clickedElement.blur()
|
||||||
}, 1000);
|
}, 1000)
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
import { get } from "svelte/store";
|
import { get } from 'svelte/store'
|
||||||
import { chatsStorage } from "./Storage.svelte";
|
import { chatsStorage } from './Storage.svelte'
|
||||||
|
|
||||||
export const exportAsMarkdown = (chatId: number) => {
|
export const exportAsMarkdown = (chatId: number) => {
|
||||||
const chats = get(chatsStorage);
|
const chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId);
|
const chat = chats.find((chat) => chat.id === chatId)
|
||||||
const messages = chat.messages;
|
const messages = chat.messages
|
||||||
console.log(chat);
|
console.log(chat)
|
||||||
let markdownContent = `# ${chat.name}\n`;
|
let markdownContent = `# ${chat.name}\n`
|
||||||
|
|
||||||
messages.forEach((message) => {
|
messages.forEach((message) => {
|
||||||
const author = message.role;
|
const author = message.role
|
||||||
const content = message.content;
|
const content = message.content
|
||||||
const messageMarkdown = `## ${author}\n${content}\n\n`;
|
const messageMarkdown = `## ${author}\n${content}\n\n`
|
||||||
|
|
||||||
markdownContent += messageMarkdown;
|
markdownContent += messageMarkdown
|
||||||
});
|
})
|
||||||
const blob = new Blob([markdownContent], { type: "text/markdown" });
|
const blob = new Blob([markdownContent], { type: 'text/markdown' })
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob)
|
||||||
const a = document.createElement("a");
|
const a = document.createElement('a')
|
||||||
a.download = `${chat.name}.md`;
|
a.download = `${chat.name}.md`
|
||||||
a.href = url;
|
a.href = url
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a)
|
||||||
a.click();
|
a.click()
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a)
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { params, replace } from "svelte-spa-router";
|
import { params, replace } from 'svelte-spa-router'
|
||||||
|
|
||||||
import { clearChats } from "./Storage.svelte";
|
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 sortedChats: Chat[];
|
export let sortedChats: Chat[]
|
||||||
export let apiKey: string;
|
export let apiKey: string
|
||||||
|
|
||||||
$: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined;
|
$: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class="menu">
|
<aside class="menu">
|
||||||
<p class="menu-label">Chats</p>
|
<p class="menu-label">Chats</p>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
{#if sortedChats.length === 0}
|
{#if sortedChats.length === 0}
|
||||||
<li><a href={"#"}>No chats yet...</a></li>
|
<li><a href={'#'}>No chats yet...</a></li>
|
||||||
{:else}
|
{:else}
|
||||||
<li>
|
<li>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -33,35 +33,35 @@
|
||||||
<p class="menu-label">Actions</p>
|
<p class="menu-label">Actions</p>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<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
|
><span class="greyscale mr-2">🔑</span> API key</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<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
|
><span class="greyscale mr-2">➕</span> New chat</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={() => {
|
on:click={() => {
|
||||||
replace("#/").then(() => {
|
replace('#/').then(() => {
|
||||||
clearChats();
|
clearChats()
|
||||||
});
|
})
|
||||||
}}><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={() => {
|
||||||
exportAsMarkdown(activeChatId);
|
exportAsMarkdown(activeChatId)
|
||||||
}}><span class="greyscale mr-2">📥</span> Export chat</a
|
}}><span class="greyscale mr-2">📥</span> Export chat</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,57 +1,57 @@
|
||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
import { persisted } from "svelte-local-storage-store";
|
import { persisted } from 'svelte-local-storage-store'
|
||||||
import { get } from "svelte/store";
|
import { get } from 'svelte/store'
|
||||||
import type { Chat, Message } from "./Types.svelte";
|
import type { Chat, Message } from './Types.svelte'
|
||||||
|
|
||||||
export const chatsStorage = persisted("chats", [] as Chat[]);
|
export const chatsStorage = persisted('chats', [] as Chat[])
|
||||||
export const apiKeyStorage = persisted("apiKey", null as string);
|
export const apiKeyStorage = persisted('apiKey', null as string)
|
||||||
|
|
||||||
export const addChat = (): number => {
|
export const addChat = (): number => {
|
||||||
const chats = get(chatsStorage);
|
const chats = get(chatsStorage)
|
||||||
|
|
||||||
// Find the max chatId
|
// 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
|
// Add a new chat
|
||||||
chats.push({
|
chats.push({
|
||||||
id: chatId,
|
id: chatId,
|
||||||
name: `Chat ${chatId}`,
|
name: `Chat ${chatId}`,
|
||||||
messages: [],
|
messages: []
|
||||||
});
|
})
|
||||||
chatsStorage.set(chats);
|
chatsStorage.set(chats)
|
||||||
return chatId;
|
return chatId
|
||||||
};
|
}
|
||||||
|
|
||||||
export const clearChats = () => {
|
export const clearChats = () => {
|
||||||
chatsStorage.set([]);
|
chatsStorage.set([])
|
||||||
};
|
}
|
||||||
|
|
||||||
export const addMessage = (chatId: number, message: Message) => {
|
export const addMessage = (chatId: number, message: Message) => {
|
||||||
const chats = get(chatsStorage);
|
const chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId);
|
const chat = chats.find((chat) => chat.id === chatId)
|
||||||
chat.messages.push(message);
|
chat.messages.push(message)
|
||||||
chatsStorage.set(chats);
|
chatsStorage.set(chats)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const editMessage = (chatId: number, index: number, newMessage: Message) => {
|
export const editMessage = (chatId: number, index: number, newMessage: Message) => {
|
||||||
const chats = get(chatsStorage);
|
const chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId);
|
const chat = chats.find((chat) => chat.id === chatId)
|
||||||
chat.messages[index] = newMessage;
|
chat.messages[index] = newMessage
|
||||||
chat.messages.splice(index + 1); // remove the rest of the messages
|
chat.messages.splice(index + 1) // remove the rest of the messages
|
||||||
chatsStorage.set(chats);
|
chatsStorage.set(chats)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const clearMessages = (chatId: number) => {
|
export const clearMessages = (chatId: number) => {
|
||||||
const chats = get(chatsStorage);
|
const chats = get(chatsStorage)
|
||||||
const chat = chats.find((chat) => chat.id === chatId);
|
const chat = chats.find((chat) => chat.id === chatId)
|
||||||
chat.messages = [];
|
chat.messages = []
|
||||||
chatsStorage.set(chats);
|
chatsStorage.set(chats)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const deleteChat = (chatId: number) => {
|
export const deleteChat = (chatId: number) => {
|
||||||
const chats = get(chatsStorage);
|
const chats = get(chatsStorage)
|
||||||
const chatIndex = chats.findIndex((chat) => chat.id === chatId);
|
const chatIndex = chats.findIndex((chat) => chat.id === chatId)
|
||||||
chats.splice(chatIndex, 1);
|
chats.splice(chatIndex, 1)
|
||||||
chatsStorage.set(chats);
|
chatsStorage.set(chats)
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,21 +1,32 @@
|
||||||
<script context="module" lang="ts">
|
<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 = {
|
export type Chat = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Message = {
|
// See: https://platform.openai.com/docs/models/model-endpoint-compatibility
|
||||||
role: "user" | "assistant" | "system" | "error";
|
export const supportedModels = [
|
||||||
content: string;
|
'gpt-4',
|
||||||
usage?: Usage;
|
'gpt-4-0314',
|
||||||
};
|
'gpt-4-32k',
|
||||||
|
'gpt-4-32k-0314',
|
||||||
export type Usage = {
|
'gpt-3.5-turbo',
|
||||||
completion_tokens: number;
|
'gpt-3.5-turbo-0301'
|
||||||
prompt_tokens: number;
|
]
|
||||||
total_tokens: number;
|
type Model = typeof supportedModels[number];
|
||||||
};
|
|
||||||
|
|
||||||
export type Request = {
|
export type Request = {
|
||||||
model?: Model;
|
model?: Model;
|
||||||
|
@ -32,19 +43,8 @@
|
||||||
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 SettingsNumber = {
|
||||||
type: "number";
|
type: 'number';
|
||||||
default: number;
|
default: number;
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SettingsSelect = {
|
export type SettingsSelect = {
|
||||||
type: "select";
|
type: 'select';
|
||||||
default: Model;
|
default: Model;
|
||||||
options: Model[];
|
options: Model[];
|
||||||
};
|
};
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
export type Response = ResponseOK & ResponseError;
|
export type Response = ResponseOK & ResponseError;
|
||||||
|
|
||||||
export type ResponseModels = {
|
export type ResponseModels = {
|
||||||
object: "list";
|
object: 'list';
|
||||||
data: {
|
data: {
|
||||||
id: string;
|
id: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
Loading…
Reference in New Issue