diff --git a/public/index.html b/public/index.html index 343455b3d..18d70de3f 100644 --- a/public/index.html +++ b/public/index.html @@ -2430,7 +2430,7 @@
-
-
-
+
+
+ +
+
Group reply strategy @@ -2576,12 +2586,13 @@
-
+
+
@@ -3207,7 +3218,7 @@
-
+
diff --git a/public/script.js b/public/script.js index b8bad5196..a473c1301 100644 --- a/public/script.js +++ b/public/script.js @@ -3742,7 +3742,7 @@ async function read_avatar_load(input) { } } -function getCropPopup(src) { +export function getCropPopup(src) { return `

Set the crop position of the avatar image and click Ok to confirm.

@@ -7351,6 +7351,8 @@ $(document).ready(function () { return; } + // Save before exporting + await createOrEditCharacter(); const body = { format, avatar_url: characters[this_chid].avatar }; const response = await fetch('/exportcharacter', { diff --git a/public/scripts/extensions/floating-prompt/index.js b/public/scripts/extensions/floating-prompt/index.js index da10671ec..520d9f53a 100644 --- a/public/scripts/extensions/floating-prompt/index.js +++ b/public/scripts/extensions/floating-prompt/index.js @@ -27,6 +27,12 @@ export const metadata_keys = { position: 'note_position', } +const chara_note_position = { + replace: 0, + before: 1, + after: 2, +} + function setNoteTextCommand(_, text) { $('#extension_floating_prompt').val(text).trigger('input'); toastr.success("Author's Note text updated"); @@ -87,11 +93,13 @@ async function onExtensionFloatingPromptInput() { chat_metadata[metadata_keys.prompt] = $(this).val(); setMainPromptTokenCounterDebounced(chat_metadata[metadata_keys.prompt]); updateSettings(); + saveMetadataDebounced(); } async function onExtensionFloatingIntervalInput() { chat_metadata[metadata_keys.interval] = Number($(this).val()); updateSettings(); + saveMetadataDebounced(); } async function onExtensionFloatingDepthInput() { @@ -104,11 +112,23 @@ async function onExtensionFloatingDepthInput() { chat_metadata[metadata_keys.depth] = value; updateSettings(); + saveMetadataDebounced(); } async function onExtensionFloatingPositionInput(e) { chat_metadata[metadata_keys.position] = e.target.value; updateSettings(); + saveMetadataDebounced(); +} + +async function onExtensionFloatingCharPositionInput(e) { + const value = e.target.value; + const charaNote = extension_settings.note.chara.find((e) => e.name === getCharaFilename()); + + if (charaNote) { + charaNote.position = Number(value); + updateSettings(); + } } function onExtensionFloatingCharaPromptInput() { @@ -143,7 +163,7 @@ function onExtensionFloatingCharaPromptInput() { if (!extension_settings.note.chara) { extension_settings.note.chara = [] } - Object.assign(tempCharaNote, { useChara: false }) + Object.assign(tempCharaNote, { useChara: false, position: chara_note_position.replace }) extension_settings.note.chara.push(tempCharaNote); } else { @@ -189,9 +209,11 @@ function loadSettings() { $('#extension_floating_chara').val(charaNote ? charaNote.prompt : ''); $('#extension_use_floating_chara').prop('checked', charaNote ? charaNote.useChara : false); + $(`input[name="extension_floating_char_position"][value="${charaNote?.position ?? chara_note_position.replace}"]`).prop('checked', true); } else { $('#extension_floating_chara').val(''); $('#extension_use_floating_chara').prop('checked', false); + $(`input[name="extension_floating_char_position"][value="${chara_note_position.replace}"]`).prop('checked', true); } $('#extension_floating_default').val(extension_settings.note.default); @@ -236,7 +258,17 @@ export function setFloatingPrompt() { // Only replace with the chara note if the user checked the box if (charaNote && charaNote.useChara) { - prompt = charaNote.prompt; + switch (charaNote.position) { + case chara_note_position.before: + prompt = charaNote.prompt + '\n' + prompt; + break; + case chara_note_position.after: + prompt = prompt + '\n' + charaNote.prompt; + break; + default: + prompt = charaNote.prompt; + break; + } } } context.setExtensionPrompt(MODULE_NAME, prompt, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]); @@ -367,8 +399,22 @@ setTimeout(function () { + Use character author's note + +
+ + + +

@@ -404,6 +450,7 @@ setTimeout(function () { $('#extension_use_floating_chara').on('input', onExtensionFloatingCharaCheckboxChanged); $('#extension_floating_default').on('input', onExtensionFloatingDefaultInput); $('input[name="extension_floating_position"]').on('change', onExtensionFloatingPositionInput); + $('input[name="extension_floating_char_position"]').on('change', onExtensionFloatingCharPositionInput); $('#ANClose').on('click', function () { $("#floatingPrompt").transition({ opacity: 0, diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index fd8d18554..ce673275d 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -3,6 +3,8 @@ import { onlyUnique, debounce, delay, + isDataURL, + createThumbnail, } from './utils.js'; import { RA_CountCharTokens, humanizedDateTime } from "./RossAscends-mods.js"; import { sortCharactersList, sortGroupMembers } from './power-user.js'; @@ -57,6 +59,7 @@ import { event_types, getCurrentChatId, setScenarioOverride, + getCropPopup, } from "../script.js"; import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect, tag_map } from './tags.js'; @@ -357,6 +360,14 @@ function updateGroupAvatar(group) { } function getGroupAvatar(group) { + if (!group) { + return $(`
`); + } + + if (isDataURL(group.avatar_url)) { + return $(`
`); + } + const memberAvatars = []; if (group && Array.isArray(group.members) && group.members.length) { for (const member of group.members) { @@ -925,6 +936,8 @@ function select_group_chats(groupId, skipAnimation) { const group = groupId && groups.find((x) => x.id == groupId); const groupName = group?.name ?? ""; setMenuType(!!group ? 'group_edit' : 'group_create'); + $("#group_avatar_preview").empty().append(getGroupAvatar(group)); + $("#rm_group_restore_avatar").toggle(!!group && isDataURL(group.avatar_url)); $("#rm_group_chat_name").val(groupName); $("#rm_group_chat_name").off(); $("#rm_group_chat_name").on("input", async function () { @@ -1049,6 +1062,67 @@ function select_group_chats(groupId, skipAnimation) { $("#rm_group_automode_label").hide(); } + $("#group_avatar_button").off('input').on("input", uploadGroupAvatar); + $("#rm_group_restore_avatar").off('click').on("click", restoreGroupAvatar); + + + async function uploadGroupAvatar(event) { + const file = event.target.files[0]; + + if (!file) { + return; + } + + const e = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = resolve; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + + $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup'); + + const croppedImage = await callPopup(getCropPopup(e.target.result), 'avatarToCrop'); + + if (!croppedImage) { + return; + } + + const thumbnail = await createThumbnail(croppedImage, 96, 144); + + if (!groupId) { + $('#group_avatar_preview img').attr('src', thumbnail); + $('#rm_group_restore_avatar').show(); + return; + } + + let _thisGroup = groups.find((x) => x.id == groupId); + _thisGroup.avatar_url = thumbnail; + $("#group_avatar_preview").empty().append(getGroupAvatar(_thisGroup)); + $("#rm_group_restore_avatar").show(); + await editGroup(groupId, true, true); + } + + async function restoreGroupAvatar() { + const confirm = await callPopup('

Are you sure you want to restore the group avatar?

Your custom image will be deleted, and a collage will be used instead.', 'confirm'); + + if (!confirm) { + return; + } + + if (!groupId) { + $("#group_avatar_preview img").attr("src", default_avatar); + $("#rm_group_restore_avatar").hide(); + return; + } + + let _thisGroup = groups.find((x) => x.id == groupId); + _thisGroup.avatar_url = ''; + $("#group_avatar_preview").empty().append(getGroupAvatar(_thisGroup)); + $("#rm_group_restore_avatar").hide(); + await editGroup(groupId, true, true); + } + $(document).off("click", ".group_member .right_menu_button"); $(document).on("click", ".group_member .right_menu_button", async function (event) { event.stopPropagation(); @@ -1174,8 +1248,7 @@ async function createGroup() { name = `Group: ${memberNames}`; } - // placeholder - const avatar_url = 'img/five.png'; + const avatar_url = $('#group_avatar_preview img').attr('src'); const chatName = humanizedDateTime(); const chats = [chatName]; @@ -1186,7 +1259,7 @@ async function createGroup() { body: JSON.stringify({ name: name, members: members, - avatar_url: avatar_url, + avatar_url: isDataURL(avatar_url) ? avatar_url : default_avatar, allow_self_responses: allow_self_responses, activation_strategy: activation_strategy, disabled_members: [], diff --git a/public/scripts/utils.js b/public/scripts/utils.js index ecbedf038..2087ffa90 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -616,3 +616,38 @@ export function extractDataFromPng(data, identifier = 'chara') { } } } + +export function createThumbnail(dataUrl, maxWidth, maxHeight) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUrl; + img.onload = () => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + // Calculate the thumbnail dimensions while maintaining the aspect ratio + const aspectRatio = img.width / img.height; + let thumbnailWidth = maxWidth; + let thumbnailHeight = maxHeight; + + if (img.width > img.height) { + thumbnailHeight = maxWidth / aspectRatio; + } else { + thumbnailWidth = maxHeight * aspectRatio; + } + + // Set the canvas dimensions and draw the resized image + canvas.width = thumbnailWidth; + canvas.height = thumbnailHeight; + ctx.drawImage(img, 0, 0, thumbnailWidth, thumbnailHeight); + + // Convert the canvas to a data URL and resolve the promise + const thumbnailDataUrl = canvas.toDataURL('image/jpeg'); + resolve(thumbnailDataUrl); + }; + + img.onerror = () => { + reject(new Error('Failed to load the image.')); + }; + }); +} diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 6c4ec8f3c..43dcfb8ce 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -45,6 +45,8 @@ const world_info_position = { }; +const worldInfoCache = {}; + async function getWorldInfoPrompt(chat2, maxContext) { let worldInfoString = "", worldInfoBefore = "", worldInfoAfter = ""; @@ -124,6 +126,10 @@ async function loadWorldInfoData(name) { return; } + if (worldInfoCache[name]) { + return worldInfoCache[name]; + } + const response = await fetch("/getworldinfo", { method: "POST", headers: getRequestHeaders(), @@ -133,6 +139,7 @@ async function loadWorldInfoData(name) { if (response.ok) { const data = await response.json(); + worldInfoCache[name] = data; return data; } @@ -557,6 +564,8 @@ async function saveWorldInfo(name, data, immediately) { return; } + delete worldInfoCache[name]; + if (immediately) { return await _save(name, data); } @@ -902,6 +911,30 @@ function convertAgnaiMemoryBook(inputObj) { return outputObj; } +function convertRisuLorebook(inputObj) { + const outputObj = { entries: {} }; + + inputObj.data.forEach((entry, index) => { + outputObj.entries[index] = { + uid: index, + key: entry.key.split(',').map(x => x.trim()), + keysecondary: entry.secondkey ? entry.secondkey.split(',').map(x => x.trim()) : [], + comment: entry.comment, + content: entry.content, + constant: entry.alwaysActive, + selective: entry.selective, + order: entry.insertorder, + position: world_info_position.before, + disable: false, + addMemo: true, + excludeRecursion: false, + displayIndex: index, + }; + }); + + return outputObj; +} + function convertNovelLorebook(inputObj) { const outputObj = { entries: {} @@ -1055,13 +1088,21 @@ jQuery(() => { // Convert Novel Lorebook if (jsonData.lorebookVersion !== undefined) { + console.log('Converting Novel Lorebook'); formData.append('convertedData', JSON.stringify(convertNovelLorebook(jsonData))); } // Convert Agnai Memory Book if (jsonData.kind === 'memory') { + console.log('Converting Agnai Memory Book'); formData.append('convertedData', JSON.stringify(convertAgnaiMemoryBook(jsonData))); } + + // Convert Risu Lorebook + if (jsonData.type === 'risu') { + console.log('Converting Risu Lorebook'); + formData.append('convertedData', JSON.stringify(convertRisuLorebook(jsonData))); + } } catch (error) { toastr.error(`Error parsing file: ${error}`); return; diff --git a/public/style.css b/public/style.css index 4ebe9bba3..6fe3f98f7 100644 --- a/public/style.css +++ b/public/style.css @@ -687,14 +687,14 @@ hr { gap: 5px; } -#avatar_div_div { +.add_avatar { border: 2px solid var(--SmartThemeBodyColor); margin: 2px; cursor: pointer; transition: filter 0.2s ease-in-out; } -#avatar_div_div:hover { +.add_avatar:hover { filter: drop-shadow(0px 0px 5px var(--SmartThemeQuoteColor)); }