diff --git a/.gitignore b/.gitignore
index 2435bba..efd090c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+tmp
+
# Logs
logs
*.log
diff --git a/README.md b/README.md
index 45e74c6..692c2bd 100644
--- a/README.md
+++ b/README.md
@@ -8,9 +8,9 @@ A HTML UI for Ollama.
## Goals
-- Zero dependencies: vanilla HTML, CSS, and Javascript
-- Simple installation: download and open in browser
- Minimal & responsive UI: mobile & desktop
+- Zero dependencies: vanilla HTML, CSS, and Javascript
+- Simple installation
## Features
@@ -22,6 +22,7 @@ A HTML UI for Ollama.
- Edit chat
- Delete chat
- Download chat
+- Scroll to top/bottom
- Copy chat to clipboard
**Chats**
@@ -32,8 +33,10 @@ A HTML UI for Ollama.
**Settings**
-- View settings
-- Update settings
+- URL
+- Model
+- System prompt
+- Model parameters
## Screenshots
@@ -56,7 +59,7 @@ A HTML UI for Ollama.
First, install and start [Olama](https://ollama.ai/).
```bash
-$ ollama run mistral
+$ ollama run dolphin-phi
```
Next, clone this repository:
@@ -97,7 +100,7 @@ Tests are written using `Playwright` and `node:test`.
The the tests can be run from the command line using this command:
```bash
-$ ollama run mistral
+$ ollama run dolphin-phi
$ node test
```
@@ -116,6 +119,7 @@ $ node test
## Done
+- [x] Model parameters
- [x] System prompt
- [x] Copy message to clipboard
- [x] Select model in settings (global)
diff --git a/css/ChatArea.scss b/css/ChatArea.scss
index eda831a..2d2ff94 100644
--- a/css/ChatArea.scss
+++ b/css/ChatArea.scss
@@ -6,6 +6,7 @@
}
#chat-title {
+ @extend h1;
padding-left: 0.5rem;
@extend .font-weight-boldest;
text-overflow: ellipsis;
diff --git a/css/ChatMenu.scss b/css/ChatMenu.scss
index fb01cc6..8dfcea4 100644
--- a/css/ChatMenu.scss
+++ b/css/ChatMenu.scss
@@ -13,6 +13,11 @@
padding: 0.5rem;
}
+
+.hamburger-menu:hover {
+ @extend .box-shadow;
+}
+
.hamburger-menu .bar {
width: 1.5rem;
height: 2px;
diff --git a/css/Sidebar.scss b/css/Sidebar.scss
index 3e018ea..52f2610 100644
--- a/css/Sidebar.scss
+++ b/css/Sidebar.scss
@@ -1,5 +1,6 @@
/* Sidebar styling */
#sidebar {
+ @extend .box-shadow;
min-width: 200px;
max-width: 480px;
//flex: 0 0 250px; /* fixed width */
diff --git a/css/Tabs.scss b/css/Tabs.scss
index 565eea8..445926a 100644
--- a/css/Tabs.scss
+++ b/css/Tabs.scss
@@ -11,8 +11,12 @@
outline: none;
}
+.active-tab-button {
+ @extend .box-shadow;
+}
+
.tab-content {
display: none;
- padding: 20px;
- border-top: 1px solid #ccc;
+ //padding: 20px;
+ //border-top: 1px solid #ccc;
}
diff --git a/css/UINotification.scss b/css/UINotification.scss
index 186b8c8..dbb73c7 100644
--- a/css/UINotification.scss
+++ b/css/UINotification.scss
@@ -9,7 +9,7 @@
padding: 1rem;
margin: 1rem;
@extend .box-shadow;
- border-radius: 1rem;
+ border-radius: $border-radius;
.button {
border-radius: 2px;
@@ -21,3 +21,7 @@
.notification-message {
color: $white;
}
+
+.notification-error {
+ background: $error-color;
+}
diff --git a/css/button.scss b/css/button.scss
index ea67d7c..c2421fb 100644
--- a/css/button.scss
+++ b/css/button.scss
@@ -12,6 +12,8 @@ button {
cursor: pointer;
i {
color: $button-primary-color;
+ width: 1rem; // Center the icon
+ display: inline-block;
}
}
@@ -19,6 +21,7 @@ button:hover,
button.selected {
color: $button-primary-color;
background-color: $light-color;
+ @extend .box-shadow;
}
.button-small {
diff --git a/css/form.scss b/css/form.scss
index e68da29..125aab6 100644
--- a/css/form.scss
+++ b/css/form.scss
@@ -3,22 +3,29 @@ label {
margin-bottom: .5rem;
}
-input {
+input, textarea {
width: 100%;
padding: 0.5rem 0.75rem;
display: inline-block;
border: 1px solid $border-color;
}
+input.error,
+textarea.error {
+ border: 1px solid $error-color !important;
+}
+
input:focus,
+textarea:focus,
[contenteditable]:focus {
border: 1px solid $border-color;
border-color: lighten($secondary-color, 40%);
- outline: none;
+ outline: none !important;
@extend .box-shadow;
}
-input:hover {
+input:hover,
+textarea:hover {
border: 1px solid lighten($secondary-color, 20%);
}
@@ -32,4 +39,5 @@ label {
textarea {
padding: 0.5rem;
+ resize: none;
}
diff --git a/css/grid.scss b/css/grid.scss
index c72b2bb..9cf5ce9 100644
--- a/css/grid.scss
+++ b/css/grid.scss
@@ -19,3 +19,7 @@
.flex-align-end {
align-self: flex-end;
}
+
+.justify-left {
+ justify-content: left;
+}
diff --git a/css/icons.scss b/css/icons.scss
index 2a308b7..5828a92 100644
--- a/css/icons.scss
+++ b/css/icons.scss
@@ -64,9 +64,7 @@ i[class^="icon-"] {
}
.icon-menu:before {
- content: '\22EF'; /* Unicode character for the chevron down icon followed by a non-breaking space */
- font-size: 14px; /* Adjust the size of the icon */
- font-weight: 900;
+ content: '\22EF'; /* Unicode character for three dots */
}
.icon-user:before {
@@ -84,3 +82,41 @@ i[class^="icon-"] {
.icon-speech2:before {
content: "\1F5E8"; /* Unicode for 🗨️ */
}
+
+.icon-scroll-to-top:before {
+ content: "\21A5";
+}
+
+.icon-scroll-to-end:before {
+ content: "\21A7";
+}
+
+.icon-gpt {
+ display: inline-block;
+ width: 24px;
+ height: 24px;
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBjbGFzcz0iaWNvbi1tZCI+PGNpcmNsZSBjeD0iNi43NSIgY3k9IjYuNzUiIHI9IjMuMjUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjIiPjwvY2lyY2xlPjxjaXJjbGUgY3g9IjE3LjI1IiBjeT0iNi43NSIgcj0iMy4yNSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMiI+PC9jaXJjbGU+PGNpcmNsZSBjeD0iNi43NSIgY3k9IjE3LjI1IiByPSIzLjI1IiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIj48L2NpcmNsZT48Y2lyY2xlIGN4PSIxNy4yNSIgY3k9IjE3LjI1IiByPSIzLjI1IiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIj48L2NpcmNsZT48L3N2Zz4=");
+}
+
+.icon-models {
+ display: inline-block;
+ width: 24px;
+ height: 24px;
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBjbGFzcz0iaWNvbi1tZCI+CiAgPHJlY3QgeD0iNC4yNSIgeT0iNC4yNSIgd2lkdGg9IjUiIGhlaWdodD0iNSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMiI+PC9yZWN0PgogIDxyZWN0IHg9IjE0Ljc1IiB5PSI0LjI1IiB3aWR0aD0iNSIgaGVpZ2h0PSI1IiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIj48L3JlY3Q+CiAgPHJlY3QgeD0iNC4yNSIgeT0iMTQuNzUiIHdpZHRoPSI1IiBoZWlnaHQ9IjUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjIiPjwvcmVjdD4KICA8cmVjdCB4PSIxNC43NSIgeT0iMTQuNzUiIHdpZHRoPSI1IiBoZWlnaHQ9IjUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjIiPjwvcmVjdD4KPC9zdmc+Cg==");
+}
+
+.icon-chats {
+ display: inline-block;
+ //background-image: url('data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjUwIiBoZWlnaHQ9IjMwIiB2aWV3Qm94PSIwIDAgNTAgMzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPCEtLSBSb3VuZGVkIFJlY3RhbmdsZSBmb3IgdGhlIEJ1YmJsZSAtLT4KICA8cmVjdCB4PSIxIiB5PSIxIiB3aWR0aD0iNDgiIGhlaWdodD0iMjAiIHJ4PSIxMCIgcnk9IjEwIiBzdHJva2U9ImJsYWNrIiBmaWxsPSJ0cmFuc3BhcmVudCIgc3Ryb2tlLXdpZHRoPSIyIi8+CgogIDwhLS0gVGFpbCBvZiB0aGUgU3BlZWNoIEJ1YmJsZSAtLT4KICA8cGF0aCBkPSJNIDIwLDIxIEwgMjUsMjUgTCAzMCwyMSBaIiBmaWxsPSJ0cmFuc3BhcmVudCIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=');
+ background-image: url("data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgY2xhc3M9Imljb24tbWQiPgogIDxwb2x5Z29uIHBvaW50cz0iNC4yNSw0LjI1IDkuMjUsNC4yNSA2Ljc1LDkuMjUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9Im5vbmUiPjwvcG9seWdvbj4KICA8cG9seWdvbiBwb2ludHM9IjE0Ljc1LDQuMjUgMTkuNzUsNC4yNSAxNy4yNSw5LjI1IiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIj48L3BvbHlnb24+CiAgPHBvbHlnb24gcG9pbnRzPSI0LjI1LDE0Ljc1IDkuMjUsMTQuNzUgNi43NSwxOS43NSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS13aWR0aD0iMiIgZmlsbD0ibm9uZSI+PC9wb2x5Z29uPgogIDxwb2x5Z29uIHBvaW50cz0iMTQuNzUsMTQuNzUgMTkuNzUsMTQuNzUgMTcuMjUsMTkuNzUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9Im5vbmUiPjwvcG9seWdvbj4KPC9zdmc+Cg==");
+ width: 24px; /* Adjust as needed */
+ height: 24px; /* Adjust as needed */
+}
+
+.icon-hamburger {
+ background-image: url('data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDI0IDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDwhLS0gVG9wIFN0cmlwZSAtLT4KICA8cmVjdCB4PSI0IiB5PSI2IiB3aWR0aD0iMTYiIGhlaWdodD0iMiIgZmlsbD0iYmxhY2siPjwvcmVjdD4KCiAgPCEtLSBNaWRkbGUgU3RyaXBlIC0tPgogIDxyZWN0IHg9IjQiIHk9IjExIiB3aWR0aD0iMTYiIGhlaWdodD0iMiIgZmlsbD0iYmxhY2siPjwvcmVjdD4KCiAgPCEtLSBCb3R0b20gU3RyaXBlIC0tPgogIDxyZWN0IHg9IjQiIHk9IjE2IiB3aWR0aD0iMTYiIGhlaWdodD0iMiIgZmlsbD0iYmxhY2siPjwvcmVjdD4KPC9zdmc+Cg==');
+ width: 24px; /* Adjust as needed */
+ height: 24px; /* Adjust as needed */
+ background-size: contain;
+ background-repeat: no-repeat;
+}
diff --git a/css/list.scss b/css/list.scss
index 6dc41c8..c1bc789 100644
--- a/css/list.scss
+++ b/css/list.scss
@@ -6,7 +6,8 @@ ul {
width: 100%;
li {
- @extend .row;
+ display: flex; /* Use flexbox layout */
+ justify-content: left;
padding: 0.5rem 0.75rem;
cursor: pointer;
width: 100%;
@@ -32,13 +33,13 @@ ul {
li.selected {
@extend .font-weight-boldest;
- color: $button-primary-color;
- background-color: $light-color;
- @extend .box-shadow;
+ //color: $button-primary-color;
+ //background-color: $light-color;
+ //@extend .box-shadow;
}
li.selected:before {
- //content: "\203A\00a0"; // \00a0 is a non-breaking space
+ content: "\203A\00a0"; // \00a0 is a non-breaking space
}
li.hover {
diff --git a/css/menu.scss b/css/menu.scss
deleted file mode 100644
index 12aa7e3..0000000
--- a/css/menu.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-.hamburger-menu {
- display: inline-block;
- cursor: pointer;
- padding: 0.75rem 1rem;
-}
-
-.hamburger-menu .bar {
- width: 100%;
- height: 5px;
- background-color: $primary-color;
- margin: 6px 0;
- transition: 0.4s;
-}
diff --git a/css/modal.scss b/css/modal.scss
index be419ca..3e42ef6 100644
--- a/css/modal.scss
+++ b/css/modal.scss
@@ -7,6 +7,7 @@
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* semi-transparent background */
z-index: 1000; /* ensures modal is on top */
+ overflow: scroll;
}
.modal-header {
@@ -16,7 +17,7 @@
border-bottom: 1px solid $border-color;
.button {
- border-radius: 2px;
+ border-radius: $border-radius;
padding: .5rem;
line-height: 0.5rem;
}
@@ -33,8 +34,8 @@
min-width: 320px;
max-width: 50vw;
margin: auto; /* Center the modal */
- border-radius: 2px;
- overflow: hidden; /* Ensures the border-radius applies to children */
+ // border-radius: 2px;
+ // overflow: hidden; /* Ensures the border-radius applies to children */
box-shadow: 0 0 15px #444;
}
diff --git a/css/spinner.scss b/css/spinner.scss
new file mode 100644
index 0000000..c82c412
--- /dev/null
+++ b/css/spinner.scss
@@ -0,0 +1,13 @@
+.spinner {
+ border: 2px solid #f3f3f3;
+ border-top: 2px solid $primary-color;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ animation: spin 2s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
diff --git a/css/style.scss b/css/style.scss
index ab64034..5c17ab4 100644
--- a/css/style.scss
+++ b/css/style.scss
@@ -29,18 +29,3 @@ a {
.text-right {
text-align: right;
}
-
-/* Spinner */
-.spinner {
- border: 2px solid #f3f3f3;
- border-top: 2px solid $primary-color;
- border-radius: 50%;
- width: 20px;
- height: 20px;
- animation: spin 2s linear infinite;
-}
-
-@keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
-}
diff --git a/css/theme.scss b/css/theme.scss
index 4230d46..dd354e7 100644
--- a/css/theme.scss
+++ b/css/theme.scss
@@ -8,8 +8,10 @@
@import 'list';
@import 'button';
@import 'utils';
+@import 'spinner';
@import 'style';
// Components
+@import 'Tabs';
@import 'ChatApp';
@import 'ChatMenu';
@import 'ChatHistory';
diff --git a/css/utils.scss b/css/utils.scss
index c4e39dc..94cd839 100644
--- a/css/utils.scss
+++ b/css/utils.scss
@@ -10,3 +10,11 @@
.mt-1 {
margin-top: 0.5rem;
}
+
+.d-inline {
+ display: inline-block;
+}
+
+.d-block {
+ display: block;
+}
diff --git a/css/variables.scss b/css/variables.scss
index f9c765c..a1ce429 100644
--- a/css/variables.scss
+++ b/css/variables.scss
@@ -3,7 +3,9 @@ $secondary-color: #222831;
$tertiary-color: #EEEEEE;
$white: #fff;
+$red: #EF4040;
+$error-color: $red;
$text-color: $tertiary-color;
$bg-color: $tertiary-color;
@@ -14,7 +16,7 @@ $button-primary-bgcolor: #EEEEEE;
$button-secondary-color: lighten($primary-color, 2);
$button-secondary-bgcolor: #EEEEEE;
-$border-radius: 0px;
+$border-radius: 2px;
$border-color: $primary-color;
$border: $border-radius $border-color;
diff --git a/index.html b/index.html
index b0591bc..0c39d9b 100644
--- a/index.html
+++ b/index.html
@@ -21,7 +21,7 @@
- New chat
+ New chat
@@ -49,7 +49,7 @@
-
+
diff --git a/js/App.js b/js/App.js
index 83b7303..48e1d95 100644
--- a/js/App.js
+++ b/js/App.js
@@ -2,9 +2,11 @@ import { Models } from './models/Models.js'
import { UINotification } from './UINotification.js'
import { Settings } from './models/Settings.js'
import { Event } from './Event.js'
+import { DOM } from './Dom.js'
import { Chats } from './models/Chats.js'
import { Sidebar } from './Sidebar.js'
import { CopyButton } from './CopyButton.js'
+import { OllamaApi } from './OllamaApi.js'
import { DownloadButton } from './DownloadButton.js'
import { DropDownMenu } from './DropDownMenu.js'
import { SettingsDialog } from './SettingsDialog.js'
@@ -17,20 +19,20 @@ export class App {
static run () {
UINotification.initialize()
const app = new App()
- this.downloadButton = new DownloadButton()
- this.copyButton = new CopyButton()
- this.dropDownMenu = new DropDownMenu()
Models.load()
return app
}
constructor () {
- this.controller = null
this.chats = new Chats()
this.sidebar = new Sidebar(this.chats)
this.chatArea = new ChatArea(this.chats)
+ this.ollamaApi = new OllamaApi()
this.settingsDialog = new SettingsDialog(this.chats)
this.chatSettingsDialog = new ChatSettingsDialog(this.chats)
+ this.downloadButton = new DownloadButton()
+ this.copyButton = new CopyButton()
+ this.dropDownMenu = new DropDownMenu()
this.initializeElements()
this.bindEventListeners()
this.logInitialization()
@@ -45,7 +47,7 @@ export class App {
}
logInitialization () {
- const msg = `~~~\nOllama HTML UI\n~~~
+ const msg = `~~~\nChat UI\n~~~
Model: ${Settings.getModel()}
URL: ${Settings.getUrl()}
Chat: ${this.chats.getCurrentChat()?.id}
@@ -72,11 +74,9 @@ Chat: ${this.chats.getCurrentChat()?.id}
}
handleAbort = () => {
- if (this.controller) {
- this.controller.abort()
- this.enableInput()
- console.log('Request aborted')
- }
+ this.ollamaApi.abort()
+ this.enableForm()
+ console.log('Request aborted')
}
handleKeyPress = (event) => {
@@ -85,57 +85,53 @@ Chat: ${this.chats.getCurrentChat()?.id}
}
}
- enableInput () {
- this.show(this.sendButton)
- this.hide(this.abortButton)
- this.enable(this.messageInput)
+ enableForm () {
+ DOM.showElement(this.sendButton)
+ .hideElement(this.abortButton)
+ .enableInput(this.messageInput)
this.messageInput.focus()
}
- disableInput () {
- this.hide(this.sendButton)
- this.show(this.abortButton)
- this.disable(this.messageInput)
- }
-
- show = (element) => {
- element.classList.remove('hidden')
- }
-
- hide = (element) => {
- element.classList.add('hidden')
- }
-
- enable = (element) => {
- element.removeAttribute('disabled')
- }
-
- disable = (element) => {
- element.setAttribute('disabled', 'disabled')
+ disableForm () {
+ DOM.hideElement(this.sendButton)
+ .showElement(this.abortButton)
+ .disableInput(this.messageInput)
}
// https://github.com/jmorganca/ollama/blob/main/docs/api.md#generate-a-completion
async sendMessage () {
const message = this.messageInput.value.trim()
+ const chat = this.chats.getCurrentChat()
+ const model = chat?.model || Settings.getModel()
this.messageInput.value = ''
+
if (message) {
- this.disableInput()
+ this.disableForm()
this.createMessageDiv(message, 'user')
+ const systemPrompt = Settings.getSystemPrompt()
+ const modelParameters = Settings.getModelParameters()
const responseDiv = this.createMessageDiv('', 'system')
- responseDiv.innerHTML = this.getSpinner()
- try {
- const data = {
- model: this.chats.getCurrentChat()?.model || Settings.getModel(),
- prompt: message,
- system: Settings.getSystemPrompt()
- }
- const response = await this.postMessage(data, responseDiv)
- this.handleResponse(response, responseDiv)
- } catch (error) {
- console.debug(error)
- console.error(`Please check the settings: ${Settings.getMessageUrl()} ${Settings.getModel()}. Error code: 843947`)
- this.updateResponse(responseDiv, '', 'system')
+ const data = {
+ prompt: message,
+ model
+ }
+ // Add system prompt
+ if (systemPrompt) {
+ data.system = systemPrompt
}
+ // Add model parameters
+ if (modelParameters) {
+ data.options = modelParameters
+ }
+ // Show spinner
+ responseDiv.innerHTML = ''
+ // Make request
+ this.ollamaApi.send(
+ data,
+ (response) => this.handleResponse(response, responseDiv),
+ (error) => this.handleResponseError(error, responseDiv),
+ (response) => this.handleDone(response, responseDiv)
+ )
}
}
@@ -165,95 +161,44 @@ Chat: ${this.chats.getCurrentChat()?.id}
return messageDiv
}
- getSpinner = () => {
- return ''
- }
-
- async postMessage (data, responseDiv) {
- this.controller = new AbortController()
- const url = Settings.getMessageUrl()
- const { signal } = this.controller
- const response = await fetch(url, {
- signal,
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(data)
- })
- if (!response.ok) {
- this.enableInput()
- throw new Error(`POST ${url} status ${response.status}`)
- }
- return response
- }
-
- async handleResponse (response, responseDiv) {
- const reader = response.body.getReader()
- let partialLine = ''
-
- try {
- while (true) {
- const { done, value } = await reader.read()
- if (done) {
- this.handleDone(responseDiv)
- break
- }
-
- const textChunk = new TextDecoder().decode(value)
- const lines = (partialLine + textChunk).split('\n')
- partialLine = lines.pop()
-
- lines.forEach(line => {
- if (line.trim()) {
- this.updateResponse(responseDiv, JSON.parse(line).response)
- }
- })
- }
-
- if (partialLine.trim()) {
- this.updateResponse(responseDiv, partialLine)
- }
- } catch (error) {
- this.handleResponseError(error, responseDiv)
- } finally {
- this.enableInput()
+ handleResponse (response, responseDiv) {
+ // Update the response div with the received response
+ const sanitizedContent = this.sanitizeContent(response)
+ if (responseDiv.initialResponse) {
+ responseDiv.textContent = sanitizedContent
+ responseDiv.initialResponse = false
+ } else {
+ responseDiv.textContent += sanitizedContent
}
+ this.chatArea.scrollToEnd()
}
- handleResponseError = (error, responseDiv) => {
+ handleResponseError (error, responseDiv) {
// Ignore "Abort" button
if (error.name !== 'AbortError') {
- this.updateResponse(responseDiv, `Error: ${error.message}`, 'system')
- }
- }
-
- updateResponse = (div, content) => {
- const sanitizedContent = this.sanitizeContent(content)
- if (div.initialResponse) {
- div.textContent = sanitizedContent
- div.initialResponse = false
- } else {
- div.textContent += sanitizedContent
+ console.error(`Error: ${error.message}`)
+ responseDiv.initialResponse = false
}
this.chatArea.scrollToEnd()
+ this.enableForm()
}
- sanitizeContent = (content) => {
- // TODO: Sanitization logic here
- return content
- }
-
- handleDone = (responseDiv) => {
+ handleDone (response, responseDiv) {
+ console.log('Done')
const chat = this.chats.getCurrentChat()
const content = this.chatHistory.innerHTML
- // TODO:
- // const formattedContent = MarkdownFormatter.format(responseDiv.textContent)
- // responseDiv.innerHTML = formattedContent
if (chat !== null) {
this.chats.update(chat.id, chat.title, content)
} else {
this.chats.add(null, content)
}
this.chats.saveData()
+ this.enableForm()
+ }
+
+ sanitizeContent = (content) => {
+ // TODO: Sanitization logic here
+ return content
}
getIdParam = () => {
diff --git a/js/App.test.js b/js/App.test.js
index a3b97bf..abe07f0 100644
--- a/js/App.test.js
+++ b/js/App.test.js
@@ -3,10 +3,10 @@ import { chromium } from 'playwright'
import { expect } from 'playwright/test'
import { exec } from 'node:child_process'
-// The Ollama server must be running mistral:latest
+// The Ollama server must be running dolphin-phi:latest
// TODO: Implement dummy server
const url = 'http://localhost:11434'
-const model = 'mistral:latest'
+const model = 'dolphin-phi:latest'
function openScreenshot (filePath) {
const openCommand = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open'
@@ -191,7 +191,7 @@ test.describe('Application tests', { only: true }, () => {
/*
test('Send message (server down)', async () => {
await app.updateSettings('http://localhost:999999')
- await app.page.fill('#message-input', 'What is Mistral?')
+ await app.page.fill('#message-input', 'What is the meaning of life?')
await app.page.click('#send-button')
await app.page.waitForTimeout(500) // Small delay to allow UI to update
await expect(app.page.locator('#abort-button')).not.toBeVisible()
@@ -277,8 +277,8 @@ test.describe('Application tests', { only: true }, () => {
test('Send message', async () => {
await app.updateSettings(url, model)
// Create chat
- await app.editChatTitle('What is Mistral?')
- await app.sendMessage('What is Mistral?')
+ await app.editChatTitle('What is the meaning of life?')
+ await app.sendMessage('What is the meaning of life?')
await app.screenshot('chat.png')
// Collapse
await app.page.click('#hamburger-menu')
diff --git a/js/Dom.js b/js/Dom.js
new file mode 100644
index 0000000..de31cdd
--- /dev/null
+++ b/js/Dom.js
@@ -0,0 +1,21 @@
+export class DOM {
+ static showElement (element) {
+ element.classList.remove('hidden')
+ return this
+ }
+
+ static hideElement (element) {
+ element.classList.add('hidden')
+ return this
+ }
+
+ static enableInput (element) {
+ element.removeAttribute('disabled')
+ return this
+ }
+
+ static disableInput (element) {
+ element.setAttribute('disabled', 'disabled')
+ return this
+ }
+}
diff --git a/js/OllamaApi.js b/js/OllamaApi.js
new file mode 100644
index 0000000..4e7a050
--- /dev/null
+++ b/js/OllamaApi.js
@@ -0,0 +1,129 @@
+import { Settings } from './models/Settings.js'
+
+export class OllamaApi {
+ constructor () {
+ this.abortController = null
+ }
+
+ async send (data, onResponse, onError, onDone) {
+ try {
+ const response = await this.postChatMessage(data)
+ await this.handleResponse(response, onResponse, onDone)
+ } catch (error) {
+ onError(error)
+ }
+ }
+
+ async postChatMessage (data) {
+ this.abortController = new AbortController()
+ const { signal } = this.abortController
+ const url = OllamaApi.getGenerateUrl()
+ const response = await fetch(url, {
+ signal,
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ })
+
+ if (!response.ok) {
+ throw new Error(`POST ${url} status ${response.status}`)
+ }
+
+ return response
+ }
+
+ async handleResponse (response, onResponse, onDone) {
+ const reader = response.body.getReader()
+ let partialLine = ''
+
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) {
+ onDone(response)
+ break
+ }
+
+ const textChunk = new TextDecoder().decode(value)
+ const lines = (partialLine + textChunk).split('\n')
+ partialLine = lines.pop()
+
+ lines.forEach(line => {
+ const responseData = JSON.parse(line)
+ if (line.trim()) {
+ this.printResponseStats(responseData)
+ onResponse(responseData.response)
+ }
+ })
+ }
+
+ if (partialLine.trim()) {
+ onResponse(partialLine)
+ }
+ }
+
+ abort () {
+ if (this.abortController) {
+ this.abortController.abort()
+ }
+ }
+
+ printResponseStats (data) {
+ if (!data.total_duration) {
+ return
+ }
+ // Convert nanoseconds to seconds for durations
+ const totalDurationInSeconds = data.total_duration / 1e9
+ const loadDurationInSeconds = data.load_duration / 1e9
+ const promptEvalDurationInSeconds = data.prompt_eval_duration / 1e9
+ const responseEvalDurationInSeconds = data.eval_duration / 1e9
+
+ // Calculate tokens per second (token/s)
+ const tokensPerSecond = data.eval_count / responseEvalDurationInSeconds
+ const output = `
+Model: ${data.model}
+Created At: ${data.created_at}
+Total Duration (s): ${totalDurationInSeconds.toFixed(2)}
+Load Duration (s): ${loadDurationInSeconds.toFixed(2)}
+Prompt Evaluation Count: ${data.prompt_eval_count}
+Prompt Evaluation Duration (s): ${promptEvalDurationInSeconds.toFixed(2)}
+Response Evaluation Count: ${data.eval_count}
+Response Evaluation Duration (s): ${responseEvalDurationInSeconds.toFixed(2)}
+Tokens Per Second: ${tokensPerSecond.toFixed(2)} token/s
+ `
+ console.log(output)
+ }
+
+ static getModels (onResponse) {
+ const url = OllamaApi.getModelsUrl()
+ if (!url) {
+ throw new Error('Invalid URL')
+ }
+
+ return fetch(url)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`Unable to fetch models from ${url}`)
+ }
+ return response.json()
+ })
+ .then(data => {
+ onResponse(data.models)
+ })
+ .catch(_ => {
+ console.error(`Please ensure the server is running at: ${OllamaApi.getModelsUrl()}. Error code: 39847`)
+ onResponse([])
+ })
+ }
+
+ static getGenerateUrl () {
+ return Settings.getUrl('/api/generate')
+ }
+
+ static getChatUrl () {
+ return Settings.getUrl('/api/chat')
+ }
+
+ static getModelsUrl () {
+ return Settings.getUrl('/api/tags')
+ }
+}
diff --git a/js/SettingsDialog.js b/js/SettingsDialog.js
index 790e739..64d5540 100644
--- a/js/SettingsDialog.js
+++ b/js/SettingsDialog.js
@@ -10,6 +10,7 @@ export class SettingsDialog extends Modal {
this.urlInput = document.getElementById('input-url')
this.modelInput = document.getElementById('input-model')
this.systemPromptInput = this.modal.querySelector('#input-system-prompt')
+ this.modelParametersInput = this.modal.querySelector('#input-model-parameters')
this.refreshModelsButton = this.modal.querySelector('.refresh-models-button')
this.modelList = new ModelsList('model-list', Settings.getModel())
this.bindEventListeners()
@@ -17,18 +18,35 @@ export class SettingsDialog extends Modal {
}
bindEventListeners () {
- this.showButton.addEventListener('click', this.show.bind(this))
this.urlInput.addEventListener('blur', () => {
- Settings.setUrl(this.urlInput.value)
+ const value = this.urlInput.value.trim()
+ Settings.setUrl(value)
})
this.systemPromptInput.addEventListener('blur', () => {
- Settings.setSystemPrompt(this.systemPromptInput.value)
+ Settings.setSystemPrompt(this.systemPromptInput.value.trim())
+ })
+ this.modelParametersInput.addEventListener('blur', () => {
+ const value = this.modelParametersInput.value.trim()
+ try {
+ const parsedValue = JSON.parse(value)
+ const prettyJSON = JSON.stringify(parsedValue, 2)
+ Settings.setModelParameters(parsedValue)
+ this.modelParametersInput.value = prettyJSON
+ this.modelParametersInput.classList.remove('error')
+ } catch (error) {
+ if (error.name === 'SyntaxError') {
+ this.modelParametersInput.classList.add('error')
+ } else {
+ console.error(error)
+ }
+ }
})
- this.closeButton.onclick = () => this.hide()
this.modelList.onClick(model => {
Settings.setModel(this.modelList.getSelected())
})
+ this.showButton.addEventListener('click', this.show.bind(this))
this.refreshModelsButton.onclick = () => this.refreshModels()
+ this.closeButton.onclick = () => this.hide()
}
refreshModels () {
diff --git a/js/Sidebar.js b/js/Sidebar.js
index 6248b61..029eec8 100644
--- a/js/Sidebar.js
+++ b/js/Sidebar.js
@@ -80,6 +80,7 @@ export class Sidebar {
toggle () {
this.element.classList.toggle('collapsed')
+ this.hamburgerButton.classList.toggle('collapsed')
if (this.element.classList.contains('collapsed')) {
this.settings.set('sidebar-collapsed', true)
} else {
diff --git a/js/UINotification.js b/js/UINotification.js
index dfc6030..c05353c 100644
--- a/js/UINotification.js
+++ b/js/UINotification.js
@@ -8,15 +8,25 @@ function simpleHash (str) {
return hash
}
+// Show all uncaught errors as UI notifications
+/*
+window.onerror = function (message, source, lineno, colno, error) {
+ const errorDetails = `${message} at ${source}:${lineno}:${colno}`
+ UINotification.show(errorDetails, 'error')
+ return true
+}
+*/
+
export class UINotification {
- constructor (message) {
+ constructor (message, type) {
+ this.type = type
this.domId = simpleHash(JSON.stringify(message))
this.container = document.body
this.template = document.getElementById('notification-template').content
}
- static show (message) {
- const notification = new UINotification(message)
+ static show (message, type) {
+ const notification = new UINotification(message, type)
notification.show(message)
}
@@ -55,6 +65,10 @@ export class UINotification {
// Assign unique ID to the notification
const notificationId = `notification-${this.domId}`
notificationElement.id = notificationId // Set ID on the actual element, not the fragment
+ // Add type, for example, error
+ if (this.type) {
+ notificationElement.classList.add(`notification-${this.type}`)
+ }
// Add close functionality
const closeButton = clone.querySelector('.close-notification-button')
diff --git a/js/models/Models.js b/js/models/Models.js
index 3268153..e05491a 100644
--- a/js/models/Models.js
+++ b/js/models/Models.js
@@ -1,33 +1,16 @@
import { Event } from '../Event.js'
import { Settings } from './Settings.js'
+import { OllamaApi } from '../OllamaApi.js'
export class Models {
static models = []
static load () {
- const url = Settings.getModelsUrl()
- if (url === undefined || url === '') {
- return
- }
- return fetch(url)
- .then(response => {
- if (!response.ok) {
- throw new Error('Network response was not ok')
- }
- return response.json()
- })
- .then(data => {
- Models.models = data.models
- Settings.set('models', Models.models)
- Event.emit('modelsLoaded', Models.models)
- })
- .catch(error => {
- console.error(`Please ensure the server is running at: ${Settings.getModelsUrl()}. Error code: 39847`)
- Models.models = []
- Event.emit('error', error)
- Event.emit('modelsLoaded', Models.models)
- Settings.set('models', Models.models)
- })
+ OllamaApi.getModels(models => {
+ Models.models = models
+ Settings.set('models', Models.models)
+ Event.emit('modelsLoaded', Models.models)
+ })
}
static getAll () {
diff --git a/js/models/Settings.js b/js/models/Settings.js
index 9346fdd..0149184 100644
--- a/js/models/Settings.js
+++ b/js/models/Settings.js
@@ -30,11 +30,15 @@ export class Settings {
}
static getUrl (uri) {
- const baseUrl = Settings.get('url')
- if (uri) {
- return new URL(uri, baseUrl).href
- } else {
- return baseUrl
+ try {
+ const baseUrl = Settings.get('url')
+ if (uri) {
+ return new URL(uri, baseUrl).href
+ } else {
+ return baseUrl
+ }
+ } catch (error) {
+ return null
}
}
@@ -55,9 +59,23 @@ export class Settings {
}
static setSystemPrompt (systemPrompt) {
+ if (systemPrompt === '') {
+ systemPrompt = null
+ }
Settings.set('system-prompt', systemPrompt)
}
+ static getModelParameters () {
+ return Settings.get('model-parameters')
+ }
+
+ static setModelParameters (modelParameters) {
+ if (modelParameters === '') {
+ modelParameters = null
+ }
+ Settings.set('model-parameters', modelParameters)
+ }
+
static getCurrentChatId () {
return Settings.get('currentChatId')
}
@@ -73,12 +91,4 @@ export class Settings {
static setChats (chats) {
Settings.set('chats', chats)
}
-
- static getMessageUrl () {
- return Settings.getUrl('/api/generate')
- }
-
- static getModelsUrl () {
- return Settings.getUrl('/api/tags')
- }
}
diff --git a/package-lock.json b/package-lock.json
index af5929a..9cd0a58 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1353,9 +1353,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001570",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz",
- "integrity": "sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==",
+ "version": "1.0.30001571",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001571.tgz",
+ "integrity": "sha512-tYq/6MoXhdezDLFZuCO/TKboTzuQ/xR5cFdgXPfDtM7/kchBO3b4VWghE/OAi/DV7tTdhmLjZiZBZi1fA/GheQ==",
"dev": true,
"funding": [
{
@@ -1415,6 +1415,18 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/chrome-trace-event": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
@@ -1811,9 +1823,9 @@
"peer": true
},
"node_modules/electron-to-chromium": {
- "version": "1.4.614",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.614.tgz",
- "integrity": "sha512-X4ze/9Sc3QWs6h92yerwqv7aB/uU8vCjZcrMjA8N9R1pjMFRe44dLsck5FzLilOYvcXuDn93B+bpGYyufc70gQ==",
+ "version": "1.4.616",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz",
+ "integrity": "sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg==",
"dev": true
},
"node_modules/entities": {
@@ -2232,18 +2244,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/glob-parent": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
- "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
- "dependencies": {
- "is-glob": "^4.0.3"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -2519,15 +2519,15 @@
}
},
"node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"dependencies": {
- "is-glob": "^4.0.1"
+ "is-glob": "^4.0.3"
},
"engines": {
- "node": ">= 6"
+ "node": ">=10.13.0"
}
},
"node_modules/globals": {
@@ -3966,9 +3966,9 @@
}
},
"node_modules/postcss-selector-parser": {
- "version": "6.0.13",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
- "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
+ "version": "6.0.14",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.14.tgz",
+ "integrity": "sha512-65xXYsT40i9GyWzlHQ5ShZoK7JZdySeOozi/tz2EezDo6c04q6+ckYMeoY7idaie1qp2dT5KoYQ2yky6JuoHnA==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
diff --git a/screenshots/chat-collapsed.png b/screenshots/chat-collapsed.png
index 8a3bc12..5c9f5d5 100644
Binary files a/screenshots/chat-collapsed.png and b/screenshots/chat-collapsed.png differ
diff --git a/screenshots/chat.png b/screenshots/chat.png
index 0f925aa..262731b 100644
Binary files a/screenshots/chat.png and b/screenshots/chat.png differ
diff --git a/screenshots/mobile-chat-collapsed.png b/screenshots/mobile-chat-collapsed.png
index c799286..ef42fe3 100644
Binary files a/screenshots/mobile-chat-collapsed.png and b/screenshots/mobile-chat-collapsed.png differ
diff --git a/screenshots/mobile-chat.png b/screenshots/mobile-chat.png
index e3ae315..4684227 100644
Binary files a/screenshots/mobile-chat.png and b/screenshots/mobile-chat.png differ
diff --git a/screenshots/mobile-search.png b/screenshots/mobile-search.png
index b0bd3c3..24f03f5 100644
Binary files a/screenshots/mobile-search.png and b/screenshots/mobile-search.png differ
diff --git a/screenshots/mobile-settings.png b/screenshots/mobile-settings.png
index 988ed54..8784162 100644
Binary files a/screenshots/mobile-settings.png and b/screenshots/mobile-settings.png differ
diff --git a/screenshots/screenshot.png b/screenshots/screenshot.png
index cad4557..e0030a4 100644
Binary files a/screenshots/screenshot.png and b/screenshots/screenshot.png differ
diff --git a/screenshots/search.png b/screenshots/search.png
index 9737c3c..c61d93e 100644
Binary files a/screenshots/search.png and b/screenshots/search.png differ
diff --git a/screenshots/settings.png b/screenshots/settings.png
index 35ae35d..70656f4 100644
Binary files a/screenshots/settings.png and b/screenshots/settings.png differ
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..bb2f7f6
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,109 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig to read more about this file */
+
+ /* Projects */
+ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
+ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
+ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
+ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
+ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
+ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
+
+ /* Language and Environment */
+ "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
+ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+ // "jsx": "preserve", /* Specify what JSX code is generated. */
+ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
+ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
+ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
+ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
+ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
+ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
+ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
+
+ /* Modules */
+ "module": "commonjs" /* Specify what module code is generated. */,
+ // "rootDir": "./", /* Specify the root folder within your source files. */
+ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
+ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
+ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
+ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
+ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
+ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
+ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
+ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
+ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
+ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
+ // "resolveJsonModule": true, /* Enable importing .json files. */
+ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
+ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
+
+ /* JavaScript Support */
+ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
+ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
+
+ /* Emit */
+ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
+ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
+ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
+ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
+ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
+ // "outDir": "./", /* Specify an output folder for all emitted files. */
+ // "removeComments": true, /* Disable emitting comments. */
+ // "noEmit": true, /* Disable emitting files from a compilation. */
+ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
+ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
+ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+ // "newLine": "crlf", /* Set the newline character for emitting files. */
+ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
+ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
+ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
+ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
+ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
+ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
+
+ /* Interop Constraints */
+ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
+ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
+ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
+ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
+ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
+
+ /* Type Checking */
+ "strict": true /* Enable all strict type-checking options. */,
+ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
+ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
+ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
+ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
+ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
+ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
+ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
+ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
+ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
+ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
+ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
+ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
+ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
+ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
+ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
+ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
+
+ /* Completeness */
+ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
+ "skipLibCheck": true /* Skip type checking all .d.ts files. */
+ }
+}