mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
[wip] Persona lorebook
This commit is contained in:
@ -4813,6 +4813,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="sync_name_button" class="menu_button fa-solid fa-sync" title="Click to set user name for all messages" data-i18n="[title]Click to set user name for all messages">
|
<div id="sync_name_button" class="menu_button fa-solid fa-sync" title="Click to set user name for all messages" data-i18n="[title]Click to set user name for all messages">
|
||||||
</div>
|
</div>
|
||||||
|
<div id="persona_lore_button" class="menu_button fa-solid fa-globe" title="Persona Lore" data-i18n="[title]Persona Lore">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 data-i18n="Persona Description">Persona Description</h4>
|
<h4 data-i18n="Persona Description">Persona Description</h4>
|
||||||
@ -5539,26 +5541,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- templates for JS to reuse when needed -->
|
<!-- templates for JS to reuse when needed -->
|
||||||
<div id="chat_world_template" class="template_element">
|
|
||||||
<div class="chat_world range-block flexFlowColumn flex-container">
|
|
||||||
<div class="range-block-title">
|
|
||||||
<h4 data-i18n="Chat Lorebook"><!-- This data-i18n attribute is kept for backward compatibility, use the ones below when translating -->
|
|
||||||
<span data-i18n="Chat Lorebook for">Chat Lorebook for</span> <span class="chat_name"></span>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
|
|
||||||
<span data-i18n="chat_world_template_txt">
|
|
||||||
A selected World Info will be bound to this chat. When generating an AI reply,
|
|
||||||
it will be combined with the entries from global and character lorebooks.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="range-block-range wide100p">
|
|
||||||
<select class="chat_world_info_selector wide100p">
|
|
||||||
<option value="">--- None ---</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="character_world_template" class="template_element">
|
<div id="character_world_template" class="template_element">
|
||||||
<div class="character_world range-block flexFlowColumn flex-container">
|
<div class="character_world range-block flexFlowColumn flex-container">
|
||||||
<div class="range-block-title">
|
<div class="range-block-title">
|
||||||
|
@ -21,8 +21,10 @@ import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSuppor
|
|||||||
import { debounce_timeout } from './constants.js';
|
import { debounce_timeout } from './constants.js';
|
||||||
import { FILTER_TYPES, FilterHelper } from './filters.js';
|
import { FILTER_TYPES, FilterHelper } from './filters.js';
|
||||||
import { selected_group } from './group-chats.js';
|
import { selected_group } from './group-chats.js';
|
||||||
import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js';
|
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
|
import { world_names } from './world-info.js';
|
||||||
|
import { renderTemplateAsync } from './templates.js';
|
||||||
|
|
||||||
let savePersonasPage = 0;
|
let savePersonasPage = 0;
|
||||||
const GRID_STORAGE_KEY = 'Personas_GridView';
|
const GRID_STORAGE_KEY = 'Personas_GridView';
|
||||||
@ -375,6 +377,7 @@ export function initPersona(avatarId, personaName, personaDescription) {
|
|||||||
position: persona_description_positions.IN_PROMPT,
|
position: persona_description_positions.IN_PROMPT,
|
||||||
depth: DEFAULT_DEPTH,
|
depth: DEFAULT_DEPTH,
|
||||||
role: DEFAULT_ROLE,
|
role: DEFAULT_ROLE,
|
||||||
|
lorebook: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
@ -418,6 +421,7 @@ export async function convertCharacterToPersona(characterId = null) {
|
|||||||
position: persona_description_positions.IN_PROMPT,
|
position: persona_description_positions.IN_PROMPT,
|
||||||
depth: DEFAULT_DEPTH,
|
depth: DEFAULT_DEPTH,
|
||||||
role: DEFAULT_ROLE,
|
role: DEFAULT_ROLE,
|
||||||
|
lorebook: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the user is currently using this persona, update the description
|
// If the user is currently using this persona, update the description
|
||||||
@ -461,6 +465,7 @@ export function setPersonaDescription() {
|
|||||||
.val(power_user.persona_description_role)
|
.val(power_user.persona_description_role)
|
||||||
.find(`option[value="${power_user.persona_description_role}"]`)
|
.find(`option[value="${power_user.persona_description_role}"]`)
|
||||||
.prop('selected', String(true));
|
.prop('selected', String(true));
|
||||||
|
$('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook);
|
||||||
countPersonaDescriptionTokens();
|
countPersonaDescriptionTokens();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,6 +495,7 @@ async function updatePersonaNameIfExists(avatarId, newName) {
|
|||||||
position: persona_description_positions.IN_PROMPT,
|
position: persona_description_positions.IN_PROMPT,
|
||||||
depth: DEFAULT_DEPTH,
|
depth: DEFAULT_DEPTH,
|
||||||
role: DEFAULT_ROLE,
|
role: DEFAULT_ROLE,
|
||||||
|
lorebook: '',
|
||||||
};
|
};
|
||||||
console.log(`Created persona name for ${avatarId} as ${newName}`);
|
console.log(`Created persona name for ${avatarId} as ${newName}`);
|
||||||
}
|
}
|
||||||
@ -535,6 +541,7 @@ async function bindUserNameToPersona(e) {
|
|||||||
position: isCurrentPersona ? power_user.persona_description_position : persona_description_positions.IN_PROMPT,
|
position: isCurrentPersona ? power_user.persona_description_position : persona_description_positions.IN_PROMPT,
|
||||||
depth: isCurrentPersona ? power_user.persona_description_depth : DEFAULT_DEPTH,
|
depth: isCurrentPersona ? power_user.persona_description_depth : DEFAULT_DEPTH,
|
||||||
role: isCurrentPersona ? power_user.persona_description_role : DEFAULT_ROLE,
|
role: isCurrentPersona ? power_user.persona_description_role : DEFAULT_ROLE,
|
||||||
|
lorebook: isCurrentPersona ? power_user.persona_description_lorebook : '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -579,12 +586,20 @@ function selectCurrentPersona() {
|
|||||||
power_user.persona_description_position = descriptor.position ?? persona_description_positions.IN_PROMPT;
|
power_user.persona_description_position = descriptor.position ?? persona_description_positions.IN_PROMPT;
|
||||||
power_user.persona_description_depth = descriptor.depth ?? DEFAULT_DEPTH;
|
power_user.persona_description_depth = descriptor.depth ?? DEFAULT_DEPTH;
|
||||||
power_user.persona_description_role = descriptor.role ?? DEFAULT_ROLE;
|
power_user.persona_description_role = descriptor.role ?? DEFAULT_ROLE;
|
||||||
|
power_user.persona_description_lorebook = descriptor.lorebook ?? '';
|
||||||
} else {
|
} else {
|
||||||
power_user.persona_description = '';
|
power_user.persona_description = '';
|
||||||
power_user.persona_description_position = persona_description_positions.IN_PROMPT;
|
power_user.persona_description_position = persona_description_positions.IN_PROMPT;
|
||||||
power_user.persona_description_depth = DEFAULT_DEPTH;
|
power_user.persona_description_depth = DEFAULT_DEPTH;
|
||||||
power_user.persona_description_role = DEFAULT_ROLE;
|
power_user.persona_description_role = DEFAULT_ROLE;
|
||||||
power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE };
|
power_user.persona_description_lorebook = '';
|
||||||
|
power_user.persona_descriptions[user_avatar] = {
|
||||||
|
description: '',
|
||||||
|
position: persona_description_positions.IN_PROMPT,
|
||||||
|
depth: DEFAULT_DEPTH,
|
||||||
|
role: DEFAULT_ROLE,
|
||||||
|
lorebook: '',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setPersonaDescription();
|
setPersonaDescription();
|
||||||
@ -652,6 +667,7 @@ async function lockPersona() {
|
|||||||
position: persona_description_positions.IN_PROMPT,
|
position: persona_description_positions.IN_PROMPT,
|
||||||
depth: DEFAULT_DEPTH,
|
depth: DEFAULT_DEPTH,
|
||||||
role: DEFAULT_ROLE,
|
role: DEFAULT_ROLE,
|
||||||
|
lorebook: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -731,6 +747,7 @@ function onPersonaDescriptionInput() {
|
|||||||
position: Number($('#persona_description_position').find(':selected').val()),
|
position: Number($('#persona_description_position').find(':selected').val()),
|
||||||
depth: Number($('#persona_depth_value').val()),
|
depth: Number($('#persona_depth_value').val()),
|
||||||
role: Number($('#persona_depth_role').find(':selected').val()),
|
role: Number($('#persona_depth_role').find(':selected').val()),
|
||||||
|
lorebook: '',
|
||||||
};
|
};
|
||||||
power_user.persona_descriptions[user_avatar] = object;
|
power_user.persona_descriptions[user_avatar] = object;
|
||||||
}
|
}
|
||||||
@ -766,6 +783,43 @@ function onPersonaDescriptionDepthRoleInput() {
|
|||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onPersonaLoreButtonClick() {
|
||||||
|
const personaName = power_user.personas[user_avatar];
|
||||||
|
const selectedLorebook = power_user.persona_description_lorebook;
|
||||||
|
|
||||||
|
if (!personaName) {
|
||||||
|
toastr.warning(t`You must bind a name to this persona before you can set a lorebook.`, t`Persona name not set`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = $(await renderTemplateAsync('personaLorebook'));
|
||||||
|
|
||||||
|
const worldSelect = template.find('select');
|
||||||
|
template.find('.persona_name').text(personaName);
|
||||||
|
|
||||||
|
for (const worldName of world_names) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = worldName;
|
||||||
|
option.innerText = worldName;
|
||||||
|
option.selected = selectedLorebook === worldName;
|
||||||
|
worldSelect.append(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
worldSelect.on('change', function () {
|
||||||
|
power_user.persona_description_lorebook = String($(this).val());
|
||||||
|
|
||||||
|
if (power_user.personas[user_avatar]) {
|
||||||
|
const object = getOrCreatePersonaDescriptor();
|
||||||
|
object.lorebook = power_user.persona_description_lorebook;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook);
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
|
||||||
|
await callGenericPopup(template, POPUP_TYPE.TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
function onPersonaDescriptionPositionInput() {
|
function onPersonaDescriptionPositionInput() {
|
||||||
power_user.persona_description_position = Number(
|
power_user.persona_description_position = Number(
|
||||||
$('#persona_description_position').find(':selected').val(),
|
$('#persona_description_position').find(':selected').val(),
|
||||||
@ -789,6 +843,7 @@ function getOrCreatePersonaDescriptor() {
|
|||||||
position: power_user.persona_description_position,
|
position: power_user.persona_description_position,
|
||||||
depth: power_user.persona_description_depth,
|
depth: power_user.persona_description_depth,
|
||||||
role: power_user.persona_description_role,
|
role: power_user.persona_description_role,
|
||||||
|
lorebook: power_user.persona_description_lorebook,
|
||||||
};
|
};
|
||||||
power_user.persona_descriptions[user_avatar] = object;
|
power_user.persona_descriptions[user_avatar] = object;
|
||||||
}
|
}
|
||||||
@ -1038,6 +1093,7 @@ async function duplicatePersona(avatarId) {
|
|||||||
position: descriptor?.position ?? persona_description_positions.IN_PROMPT,
|
position: descriptor?.position ?? persona_description_positions.IN_PROMPT,
|
||||||
depth: descriptor?.depth ?? DEFAULT_DEPTH,
|
depth: descriptor?.depth ?? DEFAULT_DEPTH,
|
||||||
role: descriptor?.role ?? DEFAULT_ROLE,
|
role: descriptor?.role ?? DEFAULT_ROLE,
|
||||||
|
lorebook: descriptor?.lorebook ?? '',
|
||||||
};
|
};
|
||||||
|
|
||||||
await uploadUserAvatar(getUserAvatar(avatarId), newAvatarId);
|
await uploadUserAvatar(getUserAvatar(avatarId), newAvatarId);
|
||||||
@ -1055,6 +1111,7 @@ export function initPersonas() {
|
|||||||
$('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
|
$('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
|
||||||
$('#persona_depth_value').on('input', onPersonaDescriptionDepthValueInput);
|
$('#persona_depth_value').on('input', onPersonaDescriptionDepthValueInput);
|
||||||
$('#persona_depth_role').on('input', onPersonaDescriptionDepthRoleInput);
|
$('#persona_depth_role').on('input', onPersonaDescriptionDepthRoleInput);
|
||||||
|
$('#persona_lore_button').on('click', onPersonaLoreButtonClick);
|
||||||
$('#personas_backup').on('click', onBackupPersonas);
|
$('#personas_backup').on('click', onBackupPersonas);
|
||||||
$('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click'));
|
$('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click'));
|
||||||
$('#personas_restore_input').on('change', onPersonasRestoreInput);
|
$('#personas_restore_input').on('change', onPersonasRestoreInput);
|
||||||
|
@ -262,6 +262,7 @@ let power_user = {
|
|||||||
persona_description_position: persona_description_positions.IN_PROMPT,
|
persona_description_position: persona_description_positions.IN_PROMPT,
|
||||||
persona_description_role: 0,
|
persona_description_role: 0,
|
||||||
persona_description_depth: 2,
|
persona_description_depth: 2,
|
||||||
|
persona_description_lorebook: '',
|
||||||
persona_show_notifications: true,
|
persona_show_notifications: true,
|
||||||
persona_sort_order: 'asc',
|
persona_sort_order: 'asc',
|
||||||
|
|
||||||
@ -1925,7 +1926,7 @@ export function fuzzySearchPersonas(data, searchValue, fuzzySearchCaches = null)
|
|||||||
const mappedData = data.map(x => ({
|
const mappedData = data.map(x => ({
|
||||||
key: x,
|
key: x,
|
||||||
name: power_user.personas[x] ?? '',
|
name: power_user.personas[x] ?? '',
|
||||||
description: power_user.persona_descriptions[x]?.description ?? ''
|
description: power_user.persona_descriptions[x]?.description ?? '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const keys = [
|
const keys = [
|
||||||
|
18
public/scripts/templates/chatLorebook.html
Normal file
18
public/scripts/templates/chatLorebook.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<div class="chat_world range-block flexFlowColumn flex-container">
|
||||||
|
<div class="range-block-title">
|
||||||
|
<h4 data-i18n="Chat Lorebook"><!-- This data-i18n attribute is kept for backward compatibility, use the ones below when translating -->
|
||||||
|
<span data-i18n="Chat Lorebook for">Chat Lorebook for</span> <span class="chat_name"></span>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
|
||||||
|
<span data-i18n="chat_world_template_txt">
|
||||||
|
A selected World Info will be bound to this chat. When generating an AI reply,
|
||||||
|
it will be combined with the entries from global and character lorebooks.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="range-block-range wide100p">
|
||||||
|
<select class="chat_world_info_selector wide100p">
|
||||||
|
<option value="">--- None ---</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
18
public/scripts/templates/personaLorebook.html
Normal file
18
public/scripts/templates/personaLorebook.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<div class="persona_world range-block flexFlowColumn flex-container">
|
||||||
|
<div class="range-block-title">
|
||||||
|
<h4>
|
||||||
|
<span data-i18n="PErsona Lorebook for">Persona Lorebook for</span> <span class="persona_name"></span>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
|
||||||
|
<span data-i18n="persona_world_template_txt">
|
||||||
|
A selected World Info will be bound to this persona. When generating an AI reply,
|
||||||
|
it will be combined with the entries from global, character and chat lorebooks.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="range-block-range wide100p">
|
||||||
|
<select class="persona_world_info_selector wide100p">
|
||||||
|
<option value="">--- None ---</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -3548,6 +3548,11 @@ async function getCharacterLore() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (power_user.persona_description_lorebook === worldName) {
|
||||||
|
console.debug(`[WI] Character ${name}'s world ${worldName} is already activated in persona lore! Skipping...`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await loadWorldInfo(worldName);
|
const data = await loadWorldInfo(worldName);
|
||||||
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : [];
|
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : [];
|
||||||
entries = entries.concat(newEntries);
|
entries = entries.concat(newEntries);
|
||||||
@ -3598,11 +3603,45 @@ async function getChatLore() {
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getPersonaLore() {
|
||||||
|
const chatWorld = chat_metadata[METADATA_KEY];
|
||||||
|
const personaWorld = power_user.persona_description_lorebook;
|
||||||
|
|
||||||
|
if (!personaWorld) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatWorld === personaWorld) {
|
||||||
|
console.debug(`[WI] Persona world ${personaWorld} is already activated in chat world! Skipping...`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected_world_info.includes(personaWorld)) {
|
||||||
|
console.debug(`[WI] Persona world ${personaWorld} is already activated in global world info! Skipping...`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await loadWorldInfo(personaWorld);
|
||||||
|
const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: personaWorld, ...rest })) : [];
|
||||||
|
|
||||||
|
console.debug(`[WI] Persona lore has ${entries.length} entries`, [personaWorld]);
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSortedEntries() {
|
export async function getSortedEntries() {
|
||||||
try {
|
try {
|
||||||
const globalLore = await getGlobalLore();
|
const [
|
||||||
const characterLore = await getCharacterLore();
|
globalLore,
|
||||||
const chatLore = await getChatLore();
|
characterLore,
|
||||||
|
chatLore,
|
||||||
|
personaLore,
|
||||||
|
] = await Promise.all([
|
||||||
|
getGlobalLore(),
|
||||||
|
getCharacterLore(),
|
||||||
|
getChatLore(),
|
||||||
|
getPersonaLore(),
|
||||||
|
]);
|
||||||
|
|
||||||
let entries;
|
let entries;
|
||||||
|
|
||||||
@ -3622,8 +3661,8 @@ export async function getSortedEntries() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chat lore always goes first
|
// Chat lore always goes first, then persona lore, then the rest
|
||||||
entries = [...chatLore.sort(sortFn), ...entries];
|
entries = [...chatLore.sort(sortFn), ...personaLore.sort(sortFn), ...entries];
|
||||||
|
|
||||||
// Calculate hash and parse decorators. Split maps to preserve old hashes.
|
// Calculate hash and parse decorators. Split maps to preserve old hashes.
|
||||||
entries = entries.map((entry) => {
|
entries = entries.map((entry) => {
|
||||||
@ -4816,9 +4855,9 @@ export async function importWorldInfo(file) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assignLorebookToChat() {
|
export async function assignLorebookToChat() {
|
||||||
const selectedName = chat_metadata[METADATA_KEY];
|
const selectedName = chat_metadata[METADATA_KEY];
|
||||||
const template = $('#chat_world_template .chat_world').clone();
|
const template = $(await renderTemplateAsync('chatLorebook'));
|
||||||
|
|
||||||
const worldSelect = template.find('select');
|
const worldSelect = template.find('select');
|
||||||
const chatName = template.find('.chat_name');
|
const chatName = template.find('.chat_name');
|
||||||
@ -4846,7 +4885,7 @@ export function assignLorebookToChat() {
|
|||||||
saveMetadata();
|
saveMetadata();
|
||||||
});
|
});
|
||||||
|
|
||||||
callPopup(template, 'text');
|
callGenericPopup(template, POPUP_TYPE.TEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
jQuery(() => {
|
jQuery(() => {
|
||||||
|
Reference in New Issue
Block a user