mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
@@ -1,4 +1,6 @@
|
||||
.git
|
||||
.github
|
||||
.vscode
|
||||
node_modules
|
||||
npm-debug.log
|
||||
readme*
|
||||
|
@@ -8,3 +8,6 @@ secrets.json
|
||||
/data
|
||||
/cache
|
||||
access.log
|
||||
.github
|
||||
.vscode
|
||||
.git
|
||||
|
@@ -48,6 +48,8 @@ allowKeysExposure: false
|
||||
skipContentCheck: false
|
||||
# Disable automatic chats backup
|
||||
disableChatBackup: false
|
||||
# Number of backups to keep for each chat and settings file
|
||||
numberOfBackups: 50
|
||||
# Allowed hosts for card downloads
|
||||
whitelistImportDomains:
|
||||
- localhost
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#loader, #preloader {
|
||||
#preloader {
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@@ -1,8 +1,6 @@
|
||||
/* iPhone copium land */
|
||||
@media screen and (max-width: 1000px) {
|
||||
.ios .popup .popup-body {
|
||||
height: fit-content;
|
||||
max-height: 90vh;
|
||||
max-height: 90svh;
|
||||
}
|
||||
.ios .popup .popup-body {
|
||||
height: fit-content;
|
||||
max-height: 90vh;
|
||||
max-height: 90svh;
|
||||
}
|
||||
|
@@ -27,8 +27,17 @@ dialog {
|
||||
backface-visibility: hidden;
|
||||
transform: translateZ(0);
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
|
||||
/* Variables setup */
|
||||
--popup-animation-speed: var(--animation-duration-slow);
|
||||
}
|
||||
|
||||
/** Popup styles applied to the main popup */
|
||||
.popup--animation-fast { --popup-animation-speed: var(--animation-duration); }
|
||||
.popup--animation-slow { --popup-animation-speed: var(--animation-duration-slow); }
|
||||
.popup--animation-none { --popup-animation-speed: 0ms; }
|
||||
|
||||
/* Styling of main popup elements */
|
||||
.popup .popup-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -60,11 +69,11 @@ dialog {
|
||||
|
||||
/* Opening animation */
|
||||
.popup[opening] {
|
||||
animation: pop-in var(--animation-duration-slow) ease-in-out;
|
||||
animation: pop-in var(--popup-animation-speed) ease-in-out;
|
||||
}
|
||||
|
||||
.popup[opening]::backdrop {
|
||||
animation: fade-in var(--animation-duration-slow) ease-in-out;
|
||||
animation: fade-in var(--popup-animation-speed) ease-in-out;
|
||||
}
|
||||
|
||||
/* Open state of the dialog */
|
||||
@@ -85,11 +94,11 @@ body.no-blur .popup[open]::backdrop {
|
||||
|
||||
/* Closing animation */
|
||||
.popup[closing] {
|
||||
animation: pop-out var(--animation-duration-slow) ease-in-out;
|
||||
animation: pop-out var(--popup-animation-speed) ease-in-out;
|
||||
}
|
||||
|
||||
.popup[closing]::backdrop {
|
||||
animation: fade-out var(--animation-duration-slow) ease-in-out;
|
||||
animation: fade-out var(--popup-animation-speed) ease-in-out;
|
||||
}
|
||||
|
||||
.popup #toast-container {
|
||||
|
@@ -3903,8 +3903,8 @@
|
||||
<label for="tag_import_setting"><small data-i18n="Import Card Tags">Import Card Tags</small></label>
|
||||
<select id="tag_import_setting" class="widthNatural flex1 margin0">
|
||||
<option data-i18n="Ask" value="1">Ask</option>
|
||||
<option data-i18n="None" value="2">None</option>
|
||||
<option data-i18n="All" value="3">All</option>
|
||||
<option data-i18n="tag_import_none" value="2">None</option>
|
||||
<option data-i18n="tag_import_all" value="3">All</option>
|
||||
<option data-i18n="Existing" value="4">Existing</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -4796,7 +4796,7 @@
|
||||
<input multiple type="file" id="character_import_file" accept=".json, image/png, .yaml, .yml, .charx" name="avatar">
|
||||
<input id="character_import_file_type" name="file_type" class="text_pole" maxlength="999" size="2" value="" autocomplete="off">
|
||||
</form>
|
||||
<input type="file" id="character_replace_file" accept="image/png" name="replace_avatar" hidden>
|
||||
<input type="file" id="character_replace_file" accept=".json, image/png, .yaml, .yml, .charx" name="replace_avatar" hidden>
|
||||
</div>
|
||||
<div name="Character List Panel" id="rm_characters_block" class="right_menu">
|
||||
<div id="charListFixedTop">
|
||||
|
@@ -163,9 +163,9 @@
|
||||
"View hidden API keys": "Посмотреть скрытые API-ключи",
|
||||
"Advanced Formatting": "Расширенное форматирование",
|
||||
"Context Template": "Шаблон контекста",
|
||||
"Replace Macro in Custom Stopping Strings": "Заменить макросы в пользовательских стоп-строках",
|
||||
"Replace Macro in Custom Stopping Strings": "Заменять макросы в пользовательских стоп-строках",
|
||||
"Story String": "Строка истории",
|
||||
"Example Separator": "Пример разделителя",
|
||||
"Example Separator": "Разделитель примеров сообщений",
|
||||
"Chat Start": "Начало чата",
|
||||
"Activation Regex": "Regex для активации",
|
||||
"Instruct Mode": "Режим Instruct",
|
||||
@@ -187,7 +187,7 @@
|
||||
"Misc. Settings": "Доп. настройки",
|
||||
"Auto-Continue": "Авто-продолжение",
|
||||
"Collapse Consecutive Newlines": "Сворачивать последовательные новые строки",
|
||||
"Allow for Chat Completion APIs": "Разрешить для API Chat Completion",
|
||||
"Allow for Chat Completion APIs": "Разрешить для Chat Completion API",
|
||||
"Target length (tokens)": "Целевая длина (в токенах)",
|
||||
"World Info": "Информация о мире",
|
||||
"Scan Depth": "Глубина сканирования",
|
||||
@@ -377,7 +377,7 @@
|
||||
"Log prompts to console": "Выводить промпты в консоль",
|
||||
"Never resize avatars": "Не менять размер аватарок",
|
||||
"Show avatar filenames": "Показывать названия файлов аватарок",
|
||||
"Import Card Tags": "Импорт тегов карточки",
|
||||
"Import Card Tags": "Импортировать теги карточки",
|
||||
"Confirm message deletion": "Подтверждение удаления сообщений",
|
||||
"Spoiler Free Mode": "Режим без спойлеров",
|
||||
"Auto-swipe": "Автоматические свайпы",
|
||||
@@ -1274,7 +1274,7 @@
|
||||
"openrouter_force_instruct": "This option is outdated and will be removed in the future. To use instruct formatting, please switch to OpenRouter under Text Completion API instead.",
|
||||
"Force_Instruct_Mode_formatting_Description": "If both Instruct Mode and this are enabled, the prompt will be formatted by SillyTavern using the current\n advanced formatting settings (except instruct System Prompt). If disabled, the prompt will be formatted by OpenRouter.",
|
||||
"Clear your cookie": "Clear your cookie",
|
||||
"Add Chat Start and Example Separator to a list of stopping strings.": "Add Chat Start and Example Separator to a list of stopping strings.",
|
||||
"Add Chat Start and Example Separator to a list of stopping strings.": "Использовать Начало чата и Разделитель примеров сообщений в качестве стоп-строк.",
|
||||
"context_allow_jailbreak": "Если в карточке есть джейлбрейк И ПРИ ЭТОМ включена опция \"Приоритет джейлбрейку из карточки персонажа\", то этот джейлбрейк добавляется в конец промпта.\nНЕ РЕКОМЕНДУЕТСЯ ДЛЯ МОДЕЛЕЙ TEXT COMPLETION, МОЖЕТ ПОРТИТЬ ВЫХОДНОЙ ТЕКСТ.",
|
||||
"Context Order": "Context Order",
|
||||
"Summary": "Summary",
|
||||
@@ -1636,5 +1636,10 @@
|
||||
"Change Password": "Сменить пароль",
|
||||
"Reset Code:": "Reset Code:",
|
||||
"Click _space": "Нажмите ",
|
||||
"Alternate Greeting #": "Вариант #"
|
||||
"Alternate Greeting #": "Вариант #",
|
||||
"Defines on importing cards which action should be chosen for importing its listed tags. 'Ask' will always display the dialog.": "Выберите, какие действия следует предпринять по отношению к тегам импортируемой карточки. При выборе опции \"Спрашивать\" вы будете решать это индивидуально для каждой карточки.",
|
||||
"Ask": "Спрашивать",
|
||||
"tag_import_all": "Все",
|
||||
"Existing": "Только существующие",
|
||||
"tag_import_none": "Не импортировать"
|
||||
}
|
||||
|
@@ -217,6 +217,7 @@
|
||||
"Character Names Behavior": "角色名称行为",
|
||||
"Helps the model to associate messages with characters.": "有助于模型将消息与角色关联起来。",
|
||||
"None": "无",
|
||||
"tag_import_none": "无",
|
||||
"character_names_none": "群聊和过去的角色除外。否则,请确保在提示词中提供了姓名。",
|
||||
"Don't add character names.": "不添加角色名称。",
|
||||
"Completion": "补全对象",
|
||||
@@ -653,7 +654,7 @@
|
||||
"Defines on importing cards which action should be chosen for importing its listed tags. 'Ask' will always display the dialog.": "定义在导入卡片时应选择哪种操作来导入其列出的标签。“询问”将始终显示对话框。",
|
||||
"Import Card Tags": "导入卡片标签",
|
||||
"Ask": "询问",
|
||||
"All": "全部",
|
||||
"tag_import_all": "全部",
|
||||
"Existing": "现存的",
|
||||
"Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring": "使用模糊匹配,在列表中通过所有数据字段搜索角色,而不仅仅是名称子字符串",
|
||||
"Advanced Character Search": "高级角色搜索",
|
||||
|
@@ -43,7 +43,6 @@ import {
|
||||
saveGroupChat,
|
||||
getGroups,
|
||||
generateGroupWrapper,
|
||||
deleteGroup,
|
||||
is_group_generating,
|
||||
resetSelectedGroup,
|
||||
select_group_chats,
|
||||
@@ -228,7 +227,7 @@ import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, de
|
||||
import { initPresetManager } from './scripts/preset-manager.js';
|
||||
import { MacrosParser, evaluateMacros } from './scripts/macros.js';
|
||||
import { currentUser, setUserControls } from './scripts/user.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
|
||||
import { POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
|
||||
import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
|
||||
import { ScraperManager } from './scripts/scrapers.js';
|
||||
import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
|
||||
@@ -266,8 +265,6 @@ await new Promise((resolve) => {
|
||||
});
|
||||
|
||||
showLoader();
|
||||
// Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
|
||||
document.getElementById('preloader').remove();
|
||||
|
||||
// Configure toast library:
|
||||
toastr.options.escapeHtml = true; // Prevent raw HTML inserts
|
||||
@@ -7829,6 +7826,8 @@ window['SillyTavern'].getContext = function () {
|
||||
registerDataBankScraper: ScraperManager.registerDataBankScraper,
|
||||
callPopup: callPopup,
|
||||
callGenericPopup: callGenericPopup,
|
||||
showLoader: showLoader,
|
||||
hideLoader: hideLoader,
|
||||
mainApi: main_api,
|
||||
extensionSettings: extension_settings,
|
||||
ModuleWorkerWrapper: ModuleWorkerWrapper,
|
||||
@@ -7899,7 +7898,7 @@ function swipe_left() { // when we swipe left..but no generation.
|
||||
}
|
||||
$(this).parent().children('.mes_block').transition({
|
||||
x: swipe_range,
|
||||
duration: swipe_duration,
|
||||
duration: animation_duration > 0 ? swipe_duration : 0,
|
||||
easing: animation_easing,
|
||||
queue: false,
|
||||
complete: async function () {
|
||||
@@ -7942,7 +7941,7 @@ function swipe_left() { // when we swipe left..but no generation.
|
||||
complete: function () {
|
||||
$(this).parent().children('.mes_block').transition({
|
||||
x: '0px',
|
||||
duration: swipe_duration,
|
||||
duration: animation_duration > 0 ? swipe_duration : 0,
|
||||
easing: animation_easing,
|
||||
queue: false,
|
||||
complete: async function () {
|
||||
@@ -7957,7 +7956,7 @@ function swipe_left() { // when we swipe left..but no generation.
|
||||
|
||||
$(this).parent().children('.avatar').transition({
|
||||
x: swipe_range,
|
||||
duration: swipe_duration,
|
||||
duration: animation_duration > 0 ? swipe_duration : 0,
|
||||
easing: animation_easing,
|
||||
queue: false,
|
||||
complete: function () {
|
||||
@@ -7969,7 +7968,7 @@ function swipe_left() { // when we swipe left..but no generation.
|
||||
complete: function () {
|
||||
$(this).parent().children('.avatar').transition({
|
||||
x: '0px',
|
||||
duration: swipe_duration,
|
||||
duration: animation_duration > 0 ? swipe_duration : 0,
|
||||
easing: animation_easing,
|
||||
queue: false,
|
||||
complete: function () {
|
||||
@@ -8076,7 +8075,7 @@ const swipe_right = () => {
|
||||
this_mes_div.children('.swipe_left').css('display', 'flex');
|
||||
this_mes_div.children('.mes_block').transition({ // this moves the div back and forth
|
||||
x: '-' + swipe_range,
|
||||
duration: swipe_duration,
|
||||
duration: animation_duration > 0 ? swipe_duration : 0,
|
||||
easing: animation_easing,
|
||||
queue: false,
|
||||
complete: async function () {
|
||||
@@ -8135,7 +8134,7 @@ const swipe_right = () => {
|
||||
complete: function () {
|
||||
this_mes_div.children('.mes_block').transition({
|
||||
x: '0px',
|
||||
duration: swipe_duration,
|
||||
duration: animation_duration > 0 ? swipe_duration : 0,
|
||||
easing: animation_easing,
|
||||
queue: false,
|
||||
complete: async function () {
|
||||
@@ -8158,7 +8157,7 @@ const swipe_right = () => {
|
||||
});
|
||||
this_mes_div.children('.avatar').transition({ // moves avatar along with swipe
|
||||
x: '-' + swipe_range,
|
||||
duration: swipe_duration,
|
||||
duration: animation_duration > 0 ? swipe_duration : 0,
|
||||
easing: animation_easing,
|
||||
queue: false,
|
||||
complete: function () {
|
||||
@@ -8170,7 +8169,7 @@ const swipe_right = () => {
|
||||
complete: function () {
|
||||
this_mes_div.children('.avatar').transition({
|
||||
x: '0px',
|
||||
duration: swipe_duration,
|
||||
duration: animation_duration > 0 ? swipe_duration : 0,
|
||||
easing: animation_easing,
|
||||
queue: false,
|
||||
complete: function () {
|
||||
@@ -8450,10 +8449,10 @@ async function connectAPISlash(_, text) {
|
||||
/**
|
||||
* Imports supported files dropped into the app window.
|
||||
* @param {File[]} files Array of files to process
|
||||
* @param {boolean?} preserveFileNames Whether to preserve original file names
|
||||
* @param {Map<File, string>} [data] Extra data to pass to the import function
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function processDroppedFiles(files, preserveFileNames = false) {
|
||||
export async function processDroppedFiles(files, data = new Map()) {
|
||||
const allowedMimeTypes = [
|
||||
'application/json',
|
||||
'image/png',
|
||||
@@ -8470,7 +8469,8 @@ export async function processDroppedFiles(files, preserveFileNames = false) {
|
||||
for (const file of files) {
|
||||
const extension = file.name.split('.').pop().toLowerCase();
|
||||
if (allowedMimeTypes.includes(file.type) || allowedExtensions.includes(extension)) {
|
||||
await importCharacter(file, preserveFileNames);
|
||||
const preservedName = data instanceof Map && data.get(file);
|
||||
await importCharacter(file, preservedName);
|
||||
} else {
|
||||
toastr.warning('Unsupported file type: ' + file.name);
|
||||
}
|
||||
@@ -8480,10 +8480,10 @@ export async function processDroppedFiles(files, preserveFileNames = false) {
|
||||
/**
|
||||
* Imports a character from a file.
|
||||
* @param {File} file File to import
|
||||
* @param {boolean?} preserveFileName Whether to preserve original file name
|
||||
* @param {string?} preserveFileName Whether to preserve original file name
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function importCharacter(file, preserveFileName = false) {
|
||||
async function importCharacter(file, preserveFileName = '') {
|
||||
if (is_group_generating || is_send_press) {
|
||||
toastr.error('Cannot import characters while generating. Stop the request and try again.', 'Import aborted');
|
||||
throw new Error('Cannot import character while generating');
|
||||
@@ -8499,7 +8499,7 @@ async function importCharacter(file, preserveFileName = false) {
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', file);
|
||||
formData.append('file_type', format);
|
||||
formData.append('preserve_file_name', String(preserveFileName));
|
||||
if (preserveFileName) formData.append('preserved_name', preserveFileName);
|
||||
|
||||
const data = await jQuery.ajax({
|
||||
type: 'POST',
|
||||
@@ -9170,14 +9170,26 @@ jQuery(async function () {
|
||||
chooseBogusFolder($(this), tagId);
|
||||
});
|
||||
|
||||
$(document).on('input', '.edit_textarea', function () {
|
||||
scroll_holder = $('#chat').scrollTop();
|
||||
$(this).height(0).height(this.scrollHeight);
|
||||
/**
|
||||
* Sets the scroll height of the edit textarea to fit the content.
|
||||
* @param {HTMLTextAreaElement} e Textarea element to auto-fit
|
||||
*/
|
||||
function autoFitEditTextArea(e) {
|
||||
scroll_holder = chatElement[0].scrollTop;
|
||||
e.style.height = '0';
|
||||
e.style.height = `${e.scrollHeight + 4}px`;
|
||||
is_use_scroll_holder = true;
|
||||
}
|
||||
const autoFitEditTextAreaDebounced = debounce(autoFitEditTextArea, debounce_timeout.short);
|
||||
document.addEventListener('input', e => {
|
||||
if (e.target instanceof HTMLTextAreaElement && e.target.classList.contains('edit_textarea')) {
|
||||
const immediately = e.target.scrollHeight > e.target.offsetHeight || e.target.value === '';
|
||||
immediately ? autoFitEditTextArea(e.target) : autoFitEditTextAreaDebounced(e.target);
|
||||
}
|
||||
});
|
||||
$('#chat').on('scroll', function () {
|
||||
document.getElementById('chat').addEventListener('scroll', function () {
|
||||
if (is_use_scroll_holder) {
|
||||
$('#chat').scrollTop(scroll_holder);
|
||||
this.scrollTop = scroll_holder;
|
||||
is_use_scroll_holder = false;
|
||||
}
|
||||
});
|
||||
@@ -9709,14 +9721,8 @@ jQuery(async function () {
|
||||
});
|
||||
|
||||
$('#newChatFromManageScreenButton').on('click', function () {
|
||||
setTimeout(() => {
|
||||
$('#option_start_new_chat').trigger('click');
|
||||
}, 1);
|
||||
setTimeout(() => {
|
||||
$('#dialogue_popup_ok').trigger('click');
|
||||
}, 1);
|
||||
doNewChat({ deleteCurrentChat: false });
|
||||
$('#select_chat_cross').trigger('click');
|
||||
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -10632,10 +10638,12 @@ jQuery(async function () {
|
||||
}
|
||||
|
||||
try {
|
||||
const cloneFile = new File([file], characters[this_chid].avatar, { type: file.type });
|
||||
const chatFile = characters[this_chid]['chat'];
|
||||
await processDroppedFiles([cloneFile], true);
|
||||
const data = new Map();
|
||||
data.set(file, characters[this_chid].avatar);
|
||||
await processDroppedFiles([file], data);
|
||||
await openCharacterChat(chatFile);
|
||||
await fetch(getThumbnailUrl('avatar', characters[this_chid].avatar), { cache: 'no-cache' });
|
||||
} catch {
|
||||
toastr.error('Failed to replace the character card.', 'Something went wrong');
|
||||
}
|
||||
|
@@ -725,7 +725,9 @@ export function initRossMods() {
|
||||
RA_autoconnect();
|
||||
}
|
||||
|
||||
if (getParsedUA()?.os?.name === 'iOS') {
|
||||
const isAppleDevice = /iPad|iPhone|iPod/.test(navigator.platform) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||
|
||||
if (getParsedUA()?.os?.name === 'iOS' || isAppleDevice) {
|
||||
document.body.classList.add('ios');
|
||||
}
|
||||
|
||||
|
@@ -1526,7 +1526,13 @@ jQuery(function () {
|
||||
const fileInput = document.getElementById('file_form_input');
|
||||
if (!(fileInput instanceof HTMLInputElement)) return;
|
||||
|
||||
fileInput.files = event.clipboardData.files;
|
||||
// Workaround for Firefox: Use a DataTransfer object to indirectly set fileInput.files
|
||||
const dataTransfer = new DataTransfer();
|
||||
for (let i = 0; i < event.clipboardData.files.length; i++) {
|
||||
dataTransfer.items.add(event.clipboardData.files[i]);
|
||||
}
|
||||
|
||||
fileInput.files = dataTransfer.files;
|
||||
await onFileAttach(fileInput.files[0]);
|
||||
});
|
||||
});
|
||||
|
@@ -5,6 +5,8 @@
|
||||
export const debounce_timeout = {
|
||||
/** [100 ms] For ultra-fast responses, typically for keypresses or executions that might happen multiple times in a loop or recursion. */
|
||||
quick: 100,
|
||||
/** [200 ms] Slightly slower than quick, but still very responsive. */
|
||||
short: 200,
|
||||
/** [300 ms] Default time for general use, good balance between responsiveness and performance. */
|
||||
standard: 300,
|
||||
/** [1.000 ms] For situations where the function triggers more intensive tasks. */
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -374,7 +374,7 @@ async function addExtensionsButtonAndMenu() {
|
||||
|
||||
$('html').on('click', function (e) {
|
||||
const clickTarget = $(e.target);
|
||||
const noCloseTargets = ['#sd_gen', '#extensionsMenuButton'];
|
||||
const noCloseTargets = ['#sd_gen', '#extensionsMenuButton', '#roll_dice'];
|
||||
if (dropdown.is(':visible') && !noCloseTargets.some(id => clickTarget.closest(id).length > 0)) {
|
||||
$(dropdown).fadeOut(animation_duration);
|
||||
}
|
||||
@@ -641,9 +641,16 @@ async function showExtensionsDetails() {
|
||||
action: async () => {
|
||||
requiresReload = true;
|
||||
await autoUpdateExtensions(true);
|
||||
popup.complete(POPUP_RESULT.AFFIRMATIVE);
|
||||
await popup.complete(POPUP_RESULT.AFFIRMATIVE);
|
||||
},
|
||||
};
|
||||
|
||||
// If we are updating an extension, the "old" popup is still active. We should close that.
|
||||
const oldPopup = Popup.util.popups.find(popup => popup.content.querySelector('.extensions_info'));
|
||||
if (oldPopup) {
|
||||
await oldPopup.complete(POPUP_RESULT.CANCELLED);
|
||||
}
|
||||
|
||||
const popup = new Popup(`<div class="extensions_info">${html}</div>`, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, customButtons: [updateAllButton], allowVerticalScrolling: true });
|
||||
popupPromise = popup.show();
|
||||
} catch (error) {
|
||||
|
@@ -520,12 +520,17 @@ jQuery(async function () {
|
||||
const messageImg = messageBlock.find('.mes_img');
|
||||
if (messageImg.hasClass(animationClass)) return;
|
||||
messageImg.addClass(animationClass);
|
||||
const index = Number(messageBlock.attr('mesid'));
|
||||
const data = getContext().chat[index];
|
||||
await captionExistingMessage(data);
|
||||
appendMediaToMessage(data, messageBlock, false);
|
||||
await saveChatConditional();
|
||||
messageImg.removeClass(animationClass);
|
||||
try {
|
||||
const index = Number(messageBlock.attr('mesid'));
|
||||
const data = getContext().chat[index];
|
||||
await captionExistingMessage(data);
|
||||
appendMediaToMessage(data, messageBlock, false);
|
||||
await saveChatConditional();
|
||||
} catch(e) {
|
||||
console.error('Message image recaption failed', e);
|
||||
} finally {
|
||||
messageImg.removeClass(animationClass);
|
||||
}
|
||||
});
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'caption',
|
||||
|
@@ -3,7 +3,6 @@ import {
|
||||
systemUserName,
|
||||
hideSwipeButtons,
|
||||
showSwipeButtons,
|
||||
callPopup,
|
||||
getRequestHeaders,
|
||||
event_types,
|
||||
eventSource,
|
||||
@@ -29,10 +28,9 @@ import { getMultimodalCaption } from '../shared.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { resolveVariable } from '../../variables.js';
|
||||
import { debounce_timeout } from '../../constants.js';
|
||||
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'sd';
|
||||
@@ -572,7 +570,7 @@ async function onDeleteStyleClick() {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await callPopup(`Are you sure you want to delete the style "${selectedStyle}"?`, 'confirm', '', { okButton: 'Delete' });
|
||||
const confirmed = await callGenericPopup(`Are you sure you want to delete the style "${selectedStyle}"?`, POPUP_TYPE.CONFIRM, '', { okButton: 'Delete', cancelButton: 'Cancel' });
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
@@ -601,7 +599,7 @@ async function onDeleteStyleClick() {
|
||||
}
|
||||
|
||||
async function onSaveStyleClick() {
|
||||
const userInput = await callPopup('Enter style name:', 'input', '', { okButton: 'Save' });
|
||||
const userInput = await callGenericPopup('Enter style name:', POPUP_TYPE.INPUT);
|
||||
|
||||
if (!userInput) {
|
||||
return;
|
||||
@@ -670,7 +668,7 @@ async function refinePrompt(prompt, allowExpand, isNegative = false) {
|
||||
|
||||
if (extension_settings.sd.refine_mode) {
|
||||
const text = isNegative ? '<h3>Review and edit the <i>negative</i> prompt:</h3>' : '<h3>Review and edit the prompt:</h3>';
|
||||
const refinedPrompt = await callPopup(text + 'Press "Cancel" to abort the image generation.', 'input', prompt.trim(), { rows: 5, okButton: 'Continue' });
|
||||
const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 5, okButton: 'Continue' });
|
||||
|
||||
if (refinedPrompt) {
|
||||
return refinedPrompt;
|
||||
@@ -2918,25 +2916,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)) {
|
||||
@@ -2944,9 +2942,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(`{
|
||||
@@ -2978,7 +2976,7 @@ async function onComfyOpenWorkflowEditorClick() {
|
||||
}),
|
||||
})).json();
|
||||
const editorHtml = $(await $.get('scripts/extensions/stable-diffusion/comfyWorkflowEditor.html'));
|
||||
const popupResult = callPopup(editorHtml, 'confirm', undefined, { okButton: 'Save', wide: true, large: true, rows: 1 });
|
||||
const popupResult = callGenericPopup(editorHtml, POPUP_TYPE.CONFIRM, '', { okButton: 'Save', cancelButton: 'Cancel', wide: true, large: true });
|
||||
const checkPlaceholders = () => {
|
||||
workflow = $('#sd_comfy_workflow_editor_workflow').val().toString();
|
||||
$('.sd_comfy_workflow_editor_placeholder_list > li[data-placeholder]').each(function (idx) {
|
||||
@@ -3058,7 +3056,7 @@ async function onComfyOpenWorkflowEditorClick() {
|
||||
}
|
||||
|
||||
async function onComfyNewWorkflowClick() {
|
||||
let name = await callPopup('<h3>Workflow name:</h3>', 'input');
|
||||
let name = await callGenericPopup('Workflow name:', POPUP_TYPE.INPUT);
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
@@ -3085,7 +3083,7 @@ async function onComfyNewWorkflowClick() {
|
||||
}
|
||||
|
||||
async function onComfyDeleteWorkflowClick() {
|
||||
const confirm = await callPopup('Delete the workflow? This action is irreversible.', 'confirm');
|
||||
const confirm = await callGenericPopup('Delete the workflow? This action is irreversible.', POPUP_TYPE.CONFIRM, '', { okButton: 'Delete', cancelButton: 'Cancel' });
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { callPopup, getRequestHeaders } from '../../../script.js';
|
||||
import { getRequestHeaders } from '../../../script.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
import { SECRET_KEYS, findSecret, secret_state, writeSecret } from '../../secrets.js';
|
||||
import { getPreviewString, saveTtsProviderSettings } from './index.js';
|
||||
export { AzureTtsProvider };
|
||||
@@ -69,13 +70,13 @@ class AzureTtsProvider {
|
||||
const popupText = 'Azure TTS API Key';
|
||||
const savedKey = secret_state[SECRET_KEYS.AZURE_TTS] ? await findSecret(SECRET_KEYS.AZURE_TTS) : '';
|
||||
|
||||
const key = await callPopup(popupText, 'input', savedKey);
|
||||
const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedKey);
|
||||
|
||||
if (key == false || key == '') {
|
||||
return;
|
||||
}
|
||||
|
||||
await writeSecret(SECRET_KEYS.AZURE_TTS, key);
|
||||
await writeSecret(SECRET_KEYS.AZURE_TTS, String(key));
|
||||
|
||||
toastr.success('API Key saved');
|
||||
$('#azure_tts_key').addClass('success');
|
||||
|
@@ -5,8 +5,8 @@ TODO:
|
||||
*/
|
||||
|
||||
import { doExtrasFetch, extension_settings, getApiUrl, modules } from '../../extensions.js';
|
||||
import { callPopup } from '../../../script.js';
|
||||
import { initVoiceMap } from './index.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
|
||||
export { CoquiTtsProvider };
|
||||
|
||||
@@ -246,7 +246,7 @@ class CoquiTtsProvider {
|
||||
}
|
||||
|
||||
// Ask user for voiceId name to save voice
|
||||
const voiceName = await callPopup('<h3>Name of Coqui voice to add to voice select dropdown:</h3>', 'input');
|
||||
const voiceName = await callGenericPopup('Name of Coqui voice to add to voice select dropdown:', POPUP_TYPE.INPUT);
|
||||
|
||||
const model_origin = $('#coqui_model_origin').val();
|
||||
const model_language = $('#coqui_api_language').val();
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { getRequestHeaders, callPopup } from '../../../script.js';
|
||||
import { getRequestHeaders } from '../../../script.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
import { splitRecursive } from '../../utils.js';
|
||||
import { getPreviewString, saveTtsProviderSettings } from './index.js';
|
||||
import { initVoiceMap } from './index.js';
|
||||
@@ -56,7 +57,7 @@ class NovelTtsProvider {
|
||||
|
||||
// Add a new Novel custom voice to provider
|
||||
async addCustomVoice() {
|
||||
const voiceName = await callPopup('<h3>Custom Voice name:</h3>', 'input');
|
||||
const voiceName = await callGenericPopup('Custom Voice name:', POPUP_TYPE.INPUT);
|
||||
this.settings.customVoices.push(voiceName);
|
||||
this.populateCustomVoices();
|
||||
initVoiceMap(); // Update TTS extension voiceMap
|
||||
|
@@ -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);
|
||||
@@ -1576,6 +1602,8 @@ export async function openGroupById(groupId) {
|
||||
}
|
||||
|
||||
if (!is_send_press && !is_group_generating) {
|
||||
select_group_chats(groupId);
|
||||
|
||||
if (selected_group !== groupId) {
|
||||
await clearChat();
|
||||
cancelTtsPlay();
|
||||
@@ -1587,8 +1615,6 @@ export async function openGroupById(groupId) {
|
||||
chat.length = 0;
|
||||
await getGroupChat(groupId);
|
||||
}
|
||||
|
||||
select_group_chats(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,25 +1,57 @@
|
||||
import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js';
|
||||
|
||||
const ELEMENT_ID = 'loader';
|
||||
|
||||
/** @type {Popup} */
|
||||
let loaderPopup;
|
||||
|
||||
let preloaderYoinked = false;
|
||||
|
||||
export function showLoader() {
|
||||
const container = $('<div></div>').attr('id', ELEMENT_ID);
|
||||
const loader = $('<div></div>').attr('id', 'load-spinner').addClass('fa-solid fa-gear fa-spin fa-3x');
|
||||
container.append(loader);
|
||||
$('body').append(container);
|
||||
// Two loaders don't make sense. Don't await, we can overlay the old loader while it closes
|
||||
if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED);
|
||||
|
||||
loaderPopup = new Popup(`
|
||||
<div id="loader">
|
||||
<div id="load-spinner" class="fa-solid fa-gear fa-spin fa-3x"></div>
|
||||
</div>`, POPUP_TYPE.DISPLAY, null, { transparent: true, animation: 'none' });
|
||||
|
||||
// No close button, loaders are not closable
|
||||
loaderPopup.closeButton.style.display = 'none';
|
||||
|
||||
loaderPopup.show();
|
||||
}
|
||||
|
||||
export async function hideLoader() {
|
||||
//Sets up a 2-step animation. Spinner blurs/fades out, and then the loader shadow does the same.
|
||||
$('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
|
||||
$(`#${ELEMENT_ID}`)
|
||||
//only fade out the spinner and replace with login screen
|
||||
.animate({ opacity: 0 }, 300, function () {
|
||||
$(`#${ELEMENT_ID}`).remove();
|
||||
if (!loaderPopup) {
|
||||
console.warn('There is no loader showing to hide');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Spinner blurs/fades out
|
||||
$('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
|
||||
$(`#${ELEMENT_ID}`).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();
|
||||
|
||||
loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE).then(() => {
|
||||
loaderPopup = null;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
$('#load-spinner')
|
||||
.css({
|
||||
'filter': 'blur(15px)',
|
||||
'opacity': '0',
|
||||
});
|
||||
});
|
||||
|
||||
$('#load-spinner')
|
||||
.css({
|
||||
'filter': 'blur(15px)',
|
||||
'opacity': '0',
|
||||
});
|
||||
}
|
||||
|
||||
function yoinkPreloader() {
|
||||
if (preloaderYoinked) return;
|
||||
document.getElementById('preloader').remove();
|
||||
preloaderYoinked = true;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -689,7 +689,7 @@ function formatWorldInfo(value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!oai_settings.wi_format) {
|
||||
if (!oai_settings.wi_format.trim()) {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { shouldSendOnEnter } from './RossAscends-mods.js';
|
||||
import { power_user } from './power-user.js';
|
||||
import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
|
||||
|
||||
@@ -35,6 +36,7 @@ export const POPUP_RESULT = {
|
||||
* @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content)
|
||||
* @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup
|
||||
* @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup
|
||||
* @property {'slow'|'fast'|'none'?} [animation='slow'] - Animation speed for the popup (opening, closing, ...)
|
||||
* @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.
|
||||
* @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.
|
||||
* @property {CustomPopupInput[]?} [customInputs=null] - Custom inputs to add to the popup. The display below the content and the input box, one by one.
|
||||
@@ -98,7 +100,7 @@ const showPopupHelper = {
|
||||
const result = await popup.show();
|
||||
if (typeof result === 'string' || typeof result === 'boolean') throw new Error(`Invalid popup result. CONFIRM popups only support numbers, or null. Result: ${result}`);
|
||||
return result;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export class Popup {
|
||||
@@ -142,7 +144,7 @@ export class Popup {
|
||||
* @param {string} [inputValue=''] - The initial value of the input field
|
||||
* @param {PopupOptions} [options={}] - Additional options for the popup
|
||||
*/
|
||||
constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) {
|
||||
constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, animation = 'fast', defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) {
|
||||
Popup.util.popups.push(this);
|
||||
|
||||
// Make this popup uniquely identifiable
|
||||
@@ -175,6 +177,7 @@ export class Popup {
|
||||
if (transparent) this.dlg.classList.add('transparent_dialogue_popup');
|
||||
if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup');
|
||||
if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup');
|
||||
if (animation) this.dlg.classList.add('popup--animation-' + animation);
|
||||
|
||||
// If custom button captions are provided, we set them beforehand
|
||||
this.okButton.textContent = typeof okButton === 'string' ? okButton : 'OK';
|
||||
@@ -210,7 +213,7 @@ export class Popup {
|
||||
this.customInputs = customInputs;
|
||||
this.customInputs?.forEach(input => {
|
||||
if (!input.id || !(typeof input.id === 'string')) {
|
||||
console.warn('Given custom input does not have a valid id set')
|
||||
console.warn('Given custom input does not have a valid id set');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -318,20 +321,20 @@ export class Popup {
|
||||
if (String(undefined) === String(resultControl.dataset.result)) return;
|
||||
if (isNaN(result)) throw new Error('Invalid result control. Result must be a number. ' + resultControl.dataset.result);
|
||||
const type = resultControl.dataset.resultEvent || 'click';
|
||||
resultControl.addEventListener(type, () => this.complete(result));
|
||||
resultControl.addEventListener(type, async () => await this.complete(result));
|
||||
});
|
||||
|
||||
// Bind dialog listeners manually, so we can be sure context is preserved
|
||||
const cancelListener = (evt) => {
|
||||
this.complete(POPUP_RESULT.CANCELLED);
|
||||
const cancelListener = async (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
await this.complete(POPUP_RESULT.CANCELLED);
|
||||
window.removeEventListener('cancel', cancelListenerBound);
|
||||
};
|
||||
const cancelListenerBound = cancelListener.bind(this);
|
||||
this.dlg.addEventListener('cancel', cancelListenerBound);
|
||||
|
||||
const keyListener = (evt) => {
|
||||
const keyListener = async (evt) => {
|
||||
switch (evt.key) {
|
||||
case 'Enter': {
|
||||
// CTRL+Enter counts as a closing action, but all other modifiers (ALT, SHIFT) should not trigger this
|
||||
@@ -342,15 +345,23 @@ export class Popup {
|
||||
if (this.dlg != document.activeElement?.closest('.popup'))
|
||||
return;
|
||||
|
||||
// Check if the current focus is a result control. Only should we apply the compelete action
|
||||
// Check if the current focus is a result control. Only should we apply the complete action
|
||||
const resultControl = document.activeElement?.closest('.result-control');
|
||||
if (!resultControl)
|
||||
return;
|
||||
|
||||
const result = Number(document.activeElement.getAttribute('data-result') ?? this.defaultResult);
|
||||
this.complete(result);
|
||||
// Check if we are inside an input type text or a textarea field and send on enter is disabled
|
||||
const textarea = document.activeElement?.closest('textarea');
|
||||
if (textarea instanceof HTMLTextAreaElement && !shouldSendOnEnter())
|
||||
return;
|
||||
const input = document.activeElement?.closest('input[type="text"]');
|
||||
if (input instanceof HTMLInputElement && !shouldSendOnEnter())
|
||||
return;
|
||||
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const result = Number(document.activeElement.getAttribute('data-result') ?? this.defaultResult);
|
||||
await this.complete(result);
|
||||
window.removeEventListener('keydown', keyListenerBound);
|
||||
|
||||
break;
|
||||
@@ -430,8 +441,10 @@ export class Popup {
|
||||
* - All other will return the result value as provided as `POPUP_RESULT` or a custom number value
|
||||
*
|
||||
* @param {POPUP_RESULT|number} result - The result of the popup (either an existing `POPUP_RESULT` or a custom result value)
|
||||
*
|
||||
* @returns {Promise<string|number|boolean?>} A promise that resolves with the value of the popup when it is completed.
|
||||
*/
|
||||
complete(result) {
|
||||
async complete(result) {
|
||||
// In all cases besides INPUT the popup value should be the result
|
||||
/** @type {POPUP_RESULT|number|boolean|string?} */
|
||||
let value = result;
|
||||
@@ -468,6 +481,8 @@ export class Popup {
|
||||
|
||||
Popup.util.lastResult = { value, result, inputResults: this.inputResults };
|
||||
this.#hide();
|
||||
|
||||
return this.#promise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -618,8 +633,13 @@ export function fixToastrForDialogs() {
|
||||
// Now another case is if we only have one popup and that is currently closing. In that case the toastr container exists,
|
||||
// but we don't have an open dialog to move it into. It's just inside the existing one that will be gone in milliseconds.
|
||||
// To prevent new toasts from being showing up in there and then vanish in an instant,
|
||||
// we move the toastr back to the main body
|
||||
// we move the toastr back to the main body, or delete if its empty
|
||||
if (!dlg && isAlreadyPresent) {
|
||||
document.body.appendChild(toastContainer);
|
||||
if (!toastContainer.childNodes.length) {
|
||||
toastContainer.remove();
|
||||
} else {
|
||||
document.body.appendChild(toastContainer);
|
||||
toastContainer.classList.add('toast-top-center');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -468,7 +468,7 @@ export function initDefaultSlashCommands() {
|
||||
description: 'display name',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
defaultValue: '{{user}}',
|
||||
enumProvider: commonEnumProviders.characters('character'),
|
||||
enumProvider: commonEnumProviders.personas,
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
@@ -1653,8 +1653,8 @@ async function buttonsCallback(args, text) {
|
||||
const buttonElement = document.createElement('div');
|
||||
buttonElement.classList.add('menu_button', 'result-control', 'wide100p');
|
||||
buttonElement.dataset.result = String(result);
|
||||
buttonElement.addEventListener('click', () => {
|
||||
popup?.complete(result);
|
||||
buttonElement.addEventListener('click', async () => {
|
||||
await popup.complete(result);
|
||||
});
|
||||
buttonElement.innerText = button;
|
||||
buttonContainer.appendChild(buttonElement);
|
||||
@@ -2762,7 +2762,7 @@ export async function sendMessageAs(args, text) {
|
||||
const isSystem = bias && !removeMacros(mesText).length;
|
||||
const compact = isTrueBoolean(args?.compact);
|
||||
|
||||
const character = characters.find(x => x.name === name);
|
||||
const character = characters.find(x => x.avatar === name) ?? characters.find(x => x.name === name);
|
||||
let force_avatar, original_avatar;
|
||||
|
||||
if (character && character.avatar !== 'none') {
|
||||
|
@@ -160,7 +160,7 @@ async function* parseStreamData(json) {
|
||||
return;
|
||||
}
|
||||
// llama.cpp?
|
||||
else if (typeof json.content === 'string' && json.content.length > 0) {
|
||||
else if (typeof json.content === 'string' && json.content.length > 0 && json.object !== 'chat.completion.chunk') {
|
||||
for (let i = 0; i < json.content.length; i++) {
|
||||
const str = json.content[i];
|
||||
yield {
|
||||
|
@@ -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,
|
||||
|
@@ -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(/\.[^/.]+$/, '');
|
||||
|
@@ -1358,7 +1358,11 @@ export function registerVariableCommands() {
|
||||
name: 'times',
|
||||
callback: timesCallback,
|
||||
returns: 'result of the last executed command',
|
||||
namedArgumentList: [],
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'guard', 'disable loop iteration limit', [ARGUMENT_TYPE.STRING], false, false, null, commonEnumProviders.boolean('onOff')(),
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'repeats',
|
||||
|
@@ -499,15 +499,16 @@ function convertWorldInfoToCharacterBook(name, entries) {
|
||||
* Import a character from a YAML file.
|
||||
* @param {string} uploadPath Path to the uploaded file
|
||||
* @param {{ request: import('express').Request, response: import('express').Response }} context Express request and response objects
|
||||
* @param {string|undefined} preservedFileName Preserved file name
|
||||
* @returns {Promise<string>} Internal name of the character
|
||||
*/
|
||||
async function importFromYaml(uploadPath, context) {
|
||||
async function importFromYaml(uploadPath, context, preservedFileName) {
|
||||
const fileText = fs.readFileSync(uploadPath, 'utf8');
|
||||
fs.rmSync(uploadPath);
|
||||
const yamlData = yaml.parse(fileText);
|
||||
console.log('Importing from YAML');
|
||||
yamlData.name = sanitize(yamlData.name);
|
||||
const fileName = getPngName(yamlData.name, context.request.user.directories);
|
||||
const fileName = preservedFileName || getPngName(yamlData.name, context.request.user.directories);
|
||||
let char = convertToV2({
|
||||
'name': yamlData.name,
|
||||
'description': yamlData.context ?? '',
|
||||
@@ -532,9 +533,10 @@ async function importFromYaml(uploadPath, context) {
|
||||
* @param {string} uploadPath
|
||||
* @param {object} params
|
||||
* @param {import('express').Request} params.request
|
||||
* @param {string|undefined} preservedFileName Preserved file name
|
||||
* @returns {Promise<string>} Internal name of the character
|
||||
*/
|
||||
async function importFromCharX(uploadPath, { request }) {
|
||||
async function importFromCharX(uploadPath, { request }, preservedFileName) {
|
||||
const data = fs.readFileSync(uploadPath);
|
||||
fs.rmSync(uploadPath);
|
||||
console.log('Importing from CharX');
|
||||
@@ -567,7 +569,7 @@ async function importFromCharX(uploadPath, { request }) {
|
||||
unsetFavFlag(card);
|
||||
card['create_date'] = humanizedISO8601DateTime();
|
||||
card.name = sanitize(card.name);
|
||||
const fileName = getPngName(card.name, request.user.directories);
|
||||
const fileName = preservedFileName || getPngName(card.name, request.user.directories);
|
||||
const result = await writeCharacterData(avatar, JSON.stringify(card), fileName, request);
|
||||
return result ? fileName : '';
|
||||
}
|
||||
@@ -576,9 +578,10 @@ async function importFromCharX(uploadPath, { request }) {
|
||||
* Import a character from a JSON file.
|
||||
* @param {string} uploadPath Path to the uploaded file
|
||||
* @param {{ request: import('express').Request, response: import('express').Response }} context Express request and response objects
|
||||
* @param {string|undefined} preservedFileName Preserved file name
|
||||
* @returns {Promise<string>} Internal name of the character
|
||||
*/
|
||||
async function importFromJson(uploadPath, { request }) {
|
||||
async function importFromJson(uploadPath, { request }, preservedFileName) {
|
||||
const data = fs.readFileSync(uploadPath, 'utf8');
|
||||
fs.unlinkSync(uploadPath);
|
||||
|
||||
@@ -590,7 +593,7 @@ async function importFromJson(uploadPath, { request }) {
|
||||
unsetFavFlag(jsonData);
|
||||
jsonData = readFromV2(jsonData);
|
||||
jsonData['create_date'] = humanizedISO8601DateTime();
|
||||
const pngName = getPngName(jsonData.data?.name || jsonData.name, request.user.directories);
|
||||
const pngName = preservedFileName || getPngName(jsonData.data?.name || jsonData.name, request.user.directories);
|
||||
const char = JSON.stringify(jsonData);
|
||||
const result = await writeCharacterData(defaultAvatarPath, char, pngName, request);
|
||||
return result ? pngName : '';
|
||||
@@ -600,7 +603,7 @@ async function importFromJson(uploadPath, { request }) {
|
||||
if (jsonData.creator_notes) {
|
||||
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
|
||||
}
|
||||
const pngName = getPngName(jsonData.name, request.user.directories);
|
||||
const pngName = preservedFileName || getPngName(jsonData.name, request.user.directories);
|
||||
let char = {
|
||||
'name': jsonData.name,
|
||||
'description': jsonData.description ?? '',
|
||||
@@ -626,7 +629,7 @@ async function importFromJson(uploadPath, { request }) {
|
||||
if (jsonData.creator_notes) {
|
||||
jsonData.creator_notes = jsonData.creator_notes.replace('Creator\'s notes go here.', '');
|
||||
}
|
||||
const pngName = getPngName(jsonData.char_name, request.user.directories);
|
||||
const pngName = preservedFileName || getPngName(jsonData.char_name, request.user.directories);
|
||||
let char = {
|
||||
'name': jsonData.char_name,
|
||||
'description': jsonData.char_persona ?? '',
|
||||
@@ -1089,8 +1092,8 @@ function getPngName(file, directories) {
|
||||
* @returns {string | undefined} - The preserved name if the request is valid, otherwise undefined
|
||||
*/
|
||||
function getPreservedName(request) {
|
||||
return request.body.file_type === 'png' && request.body.preserve_file_name === 'true' && request.file?.originalname
|
||||
? path.parse(request.file.originalname).name
|
||||
return typeof request.body.preserved_name === 'string' && request.body.preserved_name.length > 0
|
||||
? path.parse(request.body.preserved_name).name
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -1123,6 +1126,10 @@ router.post('/import', urlencodedParser, async function (request, response) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
if (preservedFileName) {
|
||||
invalidateThumbnail(request.user.directories, 'avatar', `${preservedFileName}.png`);
|
||||
}
|
||||
|
||||
response.send({ file_name: fileName });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
|
@@ -3,7 +3,7 @@ const path = require('path');
|
||||
const express = require('express');
|
||||
const _ = require('lodash');
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
const { PUBLIC_DIRECTORIES, SETTINGS_FILE } = require('../constants');
|
||||
const { SETTINGS_FILE } = require('../constants');
|
||||
const { getConfigValue, generateTimestamp, removeOldBackups } = require('../util');
|
||||
const { jsonParser } = require('../express-common');
|
||||
const { getAllUserHandles, getUserDirectories } = require('../users');
|
||||
@@ -27,12 +27,12 @@ const AUTOSAVE_FUNCTIONS = new Map();
|
||||
*/
|
||||
function triggerAutoSave(handle) {
|
||||
if (!AUTOSAVE_FUNCTIONS.has(handle)) {
|
||||
const throttledAutoSave = _.throttle(() => backupUserSettings(handle), AUTOSAVE_INTERVAL);
|
||||
const throttledAutoSave = _.throttle(() => backupUserSettings(handle, true), AUTOSAVE_INTERVAL);
|
||||
AUTOSAVE_FUNCTIONS.set(handle, throttledAutoSave);
|
||||
}
|
||||
|
||||
const functionToCall = AUTOSAVE_FUNCTIONS.get(handle);
|
||||
if (functionToCall) {
|
||||
if (functionToCall && typeof functionToCall === 'function') {
|
||||
functionToCall();
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ async function backupSettings() {
|
||||
const userHandles = await getAllUserHandles();
|
||||
|
||||
for (const handle of userHandles) {
|
||||
backupUserSettings(handle);
|
||||
backupUserSettings(handle, true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Could not backup settings file', err);
|
||||
@@ -123,13 +123,18 @@ async function backupSettings() {
|
||||
/**
|
||||
* Makes a backup of the user's settings file.
|
||||
* @param {string} handle User handle
|
||||
* @param {boolean} preventDuplicates Prevent duplicate backups
|
||||
* @returns {void}
|
||||
*/
|
||||
function backupUserSettings(handle) {
|
||||
function backupUserSettings(handle, preventDuplicates) {
|
||||
const userDirectories = getUserDirectories(handle);
|
||||
const backupFile = path.join(userDirectories.backups, `${getFilePrefix(handle)}${generateTimestamp()}.json`);
|
||||
const sourceFile = path.join(userDirectories.root, SETTINGS_FILE);
|
||||
|
||||
if (preventDuplicates && isDuplicateBackup(handle, sourceFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(sourceFile)) {
|
||||
return;
|
||||
}
|
||||
@@ -138,6 +143,52 @@ function backupUserSettings(handle) {
|
||||
removeOldBackups(userDirectories.backups, `settings_${handle}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the backup would be a duplicate.
|
||||
* @param {string} handle User handle
|
||||
* @param {string} sourceFile Source file path
|
||||
* @returns {boolean} True if the backup is a duplicate
|
||||
*/
|
||||
function isDuplicateBackup(handle, sourceFile) {
|
||||
const latestBackup = getLatestBackup(handle);
|
||||
if (!latestBackup) {
|
||||
return false;
|
||||
}
|
||||
return areFilesEqual(latestBackup, sourceFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the two files are equal.
|
||||
* @param {string} file1 File path
|
||||
* @param {string} file2 File path
|
||||
*/
|
||||
function areFilesEqual(file1, file2) {
|
||||
if (!fs.existsSync(file1) || !fs.existsSync(file2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const content1 = fs.readFileSync(file1);
|
||||
const content2 = fs.readFileSync(file2);
|
||||
return content1.toString() === content2.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the latest backup file for a user.
|
||||
* @param {string} handle User handle
|
||||
* @returns {string|null} Latest backup file. Null if no backup exists.
|
||||
*/
|
||||
function getLatestBackup(handle) {
|
||||
const userDirectories = getUserDirectories(handle);
|
||||
const backupFiles = fs.readdirSync(userDirectories.backups)
|
||||
.filter(x => x.startsWith(getFilePrefix(handle)))
|
||||
.map(x => ({ name: x, ctime: fs.statSync(path.join(userDirectories.backups, x)).ctimeMs }));
|
||||
const latestBackup = backupFiles.sort((a, b) => b.ctime - a.ctime)[0]?.name;
|
||||
if (!latestBackup) {
|
||||
return null;
|
||||
}
|
||||
return path.join(userDirectories.backups, latestBackup);
|
||||
}
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/save', jsonParser, function (request, response) {
|
||||
@@ -265,7 +316,7 @@ router.post('/load-snapshot', jsonParser, async (request, response) => {
|
||||
|
||||
router.post('/make-snapshot', jsonParser, async (request, response) => {
|
||||
try {
|
||||
backupUserSettings(request.user.profile.handle);
|
||||
backupUserSettings(request.user.profile.handle, false);
|
||||
response.sendStatus(204);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
@@ -371,7 +371,7 @@ function generateTimestamp() {
|
||||
* @param {string} prefix File prefix to filter backups by.
|
||||
*/
|
||||
function removeOldBackups(directory, prefix) {
|
||||
const MAX_BACKUPS = 50;
|
||||
const MAX_BACKUPS = Number(getConfigValue('numberOfBackups', 50));
|
||||
|
||||
let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix));
|
||||
if (files.length > MAX_BACKUPS) {
|
||||
|
Reference in New Issue
Block a user