Merge pull request #160 from Webifi/main

Force short suggested names; rename from chat menu; hidden prompt prefix
This commit is contained in:
Niek van der Maas 2023-06-10 05:09:48 +02:00 committed by GitHub
commit 284589b6f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 127 additions and 30 deletions

View File

@ -231,16 +231,16 @@ $modal-background-background-color-dark: rgba($dark, 0.86) !default; // remove t
.menu-list { .menu-list {
a:hover { a:hover {
.delete-button { .delete-button, .edit-button {
display: block !important; display: block !important;
background-color: initial; background-color: initial;
} }
} }
.delete-button { .delete-button, .edit-button {
opacity: .8; opacity: .8;
} }
.delete-button:hover { .delete-button:hover, .edit-button {
opacity: 1; opacity: 1;
} }
} }
@ -321,6 +321,17 @@ $modal-background-background-color-dark: rgba($dark, 0.86) !default; // remove t
z-index: 200; z-index: 200;
} }
.chat-menu-item .edit-button {
position: absolute;
right: 2em;
z-index: 200;
}
.chat-name-editor {
margin: .5em;
padding:.1em;
}
/* Overrides for main layout */ /* Overrides for main layout */
.side-bar-column { .side-bar-column {

View File

@ -24,7 +24,7 @@
} from './Types.svelte' } from './Types.svelte'
import Prompts from './Prompts.svelte' import Prompts from './Prompts.svelte'
import Messages from './Messages.svelte' import Messages from './Messages.svelte'
import { prepareSummaryPrompt, restartProfile } from './Profiles.svelte' import { mergeProfileFields, prepareSummaryPrompt, restartProfile } from './Profiles.svelte'
import { afterUpdate, onMount, onDestroy } from 'svelte' import { afterUpdate, onMount, onDestroy } from 'svelte'
import Fa from 'svelte-fa/src/fa.svelte' import Fa from 'svelte-fa/src/fa.svelte'
@ -37,7 +37,7 @@
faLightbulb, faLightbulb,
faCommentSlash faCommentSlash
} from '@fortawesome/free-solid-svg-icons/index' } from '@fortawesome/free-solid-svg-icons/index'
// import { encode } from 'gpt-tokenizer' import { encode } from 'gpt-tokenizer'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { countPromptTokens, getModelMaxTokens, getPrice } from './Stats.svelte' import { countPromptTokens, getModelMaxTokens, getPrice } from './Stats.svelte'
import { autoGrowInputOnEvent, scrollToMessage, sizeTextElements } from './Util.svelte' import { autoGrowInputOnEvent, scrollToMessage, sizeTextElements } from './Util.svelte'
@ -184,10 +184,17 @@
let filtered = messages.filter(messageFilter) let filtered = messages.filter(messageFilter)
// Get an estimate of the total prompt size we're sending // Get an estimate of the total prompt size we're sending
const promptTokenCount:number = countPromptTokens(filtered, model) let promptTokenCount:number = countPromptTokens(filtered, model)
let summarySize = chatSettings.summarySize let summarySize = chatSettings.summarySize
const hiddenPromptPrefix = mergeProfileFields(chatSettings, chatSettings.hiddenPromptPrefix).trim()
if (hiddenPromptPrefix && filtered.length && filtered[filtered.length - 1].role === 'user') {
// update estimate with hiddenPromptPrefix token count
promptTokenCount += encode(hiddenPromptPrefix + '\n\n').length
}
// console.log('Estimated',promptTokenCount,'prompt token for this request') // console.log('Estimated',promptTokenCount,'prompt token for this request')
if (chatSettings.continuousChat && !opts.didSummary && if (chatSettings.continuousChat && !opts.didSummary &&
@ -340,10 +347,21 @@
} }
} }
const messagePayload = filtered.map((m, i) => {
const r = { role: m.role, content: m.content }
if (i === filtered.length - 1 && m.role === 'user' && hiddenPromptPrefix && !opts.summaryRequest) {
// If the last prompt is a user prompt, and we have a hiddenPromptPrefix, inject it
r.content = hiddenPromptPrefix + '\n\n' + m.content
}
return r
}) as Message[]
// Update token count with actual
promptTokenCount = countPromptTokens(messagePayload, model)
try { try {
const request: Request = { const request: Request = {
messages: filtered.map(m => { return { role: m.role, content: m.content } }) as Message[], messages: messagePayload,
// Provide the settings by mapping the settingsMap to key/value pairs // Provide the settings by mapping the settingsMap to key/value pairs
...getRequestSettingList().reduce((acc, setting) => { ...getRequestSettingList().reduce((acc, setting) => {
const key = setting.key const key = setting.key
@ -351,16 +369,15 @@
if (typeof setting.apiTransform === 'function') { if (typeof setting.apiTransform === 'function') {
value = setting.apiTransform(chatId, setting, value) value = setting.apiTransform(chatId, setting, value)
} }
if (opts.summaryRequest && opts.maxTokens) { if (opts.maxTokens) {
// requesting summary. do overrides if (key === 'max_tokens') value = opts.maxTokens // only as large as requested
if (key === 'max_tokens') value = opts.maxTokens // only as large as we need for summary
if (key === 'n') value = 1 // never more than one completion for summary
} }
if (opts.streaming) { if (opts.streaming || opts.summaryRequest) {
/* /*
Streaming goes insane with more than one completion. Streaming goes insane with more than one completion.
Doesn't seem like there's any way to separate the jumbled mess of deltas for the Doesn't seem like there's any way to separate the jumbled mess of deltas for the
different completions. different completions.
Summary should only have one completion
*/ */
if (key === 'n') value = 1 if (key === 'n') value = 1
} }
@ -391,7 +408,11 @@
let errorResponse let errorResponse
try { try {
const errObj = await response.json() const errObj = await response.json()
errorResponse = errObj?.error?.code || 'Unexpected Response' errorResponse = errObj?.error?.code
if (!errorResponse && response.choices && response.choices[0]) {
errorResponse = response.choices[0]?.message?.content
}
errorResponse = errorResponse || 'Unexpected Response'
} catch (e) { } catch (e) {
errorResponse = 'Unknown Response' errorResponse = 'Unknown Response'
} }
@ -555,7 +576,7 @@
const suggestName = async (): Promise<void> => { const suggestName = async (): Promise<void> => {
const suggestMessage: Message = { const suggestMessage: Message = {
role: 'user', role: 'user',
content: "Can you give me a 5 word summary of this conversation's topic?", content: "Using appropriate language, please give a 5 word summary of this conversation's topic.",
uuid: uuidv4() uuid: uuidv4()
} }
@ -565,7 +586,9 @@
const response = await sendRequest(suggestMessages, { const response = await sendRequest(suggestMessages, {
chat, chat,
autoAddMessages: false, autoAddMessages: false,
streaming: false streaming: false,
summaryRequest: true,
maxTokens: 10
}) })
await response.promiseToFinish() await response.promiseToFinish()
@ -577,10 +600,10 @@
}) })
} else { } else {
response.getMessages().forEach(m => { response.getMessages().forEach(m => {
chat.name = m.content const name = m.content.split(/\s+/).slice(0, 8).join(' ').trim()
if (name) chat.name = name
}) })
saveChatStore() saveChatStore()
$checkStateChange++
} }
} }

View File

@ -1,19 +1,52 @@
<script lang="ts"> <script lang="ts">
import { replace } from 'svelte-spa-router' import { replace } from 'svelte-spa-router'
import type { Chat } from './Types.svelte' import type { Chat } from './Types.svelte'
import { apiKeyStorage, deleteChat, pinMainMenu } from './Storage.svelte' import { apiKeyStorage, deleteChat, pinMainMenu, saveChatStore } from './Storage.svelte'
import Fa from 'svelte-fa/src/fa.svelte' import Fa from 'svelte-fa/src/fa.svelte'
import { faTrash, faCircleCheck } from '@fortawesome/free-solid-svg-icons/index' import { faTrash, faCircleCheck, faPencil } from '@fortawesome/free-solid-svg-icons/index'
import { faMessage } from '@fortawesome/free-regular-svg-icons/index' import { faMessage } from '@fortawesome/free-regular-svg-icons/index'
import { onMount } from 'svelte'
export let chat:Chat export let chat:Chat
export let activeChatId:number|undefined export let activeChatId:number|undefined
export let prevChat:Chat|undefined export let prevChat:Chat|undefined
export let nextChat:Chat|undefined export let nextChat:Chat|undefined
let editing:boolean = false
let original:string
let waitingForConfirm:any = 0 let waitingForConfirm:any = 0
function delChat () { onMount(async () => {
if (!chat.name) {
chat.name = `Chat ${chat.id}`
}
})
const keydown = (event:KeyboardEvent) => {
if (event.key === 'Escape') {
event.stopPropagation()
event.preventDefault()
chat.name = original
editing = false
}
if (event.key === 'Tab' || event.key === 'Enter') {
event.stopPropagation()
event.preventDefault()
update()
}
}
const update = () => {
editing = false
if (!chat.name) {
chat.name = original
return
}
saveChatStore()
}
const delChat = () => {
if (!waitingForConfirm) { if (!waitingForConfirm) {
// wait a second for another click to avoid accidental deletes // wait a second for another click to avoid accidental deletes
waitingForConfirm = setTimeout(() => { waitingForConfirm = 0 }, 1000) waitingForConfirm = setTimeout(() => { waitingForConfirm = 0 }, 1000)
@ -35,15 +68,33 @@
} }
} }
const edit = () => {
original = chat.name
editing = true
setTimeout(() => {
const el = document.getElementById(`chat-menu-item-${chat.id}`)
el && el.focus()
}, 0)
}
</script> </script>
<li> <li>
<a class="chat-menu-item" href={`#/chat/${chat.id}`} on:click={() => { $pinMainMenu = false }} class:is-waiting={waitingForConfirm} class:is-disabled={!$apiKeyStorage} class:is-active={activeChatId === chat.id}> {#if editing}
<div id="chat-menu-item-{chat.id}" class="chat-name-editor" on:keydown={keydown} contenteditable bind:innerText={chat.name} on:blur={update} />
{:else}
<a
href={`#/chat/${chat.id}`}
class="chat-menu-item"
class:is-waiting={waitingForConfirm} class:is-disabled={!$apiKeyStorage} class:is-active={activeChatId === chat.id}
on:click={() => { $pinMainMenu = false }} >
{#if waitingForConfirm} {#if waitingForConfirm}
<a class="is-pulled-right is-hidden px-1 py-0 has-text-weight-bold delete-button" href={'$'} on:click|preventDefault={() => delChat()}><Fa icon={faCircleCheck} /></a> <a class="is-pulled-right is-hidden px-1 py-0 has-text-weight-bold delete-button" href={'$'} on:click|preventDefault={() => delChat()}><Fa icon={faCircleCheck} /></a>
{:else} {:else}
<a class="is-pulled-right is-hidden px-1 py-0 has-text-weight-bold edit-button" href={'$'} on:click|preventDefault={() => edit()}><Fa icon={faPencil} /></a>
<a class="is-pulled-right is-hidden px-1 py-0 has-text-weight-bold delete-button" href={'$'} on:click|preventDefault={() => delChat()}><Fa icon={faTrash} /></a> <a class="is-pulled-right is-hidden px-1 py-0 has-text-weight-bold delete-button" href={'$'} on:click|preventDefault={() => delChat()}><Fa icon={faTrash} /></a>
{/if} {/if}
<span class="chat-item-name"><Fa class="mr-2 chat-icon" size="xs" icon="{faMessage}"/>{chat.name || `Chat ${chat.id}`}</span> <span class="chat-item-name"><Fa class="mr-2 chat-icon" size="xs" icon="{faMessage}"/>{chat.name || `Chat ${chat.id}`}</span>
</a> </a>
{/if}
</li> </li>

View File

@ -70,22 +70,25 @@ export const getProfile = (key:string, forReset:boolean = false):ChatSettings =>
return clone return clone
} }
export const mergeProfileFields = (settings: ChatSettings, content: string|undefined, maxWords: number|undefined = undefined): string => {
if (!content?.toString) return ''
content = (content + '').replaceAll('[[CHARACTER_NAME]]', settings.characterName || 'ChatGPT')
if (maxWords) content = (content + '').replaceAll('[[MAX_WORDS]]', maxWords.toString())
return content
}
export const prepareProfilePrompt = (chatId:number) => { export const prepareProfilePrompt = (chatId:number) => {
const settings = getChatSettings(chatId) const settings = getChatSettings(chatId)
const characterName = settings.characterName return mergeProfileFields(settings, settings.systemPrompt).trim()
const currentProfilePrompt = settings.systemPrompt
return currentProfilePrompt.replaceAll('[[CHARACTER_NAME]]', characterName)
} }
export const prepareSummaryPrompt = (chatId:number, promptsSize:number, maxTokens:number|undefined = undefined) => { export const prepareSummaryPrompt = (chatId:number, promptsSize:number, maxTokens:number|undefined = undefined) => {
const settings = getChatSettings(chatId) const settings = getChatSettings(chatId)
const characterName = settings.characterName || 'ChatGPT'
maxTokens = maxTokens || settings.summarySize maxTokens = maxTokens || settings.summarySize
maxTokens = Math.min(Math.floor(promptsSize / 4), maxTokens) // Make sure we're shrinking by at least a 4th maxTokens = Math.min(Math.floor(promptsSize / 4), maxTokens) // Make sure we're shrinking by at least a 4th
const currentSummaryPrompt = settings.summaryPrompt const currentSummaryPrompt = settings.summaryPrompt
return currentSummaryPrompt // ~.75 words per token. May need to reduce
.replaceAll('[[CHARACTER_NAME]]', characterName) return mergeProfileFields(settings, currentSummaryPrompt, Math.floor(maxTokens * 0.75)).trim()
.replaceAll('[[MAX_WORDS]]', Math.floor(maxTokens * 0.75).toString()) // ~.75 words per token. May need to reduce
} }
// Restart currently loaded profile // Restart currently loaded profile

View File

@ -84,6 +84,7 @@ const defaults:ChatSettings = {
systemPrompt: '', systemPrompt: '',
autoStartSession: false, autoStartSession: false,
trainingPrompts: [], trainingPrompts: [],
hiddenPromptPrefix: '',
// useResponseAlteration: false, // useResponseAlteration: false,
// responseAlterations: [], // responseAlterations: [],
isDirty: false isDirty: false
@ -167,6 +168,14 @@ const systemPromptSettings: ChatSetting[] = [
type: 'textarea', type: 'textarea',
hide: (chatId) => !getChatSettings(chatId).useSystemPrompt hide: (chatId) => !getChatSettings(chatId).useSystemPrompt
}, },
{
key: 'hiddenPromptPrefix',
name: 'Hidden Prompt Prefix',
title: 'A prompt that will be silently injected before every user prompt.',
placeholder: 'Enter user prompt prefix here. You can remind ChatGPT how to act.',
type: 'textarea',
hide: (chatId) => !getChatSettings(chatId).useSystemPrompt
},
{ {
key: 'trainingPrompts', key: 'trainingPrompts',
name: 'Training Prompts', name: 'Training Prompts',

View File

@ -58,7 +58,6 @@
profileName: string, profileName: string,
profileDescription: string, profileDescription: string,
continuousChat: (''|'fifo'|'summary'); continuousChat: (''|'fifo'|'summary');
// useSummarization: boolean;
summaryThreshold: number; summaryThreshold: number;
summarySize: number; summarySize: number;
pinTop: number; pinTop: number;
@ -67,6 +66,7 @@
useSystemPrompt: boolean; useSystemPrompt: boolean;
systemPrompt: string; systemPrompt: string;
autoStartSession: boolean; autoStartSession: boolean;
hiddenPromptPrefix: string;
trainingPrompts?: Message[]; trainingPrompts?: Message[];
useResponseAlteration?: boolean; useResponseAlteration?: boolean;
responseAlterations?: ResponseAlteration[]; responseAlterations?: ResponseAlteration[];