Merge branch 'staging' into support-multiple-expressions

This commit is contained in:
Wolfsblvt
2025-01-26 23:11:37 +01:00
85 changed files with 2970 additions and 917 deletions

View File

@@ -11,6 +11,7 @@ import { debounce_timeout } from './constants.js';
import { renderTemplateAsync } from './templates.js';
import { Popup } from './popup.js';
import { t } from './i18n.js';
import { isMobile } from './RossAscends-mods.js';
function debouncePromise(func, delay) {
let timeoutId;
@@ -1562,6 +1563,7 @@ class PromptManager {
listItemHtml += `
<li class="${prefix}prompt_manager_prompt ${draggableClass} ${enabledClass} ${markerClass} ${importantClass}" data-pm-identifier="${escapeHtml(prompt.identifier)}">
<span class="drag-handle">☰</span>
<span class="${prefix}prompt_manager_prompt_name" data-pm-name="${encodedName}">
${isMarkerPrompt ? '<span class="fa-fw fa-solid fa-thumb-tack" title="Marker"></span>' : ''}
${isSystemPrompt ? '<span class="fa-fw fa-solid fa-square-poll-horizontal" title="Global Prompt"></span>' : ''}
@@ -1741,6 +1743,7 @@ class PromptManager {
makeDraggable() {
$(`#${this.configuration.prefix}prompt_manager_list`).sortable({
delay: this.configuration.sortableDelay,
handle: isMobile() ? '.drag-handle' : null,
items: `.${this.configuration.prefix}prompt_manager_prompt_draggable`,
update: (event, ui) => {
const promptOrder = this.getPromptOrderForCharacter(this.activeCharacter);

View File

@@ -59,6 +59,16 @@ const hash_derivations = {
// Tulu-3-8B
// Tulu-3-70B
'Tulu'
,
// DeepSeek V2.5
'54d400beedcd17f464e10063e0577f6f798fa896266a912d8a366f8a2fcc0bca':
'DeepSeek-V2.5'
,
// DeepSeek R1
'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':
'DeepSeek-V2.5'
};
const substr_derivations = {

View File

@@ -11,6 +11,7 @@ import {
getCurrentChatId,
getRequestHeaders,
hideSwipeButtons,
name1,
name2,
reloadCurrentChat,
saveChatDebounced,
@@ -21,6 +22,7 @@ import {
chat_metadata,
neutralCharacterName,
updateChatMetadata,
system_message_types,
} from '../script.js';
import { selected_group } from './group-chats.js';
import { power_user } from './power-user.js';
@@ -34,6 +36,7 @@ import {
humanFileSize,
saveBase64AsFile,
extractTextFromOffice,
download,
} from './utils.js';
import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
@@ -41,6 +44,7 @@ import { ScraperManager } from './scrapers.js';
import { DragAndDropHandler } from './dragdrop.js';
import { renderTemplateAsync } from './templates.js';
import { t } from './i18n.js';
import { humanizedDateTime } from './RossAscends-mods.js';
/**
* @typedef {Object} FileAttachment
@@ -585,10 +589,12 @@ async function enlargeMessageImage() {
const imgHolder = document.createElement('div');
imgHolder.classList.add('img_enlarged_holder');
imgHolder.append(img);
const imgContainer = $('<div><pre><code></code></pre></div>');
const imgContainer = $('<div><pre><code class="img_enlarged_title"></code></pre></div>');
imgContainer.prepend(imgHolder);
imgContainer.addClass('img_enlarged_container');
imgContainer.find('code').addClass('txt').text(title);
const codeTitle = imgContainer.find('.img_enlarged_title');
codeTitle.addClass('txt').text(title);
const titleEmpty = !title || title.trim().length === 0;
imgContainer.find('pre').toggle(!titleEmpty);
addCopyToCodeBlocks(imgContainer);
@@ -598,9 +604,17 @@ async function enlargeMessageImage() {
popup.dlg.style.width = 'unset';
popup.dlg.style.height = 'unset';
img.addEventListener('click', () => {
img.addEventListener('click', event => {
const shouldZoom = !img.classList.contains('zoomed');
img.classList.toggle('zoomed', shouldZoom);
event.stopPropagation();
});
codeTitle[0]?.addEventListener('click', event => {
event.stopPropagation();
});
popup.dlg.addEventListener('click', event => {
popup.completeCancelled();
});
await popup.show();
@@ -1427,6 +1441,19 @@ jQuery(function () {
await viewMessageFile(messageId);
});
$(document).on('click', '.assistant_note_export', async function () {
const chatToSave = [
{
user_name: name1,
character_name: name2,
chat_metadata: chat_metadata,
},
...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
];
download(JSON.stringify(chatToSave, null, 4), `Assistant - ${humanizedDateTime()}.json`, 'application/json');
});
// Do not change. #attachFile is added by extension.
$(document).on('click', '#attachFile', function () {
$('#file_form_input').trigger('click');

View File

@@ -4,7 +4,7 @@ import { eventSource, event_types, saveSettings, saveSettingsDebounced, getReque
import { showLoader } from './loader.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { renderTemplate, renderTemplateAsync } from './templates.js';
import { delay, isSubsetOf, setValueByPath } from './utils.js';
import { delay, isSubsetOf, sanitizeSelector, setValueByPath } from './utils.js';
import { getContext } from './st-context.js';
import { isAdmin } from './user.js';
import { t } from './i18n.js';
@@ -38,7 +38,8 @@ export let modules = [];
let activeExtensions = new Set();
const getApiUrl = () => extension_settings.apiUrl;
const sortManifests = (a, b) => parseInt(a.loading_order) - parseInt(b.loading_order) || String(a.display_name).localeCompare(String(b.display_name));
const sortManifestsByOrder = (a, b) => parseInt(a.loading_order) - parseInt(b.loading_order) || String(a.display_name).localeCompare(String(b.display_name));
const sortManifestsByName = (a, b) => String(a.display_name).localeCompare(String(b.display_name)) || parseInt(a.loading_order) - parseInt(b.loading_order);
let connectedToApi = false;
/**
@@ -365,7 +366,7 @@ async function getManifests(names) {
* @returns {Promise<void>}
*/
async function activateExtensions() {
const extensions = Object.entries(manifests).sort((a, b) => sortManifests(a[1], b[1]));
const extensions = Object.entries(manifests).sort((a, b) => sortManifestsByOrder(a[1], b[1]));
const promises = [];
for (let entry of extensions) {
@@ -519,10 +520,11 @@ function addExtensionStyle(name, manifest) {
return new Promise((resolve, reject) => {
const url = `/scripts/extensions/${name}/${manifest.css}`;
const id = sanitizeSelector(`${name}-css`);
if ($(`link[id="${name}"]`).length === 0) {
if ($(`link[id="${id}"]`).length === 0) {
const link = document.createElement('link');
link.id = name;
link.id = id;
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = url;
@@ -550,11 +552,12 @@ function addExtensionScript(name, manifest) {
return new Promise((resolve, reject) => {
const url = `/scripts/extensions/${name}/${manifest.js}`;
const id = sanitizeSelector(`${name}-js`);
let ready = false;
if ($(`script[id="${name}"]`).length === 0) {
if ($(`script[id="${id}"]`).length === 0) {
const script = document.createElement('script');
script.id = name;
script.id = id;
script.type = 'module';
script.src = url;
script.async = true;
@@ -690,9 +693,9 @@ function getExtensionData(extension) {
* @return {string} - The HTML string for the module information.
*/
function getModuleInformation() {
let moduleInfo = modules.length ? `<p>${DOMPurify.sanitize(modules.join(', '))}</p>` : '<p class="failure">Not connected to the API!</p>';
let moduleInfo = modules.length ? `<p>${DOMPurify.sanitize(modules.join(', '))}</p>` : '<p class="failure">' + t`Not connected to the API!` + '</p>';
return `
<h3>Modules provided by your Extras API:</h3>
<h3>` + t`Modules provided by your Extras API:` + `</h3>
${moduleInfo}
`;
}
@@ -711,16 +714,19 @@ async function showExtensionsDetails() {
initialScrollTop = oldPopup.content.scrollTop;
await oldPopup.completeCancelled();
}
const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">Built-in Extensions:</h3></div>');
const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">Installed Extensions:</h3></div>');
const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Built-in Extensions:` + '</h3></div>');
const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Installed Extensions:` + '</h3></div>');
const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
<i class="fa-solid fa-spinner fa-spin"></i>
<span>Loading third-party extensions... Please wait...</span>
<span>` + t`Loading third-party extensions... Please wait...` + `</span>
</div>`);
htmlExternal.append(htmlLoading);
const extensions = Object.entries(manifests).sort((a, b) => sortManifests(a[1], b[1])).map(getExtensionData);
const sortOrderKey = 'extensions_sortByName';
const sortByName = localStorage.getItem(sortOrderKey) === 'true';
const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;
const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);
extensions.forEach(value => {
const { isExternal, extensionHtml } = value;
@@ -736,8 +742,7 @@ async function showExtensionsDetails() {
/** @type {import('./popup.js').CustomPopupButton} */
const updateAllButton = {
text: 'Update all',
appendAtEnd: true,
text: t`Update all`,
action: async () => {
requiresReload = true;
await autoUpdateExtensions(true);
@@ -745,13 +750,23 @@ async function showExtensionsDetails() {
},
};
/** @type {import('./popup.js').CustomPopupButton} */
const sortOrderButton = {
text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`,
action: async () => {
abortController.abort();
localStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true');
await showExtensionsDetails();
},
};
let waitingForSave = false;
const popup = new Popup(html, POPUP_TYPE.TEXT, '', {
okButton: 'Close',
okButton: t`Close`,
wide: true,
large: true,
customButtons: [updateAllButton],
customButtons: [sortOrderButton, updateAllButton],
allowVerticalScrolling: true,
onClosing: async () => {
if (waitingForSave) {
@@ -770,7 +785,7 @@ async function showExtensionsDetails() {
});
popupPromise = popup.show();
popup.content.scrollTop = initialScrollTop;
checkForUpdatesManual(abortController.signal).finally(() => htmlLoading.remove());
checkForUpdatesManual(sortFn, abortController.signal).finally(() => htmlLoading.remove());
} catch (error) {
toastr.error(t`Error loading extensions. See browser console for details.`);
console.error(error);
@@ -841,7 +856,7 @@ async function updateExtension(extensionName, quiet) {
toastr.success('Extension is already up to date');
}
} else {
toastr.success(`Extension ${extensionName} updated to ${data.shortCommitHash}`, 'Reload the page to apply updates');
toastr.success(t`Extension ${extensionName} updated to ${data.shortCommitHash}`, t`Reload the page to apply updates`);
}
} catch (error) {
console.error('Error:', error);
@@ -1009,7 +1024,7 @@ export async function installExtension(url, global) {
}
const response = await request.json();
toastr.success(`Extension "${response.display_name}" by ${response.author} (version ${response.version}) has been installed successfully!`, 'Extension installation successful');
toastr.success(t`Extension '${response.display_name}' by ${response.author} (version ${response.version}) has been installed successfully!`, t`Extension installation successful`);
console.debug(`Extension "${response.display_name}" has been installed successfully at ${response.extensionPath}`);
await loadExtensionSettings({}, false, false);
await eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED);
@@ -1081,12 +1096,13 @@ function processVersionCheckQueue() {
/**
* Performs a manual check for updates on all 3rd-party extensions.
* @param {function} sortFn Sort function
* @param {AbortSignal} abortSignal Signal to abort the operation
* @returns {Promise<any[]>}
*/
async function checkForUpdatesManual(abortSignal) {
async function checkForUpdatesManual(sortFn, abortSignal) {
const promises = [];
for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party')).sort((a, b) => sortManifests(manifests[a], manifests[b]))) {
for (const id of Object.keys(manifests).filter(x => x.startsWith('third-party')).sort((a, b) => sortFn(manifests[a], manifests[b]))) {
const externalId = id.replace('third-party', '');
const promise = enqueueVersionCheck(async () => {
try {
@@ -1183,7 +1199,7 @@ async function checkForExtensionUpdates(force) {
await Promise.allSettled(promises);
if (updatesAvailable.length > 0) {
toastr.info(`${updatesAvailable.map(x => `${x}`).join('\n')}`, 'Extension updates available');
toastr.info(`${updatesAvailable.map(x => `${x}`).join('\n')}`, t`Extension updates available`);
}
}
@@ -1197,7 +1213,7 @@ async function autoUpdateExtensions(forceAll) {
return;
}
const banner = toastr.info('Auto-updating extensions. This may take several minutes.', 'Please wait...', { timeOut: 10000, extendedTimeOut: 10000 });
const banner = toastr.info(t`Auto-updating extensions. This may take several minutes.`, t`Please wait...`, { timeOut: 10000, extendedTimeOut: 10000 });
const isCurrentUserAdmin = isAdmin();
const promises = [];
for (const [id, manifest] of Object.entries(manifests)) {
@@ -1231,7 +1247,7 @@ export async function runGenerationInterceptors(chat, contextSize, type) {
exitImmediately = immediately;
};
for (const manifest of Object.values(manifests).filter(x => x.generate_interceptor).sort((a, b) => sortManifests(a, b))) {
for (const manifest of Object.values(manifests).filter(x => x.generate_interceptor).sort((a, b) => sortManifestsByOrder(a, b))) {
const interceptorKey = manifest.generate_interceptor;
if (typeof globalThis[interceptorKey] === 'function') {
try {

View File

@@ -54,6 +54,8 @@
<option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
<option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
<option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp</option>
<option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21</option>
<option data-type="google" value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219</option>
<option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option>
<option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>

View File

@@ -1,10 +1,10 @@
<div>
<h3>Included settings:</h3>
<h3 data-i18n="Included settings:">Included settings:</h3>
<div class="justifyLeft flex-container flexFlowColumn flexNoGap">
{{#each settings}}
<label class="checkbox_label">
<input type="checkbox" value="{{@key}}" name="exclude"{{#if this}} checked{{/if}}>
<span>{{@key}}</span>
<span data-i18n="{{@key}}">{{@key}}</span>
</label>
{{/each}}
</div>

View File

@@ -12,8 +12,8 @@
</div>
<div class="marginTop5">
<small>
<b>Hint:</b>
<i>Click on the setting name to omit it from the profile.</i>
<b data-i18n="Hint:">Hint:</b>
<i data-i18n="Click on the setting name to omit it from the profile.">Click on the setting name to omit it from the profile.</i>
</small>
</div>
<h3 data-i18n="Enter a name:">

View File

@@ -277,7 +277,7 @@ function makeMovable(id = 'gallery') {
const newElement = $(template);
newElement.css('background-color', 'var(--SmartThemeBlurTintColor)');
newElement.attr('forChar', id);
newElement.attr('id', `${id}`);
newElement.attr('id', id);
newElement.find('.drag-grabber').attr('id', `${id}header`);
newElement.find('.dragTitle').text('Image Gallery');
//add a div for the gallery

View File

@@ -46,10 +46,12 @@
<div class="qr--title" data-i18n="Edit Quick Replies">Edit Quick Replies</div>
<div class="qr--actions">
<select id="qr--set" class="text_pole"></select>
<div class="qr--add menu_button menu_button_icon fa-solid fa-pencil" id="qr--set-rename" title="Rename quick reply set"></div>
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-new" title="Create new quick reply set"></div>
<div class="qr--add menu_button menu_button_icon fa-solid fa-file-import" id="qr--set-import" title="Import quick reply set"></div>
<input type="file" id="qr--set-importFile" accept=".json" hidden>
<div class="qr--add menu_button menu_button_icon fa-solid fa-file-export" id="qr--set-export" title="Export quick reply set"></div>
<div class="qr-add menu_button menu_button_icon fa-solid fa-paste" id="qr--set-duplicate" title="Duplicate quick reply set"></div>
<div class="qr--del menu_button menu_button_icon fa-solid fa-trash redWarningBG" id="qr--set-delete" title="Delete quick reply set"></div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { callPopup } from '../../../../../script.js';
import { Popup } from '../../../../popup.js';
import { getSortableDelay } from '../../../../utils.js';
import { log, warn } from '../../index.js';
import { QuickReply } from '../QuickReply.js';
@@ -111,6 +111,7 @@ export class SettingsUi {
prepareQrEditor() {
// qr editor
this.dom.querySelector('#qr--set-rename').addEventListener('click', async () => this.renameQrSet());
this.dom.querySelector('#qr--set-new').addEventListener('click', async()=>this.addQrSet());
/**@type {HTMLInputElement}*/
const importFile = this.dom.querySelector('#qr--set-importFile');
@@ -119,7 +120,8 @@ export class SettingsUi {
importFile.value = null;
});
this.dom.querySelector('#qr--set-import').addEventListener('click', ()=>importFile.click());
this.dom.querySelector('#qr--set-export').addEventListener('click', async()=>this.exportQrSet());
this.dom.querySelector('#qr--set-export').addEventListener('click', async () => this.exportQrSet());
this.dom.querySelector('#qr--set-duplicate').addEventListener('click', async () => this.duplicateQrSet());
this.dom.querySelector('#qr--set-delete').addEventListener('click', async()=>this.deleteQrSet());
this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{
this.currentQrSet.addQuickReply();
@@ -279,7 +281,7 @@ export class SettingsUi {
}
async deleteQrSet() {
const confirmed = await callPopup(`Are you sure you want to delete the Quick Reply Set "${this.currentQrSet.name}"?<br>This cannot be undone.`, 'confirm');
const confirmed = await Popup.show.confirm('Delete Quick Reply Set', `Are you sure you want to delete the Quick Reply Set "${this.currentQrSet.name}"?<br>This cannot be undone.`);
if (confirmed) {
await this.doDeleteQrSet(this.currentQrSet);
this.rerender();
@@ -303,12 +305,52 @@ export class SettingsUi {
this.settings.save();
}
async renameQrSet() {
const newName = await Popup.show.input('Rename Quick Reply Set', 'Enter a new name:', this.currentQrSet.name);
if (newName && newName.length > 0) {
const existingSet = QuickReplySet.get(newName);
if (existingSet) {
toastr.error(`A Quick Reply Set named "${newName}" already exists.`);
return;
}
const oldName = this.currentQrSet.name;
this.currentQrSet.name = newName;
await this.currentQrSet.save();
// Update it in both set lists
this.settings.config.setList.forEach(set => {
if (set.set.name === oldName) {
set.set.name = newName;
}
});
this.settings.chatConfig?.setList.forEach(set => {
if (set.set.name === oldName) {
set.set.name = newName;
}
});
this.settings.save();
// Update the option in the current selected QR dropdown. All others will be refreshed via the prepare calls below.
/** @type {HTMLOptionElement} */
const option = this.currentSet.querySelector(`#qr--set option[value="${oldName}"]`);
option.value = newName;
option.textContent = newName;
this.currentSet.value = newName;
this.onQrSetChange();
this.prepareGlobalSetList();
this.prepareChatSetList();
console.info(`Quick Reply Set renamed from ""${oldName}" to "${newName}".`);
}
}
async addQrSet() {
const name = await callPopup('Quick Reply Set Name:', 'input');
const name = await Popup.show.input('Create a new World Info', 'Enter a name for the new Quick Reply Set:');
if (name && name.length > 0) {
const oldQrs = QuickReplySet.get(name);
if (oldQrs) {
const replace = await callPopup(`A Quick Reply Set named "${name}" already exists.<br>Do you want to overwrite the existing Quick Reply Set?<br>The existing set will be deleted. This cannot be undone.`, 'confirm');
const replace = Popup.show.confirm('Replace existing World Info', `A Quick Reply Set named "${name}" already exists.<br>Do you want to overwrite the existing Quick Reply Set?<br>The existing set will be deleted. This cannot be undone.`);
if (replace) {
const idx = QuickReplySet.list.indexOf(oldQrs);
await this.doDeleteQrSet(oldQrs);
@@ -369,7 +411,7 @@ export class SettingsUi {
qrs.init();
const oldQrs = QuickReplySet.get(props.name);
if (oldQrs) {
const replace = await callPopup(`A Quick Reply Set named "${qrs.name}" already exists.<br>Do you want to overwrite the existing Quick Reply Set?<br>The existing set will be deleted. This cannot be undone.`, 'confirm');
const replace = Popup.show.confirm('Replace existing World Info', `A Quick Reply Set named "${name}" already exists.<br>Do you want to overwrite the existing Quick Reply Set?<br>The existing set will be deleted. This cannot be undone.`);
if (replace) {
const idx = QuickReplySet.list.indexOf(oldQrs);
await this.doDeleteQrSet(oldQrs);
@@ -421,6 +463,40 @@ export class SettingsUi {
URL.revokeObjectURL(url);
}
async duplicateQrSet() {
const newName = await Popup.show.input('Duplicate Quick Reply Set', 'Enter a name for the new Quick Reply Set:', `${this.currentQrSet.name} (Copy)`);
if (newName && newName.length > 0) {
const existingSet = QuickReplySet.get(newName);
if (existingSet) {
toastr.error(`A Quick Reply Set named "${newName}" already exists.`);
return;
}
const newQrSet = QuickReplySet.from(JSON.parse(JSON.stringify(this.currentQrSet)));
newQrSet.name = newName;
newQrSet.qrList = this.currentQrSet.qrList.map(qr => QuickReply.from(JSON.parse(JSON.stringify(qr))));
newQrSet.addQuickReply();
const idx = QuickReplySet.list.findIndex(it => it.name.toLowerCase().localeCompare(newName.toLowerCase()) == 1);
if (idx > -1) {
QuickReplySet.list.splice(idx, 0, newQrSet);
} else {
QuickReplySet.list.push(newQrSet);
}
const opt = document.createElement('option'); {
opt.value = newQrSet.name;
opt.textContent = newQrSet.name;
if (idx > -1) {
this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt);
} else {
this.currentSet.append(opt);
}
}
this.currentSet.value = newName;
this.onQrSetChange();
this.prepareGlobalSetList();
this.prepareChatSetList();
}
}
selectQrSet(qrs) {
this.currentSet.value = qrs.name;
this.onQrSetChange();

View File

@@ -388,7 +388,7 @@ class AllTalkTtsProvider {
}
async fetchRvcVoiceObjects() {
if (this.settings.server_version !== 'v2') {
if (this.settings.server_version == 'v2') {
console.log('Skipping RVC voices fetch for V1 server');
return [];
}

View File

@@ -30,6 +30,8 @@ import { textgen_types, textgenerationwebui_settings } from '../../textgen-setti
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
@@ -1613,25 +1615,55 @@ jQuery(async () => {
callback: async (args, query) => {
const clamp = (v) => Number.isNaN(v) ? null : Math.min(1, Math.max(0, v));
const threshold = clamp(Number(args?.threshold ?? settings.score_threshold));
const validateCount = (v) => Number.isNaN(v) || !Number.isInteger(v) || v < 1 ? null : v;
const count = validateCount(Number(args?.count)) ?? settings.chunk_count_db;
const source = String(args?.source ?? '');
const attachments = source ? getDataBankAttachmentsForSource(source, false) : getDataBankAttachments(false);
const collectionIds = await ingestDataBankAttachments(String(source));
const queryResults = await queryMultipleCollections(collectionIds, String(query), settings.chunk_count_db, threshold);
// Map collection IDs to file URLs
const queryResults = await queryMultipleCollections(collectionIds, String(query), count, threshold);
// Get URLs
const urls = Object
.keys(queryResults)
.map(x => attachments.find(y => getFileCollectionId(y.url) === x))
.filter(x => x)
.map(x => x.url);
// Gets the actual text content of chunks
const getChunksText = () => {
let textResult = '';
for (const collectionId in queryResults) {
const metadata = queryResults[collectionId].metadata?.filter(x => x.text)?.sort((a, b) => a.index - b.index)?.map(x => x.text)?.filter(onlyUnique) || [];
textResult += metadata.join('\n') + '\n\n';
}
return textResult;
};
if (args.return === 'chunks') {
return getChunksText();
}
return JSON.stringify(urls);
// @ts-ignore
return slashCommandReturnHelper.doReturn(args.return ?? 'object', urls, { objectToStringFunc: list => list.join('\n') });
},
aliases: ['databank-search', 'data-bank-search'],
helpString: 'Search the Data Bank for a specific query using vector similarity. Returns a list of file URLs with the most relevant content.',
namedArgumentList: [
new SlashCommandNamedArgument('threshold', 'Threshold for the similarity score in the [0, 1] range. Uses the global config value if not set.', ARGUMENT_TYPE.NUMBER, false, false, ''),
new SlashCommandNamedArgument('count', 'Maximum number of query results to return.', ARGUMENT_TYPE.NUMBER, false, false, ''),
new SlashCommandNamedArgument('source', 'Optional filter for the attachments by source.', ARGUMENT_TYPE.STRING, false, false, '', ['global', 'character', 'chat']),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'How you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'object',
enumList: [
new SlashCommandEnumValue('chunks', 'Return the actual content chunks', enumTypes.enum, '{}'),
...slashCommandReturnHelper.enumList({ allowObject: true })
],
forceEnum: true,
})
],
unnamedArgumentList: [
new SlashCommandArgument('Query to search by.', ARGUMENT_TYPE.STRING, true, false),

View File

@@ -81,6 +81,7 @@ import { t } from './i18n.js';
export {
selected_group,
openGroupId,
is_group_automode_enabled,
hideMutedSprites,
is_group_generating,
@@ -1367,6 +1368,15 @@ function getGroupCharacterBlock(character) {
template.find('.ch_fav').val(isFav);
template.toggleClass('is_fav', isFav);
const auxFieldName = power_user.aux_field || 'character_version';
const auxFieldValue = (character.data && character.data[auxFieldName]) || '';
if (auxFieldValue) {
template.find('.character_version').text(auxFieldValue);
}
else {
template.find('.character_version').hide();
}
let queuePosition = groupChatQueueOrder.get(character.avatar);
if (queuePosition) {
template.find('.queue_position').text(queuePosition);

View File

@@ -26,6 +26,12 @@ Handlebars.registerHelper('helperMissing', function () {
* @typedef {(nonce: string) => string} MacroFunction
*/
/**
* @typedef {Object} CustomMacro
* @property {string} key - Macro name (key)
* @property {string} description - Optional description of the macro
*/
export class MacrosParser {
/**
* A map of registered macros.
@@ -33,12 +39,29 @@ export class MacrosParser {
*/
static #macros = new Map();
/**
* A map of macro descriptions.
* @type {Map<string, string>}
*/
static #descriptions = new Map();
/**
* Returns an iterator over all registered macros.
* @returns {IterableIterator<CustomMacro>}
*/
static [Symbol.iterator] = function* () {
for (const macro of MacrosParser.#macros.keys()) {
yield { key: macro, description: MacrosParser.#descriptions.get(macro) };
}
};
/**
* Registers a global macro that can be used anywhere where substitution is allowed.
* @param {string} key Macro name (key)
* @param {string|MacroFunction} value A string or a function that returns a string
* @param {string} [description] Optional description of the macro
*/
static registerMacro(key, value) {
static registerMacro(key, value, description = '') {
if (typeof key !== 'string') {
throw new Error('Macro key must be a string');
}
@@ -64,6 +87,10 @@ export class MacrosParser {
}
this.#macros.set(key, value);
if (typeof description === 'string' && description) {
this.#descriptions.set(key, description);
}
}
/**
@@ -88,6 +115,8 @@ export class MacrosParser {
if (!deleted) {
console.warn(`Macro ${key} was not registered`);
}
this.#descriptions.delete(key);
}
/**
@@ -202,10 +231,19 @@ export function getLastMessageId({ exclude_swipe_in_propress = true, filter = nu
* @returns {number|null} The ID of the first message in the context
*/
function getFirstIncludedMessageId() {
const index = Number(document.querySelector('.lastInContext')?.getAttribute('mesid'));
return chat_metadata['lastInContextMessageId'];
}
if (!isNaN(index) && index >= 0) {
return index;
/**
* Returns the ID of the first displayed message in the chat.
*
* @returns {number|null} The ID of the first displayed message
*/
function getFirstDisplayedMessageId() {
const mesId = Number(document.querySelector('#chat .mes')?.getAttribute('mesid'));
if (!isNaN(mesId) && mesId >= 0) {
return mesId;
}
return null;
@@ -467,6 +505,7 @@ export function evaluateMacros(content, env, postProcessFn) {
{ regex: /{{lastUserMessage}}/gi, replace: () => getLastUserMessage() },
{ regex: /{{lastCharMessage}}/gi, replace: () => getLastCharMessage() },
{ regex: /{{firstIncludedMessageId}}/gi, replace: () => String(getFirstIncludedMessageId() ?? '') },
{ regex: /{{firstDisplayedMessageId}}/gi, replace: () => String(getFirstDisplayedMessageId() ?? '') },
{ regex: /{{lastSwipeId}}/gi, replace: () => String(getLastSwipeId() ?? '') },
{ regex: /{{currentSwipeId}}/gi, replace: () => String(getCurrentSwipeId() ?? '') },
{ regex: /{{reverse:(.+?)}}/gi, replace: (_, str) => Array.from(str).reverse().join('') },

View File

@@ -1922,7 +1922,7 @@ async function sendOpenAIRequest(type, messages, signal) {
}
// Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite
if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE].includes(oai_settings.chat_completion_source)) {
if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK].includes(oai_settings.chat_completion_source)) {
await validateReverseProxy();
generate_data['reverse_proxy'] = oai_settings.reverse_proxy;
generate_data['proxy_password'] = oai_settings.proxy_password;
@@ -2030,6 +2030,16 @@ async function sendOpenAIRequest(type, messages, signal) {
// https://api-docs.deepseek.com/api/create-chat-completion
if (isDeepSeek) {
generate_data.top_p = generate_data.top_p || Number.EPSILON;
if (generate_data.model.endsWith('-reasoner')) {
delete generate_data.top_p;
delete generate_data.temperature;
delete generate_data.frequency_penalty;
delete generate_data.presence_penalty;
delete generate_data.top_logprobs;
delete generate_data.logprobs;
delete generate_data.logit_bias;
}
}
if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano) && oai_settings.seed >= 0) {
@@ -2085,6 +2095,7 @@ async function sendOpenAIRequest(type, messages, signal) {
let text = '';
const swipes = [];
const toolCalls = [];
const state = {};
while (true) {
const { done, value } = await reader.read();
if (done) return;
@@ -2095,9 +2106,9 @@ async function sendOpenAIRequest(type, messages, signal) {
if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {
const swipeIndex = parsed.choices[0].index - 1;
swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed);
swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed, state);
} else {
text += getStreamingReply(parsed);
text += getStreamingReply(parsed, state);
}
ToolManager.parseToolCalls(toolCalls, parsed);
@@ -2129,14 +2140,27 @@ async function sendOpenAIRequest(type, messages, signal) {
}
}
function getStreamingReply(data) {
/**
* Extracts the reply from the response data from a chat completions-like source
* @param {object} data Response data from the chat completions-like source
* @param {object} state Additional state to keep track of
* @returns {string} The reply extracted from the response data
*/
function getStreamingReply(data, state) {
if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
return data?.delta?.text || '';
} else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
return data?.candidates?.[0]?.content?.parts?.filter(x => oai_settings.show_thoughts || !x.thought)?.map(x => x.text)?.filter(x => x)?.join('\n\n') || '';
} else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
} else {
} else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
const hadThoughts = state.hadThoughts;
const thoughts = data.choices?.filter(x => oai_settings.show_thoughts || !x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '';
const content = data.choices?.[0]?.delta?.content || '';
state.hadThoughts = !!thoughts;
const separator = hadThoughts && !thoughts ? '\n\n' : '';
return [thoughts, separator, content].filter(x => x).join('\n\n');
} else {
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
}
}
@@ -3346,7 +3370,7 @@ async function getStatusOpen() {
chat_completion_source: oai_settings.chat_completion_source,
};
if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE].includes(oai_settings.chat_completion_source)) {
if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK].includes(oai_settings.chat_completion_source)) {
await validateReverseProxy();
}
@@ -3971,6 +3995,8 @@ function onSettingsPresetChange() {
settings: oai_settings,
savePreset: saveOpenAIPreset,
}).finally(r => {
$('.model_custom_select').empty();
for (const [key, [selector, setting, isCheckbox]] of Object.entries(settingsToUpdate)) {
if (preset[key] !== undefined) {
if (isCheckbox) {
@@ -4202,7 +4228,7 @@ async function onModelChange() {
$('#openai_max_context').attr('max', max_32k);
} else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206')) {
$('#openai_max_context').attr('max', max_2mil);
} else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp')) {
} else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp') || value.includes('gemini-2.0-flash-thinking-exp')) {
$('#openai_max_context').attr('max', max_1mil);
} else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {
$('#openai_max_context').attr('max', max_32k);
@@ -4486,7 +4512,7 @@ async function onModelChange() {
if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
} else if (oai_settings.deepseek_model == 'deepseek-chat') {
} else if (['deepseek-reasoner', 'deepseek-chat'].includes(oai_settings.deepseek_model)) {
$('#openai_max_context').attr('max', max_64k);
} else if (oai_settings.deepseek_model == 'deepseek-coder') {
$('#openai_max_context').attr('max', max_16k);
@@ -4723,7 +4749,7 @@ async function onConnectButtonClick(e) {
await writeSecret(SECRET_KEYS.DEEPSEEK, api_key_deepseek);
}
if (!secret_state[SECRET_KEYS.DEEPSEEK]) {
if (!secret_state[SECRET_KEYS.DEEPSEEK] && !oai_settings.reverse_proxy) {
console.log('No secret key saved for DeepSeek');
return;
}
@@ -4899,6 +4925,8 @@ export function isImageInliningSupported() {
const visionSupportedModels = [
'gpt-4-vision',
'gemini-2.0-flash-thinking-exp-1219',
'gemini-2.0-flash-thinking-exp-01-21',
'gemini-2.0-flash-thinking-exp',
'gemini-2.0-flash-exp',
'gemini-1.5-flash',
'gemini-1.5-flash-latest',
@@ -5489,8 +5517,8 @@ export function initOpenAI() {
if (!isMobile()) {
$('#model_openrouter_select').select2({
placeholder: 'Select a model',
searchInputPlaceholder: 'Search models...',
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getOpenRouterModelTemplate,

View File

@@ -2534,7 +2534,7 @@ async function loadUntilMesId(mesId) {
let target;
while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) {
showMoreMessages();
await showMoreMessages();
await delay(1);
target = $('#chat').find(`.mes[mesid=${mesId}]`);
@@ -3962,6 +3962,95 @@ $(document).ready(() => {
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'css-var',
/** @param {{to: string, varname: string }} args @param {string} value @returns {string} */
callback: (args, value) => {
// Map enum to target selector
const targetSelector = {
chat: '#chat',
background: '#bg1',
gallery: '#gallery',
zoomedAvatar: 'div.zoomed_avatar',
}[args.to || 'chat'];
if (!targetSelector) {
toastr.error(`Invalid target: ${args.to}`);
return;
}
if (!args.varname) {
toastr.error('CSS variable name is required');
return;
}
if (!args.varname.startsWith('--')) {
toastr.error('CSS variable names must start with "--"');
return;
}
const elements = document.querySelectorAll(targetSelector);
if (elements.length === 0) {
toastr.error(`No elements found for ${args.to ?? 'chat'} with selector "${targetSelector}"`);
return;
}
elements.forEach(element => {
element.style.setProperty(args.varname, value);
});
console.info(`Set CSS variable "${args.varname}" to "${value}" on "${targetSelector}"`);
},
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'varname',
description: 'CSS variable name (starting with double dashes)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
}),
SlashCommandNamedArgument.fromProps({
name: 'to',
description: 'The target element to which the CSS variable will be applied',
typeList: [ARGUMENT_TYPE.STRING],
enumList: [
new SlashCommandEnumValue('chat', null, enumTypes.enum, enumIcons.message),
new SlashCommandEnumValue('background', null, enumTypes.enum, enumIcons.image),
new SlashCommandEnumValue('zoomedAvatar', null, enumTypes.enum, enumIcons.character),
new SlashCommandEnumValue('gallery', null, enumTypes.enum, enumIcons.image),
],
defaultValue: 'chat',
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'CSS variable value',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
}),
],
helpString: `
<div>
Sets a CSS variable to a specified value on a target element.
<br />
Only setting of variable names is supported. They have to be prefixed with double dashes ("--exampleVar").
Setting actual CSS properties is not supported. Custom CSS in the theme settings can be used for that.
<br /><br />
<b>This value will be gone after a page reload!</b>
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/css-var varname="--SmartThemeBodyColor" #ff0000</code></pre>
Sets the text color of the chat to red
</li>
<li>
<pre><code>/css-var to=zoomedAvatar varname="--SmartThemeBlurStrength" 0</code></pre>
Remove the blur from the zoomed avatar
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'movingui',
callback: setmovingUIPreset,

View File

@@ -39,6 +39,7 @@ import {
setCharacterName,
setExtensionPrompt,
setUserName,
showMoreMessages,
stopGeneration,
substituteParams,
system_avatar,
@@ -1964,6 +1965,39 @@ export function initDefaultSlashCommands() {
returns: ARGUMENT_TYPE.BOOLEAN,
helpString: 'Returns true if the current device is a mobile device, false otherwise. Equivalent to <code>{{isMobile}}</code> macro.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'chat-render',
helpString: 'Renders a specified number of messages into the chat window. Displays all messages if no argument is provided.',
callback: async (args, number) => {
await showMoreMessages(number && !isNaN(Number(number)) ? Number(number) : Number.MAX_SAFE_INTEGER);
if (isTrueBoolean(String(args?.scroll ?? ''))) {
$('#chat').scrollTop(0);
}
return '';
},
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'scroll',
description: 'scroll to the top after rendering',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'false',
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
unnamedArgumentList: [
new SlashCommandArgument(
'number of messages', [ARGUMENT_TYPE.NUMBER], false,
),
],
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'chat-reload',
helpString: 'Reloads the current chat.',
callback: async () => {
await reloadCurrentChat();
return '';
},
}));
registerVariableCommands();
}

View File

@@ -37,6 +37,7 @@ export const enumIcons = {
voice: '🎤',
server: '🖥️',
popup: '🗔',
image: '🖼️',
true: '✔️',
false: '❌',

View File

@@ -22,6 +22,8 @@ import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js';
import { SlashCommandDebugController } from './SlashCommandDebugController.js';
import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js';
import { SlashCommandBreak } from './SlashCommandBreak.js';
import { MacrosParser } from '../macros.js';
import { t } from '../i18n.js';
/** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */
/** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */
@@ -494,6 +496,10 @@ export class SlashCommandParser {
li.querySelector('tt').textContent,
(li.querySelector('tt').remove(),li.innerHTML),
));
for (const macro of MacrosParser) {
if (options.find(it => it.name === macro.key)) continue;
options.push(new MacroAutoCompleteOption(macro.key, `{{${macro.key}}}`, macro.description || t`No description provided`));
}
const result = new AutoCompleteNameResult(
macro.name,
macro.start + 2,

View File

@@ -220,6 +220,21 @@ async function* parseStreamData(json) {
}
return;
}
else if (typeof json.choices[0].delta.reasoning_content === 'string' && json.choices[0].delta.reasoning_content.length > 0) {
for (let j = 0; j < json.choices[0].delta.reasoning_content.length; j++) {
const str = json.choices[0].delta.reasoning_content[j];
const isLastSymbol = j === json.choices[0].delta.reasoning_content.length - 1;
const choiceClone = structuredClone(json.choices[0]);
choiceClone.delta.reasoning_content = str;
choiceClone.delta.content = isLastSymbol ? choiceClone.delta.content : '';
const choices = [choiceClone];
yield {
data: { ...json, choices },
chunk: str,
};
}
return;
}
else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {
for (let j = 0; j < json.choices[0].delta.content.length; j++) {
const str = json.choices[0].delta.content[j];

View File

@@ -12,6 +12,7 @@ import {
extension_prompts,
Generate,
generateQuietPrompt,
getCharacters,
getCurrentChatId,
getRequestHeaders,
getThumbnailUrl,
@@ -55,7 +56,7 @@ import { MacrosParser } from './macros.js';
import { oai_settings } from './openai.js';
import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
import { power_user, registerDebugFunction } from './power-user.js';
import { isMobile, shouldSendOnEnter } from './RossAscends-mods.js';
import { humanizedDateTime, isMobile, shouldSendOnEnter } from './RossAscends-mods.js';
import { ScraperManager } from './scrapers.js';
import { executeSlashCommands, executeSlashCommandsWithOptions, registerSlashCommand } from './slash-commands.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -65,7 +66,7 @@ import { tag_map, tags } from './tags.js';
import { textgenerationwebui_settings } from './textgen-settings.js';
import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
import { ToolManager } from './tool-calling.js';
import { timestampToMoment } from './utils.js';
import { timestampToMoment, uuidv4 } from './utils.js';
export function getContext() {
return {
@@ -167,6 +168,9 @@ export function getContext() {
chatCompletionSettings: oai_settings,
textCompletionSettings: textgenerationwebui_settings,
powerUserSettings: power_user,
getCharacters,
uuidv4,
humanizedDateTime,
};
}

View File

@@ -196,9 +196,10 @@ function filterByTagState(entities, { globalDisplayFilters = false, subForEntity
return false;
}
// Hide folders that have 0 visible sub entities after the first filtering round
// Hide folders that have 0 visible sub entities after the first filtering round, unless we are inside a search via search term.
// Then we want to display folders that mach too, even if the chars inside don't match the search.
if (entity.type === 'tag') {
return entity.entities.length > 0;
return entity.entities.length > 0 || entitiesFilter.getFilterData(FILTER_TYPES.SEARCH);
}
return true;

View File

@@ -1,3 +1,9 @@
<div>
<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 data-type="assistant_note">
<div>
<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>
<span>Click the button to save it as a file.</span>
</div>
<div class="assistant_note_export menu_button menu_button_icon" title="Export as JSONL">
<i class="fa-solid fa-file-export"></i>
</div>
</div>

View File

@@ -7,7 +7,7 @@
<li><span data-i18n="char_import_2">Chub Lorebook (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>lorebooks/bartleby/example-lorebook</tt></li>
<li><span data-i18n="char_import_3">JanitorAI Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li>
<li><span data-i18n="char_import_4">Pygmalion.chat Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li>
<li><span data-i18n="char_import_5">AICharacterCard.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li>
<li><span data-i18n="char_import_5">AICharacterCards.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li>
<li><span data-i18n="char_import_6">Direct PNG Link (refer to</span> <code>config.yaml</code><span data-i18n="char_import_7"> for allowed hosts)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://files.catbox.moe/notarealfile.png</tt></li>
<li><span data-i18n="char_import_8">RisuRealm Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://realm.risuai.net/character/3ca54c71-6efe-46a2-b9d0-4f62df23d712</tt></li>
</ul>

View File

@@ -139,7 +139,7 @@
<div class="">{{finalPromptTokens}}</div>
</div>
<div class="flex-container wide100p">
<div class="flex1"><span data-i18n="Max Context">Max Context</span>&nbsp;<small data-i18n="(Context Size - Response Length)">(Context Size - Response Length)</small><span data-i18n=":">:</span></div>
<div class="flex1"><span data-i18n="Max Context">Max Context</span>&nbsp;<small data-i18n="(Context Size - Response Length)">(Context Size - Response Length)</small>:</div>
<div class="">{{thisPrompt_max_context}}</div>
</div>
</div>

View File

@@ -28,7 +28,8 @@
<li><tt>&lcub;&lcub;lastUserMessage&rcub;&rcub;</tt> <span data-i18n="help_macros_lastUser">the text of the latest user chat message.</span></li>
<li><tt>&lcub;&lcub;lastCharMessage&rcub;&rcub;</tt> <span data-i18n="help_macros_lastChar">the text of the latest character chat message.</span></li>
<li><tt>&lcub;&lcub;lastMessageId&rcub;&rcub;</tt> <span data-i18n="help_macros_21">index # of the latest chat message. Useful for slash command batching.</span></li>
<li><tt>&lcub;&lcub;firstIncludedMessageId&rcub;&rcub;</tt> <span data-i18n="help_macros_22">the ID of the first message included in the context. Requires generation to be ran at least once in the current session.</span></li>
<li><tt>&lcub;&lcub;firstIncludedMessageId&rcub;&rcub;</tt> <span data-i18n="help_macros_22">the ID of the first message included in the context. Requires generation to be run at least once in the current session. Will only be updated on generation.</span></li>
<li><tt>&lcub;&lcub;firstDisplayedMessageId&rcub;&rcub;</tt> <span data-i18n="help_macros_firstDisplayedMessageId">the ID of the first message loaded into the visible chat.</span></li>
<li><tt>&lcub;&lcub;currentSwipeId&rcub;&rcub;</tt> <span data-i18n="help_macros_23">the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li>
<li><tt>&lcub;&lcub;lastSwipeId&rcub;&rcub;</tt> <span data-i18n="help_macros_24">the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li>
<li><tt>&lcub;&lcub;reverse:(content)&rcub;&rcub;</tt> <span data-i18n="help_macros_reverse">reverses the content of the macro.</span></li>

View File

@@ -5,6 +5,7 @@ import { textgenerationwebui_settings as textgen_settings, textgen_types } from
import { tokenizers } from './tokenizers.js';
import { renderTemplateAsync } from './templates.js';
import { POPUP_TYPE, callGenericPopup } from './popup.js';
import { t } from './i18n.js';
let mancerModels = [];
let togetherModels = [];
@@ -318,8 +319,6 @@ export async function loadFeatherlessModels(data) {
return;
}
// Sort the data by model id (default A-Z)
data.sort((a, b) => a.id.localeCompare(b.id));
originalModels = data; // Store the original data for search
featherlessModels = data;
@@ -333,10 +332,8 @@ export async function loadFeatherlessModels(data) {
// Retrieve the stored number of items per page or default to 10
const perPage = Number(localStorage.getItem(storageKey)) || 10;
// Initialize pagination with the full set of models
const currentModelIndex = data.findIndex(x => x.id === textgen_settings.featherless_model);
featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;
setupPagination(originalModels, perPage);
// Initialize pagination
applyFiltersAndSort();
// Function to set up pagination (also used for filtered results)
function setupPagination(models, perPage, pageNumber = featherlessCurrentPage) {
@@ -382,7 +379,7 @@ export async function loadFeatherlessModels(data) {
const dateAddedDiv = document.createElement('div');
dateAddedDiv.classList.add('model-date-added');
dateAddedDiv.textContent = `Added On: ${new Date(model.updated_at).toLocaleDateString()}`;
dateAddedDiv.textContent = `Added On: ${new Date(model.created * 1000).toLocaleDateString()}`;
detailsContainer.appendChild(modelClassDiv);
detailsContainer.appendChild(contextLengthDiv);
@@ -471,6 +468,7 @@ export async function loadFeatherlessModels(data) {
featherlessTop = await fetchFeatherlessStats();
}
const featherlessIds = featherlessTop.map(stat => stat.id);
if (selectedCategory === 'New') {
featherlessNew = await fetchFeatherlessNew();
}
@@ -492,7 +490,7 @@ export async function loadFeatherlessModels(data) {
return matchesSearch && matchesClass && matchesNew;
}
else {
return matchesSearch;
return matchesSearch && matchesClass;
}
});
@@ -501,11 +499,14 @@ export async function loadFeatherlessModels(data) {
} else if (selectedSortOrder === 'desc') {
filteredModels.sort((a, b) => b.id.localeCompare(a.id));
} else if (selectedSortOrder === 'date_asc') {
filteredModels.sort((a, b) => a.updated_at.localeCompare(b.updated_at));
filteredModels.sort((a, b) => a.created - b.created);
} else if (selectedSortOrder === 'date_desc') {
filteredModels.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
filteredModels.sort((a, b) => b.created - a.created);
}
const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model);
featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;
setupPagination(filteredModels, Number(localStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);
}
@@ -527,7 +528,7 @@ async function fetchFeatherlessStats() {
}
async function fetchFeatherlessNew() {
const response = await fetch('https://api.featherless.ai/feather/models?sort=-created_at&perPage=10');
const response = await fetch('https://api.featherless.ai/feather/models?sort=-created_at&perPage=20');
const data = await response.json();
return data.items;
}
@@ -936,71 +937,71 @@ export function initTextGenModels() {
if (!isMobile()) {
$('#mancer_model').select2({
placeholder: 'Select a model',
searchInputPlaceholder: 'Search models...',
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getMancerModelTemplate,
});
$('#model_togetherai_select').select2({
placeholder: 'Select a model',
searchInputPlaceholder: 'Search models...',
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getTogetherModelTemplate,
});
$('#ollama_model').select2({
placeholder: 'Select a model',
searchInputPlaceholder: 'Search models...',
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
});
$('#tabby_model').select2({
placeholder: '[Currently loaded]',
searchInputPlaceholder: 'Search models...',
placeholder: t`[Currently loaded]`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
allowClear: true,
});
$('#model_infermaticai_select').select2({
placeholder: 'Select a model',
searchInputPlaceholder: 'Search models...',
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getInfermaticAIModelTemplate,
});
$('#model_dreamgen_select').select2({
placeholder: 'Select a model',
searchInputPlaceholder: 'Search models...',
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getDreamGenModelTemplate,
});
$('#openrouter_model').select2({
placeholder: 'Select a model',
searchInputPlaceholder: 'Search models...',
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getOpenRouterModelTemplate,
});
$('#vllm_model').select2({
placeholder: 'Select a model',
searchInputPlaceholder: 'Search models...',
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getVllmModelTemplate,
});
$('#aphrodite_model').select2({
placeholder: 'Select a model',
searchInputPlaceholder: 'Search models...',
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getAphroditeModelTemplate,
});
providersSelect.select2({
sorter: data => data.sort((a, b) => a.text.localeCompare(b.text)),
placeholder: 'Select providers. No selection = all providers.',
searchInputPlaceholder: 'Search providers...',
placeholder: t`Select providers. No selection = all providers.`,
searchInputPlaceholder: t`Search providers...`,
searchInputCssClass: 'text_pole',
width: '100%',
closeOnSelect: false,

View File

@@ -1231,7 +1231,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'top_p': settings.top_p,
'typical_p': settings.typical_p,
'typical': settings.typical_p,
'sampler_seed': settings.seed,
'sampler_seed': settings.seed >= 0 ? settings.seed : undefined,
'min_p': settings.min_p,
'repetition_penalty': settings.rep_pen,
'frequency_penalty': settings.freq_pen,
@@ -1294,7 +1294,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'temperature_last': (settings.type === OOBA || settings.type === APHRODITE || settings.type == TABBY) ? settings.temperature_last : undefined,
'speculative_ngram': settings.type === TABBY ? settings.speculative_ngram : undefined,
'do_sample': settings.type === OOBA ? settings.do_sample : undefined,
'seed': settings.seed,
'seed': settings.seed >= 0 ? settings.seed : undefined,
'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
'grammar_string': settings.grammar_string,

View File

@@ -67,6 +67,16 @@ export function escapeHtml(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/**
* Make string safe for use as a CSS selector.
* @param {string} str String to sanitize
* @param {string} replacement Replacement for invalid characters
* @returns {string} Sanitized string
*/
export function sanitizeSelector(str, replacement = '_') {
return String(str).replace(/[^a-z0-9_-]/ig, replacement);
}
export function isValidUrl(value) {
try {
new URL(value);
@@ -381,6 +391,26 @@ export function getStringHash(str, seed = 0) {
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}
/**
* Copy text to clipboard. Use navigator.clipboard.writeText if available, otherwise use document.execCommand.
* @param {string} text - The text to copy to the clipboard.
* @returns {Promise<void>} A promise that resolves when the text has been copied to the clipboard.
*/
export function copyText(text) {
if (navigator.clipboard) {
return navigator.clipboard.writeText(text);
}
const parent = document.querySelector('dialog[open]:last-of-type') ?? document.body;
const textArea = document.createElement('textarea');
textArea.value = text;
parent.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
parent.removeChild(textArea);
}
/**
* Map of debounced functions to their timers.
* Weak map is used to avoid memory leaks.

View File

@@ -20,6 +20,7 @@ import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
import { StructuredCloneMap } from './util/StructuredCloneMap.js';
import { renderTemplateAsync } from './templates.js';
import { t } from './i18n.js';
export const world_info_insertion_strategy = {
evenly: 0,
@@ -909,21 +910,21 @@ function registerWorldInfoSlashCommands() {
async function getEntriesFromFile(file) {
if (!file || !world_names.includes(file)) {
toastr.warning('Valid World Info file name is required');
toastr.warning(t`Valid World Info file name is required`);
return '';
}
const data = await loadWorldInfo(file);
if (!data || !('entries' in data)) {
toastr.warning('World Info file has an invalid format');
toastr.warning(t`World Info file has an invalid format`);
return '';
}
const entries = Object.values(data.entries);
if (!entries || entries.length === 0) {
toastr.warning('World Info file has no entries');
toastr.warning(t`World Info file has no entries`);
return '';
}
@@ -951,7 +952,7 @@ function registerWorldInfoSlashCommands() {
name = String(name ?? '') || context.characters[context.characterId]?.avatar || null;
const character = findChar({ name });
if (!character) {
toastr.error('Character not found.');
toastr.error(t`Character not found.`);
return '';
}
const books = [];
@@ -977,7 +978,7 @@ function registerWorldInfoSlashCommands() {
const chatId = getCurrentChatId();
if (!chatId) {
toastr.warning('Open a chat to get a name of the chat-bound lorebook');
toastr.warning(t`Open a chat to get a name of the chat-bound lorebook`);
return '';
}
@@ -4773,7 +4774,7 @@ export async function importEmbeddedWorldInfo(skipPopup = false) {
const bookName = characters[chid]?.data?.character_book?.name || `${characters[chid]?.name}'s Lorebook`;
if (!skipPopup) {
const confirmation = await Popup.show.confirm(`Are you sure you want to import "${bookName}"?`, world_names.includes(bookName) ? 'It will overwrite the World/Lorebook with the same name.' : '');
const confirmation = await Popup.show.confirm(t`Are you sure you want to import '${bookName}'?`, world_names.includes(bookName) ? t`It will overwrite the World/Lorebook with the same name.` : '');
if (!confirmation) {
return;
}
@@ -4785,7 +4786,7 @@ export async function importEmbeddedWorldInfo(skipPopup = false) {
await updateWorldInfoList();
$('#character_world').val(bookName).trigger('change');
toastr.success(`The world "${bookName}" has been imported and linked to the character successfully.`, 'World/Lorebook imported');
toastr.success(t`The world '${bookName}' has been imported and linked to the character successfully.`, t`World/Lorebook imported`);
const newIndex = world_names.indexOf(bookName);
if (newIndex >= 0) {
@@ -4813,9 +4814,9 @@ export function onWorldInfoChange(args, text) {
if (selected_world_info.includes(name)) {
selected_world_info.splice(selected_world_info.indexOf(name), 1);
wiElement.prop('selected', false);
if (!silent) toastr.success(`Deactivated world: ${name}`);
if (!silent) toastr.success(t`Deactivated world: ${name}`);
} else {
if (!silent) toastr.error(`World was not active: ${name}`);
if (!silent) toastr.error(t`World was not active: ${name}`);
}
break;
}
@@ -4823,11 +4824,11 @@ export function onWorldInfoChange(args, text) {
if (selected_world_info.includes(name)) {
selected_world_info.splice(selected_world_info.indexOf(name), 1);
wiElement.prop('selected', false);
if (!silent) toastr.success(`Deactivated world: ${name}`);
if (!silent) toastr.success(t`Deactivated world: ${name}`);
} else {
selected_world_info.push(name);
wiElement.prop('selected', true);
if (!silent) toastr.success(`Activated world: ${name}`);
if (!silent) toastr.success(t`Activated world: ${name}`);
}
break;
}
@@ -4835,16 +4836,16 @@ export function onWorldInfoChange(args, text) {
default: {
selected_world_info.push(name);
wiElement.prop('selected', true);
if (!silent) toastr.success(`Activated world: ${name}`);
if (!silent) toastr.success(t`Activated world: ${name}`);
}
}
} else {
if (!silent) toastr.error(`No world found named: ${worldName}`);
if (!silent) toastr.error(t`No world found named: ${worldName}`);
}
});
$('#world_info').trigger('change');
} else { // if no args, unset all worlds
if (!silent) toastr.success('Deactivated all worlds');
if (!silent) toastr.success(t`Deactivated all worlds`);
selected_world_info = [];
$('#world_info').val(null).trigger('change');
}
@@ -4860,7 +4861,7 @@ export function onWorldInfoChange(args, text) {
} else {
const wiElement = getWIElement(existingWorldName);
wiElement.prop('selected', false);
toastr.error(`The world with ${existingWorldName} is invalid or corrupted.`);
toastr.error(t`The world with ${existingWorldName} is invalid or corrupted.`);
}
});
}
@@ -4892,7 +4893,7 @@ export async function importWorldInfo(file) {
}
if (jsonData === undefined || jsonData === null) {
toastr.error(`File is not valid: ${file.name}`);
toastr.error(t`File is not valid: ${file.name}`);
return;
}
@@ -5038,7 +5039,7 @@ jQuery(() => {
$('#world_create_button').on('click', async () => {
const tempName = getFreeWorldName();
const finalName = await Popup.show.input('Create a new World Info', 'Enter a name for the new file:', tempName);
const finalName = await Popup.show.input(t`Create a new World Info`, t`Enter a name for the new file:`, tempName);
if (finalName) {
await createNewWorldInfo(finalName, { interactive: true });