Track change state of profile editor

This commit is contained in:
Webifi 2023-06-03 07:28:51 -05:00
parent f8fc158861
commit dfe30b12bd
5 changed files with 96 additions and 33 deletions

View File

@ -1,25 +1,54 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { getProfile } from './Profiles.svelte'
import { setChatSettingValue } from './Storage.svelte'
import type { Chat, ChatSetting, ChatSettings } from './Types.svelte'
import { cleanSettingValue, setChatSettingValue } from './Storage.svelte'
import type { Chat, ChatSetting, ChatSettings, SettingPrompt } from './Types.svelte'
import { autoGrowInputOnEvent } from './Util.svelte'
export let setting:ChatSetting
export let chatSettings:ChatSettings
export let chat:Chat
export let chatDefaults:Record<string, any>
export let defaultProfile:String
export let originalProfile:String
const chatId = chat.id
if (originalProfile) {
// eventually...
}
const dispatch = createEventDispatcher()
const refreshSettings = () => {
dispatch('refresh')
}
let debounce:any
const settingChecks:Record<string, SettingPrompt[]> = {
'profile': [
{
prompt:'Unsaved changes to the current profile will be lost.\n Continue?',
fn: (setting, newVal, oldVal) => {
return !!chatSettings.isDirty && newVal !== oldVal
},
passed: false,
},
{
prompt:'Personality change will not correctly apply to existing chat session.\n Continue?',
fn: (setting, newVal, oldVal) => {
return chat.sessionStarted && newVal !== originalProfile &&
(getProfile(newVal).characterName !== chatSettings.characterName)
},
passed: false,
},
]
}
const resetSettingCheck = (key:keyof ChatSettings) => {
const checks = settingChecks[key]
checks && checks.forEach((c)=>{c.passed=false})
}
let debounce: any
const queueSettingValueChange = (event: Event, setting: ChatSetting) => {
clearTimeout(debounce)
@ -44,26 +73,42 @@
const newVal = chatSettings[setting.key]
if (val === newVal) return
try {
(typeof setting.afterChange === 'function') && setting.afterChange(chatId, setting, chatSettings[setting.key]) &&
if((typeof setting.afterChange === 'function') && setting.afterChange(chatId, setting, chatSettings[setting.key])){
console.log('Refreshed from setting', setting.key, chatSettings[setting.key], val)
refreshSettings()
}
} catch (e) {
setChatSettingValue(chatId, setting, val)
window.alert('Unable to change:\n' + e.message)
}
dispatch('change', setting)
}
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()
const checks = settingChecks[setting.key] || []
const newVal = cleanSettingValue(setting.type, el.checked||el.value)
for (let i = 0, l = checks.length; i < l; i++) {
let c = checks[i]
if(c.passed) continue
if (c.fn(setting, newVal, val)) {
// eventually this needs to be an async call to a confirmation modal where
// "passed", not really being used here, will be reworked to some other
// state to deal with inevitable issues once a non-blocking modal is used.
if (window.confirm(c.prompt)) {
c.passed = true
} else {
// roll-back
setChatSettingValue(chatId, setting, val)
// refresh setting modal, if open
refreshSettings()
resetSettingCheck(setting.key)
return
}
} else {
c.passed = true
}
}
// passed all
resetSettingCheck(setting.key)
debounce = setTimeout(doSet, 250)
}

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { applyProfile, getDefaultProfileKey, getProfile, getProfileSelect } from './Profiles.svelte'
import { getChatDefaults, getChatSettingList, getChatSettingObjectByKey } from './Settings.svelte'
import { getChatDefaults, getChatSettingList, getChatSettingObjectByKey, getExcludeFromProfile } from './Settings.svelte'
import {
saveChatStore,
apiKeyStorage,
@ -44,11 +44,14 @@
const settingsList = getChatSettingList()
const modelSetting = getChatSettingObjectByKey('model') as ChatSetting & SettingSelect
const chatDefaults = getChatDefaults()
const excludeFromProfile = getExcludeFromProfile()
$: chat = $chatsStorage.find((chat) => chat.id === chatId) as Chat
$: chatSettings = chat.settings
$: globalStore = $globalStorage
let originalProfile = chatSettings && chatSettings.profile
afterUpdate(() => {
sizeTextElements()
})
@ -66,6 +69,7 @@
const refreshSettings = async () => {
showSettingsModal && showSettings()
setDirty()
}
const cloneProfile = () => {
@ -197,26 +201,33 @@
return cname
}
const setDirty = (e:CustomEvent) => {
// excludeFromProfile
const deepEqual = (x:any, y:any) => {
const ok = Object.keys, tx = typeof x, ty = typeof y
return x && y && tx === 'object' && tx === ty ? (
ok(x).every(key => excludeFromProfile[key] || deepEqual(x[key], y[key]))
) : (x === y || ((x===undefined||x===null||x===false) && (y===undefined||y===null||y===false)))
}
const setDirty = (e:CustomEvent|undefined = undefined) => {
if (e) {
const setting = e.detail as ChatSetting
const key = setting.key
if (key === 'profile') return
}
const profile = getProfile(chatSettings.profile)
const isDirty = profile[key] !== chatSettings[key]
console.log('Is dirty?', setting, isDirty, profile[key], chatSettings[key])
chatSettings.isDirty = isDirty
chatSettings.isDirty = !deepEqual(profile, chatSettings)
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="modal" class:is-active={showSettingsModal}>
<div class="modal-background" on:click={closeSettings} />
<div class="modal-card" on:click={() => { showProfileMenu = false }}>
<header class="modal-card-head">
<p class="modal-card-title">Chat Settings</p>
<div class="dropdown is-right" class:is-active={showProfileMenu}>
<div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu3" on:click|preventDefault|stopPropagation={() => { showProfileMenu = !showProfileMenu }}>
@ -256,7 +267,7 @@
<section class="modal-card-body">
{#key showSettingsModal}
{#each settingsList as setting}
<ChatSettingField on:refresh={refreshSettings} on:change={setDirty} chat={chat} chatDefaults={chatDefaults} chatSettings={chatSettings} setting={setting} defaultProfile={defaultProfile} />
<ChatSettingField on:refresh={refreshSettings} on:change={setDirty} chat={chat} chatDefaults={chatDefaults} chatSettings={chatSettings} setting={setting} originalProfile={originalProfile} />
{/each}
{/key}
</section>

View File

@ -100,12 +100,10 @@ const profileSetting: ChatSetting & SettingSelect = {
options: [], // Set by Profiles
type: 'select',
afterChange: (chatId, setting) => {
applyProfile(chatId, '', !getChat(chatId).sessionStarted)
applyProfile(chatId)
return true // Signal we should refresh the setting modal
},
setDefault: (chatId, setting, value) => {
}
setDefault: (chatId, setting, value) => {},
}
// Settings that will not be part of the API request
@ -230,7 +228,8 @@ const modelSetting: ChatSetting & SettingSelect = {
headerClass: 'is-warning',
options: [],
type: 'select',
forceApi: true // Need to make sure we send this
forceApi: true, // Need to make sure we send this
afterChange: (chatId, setting) => true, // refresh settings
}
const chatSettingsList: ChatSetting[] = [
@ -275,10 +274,9 @@ const chatSettingsList: ChatSetting[] = [
'The token count of your prompt plus max_tokens cannot exceed the model\'s context length. Most models have a context length of 2048 tokens (except for the newest models, which support 4096).\n',
min: 1,
max: 32768,
step: 128,
step: 1,
type: 'number',
forceApi: true, // Since default here is different than gpt default, will make sure we always send it
afterChange: (chatId, setting) => true, // refresh settings
},
{
key: 'presence_penalty',

View File

@ -170,4 +170,10 @@ type SettingBoolean = {
headerClass?: string;
} & (SettingNumber | SettingSelect | SettingBoolean | SettingText | SettingOther);
export type SettingPrompt = {
prompt: string;
fn: (setting:ChatSetting, newVal:any, oldVal:any)=>boolean;
passed: boolean;
};
</script>

View File

@ -7,12 +7,15 @@
export 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
(event.target as any).__didAutoGrow = false
autoGrowInput(event.target as HTMLTextAreaElement)
}
export const autoGrowInput = (el: HTMLTextAreaElement) => {
el.style.height = '38px' // don't use "auto" here. Firefox will over-size.
const anyEl = el as any // Oh how I hate typescript. All the markup of Java with no real payoff..
if (!anyEl.__didAutoGrow) el.style.height = '38px' // don't use "auto" here. Firefox will over-size.
el.style.height = el.scrollHeight + 'px'
anyEl.__didAutoGrow = true // don't resize this one again unless it's via an event
}
export const scrollIntoViewWithOffset = (element:HTMLElement, offset:number) => {