From 40daf1ca1d04902997aa9eed67f3beffcb44ecdd Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Wed, 27 Mar 2024 03:36:09 +0100 Subject: [PATCH 01/99] Bulk edit tag improvements - Show mutual tags on bulk edit - Update tag list on tag added/removed in bulk edit - Add "remove mutual" button to bulk edit tags --- public/scripts/BulkEditOverlay.js | 85 +++++++++++++++++++++++++++---- public/scripts/tags.js | 84 ++++++++++++++++++++++-------- public/style.css | 8 +++ 3 files changed, 144 insertions(+), 33 deletions(-) diff --git a/public/scripts/BulkEditOverlay.js b/public/scripts/BulkEditOverlay.js index eb69f279b..d11b56aa9 100644 --- a/public/scripts/BulkEditOverlay.js +++ b/public/scripts/BulkEditOverlay.js @@ -15,7 +15,7 @@ import { import { favsToHotswap } from './RossAscends-mods.js'; import { hideLoader, showLoader } from './loader.js'; import { convertCharacterToPersona } from './personas.js'; -import { createTagInput, getTagKeyForEntity, tag_map } from './tags.js'; +import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap } from './tags.js'; // Utility object for popup messages. const popupMessage = { @@ -193,8 +193,9 @@ class BulkTagPopupHandler { return `
-

Add tags to ${characterIds.length} characters

-
+

Modify tags of ${characterIds.length} characters

+ This popup allows you to modify the mutual tags of all selected characters. +
@@ -203,8 +204,15 @@ class BulkTagPopupHandler {
+ + -
@@ -215,13 +223,47 @@ class BulkTagPopupHandler { /** * Append and show the tag control * - * @param characters - The characters assigned to this control + * @param characterIds - The characters assigned to this control */ - static show(characters) { - document.body.insertAdjacentHTML('beforeend', this.#getHtml(characters)); - createTagInput('#bulkTagInput', '#bulkTagList'); + static show(characterIds) { + if (characterIds.length == 0) { + console.log('No characters selected for bulk edit tags.'); + return; + } + + document.body.insertAdjacentHTML('beforeend', this.#getHtml(characterIds)); + + this.mutualTags = this.getMutualTags(characterIds); + + // Print the tag list with all mutuable tags, marking them as removable. That is the initial fill + printTagList($('#bulkTagList'), { tags: () => this.getMutualTags(characterIds), tagOptions: { removable: true } }); + + // Tag input with empty tags so new tag gets added and it doesn't get emptied on redraw + createTagInput('#bulkTagInput', '#bulkTagList', { tags: () => this.getMutualTags(characterIds), tagOptions: { removable: true }}); + + document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this, characterIds)); + document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this, characterIds)); document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this)); - document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this, characters)); + } + + static getMutualTags(characterIds) { + if (characterIds.length == 0) { + return []; + } + + if (characterIds.length === 1) { + // Just use tags of the single character + return getTagsList(getTagKeyForEntity(characterIds[0])); + } + + // Find mutual tags for multiple characters + const allTags = characterIds.map(c => getTagsList(getTagKeyForEntity(c))); + const mutualTags = allTags.reduce((mutual, characterTags) => + mutual.filter(tag => characterTags.some(cTag => cTag.id === tag.id)) + ); + + this.mutualTags = mutualTags.sort(compareTagsForSort); + return this.mutualTags; } /** @@ -242,10 +284,31 @@ class BulkTagPopupHandler { * @param characterIds */ static resetTags(characterIds) { - characterIds.forEach((characterId) => { + for (const characterId of characterIds) { const key = getTagKeyForEntity(characterId); if (key) tag_map[key] = []; - }); + } + + $('#bulkTagList').empty(); + + printCharacters(true); + } + + /** + * Empty the tag map for the given characters + * + * @param characterIds + */ + static removeMutual(characterIds) { + const mutualTags = this.getMutualTags(characterIds); + + for (const characterId of characterIds) { + for(const tag of mutualTags) { + removeTagFromMap(tag.id, characterId); + } + } + + $('#bulkTagList').empty(); printCharacters(true); } diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 2615d87b3..8c92cd265 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -36,6 +36,7 @@ export { importTags, sortTags, compareTagsForSort, + removeTagFromMap, }; const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter'; @@ -57,12 +58,12 @@ export const tag_filter_types = { }; const ACTIONABLE_TAGS = { - FAV: { id: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: filterByFav, icon: 'fa-solid fa-star', class: 'filterByFavorites' }, - GROUP: { id: 0, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' }, - FOLDER: { id: 4, name: 'Always show folders', color: 'rgba(120, 120, 120, 0.5)', action: filterByFolder, icon: 'fa-solid fa-folder-plus', class: 'filterByFolder' }, - VIEW: { id: 2, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' }, - HINT: { id: 3, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' }, - UNFILTER: { id: 5, name: 'Clear all filters', action: onClearAllFiltersClick, icon: 'fa-solid fa-filter-circle-xmark', class: 'clearAllFilters' }, + FAV: { id: 1, sort_order: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: filterByFav, icon: 'fa-solid fa-star', class: 'filterByFavorites' }, + GROUP: { id: 0, sort_order: 2, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' }, + FOLDER: { id: 4, sort_order: 3, name: 'Always show folders', color: 'rgba(120, 120, 120, 0.5)', action: filterByFolder, icon: 'fa-solid fa-folder-plus', class: 'filterByFolder' }, + VIEW: { id: 2, sort_order: 4, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' }, + HINT: { id: 3, sort_order: 5, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' }, + UNFILTER: { id: 5, sort_order: 6, name: 'Clear all filters', action: onClearAllFiltersClick, icon: 'fa-solid fa-filter-circle-xmark', class: 'clearAllFilters' }, }; const InListActionable = { @@ -390,7 +391,15 @@ function findTag(request, resolve, listSelector) { resolve(result); } -function selectTag(event, ui, listSelector) { +/** + * Select a tag and add it to the list. This function is mostly used as an event handler for the tag selector control. + * @param {*} event - + * @param {*} ui - + * @param {*} listSelector - The selector of the list to print/add to + * @param {PrintTagListOptions} [tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before. + * @returns {boolean} false, to keep the input clear + */ +function selectTag(event, ui, listSelector, tagListOptions = {}) { let tagName = ui.item.value; let tag = tags.find(t => t.name === tagName); @@ -414,9 +423,28 @@ function selectTag(event, ui, listSelector) { saveSettingsDebounced(); + // If we have a manual list of tags to print, we should add this tag here to that manual list, otherwise it may not get printed + if (tagListOptions.tags !== undefined) { + const tagExists = (tags, tag) => tags.some(x => x.id === tag.id); + + if (typeof tagListOptions.tags === 'function') { + // If 'tags' is a function, wrap it to include new tag upon invocation + const originalTagsFunction = tagListOptions.tags; + tagListOptions.tags = () => { + const currentTags = originalTagsFunction(); + return tagExists(currentTags, tag) ? currentTags : [...currentTags, tag]; + }; + } else { + tagListOptions.tags = tagExists(tagListOptions.tags, tag) ? tags : [...tagListOptions.tags, tag]; + } + } + // add tag to the UI and internal map - we reprint so sorting and new markup is done correctly - printTagList(listSelector, { tagOptions: { removable: true } }); - printTagList($(getInlineListSelector())); + printTagList(listSelector, tagListOptions); + const inlineSelector = getInlineListSelector(); + if (inlineSelector) { + printTagList($(inlineSelector), tagListOptions); + } printTagFilters(tag_filter_types.character); printTagFilters(tag_filter_types.group_member); @@ -492,7 +520,7 @@ function createNewTag(tagName) { } /** - * @typedef {object} TagOptions + * @typedef {object} TagOptions - Options for tag behavior. (Same object will be passed into "appendTagToList") * @property {boolean} [removable=false] - Whether tags can be removed. * @property {boolean} [selectable=false] - Whether tags can be selected. * @property {function} [action=undefined] - Action to perform on tag interaction. @@ -500,20 +528,24 @@ function createNewTag(tagName) { * @property {boolean} [skipExistsCheck=false] - If true, the tag gets added even if a tag with the same id already exists. */ +/** + * @typedef {object} PrintTagListOptions - Optional parameters for printing the tag list. + * @property {Array|function(): Array} [tags=undefined] Optional override of tags that should be printed. Those will not be sorted. If no supplied, tags for the relevant character are printed. Can also be a function that returns the tags. + * @property {object|number|string} [forEntityOrKey=undefined] - Optional override for the chosen entity, otherwise the currently selected is chosen. Can be an entity with id property (character, group, tag), or directly an id or tag key. + * @property {boolean} [empty=true] - Whether the list should be initially empty. + * @property {function(object): function} [tagActionSelector=undefined] - An optional override for the action property that can be assigned to each tag via tagOptions. + * If set, the selector is executed on each tag as input argument. This allows a list of tags to be provided and each tag can have it's action based on the tag object itself. + * @property {TagOptions} [tagOptions={}] - Options for tag behavior. (Same object will be passed into "appendTagToList") + */ + /** * Prints the list of tags. * @param {JQuery} element - The container element where the tags are to be printed. - * @param {object} [options] - Optional parameters for printing the tag list. - * @param {Array} [options.tags] Optional override of tags that should be printed. Those will not be sorted. If no supplied, tags for the relevant character are printed. - * @param {object|number|string} [options.forEntityOrKey=undefined] - Optional override for the chosen entity, otherwise the currently selected is chosen. Can be an entity with id property (character, group, tag), or directly an id or tag key. - * @param {boolean} [options.empty=true] - Whether the list should be initially empty. - * @param {function(object): function} [options.tagActionSelector=undefined] - An optional override for the action property that can be assigned to each tag via tagOptions. - * If set, the selector is executed on each tag as input argument. This allows a list of tags to be provided and each tag can have it's action based on the tag object itself. - * @param {TagOptions} [options.tagOptions={}] - Options for tag behavior. (Same object will be passed into "appendTagToList") + * @param {PrintTagListOptions} [options] - Optional parameters for printing the tag list. */ function printTagList(element, { tags = undefined, forEntityOrKey = undefined, empty = true, tagActionSelector = undefined, tagOptions = {} } = {}) { const key = forEntityOrKey !== undefined ? getTagKeyForEntity(forEntityOrKey) : getTagKey(); - const printableTags = tags ?? getTagsList(key); + const printableTags = tags !== undefined ? (typeof tags === 'function' ? tags() : tags).sort(compareTagsForSort) : getTagsList(key); if (empty) { $(element).empty(); @@ -730,6 +762,8 @@ function onTagRemoveClick(event) { printTagFilters(tag_filter_types.character); printTagFilters(tag_filter_types.group_member); saveSettingsDebounced(); + + } // @ts-ignore @@ -764,12 +798,18 @@ function applyTagsOnGroupSelect() { // Nothing to do here at the moment. Tags in group interface get automatically redrawn. } -export function createTagInput(inputSelector, listSelector) { +/** + * + * @param {string} inputSelector - the selector for the tag input control + * @param {string} listSelector - the selector for the list of the tags modified by the input control + * @param {PrintTagListOptions} [tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before. + */ +export function createTagInput(inputSelector, listSelector, tagListOptions = {}) { $(inputSelector) // @ts-ignore .autocomplete({ source: (i, o) => findTag(i, o, listSelector), - select: (e, u) => selectTag(e, u, listSelector), + select: (e, u) => selectTag(e, u, listSelector, tagListOptions), minLength: 0, }) .focus(onTagInputFocus); // <== show tag list on click @@ -1152,8 +1192,8 @@ function onClearAllFiltersClick() { } jQuery(() => { - createTagInput('#tagInput', '#tagList'); - createTagInput('#groupTagInput', '#groupTagList'); + createTagInput('#tagInput', '#tagList', { tagOptions: { removable: true } }); + createTagInput('#groupTagInput', '#groupTagList', { tagOptions: { removable: true } }); $(document).on('click', '#rm_button_create', onCharacterCreateClick); $(document).on('click', '#rm_button_group_chats', onGroupCreateClick); diff --git a/public/style.css b/public/style.css index 91932cb47..6817bcc45 100644 --- a/public/style.css +++ b/public/style.css @@ -37,6 +37,7 @@ --fullred: rgba(255, 0, 0, 1); --crimson70a: rgba(100, 0, 0, 0.7); + --crimson-hover: rgba(150, 50, 50, 0.5); --okGreen70a: rgba(0, 100, 0, 0.7); --cobalt30a: rgba(100, 100, 255, 0.3); --greyCAIbg: rgb(36, 36, 37); @@ -2113,11 +2114,18 @@ grammarly-extension { } #bulk_tag_popup_reset, +#bulk_tag_popup_remove_mutual, #dialogue_popup_ok { background-color: var(--crimson70a); cursor: pointer; } +#bulk_tag_popup_reset:hover, +#bulk_tag_popup_remove_mutual:hover, +#dialogue_popup_ok:hover { + background-color: var(--crimson-hover); +} + #dialogue_popup_input { margin: 10px 0; width: 100%; From 4547e684970aea45c25b834cce085ccbb5991e0c Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Wed, 27 Mar 2024 04:28:24 +0100 Subject: [PATCH 02/99] Fix tag display issues (char create, auto load) - Fix tags not working on new character dialog - Fix display of tags for auto-loaded character on enabled auto load --- public/scripts/RossAscends-mods.js | 6 ++++- public/scripts/tags.js | 41 ++++++++++++++---------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index da93d1ed7..9aa3430a7 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -27,7 +27,7 @@ import { import { LoadLocal, SaveLocal, LoadLocalBool } from './f-localStorage.js'; import { selected_group, is_group_generating, openGroupById } from './group-chats.js'; -import { getTagKeyForEntity } from './tags.js'; +import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js'; import { SECRET_KEYS, secret_state, @@ -252,6 +252,10 @@ async function RA_autoloadchat() { const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character); if (active_character_id !== null) { await selectCharacterById(String(active_character_id)); + + // Do a little tomfoolery to spoof the tag selector + const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`) + applyTagsOnCharacterSelect.call(selectedCharElement); } } diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 8c92cd265..c4bf4626c 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -423,21 +423,8 @@ function selectTag(event, ui, listSelector, tagListOptions = {}) { saveSettingsDebounced(); - // If we have a manual list of tags to print, we should add this tag here to that manual list, otherwise it may not get printed - if (tagListOptions.tags !== undefined) { - const tagExists = (tags, tag) => tags.some(x => x.id === tag.id); - - if (typeof tagListOptions.tags === 'function') { - // If 'tags' is a function, wrap it to include new tag upon invocation - const originalTagsFunction = tagListOptions.tags; - tagListOptions.tags = () => { - const currentTags = originalTagsFunction(); - return tagExists(currentTags, tag) ? currentTags : [...currentTags, tag]; - }; - } else { - tagListOptions.tags = tagExists(tagListOptions.tags, tag) ? tags : [...tagListOptions.tags, tag]; - } - } + // We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it + tagListOptions.addTag = tag; // add tag to the UI and internal map - we reprint so sorting and new markup is done correctly printTagList(listSelector, tagListOptions); @@ -530,9 +517,10 @@ function createNewTag(tagName) { /** * @typedef {object} PrintTagListOptions - Optional parameters for printing the tag list. - * @property {Array|function(): Array} [tags=undefined] Optional override of tags that should be printed. Those will not be sorted. If no supplied, tags for the relevant character are printed. Can also be a function that returns the tags. + * @property {Array|function(): Array} [tags=undefined] - Optional override of tags that should be printed. Those will not be sorted. If no supplied, tags for the relevant character are printed. Can also be a function that returns the tags. + * @property {object} [addTag=undefined] - Optionally provide a tag that should be manually added to this print. Either to the overriden tag list or the found tags based on the entity/key. Will respect the tag exists check. * @property {object|number|string} [forEntityOrKey=undefined] - Optional override for the chosen entity, otherwise the currently selected is chosen. Can be an entity with id property (character, group, tag), or directly an id or tag key. - * @property {boolean} [empty=true] - Whether the list should be initially empty. + * @property {boolean|string} [empty=true] - Whether the list should be initially empty. If a string string is provided, 'always' will always empty the list, otherwise it'll evaluate to a boolean. * @property {function(object): function} [tagActionSelector=undefined] - An optional override for the action property that can be assigned to each tag via tagOptions. * If set, the selector is executed on each tag as input argument. This allows a list of tags to be provided and each tag can have it's action based on the tag object itself. * @property {TagOptions} [tagOptions={}] - Options for tag behavior. (Same object will be passed into "appendTagToList") @@ -543,18 +531,27 @@ function createNewTag(tagName) { * @param {JQuery} element - The container element where the tags are to be printed. * @param {PrintTagListOptions} [options] - Optional parameters for printing the tag list. */ -function printTagList(element, { tags = undefined, forEntityOrKey = undefined, empty = true, tagActionSelector = undefined, tagOptions = {} } = {}) { +function printTagList(element, { tags = undefined, addTag = undefined, forEntityOrKey = undefined, empty = true, tagActionSelector = undefined, tagOptions = {} } = {}) { const key = forEntityOrKey !== undefined ? getTagKeyForEntity(forEntityOrKey) : getTagKey(); - const printableTags = tags !== undefined ? (typeof tags === 'function' ? tags() : tags).sort(compareTagsForSort) : getTagsList(key); + let printableTags = tags ? (typeof tags === 'function' ? tags() : tags) : getTagsList(key); - if (empty) { + if (empty === 'always' || (empty && (printableTags?.length > 0 || key))) { $(element).empty(); } + if (addTag && (tagOptions.skipExistsCheck || !printableTags.some(x => x.id === addTag.id))) { + printableTags = [...printableTags, addTag]; + } + + // one last sort, because we might have modified the tag list or manually retrieved it from a function + printableTags = printableTags.sort(compareTagsForSort); + + const customAction = typeof tagActionSelector === 'function' ? tagActionSelector : null; + for (const tag of printableTags) { // If we have a custom action selector, we override that tag options for each tag - if (tagActionSelector && typeof tagActionSelector === 'function') { - const action = tagActionSelector(tag); + if (customAction) { + const action = customAction(tag); if (action && typeof action !== 'function') { console.error('The action parameter must return a function for tag.', tag); } else { From a4c4f36fc69162b714593818751793b094535a73 Mon Sep 17 00:00:00 2001 From: Wolfsblvt Date: Wed, 27 Mar 2024 08:22:03 +0100 Subject: [PATCH 03/99] Bulk edit select improvements & bulk tag edit inline avatars - bulk edit tags shows inline avatars for all selected characters - allow shift-click selecting/deselecting multiple characters on bulk edit - bulk select all button added - bulk select shows selected character count --- public/index.html | 4 +- public/script.js | 25 ++++---- public/scripts/BulkEditOverlay.js | 101 +++++++++++++++++++++++++----- public/scripts/bulk-edit.js | 47 +++++++++++--- public/style.css | 4 ++ 5 files changed, 145 insertions(+), 36 deletions(-) diff --git a/public/index.html b/public/index.html index 751cce6b3..bca9823f9 100644 --- a/public/index.html +++ b/public/index.html @@ -4319,7 +4319,9 @@
- +
+ +
diff --git a/public/script.js b/public/script.js index 38ed270cd..6cf82dc27 100644 --- a/public/script.js +++ b/public/script.js @@ -279,6 +279,7 @@ export { default_ch_mes, extension_prompt_types, mesForShowdownParse, + characterGroupOverlay, printCharacters, isOdd, countOccurrences, @@ -1343,19 +1344,19 @@ async function printCharacters(fullRefresh = false) { favsToHotswap(); } +export function characterToEntity(character, id) { + return { item: character, id, type: 'character' }; +} + +export function groupToEntity(group) { + return { item: group, id: group.id, type: 'group' }; +} + +export function tagToEntity(tag) { + return { item: structuredClone(tag), id: tag.id, type: 'tag', entities: [] }; +} + export function getEntitiesList({ doFilter = false, doSort = true } = {}) { - function characterToEntity(character, id) { - return { item: character, id, type: 'character' }; - } - - function groupToEntity(group) { - return { item: group, id: group.id, type: 'group' }; - } - - function tagToEntity(tag) { - return { item: structuredClone(tag), id: tag.id, type: 'tag', entities: [] }; - } - let entities = [ ...characters.map((item, index) => characterToEntity(item, index)), ...groups.map(item => groupToEntity(item)), diff --git a/public/scripts/BulkEditOverlay.js b/public/scripts/BulkEditOverlay.js index d11b56aa9..c2d3e44e8 100644 --- a/public/scripts/BulkEditOverlay.js +++ b/public/scripts/BulkEditOverlay.js @@ -10,6 +10,8 @@ import { getPastCharacterChats, getRequestHeaders, printCharacters, + buildAvatarList, + characterToEntity, } from '../script.js'; import { favsToHotswap } from './RossAscends-mods.js'; @@ -194,7 +196,8 @@ class BulkTagPopupHandler {

Modify tags of ${characterIds.length} characters

- This popup allows you to modify the mutual tags of all selected characters. + Add or remove the mutual tags of all selected characters. +

@@ -233,7 +236,8 @@ class BulkTagPopupHandler { document.body.insertAdjacentHTML('beforeend', this.#getHtml(characterIds)); - this.mutualTags = this.getMutualTags(characterIds); + const entities = characterIds.map(id => characterToEntity(characters[id], id)).filter(entity => entity.item !== undefined); + buildAvatarList($('#bulk_tags_avatars_block'), entities); // Print the tag list with all mutuable tags, marking them as removable. That is the initial fill printTagList($('#bulkTagList'), { tags: () => this.getMutualTags(characterIds), tagOptions: { removable: true } }); @@ -257,7 +261,7 @@ class BulkTagPopupHandler { } // Find mutual tags for multiple characters - const allTags = characterIds.map(c => getTagsList(getTagKeyForEntity(c))); + const allTags = characterIds.map(cid => getTagsList(getTagKeyForEntity(cid))); const mutualTags = allTags.reduce((mutual, characterTags) => mutual.filter(tag => characterTags.some(cTag => cTag.id === tag.id)) ); @@ -345,6 +349,7 @@ class BulkEditOverlay { static selectModeClass = 'group_overlay_mode_select'; static selectedClass = 'character_selected'; static legacySelectedClass = 'bulk_select_checkbox'; + static bulkSelectedCountId = 'bulkSelectedCount'; static longPressDelay = 2500; @@ -353,6 +358,17 @@ class BulkEditOverlay { #stateChangeCallbacks = []; #selectedCharacters = []; + /** + * @typedef {object} LastSelected - An object noting the last selected character and its state. + * @property {string} [characterId] - The character id of the last selected character. + * @property {boolean} [select] - The selected state of the last selected character. true if it was selected, false if it was deselected. + */ + + /** + * @type {LastSelected} - An object noting the last selected character and its state. + */ + lastSelected = { characterId: undefined, select: undefined }; + /** * Locks other pointer actions when the context menu is open * @@ -588,27 +604,80 @@ class BulkEditOverlay { event.stopPropagation(); const character = event.currentTarget; - const characterId = character.getAttribute('chid'); - const alreadySelected = this.selectedCharacters.includes(characterId); + if (!this.#contextMenuOpen && !this.#cancelNextToggle) { + if (event.shiftKey) { + // Shift click might have selected text that we don't want to. Unselect it. + document.getSelection().removeAllRanges(); - const legacyBulkEditCheckbox = character.querySelector('.' + BulkEditOverlay.legacySelectedClass); - - // Only toggle when context menu is closed and wasn't just closed. - if (!this.#contextMenuOpen && !this.#cancelNextToggle) - if (alreadySelected) { - character.classList.remove(BulkEditOverlay.selectedClass); - if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = false; - this.dismissCharacter(characterId); + this.handleShiftClick(character); } else { - character.classList.add(BulkEditOverlay.selectedClass); - if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = true; - this.selectCharacter(characterId); + this.toggleSingleCharacter(character); } + } this.#cancelNextToggle = false; }; + handleShiftClick = (currentCharacter) => { + const characterId = currentCharacter.getAttribute('chid'); + const select = !this.selectedCharacters.includes(characterId); + + if (this.lastSelected.characterId && this.lastSelected.select !== undefined) { + // Only if select state and the last select state match we execute the range select + if (select === this.lastSelected.select) { + this.selectCharactersInRange(currentCharacter, select); + } + } + }; + + toggleSingleCharacter = (character, { markState = true } = {}) => { + const characterId = character.getAttribute('chid'); + + const select = !this.selectedCharacters.includes(characterId); + const legacyBulkEditCheckbox = character.querySelector('.' + BulkEditOverlay.legacySelectedClass); + + if (select) { + character.classList.add(BulkEditOverlay.selectedClass); + if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = true; + this.selectCharacter(characterId); + } else { + character.classList.remove(BulkEditOverlay.selectedClass); + if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = false; + this.dismissCharacter(characterId); + } + + this.updateSelectedCount(); + + if (markState) { + this.lastSelected.characterId = characterId; + this.lastSelected.select = select; + } + }; + + updateSelectedCount = (countOverride = undefined) => { + const count = countOverride ?? this.selectedCharacters.length; + $(`#${BulkEditOverlay.bulkSelectedCountId}`).text(count).attr('title', `${count} characters selected`); + }; + + selectCharactersInRange = (currentCharacter, select) => { + const currentCharacterId = currentCharacter.getAttribute('chid'); + const characters = Array.from(document.querySelectorAll('#' + BulkEditOverlay.containerId + ' .' + BulkEditOverlay.characterClass)); + + const startIndex = characters.findIndex(c => c.getAttribute('chid') === this.lastSelected.characterId); + const endIndex = characters.findIndex(c => c.getAttribute('chid') === currentCharacterId); + + for (let i = Math.min(startIndex, endIndex); i <= Math.max(startIndex, endIndex); i++) { + const character = characters[i]; + const characterId = character.getAttribute('chid'); + const isCharacterSelected = this.selectedCharacters.includes(characterId); + + if (select && !isCharacterSelected || !select && isCharacterSelected) { + this.toggleSingleCharacter(character, { markState: currentCharacterId == i }); + } + } + }; + handleContextMenuShow = (event) => { event.preventDefault(); CharacterContextMenu.show(...this.#getContextMenuPosition(event)); diff --git a/public/scripts/bulk-edit.js b/public/scripts/bulk-edit.js index 7cb0d17b9..95d0d79bb 100644 --- a/public/scripts/bulk-edit.js +++ b/public/scripts/bulk-edit.js @@ -1,4 +1,4 @@ -import { characters, getCharacters, handleDeleteCharacter, callPopup } from '../script.js'; +import { characters, getCharacters, handleDeleteCharacter, callPopup, characterGroupOverlay } from '../script.js'; import { BulkEditOverlay, BulkEditOverlayState } from './BulkEditOverlay.js'; @@ -6,18 +6,20 @@ let is_bulk_edit = false; const enableBulkEdit = () => { enableBulkSelect(); - (new BulkEditOverlay()).selectState(); - // show the delete button - $('#bulkDeleteButton').show(); + characterGroupOverlay.selectState(); + // show the bulk edit option buttons + $('.bulkEditOptionElement').show(); is_bulk_edit = true; + characterGroupOverlay.updateSelectedCount(0); }; const disableBulkEdit = () => { disableBulkSelect(); - (new BulkEditOverlay()).browseState(); - // hide the delete button - $('#bulkDeleteButton').hide(); + characterGroupOverlay.browseState(); + // hide the bulk edit option buttons + $('.bulkEditOptionElement').hide(); is_bulk_edit = false; + characterGroupOverlay.updateSelectedCount(0); }; const toggleBulkEditMode = (isBulkEdit) => { @@ -41,6 +43,32 @@ function onEditButtonClick() { toggleBulkEditMode(is_bulk_edit); } +/** + * Toggles the select state of all characters in bulk edit mode to selected. If all are selected, they'll be deselected. + */ +function onSelectAllButtonClick() { + console.log('Bulk select all button clicked'); + const characters = Array.from(document.querySelectorAll('#' + BulkEditOverlay.containerId + ' .' + BulkEditOverlay.characterClass)); + let atLeastOneSelected = false; + for (const character of characters) { + const checked = $(character).find('.bulk_select_checkbox:checked').length > 0; + if (!checked) { + characterGroupOverlay.toggleSingleCharacter(character); + atLeastOneSelected = true; + } + } + + if (!atLeastOneSelected) { + // If none was selected, trigger click on all to deselect all of them + for(const character of characters) { + const checked = $(character).find('.bulk_select_checkbox:checked') ?? false; + if (checked) { + characterGroupOverlay.toggleSingleCharacter(character); + } + } + } +} + /** * Deletes the character with the given chid. * @@ -89,6 +117,10 @@ async function onDeleteButtonClick() { */ function enableBulkSelect() { $('#rm_print_characters_block .character_select').each((i, el) => { + // Prevent checkbox from adding multiple times (because of stage change callback) + if ($(el).find('.bulk_select_checkbox').length > 0) { + return; + } const checkbox = $(''); checkbox.on('change', () => { // Do something when the checkbox is changed @@ -115,5 +147,6 @@ function disableBulkSelect() { */ jQuery(() => { $('#bulkEditButton').on('click', onEditButtonClick); + $('#bulkSelectAllButton').on('click', onSelectAllButtonClick); $('#bulkDeleteButton').on('click', onDeleteButtonClick); }); diff --git a/public/style.css b/public/style.css index 6817bcc45..9efcbc55f 100644 --- a/public/style.css +++ b/public/style.css @@ -2126,6 +2126,10 @@ grammarly-extension { background-color: var(--crimson-hover); } +#bulk_tags_avatars_block { + max-height: 70vh; +} + #dialogue_popup_input { margin: 10px 0; width: 100%; From 06510f25bf4945b90e1d8703318ef3ee20e5bfed Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 27 Mar 2024 23:37:28 +0200 Subject: [PATCH 04/99] Fix double append of example dialogue names in group chats for Claude --- src/prompt-converters.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/prompt-converters.js b/src/prompt-converters.js index 0b5af8f80..42f7abaf7 100644 --- a/src/prompt-converters.js +++ b/src/prompt-converters.js @@ -89,11 +89,16 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi if (messages[i].role !== 'system') { break; } + // Append example names if not already done by the frontend (e.g. for group chats). if (userName && messages[i].name === 'example_user') { - messages[i].content = `${userName}: ${messages[i].content}`; + if (!messages[i].content.startsWith(`${userName}: `)) { + messages[i].content = `${userName}: ${messages[i].content}`; + } } if (charName && messages[i].name === 'example_assistant') { - messages[i].content = `${charName}: ${messages[i].content}`; + if (!messages[i].content.startsWith(`${charName}: `)) { + messages[i].content = `${charName}: ${messages[i].content}`; + } } systemPrompt += `${messages[i].content}\n\n`; } From 01d17f43965b2d8d8f1fcc99ca5aa97d5ad0a4b7 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 27 Mar 2024 23:38:13 +0200 Subject: [PATCH 05/99] Export Chat Comps examples parser --- public/scripts/openai.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 3a722dc97..5a85da15e 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -523,7 +523,7 @@ function setOpenAIMessageExamples(mesExamplesArray) { for (let item of mesExamplesArray) { // remove {Example Dialogue:} and replace \r\n with just \n let replaced = item.replace(//i, '{Example Dialogue:}').replace(/\r/gm, ''); - let parsed = parseExampleIntoIndividual(replaced); + let parsed = parseExampleIntoIndividual(replaced, true); // add to the example message blocks array examples.push(parsed); } @@ -584,7 +584,13 @@ function setupChatCompletionPromptManager(openAiSettings) { return promptManager; } -function parseExampleIntoIndividual(messageExampleString) { +/** + * Parses the example messages into individual messages. + * @param {string} messageExampleString - The string containing the example messages + * @param {boolean} appendNamesForGroup - Whether to append the character name for group chats + * @returns {object[]} Array of message objects + */ +export function parseExampleIntoIndividual(messageExampleString, appendNamesForGroup = true) { let result = []; // array of msgs let tmp = messageExampleString.split('\n'); let cur_msg_lines = []; @@ -597,7 +603,7 @@ function parseExampleIntoIndividual(messageExampleString) { // strip to remove extra spaces let parsed_msg = cur_msg_lines.join('\n').replace(name + ':', '').trim(); - if (selected_group && ['example_user', 'example_assistant'].includes(system_name)) { + if (appendNamesForGroup && selected_group && ['example_user', 'example_assistant'].includes(system_name)) { parsed_msg = `${name}: ${parsed_msg}`; } From 8b7b32a141b70c2a11defc04a1618acddce42e42 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 27 Mar 2024 23:40:10 +0200 Subject: [PATCH 06/99] (WIP) Add new instruct mode sequences. Deprecates separator sequence. 1. Separate suffixes for all roles 2. System message sequences 3. User alignment message --- public/index.html | 107 +++++++++++++++++++++++--------- public/scripts/instruct-mode.js | 56 ++++++++++++++--- public/scripts/power-user.js | 8 ++- 3 files changed, 131 insertions(+), 40 deletions(-) diff --git a/public/index.html b/public/index.html index baf5c2d3d..1140599f7 100644 --- a/public/index.html +++ b/public/index.html @@ -2869,36 +2869,99 @@
+
+ Chat Messages Wrapping +
-
+
-
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+
-
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ System Prompt Wrapping +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ Misc. Sequences +
+
+
-
+
@@ -2906,25 +2969,15 @@
-
-
-
-
+
@@ -2932,14 +2985,6 @@
-
- -
- -
-
diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index 4737dec48..4159b3a66 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -19,9 +19,13 @@ const controls = [ { id: 'instruct_system_prompt', property: 'system_prompt', isCheckbox: false }, { id: 'instruct_system_sequence_prefix', property: 'system_sequence_prefix', isCheckbox: false }, { id: 'instruct_system_sequence_suffix', property: 'system_sequence_suffix', isCheckbox: false }, - { id: 'instruct_separator_sequence', property: 'separator_sequence', isCheckbox: false }, { id: 'instruct_input_sequence', property: 'input_sequence', isCheckbox: false }, + { id: 'instruct_input_suffix', property: 'input_suffix', isCheckbox: false }, { id: 'instruct_output_sequence', property: 'output_sequence', isCheckbox: false }, + { id: 'instruct_output_suffix', property: 'output_suffix', isCheckbox: false }, + { id: 'instruct_system_sequence', property: 'system_sequence', isCheckbox: false }, + { id: 'instruct_system_suffix', property: 'system_suffix', isCheckbox: false }, + { id: 'instruct_user_alignment_message', property: 'user_alignment_message', isCheckbox: false }, { id: 'instruct_stop_sequence', property: 'stop_sequence', isCheckbox: false }, { id: 'instruct_names', property: 'names', isCheckbox: true }, { id: 'instruct_macro', property: 'macro', isCheckbox: true }, @@ -33,6 +37,46 @@ const controls = [ { id: 'instruct_skip_examples', property: 'skip_examples', isCheckbox: true }, ]; +/** + * Migrates instruct mode settings into the evergreen format. + * @param {object} settings Instruct mode settings. + * @returns {void} + */ +function migrateInstructModeSettings(settings) { + // Separator sequence => Output suffix + if (settings.separator_sequence === undefined) { + return; + } + + settings.output_suffix = settings.separator_sequence || ''; + delete settings.separator_sequence; + + // Init the rest with default values + if (settings.input_suffix === undefined) { + settings.input_suffix = ''; + } + + if (settings.system_sequence === undefined) { + settings.system_sequence = ''; + } + + if (settings.system_suffix === undefined) { + settings.system_suffix = ''; + } + + if (settings.user_alignment_message === undefined) { + settings.user_alignment_message = ''; + } + + if (settings.names_force_groups === undefined) { + settings.names_force_groups = true; + } + + if (settings.skip_examples === undefined) { + settings.skip_examples = false; + } +} + /** * Loads instruct mode settings from the given data object. * @param {object} data Settings data object. @@ -42,13 +86,7 @@ export function loadInstructMode(data) { instruct_presets = data.instruct; } - if (power_user.instruct.names_force_groups === undefined) { - power_user.instruct.names_force_groups = true; - } - - if (power_user.instruct.skip_examples === undefined) { - power_user.instruct.skip_examples = false; - } + migrateInstructModeSettings(power_user.instruct); controls.forEach(control => { const $element = $(`#${control.id}`); @@ -442,6 +480,8 @@ jQuery(() => { return; } + migrateInstructModeSettings(preset); + power_user.instruct.preset = String(name); controls.forEach(control => { if (preset[control.property] !== undefined) { diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index d0cc3fc06..d91254c44 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -197,19 +197,25 @@ let power_user = { preset: 'Alpaca', system_prompt: 'Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\nWrite {{char}}\'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n', input_sequence: '### Instruction:', + input_suffix: '', output_sequence: '### Response:', + output_suffix: '', + system_sequence: '', + system_suffix: '', first_output_sequence: '', last_output_sequence: '', system_sequence_prefix: '', system_sequence_suffix: '', stop_sequence: '', - separator_sequence: '', wrap: true, macro: true, names: false, names_force_groups: true, activation_regex: '', bind_to_context: false, + user_alignment_message: '', + /** @deprecated Use output_suffix instead */ + separator_sequence: '', }, default_context: 'Default', From 1c01aafd51b98670d09565e4320c3b1fc8a084db Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:16:35 +0200 Subject: [PATCH 07/99] Unrestrict console depth nesting --- server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server.js b/server.js index 77ab7073f..d8ad9c7e5 100644 --- a/server.js +++ b/server.js @@ -30,6 +30,7 @@ const fetch = require('node-fetch').default; // Unrestrict console logs display limit util.inspect.defaultOptions.maxArrayLength = null; util.inspect.defaultOptions.maxStringLength = null; +util.inspect.defaultOptions.depth = null; // local library imports const basicAuthMiddleware = require('./src/middleware/basicAuth'); From 39768b78ce656e44da7caffe98d283bcf9483045 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:27:00 +0200 Subject: [PATCH 08/99] Decrease brightness of disabled inputs --- public/css/st-tailwind.css | 1 + public/scripts/extensions/token-counter/index.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/public/css/st-tailwind.css b/public/css/st-tailwind.css index fa3c90339..6018577b2 100644 --- a/public/css/st-tailwind.css +++ b/public/css/st-tailwind.css @@ -456,6 +456,7 @@ input:disabled, textarea:disabled { cursor: not-allowed; + filter: brightness(0.5); } .debug-red { diff --git a/public/scripts/extensions/token-counter/index.js b/public/scripts/extensions/token-counter/index.js index aaa1d58e2..90cdf9ee8 100644 --- a/public/scripts/extensions/token-counter/index.js +++ b/public/scripts/extensions/token-counter/index.js @@ -33,7 +33,7 @@ async function doTokenCounter() {

Token IDs:
- +
`; From 310acfe810c8cc2e989e6a8b69ce0ade942a9b50 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 28 Mar 2024 00:52:20 +0200 Subject: [PATCH 09/99] Use new instruct sequences in prompt formatting --- public/index.html | 10 ++- public/script.js | 10 +-- public/scripts/instruct-mode.js | 132 ++++++++++++++++++++++++-------- public/scripts/openai.js | 2 +- public/scripts/power-user.js | 1 + 5 files changed, 114 insertions(+), 41 deletions(-) diff --git a/public/index.html b/public/index.html index 1140599f7..cb4d6dcf9 100644 --- a/public/index.html +++ b/public/index.html @@ -2909,7 +2909,7 @@
-
+
@@ -2917,7 +2917,7 @@
-
+
@@ -2925,6 +2925,12 @@
+
+ +
System Prompt Wrapping diff --git a/public/script.js b/public/script.js index ba787581b..4b5ba72fb 100644 --- a/public/script.js +++ b/public/script.js @@ -3127,10 +3127,6 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu mesExamples = ''; } const mesExamplesRaw = mesExamples; - if (mesExamples && isInstruct) { - mesExamples = formatInstructModeExamples(mesExamples, name1, name2); - } - /** * Adds a block heading to the examples string. * @param {string} examplesStr @@ -3138,13 +3134,17 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu */ function addBlockHeading(examplesStr) { const exampleSeparator = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : ''; - const blockHeading = main_api === 'openai' ? '\n' : exampleSeparator; + const blockHeading = main_api === 'openai' ? '\n' : (exampleSeparator || (isInstruct ? '\n' : '')); return examplesStr.split(//gi).slice(1).map(block => `${blockHeading}${block.trim()}\n`); } let mesExamplesArray = addBlockHeading(mesExamples); let mesExamplesRawArray = addBlockHeading(mesExamplesRaw); + if (mesExamplesArray && isInstruct) { + mesExamplesArray = formatInstructModeExamples(mesExamplesArray, name1, name2); + } + // First message in fresh 1-on-1 chat reacts to user/character settings changes if (chat.length) { chat[0].mes = substituteParams(chat[0].mes); diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index 4159b3a66..e30d81a3f 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -1,7 +1,8 @@ 'use strict'; -import { saveSettingsDebounced, substituteParams } from '../script.js'; +import { name1, name2, saveSettingsDebounced, substituteParams } from '../script.js'; import { selected_group } from './group-chats.js'; +import { parseExampleIntoIndividual } from './openai.js'; import { power_user, context_presets, @@ -35,6 +36,7 @@ const controls = [ { id: 'instruct_activation_regex', property: 'activation_regex', isCheckbox: false }, { id: 'instruct_bind_to_context', property: 'bind_to_context', isCheckbox: true }, { id: 'instruct_skip_examples', property: 'skip_examples', isCheckbox: true }, + { id: 'instruct_system_same_as_user', property: 'system_same_as_user', isCheckbox: true, trigger: true }, ]; /** @@ -75,6 +77,10 @@ function migrateInstructModeSettings(settings) { if (settings.skip_examples === undefined) { settings.skip_examples = false; } + + if (settings.system_same_as_user === undefined) { + settings.system_same_as_user = false; + } } /** @@ -104,6 +110,10 @@ export function loadInstructMode(data) { resetScrollHeight($element); } }); + + if (control.trigger) { + $element.trigger('input'); + } }); instruct_presets.forEach((preset) => { @@ -248,12 +258,14 @@ export function getInstructStoppingSequences() { const result = []; if (power_user.instruct.enabled) { - const input_sequence = power_user.instruct.input_sequence; - const output_sequence = power_user.instruct.output_sequence; - const first_output_sequence = power_user.instruct.first_output_sequence; - const last_output_sequence = power_user.instruct.last_output_sequence; + const stop_sequence = power_user.instruct.stop_sequence; + const input_sequence = power_user.instruct.input_sequence.replace(/{{name}}/gi, name1); + const output_sequence = power_user.instruct.output_sequence.replace(/{{name}}/gi, name2); + const first_output_sequence = power_user.instruct.first_output_sequence.replace(/{{name}}/gi, name2); + const last_output_sequence = power_user.instruct.last_output_sequence.replace(/{{name}}/gi, name2); + const system_sequence = power_user.instruct.system_sequence.replace(/{{name}}/gi, 'System'); - const combined_sequence = `${input_sequence}\n${output_sequence}\n${first_output_sequence}\n${last_output_sequence}`; + const combined_sequence = `${stop_sequence}\n${input_sequence}\n${output_sequence}\n${first_output_sequence}\n${last_output_sequence}\n${system_sequence}`; combined_sequence.split('\n').filter((line, index, self) => self.indexOf(line) === index).forEach(addInstructSequence); } @@ -295,26 +307,48 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata includeNames = true; } - let sequence = (isUser || isNarrator) ? power_user.instruct.input_sequence : power_user.instruct.output_sequence; - - if (forceOutputSequence && sequence === power_user.instruct.output_sequence) { - if (forceOutputSequence === force_output_sequence.FIRST && power_user.instruct.first_output_sequence) { - sequence = power_user.instruct.first_output_sequence; - } else if (forceOutputSequence === force_output_sequence.LAST && power_user.instruct.last_output_sequence) { - sequence = power_user.instruct.last_output_sequence; + function getPrefix() { + if (isNarrator) { + return power_user.instruct.system_same_as_user ? power_user.instruct.input_sequence : power_user.instruct.system_sequence; } + + if (isUser) { + return power_user.instruct.input_sequence; + } + + if (forceOutputSequence === force_output_sequence.FIRST) { + return power_user.instruct.first_output_sequence || power_user.instruct.output_sequence; + } + + if (forceOutputSequence === force_output_sequence.LAST) { + return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence; + } + + return power_user.instruct.output_sequence; } + function getSuffix() { + if (isNarrator) { + return power_user.instruct.system_same_as_user ? power_user.instruct.input_suffix : power_user.instruct.system_suffix; + } + + if (isUser) { + return power_user.instruct.input_suffix; + } + + return power_user.instruct.output_suffix; + } + + let prefix = getPrefix() || ''; + let suffix = getSuffix() || ''; + if (power_user.instruct.macro) { - sequence = substituteParams(sequence, name1, name2); - sequence = sequence.replace(/{{name}}/gi, name || 'System'); + prefix = substituteParams(prefix, name1, name2); + prefix = prefix.replace(/{{name}}/gi, name || 'System'); } const separator = power_user.instruct.wrap ? '\n' : ''; - const separatorSequence = power_user.instruct.separator_sequence && !isUser - ? power_user.instruct.separator_sequence - : separator; - const textArray = includeNames ? [sequence, `${name}: ${mes}` + separatorSequence] : [sequence, mes + separatorSequence]; + const textArray = includeNames ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix]; const text = textArray.filter(x => x).join(separator); return text; } @@ -324,7 +358,7 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata * @param {string} systemPrompt System prompt string. * @returns {string} Formatted instruct mode system prompt. */ -export function formatInstructModeSystemPrompt(systemPrompt){ +export function formatInstructModeSystemPrompt(systemPrompt) { const separator = power_user.instruct.wrap ? '\n' : ''; if (power_user.instruct.system_sequence_prefix) { @@ -340,33 +374,59 @@ export function formatInstructModeSystemPrompt(systemPrompt){ /** * Formats example messages according to instruct mode settings. - * @param {string} mesExamples Example messages string. + * @param {string[]} mesExamplesArray Example messages array. * @param {string} name1 User name. * @param {string} name2 Character name. - * @returns {string} Formatted example messages string. + * @returns {string[]} Formatted example messages string. */ -export function formatInstructModeExamples(mesExamples, name1, name2) { +export function formatInstructModeExamples(mesExamplesArray, name1, name2) { if (power_user.instruct.skip_examples) { - return mesExamples; + return mesExamplesArray.map(x => x.replace(/\n/i, '')); } const includeNames = power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups); - let inputSequence = power_user.instruct.input_sequence; - let outputSequence = power_user.instruct.output_sequence; + let inputPrefix = power_user.instruct.input_sequence || ''; + let outputPrefix = power_user.instruct.output_sequence || ''; + let inputSuffix = power_user.instruct.output_suffix || ''; + let outputSuffix = power_user.instruct.output_suffix || ''; if (power_user.instruct.macro) { - inputSequence = substituteParams(inputSequence, name1, name2); - outputSequence = substituteParams(outputSequence, name1, name2); + inputPrefix = substituteParams(inputPrefix, name1, name2); + outputPrefix = substituteParams(outputPrefix, name1, name2); + inputSuffix = substituteParams(inputSuffix, name1, name2); + outputSuffix = substituteParams(outputSuffix, name1, name2); + + inputPrefix = inputPrefix.replace(/{{name}}/gi, name1); + outputPrefix = outputPrefix.replace(/{{name}}/gi, name2); } const separator = power_user.instruct.wrap ? '\n' : ''; - const separatorSequence = power_user.instruct.separator_sequence ? power_user.instruct.separator_sequence : separator; + const parsedExamples = []; - mesExamples = mesExamples.replace(new RegExp(`\n${name1}: `, 'gm'), separatorSequence + inputSequence + separator + (includeNames ? `${name1}: ` : '')); - mesExamples = mesExamples.replace(new RegExp(`\n${name2}: `, 'gm'), separator + outputSequence + separator + (includeNames ? `${name2}: ` : '')); + for (const item of mesExamplesArray) { + const cleanedItem = item.replace(//i, '{Example Dialogue:}').replace(/\r/gm, ''); + const blockExamples = parseExampleIntoIndividual(cleanedItem); + parsedExamples.push(...blockExamples); + } - return mesExamples; + // Not something we can parse, return as is + if (!Array.isArray(parsedExamples) || parsedExamples.length === 0) { + return mesExamplesArray; + } + + const formattedExamples = []; + + for (const example of parsedExamples) { + const prefix = example.name == 'example_user' ? inputPrefix : outputPrefix; + const suffix = example.name == 'example_user' ? inputSuffix : outputSuffix; + const name = example.name == 'example_user' ? name1 : name2; + const messageContent = includeNames ? `${name}: ${example.content}` : example.content; + const formattedMessage = [prefix, messageContent + suffix].filter(x => x).join(separator); + formattedExamples.push(formattedMessage); + } + + return formattedExamples; } /** @@ -458,6 +518,12 @@ jQuery(() => { saveSettingsDebounced(); }); + $('#instruct_system_same_as_user').on('input', function () { + const state = !!$(this).prop('checked'); + $('#instruct_system_sequence').prop('disabled', state); + $('#instruct_system_suffix').prop('disabled', state); + }); + $('#instruct_enabled').on('change', function () { if (!power_user.instruct.bind_to_context) { return; @@ -466,8 +532,8 @@ jQuery(() => { // When instruct mode gets enabled, select context template matching selected instruct preset if (power_user.instruct.enabled) { selectMatchingContextTemplate(power_user.instruct.preset); - // When instruct mode gets disabled, select default context preset } else { + // When instruct mode gets disabled, select default context preset selectContextPreset(power_user.default_context); } }); diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 5a85da15e..bdfd3b827 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -588,7 +588,7 @@ function setupChatCompletionPromptManager(openAiSettings) { * Parses the example messages into individual messages. * @param {string} messageExampleString - The string containing the example messages * @param {boolean} appendNamesForGroup - Whether to append the character name for group chats - * @returns {object[]} Array of message objects + * @returns {Message[]} Array of message objects */ export function parseExampleIntoIndividual(messageExampleString, appendNamesForGroup = true) { let result = []; // array of msgs diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index d91254c44..3602061dd 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -214,6 +214,7 @@ let power_user = { activation_regex: '', bind_to_context: false, user_alignment_message: '', + system_same_as_user: false, /** @deprecated Use output_suffix instead */ separator_sequence: '', }, From 4b6a3054b1a63e89ab6b2e0f4b1b095ecea0d196 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 28 Mar 2024 02:27:37 +0200 Subject: [PATCH 10/99] Implement user alignment message --- public/script.js | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/public/script.js b/public/script.js index b07ca95be..634c3b5f4 100644 --- a/public/script.js +++ b/public/script.js @@ -3264,6 +3264,8 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu let chat2 = []; let continue_mag = ''; + const userMessageIndices = []; + for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) { if (main_api == 'openai') { chat2[i] = coreChat[j].mes; @@ -3291,6 +3293,22 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu chat2[i] = chat2[i].slice(0, chat2[i].lastIndexOf(coreChat[j].mes) + coreChat[j].mes.length); continue_mag = coreChat[j].mes; } + + if (coreChat[j].is_user) { + userMessageIndices.push(i); + } + } + + let addUserAlignment = isInstruct && power_user.instruct.user_alignment_message; + let userAlignmentMessage = ''; + + if (addUserAlignment) { + const alignmentMessage = { + name: name1, + mes: power_user.instruct.user_alignment_message, + is_user: true, + }; + userAlignmentMessage = formatMessageHistoryItem(alignmentMessage, isInstruct, false); } // Add persona description to prompt @@ -3349,6 +3367,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu allAnchors, quiet_prompt, cyclePrompt, + userAlignmentMessage, ].join('').replace(/\r/gm, ''); return getTokenCount(encodeString, power_user.token_padding); } @@ -3367,16 +3386,19 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu // Collect enough messages to fill the context let arrMes = []; let tokenCount = getMessagesTokenCount(); - for (let item of chat2) { + let lastAddedIndex = -1; + for (let i = 0; i < chat2.length; i++) { // not needed for OAI prompting if (main_api == 'openai') { break; } + const item = chat2[i]; tokenCount += getTokenCount(item.replace(/\r/gm, '')); chatString = item + chatString; if (tokenCount < this_max_context) { arrMes[arrMes.length] = item; + lastAddedIndex = i; } else { break; } @@ -3385,8 +3407,21 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu await delay(1); } + const stoppedAtUser = userMessageIndices.includes(lastAddedIndex); + if (addUserAlignment && !stoppedAtUser) { + tokenCount += getTokenCount(userAlignmentMessage.replace(/\r/gm, '')); + chatString = userAlignmentMessage + chatString; + arrMes[arrMes.length] = userAlignmentMessage; + // Injected indices shift by 1 for user alignment message at the beginning + injectedIndices.forEach((value, index) => (injectedIndices[index] = value + 1)); + injectedIndices.push(0); + } + + // Filter injections which don't fit in the context + injectedIndices = injectedIndices.filter(value => value < arrMes.length); + if (main_api !== 'openai') { - setInContextMessages(arrMes.length, type); + setInContextMessages(arrMes.length - injectedIndices.length, type); } // Estimate how many unpinned example messages fit in the context @@ -3664,7 +3699,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu }; finalMesSend.forEach((item, i) => { - item.injected = Array.isArray(injectedIndices) && injectedIndices.includes(i); + item.injected = injectedIndices.includes(finalMesSend.length - i - 1); }); let data = { @@ -4030,10 +4065,6 @@ function doChatInject(messages, isContinue) { } } - for (let i = 0; i < injectedIndices.length; i++) { - injectedIndices[i] = messages.length - injectedIndices[i] - 1; - } - messages.reverse(); return injectedIndices; } From 689af3151a9a50b7b0770e55958ccab2be884bae Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 28 Mar 2024 02:59:52 +0200 Subject: [PATCH 11/99] Pre-populate chat history with injections --- public/script.js | 64 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/public/script.js b/public/script.js index 634c3b5f4..cfbfc7700 100644 --- a/public/script.js +++ b/public/script.js @@ -3384,21 +3384,19 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu } // Collect enough messages to fill the context - let arrMes = []; + let arrMes = new Array(chat2.length); let tokenCount = getMessagesTokenCount(); let lastAddedIndex = -1; - for (let i = 0; i < chat2.length; i++) { - // not needed for OAI prompting - if (main_api == 'openai') { - break; - } - const item = chat2[i]; + // Pre-allocate all injections first. + // If it doesn't fit - user shot himself in the foot + for (const index of injectedIndices) { + const item = chat2[index]; tokenCount += getTokenCount(item.replace(/\r/gm, '')); chatString = item + chatString; if (tokenCount < this_max_context) { - arrMes[arrMes.length] = item; - lastAddedIndex = i; + arrMes[index] = item; + lastAddedIndex = Math.max(lastAddedIndex, index); } else { break; } @@ -3407,18 +3405,54 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu await delay(1); } + for (let i = 0; i < chat2.length; i++) { + // not needed for OAI prompting + if (main_api == 'openai') { + break; + } + + // Skip already injected messages + if (arrMes[i] !== undefined) { + continue; + } + + const item = chat2[i]; + tokenCount += getTokenCount(item.replace(/\r/gm, '')); + chatString = item + chatString; + if (tokenCount < this_max_context) { + arrMes[i] = item; + lastAddedIndex = Math.max(lastAddedIndex, i); + } else { + break; + } + + // Prevent UI thread lock on tokenization + await delay(1); + } + + // Add user alignment message if last message is not a user message const stoppedAtUser = userMessageIndices.includes(lastAddedIndex); if (addUserAlignment && !stoppedAtUser) { tokenCount += getTokenCount(userAlignmentMessage.replace(/\r/gm, '')); chatString = userAlignmentMessage + chatString; - arrMes[arrMes.length] = userAlignmentMessage; - // Injected indices shift by 1 for user alignment message at the beginning - injectedIndices.forEach((value, index) => (injectedIndices[index] = value + 1)); - injectedIndices.push(0); + arrMes.push(userAlignmentMessage); + injectedIndices.push(arrMes.length - 1); } - // Filter injections which don't fit in the context - injectedIndices = injectedIndices.filter(value => value < arrMes.length); + // Unsparse the array. Adjust injected indices + const newArrMes = []; + const newInjectedIndices = []; + for (let i = 0; i < arrMes.length; i++) { + if (arrMes[i] !== undefined) { + newArrMes.push(arrMes[i]); + if (injectedIndices.includes(i)) { + newInjectedIndices.push(newArrMes.length - 1); + } + } + } + + arrMes = newArrMes; + injectedIndices = newInjectedIndices; if (main_api !== 'openai') { setInContextMessages(arrMes.length - injectedIndices.length, type); From 423a1f85bef88fa4c1bd3b52e714fe8e2660d8c8 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 28 Mar 2024 20:28:30 +0200 Subject: [PATCH 12/99] Adjust naming and layout of sequences drawer --- public/index.html | 48 +++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/public/index.html b/public/index.html index 6cd95df82..a7a489d6c 100644 --- a/public/index.html +++ b/public/index.html @@ -2869,6 +2869,27 @@
+
+ System Prompt Wrapping +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
Chat Messages Wrapping
@@ -2932,27 +2953,6 @@
-
- System Prompt Wrapping -
-
-
- -
- -
-
-
- -
- -
-
-
Misc. Sequences
@@ -2965,7 +2965,7 @@
-
+
@@ -2977,13 +2977,13 @@
-
+
From e33fbbfbbf830e3846e091a456291c2f2915f259 Mon Sep 17 00:00:00 2001 From: deffcolony <61471128+deffcolony@users.noreply.github.com> Date: Thu, 28 Mar 2024 20:39:49 +0100 Subject: [PATCH 13/99] update issue template updates with correct label name --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 +- .github/ISSUE_TEMPLATE/feature-request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 81693c5db..4d09b5693 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,7 +1,7 @@ name: Bug Report 🐛 description: Report something that's not working the intended way. Support requests for external programs (reverse proxies, 3rd party servers, other peoples' forks) will be refused! title: '[BUG] ' -labels: ['bug'] +labels: ['🐛Bug'] body: - type: dropdown id: environment diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 9494c7224..4180f93da 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -1,7 +1,7 @@ name: Feature Request ✨ description: Suggest an idea for future development of this project title: '[FEATURE_REQUEST] <title>' -labels: ['enhancement'] +labels: ['🦄 Feature Request'] body: From c91ffb04f36d5e5293161e6f75ca9b0a8fbf0c9b Mon Sep 17 00:00:00 2001 From: deffcolony <61471128+deffcolony@users.noreply.github.com> Date: Thu, 28 Mar 2024 20:43:45 +0100 Subject: [PATCH 14/99] small correction --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 4d09b5693..82cc9c9cf 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,7 +1,7 @@ name: Bug Report 🐛 description: Report something that's not working the intended way. Support requests for external programs (reverse proxies, 3rd party servers, other peoples' forks) will be refused! title: '[BUG] <title>' -labels: ['🐛Bug'] +labels: ['🐛 Bug'] body: - type: dropdown id: environment From fcdd90cec43ff7864de8c391b4569a617268c609 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 28 Mar 2024 21:51:02 +0200 Subject: [PATCH 15/99] Adjust last prompt line of quite gens --- public/index.html | 4 +- public/script.js | 11 +++-- public/scripts/instruct-mode.js | 77 +++++++++++++++++--------------- public/scripts/openai.js | 4 +- public/scripts/slash-commands.js | 6 ++- 5 files changed, 58 insertions(+), 44 deletions(-) diff --git a/public/index.html b/public/index.html index a7a489d6c..d79109cd2 100644 --- a/public/index.html +++ b/public/index.html @@ -2930,7 +2930,7 @@ </div> </div> <div class="flex-container"> - <div class="flex1" title="Inserted before a System (added by slash commands or extensions) message and as a last prompt line when generating a neutral/system reply."> + <div class="flex1" title="Inserted before a System (added by slash commands or extensions) message."> <label for="instruct_system_sequence"> <small data-i18n="System Message Prefix">System Message Prefix</small> </label> @@ -2965,7 +2965,7 @@ <textarea id="instruct_first_output_sequence" class="text_pole textarea_compact autoSetHeight" maxlength="2000" placeholder="—" rows="1"></textarea> </div> </div> - <div class="flex1" title="Inserted before the last Assistant's message."> + <div class="flex1" title="Inserted before the last Assistant's message or as a last prompt line when generating an AI reply (except a neutral/system role)."> <label for="instruct_last_output_sequence"> <small data-i18n="Last Output Prefix">Last Output Prefix</small> </label> diff --git a/public/script.js b/public/script.js index f77bd991f..c2096a108 100644 --- a/public/script.js +++ b/public/script.js @@ -2845,20 +2845,22 @@ class StreamingProcessor { * @param {string} prompt Prompt to generate a message from * @param {string} api API to use. Main API is used if not specified. * @param {boolean} instructOverride true to override instruct mode, false to use the default value + * @param {boolean} quietToLoud true to generate a message in system mode, false to generate a message in character mode * @returns {Promise<string>} Generated message */ -export async function generateRaw(prompt, api, instructOverride) { +export async function generateRaw(prompt, api, instructOverride, quietToLoud) { if (!api) { api = main_api; } const abortController = new AbortController(); const isInstruct = power_user.instruct.enabled && main_api !== 'openai' && main_api !== 'novel' && !instructOverride; + const isQuiet = true; prompt = substituteParams(prompt); prompt = api == 'novel' ? adjustNovelInstructionPrompt(prompt) : prompt; prompt = isInstruct ? formatInstructModeChat(name1, prompt, false, true, '', name1, name2, false) : prompt; - prompt = isInstruct ? (prompt + formatInstructModePrompt(name2, false, '', name1, name2)) : (prompt + '\n'); + prompt = isInstruct ? (prompt + formatInstructModePrompt(name2, false, '', name1, name2, isQuiet, quietToLoud)) : (prompt + '\n'); let generateData = {}; @@ -3561,7 +3563,8 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu // Get instruct mode line if (isInstruct && !isContinue) { const name = (quiet_prompt && !quietToLoud) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2); - lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2); + const isQuiet = quiet_prompt && type == 'quiet'; + lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, quietToLoud); } // Get non-instruct impersonation line @@ -8351,7 +8354,7 @@ function addDebugFunctions() { registerDebugFunction('generationTest', 'Send a generation request', 'Generates text using the currently selected API.', async () => { const text = prompt('Input text:', 'Hello'); toastr.info('Working on it...'); - const message = await generateRaw(text, null, ''); + const message = await generateRaw(text, null, false, false); alert(message); }); diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index e30d81a3f..eaf0df137 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -46,40 +46,25 @@ const controls = [ */ function migrateInstructModeSettings(settings) { // Separator sequence => Output suffix - if (settings.separator_sequence === undefined) { - return; + if (settings.separator_sequence !== undefined) { + settings.output_suffix = settings.separator_sequence || ''; + delete settings.separator_sequence; } - settings.output_suffix = settings.separator_sequence || ''; - delete settings.separator_sequence; + const defaults = { + input_suffix: '', + system_sequence: '', + system_suffix: '', + user_alignment_message: '', + names_force_groups: true, + skip_examples: false, + system_same_as_user: false, + }; - // Init the rest with default values - if (settings.input_suffix === undefined) { - settings.input_suffix = ''; - } - - if (settings.system_sequence === undefined) { - settings.system_sequence = ''; - } - - if (settings.system_suffix === undefined) { - settings.system_suffix = ''; - } - - if (settings.user_alignment_message === undefined) { - settings.user_alignment_message = ''; - } - - if (settings.names_force_groups === undefined) { - settings.names_force_groups = true; - } - - if (settings.skip_examples === undefined) { - settings.skip_examples = false; - } - - if (settings.system_same_as_user === undefined) { - settings.system_same_as_user = false; + for (let key in defaults) { + if (settings[key] === undefined) { + settings[key] = defaults[key]; + } } } @@ -436,12 +421,34 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { * @param {string} promptBias Prompt bias string. * @param {string} name1 User name. * @param {string} name2 Character name. + * @param {boolean} isQuiet Is quiet mode generation. + * @param {boolean} isQuietToLoud Is quiet to loud generation. * @returns {string} Formatted instruct mode last prompt line. */ -export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2) { - const includeNames = name && (power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups)); - const getOutputSequence = () => power_user.instruct.last_output_sequence || power_user.instruct.output_sequence; - let sequence = isImpersonate ? power_user.instruct.input_sequence : getOutputSequence(); +export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, isQuietToLoud) { + const includeNames = name && (power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups)) && !(isQuiet && !isQuietToLoud); + + function getSequence() { + // User impersonation prompt + if (isImpersonate) { + return power_user.instruct.input_sequence; + } + + // Neutral / system prompt + if (isQuiet && !isQuietToLoud) { + return power_user.instruct.output_sequence; + } + + // Quiet in-character prompt + if (isQuiet && isQuietToLoud) { + return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence; + } + + // Default AI response + return power_user.instruct.last_output_sequence || power_user.instruct.output_sequence; + } + + let sequence = getSequence() || ''; if (power_user.instruct.macro) { sequence = substituteParams(sequence, name1, name2); diff --git a/public/scripts/openai.js b/public/scripts/openai.js index bdfd3b827..e7b83c285 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -448,8 +448,10 @@ function convertChatCompletionToInstruct(messages, type) { const isImpersonate = type === 'impersonate'; const isContinue = type === 'continue'; + const isQuiet = type === 'quiet'; + const isQuietToLoud = false; // Quiet to loud not implemented for Chat Completion const promptName = isImpersonate ? name1 : name2; - const promptLine = isContinue ? '' : formatInstructModePrompt(promptName, isImpersonate, '', name1, name2).trimStart(); + const promptLine = isContinue ? '' : formatInstructModePrompt(promptName, isImpersonate, '', name1, name2, isQuiet, isQuietToLoud).trimStart(); let prompt = [systemPromptText, examplesText, chatMessagesText, promptLine] .filter(x => x) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 20f3e8666..7d4c63dbe 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -231,7 +231,7 @@ parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '<span class="m parser.addCommand('echo', echoCallback, [], '<span class="monospace">(title=string severity=info/warning/error/success [text])</span> – echoes the text to toast message. Useful for pipes debugging.', true, true); //parser.addCommand('#', (_, value) => '', [], ' – a comment, does nothing, e.g. <tt>/# the next three commands switch variables a and b</tt>', true, true); parser.addCommand('gen', generateCallback, [], '<span class="monospace">(lock=on/off name="System" [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating and allowing to configure the in-prompt name for instruct mode (default = "System").', true, true); -parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(lock=on/off [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. <tt>/genraw instruct=off Why is the sky blue?</tt>. Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. <tt>/genraw stop=["\\n"] Say hi</tt>', true, true); +parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(lock=on/off instruct=on/off stop=[] as=system/char [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. <tt>/genraw instruct=off Why is the sky blue?</tt>. Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. <tt>/genraw stop=["\\n"] Say hi</tt>. "as" argument controls the role of the output prompt: system (default) or char.', true, true); parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '<span class="monospace">(text)</span> – adds a swipe to the last chat message.', true, true); parser.addCommand('abort', abortCallback, [], ' – aborts the slash command batch execution', true, true); parser.addCommand('fuzzy', fuzzyCallback, [], 'list=["a","b","c"] threshold=0.4 (text to search) – performs a fuzzy match of each items of list within the text to search. If any item matches then its name is returned. If no item list matches the text to search then no value is returned. The optional threshold (default is 0.4) allows some control over the matching. A low value (min 0.0) means the match is very strict. At 1.0 (max) the match is very loose and probably matches anything. The returned value passes to the next command through the pipe.', true, true); parser.addCommand('pass', (_, arg) => arg, ['return'], '<span class="monospace">(text)</span> – passes the text to the next command through the pipe.', true, true); @@ -659,6 +659,8 @@ async function generateRawCallback(args, value) { // Prevent generate recursion $('#send_textarea').val('').trigger('input'); const lock = isTrueBoolean(args?.lock); + const as = args?.as || 'system'; + const quietToLoud = as === 'char'; try { if (lock) { @@ -666,7 +668,7 @@ async function generateRawCallback(args, value) { } setEphemeralStopStrings(resolveVariable(args?.stop)); - const result = await generateRaw(value, '', isFalseBoolean(args?.instruct)); + const result = await generateRaw(value, '', isFalseBoolean(args?.instruct), quietToLoud); return result; } finally { if (lock) { From af7c89678c79ef92ec65077480a8755ece1e28d2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 28 Mar 2024 22:36:18 +0200 Subject: [PATCH 16/99] Add .gitkeeps --- public/characters/{README.md => .gitkeep} | 0 public/chats/{README.md => .gitkeep} | 0 public/context/.gitkeep | 0 public/instruct/.gitkeep | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename public/characters/{README.md => .gitkeep} (100%) rename public/chats/{README.md => .gitkeep} (100%) create mode 100644 public/context/.gitkeep create mode 100644 public/instruct/.gitkeep diff --git a/public/characters/README.md b/public/characters/.gitkeep similarity index 100% rename from public/characters/README.md rename to public/characters/.gitkeep diff --git a/public/chats/README.md b/public/chats/.gitkeep similarity index 100% rename from public/chats/README.md rename to public/chats/.gitkeep diff --git a/public/context/.gitkeep b/public/context/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/public/instruct/.gitkeep b/public/instruct/.gitkeep new file mode 100644 index 000000000..e69de29bb From 4f58e04ef3e89433a93045e37a4fdeec2ba9207e Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 28 Mar 2024 22:40:43 +0200 Subject: [PATCH 17/99] Move default instruct/context templates out of public --- .../content/presets/context/Adventure.json | 12 +++++++++ .../presets/context/Alpaca-Roleplay.json | 12 +++++++++ .../presets/context/Alpaca-Single-Turn.json | 12 +++++++++ default/content/presets/context/ChatML.json | 12 +++++++++ default/content/presets/context/Default.json | 12 +++++++++ .../context/DreamGen Role-Play V1.json | 12 +++++++++ .../content/presets/context/Libra-32B.json | 12 +++++++++ .../presets/context/Lightning 1.1.json | 12 +++++++++ .../content/presets/context/Minimalist.json | 12 +++++++++ default/content/presets/context/Mistral.json | 12 +++++++++ default/content/presets/context/NovelAI.json | 12 +++++++++ .../content/presets/context/OldDefault.json | 12 +++++++++ .../content/presets/context/Pygmalion.json | 12 +++++++++ default/content/presets/context/Story.json | 12 +++++++++ .../context/simple-proxy-for-tavern.json | 12 +++++++++ .../content/presets}/instruct/Adventure.json | 7 ++++- .../presets}/instruct/Alpaca-Roleplay.json | 20 +++++++++----- .../presets}/instruct/Alpaca-Single-Turn.json | 14 +++++++--- .../content/presets}/instruct/Alpaca.json | 20 +++++++++----- default/content/presets/instruct/ChatML.json | 23 ++++++++++++++++ .../instruct/DreamGen Role-Play V1.json | 19 +++++++++----- .../content/presets}/instruct/Koala.json | 20 +++++++++----- .../content/presets}/instruct/Libra-32B.json | 22 ++++++++++------ .../presets}/instruct/Lightning 1.1.json | 23 +++++++++------- .../presets/instruct/Llama 2 Chat.json | 23 ++++++++++++++++ .../content/presets}/instruct/Metharme.json | 20 +++++++++----- .../content/presets}/instruct/Mistral.json | 24 ++++++++++------- .../presets}/instruct/OpenOrca-OpenChat.json | 24 ++++++++++------- .../content/presets}/instruct/Pygmalion.json | 20 +++++++++----- .../content/presets}/instruct/Story.json | 7 ++++- .../content/presets}/instruct/Synthia.json | 24 ++++++++++------- .../content/presets}/instruct/Vicuna 1.0.json | 20 +++++++++----- .../content/presets}/instruct/Vicuna 1.1.json | 20 +++++++++----- .../presets}/instruct/WizardLM-13B.json | 20 +++++++++----- .../content/presets}/instruct/WizardLM.json | 20 +++++++++----- .../instruct/simple-proxy-for-tavern.json | 26 ++++++++++++------- public/context/Adventure.json | 2 ++ public/context/Alpaca-Roleplay.json | 12 ++++++--- public/context/Alpaca-Single-Turn.json | 1 + public/context/ChatML.json | 12 ++++++--- public/context/Default.json | 12 ++++++--- public/context/DreamGen Role-Play V1.json | 1 + public/context/Libra-32B.json | 8 +++++- public/context/Lightning 1.1.json | 10 +++++-- public/context/Minimalist.json | 12 ++++++--- public/context/Mistral.json | 8 +++++- public/context/NovelAI.json | 10 +++++-- public/context/OldDefault.json | 8 +++++- public/context/Pygmalion.json | 12 ++++++--- public/context/Story.json | 8 +++++- public/context/simple-proxy-for-tavern.json | 12 ++++++--- public/instruct/ChatML.json | 17 ------------ public/instruct/Llama 2 Chat.json | 17 ------------ 53 files changed, 568 insertions(+), 190 deletions(-) create mode 100644 default/content/presets/context/Adventure.json create mode 100644 default/content/presets/context/Alpaca-Roleplay.json create mode 100644 default/content/presets/context/Alpaca-Single-Turn.json create mode 100644 default/content/presets/context/ChatML.json create mode 100644 default/content/presets/context/Default.json create mode 100644 default/content/presets/context/DreamGen Role-Play V1.json create mode 100644 default/content/presets/context/Libra-32B.json create mode 100644 default/content/presets/context/Lightning 1.1.json create mode 100644 default/content/presets/context/Minimalist.json create mode 100644 default/content/presets/context/Mistral.json create mode 100644 default/content/presets/context/NovelAI.json create mode 100644 default/content/presets/context/OldDefault.json create mode 100644 default/content/presets/context/Pygmalion.json create mode 100644 default/content/presets/context/Story.json create mode 100644 default/content/presets/context/simple-proxy-for-tavern.json rename {public => default/content/presets}/instruct/Adventure.json (79%) rename {public => default/content/presets}/instruct/Alpaca-Roleplay.json (70%) rename {public => default/content/presets}/instruct/Alpaca-Single-Turn.json (77%) rename {public => default/content/presets}/instruct/Alpaca.json (69%) create mode 100644 default/content/presets/instruct/ChatML.json rename {public => default/content/presets}/instruct/DreamGen Role-Play V1.json (56%) rename {public => default/content/presets}/instruct/Koala.json (64%) rename {public => default/content/presets}/instruct/Libra-32B.json (74%) rename {public => default/content/presets}/instruct/Lightning 1.1.json (82%) create mode 100644 default/content/presets/instruct/Llama 2 Chat.json rename {public => default/content/presets}/instruct/Metharme.json (63%) rename {public => default/content/presets}/instruct/Mistral.json (52%) rename {public => default/content/presets}/instruct/OpenOrca-OpenChat.json (72%) rename {public => default/content/presets}/instruct/Pygmalion.json (68%) rename {public => default/content/presets}/instruct/Story.json (70%) rename {public => default/content/presets}/instruct/Synthia.json (58%) rename {public => default/content/presets}/instruct/Vicuna 1.0.json (70%) rename {public => default/content/presets}/instruct/Vicuna 1.1.json (71%) rename {public => default/content/presets}/instruct/WizardLM-13B.json (70%) rename {public => default/content/presets}/instruct/WizardLM.json (62%) rename {public => default/content/presets}/instruct/simple-proxy-for-tavern.json (58%) delete mode 100644 public/instruct/ChatML.json delete mode 100644 public/instruct/Llama 2 Chat.json diff --git a/default/content/presets/context/Adventure.json b/default/content/presets/context/Adventure.json new file mode 100644 index 000000000..3318d518f --- /dev/null +++ b/default/content/presets/context/Adventure.json @@ -0,0 +1,12 @@ +{ + "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": false, + "trim_sentences": false, + "include_newline": false, + "single_line": true, + "name": "Adventure" +} \ No newline at end of file diff --git a/default/content/presets/context/Alpaca-Roleplay.json b/default/content/presets/context/Alpaca-Roleplay.json new file mode 100644 index 000000000..d564a1dd7 --- /dev/null +++ b/default/content/presets/context/Alpaca-Roleplay.json @@ -0,0 +1,12 @@ +{ + "story_string": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.\n\n{{#if system}}{{system}}\n\n{{/if}}### Input:\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "### New Roleplay:", + "chat_start": "### New Roleplay:", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Alpaca-Roleplay" +} \ No newline at end of file diff --git a/default/content/presets/context/Alpaca-Single-Turn.json b/default/content/presets/context/Alpaca-Single-Turn.json new file mode 100644 index 000000000..ea58fe9d5 --- /dev/null +++ b/default/content/presets/context/Alpaca-Single-Turn.json @@ -0,0 +1,12 @@ +{ + "story_string": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": false, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Alpaca-Single-Turn" +} \ No newline at end of file diff --git a/default/content/presets/context/ChatML.json b/default/content/presets/context/ChatML.json new file mode 100644 index 000000000..8515598f3 --- /dev/null +++ b/default/content/presets/context/ChatML.json @@ -0,0 +1,12 @@ +{ + "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "ChatML" +} \ No newline at end of file diff --git a/default/content/presets/context/Default.json b/default/content/presets/context/Default.json new file mode 100644 index 000000000..7c8a231cf --- /dev/null +++ b/default/content/presets/context/Default.json @@ -0,0 +1,12 @@ +{ + "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "***", + "chat_start": "***", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Default" +} \ No newline at end of file diff --git a/default/content/presets/context/DreamGen Role-Play V1.json b/default/content/presets/context/DreamGen Role-Play V1.json new file mode 100644 index 000000000..6698d27fa --- /dev/null +++ b/default/content/presets/context/DreamGen Role-Play V1.json @@ -0,0 +1,12 @@ +{ + "story_string": "<|im_start|>system\n{{#if system}}{{system}}\n\n\n{{/if}}## Overall plot description:\n\n{{#if scenario}}{{scenario}}{{else}}Conversation between {{char}} and {{user}}.{{/if}}{{#if wiBefore}}\n\n{{wiBefore}}{{/if}}\n\n\n## Characters:\n\n### {{char}}\n\n{{#if description}}{{description}}\n\n{{/if}}{{#if personality}}{{personality}}\n\n{{/if}}### {{user}}\n\n{{#if persona}}{{persona}}{{else}}{{user}} is the protagonist of the role-play.{{/if}}{{#if wiAfter}}\n\n{{wiAfter}}{{/if}}{{#if mesExamples}}\n\n{{mesExamples}}{{/if}}", + "example_separator": "", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": false, + "trim_sentences": true, + "include_newline": false, + "single_line": false, + "name": "DreamGen Role-Play V1" +} \ No newline at end of file diff --git a/default/content/presets/context/Libra-32B.json b/default/content/presets/context/Libra-32B.json new file mode 100644 index 000000000..b5dee2872 --- /dev/null +++ b/default/content/presets/context/Libra-32B.json @@ -0,0 +1,12 @@ +{ + "story_string": "### Instruction:\nWrite {{char}}'s next reply in this roleplay with {{user}}. Use the provided character sheet and example dialogue for formatting direction and character speech patterns.\n\n{{#if system}}{{system}}\n\n{{/if}}### Character Sheet:\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "### Example:", + "chat_start": "### START ROLEPLAY:", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Libra-32B" +} \ No newline at end of file diff --git a/default/content/presets/context/Lightning 1.1.json b/default/content/presets/context/Lightning 1.1.json new file mode 100644 index 000000000..3b0190c92 --- /dev/null +++ b/default/content/presets/context/Lightning 1.1.json @@ -0,0 +1,12 @@ +{ + "story_string": "{{system}}\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{char}}'s description:{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality:{{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{user}}'s persona: {{persona}}\n{{/if}}", + "example_separator": "Example of an interaction:", + "chat_start": "This is the history of the roleplay:", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Lightning 1.1" +} \ No newline at end of file diff --git a/default/content/presets/context/Minimalist.json b/default/content/presets/context/Minimalist.json new file mode 100644 index 000000000..cc7550c51 --- /dev/null +++ b/default/content/presets/context/Minimalist.json @@ -0,0 +1,12 @@ +{ + "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Minimalist" +} \ No newline at end of file diff --git a/default/content/presets/context/Mistral.json b/default/content/presets/context/Mistral.json new file mode 100644 index 000000000..d8c437e0e --- /dev/null +++ b/default/content/presets/context/Mistral.json @@ -0,0 +1,12 @@ +{ + "story_string": "[INST] {{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}[/INST]", + "example_separator": "Examples:", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Mistral" +} \ No newline at end of file diff --git a/default/content/presets/context/NovelAI.json b/default/content/presets/context/NovelAI.json new file mode 100644 index 000000000..1a7887a90 --- /dev/null +++ b/default/content/presets/context/NovelAI.json @@ -0,0 +1,12 @@ +{ + "story_string": "{{#if system}}{{system}}{{/if}}\n{{#if wiBefore}}{{wiBefore}}{{/if}}\n{{#if persona}}{{persona}}{{/if}}\n{{#if description}}{{description}}{{/if}}\n{{#if personality}}Personality: {{personality}}{{/if}}\n{{#if scenario}}Scenario: {{scenario}}{{/if}}\n{{#if wiAfter}}{{wiAfter}}{{/if}}", + "example_separator": "***", + "chat_start": "***", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "NovelAI" +} \ No newline at end of file diff --git a/default/content/presets/context/OldDefault.json b/default/content/presets/context/OldDefault.json new file mode 100644 index 000000000..542971f21 --- /dev/null +++ b/default/content/presets/context/OldDefault.json @@ -0,0 +1,12 @@ +{ + "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Circumstances and context of the dialogue: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "This is how {{char}} should talk", + "chat_start": "\nThen the roleplay chat between {{user}} and {{char}} begins.\n", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "OldDefault" +} \ No newline at end of file diff --git a/default/content/presets/context/Pygmalion.json b/default/content/presets/context/Pygmalion.json new file mode 100644 index 000000000..68de8c1d0 --- /dev/null +++ b/default/content/presets/context/Pygmalion.json @@ -0,0 +1,12 @@ +{ + "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Pygmalion" +} \ No newline at end of file diff --git a/default/content/presets/context/Story.json b/default/content/presets/context/Story.json new file mode 100644 index 000000000..26f70937b --- /dev/null +++ b/default/content/presets/context/Story.json @@ -0,0 +1,12 @@ +{ + "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Story" +} \ No newline at end of file diff --git a/default/content/presets/context/simple-proxy-for-tavern.json b/default/content/presets/context/simple-proxy-for-tavern.json new file mode 100644 index 000000000..38003c68d --- /dev/null +++ b/default/content/presets/context/simple-proxy-for-tavern.json @@ -0,0 +1,12 @@ +{ + "story_string": "## {{char}}\n- You're \"{{char}}\" in this never-ending roleplay with \"{{user}}\".\n### Input:\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}### Response:\n(OOC) Understood. I will take this info into account for the roleplay. (end OOC)", + "example_separator": "### New Roleplay:", + "chat_start": "### New Roleplay:", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "simple-proxy-for-tavern" +} \ No newline at end of file diff --git a/public/instruct/Adventure.json b/default/content/presets/instruct/Adventure.json similarity index 79% rename from public/instruct/Adventure.json rename to default/content/presets/instruct/Adventure.json index 29fe38871..827c6d5c9 100644 --- a/public/instruct/Adventure.json +++ b/default/content/presets/instruct/Adventure.json @@ -5,7 +5,6 @@ "last_output_sequence": "", "system_sequence": "", "stop_sequence": "", - "separator_sequence": "", "wrap": true, "macro": true, "names": false, @@ -14,5 +13,11 @@ "system_sequence_prefix": "", "system_sequence_suffix": "", "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": false, "name": "Adventure" } \ No newline at end of file diff --git a/public/instruct/Alpaca-Roleplay.json b/default/content/presets/instruct/Alpaca-Roleplay.json similarity index 70% rename from public/instruct/Alpaca-Roleplay.json rename to default/content/presets/instruct/Alpaca-Roleplay.json index 757c5fa77..37bf6cae7 100644 --- a/public/instruct/Alpaca-Roleplay.json +++ b/default/content/presets/instruct/Alpaca-Roleplay.json @@ -1,17 +1,23 @@ { - "name": "Alpaca-Roleplay", "system_prompt": "Develop the plot slowly, always stay in character. Describe all actions in full, elaborate, explicit, graphic, and vivid detail. Mention all relevant sensory perceptions.", "input_sequence": "\n### Instruction:", "output_sequence": "\n### Response:", - "first_output_sequence": "", "last_output_sequence": "\n### Response (2 paragraphs, engaging, natural, authentic, descriptive, creative):", - "system_sequence_prefix": "", - "system_sequence_suffix": "", + "system_sequence": "", "stop_sequence": "", - "separator_sequence": "", "wrap": true, "macro": true, "names": true, "names_force_groups": true, - "activation_regex": "" -} + "activation_regex": "", + "system_sequence_prefix": "", + "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": true, + "name": "Alpaca-Roleplay" +} \ No newline at end of file diff --git a/public/instruct/Alpaca-Single-Turn.json b/default/content/presets/instruct/Alpaca-Single-Turn.json similarity index 77% rename from public/instruct/Alpaca-Single-Turn.json rename to default/content/presets/instruct/Alpaca-Single-Turn.json index a86359b58..6a6f052d2 100644 --- a/public/instruct/Alpaca-Single-Turn.json +++ b/default/content/presets/instruct/Alpaca-Single-Turn.json @@ -2,16 +2,22 @@ "system_prompt": "Write {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\nWrite 1 reply only, italicize actions, and avoid quotation marks. Use markdown. Be proactive, creative, and drive the plot and conversation forward. Include dialog as well as narration.", "input_sequence": "", "output_sequence": "", - "first_output_sequence": "<START OF ROLEPLAY>", "last_output_sequence": "\n### Response:", - "system_sequence_prefix": "", - "system_sequence_suffix": "", + "system_sequence": "", "stop_sequence": "", - "separator_sequence": "", "wrap": true, "macro": true, "names": false, "names_force_groups": true, "activation_regex": "", + "system_sequence_prefix": "", + "system_sequence_suffix": "", + "first_output_sequence": "<START OF ROLEPLAY>", + "skip_examples": false, + "output_suffix": "", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": false, "name": "Alpaca-Single-Turn" } \ No newline at end of file diff --git a/public/instruct/Alpaca.json b/default/content/presets/instruct/Alpaca.json similarity index 69% rename from public/instruct/Alpaca.json rename to default/content/presets/instruct/Alpaca.json index 2d48e586c..f3b3b4cc8 100644 --- a/public/instruct/Alpaca.json +++ b/default/content/presets/instruct/Alpaca.json @@ -1,17 +1,23 @@ { - "name": "Alpaca", "system_prompt": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\nWrite {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n", "input_sequence": "### Instruction:", "output_sequence": "### Response:", - "first_output_sequence": "", "last_output_sequence": "", - "system_sequence_prefix": "", - "system_sequence_suffix": "", + "system_sequence": "", "stop_sequence": "", - "separator_sequence": "", "wrap": true, "macro": true, "names": false, "names_force_groups": true, - "activation_regex": "" -} + "activation_regex": "", + "system_sequence_prefix": "", + "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": true, + "name": "Alpaca" +} \ No newline at end of file diff --git a/default/content/presets/instruct/ChatML.json b/default/content/presets/instruct/ChatML.json new file mode 100644 index 000000000..bd19cabd7 --- /dev/null +++ b/default/content/presets/instruct/ChatML.json @@ -0,0 +1,23 @@ +{ + "system_prompt": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.", + "input_sequence": "\n<|im_start|>user\n", + "output_sequence": "\n<|im_start|>assistant\n", + "last_output_sequence": "", + "system_sequence": "\n<|im_start|>system\n", + "stop_sequence": "", + "wrap": false, + "macro": true, + "names": true, + "names_force_groups": true, + "activation_regex": "", + "system_sequence_prefix": "<|im_start|>system\n", + "system_sequence_suffix": "<|im_end|>", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "<|im_end|>", + "input_suffix": "<|im_end|>", + "system_suffix": "<|im_end|>", + "user_alignment_message": "", + "system_same_as_user": false, + "name": "ChatML" +} \ No newline at end of file diff --git a/public/instruct/DreamGen Role-Play V1.json b/default/content/presets/instruct/DreamGen Role-Play V1.json similarity index 56% rename from public/instruct/DreamGen Role-Play V1.json rename to default/content/presets/instruct/DreamGen Role-Play V1.json index 419aec4d7..07f0301fc 100644 --- a/public/instruct/DreamGen Role-Play V1.json +++ b/default/content/presets/instruct/DreamGen Role-Play V1.json @@ -1,18 +1,23 @@ { "system_prompt": "You are an intelligent, skilled, versatile writer.\n\nYour task is to write a role-play based on the information below.", - "input_sequence": "<|im_end|>\n<|im_start|>text names= {{user}}\n", - "output_sequence": "<|im_end|>\n<|im_start|>text names= {{char}}\n", - "first_output_sequence": "", + "input_sequence": "\n<|im_start|>text names= {{name}}\n", + "output_sequence": "\n<|im_start|>text names= {{name}}\n", "last_output_sequence": "", - "system_sequence_prefix": "", - "system_sequence_suffix": "", - "stop_sequence": "", - "separator_sequence": "", + "system_sequence": "", + "stop_sequence": "\n<|im_start|>", "wrap": false, "macro": true, "names": false, "names_force_groups": false, "activation_regex": "", + "system_sequence_prefix": "", + "system_sequence_suffix": "", + "first_output_sequence": "", "skip_examples": false, + "output_suffix": "<|im_end|>", + "input_suffix": "<|im_end|>", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": true, "name": "DreamGen Role-Play V1" } \ No newline at end of file diff --git a/public/instruct/Koala.json b/default/content/presets/instruct/Koala.json similarity index 64% rename from public/instruct/Koala.json rename to default/content/presets/instruct/Koala.json index eeaf126d1..980482c1a 100644 --- a/public/instruct/Koala.json +++ b/default/content/presets/instruct/Koala.json @@ -1,17 +1,23 @@ { - "name": "Koala", "system_prompt": "Write {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n", "input_sequence": "USER: ", "output_sequence": "GPT: ", - "first_output_sequence": "", "last_output_sequence": "", - "system_sequence_prefix": "BEGINNING OF CONVERSATION: ", - "system_sequence_suffix": "", + "system_sequence": "", "stop_sequence": "", - "separator_sequence": "</s>", "wrap": false, "macro": true, "names": false, "names_force_groups": true, - "activation_regex": "" -} + "activation_regex": "", + "system_sequence_prefix": "BEGINNING OF CONVERSATION: ", + "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "</s>", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": true, + "name": "Koala" +} \ No newline at end of file diff --git a/public/instruct/Libra-32B.json b/default/content/presets/instruct/Libra-32B.json similarity index 74% rename from public/instruct/Libra-32B.json rename to default/content/presets/instruct/Libra-32B.json index 43ecef7a8..6014546f6 100644 --- a/public/instruct/Libra-32B.json +++ b/default/content/presets/instruct/Libra-32B.json @@ -1,17 +1,23 @@ { - "wrap": true, - "names": true, "system_prompt": "Avoid repetition, don't loop. Develop the plot slowly, always stay in character. Describe all actions in full, elaborate, explicit, graphic, and vivid detail. Mention all relevant sensory perceptions.", - "system_sequence_prefix": "", - "stop_sequence": "", "input_sequence": "", "output_sequence": "", - "separator_sequence": "", - "macro": true, - "names_force_groups": true, "last_output_sequence": "\n### Response:", + "system_sequence": "", + "stop_sequence": "", + "wrap": true, + "macro": true, + "names": true, + "names_force_groups": true, "activation_regex": "", - "first_output_sequence": "", + "system_sequence_prefix": "", "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": false, "name": "Libra-32B" } \ No newline at end of file diff --git a/public/instruct/Lightning 1.1.json b/default/content/presets/instruct/Lightning 1.1.json similarity index 82% rename from public/instruct/Lightning 1.1.json rename to default/content/presets/instruct/Lightning 1.1.json index a653af92d..bf79e1358 100644 --- a/public/instruct/Lightning 1.1.json +++ b/default/content/presets/instruct/Lightning 1.1.json @@ -1,18 +1,23 @@ { - "wrap": true, - "names": false, "system_prompt": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\nTake the role of {{char}} in a play that leaves a lasting impression on {{user}}. Write {{char}}'s next reply.\nNever skip or gloss over {{char}}’s actions. Progress the scene at a naturally slow pace.\n\n", - "system_sequence": "", - "stop_sequence": "", "input_sequence": "### Instruction:", "output_sequence": "### Response: (length = unlimited)", - "separator_sequence": "", - "macro": true, - "names_force_groups": true, "last_output_sequence": "", + "system_sequence": "", + "stop_sequence": "", + "wrap": true, + "macro": true, + "names": false, + "names_force_groups": true, + "activation_regex": "", "system_sequence_prefix": "", "system_sequence_suffix": "", "first_output_sequence": "", - "activation_regex": "", + "skip_examples": false, + "output_suffix": "", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": true, "name": "Lightning 1.1" -} +} \ No newline at end of file diff --git a/default/content/presets/instruct/Llama 2 Chat.json b/default/content/presets/instruct/Llama 2 Chat.json new file mode 100644 index 000000000..c129e71d1 --- /dev/null +++ b/default/content/presets/instruct/Llama 2 Chat.json @@ -0,0 +1,23 @@ +{ + "system_prompt": "Write {{char}}'s next reply in this fictional roleplay with {{user}}.", + "input_sequence": "[INST] ", + "output_sequence": " ", + "last_output_sequence": "", + "system_sequence": "", + "stop_sequence": "", + "wrap": false, + "macro": true, + "names": false, + "names_force_groups": true, + "activation_regex": "", + "system_sequence_prefix": "[INST] <<SYS>>\n", + "system_sequence_suffix": "\n<</SYS>>\n", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": " ", + "input_suffix": " [/INST]", + "system_suffix": "", + "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.", + "system_same_as_user": true, + "name": "Llama 2 Chat" +} \ No newline at end of file diff --git a/public/instruct/Metharme.json b/default/content/presets/instruct/Metharme.json similarity index 63% rename from public/instruct/Metharme.json rename to default/content/presets/instruct/Metharme.json index 818dafde7..1c8474cdf 100644 --- a/public/instruct/Metharme.json +++ b/default/content/presets/instruct/Metharme.json @@ -1,17 +1,23 @@ { - "name": "Metharme", "system_prompt": "Enter roleplay mode. You must act as {{char}}, whose persona follows:", "input_sequence": "<|user|>", "output_sequence": "<|model|>", - "first_output_sequence": "", "last_output_sequence": "", - "system_sequence_prefix": "<|system|>", - "system_sequence_suffix": "", + "system_sequence": "", "stop_sequence": "</s>", - "separator_sequence": "", "wrap": false, "macro": true, "names": false, "names_force_groups": true, - "activation_regex": "" -} + "activation_regex": "", + "system_sequence_prefix": "<|system|>", + "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": true, + "name": "Metharme" +} \ No newline at end of file diff --git a/public/instruct/Mistral.json b/default/content/presets/instruct/Mistral.json similarity index 52% rename from public/instruct/Mistral.json rename to default/content/presets/instruct/Mistral.json index 2cc52fda1..2dbf47fdc 100644 --- a/public/instruct/Mistral.json +++ b/default/content/presets/instruct/Mistral.json @@ -1,17 +1,23 @@ { - "wrap": false, - "names": true, "system_prompt": "Write {{char}}'s next reply in this fictional roleplay with {{user}}.", - "system_sequence_prefix": "", - "stop_sequence": "", "input_sequence": "[INST] ", - "output_sequence": " [/INST]\n", - "separator_sequence": "\n", - "macro": true, - "names_force_groups": true, + "output_sequence": "\n", "last_output_sequence": "", + "system_sequence": "", + "stop_sequence": "", + "wrap": false, + "macro": true, + "names": true, + "names_force_groups": true, "activation_regex": "", - "first_output_sequence": "\n", + "system_sequence_prefix": "", "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "\n", + "input_suffix": " [/INST]", + "system_suffix": "", + "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.", + "system_same_as_user": true, "name": "Mistral" } \ No newline at end of file diff --git a/public/instruct/OpenOrca-OpenChat.json b/default/content/presets/instruct/OpenOrca-OpenChat.json similarity index 72% rename from public/instruct/OpenOrca-OpenChat.json rename to default/content/presets/instruct/OpenOrca-OpenChat.json index 6eaf74fdd..924ea94f7 100644 --- a/public/instruct/OpenOrca-OpenChat.json +++ b/default/content/presets/instruct/OpenOrca-OpenChat.json @@ -1,17 +1,23 @@ { - "name": "OpenOrca-OpenChat", "system_prompt": "You are a helpful assistant. Please answer truthfully and write out your thinking step by step to be sure you get the right answer. If you make a mistake or encounter an error in your thinking, say so out loud and attempt to correct it. If you don't know or aren't sure about something, say so clearly. You will act as a professional logician, mathematician, and physicist. You will also act as the most appropriate type of expert to answer any particular question or solve the relevant problem; state which expert type your are, if so. Also think of any particular named expert that would be ideal to answer the relevant question or solve the relevant problem; name and act as them, if appropriate.\n", - "input_sequence": "User: ", - "output_sequence": "<|end_of_turn|>\nAssistant: ", - "first_output_sequence": "", + "input_sequence": "\nUser: ", + "output_sequence": "\nAssistant: ", "last_output_sequence": "", - "system_sequence_prefix": "", - "system_sequence_suffix": "", + "system_sequence": "", "stop_sequence": "", - "separator_sequence": "<|end_of_turn|>\n", "wrap": false, "macro": true, "names": false, "names_force_groups": true, - "activation_regex": "" -} + "activation_regex": "", + "system_sequence_prefix": "", + "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "<|end_of_turn|>", + "input_suffix": "<|end_of_turn|>", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": false, + "name": "OpenOrca-OpenChat" +} \ No newline at end of file diff --git a/public/instruct/Pygmalion.json b/default/content/presets/instruct/Pygmalion.json similarity index 68% rename from public/instruct/Pygmalion.json rename to default/content/presets/instruct/Pygmalion.json index 2e225bb47..6278c0d23 100644 --- a/public/instruct/Pygmalion.json +++ b/default/content/presets/instruct/Pygmalion.json @@ -1,17 +1,23 @@ { - "name": "Pygmalion", "system_prompt": "Enter RP mode. You shall reply to {{user}} while staying in character. Your responses must be detailed, creative, immersive, and drive the scenario forward. You will follow {{char}}'s persona.", "input_sequence": "<|user|>", "output_sequence": "<|model|>", - "first_output_sequence": "", "last_output_sequence": "", - "system_sequence_prefix": "<|system|>", - "system_sequence_suffix": "", + "system_sequence": "", "stop_sequence": "<|user|>", - "separator_sequence": "", "wrap": false, "macro": true, "names": true, "names_force_groups": true, - "activation_regex": "" -} + "activation_regex": "", + "system_sequence_prefix": "<|system|>", + "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": true, + "name": "Pygmalion" +} \ No newline at end of file diff --git a/public/instruct/Story.json b/default/content/presets/instruct/Story.json similarity index 70% rename from public/instruct/Story.json rename to default/content/presets/instruct/Story.json index 11b167afe..1e42d3281 100644 --- a/public/instruct/Story.json +++ b/default/content/presets/instruct/Story.json @@ -5,7 +5,6 @@ "last_output_sequence": "", "system_sequence": "", "stop_sequence": "", - "separator_sequence": "", "wrap": true, "macro": true, "names": false, @@ -14,5 +13,11 @@ "system_sequence_prefix": "", "system_sequence_suffix": "", "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": false, "name": "Story" } \ No newline at end of file diff --git a/public/instruct/Synthia.json b/default/content/presets/instruct/Synthia.json similarity index 58% rename from public/instruct/Synthia.json rename to default/content/presets/instruct/Synthia.json index 05f9fff9c..3315c9046 100644 --- a/public/instruct/Synthia.json +++ b/default/content/presets/instruct/Synthia.json @@ -1,17 +1,23 @@ { - "wrap": false, - "names": false, "system_prompt": "Elaborate on the topic using a Tree of Thoughts and backtrack when necessary to construct a clear, cohesive Chain of Thought reasoning. Always answer without hesitation.", - "system_sequence_prefix": "SYSTEM: ", - "stop_sequence": "", - "input_sequence": "USER: ", + "input_sequence": "\nUSER: ", "output_sequence": "\nASSISTANT: ", - "separator_sequence": "\n", - "macro": true, - "names_force_groups": true, "last_output_sequence": "", + "system_sequence": "", + "stop_sequence": "", + "wrap": false, + "macro": true, + "names": false, + "names_force_groups": true, "activation_regex": "", - "first_output_sequence": "ASSISTANT: ", + "system_sequence_prefix": "SYSTEM: ", "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.", + "system_same_as_user": true, "name": "Synthia" } \ No newline at end of file diff --git a/public/instruct/Vicuna 1.0.json b/default/content/presets/instruct/Vicuna 1.0.json similarity index 70% rename from public/instruct/Vicuna 1.0.json rename to default/content/presets/instruct/Vicuna 1.0.json index 1912e4885..fbc8a2bf5 100644 --- a/public/instruct/Vicuna 1.0.json +++ b/default/content/presets/instruct/Vicuna 1.0.json @@ -1,17 +1,23 @@ { - "name": "Vicuna 1.0", "system_prompt": "A chat between a curious human and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the human's questions.\n\nWrite {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n", "input_sequence": "### Human:", "output_sequence": "### Assistant:", - "first_output_sequence": "", "last_output_sequence": "", - "system_sequence_prefix": "", - "system_sequence_suffix": "", + "system_sequence": "", "stop_sequence": "", - "separator_sequence": "", "wrap": true, "macro": true, "names": false, "names_force_groups": true, - "activation_regex": "" -} + "activation_regex": "", + "system_sequence_prefix": "", + "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": true, + "name": "Vicuna 1.0" +} \ No newline at end of file diff --git a/public/instruct/Vicuna 1.1.json b/default/content/presets/instruct/Vicuna 1.1.json similarity index 71% rename from public/instruct/Vicuna 1.1.json rename to default/content/presets/instruct/Vicuna 1.1.json index fdab31e28..a31698d03 100644 --- a/public/instruct/Vicuna 1.1.json +++ b/default/content/presets/instruct/Vicuna 1.1.json @@ -1,17 +1,23 @@ { - "name": "Vicuna 1.1", "system_prompt": "A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.\n\nWrite {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n", "input_sequence": "\nUSER: ", "output_sequence": "\nASSISTANT: ", - "first_output_sequence": "", "last_output_sequence": "", - "system_sequence_prefix": "BEGINNING OF CONVERSATION:", - "system_sequence_suffix": "", + "system_sequence": "", "stop_sequence": "", - "separator_sequence": "</s>", "wrap": false, "macro": true, "names": false, "names_force_groups": true, - "activation_regex": "" -} + "activation_regex": "", + "system_sequence_prefix": "BEGINNING OF CONVERSATION:", + "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "</s>", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": true, + "name": "Vicuna 1.1" +} \ No newline at end of file diff --git a/public/instruct/WizardLM-13B.json b/default/content/presets/instruct/WizardLM-13B.json similarity index 70% rename from public/instruct/WizardLM-13B.json rename to default/content/presets/instruct/WizardLM-13B.json index 3b03c05f1..21e7bd555 100644 --- a/public/instruct/WizardLM-13B.json +++ b/default/content/presets/instruct/WizardLM-13B.json @@ -1,17 +1,23 @@ { - "name": "WizardLM-13B", "system_prompt": "A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.\n\nWrite {{char}}'s next detailed reply in a fictional roleplay chat between {{user}} and {{char}}.", "input_sequence": "USER: ", "output_sequence": "ASSISTANT: ", - "first_output_sequence": "", "last_output_sequence": "", - "system_sequence_prefix": "", - "system_sequence_suffix": "", + "system_sequence": "", "stop_sequence": "", - "separator_sequence": "", "wrap": true, "macro": true, "names": false, "names_force_groups": true, - "activation_regex": "" -} + "activation_regex": "", + "system_sequence_prefix": "", + "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": true, + "name": "WizardLM-13B" +} \ No newline at end of file diff --git a/public/instruct/WizardLM.json b/default/content/presets/instruct/WizardLM.json similarity index 62% rename from public/instruct/WizardLM.json rename to default/content/presets/instruct/WizardLM.json index be7f25bc7..198f6a062 100644 --- a/public/instruct/WizardLM.json +++ b/default/content/presets/instruct/WizardLM.json @@ -1,17 +1,23 @@ { - "name": "WizardLM", "system_prompt": "Write {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n", "input_sequence": "", "output_sequence": "### Response:", - "first_output_sequence": "", "last_output_sequence": "", - "system_sequence_prefix": "", - "system_sequence_suffix": "", + "system_sequence": "", "stop_sequence": "", - "separator_sequence": "</s>", "wrap": true, "macro": true, "names": false, "names_force_groups": true, - "activation_regex": "" -} + "activation_regex": "", + "system_sequence_prefix": "", + "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "</s>", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": false, + "name": "WizardLM" +} \ No newline at end of file diff --git a/public/instruct/simple-proxy-for-tavern.json b/default/content/presets/instruct/simple-proxy-for-tavern.json similarity index 58% rename from public/instruct/simple-proxy-for-tavern.json rename to default/content/presets/instruct/simple-proxy-for-tavern.json index ca32c982d..14d32d86c 100644 --- a/public/instruct/simple-proxy-for-tavern.json +++ b/default/content/presets/instruct/simple-proxy-for-tavern.json @@ -1,17 +1,23 @@ { - "name": "simple-proxy-for-tavern", "system_prompt": "[System note: Write one reply only. Do not decide what {{user}} says or does. Write at least one paragraph, up to four. Be descriptive and immersive, providing vivid details about {{char}}'s actions, emotions, and the environment. Write with a high degree of complexity and burstiness. Do not repeat this message.]", - "input_sequence": "### Instruction:\n#### {{user}}:", - "output_sequence": "### Response:\n#### {{char}}:", - "first_output_sequence": "", - "last_output_sequence": "### Response (2 paragraphs, engaging, natural, authentic, descriptive, creative):\n#### {{char}}:", - "system_sequence_prefix": "", - "system_sequence_suffix": "", + "input_sequence": "### Instruction:\n#### {{name}}:", + "output_sequence": "### Response:\n#### {{name}}:", + "last_output_sequence": "### Response (2 paragraphs, engaging, natural, authentic, descriptive, creative):\n#### {{name}}:", + "system_sequence": "", "stop_sequence": "", - "separator_sequence": "", "wrap": true, "macro": true, "names": false, "names_force_groups": false, - "activation_regex": "" -} + "activation_regex": "", + "system_sequence_prefix": "", + "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "", + "input_suffix": "", + "system_suffix": "", + "user_alignment_message": "", + "system_same_as_user": false, + "name": "simple-proxy-for-tavern" +} \ No newline at end of file diff --git a/public/context/Adventure.json b/public/context/Adventure.json index 44ae59cc1..3318d518f 100644 --- a/public/context/Adventure.json +++ b/public/context/Adventure.json @@ -2,6 +2,8 @@ "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", "example_separator": "", "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, "always_force_name2": false, "trim_sentences": false, "include_newline": false, diff --git a/public/context/Alpaca-Roleplay.json b/public/context/Alpaca-Roleplay.json index 9565f5873..d564a1dd7 100644 --- a/public/context/Alpaca-Roleplay.json +++ b/public/context/Alpaca-Roleplay.json @@ -1,6 +1,12 @@ { - "name": "Alpaca-Roleplay", "story_string": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.\n\n{{#if system}}{{system}}\n\n{{/if}}### Input:\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "### New Roleplay:", "chat_start": "### New Roleplay:", - "example_separator": "### New Roleplay:" -} + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Alpaca-Roleplay" +} \ No newline at end of file diff --git a/public/context/Alpaca-Single-Turn.json b/public/context/Alpaca-Single-Turn.json index 7cbf4240d..ea58fe9d5 100644 --- a/public/context/Alpaca-Single-Turn.json +++ b/public/context/Alpaca-Single-Turn.json @@ -3,6 +3,7 @@ "example_separator": "", "chat_start": "", "use_stop_strings": false, + "allow_jailbreak": false, "always_force_name2": false, "trim_sentences": false, "include_newline": false, diff --git a/public/context/ChatML.json b/public/context/ChatML.json index e4e17d623..8515598f3 100644 --- a/public/context/ChatML.json +++ b/public/context/ChatML.json @@ -1,6 +1,12 @@ { - "story_string": "<|im_start|>system\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}<|im_end|>", - "chat_start": "", + "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", "example_separator": "", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, "name": "ChatML" -} +} \ No newline at end of file diff --git a/public/context/Default.json b/public/context/Default.json index 27ec1ea93..7c8a231cf 100644 --- a/public/context/Default.json +++ b/public/context/Default.json @@ -1,6 +1,12 @@ { - "name": "Default", "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "***", "chat_start": "***", - "example_separator": "***" -} + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Default" +} \ No newline at end of file diff --git a/public/context/DreamGen Role-Play V1.json b/public/context/DreamGen Role-Play V1.json index 24ed8b574..6698d27fa 100644 --- a/public/context/DreamGen Role-Play V1.json +++ b/public/context/DreamGen Role-Play V1.json @@ -3,6 +3,7 @@ "example_separator": "", "chat_start": "", "use_stop_strings": false, + "allow_jailbreak": false, "always_force_name2": false, "trim_sentences": true, "include_newline": false, diff --git a/public/context/Libra-32B.json b/public/context/Libra-32B.json index 83207c99f..b5dee2872 100644 --- a/public/context/Libra-32B.json +++ b/public/context/Libra-32B.json @@ -1,6 +1,12 @@ { "story_string": "### Instruction:\nWrite {{char}}'s next reply in this roleplay with {{user}}. Use the provided character sheet and example dialogue for formatting direction and character speech patterns.\n\n{{#if system}}{{system}}\n\n{{/if}}### Character Sheet:\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", - "chat_start": "### START ROLEPLAY:", "example_separator": "### Example:", + "chat_start": "### START ROLEPLAY:", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, "name": "Libra-32B" } \ No newline at end of file diff --git a/public/context/Lightning 1.1.json b/public/context/Lightning 1.1.json index 97dec26ce..3b0190c92 100644 --- a/public/context/Lightning 1.1.json +++ b/public/context/Lightning 1.1.json @@ -1,6 +1,12 @@ { "story_string": "{{system}}\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{char}}'s description:{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality:{{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{user}}'s persona: {{persona}}\n{{/if}}", - "chat_start": "This is the history of the roleplay:", "example_separator": "Example of an interaction:", + "chat_start": "This is the history of the roleplay:", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, "name": "Lightning 1.1" -} +} \ No newline at end of file diff --git a/public/context/Minimalist.json b/public/context/Minimalist.json index 92ee66755..cc7550c51 100644 --- a/public/context/Minimalist.json +++ b/public/context/Minimalist.json @@ -1,6 +1,12 @@ { - "name": "Minimalist", "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "", "chat_start": "", - "example_separator": "" -} + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Minimalist" +} \ No newline at end of file diff --git a/public/context/Mistral.json b/public/context/Mistral.json index 5497a0c18..d8c437e0e 100644 --- a/public/context/Mistral.json +++ b/public/context/Mistral.json @@ -1,6 +1,12 @@ { "story_string": "[INST] {{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}[/INST]", - "chat_start": "", "example_separator": "Examples:", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, "name": "Mistral" } \ No newline at end of file diff --git a/public/context/NovelAI.json b/public/context/NovelAI.json index b22590ab0..1a7887a90 100644 --- a/public/context/NovelAI.json +++ b/public/context/NovelAI.json @@ -1,6 +1,12 @@ { - "name": "NovelAI", "story_string": "{{#if system}}{{system}}{{/if}}\n{{#if wiBefore}}{{wiBefore}}{{/if}}\n{{#if persona}}{{persona}}{{/if}}\n{{#if description}}{{description}}{{/if}}\n{{#if personality}}Personality: {{personality}}{{/if}}\n{{#if scenario}}Scenario: {{scenario}}{{/if}}\n{{#if wiAfter}}{{wiAfter}}{{/if}}", + "example_separator": "***", "chat_start": "***", - "example_separator": "***" + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "NovelAI" } \ No newline at end of file diff --git a/public/context/OldDefault.json b/public/context/OldDefault.json index ff8b2b983..542971f21 100644 --- a/public/context/OldDefault.json +++ b/public/context/OldDefault.json @@ -1,6 +1,12 @@ { "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Circumstances and context of the dialogue: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", - "chat_start": "\nThen the roleplay chat between {{user}} and {{char}} begins.\n", "example_separator": "This is how {{char}} should talk", + "chat_start": "\nThen the roleplay chat between {{user}} and {{char}} begins.\n", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, "name": "OldDefault" } \ No newline at end of file diff --git a/public/context/Pygmalion.json b/public/context/Pygmalion.json index 1a57d73d7..68de8c1d0 100644 --- a/public/context/Pygmalion.json +++ b/public/context/Pygmalion.json @@ -1,6 +1,12 @@ { - "name": "Pygmalion", "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "", "chat_start": "", - "example_separator": "" -} + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Pygmalion" +} \ No newline at end of file diff --git a/public/context/Story.json b/public/context/Story.json index 90e7f09a1..26f70937b 100644 --- a/public/context/Story.json +++ b/public/context/Story.json @@ -1,6 +1,12 @@ { "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", - "chat_start": "", "example_separator": "", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, "name": "Story" } \ No newline at end of file diff --git a/public/context/simple-proxy-for-tavern.json b/public/context/simple-proxy-for-tavern.json index 99e19888a..38003c68d 100644 --- a/public/context/simple-proxy-for-tavern.json +++ b/public/context/simple-proxy-for-tavern.json @@ -1,6 +1,12 @@ { - "name": "simple-proxy-for-tavern", "story_string": "## {{char}}\n- You're \"{{char}}\" in this never-ending roleplay with \"{{user}}\".\n### Input:\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}### Response:\n(OOC) Understood. I will take this info into account for the roleplay. (end OOC)", + "example_separator": "### New Roleplay:", "chat_start": "### New Roleplay:", - "example_separator": "### New Roleplay:" -} + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "simple-proxy-for-tavern" +} \ No newline at end of file diff --git a/public/instruct/ChatML.json b/public/instruct/ChatML.json deleted file mode 100644 index 2fb02f9c1..000000000 --- a/public/instruct/ChatML.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "wrap": false, - "names": true, - "system_prompt": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.", - "system_sequence_prefix": "", - "stop_sequence": "", - "input_sequence": "<|im_start|>user\n", - "output_sequence": "<|im_end|>\n<|im_start|>assistant\n", - "separator_sequence": "<|im_end|>\n", - "macro": true, - "names_force_groups": true, - "last_output_sequence": "", - "activation_regex": "", - "first_output_sequence": "<|im_start|>assistant\n", - "system_sequence_suffix": "", - "name": "ChatML" -} \ No newline at end of file diff --git a/public/instruct/Llama 2 Chat.json b/public/instruct/Llama 2 Chat.json deleted file mode 100644 index 23eb2b346..000000000 --- a/public/instruct/Llama 2 Chat.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "Llama 2 Chat", - "system_prompt": "Write {{char}}'s next reply in this fictional roleplay with {{user}}.", - "input_sequence": "[INST] ", - "output_sequence": " [/INST] ", - "first_output_sequence": "[/INST] ", - "last_output_sequence": "", - "system_sequence_prefix": "[INST] <<SYS>>\n", - "system_sequence_suffix": "\n<</SYS>>\n", - "stop_sequence": "", - "separator_sequence": " ", - "wrap": false, - "macro": true, - "names": false, - "names_force_groups": true, - "activation_regex": "" -} From 0551c8023e9033ccd5ac3f5525355821340f6a3d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 28 Mar 2024 22:54:37 +0200 Subject: [PATCH 18/99] Move context/instruct templates to default context index --- default/content/index.json | 144 ++++++++++++++++++++ public/context/Adventure.json | 12 -- public/context/Alpaca-Roleplay.json | 12 -- public/context/Alpaca-Single-Turn.json | 12 -- public/context/ChatML.json | 12 -- public/context/Default.json | 12 -- public/context/DreamGen Role-Play V1.json | 12 -- public/context/Libra-32B.json | 12 -- public/context/Lightning 1.1.json | 12 -- public/context/Minimalist.json | 12 -- public/context/Mistral.json | 12 -- public/context/NovelAI.json | 12 -- public/context/OldDefault.json | 12 -- public/context/Pygmalion.json | 12 -- public/context/Story.json | 12 -- public/context/simple-proxy-for-tavern.json | 12 -- src/endpoints/content-manager.js | 4 + 17 files changed, 148 insertions(+), 180 deletions(-) delete mode 100644 public/context/Adventure.json delete mode 100644 public/context/Alpaca-Roleplay.json delete mode 100644 public/context/Alpaca-Single-Turn.json delete mode 100644 public/context/ChatML.json delete mode 100644 public/context/Default.json delete mode 100644 public/context/DreamGen Role-Play V1.json delete mode 100644 public/context/Libra-32B.json delete mode 100644 public/context/Lightning 1.1.json delete mode 100644 public/context/Minimalist.json delete mode 100644 public/context/Mistral.json delete mode 100644 public/context/NovelAI.json delete mode 100644 public/context/OldDefault.json delete mode 100644 public/context/Pygmalion.json delete mode 100644 public/context/Story.json delete mode 100644 public/context/simple-proxy-for-tavern.json diff --git a/default/content/index.json b/default/content/index.json index d7345c84e..fd66ea8d9 100644 --- a/default/content/index.json +++ b/default/content/index.json @@ -355,5 +355,149 @@ { "filename": "presets/openai/Default.json", "type": "openai_preset" + }, + { + "filename": "presets/context/Adventure.json", + "type": "context" + }, + { + "filename": "presets/context/Alpaca-Roleplay.json", + "type": "context" + }, + { + "filename": "presets/context/Alpaca-Single-Turn.json", + "type": "context" + }, + { + "filename": "presets/context/ChatML.json", + "type": "context" + }, + { + "filename": "presets/context/Default.json", + "type": "context" + }, + { + "filename": "presets/context/DreamGen Role-Play V1.json", + "type": "context" + }, + { + "filename": "presets/context/Libra-32B.json", + "type": "context" + }, + { + "filename": "presets/context/Lightning 1.1.json", + "type": "context" + }, + { + "filename": "presets/context/Minimalist.json", + "type": "context" + }, + { + "filename": "presets/context/Mistral.json", + "type": "context" + }, + { + "filename": "presets/context/NovelAI.json", + "type": "context" + }, + { + "filename": "presets/context/OldDefault.json", + "type": "context" + }, + { + "filename": "presets/context/Pygmalion.json", + "type": "context" + }, + { + "filename": "presets/context/Story.json", + "type": "context" + }, + { + "filename": "presets/context/simple-proxy-for-tavern.json", + "type": "context" + }, + { + "filename": "presets/instruct/Adventure.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/Alpaca-Roleplay.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/Alpaca-Single-Turn.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/Alpaca.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/ChatML.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/DreamGen Role-Play V1.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/Koala.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/Libra-32B.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/Lightning 1.1.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/Llama 2 Chat.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/Metharme.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/Mistral.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/OpenOrca-OpenChat.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/Pygmalion.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/Story.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/Synthia.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/Vicuna 1.0.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/Vicuna 1.1.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/WizardLM-13B.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/WizardLM.json", + "type": "instruct" + }, + { + "filename": "presets/instruct/simple-proxy-for-tavern.json", + "type": "instruct" } ] diff --git a/public/context/Adventure.json b/public/context/Adventure.json deleted file mode 100644 index 3318d518f..000000000 --- a/public/context/Adventure.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", - "example_separator": "", - "chat_start": "", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": false, - "trim_sentences": false, - "include_newline": false, - "single_line": true, - "name": "Adventure" -} \ No newline at end of file diff --git a/public/context/Alpaca-Roleplay.json b/public/context/Alpaca-Roleplay.json deleted file mode 100644 index d564a1dd7..000000000 --- a/public/context/Alpaca-Roleplay.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.\n\n{{#if system}}{{system}}\n\n{{/if}}### Input:\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", - "example_separator": "### New Roleplay:", - "chat_start": "### New Roleplay:", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": true, - "trim_sentences": false, - "include_newline": false, - "single_line": false, - "name": "Alpaca-Roleplay" -} \ No newline at end of file diff --git a/public/context/Alpaca-Single-Turn.json b/public/context/Alpaca-Single-Turn.json deleted file mode 100644 index ea58fe9d5..000000000 --- a/public/context/Alpaca-Single-Turn.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\n### Instruction:\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", - "example_separator": "", - "chat_start": "", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": false, - "trim_sentences": false, - "include_newline": false, - "single_line": false, - "name": "Alpaca-Single-Turn" -} \ No newline at end of file diff --git a/public/context/ChatML.json b/public/context/ChatML.json deleted file mode 100644 index 8515598f3..000000000 --- a/public/context/ChatML.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", - "example_separator": "", - "chat_start": "", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": true, - "trim_sentences": false, - "include_newline": false, - "single_line": false, - "name": "ChatML" -} \ No newline at end of file diff --git a/public/context/Default.json b/public/context/Default.json deleted file mode 100644 index 7c8a231cf..000000000 --- a/public/context/Default.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", - "example_separator": "***", - "chat_start": "***", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": true, - "trim_sentences": false, - "include_newline": false, - "single_line": false, - "name": "Default" -} \ No newline at end of file diff --git a/public/context/DreamGen Role-Play V1.json b/public/context/DreamGen Role-Play V1.json deleted file mode 100644 index 6698d27fa..000000000 --- a/public/context/DreamGen Role-Play V1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "<|im_start|>system\n{{#if system}}{{system}}\n\n\n{{/if}}## Overall plot description:\n\n{{#if scenario}}{{scenario}}{{else}}Conversation between {{char}} and {{user}}.{{/if}}{{#if wiBefore}}\n\n{{wiBefore}}{{/if}}\n\n\n## Characters:\n\n### {{char}}\n\n{{#if description}}{{description}}\n\n{{/if}}{{#if personality}}{{personality}}\n\n{{/if}}### {{user}}\n\n{{#if persona}}{{persona}}{{else}}{{user}} is the protagonist of the role-play.{{/if}}{{#if wiAfter}}\n\n{{wiAfter}}{{/if}}{{#if mesExamples}}\n\n{{mesExamples}}{{/if}}", - "example_separator": "", - "chat_start": "", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": false, - "trim_sentences": true, - "include_newline": false, - "single_line": false, - "name": "DreamGen Role-Play V1" -} \ No newline at end of file diff --git a/public/context/Libra-32B.json b/public/context/Libra-32B.json deleted file mode 100644 index b5dee2872..000000000 --- a/public/context/Libra-32B.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "### Instruction:\nWrite {{char}}'s next reply in this roleplay with {{user}}. Use the provided character sheet and example dialogue for formatting direction and character speech patterns.\n\n{{#if system}}{{system}}\n\n{{/if}}### Character Sheet:\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", - "example_separator": "### Example:", - "chat_start": "### START ROLEPLAY:", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": true, - "trim_sentences": false, - "include_newline": false, - "single_line": false, - "name": "Libra-32B" -} \ No newline at end of file diff --git a/public/context/Lightning 1.1.json b/public/context/Lightning 1.1.json deleted file mode 100644 index 3b0190c92..000000000 --- a/public/context/Lightning 1.1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "{{system}}\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{char}}'s description:{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality:{{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{user}}'s persona: {{persona}}\n{{/if}}", - "example_separator": "Example of an interaction:", - "chat_start": "This is the history of the roleplay:", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": true, - "trim_sentences": false, - "include_newline": false, - "single_line": false, - "name": "Lightning 1.1" -} \ No newline at end of file diff --git a/public/context/Minimalist.json b/public/context/Minimalist.json deleted file mode 100644 index cc7550c51..000000000 --- a/public/context/Minimalist.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", - "example_separator": "", - "chat_start": "", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": true, - "trim_sentences": false, - "include_newline": false, - "single_line": false, - "name": "Minimalist" -} \ No newline at end of file diff --git a/public/context/Mistral.json b/public/context/Mistral.json deleted file mode 100644 index d8c437e0e..000000000 --- a/public/context/Mistral.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "[INST] {{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}[/INST]", - "example_separator": "Examples:", - "chat_start": "", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": true, - "trim_sentences": false, - "include_newline": false, - "single_line": false, - "name": "Mistral" -} \ No newline at end of file diff --git a/public/context/NovelAI.json b/public/context/NovelAI.json deleted file mode 100644 index 1a7887a90..000000000 --- a/public/context/NovelAI.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "{{#if system}}{{system}}{{/if}}\n{{#if wiBefore}}{{wiBefore}}{{/if}}\n{{#if persona}}{{persona}}{{/if}}\n{{#if description}}{{description}}{{/if}}\n{{#if personality}}Personality: {{personality}}{{/if}}\n{{#if scenario}}Scenario: {{scenario}}{{/if}}\n{{#if wiAfter}}{{wiAfter}}{{/if}}", - "example_separator": "***", - "chat_start": "***", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": true, - "trim_sentences": false, - "include_newline": false, - "single_line": false, - "name": "NovelAI" -} \ No newline at end of file diff --git a/public/context/OldDefault.json b/public/context/OldDefault.json deleted file mode 100644 index 542971f21..000000000 --- a/public/context/OldDefault.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Circumstances and context of the dialogue: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", - "example_separator": "This is how {{char}} should talk", - "chat_start": "\nThen the roleplay chat between {{user}} and {{char}} begins.\n", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": true, - "trim_sentences": false, - "include_newline": false, - "single_line": false, - "name": "OldDefault" -} \ No newline at end of file diff --git a/public/context/Pygmalion.json b/public/context/Pygmalion.json deleted file mode 100644 index 68de8c1d0..000000000 --- a/public/context/Pygmalion.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", - "example_separator": "", - "chat_start": "", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": true, - "trim_sentences": false, - "include_newline": false, - "single_line": false, - "name": "Pygmalion" -} \ No newline at end of file diff --git a/public/context/Story.json b/public/context/Story.json deleted file mode 100644 index 26f70937b..000000000 --- a/public/context/Story.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", - "example_separator": "", - "chat_start": "", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": true, - "trim_sentences": false, - "include_newline": false, - "single_line": false, - "name": "Story" -} \ No newline at end of file diff --git a/public/context/simple-proxy-for-tavern.json b/public/context/simple-proxy-for-tavern.json deleted file mode 100644 index 38003c68d..000000000 --- a/public/context/simple-proxy-for-tavern.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "story_string": "## {{char}}\n- You're \"{{char}}\" in this never-ending roleplay with \"{{user}}\".\n### Input:\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}### Response:\n(OOC) Understood. I will take this info into account for the roleplay. (end OOC)", - "example_separator": "### New Roleplay:", - "chat_start": "### New Roleplay:", - "use_stop_strings": false, - "allow_jailbreak": false, - "always_force_name2": true, - "trim_sentences": false, - "include_newline": false, - "single_line": false, - "name": "simple-proxy-for-tavern" -} \ No newline at end of file diff --git a/src/endpoints/content-manager.js b/src/endpoints/content-manager.js index e40c8ff85..16460bf16 100644 --- a/src/endpoints/content-manager.js +++ b/src/endpoints/content-manager.js @@ -159,6 +159,10 @@ function getTargetByType(type) { return DIRECTORIES.novelAI_Settings; case 'textgen_preset': return DIRECTORIES.textGen_Settings; + case 'instruct': + return DIRECTORIES.instruct; + case 'context': + return DIRECTORIES.context; default: return null; } From 65a580a402e6ec5e2fb3732c792e9f2db28e3911 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 28 Mar 2024 23:12:19 +0200 Subject: [PATCH 19/99] Adjust sequence naming --- public/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index d79109cd2..e96da2b01 100644 --- a/public/index.html +++ b/public/index.html @@ -2959,7 +2959,7 @@ <div class="flex-container"> <div class="flex1" title="Inserted before the first Assistant's message."> <label for="instruct_first_output_sequence"> - <small data-i18n="First Output Prefix">First Output Prefix</small> + <small data-i18n="First Assistant Prefix">First Assistant Prefix</small> </label> <div> <textarea id="instruct_first_output_sequence" class="text_pole textarea_compact autoSetHeight" maxlength="2000" placeholder="—" rows="1"></textarea> @@ -2967,7 +2967,7 @@ </div> <div class="flex1" title="Inserted before the last Assistant's message or as a last prompt line when generating an AI reply (except a neutral/system role)."> <label for="instruct_last_output_sequence"> - <small data-i18n="Last Output Prefix">Last Output Prefix</small> + <small data-i18n="Last Assistant Prefix">Last Assistant Prefix</small> </label> <div> <textarea id="instruct_last_output_sequence" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="—" rows="1"></textarea> From 3c733b3243488c61908fbb2ebc931fdce6735d34 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 28 Mar 2024 23:12:33 +0200 Subject: [PATCH 20/99] New instruct macros --- public/scripts/instruct-mode.js | 20 ++++++++++++-------- public/scripts/templates/macros.html | 20 ++++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index eaf0df137..e646cdd79 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -495,15 +495,19 @@ export function replaceInstructMacros(input) { return ''; } - input = input.replace(/{{instructSystem}}/gi, power_user.instruct.enabled ? power_user.instruct.system_prompt : ''); - input = input.replace(/{{instructSystemPrefix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence_prefix : ''); - input = input.replace(/{{instructSystemSuffix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence_suffix : ''); - input = input.replace(/{{instructInput}}/gi, power_user.instruct.enabled ? power_user.instruct.input_sequence : ''); - input = input.replace(/{{instructOutput}}/gi, power_user.instruct.enabled ? power_user.instruct.output_sequence : ''); - input = input.replace(/{{instructFirstOutput}}/gi, power_user.instruct.enabled ? (power_user.instruct.first_output_sequence || power_user.instruct.output_sequence) : ''); - input = input.replace(/{{instructLastOutput}}/gi, power_user.instruct.enabled ? (power_user.instruct.last_output_sequence || power_user.instruct.output_sequence) : ''); - input = input.replace(/{{instructSeparator}}/gi, power_user.instruct.enabled ? power_user.instruct.separator_sequence : ''); + input = input.replace(/{{(instructSystem|instructSystemPrompt)}}/gi, power_user.instruct.enabled ? power_user.instruct.system_prompt : ''); + input = input.replace(/{{instructSystemPromptPrefix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence_prefix : ''); + input = input.replace(/{{instructSystemPromptSuffix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence_suffix : ''); + input = input.replace(/{{(instructInput|instructUserPrefix)}}/gi, power_user.instruct.enabled ? power_user.instruct.input_sequence : ''); + input = input.replace(/{{instructUserSuffix}}/gi, power_user.instruct.enabled ? power_user.instruct.input_suffix : ''); + input = input.replace(/{{(instructOutput|instructAssistantPrefix)}}/gi, power_user.instruct.enabled ? power_user.instruct.output_sequence : ''); + input = input.replace(/{{(instructSeparator|instructAssistantSuffix)}}/gi, power_user.instruct.enabled ? power_user.instruct.output_suffix : ''); + input = input.replace(/{{instructSystemPrefix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence : ''); + input = input.replace(/{{instructSystemSuffix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_suffix : ''); + input = input.replace(/{{(instructFirstOutput|instructFirstAssistantPrefix)}}/gi, power_user.instruct.enabled ? (power_user.instruct.first_output_sequence || power_user.instruct.output_sequence) : ''); + input = input.replace(/{{(instructLastOutput|instructLastAssistantPrefix)}}/gi, power_user.instruct.enabled ? (power_user.instruct.last_output_sequence || power_user.instruct.output_sequence) : ''); input = input.replace(/{{instructStop}}/gi, power_user.instruct.enabled ? power_user.instruct.stop_sequence : ''); + input = input.replace(/{{instructUserFiller}}/gi, power_user.instruct.enabled ? power_user.instruct.user_alignment_message : ''); input = input.replace(/{{exampleSeparator}}/gi, power_user.context.example_separator); input = input.replace(/{{chatStart}}/gi, power_user.context.chat_start); diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html index a2e1ff009..1b137122a 100644 --- a/public/scripts/templates/macros.html +++ b/public/scripts/templates/macros.html @@ -48,14 +48,18 @@ <li><tt>{{maxPrompt}}</tt> – max allowed prompt length in tokens = (context size - response length)</li> <li><tt>{{exampleSeparator}}</tt> – context template example dialogues separator</li> <li><tt>{{chatStart}}</tt> – context template chat start line</li> - <li><tt>{{instructSystem}}</tt> – instruct system prompt</li> - <li><tt>{{instructSystemPrefix}}</tt> – instruct system prompt prefix sequence</li> - <li><tt>{{instructSystemSuffix}}</tt> – instruct system prompt suffix sequence</li> - <li><tt>{{instructInput}}</tt> – instruct user input sequence</li> - <li><tt>{{instructOutput}}</tt> – instruct assistant output sequence</li> - <li><tt>{{instructFirstOutput}}</tt> – instruct assistant first output sequence</li> - <li><tt>{{instructLastOutput}}</tt> – instruct assistant last output sequence</li> - <li><tt>{{instructSeparator}}</tt> – instruct turn separator sequence</li> + <li><tt>{{instructSystemPrompt}}</tt> – instruct system prompt</li> + <li><tt>{{instructSystemPromptPrefix}}</tt> – instruct system prompt prefix sequence</li> + <li><tt>{{instructSystemPromptSuffix}}</tt> – instruct system prompt suffix sequence</li> + <li><tt>{{instructUserPrefix}}</tt> – instruct user prefix sequence</li> + <li><tt>{{instructUserSuffix}}</tt> – instruct user suffix sequence</li> + <li><tt>{{instructAssistantPrefix}}</tt> – instruct assistant prefix sequence</li> + <li><tt>{{instructAssistantSuffix}}</tt> – instruct assistant suffix sequence</li> + <li><tt>{{instructFirstAssistantPrefix}}</tt> – instruct assistant first output sequence</li> + <li><tt>{{instructLastAssistantPrefix}}</tt> – instruct assistant last output sequence</li> + <li><tt>{{instructSystemPrefix}}</tt> – instruct system message prefix sequence</li> + <li><tt>{{instructSystemSuffix}}</tt> – instruct system message suffix sequence</li> + <li><tt>{{instructUserFiller}}</tt> – instruct first user message filler</li> <li><tt>{{instructStop}}</tt> – instruct stop sequence</li> </ul> <div> From a951f68c8df1b4b26374d2e90b726d8e8f4d6260 Mon Sep 17 00:00:00 2001 From: Wolfsblvt <wolfsblvt@gmx.de> Date: Fri, 29 Mar 2024 02:20:16 +0100 Subject: [PATCH 21/99] cli server args precedency fix + port/listen arg - Fixes precedence: cli > (env) > yaml > default - Add cli arguments for port and listen --- server.js | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/server.js b/server.js index 77ab7073f..ce7007f9b 100644 --- a/server.js +++ b/server.js @@ -55,15 +55,29 @@ if (process.versions && process.versions.node && process.versions.node.match(/20 // Set default DNS resolution order to IPv4 first dns.setDefaultResultOrder('ipv4first'); +const DEFAULT_PORT = 8000; +const DEFAULT_AUTORUN = false; +const DEFAULT_LISTEN = false; +const DEFAULT_CORS_PROXY = false; + const cliArguments = yargs(hideBin(process.argv)) - .option('autorun', { + .usage('Usage: <your-start-script> <command> [options]') + .option('port', { + type: 'number', + default: null, + describe: `Sets the port under which SillyTavern will run.\nIf not provided falls back to yaml config 'port'.\n[config default: ${DEFAULT_PORT}]`, + }).option('autorun', { type: 'boolean', - default: false, - describe: 'Automatically launch SillyTavern in the browser.', + default: null, + describe: `Automatically launch SillyTavern in the browser.\nAutorun is automatically disabled if --ssl is set to true.\nIf not provided falls back to yaml config 'autorun'.\n[config default: ${DEFAULT_AUTORUN}]`, + }).option('listen', { + type: 'boolean', + default: null, + describe: `SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If false, will limit it only to internal localhost (127.0.0.1).\nIf not provided falls back to yaml config 'listen'.\n[config default: ${DEFAULT_LISTEN}]`, }).option('corsProxy', { type: 'boolean', - default: false, - describe: 'Enables CORS proxy', + default: null, + describe: `Enables CORS proxy\nIf not provided falls back to yaml config 'enableCorsProxy'.\n[config default: ${DEFAULT_CORS_PROXY}]`, }).option('disableCsrf', { type: 'boolean', default: false, @@ -91,10 +105,10 @@ const app = express(); app.use(compression()); app.use(responseTime()); -const server_port = process.env.SILLY_TAVERN_PORT || getConfigValue('port', 8000); - -const autorun = (getConfigValue('autorun', false) || cliArguments.autorun) && !cliArguments.ssl; -const listen = getConfigValue('listen', false); +const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT); +const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; +const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); +const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY) const { DIRECTORIES, UPLOADS_PATH } = require('./src/constants'); @@ -144,7 +158,7 @@ if (!cliArguments.disableCsrf) { }); } -if (getConfigValue('enableCorsProxy', false) || cliArguments.corsProxy) { +if (enableCorsProxy) { const bodyParser = require('body-parser'); app.use(bodyParser.json({ limit: '200mb', From 167673fcf56f164c6c6adb10b6917ce1199c02fc Mon Sep 17 00:00:00 2001 From: Wolfsblvt <wolfsblvt@gmx.de> Date: Fri, 29 Mar 2024 04:41:16 +0100 Subject: [PATCH 22/99] Updated code documentation - Updated code documentation for all methods added/changed with this PR - Expanded tooltip to "bulk edit" to explain how it works --- public/index.html | 2 +- public/scripts/BulkEditOverlay.js | 74 ++++++++++++++++++++++--------- public/scripts/tags.js | 14 +++--- 3 files changed, 63 insertions(+), 27 deletions(-) diff --git a/public/index.html b/public/index.html index 8b2ee23a5..098ceb30d 100644 --- a/public/index.html +++ b/public/index.html @@ -4327,7 +4327,7 @@ <div id="rm_print_characters_pagination"> <i id="charListGridToggle" class="fa-solid fa-table-cells-large menu_button" title="Toggle character grid view" data-i18n="[title]Toggle character grid view"></i> - <i id="bulkEditButton" class="fa-solid fa-edit menu_button bulkEditButton" title="Bulk edit characters" data-i18n="[title]Bulk edit characters"></i> + <i id="bulkEditButton" class="fa-solid fa-edit menu_button bulkEditButton" title="Bulk edit characters Click to toggle characters Shift + Click to select/deselect a range of characters Right-click for actions" data-i18n="[title]Bulk edit characters Click to toggle characters Shift + Click to select/deselect a range of characters Right-click for actions"></i> <div id="bulkSelectedCount" class="bulkEditOptionElement paginationjs-nav"></div> <i id="bulkSelectAllButton" class="fa-solid fa-check-double menu_button bulkEditOptionElement bulkSelectAllButton" title="Bulk select all characters" data-i18n="[title]Bulk select all characters" style="display: none;"></i> <i id="bulkDeleteButton" class="fa-solid fa-trash menu_button bulkEditOptionElement bulkDeleteButton" title="Bulk delete characters" data-i18n="[title]Bulk delete characters" style="display: none;"></i> diff --git a/public/scripts/BulkEditOverlay.js b/public/scripts/BulkEditOverlay.js index 8da4dc86b..48d95e68b 100644 --- a/public/scripts/BulkEditOverlay.js +++ b/public/scripts/BulkEditOverlay.js @@ -40,7 +40,7 @@ class CharacterContextMenu { * Tag one or more characters, * opens a popup. * - * @param selectedCharacters + * @param {Array<number>} selectedCharacters */ static tag = (selectedCharacters) => { BulkTagPopupHandler.show(selectedCharacters); @@ -49,7 +49,7 @@ class CharacterContextMenu { /** * Duplicate one or more characters * - * @param characterId + * @param {number} characterId * @returns {Promise<any>} */ static duplicate = async (characterId) => { @@ -74,7 +74,7 @@ class CharacterContextMenu { * Favorite a character * and highlight it. * - * @param characterId + * @param {number} characterId * @returns {Promise<void>} */ static favorite = async (characterId) => { @@ -110,7 +110,7 @@ class CharacterContextMenu { * Convert one or more characters to persona, * may open a popup for one or more characters. * - * @param characterId + * @param {number} characterId * @returns {Promise<void>} */ static persona = async (characterId) => await convertCharacterToPersona(characterId); @@ -119,8 +119,8 @@ class CharacterContextMenu { * Delete one or more characters, * opens a popup. * - * @param characterId - * @param deleteChats + * @param {number} characterId + * @param {boolean} [deleteChats] * @returns {Promise<void>} */ static delete = async (characterId, deleteChats = false) => { @@ -234,7 +234,7 @@ class BulkTagPopupHandler { /** * Append and show the tag control * - * @param characterIds - The characters assigned to this control + * @param {Array<number>} characterIds - The characters assigned to this control */ static show(characterIds) { if (characterIds.length == 0) { @@ -250,7 +250,7 @@ class BulkTagPopupHandler { // Print the tag list with all mutuable tags, marking them as removable. That is the initial fill printTagList($('#bulkTagList'), { tags: () => this.getMutualTags(characterIds), tagOptions: { removable: true } }); - // Tag input with empty tags so new tag gets added and it doesn't get emptied on redraw + // Tag input with resolvable list for the mutual tags to get redrawn, so that newly added tags get sorted correctly createTagInput('#bulkTagInput', '#bulkTagList', { tags: () => this.getMutualTags(characterIds), tagOptions: { removable: true }}); document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this, characterIds)); @@ -258,6 +258,12 @@ class BulkTagPopupHandler { document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this)); } + /** + * Builds a list of all tags that the provided characters have in common. + * + * @param {Array<number>} characterIds - The characters to find mutual tags for + * @returns {Array<object>} A list of mutual tags + */ static getMutualTags(characterIds) { if (characterIds.length == 0) { return []; @@ -293,7 +299,7 @@ class BulkTagPopupHandler { /** * Empty the tag map for the given characters * - * @param characterIds + * @param {Array<number>} characterIds */ static resetTags(characterIds) { for (const characterId of characterIds) { @@ -307,9 +313,9 @@ class BulkTagPopupHandler { } /** - * Empty the tag map for the given characters + * Remove the mutual tags for all given characters * - * @param characterIds + * @param {Array<number>} characterIds */ static removeMutual(characterIds) { const mutualTags = this.getMutualTags(characterIds); @@ -627,6 +633,15 @@ class BulkEditOverlay { this.#cancelNextToggle = false; }; + /** + * When shift click was held down, this function handles the multi select of characters in a single click. + * + * If the last clicked character was deselected, and the current one was deselected too, it will deselect all currently selected characters between those two. + * If the last clicked character was selected, and the current one was selected too, it will select all currently not selected characters between those two. + * If the states do not match, nothing will happen. + * + * @param {HTMLElement} currentCharacter - The html element of the currently toggled character + */ handleShiftClick = (currentCharacter) => { const characterId = currentCharacter.getAttribute('chid'); const select = !this.selectedCharacters.includes(characterId); @@ -634,11 +649,18 @@ class BulkEditOverlay { if (this.lastSelected.characterId && this.lastSelected.select !== undefined) { // Only if select state and the last select state match we execute the range select if (select === this.lastSelected.select) { - this.selectCharactersInRange(currentCharacter, select); + this.toggleCharactersInRange(currentCharacter, select); } } }; + /** + * Toggles the selection of a given characters + * + * @param {HTMLElement} character - The html element of a character + * @param {object} param1 - Optional params + * @param {boolean} [param1.markState] - Whether the toggle of this character should be remembered as the last done toggle + */ toggleSingleCharacter = (character, { markState = true } = {}) => { const characterId = character.getAttribute('chid'); @@ -648,11 +670,11 @@ class BulkEditOverlay { if (select) { character.classList.add(BulkEditOverlay.selectedClass); if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = true; - this.selectCharacter(characterId); + this.#selectedCharacters.push(String(characterId)); } else { character.classList.remove(BulkEditOverlay.selectedClass); if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = false; - this.dismissCharacter(characterId); + this.#selectedCharacters = this.#selectedCharacters.filter(item => String(characterId) !== item) } this.updateSelectedCount(); @@ -663,12 +685,24 @@ class BulkEditOverlay { } }; + /** + * Updates the selected count element with the current count + * + * @param {number} [countOverride] - optional override for a manual number to set + */ updateSelectedCount = (countOverride = undefined) => { const count = countOverride ?? this.selectedCharacters.length; $(`#${BulkEditOverlay.bulkSelectedCountId}`).text(count).attr('title', `${count} characters selected`); }; - selectCharactersInRange = (currentCharacter, select) => { + /** + * Toggles the selection of characters in a given range. + * The range is provided by the given character and the last selected one remembered in the selection state. + * + * @param {HTMLElement} currentCharacter - The html element of the currently toggled character + * @param {boolean} select - <c>true</c> if the characters in the range are to be selected, <c>false</c> if deselected + */ + toggleCharactersInRange = (currentCharacter, select) => { const currentCharacterId = currentCharacter.getAttribute('chid'); const characters = Array.from(document.querySelectorAll('#' + BulkEditOverlay.containerId + ' .' + BulkEditOverlay.characterClass)); @@ -680,8 +714,10 @@ class BulkEditOverlay { const characterId = character.getAttribute('chid'); const isCharacterSelected = this.selectedCharacters.includes(characterId); - if (select && !isCharacterSelected || !select && isCharacterSelected) { - this.toggleSingleCharacter(character, { markState: currentCharacterId == i }); + // Only toggle the character if it wasn't on the state we have are toggling towards. + // Also doing a weird type check, because typescript checker doesn't like the return of 'querySelectorAll'. + if ((select && !isCharacterSelected || !select && isCharacterSelected) && character instanceof HTMLElement) { + this.toggleSingleCharacter(character, { markState: currentCharacterId == characterId }); } } }; @@ -771,10 +807,6 @@ class BulkEditOverlay { addStateChangeCallback = callback => this.stateChangeCallbacks.push(callback); - selectCharacter = characterId => this.selectedCharacters.push(String(characterId)); - - dismissCharacter = characterId => this.#selectedCharacters = this.selectedCharacters.filter(item => String(characterId) !== item); - /** * Clears internal character storage and * removes visual highlight. diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 8895545e8..816087d73 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -320,7 +320,8 @@ function getTagKey() { /** * Gets the tag key for any provided entity/id/key. If a valid tag key is provided, it just returns this. - * Robust method to find a valid tag key for any entity + * Robust method to find a valid tag key for any entity. + * * @param {object|number|string} entityOrKey An entity with id property (character, group, tag), or directly an id or tag key. * @returns {string} The tag key that can be found. */ @@ -394,9 +395,10 @@ function findTag(request, resolve, listSelector) { } /** - * Select a tag and add it to the list. This function is mostly used as an event handler for the tag selector control. - * @param {*} event - - * @param {*} ui - + * Select a tag and add it to the list. This function is (mostly) used as an event handler for the tag selector control. + * + * @param {*} event - The event that fired on autocomplete select + * @param {*} ui - An Object with label and value properties for the selected option * @param {*} listSelector - The selector of the list to print/add to * @param {PrintTagListOptions} [tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before. * @returns {boolean} <c>false</c>, to keep the input clear @@ -529,7 +531,8 @@ function createNewTag(tagName) { */ /** - * Prints the list of tags. + * Prints the list of tags + * * @param {JQuery<HTMLElement>} element - The container element where the tags are to be printed. * @param {PrintTagListOptions} [options] - Optional parameters for printing the tag list. */ @@ -798,6 +801,7 @@ function applyTagsOnGroupSelect() { } /** + * Create a tag input by enabling the autocomplete feature of a given input element. Tags will be added to the given list. * * @param {string} inputSelector - the selector for the tag input control * @param {string} listSelector - the selector for the list of the tags modified by the input control From bf8b6b80d70b0bd779625351c033e8202daee9f2 Mon Sep 17 00:00:00 2001 From: Wolfsblvt <wolfsblvt@gmx.de> Date: Fri, 29 Mar 2024 05:53:26 +0100 Subject: [PATCH 23/99] Refactor and improve bulk delete popup - Improve bulk edit popup with display of avatars and better format - Refactor both calls of bulk delete to use the same method - Add display of filename on avatar hover for inline avatars (@Cohee you forgot this one (: ) --- public/script.js | 2 +- public/scripts/BulkEditOverlay.js | 59 +++++++++++++++++++++---------- public/scripts/bulk-edit.js | 28 ++------------- public/style.css | 2 +- 4 files changed, 45 insertions(+), 46 deletions(-) diff --git a/public/script.js b/public/script.js index 9093abf67..e9bd89da0 100644 --- a/public/script.js +++ b/public/script.js @@ -5357,7 +5357,7 @@ function buildAvatarList(block, entities, { templateId = 'inline_avatar_template avatarTemplate.attr('data-type', entity.type); avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` }); avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name); - avatarTemplate.attr('title', `[Character] ${entity.item.name}`); + avatarTemplate.attr('title', `[Character] ${entity.item.name}\nFile: ${entity.item.avatar}`); if (highlightFavs) { avatarTemplate.toggleClass('is_fav', entity.item.fav || entity.item.fav == 'true'); avatarTemplate.find('.ch_fav').val(entity.item.fav); diff --git a/public/scripts/BulkEditOverlay.js b/public/scripts/BulkEditOverlay.js index 48d95e68b..19955e7ac 100644 --- a/public/scripts/BulkEditOverlay.js +++ b/public/scripts/BulkEditOverlay.js @@ -19,18 +19,6 @@ import { hideLoader, showLoader } from './loader.js'; import { convertCharacterToPersona } from './personas.js'; import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap } from './tags.js'; -// Utility object for popup messages. -const popupMessage = { - deleteChat(characterCount) { - return `<h3>Delete ${characterCount} characters?</h3> - <b>THIS IS PERMANENT!<br><br> - <label for="del_char_checkbox" class="checkbox_label justifyCenter"> - <input type="checkbox" id="del_char_checkbox" /> - <span>Also delete the chat files</span> - </label><br></b>`; - }, -}; - /** * Static object representing the actions of the * character context menu override. @@ -198,6 +186,12 @@ class CharacterContextMenu { * Represents a tag control not bound to a single character */ class BulkTagPopupHandler { + /** + * Gets the HTML as a string that is going to be the popup for the bulk tag edit + * + * @param {Array<number>} characterIds - The characters that are shown inside the popup + * @returns String containing the html for the popup + */ static #getHtml = (characterIds) => { const characterData = JSON.stringify({ characterIds: characterIds }); return `<div id="bulk_tag_shadow_popup"> @@ -227,8 +221,7 @@ class BulkTagPopupHandler { </div> </div> </div> - </div> - `; + </div>`; }; /** @@ -430,7 +423,7 @@ class BulkEditOverlay { /** * - * @returns {*[]} + * @returns {number[]} */ get selectedCharacters() { return this.#selectedCharacters; @@ -775,6 +768,29 @@ class BulkEditOverlay { this.browseState(); }; + /** + * Gets the HTML as a string that is displayed inside the popup for the bulk delete + * + * @param {Array<number>} characterIds - The characters that are shown inside the popup + * @returns String containing the html for the popup content + */ + static #getDeletePopupContentHtml = (characterIds) => { + return ` + <h3 class="marginBot5">Delete ${characterIds.length} characters?</h3> + <span class="bulk_delete_note"> + <i class="fa-solid fa-triangle-exclamation warning margin-r5"></i> + <b>THIS IS PERMANENT!</b> + </span> + <div id="bulk_delete_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline m-t-1"></div> + <br> + <div id="bulk_delete_options" class="m-b-1"> + <label for="del_char_checkbox" class="checkbox_label justifyCenter"> + <input type="checkbox" id="del_char_checkbox" /> + <span>Also delete the chat files</span> + </label> + </div>`; + } + /** * Request user input before concurrently handle deletion * requests. @@ -782,8 +798,9 @@ class BulkEditOverlay { * @returns {Promise<number>} */ handleContextMenuDelete = () => { - callPopup( - popupMessage.deleteChat(this.selectedCharacters.length), null) + const characterIds = this.selectedCharacters; + const popupContent = BulkEditOverlay.#getDeletePopupContentHtml(characterIds); + const promise = callPopup(popupContent, null) .then((accept) => { if (true !== accept) return; @@ -791,11 +808,17 @@ class BulkEditOverlay { showLoader(); toastr.info('We\'re deleting your characters, please wait...', 'Working on it'); - Promise.allSettled(this.selectedCharacters.map(async characterId => CharacterContextMenu.delete(characterId, deleteChats))) + return Promise.allSettled(characterIds.map(async characterId => CharacterContextMenu.delete(characterId, deleteChats))) .then(() => getCharacters()) .then(() => this.browseState()) .finally(() => hideLoader()); }); + + // At this moment the popup is already changed in the dom, but not yet closed/resolved. We build the avatar list here + const entities = characterIds.map(id => characterToEntity(characters[id], id)).filter(entity => entity.item !== undefined); + buildAvatarList($('#bulk_delete_avatars_block'), entities); + + return promise; }; /** diff --git a/public/scripts/bulk-edit.js b/public/scripts/bulk-edit.js index 95d0d79bb..c266e148b 100644 --- a/public/scripts/bulk-edit.js +++ b/public/scripts/bulk-edit.js @@ -84,32 +84,8 @@ async function deleteCharacter(this_chid) { async function onDeleteButtonClick() { console.log('Delete button clicked'); - // Create a mapping of chid to avatar - let toDelete = []; - $('.bulk_select_checkbox:checked').each((i, el) => { - const chid = $(el).parent().attr('chid'); - const avatar = characters[chid].avatar; - // Add the avatar to the list of avatars to delete - toDelete.push(avatar); - }); - - const confirm = await callPopup('<h3>Are you sure you want to delete these characters?</h3>You would need to delete the chat files manually.<br>', 'confirm'); - - if (!confirm) { - console.log('User cancelled delete'); - return; - } - - // Delete the characters - for (const avatar of toDelete) { - console.log(`Deleting character with avatar ${avatar}`); - await getCharacters(); - - //chid should be the key of the character with the given avatar - const chid = Object.keys(characters).find((key) => characters[key].avatar === avatar); - console.log(`Deleting character with chid ${chid}`); - await deleteCharacter(chid); - } + // We just let the button trigger the context menu delete option + await characterGroupOverlay.handleContextMenuDelete(); } /** diff --git a/public/style.css b/public/style.css index f8aecba0c..e6218e820 100644 --- a/public/style.css +++ b/public/style.css @@ -3135,7 +3135,7 @@ body.big-avatars .missing-avatar { } } -span.warning { +.warning { color: var(--warning); font-weight: bolder; } From c39b0ed3d96b88b597787ef683b753a401ce5f18 Mon Sep 17 00:00:00 2001 From: deffcolony <61471128+deffcolony@users.noreply.github.com> Date: Fri, 29 Mar 2024 13:32:47 +0100 Subject: [PATCH 24/99] issue labeler +added automatic label system for a more easy filter --- .github/ISSUE_TEMPLATE/bug-report.yml | 10 +++++----- .github/labeler.yml | 6 ++++++ .github/workflows/labeler.yml | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/labeler.yml diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 82cc9c9cf..58f8ae2eb 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -9,11 +9,11 @@ body: label: Environment description: Where are you running SillyTavern? options: - - Self-Hosted (Bare Metal) - - Self-Hosted (Docker) - - Android (Termux) - - Cloud Service (Static) - - Other (Specify below) + - 🪟 Windows + - 🐧 Linux + - 📱 Termux + - 🐋 Docker + - 🍎 Mac validations: required: true diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..fafd4435d --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,6 @@ +# Add/remove 'critical' label if issue contains the words 'urgent' or 'critical' +#critical: +# - '(critical|urgent)' + +Environment: + - '(🪟 Windows|🍎 Mac|🐋 Docker|📱 Termux|🐧 Linux)' \ No newline at end of file diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 000000000..554c588b8 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,19 @@ +name: "Issue Labeler" +on: + issues: + types: [opened, edited] + +permissions: + issues: write + contents: read + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: github/issue-labeler@v3.4 + with: + configuration-path: .github/labeler.yml +# not-before: 2020-01-15T02:54:32Z # optional and will result in any issues prior to this timestamp to be ignored. + enable-versioned-regex: 0 + repo-token: ${{ github.token }} \ No newline at end of file From da035d4984555da0b19b028d4a0e635056142c73 Mon Sep 17 00:00:00 2001 From: deffcolony <61471128+deffcolony@users.noreply.github.com> Date: Fri, 29 Mar 2024 13:48:20 +0100 Subject: [PATCH 25/99] Update labeler.yml --- .github/labeler.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index fafd4435d..850096c9e 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -2,5 +2,17 @@ #critical: # - '(critical|urgent)' -Environment: - - '(🪟 Windows|🍎 Mac|🐋 Docker|📱 Termux|🐧 Linux)' \ No newline at end of file +🪟 Windows: + - '(🪟 Windows)' + +🍎 Mac: + - '(🍎 Mac)' + +🐋 Docker: + - '(🐋 Docker)' + +📱 Termux: + - '(📱 Termux)' + +🐧 Linux: + - '(🐧 Linux)' \ No newline at end of file From 2d5b871f2a8a397ffbce0f02835759c0aa2f6552 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 29 Mar 2024 15:01:08 +0200 Subject: [PATCH 26/99] Fix array access --- public/script.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/public/script.js b/public/script.js index c2096a108..04fb0238d 100644 --- a/public/script.js +++ b/public/script.js @@ -3389,6 +3389,11 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu // If it doesn't fit - user shot himself in the foot for (const index of injectedIndices) { const item = chat2[index]; + + if (typeof item !== 'string') { + continue; + } + tokenCount += getTokenCount(item.replace(/\r/gm, '')); chatString = item + chatString; if (tokenCount < this_max_context) { @@ -3414,6 +3419,11 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu } const item = chat2[i]; + + if (typeof item !== 'string') { + continue; + } + tokenCount += getTokenCount(item.replace(/\r/gm, '')); chatString = item + chatString; if (tokenCount < this_max_context) { From a3ec0938c55c96aa9f243a9efd959d22851b8ad0 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 29 Mar 2024 17:28:28 +0200 Subject: [PATCH 27/99] KoboldCpp grammar fix --- public/scripts/textgen-settings.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 515355e59..fdd2380f7 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -38,7 +38,7 @@ export const textgen_types = { OPENROUTER: 'openrouter', }; -const { MANCER, APHRODITE, TABBY, TOGETHERAI, OOBA, OLLAMA, LLAMACPP, INFERMATICAI, DREAMGEN, OPENROUTER } = textgen_types; +const { MANCER, APHRODITE, TABBY, TOGETHERAI, OOBA, OLLAMA, LLAMACPP, INFERMATICAI, DREAMGEN, OPENROUTER, KOBOLDCPP } = textgen_types; const LLAMACPP_DEFAULT_ORDER = [ 'top_k', @@ -1047,6 +1047,10 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, //'prompt_logprobs': settings.prompt_log_probs_aphrodite, }; + if (settings.type === KOBOLDCPP) { + params.grammar = settings.grammar_string; + } + if (settings.type === MANCER) { params.n = canMultiSwipe ? settings.n : 1; params.epsilon_cutoff /= 1000; From 6a688cc3835404907034ec935b3fce4524ec2f4c Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 29 Mar 2024 18:07:45 +0200 Subject: [PATCH 28/99] Add fallback if tag_map is uninitialized --- public/scripts/tags.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 816087d73..a20f0ff40 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -339,6 +339,12 @@ export function getTagKeyForEntity(entityOrKey) { x = character.avatar; } + // Uninitialized character tag map + if (character && !(x in tag_map)) { + tag_map[x] = []; + return x; + } + // We should hopefully have a key now. Let's check if (x in tag_map) { return x; @@ -349,7 +355,7 @@ export function getTagKeyForEntity(entityOrKey) { } function addTagToMap(tagId, characterId = null) { - const key = getTagKey() ?? getTagKeyForEntity(characterId); + const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey(); if (!key) { return; @@ -365,7 +371,7 @@ function addTagToMap(tagId, characterId = null) { } function removeTagFromMap(tagId, characterId = null) { - const key = getTagKey() ?? getTagKeyForEntity(characterId); + const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey(); if (!key) { return; From 06d1369f582e3643519a62a485d314461332d856 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 29 Mar 2024 19:39:59 +0200 Subject: [PATCH 29/99] Make default instruct/context restorable --- public/index.html | 33 ++++++++++++++++++++------------ src/endpoints/content-manager.js | 2 +- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/public/index.html b/public/index.html index e96da2b01..399003f39 100644 --- a/public/index.html +++ b/public/index.html @@ -2715,8 +2715,14 @@ <div class="flex-container"> <div id="PygOverrides"> <div> - <h4 data-i18n="Context Template"> - Context Template + <h4 class="standoutHeader title_restorable"> + <span data-i18n="Context Template">Context Template</span> + <div class="flex-container"> + <i data-newbie-hidden data-preset-manager-import="context" class="margin0 menu_button fa-solid fa-file-import" title="Import preset" data-i18n="[title]Import preset"></i> + <i data-newbie-hidden data-preset-manager-export="context" class="margin0 menu_button fa-solid fa-file-export" title="Export preset" data-i18n="[title]Export preset"></i> + <i data-newbie-hidden data-preset-manager-restore="context" class="menu_button fa-solid fa-recycle" title="Restore current preset" data-i18n="[title]Restore current preset"></i> + <i data-newbie-hidden id="context_delete_preset" data-preset-manager-delete="context" class="margin0 menu_button fa-solid fa-trash-can" title="Delete the preset" data-i18n="[title]Delete the preset"></i> + </div> </h4> <div class="flex-container"> <select id="context_presets" data-preset-manager-for="context" class="flex1 text_pole"></select> @@ -2724,9 +2730,6 @@ <i id="context_set_default" class="menu_button fa-solid fa-heart" title="Auto-select this preset for Instruct Mode." data-i18n="[title]Auto-select this preset for Instruct Mode"></i> <i data-newbie-hidden data-preset-manager-update="context" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i> <i data-newbie-hidden data-preset-manager-new="context" class="menu_button fa-solid fa-file-circle-plus" title="Save preset as" data-i18n="[title]Save preset as"></i> - <i data-newbie-hidden data-preset-manager-import="context" class="menu_button fa-solid fa-file-import" title="Import preset" data-i18n="[title]Import preset"></i> - <i data-newbie-hidden data-preset-manager-export="context" class="menu_button fa-solid fa-file-export" title="Export preset" data-i18n="[title]Export preset"></i> - <i data-newbie-hidden id="context_delete_preset" data-preset-manager-delete="context" class="menu_button fa-solid fa-trash-can" title="Delete the preset" data-i18n="[title]Delete the preset"></i> </div> <div data-newbie-hidden> <label for="context_story_string"> @@ -2799,10 +2802,19 @@ </div> </div> <div> - <h4 data-i18n="Instruct Mode">Instruct Mode - <a href="https://docs.sillytavern.app/usage/core-concepts/instructmode/" class="notes-link" target="_blank"> - <span class="fa-solid fa-circle-question note-link-span"></span> - </a> + <h4 class="standoutHeader title_restorable"> + <div> + <span data-i18n="Instruct Mode">Instruct Mode</span> + <a href="https://docs.sillytavern.app/usage/core-concepts/instructmode/" class="notes-link" target="_blank"> + <span class="fa-solid fa-circle-question note-link-span"></span> + </a> + </div> + <div class="flex-container"> + <i data-newbie-hidden data-preset-manager-import="instruct" class="margin0 menu_button fa-solid fa-file-import" title="Import preset" data-i18n="[title]Import preset"></i> + <i data-newbie-hidden data-preset-manager-export="instruct" class="margin0 menu_button fa-solid fa-file-export" title="Export preset" data-i18n="[title]Export preset"></i> + <i data-newbie-hidden data-preset-manager-restore="instruct" class="margin0 menu_button fa-solid fa-recycle" title="Restore current preset" data-i18n="[title]Restore current preset"></i> + <i data-newbie-hidden data-preset-manager-delete="instruct" class="margin0 menu_button fa-solid fa-trash-can" title="Delete the preset" data-i18n="[title]Delete the preset"></i> + </div> </h4> <div class="flex-container"> <label for="instruct_enabled" class="checkbox_label flex1"> @@ -2823,9 +2835,6 @@ <i id="instruct_set_default" class="menu_button fa-solid fa-heart" title="Auto-select this preset on API connection." data-i18n="[title]Auto-select this preset on API connection"></i> <i data-newbie-hidden data-preset-manager-update="instruct" class="menu_button fa-solid fa-save" title="Update current preset" data-i18n="[title]Update current preset"></i> <i data-newbie-hidden data-preset-manager-new="instruct" class="menu_button fa-solid fa-file-circle-plus" title="Save preset as" data-i18n="[title]Save preset as"></i> - <i data-newbie-hidden data-preset-manager-import="instruct" class="menu_button fa-solid fa-file-import" title="Import preset" data-i18n="[title]Import preset"></i> - <i data-newbie-hidden data-preset-manager-export="instruct" class="menu_button fa-solid fa-file-export" title="Export preset" data-i18n="[title]Export preset"></i> - <i data-newbie-hidden data-preset-manager-delete="instruct" class="menu_button fa-solid fa-trash-can" title="Delete the preset" data-i18n="[title]Delete the preset"></i> </div> <label data-newbie-hidden> <small data-i18n="Activation Regex"> diff --git a/src/endpoints/content-manager.js b/src/endpoints/content-manager.js index 16460bf16..bbb444faf 100644 --- a/src/endpoints/content-manager.js +++ b/src/endpoints/content-manager.js @@ -24,7 +24,7 @@ function getDefaultPresets() { const presets = []; for (const contentItem of contentIndex) { - if (contentItem.type.endsWith('_preset')) { + if (contentItem.type.endsWith('_preset') || contentItem.type === 'instruct' || contentItem.type === 'context') { contentItem.name = path.parse(contentItem.filename).name; contentItem.folder = getTargetByType(contentItem.type); presets.push(contentItem); From 8b0fde21d42ba920627acdbc9dc1fbaef4a4a833 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 29 Mar 2024 19:40:06 +0200 Subject: [PATCH 30/99] Update ChatML templates --- default/content/presets/context/ChatML.json | 4 ++-- default/content/presets/instruct/ChatML.json | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/default/content/presets/context/ChatML.json b/default/content/presets/context/ChatML.json index 8515598f3..80046d170 100644 --- a/default/content/presets/context/ChatML.json +++ b/default/content/presets/context/ChatML.json @@ -1,5 +1,5 @@ { - "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "story_string": "<|im_start|>system\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}<|im_end|>", "example_separator": "", "chat_start": "", "use_stop_strings": false, @@ -9,4 +9,4 @@ "include_newline": false, "single_line": false, "name": "ChatML" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/ChatML.json b/default/content/presets/instruct/ChatML.json index bd19cabd7..f6bf0aa44 100644 --- a/default/content/presets/instruct/ChatML.json +++ b/default/content/presets/instruct/ChatML.json @@ -4,14 +4,14 @@ "output_sequence": "\n<|im_start|>assistant\n", "last_output_sequence": "", "system_sequence": "\n<|im_start|>system\n", - "stop_sequence": "", + "stop_sequence": "<|im_end|>", "wrap": false, "macro": true, "names": true, "names_force_groups": true, "activation_regex": "", - "system_sequence_prefix": "<|im_start|>system\n", - "system_sequence_suffix": "<|im_end|>", + "system_sequence_prefix": "", + "system_sequence_suffix": "", "first_output_sequence": "", "skip_examples": false, "output_suffix": "<|im_end|>", @@ -20,4 +20,4 @@ "user_alignment_message": "", "system_same_as_user": false, "name": "ChatML" -} \ No newline at end of file +} From 03d0182cfbe83826f6a0621c38481e71135e17ad Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 29 Mar 2024 19:47:52 +0200 Subject: [PATCH 31/99] Fix button margin --- public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 399003f39..d91958044 100644 --- a/public/index.html +++ b/public/index.html @@ -2720,7 +2720,7 @@ <div class="flex-container"> <i data-newbie-hidden data-preset-manager-import="context" class="margin0 menu_button fa-solid fa-file-import" title="Import preset" data-i18n="[title]Import preset"></i> <i data-newbie-hidden data-preset-manager-export="context" class="margin0 menu_button fa-solid fa-file-export" title="Export preset" data-i18n="[title]Export preset"></i> - <i data-newbie-hidden data-preset-manager-restore="context" class="menu_button fa-solid fa-recycle" title="Restore current preset" data-i18n="[title]Restore current preset"></i> + <i data-newbie-hidden data-preset-manager-restore="context" class="margin0 menu_button fa-solid fa-recycle" title="Restore current preset" data-i18n="[title]Restore current preset"></i> <i data-newbie-hidden id="context_delete_preset" data-preset-manager-delete="context" class="margin0 menu_button fa-solid fa-trash-can" title="Delete the preset" data-i18n="[title]Delete the preset"></i> </div> </h4> From bcfa097c224df1ea7c8b3235d7e1bfbb12ffbfd5 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 29 Mar 2024 19:52:19 +0200 Subject: [PATCH 32/99] readme to .gitkeep --- public/group chats/{README.md => .gitkeep} | 0 public/groups/{README.md => .gitkeep} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename public/group chats/{README.md => .gitkeep} (100%) rename public/groups/{README.md => .gitkeep} (100%) diff --git a/public/group chats/README.md b/public/group chats/.gitkeep similarity index 100% rename from public/group chats/README.md rename to public/group chats/.gitkeep diff --git a/public/groups/README.md b/public/groups/.gitkeep similarity index 100% rename from public/groups/README.md rename to public/groups/.gitkeep From 19fd0f18d877130638595d1adab84c7a9619fc64 Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:33:46 -0500 Subject: [PATCH 33/99] Update Alpaca.json --- default/content/presets/instruct/Alpaca.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/default/content/presets/instruct/Alpaca.json b/default/content/presets/instruct/Alpaca.json index f3b3b4cc8..0edd429e4 100644 --- a/default/content/presets/instruct/Alpaca.json +++ b/default/content/presets/instruct/Alpaca.json @@ -11,13 +11,13 @@ "names_force_groups": true, "activation_regex": "", "system_sequence_prefix": "", - "system_sequence_suffix": "", + "system_sequence_suffix": "### Input:", "first_output_sequence": "", "skip_examples": false, - "output_suffix": "", - "input_suffix": "", - "system_suffix": "", + "output_suffix": "\n\n", + "input_suffix": "\n\n", + "system_suffix": "\n\n", "user_alignment_message": "", "system_same_as_user": true, "name": "Alpaca" -} \ No newline at end of file +} From d9730ba6ec11eeb837ee866a13a6e0ab4b5a6c54 Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:35:07 -0500 Subject: [PATCH 34/99] Create Alpaca.json --- default/content/presets/context/Alpaca.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 default/content/presets/context/Alpaca.json diff --git a/default/content/presets/context/Alpaca.json b/default/content/presets/context/Alpaca.json new file mode 100644 index 000000000..6e9418549 --- /dev/null +++ b/default/content/presets/context/Alpaca.json @@ -0,0 +1,12 @@ +{ + "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}\n\n", + "example_separator": "", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Alpaca" +} From 385b5324283879f1dfdaa5ea300be26359a8c885 Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:36:58 -0500 Subject: [PATCH 35/99] Update Alpaca-Roleplay.json --- .../presets/instruct/Alpaca-Roleplay.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/default/content/presets/instruct/Alpaca-Roleplay.json b/default/content/presets/instruct/Alpaca-Roleplay.json index 37bf6cae7..5a5054340 100644 --- a/default/content/presets/instruct/Alpaca-Roleplay.json +++ b/default/content/presets/instruct/Alpaca-Roleplay.json @@ -1,9 +1,9 @@ { "system_prompt": "Develop the plot slowly, always stay in character. Describe all actions in full, elaborate, explicit, graphic, and vivid detail. Mention all relevant sensory perceptions.", - "input_sequence": "\n### Instruction:", - "output_sequence": "\n### Response:", - "last_output_sequence": "\n### Response (2 paragraphs, engaging, natural, authentic, descriptive, creative):", - "system_sequence": "", + "input_sequence": "### Instruction:", + "output_sequence": "### Response:", + "last_output_sequence": "### Response (2 paragraphs, engaging, natural, authentic, descriptive, creative):", + "system_sequence": "### Input:", "stop_sequence": "", "wrap": true, "macro": true, @@ -14,10 +14,10 @@ "system_sequence_suffix": "", "first_output_sequence": "", "skip_examples": false, - "output_suffix": "", - "input_suffix": "", - "system_suffix": "", + "output_suffix": "\n\n", + "input_suffix": "\n\n", + "system_suffix": "\n\n", "user_alignment_message": "", - "system_same_as_user": true, + "system_same_as_user": false, "name": "Alpaca-Roleplay" -} \ No newline at end of file +} From 86cb0a3551e4087e5c76b81b46219a0b82edc060 Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:38:15 -0500 Subject: [PATCH 36/99] Update Alpaca-Roleplay.json --- default/content/presets/context/Alpaca-Roleplay.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/default/content/presets/context/Alpaca-Roleplay.json b/default/content/presets/context/Alpaca-Roleplay.json index d564a1dd7..e7a45c2d8 100644 --- a/default/content/presets/context/Alpaca-Roleplay.json +++ b/default/content/presets/context/Alpaca-Roleplay.json @@ -1,5 +1,5 @@ { - "story_string": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.\n\n{{#if system}}{{system}}\n\n{{/if}}### Input:\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "story_string": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.\n\n{{#if system}}{{system}}\n\n{{/if}}### Input:\n{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}\n\n", "example_separator": "### New Roleplay:", "chat_start": "### New Roleplay:", "use_stop_strings": false, @@ -9,4 +9,4 @@ "include_newline": false, "single_line": false, "name": "Alpaca-Roleplay" -} \ No newline at end of file +} From 3174ea1d539c6fc1602443bbb51d7d75f348649d Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:41:03 -0500 Subject: [PATCH 37/99] Update Synthia.json --- default/content/presets/instruct/Synthia.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/default/content/presets/instruct/Synthia.json b/default/content/presets/instruct/Synthia.json index 3315c9046..d8c0534e0 100644 --- a/default/content/presets/instruct/Synthia.json +++ b/default/content/presets/instruct/Synthia.json @@ -1,9 +1,9 @@ { "system_prompt": "Elaborate on the topic using a Tree of Thoughts and backtrack when necessary to construct a clear, cohesive Chain of Thought reasoning. Always answer without hesitation.", - "input_sequence": "\nUSER: ", - "output_sequence": "\nASSISTANT: ", + "input_sequence": "USER: ", + "output_sequence": "ASSISTANT: ", "last_output_sequence": "", - "system_sequence": "", + "system_sequence": "SYSTEM: ", "stop_sequence": "", "wrap": false, "macro": true, @@ -14,10 +14,10 @@ "system_sequence_suffix": "", "first_output_sequence": "", "skip_examples": false, - "output_suffix": "", - "input_suffix": "", - "system_suffix": "", + "output_suffix": "\n", + "input_suffix": "\n", + "system_suffix": "\n", "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.", "system_same_as_user": true, "name": "Synthia" -} \ No newline at end of file +} From 8892e320e5a91cf62ae85ac696c473945301bca2 Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:42:36 -0500 Subject: [PATCH 38/99] Create Synthia.json --- default/content/presets/context/Synthia.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 default/content/presets/context/Synthia.json diff --git a/default/content/presets/context/Synthia.json b/default/content/presets/context/Synthia.json new file mode 100644 index 000000000..8bffe47d3 --- /dev/null +++ b/default/content/presets/context/Synthia.json @@ -0,0 +1,12 @@ +{ + "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}", + "example_separator": "", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Synthia" +} From b49a2e6df2038d59d29490f8019d4b27258f2c7a Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:46:59 -0500 Subject: [PATCH 39/99] Update Mistral.json --- default/content/presets/instruct/Mistral.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/default/content/presets/instruct/Mistral.json b/default/content/presets/instruct/Mistral.json index 2dbf47fdc..4f35139fa 100644 --- a/default/content/presets/instruct/Mistral.json +++ b/default/content/presets/instruct/Mistral.json @@ -1,7 +1,7 @@ { "system_prompt": "Write {{char}}'s next reply in this fictional roleplay with {{user}}.", "input_sequence": "[INST] ", - "output_sequence": "\n", + "output_sequence": "", "last_output_sequence": "", "system_sequence": "", "stop_sequence": "", @@ -15,9 +15,9 @@ "first_output_sequence": "", "skip_examples": false, "output_suffix": "\n", - "input_suffix": " [/INST]", + "input_suffix": " [/INST]\n", "system_suffix": "", "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.", "system_same_as_user": true, "name": "Mistral" -} \ No newline at end of file +} From 7a3a2a7874bbfca32cceb6e50ef6241820533d88 Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:48:07 -0500 Subject: [PATCH 40/99] Update Mistral.json --- default/content/presets/context/Mistral.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/default/content/presets/context/Mistral.json b/default/content/presets/context/Mistral.json index d8c437e0e..ebc691a5e 100644 --- a/default/content/presets/context/Mistral.json +++ b/default/content/presets/context/Mistral.json @@ -1,5 +1,5 @@ { - "story_string": "[INST] {{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}[/INST]", + "story_string": "[INST] {{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}} [/INST]", "example_separator": "Examples:", "chat_start": "", "use_stop_strings": false, @@ -9,4 +9,4 @@ "include_newline": false, "single_line": false, "name": "Mistral" -} \ No newline at end of file +} From 4b466a9871873c63211acafe551cdcfeabe0fd15 Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:50:24 -0500 Subject: [PATCH 41/99] Update Llama 2 Chat.json --- default/content/presets/instruct/Llama 2 Chat.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/default/content/presets/instruct/Llama 2 Chat.json b/default/content/presets/instruct/Llama 2 Chat.json index c129e71d1..aeb4e13fd 100644 --- a/default/content/presets/instruct/Llama 2 Chat.json +++ b/default/content/presets/instruct/Llama 2 Chat.json @@ -1,7 +1,7 @@ { "system_prompt": "Write {{char}}'s next reply in this fictional roleplay with {{user}}.", "input_sequence": "[INST] ", - "output_sequence": " ", + "output_sequence": "", "last_output_sequence": "", "system_sequence": "", "stop_sequence": "", @@ -10,14 +10,14 @@ "names": false, "names_force_groups": true, "activation_regex": "", - "system_sequence_prefix": "[INST] <<SYS>>\n", - "system_sequence_suffix": "\n<</SYS>>\n", + "system_sequence_prefix": "", + "system_sequence_suffix": "", "first_output_sequence": "", "skip_examples": false, - "output_suffix": " ", - "input_suffix": " [/INST]", + "output_suffix": "\n", + "input_suffix": " [/INST]\n", "system_suffix": "", "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.", "system_same_as_user": true, "name": "Llama 2 Chat" -} \ No newline at end of file +} From b9fa614093b800aa8b555eaa6f2273ca1a3aeb4a Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:51:16 -0500 Subject: [PATCH 42/99] Create Llama 2 Chat.json --- default/content/presets/context/Llama 2 Chat.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 default/content/presets/context/Llama 2 Chat.json diff --git a/default/content/presets/context/Llama 2 Chat.json b/default/content/presets/context/Llama 2 Chat.json new file mode 100644 index 000000000..a5e948925 --- /dev/null +++ b/default/content/presets/context/Llama 2 Chat.json @@ -0,0 +1,12 @@ +{ + "story_string": "[INST] <<SYS>>\n{{#if system}}{{system}}\n<</SYS>>\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}} [/INST]", + "example_separator": "", + "chat_start": "", + "use_stop_strings": false, + "allow_jailbreak": false, + "always_force_name2": true, + "trim_sentences": false, + "include_newline": false, + "single_line": false, + "name": "Llama 2 Chat" +} From 79548d93a98b59dfc9050a02dd7fae6b03716756 Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Fri, 29 Mar 2024 21:00:36 -0500 Subject: [PATCH 43/99] Update Alpaca.json --- default/content/presets/instruct/Alpaca.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/default/content/presets/instruct/Alpaca.json b/default/content/presets/instruct/Alpaca.json index 0edd429e4..4ccdb7175 100644 --- a/default/content/presets/instruct/Alpaca.json +++ b/default/content/presets/instruct/Alpaca.json @@ -3,7 +3,7 @@ "input_sequence": "### Instruction:", "output_sequence": "### Response:", "last_output_sequence": "", - "system_sequence": "", + "system_sequence": "### Input:", "stop_sequence": "", "wrap": true, "macro": true, @@ -11,7 +11,7 @@ "names_force_groups": true, "activation_regex": "", "system_sequence_prefix": "", - "system_sequence_suffix": "### Input:", + "system_sequence_suffix": "", "first_output_sequence": "", "skip_examples": false, "output_suffix": "\n\n", From 3c627996e0c47690054b09ada751351088c63165 Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Fri, 29 Mar 2024 21:03:23 -0500 Subject: [PATCH 44/99] Update Alpaca.json --- default/content/presets/instruct/Alpaca.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default/content/presets/instruct/Alpaca.json b/default/content/presets/instruct/Alpaca.json index 4ccdb7175..96fd2cc83 100644 --- a/default/content/presets/instruct/Alpaca.json +++ b/default/content/presets/instruct/Alpaca.json @@ -18,6 +18,6 @@ "input_suffix": "\n\n", "system_suffix": "\n\n", "user_alignment_message": "", - "system_same_as_user": true, + "system_same_as_user": false, "name": "Alpaca" } From 80f4bd4d9efefd65f39454ea6598ee662b1b0113 Mon Sep 17 00:00:00 2001 From: Wolfsblvt <wolfsblvt@gmx.de> Date: Sat, 30 Mar 2024 03:06:40 +0100 Subject: [PATCH 45/99] Global refactor of printCharacter and filter print - (!) Refactor character list and filter redrawing to one global debounce - Refactor all places where character list and filters where redrawn to the correct usage (hope I didn't miss any) - Automatically redraw character list on each tag bulk edit - Fix tags not being sorted in bulk edit mutual tags list - Refactor bulk tag edit class to actually be an instance object - Remember scroll position on character list redraw - unless it's a full refresh --- public/script.js | 41 +++++++++---- public/scripts/BulkEditOverlay.js | 99 ++++++++++++++++++++----------- public/scripts/bulk-edit.js | 6 +- public/scripts/power-user.js | 12 ++-- public/scripts/tags.js | 42 ++++++------- 5 files changed, 122 insertions(+), 78 deletions(-) diff --git a/public/script.js b/public/script.js index e9bd89da0..9df870852 100644 --- a/public/script.js +++ b/public/script.js @@ -282,6 +282,7 @@ export { mesForShowdownParse, characterGroupOverlay, printCharacters, + printCharactersDebounced, isOdd, countOccurrences, }; @@ -498,6 +499,14 @@ const durationSaveEdit = 1000; const saveSettingsDebounced = debounce(() => saveSettings(), durationSaveEdit); export const saveCharacterDebounced = debounce(() => $('#create_button').trigger('click'), durationSaveEdit); +/** + * Prints the character list in a debounced fashion without blocking, with a delay of 100 milliseconds. + * Use this function instead of a direct `printCharacters()` whenever the reprinting of the character list is not the primary focus. + * + * The printing will also always reprint all filter options of the global list, to keep them up to date. + */ +const printCharactersDebounced = debounce(() => { printCharacters(false); }, 100); + /** * @enum {string} System message types */ @@ -836,7 +845,7 @@ export let active_character = ''; /** The tag of the active group. (Coincidentally also the id) */ export let active_group = ''; -export const entitiesFilter = new FilterHelper(debounce(printCharacters, 100)); +export const entitiesFilter = new FilterHelper(printCharactersDebounced); export const personasFilter = new FilterHelper(debounce(getUserAvatars, 100)); export function getRequestHeaders() { @@ -1275,19 +1284,31 @@ function getCharacterBlock(item, id) { return template; } +/** + * Prints the global character list, optionally doing a full refresh of the list + * Use this function whenever the reprinting of the character list is the primary focus, otherwise using `printCharactersDebounced` is preferred for a cleaner, non-blocking experience. + * + * The printing will also always reprint all filter options of the global list, to keep them up to date. + * + * @param {boolean} fullRefresh - If true, the list is fully refreshed and the navigation is being reset + */ async function printCharacters(fullRefresh = false) { - if (fullRefresh) { - saveCharactersPage = 0; - printTagFilters(tag_filter_types.character); - printTagFilters(tag_filter_types.group_member); - - await delay(1); - } - const storageKey = 'Characters_PerPage'; const listId = '#rm_print_characters_block'; const entities = getEntitiesList({ doFilter: true }); + let currentScrollTop = $(listId).scrollTop(); + + if (fullRefresh) { + saveCharactersPage = 0; + currentScrollTop = 0; + await delay(1); + } + + // We are actually always reprinting filters, as it "doesn't hurt", and this way they are always up to date + printTagFilters(tag_filter_types.character); + printTagFilters(tag_filter_types.group_member); + $('#rm_print_characters_pagination').pagination({ dataSource: entities, pageSize: Number(localStorage.getItem(storageKey)) || per_page_default, @@ -1340,7 +1361,7 @@ async function printCharacters(fullRefresh = false) { saveCharactersPage = e; }, afterRender: function () { - $(listId).scrollTop(0); + $(listId).scrollTop(currentScrollTop); }, }); diff --git a/public/scripts/BulkEditOverlay.js b/public/scripts/BulkEditOverlay.js index 19955e7ac..f3df8156d 100644 --- a/public/scripts/BulkEditOverlay.js +++ b/public/scripts/BulkEditOverlay.js @@ -1,6 +1,7 @@ 'use strict'; import { + characterGroupOverlay, callPopup, characters, deleteCharacter, @@ -9,9 +10,9 @@ import { getCharacters, getPastCharacterChats, getRequestHeaders, - printCharacters, buildAvatarList, characterToEntity, + printCharactersDebounced, } from '../script.js'; import { favsToHotswap } from './RossAscends-mods.js'; @@ -31,7 +32,7 @@ class CharacterContextMenu { * @param {Array<number>} selectedCharacters */ static tag = (selectedCharacters) => { - BulkTagPopupHandler.show(selectedCharacters); + characterGroupOverlay.bulkTagPopupHandler.show(selectedCharacters); }; /** @@ -186,18 +187,36 @@ class CharacterContextMenu { * Represents a tag control not bound to a single character */ class BulkTagPopupHandler { + /** + * The characters for this popup + * @type {number[]} + */ + characterIds; + + /** + * A storage of the current mutual tags, as calculated by getMutualTags() + * @type {object[]} + */ + currentMutualTags; + + /** + * Sets up the bulk popup menu handler for the given overlay. + * + * Characters can be passed in with the show() call. + */ + constructor() { } + /** * Gets the HTML as a string that is going to be the popup for the bulk tag edit * - * @param {Array<number>} characterIds - The characters that are shown inside the popup * @returns String containing the html for the popup */ - static #getHtml = (characterIds) => { - const characterData = JSON.stringify({ characterIds: characterIds }); + #getHtml = () => { + const characterData = JSON.stringify({ characterIds: this.characterIds }); return `<div id="bulk_tag_shadow_popup"> <div id="bulk_tag_popup"> <div id="bulk_tag_popup_holder"> - <h3 class="marginBot5">Modify tags of ${characterIds.length} characters</h3> + <h3 class="marginBot5">Modify tags of ${this.characterIds.length} characters</h3> <small class="bulk_tags_desc m-b-1">Add or remove the mutual tags of all selected characters.</small> <div id="bulk_tags_avatars_block" class="avatars_inline avatars_inline_small tags tags_inline"></div> <br> @@ -227,93 +246,91 @@ class BulkTagPopupHandler { /** * Append and show the tag control * - * @param {Array<number>} characterIds - The characters assigned to this control + * @param {number[]} characterIds - The characters that are shown inside the popup */ - static show(characterIds) { - if (characterIds.length == 0) { + show(characterIds) { + // shallow copy character ids persistently into this tooltip + this.characterIds = characterIds.slice(); + + if (this.characterIds.length == 0) { console.log('No characters selected for bulk edit tags.'); return; } - document.body.insertAdjacentHTML('beforeend', this.#getHtml(characterIds)); + document.body.insertAdjacentHTML('beforeend', this.#getHtml()); - const entities = characterIds.map(id => characterToEntity(characters[id], id)).filter(entity => entity.item !== undefined); + const entities = this.characterIds.map(id => characterToEntity(characters[id], id)).filter(entity => entity.item !== undefined); buildAvatarList($('#bulk_tags_avatars_block'), entities); // Print the tag list with all mutuable tags, marking them as removable. That is the initial fill - printTagList($('#bulkTagList'), { tags: () => this.getMutualTags(characterIds), tagOptions: { removable: true } }); + printTagList($('#bulkTagList'), { tags: () => this.getMutualTags(), tagOptions: { removable: true } }); // Tag input with resolvable list for the mutual tags to get redrawn, so that newly added tags get sorted correctly - createTagInput('#bulkTagInput', '#bulkTagList', { tags: () => this.getMutualTags(characterIds), tagOptions: { removable: true }}); + createTagInput('#bulkTagInput', '#bulkTagList', { tags: () => this.getMutualTags(), tagOptions: { removable: true }}); - document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this, characterIds)); - document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this, characterIds)); + document.querySelector('#bulk_tag_popup_reset').addEventListener('click', this.resetTags.bind(this)); + document.querySelector('#bulk_tag_popup_remove_mutual').addEventListener('click', this.removeMutual.bind(this)); document.querySelector('#bulk_tag_popup_cancel').addEventListener('click', this.hide.bind(this)); } /** * Builds a list of all tags that the provided characters have in common. * - * @param {Array<number>} characterIds - The characters to find mutual tags for * @returns {Array<object>} A list of mutual tags */ - static getMutualTags(characterIds) { - if (characterIds.length == 0) { + getMutualTags() { + if (this.characterIds.length == 0) { return []; } - if (characterIds.length === 1) { + if (this.characterIds.length === 1) { // Just use tags of the single character - return getTagsList(getTagKeyForEntity(characterIds[0])); + return getTagsList(getTagKeyForEntity(this.characterIds[0])); } // Find mutual tags for multiple characters - const allTags = characterIds.map(cid => getTagsList(getTagKeyForEntity(cid))); + const allTags = this.characterIds.map(cid => getTagsList(getTagKeyForEntity(cid))); const mutualTags = allTags.reduce((mutual, characterTags) => mutual.filter(tag => characterTags.some(cTag => cTag.id === tag.id)) ); - this.mutualTags = mutualTags.sort(compareTagsForSort); - return this.mutualTags; + this.currentMutualTags = mutualTags.sort(compareTagsForSort); + return this.currentMutualTags; } /** * Hide and remove the tag control */ - static hide() { + hide() { let popupElement = document.querySelector('#bulk_tag_shadow_popup'); if (popupElement) { document.body.removeChild(popupElement); } - printCharacters(true); + // No need to redraw here, all tags actions were redrawn when they happened } /** * Empty the tag map for the given characters - * - * @param {Array<number>} characterIds */ - static resetTags(characterIds) { - for (const characterId of characterIds) { + resetTags() { + for (const characterId of this.characterIds) { const key = getTagKeyForEntity(characterId); if (key) tag_map[key] = []; } $('#bulkTagList').empty(); - printCharacters(true); + printCharactersDebounced(); } /** * Remove the mutual tags for all given characters - * - * @param {Array<number>} characterIds */ - static removeMutual(characterIds) { - const mutualTags = this.getMutualTags(characterIds); + removeMutual() { + const mutualTags = this.getMutualTags(); - for (const characterId of characterIds) { + for (const characterId of this.characterIds) { for(const tag of mutualTags) { removeTagFromMap(tag.id, characterId); } @@ -321,7 +338,7 @@ class BulkTagPopupHandler { $('#bulkTagList').empty(); - printCharacters(true); + printCharactersDebounced(); } } @@ -364,6 +381,7 @@ class BulkEditOverlay { #longPress = false; #stateChangeCallbacks = []; #selectedCharacters = []; + #bulkTagPopupHandler = new BulkTagPopupHandler(); /** * @typedef {object} LastSelected - An object noting the last selected character and its state. @@ -429,6 +447,15 @@ class BulkEditOverlay { return this.#selectedCharacters; } + /** + * The instance of the bulk tag popup handler that handles tagging of all selected characters + * + * @returns {BulkTagPopupHandler} + */ + get bulkTagPopupHandler() { + return this.#bulkTagPopupHandler; + } + constructor() { if (bulkEditOverlayInstance instanceof BulkEditOverlay) return bulkEditOverlayInstance; diff --git a/public/scripts/bulk-edit.js b/public/scripts/bulk-edit.js index c266e148b..f09e290df 100644 --- a/public/scripts/bulk-edit.js +++ b/public/scripts/bulk-edit.js @@ -30,7 +30,7 @@ const toggleBulkEditMode = (isBulkEdit) => { } }; -(new BulkEditOverlay()).addStateChangeCallback((state) => { +characterGroupOverlay.addStateChangeCallback((state) => { if (state === BulkEditOverlayState.select) enableBulkEdit(); if (state === BulkEditOverlayState.browse) disableBulkEdit(); }); @@ -52,7 +52,7 @@ function onSelectAllButtonClick() { let atLeastOneSelected = false; for (const character of characters) { const checked = $(character).find('.bulk_select_checkbox:checked').length > 0; - if (!checked) { + if (!checked && character instanceof HTMLElement) { characterGroupOverlay.toggleSingleCharacter(character); atLeastOneSelected = true; } @@ -62,7 +62,7 @@ function onSelectAllButtonClick() { // If none was selected, trigger click on all to deselect all of them for(const character of characters) { const checked = $(character).find('.bulk_select_checkbox:checked') ?? false; - if (checked) { + if (checked && character instanceof HTMLElement) { characterGroupOverlay.toggleSingleCharacter(character); } } diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index d0cc3fc06..c7a9d3502 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -10,7 +10,7 @@ import { eventSource, event_types, getCurrentChatId, - printCharacters, + printCharactersDebounced, setCharacterId, setEditedMessageId, renderTemplate, @@ -1288,7 +1288,7 @@ async function applyTheme(name) { key: 'bogus_folders', action: async () => { $('#bogus_folders').prop('checked', power_user.bogus_folders); - await printCharacters(true); + printCharactersDebounced(); }, }, { @@ -3045,7 +3045,7 @@ $(document).ready(() => { $('#show_card_avatar_urls').on('input', function () { power_user.show_card_avatar_urls = !!$(this).prop('checked'); - printCharacters(); + printCharactersDebounced(); saveSettingsDebounced(); }); @@ -3068,7 +3068,7 @@ $(document).ready(() => { power_user.sort_field = $(this).find(':selected').data('field'); power_user.sort_order = $(this).find(':selected').data('order'); power_user.sort_rule = $(this).find(':selected').data('rule'); - printCharacters(); + printCharactersDebounced(); saveSettingsDebounced(); }); @@ -3365,15 +3365,15 @@ $(document).ready(() => { $('#bogus_folders').on('input', function () { const value = !!$(this).prop('checked'); power_user.bogus_folders = value; + printCharactersDebounced(); saveSettingsDebounced(); - printCharacters(true); }); $('#aux_field').on('change', function () { const value = $(this).find(':selected').val(); power_user.aux_field = String(value); + printCharactersDebounced(); saveSettingsDebounced(); - printCharacters(false); }); $('#restore_user_input').on('input', function () { diff --git a/public/scripts/tags.js b/public/scripts/tags.js index a20f0ff40..8c38ec864 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -7,6 +7,7 @@ import { getCharacters, entitiesFilter, printCharacters, + printCharactersDebounced, buildAvatarList, eventSource, event_types, @@ -48,12 +49,6 @@ function getFilterHelper(listSelector) { return $(listSelector).is(GROUP_FILTER_SELECTOR) ? groupCandidatesFilter : entitiesFilter; } -const redrawCharsAndFiltersDebounced = debounce(() => { - printCharacters(false); - printTagFilters(tag_filter_types.character); - printTagFilters(tag_filter_types.group_member); -}, 100); - export const tag_filter_types = { character: 0, group_member: 1, @@ -406,10 +401,11 @@ function findTag(request, resolve, listSelector) { * @param {*} event - The event that fired on autocomplete select * @param {*} ui - An Object with label and value properties for the selected option * @param {*} listSelector - The selector of the list to print/add to - * @param {PrintTagListOptions} [tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before. + * @param {object} param1 - Optional parameters for this method call + * @param {PrintTagListOptions} [param1.tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before. * @returns {boolean} <c>false</c>, to keep the input clear */ -function selectTag(event, ui, listSelector, tagListOptions = {}) { +function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) { let tagName = ui.item.value; let tag = tags.find(t => t.name === tagName); @@ -431,6 +427,7 @@ function selectTag(event, ui, listSelector, tagListOptions = {}) { addTagToMap(tag.id); } + printCharactersDebounced(); saveSettingsDebounced(); // We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it @@ -443,9 +440,6 @@ function selectTag(event, ui, listSelector, tagListOptions = {}) { printTagList($(inlineSelector), tagListOptions); } - printTagFilters(tag_filter_types.character); - printTagFilters(tag_filter_types.group_member); - // need to return false to keep the input clear return false; } @@ -493,10 +487,11 @@ async function importTags(imported_char) { console.debug('added tag to map', tag, imported_char.name); } } + saveSettingsDebounced(); + + // Await the character list, which will automatically reprint it and all tag filters await getCharacters(); - printTagFilters(tag_filter_types.character); - printTagFilters(tag_filter_types.group_member); // need to return false to keep the input clear return false; @@ -767,8 +762,7 @@ function onTagRemoveClick(event) { $(`${getInlineListSelector()} .tag[id="${tagId}"]`).remove(); - printTagFilters(tag_filter_types.character); - printTagFilters(tag_filter_types.group_member); + printCharactersDebounced(); saveSettingsDebounced(); @@ -818,7 +812,7 @@ export function createTagInput(inputSelector, listSelector, tagListOptions = {}) // @ts-ignore .autocomplete({ source: (i, o) => findTag(i, o, listSelector), - select: (e, u) => selectTag(e, u, listSelector, tagListOptions), + select: (e, u) => selectTag(e, u, listSelector, { tagListOptions: tagListOptions }), minLength: 0, }) .focus(onTagInputFocus); // <== show tag list on click @@ -900,10 +894,9 @@ function makeTagListDraggable(tagContainer) { } }); - saveSettingsDebounced(); - // If the order of tags in display has changed, we need to redraw some UI elements. Do it debounced so it doesn't block and you can drag multiple tags. - redrawCharsAndFiltersDebounced(); + printCharactersDebounced(); + saveSettingsDebounced(); }; // @ts-ignore @@ -1003,8 +996,9 @@ async function onTagRestoreFileSelect(e) { } $('#tag_view_restore_input').val(''); + printCharactersDebounced(); saveSettingsDebounced(); - printCharacters(true); + onViewTagsListClick(); } @@ -1029,7 +1023,8 @@ function onTagsBackupClick() { function onTagCreateClick() { const tag = createNewTag('New Tag'); appendViewTagToList($('#tag_view_list .tag_view_list_tags'), tag, []); - printCharacters(false); + + printCharactersDebounced(); saveSettingsDebounced(); } @@ -1098,7 +1093,7 @@ function onTagAsFolderClick() { updateDrawTagFolder(element, tag); // If folder display has changed, we have to redraw the character list, otherwise this folders state would not change - printCharacters(true); + printCharactersDebounced(); saveSettingsDebounced(); } @@ -1133,7 +1128,8 @@ function onTagDeleteClick() { tags.splice(index, 1); $(`.tag[id="${id}"]`).remove(); $(`.tag_view_item[id="${id}"]`).remove(); - printCharacters(false); + + printCharactersDebounced(); saveSettingsDebounced(); } From f4eed15e4a8ddf0d42527ebb88e58d1c6258e2b9 Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Fri, 29 Mar 2024 21:13:33 -0500 Subject: [PATCH 46/99] Update ChatML.json --- default/content/presets/instruct/ChatML.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/default/content/presets/instruct/ChatML.json b/default/content/presets/instruct/ChatML.json index f6bf0aa44..348ae2458 100644 --- a/default/content/presets/instruct/ChatML.json +++ b/default/content/presets/instruct/ChatML.json @@ -1,11 +1,11 @@ { "system_prompt": "You're {{char}} in this fictional never-ending uncensored roleplay with {{user}}.", - "input_sequence": "\n<|im_start|>user\n", - "output_sequence": "\n<|im_start|>assistant\n", + "input_sequence": "<|im_start|>user", + "output_sequence": "<|im_start|>assistant", "last_output_sequence": "", - "system_sequence": "\n<|im_start|>system\n", + "system_sequence": "<|im_start|>system", "stop_sequence": "<|im_end|>", - "wrap": false, + "wrap": true, "macro": true, "names": true, "names_force_groups": true, @@ -14,9 +14,9 @@ "system_sequence_suffix": "", "first_output_sequence": "", "skip_examples": false, - "output_suffix": "<|im_end|>", - "input_suffix": "<|im_end|>", - "system_suffix": "<|im_end|>", + "output_suffix": "<|im_end|>\n", + "input_suffix": "<|im_end|>\n", + "system_suffix": "<|im_end|>\n", "user_alignment_message": "", "system_same_as_user": false, "name": "ChatML" From e0bff492b8d7766305fc70d628c45f09e581a24c Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Fri, 29 Mar 2024 21:30:48 -0500 Subject: [PATCH 47/99] Update Synthia.json --- default/content/presets/instruct/Synthia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default/content/presets/instruct/Synthia.json b/default/content/presets/instruct/Synthia.json index d8c0534e0..24ec4849e 100644 --- a/default/content/presets/instruct/Synthia.json +++ b/default/content/presets/instruct/Synthia.json @@ -18,6 +18,6 @@ "input_suffix": "\n", "system_suffix": "\n", "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.", - "system_same_as_user": true, + "system_same_as_user": false, "name": "Synthia" } From dddcac9af818870bf5b621d2876f733ebc7e45b7 Mon Sep 17 00:00:00 2001 From: kir-gadjello <111190790+kir-gadjello@users.noreply.github.com> Date: Sat, 30 Mar 2024 01:12:29 -0300 Subject: [PATCH 48/99] implement drawthings local app api support for sd extension --- .../extensions/stable-diffusion/index.js | 144 ++++++++++++++++++ .../extensions/stable-diffusion/settings.html | 16 ++ src/endpoints/stable-diffusion.js | 73 +++++++++ 3 files changed, 233 insertions(+) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index bb12cb416..7fd51ddc5 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -47,6 +47,7 @@ const sources = { openai: 'openai', comfy: 'comfy', togetherai: 'togetherai', + drawthings: 'drawthings', }; const generationMode = { @@ -217,6 +218,9 @@ const defaultSettings = { vlad_url: 'http://localhost:7860', vlad_auth: '', + drawthings_url: 'http://localhost:7860', + drawthings_auth: '', + hr_upscaler: 'Latent', hr_scale: 2.0, hr_scale_min: 1.0, @@ -312,6 +316,8 @@ function getSdRequestBody() { return { url: extension_settings.sd.vlad_url, auth: extension_settings.sd.vlad_auth }; case sources.auto: return { url: extension_settings.sd.auto_url, auth: extension_settings.sd.auto_auth }; + case sources.drawthings: + return { url: extension_settings.sd.drawthings_url }; default: throw new Error('Invalid SD source.'); } @@ -385,6 +391,8 @@ async function loadSettings() { $('#sd_auto_auth').val(extension_settings.sd.auto_auth); $('#sd_vlad_url').val(extension_settings.sd.vlad_url); $('#sd_vlad_auth').val(extension_settings.sd.vlad_auth); + $('#sd_drawthings_url').val(extension_settings.sd.drawthings_url); + $('#sd_drawthings_auth').val(extension_settings.sd.drawthings_auth); $('#sd_interactive_mode').prop('checked', extension_settings.sd.interactive_mode); $('#sd_openai_style').val(extension_settings.sd.openai_style); $('#sd_openai_quality').val(extension_settings.sd.openai_quality); @@ -844,6 +852,16 @@ function onVladAuthInput() { saveSettingsDebounced(); } +function onDrawthingsUrlInput() { + extension_settings.sd.drawthings_url = $('#sd_drawthings_url').val(); + saveSettingsDebounced(); +} + +function onDrawthingsAuthInput() { + extension_settings.sd.drawthings_auth = $('#sd_drawthings_auth').val(); + saveSettingsDebounced(); +} + function onHrUpscalerChange() { extension_settings.sd.hr_upscaler = $('#sd_hr_upscaler').find(':selected').val(); saveSettingsDebounced(); @@ -910,6 +928,29 @@ async function validateAutoUrl() { } } +async function validateDrawthingsUrl() { + try { + if (!extension_settings.sd.drawthings_url) { + throw new Error('URL is not set.'); + } + + const result = await fetch('/api/sd/drawthings/ping', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify(getSdRequestBody()), + }); + + if (!result.ok) { + throw new Error('SD Drawthings returned an error.'); + } + + await loadSettingOptions(); + toastr.success('SD Drawthings API connected.'); + } catch (error) { + toastr.error(`Could not validate SD Drawthings API: ${error.message}`); + } +} + async function validateVladUrl() { try { if (!extension_settings.sd.vlad_url) { @@ -997,6 +1038,27 @@ async function getAutoRemoteModel() { } } +async function getDrawthingsRemoteModel() { + try { + const result = await fetch('/api/sd/drawthings/get-model', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify(getSdRequestBody()), + }); + + if (!result.ok) { + throw new Error('SD DrawThings API returned an error.'); + } + + const data = await result.text(); + + return data; + } catch (error) { + console.error(error); + return null; + } +} + async function onVaeChange() { extension_settings.sd.vae = $('#sd_vae').find(':selected').val(); } @@ -1087,6 +1149,9 @@ async function loadSamplers() { case sources.auto: samplers = await loadAutoSamplers(); break; + case sources.drawthings: + samplers = await loadDrawthingsSamplers(); + break; case sources.novel: samplers = await loadNovelSamplers(); break; @@ -1172,6 +1237,11 @@ async function loadAutoSamplers() { } } +async function loadDrawthingsSamplers() { + // The app developer doesn't provide an API to get these yet + return ["UniPC", "DPM++ 2M Karras", "Euler a", "DPM++ SDE Karras", "PLMS", "DDIM", "LCM", "Euler A Substep", "DPM++ SDE Substep", "TCD"]; +} + async function loadVladSamplers() { if (!extension_settings.sd.vlad_url) { return []; @@ -1248,6 +1318,9 @@ async function loadModels() { case sources.auto: models = await loadAutoModels(); break; + case sources.drawthings: + models = await loadDrawthingsModels(); + break; case sources.novel: models = await loadNovelModels(); break; @@ -1384,6 +1457,27 @@ async function loadAutoModels() { } } +async function loadDrawthingsModels() { + if (!extension_settings.sd.drawthings_url) { + return []; + } + + try { + const currentModel = await getDrawthingsRemoteModel(); + + if (currentModel) { + extension_settings.sd.model = currentModel; + } + + const data = [{value: currentModel, text: currentModel}]; + + return data; + } catch (error) { + console.log("Error loading DrawThings API models:", error); + return []; + } +} + async function loadOpenAiModels() { return [ { value: 'dall-e-3', text: 'DALL-E 3' }, @@ -1506,6 +1600,9 @@ async function loadSchedulers() { case sources.vlad: schedulers = ['N/A']; break; + case sources.drawthings: + schedulers = ['N/A']; + break; case sources.openai: schedulers = ['N/A']; break; @@ -1568,6 +1665,9 @@ async function loadVaes() { case sources.vlad: vaes = ['N/A']; break; + case sources.drawthings: + vaes = ['N/A']; + break; case sources.openai: vaes = ['N/A']; break; @@ -1975,6 +2075,9 @@ async function sendGenerationRequest(generationType, prompt, characterName = nul case sources.vlad: result = await generateAutoImage(prefixedPrompt, negativePrompt); break; + case sources.drawthings: + result = await generateDrawthingsImage(prefixedPrompt, negativePrompt); + break; case sources.auto: result = await generateAutoImage(prefixedPrompt, negativePrompt); break; @@ -2157,6 +2260,42 @@ async function generateAutoImage(prompt, negativePrompt) { } } +/** + * Generates an image in Drawthings API using the provided prompt and configuration settings. + * + * @param {string} prompt - The main instruction used to guide the image generation. + * @param {string} negativePrompt - The instruction used to restrict the image generation. + * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. + */ +async function generateDrawthingsImage(prompt, negativePrompt) { + const result = await fetch('/api/sd/drawthings/generate', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + ...getSdRequestBody(), + prompt: prompt, + negative_prompt: negativePrompt, + sampler_name: extension_settings.sd.sampler, + steps: extension_settings.sd.steps, + cfg_scale: extension_settings.sd.scale, + width: extension_settings.sd.width, + height: extension_settings.sd.height, + restore_faces: !!extension_settings.sd.restore_faces, + enable_hr: !!extension_settings.sd.enable_hr, + denoising_strength: extension_settings.sd.denoising_strength, + // TODO: advanced API parameters: hr, upscaler + }), + }); + + if (result.ok) { + const data = await result.json(); + return { format: 'png', data: data.images[0] }; + } else { + const text = await result.text(); + throw new Error(text); + } +} + /** * Generates an image in NovelAI API using the provided prompt and configuration settings. * @@ -2573,6 +2712,8 @@ function isValidState() { return true; case sources.auto: return !!extension_settings.sd.auto_url; + case sources.drawthings: + return !!extension_settings.sd.drawthings_url; case sources.vlad: return !!extension_settings.sd.vlad_url; case sources.novel: @@ -2715,6 +2856,9 @@ jQuery(async () => { $('#sd_auto_validate').on('click', validateAutoUrl); $('#sd_auto_url').on('input', onAutoUrlInput); $('#sd_auto_auth').on('input', onAutoAuthInput); + $('#sd_drawthings_validate').on('click', validateDrawthingsUrl); + $('#sd_drawthings_url').on('input', onDrawthingsUrlInput); + $('#sd_drawthings_auth').on('input', onDrawthingsAuthInput); $('#sd_vlad_validate').on('click', validateVladUrl); $('#sd_vlad_url').on('input', onVladUrlInput); $('#sd_vlad_auth').on('input', onVladAuthInput); diff --git a/public/scripts/extensions/stable-diffusion/settings.html b/public/scripts/extensions/stable-diffusion/settings.html index 9fcefe3bc..3cb34d3aa 100644 --- a/public/scripts/extensions/stable-diffusion/settings.html +++ b/public/scripts/extensions/stable-diffusion/settings.html @@ -36,6 +36,7 @@ <option value="horde">Stable Horde</option> <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option> <option value="vlad">SD.Next (vladmandic)</option> + <option value="drawthings">DrawThings HTTP API</option> <option value="novel">NovelAI Diffusion</option> <option value="openai">OpenAI (DALL-E)</option> <option value="comfy">ComfyUI</option> @@ -56,6 +57,21 @@ <input id="sd_auto_auth" type="text" class="text_pole" placeholder="Example: username:password" value="" /> <i><b>Important:</b> run SD Web UI with the <tt>--api</tt> flag! The server must be accessible from the SillyTavern host machine.</i> </div> + <div data-sd-source="drawthings"> + <label for="sd_drawthings_url">DrawThings API URL</label> + <div class="flex-container flexnowrap"> + <input id="sd_drawthings_url" type="text" class="text_pole" placeholder="Example: {{drawthings_url}}" value="{{drawthings_url}}" /> + <div id="sd_drawthings_validate" class="menu_button menu_button_icon"> + <i class="fa-solid fa-check"></i> + <span data-i18n="Connect"> + Connect + </span> + </div> + </div> + <label for="sd_drawthings_auth">Authentication (optional)</label> + <input id="sd_drawthings_auth" type="text" class="text_pole" placeholder="Example: username:password" value="" /> + <i><b>Important:</b> run DrawThings app with HTTP API switch enabled in the UI! The server must be accessible from the SillyTavern host machine.</i> + </div> <div data-sd-source="vlad"> <label for="sd_vlad_url">SD.Next API URL</label> <div class="flex-container flexnowrap"> diff --git a/src/endpoints/stable-diffusion.js b/src/endpoints/stable-diffusion.js index 1054d2d6b..e2168cd80 100644 --- a/src/endpoints/stable-diffusion.js +++ b/src/endpoints/stable-diffusion.js @@ -638,7 +638,80 @@ together.post('/generate', jsonParser, async (request, response) => { } }); +const drawthings = express.Router(); + +drawthings.post('/ping', jsonParser, async (request, response) => { + try { + const url = new URL(request.body.url); + url.pathname = '/'; + + const result = await fetch(url, { + method: 'HEAD', + }); + + if (!result.ok) { + throw new Error('SD DrawThings API returned an error.'); + } + + return response.sendStatus(200); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } +}); + +drawthings.post('/get-model', jsonParser, async (request, response) => { + try { + const url = new URL(request.body.url); + url.pathname = '/'; + + const result = await fetch(url, { + method: 'GET', + }); + const data = await result.json(); + + return response.send(data['model']); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } +}); + +drawthings.post('/generate', jsonParser, async (request, response) => { + try { + console.log('SD DrawThings API request:', request.body); + + const url = new URL(request.body.url); + url.pathname = '/sdapi/v1/txt2img'; + + const body = {...request.body}; + delete body.url; + + const result = await fetch(url, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + 'Authorization': getBasicAuthHeader(request.body.auth), + }, + timeout: 0, + }); + + if (!result.ok) { + const text = await result.text(); + throw new Error('SD DrawThings API returned an error.', { cause: text }); + } + + const data = await result.json(); + return response.send(data); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } +}); + router.use('/comfy', comfy); router.use('/together', together); +router.use('/drawthings', drawthings); module.exports = { router }; From ea4ba57408869eb1dbecd7189ec5ad060cddc915 Mon Sep 17 00:00:00 2001 From: Wolfsblvt <wolfsblvt@gmx.de> Date: Sat, 30 Mar 2024 05:41:54 +0100 Subject: [PATCH 49/99] Fix horizontal scrollbar appearing in popups - Fix that annoying horizontal scrollbar appearing in popups, e.g. the tag popup when you drag tags around - Still provide possibility to make popups actually utilize scrollbars --- public/script.js | 7 ++++--- public/style.css | 9 +++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/public/script.js b/public/script.js index 9df870852..9057cfec3 100644 --- a/public/script.js +++ b/public/script.js @@ -6810,18 +6810,19 @@ function onScenarioOverrideRemoveClick() { * @param {string} type * @param {string} inputValue - Value to set the input to. * @param {PopupOptions} options - Options for the popup. - * @typedef {{okButton?: string, rows?: number, wide?: boolean, large?: boolean }} PopupOptions - Options for the popup. + * @typedef {{okButton?: string, rows?: number, wide?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean }} PopupOptions - Options for the popup. * @returns */ -function callPopup(text, type, inputValue = '', { okButton, rows, wide, large } = {}) { +function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) { dialogueCloseStop = true; if (type) { popup_type = type; } $('#dialogue_popup').toggleClass('wide_dialogue_popup', !!wide); - $('#dialogue_popup').toggleClass('large_dialogue_popup', !!large); + $('#dialogue_popup').toggleClass('horizontal_scrolling_dialogue_popup', !!allowHorizontalScrolling); + $('#dialogue_popup').toggleClass('vertical_scrolling_dialogue_popup', !!allowVerticalScrolling); $('#dialogue_popup_cancel').css('display', 'inline-block'); switch (popup_type) { diff --git a/public/style.css b/public/style.css index e6218e820..90c04de9f 100644 --- a/public/style.css +++ b/public/style.css @@ -2081,6 +2081,7 @@ grammarly-extension { display: flex; flex-direction: column; overflow-y: hidden; + overflow-x: hidden; } .rm_stat_block { @@ -2101,6 +2102,14 @@ grammarly-extension { min-width: var(--sheldWidth); } +.horizontal_scrolling_dialogue_popup { + overflow-x: unset !important; +} + +.vertical_scrolling_dialogue_popup { + overflow-y: unset !important; +} + #bulk_tag_popup_holder, #dialogue_popup_holder { display: flex; From 12a36341f8c7ff1f38610ef09028a1d30256ecae Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 11:50:20 +0200 Subject: [PATCH 50/99] Fix newline trimming for the last message --- public/script.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/public/script.js b/public/script.js index 04fb0238d..7d1f85bdb 100644 --- a/public/script.js +++ b/public/script.js @@ -3505,15 +3505,18 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu console.debug('generating prompt'); chatString = ''; arrMes = arrMes.reverse(); - arrMes.forEach(function (item, i, arr) {// For added anchors and others + arrMes.forEach(function (item, i, arr) { // OAI doesn't need all of this if (main_api === 'openai') { return; } - // Cohee: I'm not even sure what this is for anymore + // Cohee: This removes a newline from the end of the last message in the context + // Last prompt line will add a newline if it's not a continuation if (i === arrMes.length - 1 && type !== 'continue') { - item = item.replace(/\n?$/, ''); + if (!isInstruct || power_user.instruct.wrap) { + item = item.replace(/\n?$/, ''); + } } mesSend[mesSend.length] = { message: item, extensionPrompts: [] }; From 06e15e6d5fc81b7a09844b909ca5c8a6e3434a04 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 12:00:10 +0200 Subject: [PATCH 51/99] L + skill issue + bad copypaste --- public/scripts/instruct-mode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index e646cdd79..48fec1129 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -373,7 +373,7 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { let inputPrefix = power_user.instruct.input_sequence || ''; let outputPrefix = power_user.instruct.output_sequence || ''; - let inputSuffix = power_user.instruct.output_suffix || ''; + let inputSuffix = power_user.instruct.input_suffix || ''; let outputSuffix = power_user.instruct.output_suffix || ''; if (power_user.instruct.macro) { From d997f8dc530c0f027ca6b493f9f67dd75c67d8aa Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 12:26:21 +0200 Subject: [PATCH 52/99] Add {{trim}} macro --- default/content/presets/context/ChatML.json | 2 +- default/content/presets/context/Llama 2 Chat.json | 2 +- default/content/presets/context/Mistral.json | 2 +- public/scripts/macros.js | 4 ++++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/default/content/presets/context/ChatML.json b/default/content/presets/context/ChatML.json index 80046d170..2184e91d3 100644 --- a/default/content/presets/context/ChatML.json +++ b/default/content/presets/context/ChatML.json @@ -1,5 +1,5 @@ { - "story_string": "<|im_start|>system\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}<|im_end|>", + "story_string": "<|im_start|>system\n{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}}<|im_end|>", "example_separator": "", "chat_start": "", "use_stop_strings": false, diff --git a/default/content/presets/context/Llama 2 Chat.json b/default/content/presets/context/Llama 2 Chat.json index a5e948925..be18ad69d 100644 --- a/default/content/presets/context/Llama 2 Chat.json +++ b/default/content/presets/context/Llama 2 Chat.json @@ -1,5 +1,5 @@ { - "story_string": "[INST] <<SYS>>\n{{#if system}}{{system}}\n<</SYS>>\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}} [/INST]", + "story_string": "[INST] <<SYS>>\n{{#if system}}{{system}}\n<</SYS>>\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}} [/INST]", "example_separator": "", "chat_start": "", "use_stop_strings": false, diff --git a/default/content/presets/context/Mistral.json b/default/content/presets/context/Mistral.json index ebc691a5e..d9551afe8 100644 --- a/default/content/presets/context/Mistral.json +++ b/default/content/presets/context/Mistral.json @@ -1,5 +1,5 @@ { - "story_string": "[INST] {{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}} [/INST]", + "story_string": "[INST] {{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}{{trim}} [/INST]", "example_separator": "Examples:", "chat_start": "", "use_stop_strings": false, diff --git a/public/scripts/macros.js b/public/scripts/macros.js index 63da09b0c..f3527d451 100644 --- a/public/scripts/macros.js +++ b/public/scripts/macros.js @@ -4,6 +4,9 @@ import { textgenerationwebui_banned_in_macros } from './textgen-settings.js'; import { replaceInstructMacros } from './instruct-mode.js'; import { replaceVariableMacros } from './variables.js'; +// Register any macro that you want to leave in the compiled story string +Handlebars.registerHelper('trim', () => '{{trim}}'); + /** * Returns the ID of the last message in the chat. * @returns {string} The ID of the last message in the chat. @@ -257,6 +260,7 @@ export function evaluateMacros(content, env) { content = replaceInstructMacros(content); content = replaceVariableMacros(content); content = content.replace(/{{newline}}/gi, '\n'); + content = content.replace(/\n*{{trim}}\n*/gi, ''); content = content.replace(/{{input}}/gi, () => String($('#send_textarea').val())); // Substitute passed-in variables From b747bdf89ba4e3b10ce299bcdfc27e36e9aa2796 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 13:46:46 +0200 Subject: [PATCH 53/99] Fix nav styles for narrower screens --- public/style.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/style.css b/public/style.css index 90c04de9f..711d4fcca 100644 --- a/public/style.css +++ b/public/style.css @@ -3887,6 +3887,7 @@ body:not(.movingUI) .drawer-content.maximized { .paginationjs-size-changer select { width: unset; margin: 0; + font-size: calc(var(--mainFontSize) * 0.85); } .paginationjs-pages ul li a { @@ -3916,10 +3917,10 @@ body:not(.movingUI) .drawer-content.maximized { } .paginationjs-nav { - padding: 5px; + padding: 2px; font-size: calc(var(--mainFontSize) * .8); font-weight: bold; - width: max-content; + width: auto; } .onboarding { From 652987ae0195a644aed055c5cd5a2df2d4e95ddd Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 14:23:50 +0200 Subject: [PATCH 54/99] Add missing auth header --- public/scripts/extensions/stable-diffusion/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 7fd51ddc5..5f1a561c9 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -317,7 +317,7 @@ function getSdRequestBody() { case sources.auto: return { url: extension_settings.sd.auto_url, auth: extension_settings.sd.auto_auth }; case sources.drawthings: - return { url: extension_settings.sd.drawthings_url }; + return { url: extension_settings.sd.drawthings_url, auth: extension_settings.sd.drawthings_auth }; default: throw new Error('Invalid SD source.'); } From 9987ec33d95f7f8d40df38ed1b291476ea7c8344 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 15:48:24 +0200 Subject: [PATCH 55/99] Add new contexts to content index --- default/content/index.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/default/content/index.json b/default/content/index.json index fd66ea8d9..8a914b959 100644 --- a/default/content/index.json +++ b/default/content/index.json @@ -368,6 +368,10 @@ "filename": "presets/context/Alpaca-Single-Turn.json", "type": "context" }, + { + "filename": "presets/context/Alpaca.json", + "type": "context" + }, { "filename": "presets/context/ChatML.json", "type": "context" @@ -388,6 +392,10 @@ "filename": "presets/context/Lightning 1.1.json", "type": "context" }, + { + "filename": "presets/context/Llama 2 Chat.json", + "type": "context" + }, { "filename": "presets/context/Minimalist.json", "type": "context" @@ -412,6 +420,10 @@ "filename": "presets/context/Story.json", "type": "context" }, + { + "filename": "presets/context/Synthia.json", + "type": "context" + }, { "filename": "presets/context/simple-proxy-for-tavern.json", "type": "context" From e99d37d549520d47e72f5dcf5a98b37e98419d69 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 15:54:42 +0200 Subject: [PATCH 56/99] Remove extraneous newlines when formatting quiet prompts --- public/script.js | 2 +- public/scripts/instruct-mode.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 7d1f85bdb..83a587d2d 100644 --- a/public/script.js +++ b/public/script.js @@ -3555,7 +3555,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu //TODO: respect output_sequence vs last_output_sequence settings //TODO: decide how to prompt this to clarify who is talking 'Narrator', 'System', etc. if (isInstruct) { - lastMesString += '\n' + quietAppend; // + power_user.instruct.output_sequence + '\n'; + lastMesString += quietAppend; // + power_user.instruct.output_sequence + '\n'; } else { lastMesString += quietAppend; } diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index 48fec1129..de8b93c5b 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -458,6 +458,11 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, const separator = power_user.instruct.wrap ? '\n' : ''; let text = includeNames ? (separator + sequence + separator + `${name}:`) : (separator + sequence); + // Quiet prompt already has a newline at the end + if (isQuiet && separator) { + text = text.slice(separator.length); + } + if (!isImpersonate && promptBias) { text += (includeNames ? promptBias : (separator + promptBias.trimStart())); } From 34c79049ad054135538583fb9bd0b7d2c81642fe Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 16:04:10 +0200 Subject: [PATCH 57/99] Fix edge case with newlines in quiet prompts with wrap enabled --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 83a587d2d..4c7e06f88 100644 --- a/public/script.js +++ b/public/script.js @@ -3514,7 +3514,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu // Cohee: This removes a newline from the end of the last message in the context // Last prompt line will add a newline if it's not a continuation if (i === arrMes.length - 1 && type !== 'continue') { - if (!isInstruct || power_user.instruct.wrap) { + if (!isInstruct || (power_user.instruct.wrap && type !== 'quiet')) { item = item.replace(/\n?$/, ''); } } From 26690353e1db9fc5b82c025f5fcf6c895a712ad5 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 16:05:43 +0200 Subject: [PATCH 58/99] + edge case comment --- public/script.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/script.js b/public/script.js index 4c7e06f88..5a2be853e 100644 --- a/public/script.js +++ b/public/script.js @@ -3513,6 +3513,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu // Cohee: This removes a newline from the end of the last message in the context // Last prompt line will add a newline if it's not a continuation + // In instruct mode it only removes it if wrap is enabled and it's not a quiet generation if (i === arrMes.length - 1 && type !== 'continue') { if (!isInstruct || (power_user.instruct.wrap && type !== 'quiet')) { item = item.replace(/\n?$/, ''); From bd62c2fb70ff0505616175a2c1bc0e9ad6e36be9 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 18:13:55 +0200 Subject: [PATCH 59/99] Add 'as' argument for /gen command --- public/scripts/slash-commands.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 7d4c63dbe..fb745364d 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -230,7 +230,7 @@ parser.addCommand('peek', peekCallback, [], '<span class="monospace">(message in parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '<span class="monospace">(optional 1-based id)</span> – deletes a swipe from the last chat message. If swipe id not provided - deletes the current swipe.', true, true); parser.addCommand('echo', echoCallback, [], '<span class="monospace">(title=string severity=info/warning/error/success [text])</span> – echoes the text to toast message. Useful for pipes debugging.', true, true); //parser.addCommand('#', (_, value) => '', [], ' – a comment, does nothing, e.g. <tt>/# the next three commands switch variables a and b</tt>', true, true); -parser.addCommand('gen', generateCallback, [], '<span class="monospace">(lock=on/off name="System" [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating and allowing to configure the in-prompt name for instruct mode (default = "System").', true, true); +parser.addCommand('gen', generateCallback, [], '<span class="monospace">(lock=on/off name="System" [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating and allowing to configure the in-prompt name for instruct mode (default = "System"). "as" argument controls the role of the output prompt: system (default) or char.', true, true); parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(lock=on/off instruct=on/off stop=[] as=system/char [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. <tt>/genraw instruct=off Why is the sky blue?</tt>. Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. <tt>/genraw stop=["\\n"] Say hi</tt>. "as" argument controls the role of the output prompt: system (default) or char.', true, true); parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '<span class="monospace">(text)</span> – adds a swipe to the last chat message.', true, true); parser.addCommand('abort', abortCallback, [], ' – aborts the slash command batch execution', true, true); @@ -687,6 +687,8 @@ async function generateCallback(args, value) { // Prevent generate recursion $('#send_textarea').val('').trigger('input'); const lock = isTrueBoolean(args?.lock); + const as = args?.as || 'system'; + const quietToLoud = as === 'char'; try { if (lock) { @@ -695,7 +697,7 @@ async function generateCallback(args, value) { setEphemeralStopStrings(resolveVariable(args?.stop)); const name = args?.name; - const result = await generateQuietPrompt(value, false, false, '', name); + const result = await generateQuietPrompt(value, quietToLoud, false, '', name); return result; } finally { if (lock) { From a8388259ab5929ddfad531c570152b2c4a73af82 Mon Sep 17 00:00:00 2001 From: Lumi <kevin+github@lumify.cat> Date: Sat, 30 Mar 2024 19:57:23 +0100 Subject: [PATCH 60/99] Update server.js Print warning if basicAuth username or password fails to parse. In a normal case the user has no way to be informed if the username or password fails to parse. While this might end up being a skill issue on the users side it could help them to troubleshoot the issue. --- server.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server.js b/server.js index 538679dbc..8aa2397a6 100644 --- a/server.js +++ b/server.js @@ -655,6 +655,18 @@ const setupTasks = async function () { if (listen) { console.log('\n0.0.0.0 means SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If you want to limit it only to internal localhost (127.0.0.1), change the setting in config.yaml to "listen: false". Check "access.log" file in the SillyTavern directory if you want to inspect incoming connections.\n'); } + + if (getConfigValue("basicAuthMode", false)) { + const basicAuthUser = getConfigValue("basicAuthUser", null); + if (!basicAuthUser.username || !basicAuthUser.password) { + console.warn( + color.yellow( + "Basic Authentication is set, but username or password is not set or empty!" + ) + ); + } + } + }; /** From 6fe7c1fdaf873f85f8a064cdbb35b1f7e6aa60c9 Mon Sep 17 00:00:00 2001 From: Wolfsblvt <wolfsblvt@gmx.de> Date: Sat, 30 Mar 2024 20:33:08 +0100 Subject: [PATCH 61/99] Fix reprint loop on tag filters - Fix endless loop if a tag was selected - Tag selection is now saved, both 'selected' and 'excluded' (old state is lost though) - Streamlined reprinting even more by refactoring bogus drilldown --- public/scripts/filters.js | 3 +- public/scripts/tags.js | 61 +++++++++++++++++---------------------- 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/public/scripts/filters.js b/public/scripts/filters.js index d880cbf37..92ed6992c 100644 --- a/public/scripts/filters.js +++ b/public/scripts/filters.js @@ -24,6 +24,7 @@ export const FILTER_STATES = { EXCLUDED: { key: 'EXCLUDED', class: 'excluded' }, UNDEFINED: { key: 'UNDEFINED', class: 'undefined' }, }; +export const DEFAULT_FILTER_STATE = FILTER_STATES.UNDEFINED.key; /** * Robust check if one state equals the other. It does not care whether it's the state key or the state value object. @@ -203,7 +204,7 @@ export class FilterHelper { return this.filterDataByState(data, state, isFolder); } - filterDataByState(data, state, filterFunc, { includeFolders } = {}) { + filterDataByState(data, state, filterFunc, { includeFolders = false } = {}) { if (isFilterState(state, FILTER_STATES.SELECTED)) { return data.filter(entity => filterFunc(entity) || (includeFolders && entity.type == 'tag')); } diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 8c38ec864..dddc3c337 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -13,7 +13,7 @@ import { event_types, } from '../script.js'; // eslint-disable-next-line no-unused-vars -import { FILTER_TYPES, FILTER_STATES, isFilterState, FilterHelper } from './filters.js'; +import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js'; import { groupCandidatesFilter, groups, selected_group } from './group-chats.js'; import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, debounce } from './utils.js'; @@ -180,6 +180,7 @@ function isBogusFolderOpen() { /** * Function to be called when a specific tag/folder is chosen to "drill down". + * * @param {*} source The jQuery element clicked when choosing the folder * @param {string} tagId The tag id that is behind the chosen folder * @param {boolean} remove Whether the given tag should be removed (otherwise it is added/chosen) @@ -197,12 +198,9 @@ function chooseBogusFolder(source, tagId, remove = false) { // Instead of manually updating the filter conditions, we just "click" on the filter tag // We search inside which filter block we are located in and use that one const FILTER_SELECTOR = ($(source).closest('#rm_characters_block') ?? $(source).closest('#rm_group_chats_block')).find('.rm_tag_filter'); - if (remove) { - // Click twice to skip over the 'excluded' state - $(FILTER_SELECTOR).find(`.tag[id=${tagId}]`).trigger('click').trigger('click'); - } else { - $(FILTER_SELECTOR).find(`.tag[id=${tagId}]`).trigger('click'); - } + const tagElement = $(FILTER_SELECTOR).find(`.tag[id=${tagId}]`); + + toggleTagThreeState(tagElement, { stateOverride: DEFAULT_FILTER_STATE, simulateClick: true }); } /** @@ -603,8 +601,8 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal tagElement.find('.tag_name').text('').attr('title', tag.name).addClass(tag.icon); } - if (tag.excluded && isGeneralList) { - toggleTagThreeState(tagElement, { stateOverride: FILTER_STATES.EXCLUDED }); + if (selectable && isGeneralList) { + toggleTagThreeState(tagElement, { stateOverride: tag.filterState ?? DEFAULT_FILTER_STATE }); } if (selectable) { @@ -629,34 +627,28 @@ function onTagFilterClick(listElement) { let state = toggleTagThreeState($(this)); - // Manual undefined check required for three-state boolean if (existingTag) { - existingTag.excluded = isFilterState(state, FILTER_STATES.EXCLUDED); - + existingTag.filterState = state; saveSettingsDebounced(); } - // Update bogus folder if applicable - if (isBogusFolder(existingTag)) { - // Update bogus drilldown - if ($(this).hasClass('selected')) { - appendTagToList($('.rm_tag_controls .rm_tag_bogus_drilldown'), existingTag, { removable: true }); - } else { - $(listElement).closest('.rm_tag_controls').find(`.rm_tag_bogus_drilldown .tag[id=${tagId}]`).remove(); - } - } - + // We don't print anything manually, updating the filter will automatically trigger a redraw of all relevant stuff runTagFilters(listElement); - updateTagFilterIndicator(); } function toggleTagThreeState(element, { stateOverride = undefined, simulateClick = false } = {}) { const states = Object.keys(FILTER_STATES); + // Make it clear we're getting indexes and handling the 'not found' case in one place + function getStateIndex(key, fallback) { + const index = states.indexOf(key); + return index !== -1 ? index : states.indexOf(fallback); + } + const overrideKey = states.includes(stateOverride) ? stateOverride : Object.keys(FILTER_STATES).find(key => FILTER_STATES[key] === stateOverride); - const currentStateIndex = states.indexOf(element.attr('data-toggle-state')) ?? states.length - 1; - const targetStateIndex = overrideKey !== undefined ? states.indexOf(overrideKey) : (currentStateIndex + 1) % states.length; + const currentStateIndex = getStateIndex(element.attr('data-toggle-state'), DEFAULT_FILTER_STATE); + const targetStateIndex = overrideKey !== undefined ? getStateIndex(overrideKey, DEFAULT_FILTER_STATE) : (currentStateIndex + 1) % states.length; if (simulateClick) { // Calculate how many clicks are needed to go from the current state to the target state @@ -695,10 +687,8 @@ function runTagFilters(listElement) { } function printTagFilters(type = tag_filter_types.character) { - const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG)); const FILTER_SELECTOR = type === tag_filter_types.character ? CHARACTER_FILTER_SELECTOR : GROUP_FILTER_SELECTOR; $(FILTER_SELECTOR).empty(); - $(FILTER_SELECTOR).siblings('.rm_tag_bogus_drilldown').empty(); // Print all action tags. (Exclude folder if that setting isn't chosen) const actionTags = Object.values(ACTIONABLE_TAGS).filter(tag => power_user.bogus_folders || tag.id != ACTIONABLE_TAGS.FOLDER.id); @@ -708,18 +698,21 @@ function printTagFilters(type = tag_filter_types.character) { printTagList($(FILTER_SELECTOR), { empty: false, tags: inListActionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } }); const characterTagIds = Object.values(tag_map).flat(); - const tagsToDisplay = tags - .filter(x => characterTagIds.includes(x.id)) - .sort(compareTagsForSort); + const tagsToDisplay = tags.filter(x => characterTagIds.includes(x.id)).sort(compareTagsForSort); printTagList($(FILTER_SELECTOR), { empty: false, tags: tagsToDisplay, tagOptions: { selectable: true, isGeneralList: true } }); - runTagFilters(FILTER_SELECTOR); + // Print bogus folder navigation + const bogusDrilldown = $(FILTER_SELECTOR).siblings('.rm_tag_bogus_drilldown'); + bogusDrilldown.empty(); + if (power_user.bogus_folders && bogusDrilldown.length > 0) { + const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG)); + const navigatedTags = filterData.selected.map(x => tags.find(t => t.id == x)).filter(x => isBogusFolder(x)); - // Simulate clicks on all "selected" tags when we reprint, otherwise their filter gets lost. "excluded" is persisted. - for (const tagId of filterData.selected) { - toggleTagThreeState($(`${FILTER_SELECTOR} .tag[id="${tagId}"]`), { stateOverride: FILTER_STATES.SELECTED, simulateClick: true }); + printTagList(bogusDrilldown, { tags: navigatedTags, tagOptions: { removable: true } }); } + runTagFilters(FILTER_SELECTOR); + if (power_user.show_tag_filters) { $('.rm_tag_controls .showTagList').addClass('selected'); $('.rm_tag_controls').find('.tag:not(.actionable)').show(); From 4d98310848a26ede8d03fe7650c90be24b89ef5d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 22:38:09 +0200 Subject: [PATCH 62/99] Limit console log depth again (a little bit) --- server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.js b/server.js index 61aedc313..edecb3d7c 100644 --- a/server.js +++ b/server.js @@ -30,7 +30,7 @@ const fetch = require('node-fetch').default; // Unrestrict console logs display limit util.inspect.defaultOptions.maxArrayLength = null; util.inspect.defaultOptions.maxStringLength = null; -util.inspect.defaultOptions.depth = null; +util.inspect.defaultOptions.depth = 4; // local library imports const basicAuthMiddleware = require('./src/middleware/basicAuth'); From c94460714d56fd6db3ed4214f417ccb0b6801fd3 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 22:42:51 +0200 Subject: [PATCH 63/99] Whitelist to check listen mode via console --- server.js | 2 +- src/middleware/whitelist.js | 52 +++++++++++++++++++++---------------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/server.js b/server.js index edecb3d7c..553997b5f 100644 --- a/server.js +++ b/server.js @@ -123,7 +123,7 @@ app.use(CORS); if (listen && getConfigValue('basicAuthMode', false)) app.use(basicAuthMiddleware); -app.use(whitelistMiddleware); +app.use(whitelistMiddleware(listen)); // CSRF Protection // if (!cliArguments.disableCsrf) { diff --git a/src/middleware/whitelist.js b/src/middleware/whitelist.js index 5d9798680..87d5ac5a5 100644 --- a/src/middleware/whitelist.js +++ b/src/middleware/whitelist.js @@ -8,7 +8,6 @@ const { color, getConfigValue } = require('../util'); const whitelistPath = path.join(process.cwd(), './whitelist.txt'); let whitelist = getConfigValue('whitelist', []); let knownIPs = new Set(); -const listen = getConfigValue('listen', false); const whitelistMode = getConfigValue('whitelistMode', true); if (fs.existsSync(whitelistPath)) { @@ -34,30 +33,37 @@ function getIpFromRequest(req) { return clientIp; } -const whitelistMiddleware = function (req, res, next) { - const clientIp = getIpFromRequest(req); +/** + * Returns a middleware function that checks if the client IP is in the whitelist. + * @param {boolean} listen If listen mode is enabled via config or command line + * @returns {import('express').RequestHandler} The middleware function + */ +function whitelistMiddleware(listen) { + return function (req, res, next) { + const clientIp = getIpFromRequest(req); - if (listen && !knownIPs.has(clientIp)) { - const userAgent = req.headers['user-agent']; - console.log(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`)); - knownIPs.add(clientIp); + if (listen && !knownIPs.has(clientIp)) { + const userAgent = req.headers['user-agent']; + console.log(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`)); + knownIPs.add(clientIp); - // Write access log - const timestamp = new Date().toISOString(); - const log = `${timestamp} ${clientIp} ${userAgent}\n`; - fs.appendFile('access.log', log, (err) => { - if (err) { - console.error('Failed to write access log:', err); - } - }); - } + // Write access log + const timestamp = new Date().toISOString(); + const log = `${timestamp} ${clientIp} ${userAgent}\n`; + fs.appendFile('access.log', log, (err) => { + if (err) { + console.error('Failed to write access log:', err); + } + }); + } - //clientIp = req.connection.remoteAddress.split(':').pop(); - if (whitelistMode === true && !whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x)))) { - console.log(color.red('Forbidden: Connection attempt from ' + clientIp + '. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.\n')); - return res.status(403).send('<b>Forbidden</b>: Connection attempt from <b>' + clientIp + '</b>. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.'); - } - next(); -}; + //clientIp = req.connection.remoteAddress.split(':').pop(); + if (whitelistMode === true && !whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x)))) { + console.log(color.red('Forbidden: Connection attempt from ' + clientIp + '. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.\n')); + return res.status(403).send('<b>Forbidden</b>: Connection attempt from <b>' + clientIp + '</b>. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.'); + } + next(); + }; +} module.exports = whitelistMiddleware; From af6deda64d1e687e711a0b181f231c21e24736a2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 22:46:18 +0200 Subject: [PATCH 64/99] Null safety + reuse variable --- server.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/server.js b/server.js index a57b082cc..af8e692b7 100644 --- a/server.js +++ b/server.js @@ -109,7 +109,8 @@ app.use(responseTime()); const server_port = cliArguments.port ?? process.env.SILLY_TAVERN_PORT ?? getConfigValue('port', DEFAULT_PORT); const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN)) && !cliArguments.ssl; const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); -const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY) +const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); +const basicAuthMode = getConfigValue('basicAuthMode', false); const { DIRECTORIES, UPLOADS_PATH } = require('./src/constants'); @@ -121,7 +122,7 @@ const CORS = cors({ app.use(CORS); -if (listen && getConfigValue('basicAuthMode', false)) app.use(basicAuthMiddleware); +if (listen && basicAuthMode) app.use(basicAuthMiddleware); app.use(whitelistMiddleware(listen)); @@ -516,14 +517,10 @@ const setupTasks = async function () { console.log('\n0.0.0.0 means SillyTavern is listening on all network interfaces (Wi-Fi, LAN, localhost). If you want to limit it only to internal localhost (127.0.0.1), change the setting in config.yaml to "listen: false". Check "access.log" file in the SillyTavern directory if you want to inspect incoming connections.\n'); } - if (getConfigValue("basicAuthMode", false)) { - const basicAuthUser = getConfigValue("basicAuthUser", null); - if (!basicAuthUser.username || !basicAuthUser.password) { - console.warn( - color.yellow( - "Basic Authentication is set, but username or password is not set or empty!" - ) - ); + if (basicAuthMode) { + const basicAuthUser = getConfigValue('basicAuthUser', {}); + if (!basicAuthUser?.username || !basicAuthUser?.password) { + console.warn(color.yellow('Basic Authentication is enabled, but username or password is not set or empty!')); } } @@ -541,7 +538,7 @@ async function loadPlugins() { return cleanupPlugins; } catch { console.log('Plugin loading failed.'); - return () => {}; + return () => { }; } } From 50670c1e6a1f555dfcee5040cff242e336ab0877 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 22:52:57 +0200 Subject: [PATCH 65/99] + more reused config variable --- server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.js b/server.js index af8e692b7..d57f73f72 100644 --- a/server.js +++ b/server.js @@ -542,7 +542,7 @@ async function loadPlugins() { } } -if (listen && !getConfigValue('whitelistMode', true) && !getConfigValue('basicAuthMode', false)) { +if (listen && !getConfigValue('whitelistMode', true) && !basicAuthMode) { if (getConfigValue('securityOverride', false)) { console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.')); } From 71a630ad8500f2d83532c7d6a943f05c2224ec06 Mon Sep 17 00:00:00 2001 From: Wolfsblvt <wolfsblvt@gmx.de> Date: Sat, 30 Mar 2024 22:06:50 +0100 Subject: [PATCH 66/99] Code documentation for tags & bogus state - Add lots of code documentation for tag functions (I'm sorry, I live in object oriented languages...) - Fix bogus folder setting not being respected for some controls --- public/script.js | 43 +++++++++++++- public/scripts/tags.js | 125 +++++++++++++++++++++++++++++++++-------- 2 files changed, 145 insertions(+), 23 deletions(-) diff --git a/public/script.js b/public/script.js index 9b5438791..8ed0323e2 100644 --- a/public/script.js +++ b/public/script.js @@ -1324,7 +1324,7 @@ async function printCharacters(fullRefresh = false) { showNavigator: true, callback: function (data) { $(listId).empty(); - if (isBogusFolderOpen()) { + if (power_user.bogus_folders && isBogusFolderOpen()) { $(listId).append(getBackBlock()); } if (!data.length) { @@ -1368,18 +1368,59 @@ async function printCharacters(fullRefresh = false) { favsToHotswap(); } +/** @typedef {object} Character - A character */ +/** @typedef {object} Group - A group */ + +/** + * @typedef {object} Entity - Object representing a display entity + * @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item + * @property {string|number} id - The id + * @property {string} type - The type of this entity (character, group, tag) + * @property {Entity[]} [entities] - An optional list of entities relevant for this item + * @property {number} [hidden] - An optional number representing how many hidden entities this entity contains + */ + +/** + * Converts the given character to its entity representation + * + * @param {Character} character - The character + * @param {string|number} id - The id of this character + * @returns {Entity} The entity for this character + */ export function characterToEntity(character, id) { return { item: character, id, type: 'character' }; } +/** + * Converts the given group to its entity representation + * + * @param {Group} group - The group + * @returns {Entity} The entity for this group + */ export function groupToEntity(group) { return { item: group, id: group.id, type: 'group' }; } +/** + * Converts the given tag to its entity representation + * + * @param {import('./scripts/tags.js').Tag} tag - The tag + * @returns {Entity} The entity for this tag + */ export function tagToEntity(tag) { return { item: structuredClone(tag), id: tag.id, type: 'tag', entities: [] }; } +/** + * Builds the full list of all entities available + * + * They will be correctly marked and filtered. + * + * @param {object} param0 - Optional parameters + * @param {boolean} [param0.doFilter] - Whether this entity list should already be filtered based on the global filters + * @param {boolean} [param0.doSort] - Whether the entity list should be sorted when returned + * @returns {Entity[]} All entities + */ export function getEntitiesList({ doFilter = false, doSort = true } = {}) { let entities = [ ...characters.map((item, index) => characterToEntity(item, index)), diff --git a/public/scripts/tags.js b/public/scripts/tags.js index dddc3c337..a33b338f0 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -6,7 +6,6 @@ import { menu_type, getCharacters, entitiesFilter, - printCharacters, printCharactersDebounced, buildAvatarList, eventSource, @@ -55,12 +54,12 @@ export const tag_filter_types = { }; const ACTIONABLE_TAGS = { - FAV: { id: 1, sort_order: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: filterByFav, icon: 'fa-solid fa-star', class: 'filterByFavorites' }, - GROUP: { id: 0, sort_order: 2, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' }, - FOLDER: { id: 4, sort_order: 3, name: 'Always show folders', color: 'rgba(120, 120, 120, 0.5)', action: filterByFolder, icon: 'fa-solid fa-folder-plus', class: 'filterByFolder' }, - VIEW: { id: 2, sort_order: 4, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' }, - HINT: { id: 3, sort_order: 5, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' }, - UNFILTER: { id: 5, sort_order: 6, name: 'Clear all filters', action: onClearAllFiltersClick, icon: 'fa-solid fa-filter-circle-xmark', class: 'clearAllFilters' }, + FAV: { id: "1", sort_order: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: filterByFav, icon: 'fa-solid fa-star', class: 'filterByFavorites' }, + GROUP: { id: "0", sort_order: 2, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' }, + FOLDER: { id: "4", sort_order: 3, name: 'Always show folders', color: 'rgba(120, 120, 120, 0.5)', action: filterByFolder, icon: 'fa-solid fa-folder-plus', class: 'filterByFolder' }, + VIEW: { id: "2", sort_order: 4, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' }, + HINT: { id: "3", sort_order: 5, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' }, + UNFILTER: { id: "5", sort_order: 6, name: 'Clear all filters', action: onClearAllFiltersClick, icon: 'fa-solid fa-filter-circle-xmark', class: 'clearAllFilters' }, }; const InListActionable = { @@ -82,8 +81,31 @@ const TAG_FOLDER_TYPES = { }; const TAG_FOLDER_DEFAULT_TYPE = 'NONE'; +/** + * @typedef {object} Tag - Object representing a tag + * @property {string} id - The id of the tag (As a kind of has string. This is used whenever the tag is referenced or linked, as the name might change) + * @property {string} name - The name of the tag + * @property {string} [folder_type] - The bogus folder type of this tag (based on `TAG_FOLDER_TYPES`) + * @property {string} [filter_state] - The saved state of the filter chosen of this tag (based on `FILTER_STATES`) + * @property {number} [sort_order] - A custom integer representing the sort order if tags are sorted + * @property {string} [color] - The background color of the tag + * @property {string} [color2] - The foreground color of the tag + * @property {number} [create_date] - A number representing the date when this tag was created + * + * @property {string} [class] - An optional css class added to the control representing this tag when printed. Used for custom tags in the filters. + * @property {string} [icon] - An optional css class of an icon representing this tag when printed. This will replace the tag name with the icon. Used for custom tags in the filters. + */ +/** + * An list of all tags that are available + * @type {Tag[]} + */ let tags = []; + +/** + * A map representing the key of an entity (character avatar, group id, etc) with a corresponding array of tags this entity has assigned. The array might not exist if no tags were assigned yet. + * @type {Object.<string, string[]?>} + */ let tag_map = {}; /** @@ -136,6 +158,15 @@ function filterByTagState(entities, { globalDisplayFilters = false, subForEntity return entities; } +/** + * Filter a a list of entities based on a given tag, returning all entities that represent "sub entities" + * + * @param {Tag} tag - The to filter the entities for + * @param {object[]} entities - The list of possible entities (tag, group, folder) that should get filtered + * @param {object} param2 - optional parameteres + * @param {boolean} [param2.filterHidden] - Whether hidden entities should be filtered out too + * @returns {object[]} The filtered list of entities that apply to the given tag + */ function filterTagSubEntities(tag, entities, { filterHidden = true } = {}) { const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG)); @@ -160,7 +191,9 @@ function filterTagSubEntities(tag, entities, { filterHidden = true } = {}) { /** * Indicates whether a given tag is defined as a folder. Meaning it's neither undefined nor 'NONE'. - * @returns {boolean} If it's a tag folder + * + * @param {Tag} tag - The tag to check + * @returns {boolean} Whether it's a tag folder */ function isBogusFolder(tag) { return tag?.folder_type !== undefined && tag.folder_type !== TAG_FOLDER_DEFAULT_TYPE; @@ -168,6 +201,7 @@ function isBogusFolder(tag) { /** * Indicates whether a user is currently in a bogus folder. + * * @returns {boolean} If currently viewing a folder */ function isBogusFolderOpen() { @@ -205,21 +239,22 @@ function chooseBogusFolder(source, tagId, remove = false) { /** * Builds the tag block for the specified item. - * @param {Object} item The tag item + * + * @param {Tag} tag The tag item * @param {*} entities The list ob sub items for this tag * @param {*} hidden A count of how many sub items are hidden * @returns The html for the tag block */ -function getTagBlock(item, entities, hidden = 0) { +function getTagBlock(tag, entities, hidden = 0) { let count = entities.length; - const tagFolder = TAG_FOLDER_TYPES[item.folder_type]; + const tagFolder = TAG_FOLDER_TYPES[tag.folder_type]; const template = $('#bogus_folder_template .bogus_folder_select').clone(); template.addClass(tagFolder.class); - template.attr({ 'tagid': item.id, 'id': `BogusFolder${item.id}` }); - template.find('.avatar').css({ 'background-color': item.color, 'color': item.color2 }).attr('title', `[Folder] ${item.name}`); - template.find('.ch_name').text(item.name).attr('title', `[Folder] ${item.name}`); + template.attr({ 'tagid': tag.id, 'id': `BogusFolder${tag.id}` }); + template.find('.avatar').css({ 'background-color': tag.color, 'color': tag.color2 }).attr('title', `[Folder] ${tag.name}`); + template.find('.ch_name').text(tag.name).attr('title', `[Folder] ${tag.name}`); template.find('.bogus_folder_hidden_counter').text(hidden > 0 ? `${hidden} hidden` : ''); template.find('.bogus_folder_counter').text(`${count} ${count != 1 ? 'characters' : 'character'}`); template.find('.bogus_folder_icon').addClass(tagFolder.fa_icon); @@ -275,6 +310,13 @@ function createTagMapFromList(listElement, key) { saveSettingsDebounced(); } +/** + * Gets a list of all tags for a given entity key. + * If you have an entity, you can get it's key easily via `getTagKeyForEntity(entity)`. + * + * @param {string} key - The key for which to get tags via the tag map + * @returns {Tag[]} A list of tags + */ function getTagsList(key) { if (!Array.isArray(tag_map[key])) { tag_map[key] = []; @@ -299,6 +341,9 @@ function getInlineListSelector() { return null; } +/** + * Gets the current tag key based on the currently selected character or group + */ function getTagKey() { if (selected_group && menu_type === 'group_edit') { return selected_group; @@ -442,6 +487,12 @@ function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) { return false; } +/** + * Get a list of existing tags matching a list of provided new tag names + * + * @param {string[]} new_tags - A list of strings representing tag names + * @returns List of existing tags + */ function getExistingTags(new_tags) { let existing_tags = []; for (let tag of new_tags) { @@ -495,11 +546,18 @@ async function importTags(imported_char) { return false; } +/** + * Creates a new tag with default properties and a randomly generated id + * + * @param {string} tagName - name of the tag + * @returns {Tag} + */ function createNewTag(tagName) { const tag = { id: uuidv4(), name: tagName, folder_type: TAG_FOLDER_DEFAULT_TYPE, + filter_state: DEFAULT_FILTER_STATE, sort_order: tags.length, color: '', color2: '', @@ -520,8 +578,8 @@ function createNewTag(tagName) { /** * @typedef {object} PrintTagListOptions - Optional parameters for printing the tag list. - * @property {Array<object>|function(): Array<object>} [tags=undefined] - Optional override of tags that should be printed. Those will not be sorted. If no supplied, tags for the relevant character are printed. Can also be a function that returns the tags. - * @property {object} [addTag=undefined] - Optionally provide a tag that should be manually added to this print. Either to the overriden tag list or the found tags based on the entity/key. Will respect the tag exists check. + * @property {Tag[]|function(): Tag[]} [tags=undefined] - Optional override of tags that should be printed. Those will not be sorted. If no supplied, tags for the relevant character are printed. Can also be a function that returns the tags. + * @property {Tag} [addTag=undefined] - Optionally provide a tag that should be manually added to this print. Either to the overriden tag list or the found tags based on the entity/key. Will respect the tag exists check. * @property {object|number|string} [forEntityOrKey=undefined] - Optional override for the chosen entity, otherwise the currently selected is chosen. Can be an entity with id property (character, group, tag), or directly an id or tag key. * @property {boolean|string} [empty=true] - Whether the list should be initially empty. If a string string is provided, 'always' will always empty the list, otherwise it'll evaluate to a boolean. * @property {function(object): function} [tagActionSelector=undefined] - An optional override for the action property that can be assigned to each tag via tagOptions. @@ -568,10 +626,11 @@ function printTagList(element, { tags = undefined, addTag = undefined, forEntity } /** - * Appends a tag to the list element. - * @param {JQuery<HTMLElement>} listElement List element. - * @param {object} tag Tag object to append. - * @param {TagOptions} [options={}] - Options for tag behavior. + * Appends a tag to the list element + * + * @param {JQuery<HTMLElement>} listElement - List element + * @param {Tag} tag - Tag object to append + * @param {TagOptions} [options={}] - Options for tag behavior * @returns {void} */ function appendTagToList(listElement, tag, { removable = false, selectable = false, action = undefined, isGeneralList = false, skipExistsCheck = false } = {}) { @@ -602,7 +661,7 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal } if (selectable && isGeneralList) { - toggleTagThreeState(tagElement, { stateOverride: tag.filterState ?? DEFAULT_FILTER_STATE }); + toggleTagThreeState(tagElement, { stateOverride: tag.filter_state ?? DEFAULT_FILTER_STATE }); } if (selectable) { @@ -628,7 +687,7 @@ function onTagFilterClick(listElement) { let state = toggleTagThreeState($(this)); if (existingTag) { - existingTag.filterState = state; + existingTag.filter_state = state; saveSettingsDebounced(); } @@ -636,6 +695,15 @@ function onTagFilterClick(listElement) { runTagFilters(listElement); } +/** + * Toggle the filter state of a given tag element + * + * @param {JQuery<HTMLElement>} element - The jquery element representing the tag for which the state should be toggled + * @param {object} param1 - Optional parameters + * @param {string} [param1.stateOverride] - Optional state override to which the state should be toggled to. If not set, the state will move to the next one in the chain. + * @param {boolean} [param1.simulateClick] - Optionally specify that the state should not just be set on the html element, but actually achieved via triggering the "click" on it, which follows up with the general click handlers and reprinting + * @returns {string} The string representing the new state + */ function toggleTagThreeState(element, { stateOverride = undefined, simulateClick = false } = {}) { const states = Object.keys(FILTER_STATES); @@ -900,10 +968,23 @@ function makeTagListDraggable(tagContainer) { }); } +/** + * Sorts the given tags, returning a shallow copy of it + * + * @param {Tag[]} tags - The tags + * @returns {Tag[]} The sorted tags + */ function sortTags(tags) { return tags.slice().sort(compareTagsForSort); } +/** + * Compares two given tags and returns the compare result + * + * @param {Tag} a - First tag + * @param {Tag} b - Second tag + * @returns The compare result + */ function compareTagsForSort(a, b) { if (a.sort_order !== undefined && b.sort_order !== undefined) { return a.sort_order - b.sort_order; From 153f75cf1afd9727383a5f478f16968ae607b9d5 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 23:12:01 +0200 Subject: [PATCH 67/99] Add role selection to Character's Note depth prompt --- public/index.html | 14 ++++++++++++-- public/script.js | 36 +++++++++++++++++++++++++++++++++-- public/scripts/group-chats.js | 6 ++++-- src/endpoints/characters.js | 6 +++++- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/public/index.html b/public/index.html index d91958044..d28775b27 100644 --- a/public/index.html +++ b/public/index.html @@ -4505,7 +4505,7 @@ Character's Note </span> </h4> - <textarea id="depth_prompt_prompt" name="depth_prompt_prompt" class="text_pole" rows="2" maxlength="50000" autocomplete="off" form="form_create" placeholder="(Text to be inserted in-chat @ designated depth)"></textarea> + <textarea id="depth_prompt_prompt" name="depth_prompt_prompt" class="text_pole" rows="5" maxlength="50000" autocomplete="off" form="form_create" placeholder="(Text to be inserted in-chat @ designated depth and role)"></textarea> </div> <div> <h4> @@ -4513,7 +4513,17 @@ @ Depth </span> </h4> - <input id="depth_prompt_depth" name="depth_prompt_depth" class="text_pole widthUnset m-t-0" type="number" min="0" max="999" value="4" form="form_create" /> + <input id="depth_prompt_depth" name="depth_prompt_depth" class="text_pole textarea_compact m-t-0" type="number" min="0" max="999" value="4" form="form_create" /> + <h4> + <span data-i18n="Role"> + Role + </span> + </h4> + <select id="depth_prompt_role" name="depth_prompt_role" form="form_create" class="text_pole textarea_compact m-t-0"> + <option value="system" data-i18n="System">System</option> + <option value="user" data-i18n="User">User</option> + <option value="assistant" data-i18n="Assistant">Assistant</option> + </select> <div class="extension_token_counter"> Tokens: <span data-token-counter="depth_prompt_prompt" data-token-permanent="true">counting...</span> </div> diff --git a/public/script.js b/public/script.js index 5a2be853e..acb63bcf3 100644 --- a/public/script.js +++ b/public/script.js @@ -752,6 +752,7 @@ function getCurrentChatId() { const talkativeness_default = 0.5; export const depth_prompt_depth_default = 4; +export const depth_prompt_role_default = 'system'; const per_page_default = 50; var is_advanced_char_open = false; @@ -778,6 +779,7 @@ let create_save = { alternate_greetings: [], depth_prompt_prompt: '', depth_prompt_depth: depth_prompt_depth_default, + depth_prompt_role: depth_prompt_role_default, extensions: {}, }; @@ -3114,12 +3116,14 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu if (selected_group && Array.isArray(groupDepthPrompts) && groupDepthPrompts.length > 0) { groupDepthPrompts.forEach((value, index) => { - setExtensionPrompt('DEPTH_PROMPT_' + index, value.text, extension_prompt_types.IN_CHAT, value.depth, extension_settings.note.allowWIScan); + const role = getExtensionPromptRoleByName(value.role); + setExtensionPrompt('DEPTH_PROMPT_' + index, value.text, extension_prompt_types.IN_CHAT, value.depth, extension_settings.note.allowWIScan, role); }); } else { const depthPromptText = baseChatReplace(characters[this_chid].data?.extensions?.depth_prompt?.prompt?.trim(), name1, name2) || ''; const depthPromptDepth = characters[this_chid].data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default; - setExtensionPrompt('DEPTH_PROMPT', depthPromptText, extension_prompt_types.IN_CHAT, depthPromptDepth, extension_settings.note.allowWIScan); + const depthPromptRole = getExtensionPromptRoleByName(characters[this_chid].data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default); + setExtensionPrompt('DEPTH_PROMPT', depthPromptText, extension_prompt_types.IN_CHAT, depthPromptDepth, extension_settings.note.allowWIScan, depthPromptRole); } // Parse example messages @@ -6697,6 +6701,7 @@ export function select_selected_character(chid) { $('#scenario_pole').val(characters[chid].scenario); $('#depth_prompt_prompt').val(characters[chid].data?.extensions?.depth_prompt?.prompt ?? ''); $('#depth_prompt_depth').val(characters[chid].data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default); + $('#depth_prompt_role').val(characters[chid].data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default); $('#talkativeness_slider').val(characters[chid].talkativeness || talkativeness_default); $('#mes_example_textarea').val(characters[chid].mes_example); $('#selected_chat_pole').val(characters[chid].chat); @@ -6767,6 +6772,7 @@ function select_rm_create() { $('#scenario_pole').val(create_save.scenario); $('#depth_prompt_prompt').val(create_save.depth_prompt_prompt); $('#depth_prompt_depth').val(create_save.depth_prompt_depth); + $('#depth_prompt_role').val(create_save.depth_prompt_role); $('#mes_example_textarea').val(create_save.mes_example); $('#character_json_data').val(''); $('#avatar_div').css('display', 'flex'); @@ -6810,6 +6816,30 @@ export function setExtensionPrompt(key, value, position, depth, scan = false, ro }; } +/** + * Gets a enum value of the extension prompt role by its name. + * @param {string} roleName The name of the extension prompt role. + * @returns {number} The role id of the extension prompt. + */ +export function getExtensionPromptRoleByName(roleName) { + // If the role is already a valid number, return it + if (typeof roleName === 'number' && Object.values(extension_prompt_roles).includes(roleName)) { + return roleName; + } + + switch (roleName) { + case 'system': + return extension_prompt_roles.SYSTEM; + case 'user': + return extension_prompt_roles.USER; + case 'assistant': + return extension_prompt_roles.ASSISTANT; + } + + // Skill issue? + return extension_prompt_roles.SYSTEM; +} + /** * Removes all char A/N prompt injections from the chat. * To clean up when switching from groups to solo and vice versa. @@ -7425,6 +7455,7 @@ async function createOrEditCharacter(e) { { id: '#scenario_pole', callback: value => create_save.scenario = value }, { id: '#depth_prompt_prompt', callback: value => create_save.depth_prompt_prompt = value }, { id: '#depth_prompt_depth', callback: value => create_save.depth_prompt_depth = value, defaultValue: depth_prompt_depth_default }, + { id: '#depth_prompt_role', callback: value => create_save.depth_prompt_role = value, defaultValue: depth_prompt_role_default }, { id: '#mes_example_textarea', callback: value => create_save.mes_example = value }, { id: '#character_json_data', callback: () => { } }, { id: '#alternate_greetings_template', callback: value => create_save.alternate_greetings = value, defaultValue: [] }, @@ -8794,6 +8825,7 @@ jQuery(async function () { '#talkativeness_slider': function () { create_save.talkativeness = Number($('#talkativeness_slider').val()); }, '#depth_prompt_prompt': function () { create_save.depth_prompt_prompt = String($('#depth_prompt_prompt').val()); }, '#depth_prompt_depth': function () { create_save.depth_prompt_depth = Number($('#depth_prompt_depth').val()); }, + '#depth_prompt_role': function () { create_save.depth_prompt_role = String($('#depth_prompt_role').val()); }, }; Object.keys(elementsToUpdate).forEach(function (id) { diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 27708edc5..3b2f1c121 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -68,6 +68,7 @@ import { depth_prompt_depth_default, loadItemizedPrompts, animation_duration, + depth_prompt_role_default, } from '../script.js'; import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map } from './tags.js'; import { FILTER_TYPES, FilterHelper } from './filters.js'; @@ -284,7 +285,7 @@ export function findGroupMemberId(arg) { * Gets depth prompts for group members. * @param {string} groupId Group ID * @param {number} characterId Current Character ID - * @returns {{depth: number, text: string}[]} Array of depth prompts + * @returns {{depth: number, text: string, role: string}[]} Array of depth prompts */ export function getGroupDepthPrompts(groupId, characterId) { if (!groupId) { @@ -320,9 +321,10 @@ export function getGroupDepthPrompts(groupId, characterId) { const depthPromptText = baseChatReplace(character.data?.extensions?.depth_prompt?.prompt?.trim(), name1, character.name) || ''; const depthPromptDepth = character.data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default; + const depthPromptRole = character.data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default; if (depthPromptText) { - depthPrompts.push({ text: depthPromptText, depth: depthPromptDepth }); + depthPrompts.push({ text: depthPromptText, depth: depthPromptDepth, role: depthPromptRole }); } } diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index 613f88c79..45d2896ac 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -210,7 +210,8 @@ function convertToV2(char) { creator: char.creator, tags: char.tags, depth_prompt_prompt: char.depth_prompt_prompt, - depth_prompt_response: char.depth_prompt_response, + depth_prompt_depth: char.depth_prompt_depth, + depth_prompt_role: char.depth_prompt_role, }); result.chat = char.chat ?? humanizedISO8601DateTime(); @@ -331,9 +332,12 @@ function charaFormatData(data) { // Spec extension: depth prompt const depth_default = 4; + const role_default = 'system'; const depth_value = !isNaN(Number(data.depth_prompt_depth)) ? Number(data.depth_prompt_depth) : depth_default; + const role_value = data.depth_prompt_role ?? role_default; _.set(char, 'data.extensions.depth_prompt.prompt', data.depth_prompt_prompt ?? ''); _.set(char, 'data.extensions.depth_prompt.depth', depth_value); + _.set(char, 'data.extensions.depth_prompt.role', role_value); //_.set(char, 'data.extensions.create_date', humanizedISO8601DateTime()); //_.set(char, 'data.extensions.avatar', 'none'); //_.set(char, 'data.extensions.chat', data.ch_name + ' - ' + humanizedISO8601DateTime()); From 32cde5f13f65a690bd9ecf65dd1ee075983e697b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 23:20:46 +0200 Subject: [PATCH 68/99] Fix tag map cleanup on tag deletion, run lint --- public/scripts/tags.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/public/scripts/tags.js b/public/scripts/tags.js index a33b338f0..06a8fbc66 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -15,7 +15,7 @@ import { import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js'; import { groupCandidatesFilter, groups, selected_group } from './group-chats.js'; -import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, debounce } from './utils.js'; +import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay } from './utils.js'; import { power_user } from './power-user.js'; export { @@ -54,12 +54,12 @@ export const tag_filter_types = { }; const ACTIONABLE_TAGS = { - FAV: { id: "1", sort_order: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: filterByFav, icon: 'fa-solid fa-star', class: 'filterByFavorites' }, - GROUP: { id: "0", sort_order: 2, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' }, - FOLDER: { id: "4", sort_order: 3, name: 'Always show folders', color: 'rgba(120, 120, 120, 0.5)', action: filterByFolder, icon: 'fa-solid fa-folder-plus', class: 'filterByFolder' }, - VIEW: { id: "2", sort_order: 4, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' }, - HINT: { id: "3", sort_order: 5, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' }, - UNFILTER: { id: "5", sort_order: 6, name: 'Clear all filters', action: onClearAllFiltersClick, icon: 'fa-solid fa-filter-circle-xmark', class: 'clearAllFilters' }, + FAV: { id: '1', sort_order: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: filterByFav, icon: 'fa-solid fa-star', class: 'filterByFavorites' }, + GROUP: { id: '0', sort_order: 2, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' }, + FOLDER: { id: '4', sort_order: 3, name: 'Always show folders', color: 'rgba(120, 120, 120, 0.5)', action: filterByFolder, icon: 'fa-solid fa-folder-plus', class: 'filterByFolder' }, + VIEW: { id: '2', sort_order: 4, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' }, + HINT: { id: '3', sort_order: 5, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' }, + UNFILTER: { id: '5', sort_order: 6, name: 'Clear all filters', action: onClearAllFiltersClick, icon: 'fa-solid fa-filter-circle-xmark', class: 'clearAllFilters' }, }; const InListActionable = { @@ -983,7 +983,7 @@ function sortTags(tags) { * * @param {Tag} a - First tag * @param {Tag} b - Second tag - * @returns The compare result + * @returns {number} The compare result */ function compareTagsForSort(a, b) { if (a.sort_order !== undefined && b.sort_order !== undefined) { @@ -1196,7 +1196,7 @@ function onTagDeleteClick() { const id = $(this).closest('.tag_view_item').attr('id'); for (const key of Object.keys(tag_map)) { - tag_map[key] = tag_map[key].filter(x => x.id !== id); + tag_map[key] = tag_map[key].filter(x => x !== id); } const index = tags.findIndex(x => x.id === id); tags.splice(index, 1); From 8c5a81baff8407ce9b5e0274ce1d42c56c9b9511 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 23:23:14 +0200 Subject: [PATCH 69/99] Only transition actionable tag filters --- public/css/tags.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/css/tags.css b/public/css/tags.css index b919b8300..93cc8b284 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -139,11 +139,13 @@ cursor: pointer; opacity: 0.6; filter: brightness(0.8); +} + +.rm_tag_filter .tag.actionable { transition: opacity 200ms; } .rm_tag_filter .tag:hover { - opacity: 1; filter: brightness(1); } From 4d0cef75165178fb28d5b14c91af907321a9a7d0 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 30 Mar 2024 23:57:49 +0200 Subject: [PATCH 70/99] Add gitkeep to themes --- public/themes/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/themes/.gitkeep diff --git a/public/themes/.gitkeep b/public/themes/.gitkeep new file mode 100644 index 000000000..e69de29bb From a96bb4050595ab1e1c9d98cede6fedab09d522eb Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 31 Mar 2024 00:27:12 +0200 Subject: [PATCH 71/99] #1991 Add API key for llama.cpp --- public/index.html | 9 +++++++ public/script.js | 1 + public/scripts/secrets.js | 2 ++ src/additional-headers.js | 55 ++++++++++++++++----------------------- src/endpoints/secrets.js | 1 + 5 files changed, 35 insertions(+), 33 deletions(-) diff --git a/public/index.html b/public/index.html index d28775b27..afd279d6e 100644 --- a/public/index.html +++ b/public/index.html @@ -2151,6 +2151,15 @@ ggerganov/llama.cpp (inference server) </a> </div> + <h4 data-i18n="API key (optional)">API key (optional)</h4> + <div class="flex-container"> + <input id="api_key_llamacpp" name="api_key_llamacpp" class="text_pole flex1 wide100p" maxlength="500" size="35" type="text" autocomplete="off"> + <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_llamacpp"> + </div> + </div> + <div data-for="api_key_llamacpp" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page."> + For privacy reasons, your API key will be hidden after you reload the page. + </div> <div class="flex1"> <h4 data-i18n="API url">API URL</h4> <small data-i18n="Example: 127.0.0.1:8080">Example: http://127.0.0.1:8080</small> diff --git a/public/script.js b/public/script.js index acb63bcf3..61669e028 100644 --- a/public/script.js +++ b/public/script.js @@ -8991,6 +8991,7 @@ jQuery(async function () { { id: 'api_key_dreamgen', secret: SECRET_KEYS.DREAMGEN }, { id: 'api_key_openrouter-tg', secret: SECRET_KEYS.OPENROUTER }, { id: 'api_key_koboldcpp', secret: SECRET_KEYS.KOBOLDCPP }, + { id: 'api_key_llamacpp', secret: SECRET_KEYS.LLAMACPP }, ]; for (const key of keys) { diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index 9ba9dcedd..a6d82e5e7 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -22,6 +22,7 @@ export const SECRET_KEYS = { OOBA: 'api_key_ooba', NOMICAI: 'api_key_nomicai', KOBOLDCPP: 'api_key_koboldcpp', + LLAMACPP: 'api_key_llamacpp', }; const INPUT_MAP = { @@ -45,6 +46,7 @@ const INPUT_MAP = { [SECRET_KEYS.DREAMGEN]: '#api_key_dreamgen', [SECRET_KEYS.NOMICAI]: '#api_key_nomicai', [SECRET_KEYS.KOBOLDCPP]: '#api_key_koboldcpp', + [SECRET_KEYS.LLAMACPP]: '#api_key_llamacpp', }; async function clearSecret() { diff --git a/src/additional-headers.js b/src/additional-headers.js index e69872bf3..4ac30d25c 100644 --- a/src/additional-headers.js +++ b/src/additional-headers.js @@ -60,6 +60,14 @@ function getTabbyHeaders() { }) : {}; } +function getLlamaCppHeaders() { + const apiKey = readSecret(SECRET_KEYS.LLAMACPP); + + return apiKey ? ({ + 'Authorization': `Bearer ${apiKey}`, + }) : {}; +} + function getOobaHeaders() { const apiKey = readSecret(SECRET_KEYS.OOBA); @@ -93,40 +101,21 @@ function getOverrideHeaders(urlHost) { * @param {string|null} server API server for new request */ function setAdditionalHeaders(request, args, server) { - let headers; + const headerGetters = { + [TEXTGEN_TYPES.MANCER]: getMancerHeaders, + [TEXTGEN_TYPES.APHRODITE]: getAphroditeHeaders, + [TEXTGEN_TYPES.TABBY]: getTabbyHeaders, + [TEXTGEN_TYPES.TOGETHERAI]: getTogetherAIHeaders, + [TEXTGEN_TYPES.OOBA]: getOobaHeaders, + [TEXTGEN_TYPES.INFERMATICAI]: getInfermaticAIHeaders, + [TEXTGEN_TYPES.DREAMGEN]: getDreamGenHeaders, + [TEXTGEN_TYPES.OPENROUTER]: getOpenRouterHeaders, + [TEXTGEN_TYPES.KOBOLDCPP]: getKoboldCppHeaders, + [TEXTGEN_TYPES.LLAMACPP]: getLlamaCppHeaders, + }; - switch (request.body.api_type) { - case TEXTGEN_TYPES.MANCER: - headers = getMancerHeaders(); - break; - case TEXTGEN_TYPES.APHRODITE: - headers = getAphroditeHeaders(); - break; - case TEXTGEN_TYPES.TABBY: - headers = getTabbyHeaders(); - break; - case TEXTGEN_TYPES.TOGETHERAI: - headers = getTogetherAIHeaders(); - break; - case TEXTGEN_TYPES.OOBA: - headers = getOobaHeaders(); - break; - case TEXTGEN_TYPES.INFERMATICAI: - headers = getInfermaticAIHeaders(); - break; - case TEXTGEN_TYPES.DREAMGEN: - headers = getDreamGenHeaders(); - break; - case TEXTGEN_TYPES.OPENROUTER: - headers = getOpenRouterHeaders(); - break; - case TEXTGEN_TYPES.KOBOLDCPP: - headers = getKoboldCppHeaders(); - break; - default: - headers = {}; - break; - } + const getHeaders = headerGetters[request.body.api_type]; + const headers = getHeaders ? getHeaders() : {}; if (typeof server === 'string' && server.length > 0) { try { diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index 10ba9e556..55c5df008 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -34,6 +34,7 @@ const SECRET_KEYS = { DREAMGEN: 'api_key_dreamgen', NOMICAI: 'api_key_nomicai', KOBOLDCPP: 'api_key_koboldcpp', + LLAMACPP: 'api_key_llamacpp', }; // These are the keys that are safe to expose, even if allowKeysExposure is false From c58fcfd4da6902541fccab5457c824677d6c9056 Mon Sep 17 00:00:00 2001 From: Wolfsblvt <wolfsblvt@gmx.de> Date: Sun, 31 Mar 2024 00:21:33 +0100 Subject: [PATCH 72/99] Fix actionable filters and bogus selection again - Fix actionable filters and their toggle state - Make bogus folders clickable again - Even more code documentation --- public/scripts/filters.js | 24 ++++++++++++++++-------- public/scripts/tags.js | 33 +++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/public/scripts/filters.js b/public/scripts/filters.js index 92ed6992c..2743ccdba 100644 --- a/public/scripts/filters.js +++ b/public/scripts/filters.js @@ -2,8 +2,8 @@ import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchPersonas, fuzzySea import { tag_map } from './tags.js'; /** - * The filter types. - * @type {Object.<string, string>} + * The filter types + * @type {{ SEARCH: string, TAG: string, FOLDER: string, FAV: string, GROUP: string, WORLD_INFO_SEARCH: string, PERSONA_SEARCH: string, [key: string]: string }} */ export const FILTER_TYPES = { SEARCH: 'search', @@ -16,26 +16,34 @@ export const FILTER_TYPES = { }; /** - * The filter states. - * @type {Object.<string, Object>} + * @typedef FilterState One of the filter states + * @property {string} key - The key of the state + * @property {string} class - The css class for this state + */ + +/** + * The filter states + * @type {{ SELECTED: FilterState, EXCLUDED: FilterState, UNDEFINED: FilterState, [key: string]: FilterState }} */ export const FILTER_STATES = { SELECTED: { key: 'SELECTED', class: 'selected' }, EXCLUDED: { key: 'EXCLUDED', class: 'excluded' }, UNDEFINED: { key: 'UNDEFINED', class: 'undefined' }, }; +/** @type {string} the default filter state of `FILTER_STATES` */ export const DEFAULT_FILTER_STATE = FILTER_STATES.UNDEFINED.key; /** * Robust check if one state equals the other. It does not care whether it's the state key or the state value object. - * @param {Object} a First state - * @param {Object} b Second state + * @param {FilterState|string} a First state + * @param {FilterState|string} b Second state + * @returns {boolean} */ export function isFilterState(a, b) { const states = Object.keys(FILTER_STATES); - const aKey = states.includes(a) ? a : states.find(key => FILTER_STATES[key] === a); - const bKey = states.includes(b) ? b : states.find(key => FILTER_STATES[key] === b); + const aKey = typeof a == 'string' && states.includes(a) ? a : states.find(key => FILTER_STATES[key] === a); + const bKey = typeof b == 'string' && states.includes(b) ? b : states.find(key => FILTER_STATES[key] === b); return aKey === bKey; } diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 06a8fbc66..ac2e722da 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -53,6 +53,10 @@ export const tag_filter_types = { group_member: 1, }; +/** + * @type {{ FAV: Tag, GROUP: Tag, FOLDER: Tag, VIEW: Tag, HINT: Tag, UNFILTER: Tag }} + * A collection of global actional tags for the filter panel + * */ const ACTIONABLE_TAGS = { FAV: { id: '1', sort_order: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: filterByFav, icon: 'fa-solid fa-star', class: 'filterByFavorites' }, GROUP: { id: '0', sort_order: 2, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' }, @@ -62,9 +66,11 @@ const ACTIONABLE_TAGS = { UNFILTER: { id: '5', sort_order: 6, name: 'Clear all filters', action: onClearAllFiltersClick, icon: 'fa-solid fa-filter-circle-xmark', class: 'clearAllFilters' }, }; +/** @type {{[key: string]: Tag}} An optional list of actionables that can be utilized by extensions */ const InListActionable = { }; +/** @type {Tag[]} A list of default tags */ const DEFAULT_TAGS = [ { id: uuidv4(), name: 'Plain Text', create_date: Date.now() }, { id: uuidv4(), name: 'OpenAI', create_date: Date.now() }, @@ -74,6 +80,20 @@ const DEFAULT_TAGS = [ { id: uuidv4(), name: 'AliChat', create_date: Date.now() }, ]; +/** + * @typedef FolderType Bogus folder type + * @property {string} icon - The icon as a string representation / character + * @property {string} class - The class to apply to the folder type element + * @property {string} [fa_icon] - Optional font-awesome icon class representing the folder type element + * @property {string} [tooltip] - Optional tooltip for the folder type element + * @property {string} [color] - Optional color for the folder type element + * @property {string} [size] - A string representation of the size that the folder type element should be + */ + +/** + * @type {{ OPEN: FolderType, CLOSED: FolderType, NONE: FolderType, [key: string]: FolderType }} + * The list of all possible tag folder types + */ const TAG_FOLDER_TYPES = { OPEN: { icon: '✔', class: 'folder_open', fa_icon: 'fa-folder-open', tooltip: 'Open Folder (Show all characters even if not selected)', color: 'green', size: '1' }, CLOSED: { icon: '👁', class: 'folder_closed', fa_icon: 'fa-eye-slash', tooltip: 'Closed Folder (Hide all characters unless selected)', color: 'lightgoldenrodyellow', size: '0.7' }, @@ -92,6 +112,7 @@ const TAG_FOLDER_DEFAULT_TYPE = 'NONE'; * @property {string} [color2] - The foreground color of the tag * @property {number} [create_date] - A number representing the date when this tag was created * + * @property {function} [action] - An optional function that gets executed when this tag is an actionable tag and is clicked on. * @property {string} [class] - An optional css class added to the control representing this tag when printed. Used for custom tags in the filters. * @property {string} [icon] - An optional css class of an icon representing this tag when printed. This will replace the tag name with the icon. Used for custom tags in the filters. */ @@ -234,7 +255,7 @@ function chooseBogusFolder(source, tagId, remove = false) { const FILTER_SELECTOR = ($(source).closest('#rm_characters_block') ?? $(source).closest('#rm_group_chats_block')).find('.rm_tag_filter'); const tagElement = $(FILTER_SELECTOR).find(`.tag[id=${tagId}]`); - toggleTagThreeState(tagElement, { stateOverride: DEFAULT_FILTER_STATE, simulateClick: true }); + toggleTagThreeState(tagElement, { stateOverride: !remove ? FILTER_STATES.SELECTED : DEFAULT_FILTER_STATE, simulateClick: true }); } /** @@ -271,6 +292,7 @@ function getTagBlock(tag, entities, hidden = 0) { */ function filterByFav(filterHelper) { const state = toggleTagThreeState($(this)); + ACTIONABLE_TAGS.FAV.filter_state = state; filterHelper.setFilterData(FILTER_TYPES.FAV, state); } @@ -280,6 +302,7 @@ function filterByFav(filterHelper) { */ function filterByGroups(filterHelper) { const state = toggleTagThreeState($(this)); + ACTIONABLE_TAGS.GROUP.filter_state = state; filterHelper.setFilterData(FILTER_TYPES.GROUP, state); } @@ -289,6 +312,7 @@ function filterByGroups(filterHelper) { */ function filterByFolder(filterHelper) { const state = toggleTagThreeState($(this)); + ACTIONABLE_TAGS.FOLDER.filter_state = state; filterHelper.setFilterData(FILTER_TYPES.FOLDER, state); } @@ -660,7 +684,8 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal tagElement.find('.tag_name').text('').attr('title', tag.name).addClass(tag.icon); } - if (selectable && isGeneralList) { + // If this is a tag for a general list and its either selectable or actionable, lets mark its current state + if ((selectable || action) && isGeneralList) { toggleTagThreeState(tagElement, { stateOverride: tag.filter_state ?? DEFAULT_FILTER_STATE }); } @@ -700,7 +725,7 @@ function onTagFilterClick(listElement) { * * @param {JQuery<HTMLElement>} element - The jquery element representing the tag for which the state should be toggled * @param {object} param1 - Optional parameters - * @param {string} [param1.stateOverride] - Optional state override to which the state should be toggled to. If not set, the state will move to the next one in the chain. + * @param {import('./filters.js').FilterState|string} [param1.stateOverride] - Optional state override to which the state should be toggled to. If not set, the state will move to the next one in the chain. * @param {boolean} [param1.simulateClick] - Optionally specify that the state should not just be set on the html element, but actually achieved via triggering the "click" on it, which follows up with the general click handlers and reprinting * @returns {string} The string representing the new state */ @@ -713,7 +738,7 @@ function toggleTagThreeState(element, { stateOverride = undefined, simulateClick return index !== -1 ? index : states.indexOf(fallback); } - const overrideKey = states.includes(stateOverride) ? stateOverride : Object.keys(FILTER_STATES).find(key => FILTER_STATES[key] === stateOverride); + const overrideKey = typeof stateOverride == 'string' && states.includes(stateOverride) ? stateOverride : Object.keys(FILTER_STATES).find(key => FILTER_STATES[key] === stateOverride); const currentStateIndex = getStateIndex(element.attr('data-toggle-state'), DEFAULT_FILTER_STATE); const targetStateIndex = overrideKey !== undefined ? getStateIndex(overrideKey, DEFAULT_FILTER_STATE) : (currentStateIndex + 1) % states.length; From 0a71d09fe16e50bc3b70537095beb7d49a31fabd Mon Sep 17 00:00:00 2001 From: Hirose <86906598+HiroseKoichi@users.noreply.github.com> Date: Sat, 30 Mar 2024 17:37:06 -0500 Subject: [PATCH 73/99] Fix default instruct format in settings.json Updated the default Alpaca instruct in settings.json to match the latest PR. --- default/settings.json | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/default/settings.json b/default/settings.json index 3c8faadbf..dbd731c45 100644 --- a/default/settings.json +++ b/default/settings.json @@ -155,17 +155,23 @@ "system_prompt": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\nWrite {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n", "input_sequence": "### Instruction:", "output_sequence": "### Response:", - "first_output_sequence": "", "last_output_sequence": "", - "system_sequence_prefix": "", - "system_sequence_suffix": "", + "system_sequence": "### Input:", "stop_sequence": "", - "separator_sequence": "", "wrap": true, "macro": true, "names": false, "names_force_groups": true, - "activation_regex": "" + "activation_regex": "", + "system_sequence_prefix": "", + "system_sequence_suffix": "", + "first_output_sequence": "", + "skip_examples": false, + "output_suffix": "\n\n", + "input_suffix": "\n\n", + "system_suffix": "\n\n", + "user_alignment_message": "", + "system_same_as_user": false }, "default_context": "Default", "context": { From b0fb50aef6d6be42bc9b66e0db0e913bc140992d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 31 Mar 2024 10:30:06 +0300 Subject: [PATCH 74/99] Fix example dialogue separator being skipped in instruct mode --- public/scripts/instruct-mode.js | 40 +++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index de8b93c5b..9c75977e1 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -365,8 +365,10 @@ export function formatInstructModeSystemPrompt(systemPrompt) { * @returns {string[]} Formatted example messages string. */ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { + const blockHeading = power_user.context.example_separator ? power_user.context.example_separator + '\n' : ''; + if (power_user.instruct.skip_examples) { - return mesExamplesArray.map(x => x.replace(/<START>\n/i, '')); + return mesExamplesArray.map(x => x.replace(/<START>\n/i, blockHeading)); } const includeNames = power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups); @@ -387,28 +389,32 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { } const separator = power_user.instruct.wrap ? '\n' : ''; - const parsedExamples = []; + const formattedExamples = []; for (const item of mesExamplesArray) { const cleanedItem = item.replace(/<START>/i, '{Example Dialogue:}').replace(/\r/gm, ''); const blockExamples = parseExampleIntoIndividual(cleanedItem); - parsedExamples.push(...blockExamples); + + if (blockExamples.length === 0) { + continue; + } + + if (blockHeading) { + formattedExamples.push(power_user.blockHeading); + } + + for (const example of blockExamples) { + const prefix = example.name == 'example_user' ? inputPrefix : outputPrefix; + const suffix = example.name == 'example_user' ? inputSuffix : outputSuffix; + const name = example.name == 'example_user' ? name1 : name2; + const messageContent = includeNames ? `${name}: ${example.content}` : example.content; + const formattedMessage = [prefix, messageContent + suffix].filter(x => x).join(separator); + formattedExamples.push(formattedMessage); + } } - // Not something we can parse, return as is - if (!Array.isArray(parsedExamples) || parsedExamples.length === 0) { - return mesExamplesArray; - } - - const formattedExamples = []; - - for (const example of parsedExamples) { - const prefix = example.name == 'example_user' ? inputPrefix : outputPrefix; - const suffix = example.name == 'example_user' ? inputSuffix : outputSuffix; - const name = example.name == 'example_user' ? name1 : name2; - const messageContent = includeNames ? `${name}: ${example.content}` : example.content; - const formattedMessage = [prefix, messageContent + suffix].filter(x => x).join(separator); - formattedExamples.push(formattedMessage); + if (formattedExamples.length === 0) { + return mesExamplesArray.map(x => x.replace(/<START>\n/i, blockHeading)); } return formattedExamples; From e99baac9c041098b79b632a7d2aa7cc0792ad89c Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 31 Mar 2024 10:48:23 +0300 Subject: [PATCH 75/99] Adjust drilldown arrow style This thing was huge --- public/css/tags.css | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/public/css/tags.css b/public/css/tags.css index 93cc8b284..9a3e02064 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -232,18 +232,16 @@ .rm_tag_bogus_drilldown .tag:not(:first-child) { position: relative; - margin-left: calc(var(--mainFontSize) * 2); + margin-left: 1em; } .rm_tag_bogus_drilldown .tag:not(:first-child)::before { + font-family: 'Font Awesome 6 Free'; + content: "\f054"; position: absolute; - left: calc(var(--mainFontSize) * -2); - top: -1px; - content: "\21E8"; - font-size: calc(var(--mainFontSize) * 2); + left: -1em; + top: auto; color: var(--SmartThemeBodyColor); - line-height: calc(var(--mainFontSize) * 1.3); - text-align: center; text-shadow: 1px 1px 0px black, -1px -1px 0px black, -1px 1px 0px black, From b2f42f1b9f17a1a28533e719116893a5d3f15cbb Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 31 Mar 2024 10:54:23 +0300 Subject: [PATCH 76/99] Close context menu immediately when clicked on mass tag --- public/scripts/BulkEditOverlay.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/scripts/BulkEditOverlay.js b/public/scripts/BulkEditOverlay.js index f3df8156d..fccf12da3 100644 --- a/public/scripts/BulkEditOverlay.js +++ b/public/scripts/BulkEditOverlay.js @@ -853,6 +853,7 @@ class BulkEditOverlay { */ handleContextMenuTag = () => { CharacterContextMenu.tag(this.selectedCharacters); + this.browseState(); }; addStateChangeCallback = callback => this.stateChangeCallbacks.push(callback); From 2e28f242519030e3175dc6f266116499ec5db22a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 31 Mar 2024 14:27:43 +0300 Subject: [PATCH 77/99] Fix summary controls disappearing when switching Chat Completion sources --- public/scripts/extensions/memory/index.js | 92 +------------------ .../scripts/extensions/memory/settings.html | 85 +++++++++++++++++ 2 files changed, 89 insertions(+), 88 deletions(-) create mode 100644 public/scripts/extensions/memory/settings.html diff --git a/public/scripts/extensions/memory/index.js b/public/scripts/extensions/memory/index.js index 19003052e..da0ae00e7 100644 --- a/public/scripts/extensions/memory/index.js +++ b/public/scripts/extensions/memory/index.js @@ -1,5 +1,5 @@ import { getStringHash, debounce, waitUntilCondition, extractAllWords } from '../../utils.js'; -import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules } from '../../extensions.js'; +import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules, renderExtensionTemplate } from '../../extensions.js'; import { animation_duration, eventSource, event_types, extension_prompt_roles, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from '../../../script.js'; import { is_group_generating, selected_group } from '../../group-chats.js'; import { registerSlashCommand } from '../../slash-commands.js'; @@ -98,8 +98,8 @@ function onSummarySourceChange(event) { } function switchSourceControls(value) { - $('#memory_settings [data-source]').each((_, element) => { - const source = $(element).data('source'); + $('#memory_settings [data-summary-source]').each((_, element) => { + const source = $(element).data('summary-source'); $(element).toggle(source === value); }); } @@ -581,91 +581,7 @@ function setupListeners() { jQuery(function () { function addExtensionControls() { - const settingsHtml = ` - <div id="memory_settings"> - <div class="inline-drawer"> - <div class="inline-drawer-toggle inline-drawer-header"> - <div class="flex-container alignitemscenter margin0"><b>Summarize</b><i id="summaryExtensionPopoutButton" class="fa-solid fa-window-restore menu_button margin0"></i></div> - <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div> - </div> - <div class="inline-drawer-content"> - <div id="summaryExtensionDrawerContents"> - <label for="summary_source">Summarize with:</label> - <select id="summary_source"> - <option value="main">Main API</option> - <option value="extras">Extras API</option> - </select><br> - - <div class="flex-container justifyspacebetween alignitemscenter"> - <span class="flex1">Current summary:</span> - <div id="memory_restore" class="menu_button flex1 margin0"><span>Restore Previous</span></div> - </div> - - <textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea> - <div class="memory_contents_controls"> - <div id="memory_force_summarize" data-source="main" class="menu_button menu_button_icon" title="Trigger a summary update right now." data-i18n="Trigger a summary update right now."> - <i class="fa-solid fa-database"></i> - <span>Summarize now</span> - </div> - <label for="memory_frozen" title="Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API)." data-i18n="[title]Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API)."><input id="memory_frozen" type="checkbox" />Pause</label> - <label for="memory_skipWIAN" title="Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN." data-i18n="[title]Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN."><input id="memory_skipWIAN" type="checkbox" />No WI/AN</label> - </div> - <div class="memory_contents_controls"> - <div id="summarySettingsBlockToggle" class="menu_button menu_button_icon" title="Edit summarization prompt, insertion position, etc."> - <i class="fa-solid fa-cog"></i> - <span>Summary Settings</span> - </div> - </div> - <div id="summarySettingsBlock" style="display:none;"> - <div class="memory_template"> - <label for="memory_template">Insertion Template</label> - <textarea id="memory_template" class="text_pole textarea_compact" rows="2" placeholder="{{summary}} will resolve to the current summary contents."></textarea> - </div> - <label for="memory_position">Injection Position</label> - <div class="radio_group"> - <label> - <input type="radio" name="memory_position" value="2" /> - Before Main Prompt / Story String - </label> - <label> - <input type="radio" name="memory_position" value="0" /> - After Main Prompt / Story String - </label> - <label class="flex-container alignItemsCenter" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat."> - <input type="radio" name="memory_position" value="1" /> - In-chat @ Depth <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="999" /> - as - <select id="memory_role" class="text_pole widthNatural"> - <option value="0">System</option> - <option value="1">User</option> - <option value="2">Assistant</option> - </select> - </label> - </div> - <div data-source="main" class="memory_contents_controls"> - </div> - <div data-source="main"> - <label for="memory_prompt" class="title_restorable"> - Summary Prompt - - </label> - <textarea id="memory_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation. {{words}} will resolve to the 'Number of words' parameter."></textarea> - <label for="memory_prompt_words">Summary length (<span id="memory_prompt_words_value"></span> words)</label> - <input id="memory_prompt_words" type="range" value="${defaultSettings.promptWords}" min="${defaultSettings.promptMinWords}" max="${defaultSettings.promptMaxWords}" step="${defaultSettings.promptWordsStep}" /> - <label for="memory_prompt_interval">Update every <span id="memory_prompt_interval_value"></span> messages</label> - <small>0 = disable</small> - <input id="memory_prompt_interval" type="range" value="${defaultSettings.promptInterval}" min="${defaultSettings.promptMinInterval}" max="${defaultSettings.promptMaxInterval}" step="${defaultSettings.promptIntervalStep}" /> - <label for="memory_prompt_words_force">Update every <span id="memory_prompt_words_force_value"></span> words</label> - <small>0 = disable</small> - <input id="memory_prompt_words_force" type="range" value="${defaultSettings.promptForceWords}" min="${defaultSettings.promptMinForceWords}" max="${defaultSettings.promptMaxForceWords}" step="${defaultSettings.promptForceWordsStep}" /> - <small>If both sliders are non-zero, then both will trigger summary updates a their respective intervals.</small> - </div> - </div> - </div> - </div> - </div> - </div> - `; + const settingsHtml = renderExtensionTemplate('memory', 'settings', { defaultSettings }); $('#extensions_settings2').append(settingsHtml); setupListeners(); $('#summaryExtensionPopoutButton').off('click').on('click', function (e) { diff --git a/public/scripts/extensions/memory/settings.html b/public/scripts/extensions/memory/settings.html new file mode 100644 index 000000000..b5b54142c --- /dev/null +++ b/public/scripts/extensions/memory/settings.html @@ -0,0 +1,85 @@ +<div id="memory_settings"> + <div class="inline-drawer"> + <div class="inline-drawer-toggle inline-drawer-header"> + <div class="flex-container alignitemscenter margin0"> + <b>Summarize</b> + <i id="summaryExtensionPopoutButton" class="fa-solid fa-window-restore menu_button margin0"></i> + </div> + <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div> + </div> + <div class="inline-drawer-content"> + <div id="summaryExtensionDrawerContents"> + <label for="summary_source">Summarize with:</label> + <select id="summary_source"> + <option value="main">Main API</option> + <option value="extras">Extras API</option> + </select><br> + + <div class="flex-container justifyspacebetween alignitemscenter"> + <span class="flex1">Current summary:</span> + <div id="memory_restore" class="menu_button flex1 margin0"><span>Restore Previous</span></div> + </div> + + <textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea> + <div class="memory_contents_controls"> + <div id="memory_force_summarize" data-summary-source="main" class="menu_button menu_button_icon" title="Trigger a summary update right now." data-i18n="Trigger a summary update right now."> + <i class="fa-solid fa-database"></i> + <span>Summarize now</span> + </div> + <label for="memory_frozen" title="Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API)." data-i18n="[title]Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API)."><input id="memory_frozen" type="checkbox" />Pause</label> + <label for="memory_skipWIAN" title="Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN." data-i18n="[title]Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN."><input id="memory_skipWIAN" type="checkbox" />No WI/AN</label> + </div> + <div class="memory_contents_controls"> + <div id="summarySettingsBlockToggle" class="menu_button menu_button_icon" title="Edit summarization prompt, insertion position, etc."> + <i class="fa-solid fa-cog"></i> + <span>Summary Settings</span> + </div> + </div> + <div id="summarySettingsBlock" style="display:none;"> + <div class="memory_template"> + <label for="memory_template">Insertion Template</label> + <textarea id="memory_template" class="text_pole textarea_compact" rows="2" placeholder="{{summary{{ will resolve to the current summary contents."></textarea> + </div> + <label for="memory_position">Injection Position</label> + <div class="radio_group"> + <label> + <input type="radio" name="memory_position" value="2" /> + Before Main Prompt / Story String + </label> + <label> + <input type="radio" name="memory_position" value="0" /> + After Main Prompt / Story String + </label> + <label class="flex-container alignItemsCenter" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat."> + <input type="radio" name="memory_position" value="1" /> + In-chat @ Depth <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="999" /> + as + <select id="memory_role" class="text_pole widthNatural"> + <option value="0">System</option> + <option value="1">User</option> + <option value="2">Assistant</option> + </select> + </label> + </div> + <div data-summary-source="main" class="memory_contents_controls"> + </div> + <div data-summary-source="main"> + <label for="memory_prompt" class="title_restorable"> + Summary Prompt + </label> + <textarea id="memory_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation. {{words}} will resolve to the 'Number of words' parameter."></textarea> + <label for="memory_prompt_words">Summary length (<span id="memory_prompt_words_value"></span> words)</label> + <input id="memory_prompt_words" type="range" value="{{defaultSettings.promptWords}}" min="{{defaultSettings.promptMinWords}}" max="{{defaultSettings.promptMaxWords}}" step="{{defaultSettings.promptWordsStep}}" /> + <label for="memory_prompt_interval">Update every <span id="memory_prompt_interval_value"></span> messages</label> + <small>0 = disable</small> + <input id="memory_prompt_interval" type="range" value="{{defaultSettings.promptInterval}}" min="{{defaultSettings.promptMinInterval}}" max="{{defaultSettings.promptMaxInterval}}" step="{{defaultSettings.promptIntervalStep}}" /> + <label for="memory_prompt_words_force">Update every <span id="memory_prompt_words_force_value"></span> words</label> + <small>0 = disable</small> + <input id="memory_prompt_words_force" type="range" value="{{defaultSettings.promptForceWords}}" min="{{defaultSettings.promptMinForceWords}}" max="{{defaultSettings.promptMaxForceWords}}" step="{{defaultSettings.promptForceWordsStep}}" /> + <small>If both sliders are non-zero, then both will trigger summary updates a their respective intervals.</small> + </div> + </div> + </div> + </div> + </div> +</div> From ad4269f4764a856879d2206bf3e499f33bbbdc58 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 31 Mar 2024 19:42:12 +0300 Subject: [PATCH 78/99] Add system prompt to /genraw command --- public/script.js | 17 ++++++++++++++--- public/scripts/slash-commands.js | 5 +++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/public/script.js b/public/script.js index f32538823..99fae36f4 100644 --- a/public/script.js +++ b/public/script.js @@ -2911,17 +2911,24 @@ class StreamingProcessor { * @param {string} api API to use. Main API is used if not specified. * @param {boolean} instructOverride true to override instruct mode, false to use the default value * @param {boolean} quietToLoud true to generate a message in system mode, false to generate a message in character mode + * @param {string} [systemPrompt] System prompt to use. Only Instruct mode or OpenAI. * @returns {Promise<string>} Generated message */ -export async function generateRaw(prompt, api, instructOverride, quietToLoud) { +export async function generateRaw(prompt, api, instructOverride, quietToLoud, systemPrompt) { if (!api) { api = main_api; } const abortController = new AbortController(); - const isInstruct = power_user.instruct.enabled && main_api !== 'openai' && main_api !== 'novel' && !instructOverride; + const isInstruct = power_user.instruct.enabled && api !== 'openai' && api !== 'novel' && !instructOverride; const isQuiet = true; + if (systemPrompt) { + systemPrompt = substituteParams(systemPrompt); + systemPrompt = isInstruct ? formatInstructModeSystemPrompt(systemPrompt) : systemPrompt; + prompt = api === 'openai' ? prompt : `${systemPrompt}\n${prompt}`; + } + prompt = substituteParams(prompt); prompt = api == 'novel' ? adjustNovelInstructionPrompt(prompt) : prompt; prompt = isInstruct ? formatInstructModeChat(name1, prompt, false, true, '', name1, name2, false) : prompt; @@ -2948,8 +2955,12 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud) { case 'textgenerationwebui': generateData = getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet'); break; - case 'openai': + case 'openai': { generateData = [{ role: 'user', content: prompt.trim() }]; + if (systemPrompt) { + generateData.unshift({ role: 'system', content: systemPrompt.trim() }); + } + } break; } let data = {}; diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index fb745364d..f07f66d24 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -231,7 +231,7 @@ parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '<span class="m parser.addCommand('echo', echoCallback, [], '<span class="monospace">(title=string severity=info/warning/error/success [text])</span> – echoes the text to toast message. Useful for pipes debugging.', true, true); //parser.addCommand('#', (_, value) => '', [], ' – a comment, does nothing, e.g. <tt>/# the next three commands switch variables a and b</tt>', true, true); parser.addCommand('gen', generateCallback, [], '<span class="monospace">(lock=on/off name="System" [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating and allowing to configure the in-prompt name for instruct mode (default = "System"). "as" argument controls the role of the output prompt: system (default) or char.', true, true); -parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(lock=on/off instruct=on/off stop=[] as=system/char [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. <tt>/genraw instruct=off Why is the sky blue?</tt>. Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. <tt>/genraw stop=["\\n"] Say hi</tt>. "as" argument controls the role of the output prompt: system (default) or char.', true, true); +parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(lock=on/off instruct=on/off stop=[] as=system/char system="system prompt" [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. <tt>/genraw instruct=off Why is the sky blue?</tt>. Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. <tt>/genraw stop=["\\n"] Say hi</tt>. "as" argument controls the role of the output prompt: system (default) or char. "system" argument adds an (optional) system prompt at the start.', true, true); parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '<span class="monospace">(text)</span> – adds a swipe to the last chat message.', true, true); parser.addCommand('abort', abortCallback, [], ' – aborts the slash command batch execution', true, true); parser.addCommand('fuzzy', fuzzyCallback, [], 'list=["a","b","c"] threshold=0.4 (text to search) – performs a fuzzy match of each items of list within the text to search. If any item matches then its name is returned. If no item list matches the text to search then no value is returned. The optional threshold (default is 0.4) allows some control over the matching. A low value (min 0.0) means the match is very strict. At 1.0 (max) the match is very loose and probably matches anything. The returned value passes to the next command through the pipe.', true, true); parser.addCommand('pass', (_, arg) => arg, ['return'], '<span class="monospace">(text)</span> – passes the text to the next command through the pipe.', true, true); @@ -661,6 +661,7 @@ async function generateRawCallback(args, value) { const lock = isTrueBoolean(args?.lock); const as = args?.as || 'system'; const quietToLoud = as === 'char'; + const systemPrompt = resolveVariable(args?.system) || ''; try { if (lock) { @@ -668,7 +669,7 @@ async function generateRawCallback(args, value) { } setEphemeralStopStrings(resolveVariable(args?.stop)); - const result = await generateRaw(value, '', isFalseBoolean(args?.instruct), quietToLoud); + const result = await generateRaw(value, '', isFalseBoolean(args?.instruct), quietToLoud, systemPrompt); return result; } finally { if (lock) { From 3331cb64917d7892cf0648f16258f37890222d0c Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 31 Mar 2024 21:02:38 +0300 Subject: [PATCH 79/99] Add ability to temporarily override response length for /gen and /genraw --- public/script.js | 203 ++++++++++++++++++++----------- public/scripts/slash-commands.js | 10 +- 2 files changed, 138 insertions(+), 75 deletions(-) diff --git a/public/script.js b/public/script.js index 99fae36f4..8252c0b79 100644 --- a/public/script.js +++ b/public/script.js @@ -2372,21 +2372,31 @@ function getStoppingStrings(isImpersonate, isContinue) { * @param {boolean} skipWIAN whether to skip addition of World Info and Author's Note into the prompt * @param {string} quietImage Image to use for the quiet prompt * @param {string} quietName Name to use for the quiet prompt (defaults to "System:") + * @param {number} [responseLength] Maximum response length. If unset, the global default value is used. * @returns */ -export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, quietImage = null, quietName = null) { +export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, quietImage = null, quietName = null, responseLength = null) { console.log('got into genQuietPrompt'); - /** @type {GenerateOptions} */ - const options = { - quiet_prompt, - quietToLoud, - skipWIAN: skipWIAN, - force_name2: true, - quietImage: quietImage, - quietName: quietName, - }; - const generateFinished = await Generate('quiet', options); - return generateFinished; + const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0; + let originalResponseLength = -1; + try { + /** @type {GenerateOptions} */ + const options = { + quiet_prompt, + quietToLoud, + skipWIAN: skipWIAN, + force_name2: true, + quietImage: quietImage, + quietName: quietName, + }; + originalResponseLength = responseLengthCustomized ? saveResponseLength(main_api, responseLength) : -1; + const generateFinished = await Generate('quiet', options); + return generateFinished; + } finally { + if (responseLengthCustomized) { + restoreResponseLength(main_api, originalResponseLength); + } + } } /** @@ -2912,14 +2922,17 @@ class StreamingProcessor { * @param {boolean} instructOverride true to override instruct mode, false to use the default value * @param {boolean} quietToLoud true to generate a message in system mode, false to generate a message in character mode * @param {string} [systemPrompt] System prompt to use. Only Instruct mode or OpenAI. + * @param {number} [responseLength] Maximum response length. If unset, the global default value is used. * @returns {Promise<string>} Generated message */ -export async function generateRaw(prompt, api, instructOverride, quietToLoud, systemPrompt) { +export async function generateRaw(prompt, api, instructOverride, quietToLoud, systemPrompt, responseLength) { if (!api) { api = main_api; } const abortController = new AbortController(); + const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0; + let originalResponseLength = -1; const isInstruct = power_user.instruct.enabled && api !== 'openai' && api !== 'novel' && !instructOverride; const isQuiet = true; @@ -2934,70 +2947,109 @@ export async function generateRaw(prompt, api, instructOverride, quietToLoud, sy prompt = isInstruct ? formatInstructModeChat(name1, prompt, false, true, '', name1, name2, false) : prompt; prompt = isInstruct ? (prompt + formatInstructModePrompt(name2, false, '', name1, name2, isQuiet, quietToLoud)) : (prompt + '\n'); - let generateData = {}; + try { + originalResponseLength = responseLengthCustomized ? saveResponseLength(api, responseLength) : -1; + let generateData = {}; - switch (api) { - case 'kobold': - case 'koboldhorde': - if (preset_settings === 'gui') { - generateData = { prompt: prompt, gui_settings: true, max_length: amount_gen, max_context_length: max_context, api_server }; - } else { - const isHorde = api === 'koboldhorde'; - const koboldSettings = koboldai_settings[koboldai_setting_names[preset_settings]]; - generateData = getKoboldGenerationData(prompt, koboldSettings, amount_gen, max_context, isHorde, 'quiet'); + switch (api) { + case 'kobold': + case 'koboldhorde': + if (preset_settings === 'gui') { + generateData = { prompt: prompt, gui_settings: true, max_length: amount_gen, max_context_length: max_context, api_server }; + } else { + const isHorde = api === 'koboldhorde'; + const koboldSettings = koboldai_settings[koboldai_setting_names[preset_settings]]; + generateData = getKoboldGenerationData(prompt, koboldSettings, amount_gen, max_context, isHorde, 'quiet'); + } + break; + case 'novel': { + const novelSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]]; + generateData = getNovelGenerationData(prompt, novelSettings, amount_gen, false, false, null, 'quiet'); + break; } - break; - case 'novel': { - const novelSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]]; - generateData = getNovelGenerationData(prompt, novelSettings, amount_gen, false, false, null, 'quiet'); - break; + case 'textgenerationwebui': + generateData = getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet'); + break; + case 'openai': { + generateData = [{ role: 'user', content: prompt.trim() }]; + if (systemPrompt) { + generateData.unshift({ role: 'system', content: systemPrompt.trim() }); + } + } break; } - case 'textgenerationwebui': - generateData = getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet'); - break; - case 'openai': { - generateData = [{ role: 'user', content: prompt.trim() }]; - if (systemPrompt) { - generateData.unshift({ role: 'system', content: systemPrompt.trim() }); + + let data = {}; + + if (api == 'koboldhorde') { + data = await generateHorde(prompt, generateData, abortController.signal, false); + } else if (api == 'openai') { + data = await sendOpenAIRequest('quiet', generateData, abortController.signal); + } else { + const generateUrl = getGenerateUrl(api); + const response = await fetch(generateUrl, { + method: 'POST', + headers: getRequestHeaders(), + cache: 'no-cache', + body: JSON.stringify(generateData), + signal: abortController.signal, + }); + + if (!response.ok) { + const error = await response.json(); + throw error; } - } break; + + data = await response.json(); + } + + if (data.error) { + throw new Error(data.error); + } + + const message = cleanUpMessage(extractMessageFromData(data), false, false, true); + + if (!message) { + throw new Error('No message generated'); + } + + return message; + } finally { + if (responseLengthCustomized) { + restoreResponseLength(api, originalResponseLength); + } } +} - let data = {}; - - if (api == 'koboldhorde') { - data = await generateHorde(prompt, generateData, abortController.signal, false); - } else if (api == 'openai') { - data = await sendOpenAIRequest('quiet', generateData, abortController.signal); +/** + * Temporarily change the response length for the specified API. + * @param {string} api API to use. + * @param {number} responseLength Target response length. + * @returns {number} The original response length. + */ +function saveResponseLength(api, responseLength) { + let oldValue = -1; + if (api === 'openai') { + oldValue = oai_settings.openai_max_tokens; + oai_settings.openai_max_tokens = responseLength; } else { - const generateUrl = getGenerateUrl(api); - const response = await fetch(generateUrl, { - method: 'POST', - headers: getRequestHeaders(), - cache: 'no-cache', - body: JSON.stringify(generateData), - signal: abortController.signal, - }); - - if (!response.ok) { - const error = await response.json(); - throw error; - } - - data = await response.json(); + oldValue = max_context; + max_context = responseLength; } + return oldValue; +} - if (data.error) { - throw new Error(data.error); +/** + * Restore the original response length for the specified API. + * @param {string} api API to use. + * @param {number} responseLength Target response length. + * @returns {void} + */ +function restoreResponseLength(api, responseLength) { + if (api === 'openai') { + oai_settings.openai_max_tokens = responseLength; + } else { + max_context = responseLength; } - - const message = cleanUpMessage(extractMessageFromData(data), false, false, true); - - if (!message) { - throw new Error('No message generated'); - } - - return message; } /** @@ -4390,10 +4442,19 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul } } -export function getMaxContextSize() { +/** + * Gets the maximum usable context size for the current API. + * @param {number|null} overrideResponseLength Optional override for the response length. + * @returns {number} Maximum usable context size. + */ +export function getMaxContextSize(overrideResponseLength = null) { + if (typeof overrideResponseLength !== 'number' || overrideResponseLength <= 0 || isNaN(overrideResponseLength)) { + overrideResponseLength = null; + } + let this_max_context = 1487; if (main_api == 'kobold' || main_api == 'koboldhorde' || main_api == 'textgenerationwebui') { - this_max_context = (max_context - amount_gen); + this_max_context = (max_context - (overrideResponseLength || amount_gen)); } if (main_api == 'novel') { this_max_context = Number(max_context); @@ -4410,10 +4471,10 @@ export function getMaxContextSize() { } } - this_max_context = this_max_context - amount_gen; + this_max_context = this_max_context - (overrideResponseLength || amount_gen); } if (main_api == 'openai') { - this_max_context = oai_settings.openai_max_context - oai_settings.openai_max_tokens; + this_max_context = oai_settings.openai_max_context - (overrideResponseLength || oai_settings.openai_max_tokens); } return this_max_context; } diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index f07f66d24..aef1de058 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -230,8 +230,8 @@ parser.addCommand('peek', peekCallback, [], '<span class="monospace">(message in parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '<span class="monospace">(optional 1-based id)</span> – deletes a swipe from the last chat message. If swipe id not provided - deletes the current swipe.', true, true); parser.addCommand('echo', echoCallback, [], '<span class="monospace">(title=string severity=info/warning/error/success [text])</span> – echoes the text to toast message. Useful for pipes debugging.', true, true); //parser.addCommand('#', (_, value) => '', [], ' – a comment, does nothing, e.g. <tt>/# the next three commands switch variables a and b</tt>', true, true); -parser.addCommand('gen', generateCallback, [], '<span class="monospace">(lock=on/off name="System" [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating and allowing to configure the in-prompt name for instruct mode (default = "System"). "as" argument controls the role of the output prompt: system (default) or char.', true, true); -parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(lock=on/off instruct=on/off stop=[] as=system/char system="system prompt" [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. <tt>/genraw instruct=off Why is the sky blue?</tt>. Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. <tt>/genraw stop=["\\n"] Say hi</tt>. "as" argument controls the role of the output prompt: system (default) or char. "system" argument adds an (optional) system prompt at the start.', true, true); +parser.addCommand('gen', generateCallback, [], '<span class="monospace">(lock=on/off name="System" length=123 [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating and allowing to configure the in-prompt name for instruct mode (default = "System"). "as" argument controls the role of the output prompt: system (default) or char. If "length" argument is provided as a number in tokens, allows to temporarily override an API response length.', true, true); +parser.addCommand('genraw', generateRawCallback, [], '<span class="monospace">(lock=on/off instruct=on/off stop=[] as=system/char system="system prompt" length=123 [prompt])</span> – generates text using the provided prompt and passes it to the next command through the pipe, optionally locking user input while generating. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. <tt>/genraw instruct=off Why is the sky blue?</tt>. Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. <tt>/genraw stop=["\\n"] Say hi</tt>. "as" argument controls the role of the output prompt: system (default) or char. "system" argument adds an (optional) system prompt at the start. If "length" argument is provided as a number in tokens, allows to temporarily override an API response length.', true, true); parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '<span class="monospace">(text)</span> – adds a swipe to the last chat message.', true, true); parser.addCommand('abort', abortCallback, [], ' – aborts the slash command batch execution', true, true); parser.addCommand('fuzzy', fuzzyCallback, [], 'list=["a","b","c"] threshold=0.4 (text to search) – performs a fuzzy match of each items of list within the text to search. If any item matches then its name is returned. If no item list matches the text to search then no value is returned. The optional threshold (default is 0.4) allows some control over the matching. A low value (min 0.0) means the match is very strict. At 1.0 (max) the match is very loose and probably matches anything. The returned value passes to the next command through the pipe.', true, true); parser.addCommand('pass', (_, arg) => arg, ['return'], '<span class="monospace">(text)</span> – passes the text to the next command through the pipe.', true, true); @@ -662,6 +662,7 @@ async function generateRawCallback(args, value) { const as = args?.as || 'system'; const quietToLoud = as === 'char'; const systemPrompt = resolveVariable(args?.system) || ''; + const length = Number(resolveVariable(args?.length) ?? 0) || 0; try { if (lock) { @@ -669,7 +670,7 @@ async function generateRawCallback(args, value) { } setEphemeralStopStrings(resolveVariable(args?.stop)); - const result = await generateRaw(value, '', isFalseBoolean(args?.instruct), quietToLoud, systemPrompt); + const result = await generateRaw(value, '', isFalseBoolean(args?.instruct), quietToLoud, systemPrompt, length); return result; } finally { if (lock) { @@ -690,6 +691,7 @@ async function generateCallback(args, value) { const lock = isTrueBoolean(args?.lock); const as = args?.as || 'system'; const quietToLoud = as === 'char'; + const length = Number(resolveVariable(args?.length) ?? 0) || 0; try { if (lock) { @@ -698,7 +700,7 @@ async function generateCallback(args, value) { setEphemeralStopStrings(resolveVariable(args?.stop)); const name = args?.name; - const result = await generateQuietPrompt(value, quietToLoud, false, '', name); + const result = await generateQuietPrompt(value, quietToLoud, false, '', name, length); return result; } finally { if (lock) { From b990eb523bd0eb7c3be0243c63e7d2c1ec60d06a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 31 Mar 2024 21:22:27 +0300 Subject: [PATCH 80/99] #1980 Add raw summary prompt builder mode --- public/scripts/extensions/memory/index.js | 249 +++++++++++++++++- .../scripts/extensions/memory/settings.html | 79 ++++-- public/scripts/extensions/memory/style.css | 12 +- 3 files changed, 310 insertions(+), 30 deletions(-) diff --git a/public/scripts/extensions/memory/index.js b/public/scripts/extensions/memory/index.js index da0ae00e7..00c6de443 100644 --- a/public/scripts/extensions/memory/index.js +++ b/public/scripts/extensions/memory/index.js @@ -1,11 +1,25 @@ -import { getStringHash, debounce, waitUntilCondition, extractAllWords } from '../../utils.js'; +import { getStringHash, debounce, waitUntilCondition, extractAllWords, delay } from '../../utils.js'; import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules, renderExtensionTemplate } from '../../extensions.js'; -import { animation_duration, eventSource, event_types, extension_prompt_roles, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from '../../../script.js'; +import { + activateSendButtons, + deactivateSendButtons, + animation_duration, + eventSource, + event_types, + extension_prompt_roles, + extension_prompt_types, + generateQuietPrompt, + is_send_press, + saveSettingsDebounced, + substituteParams, + generateRaw, + getMaxContextSize, +} from '../../../script.js'; import { is_group_generating, selected_group } from '../../group-chats.js'; import { registerSlashCommand } from '../../slash-commands.js'; import { loadMovingUIState } from '../../power-user.js'; import { dragElement } from '../../RossAscends-mods.js'; -import { getTextTokens, tokenizers } from '../../tokenizers.js'; +import { getTextTokens, getTokenCount, tokenizers } from '../../tokenizers.js'; export { MODULE_NAME }; const MODULE_NAME = '1_memory'; @@ -39,7 +53,13 @@ const summary_sources = { 'main': 'main', }; -const defaultPrompt = '[Pause your roleplay. Summarize the most important facts and events that have happened in the chat so far. If a summary already exists in your memory, use that as a base and expand with new facts. Limit the summary to {{words}} words or less. Your response should include nothing but the summary.]'; +const prompt_builders = { + DEFAULT: 0, + RAW_BLOCKING: 1, + RAW_NON_BLOCKING: 2, +}; + +const defaultPrompt = '[Pause your roleplay. Summarize the most important facts and events in the story so far. If a summary already exists in your memory, use that as a base and expand with new facts. Limit the summary to {{words}} words or less. Your response should include nothing but the summary.]'; const defaultTemplate = '[Summary: {{summary}}]'; const defaultSettings = { @@ -57,12 +77,21 @@ const defaultSettings = { promptWordsStep: 25, promptInterval: 10, promptMinInterval: 0, - promptMaxInterval: 100, + promptMaxInterval: 250, promptIntervalStep: 1, promptForceWords: 0, promptForceWordsStep: 100, promptMinForceWords: 0, promptMaxForceWords: 10000, + overrideResponseLength: 0, + overrideResponseLengthMin: 0, + overrideResponseLengthMax: 4096, + overrideResponseLengthStep: 16, + maxMessagesPerRequest: 0, + maxMessagesPerRequestMin: 0, + maxMessagesPerRequestMax: 250, + maxMessagesPerRequestStep: 1, + prompt_builder: prompt_builders.RAW_BLOCKING, }; function loadSettings() { @@ -87,9 +116,50 @@ function loadSettings() { $('#memory_role').val(extension_settings.memory.role).trigger('input'); $(`input[name="memory_position"][value="${extension_settings.memory.position}"]`).prop('checked', true).trigger('input'); $('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input'); + $(`input[name="memory_prompt_builder"][value="${extension_settings.memory.prompt_builder}"]`).prop('checked', true).trigger('input'); + $('#memory_override_response_length').val(extension_settings.memory.overrideResponseLength).trigger('input'); + $('#memory_max_messages_per_request').val(extension_settings.memory.maxMessagesPerRequest).trigger('input'); switchSourceControls(extension_settings.memory.source); } +async function onPromptIntervalAutoClick() { + const context = getContext(); + const maxPromptLength = getMaxContextSize(extension_settings.memory.overrideResponseLength); + const chat = context.chat; + const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes); + const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length; + const messagesTokenCount = getTokenCount(allMessages.join('\n')); + const tokensPerWord = messagesTokenCount / messagesWordCount; + const averageMessageTokenCount = messagesTokenCount / allMessages.length; + const targetSummaryTokens = Math.round(extension_settings.memory.promptWords * tokensPerWord); + const promptTokens = getTokenCount(extension_settings.memory.prompt); + const promptAllowance = maxPromptLength - promptTokens - targetSummaryTokens; + const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0; + const averageMessagesPerPrompt = Math.floor(promptAllowance / averageMessageTokenCount); + const unfitMessages = maxMessagesPerSummary > 0 ? averageMessagesPerPrompt - maxMessagesPerSummary : 0; + const adjustedAverageMessagesPerPrompt = Math.max(1, averageMessagesPerPrompt - (unfitMessages > 0 ? Math.ceil(unfitMessages / 2) : 0)); + + console.table({ + maxPromptLength, + promptAllowance, + targetSummaryTokens, + promptTokens, + messagesWordCount, + messagesTokenCount, + tokensPerWord, + averageMessageTokenCount, + averageMessagesPerPrompt, + adjustedAverageMessagesPerPrompt, + maxMessagesPerSummary, + unfitMessages, + }); + + const ROUNDING = 5; + extension_settings.memory.promptInterval = Math.max(1, Math.floor(adjustedAverageMessagesPerPrompt / ROUNDING) * ROUNDING); + + $('#memory_prompt_interval').val(extension_settings.memory.promptInterval).trigger('input'); +} + function onSummarySourceChange(event) { const value = event.target.value; extension_settings.memory.source = value; @@ -130,6 +200,10 @@ function onMemoryPromptIntervalInput() { saveSettingsDebounced(); } +function onMemoryPromptRestoreClick() { + $('#memory_prompt').val(defaultPrompt).trigger('input'); +} + function onMemoryPromptInput() { const value = $(this).val(); extension_settings.memory.prompt = value; @@ -171,6 +245,20 @@ function onMemoryPromptWordsForceInput() { saveSettingsDebounced(); } +function onOverrideResponseLengthInput() { + const value = $(this).val(); + extension_settings.memory.overrideResponseLength = Number(value); + $('#memory_override_response_length_value').text(extension_settings.memory.overrideResponseLength); + saveSettingsDebounced(); +} + +function onMaxMessagesPerRequestInput() { + const value = $(this).val(); + extension_settings.memory.maxMessagesPerRequest = Number(value); + $('#memory_max_messages_per_request_value').text(extension_settings.memory.maxMessagesPerRequest); + saveSettingsDebounced(); +} + function saveLastValues() { const context = getContext(); lastGroupId = context.groupId; @@ -196,6 +284,22 @@ function getLatestMemoryFromChat(chat) { return ''; } +function getIndexOfLatestChatSummary(chat) { + if (!Array.isArray(chat) || !chat.length) { + return -1; + } + + const reversedChat = chat.slice().reverse(); + reversedChat.shift(); + for (let mes of reversedChat) { + if (mes.extra && mes.extra.memory) { + return chat.indexOf(mes); + } + } + + return -1; +} + async function onChatEvent() { // Module not enabled if (extension_settings.memory.source === summary_sources.extras) { @@ -359,8 +463,41 @@ async function summarizeChatMain(context, force, skipWIAN) { console.debug('Summarization prompt is empty. Skipping summarization.'); return; } + console.log('sending summary prompt'); - const summary = await generateQuietPrompt(prompt, false, skipWIAN); + let summary = ''; + let index = null; + + if (prompt_builders.DEFAULT === extension_settings.memory.prompt_builder) { + summary = await generateQuietPrompt(prompt, false, skipWIAN, '', '', extension_settings.memory.overrideResponseLength); + } + + if ([prompt_builders.RAW_BLOCKING, prompt_builders.RAW_NON_BLOCKING].includes(extension_settings.memory.prompt_builder)) { + const lock = extension_settings.memory.prompt_builder === prompt_builders.RAW_BLOCKING; + try { + if (lock) { + deactivateSendButtons(); + } + + const { rawPrompt, lastUsedIndex } = await getRawSummaryPrompt(context, prompt); + + if (lastUsedIndex === null || lastUsedIndex === -1) { + if (force) { + toastr.info('To try again, remove the latest summary.', 'No messages found to summarize'); + } + + return null; + } + + summary = await generateRaw(rawPrompt, '', false, false, prompt, extension_settings.memory.overrideResponseLength); + index = lastUsedIndex; + } finally { + if (lock) { + activateSendButtons(); + } + } + } + const newContext = getContext(); // something changed during summarization request @@ -371,10 +508,83 @@ async function summarizeChatMain(context, force, skipWIAN) { return; } - setMemoryContext(summary, true); + setMemoryContext(summary, true, index); return summary; } +/** + * Get the raw summarization prompt from the chat context. + * @param {object} context ST context + * @param {string} prompt Summarization system prompt + * @returns {Promise<{rawPrompt: string, lastUsedIndex: number}>} Raw summarization prompt + */ +async function getRawSummaryPrompt(context, prompt) { + /** + * Get the memory string from the chat buffer. + * @param {boolean} includeSystem Include prompt into the memory string + * @returns {string} Memory string + */ + function getMemoryString(includeSystem) { + const delimiter = '\n\n'; + const stringBuilder = []; + const bufferString = chatBuffer.slice().join(delimiter); + + if (includeSystem) { + stringBuilder.push(prompt); + } + + if (latestSummary) { + stringBuilder.push(latestSummary); + } + + stringBuilder.push(bufferString); + + return stringBuilder.join(delimiter).trim(); + } + + const chat = context.chat.slice(); + const latestSummary = getLatestMemoryFromChat(chat); + const latestSummaryIndex = getIndexOfLatestChatSummary(chat); + chat.pop(); // We always exclude the last message from the buffer + const chatBuffer = []; + const PADDING = 64; + const PROMPT_SIZE = getMaxContextSize(extension_settings.memory.overrideResponseLength); + let latestUsedMessage = null; + + for (let index = latestSummaryIndex + 1; index < chat.length; index++) { + const message = chat[index]; + + if (!message) { + break; + } + + if (message.is_system || !message.mes) { + continue; + } + + const entry = `${message.name}:\n${message.mes}`; + chatBuffer.push(entry); + + const tokens = getTokenCount(getMemoryString(true), PADDING); + await delay(1); + + if (tokens > PROMPT_SIZE) { + chatBuffer.pop(); + break; + } + + latestUsedMessage = message; + + if (extension_settings.memory.maxMessagesPerRequest > 0 && chatBuffer.length >= extension_settings.memory.maxMessagesPerRequest) { + break; + } + } + + const lastUsedIndex = context.chat.indexOf(latestUsedMessage); + const rawPrompt = getMemoryString(false); + return { rawPrompt, lastUsedIndex }; +} + async function summarizeChatExtras(context) { function getMemoryString() { return (longMemory + '\n\n' + memoryBuffer.slice().reverse().join('\n\n')).trim(); @@ -482,12 +692,24 @@ function onMemoryContentInput() { setMemoryContext(value, true); } +function onMemoryPromptBuilderInput(e) { + const value = Number(e.target.value); + extension_settings.memory.prompt_builder = value; + saveSettingsDebounced(); +} + function reinsertMemory() { - const existingValue = $('#memory_contents').val(); + const existingValue = String($('#memory_contents').val()); setMemoryContext(existingValue, false); } -function setMemoryContext(value, saveToMessage) { +/** + * Set the summary value to the context and save it to the chat message extra. + * @param {string} value Value of a summary + * @param {boolean} saveToMessage Should the summary be saved to the chat message extra + * @param {number|null} index Index of the chat message to save the summary to. If null, the pre-last message is used. + */ +function setMemoryContext(value, saveToMessage, index = null) { const context = getContext(); context.setExtensionPrompt(MODULE_NAME, formatMemoryValue(value), extension_settings.memory.position, extension_settings.memory.depth, false, extension_settings.memory.role); $('#memory_contents').val(value); @@ -497,7 +719,7 @@ function setMemoryContext(value, saveToMessage) { console.debug('Role: ' + extension_settings.memory.role); if (saveToMessage && context.chat.length) { - const idx = context.chat.length - 2; + const idx = index ?? context.chat.length - 2; const mes = context.chat[idx < 0 ? 0 : idx]; if (!mes.extra) { @@ -573,6 +795,13 @@ function setupListeners() { $('#memory_role').off('click').on('input', onMemoryRoleInput); $('input[name="memory_position"]').off('click').on('change', onMemoryPositionChange); $('#memory_prompt_words_force').off('click').on('input', onMemoryPromptWordsForceInput); + $('#memory_prompt_builder_default').off('click').on('input', onMemoryPromptBuilderInput); + $('#memory_prompt_builder_raw_blocking').off('click').on('input', onMemoryPromptBuilderInput); + $('#memory_prompt_builder_raw_non_blocking').off('click').on('input', onMemoryPromptBuilderInput); + $('#memory_prompt_restore').off('click').on('click', onMemoryPromptRestoreClick); + $('#memory_prompt_interval_auto').off('click').on('click', onPromptIntervalAutoClick); + $('#memory_override_response_length').off('click').on('input', onOverrideResponseLengthInput); + $('#memory_max_messages_per_request').off('click').on('input', onMaxMessagesPerRequestInput); $('#summarySettingsBlockToggle').off('click').on('click', function () { console.log('saw settings button click'); $('#summarySettingsBlock').slideToggle(200, 'swing'); //toggleClass("hidden"); diff --git a/public/scripts/extensions/memory/settings.html b/public/scripts/extensions/memory/settings.html index b5b54142c..7bf3626e7 100644 --- a/public/scripts/extensions/memory/settings.html +++ b/public/scripts/extensions/memory/settings.html @@ -36,9 +36,67 @@ </div> </div> <div id="summarySettingsBlock" style="display:none;"> + <div data-summary-source="main"> + <label> + Prompt builder + </label> + <label class="checkbox_label" for="memory_prompt_builder_raw_blocking" title="Extension will build its own prompt using messages that were not summarized yet. Blocks the chat until the summary is generated."> + <input id="memory_prompt_builder_raw_blocking" type="radio" name="memory_prompt_builder" value="1" /> + <span>Raw, blocking</span> + </label> + <label class="checkbox_label" for="memory_prompt_builder_raw_non_blocking" title="Extension will build its own prompt using messages that were not summarized yet. Does not block the chat while the summary is being generated. Not all backends support this mode."> + <input id="memory_prompt_builder_raw_non_blocking" type="radio" name="memory_prompt_builder" value="2" /> + <span>Raw, non-blocking</span> + </label> + <label class="checkbox_label" id="memory_prompt_builder_default" title="Extension will use the regular main prompt builder and add the summary request to it as the last system message."> + <input id="memory_prompt_builder_default" type="radio" name="memory_prompt_builder" value="0" /> + <span>Classic, blocking</span> + </label> + </div> + <div data-summary-source="main"> + <label for="memory_prompt" class="title_restorable"> + <span data-i18n="Summary Prompt">Summary Prompt</span> + <div id="memory_prompt_restore" title="Restore default prompt" class="right_menu_button"> + <div class="fa-solid fa-clock-rotate-left"></div> + </div> + </label> + <textarea id="memory_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation. {{words}} will resolve to the 'Number of words' parameter."></textarea> + <label for="memory_prompt_words">Target summary length (<span id="memory_prompt_words_value"></span> words)</label> + <input id="memory_prompt_words" type="range" value="{{defaultSettings.promptWords}}" min="{{defaultSettings.promptMinWords}}" max="{{defaultSettings.promptMaxWords}}" step="{{defaultSettings.promptWordsStep}}" /> + <label for="memory_override_response_length"> + API response length (<span id="memory_override_response_length_value"></span> tokens) + <small class="memory_disabled_hint">0 = default</small> + </label> + <input id="memory_override_response_length" type="range" value="{{defaultSettings.overrideResponseLength}}" min="{{defaultSettings.overrideResponseLengthMin}}" max="{{defaultSettings.overrideResponseLengthMax}}" step="{{defaultSettings.overrideResponseLengthStep}}" /> + <label for="memory_max_messages_per_request"> + [Raw] Max messages per request (<span id="memory_max_messages_per_request_value"></span>) + <small class="memory_disabled_hint">0 = unlimited</small> + </label> + <input id="memory_max_messages_per_request" type="range" value="{{defaultSettings.maxMessagesPerRequest}}" min="{{defaultSettings.maxMessagesPerRequestMin}}" max="{{defaultSettings.maxMessagesPerRequestMax}}" step="{{defaultSettings.maxMessagesPerRequestStep}}" /> + <h4 data-i18n="Update frequency" class="textAlignCenter"> + Update frequency + </h4> + <label for="memory_prompt_interval" class="title_restorable"> + <span> + Update every <span id="memory_prompt_interval_value"></span> messages + <small class="memory_disabled_hint">0 = disable</small> + </span> + <div id="memory_prompt_interval_auto" title="Try to automatically adjust the interval based on the chat metrics." class="right_menu_button"> + <div class="fa-solid fa-wand-magic-sparkles"></div> + </div> + </label> + <input id="memory_prompt_interval" type="range" value="{{defaultSettings.promptInterval}}" min="{{defaultSettings.promptMinInterval}}" max="{{defaultSettings.promptMaxInterval}}" step="{{defaultSettings.promptIntervalStep}}" /> + <label for="memory_prompt_words_force"> + Update every <span id="memory_prompt_words_force_value"></span> words + <small class="memory_disabled_hint">0 = disable</small> + </label> + <input id="memory_prompt_words_force" type="range" value="{{defaultSettings.promptForceWords}}" min="{{defaultSettings.promptMinForceWords}}" max="{{defaultSettings.promptMaxForceWords}}" step="{{defaultSettings.promptForceWordsStep}}" /> + <small>If both sliders are non-zero, then both will trigger summary updates at their respective intervals.</small> + <hr> + </div> <div class="memory_template"> - <label for="memory_template">Insertion Template</label> - <textarea id="memory_template" class="text_pole textarea_compact" rows="2" placeholder="{{summary{{ will resolve to the current summary contents."></textarea> + <label for="memory_template">Injection Template</label> + <textarea id="memory_template" class="text_pole textarea_compact" rows="2" placeholder="{{summary}} will resolve to the current summary contents."></textarea> </div> <label for="memory_position">Injection Position</label> <div class="radio_group"> @@ -61,23 +119,6 @@ </select> </label> </div> - <div data-summary-source="main" class="memory_contents_controls"> - </div> - <div data-summary-source="main"> - <label for="memory_prompt" class="title_restorable"> - Summary Prompt - </label> - <textarea id="memory_prompt" class="text_pole textarea_compact" rows="6" placeholder="This prompt will be sent to AI to request the summary generation. {{words}} will resolve to the 'Number of words' parameter."></textarea> - <label for="memory_prompt_words">Summary length (<span id="memory_prompt_words_value"></span> words)</label> - <input id="memory_prompt_words" type="range" value="{{defaultSettings.promptWords}}" min="{{defaultSettings.promptMinWords}}" max="{{defaultSettings.promptMaxWords}}" step="{{defaultSettings.promptWordsStep}}" /> - <label for="memory_prompt_interval">Update every <span id="memory_prompt_interval_value"></span> messages</label> - <small>0 = disable</small> - <input id="memory_prompt_interval" type="range" value="{{defaultSettings.promptInterval}}" min="{{defaultSettings.promptMinInterval}}" max="{{defaultSettings.promptMaxInterval}}" step="{{defaultSettings.promptIntervalStep}}" /> - <label for="memory_prompt_words_force">Update every <span id="memory_prompt_words_force_value"></span> words</label> - <small>0 = disable</small> - <input id="memory_prompt_words_force" type="range" value="{{defaultSettings.promptForceWords}}" min="{{defaultSettings.promptMinForceWords}}" max="{{defaultSettings.promptMaxForceWords}}" step="{{defaultSettings.promptForceWordsStep}}" /> - <small>If both sliders are non-zero, then both will trigger summary updates a their respective intervals.</small> - </div> </div> </div> </div> diff --git a/public/scripts/extensions/memory/style.css b/public/scripts/extensions/memory/style.css index 20dfb5e3c..2f3ddbb25 100644 --- a/public/scripts/extensions/memory/style.css +++ b/public/scripts/extensions/memory/style.css @@ -24,4 +24,14 @@ label[for="memory_frozen"] input { flex-direction: row; align-items: center; justify-content: space-between; -} \ No newline at end of file +} + +.memory_disabled_hint { + margin-left: 2px; +} + +#summarySettingsBlock { + display: flex; + flex-direction: column; + row-gap: 5px; +} From 6aa97c73e92c48a4c6ff8e2ee74b7203486aa993 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 31 Mar 2024 21:27:13 +0300 Subject: [PATCH 81/99] Fix example dialogue formatting with block header --- public/scripts/instruct-mode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index 9c75977e1..c7aa812ce 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -400,7 +400,7 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { } if (blockHeading) { - formattedExamples.push(power_user.blockHeading); + formattedExamples.push(blockHeading); } for (const example of blockExamples) { From 159404c3e21adeb25c4d0e64fa1b5c71af13bc6d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 1 Apr 2024 01:18:29 +0300 Subject: [PATCH 82/99] Hide "no WI/AN" in Extras summary mode --- public/scripts/extensions/memory/settings.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/memory/settings.html b/public/scripts/extensions/memory/settings.html index 7bf3626e7..c6f05fd2d 100644 --- a/public/scripts/extensions/memory/settings.html +++ b/public/scripts/extensions/memory/settings.html @@ -17,7 +17,9 @@ <div class="flex-container justifyspacebetween alignitemscenter"> <span class="flex1">Current summary:</span> - <div id="memory_restore" class="menu_button flex1 margin0"><span>Restore Previous</span></div> + <div id="memory_restore" class="menu_button flex1 margin0"> + <span>Restore Previous</span> + </div> </div> <textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea> @@ -27,7 +29,10 @@ <span>Summarize now</span> </div> <label for="memory_frozen" title="Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API)." data-i18n="[title]Disable automatic summary updates. While paused, the summary remains as-is. You can still force an update by pressing the Summarize now button (which is only available with the Main API)."><input id="memory_frozen" type="checkbox" />Pause</label> - <label for="memory_skipWIAN" title="Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN." data-i18n="[title]Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN."><input id="memory_skipWIAN" type="checkbox" />No WI/AN</label> + <label data-summary-source="main" for="memory_skipWIAN" title="Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN." data-i18n="[title]Omit World Info and Author's Note from text to be summarized. Only has an effect when using the Main API. The Extras API always omits WI/AN."> + <input id="memory_skipWIAN" type="checkbox" /> + <span>No WI/AN</span> + </label> </div> <div class="memory_contents_controls"> <div id="summarySettingsBlockToggle" class="menu_button menu_button_icon" title="Edit summarization prompt, insertion position, etc."> From a5d00d356b9e78e256eca8dde3730eb0fbb879af Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 1 Apr 2024 02:11:52 +0300 Subject: [PATCH 83/99] Auto-calculate summary words --- public/scripts/extensions/memory/index.js | 36 +++++++++++++++++++ .../scripts/extensions/memory/settings.html | 11 ++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/memory/index.js b/public/scripts/extensions/memory/index.js index 00c6de443..093e7f47b 100644 --- a/public/scripts/extensions/memory/index.js +++ b/public/scripts/extensions/memory/index.js @@ -122,6 +122,41 @@ function loadSettings() { switchSourceControls(extension_settings.memory.source); } +async function onPromptForceWordsAutoClick() { + const context = getContext(); + const maxPromptLength = getMaxContextSize(extension_settings.memory.overrideResponseLength); + const chat = context.chat; + const allMessages = chat.filter(m => !m.is_system && m.mes).map(m => m.mes); + const messagesWordCount = allMessages.map(m => extractAllWords(m)).flat().length; + const averageMessageWordCount = messagesWordCount / allMessages.length; + const tokensPerWord = getTokenCount(allMessages.join('\n')) / messagesWordCount; + const wordsPerToken = 1 / tokensPerWord; + const maxPromptLengthWords = Math.round(maxPromptLength * wordsPerToken); + // How many words should pass so that messages will start be dropped out of context; + const wordsPerPrompt = Math.floor(maxPromptLength / tokensPerWord); + // How many words will be needed to fit the allowance buffer + const summaryPromptWords = extractAllWords(extension_settings.memory.prompt).length; + const promptAllowanceWords = maxPromptLengthWords - extension_settings.memory.promptWords - summaryPromptWords; + const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0; + const additionalWords = maxMessagesPerSummary > 0 ? Math.floor(averageMessageWordCount * maxMessagesPerSummary) : Math.max(0, promptAllowanceWords); + const targetSummaryWords = Math.round((wordsPerPrompt / 2) + additionalWords); + + console.table({ + maxPromptLength, + maxPromptLengthWords, + promptAllowanceWords, + targetSummaryWords, + wordsPerPrompt, + wordsPerToken, + tokensPerWord, + messagesWordCount, + }); + + const ROUNDING = 100; + extension_settings.memory.promptForceWords = Math.max(1, Math.floor(targetSummaryWords / ROUNDING) * ROUNDING); + $('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input'); +} + async function onPromptIntervalAutoClick() { const context = getContext(); const maxPromptLength = getMaxContextSize(extension_settings.memory.overrideResponseLength); @@ -800,6 +835,7 @@ function setupListeners() { $('#memory_prompt_builder_raw_non_blocking').off('click').on('input', onMemoryPromptBuilderInput); $('#memory_prompt_restore').off('click').on('click', onMemoryPromptRestoreClick); $('#memory_prompt_interval_auto').off('click').on('click', onPromptIntervalAutoClick); + $('#memory_prompt_words_auto').off('click').on('click', onPromptForceWordsAutoClick); $('#memory_override_response_length').off('click').on('input', onOverrideResponseLengthInput); $('#memory_max_messages_per_request').off('click').on('input', onMaxMessagesPerRequestInput); $('#summarySettingsBlockToggle').off('click').on('click', function () { diff --git a/public/scripts/extensions/memory/settings.html b/public/scripts/extensions/memory/settings.html index c6f05fd2d..ed3b31ad7 100644 --- a/public/scripts/extensions/memory/settings.html +++ b/public/scripts/extensions/memory/settings.html @@ -91,9 +91,14 @@ </div> </label> <input id="memory_prompt_interval" type="range" value="{{defaultSettings.promptInterval}}" min="{{defaultSettings.promptMinInterval}}" max="{{defaultSettings.promptMaxInterval}}" step="{{defaultSettings.promptIntervalStep}}" /> - <label for="memory_prompt_words_force"> - Update every <span id="memory_prompt_words_force_value"></span> words - <small class="memory_disabled_hint">0 = disable</small> + <label for="memory_prompt_words_force" class="title_restorable"> + <span> + Update every <span id="memory_prompt_words_force_value"></span> words + <small class="memory_disabled_hint">0 = disable</small> + </span> + <div id="memory_prompt_words_auto" title="Try to automatically adjust the interval based on the chat metrics." class="right_menu_button"> + <div class="fa-solid fa-wand-magic-sparkles"></div> + </div> </label> <input id="memory_prompt_words_force" type="range" value="{{defaultSettings.promptForceWords}}" min="{{defaultSettings.promptMinForceWords}}" max="{{defaultSettings.promptMaxForceWords}}" step="{{defaultSettings.promptForceWordsStep}}" /> <small>If both sliders are non-zero, then both will trigger summary updates at their respective intervals.</small> From 70adee3c631a0290ce182f8612e33e99ab62ad74 Mon Sep 17 00:00:00 2001 From: Wolfsblvt <wolfsblvt@gmx.de> Date: Mon, 1 Apr 2024 01:47:56 +0200 Subject: [PATCH 84/99] Add {{pick}} macro replacement - Pick macro that works like random, but is consistent for the chat and context - Change help text for random to actually utilize the new, preferred syntax --- public/scripts/macros.js | 30 ++++++++++++++++++++++++++-- public/scripts/templates/macros.html | 3 ++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/public/scripts/macros.js b/public/scripts/macros.js index f3527d451..a8d178604 100644 --- a/public/scripts/macros.js +++ b/public/scripts/macros.js @@ -1,5 +1,5 @@ -import { chat, main_api, getMaxContextSize } from '../script.js'; -import { timestampToMoment, isDigitsOnly } from './utils.js'; +import { chat, main_api, getMaxContextSize, getCurrentChatId } from '../script.js'; +import { timestampToMoment, isDigitsOnly, getStringHash } from './utils.js'; import { textgenerationwebui_banned_in_macros } from './textgen-settings.js'; import { replaceInstructMacros } from './instruct-mode.js'; import { replaceVariableMacros } from './variables.js'; @@ -211,6 +211,29 @@ function randomReplace(input, emptyListPlaceholder = '') { return input; } +function pickReplace(input, rawContent, emptyListPlaceholder = '') { + const pickPattern = /{{pick\s?::\s?([^}]+)}}/gi; + const chatIdHash = getStringHash(getCurrentChatId()); + const rawContentHash = getStringHash(rawContent); + + return input.replace(pickPattern, (match, listString, offset) => { + const list = listString.includes('::') + ? listString.split('::').filter(item => item.length > 0) + : listString.split(',').map(item => item.trim()).filter(item => item.length > 0); + + if (list.length === 0) { + return emptyListPlaceholder; + } + + const combinedSeedString = `${chatIdHash}-${rawContentHash}-${offset}`; + const finalSeed = getStringHash(combinedSeedString); + const rng = new Math.seedrandom(finalSeed); + + const randomIndex = Math.floor(rng() * list.length); + return list[randomIndex].trim(); + }); +} + function diceRollReplace(input, invalidRollPlaceholder = '') { const rollPattern = /{{roll[ : ]([^}]+)}}/gi; @@ -245,6 +268,8 @@ export function evaluateMacros(content, env) { return ''; } + const rawContent = content; + // Legacy non-macro substitutions content = content.replace(/<USER>/gi, typeof env.user === 'function' ? env.user() : env.user); content = content.replace(/<BOT>/gi, typeof env.char === 'function' ? env.char() : env.char); @@ -300,5 +325,6 @@ export function evaluateMacros(content, env) { }); content = bannedWordsReplace(content); content = randomReplace(content); + content = pickReplace(content, rawContent); return content; } diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html index 1b137122a..1c27718cc 100644 --- a/public/scripts/templates/macros.html +++ b/public/scripts/templates/macros.html @@ -34,8 +34,9 @@ <li><tt>{{idle_duration}}</tt> – the time since the last user message was sent</li> <li><tt>{{bias "text here"}}</tt> – sets a behavioral bias for the AI until the next user input. Quotes around the text are important.</li> <li><tt>{{roll:(formula)}}</tt> – rolls a dice. (ex: <tt>>{{roll:1d6}}</tt> will roll a 6-sided dice and return a number between 1 and 6)</li> - <li><tt>{{random:(args)}}</tt> – returns a random item from the list. (ex: <tt>{{random:1,2,3,4}}</tt> will return 1 of the 4 numbers at random. Works with text lists too.</li> + <li><tt>{{random::(args)}}</tt> – returns a random item from the list. (ex: <tt>{{random::1,2,3,4}}</tt> will return 1 of the 4 numbers at random. Works with text lists too.</li> <li><tt>{{random::(arg1)::(arg2)}}</tt> – alternative syntax for random that allows to use commas in the list items.</li> + <li><tt>{{pick::(args)}}</tt> – picks a random item from the list. Works the same as {{random}}, with the same possible syntax options, but the pick will stay consistent for this chat once picked and won't be re-rolled on consecutive messages and prompt processing.</li> <li><tt>{{banned "text here"}}</tt> – dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.</li> </ul> <div> From 41860bdc573a04c380e2e8fc8fa21ad5ee7b28cf Mon Sep 17 00:00:00 2001 From: Aisu Wata <aisu.wata0@gmail.com> Date: Mon, 1 Apr 2024 01:24:29 -0300 Subject: [PATCH 85/99] fix: squashing system messages would fail to skip example messages --- public/scripts/openai.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/public/scripts/openai.js b/public/scripts/openai.js index e7b83c285..415d7dead 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -2233,8 +2233,12 @@ export class ChatCompletion { continue; } - if (!excludeList.includes(message.identifier) && message.role === 'system' && !message.name) { - if (lastMessage && lastMessage.role === 'system') { + const shouldSquash = (message) => { + return !excludeList.includes(message.identifier) && message.role === 'system' && !message.name; + } + + if (shouldSquash(message)) { + if (lastMessage && shouldSquash(lastMessage)) { lastMessage.content += '\n' + message.content; lastMessage.tokens = tokenHandler.count({ role: lastMessage.role, content: lastMessage.content }); } From 9c6d8e6895dfa129fe2beadb4d5524f62674f074 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 1 Apr 2024 18:57:42 +0300 Subject: [PATCH 86/99] Update auto summary interval calculation algorithms --- public/scripts/extensions/memory/index.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/public/scripts/extensions/memory/index.js b/public/scripts/extensions/memory/index.js index 093e7f47b..7071eb8a3 100644 --- a/public/scripts/extensions/memory/index.js +++ b/public/scripts/extensions/memory/index.js @@ -137,14 +137,17 @@ async function onPromptForceWordsAutoClick() { // How many words will be needed to fit the allowance buffer const summaryPromptWords = extractAllWords(extension_settings.memory.prompt).length; const promptAllowanceWords = maxPromptLengthWords - extension_settings.memory.promptWords - summaryPromptWords; + const averageMessagesPerPrompt = Math.floor(promptAllowanceWords / averageMessageWordCount); const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0; - const additionalWords = maxMessagesPerSummary > 0 ? Math.floor(averageMessageWordCount * maxMessagesPerSummary) : Math.max(0, promptAllowanceWords); - const targetSummaryWords = Math.round((wordsPerPrompt / 2) + additionalWords); + const targetMessagesInPrompt = maxMessagesPerSummary > 0 ? maxMessagesPerSummary : Math.max(0, averageMessagesPerPrompt); + const targetSummaryWords = (targetMessagesInPrompt * averageMessageWordCount) + (promptAllowanceWords / 4); console.table({ maxPromptLength, maxPromptLengthWords, promptAllowanceWords, + averageMessagesPerPrompt, + targetMessagesInPrompt, targetSummaryWords, wordsPerPrompt, wordsPerToken, @@ -171,8 +174,8 @@ async function onPromptIntervalAutoClick() { const promptAllowance = maxPromptLength - promptTokens - targetSummaryTokens; const maxMessagesPerSummary = extension_settings.memory.maxMessagesPerRequest || 0; const averageMessagesPerPrompt = Math.floor(promptAllowance / averageMessageTokenCount); - const unfitMessages = maxMessagesPerSummary > 0 ? averageMessagesPerPrompt - maxMessagesPerSummary : 0; - const adjustedAverageMessagesPerPrompt = Math.max(1, averageMessagesPerPrompt - (unfitMessages > 0 ? Math.ceil(unfitMessages / 2) : 0)); + const targetMessagesInPrompt = maxMessagesPerSummary > 0 ? maxMessagesPerSummary : Math.max(0, averageMessagesPerPrompt); + const adjustedAverageMessagesPerPrompt = targetMessagesInPrompt + (averageMessagesPerPrompt - targetMessagesInPrompt) / 4; console.table({ maxPromptLength, @@ -184,9 +187,9 @@ async function onPromptIntervalAutoClick() { tokensPerWord, averageMessageTokenCount, averageMessagesPerPrompt, + targetMessagesInPrompt, adjustedAverageMessagesPerPrompt, maxMessagesPerSummary, - unfitMessages, }); const ROUNDING = 5; From 1be9551e9a27d016ffb2fe8f8049b0743b361e0f Mon Sep 17 00:00:00 2001 From: Wolfsblvt <wolfsblvt@gmx.de> Date: Mon, 1 Apr 2024 23:12:54 +0200 Subject: [PATCH 87/99] Fix autoscroll stop not working on firefox --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 8252c0b79..0ddd66696 100644 --- a/public/script.js +++ b/public/script.js @@ -8587,7 +8587,7 @@ jQuery(async function () { $('#groupCurrentMemberListToggle .inline-drawer-icon').trigger('click'); }, 200); - $('#chat').on('mousewheel touchstart', () => { + $('#chat').on('wheel touchstart', () => { scrollLock = true; }); From 9838ba80449d4d14b3ca9ca5649769ddc97a2113 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 2 Apr 2024 00:20:17 +0300 Subject: [PATCH 88/99] #1994 Add Cohere as a Chat Completion source --- public/img/cohere.svg | 12 ++ public/index.html | 61 ++++++--- public/script.js | 7 +- public/scripts/RossAscends-mods.js | 1 + public/scripts/openai.js | 65 ++++++++- public/scripts/secrets.js | 2 + public/scripts/slash-commands.js | 1 + src/constants.js | 1 + src/endpoints/backends/chat-completions.js | 146 ++++++++++++++++++++- src/endpoints/secrets.js | 1 + src/polyfill.js | 8 ++ src/prompt-converters.js | 61 +++++++++ 12 files changed, 347 insertions(+), 19 deletions(-) create mode 100644 public/img/cohere.svg create mode 100644 src/polyfill.js diff --git a/public/img/cohere.svg b/public/img/cohere.svg new file mode 100644 index 000000000..a213ae8d8 --- /dev/null +++ b/public/img/cohere.svg @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg width="47.403999mm" height="47.58918mm" viewBox="0 0 47.403999 47.58918" version="1.1" id="svg1" xml:space="preserve" inkscape:version="1.3 (0e150ed, 2023-07-21)" sodipodi:docname="cohere.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" inkscape:clip-to-page="false" inkscape:zoom="0.69294747" inkscape:cx="67.826209" inkscape:cy="74.320208" inkscape:window-width="1280" inkscape:window-height="688" inkscape:window-x="0" inkscape:window-y="25" inkscape:window-maximized="1" inkscape:current-layer="svg1" /> + <defs id="defs1" /> + <path id="path7" fill="currentColor" d="m 88.320761,61.142067 c -5.517973,0.07781 -11.05887,-0.197869 -16.558458,0.321489 -6.843243,0.616907 -12.325958,7.018579 -12.29857,13.807832 -0.139102,5.883715 3.981307,11.431418 9.578012,13.180923 3.171819,1.100505 6.625578,1.228214 9.855341,0.291715 3.455286,-0.847586 6.634981,-2.530123 9.969836,-3.746213 4.659947,-1.981154 9.49864,-3.782982 13.612498,-6.795254 3.80146,-2.664209 4.45489,-8.316688 2.00772,-12.1054 -1.74871,-3.034851 -5.172793,-4.896444 -8.663697,-4.741041 -2.49833,-0.140901 -5.000698,-0.196421 -7.502682,-0.214051 z m 7.533907,25.636161 c -3.334456,0.15056 -6.379399,1.79356 -9.409724,3.054098 -2.379329,1.032102 -4.911953,2.154839 -6.246333,4.528375 -2.118159,3.080424 -2.02565,7.404239 0.309716,10.346199 1.877703,2.72985 5.192756,4.03199 8.428778,3.95319 3.087361,0.0764 6.223907,0.19023 9.275119,-0.34329 5.816976,-1.32118 9.855546,-7.83031 8.101436,-13.600351 -1.30234,-4.509858 -5.762,-7.905229 -10.458992,-7.938221 z m -28.342456,4.770768 c -4.357593,-0.129828 -8.148265,3.780554 -8.168711,8.09095 -0.296313,4.101314 2.711752,8.289544 6.873869,8.869074 4.230007,0.80322 8.929483,-2.66416 9.017046,-7.07348 0.213405,-2.445397 0.09191,-5.152074 -1.705492,-7.039611 -1.484313,-1.763448 -3.717801,-2.798154 -6.016712,-2.846933 z" transform="translate(-59.323375,-61.136763)" /> +</svg> diff --git a/public/index.html b/public/index.html index 684226abf..928061b0c 100644 --- a/public/index.html +++ b/public/index.html @@ -458,7 +458,7 @@ </span> </div> </div> - <div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom"> + <div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom,cohere"> <div class="range-block-title" data-i18n="Temperature"> Temperature </div> @@ -471,7 +471,7 @@ </div> </div> </div> - <div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom"> + <div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom,cohere"> <div class="range-block-title" data-i18n="Frequency Penalty"> Frequency Penalty </div> @@ -484,7 +484,7 @@ </div> </div> </div> - <div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom"> + <div data-newbie-hidden class="range-block" data-source="openai,openrouter,ai21,custom,cohere"> <div class="range-block-title" data-i18n="Presence Penalty"> Presence Penalty </div> @@ -510,20 +510,20 @@ </div> </div> </div> - <div data-newbie-hidden class="range-block" data-source="claude,openrouter,ai21,makersuite"> + <div data-newbie-hidden class="range-block" data-source="claude,openrouter,ai21,makersuite,cohere"> <div class="range-block-title" data-i18n="Top K"> Top K </div> <div class="range-block-range-and-counter"> <div class="range-block-range"> - <input type="range" id="top_k_openai" name="volume" min="0" max="200" step="1"> + <input type="range" id="top_k_openai" name="volume" min="0" max="500" step="1"> </div> <div class="range-block-counter"> <input type="number" min="0" max="200" step="1" data-for="top_k_openai" id="top_k_counter_openai"> </div> </div> </div> - <div data-newbie-hidden class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom"> + <div data-newbie-hidden class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom,cohere"> <div class="range-block-title" data-i18n="Top-p"> Top P </div> @@ -759,7 +759,7 @@ </div> </div> </div> - <div data-newbie-hidden class="range-block" data-source="openai,openrouter,mistralai,custom"> + <div data-newbie-hidden class="range-block" data-source="openai,openrouter,mistralai,custom,cohere"> <div class="range-block-title justifyLeft" data-i18n="Seed"> Seed </div> @@ -2259,15 +2259,20 @@ Chat Completion Source </h4> <select id="chat_completion_source"> - <option value="openai">OpenAI</option> - <option value="windowai">Window AI</option> - <option value="openrouter">OpenRouter</option> - <option value="claude">Claude</option> - <option value="scale">Scale</option> - <option value="ai21">AI21</option> - <option value="makersuite">Google MakerSuite</option> - <option value="mistralai">MistralAI</option> - <option value="custom">Custom (OpenAI-compatible)</option> + <optgroup> + <option value="openai">OpenAI</option> + <option value="custom">Custom (OpenAI-compatible)</option> + </optgroup> + <optgroup> + <option value="ai21">AI21</option> + <option value="claude">Claude</option> + <option value="cohere">Cohere</option> + <option value="makersuite">Google MakerSuite</option> + <option value="mistralai">MistralAI</option> + <option value="openrouter">OpenRouter</option> + <option value="scale">Scale</option> + <option value="windowai">Window AI</option> + </optgroup> </select> <div data-newbie-hidden class="inline-drawer wide100p" data-source="openai,claude,mistralai"> <div class="inline-drawer-toggle inline-drawer-header"> @@ -2659,6 +2664,30 @@ </select> </div> </form> + <form id="cohere_form" data-source="cohere" action="javascript:void(null);" method="post" enctype="multipart/form-data"> + <h4 data-i18n="Cohere API Key">Cohere API Key</h4> + <div class="flex-container"> + <input id="api_key_cohere" name="api_key_cohere" class="text_pole flex1" maxlength="500" value="" type="text" autocomplete="off"> + <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_cohere"></div> + </div> + <div data-for="api_key_cohere" class="neutral_warning"> + For privacy reasons, your API key will be hidden after you reload the page. + </div> + <div> + <h4 data-i18n="Cohere Model">Cohere Model</h4> + <select id="model_cohere_select"> + <optgroup label="Stable"> + <option value="command-light">command-light</option> + <option value="command">command</option> + <option value="command-r">command-r</option> + </optgroup> + <optgroup label="Nightly"> + <option value="command-light-nightly">command-light-nightly</option> + <option value="command-nightly">command-nightly</option> + </optgroup> + </select> + </div> + </form> <form id="custom_form" data-source="custom"> <h4 data-i18n="Custom Endpoint (Base URL)">Custom Endpoint (Base URL)</h4> <div class="flex-container"> diff --git a/public/script.js b/public/script.js index 8252c0b79..2b775650f 100644 --- a/public/script.js +++ b/public/script.js @@ -4836,7 +4836,7 @@ function extractMessageFromData(data) { case 'novel': return data.output; case 'openai': - return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? ''; + return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? ''; default: return ''; } @@ -8187,6 +8187,11 @@ const CONNECT_API_MAP = { button: '#api_button_openai', source: chat_completion_sources.CUSTOM, }, + 'cohere': { + selected: 'cohere', + button: '#api_button_openai', + source: chat_completion_sources.COHERE, + }, 'infermaticai': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 054641268..87cbbff2c 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -350,6 +350,7 @@ function RA_autoconnect(PrevApi) { || (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21) || (secret_state[SECRET_KEYS.MAKERSUITE] && oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) || (secret_state[SECRET_KEYS.MISTRALAI] && oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) + || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE) || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) ) { $('#api_button_openai').trigger('click'); diff --git a/public/scripts/openai.js b/public/scripts/openai.js index e7b83c285..58060ccb9 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -171,6 +171,7 @@ export const chat_completion_sources = { MAKERSUITE: 'makersuite', MISTRALAI: 'mistralai', CUSTOM: 'custom', + COHERE: 'cohere', }; const character_names_behavior = { @@ -230,6 +231,7 @@ const default_settings = { google_model: 'gemini-pro', ai21_model: 'j2-ultra', mistralai_model: 'mistral-medium-latest', + cohere_model: 'command-r', custom_model: '', custom_url: '', custom_include_body: '', @@ -298,6 +300,7 @@ const oai_settings = { google_model: 'gemini-pro', ai21_model: 'j2-ultra', mistralai_model: 'mistral-medium-latest', + cohere_model: 'command-r', custom_model: '', custom_url: '', custom_include_body: '', @@ -1384,6 +1387,8 @@ function getChatCompletionModel() { return oai_settings.mistralai_model; case chat_completion_sources.CUSTOM: return oai_settings.custom_model; + case chat_completion_sources.COHERE: + return oai_settings.cohere_model; default: throw new Error(`Unknown chat completion source: ${oai_settings.chat_completion_source}`); } @@ -1603,6 +1608,7 @@ async function sendOpenAIRequest(type, messages, signal) { const isOAI = oai_settings.chat_completion_source == chat_completion_sources.OPENAI; const isMistral = oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI; const isCustom = oai_settings.chat_completion_source == chat_completion_sources.CUSTOM; + const isCohere = oai_settings.chat_completion_source == chat_completion_sources.COHERE; const isTextCompletion = (isOAI && textCompletionModels.includes(oai_settings.openai_model)) || (isOpenRouter && oai_settings.openrouter_force_instruct && power_user.instruct.enabled); const isQuiet = type === 'quiet'; const isImpersonate = type === 'impersonate'; @@ -1737,7 +1743,17 @@ async function sendOpenAIRequest(type, messages, signal) { generate_data['custom_include_headers'] = oai_settings.custom_include_headers; } - if ((isOAI || isOpenRouter || isMistral || isCustom) && oai_settings.seed >= 0) { + if (isCohere) { + // Clamp to 0.01 -> 0.99 + generate_data['top_p'] = Math.min(Math.max(Number(oai_settings.top_p_openai), 0.01), 0.99); + generate_data['top_k'] = Number(oai_settings.top_k_openai); + // Clamp to 0 -> 1 + generate_data['frequency_penalty'] = Math.min(Math.max(Number(oai_settings.freq_pen_openai), 0), 1); + generate_data['presence_penalty'] = Math.min(Math.max(Number(oai_settings.pres_pen_openai), 0), 1); + generate_data['stop'] = getCustomStoppingStrings(5); + } + + if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere) && oai_settings.seed >= 0) { generate_data['seed'] = oai_settings.seed; } @@ -2597,6 +2613,7 @@ function loadOpenAISettings(data, settings) { oai_settings.openrouter_force_instruct = settings.openrouter_force_instruct ?? default_settings.openrouter_force_instruct; oai_settings.ai21_model = settings.ai21_model ?? default_settings.ai21_model; oai_settings.mistralai_model = settings.mistralai_model ?? default_settings.mistralai_model; + oai_settings.cohere_model = settings.cohere_model ?? default_settings.cohere_model; oai_settings.custom_model = settings.custom_model ?? default_settings.custom_model; oai_settings.custom_url = settings.custom_url ?? default_settings.custom_url; oai_settings.custom_include_body = settings.custom_include_body ?? default_settings.custom_include_body; @@ -2657,6 +2674,8 @@ function loadOpenAISettings(data, settings) { $(`#model_ai21_select option[value="${oai_settings.ai21_model}"`).attr('selected', true); $('#model_mistralai_select').val(oai_settings.mistralai_model); $(`#model_mistralai_select option[value="${oai_settings.mistralai_model}"`).attr('selected', true); + $('#model_cohere_select').val(oai_settings.cohere_model); + $(`#model_cohere_select option[value="${oai_settings.cohere_model}"`).attr('selected', true); $('#custom_model_id').val(oai_settings.custom_model); $('#custom_api_url_text').val(oai_settings.custom_url); $('#openai_max_context').val(oai_settings.openai_max_context); @@ -2893,6 +2912,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { openrouter_sort_models: settings.openrouter_sort_models, ai21_model: settings.ai21_model, mistralai_model: settings.mistralai_model, + cohere_model: settings.cohere_model, custom_model: settings.custom_model, custom_url: settings.custom_url, custom_include_body: settings.custom_include_body, @@ -3281,6 +3301,7 @@ function onSettingsPresetChange() { openrouter_sort_models: ['#openrouter_sort_models', 'openrouter_sort_models', false], ai21_model: ['#model_ai21_select', 'ai21_model', false], mistralai_model: ['#model_mistralai_select', 'mistralai_model', false], + cohere_model: ['#model_cohere_select', 'cohere_model', false], custom_model: ['#custom_model_id', 'custom_model', false], custom_url: ['#custom_api_url_text', 'custom_url', false], custom_include_body: ['#custom_include_body', 'custom_include_body', false], @@ -3496,6 +3517,11 @@ async function onModelChange() { $('#model_mistralai_select').val(oai_settings.mistralai_model); } + if ($(this).is('#model_cohere_select')) { + console.log('Cohere model changed to', value); + oai_settings.cohere_model = value; + } + if (value && $(this).is('#model_custom_select')) { console.log('Custom model changed to', value); oai_settings.custom_model = value; @@ -3619,6 +3645,26 @@ async function onModelChange() { $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input'); } + if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) { + if (oai_settings.max_context_unlocked) { + $('#openai_max_context').attr('max', unlocked_max); + } + else if (['command-light', 'command'].includes(oai_settings.cohere_model)) { + $('#openai_max_context').attr('max', max_4k); + } + else if (['command-light-nightly', 'command-nightly'].includes(oai_settings.cohere_model)) { + $('#openai_max_context').attr('max', max_8k); + } + else if (['command-r'].includes(oai_settings.cohere_model)) { + $('#openai_max_context').attr('max', max_128k); + } + else { + $('#openai_max_context').attr('max', max_4k); + } + oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context); + $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input'); + } + if (oai_settings.chat_completion_source == chat_completion_sources.AI21) { if (oai_settings.max_context_unlocked) { $('#openai_max_context').attr('max', unlocked_max); @@ -3812,6 +3858,19 @@ async function onConnectButtonClick(e) { } } + if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) { + const api_key_cohere = String($('#api_key_cohere').val()).trim(); + + if (api_key_cohere.length) { + await writeSecret(SECRET_KEYS.COHERE, api_key_cohere); + } + + if (!secret_state[SECRET_KEYS.COHERE]) { + console.log('No secret key saved for Cohere'); + return; + } + } + startStatusLoading(); saveSettingsDebounced(); await getStatusOpen(); @@ -3847,6 +3906,9 @@ function toggleChatCompletionForms() { else if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) { $('#model_mistralai_select').trigger('change'); } + else if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) { + $('#model_cohere_select').trigger('change'); + } else if (oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) { $('#model_custom_select').trigger('change'); } @@ -4499,6 +4561,7 @@ $(document).ready(async function () { $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange); $('#model_ai21_select').on('change', onModelChange); $('#model_mistralai_select').on('change', onModelChange); + $('#model_cohere_select').on('change', onModelChange); $('#model_custom_select').on('change', onModelChange); $('#settings_preset_openai').on('change', onSettingsPresetChange); $('#new_oai_preset').on('click', onNewPresetClick); diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index a6d82e5e7..a6bed1057 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -23,6 +23,7 @@ export const SECRET_KEYS = { NOMICAI: 'api_key_nomicai', KOBOLDCPP: 'api_key_koboldcpp', LLAMACPP: 'api_key_llamacpp', + COHERE: 'api_key_cohere', }; const INPUT_MAP = { @@ -47,6 +48,7 @@ const INPUT_MAP = { [SECRET_KEYS.NOMICAI]: '#api_key_nomicai', [SECRET_KEYS.KOBOLDCPP]: '#api_key_koboldcpp', [SECRET_KEYS.LLAMACPP]: '#api_key_llamacpp', + [SECRET_KEYS.COHERE]: '#api_key_cohere', }; async function clearSecret() { diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index aef1de058..70042dd3c 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1660,6 +1660,7 @@ function modelCallback(_, model) { { id: 'model_google_select', api: 'openai', type: chat_completion_sources.MAKERSUITE }, { id: 'model_mistralai_select', api: 'openai', type: chat_completion_sources.MISTRALAI }, { id: 'model_custom_select', api: 'openai', type: chat_completion_sources.CUSTOM }, + { id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE }, { id: 'model_novel_select', api: 'novel', type: null }, { id: 'horde_model', api: 'koboldhorde', type: null }, ]; diff --git a/src/constants.js b/src/constants.js index db113a92c..918374eab 100644 --- a/src/constants.js +++ b/src/constants.js @@ -162,6 +162,7 @@ const CHAT_COMPLETION_SOURCES = { MAKERSUITE: 'makersuite', MISTRALAI: 'mistralai', CUSTOM: 'custom', + COHERE: 'cohere', }; const UPLOADS_PATH = './uploads'; diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index c695e230a..8fe7cb6bf 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -1,10 +1,11 @@ const express = require('express'); const fetch = require('node-fetch').default; +const Readable = require('stream').Readable; const { jsonParser } = require('../../express-common'); const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants'); const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util'); -const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt } = require('../../prompt-converters'); +const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages } = require('../../prompt-converters'); const { readSecret, SECRET_KEYS } = require('../secrets'); const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sentencepieceTokenizers, TEXT_COMPLETION_MODELS } = require('../tokenizers'); @@ -12,6 +13,61 @@ const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sente const API_OPENAI = 'https://api.openai.com/v1'; const API_CLAUDE = 'https://api.anthropic.com/v1'; const API_MISTRAL = 'https://api.mistral.ai/v1'; +const API_COHERE = 'https://api.cohere.ai/v1'; + +/** + * Ollama strikes back. Special boy #2's steaming routine. + * Wrap this abomination into proper SSE stream, again. + * @param {import('node-fetch').Response} jsonStream JSON stream + * @param {import('express').Request} request Express request + * @param {import('express').Response} response Express response + * @returns {Promise<any>} Nothing valuable + */ +async function parseCohereStream(jsonStream, request, response) { + try { + let partialData = ''; + jsonStream.body.on('data', (data) => { + const chunk = data.toString(); + partialData += chunk; + while (true) { + let json; + try { + json = JSON.parse(partialData); + } catch (e) { + break; + } + if (json.event_type === 'text-generation') { + const text = json.text || ''; + const chunk = { choices: [{ text }] }; + response.write(`data: ${JSON.stringify(chunk)}\n\n`); + partialData = ''; + } else { + partialData = ''; + break; + } + } + }); + + request.socket.on('close', function () { + if (jsonStream.body instanceof Readable) jsonStream.body.destroy(); + response.end(); + }); + + jsonStream.body.on('end', () => { + console.log('Streaming request finished'); + response.write('data: [DONE]\n\n'); + response.end(); + }); + } catch (error) { + console.log('Error forwarding streaming response:', error); + if (!response.headersSent) { + return response.status(500).send({ error: true }); + } else { + return response.end(); + } + } +} + /** * Sends a request to Claude API. * @param {express.Request} request Express request @@ -460,6 +516,85 @@ async function sendMistralAIRequest(request, response) { } } +async function sendCohereRequest(request, response) { + const apiKey = readSecret(SECRET_KEYS.COHERE); + const controller = new AbortController(); + request.socket.removeAllListeners('close'); + request.socket.on('close', function () { + controller.abort(); + }); + + if (!apiKey) { + console.log('Cohere API key is missing.'); + return response.status(400).send({ error: true }); + } + + try { + const convertedHistory = convertCohereMessages(request.body.messages); + + // https://docs.cohere.com/reference/chat + const requestBody = { + stream: Boolean(request.body.stream), + model: request.body.model, + message: convertedHistory.userPrompt, + preamble: convertedHistory.systemPrompt, + chat_history: convertedHistory.chatHistory, + temperature: request.body.temperature, + max_tokens: request.body.max_tokens, + k: request.body.top_k, + p: request.body.top_p, + seed: request.body.seed, + stop_sequences: request.body.stop, + frequency_penalty: request.body.frequency_penalty, + presence_penalty: request.body.presence_penalty, + prompt_truncation: 'AUTO_PRESERVE_ORDER', + connectors: [], // TODO + documents: [], + tools: [], + tool_results: [], + search_queries_only: false, + }; + + console.log('Cohere request:', requestBody); + + const config = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + apiKey, + }, + body: JSON.stringify(requestBody), + signal: controller.signal, + timeout: 0, + }; + + const apiUrl = API_COHERE + '/chat'; + + if (request.body.stream) { + const stream = await fetch(apiUrl, config); + parseCohereStream(stream, request, response); + } else { + const generateResponse = await fetch(apiUrl, config); + if (!generateResponse.ok) { + console.log(`Cohere API returned error: ${generateResponse.status} ${generateResponse.statusText} ${await generateResponse.text()}`); + // a 401 unauthorized response breaks the frontend auth, so return a 500 instead. prob a better way of dealing with this. + // 401s are already handled by the streaming processor and dont pop up an error toast, that should probably be fixed too. + return response.status(generateResponse.status === 401 ? 500 : generateResponse.status).send({ error: true }); + } + const generateResponseJson = await generateResponse.json(); + console.log('Cohere response:', generateResponseJson); + return response.send(generateResponseJson); + } + } catch (error) { + console.log('Error communicating with Cohere API: ', error); + if (!response.headersSent) { + response.send({ error: true }); + } else { + response.end(); + } + } +} + const router = express.Router(); router.post('/status', jsonParser, async function (request, response_getstatus_openai) { @@ -487,6 +622,10 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o api_key_openai = readSecret(SECRET_KEYS.CUSTOM); headers = {}; mergeObjectWithYaml(headers, request.body.custom_include_headers); + } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COHERE) { + api_url = API_COHERE; + api_key_openai = readSecret(SECRET_KEYS.COHERE); + headers = {}; } else { console.log('This chat completion source is not supported yet.'); return response_getstatus_openai.status(400).send({ error: true }); @@ -510,6 +649,10 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o const data = await response.json(); response_getstatus_openai.send(data); + if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COHERE && Array.isArray(data?.models)) { + data.data = data.models.map(model => ({ id: model.name, ...model })); + } + if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER && Array.isArray(data?.data)) { let models = []; @@ -635,6 +778,7 @@ router.post('/generate', jsonParser, function (request, response) { case CHAT_COMPLETION_SOURCES.AI21: return sendAI21Request(request, response); case CHAT_COMPLETION_SOURCES.MAKERSUITE: return sendMakerSuiteRequest(request, response); case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response); + case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response); } let apiUrl; diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index 55c5df008..afd41a1f7 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -35,6 +35,7 @@ const SECRET_KEYS = { NOMICAI: 'api_key_nomicai', KOBOLDCPP: 'api_key_koboldcpp', LLAMACPP: 'api_key_llamacpp', + COHERE: 'api_key_cohere', }; // These are the keys that are safe to expose, even if allowKeysExposure is false diff --git a/src/polyfill.js b/src/polyfill.js new file mode 100644 index 000000000..7bed18a1f --- /dev/null +++ b/src/polyfill.js @@ -0,0 +1,8 @@ +if (!Array.prototype.findLastIndex) { + Array.prototype.findLastIndex = function (callback, thisArg) { + for (let i = this.length - 1; i >= 0; i--) { + if (callback.call(thisArg, this[i], i, this)) return i; + } + return -1; + }; +} diff --git a/src/prompt-converters.js b/src/prompt-converters.js index 42f7abaf7..72b75e223 100644 --- a/src/prompt-converters.js +++ b/src/prompt-converters.js @@ -1,3 +1,5 @@ +require('./polyfill.js'); + /** * Convert a prompt from the ChatML objects to the format used by Claude. * @param {object[]} messages Array of messages @@ -188,6 +190,64 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi return { messages: mergedMessages, systemPrompt: systemPrompt.trim() }; } +/** + * Convert a prompt from the ChatML objects to the format used by Cohere. + * @param {object[]} messages Array of messages + * @param {string} charName Character name + * @param {string} userName User name + * @returns {{systemPrompt: string, chatHistory: object[], userPrompt: string}} Prompt for Cohere + */ +function convertCohereMessages(messages, charName = '', userName = '') { + const roleMap = { + 'system': 'SYSTEM', + 'user': 'USER', + 'assistant': 'CHATBOT', + }; + const placeholder = '[Start a new chat]'; + let systemPrompt = ''; + + // Collect all the system messages up until the first instance of a non-system message, and then remove them from the messages array. + let i; + for (i = 0; i < messages.length; i++) { + if (messages[i].role !== 'system') { + break; + } + // Append example names if not already done by the frontend (e.g. for group chats). + if (userName && messages[i].name === 'example_user') { + if (!messages[i].content.startsWith(`${userName}: `)) { + messages[i].content = `${userName}: ${messages[i].content}`; + } + } + if (charName && messages[i].name === 'example_assistant') { + if (!messages[i].content.startsWith(`${charName}: `)) { + messages[i].content = `${charName}: ${messages[i].content}`; + } + } + systemPrompt += `${messages[i].content}\n\n`; + } + + messages.splice(0, i); + + if (messages.length === 0) { + messages.unshift({ + role: 'user', + content: placeholder, + }); + } + + const lastNonSystemMessageIndex = messages.findLastIndex(msg => msg.role === 'user' || msg.role === 'assistant'); + const userPrompt = messages.slice(lastNonSystemMessageIndex).map(msg => msg.content).join('\n\n') || placeholder; + + const chatHistory = messages.slice(0, lastNonSystemMessageIndex).map(msg => { + return { + role: roleMap[msg.role] || 'USER', + message: msg.content, + }; + }); + + return { systemPrompt: systemPrompt.trim(), chatHistory, userPrompt }; +} + /** * Convert a prompt from the ChatML objects to the format used by Google MakerSuite models. * @param {object[]} messages Array of messages @@ -300,4 +360,5 @@ module.exports = { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, + convertCohereMessages, }; From 4f6127c8f22e92e79abd4c7f1c0a5e0234cde32a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 2 Apr 2024 01:16:25 +0300 Subject: [PATCH 89/99] Actually support comma-split syntax in random --- public/scripts/macros.js | 5 ++++- public/scripts/templates/macros.html | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/scripts/macros.js b/public/scripts/macros.js index a8d178604..cd40f0245 100644 --- a/public/scripts/macros.js +++ b/public/scripts/macros.js @@ -190,7 +190,10 @@ function randomReplace(input, emptyListPlaceholder = '') { input = input.replace(randomPatternNew, (match, listString) => { //split on double colons instead of commas to allow for commas inside random items - const list = listString.split('::').filter(item => item.length > 0); + const list = listString.includes('::') + ? listString.split('::').filter(item => item.length > 0) + : listString.split(',').map(item => item.trim()).filter(item => item.length > 0); + if (list.length === 0) { return emptyListPlaceholder; } diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html index 1c27718cc..5ba45c9f9 100644 --- a/public/scripts/templates/macros.html +++ b/public/scripts/templates/macros.html @@ -34,7 +34,7 @@ <li><tt>{{idle_duration}}</tt> – the time since the last user message was sent</li> <li><tt>{{bias "text here"}}</tt> – sets a behavioral bias for the AI until the next user input. Quotes around the text are important.</li> <li><tt>{{roll:(formula)}}</tt> – rolls a dice. (ex: <tt>>{{roll:1d6}}</tt> will roll a 6-sided dice and return a number between 1 and 6)</li> - <li><tt>{{random::(args)}}</tt> – returns a random item from the list. (ex: <tt>{{random::1,2,3,4}}</tt> will return 1 of the 4 numbers at random. Works with text lists too.</li> + <li><tt>{{random::(args)}}</tt> – returns a random item from the list. (ex: <tt>{{random::1::2::3::4}}</tt> will return 1 of the 4 numbers at random. Works with text lists too.</li> <li><tt>{{random::(arg1)::(arg2)}}</tt> – alternative syntax for random that allows to use commas in the list items.</li> <li><tt>{{pick::(args)}}</tt> – picks a random item from the list. Works the same as {{random}}, with the same possible syntax options, but the pick will stay consistent for this chat once picked and won't be re-rolled on consecutive messages and prompt processing.</li> <li><tt>{{banned "text here"}}</tt> – dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.</li> From d7817d1882ac4c912efda6ef76d59d1831301fdb Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 2 Apr 2024 01:24:36 +0300 Subject: [PATCH 90/99] Set default newline suffix if wrap is enabled and there's no suffix in instruct template --- public/scripts/instruct-mode.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index c7aa812ce..033794009 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -332,6 +332,10 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata prefix = prefix.replace(/{{name}}/gi, name || 'System'); } + if (!suffix && power_user.instruct.wrap) { + suffix = '\n'; + } + const separator = power_user.instruct.wrap ? '\n' : ''; const textArray = includeNames ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix]; const text = textArray.filter(x => x).join(separator); @@ -386,6 +390,14 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { inputPrefix = inputPrefix.replace(/{{name}}/gi, name1); outputPrefix = outputPrefix.replace(/{{name}}/gi, name2); + + if (!inputSuffix && power_user.instruct.wrap) { + inputSuffix = '\n'; + } + + if (!outputSuffix && power_user.instruct.wrap) { + outputSuffix = '\n'; + } } const separator = power_user.instruct.wrap ? '\n' : ''; From 03a203d60780356846c5b6c4ebdfe0713e10d030 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 2 Apr 2024 01:28:31 +0300 Subject: [PATCH 91/99] Fix macro help --- public/scripts/templates/macros.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html index 5ba45c9f9..a0f18ab9d 100644 --- a/public/scripts/templates/macros.html +++ b/public/scripts/templates/macros.html @@ -34,7 +34,7 @@ <li><tt>{{idle_duration}}</tt> – the time since the last user message was sent</li> <li><tt>{{bias "text here"}}</tt> – sets a behavioral bias for the AI until the next user input. Quotes around the text are important.</li> <li><tt>{{roll:(formula)}}</tt> – rolls a dice. (ex: <tt>>{{roll:1d6}}</tt> will roll a 6-sided dice and return a number between 1 and 6)</li> - <li><tt>{{random::(args)}}</tt> – returns a random item from the list. (ex: <tt>{{random::1::2::3::4}}</tt> will return 1 of the 4 numbers at random. Works with text lists too.</li> + <li><tt>{{random:(args)}}</tt> – returns a random item from the list. (ex: <tt>{{random:1,2,3,4}}</tt> will return 1 of the 4 numbers at random. Works with text lists too.</li> <li><tt>{{random::(arg1)::(arg2)}}</tt> – alternative syntax for random that allows to use commas in the list items.</li> <li><tt>{{pick::(args)}}</tt> – picks a random item from the list. Works the same as {{random}}, with the same possible syntax options, but the pick will stay consistent for this chat once picked and won't be re-rolled on consecutive messages and prompt processing.</li> <li><tt>{{banned "text here"}}</tt> – dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.</li> From 9b24397f5af97f0bbc9c8b8f543d92481027ec54 Mon Sep 17 00:00:00 2001 From: Wolfsblvt <wolfsblvt@gmx.de> Date: Tue, 2 Apr 2024 00:42:13 +0200 Subject: [PATCH 92/99] Unify {{pick}} and {{random}} regex - Allow all four possible syntax forms, just to make this easier: {{random::one::two}}, {{random:one::two}}, {{random::one,two}} and {{random:one,two}} --- public/scripts/macros.js | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/public/scripts/macros.js b/public/scripts/macros.js index cd40f0245..fcda91cf6 100644 --- a/public/scripts/macros.js +++ b/public/scripts/macros.js @@ -185,8 +185,7 @@ function getTimeSinceLastMessage() { } function randomReplace(input, emptyListPlaceholder = '') { - const randomPatternNew = /{{random\s?::\s?([^}]+)}}/gi; - const randomPatternOld = /{{random\s?:\s?([^}]+)}}/gi; + const randomPatternNew = /{{random\s?::?\s?([^}]+)}}/gi; input = input.replace(randomPatternNew, (match, listString) => { //split on double colons instead of commas to allow for commas inside random items @@ -202,20 +201,11 @@ function randomReplace(input, emptyListPlaceholder = '') { //trim() at the end to allow for empty random values return list[randomIndex].trim(); }); - input = input.replace(randomPatternOld, (match, listString) => { - const list = listString.split(',').map(item => item.trim()).filter(item => item.length > 0); - if (list.length === 0) { - return emptyListPlaceholder; - } - const rng = new Math.seedrandom('added entropy.', { entropy: true }); - const randomIndex = Math.floor(rng() * list.length); - return list[randomIndex]; - }); return input; } function pickReplace(input, rawContent, emptyListPlaceholder = '') { - const pickPattern = /{{pick\s?::\s?([^}]+)}}/gi; + const pickPattern = /{{pick\s?::?\s?([^}]+)}}/gi; const chatIdHash = getStringHash(getCurrentChatId()); const rawContentHash = getStringHash(rawContent); From 3632631997f7107032d78803dc3f1f873b914300 Mon Sep 17 00:00:00 2001 From: Wolfsblvt <wolfsblvt@gmx.de> Date: Tue, 2 Apr 2024 01:02:02 +0200 Subject: [PATCH 93/99] random and pick allow empty items and trim correctly --- public/scripts/macros.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/public/scripts/macros.js b/public/scripts/macros.js index fcda91cf6..504b05596 100644 --- a/public/scripts/macros.js +++ b/public/scripts/macros.js @@ -185,45 +185,46 @@ function getTimeSinceLastMessage() { } function randomReplace(input, emptyListPlaceholder = '') { - const randomPatternNew = /{{random\s?::?\s?([^}]+)}}/gi; + const randomPattern = /{{random\s?::?([^}]+)}}/gi; - input = input.replace(randomPatternNew, (match, listString) => { - //split on double colons instead of commas to allow for commas inside random items + input = input.replace(randomPattern, (match, listString) => { + // Split on either double colons or comma. If comma is the separator, we are also trimming all items. const list = listString.includes('::') - ? listString.split('::').filter(item => item.length > 0) - : listString.split(',').map(item => item.trim()).filter(item => item.length > 0); + ? listString.split('::') + : listString.split(',').map(item => item.trim()); if (list.length === 0) { return emptyListPlaceholder; } const rng = new Math.seedrandom('added entropy.', { entropy: true }); const randomIndex = Math.floor(rng() * list.length); - //trim() at the end to allow for empty random values - return list[randomIndex].trim(); + return list[randomIndex]; }); return input; } function pickReplace(input, rawContent, emptyListPlaceholder = '') { - const pickPattern = /{{pick\s?::?\s?([^}]+)}}/gi; + const pickPattern = /{{pick\s?::?([^}]+)}}/gi; const chatIdHash = getStringHash(getCurrentChatId()); const rawContentHash = getStringHash(rawContent); return input.replace(pickPattern, (match, listString, offset) => { + // Split on either double colons or comma. If comma is the separator, we are also trimming all items. const list = listString.includes('::') - ? listString.split('::').filter(item => item.length > 0) - : listString.split(',').map(item => item.trim()).filter(item => item.length > 0); + ? listString.split('::') + : listString.split(',').map(item => item.trim()); if (list.length === 0) { return emptyListPlaceholder; } + // We build a hash seed based on: unique chat file, raw content, and the placement inside this content + // This allows us to get unique but repeatable picks in nearly all cases const combinedSeedString = `${chatIdHash}-${rawContentHash}-${offset}`; const finalSeed = getStringHash(combinedSeedString); const rng = new Math.seedrandom(finalSeed); - const randomIndex = Math.floor(rng() * list.length); - return list[randomIndex].trim(); + return list[randomIndex]; }); } From ac0a431cbec5fc94b5ae3faaf23b97cf5429a74a Mon Sep 17 00:00:00 2001 From: Aisu Wata <aisu.wata0@gmail.com> Date: Mon, 1 Apr 2024 22:44:16 -0300 Subject: [PATCH 94/99] fix: avoid skipping example chats in a dialogue when close to quota --- public/scripts/openai.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/public/scripts/openai.js b/public/scripts/openai.js index e63247dff..0c30c0640 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -818,18 +818,20 @@ function populateDialogueExamples(prompts, chatCompletion, messageExamples) { if (chatCompletion.canAfford(newExampleChat)) chatCompletion.insert(newExampleChat, 'dialogueExamples'); - dialogue.forEach((prompt, promptIndex) => { + for (let promptIndex = 0; promptIndex < dialogue.length; promptIndex++) { + const prompt = dialogue[promptIndex]; const role = 'system'; const content = prompt.content || ''; const identifier = `dialogueExamples ${dialogueIndex}-${promptIndex}`; const chatMessage = new Message(role, content, identifier); chatMessage.setName(prompt.name); - if (chatCompletion.canAfford(chatMessage)) { - chatCompletion.insert(chatMessage, 'dialogueExamples'); - examplesAdded++; + if (!chatCompletion.canAfford(chatMessage)) { + break; } - }); + chatCompletion.insert(chatMessage, 'dialogueExamples'); + examplesAdded++; + } if (0 === examplesAdded) { chatCompletion.removeLastFrom('dialogueExamples'); From 4f0322351ecb5eba52c923ccc9d4188d3c06db2f Mon Sep 17 00:00:00 2001 From: kingbri <bdashore3@proton.me> Date: Tue, 2 Apr 2024 00:59:21 -0400 Subject: [PATCH 95/99] Sampling: Add ability to send JSON schemas TabbyAPI supports the ability to send JSON schemas with prompts in addition to EBNF strings supported by outlines. Add an extra box for TabbyAPI only. Signed-off-by: kingbri <bdashore3@proton.me> --- public/index.html | 11 +++++++++++ public/scripts/textgen-settings.js | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/public/index.html b/public/index.html index 928061b0c..5d93ef362 100644 --- a/public/index.html +++ b/public/index.html @@ -1516,6 +1516,17 @@ </div> </div> </div> + <div data-newbie-hidden id="json_schema_block" data-tg-type="tabby" class="wide100p"> + <hr class="wide100p"> + <h4 class="wide100p textAlignCenter"><span data-i18n="JSON Schema">JSON Schema</span> + <a href="https://json-schema.org/learn/getting-started-step-by-step" target="_blank"> + <small> + <div class="fa-solid fa-circle-question note-link-span"></div> + </small> + </a> + </h4> + <textarea id="tabby_json_schema" rows="4" class="text_pole textarea_compact monospace" data-i18n="[placeholder]Type in the desired JSON schema" placeholder="Type in the desired JSON schema"></textarea> + </div> <div data-newbie-hidden id="grammar_block_ooba" class="wide100p"> <hr class="wide100p"> <h4 class="wide100p textAlignCenter"> diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index fdd2380f7..4d7fe4007 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -128,6 +128,7 @@ const settings = { guidance_scale: 1, negative_prompt: '', grammar_string: '', + json_schema: {}, banned_tokens: '', sampler_priority: OOBA_DEFAULT_ORDER, samplers: LLAMACPP_DEFAULT_ORDER, @@ -201,6 +202,7 @@ const setting_names = [ 'guidance_scale', 'negative_prompt', 'grammar_string', + 'json_schema', 'banned_tokens', 'legacy_api', //'n_aphrodite', @@ -562,6 +564,16 @@ jQuery(function () { }, }); + $('#tabby_json_schema').on('input', function () { + const json_schema_string = $(this).val(); + + // Ignore errors from here + try { + settings.json_schema = JSON.parse(json_schema_string ?? "{}"); + } catch {} + saveSettingsDebounced(); + }); + $('#textgenerationwebui_default_order').on('click', function () { sortOobaItemsByOrder(OOBA_DEFAULT_ORDER); settings.sampler_priority = OOBA_DEFAULT_ORDER; @@ -757,6 +769,12 @@ function setSettingByName(setting, value, trigger) { return; } + if ('json_schema' === setting) { + settings.json_schema = value ?? {} + $('#tabby_json_schema').text(JSON.stringify(settings.json_schema, null, 2)) + return; + } + const isCheckbox = $(`#${setting}_textgenerationwebui`).attr('type') == 'checkbox'; const isText = $(`#${setting}_textgenerationwebui`).attr('type') == 'text' || $(`#${setting}_textgenerationwebui`).is('textarea'); if (isCheckbox) { @@ -1027,6 +1045,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, 'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1, 'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '', 'grammar_string': settings.grammar_string, + 'json_schema': settings.type === TABBY ? settings.json_schema : undefined, // llama.cpp aliases. In case someone wants to use LM Studio as Text Completion API 'repeat_penalty': settings.rep_pen, 'tfs_z': settings.tfs, From 5210db567981ff46873c493dbbc69040196e4d8b Mon Sep 17 00:00:00 2001 From: kingbri <bdashore3@proton.me> Date: Tue, 2 Apr 2024 01:01:59 -0400 Subject: [PATCH 96/99] Format Signed-off-by: kingbri <bdashore3@proton.me> --- public/scripts/textgen-settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 4d7fe4007..11d2221cd 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -569,7 +569,7 @@ jQuery(function () { // Ignore errors from here try { - settings.json_schema = JSON.parse(json_schema_string ?? "{}"); + settings.json_schema = JSON.parse(json_schema_string ?? '{}'); } catch {} saveSettingsDebounced(); }); From 0b76e1d3509230f0093fed53ea6878dbeb8605d6 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:23:29 +0300 Subject: [PATCH 97/99] Fix schema not loading from presets. Fix ESLint warnings --- public/scripts/textgen-settings.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 11d2221cd..d7cee2625 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -565,12 +565,13 @@ jQuery(function () { }); $('#tabby_json_schema').on('input', function () { - const json_schema_string = $(this).val(); + const json_schema_string = String($(this).val()); - // Ignore errors from here try { settings.json_schema = JSON.parse(json_schema_string ?? '{}'); - } catch {} + } catch { + // Ignore errors from here + } saveSettingsDebounced(); }); @@ -770,8 +771,8 @@ function setSettingByName(setting, value, trigger) { } if ('json_schema' === setting) { - settings.json_schema = value ?? {} - $('#tabby_json_schema').text(JSON.stringify(settings.json_schema, null, 2)) + settings.json_schema = value ?? {}; + $('#tabby_json_schema').val(JSON.stringify(settings.json_schema, null, 2)); return; } From 04edf32ef07ed482dba65a54406490771969db67 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:29:49 +0300 Subject: [PATCH 98/99] Do not send dynatemp to backends if disabled --- public/scripts/textgen-settings.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index d7cee2625..6653149ba 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -1003,11 +1003,11 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, 'length_penalty': settings.length_penalty, 'early_stopping': settings.early_stopping, 'add_bos_token': settings.add_bos_token, - 'dynamic_temperature': settings.dynatemp, - 'dynatemp_low': settings.dynatemp ? settings.min_temp : 1, - 'dynatemp_high': settings.dynatemp ? settings.max_temp : 1, - 'dynatemp_range': settings.dynatemp ? (settings.max_temp - settings.min_temp) / 2 : 0, - 'dynatemp_exponent': settings.dynatemp ? settings.dynatemp_exponent : 1, + 'dynamic_temperature': settings.dynatemp ? true : undefined, + 'dynatemp_low': settings.dynatemp ? settings.min_temp : undefined, + 'dynatemp_high': settings.dynatemp ? settings.max_temp : undefined, + 'dynatemp_range': settings.dynatemp ? (settings.max_temp - settings.min_temp) / 2 : undefined, + 'dynatemp_exponent': settings.dynatemp ? settings.dynatemp_exponent : undefined, 'smoothing_factor': settings.smoothing_factor, 'smoothing_curve': settings.smoothing_curve, 'max_tokens_second': settings.max_tokens_second, From c0fffde739d76bfef5a5c96bd888909129b2330f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 2 Apr 2024 13:15:31 +0300 Subject: [PATCH 99/99] Fix SD "Raw last message" in groups --- .../extensions/stable-diffusion/index.js | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index abdc52925..f64ba5156 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -1260,7 +1260,18 @@ async function loadAutoSamplers() { async function loadDrawthingsSamplers() { // The app developer doesn't provide an API to get these yet - return ["UniPC", "DPM++ 2M Karras", "Euler a", "DPM++ SDE Karras", "PLMS", "DDIM", "LCM", "Euler A Substep", "DPM++ SDE Substep", "TCD"]; + return [ + 'UniPC', + 'DPM++ 2M Karras', + 'Euler a', + 'DPM++ SDE Karras', + 'PLMS', + 'DDIM', + 'LCM', + 'Euler A Substep', + 'DPM++ SDE Substep', + 'TCD', + ]; } async function loadVladSamplers() { @@ -1490,11 +1501,11 @@ async function loadDrawthingsModels() { extension_settings.sd.model = currentModel; } - const data = [{value: currentModel, text: currentModel}]; + const data = [{ value: currentModel, text: currentModel }]; return data; } catch (error) { - console.log("Error loading DrawThings API models:", error); + console.log('Error loading DrawThings API models:', error); return []; } } @@ -1817,7 +1828,10 @@ function getRawLastMessage() { continue; } - return message.mes; + return { + mes: message.mes, + original_avatar: message.original_avatar, + }; } toastr.warning('No usable messages found.', 'Image Generation'); @@ -1825,10 +1839,17 @@ function getRawLastMessage() { }; const context = getContext(); - const lastMessage = getLastUsableMessage(), - characterDescription = context.characters[context.characterId].description, - situation = context.characters[context.characterId].scenario; - return `((${processReply(lastMessage)})), (${processReply(situation)}:0.7), (${processReply(characterDescription)}:0.5)`; + const lastMessage = getLastUsableMessage(); + const character = context.groupId + ? context.characters.find(c => c.avatar === lastMessage.original_avatar) + : context.characters[context.characterId]; + + if (!character) { + console.debug('Character not found, using raw message.'); + return processReply(lastMessage.mes); + } + + return `((${processReply(lastMessage.mes)})), (${processReply(character.scenario)}:0.7), (${processReply(character.description)}:0.5)`; } async function generatePicture(args, trigger, message, callback) { @@ -2900,7 +2921,7 @@ jQuery(async () => { $('#sd_novel_anlas_guard').on('input', onNovelAnlasGuardInput); $('#sd_novel_view_anlas').on('click', onViewAnlasClick); $('#sd_novel_sm').on('input', onNovelSmInput); - $('#sd_novel_sm_dyn').on('input', onNovelSmDynInput);; + $('#sd_novel_sm_dyn').on('input', onNovelSmDynInput); $('#sd_comfy_validate').on('click', validateComfyUrl); $('#sd_comfy_url').on('input', onComfyUrlInput); $('#sd_comfy_workflow').on('change', onComfyWorkflowChange);