mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'dev' of https://github.com/Cohee1207/SillyTavern into dev
This commit is contained in:
@@ -2262,7 +2262,10 @@
|
||||
</a>
|
||||
</label>
|
||||
<label for="never_resize_avatars"><input id="never_resize_avatars" type="checkbox" />
|
||||
<span data-i18n="Never Resize Avatars">Never Resize Avatars</span>
|
||||
<span data-i18n="Never resize avatars">Never resize avatars</span>
|
||||
</label>
|
||||
<label for="show_card_avatar_urls"><input id="show_card_avatar_urls" type="checkbox" />
|
||||
<span data-i18n="Show avatar filenames">Show avatar filenames</span>
|
||||
</label>
|
||||
|
||||
<div class="inline-drawer wide100p flexFlowColumn">
|
||||
@@ -2673,6 +2676,7 @@
|
||||
<form id="form_character_search_form" action="javascript:void(null);">
|
||||
<div id="rm_button_create" title="Create New Character" class="menu_button fa-solid fa-user-plus "></div>
|
||||
<div id="character_import_button" title="Import Character from File" class="menu_button fa-solid fa-file-arrow-up faSmallFontSquareFix"></div>
|
||||
<div id="external_import_button" title="Import content from external URL" class="menu_button fa-solid fa-cloud-arrow-down faSmallFontSquareFix"></div>
|
||||
<div id="rm_button_group_chats" title="Create New Chat Group" class="menu_button fa-solid fa-users-gear "></div>
|
||||
<input id="character_search_bar" class="text_pole width100p" type="search" placeholder="Search..." maxlength="50" />
|
||||
<select id="character_sort_order" title="Characters sorting order">
|
||||
@@ -3151,7 +3155,7 @@
|
||||
<img src="">
|
||||
</div>
|
||||
<div class="flex-container wide100pLess70px">
|
||||
<div class="ch_name"></div>
|
||||
<div class="wide100p"><span class="ch_name"></span> <i class="ch_avatar_url"></i></div>
|
||||
<i class="ch_fav_icon fa-solid fa-star"></i>
|
||||
<input class="ch_fav" value="" hidden />
|
||||
<div class="ch_description"></div>
|
||||
|
@@ -32,6 +32,7 @@ import {
|
||||
importEmbeddedWorldInfo,
|
||||
checkEmbeddedWorld,
|
||||
setWorldInfoButtonClass,
|
||||
importWorldInfo,
|
||||
} from "./scripts/world-info.js";
|
||||
|
||||
import {
|
||||
@@ -227,6 +228,7 @@ export {
|
||||
extension_prompt_types,
|
||||
updateVisibleDivs,
|
||||
mesForShowdownParse,
|
||||
printCharacters,
|
||||
}
|
||||
|
||||
// API OBJECT FOR EXTERNAL WIRING
|
||||
@@ -805,6 +807,9 @@ async function printCharacters() {
|
||||
template.find('img').attr('src', this_avatar);
|
||||
template.find('.avatar').attr('title', item.avatar);
|
||||
template.find('.ch_name').text(item.name);
|
||||
if (power_user.show_card_avatar_urls) {
|
||||
template.find('.ch_avatar_url').text(item.avatar);
|
||||
}
|
||||
template.find('.ch_fav_icon').css("display", 'none');
|
||||
template.toggleClass('is_fav', item.fav || item.fav == 'true');
|
||||
template.find('.ch_fav').val(item.fav);
|
||||
@@ -7958,6 +7963,55 @@ $(document).ready(function () {
|
||||
restoreCaretPosition($(this).get(0), caretPosition);
|
||||
});
|
||||
|
||||
$('#external_import_button').on('click', async () => {
|
||||
const html = `<h3>Enter the URL of the content to import</h3>
|
||||
Supported sources:<br>
|
||||
<ul class="justifyLeft">
|
||||
<li>Chub characters (direct link or id)<br>Example: <tt>lorebooks/bartleby/example-lorebook</tt></li>
|
||||
<li>Chub lorebooks (direct link or id)<br>Example: <tt>Anonymous/example-character</tt></li>
|
||||
<li>More coming soon...</li>
|
||||
<ul>`
|
||||
const input = await callPopup(html, 'input');
|
||||
|
||||
if (!input) {
|
||||
console.debug('Custom content import cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = input.trim();
|
||||
console.debug('Custom content import started', url);
|
||||
|
||||
const request = await fetch('/import_custom', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ url }),
|
||||
});
|
||||
|
||||
if (!request.ok) {
|
||||
toastr.info(request.statusText, 'Custom content import failed');
|
||||
console.error('Custom content import failed', request.status, request.statusText);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await request.blob();
|
||||
const customContentType = request.headers.get('X-Custom-Content-Type');
|
||||
const fileName = request.headers.get('Content-Disposition').split('filename=')[1].replace(/"/g, '');
|
||||
const file = new File([data], fileName, { type: data.type });
|
||||
|
||||
switch (customContentType) {
|
||||
case 'character':
|
||||
processDroppedFiles([file]);
|
||||
break;
|
||||
case 'lorebook':
|
||||
await importWorldInfo(file);
|
||||
break;
|
||||
default:
|
||||
toastr.warn('Unknown content type');
|
||||
console.error('Unknown content type', customContentType);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const $dropzone = $(document.body);
|
||||
|
||||
$dropzone.on('dragover', (event) => {
|
||||
|
@@ -12,6 +12,7 @@ import {
|
||||
eventSource,
|
||||
event_types,
|
||||
getCurrentChatId,
|
||||
printCharacters,
|
||||
name1,
|
||||
name2,
|
||||
} from "../script.js";
|
||||
@@ -107,6 +108,7 @@ let power_user = {
|
||||
chat_display: chat_styles.DEFAULT,
|
||||
sheld_width: sheld_width.DEFAULT,
|
||||
never_resize_avatars: false,
|
||||
show_card_avatar_urls: false,
|
||||
play_message_sound: false,
|
||||
play_sound_unfocused: true,
|
||||
auto_save_msg_edits: false,
|
||||
@@ -592,6 +594,7 @@ function loadPowerUserSettings(settings, data) {
|
||||
$("#play_message_sound").prop("checked", power_user.play_message_sound);
|
||||
$("#play_sound_unfocused").prop("checked", power_user.play_sound_unfocused);
|
||||
$("#never_resize_avatars").prop("checked", power_user.never_resize_avatars);
|
||||
$("#show_card_avatar_urls").prop("checked", power_user.show_card_avatar_urls);
|
||||
$("#auto_save_msg_edits").prop("checked", power_user.auto_save_msg_edits);
|
||||
$("#allow_name1_display").prop("checked", power_user.allow_name1_display);
|
||||
$("#allow_name2_display").prop("checked", power_user.allow_name2_display);
|
||||
@@ -1205,6 +1208,11 @@ $(document).ready(() => {
|
||||
power_user.never_resize_avatars = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$("#show_card_avatar_urls").on('input', function () {
|
||||
power_user.show_card_avatar_urls = !!$(this).prop('checked');
|
||||
printCharacters();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$("#play_message_sound").on('input', function () {
|
||||
power_user.play_message_sound = !!$(this).prop('checked');
|
||||
|
@@ -250,7 +250,7 @@ function displayWorldEntries(name, data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingCharLores = world_info.charLore.filter((e) => e.extraBooks.includes(name));
|
||||
const existingCharLores = world_info.charLore?.filter((e) => e.extraBooks.includes(name));
|
||||
if (existingCharLores && existingCharLores.length > 0) {
|
||||
existingCharLores.forEach((charLore) => {
|
||||
const tempCharLore = charLore.extraBooks.filter((e) => e !== name);
|
||||
@@ -627,7 +627,7 @@ async function renameWorldInfo(name, data) {
|
||||
await saveWorldInfo(newName, data, true);
|
||||
await deleteWorldInfo(oldName);
|
||||
|
||||
const existingCharLores = world_info.charLore.filter((e) => e.extraBooks.includes(oldName));
|
||||
const existingCharLores = world_info.charLore?.filter((e) => e.extraBooks.includes(oldName));
|
||||
if (existingCharLores && existingCharLores.length > 0) {
|
||||
existingCharLores.forEach((charLore) => {
|
||||
const tempCharLore = charLore.extraBooks.filter((e) => e !== oldName);
|
||||
@@ -747,7 +747,7 @@ async function getCharacterLore() {
|
||||
|
||||
// TODO: Maybe make the utility function not use the window context?
|
||||
const fileName = getCharaFilename(this_chid);
|
||||
const extraCharLore = world_info.charLore.find((e) => e.name === fileName);
|
||||
const extraCharLore = world_info.charLore?.find((e) => e.name === fileName);
|
||||
if (extraCharLore) {
|
||||
worldsToSearch = new Set([...worldsToSearch, ...extraCharLore.extraBooks]);
|
||||
}
|
||||
@@ -1183,47 +1183,13 @@ function onWorldInfoChange(_, text) {
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
jQuery(() => {
|
||||
|
||||
$(document).ready(function () {
|
||||
registerSlashCommand('world', onWorldInfoChange, [], "– sets active World, or unsets if no args provided", true, true);
|
||||
})
|
||||
|
||||
let selectScrollTop = null;
|
||||
|
||||
$("#world_info").on('mousedown change', async function (e) {
|
||||
// If there's no world names, don't do anything
|
||||
if (world_names.length === 0) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (deviceInfo.device.type === 'desktop') {
|
||||
e.preventDefault();
|
||||
const option = $(e.target);
|
||||
const selectElement = $(this)[0];
|
||||
selectScrollTop = selectElement.scrollTop;
|
||||
option.prop('selected', !option.prop('selected'));
|
||||
await delay(1);
|
||||
selectElement.scrollTop = selectScrollTop;
|
||||
}
|
||||
|
||||
onWorldInfoChange('__notSlashCommand__');
|
||||
});
|
||||
|
||||
//**************************WORLD INFO IMPORT EXPORT*************************//
|
||||
$("#world_import_button").on('click', function () {
|
||||
$("#world_import_file").trigger('click');
|
||||
});
|
||||
|
||||
$("#world_import_file").on("change", async function (e) {
|
||||
const file = e.target.files[0];
|
||||
|
||||
export async function importWorldInfo(file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData($("#form_world_import").get(0));
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', file);
|
||||
|
||||
try {
|
||||
let jsonData;
|
||||
@@ -1285,6 +1251,45 @@ jQuery(() => {
|
||||
},
|
||||
error: (jqXHR, exception) => { },
|
||||
});
|
||||
}
|
||||
|
||||
jQuery(() => {
|
||||
|
||||
$(document).ready(function () {
|
||||
registerSlashCommand('world', onWorldInfoChange, [], "– sets active World, or unsets if no args provided", true, true);
|
||||
})
|
||||
|
||||
let selectScrollTop = null;
|
||||
|
||||
$("#world_info").on('mousedown change', async function (e) {
|
||||
// If there's no world names, don't do anything
|
||||
if (world_names.length === 0) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (deviceInfo.device.type === 'desktop') {
|
||||
e.preventDefault();
|
||||
const option = $(e.target);
|
||||
const selectElement = $(this)[0];
|
||||
selectScrollTop = selectElement.scrollTop;
|
||||
option.prop('selected', !option.prop('selected'));
|
||||
await delay(1);
|
||||
selectElement.scrollTop = selectScrollTop;
|
||||
}
|
||||
|
||||
onWorldInfoChange('__notSlashCommand__');
|
||||
});
|
||||
|
||||
//**************************WORLD INFO IMPORT EXPORT*************************//
|
||||
$("#world_import_button").on('click', function () {
|
||||
$("#world_import_file").trigger('click');
|
||||
});
|
||||
|
||||
$("#world_import_file").on("change", async function (e) {
|
||||
const file = e.target.files[0];
|
||||
|
||||
await importWorldInfo(file);
|
||||
|
||||
// Will allow to select the same file twice in a row
|
||||
$("#form_world_import").trigger("reset");
|
||||
|
@@ -1311,13 +1311,14 @@ select option:not(:checked) {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
border-radius: 7px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#character_search_bar {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
/* padding-left: 0.75em; */
|
||||
height: fit-content;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
input[type=search]::-webkit-search-cancel-button {
|
||||
@@ -1353,6 +1354,10 @@ input[type=search]:focus::-webkit-search-cancel-button {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.ch_avatar_url {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.character_select .avatar {
|
||||
align-self: center;
|
||||
}
|
||||
|
110
server.js
110
server.js
@@ -3857,6 +3857,116 @@ app.post('/upload_sprite', urlencodedParser, async (request, response) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/import_custom', jsonParser, async (request, response) => {
|
||||
if (!request.body.url) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
try {
|
||||
const url = request.body.url;
|
||||
let result;
|
||||
|
||||
const chubParsed = parseChubUrl(url);
|
||||
|
||||
if (chubParsed?.type === 'character') {
|
||||
console.log('Downloading chub character:', chubParsed.id);
|
||||
result = await downloadChubCharacter(chubParsed.id);
|
||||
}
|
||||
else if (chubParsed?.type === 'lorebook') {
|
||||
console.log('Downloading chub lorebook:', chubParsed.id);
|
||||
result = await downloadChubLorebook(chubParsed.id);
|
||||
}
|
||||
else {
|
||||
return response.sendStatus(404);
|
||||
}
|
||||
|
||||
response.set('Content-Type', result.fileType);
|
||||
response.set('Content-Disposition', `attachment; filename="${result.fileName}"`);
|
||||
response.set('X-Custom-Content-Type', chubParsed?.type);
|
||||
return response.send(result.buffer);
|
||||
} catch (error) {
|
||||
console.log('Importing custom content failed', error);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
async function downloadChubLorebook(id) {
|
||||
const fetch = require('node-fetch').default;
|
||||
|
||||
const result = await fetch('https://api.chub.ai/api/lorebooks/download', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
"fullPath": id,
|
||||
"format": "SILLYTAVERN",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error('Failed to download lorebook');
|
||||
}
|
||||
|
||||
const name = id.split('/').pop();
|
||||
const buffer = await result.buffer();
|
||||
const fileName = `${sanitize(name)}.json`;
|
||||
const fileType = result.headers.get('content-type');
|
||||
|
||||
return { buffer, fileName, fileType };
|
||||
}
|
||||
|
||||
async function downloadChubCharacter(id) {
|
||||
const fetch = require('node-fetch').default;
|
||||
|
||||
const result = await fetch('https://api.chub.ai/api/characters/download', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
"format": "tavern",
|
||||
"fullPath": id,
|
||||
})
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error('Failed to download character');
|
||||
}
|
||||
|
||||
const buffer = await result.buffer();
|
||||
const fileName = result.headers.get('content-disposition').split('filename=')[1];
|
||||
const fileType = result.headers.get('content-type');
|
||||
|
||||
return { buffer, fileName, fileType };
|
||||
}
|
||||
|
||||
function parseChubUrl(str) {
|
||||
const splitStr = str.split('/');
|
||||
const length = splitStr.length;
|
||||
|
||||
if (length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const domainIndex = splitStr.indexOf('chub.ai');
|
||||
const lastTwo = domainIndex !== -1 ? splitStr.slice(domainIndex + 1) : splitStr;
|
||||
|
||||
const firstPart = lastTwo[0].toLowerCase();
|
||||
|
||||
if (firstPart === 'characters' || firstPart === 'lorebooks') {
|
||||
const type = firstPart === 'characters' ? 'character' : 'lorebook';
|
||||
const id = type === 'character' ? lastTwo.slice(1).join('/') : lastTwo.join('/');
|
||||
return {
|
||||
id: id,
|
||||
type: type
|
||||
};
|
||||
} else if (length === 2) {
|
||||
return {
|
||||
id: lastTwo.join('/'),
|
||||
type: 'character'
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function importRisuSprites(data) {
|
||||
try {
|
||||
const name = data?.data?.name;
|
||||
|
Reference in New Issue
Block a user