diff --git a/src/app.scss b/src/app.scss
index 11c7c2a..30ea08b 100644
--- a/src/app.scss
+++ b/src/app.scss
@@ -17,18 +17,36 @@
section.section {
flex-grow: 1;
}
+
+ .chat-option-menu.navbar-item {
+ margin-left: auto;
+ }
+
+ /* temp. fix to keep navbar from getting huge on small screen devices
+ if the right menu is put in the proper navbar-end container */
+ .navbar-brand {
+ /* margin-right: 0; */
+ width: 100%;
+ }
+
+ .dropdown-item .menu-icon {
+ padding-right: .5em;
+ }
+ .dropdown-menu {
+ max-height: calc(100vh - 60px);;
+ }
+}
+
+.is-disabled {
+ pointer-events: none;
+ cursor: default;
+ opacity: .65;
}
.rotate {
animation: rotating 10s linear infinite;
}
-a.is-disabled {
- pointer-events: none;
- cursor: default;
- opacity: 0.5;
-}
-
.greyscale {
filter: grayscale(100%);
}
@@ -100,7 +118,16 @@ $modal-content-width: 1000px;
$modal-background-background-color-dark: rgba($dark, 0.86) !default; // remove this once a new version of bulma-prefers-dark is released
@import "/node_modules/bulma-prefers-dark/build/bulma-prefers-dark.sass";
+/* For the message notes on light mode */
+.message-note, .running-totals {
+ opacity: 0.7;
+}
+
@media (prefers-color-scheme: dark) {
+ /* For the message notes on dark mode */
+ .message-note, .running-totals {
+ opacity: 0.5;
+ }
.modal-card-body {
// remove this once https: //github.com/jloh/bulma-prefers-dark/pull/90 is merged and released
background-color: $background-dark;
@@ -150,4 +177,4 @@ $modal-background-background-color-dark: rgba($dark, 0.86) !default; // remove t
.dropdown-menu {
width: 100%;
}
-}
\ No newline at end of file
+}
diff --git a/src/lib/Chat.svelte b/src/lib/Chat.svelte
index b213eca..fc7443a 100644
--- a/src/lib/Chat.svelte
+++ b/src/lib/Chat.svelte
@@ -4,62 +4,40 @@
saveChatStore,
apiKeyStorage,
chatsStorage,
- globalStorage,
addMessage,
insertMessages,
- clearMessages,
- copyChat,
getChatSettingValueNullDefault,
- saveCustomProfile,
- deleteCustomProfile,
- setGlobalSettingValueByKey,
updateChatSettings,
- resetChatSettings,
- setChatSettingValue,
- addChatFromJSON,
- updateRunningTotal
+ updateRunningTotal,
+ checkStateChange,
+ showSetChatSettings
} from './Storage.svelte'
- import { getChatSettingObjectByKey, getChatSettingList, getRequestSettingList, getChatDefaults, defaultModel } from './Settings.svelte'
+ import { getRequestSettingList, defaultModel } from './Settings.svelte'
import {
type Request,
type Response,
type Message,
- type ChatSetting,
- type ResponseModels,
- type SettingSelect,
- type Chat,
- type SelectOption,
- supportedModels
+ type Chat
} from './Types.svelte'
import Prompts from './Prompts.svelte'
import Messages from './Messages.svelte'
- import { applyProfile, getProfile, getProfileSelect, prepareSummaryPrompt, getDefaultProfileKey } from './Profiles.svelte'
+ import { applyProfile, getProfile, prepareSummaryPrompt } from './Profiles.svelte'
import { afterUpdate, onMount } from 'svelte'
- import { replace } from 'svelte-spa-router'
import Fa from 'svelte-fa/src/fa.svelte'
import {
faArrowUpFromBracket,
faPaperPlane,
faGear,
faPenToSquare,
- faTrash,
faMicrophone,
- faLightbulb,
- faClone,
- faEllipsisVertical,
- faFloppyDisk,
- faThumbtack,
- faDownload,
- faUpload,
- faEraser,
- faRotateRight
+ faLightbulb
} from '@fortawesome/free-solid-svg-icons/index'
// import { encode } from 'gpt-tokenizer'
import { v4 as uuidv4 } from 'uuid'
- import { exportChatAsJSON, exportProfileAsJSON } from './Export.svelte'
- import { clickOutside } from 'svelte-use-click-outside'
import { countPromptTokens, getMaxModelPrompt, getPrice } from './Stats.svelte'
+ import { autoGrowInputOnEvent, sizeTextElements } from './Util.svelte'
+ import ChatSettingsModal from './ChatSettingsModal.svelte'
// This makes it possible to override the OpenAI API base URL in the .env file
const apiBase = import.meta.env.VITE_API_BASE || 'https://api.openai.com'
@@ -74,19 +52,33 @@
let chatNameSettings: HTMLFormElement
let recognition: any = null
let recording = false
- let chatFileInput
- let profileFileInput
- let showSettingsModal = 0
- let showProfileMenu = false
- let showChatMenu = false
-
- const settingsList = getChatSettingList()
- const modelSetting = getChatSettingObjectByKey('model') as ChatSetting & SettingSelect
- const chatDefaults = getChatDefaults()
$: chat = $chatsStorage.find((chat) => chat.id === chatId) as Chat
$: chatSettings = chat.settings
- $: globalStore = $globalStorage
+ let showSettingsModal
+
+ let scDelay
+ const onStateChange = (...args:any) => {
+ clearTimeout(scDelay)
+ setTimeout(() => {
+ if (chat.startSession) {
+ const profile = getProfile('') // get default profile
+ applyProfile(chatId, profile.profile as any)
+ if (chat.startSession) {
+ chat.startSession = false
+ saveChatStore()
+ // Auto start the session
+ submitForm(false, true)
+ }
+ }
+ if ($showSetChatSettings) {
+ $showSetChatSettings = false
+ showSettingsModal()
+ }
+ })
+ }
+
+ $: onStateChange($checkStateChange, $showSetChatSettings)
// Make sure chat object is ready to go
updateChatSettings(chatId)
@@ -452,14 +444,6 @@
}
}
- const deleteChat = () => {
- if (window.confirm('Are you sure you want to delete this chat?')) {
- replace('/').then(() => {
- chatsStorage.update((chats) => chats.filter((chat) => chat.id !== chatId))
- })
- }
- }
-
const showChatNameSettings = () => {
chatNameSettings.classList.add('is-active');
(chatNameSettings.querySelector('#settings-chat-name') as HTMLInputElement).focus();
@@ -480,78 +464,6 @@
chatNameSettings.classList.remove('is-active')
}
- const updateProfileSelectOptions = () => {
- const profileSelect = getChatSettingObjectByKey('profile') as ChatSetting & SettingSelect
- profileSelect.options = getProfileSelect()
- chatDefaults.profile = getDefaultProfileKey()
- // const defaultProfile = globalStore.defaultProfile || profileSelect.options[0].value
- }
-
- const refreshSettings = async () => {
- showSettingsModal && showSettings()
- }
-
- const showSettings = async () => {
- // Show settings modal
- showSettingsModal++
-
- // Get profile options
- updateProfileSelectOptions()
-
- // Refresh settings modal
- showSettingsModal++
-
- // Load available models from OpenAI
- const allModels = (await (
- await fetch(apiBase + '/v1/models', {
- method: 'GET',
- headers: {
- Authorization: `Bearer ${$apiKeyStorage}`,
- 'Content-Type': 'application/json'
- }
- })
- ).json()) as ResponseModels
- const filteredModels = supportedModels.filter((model) => allModels.data.find((m) => m.id === model))
-
- const modelOptions:SelectOption[] = filteredModels.reduce((a, m) => {
- const o:SelectOption = {
- value: m,
- text: m
- }
- a.push(o)
- return a
- }, [] as SelectOption[])
-
- // Update the models in the settings
- if (modelSetting) {
- modelSetting.options = modelOptions
- }
- // Refresh settings modal
- showSettingsModal++
-
- setTimeout(() => sizeTextElements, 100)
- }
-
- const sizeTextElements = () => {
- const els = document.querySelectorAll('textarea.auto-size')
- for (let i:number = 0, l = els.length; i < l; i++) autoGrowInput(els[i] as HTMLTextAreaElement)
- }
-
- const closeSettings = () => {
- showSettingsModal = 0
- showProfileMenu = false
- if (chat.startSession) {
- chat.startSession = false
- saveChatStore()
- submitForm(false, true)
- }
- }
-
- const clearSettings = () => {
- resetChatSettings(chatId)
- showSettingsModal++ // Make sure the dialog updates
- }
-
const recordToggle = () => {
// Check if already recording - if so, stop - else start
if (recording) {
@@ -562,147 +474,6 @@
}
}
- const debounce = {}
-
- const queueSettingValueChange = (event: Event, setting: ChatSetting) => {
- clearTimeout(debounce[setting.key])
- if (event.target === null) return
- const val = chatSettings[setting.key]
- const el = (event.target as HTMLInputElement)
- const doSet = () => {
- try {
- (typeof setting.beforeChange === 'function') && setting.beforeChange(chatId, setting, el.checked || el.value) &&
- refreshSettings()
- } catch (e) {
- window.alert('Unable to change:\n' + e.message)
- }
- switch (setting.type) {
- case 'boolean':
- setChatSettingValue(chatId, setting, el.checked)
- refreshSettings()
- break
- default:
- setChatSettingValue(chatId, setting, el.value)
- }
- try {
- (typeof setting.afterChange === 'function') && setting.afterChange(chatId, setting, chatSettings[setting.key]) &&
- refreshSettings()
- } catch (e) {
- setChatSettingValue(chatId, setting, val)
- window.alert('Unable to change:\n' + e.message)
- }
- }
- if (setting.key === 'profile' && chat.sessionStarted &&
- (getProfile(el.value).characterName !== chatSettings.characterName)) {
- const val = chatSettings[setting.key]
- if (window.confirm('Personality change will not correctly apply to existing chat session.\n Continue?')) {
- doSet()
- } else {
- // roll-back
- setChatSettingValue(chatId, setting, val)
- // refresh setting modal, if open
- showSettingsModal && showSettingsModal++
- }
- }
- debounce[setting.key] = setTimeout(doSet, 250)
- }
- const autoGrowInputOnEvent = (event: Event) => {
- // Resize the textarea to fit the content - auto is important to reset the height after deleting content
- if (event.target === null) return
- autoGrowInput(event.target as HTMLTextAreaElement)
- }
-
- const autoGrowInput = (el: HTMLTextAreaElement) => {
- el.style.height = '38px' // don't use "auto" here. Firefox will over-size.
- el.style.height = el.scrollHeight + 'px'
- }
-
- const saveProfile = () => {
- showProfileMenu = false
- try {
- saveCustomProfile(chat.settings)
- refreshSettings()
- } catch (e) {
- window.alert('Error saving profile: \n' + e.message)
- }
- }
-
- const newNameForProfile = (name:string):string => {
- const profiles = getProfileSelect()
- const nameMap = profiles.reduce((a, p) => { a[p.text] = p; return a }, {})
- if (!nameMap[name]) return name
- let i:number = 1
- let cname = name + `-${i}`
- while (nameMap[cname]) {
- i++
- cname = name + `-${i}`
- }
- return cname
- }
-
- const cloneProfile = () => {
- showProfileMenu = false
- const clone = JSON.parse(JSON.stringify(chat.settings))
- const name = chat.settings.profileName
- clone.profileName = newNameForProfile(name || '')
- clone.profile = null
- try {
- saveCustomProfile(clone)
- chat.settings.profile = clone.profile
- chat.settings.profileName = clone.profileName
- refreshSettings()
- } catch (e) {
- window.alert('Error cloning profile: \n' + e.message)
- }
- }
-
- const deleteProfile = () => {
- showProfileMenu = false
- try {
- deleteCustomProfile(chatId, chat.settings.profile as any)
- chat.settings.profile = globalStore.defaultProfile || ''
- saveChatStore()
- setGlobalSettingValueByKey('lastProfile', chat.settings.profile)
- applyProfile(chatId, chat.settings.profile as any)
- refreshSettings()
- } catch (e) {
- window.alert('Error deleting profile: \n' + e.message)
- }
- }
-
- const pinDefaultProfile = () => {
- showProfileMenu = false
- setGlobalSettingValueByKey('defaultProfile', chat.settings.profile)
- refreshSettings()
- }
-
- const importProfileFromFile = (e) => {
- const image = e.target.files[0]
- const reader = new FileReader()
- reader.readAsText(image)
- reader.onload = e => {
- const json = (e.target || {}).result as string
- try {
- const profile = JSON.parse(json)
- profile.profileName = newNameForProfile(profile.profileName || '')
- profile.profile = null
- saveCustomProfile(profile)
- refreshSettings()
- } catch (e) {
- window.alert('Unable to import profile: \n' + e.message)
- }
- }
- }
-
- const importChatFromFile = (e) => {
- const image = e.target.files[0]
- const reader = new FileReader()
- reader.readAsText(image)
- reader.onload = e => {
- const json = (e.target || {}).result as string
- addChatFromJSON(json)
- }
- }
-
{
- if (event.key === 'Escape') {
- closeSettings()
- closeChatNameSettings()
- showChatMenu = false
- }
- }}
-/>
-
-
-
-
-
{ showProfileMenu = false }}>
-
-
-
- {#key showSettingsModal}
- {#each settingsList as setting}
- {#if (typeof setting.hide !== 'function') || !setting.hide(chatId)}
- {#if setting.header}
-
- {/if}
-
- {#if setting.type === 'boolean'}
-
-
-
- {:else if setting.type === 'textarea'}
-
-
-
-
- {:else}
-
-
-
- {/if}
-
-
- {#if setting.type === 'number'}
-
queueSettingValueChange(e, setting)}
- />
- {:else if setting.type === 'select'}
-
-
-
- {:else if setting.type === 'text'}
-
- { queueSettingValueChange(e, setting) }}
- >
-
- {/if}
-
-
-
- {/if}
- {/each}
- {/key}
-
-
-
-
-
-
-
- importChatFromFile(e)} bind:this={chatFileInput} >
- importProfileFromFile(e)} bind:this={profileFileInput} >
+
+ {
+ if (event.key === 'Escape') {
+ closeChatNameSettings()
+ }
+ }}
+/>
+
diff --git a/src/lib/ChatOptionMenu.svelte b/src/lib/ChatOptionMenu.svelte
new file mode 100644
index 0000000..0df4efb
--- /dev/null
+++ b/src/lib/ChatOptionMenu.svelte
@@ -0,0 +1,118 @@
+
+
+ { showChatMenu = false }}>
+
+
+
+
+
+
+ importChatFromFile(e)} bind:this={chatFileInput} >
+
+ {
+ if (event.key === 'Escape') {
+ showChatMenu = false
+ }
+ }}
+/>
diff --git a/src/lib/ChatSettingsModal.svelte b/src/lib/ChatSettingsModal.svelte
new file mode 100644
index 0000000..e6812f6
--- /dev/null
+++ b/src/lib/ChatSettingsModal.svelte
@@ -0,0 +1,383 @@
+
+
+
+
+
+
+
{ showProfileMenu = false }}>
+
+
+
+ {#key showSettingsModal}
+ {#each settingsList as setting}
+ {#if (typeof setting.hide !== 'function') || !setting.hide(chatId)}
+ {#if setting.header}
+
+ {/if}
+
+ {#if setting.type === 'boolean'}
+
+
+
+ {:else if setting.type === 'textarea'}
+
+
+
+
+ {:else}
+
+
+
+ {/if}
+
+
+ {#if setting.type === 'number'}
+
queueSettingValueChange(e, setting)}
+ />
+ {:else if setting.type === 'select'}
+
+
+
+ {:else if setting.type === 'text'}
+
+ { queueSettingValueChange(e, setting) }}
+ >
+
+ {/if}
+
+
+
+ {/if}
+ {/each}
+ {/key}
+
+
+
+
+
+
+ importProfileFromFile(e)} bind:this={profileFileInput} >
+
+ {
+ if (event.key === 'Escape') {
+ closeSettings()
+ }
+ }}
+/>
\ No newline at end of file
diff --git a/src/lib/EditMessage.svelte b/src/lib/EditMessage.svelte
index 2f2be40..0a62dd3 100644
--- a/src/lib/EditMessage.svelte
+++ b/src/lib/EditMessage.svelte
@@ -7,6 +7,7 @@
import type { Message, Model, Chat } from './Types.svelte'
import Fa from 'svelte-fa/src/fa.svelte'
import { faTrash, faDiagramPredecessor, faDiagramNext } from '@fortawesome/free-solid-svg-icons/index'
+ import { scrollIntoViewWithOffset } from './Util.svelte'
export let message:Message
export let chatId:number
@@ -102,7 +103,7 @@
}
const el = document.getElementById('message-' + uuid)
if (el) {
- el.scrollIntoView({ behavior: 'smooth' })
+ scrollIntoViewWithOffset(el, 60)
} else {
console.error("Can't find element with message ID", uuid)
}
@@ -205,7 +206,6 @@
.message-note {
padding-top: .6em;
margin-bottom: -0.6em;
- opacity: 0.5;
}
.message-edit {
display: block;
diff --git a/src/lib/Home.svelte b/src/lib/Home.svelte
index ec3d765..14a4bf4 100644
--- a/src/lib/Home.svelte
+++ b/src/lib/Home.svelte
@@ -1,7 +1,8 @@
diff --git a/src/lib/Navbar.svelte b/src/lib/Navbar.svelte
index a8f0d0b..2814155 100644
--- a/src/lib/Navbar.svelte
+++ b/src/lib/Navbar.svelte
@@ -1,12 +1,21 @@
-