Merge branch 'staging' into tool-calling

This commit is contained in:
Cohee 2024-10-05 20:31:28 +03:00
commit 6369882be3
22 changed files with 496 additions and 248 deletions

View File

@ -6641,7 +6641,7 @@
<script src="lib/popper.js"></script>
<script src="lib/purify.min.js"></script>
<script src="lib/highlight.min.js"></script>
<script src="lib/moment.min.js"></script>
<script src="lib/moment-with-locales.min.js"></script>
<script src="lib/cropper.min.js"></script>
<script src="lib/jquery-cropper.min.js"></script>
<script src="lib/toastr.min.js"></script>

2
public/lib/moment-with-locales.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -56,7 +56,7 @@
"Rep. Pen. Slope": "Rep. Pen. Slope",
"Top K": "Top K",
"Top P": "Top P",
"Do Sample": "Сделать образец",
"Do Sample": "Включить сэмплинг",
"Add BOS Token": "Добавлять BOS-токен",
"Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative": "Добавлять BOS-токен в начале промпта. Если выключить, ответы могут стать более креативными.",
"Ban EOS Token": "Запретить EOS-токен",
@ -451,8 +451,8 @@
"Creator's Notes": "Заметки создателя",
"A-Z": "A-Z",
"Z-A": "Z-A",
"Newest": "Новейшие",
"Oldest": "Старейшие",
"Newest": "Сначала новые",
"Oldest": "Сначала старые",
"Favorites": "Избранные",
"Recent": "Последние",
"Most chats": "Больше всего чатов",
@ -564,7 +564,7 @@
"Advanced Definition": "Расширенное описание",
"Character Lore": "Лор персонажа",
"Export and Download": "Экспортировать и скачать",
"Duplicate Character": "Дублировать персонажа",
"Duplicate Character": "Клонировать персонажа",
"Create Character": "Создать персонажа",
"Delete Character": "Удалить персонажа",
"View all tags": "Показать все тэги",
@ -1690,7 +1690,7 @@
"Delete Message": "Удалить сообщение",
"Delete Swipe": "Удалить свайп",
"Could not get a reply from API. Check your connection settings / API key and try again.": "Не удалось получить ответ от API. Проверьте настройки соединения и API-ключ и повторите попытку.",
"Connecting To Proxy": "Подключение к прокси",
"Connecting To Proxy": "Подключиться к прокси",
"Are you sure you want to connect to the following proxy URL?": "Вы точно хотите соединиться с прокси по этому адресу?",
"API connection successful!": "Соединение с API установлено!",
"Proxy Saved": "Прокси сохранена",
@ -1747,5 +1747,151 @@
"Markdown Hotkeys": "Горячие клавиши для разметки",
"markdown_hotkeys_desc": "Включить горячие клавиши для вставки символов разметки в некоторых полях ввода. См. '/help hotkeys'.",
"Save and Update": "Сохранить и обновить",
"Profile name:": "Название профиля:"
"Profile name:": "Название профиля:",
"API returned an error": "API вернуло ошибку",
"Failed to save preset": "Не удалось сохранить пресет",
"Preset name should be unique.": "Название пресета должно быть уникальным.",
"Invalid file": "Невалидный файл",
"No preset selected": "Пресет не выбран",
"Invalid logit bias preset file.": "Файл пресета невалиден.",
"Preset was not deleted from server": "Пресет не удалён с сервера",
"Preset deleted": "Пресет удалён",
"Delete the preset?": "Удалить пресет?",
"Preset updated": "Пресет обновлён",
"Entered reverse proxy address is not a valid URL": "Введённый адрес прокси невалиден",
"An error occurred while counting tokens: Token budget exceeded.": "Ошибка при подсчёте токенов: Превышен бюджет токенов",
"An error occurred while counting tokens: Invalid character name": "Ошибка при подсчёте токенов: Невалидное имя персонажа",
"Not enough free tokens for mandatory prompts. Raise your token Limit or disable custom prompts.": "Недостаточно токенов для всех выбранных промптов. Повысьте лимит токенов или отключите часть промптов.",
"The name of at least one character contained whitespaces or special characters. Please check your user and character name.": "В имени одного из персонажей содержится пробел или иной спецсимвол. Проверьте имена пользователя и персонажа.",
"An unknown error occurred while counting tokens. Further information may be available in console.": "Неизвестная ошибка при подсчёте токенов. Проверьте консоль, возможно, подробная информация есть там.",
"Encountered an error while processing your request.": "При обработке вашего запроса возникла ошибка.",
"Check you have credits available on your": "Убедитесь, что на вашем",
"OpenAI account quora_error": "аккаунте OpenAI",
"dot quota_error": "имеется достаточно кредитов.",
"If you have sufficient credits, please try again later.": "Если кредитов достаточно, то повторите попытку позднее.",
"Proxy preset '${0}' not found": "Пресет '${0}' не найден",
"Window.ai returned an error": "Window.ai вернул ошибку",
"Get it here:": "Загрузите здесь:",
"Extension is not installed": "Расширение не установлено",
"Update or remove your reverse proxy settings.": "Измените или удалите ваши настройки прокси.",
"An error occurred while importing prompts. More info available in console.": "В процессе импорта произошла ошибка. Подробную информацию см. в консоли.",
"Could not import prompts. Export failed validation.": "Не удалось импортировать промпты. Не пройдена валидация при экспорте.",
"Prompt import complete.": "Импорт завершён.",
"Are you sure you want to delete this prompt?": "Вы точно хотите удалить этот промпт?",
"Existing prompts with the same ID will be overridden. Do you want to proceed?": "Имеющиеся промпты с совпадающими идентификаторами будут перезаписаны. Продолжить?",
"This will reset the prompt order for this character. You will not lose any prompts.": "Будет сброшен порядок промптов для этого персонажа. Сами промпты вы не потеряете.",
"Note:": "Примечание:",
"this chat is temporary and will be deleted as soon as you leave it.": "это временный чат, он будет удалён, как только вы из него выйдете.",
"help_hotkeys_20": "Горячие клавиши для разметки",
"help_hotkeys_21": "Работают в окне ввода чата, а также в полях, отмеченных этим значком:",
"help_hotkeys_22": "**полужирный**",
"help_hotkeys_23": "*курсив*",
"help_hotkeys_24": "__подчёркивание__",
"help_hotkeys_25": "`inline-код`",
"help_hotkeys_26": "~~зачёркнутый~~",
"ext_regex_only_format_visual_desc": "Содержимое файла с историей чата останется нетронутым, изменения будут лишь визуальными (в UI).",
"Could not convert file": "Не удалось сконвертировать файл",
"Could not upload file": "Не удалось загрузить файл",
"Could not download file": "Не удалось скачать файл",
"File is too big. Maximum size is ${0}.": "Слишком большой файл. Максимальный размер: ${0}.",
"Binary files are not supported. Select a text file or image.": "Бинарные файлы не поддерживаются. Выберите текстовый файл или изображение.",
"No character or group selected": "Не выбрано ни одного персонажа или группы",
"Could not delete file": "Не удалось удалить файл",
"No attachments selected.": "Вложение не выбрано.",
"Data Bank": "Банк данных",
"No files were scraped.": "Скрапинг не выполнен.",
"Scraped ${0} files from ${1} to ${2}.": "Соскраплено из ${0} файлов из ${1} в ${2}.",
"Check browser console for details.": "Подробности см. в консоли браузера.",
"Scraping failed": "Ошибка скрапинга",
"External media has been blocked": "Внешние медиа отключены",
"Use the 'Ext. Media' button to allow it. Click on this message to dismiss.": "Разрешить можно с помощью кнопки 'Внешн. медиа'. Нажмите на это сообщение, чтобы его скрыть.",
"Couldn't get CSRF token. Please refresh the page.": "Не удалось получить CSRF токен. Попробуйте перезагрузить страницу.",
"Error": "Ошибка",
"API Error": "Ошибка API",
"Please wait until the chat is saved before switching characters.": "Пожалуйста, дождитесь сохранения чата, прежде чем переключать персонажа.",
"Your chat is still saving...": "Чат всё ещё сохраняется...",
"Character ${0} not found in the list": "Персонаж ${0} не найден в списке",
"Streaming is enabled, but the version of Kobold used does not support token streaming.": "Включён стриминг текста, но ваша версия Kobold не поддерживает стриминг токенов.",
"Streaming is not supported for the Legacy API. Update Ooba and use new API to enable streaming.": "Для устаревшего API стриминг недоступен. Обновите oobaboga и используйте новый API, чтобы включить стриминг.",
"Verify that the server is running and accessible.": "Убедитесь, что сервер запущен и доступен по сети.",
"ST Server cannot be reached": "Не удалось соединиться с сервером ST",
"You must first select a character to duplicate!": "Вы не выбрали персонажа, которого хотите клонировать!",
"Character Duplicated": "Персонаж склонирован",
"No character name provided.": "Вы не ввели имя персонажа.",
"Rename Character": "Переименование",
"No character selected.": "Вы не выбрали персонажа.",
"New name:": "Новое имя:",
"Same character name provided, so name did not change.": "Введено то же самое имя, ничего не изменилось.",
"Character renamed and past chats updated!": "Персонаж переименован, а чаты обновлены!",
"Character renamed!": "Персонаж переименован!",
"Something went wrong. The page will be reloaded.": "Что-то пошло не так. Страница будет перезагружена.",
"Past chat could not be updated: ${0}": "Не удалось обновить чат ${0}",
"Trying to save group chat with regular saveChat function. Aborting to prevent corruption.": "Произошла попытка сохранения группового чата функцией saveChat. Откатываем изменения, чтобы предотвратить потерю данных.",
"Check the server connection and reload the page to prevent data loss.": "Проверьте связь с сервером и перезагрузите страницу, чтобы избежать потери данных.",
"Chat could not be saved": "Не удалось сохранить чат",
"Settings could not be loaded after multiple attempts. Please try again later.": "Не удалось загрузить настройки за несколько попыток. Попробуйте позднее.",
"Settings could not be saved": "Не удалось сохранить настройки",
"Could not load chat data. Try reloading the page.": "Не удалось загрузить чат. Попробуйте обновить страницу.",
"Invalid process (no 'type')": "Невалидный процесс (нет параметра 'type')",
"Character Deleted: ${0}": "Персонаж удалён: ${0}",
"Character Created: ${0}": "Персонаж создан: ${0}",
"Group Created": "Группа создана",
"Group Deleted": "Группа удалена",
"Character Imported: ${0}": "Персонаж импортирован: ${0}",
"Invalid swipe ID: ${0}": "Некорректный идентификатор свайпа: ${0}",
"No messages to delete swipes from.": "Сообщение, из которого требуется удалить свайп, не найдено.",
"Can't delete the last swipe.": "Невозможно удалить единственный свайп.",
"GUI Settings preset is not supported for Horde. Please select another preset.": "Для Horde не поддерживаются пресеты настроек GUI. Пожалуйста, выберите другой пресет.",
"Embedded lorebook will be removed from this character.": "Встроенный лорбук будет удалён из персонажа.",
"Name is required": "Введите имя",
"Cannot create characters while generating. Stop the request and try again.": "Во время генерации ответа создать персонажа невозможно. Остановите запрос и повторите попытку.",
"Creation aborted": "Процесс создания прерван",
"Failed to create character": "Не удалось создать персонажа",
"Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.": "Что-то пошло не так в процессе сохранения персонажа, либо вы загрузили файл некорректного формата. Проверьте, что изображение точно не в формате webp.",
"Context template '${0}' not found": "Шаблон контекста '${0}' не найден",
"Instruct template '${0}' not found": "Шаблон Instruct-режима '${0}' не найден",
"Error: ${0} is not a valid API": "Ошибка: ${0} не является валидным API",
"API set to ${0}, trying to connect..": "Установлено API ${0}, пробуем подключиться...",
"Unsupported file type: ": "Неподдерживаемый тип файла: ",
"Cannot import characters while generating. Stop the request and try again.": "Во время генерации ответа импорт персонажа невозможен. Остановите запрос и повторите попытку.",
"Import aborted": "Процесс импорта прерван",
"The file is likely invalid or corrupted.": "Вероятно, файл невалиден или повреждён.",
"Could not import character": "Не удалось импортировать персонажа",
"Cannot run /impersonate command while the reply is being generated.": "Во время генерации ответа выполнить /impersonate невозможно.",
"Name must be provided as an argument to rename this chat.": "Для переименования чата необходимо предоставить новое имя в качестве аргумента.",
"No chat selected that can be renamed.": "Чат, который требуется переименовать, не найден.",
"Successfully renamed chat to: ${0}": "Чат успешно переименован в: ${0}",
"Character ${0} not found. Skipping deletion.": "Персонаж ${0} не найден. Удаление пропускается.",
"Failed to delete character": "При удалении персонажа произошло ошибка",
"Are you sure you want to duplicate this character?": "Вы точно хотите клонировать этого персонажа?",
"If you just want to start a new chat with the same character...": "Если вы хотите просто создать новый чат, воспользуйтесь кнопкой \"Начать новый чат\" в меню слева внизу.",
"THIS IS PERMANENT!": "ОТМЕНИТЬ БУДЕТ НЕВОЗМОЖНО!",
"Also delete the chat files": "Также удалить файлы чатов",
"Delete the character?": "Удалить персонажа?",
"Not a valid number": "Некорректное число",
"Author's Note depth updated": "Глубина заметок автора обновлена",
"Author's Note frequency updated": "Частота заметок автора обновлена",
"Not a valid position": "Некорректная позиция",
"Author's Note position updated": "Позиция заметок автора обновлена",
"Something went wrong. Could not save character's author's note.": "Что-то пошло не так. Не удалось сохранить заметки автора для этого персонажа.",
"Select a character before trying to use Author's Note": "Сначала необходимо выбрать персонажа",
"Author's Note text updated": "Текст заметок автора обновлён",
"Group Validation": "Валидация группы",
"Warning: Listed member ${0} does not exist as a character. It will be removed from the group.": "Предупреждение: персонаж ${0} не существует в виде карточки. Он будет удалён из группы.",
"Group Chat could not be saved": "Не удалось сохранить групповой чат",
"Deleted group member swiped. To get a reply, add them back to the group.": "Вы пытаетесь свайпнуть удалённого члена группы. Чтобы получить ответ, добавьте этого персонажа обратно в группу.",
"Currently no group selected.": "В данный момент не выбрано ни одной группы.",
"Not so fast! Wait for the characters to stop typing before deleting the group.": "Чуть помедленнее! Перед удалением группы дождитесь, пока персонаж закончит печатать.",
"Delete the group?": "Удалить группу?",
"This will also delete all your chats with that group. If you want to delete a single conversation, select a \"View past chats\" option in the lower left menu.": "Вместе с ней будут удалены и все её чаты. Если требуется удалить только один чат, воспользуйтесь кнопкой \"Все чаты\" в меню в левом нижнем углу.",
"Can't peek a character while group reply is being generated": "Невозможно открыть карточку персонажа во время генерации ответа",
"Threshold": "Порог",
"DRY Repetition Penalty": "DRY Штраф за повтор",
"Multiplier": "Множитель",
"DRY_Multiplier_desc": "Поставьте в положение > 0, чтобы включить DRY. Определяет величину штрафа для кратчайшей \"штрафуемой\" строки.",
"DRY_Repetition_Penalty_desc": "DRY налагает штраф на токены, генерация которых приведёт к появлению строки, которая уже была в тексте раньше. Установите множитель = 0, чтобы отключить.",
"Base": "Основание",
"DRY_Base_desc": "Определяет, насколько быстро возрастает штраф с увеличением длины строки.",
"Allowed Length": "Допустимая длина",
"DRY_Allowed_Length_desc": "Длина повторяющейся строки, при превышении которой DRY начинает налагать штраф."
}

View File

@ -294,10 +294,17 @@ DOMPurify.addHook('afterSanitizeAttributes', function (node) {
}
});
DOMPurify.addHook('uponSanitizeAttribute', (_, data, config) => {
DOMPurify.addHook('uponSanitizeAttribute', (node, data, config) => {
if (!config['MESSAGE_SANITIZE']) {
return;
}
/* Retain the classes on UI elements of messages that interact with the main UI */
const permittedNodeTypes = ['BUTTON', 'DIV'];
if (config['MESSAGE_ALLOW_SYSTEM_UI'] && node.classList.contains('menu_button') && permittedNodeTypes.includes(node.nodeName)) {
return;
}
switch (data.attrName) {
case 'class': {
if (data.attrValue) {
@ -385,8 +392,8 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
if (localStorage.getItem(warningShownKey) === null) {
const warningToast = toastr.warning(
'Use the "Ext. Media" button to allow it. Click on this message to dismiss.',
'External media has been blocked',
t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,
t`External media has been blocked`,
{
timeOut: 0,
preventDuplicates: true,
@ -651,6 +658,7 @@ async function getSystemMessages() {
force_avatar: system_avatar,
is_user: false,
is_system: true,
uses_system_ui: true,
mes: await renderTemplateAsync('welcome', { displayVersion }),
},
group: {
@ -926,7 +934,7 @@ async function firstLoadInit() {
token = tokenData.token;
} catch {
hideLoader();
toastr.error('Couldn\'t get CSRF token. Please refresh the page.', 'Error', { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true });
toastr.error(t`Couldn't get CSRF token. Please refresh the page.`, t`Error`, { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true });
throw new Error('Initialization failed');
}
@ -1142,7 +1150,7 @@ async function getStatusKobold() {
// We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress.
if (online_status === 'no_connection' && data.response) {
toastr.error(data.response, 'API Error', { timeOut: 5000, preventDuplicates: true });
toastr.error(data.response, t`API Error`, { timeOut: 5000, preventDuplicates: true });
}
} catch (err) {
console.error('Error getting status', err);
@ -1228,7 +1236,7 @@ async function getStatusTextgen() {
// We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress.
if (online_status === 'no_connection' && data.response) {
toastr.error(data.response, 'API Error', { timeOut: 5000, preventDuplicates: true });
toastr.error(data.response, t`API Error`, { timeOut: 5000, preventDuplicates: true });
}
} catch (err) {
if (err instanceof AbortReason) {
@ -1280,7 +1288,7 @@ export async function selectCharacterById(id) {
}
if (isChatSaving) {
toastr.info('Please wait until the chat is saved before switching characters.', 'Your chat is still saving...');
toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
return;
}
@ -1634,7 +1642,7 @@ export async function getOneCharacter(avatarUrl) {
if (indexOf !== -1) {
characters[indexOf] = getData;
} else {
toastr.error(`Character ${avatarUrl} not found in the list`, 'Error', { timeOut: 5000, preventDuplicates: true });
toastr.error(t`Character ${avatarUrl} not found in the list`, t`Error`, { timeOut: 5000, preventDuplicates: true });
}
}
}
@ -1917,9 +1925,10 @@ export async function sendTextareaMessage() {
* @param {boolean} isSystem If the message was sent by the system
* @param {boolean} isUser If the message was sent by the user
* @param {number} messageId Message index in chat array
* @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides
* @returns {string} HTML string
*/
export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {
export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}) {
if (!mes) {
return '';
}
@ -1994,15 +2003,33 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {
});
}
mes = mes.replace(/```[\s\S]*?```|``[\s\S]*?``|`[\s\S]*?`|(".+?")|(\u201C.+?\u201D)/gm, function (match, p1, p2) {
if (p1) {
return '<q>"' + p1.replace(/"/g, '') + '"</q>';
} else if (p2) {
return '<q>“' + p2.replace(/\u201C|\u201D/g, '') + '”</q>';
} else {
return match;
mes = mes.replace(
/```[\s\S]*?```|``[\s\S]*?``|`[\s\S]*?`|(".*?")|(\u201C.*?\u201D)|(\u00AB.*?\u00BB)|(\u300C.*?\u300D)|(\u300E.*?\u300F)|(\uFF02.*?\uFF02)/gm,
function (match, p1, p2, p3, p4, p5, p6) {
if (p1) {
// English double quotes
return `<q>"${p1.slice(1, -1)}"</q>`;
} else if (p2) {
// Curly double quotes “ ”
return `<q>“${p2.slice(1, -1)}”</q>`;
} else if (p3) {
// Guillemets « »
return `<q>«${p3.slice(1, -1)}»</q>`;
} else if (p4) {
// Corner brackets 「 」
return `<q>「${p4.slice(1, -1)}」</q>`;
} else if (p5) {
// White corner brackets 『 』
return `<q>『${p5.slice(1, -1)}』</q>`;
} else if (p6) {
// Fullwidth quotes
return `<q>${p6.slice(1, -1)}</q>`;
} else {
// Return the original match if no quotes are found
return match;
}
}
});
);
// Restore double quotes in tags
if (!power_user.encode_tags) {
@ -2030,7 +2057,7 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {
}
/** @type {any} */
const config = { MESSAGE_SANITIZE: true, ADD_TAGS: ['custom-style'] };
const config = { MESSAGE_SANITIZE: true, ADD_TAGS: ['custom-style'], ...sanitizerOverrides };
mes = encodeStyleTags(mes);
mes = DOMPurify.sanitize(mes, config);
mes = decodeStyleTags(mes);
@ -2235,6 +2262,18 @@ export function addCopyToCodeBlocks(messageElement) {
}
/**
* Adds a single message to the chat.
* @param {object} mes Message object
* @param {object} [options] Options
* @param {string} [options.type='normal'] Message type
* @param {number} [options.insertAfter=null] Message ID to insert the new message after
* @param {boolean} [options.scroll=true] Whether to scroll to the new message
* @param {number} [options.insertBefore=null] Message ID to insert the new message before
* @param {number} [options.forceId=null] Force the message ID
* @param {boolean} [options.showSwipes=true] Whether to show swipe buttons
* @returns {void}
*/
export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) {
let messageText = mes['mes'];
const momentDate = timestampToMoment(mes.send_date);
@ -2263,7 +2302,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
} else if (this_chid === undefined) {
avatarImg = system_avatar;
} else {
if (characters[this_chid].avatar != 'none') {
if (characters[this_chid].avatar !== 'none') {
avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar);
} else {
avatarImg = default_avatar;
@ -2278,12 +2317,16 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
avatarImg = mes['force_avatar'];
}
// if mes.uses_system_ui is true, set an override on the sanitizer options
const sanitizerOverrides = mes.uses_system_ui ? { MESSAGE_ALLOW_SYSTEM_UI: true } : {};
messageText = messageFormatting(
messageText,
mes.name,
isSystem,
mes.is_user,
chat.indexOf(mes),
sanitizerOverrides,
);
const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);
let bookmarkLink = mes?.extra?.bookmark_link ?? '';
@ -2335,7 +2378,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
}
//shows or hides the Prompt display button
let mesIdToFind = type == 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId'));
let mesIdToFind = type === 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId'));
//if we have itemized messages, and the array isn't null..
if (params.isUser === false && Array.isArray(itemizedPrompts) && itemizedPrompts.length > 0) {
@ -2656,7 +2699,7 @@ export function sendSystemMessage(type, text, extra = {}) {
newMessage.mes = text;
}
if (type == system_message_types.SLASH_COMMANDS) {
if (type === system_message_types.SLASH_COMMANDS) {
newMessage.mes = getSlashCommandsHelp();
}
@ -2670,7 +2713,7 @@ export function sendSystemMessage(type, text, extra = {}) {
chat.push(newMessage);
addOneMessage(newMessage);
is_send_press = false;
if (type == system_message_types.SLASH_COMMANDS) {
if (type === system_message_types.SLASH_COMMANDS) {
const browser = new SlashCommandBrowser();
const spinner = document.querySelector('#chat .last_mes .custom-slashHelp');
const parent = spinner.parentElement;
@ -3398,7 +3441,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
await eventSource.emit(event_types.GENERATION_AFTER_COMMANDS, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage }, dryRun);
if (main_api == 'kobold' && kai_settings.streaming_kobold && !kai_flags.can_use_streaming) {
toastr.error('Streaming is enabled, but the version of Kobold used does not support token streaming.', undefined, { timeOut: 10000, preventDuplicates: true });
toastr.error(t`Streaming is enabled, but the version of Kobold used does not support token streaming.`, undefined, { timeOut: 10000, preventDuplicates: true });
unblockGeneration(type);
return Promise.resolve();
}
@ -3407,7 +3450,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
textgen_settings.streaming &&
textgen_settings.legacy_api &&
textgen_settings.type === OOBA) {
toastr.error('Streaming is not supported for the Legacy API. Update Ooba and use new API to enable streaming.', undefined, { timeOut: 10000, preventDuplicates: true });
toastr.error(t`Streaming is not supported for the Legacy API. Update Ooba and use new API to enable streaming.`, undefined, { timeOut: 10000, preventDuplicates: true });
unblockGeneration(type);
return Promise.resolve();
}
@ -3423,7 +3466,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
if (!pingResult) {
unblockGeneration(type);
toastr.error('Verify that the server is running and accessible.', 'ST Server cannot be reached');
toastr.error(t`Verify that the server is running and accessible.`, t`ST Server cannot be reached`);
throw new Error('Server unreachable');
}
@ -4476,7 +4519,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
generatedPromptCache = '';
if (data?.response) {
toastr.error(data.response, 'API Error', { preventDuplicates: true });
toastr.error(data.response, t`API Error`, { preventDuplicates: true });
}
throw new Error(data?.response);
}
@ -4585,7 +4628,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
function onError(exception) {
if (typeof exception?.error?.message === 'string') {
toastr.error(exception.error.message, 'Error', { timeOut: 10000, extendedTimeOut: 20000 });
toastr.error(exception.error.message, t`Error`, { timeOut: 10000, extendedTimeOut: 20000 });
}
generatedPromptCache = '';
@ -4989,7 +5032,7 @@ function addChatsSeparator(mesSendString) {
async function duplicateCharacter() {
if (!this_chid) {
toastr.warning('You must first select a character to duplicate!');
toastr.warning(t`You must first select a character to duplicate!`);
return '';
}
@ -5008,7 +5051,7 @@ async function duplicateCharacter() {
body: JSON.stringify(body),
});
if (response.ok) {
toastr.success('Character Duplicated');
toastr.success(t`Character Duplicated`);
const data = await response.json();
await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: body.avatar_url, newAvatar: data.path });
await getCharacters();
@ -5855,23 +5898,23 @@ export function setSendButtonState(value) {
export async function renameCharacter(name = null, { silent = false, renameChats = null } = {}) {
if (!name && silent) {
toastr.warning('No character name provided.', 'Rename Character');
toastr.warning(t`No character name provided.`, t`Rename Character`);
return false;
}
if (this_chid === undefined) {
toastr.warning('No character selected.', 'Rename Character');
toastr.warning(t`No character selected.`, t`Rename Character`);
return false;
}
const oldAvatar = characters[this_chid].avatar;
const newValue = name || await callGenericPopup('<h3>New name:</h3>', POPUP_TYPE.INPUT, characters[this_chid].name);
const newValue = name || await callGenericPopup('<h3>' + t`New name:` + '</h3>', POPUP_TYPE.INPUT, characters[this_chid].name);
if (!newValue) {
toastr.warning('No character name provided.', 'Rename Character');
toastr.warning(t`No character name provided.`, t`Rename Character`);
return false;
}
if (newValue === characters[this_chid].name) {
toastr.info('Same character name provided, so name did not change.', 'Rename Character');
toastr.info(t`Same character name provided, so name did not change.`, t`Rename Character`);
return false;
}
@ -5918,9 +5961,9 @@ export async function renameCharacter(name = null, { silent = false, renameChats
if (renamePastChatsConfirm) {
await renamePastChats(newAvatar, newValue);
await reloadCurrentChat();
toastr.success('Character renamed and past chats updated!', 'Rename Character');
toastr.success(t`Character renamed and past chats updated!`, t`Rename Character`);
} else {
toastr.success('Character renamed!', 'Rename Character');
toastr.success(t`Character renamed!`, t`Rename Character`);
}
}
else {
@ -5933,8 +5976,8 @@ export async function renameCharacter(name = null, { silent = false, renameChats
}
catch (error) {
// Reloading to prevent data corruption
if (!silent) await callPopup('Something went wrong. The page will be reloaded.', 'text');
else toastr.error('Something went wrong. The page will be reloaded.', 'Rename Character');
if (!silent) await callPopup(t`Something went wrong. The page will be reloaded.`, 'text');
else toastr.error(t`Something went wrong. The page will be reloaded.`, t`Rename Character`);
console.log('Renaming character error:', error);
location.reload();
@ -5991,7 +6034,7 @@ async function renamePastChats(newAvatar, newValue) {
}
}
} catch (error) {
toastr.error(`Past chat could not be updated: ${file_name}`);
toastr.error(t`Past chat could not be updated: ${file_name}`);
console.error(error);
}
}
@ -6040,7 +6083,7 @@ export async function saveChat(chatName, withMetadata, mesId) {
characters[this_chid]['date_last_chat'] = Date.now();
chat.forEach(function (item, i) {
if (item['is_group']) {
toastr.error('Trying to save group chat with regular saveChat function. Aborting to prevent corruption.');
toastr.error(t`Trying to save group chat with regular saveChat function. Aborting to prevent corruption.`);
throw new Error('Group chat saved from saveChat');
}
/*
@ -6085,7 +6128,7 @@ export async function saveChat(chatName, withMetadata, mesId) {
contentType: 'application/json',
success: function (data) { },
error: function (jqXHR, exception) {
toastr.error('Check the server connection and reload the page to prevent data loss.', 'Chat could not be saved');
toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Chat could not be saved`);
console.log(exception);
console.log(jqXHR);
},
@ -6482,7 +6525,7 @@ export async function getSettings() {
if (!response.ok) {
reloadLoop();
toastr.error('Settings could not be loaded after multiple attempts. Please try again later.');
toastr.error(t`Settings could not be loaded after multiple attempts. Please try again later.`);
throw new Error('Error getting settings');
}
@ -6709,7 +6752,7 @@ export async function saveSettings(type) {
eventSource.emit(event_types.SETTINGS_UPDATED);
},
error: function (jqXHR, exception) {
toastr.error('Check the server connection and reload the page to prevent data loss.', 'Settings could not be saved');
toastr.error(t`Check the server connection and reload the page to prevent data loss.t`, t`Settings could not be saved`);
console.log(exception);
console.log(jqXHR);
},
@ -6975,7 +7018,7 @@ export async function displayPastChats() {
const data = await (selected_group ? getGroupPastChats(selected_group) : getPastCharacterChats());
if (!data) {
toastr.error('Could not load chat data. Try reloading the page.');
toastr.error(t`Could not load chat data. Try reloading the page.`);
return;
}
@ -7108,7 +7151,7 @@ export function selectRightMenuWithAnimation(selectedMenuId) {
export function select_rm_info(type, charId, previousCharId = null) {
if (!type) {
toastr.error('Invalid process (no \'type\')');
toastr.error(t`Invalid process (no 'type')`);
return;
}
if (type !== 'group_create') {
@ -7116,20 +7159,20 @@ export function select_rm_info(type, charId, previousCharId = null) {
}
if (type === 'char_delete') {
toastr.warning(`Character Deleted: ${displayName}`);
toastr.warning(t`Character Deleted: ${displayName}`);
}
if (type === 'char_create') {
toastr.success(`Character Created: ${displayName}`);
toastr.success(t`Character Created: ${displayName}`);
}
if (type === 'group_create') {
toastr.success('Group Created');
toastr.success(t`Group Created`);
}
if (type === 'group_delete') {
toastr.warning('Group Deleted');
toastr.warning(t`Group Deleted`);
}
if (type === 'char_import') {
toastr.success(`Character Imported: ${displayName}`);
toastr.success(t`Character Imported: ${displayName}`);
}
selectRightMenuWithAnimation('rm_characters_block');
@ -7587,25 +7630,25 @@ export function hideSwipeButtons() {
*/
export async function deleteSwipe(swipeId = null) {
if (swipeId && (isNaN(swipeId) || swipeId < 0)) {
toastr.warning(`Invalid swipe ID: ${swipeId + 1}`);
toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`);
return;
}
const lastMessage = chat[chat.length - 1];
if (!lastMessage || !Array.isArray(lastMessage.swipes) || !lastMessage.swipes.length) {
toastr.warning('No messages to delete swipes from.');
toastr.warning(t`No messages to delete swipes from.`);
return;
}
if (lastMessage.swipes.length <= 1) {
toastr.warning('Can\'t delete the last swipe.');
toastr.warning(t`Can't delete the last swipe.`);
return;
}
swipeId = swipeId ?? lastMessage.swipe_id;
if (swipeId < 0 || swipeId >= lastMessage.swipes.length) {
toastr.warning(`Invalid swipe ID: ${swipeId + 1}`);
toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`);
return;
}
@ -7744,7 +7787,7 @@ export function setGenerationProgress(progress) {
function isHordeGenerationNotAllowed() {
if (main_api == 'koboldhorde' && preset_settings == 'gui') {
toastr.error('GUI Settings preset is not supported for Horde. Please select another preset.');
toastr.error(t`GUI Settings preset is not supported for Horde. Please select another preset.`);
return true;
}
@ -7793,7 +7836,7 @@ function openCharacterWorldPopup() {
}
$('#character_json_data').val(JSON.stringify(data));
toastr.info('Embedded lorebook will be removed from this character.');
toastr.info(t`Embedded lorebook will be removed from this character.`);
} catch {
console.error('Failed to parse character JSON data.');
}
@ -7984,11 +8027,11 @@ async function createOrEditCharacter(e) {
if ($('#form_create').attr('actiontype') == 'createcharacter') {
if (String($('#character_name_pole').val()).length === 0) {
toastr.error('Name is required');
toastr.error(t`Name is required`);
return;
}
if (is_group_generating || is_send_press) {
toastr.error('Cannot create characters while generating. Stop the request and try again.', 'Creation aborted');
toastr.error(t`Cannot create characters while generating. Stop the request and try again.`, t`Creation aborted`);
return;
}
try {
@ -8072,7 +8115,7 @@ async function createOrEditCharacter(e) {
} catch (error) {
console.error('Error creating character', error);
toastr.error('Failed to create character');
toastr.error(t`Failed to create character`);
}
} else {
try {
@ -8131,7 +8174,7 @@ async function createOrEditCharacter(e) {
}
} catch (error) {
console.log(error);
toastr.error('Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.');
toastr.error(t`Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.`);
}
}
}
@ -8641,7 +8684,7 @@ async function selectContextCallback(args, name) {
const result = fuse.search(name);
if (result.length === 0) {
!quiet && toastr.warning(`Context template "${name}" not found`);
!quiet && toastr.warning(t`Context template '${name}' not found`);
return '';
}
@ -8661,7 +8704,7 @@ async function selectInstructCallback(args, name) {
const result = fuse.search(name);
if (result.length === 0) {
!quiet && toastr.warning(`Instruct template "${name}" not found`);
!quiet && toastr.warning(t`Instruct template '${name}' not found`);
return '';
}
@ -8713,7 +8756,7 @@ async function connectAPISlash(args, text) {
const apiConfig = CONNECT_API_MAP[text.toLowerCase()];
if (!apiConfig) {
toastr.error(`Error: ${text} is not a valid API`);
toastr.error(t`Error: ${text} is not a valid API`);
return '';
}
@ -8742,7 +8785,7 @@ async function connectAPISlash(args, text) {
}
const quiet = isTrueBoolean(args?.quiet);
const toast = quiet ? jQuery() : toastr.info(`API set to ${text}, trying to connect..`);
const toast = quiet ? jQuery() : toastr.info(t`API set to ${text}, trying to connect..`);
try {
await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100);
@ -8781,7 +8824,7 @@ export async function processDroppedFiles(files, data = new Map()) {
const preservedName = data instanceof Map && data.get(file);
await importCharacter(file, preservedName);
} else {
toastr.warning('Unsupported file type: ' + file.name);
toastr.warning(t`Unsupported file type: ` + file.name);
}
}
}
@ -8794,7 +8837,7 @@ export async function processDroppedFiles(files, data = new Map()) {
*/
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');
toastr.error(t`Cannot import characters while generating. Stop the request and try again.`, t`Import aborted`);
throw new Error('Cannot import character while generating');
}
@ -8821,7 +8864,7 @@ async function importCharacter(file, preserveFileName = '') {
});
if (data.error) {
toastr.error('The file is likely invalid or corrupted.', 'Could not import character');
toastr.error(t`The file is likely invalid or corrupted.`, t`Could not import character`);
return;
}
@ -8874,7 +8917,7 @@ async function doImpersonate(args, prompt) {
await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
} catch {
console.warn('Timeout waiting for generation unlock');
toastr.warning('Cannot run /impersonate command while the reply is being generated.');
toastr.warning(t`Cannot run /impersonate command while the reply is being generated.`);
return '';
}
@ -8936,19 +8979,19 @@ async function doDeleteChat() {
async function doRenameChat(_, chatName) {
if (!chatName) {
toastr.warning('Name must be provided as an argument to rename this chat.');
toastr.warning(t`Name must be provided as an argument to rename this chat.`);
return '';
}
const currentChatName = getCurrentChatId();
if (!currentChatName) {
toastr.warning('No chat selected that can be renamed.');
toastr.warning(t`No chat selected that can be renamed.`);
return '';
}
await renameChat(currentChatName, chatName);
toastr.success(`Successfully renamed chat to: ${chatName}`);
toastr.success(t`Successfully renamed chat to: ${chatName}`);
return '';
}
@ -9063,7 +9106,7 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {})
for (const key of characterKey) {
const character = characters.find(x => x.avatar == key);
if (!character) {
toastr.warning(`Character ${key} not found. Skipping deletion.`);
toastr.warning(t`Character ${key} not found. Skipping deletion.`);
continue;
}
@ -9080,7 +9123,7 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {})
});
if (!response.ok) {
toastr.error(`${response.status} ${response.statusText}`, 'Failed to delete character');
toastr.error(`${response.status} ${response.statusText}`, t`Failed to delete character`);
continue;
}
@ -9132,6 +9175,90 @@ function doTogglePanels() {
return '';
}
/**
* Event handler to open a navbar drawer when a drawer open button is clicked.
* Handles click events on .drawer-opener elements.
* Opens the drawer associated with the clicked button according to the data-target attribute.
* @returns {void}
*/
function doDrawerOpenClick() {
const targetDrawerID = $(this).attr('data-target');
const drawer = $(`#${targetDrawerID}`);
const drawerToggle = drawer.find('.drawer-toggle');
const drawerWasOpenAlready = drawerToggle.parent().find('.drawer-content').hasClass('openDrawer');
if (drawerWasOpenAlready || drawer.hasClass('resizing')) { return; }
doNavbarIconClick.call(drawerToggle);
}
/**
* Event handler to open or close a navbar drawer when a navbar icon is clicked.
* Handles click events on .drawer-toggle elements.
* @returns {void}
*/
function doNavbarIconClick() {
var icon = $(this).find('.drawer-icon');
var drawer = $(this).parent().find('.drawer-content');
if (drawer.hasClass('resizing')) { return; }
var drawerWasOpenAlready = $(this).parent().find('.drawer-content').hasClass('openDrawer');
let targetDrawerID = $(this).parent().find('.drawer-content').attr('id');
const pinnedDrawerClicked = drawer.hasClass('pinnedOpen');
if (!drawerWasOpenAlready) { //to open the drawer
$('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
});
$('.openIcon').toggleClass('closedIcon openIcon');
$('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');
icon.toggleClass('openIcon closedIcon');
drawer.toggleClass('openDrawer closedDrawer');
//console.log(targetDrawerID);
if (targetDrawerID === 'right-nav-panel') {
$(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle({
duration: 200,
easing: 'swing',
start: function () {
jQuery(this).css('display', 'flex'); //flex needed to make charlist scroll
},
complete: async function () {
favsToHotswap();
await delay(50);
$(this).closest('.drawer-content').removeClass('resizing');
$('#rm_print_characters_block').trigger('scroll');
},
});
} else {
$(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
});
}
// Set the height of "autoSetHeight" textareas within the drawer to their scroll height
if (!CSS.supports('field-sizing', 'content')) {
$(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(async function () {
await resetScrollHeight($(this));
return;
});
}
} else if (drawerWasOpenAlready) { //to close manually
icon.toggleClass('closedIcon openIcon');
if (pinnedDrawerClicked) {
$(drawer).addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).removeClass('resizing');
});
}
else {
$('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
});
}
drawer.toggleClass('closedDrawer openDrawer');
}
}
function addDebugFunctions() {
const doBackfill = async () => {
for (const message of chat) {
@ -9755,12 +9882,7 @@ jQuery(async function () {
let deleteChats = false;
const confirm = await Popup.show.confirm('Delete the character?', `
<b>THIS IS PERMANENT!<br><br>
<label for="del_char_checkbox" class="checkbox_label justifyCenter">
<input type="checkbox" id="del_char_checkbox" />
<small>Also delete the chat files</small>
</label></b>`, {
const confirm = await Popup.show.confirm(t`Delete the character?`, await renderTemplateAsync('deleteConfirm'), {
onClose: () => deleteChats = !!$('#del_char_checkbox').prop('checked'),
});
if (!confirm) {
@ -10721,69 +10843,8 @@ jQuery(async function () {
stopScriptExecution();
});
$('.drawer-toggle').on('click', function () {
var icon = $(this).find('.drawer-icon');
var drawer = $(this).parent().find('.drawer-content');
if (drawer.hasClass('resizing')) { return; }
var drawerWasOpenAlready = $(this).parent().find('.drawer-content').hasClass('openDrawer');
let targetDrawerID = $(this).parent().find('.drawer-content').attr('id');
const pinnedDrawerClicked = drawer.hasClass('pinnedOpen');
if (!drawerWasOpenAlready) { //to open the drawer
$('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
});
$('.openIcon').not('.drawerPinnedOpen').toggleClass('closedIcon openIcon');
$('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');
icon.toggleClass('openIcon closedIcon');
drawer.toggleClass('openDrawer closedDrawer');
//console.log(targetDrawerID);
if (targetDrawerID === 'right-nav-panel') {
$(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle({
duration: 200,
easing: 'swing',
start: function () {
jQuery(this).css('display', 'flex'); //flex needed to make charlist scroll
},
complete: async function () {
favsToHotswap();
await delay(50);
$(this).closest('.drawer-content').removeClass('resizing');
$('#rm_print_characters_block').trigger('scroll');
},
});
} else {
$(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
});
}
// Set the height of "autoSetHeight" textareas within the drawer to their scroll height
if (!CSS.supports('field-sizing', 'content')) {
$(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(async function () {
await resetScrollHeight($(this));
return;
});
}
} else if (drawerWasOpenAlready) { //to close manually
icon.toggleClass('closedIcon openIcon');
if (pinnedDrawerClicked) {
$(drawer).addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).removeClass('resizing');
});
}
else {
$('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () {
await delay(50); $(this).closest('.drawer-content').removeClass('resizing');
});
}
drawer.toggleClass('closedDrawer openDrawer');
}
});
$(document).on('click', '.drawer-opener', doDrawerOpenClick);
$('.drawer-toggle').on('click', doNavbarIconClick);
$('html').on('touchstart mousedown', function (e) {
var clickTarget = $(e.target);

View File

@ -8,6 +8,7 @@ import { debounce, waitUntilCondition, escapeHtml } from './utils.js';
import { debounce_timeout } from './constants.js';
import { renderTemplateAsync } from './templates.js';
import { Popup } from './popup.js';
import { t } from './i18n.js';
function debouncePromise(func, delay) {
let timeoutId;
@ -455,7 +456,7 @@ class PromptManager {
// Delete selected prompt from list form and close edit form
this.handleDeletePrompt = async (event) => {
Popup.show.confirm('Are you sure you want to delete this prompt?', null).then((userChoice) => {
Popup.show.confirm(t`Are you sure you want to delete this prompt?`, null).then((userChoice) => {
if (!userChoice) return;
const promptID = document.getElementById(this.configuration.prefix + 'prompt_manager_footer_append_prompt').value;
const prompt = this.getPromptById(promptID);
@ -531,7 +532,7 @@ class PromptManager {
// Import prompts for the selected character
this.handleImport = () => {
Popup.show.confirm('Existing prompts with the same ID will be overridden. Do you want to proceed?', null)
Popup.show.confirm(t`Existing prompts with the same ID will be overridden. Do you want to proceed?`, null)
.then(userChoice => {
if (!userChoice) return;
@ -552,7 +553,7 @@ class PromptManager {
const data = JSON.parse(fileContent);
this.import(data);
} catch (err) {
toastr.error('An error occurred while importing prompts. More info available in console.');
toastr.error(t`An error occurred while importing prompts. More info available in console.`);
console.log('An error occurred while importing prompts');
console.log(err.toString());
}
@ -567,7 +568,7 @@ class PromptManager {
// Restore default state of a characters prompt order
this.handleCharacterReset = () => {
Popup.show.confirm('This will reset the prompt order for this character. You will not lose any prompts.', null)
Popup.show.confirm(t`This will reset the prompt order for this character. You will not lose any prompts.`, null)
.then(userChoice => {
if (!userChoice) return;
@ -1649,7 +1650,7 @@ class PromptManager {
};
if (false === this.validateObject(controlObj, importData)) {
toastr.warning('Could not import prompts. Export failed validation.');
toastr.warning(t`Could not import prompts. Export failed validation.`);
return;
}
@ -1672,7 +1673,7 @@ class PromptManager {
throw new Error('Prompt order strategy not supported.');
}
toastr.success('Prompt import complete.');
toastr.success(t`Prompt import complete.`);
this.saveServiceSettings().then(() => this.render());
}

View File

@ -16,6 +16,7 @@ import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
export { MODULE_NAME as NOTE_MODULE_NAME };
import { t } from './i18n.js';
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
@ -37,7 +38,7 @@ const chara_note_position = {
function setNoteTextCommand(_, text) {
$('#extension_floating_prompt').val(text).trigger('input');
toastr.success('Author\'s Note text updated');
toastr.success(t`Author's Note text updated`);
return '';
}
@ -45,12 +46,12 @@ function setNoteDepthCommand(_, text) {
const value = Number(text);
if (Number.isNaN(value)) {
toastr.error('Not a valid number');
toastr.error(t`Not a valid number`);
return;
}
$('#extension_floating_depth').val(Math.abs(value)).trigger('input');
toastr.success('Author\'s Note depth updated');
toastr.success(t`Author's Note depth updated`);
return '';
}
@ -58,12 +59,12 @@ function setNoteIntervalCommand(_, text) {
const value = Number(text);
if (Number.isNaN(value)) {
toastr.error('Not a valid number');
toastr.error(t`Not a valid number`);
return;
}
$('#extension_floating_interval').val(Math.abs(value)).trigger('input');
toastr.success('Author\'s Note frequency updated');
toastr.success(t`Author's Note frequency updated`);
return '';
}
@ -76,12 +77,12 @@ function setNotePositionCommand(_, text) {
const position = validPositions[text?.trim()];
if (Number.isNaN(position)) {
toastr.error('Not a valid position');
toastr.error(t`Not a valid position`);
return;
}
$(`input[name="extension_floating_position"][value="${position}"]`).prop('checked', true).trigger('input');
toastr.info('Author\'s Note position updated');
toastr.info(t`Author's Note position updated`);
return '';
}
@ -206,7 +207,7 @@ function onExtensionFloatingCharaPromptInput() {
extension_settings.note.chara.push(tempCharaNote);
} else {
console.log('Character author\'s note error: No avatar name key could be found.');
toastr.error('Something went wrong. Could not save character\'s author\'s note.');
toastr.error(t`Something went wrong. Could not save character's author's note.`);
// Don't save settings if something went wrong
return;
@ -397,7 +398,7 @@ function onANMenuItemClick() {
//because this listener takes priority
$('#options').stop().fadeOut(animation_duration);
} else {
toastr.warning('Select a character before trying to use Author\'s Note', '', { timeOut: 2000 });
toastr.warning(t`Select a character before trying to use Author's Note`, '', { timeOut: 2000 });
}
}

View File

@ -40,6 +40,7 @@ import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { ScraperManager } from './scrapers.js';
import { DragAndDropHandler } from './dragdrop.js';
import { renderTemplateAsync } from './templates.js';
import { t } from './i18n.js';
/**
* @typedef {Object} FileAttachment
@ -206,7 +207,7 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
const fileText = await converter(file);
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
} catch (error) {
toastr.error(String(error), 'Could not convert file');
toastr.error(String(error), t`Could not convert file`);
console.error('Could not convert file', error);
}
}
@ -257,7 +258,7 @@ export async function uploadFileAttachment(fileName, base64Data) {
const responseData = await result.json();
return responseData.path;
} catch (error) {
toastr.error(String(error), 'Could not upload file');
toastr.error(String(error), t`Could not upload file`);
console.error('Could not upload file', error);
}
}
@ -283,7 +284,7 @@ export async function getFileAttachment(url) {
const text = await result.text();
return text;
} catch (error) {
toastr.error(error, 'Could not download file');
toastr.error(error, t`Could not download file`);
console.error('Could not download file', error);
}
}
@ -299,13 +300,13 @@ async function validateFile(file) {
const isBinary = /^[\x00-\x08\x0E-\x1F\x7F-\xFF]*$/.test(fileText);
if (!isImage && file.size > fileSizeLimit) {
toastr.error(`File is too big. Maximum size is ${humanFileSize(fileSizeLimit)}.`);
toastr.error(t`File is too big. Maximum size is ${humanFileSize(fileSizeLimit)}.`);
return false;
}
// If file is binary
if (isBinary && !isImage && !isConvertible(file.type)) {
toastr.error('Binary files are not supported. Select a text file or image.');
toastr.error(t`Binary files are not supported. Select a text file or image.`);
return false;
}
@ -521,7 +522,7 @@ async function openExternalMediaOverridesDialog() {
const entityId = getCurrentEntityId();
if (!entityId) {
toastr.info('No character or group selected');
toastr.info(t`No character or group selected`);
return;
}
@ -646,7 +647,7 @@ async function deleteFileFromServer(url, silent = false) {
await eventSource.emit(event_types.FILE_ATTACHMENT_DELETED, url);
return true;
} catch (error) {
toastr.error(String(error), 'Could not delete file');
toastr.error(String(error), t`Could not delete file`);
console.error('Could not delete file', error);
return false;
}
@ -1054,7 +1055,7 @@ async function openAttachmentManager() {
const selectedAttachments = document.querySelectorAll('.attachmentListItemCheckboxContainer .attachmentListItemCheckbox:checked');
if (selectedAttachments.length === 0) {
toastr.info('No attachments selected.', 'Data Bank');
toastr.info(t`No attachments selected.`, t`Data Bank`);
return;
}
@ -1168,7 +1169,7 @@ async function runScraper(scraperId, target, callback) {
if (files.length === 0) {
console.warn('Scraping returned no files');
toastr.info('No files were scraped.', 'Data Bank');
toastr.info(t`No files were scraped.`, t`Data Bank`);
return;
}
@ -1176,12 +1177,12 @@ async function runScraper(scraperId, target, callback) {
await uploadFileAttachmentToServer(file, target);
}
toastr.success(`Scraped ${files.length} files from ${scraperId} to ${target}.`, 'Data Bank');
toastr.success(t`Scraped ${files.length} files from ${scraperId} to ${target}.`, t`Data Bank`);
callback();
}
catch (error) {
console.error('Scraping failed', error);
toastr.error('Check browser console for details.', 'Scraping failed');
toastr.error(t`Check browser console for details.`, t`Scraping failed`);
}
}
@ -1208,7 +1209,7 @@ export async function uploadFileAttachmentToServer(file, target) {
const fileText = await converter(file);
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
} catch (error) {
toastr.error(String(error), 'Could not convert file');
toastr.error(String(error), t`Could not convert file`);
console.error('Could not convert file', error);
}
} else {

View File

@ -132,7 +132,7 @@
<small data-i18n="ext_regex_other_options" data-i18n="Ephemerality">Ephemerality</small>
<span class="fa-solid fa-circle-question note-link-span" title="By default, regex scripts alter the chat file directly and irreversibly.&#13;Enabling either (or both) of the options below will prevent chat file alteration, while still altering the specified item(s)."></span>
</span>
<label class="checkbox flex-container" title="Chat history file contents won't change, but regex will be applied to the messages displayed in the Chat UI.">
<label class="checkbox flex-container" data-i18n="[title]ext_regex_only_format_visual_desc" title="Chat history file contents won't change, but regex will be applied to the messages displayed in the Chat UI.">
<input type="checkbox" name="only_format_display" />
<span data-i18n="Only Format Display">Alter Chat Display</span>
</label>

View File

@ -474,7 +474,7 @@ async function processTtsQueue() {
}
if (extension_settings.tts.narrate_quoted_only) {
const special_quotes = /[“”«»]/g; // Extend this regex to include other special quotes
const special_quotes = /[“”«»「」『』""]/g; // Extend this regex to include other special quotes
text = text.replace(special_quotes, '"');
const matches = text.match(/".*?"/g); // Matches text inside double quotes, non-greedily
const partJoiner = (ttsProvider?.separator || ' ... ');

View File

@ -189,8 +189,8 @@ async function validateGroup(group) {
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');
const msg = t`Warning: Listed member ${member} does not exist as a character. It will be removed from the group.`;
toastr.warning(msg, t`Group Validation`);
console.warn(msg);
dirty = true;
}
@ -522,7 +522,7 @@ async function saveGroupChat(groupId, shouldSaveGroup) {
});
if (!response.ok) {
toastr.error('Check the server connection and reload the page to prevent data loss.', 'Group Chat could not be saved');
toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Group Chat could not be saved`);
console.error('Group chat could not be saved', response);
return;
}
@ -837,7 +837,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
activatedMembers = activateSwipe(group.members);
if (activatedMembers.length === 0) {
toastr.warning('Deleted group member swiped. To get a reply, add them back to the group.');
toastr.warning(t`Deleted group member swiped. To get a reply, add them back to the group.`);
throw new Error('Deleted group member swiped');
}
}
@ -1368,15 +1368,15 @@ function isGroupMemberDisabled(avatarId) {
async function onDeleteGroupClick() {
if (!openGroupId) {
toastr.warning('Currently no group selected.');
toastr.warning(t`Currently no group selected.`);
return;
}
if (is_group_generating) {
toastr.warning('Not so fast! Wait for the characters to stop typing before deleting the group.');
toastr.warning(t`Not so fast! Wait for the characters to stop typing before deleting the group.`);
return;
}
const confirm = await Popup.show.confirm('Delete the group?', '<p>This will also delete all your chats with that group. If you want to delete a single conversation, select a "View past chats" option in the lower left menu.</p>');
const confirm = await Popup.show.confirm(t`Delete the group?`, '<p>' + t`This will also delete all your chats with that group. If you want to delete a single conversation, select a "View past chats" option in the lower left menu.` + '</p>');
if (confirm) {
deleteGroup(openGroupId);
}
@ -1630,7 +1630,7 @@ function updateFavButtonState(state) {
export async function openGroupById(groupId) {
if (isChatSaving) {
toastr.info('Please wait until the chat is saved before switching characters.', 'Your chat is still saving...');
toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
return;
}
@ -1659,7 +1659,7 @@ export async function openGroupById(groupId) {
function openCharacterDefinition(characterSelect) {
if (is_group_generating) {
toastr.warning('Can\'t peek a character while group reply is being generated');
toastr.warning(t`Can't peek a character while group reply is being generated`);
console.warn('Can\'t peek a character def while group reply is being generated');
return;
}
@ -1908,7 +1908,7 @@ export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {
});
if (!response.ok) {
toastr.error('Check the server connection and reload the page to prevent data loss.', 'Group chat could not be saved');
toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Group chat could not be saved`);
console.error('Group chat could not be saved', response);
}
}

View File

@ -215,6 +215,7 @@ function addLanguagesToDropdown() {
}
export async function initLocales() {
moment.locale(localeFile);
langs = await fetch('/locales/lang.json').then(response => response.json());
localeData = await getLocaleData(localeFile);
applyLocale();

View File

@ -392,7 +392,7 @@ async function validateReverseProxy() {
new URL(oai_settings.reverse_proxy);
}
catch (err) {
toastr.error('Entered reverse proxy address is not a valid URL');
toastr.error(t`Entered reverse proxy address is not a valid URL`);
setOnlineStatus('no_connection');
resultCheckStatus();
throw err;
@ -403,7 +403,7 @@ async function validateReverseProxy() {
const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) }));
if (!confirmation) {
toastr.error('Update or remove your reverse proxy settings.');
toastr.error(t`Update or remove your reverse proxy settings.`);
setOnlineStatus('no_connection');
resultCheckStatus();
throw new Error('Proxy connection denied.');
@ -1259,15 +1259,15 @@ export async function prepareOpenAIMessages({
await populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples });
} catch (error) {
if (error instanceof TokenBudgetExceededError) {
toastr.error('An error occurred while counting tokens: Token budget exceeded.');
toastr.error(t`An error occurred while counting tokens: Token budget exceeded.`);
chatCompletion.log('Token budget exceeded.');
promptManager.error = 'Not enough free tokens for mandatory prompts. Raise your token Limit or disable custom prompts.';
promptManager.error = t`Not enough free tokens for mandatory prompts. Raise your token Limit or disable custom prompts.`;
} else if (error instanceof InvalidCharacterNameError) {
toastr.warning('An error occurred while counting tokens: Invalid character name');
toastr.warning(t`An error occurred while counting tokens: Invalid character name`);
chatCompletion.log('Invalid character name');
promptManager.error = 'The name of at least one character contained whitespaces or special characters. Please check your user and character name.';
promptManager.error = t`The name of at least one character contained whitespaces or special characters. Please check your user and character name.`;
} else {
toastr.error('An unknown error occurred while counting tokens. Further information may be available in console.');
toastr.error(t`An unknown error occurred while counting tokens. Further information may be available in console.`);
chatCompletion.log('----- Unexpected error while preparing prompts -----');
chatCompletion.log(error);
chatCompletion.log(error.stack);
@ -1321,11 +1321,8 @@ function tryParseStreamingError(response, decoded) {
}
}
function checkQuotaError(data) {
const errorText = `<h3>Encountered an error while processing your request.<br>
Check you have credits available on your
<a href="https://platform.openai.com/account/usage" target="_blank">OpenAI account</a>.<br>
If you have sufficient credits, please try again later.</h3>`;
async function checkQuotaError(data) {
const errorText = await renderTemplateAsync('quotaError');
if (!data) {
return;
@ -1964,11 +1961,11 @@ async function sendOpenAIRequest(type, messages, signal) {
else {
const data = await response.json();
checkQuotaError(data);
await checkQuotaError(data);
checkModerationError(data);
if (data.error) {
toastr.error(data.error.message || response.statusText, 'API returned an error');
toastr.error(data.error.message || response.statusText, t`API returned an error`);
throw new Error(data);
}
@ -2078,7 +2075,7 @@ function parseOpenAITextLogprobs(logprobs) {
function handleWindowError(err) {
const text = parseWindowError(err);
toastr.error(text, 'Window.ai returned an error');
toastr.error(text, t`Window.ai returned an error`);
throw err;
}
@ -3186,7 +3183,7 @@ async function getStatusOpen() {
}
function showWindowExtensionError() {
toastr.error('Get it here: <a href="https://windowai.io/" target="_blank">windowai.io</a>', 'Extension is not installed', {
toastr.error(t`Get it here:` + ' <a href="https://windowai.io/" target="_blank">windowai.io</a>', t`Extension is not installed`, {
escapeHtml: false,
timeOut: 0,
extendedTimeOut: 0,
@ -3305,7 +3302,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
if (triggerUi) $('#settings_preset_openai').append(option).trigger('change');
}
} else {
toastr.error('Failed to save preset');
toastr.error(t`Failed to save preset`);
throw new Error('Failed to save preset');
}
}
@ -3384,7 +3381,7 @@ async function createNewLogitBiasPreset() {
}
if (name in oai_settings.bias_presets) {
toastr.error('Preset name should be unique.');
toastr.error(t`Preset name should be unique.`);
return;
}
@ -3428,7 +3425,7 @@ async function onPresetImportFileChange(e) {
try {
presetBody = JSON.parse(importedFile);
} catch (err) {
toastr.error('Invalid file');
toastr.error(t`Invalid file`);
return;
}
@ -3469,7 +3466,7 @@ async function onPresetImportFileChange(e) {
});
if (!savePresetSettings.ok) {
toastr.error('Failed to save preset');
toastr.error(t`Failed to save preset`);
return;
}
@ -3494,7 +3491,7 @@ async function onPresetImportFileChange(e) {
async function onExportPresetClick() {
if (!oai_settings.preset_settings_openai) {
toastr.error('No preset selected');
toastr.error(t`No preset selected`);
return;
}
@ -3535,12 +3532,12 @@ async function onLogitBiasPresetImportFileChange(e) {
e.target.value = '';
if (name in oai_settings.bias_presets) {
toastr.error('Preset name should be unique.');
toastr.error(t`Preset name should be unique.`);
return;
}
if (!Array.isArray(importedFile)) {
toastr.error('Invalid logit bias preset file.');
toastr.error(t`Invalid logit bias preset file.`);
return;
}
@ -3599,16 +3596,16 @@ async function onDeletePresetClick() {
});
if (!response.ok) {
toastr.warning('Preset was not deleted from server');
toastr.warning(t`Preset was not deleted from server`);
} else {
toastr.success('Preset deleted');
toastr.success(t`Preset deleted`);
}
saveSettingsDebounced();
}
async function onLogitBiasPresetDeleteClick() {
const value = await callPopup('Delete the preset?', 'confirm');
const value = await callPopup(t`Delete the preset?`, 'confirm');
if (!value) {
return;
@ -4745,7 +4742,7 @@ function runProxyCallback(_, value) {
const result = fuse.search(value);
if (result.length === 0) {
toastr.warning(`Proxy preset "${value}" not found`);
toastr.warning(t`Proxy preset '${value}' not found`);
return '';
}
@ -4919,7 +4916,7 @@ export function initOpenAI() {
$('#update_oai_preset').on('click', async function () {
const name = oai_settings.preset_settings_openai;
await saveOpenAIPreset(name, oai_settings);
toastr.success('Preset updated');
toastr.success(t`Preset updated`);
});
$('#impersonation_prompt_restore').on('click', function () {

View File

@ -1,3 +1,3 @@
<div>
<b>Note:</b> this chat is temporary and will be deleted as soon as you leave it.
<b data-i18n="Note:">Note:</b> <span data-i18n="this chat is temporary and will be deleted as soon as you leave it.">this chat is temporary and will be deleted as soon as you leave it.</span>
</div>

View File

@ -0,0 +1,5 @@
<b><span data-i18n="THIS IS PERMANENT!">THIS IS PERMANENT!</span><br><br>
<label for="del_char_checkbox" class="checkbox_label justifyCenter">
<input type="checkbox" id="del_char_checkbox" />
<small data-i18n="Also delete the chat files">Also delete the chat files</small>
</label></b>

View File

@ -1,5 +1,5 @@
<div>
<h3>Are you sure you want to duplicate this character?</h3>
<span>If you just want to start a new chat with the same character, use "Start new chat" option in the bottom-left options menu.</span>
<h3 data-i18n="Are you sure you want to duplicate this character?">Are you sure you want to duplicate this character?</h3>
<span data-i18n="If you just want to start a new chat with the same character...">If you just want to start a new chat with the same character, use "Start new chat" option in the bottom-left options menu.</span>
<br>
</div>

View File

@ -14,18 +14,18 @@
<li><kbd data-i18n="help_hotkeys_19">Ctrl+Shift+Down</kbd> = <span data-i18n="help_hotkeys_20">Scroll chat to bottom</span></li>
</ul>
<div>
<strong>Markdown Hotkeys</strong>
<strong data-i18n="help_hotkeys_20">Markdown Hotkeys</strong>
</div>
<div>
<small>
<span>Works in the chatbar and textareas marked with this icon:</span>
<span data-i18n="help_hotkeys_21">Works in the chatbar and textareas marked with this icon:</span>
<code><i class="fa-brands fa-markdown"></i></code>
</small>
</div>
<ul>
<li><kbd>Ctrl+B</kbd> = <span>**bold**</span></li>
<li><kbd>Ctrl+I</kbd> = <span>*italic*</span></li>
<li><kbd>Ctrl+U</kbd> = <span>__underline__</span></li>
<li><kbd>Ctrl+K</kbd> = <span>`inline code`</span></li>
<li><kbd>Ctrl+Shift+~</kbd> = <span>~~strikethrough~~</span></li>
<li><kbd>Ctrl+B</kbd> = <span data-i18n="help_hotkeys_22">**bold**</span></li>
<li><kbd>Ctrl+I</kbd> = <span data-i18n="help_hotkeys_23">*italic*</span></li>
<li><kbd>Ctrl+U</kbd> = <span data-i18n="help_hotkeys_24">__underline__</span></li>
<li><kbd>Ctrl+K</kbd> = <span data-i18n="help_hotkeys_25">`inline code`</span></li>
<li><kbd>Ctrl+Shift+~</kbd> = <span data-i18n="help_hotkeys_26">~~strikethrough~~</span></li>
</ul>

View File

@ -0,0 +1,4 @@
<h3><span data-i18n="Encountered an error while processing your request.">Encountered an error while processing your request.</span><br>
<span data-i18n="Check you have credits available on your">Check you have credits available on your</span>
<a href="https://platform.openai.com/account/usage" target="_blank" data-i18n="OpenAI account quora_error">OpenAI account</a><span data-i18n="dot quota_error">.</span><br>
<span data-i18n="If you have sufficient credits, please try again later.">If you have sufficient credits, please try again later.</span></h3>

View File

@ -4,30 +4,56 @@
<a href="https://docs.sillytavern.app/usage/update/" target="_blank" data-i18n="Want to update?">
Want to update?
</a>
<hr>
<h3 data-i18n="How to start chatting?">How to start chatting?</h3>
<ol>
<li>
<span data-i18n="Click _space">Click </span><code><i class="fa-solid fa-plug"></i></code><span data-i18n="and select a"> and select a </span><a href="https://docs.sillytavern.app/usage/api-connections/" target="_blank" data-i18n="Chat API">Chat API</a>.</span>
<span data-i18n="Click _space">Click </span>
<button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="sys-settings-button">
<i class="fa-solid fa-plug"></i>
<span data-i18n="[title]API Connections">API Connections</span>
</button>
<span data-i18n="and connect to an">and connect to an</span>
<a href="https://docs.sillytavern.app/usage/api-connections/" target="_blank">
<span class="fa-solid fa-circle-question"></span>
<span data-i18n="API">API</span></a>.
</li>
<li>
<span data-i18n="Click _space">Click </span><code><i class="fa-solid fa-address-card"></i></code><span data-i18n="and pick a character."> and pick a character.</span>
<span data-i18n="Click _space">Click </span>
<button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="rightNavHolder">
<i class="fa-solid fa-address-card"></i>
<span data-i18n="[title]Character Management">Character Management</span>
</button>
<span data-i18n="and pick a character."> and pick a character.</span>
</li>
</ol>
<div>
<span data-i18n="You can browse a list of bundled characters in the">
You can browse a list of bundled characters in the
</span>
<i data-i18n="Download Extensions & Assets">
Download Extensions & Assets
</i>
<span data-i18n="menu within">
menu within
</span>
<code><i class="fa-solid fa-cubes"></i></code>
<span>.</span>
<span data-i18n="You can add more">You can add more</span>
<button class="open_characters_library menu_button menu_button_icon inline-flex">
<i class="fa-solid fa-image-portrait"></i>
<span data-i18n="Sample characters">Sample characters</span>
</button>
<span data-i18n="or">or</span>
<button class="external_import_button menu_button menu_button_icon inline-flex">
<i class="fa-solid fa-cloud-arrow-down"></i>
<span data-i18n="Import Characters">Import characters</span>
</button>
<span data-i18n="from other websites">from other websites.</span>
</div>
<hr>
<div>
<span data-i18n="Go to the">Go to the</span>
<i data-i18n="Download Extensions & Assets">Download Extensions & Assets</i>
<span data-i18n="menu within">menu within</span>
<button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="extensions-settings-button">
<i class="fa-solid fa-cubes"></i>
<span data-i18n="[title]Extensions">Extensions</span>
</button>
<span data-i18n="to install additional features.">to install additional features.</span>
</div>
<h3 data-i18n="Confused or lost?">Confused or lost?</h3>
<ul>
<li>
@ -43,7 +69,6 @@
</li>
</ul>
<hr>
<h3 data-i18n="Still have questions?">Still have questions?</h3>
<ul>
<li>
@ -62,4 +87,3 @@
</a>
</li>
</ul>

View File

@ -314,6 +314,12 @@ input[type='checkbox']:focus-visible {
display: inline-block;
}
.mes_text ol,
.mes_text ul {
margin-top: 5px;
margin-bottom: 5px;
}
.mes_text br,
.mes_bias br {
content: ' ';
@ -1218,8 +1224,9 @@ textarea.autoSetHeight {
}
input,
select {
font-family: var(--mainFontFamily);
select,
button {
font-family: var(--mainFontFamily), sans-serif;
font-size: var(--mainFontSize);
color: var(--SmartThemeBodyColor);
}