Merge pull request #160 from Webifi/main
Force short suggested names; rename from chat menu; hidden prompt prefix
This commit is contained in:
commit
284589b6f3
17
src/app.scss
17
src/app.scss
|
@ -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 {
|
||||||
|
|
|
@ -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++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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[];
|
||||||
|
|
Loading…
Reference in New Issue