Compare commits
9 Commits
c2256c2ac7
...
ef5d505de3
Author | SHA1 | Date |
---|---|---|
Cohee | ef5d505de3 | |
Cohee | 842b463e60 | |
Cohee | 5992c34fb5 | |
Cohee | bae74fbbd7 | |
Cohee | 4264d170e2 | |
Cohee | 274359d1ec | |
Cohee | dac89a87b7 | |
Cohee | fb5d998cd0 | |
Cohee | ca89be8930 |
|
@ -18,7 +18,7 @@
|
|||
"input_suffix": "<|eot_id|>",
|
||||
"system_suffix": "<|eot_id|>",
|
||||
"user_alignment_message": "",
|
||||
"system_same_as_user": false,
|
||||
"system_same_as_user": true,
|
||||
"last_system_sequence": "",
|
||||
"name": "Llama 3 Instruct"
|
||||
}
|
||||
|
|
|
@ -6773,8 +6773,9 @@ function select_rm_info(type, charId, previousCharId = null) {
|
|||
importFlashTimeout = setTimeout(function () {
|
||||
if (type === 'char_import' || type === 'char_create') {
|
||||
// Find the page at which the character is located
|
||||
const avatarFileName = `${charId}.png`;
|
||||
const charData = getEntitiesList({ doFilter: true });
|
||||
const charIndex = charData.findIndex((x) => x?.item?.avatar?.startsWith(charId));
|
||||
const charIndex = charData.findIndex((x) => x?.item?.avatar?.startsWith(avatarFileName));
|
||||
|
||||
if (charIndex === -1) {
|
||||
console.log(`Could not find character ${charId} in the list`);
|
||||
|
@ -6784,7 +6785,7 @@ function select_rm_info(type, charId, previousCharId = null) {
|
|||
try {
|
||||
const perPage = Number(localStorage.getItem('Characters_PerPage')) || per_page_default;
|
||||
const page = Math.floor(charIndex / perPage) + 1;
|
||||
const selector = `#rm_print_characters_block [title^="${charId}"]`;
|
||||
const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`;
|
||||
$('#rm_print_characters_pagination').pagination('go', page);
|
||||
|
||||
waitUntilCondition(() => document.querySelector(selector) !== null).then(() => {
|
||||
|
|
|
@ -1398,7 +1398,8 @@ class PromptManager {
|
|||
`;
|
||||
|
||||
const rangeBlockDiv = promptManagerDiv.querySelector('.range-block');
|
||||
rangeBlockDiv.insertAdjacentHTML('beforeend', footerHtml);
|
||||
const headerDiv = promptManagerDiv.querySelector('.completion_prompt_manager_header');
|
||||
headerDiv.insertAdjacentHTML('afterend', footerHtml);
|
||||
rangeBlockDiv.querySelector('#prompt-manager-reset-character').addEventListener('click', this.handleCharacterReset);
|
||||
|
||||
const footerDiv = rangeBlockDiv.querySelector(`.${this.configuration.prefix}prompt_manager_footer`);
|
||||
|
@ -1427,7 +1428,12 @@ class PromptManager {
|
|||
|
||||
rangeBlockDiv.insertAdjacentHTML('beforeend', exportPopup);
|
||||
|
||||
let exportPopper = Popper.createPopper(
|
||||
// Destroy previous popper instance if it exists
|
||||
if (this.exportPopper) {
|
||||
this.exportPopper.destroy();
|
||||
}
|
||||
|
||||
this.exportPopper = Popper.createPopper(
|
||||
document.getElementById('prompt-manager-export'),
|
||||
document.getElementById('prompt-manager-export-format-popup'),
|
||||
{ placement: 'bottom' },
|
||||
|
@ -1440,7 +1446,7 @@ class PromptManager {
|
|||
if (show) popup.removeAttribute('data-show');
|
||||
else popup.setAttribute('data-show', '');
|
||||
|
||||
exportPopper.update();
|
||||
this.exportPopper.update();
|
||||
};
|
||||
|
||||
footerDiv.querySelector('#prompt-manager-import').addEventListener('click', this.handleImport);
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
getStringHash,
|
||||
humanFileSize,
|
||||
saveBase64AsFile,
|
||||
extractTextFromOffice,
|
||||
} from './utils.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
||||
|
@ -46,6 +47,12 @@ import { ScraperManager } from './scrapers.js';
|
|||
* @property {string} [text] File text
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {function} ConverterFunction
|
||||
* @param {File} file File object
|
||||
* @returns {Promise<string>} Converted file text
|
||||
*/
|
||||
|
||||
const fileSizeLimit = 1024 * 1024 * 10; // 10 MB
|
||||
const ATTACHMENT_SOURCE = {
|
||||
GLOBAL: 'global',
|
||||
|
@ -53,20 +60,60 @@ const ATTACHMENT_SOURCE = {
|
|||
CHARACTER: 'character',
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {Record<string, ConverterFunction>} File converters
|
||||
*/
|
||||
const converters = {
|
||||
'application/pdf': extractTextFromPDF,
|
||||
'text/html': extractTextFromHTML,
|
||||
'text/markdown': extractTextFromMarkdown,
|
||||
'application/epub+zip': extractTextFromEpub,
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': extractTextFromOffice,
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': extractTextFromOffice,
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': extractTextFromOffice,
|
||||
'application/vnd.oasis.opendocument.text': extractTextFromOffice,
|
||||
'application/vnd.oasis.opendocument.presentation': extractTextFromOffice,
|
||||
'application/vnd.oasis.opendocument.spreadsheet': extractTextFromOffice,
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a matching key in the converters object.
|
||||
* @param {string} type MIME type
|
||||
* @returns {string} Matching key
|
||||
*/
|
||||
function findConverterKey(type) {
|
||||
return Object.keys(converters).find((key) => {
|
||||
// Match exact type
|
||||
if (type === key) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Match wildcards
|
||||
if (key.endsWith('*')) {
|
||||
return type.startsWith(key.substring(0, key.length - 1));
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the file type has a converter function.
|
||||
* @param {string} type MIME type
|
||||
* @returns {boolean} True if the file type is convertible, false otherwise.
|
||||
*/
|
||||
function isConvertible(type) {
|
||||
return Object.keys(converters).includes(type);
|
||||
return Boolean(findConverterKey(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the converter function for a file type.
|
||||
* @param {string} type MIME type
|
||||
* @returns {ConverterFunction} Converter function
|
||||
*/
|
||||
function getConverter(type) {
|
||||
const key = findConverterKey(type);
|
||||
return key && converters[key];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -152,7 +199,7 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
|
|||
|
||||
if (isConvertible(file.type)) {
|
||||
try {
|
||||
const converter = converters[file.type];
|
||||
const converter = getConverter(file.type);
|
||||
const fileText = await converter(file);
|
||||
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
|
||||
} catch (error) {
|
||||
|
@ -584,18 +631,59 @@ async function openFilePopup(attachment) {
|
|||
callGenericPopup(modalTemplate, POPUP_TYPE.TEXT, '', { wide: true, large: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a file attachment in a notepad-like modal.
|
||||
* @param {FileAttachment} attachment Attachment to edit
|
||||
* @param {string} source Attachment source
|
||||
* @param {function} callback Callback function
|
||||
*/
|
||||
async function editAttachment(attachment, source, callback) {
|
||||
const originalFileText = attachment.text || (await getFileAttachment(attachment.url));
|
||||
const template = $(await renderExtensionTemplateAsync('attachments', 'notepad'));
|
||||
|
||||
let editedFileText = originalFileText;
|
||||
template.find('[name="notepadFileContent"]').val(editedFileText).on('input', function () {
|
||||
editedFileText = String($(this).val());
|
||||
});
|
||||
|
||||
let editedFileName = attachment.name;
|
||||
template.find('[name="notepadFileName"]').val(editedFileName).on('input', function () {
|
||||
editedFileName = String($(this).val());
|
||||
});
|
||||
|
||||
const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { wide: true, large: true, okButton: 'Save', cancelButton: 'Cancel' });
|
||||
|
||||
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (editedFileText === originalFileText && editedFileName === attachment.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nullCallback = () => { };
|
||||
await deleteAttachment(attachment, source, nullCallback, false);
|
||||
const file = new File([editedFileText], editedFileName, { type: 'text/plain' });
|
||||
await uploadFileAttachmentToServer(file, source);
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an attachment from the server and the chat.
|
||||
* @param {FileAttachment} attachment Attachment to delete
|
||||
* @param {string} source Source of the attachment
|
||||
* @param {function} callback Callback function
|
||||
* @param {boolean} [confirm=true] If true, show a confirmation dialog
|
||||
* @returns {Promise<void>} A promise that resolves when the attachment is deleted.
|
||||
*/
|
||||
async function deleteAttachment(attachment, source, callback) {
|
||||
const confirm = await callGenericPopup('Are you sure you want to delete this attachment?', POPUP_TYPE.CONFIRM);
|
||||
async function deleteAttachment(attachment, source, callback, confirm = true) {
|
||||
if (confirm) {
|
||||
const result = await callGenericPopup('Are you sure you want to delete this attachment?', POPUP_TYPE.CONFIRM);
|
||||
|
||||
if (confirm !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
return;
|
||||
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ensureAttachmentsExist();
|
||||
|
@ -672,6 +760,7 @@ async function openAttachmentManager() {
|
|||
attachmentTemplate.find('.attachmentListItemSize').text(humanFileSize(attachment.size));
|
||||
attachmentTemplate.find('.attachmentListItemCreated').text(new Date(attachment.created).toLocaleString());
|
||||
attachmentTemplate.find('.viewAttachmentButton').on('click', () => openFilePopup(attachment));
|
||||
attachmentTemplate.find('.editAttachmentButton').on('click', () => editAttachment(attachment, source, renderAttachments));
|
||||
attachmentTemplate.find('.deleteAttachmentButton').on('click', () => deleteAttachment(attachment, source, renderAttachments));
|
||||
template.find(sources[source]).append(attachmentTemplate);
|
||||
}
|
||||
|
@ -748,7 +837,7 @@ async function openAttachmentManager() {
|
|||
}
|
||||
|
||||
async function renderAttachments() {
|
||||
/** @type {FileAttachment[]} */
|
||||
/** @type {FileAttachment[]} */
|
||||
const globalAttachments = extension_settings.attachments ?? [];
|
||||
/** @type {FileAttachment[]} */
|
||||
const chatAttachments = chat_metadata.attachments ?? [];
|
||||
|
@ -855,7 +944,7 @@ export async function uploadFileAttachmentToServer(file, target) {
|
|||
|
||||
if (isConvertible(file.type)) {
|
||||
try {
|
||||
const converter = converters[file.type];
|
||||
const converter = getConverter(file.type);
|
||||
const fileText = await converter(file);
|
||||
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
|
||||
} catch (error) {
|
||||
|
@ -950,6 +1039,26 @@ export function getDataBankAttachmentsForSource(source) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a file converter function.
|
||||
* @param {string} mimeType MIME type
|
||||
* @param {ConverterFunction} converter Function to convert file
|
||||
* @returns {void}
|
||||
*/
|
||||
export function registerFileConverter(mimeType, converter) {
|
||||
if (typeof mimeType !== 'string' || typeof converter !== 'function') {
|
||||
console.error('Invalid converter registration');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(converters).includes(mimeType)) {
|
||||
console.error('Converter already registered');
|
||||
return;
|
||||
}
|
||||
|
||||
converters[mimeType] = converter;
|
||||
}
|
||||
|
||||
jQuery(function () {
|
||||
$(document).on('click', '.mes_hide', async function () {
|
||||
const messageBlock = $(this).closest('.mes');
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
<small class="attachmentListItemCreated"></small>
|
||||
<small class="attachmentListItemSize"></small>
|
||||
<div class="viewAttachmentButton right_menu_button fa-solid fa-magnifying-glass" title="View attachment content"></div>
|
||||
<div class="editAttachmentButton right_menu_button fa-solid fa-pencil" title="Edit attachment"></div>
|
||||
<div class="deleteAttachmentButton right_menu_button fa-solid fa-trash" title="Delete attachment"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<div class="flex-container flexFlowColumn height100p">
|
||||
<label for="notepadFileName">
|
||||
File Name
|
||||
</label>
|
||||
<input type="text" class="text_pole" id="notepadFileName" name="notepadFileName" value="" />
|
||||
<labels>
|
||||
File Content
|
||||
</label>
|
||||
<textarea id="notepadFileContent" name="notepadFileContent" class="text_pole textarea_compact monospace flex1" placeholder="Enter your notes here."></textarea>
|
||||
</div>
|
|
@ -53,6 +53,7 @@ const settings = {
|
|||
|
||||
// For files
|
||||
enabled_files: false,
|
||||
translate_files: false,
|
||||
size_threshold: 10,
|
||||
chunk_size: 5000,
|
||||
chunk_count: 2,
|
||||
|
@ -437,6 +438,12 @@ async function retrieveFileChunks(queryText, collectionId) {
|
|||
*/
|
||||
async function vectorizeFile(fileText, fileName, collectionId, chunkSize) {
|
||||
try {
|
||||
if (settings.translate_files && typeof window['translate'] === 'function') {
|
||||
console.log(`Vectors: Translating file ${fileName} to English...`);
|
||||
const translatedText = await window['translate'](fileText, 'en');
|
||||
fileText = translatedText;
|
||||
}
|
||||
|
||||
const toast = toastr.info('Vectorization may take some time, please wait...', `Ingesting file ${fileName}`);
|
||||
const chunks = splitRecursive(fileText, chunkSize);
|
||||
console.debug(`Vectors: Split file ${fileName} into ${chunks.length} chunks`, chunks);
|
||||
|
@ -1121,6 +1128,12 @@ jQuery(async () => {
|
|||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_translate_files').prop('checked', settings.translate_files).on('input', () => {
|
||||
settings.translate_files = !!$('#vectors_translate_files').prop('checked');
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
const validSecret = !!secret_state[SECRET_KEYS.NOMICAI];
|
||||
const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key';
|
||||
$('#api_key_nomicai').attr('placeholder', placeholder);
|
||||
|
|
|
@ -107,6 +107,13 @@
|
|||
</label>
|
||||
|
||||
<div id="vectors_files_settings" class="marginTopBot5">
|
||||
<label class="checkbox_label" for="vectors_translate_files" title="This can help with retrieval accuracy if using embedding models that are trained on English data. Uses the selected API from Chat Translation extension settings.">
|
||||
<input id="vectors_translate_files" type="checkbox" class="checkbox">
|
||||
<span data-i18n="Translate files into English before processing">
|
||||
Translate files into English before processing
|
||||
</span>
|
||||
<i class="fa-solid fa-flask" title="Experimental feature"></i>
|
||||
</label>
|
||||
<div class="flex justifyCenter" title="These settings apply to files attached directly to messages.">
|
||||
<span>Message attachments</span>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId } from '../script.js';
|
||||
import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId, substituteParams } from '../script.js';
|
||||
import { timestampToMoment, isDigitsOnly, getStringHash } from './utils.js';
|
||||
import { textgenerationwebui_banned_in_macros } from './textgen-settings.js';
|
||||
import { replaceInstructMacros } from './instruct-mode.js';
|
||||
|
@ -6,6 +6,12 @@ import { replaceVariableMacros } from './variables.js';
|
|||
|
||||
// Register any macro that you want to leave in the compiled story string
|
||||
Handlebars.registerHelper('trim', () => '{{trim}}');
|
||||
// Catch-all helper for any macro that is not defined for story strings
|
||||
Handlebars.registerHelper('helperMissing', function () {
|
||||
const options = arguments[arguments.length - 1];
|
||||
const macroName = options.name;
|
||||
return substituteParams(`{{${macroName}}}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Gets a hashed id of the current chat from the metadata.
|
||||
|
|
|
@ -77,6 +77,52 @@ export class ScraperManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a text file from a string.
|
||||
* @implements {Scraper}
|
||||
*/
|
||||
class Notepad {
|
||||
constructor() {
|
||||
this.id = 'text';
|
||||
this.name = 'Notepad';
|
||||
this.description = 'Create a text file from scratch.';
|
||||
this.iconClass = 'fa-solid fa-note-sticky';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the scraper is available.
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async isAvailable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a text file from a string.
|
||||
* @returns {Promise<File[]>} File attachments scraped from the text
|
||||
*/
|
||||
async scrape() {
|
||||
const template = $(await renderExtensionTemplateAsync('attachments', 'notepad', {}));
|
||||
let fileName = `Untitled - ${new Date().toLocaleString()}`;
|
||||
let text = '';
|
||||
template.find('input[name="notepadFileName"]').val(fileName).on('input', function () {
|
||||
fileName = String($(this).val()).trim();
|
||||
});
|
||||
template.find('textarea[name="notepadFileContent"]').on('input', function () {
|
||||
text = String($(this).val());
|
||||
});
|
||||
|
||||
const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { wide: true, large: true, okButton: 'Save', cancelButton: 'Cancel' });
|
||||
|
||||
if (!result || text === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = new File([text], `Notepad - ${fileName}.txt`, { type: 'text/plain' });
|
||||
return [file];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrape data from a webpage.
|
||||
* @implements {Scraper}
|
||||
|
@ -179,7 +225,7 @@ class FileScraper {
|
|||
return new Promise(resolve => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = '.txt, .md, .pdf, .html, .htm, .epub';
|
||||
fileInput.accept = '*/*';
|
||||
fileInput.multiple = true;
|
||||
fileInput.onchange = () => resolve(Array.from(fileInput.files));
|
||||
fileInput.click();
|
||||
|
@ -364,6 +410,7 @@ class YouTubeScraper {
|
|||
}
|
||||
|
||||
ScraperManager.registerDataBankScraper(new FileScraper());
|
||||
ScraperManager.registerDataBankScraper(new Notepad());
|
||||
ScraperManager.registerDataBankScraper(new WebScraper());
|
||||
ScraperManager.registerDataBankScraper(new FandomScraper());
|
||||
ScraperManager.registerDataBankScraper(new YouTubeScraper());
|
||||
|
|
|
@ -1355,6 +1355,47 @@ export async function extractTextFromEpub(blob) {
|
|||
return postProcessText(text.join('\n'), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts text from an Office document using the server plugin.
|
||||
* @param {File} blob File to extract text from
|
||||
* @returns {Promise<string>} A promise that resolves to the extracted text.
|
||||
*/
|
||||
export async function extractTextFromOffice(blob) {
|
||||
async function checkPluginAvailability() {
|
||||
try {
|
||||
const result = await fetch('/api/plugins/office/probe', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
|
||||
return result.ok;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const isPluginAvailable = await checkPluginAvailability();
|
||||
|
||||
if (!isPluginAvailable) {
|
||||
throw new Error('Importing Office documents requires a server plugin. Please refer to the documentation for more information.');
|
||||
}
|
||||
|
||||
const base64 = await getBase64Async(blob);
|
||||
|
||||
const response = await fetch('/api/plugins/office/parse', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ data: base64 }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to parse the Office document');
|
||||
}
|
||||
|
||||
const data = await response.text();
|
||||
return postProcessText(data, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a value in an object by a path.
|
||||
* @param {object} obj Object to set value in
|
||||
|
|
Loading…
Reference in New Issue