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>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,8 +5,8 @@
 | 
			
		|||
    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