From da5745ffdf0423d2f1857a90bb0fbe2ae3a16964 Mon Sep 17 00:00:00 2001 From: Webifi Date: Mon, 29 May 2023 17:57:14 -0500 Subject: [PATCH] Componentize Settings - UI Changes --- src/App.svelte | 3 +- src/app.scss | 41 ++- src/lib/Chat.svelte | 499 +++---------------------------- src/lib/ChatOptionMenu.svelte | 118 ++++++++ src/lib/ChatSettingsModal.svelte | 383 ++++++++++++++++++++++++ src/lib/EditMessage.svelte | 4 +- src/lib/Home.svelte | 5 +- src/lib/Navbar.svelte | 13 +- src/lib/Sidebar.svelte | 39 +-- src/lib/Storage.svelte | 5 +- src/lib/Util.svelte | 27 ++ 11 files changed, 648 insertions(+), 489 deletions(-) create mode 100644 src/lib/ChatOptionMenu.svelte create mode 100644 src/lib/ChatSettingsModal.svelte create mode 100644 src/lib/Util.svelte diff --git a/src/App.svelte b/src/App.svelte index 192ac00..efe1dcc 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -37,10 +37,11 @@ '*': Home } + document.body.classList.add('has-navbar-fixed-top') - +
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 - } - }} -/> - - -