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 @@

-
+
- +
@@ -97,21 +97,24 @@

Settings

@@ -127,12 +130,6 @@

Chat settings

- -
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. */ + } +}