Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into staging

This commit is contained in:
WBlair1
2024-07-03 16:31:15 -07:00
30 changed files with 393 additions and 143 deletions

View File

@@ -725,8 +725,14 @@ export function initRossMods() {
RA_autoconnect();
}
if (getParsedUA()?.os?.name === 'iOS') {
document.body.classList.add('ios');
const userAgent = getParsedUA();
console.debug('User Agent', userAgent);
const isMobileSafari = /iPad|iPhone|iPod/.test(navigator.platform) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
const isDesktopSafari = userAgent?.browser?.name === 'Safari' && userAgent?.platform?.type === 'desktop';
const isIOS = userAgent?.os?.name === 'iOS';
if (isIOS || isMobileSafari || isDesktopSafari) {
document.body.classList.add('safari');
}
$('#main_api').change(function () {

View File

@@ -1430,7 +1430,7 @@ jQuery(function () {
wrapper.classList.add('flexFlowColumn', 'justifyCenter', 'alignitemscenter');
const textarea = document.createElement('textarea');
textarea.value = String(bro.val());
textarea.classList.add('height100p', 'wide100p');
textarea.classList.add('height100p', 'wide100p', 'maximized_textarea');
bro.hasClass('monospace') && textarea.classList.add('monospace');
textarea.addEventListener('input', function () {
bro.val(textarea.value).trigger('input');

View File

@@ -154,7 +154,7 @@ export function initDynamicStyles() {
// Process all stylesheets on initial load
Array.from(document.styleSheets).forEach(sheet => {
try {
applyDynamicFocusStyles(sheet, { fromExtension: sheet.href.toLowerCase().includes('scripts/extensions') });
applyDynamicFocusStyles(sheet, { fromExtension: sheet.href?.toLowerCase().includes('scripts/extensions') == true });
} catch (e) {
console.warn('Failed to process stylesheet on initial load:', e);
}

View File

@@ -3017,25 +3017,25 @@ async function generateComfyImage(prompt, negativePrompt) {
const text = await workflowResponse.text();
toastr.error(`Failed to load workflow.\n\n${text}`);
}
let workflow = (await workflowResponse.json()).replace('"%prompt%"', JSON.stringify(prompt));
workflow = workflow.replace('"%negative_prompt%"', JSON.stringify(negativePrompt));
let workflow = (await workflowResponse.json()).replaceAll('"%prompt%"', JSON.stringify(prompt));
workflow = workflow.replaceAll('"%negative_prompt%"', JSON.stringify(negativePrompt));
const seed = extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
workflow = workflow.replaceAll('"%seed%"', JSON.stringify(seed));
placeholders.forEach(ph => {
workflow = workflow.replace(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));
workflow = workflow.replaceAll(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));
});
(extension_settings.sd.comfy_placeholders ?? []).forEach(ph => {
workflow = workflow.replace(`"%${ph.find}%"`, JSON.stringify(substituteParams(ph.replace)));
workflow = workflow.replaceAll(`"%${ph.find}%"`, JSON.stringify(substituteParams(ph.replace)));
});
if (/%user_avatar%/gi.test(workflow)) {
const response = await fetch(getUserAvatarUrl());
if (response.ok) {
const avatarBlob = await response.blob();
const avatarBase64 = await getBase64Async(avatarBlob);
workflow = workflow.replace('"%user_avatar%"', JSON.stringify(avatarBase64));
workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(avatarBase64));
} else {
workflow = workflow.replace('"%user_avatar%"', JSON.stringify(PNG_PIXEL));
workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(PNG_PIXEL));
}
}
if (/%char_avatar%/gi.test(workflow)) {
@@ -3043,9 +3043,9 @@ async function generateComfyImage(prompt, negativePrompt) {
if (response.ok) {
const avatarBlob = await response.blob();
const avatarBase64 = await getBase64Async(avatarBlob);
workflow = workflow.replace('"%char_avatar%"', JSON.stringify(avatarBase64));
workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(avatarBase64));
} else {
workflow = workflow.replace('"%char_avatar%"', JSON.stringify(PNG_PIXEL));
workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(PNG_PIXEL));
}
}
console.log(`{

View File

@@ -178,8 +178,37 @@ async function loadGroupChat(chatId) {
return [];
}
async function validateGroup(group) {
if (!group) return;
// Validate that all members exist as characters
let dirty = false;
group.members = group.members.filter(member => {
const character = characters.find(x => x.avatar === member || x.name === member);
if (!character) {
const msg = `Warning: Listed member ${member} does not exist as a character. It will be removed from the group.`;
toastr.warning(msg, 'Group Validation');
console.warn(msg);
dirty = true;
}
return character;
});
if (dirty) {
await editGroup(group.id, true, false);
}
}
export async function getGroupChat(groupId, reload = false) {
const group = groups.find((x) => x.id === groupId);
if (!group) {
console.warn('Group not found', groupId);
return;
}
// Run validation before any loading
validateGroup(group);
const chat_id = group.chat_id;
const data = await loadGroupChat(chat_id);
let freshChat = false;
@@ -197,7 +226,6 @@ export async function getGroupChat(groupId, reload = false) {
if (group && Array.isArray(group.members)) {
for (let member of group.members) {
const character = characters.find(x => x.avatar === member || x.name === member);
if (!character) {
continue;
}
@@ -219,10 +247,8 @@ export async function getGroupChat(groupId, reload = false) {
freshChat = true;
}
if (group) {
let metadata = group.chat_metadata ?? {};
updateChatMetadata(metadata, true);
}
let metadata = group.chat_metadata ?? {};
updateChatMetadata(metadata, true);
if (reload) {
select_group_chats(groupId, true);

View File

@@ -1,7 +1,5 @@
import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js';
const ELEMENT_ID = 'loader';
/** @type {Popup} */
let loaderPopup;
@@ -31,7 +29,7 @@ export async function hideLoader() {
return new Promise((resolve) => {
// Spinner blurs/fades out
$('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
$(`#${ELEMENT_ID}`).remove();
$('#loader').remove();
// Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
// If it's present, we remove it once and then it's gone.
yoinkPreloader();

View File

@@ -254,7 +254,7 @@ function getCurrentSwipeId() {
// For swipe macro, we are accepting using the message that is currently being swiped
const mid = getLastMessageId({ exclude_swipe_in_propress: false });
const swipeId = chat[mid]?.swipe_id;
return swipeId ? swipeId + 1 : null;
return swipeId !== null ? swipeId + 1 : null;
}
/**
@@ -401,7 +401,7 @@ function timeDiffReplace(input) {
const time2 = moment(matchPart2);
const timeDifference = moment.duration(time1.diff(time2));
return timeDifference.humanize();
return timeDifference.humanize(true);
});
return output;

View File

@@ -689,7 +689,7 @@ function formatWorldInfo(value) {
return '';
}
if (!oai_settings.wi_format) {
if (!oai_settings.wi_format.trim()) {
return value;
}

View File

@@ -40,7 +40,7 @@ import { tokenizers } from './tokenizers.js';
import { BIAS_CACHE } from './logit-bias.js';
import { renderTemplateAsync } from './templates.js';
import { countOccurrences, debounce, delay, download, getFileText, isOdd, isTrueBoolean, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
import { countOccurrences, debounce, delay, download, getFileText, getStringHash, isOdd, isTrueBoolean, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
import { FILTER_TYPES } from './filters.js';
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -335,6 +335,8 @@ const storage_keys = {
compact_input_area: 'compact_input_area',
auto_connect_legacy: 'AutoConnectEnabled',
auto_load_chat_legacy: 'AutoLoadChatEnabled',
storyStringValidationCache: 'StoryStringValidationCache',
};
const contextControls = [
@@ -2105,6 +2107,9 @@ export function fuzzySearchGroups(searchValue) {
*/
export function renderStoryString(params) {
try {
// Validate and log possible warnings/errors
validateStoryString(power_user.context.story_string, params);
// compile the story string template into a function, with no HTML escaping
const compiledTemplate = Handlebars.compile(power_user.context.story_string, { noEscape: true });
@@ -2132,6 +2137,55 @@ export function renderStoryString(params) {
}
}
/**
* Validate the story string for possible warnings or issues
*
* @param {string} storyString - The story string
* @param {Object} params - The story string parameters
*/
function validateStoryString(storyString, params) {
/** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */
const cache = JSON.parse(localStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };
const hash = getStringHash(storyString);
// Initialize the cache for the current hash if it doesn't exist
if (!cache.hashCache[hash]) {
cache.hashCache[hash] = { fieldsWarned: {} };
}
const currentCache = cache.hashCache[hash];
const fieldsToWarn = [];
function validateMissingField(field, fallbackLegacyField = null) {
const contains = storyString.includes(`{{${field}}}`) || (!!fallbackLegacyField && storyString.includes(`{{${fallbackLegacyField}}}`));
if (!contains && params[field]) {
const wasLogged = currentCache.fieldsWarned[field];
if (!wasLogged) {
fieldsToWarn.push(field);
currentCache.fieldsWarned[field] = true;
}
console.warn(`The story string does not contain {{${field}}}, but it would contain content:\n`, params[field]);
}
}
validateMissingField('description');
validateMissingField('personality');
validateMissingField('persona');
validateMissingField('scenario');
validateMissingField('system');
validateMissingField('wiBefore', 'loreBefore');
validateMissingField('wiAfter', 'loreAfter');
if (fieldsToWarn.length > 0) {
const fieldsList = fieldsToWarn.map(field => `{{${field}}}`).join(', ');
toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');
}
localStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));
}
const sortFunc = (a, b) => power_user.sort_order == 'asc' ? compareFunc(a, b) : compareFunc(b, a);
const compareFunc = (first, second) => {
const a = first[power_user.sort_field];

View File

@@ -33,6 +33,7 @@ import {
setCharacterName,
setExtensionPrompt,
setUserName,
stopGeneration,
substituteParams,
system_avatar,
system_message_types,
@@ -898,6 +899,24 @@ export function initDefaultSlashCommands() {
],
helpString: 'Adds a swipe to the last chat message.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'stop',
callback: () => {
const stopped = stopGeneration();
return String(stopped);
},
returns: 'true/false, whether the generation was running and got stopped',
helpString: `
<div>
Stops the generation and any streaming if it is currently running.
</div>
<div>
Note: This command cannot be executed from the chat input, as sending any message or script from there is blocked during generation.
But it can be executed via automations or QR scripts/buttons.
</div>
`,
aliases: ['generate-stop'],
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'abort',
callback: abortCallback,

View File

@@ -263,6 +263,8 @@ export const setting_names = [
'bypass_status_check',
];
const DYNATEMP_BLOCK = document.getElementById('dynatemp_block_ooba');
export function validateTextGenUrl() {
const selector = SERVER_INPUTS[settings.type];
@@ -1045,6 +1047,10 @@ export function isJsonSchemaSupported() {
return [TABBY, LLAMACPP].includes(settings.type) && main_api === 'textgenerationwebui';
}
function isDynamicTemperatureSupported() {
return settings.dynatemp && DYNATEMP_BLOCK?.dataset?.tgType?.includes(settings.type);
}
function getLogprobsNumber() {
if (settings.type === VLLM || settings.type === INFERMATICAI) {
return 5;
@@ -1055,6 +1061,7 @@ function getLogprobsNumber() {
export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';
const dynatemp = isDynamicTemperatureSupported();
const { banned_tokens, banned_strings } = getCustomTokenBans();
let params = {
@@ -1063,7 +1070,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'max_new_tokens': maxTokens,
'max_tokens': maxTokens,
'logprobs': power_user.request_token_probabilities ? getLogprobsNumber() : undefined,
'temperature': settings.dynatemp ? (settings.min_temp + settings.max_temp) / 2 : settings.temp,
'temperature': dynatemp ? (settings.min_temp + settings.max_temp) / 2 : settings.temp,
'top_p': settings.top_p,
'typical_p': settings.typical_p,
'typical': settings.typical_p,
@@ -1081,11 +1088,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 ? 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,
'dynamic_temperature': dynatemp ? true : undefined,
'dynatemp_low': dynatemp ? settings.min_temp : undefined,
'dynatemp_high': dynatemp ? settings.max_temp : undefined,
'dynatemp_range': dynatemp ? (settings.max_temp - settings.min_temp) / 2 : undefined,
'dynatemp_exponent': dynatemp ? settings.dynatemp_exponent : undefined,
'smoothing_factor': settings.smoothing_factor,
'smoothing_curve': settings.smoothing_curve,
'dry_allowed_length': settings.dry_allowed_length,

View File

@@ -803,7 +803,7 @@ export function getImageSizeFromDataURL(dataUrl) {
export function getCharaFilename(chid) {
const context = getContext();
const fileName = context.characters[chid ?? context.characterId].avatar;
const fileName = context.characters[chid ?? context.characterId]?.avatar;
if (fileName) {
return fileName.replace(/\.[^/.]+$/, '');