From 630111c737fdb4eda65f22086c1b79d56e6368f2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 May 2024 13:25:21 +0300 Subject: [PATCH 01/44] Remove imports from embedded styles --- public/scripts/chats.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 3187276d7..4bea9ed58 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -471,6 +471,9 @@ export function decodeStyleTags(text) { const rules = ast?.stylesheet?.rules; if (rules) { for (const rule of rules) { + if (rule.type === 'import') { + rules.splice(rules.indexOf(rule), 1); + } if (rule.type === 'rule') { if (rule.selectors) { From 2c049e561160a93da717d915b6277913eb9ad4ce Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 May 2024 13:25:21 +0300 Subject: [PATCH 02/44] Remove imports from embedded styles --- public/scripts/chats.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 3187276d7..4bea9ed58 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -471,6 +471,9 @@ export function decodeStyleTags(text) { const rules = ast?.stylesheet?.rules; if (rules) { for (const rule of rules) { + if (rule.type === 'import') { + rules.splice(rules.indexOf(rule), 1); + } if (rule.type === 'rule') { if (rule.selectors) { From 1bc45d2869dcd8622053bf883ce8313a65a78745 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 May 2024 13:43:59 +0300 Subject: [PATCH 03/44] Improve external media removal in style blocks --- public/scripts/chats.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 4bea9ed58..84f316e63 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -463,6 +463,7 @@ export function encodeStyleTags(text) { */ export function decodeStyleTags(text) { const styleDecodeRegex = /(.+?)<\/custom-style>/gms; + const mediaAllowed = isExternalMediaAllowed(); return text.replaceAll(styleDecodeRegex, (_, style) => { try { @@ -491,6 +492,13 @@ export function decodeStyleTags(text) { } } } + if (!mediaAllowed && Array.isArray(rule.declarations) && rule.declarations.length > 0) { + for (const declaration of rule.declarations) { + if (declaration.value.includes('://')) { + rule.declarations.splice(rule.declarations.indexOf(declaration), 1); + } + } + } } } } From 8726def6e020c6ef5ee5abfd917f9fa63ade9065 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 May 2024 14:26:59 +0300 Subject: [PATCH 04/44] Use recursive stylesheet sanitation --- public/scripts/chats.js | 74 +++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 84f316e63..7223ed27c 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -465,42 +465,52 @@ export function decodeStyleTags(text) { const styleDecodeRegex = /(.+?)<\/custom-style>/gms; const mediaAllowed = isExternalMediaAllowed(); + function sanitizeRule(rule) { + if (rule.selectors) { + for (let i = 0; i < rule.selectors.length; i++) { + let selector = rule.selectors[i]; + if (selector) { + let selectors = (selector.split(' ') ?? []).map((v) => { + if (v.startsWith('.')) { + return '.custom-' + v.substring(1); + } + return v; + }).join(' '); + + rule.selectors[i] = '.mes_text ' + selectors; + } + } + } + if (!mediaAllowed && Array.isArray(rule.declarations) && rule.declarations.length > 0) { + for (const declaration of rule.declarations) { + if (declaration.value.includes('://')) { + rule.declarations.splice(rule.declarations.indexOf(declaration), 1); + } + } + } + } + + function sanitizeRuleSet(ruleSet) { + if (ruleSet.type === 'rule') { + sanitizeRule(ruleSet); + } + + if (Array.isArray(ruleSet.rules)) { + ruleSet.rules = ruleSet.rules.filter(rule => rule.type !== 'import'); + + for (const mediaRule of ruleSet.rules) { + sanitizeRuleSet(mediaRule); + } + } + } + return text.replaceAll(styleDecodeRegex, (_, style) => { try { let styleCleaned = unescape(style).replaceAll(//g, ''); const ast = css.parse(styleCleaned); - const rules = ast?.stylesheet?.rules; - if (rules) { - for (const rule of rules) { - if (rule.type === 'import') { - rules.splice(rules.indexOf(rule), 1); - } - - if (rule.type === 'rule') { - if (rule.selectors) { - for (let i = 0; i < rule.selectors.length; i++) { - let selector = rule.selectors[i]; - if (selector) { - let selectors = (selector.split(' ') ?? []).map((v) => { - if (v.startsWith('.')) { - return '.custom-' + v.substring(1); - } - return v; - }).join(' '); - - rule.selectors[i] = '.mes_text ' + selectors; - } - } - } - if (!mediaAllowed && Array.isArray(rule.declarations) && rule.declarations.length > 0) { - for (const declaration of rule.declarations) { - if (declaration.value.includes('://')) { - rule.declarations.splice(rule.declarations.indexOf(declaration), 1); - } - } - } - } - } + const sheet = ast?.stylesheet; + if (sheet) { + sanitizeRuleSet(ast.stylesheet); } return ``; } catch (error) { From 99e09f0b91fc918b547f63f0a88f6f7289d8d898 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 May 2024 13:43:59 +0300 Subject: [PATCH 05/44] Improve external media removal in style blocks --- public/scripts/chats.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 4bea9ed58..84f316e63 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -463,6 +463,7 @@ export function encodeStyleTags(text) { */ export function decodeStyleTags(text) { const styleDecodeRegex = /(.+?)<\/custom-style>/gms; + const mediaAllowed = isExternalMediaAllowed(); return text.replaceAll(styleDecodeRegex, (_, style) => { try { @@ -491,6 +492,13 @@ export function decodeStyleTags(text) { } } } + if (!mediaAllowed && Array.isArray(rule.declarations) && rule.declarations.length > 0) { + for (const declaration of rule.declarations) { + if (declaration.value.includes('://')) { + rule.declarations.splice(rule.declarations.indexOf(declaration), 1); + } + } + } } } } From 62a1919402faa555c3d670d517fa8872be60a898 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 May 2024 14:26:59 +0300 Subject: [PATCH 06/44] Use recursive stylesheet sanitation --- public/scripts/chats.js | 74 +++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 84f316e63..7223ed27c 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -465,42 +465,52 @@ export function decodeStyleTags(text) { const styleDecodeRegex = /(.+?)<\/custom-style>/gms; const mediaAllowed = isExternalMediaAllowed(); + function sanitizeRule(rule) { + if (rule.selectors) { + for (let i = 0; i < rule.selectors.length; i++) { + let selector = rule.selectors[i]; + if (selector) { + let selectors = (selector.split(' ') ?? []).map((v) => { + if (v.startsWith('.')) { + return '.custom-' + v.substring(1); + } + return v; + }).join(' '); + + rule.selectors[i] = '.mes_text ' + selectors; + } + } + } + if (!mediaAllowed && Array.isArray(rule.declarations) && rule.declarations.length > 0) { + for (const declaration of rule.declarations) { + if (declaration.value.includes('://')) { + rule.declarations.splice(rule.declarations.indexOf(declaration), 1); + } + } + } + } + + function sanitizeRuleSet(ruleSet) { + if (ruleSet.type === 'rule') { + sanitizeRule(ruleSet); + } + + if (Array.isArray(ruleSet.rules)) { + ruleSet.rules = ruleSet.rules.filter(rule => rule.type !== 'import'); + + for (const mediaRule of ruleSet.rules) { + sanitizeRuleSet(mediaRule); + } + } + } + return text.replaceAll(styleDecodeRegex, (_, style) => { try { let styleCleaned = unescape(style).replaceAll(//g, ''); const ast = css.parse(styleCleaned); - const rules = ast?.stylesheet?.rules; - if (rules) { - for (const rule of rules) { - if (rule.type === 'import') { - rules.splice(rules.indexOf(rule), 1); - } - - if (rule.type === 'rule') { - if (rule.selectors) { - for (let i = 0; i < rule.selectors.length; i++) { - let selector = rule.selectors[i]; - if (selector) { - let selectors = (selector.split(' ') ?? []).map((v) => { - if (v.startsWith('.')) { - return '.custom-' + v.substring(1); - } - return v; - }).join(' '); - - rule.selectors[i] = '.mes_text ' + selectors; - } - } - } - if (!mediaAllowed && Array.isArray(rule.declarations) && rule.declarations.length > 0) { - for (const declaration of rule.declarations) { - if (declaration.value.includes('://')) { - rule.declarations.splice(rule.declarations.indexOf(declaration), 1); - } - } - } - } - } + const sheet = ast?.stylesheet; + if (sheet) { + sanitizeRuleSet(ast.stylesheet); } return ``; } catch (error) { From 66db820c9e6a66300499ca3fe46c934b7f9d4f8e Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 May 2024 19:55:55 +0300 Subject: [PATCH 07/44] Fix external style declaration filtering --- public/scripts/chats.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 7223ed27c..83352f644 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -466,11 +466,11 @@ export function decodeStyleTags(text) { const mediaAllowed = isExternalMediaAllowed(); function sanitizeRule(rule) { - if (rule.selectors) { + if (Array.isArray(rule.selectors)) { for (let i = 0; i < rule.selectors.length; i++) { - let selector = rule.selectors[i]; + const selector = rule.selectors[i]; if (selector) { - let selectors = (selector.split(' ') ?? []).map((v) => { + const selectors = (selector.split(' ') ?? []).map((v) => { if (v.startsWith('.')) { return '.custom-' + v.substring(1); } @@ -482,16 +482,12 @@ export function decodeStyleTags(text) { } } if (!mediaAllowed && Array.isArray(rule.declarations) && rule.declarations.length > 0) { - for (const declaration of rule.declarations) { - if (declaration.value.includes('://')) { - rule.declarations.splice(rule.declarations.indexOf(declaration), 1); - } - } + rule.declarations = rule.declarations.filter(declaration => !declaration.value.includes('://')); } } function sanitizeRuleSet(ruleSet) { - if (ruleSet.type === 'rule') { + if (Array.isArray(ruleSet.selectors) || Array.isArray(ruleSet.declarations)) { sanitizeRule(ruleSet); } From 80e104e7239a7209eebd11a37e55f7b5f4b119c2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 May 2024 21:50:42 +0300 Subject: [PATCH 08/44] Don't open click to edit in document mode if text selected --- public/scripts/chats.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 83352f644..f3019759a 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -1391,6 +1391,7 @@ jQuery(function () { }); $(document).on('click', 'body.documentstyle .mes .mes_text', function () { + if (window.getSelection().toString()) return; if ($('.edit_textarea').length) return; $(this).closest('.mes').find('.mes_edit').trigger('click'); }); From 965dac6514bff434688b559b3b44de52415ea138 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 27 May 2024 22:22:21 +0300 Subject: [PATCH 09/44] #2296 Add data bank bulk edit --- public/scripts/chats.js | 63 +++++++++++++++++++ .../extensions/attachments/manager.html | 25 +++++++- .../scripts/extensions/attachments/style.css | 24 +++++++ 3 files changed, 110 insertions(+), 2 deletions(-) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index f3019759a..a5ec1abd3 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -855,6 +855,12 @@ async function openAttachmentManager() { [ATTACHMENT_SOURCE.CHAT]: '.chatAttachmentsList', }; + const selected = template + .find(sources[source]) + .find('.attachmentListItemCheckbox:checked') + .map((_, el) => $(el).closest('.attachmentListItem').attr('data-attachment-url')) + .get(); + template.find(sources[source]).empty(); // Sort attachments by sortField and sortOrder, and apply filter @@ -864,6 +870,8 @@ async function openAttachmentManager() { const isDisabled = isAttachmentDisabled(attachment); const attachmentTemplate = template.find('.attachmentListItemTemplate .attachmentListItem').clone(); attachmentTemplate.toggleClass('disabled', isDisabled); + attachmentTemplate.attr('data-attachment-url', attachment.url); + attachmentTemplate.attr('data-attachment-source', source); attachmentTemplate.find('.attachmentFileIcon').attr('title', attachment.url); attachmentTemplate.find('.attachmentListItemName').text(attachment.name); attachmentTemplate.find('.attachmentListItemSize').text(humanFileSize(attachment.size)); @@ -876,6 +884,10 @@ async function openAttachmentManager() { attachmentTemplate.find('.enableAttachmentButton').toggle(isDisabled).on('click', () => enableAttachment(attachment, renderAttachments)); attachmentTemplate.find('.disableAttachmentButton').toggle(!isDisabled).on('click', () => disableAttachment(attachment, renderAttachments)); template.find(sources[source]).append(attachmentTemplate); + + if (selected.includes(attachment.url)) { + attachmentTemplate.find('.attachmentListItemCheckbox').prop('checked', true); + } } } @@ -1044,6 +1056,57 @@ async function openAttachmentManager() { localStorage.setItem('DataBank_sortOrder', sortOrder); renderAttachments(); }); + template.find('.bulkActionDelete').on('click', async () => { + const selectedAttachments = document.querySelectorAll('.attachmentListItemCheckboxContainer .attachmentListItemCheckbox:checked'); + + if (selectedAttachments.length === 0) { + toastr.info('No attachments selected.', 'Data Bank'); + return; + } + + const confirm = await callGenericPopup('Are you sure you want to delete the selected attachments?', POPUP_TYPE.CONFIRM); + + if (confirm !== POPUP_RESULT.AFFIRMATIVE) { + return; + } + + const attachments = getDataBankAttachments(); + selectedAttachments.forEach(async (checkbox) => { + const listItem = checkbox.closest('.attachmentListItem'); + if (!(listItem instanceof HTMLElement)) { + return; + } + const url = listItem.dataset.attachmentUrl; + const source = listItem.dataset.attachmentSource; + const attachment = attachments.find(a => a.url === url); + if (!attachment) { + return; + } + await deleteAttachment(attachment, source, () => {}, false); + }); + + document.querySelectorAll('.attachmentListItemCheckbox, .attachmentsBulkEditCheckbox').forEach(checkbox => { + if (checkbox instanceof HTMLInputElement) { + checkbox.checked = false; + } + }); + + await renderAttachments(); + }); + template.find('.bulkActionSelectAll').on('click', () => { + $('.attachmentListItemCheckbox:visible').each((_, checkbox) => { + if (checkbox instanceof HTMLInputElement) { + checkbox.checked = true; + } + }); + }); + template.find('.bulkActionSelectNone').on('click', () => { + $('.attachmentListItemCheckbox:visible').each((_, checkbox) => { + if (checkbox instanceof HTMLInputElement) { + checkbox.checked = false; + } + }); + }); const cleanupFn = await renderButtons(); await verifyAttachments(); diff --git a/public/scripts/extensions/attachments/manager.html b/public/scripts/extensions/attachments/manager.html index ea1ce807d..dad00cac8 100644 --- a/public/scripts/extensions/attachments/manager.html +++ b/public/scripts/extensions/attachments/manager.html @@ -1,4 +1,4 @@ -
+

Data Bank @@ -37,7 +37,27 @@ Size (Largest First) - + +

+
+
+ + + +

@@ -102,6 +122,7 @@
+
diff --git a/public/scripts/extensions/attachments/style.css b/public/scripts/extensions/attachments/style.css index 3c46e7cc9..d2abc4985 100644 --- a/public/scripts/extensions/attachments/style.css +++ b/public/scripts/extensions/attachments/style.css @@ -37,3 +37,27 @@ .attachmentListItemCreated { text-align: right; } + +.attachmentListItemCheckboxContainer, +.attachmentBulkActionsContainer, +.attachmentsBulkEditCheckbox { + display: none; +} + +@supports selector(:has(*)) { + .dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentsBulkEditButton { + color: var(--golden); + } + + .dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentBulkActionsContainer { + display: flex; + } + + .dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentListItemCheckboxContainer { + display: inline-flex; + } + + .dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentFileIcon { + display: none; + } +} From 4528655bb7d5e3175842c44b37623b8b96e4f53b Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 28 May 2024 00:55:57 -0400 Subject: [PATCH 10/44] Textgen: Add multiswipe support for TabbyAPI Tabby now supports batching and the "n" parameter for both non-streaming and streaming. Add this into SillyTavern. Signed-off-by: kingbri --- public/index.html | 2 +- public/script.js | 4 ++-- public/scripts/textgen-settings.js | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index 02c292ddf..b0693b9c0 100644 --- a/public/index.html +++ b/public/index.html @@ -1137,7 +1137,7 @@
-
+
Multiple swipes per generation
diff --git a/public/script.js b/public/script.js index ae7cc70ad..712752d44 100644 --- a/public/script.js +++ b/public/script.js @@ -22,7 +22,7 @@ import { parseTabbyLogprobs, } from './scripts/textgen-settings.js'; -const { MANCER, TOGETHERAI, OOBA, VLLM, APHRODITE, OLLAMA, INFERMATICAI, DREAMGEN, OPENROUTER } = textgen_types; +const { MANCER, TOGETHERAI, OOBA, VLLM, APHRODITE, TABBY, OLLAMA, INFERMATICAI, DREAMGEN, OPENROUTER } = textgen_types; import { world_info, @@ -5005,7 +5005,7 @@ function extractMultiSwipes(data, type) { return swipes; } - if (main_api === 'openai' || (main_api === 'textgenerationwebui' && [MANCER, VLLM, APHRODITE].includes(textgen_settings.type))) { + if (main_api === 'openai' || (main_api === 'textgenerationwebui' && [MANCER, VLLM, APHRODITE, TABBY].includes(textgen_settings.type))) { if (!Array.isArray(data.choices)) { return swipes; } diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 9d2cd6214..cf61b3daa 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -1143,6 +1143,10 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, delete params.dynatemp_high; } + if (settings.type === TABBY) { + params.n = canMultiSwipe ? settings.n : 1; + } + switch (settings.type) { case VLLM: params = Object.assign(params, vllmParams); From 1d32749ed26a6d2916f5b1b98380019c50215d0b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 28 May 2024 15:24:36 +0300 Subject: [PATCH 11/44] Update latest tag for release branch pushes --- .github/workflows/docker-publish.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 284ae8184..961a1d03d 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -67,8 +67,10 @@ jobs: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} # Release version tag if the workflow is triggered by a release # Branch name tag if the workflow is triggered by a push + # Latest tag if the branch is release and the workflow is triggered by a push tags: | ${{ github.event_name == 'release' && github.ref_name || env.BRANCH_NAME }} + ${{ github.event_name == 'push' && env.BRANCH_NAME == 'release' && 'latest' || '' }} # Login into package repository as the person who created the release - name: Log in to the Container registry @@ -90,11 +92,3 @@ jobs: push: true tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} - - # If the workflow is triggered by a release, marks and push the image as such - - name: Docker tag latest and push - if: ${{ github.event_name == 'release' }} - run: | - docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} - docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest From e66b270811310f51cd7575152b127f9ef9926238 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 28 May 2024 17:49:34 +0300 Subject: [PATCH 12/44] Change backups to be user data scoped --- backups/!README.md | 9 +++++++++ src/constants.js | 1 + src/endpoints/chats.js | 17 +++++++---------- src/endpoints/settings.js | 16 ++++++---------- src/users.js | 1 + src/util.js | 12 ++++++------ 6 files changed, 30 insertions(+), 26 deletions(-) create mode 100644 backups/!README.md diff --git a/backups/!README.md b/backups/!README.md new file mode 100644 index 000000000..8c9a74cf2 --- /dev/null +++ b/backups/!README.md @@ -0,0 +1,9 @@ +# Looking for setting snapshots or chat backups? + +Individual user backups are now located in the data directory. + +Example for the default user under default data root: + +/data/default-user/backups + +This folder remains for historical purposes only. diff --git a/src/constants.js b/src/constants.js index 7ca267f74..b4945fa6f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -41,6 +41,7 @@ const USER_DIRECTORY_TEMPLATE = Object.freeze({ comfyWorkflows: 'user/workflows', files: 'user/files', vectors: 'vectors', + backups: 'backups', }); /** diff --git a/src/endpoints/chats.js b/src/endpoints/chats.js index ff55d3ff0..d88b27fa3 100644 --- a/src/endpoints/chats.js +++ b/src/endpoints/chats.js @@ -6,15 +6,16 @@ const sanitize = require('sanitize-filename'); const writeFileAtomicSync = require('write-file-atomic').sync; const { jsonParser, urlencodedParser } = require('../express-common'); -const { PUBLIC_DIRECTORIES, UPLOADS_PATH } = require('../constants'); +const { UPLOADS_PATH } = require('../constants'); const { getConfigValue, humanizedISO8601DateTime, tryParse, generateTimestamp, removeOldBackups } = require('../util'); /** * Saves a chat to the backups directory. + * @param {string} directory The user's backups directory. * @param {string} name The name of the chat. * @param {string} chat The serialized chat to save. */ -function backupChat(name, chat) { +function backupChat(directory, name, chat) { try { const isBackupDisabled = getConfigValue('disableChatBackup', false); @@ -22,17 +23,13 @@ function backupChat(name, chat) { return; } - if (!fs.existsSync(PUBLIC_DIRECTORIES.backups)) { - fs.mkdirSync(PUBLIC_DIRECTORIES.backups); - } - // replace non-alphanumeric characters with underscores name = sanitize(name).replace(/[^a-z0-9]/gi, '_').toLowerCase(); - const backupFile = path.join(PUBLIC_DIRECTORIES.backups, `chat_${name}_${generateTimestamp()}.jsonl`); + const backupFile = path.join(directory, `chat_${name}_${generateTimestamp()}.jsonl`); writeFileAtomicSync(backupFile, chat, 'utf-8'); - removeOldBackups(`chat_${name}_`); + removeOldBackups(directory, `chat_${name}_`); } catch (err) { console.log(`Could not backup chat for ${name}`, err); } @@ -151,7 +148,7 @@ router.post('/save', jsonParser, function (request, response) { const fileName = `${sanitize(String(request.body.file_name))}.jsonl`; const filePath = path.join(request.user.directories.chats, directoryName, fileName); writeFileAtomicSync(filePath, jsonlData, 'utf8'); - backupChat(directoryName, jsonlData); + backupChat(request.user.directories.backups, directoryName, jsonlData); return response.send({ result: 'ok' }); } catch (error) { response.send(error); @@ -455,7 +452,7 @@ router.post('/group/save', jsonParser, (request, response) => { let chat_data = request.body.chat; let jsonlData = chat_data.map(JSON.stringify).join('\n'); writeFileAtomicSync(pathToFile, jsonlData, 'utf8'); - backupChat(String(id), jsonlData); + backupChat(request.user.directories.backups, String(id), jsonlData); return response.send({ ok: true }); }); diff --git a/src/endpoints/settings.js b/src/endpoints/settings.js index a907fab5b..367ab6553 100644 --- a/src/endpoints/settings.js +++ b/src/endpoints/settings.js @@ -110,10 +110,6 @@ function readPresetsFromDirectory(directoryPath, options = {}) { async function backupSettings() { try { - if (!fs.existsSync(PUBLIC_DIRECTORIES.backups)) { - fs.mkdirSync(PUBLIC_DIRECTORIES.backups); - } - const userHandles = await getAllUserHandles(); for (const handle of userHandles) { @@ -131,7 +127,7 @@ async function backupSettings() { */ function backupUserSettings(handle) { const userDirectories = getUserDirectories(handle); - const backupFile = path.join(PUBLIC_DIRECTORIES.backups, `${getFilePrefix(handle)}${generateTimestamp()}.json`); + const backupFile = path.join(userDirectories.backups, `${getFilePrefix(handle)}${generateTimestamp()}.json`); const sourceFile = path.join(userDirectories.root, SETTINGS_FILE); if (!fs.existsSync(sourceFile)) { @@ -139,7 +135,7 @@ function backupUserSettings(handle) { } fs.copyFileSync(sourceFile, backupFile); - removeOldBackups(`settings_${handle}`); + removeOldBackups(userDirectories.backups, `settings_${handle}`); } const router = express.Router(); @@ -227,12 +223,12 @@ router.post('/get', jsonParser, (request, response) => { router.post('/get-snapshots', jsonParser, async (request, response) => { try { - const snapshots = fs.readdirSync(PUBLIC_DIRECTORIES.backups); + const snapshots = fs.readdirSync(request.user.directories.backups); const userFilesPattern = getFilePrefix(request.user.profile.handle); const userSnapshots = snapshots.filter(x => x.startsWith(userFilesPattern)); const result = userSnapshots.map(x => { - const stat = fs.statSync(path.join(PUBLIC_DIRECTORIES.backups, x)); + const stat = fs.statSync(path.join(request.user.directories.backups, x)); return { date: stat.ctimeMs, name: x, size: stat.size }; }); @@ -252,7 +248,7 @@ router.post('/load-snapshot', jsonParser, async (request, response) => { } const snapshotName = request.body.name; - const snapshotPath = path.join(PUBLIC_DIRECTORIES.backups, snapshotName); + const snapshotPath = path.join(request.user.directories.backups, snapshotName); if (!fs.existsSync(snapshotPath)) { return response.sendStatus(404); @@ -286,7 +282,7 @@ router.post('/restore-snapshot', jsonParser, async (request, response) => { } const snapshotName = request.body.name; - const snapshotPath = path.join(PUBLIC_DIRECTORIES.backups, snapshotName); + const snapshotPath = path.join(request.user.directories.backups, snapshotName); if (!fs.existsSync(snapshotPath)) { return response.sendStatus(404); diff --git a/src/users.js b/src/users.js index 10bea3fd0..d01419248 100644 --- a/src/users.js +++ b/src/users.js @@ -87,6 +87,7 @@ const STORAGE_KEYS = { * @property {string} comfyWorkflows - The directory where the ComfyUI workflows are stored * @property {string} files - The directory where the uploaded files are stored * @property {string} vectors - The directory where the vectors are stored + * @property {string} backups - The directory where the backups are stored */ /** diff --git a/src/util.js b/src/util.js index 31d134bb0..5a44933f5 100644 --- a/src/util.js +++ b/src/util.js @@ -9,8 +9,6 @@ const yaml = require('yaml'); const { default: simpleGit } = require('simple-git'); const { Readable } = require('stream'); -const { PUBLIC_DIRECTORIES } = require('./constants'); - /** * Parsed config object. */ @@ -360,14 +358,16 @@ function generateTimestamp() { } /** - * @param {string} prefix + * Remove old backups with the given prefix from a specified directory. + * @param {string} directory The root directory to remove backups from. + * @param {string} prefix File prefix to filter backups by. */ -function removeOldBackups(prefix) { +function removeOldBackups(directory, prefix) { const MAX_BACKUPS = 50; - let files = fs.readdirSync(PUBLIC_DIRECTORIES.backups).filter(f => f.startsWith(prefix)); + let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix)); if (files.length > MAX_BACKUPS) { - files = files.map(f => path.join(PUBLIC_DIRECTORIES.backups, f)); + files = files.map(f => path.join(directory, f)); files.sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs); fs.rmSync(files[0]); From 2b3dfc5ae208a2788f5eeea20cbbc5fd7fc4128c Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 28 May 2024 22:54:50 +0300 Subject: [PATCH 13/44] Add ollama and llamacpp as vector sources --- public/scripts/extensions/vectors/index.js | 88 ++++++++++++++++--- .../scripts/extensions/vectors/settings.html | 27 +++++- src/additional-headers.js | 20 ++++- src/endpoints/vectors.js | 41 ++++++++- src/vectors/llamacpp-vectors.js | 61 +++++++++++++ src/vectors/ollama-vectors.js | 69 +++++++++++++++ 6 files changed, 286 insertions(+), 20 deletions(-) create mode 100644 src/vectors/llamacpp-vectors.js create mode 100644 src/vectors/ollama-vectors.js diff --git a/public/scripts/extensions/vectors/index.js b/public/scripts/extensions/vectors/index.js index a68faeb2e..58d3d43e9 100644 --- a/public/scripts/extensions/vectors/index.js +++ b/public/scripts/extensions/vectors/index.js @@ -25,6 +25,7 @@ import { getDataBankAttachments, getFileAttachment } from '../../chats.js'; import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js'; import { debounce_timeout } from '../../constants.js'; import { getSortedEntries } from '../../world-info.js'; +import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js'; const MODULE_NAME = 'vectors'; @@ -38,6 +39,8 @@ const settings = { togetherai_model: 'togethercomputer/m2-bert-80M-32k-retrieval', openai_model: 'text-embedding-ada-002', cohere_model: 'embed-english-v3.0', + ollama_model: 'mxbai-embed-large', + ollama_keep: false, summarize: false, summarize_sent: false, summary_source: 'main', @@ -272,6 +275,10 @@ async function synchronizeChat(batchSize = 5) { switch (cause) { case 'api_key_missing': return 'API key missing. Save it in the "API Connections" panel.'; + case 'api_url_missing': + return 'API URL missing. Save it in the "API Connections" panel.'; + case 'api_model_missing': + return 'Vectorization Source Model is required, but not set.'; case 'extras_module_missing': return 'Extras API must provide an "embeddings" module.'; default: @@ -637,6 +644,12 @@ function getVectorHeaders() { case 'cohere': addCohereHeaders(headers); break; + case 'ollama': + addOllamaHeaders(headers); + break; + case 'llamacpp': + addLlamaCppHeaders(headers); + break; default: break; } @@ -685,6 +698,28 @@ function addCohereHeaders(headers) { }); } +/** + * Add headers for the Ollama API source. + * @param {object} headers Header object + */ +function addOllamaHeaders(headers) { + Object.assign(headers, { + 'X-Ollama-Model': extension_settings.vectors.ollama_model, + 'X-Ollama-URL': textgenerationwebui_settings.server_urls[textgen_types.OLLAMA], + 'X-Ollama-Keep': !!extension_settings.vectors.ollama_keep, + }); +} + +/** + * Add headers for the LlamaCpp API source. + * @param {object} headers Header object + */ +function addLlamaCppHeaders(headers) { + Object.assign(headers, { + 'X-LlamaCpp-URL': textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP], + }); +} + /** * Inserts vector items into a collection * @param {string} collectionId - The collection to insert into @@ -692,18 +727,7 @@ function addCohereHeaders(headers) { * @returns {Promise} */ async function insertVectorItems(collectionId, items) { - if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] || - settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] || - settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] || - settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI] || - settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI] || - settings.source === 'cohere' && !secret_state[SECRET_KEYS.COHERE]) { - throw new Error('Vectors: API key missing', { cause: 'api_key_missing' }); - } - - if (settings.source === 'extras' && !modules.includes('embeddings')) { - throw new Error('Vectors: Embeddings module missing', { cause: 'extras_module_missing' }); - } + throwIfSourceInvalid(); const headers = getVectorHeaders(); @@ -722,6 +746,33 @@ async function insertVectorItems(collectionId, items) { } } +/** + * Throws an error if the source is invalid (missing API key or URL, or missing module) + */ +function throwIfSourceInvalid() { + if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] || + settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] || + settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] || + settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI] || + settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI] || + settings.source === 'cohere' && !secret_state[SECRET_KEYS.COHERE]) { + throw new Error('Vectors: API key missing', { cause: 'api_key_missing' }); + } + + if (settings.source === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA] || + settings.source === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) { + throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' }); + } + + if (settings.source === 'ollama' && !settings.ollama_model) { + throw new Error('Vectors: API model missing', { cause: 'api_model_missing' }); + } + + if (settings.source === 'extras' && !modules.includes('embeddings')) { + throw new Error('Vectors: Embeddings module missing', { cause: 'extras_module_missing' }); + } +} + /** * Deletes vector items from a collection * @param {string} collectionId - The collection to delete from @@ -870,6 +921,8 @@ function toggleSettings() { $('#together_vectorsModel').toggle(settings.source === 'togetherai'); $('#openai_vectorsModel').toggle(settings.source === 'openai'); $('#cohere_vectorsModel').toggle(settings.source === 'cohere'); + $('#ollama_vectorsModel').toggle(settings.source === 'ollama'); + $('#llamacpp_vectorsModel').toggle(settings.source === 'llamacpp'); $('#nomicai_apiKey').toggle(settings.source === 'nomicai'); } @@ -1154,6 +1207,17 @@ jQuery(async () => { Object.assign(extension_settings.vectors, settings); saveSettingsDebounced(); }); + $('#vectors_ollama_model').val(settings.ollama_model).on('input', () => { + $('#vectors_modelWarning').show(); + settings.ollama_model = String($('#vectors_ollama_model').val()); + Object.assign(extension_settings.vectors, settings); + saveSettingsDebounced(); + }); + $('#vectors_ollama_keep').prop('checked', settings.ollama_keep).on('input', () => { + settings.ollama_keep = $('#vectors_ollama_keep').prop('checked'); + Object.assign(extension_settings.vectors, settings); + saveSettingsDebounced(); + }); $('#vectors_template').val(settings.template).on('input', () => { settings.template = String($('#vectors_template').val()); Object.assign(extension_settings.vectors, settings); diff --git a/public/scripts/extensions/vectors/settings.html b/public/scripts/extensions/vectors/settings.html index efb78f6b9..d21a6c71d 100644 --- a/public/scripts/extensions/vectors/settings.html +++ b/public/scripts/extensions/vectors/settings.html @@ -12,14 +12,37 @@
+
+ + + + + Hint: Download models and set the URL in the API connection settings. + +
+
+ + The server MUST be started with the --embedding flag to use this feature! + + + Hint: Set the URL in the API connection settings. + +
From 309eb80748e9db84129e95f02d725e74236cc046 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 29 May 2024 00:56:55 +0300 Subject: [PATCH 15/44] Function calling for Claude and OpenRouter --- public/index.html | 2 +- public/scripts/openai.js | 19 ++++++++++++++++++- src/endpoints/backends/chat-completions.js | 14 ++++++++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/public/index.html b/public/index.html index b0693b9c0..251da83eb 100644 --- a/public/index.html +++ b/public/index.html @@ -1739,7 +1739,7 @@

-
+
-
+
+
+ + +
diff --git a/src/endpoints/horde.js b/src/endpoints/horde.js index 32cb08f7b..565c94f95 100644 --- a/src/endpoints/horde.js +++ b/src/endpoints/horde.js @@ -325,6 +325,7 @@ router.post('/generate-image', jsonParser, async (request, response) => { width: request.body.width, height: request.body.height, karras: Boolean(request.body.karras), + clip_skip: request.body.clip_skip, n: 1, }, r2: false, From 24b6f99abf44da311199309b2ad813610219308e Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 29 May 2024 02:25:32 +0300 Subject: [PATCH 20/44] Fix Claude function tools with prefills --- src/endpoints/backends/chat-completions.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 8733bfd50..e62c8dcad 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -138,6 +138,10 @@ async function sendClaudeRequest(request, response) { requestBody.system = converted_prompt.systemPrompt; } if (Array.isArray(request.body.tools) && request.body.tools.length > 0) { + // Claude doesn't do prefills on function calls, and doesn't allow empty messages + if (converted_prompt.messages.length && converted_prompt.messages[converted_prompt.messages.length - 1].role === 'assistant') { + converted_prompt.messages.push({ role: 'user', content: '.' }); + } additionalHeaders['anthropic-beta'] = 'tools-2024-05-16'; requestBody.tool_choice = { type: request.body.tool_choice === 'required' ? 'any' : 'auto' }; requestBody.tools = request.body.tools From 110d343eea960f9a0fdb0f361cba2596707ec579 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 29 May 2024 02:49:13 +0300 Subject: [PATCH 21/44] Add upscale amount control to DrawThings --- .../extensions/stable-diffusion/index.js | 37 +++++++++++++++++++ .../extensions/stable-diffusion/settings.html | 4 +- src/endpoints/stable-diffusion.js | 17 +++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 4fae374e5..92f49c07d 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -1152,6 +1152,27 @@ async function getVladRemoteUpscalers() { } } +async function getDrawthingsRemoteUpscalers() { + try { + const result = await fetch('/api/sd/drawthings/get-upscaler', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify(getSdRequestBody()), + }); + + if (!result.ok) { + throw new Error('SD DrawThings API returned an error.'); + } + + const data = await result.text(); + + return data ? [data] : ['N/A']; + } catch (error) { + console.error(error); + return ['N/A']; + } +} + async function updateAutoRemoteModel() { try { const result = await fetch('/api/sd/set-model', { @@ -1586,6 +1607,21 @@ async function loadDrawthingsModels() { const data = [{ value: currentModel, text: currentModel }]; + + const upscalers = await getDrawthingsRemoteUpscalers(); + + if (Array.isArray(upscalers) && upscalers.length > 0) { + $('#sd_hr_upscaler').empty(); + + for (const upscaler of upscalers) { + const option = document.createElement('option'); + option.innerText = upscaler; + option.value = upscaler; + option.selected = upscaler === extension_settings.sd.hr_upscaler; + $('#sd_hr_upscaler').append(option); + } + } + return data; } catch (error) { console.log('Error loading DrawThings API models:', error); @@ -2469,6 +2505,7 @@ async function generateDrawthingsImage(prompt, negativePrompt) { enable_hr: !!extension_settings.sd.enable_hr, denoising_strength: extension_settings.sd.denoising_strength, clip_skip: extension_settings.sd.clip_skip, + upscaler_scale: extension_settings.sd.hr_scale, // TODO: advanced API parameters: hr, upscaler }), }); diff --git a/public/scripts/extensions/stable-diffusion/settings.html b/public/scripts/extensions/stable-diffusion/settings.html index 6fac4c41a..bdb86eae8 100644 --- a/public/scripts/extensions/stable-diffusion/settings.html +++ b/public/scripts/extensions/stable-diffusion/settings.html @@ -236,11 +236,13 @@
-
+
+
+
diff --git a/src/endpoints/stable-diffusion.js b/src/endpoints/stable-diffusion.js index 857424af3..bdecb5a7e 100644 --- a/src/endpoints/stable-diffusion.js +++ b/src/endpoints/stable-diffusion.js @@ -676,6 +676,23 @@ drawthings.post('/get-model', jsonParser, async (request, response) => { } }); +drawthings.post('/get-upscaler', jsonParser, async (request, response) => { + try { + const url = new URL(request.body.url); + url.pathname = '/'; + + const result = await fetch(url, { + method: 'GET', + }); + const data = await result.json(); + + return response.send(data['upscaler']); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } +}); + drawthings.post('/generate', jsonParser, async (request, response) => { try { console.log('SD DrawThings API request:', request.body); From d350dbf0d7fae941c0fb0ef17ece9f7d32a90c7f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 29 May 2024 03:00:42 +0300 Subject: [PATCH 22/44] Add Novel decrisper control --- .../scripts/extensions/stable-diffusion/index.js | 9 +++++++++ .../extensions/stable-diffusion/settings.html | 16 ++++++++++++---- src/endpoints/novelai.js | 2 +- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 92f49c07d..c8124dc6f 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -243,6 +243,7 @@ const defaultSettings = { novel_anlas_guard: false, novel_sm: false, novel_sm_dyn: false, + novel_decrisper: false, // OpenAI settings openai_style: 'vivid', @@ -387,6 +388,7 @@ async function loadSettings() { $('#sd_novel_sm').prop('checked', extension_settings.sd.novel_sm); $('#sd_novel_sm_dyn').prop('checked', extension_settings.sd.novel_sm_dyn); $('#sd_novel_sm_dyn').prop('disabled', !extension_settings.sd.novel_sm); + $('#sd_novel_decrisper').prop('checked', extension_settings.sd.novel_decrisper); $('#sd_pollinations_enhance').prop('checked', extension_settings.sd.pollinations_enhance); $('#sd_pollinations_refine').prop('checked', extension_settings.sd.pollinations_refine); $('#sd_horde').prop('checked', extension_settings.sd.horde); @@ -846,6 +848,11 @@ function onNovelSmDynInput() { saveSettingsDebounced(); } +function onNovelDecrisperInput() { + extension_settings.sd.novel_decrisper = !!$('#sd_novel_decrisper').prop('checked'); + saveSettingsDebounced(); +} + function onPollinationsEnhanceInput() { extension_settings.sd.pollinations_enhance = !!$('#sd_pollinations_enhance').prop('checked'); saveSettingsDebounced(); @@ -2542,6 +2549,7 @@ async function generateNovelImage(prompt, negativePrompt) { height: height, negative_prompt: negativePrompt, upscale_ratio: extension_settings.sd.novel_upscale_ratio, + decrisper: extension_settings.sd.novel_decrisper, sm: sm, sm_dyn: sm_dyn, }), @@ -3185,6 +3193,7 @@ jQuery(async () => { $('#sd_novel_view_anlas').on('click', onViewAnlasClick); $('#sd_novel_sm').on('input', onNovelSmInput); $('#sd_novel_sm_dyn').on('input', onNovelSmDynInput); + $('#sd_novel_decrisper').on('input', onNovelDecrisperInput); $('#sd_pollinations_enhance').on('input', onPollinationsEnhanceInput); $('#sd_pollinations_refine').on('input', onPollinationsRefineInput); $('#sd_comfy_validate').on('click', validateComfyUrl); diff --git a/public/scripts/extensions/stable-diffusion/settings.html b/public/scripts/extensions/stable-diffusion/settings.html index bdb86eae8..74d722772 100644 --- a/public/scripts/extensions/stable-diffusion/settings.html +++ b/public/scripts/extensions/stable-diffusion/settings.html @@ -182,6 +182,14 @@
+
+ +
@@ -192,13 +200,13 @@ -
From bc94bcb25cb75e0c819dcb16b17b4bdfedbf15ac Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 30 May 2024 01:47:33 +0300 Subject: [PATCH 30/44] Add data bank management commands --- public/scripts/chats.js | 11 +- .../scripts/extensions/attachments/index.js | 193 +++++++++++++++++- 2 files changed, 199 insertions(+), 5 deletions(-) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index a5ec1abd3..97059fa7d 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -768,7 +768,7 @@ async function moveAttachment(attachment, source, callback) { * @param {boolean} [confirm=true] If true, show a confirmation dialog * @returns {Promise} A promise that resolves when the attachment is deleted. */ -async function deleteAttachment(attachment, source, callback, confirm = true) { +export 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); @@ -1179,7 +1179,7 @@ async function runScraper(scraperId, target, callback) { * Uploads a file attachment to the server. * @param {File} file File to upload * @param {string} target Target for the attachment - * @returns + * @returns {Promise} Path to the uploaded file */ export async function uploadFileAttachmentToServer(file, target) { const isValid = await validateFile(file); @@ -1236,6 +1236,8 @@ export async function uploadFileAttachmentToServer(file, target) { saveSettingsDebounced(); break; } + + return fileUrl; } function ensureAttachmentsExist() { @@ -1264,15 +1266,16 @@ function ensureAttachmentsExist() { /** * Gets all currently available attachments. Ignores disabled attachments. + * @param {boolean} [includeDisabled=false] If true, include disabled attachments * @returns {FileAttachment[]} List of attachments */ -export function getDataBankAttachments() { +export function getDataBankAttachments(includeDisabled = false) { ensureAttachmentsExist(); const globalAttachments = extension_settings.attachments ?? []; const chatAttachments = chat_metadata.attachments ?? []; const characterAttachments = extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? []; - return [...globalAttachments, ...chatAttachments, ...characterAttachments].filter(x => !isAttachmentDisabled(x)); + return [...globalAttachments, ...chatAttachments, ...characterAttachments].filter(x => includeDisabled || !isAttachmentDisabled(x)); } /** diff --git a/public/scripts/extensions/attachments/index.js b/public/scripts/extensions/attachments/index.js index 7c9a7a824..7db53c9c7 100644 --- a/public/scripts/extensions/attachments/index.js +++ b/public/scripts/extensions/attachments/index.js @@ -1,15 +1,206 @@ +import { deleteAttachment, getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment, uploadFileAttachmentToServer } from '../../chats.js'; import { renderExtensionTemplateAsync } from '../../extensions.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; +import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; +/** + * List of attachment sources + * @type {string[]} + */ +const TYPES = ['global', 'character', 'chat']; +const FIELDS = ['name', 'url']; + +/** + * Get attachments from the data bank + * @param {string} [source] Source for the attachments + * @returns {import('../../chats').FileAttachment[]} List of attachments + */ +function getAttachments(source) { + if (!source || !TYPES.includes(source)) { + return getDataBankAttachments(true); + } + + return getDataBankAttachmentsForSource(source); +} + +/** + * Callback for listing attachments in the data bank. + * @param {object} args Named arguments + * @returns {string} JSON string of the list of attachments + */ +function listDataBankAttachments(args) { + const attachments = getAttachments(args?.source); + const field = args?.field; + return JSON.stringify(attachments.map(a => FIELDS.includes(field) ? a[field] : a.url)); +} + +/** + * Callback for getting text from an attachment in the data bank. + * @param {object} args Named arguments + * @param {string} value Name or URL of the attachment + * @returns {Promise} Content of the attachment + */ +async function getDataBankText(args, value) { + if (!value) { + toastr.warning('No attachment name or URL provided.'); + return; + } + + const attachments = getAttachments(args?.source); + const match = (a) => String(a).trim().toLowerCase() === String(value).trim().toLowerCase(); + const fullMatchByURL = attachments.find(it => match(it.url)); + const fullMatchByName = attachments.find(it => match(it.name)); + const attachment = fullMatchByURL || fullMatchByName; + + if (!attachment) { + toastr.warning('Attachment not found.'); + return; + } + + const content = await getFileAttachment(attachment.url); + return content; +} + +/** + * Callback for adding an attachment to the data bank. + * @param {object} args Named arguments + * @param {string} value Content of the attachment + * @returns {Promise} URL of the attachment + */ +async function uploadDataBankAttachment(args, value) { + const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat'; + const name = args?.name || new Date().toLocaleString(); + const file = new File([value], name, { type: 'text/plain' }); + const url = await uploadFileAttachmentToServer(file, source); + return url; +} + +/** + * Callback for updating an attachment in the data bank. + * @param {object} args Named arguments + * @param {string} value Content of the attachment + * @returns {Promise} URL of the attachment + */ +async function updateDataBankAttachment(args, value) { + const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat'; + const attachments = getAttachments(source); + const fullMatchByURL = attachments.find(it => String(it.url).trim().toLowerCase() === String(args?.url).trim().toLowerCase()); + const fullMatchByName = attachments.find(it => String(it.name).trim().toLowerCase() === String(args?.name).trim().toLowerCase()); + const attachment = fullMatchByURL || fullMatchByName; + + if (!attachment) { + toastr.warning('Attachment not found.'); + return ''; + } + + await deleteAttachment(attachment, source, () => { }, false); + const file = new File([value], attachment.name, { type: 'text/plain' }); + const url = await uploadFileAttachmentToServer(file, source); + return url; +} + +/** + * Callback for deleting an attachment from the data bank. + * @param {object} args Named arguments + * @param {string} value Name or URL of the attachment + * @returns {Promise} Empty string + */ +async function deleteDataBankAttachment(args, value) { + const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat'; + const attachments = getAttachments(source); + const match = (a) => String(a).trim().toLowerCase() === String(value).trim().toLowerCase(); + const fullMatchByURL = attachments.find(it => match(it.url)); + const fullMatchByName = attachments.find(it => match(it.name)); + const attachment = fullMatchByURL || fullMatchByName; + + if (!attachment) { + toastr.warning('Attachment not found.'); + return ''; + } + + await deleteAttachment(attachment, source, () => { }, false); + return ''; +} + jQuery(async () => { const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {}); $('#extensionsMenu').prepend(buttons); - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'db', + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'db', callback: () => document.getElementById('manageAttachments')?.click(), aliases: ['databank', 'data-bank'], helpString: 'Open the data bank', })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'db-list', + callback: listDataBankAttachments, + aliases: ['databank-list', 'data-bank-list'], + helpString: 'List attachments in the data bank as a JSON-serialized array. Optionally, provide the source of the attachments and the field to list by.', + namedArgumentList: [ + new SlashCommandNamedArgument('source', 'The source of the attachments.', ARGUMENT_TYPE.STRING, false, false, '', TYPES), + new SlashCommandNamedArgument('field', 'The field to list by.', ARGUMENT_TYPE.STRING, false, false, 'url', FIELDS), + ], + returns: ARGUMENT_TYPE.LIST, + })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'db-get', + callback: getDataBankText, + aliases: ['databank-get', 'data-bank-get'], + helpString: 'Get attachment text from the data bank. Either provide the name or URL of the attachment. Optionally, provide the source of the attachment.', + namedArgumentList: [ + new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES), + ], + unnamedArgumentList: [ + new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false), + ], + returns: ARGUMENT_TYPE.STRING, + })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'db-add', + callback: uploadDataBankAttachment, + aliases: ['databank-add', 'data-bank-add'], + helpString: 'Add an attachment to the data bank. If name is not provided, it will be generated automatically. Returns the URL of the attachment.', + namedArgumentList: [ + new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES), + new SlashCommandNamedArgument('name', 'The name of the attachment.', ARGUMENT_TYPE.STRING, false, false), + ], + unnamedArgumentList: [ + new SlashCommandArgument('The content of the file attachment.', ARGUMENT_TYPE.STRING, true, false), + ], + returns: ARGUMENT_TYPE.STRING, + })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'db-update', + callback: updateDataBankAttachment, + aliases: ['databank-update', 'data-bank-update'], + helpString: 'Update an attachment in the data bank, preserving its name. Returns a new URL of the attachment.', + namedArgumentList: [ + new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES), + new SlashCommandNamedArgument('name', 'The name of the attachment.', ARGUMENT_TYPE.STRING, false, false), + new SlashCommandNamedArgument('url', 'The URL of the attachment to update.', ARGUMENT_TYPE.STRING, false, false), + ], + unnamedArgumentList: [ + new SlashCommandArgument('The content of the file attachment.', ARGUMENT_TYPE.STRING, true, false), + ], + returns: ARGUMENT_TYPE.STRING, + })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'db-delete', + callback: deleteDataBankAttachment, + aliases: ['databank-delete', 'data-bank-delete'], + helpString: 'Delete an attachment from the data bank.', + namedArgumentList: [ + new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES), + ], + unnamedArgumentList: [ + new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false), + ], + })); }); From 6a832bdf2a3abf0a16a1989d9be9177481d2b63f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 30 May 2024 01:48:27 +0300 Subject: [PATCH 31/44] Fix summarize command return type --- public/scripts/extensions/memory/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/scripts/extensions/memory/index.js b/public/scripts/extensions/memory/index.js index a697f89da..2a2fa574d 100644 --- a/public/scripts/extensions/memory/index.js +++ b/public/scripts/extensions/memory/index.js @@ -926,5 +926,6 @@ jQuery(async function () { new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''), ], helpString: 'Summarizes the given text. If no text is provided, the current chat will be summarized. Can specify the source and the prompt to use.', + returns: ARGUMENT_TYPE.STRING, })); }); From d25ba41fb590c15be9ba4cccf801c0f7e3610a41 Mon Sep 17 00:00:00 2001 From: Vhallo Date: Thu, 30 May 2024 01:29:28 +0200 Subject: [PATCH 32/44] Extending punctuation Extending punctuation for thoughts. --- public/scripts/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 516a70412..46c1c767a 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -477,7 +477,7 @@ export function sortByCssOrder(a, b) { * trimToEndSentence('Hello, world! I am from'); // 'Hello, world!' */ export function trimToEndSentence(input, include_newline = false) { - const punctuation = new Set(['.', '!', '?', '*', '"', ')', '}', '`', ']', '$', '。', '!', '?', '”', ')', '】', '’', '」']); // extend this as you see fit + const punctuation = new Set(['.', '!', '?', '*', '"', ')', '}', '`', ']', '$', '。', '!', '?', '”', ')', '】', '’', '」', '_']); // extend this as you see fit let last = -1; for (let i = input.length - 1; i >= 0; i--) { From 2c911a3ea2069629fd40a053cccd109e5364ef74 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 30 May 2024 14:49:57 +0300 Subject: [PATCH 33/44] Add more Data Bank script commands --- public/scripts/chats.js | 25 ++-- .../scripts/extensions/attachments/index.js | 10 +- public/scripts/extensions/vectors/index.js | 125 ++++++++++++++---- .../slash-commands/SlashCommandArgument.js | 4 +- 4 files changed, 120 insertions(+), 44 deletions(-) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 97059fa7d..cc644362d 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -1279,23 +1279,28 @@ export function getDataBankAttachments(includeDisabled = false) { } /** - * Gets all attachments for a specific source. Includes disabled attachments. + * Gets all attachments for a specific source. * @param {string} source Attachment source + * @param {boolean} [includeDisabled=true] If true, include disabled attachments * @returns {FileAttachment[]} List of attachments */ -export function getDataBankAttachmentsForSource(source) { +export function getDataBankAttachmentsForSource(source, includeDisabled = true) { ensureAttachmentsExist(); - switch (source) { - case ATTACHMENT_SOURCE.GLOBAL: - return extension_settings.attachments ?? []; - case ATTACHMENT_SOURCE.CHAT: - return chat_metadata.attachments ?? []; - case ATTACHMENT_SOURCE.CHARACTER: - return extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? []; + function getBySource() { + switch (source) { + case ATTACHMENT_SOURCE.GLOBAL: + return extension_settings.attachments ?? []; + case ATTACHMENT_SOURCE.CHAT: + return chat_metadata.attachments ?? []; + case ATTACHMENT_SOURCE.CHARACTER: + return extension_settings.character_attachments?.[characters[this_chid]?.avatar] ?? []; + } + + return []; } - return []; + return getBySource().filter(x => includeDisabled || !isAttachmentDisabled(x)); } /** diff --git a/public/scripts/extensions/attachments/index.js b/public/scripts/extensions/attachments/index.js index 7db53c9c7..0c6d13341 100644 --- a/public/scripts/extensions/attachments/index.js +++ b/public/scripts/extensions/attachments/index.js @@ -138,7 +138,7 @@ jQuery(async () => { name: 'db-list', callback: listDataBankAttachments, aliases: ['databank-list', 'data-bank-list'], - helpString: 'List attachments in the data bank as a JSON-serialized array. Optionally, provide the source of the attachments and the field to list by.', + helpString: 'List attachments in the Data Bank as a JSON-serialized array. Optionally, provide the source of the attachments and the field to list by.', namedArgumentList: [ new SlashCommandNamedArgument('source', 'The source of the attachments.', ARGUMENT_TYPE.STRING, false, false, '', TYPES), new SlashCommandNamedArgument('field', 'The field to list by.', ARGUMENT_TYPE.STRING, false, false, 'url', FIELDS), @@ -150,7 +150,7 @@ jQuery(async () => { name: 'db-get', callback: getDataBankText, aliases: ['databank-get', 'data-bank-get'], - helpString: 'Get attachment text from the data bank. Either provide the name or URL of the attachment. Optionally, provide the source of the attachment.', + helpString: 'Get attachment text from the Data Bank. Either provide the name or URL of the attachment. Optionally, provide the source of the attachment.', namedArgumentList: [ new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES), ], @@ -164,7 +164,7 @@ jQuery(async () => { name: 'db-add', callback: uploadDataBankAttachment, aliases: ['databank-add', 'data-bank-add'], - helpString: 'Add an attachment to the data bank. If name is not provided, it will be generated automatically. Returns the URL of the attachment.', + helpString: 'Add an attachment to the Data Bank. If name is not provided, it will be generated automatically. Returns the URL of the attachment.', namedArgumentList: [ new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES), new SlashCommandNamedArgument('name', 'The name of the attachment.', ARGUMENT_TYPE.STRING, false, false), @@ -179,7 +179,7 @@ jQuery(async () => { name: 'db-update', callback: updateDataBankAttachment, aliases: ['databank-update', 'data-bank-update'], - helpString: 'Update an attachment in the data bank, preserving its name. Returns a new URL of the attachment.', + helpString: 'Update an attachment in the Data Bank, preserving its name. Returns a new URL of the attachment.', namedArgumentList: [ new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES), new SlashCommandNamedArgument('name', 'The name of the attachment.', ARGUMENT_TYPE.STRING, false, false), @@ -195,7 +195,7 @@ jQuery(async () => { name: 'db-delete', callback: deleteDataBankAttachment, aliases: ['databank-delete', 'data-bank-delete'], - helpString: 'Delete an attachment from the data bank.', + helpString: 'Delete an attachment from the Data Bank.', namedArgumentList: [ new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES), ], diff --git a/public/scripts/extensions/vectors/index.js b/public/scripts/extensions/vectors/index.js index 58d3d43e9..7ff7b30f4 100644 --- a/public/scripts/extensions/vectors/index.js +++ b/public/scripts/extensions/vectors/index.js @@ -21,11 +21,14 @@ import { } from '../../extensions.js'; import { collapseNewlines } from '../../power-user.js'; import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js'; -import { getDataBankAttachments, getFileAttachment } from '../../chats.js'; +import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '../../chats.js'; import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js'; import { debounce_timeout } from '../../constants.js'; import { getSortedEntries } from '../../world-info.js'; import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js'; +import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; +import { SlashCommand } from '../../slash-commands/SlashCommand.js'; +import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; const MODULE_NAME = 'vectors'; @@ -332,28 +335,7 @@ async function processFiles(chat) { return; } - const dataBank = getDataBankAttachments(); - const dataBankCollectionIds = []; - - for (const file of dataBank) { - const collectionId = getFileCollectionId(file.url); - const hashesInCollection = await getSavedHashes(collectionId); - dataBankCollectionIds.push(collectionId); - - // File is already in the collection - if (hashesInCollection.length) { - continue; - } - - // Download and process the file - file.text = await getFileAttachment(file.url); - console.log(`Vectors: Retrieved file ${file.name} from Data Bank`); - // Convert kilobytes to string length - const thresholdLength = settings.size_threshold_db * 1024; - // Use chunk size from settings if file is larger than threshold - const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1; - await vectorizeFile(file.text, file.name, collectionId, chunkSize); - } + const dataBankCollectionIds = await ingestDataBankAttachments(); if (dataBankCollectionIds.length) { const queryText = await getQueryText(chat); @@ -400,6 +382,39 @@ async function processFiles(chat) { } } +/** + * Ensures that data bank attachments are ingested and inserted into the vector index. + * @param {string} [source] Optional source filter for data bank attachments. + * @returns {Promise} Collection IDs + */ +async function ingestDataBankAttachments(source) { + // Exclude disabled files + const dataBank = source ? getDataBankAttachmentsForSource(source, false) : getDataBankAttachments(false); + const dataBankCollectionIds = []; + + for (const file of dataBank) { + const collectionId = getFileCollectionId(file.url); + const hashesInCollection = await getSavedHashes(collectionId); + dataBankCollectionIds.push(collectionId); + + // File is already in the collection + if (hashesInCollection.length) { + continue; + } + + // Download and process the file + file.text = await getFileAttachment(file.url); + console.log(`Vectors: Retrieved file ${file.name} from Data Bank`); + // Convert kilobytes to string length + const thresholdLength = settings.size_threshold_db * 1024; + // Use chunk size from settings if file is larger than threshold + const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1; + await vectorizeFile(file.text, file.name, collectionId, chunkSize); + } + + return dataBankCollectionIds; +} + /** * Inserts file chunks from the Data Bank into the prompt. * @param {string} queryText Text to query @@ -408,7 +423,7 @@ async function processFiles(chat) { */ async function injectDataBankChunks(queryText, collectionIds) { try { - const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.chunk_count_db); + const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.chunk_count_db, settings.score_threshold); console.debug(`Vectors: Retrieved ${collectionIds.length} Data Bank collections`, queryResults); let textResult = ''; @@ -828,9 +843,10 @@ async function queryCollection(collectionId, searchText, topK) { * @param {string[]} collectionIds - Collection IDs to query * @param {string} searchText - Text to query * @param {number} topK - Number of results to return + * @param {number} threshold - Score threshold * @returns {Promise>} - Results mapped to collection IDs */ -async function queryMultipleCollections(collectionIds, searchText, topK) { +async function queryMultipleCollections(collectionIds, searchText, topK, threshold) { const headers = getVectorHeaders(); const response = await fetch('/api/vector/query-multi', { @@ -841,7 +857,7 @@ async function queryMultipleCollections(collectionIds, searchText, topK) { searchText: searchText, topK: topK, source: settings.source, - threshold: settings.score_threshold, + threshold: threshold ?? settings.score_threshold, }), }); @@ -1125,7 +1141,7 @@ async function activateWorldInfo(chat) { return; } - const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.max_entries); + const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.max_entries, settings.score_threshold); const activatedHashes = Object.values(queryResults).flatMap(x => x.hashes).filter(onlyUnique); const activatedEntries = []; @@ -1396,4 +1412,59 @@ jQuery(async () => { eventSource.on(event_types.CHAT_DELETED, purgeVectorIndex); eventSource.on(event_types.GROUP_CHAT_DELETED, purgeVectorIndex); eventSource.on(event_types.FILE_ATTACHMENT_DELETED, purgeFileVectorIndex); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'db-ingest', + callback: async () => { + await ingestDataBankAttachments(); + return ''; + }, + aliases: ['databank-ingest', 'data-bank-ingest'], + helpString: 'Force the ingestion of all Data Bank attachments.', + })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'db-purge', + callback: async () => { + const dataBank = getDataBankAttachments(); + + for (const file of dataBank) { + await purgeFileVectorIndex(file.url); + } + + return ''; + }, + aliases: ['databank-purge', 'data-bank-purge'], + helpString: 'Purge the vector index for all Data Bank attachments.', + })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'db-search', + callback: async (args, query) => { + const threshold = Number(args?.threshold ?? settings.score_threshold); + 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 urls = Object + .keys(queryResults) + .map(x => attachments.find(y => getFileCollectionId(y.url) === x)) + .filter(x => x) + .map(x => x.url); + + return JSON.stringify(urls); + }, + 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. Uses the global config value if not set.', ARGUMENT_TYPE.NUMBER, false, false, ''), + new SlashCommandNamedArgument('source', 'Optional filter for the attachments by source.', ARGUMENT_TYPE.STRING, false, false, '', ['global', 'character', 'chat']), + ], + unnamedArgumentList: [ + new SlashCommandArgument('Query to search by.', ARGUMENT_TYPE.STRING, true, false), + ], + returns: ARGUMENT_TYPE.LIST, + })); }); diff --git a/public/scripts/slash-commands/SlashCommandArgument.js b/public/scripts/slash-commands/SlashCommandArgument.js index 21985b9a5..12d1c3b1a 100644 --- a/public/scripts/slash-commands/SlashCommandArgument.js +++ b/public/scripts/slash-commands/SlashCommandArgument.js @@ -21,7 +21,7 @@ export const ARGUMENT_TYPE = { export class SlashCommandArgument { /** - * Creates an unnamed argument from a poperties object. + * Creates an unnamed argument from a properties object. * @param {Object} props * @param {string} props.description description of the argument * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} props.typeList default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE) @@ -75,7 +75,7 @@ export class SlashCommandArgument { export class SlashCommandNamedArgument extends SlashCommandArgument { /** - * Creates an unnamed argument from a poperties object. + * Creates an unnamed argument from a properties object. * @param {Object} props * @param {string} props.name the argument's name * @param {string[]} [props.aliasList] list of aliases From 43f52d5436ad2ad2f955a1e127445f802a2ea7ee Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 30 May 2024 17:01:00 +0300 Subject: [PATCH 34/44] Add /yt-script command --- public/scripts/scrapers.js | 57 ++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/public/scripts/scrapers.js b/public/scripts/scrapers.js index 14ce4e5d8..61b076d11 100644 --- a/public/scripts/scrapers.js +++ b/public/scripts/scrapers.js @@ -1,6 +1,9 @@ import { getRequestHeaders } from '../script.js'; import { renderExtensionTemplateAsync } from './extensions.js'; import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js'; +import { SlashCommand } from './slash-commands/SlashCommand.js'; +import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; +import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { isValidUrl } from './utils.js'; /** @@ -441,6 +444,32 @@ class YouTubeScraper { this.description = 'Download a transcript from a YouTube video.'; this.iconClass = 'fa-brands fa-youtube'; this.iconAvailable = true; + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'yt-script', + callback: async (args, url) => { + try { + if (!url) { + throw new Error('URL or ID of the YouTube video is required'); + } + + const lang = String(args?.lang || ''); + const { transcript } = await this.getScript(String(url).trim(), lang); + return transcript; + } catch (error) { + toastr.error(error.message); + return ''; + } + }, + helpString: 'Scrape a transcript from a YouTube video by ID or URL.', + returns: ARGUMENT_TYPE.STRING, + namedArgumentList: [ + new SlashCommandNamedArgument('lang', 'ISO 639-1 language code of the transcript, e.g. "en"', ARGUMENT_TYPE.STRING, false, false, ''), + ], + unnamedArgumentList: [ + new SlashCommandArgument('URL or ID of the YouTube video', ARGUMENT_TYPE.STRING, true, false), + ], + })); } /** @@ -456,7 +485,12 @@ class YouTubeScraper { * @param {string} url URL of the YouTube video * @returns {string} ID of the YouTube video */ - parseId(url){ + parseId(url) { + // If the URL is already an ID, return it + if (/^[a-zA-Z0-9_-]{11}$/.test(url)) { + return url; + } + const regex = /^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/|shorts\/)|(?:(?:watch)?\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/; const match = url.match(regex); return (match?.length && match[1] ? match[1] : url); @@ -479,8 +513,22 @@ class YouTubeScraper { return; } - const id = this.parseId(String(videoUrl).trim()); const toast = toastr.info('Working, please wait...'); + const { transcript, id } = await this.getScript(videoUrl, lang); + toastr.clear(toast); + + const file = new File([transcript], `YouTube - ${id} - ${Date.now()}.txt`, { type: 'text/plain' }); + return [file]; + } + + /** + * Fetches the transcript of a YouTube video. + * @param {string} videoUrl Video URL or ID + * @param {string} lang Video language + * @returns {Promise<{ transcript: string, id: string }>} Transcript of the YouTube video with the video ID + */ + async getScript(videoUrl, lang) { + const id = this.parseId(String(videoUrl).trim()); const result = await fetch('/api/serpapi/transcript', { method: 'POST', @@ -494,10 +542,7 @@ class YouTubeScraper { } const transcript = await result.text(); - toastr.clear(toast); - - const file = new File([transcript], `YouTube - ${id} - ${Date.now()}.txt`, { type: 'text/plain' }); - return [file]; + return { transcript, id }; } } From 716366070b24563d94674b9e4e8a24675f179c8f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 30 May 2024 17:15:17 +0300 Subject: [PATCH 35/44] Clamp /db-search threshold arg --- public/scripts/extensions/vectors/index.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/public/scripts/extensions/vectors/index.js b/public/scripts/extensions/vectors/index.js index 7ff7b30f4..44e2d481b 100644 --- a/public/scripts/extensions/vectors/index.js +++ b/public/scripts/extensions/vectors/index.js @@ -969,8 +969,8 @@ async function onViewStatsClick() { toastr.info(`Total hashes: ${totalHashes}
Unique hashes: ${uniqueHashes}

I'll mark collected messages with a green circle.`, - `Stats for chat ${chatId}`, - { timeOut: 10000, escapeHtml: false }); + `Stats for chat ${chatId}`, + { timeOut: 10000, escapeHtml: false }); const chat = getContext().chat; for (const message of chat) { @@ -1441,7 +1441,8 @@ jQuery(async () => { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'db-search', callback: async (args, query) => { - const threshold = Number(args?.threshold ?? settings.score_threshold); + const clamp = (v) => Number.isNaN(v) ? null : Math.min(1, Math.max(0, v)); + const threshold = clamp(Number(args?.threshold ?? settings.score_threshold)); const source = String(args?.source ?? ''); const attachments = source ? getDataBankAttachmentsForSource(source, false) : getDataBankAttachments(false); const collectionIds = await ingestDataBankAttachments(String(source)); @@ -1459,7 +1460,7 @@ jQuery(async () => { 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. Uses the global config value if not set.', ARGUMENT_TYPE.NUMBER, false, false, ''), + 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('source', 'Optional filter for the attachments by source.', ARGUMENT_TYPE.STRING, false, false, '', ['global', 'character', 'chat']), ], unnamedArgumentList: [ From e3ec65fd31fa14513954e64f932e878958306596 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 30 May 2024 17:58:17 +0300 Subject: [PATCH 36/44] Collapse send buttons when commands are executed --- public/style.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/style.css b/public/style.css index ff355a226..90683552b 100644 --- a/public/style.css +++ b/public/style.css @@ -690,8 +690,13 @@ body .panelControlBar { #form_sheld.isExecutingCommandsFromChatInput { - #send_but { + + #send_but, + #mes_continue { visibility: hidden; + width: 0; + height: 0; + opacity: 0; } #rightSendForm>div:not(.mes_send).stscript_btn { From e0ba5165515d54edb1bacd54b10b701a09cc59fc Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 30 May 2024 18:01:04 +0300 Subject: [PATCH 37/44] Transition only opacity on send form buttons --- public/style.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/style.css b/public/style.css index 90683552b..6f7d25d1b 100644 --- a/public/style.css +++ b/public/style.css @@ -656,12 +656,11 @@ body .panelControlBar { outline: none; border: none; cursor: pointer; - transition: 0.3s; opacity: 0.7; display: flex; align-items: center; justify-content: center; - transition: all 300ms; + transition: opacity 300ms; } #rightSendForm>div:hover, From 6228d1d3b1806ce882511f1b611d8bbddda7fc1c Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 30 May 2024 21:04:22 +0300 Subject: [PATCH 38/44] Add schedulers selection for AUTO1111 --- .../extensions/stable-diffusion/index.js | 31 +++++++++++++++++-- .../extensions/stable-diffusion/settings.html | 10 +++--- src/endpoints/stable-diffusion.js | 25 +++++++++++++++ 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 05797c0d4..dc104103f 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -858,6 +858,7 @@ async function onSourceChange() { extension_settings.sd.source = $('#sd_source').find(':selected').val(); extension_settings.sd.model = null; extension_settings.sd.sampler = null; + extension_settings.sd.scheduler = null; toggleSourceControls(); saveSettingsDebounced(); await loadSettingOptions(); @@ -1205,6 +1206,26 @@ async function getAutoRemoteUpscalers() { } } +async function getAutoRemoteSchedulers() { + try { + const result = await fetch('/api/sd/schedulers', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify(getSdRequestBody()), + }); + + if (!result.ok) { + throw new Error('SD WebUI returned an error.'); + } + + const data = await result.json(); + return data; + } catch (error) { + console.error(error); + return ['N/A']; + } +} + async function getVladRemoteUpscalers() { try { const result = await fetch('/api/sd/sd-next/upscalers', { @@ -1820,13 +1841,13 @@ async function loadSchedulers() { schedulers = ['N/A']; break; case sources.auto: - schedulers = ['N/A']; + schedulers = await getAutoRemoteSchedulers(); break; case sources.novel: schedulers = ['N/A']; break; case sources.vlad: - schedulers = ['N/A']; + schedulers = await getAutoRemoteSchedulers(); break; case sources.drawthings: schedulers = ['N/A']; @@ -1852,6 +1873,11 @@ async function loadSchedulers() { option.selected = scheduler === extension_settings.sd.scheduler; $('#sd_scheduler').append(option); } + + if (!extension_settings.sd.scheduler && schedulers.length > 0 && schedulers[0] !== 'N/A') { + extension_settings.sd.scheduler = schedulers[0]; + $('#sd_scheduler').val(extension_settings.sd.scheduler).trigger('change'); + } } async function loadComfySchedulers() { @@ -2529,6 +2555,7 @@ async function generateAutoImage(prompt, negativePrompt) { prompt: prompt, negative_prompt: negativePrompt, sampler_name: extension_settings.sd.sampler, + scheduler: extension_settings.sd.scheduler, steps: extension_settings.sd.steps, cfg_scale: extension_settings.sd.scale, width: extension_settings.sd.width, diff --git a/public/scripts/extensions/stable-diffusion/settings.html b/public/scripts/extensions/stable-diffusion/settings.html index 0753ca58e..d87ed88df 100644 --- a/public/scripts/extensions/stable-diffusion/settings.html +++ b/public/scripts/extensions/stable-diffusion/settings.html @@ -196,6 +196,8 @@ + + @@ -220,9 +222,7 @@
- - -
+
@@ -240,7 +240,7 @@ Hires. Fix
-
+
@@ -260,7 +260,7 @@
-
+
(-1 for random) diff --git a/src/endpoints/stable-diffusion.js b/src/endpoints/stable-diffusion.js index ce034b179..a93582803 100644 --- a/src/endpoints/stable-diffusion.js +++ b/src/endpoints/stable-diffusion.js @@ -160,6 +160,31 @@ router.post('/samplers', jsonParser, async (request, response) => { } }); +router.post('/schedulers', jsonParser, async (request, response) => { + try { + const url = new URL(request.body.url); + url.pathname = '/sdapi/v1/schedulers'; + + const result = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': getBasicAuthHeader(request.body.auth), + }, + }); + + if (!result.ok) { + throw new Error('SD WebUI returned an error.'); + } + + const data = await result.json(); + const names = data.map(x => x.name); + return response.send(names); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } +}); + router.post('/models', jsonParser, async (request, response) => { try { const url = new URL(request.body.url); From 760af1225233f35f646348f13d2e8eebf8fc48f9 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 30 May 2024 21:09:53 +0300 Subject: [PATCH 39/44] Update AI Horde client library --- package-lock.json | 20 ++++++++------------ package.json | 7 ++----- src/endpoints/horde.js | 11 +++++------ 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0aa9404ba..287fef74b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@agnai/sentencepiece-js": "^1.1.1", "@agnai/web-tokenizers": "^0.1.3", - "@zeldafan0225/ai_horde": "^4.0.1", + "@zeldafan0225/ai_horde": "^5.1.0", "archiver": "^7.0.1", "bing-translate-api": "^2.9.1", "body-parser": "^1.20.2", @@ -880,12 +880,14 @@ "license": "ISC" }, "node_modules/@zeldafan0225/ai_horde": { - "version": "4.0.1", - "license": "MIT", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@zeldafan0225/ai_horde/-/ai_horde-5.1.0.tgz", + "integrity": "sha512-rPC0nmmFSXK808Oon0zFPA7yGSUKBXiLtMejkmKTyfAzzOHHQt/i2lO4ccfN2e355LzX1lBLwSi+nlATVA43Sw==", "dependencies": { - "@thunder04/supermap": "^3.0.2", - "centra": "^2.5.0", - "esbuild": "^0.12.28" + "@thunder04/supermap": "^3.0.2" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/abort-controller": { @@ -2122,12 +2124,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/esbuild": { - "name": "dry-uninstall", - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/dry-uninstall/-/dry-uninstall-0.3.0.tgz", - "integrity": "sha512-b8h94RVpETWkVV59x62NsY++79bM7Si6Dxq7a4iVxRcJU3ZJJ4vaiC7wUZwM8WDK0ySRL+i+T/1SMAzbJLejYA==" - }, "node_modules/escalade": { "version": "3.1.1", "license": "MIT", diff --git a/package.json b/package.json index a3bea6544..414cc3f98 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "dependencies": { "@agnai/sentencepiece-js": "^1.1.1", "@agnai/web-tokenizers": "^0.1.3", - "@zeldafan0225/ai_horde": "^4.0.1", + "@zeldafan0225/ai_horde": "^5.1.0", "archiver": "^7.0.1", "bing-translate-api": "^2.9.1", "body-parser": "^1.20.2", @@ -44,7 +44,7 @@ "yauzl": "^2.10.0" }, "engines": { - "node": ">= 18" + "node": ">= 18" }, "overrides": { "parse-bmfont-xml": { @@ -59,9 +59,6 @@ "axios": { "follow-redirects": "^1.15.4" }, - "@zeldafan0225/ai_horde": { - "esbuild": "npm:dry-uninstall" - }, "node-fetch": { "whatwg-url": "^14.0.0" } diff --git a/src/endpoints/horde.js b/src/endpoints/horde.js index e7528d6b7..e229fb910 100644 --- a/src/endpoints/horde.js +++ b/src/endpoints/horde.js @@ -1,6 +1,6 @@ const fetch = require('node-fetch').default; const express = require('express'); -const AIHorde = require('@zeldafan0225/ai_horde'); +const { AIHorde, ModelGenerationInputStableSamplers, ModelInterrogationFormTypes, HordeAsyncRequestStates } = require('@zeldafan0225/ai_horde'); const { getVersion, delay, Cache } = require('../util'); const { readSecret, SECRET_KEYS } = require('./secrets'); const { jsonParser } = require('../express-common'); @@ -191,8 +191,7 @@ router.post('/generate-text', jsonParser, async (request, response) => { router.post('/sd-samplers', jsonParser, async (_, response) => { try { - const ai_horde = await getHordeClient(); - const samplers = Object.values(ai_horde.ModelGenerationInputStableSamplers); + const samplers = Object.values(ModelGenerationInputStableSamplers); response.send(samplers); } catch (error) { console.error(error); @@ -217,7 +216,7 @@ router.post('/caption-image', jsonParser, async (request, response) => { const ai_horde = await getHordeClient(); const result = await ai_horde.postAsyncInterrogate({ source_image: request.body.image, - forms: [{ name: AIHorde.ModelInterrogationFormTypes.caption }], + forms: [{ name: ModelInterrogationFormTypes.caption }], }, { token: api_key_horde }); if (!result.id) { @@ -233,7 +232,7 @@ router.post('/caption-image', jsonParser, async (request, response) => { const status = await ai_horde.getInterrogationStatus(result.id); console.log(status); - if (status.state === AIHorde.HordeAsyncRequestStates.done) { + if (status.state === HordeAsyncRequestStates.done) { if (status.forms === undefined) { console.error('Image interrogation request failed: no forms found.'); @@ -251,7 +250,7 @@ router.post('/caption-image', jsonParser, async (request, response) => { return response.send({ caption }); } - if (status.state === AIHorde.HordeAsyncRequestStates.faulted || status.state === AIHorde.HordeAsyncRequestStates.cancelled) { + if (status.state === HordeAsyncRequestStates.faulted || status.state === HordeAsyncRequestStates.cancelled) { console.log('Image interrogation request is not successful.'); return response.sendStatus(503); } From e660ec1f14509b667d38da45e76cadb7531b36da Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 30 May 2024 21:23:13 +0300 Subject: [PATCH 40/44] Remove stray newlines from WI/AN entries --- public/scripts/world-info.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index f971acc56..72a52d458 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -3071,7 +3071,7 @@ async function checkWorldInfo(chat, maxContext) { if (shouldWIAddPrompt) { const originalAN = context.extensionPrompts[NOTE_MODULE_NAME].value; - const ANWithWI = `${ANTopEntries.join('\n')}\n${originalAN}\n${ANBottomEntries.join('\n')}`; + const ANWithWI = `${ANTopEntries.join('\n')}\n${originalAN}\n${ANBottomEntries.join('\n')}`.replace(/(^\n)|(\n$)/g, ''); context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan, chat_metadata[metadata_keys.role]); } From 62eb790b0b2ba5815074182e892d0422b8b3c6ea Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 30 May 2024 22:03:51 +0300 Subject: [PATCH 41/44] Add /translate command --- public/scripts/extensions/translate/index.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/public/scripts/extensions/translate/index.js b/public/scripts/extensions/translate/index.js index dbc8ab587..20519db5b 100644 --- a/public/scripts/extensions/translate/index.js +++ b/public/scripts/extensions/translate/index.js @@ -12,6 +12,9 @@ import { } from '../../../script.js'; import { extension_settings, getContext } from '../../extensions.js'; import { findSecret, secret_state, writeSecret } from '../../secrets.js'; +import { SlashCommand } from '../../slash-commands/SlashCommand.js'; +import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; +import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { splitRecursive } from '../../utils.js'; export const autoModeOptions = { @@ -649,4 +652,21 @@ jQuery(() => { eventSource.on(event_types.MESSAGE_UPDATED, handleMessageEdit); document.body.classList.add('translate'); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'translate', + helpString: 'Translate text to a target language. If target language is not provided, the value from the extension settings will be used.', + namedArgumentList: [ + new SlashCommandNamedArgument('target', 'The target language code to translate to', ARGUMENT_TYPE.STRING, false, false, '', Object.values(languageCodes)), + ], + unnamedArgumentList: [ + new SlashCommandArgument('The text to translate', ARGUMENT_TYPE.STRING, true, false, ''), + ], + callback: async (args, value) => { + const target = args?.target && Object.values(languageCodes).includes(String(args.target)) + ? String(args.target) + : extension_settings.translate.target_language; + return await translate(String(value), target); + }, + })); }); From 886f5adce70777f3e0f939305565e6a29208fdd1 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 30 May 2024 22:18:00 +0300 Subject: [PATCH 42/44] Add /db-enable and /db-disable commands --- public/scripts/chats.js | 4 +- .../scripts/extensions/attachments/index.js | 123 ++++++++++++++++-- 2 files changed, 111 insertions(+), 16 deletions(-) diff --git a/public/scripts/chats.js b/public/scripts/chats.js index cc644362d..f7bc0f361 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -1265,7 +1265,7 @@ function ensureAttachmentsExist() { } /** - * Gets all currently available attachments. Ignores disabled attachments. + * Gets all currently available attachments. Ignores disabled attachments by default. * @param {boolean} [includeDisabled=false] If true, include disabled attachments * @returns {FileAttachment[]} List of attachments */ @@ -1279,7 +1279,7 @@ export function getDataBankAttachments(includeDisabled = false) { } /** - * Gets all attachments for a specific source. + * Gets all attachments for a specific source. Includes disabled attachments by default. * @param {string} source Attachment source * @param {boolean} [includeDisabled=true] If true, include disabled attachments * @returns {FileAttachment[]} List of attachments diff --git a/public/scripts/extensions/attachments/index.js b/public/scripts/extensions/attachments/index.js index 0c6d13341..d43d1d0a7 100644 --- a/public/scripts/extensions/attachments/index.js +++ b/public/scripts/extensions/attachments/index.js @@ -1,5 +1,5 @@ import { deleteAttachment, getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment, uploadFileAttachmentToServer } from '../../chats.js'; -import { renderExtensionTemplateAsync } from '../../extensions.js'; +import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; @@ -12,7 +12,7 @@ const TYPES = ['global', 'character', 'chat']; const FIELDS = ['name', 'url']; /** - * Get attachments from the data bank + * Get attachments from the data bank. Includes disabled attachments. * @param {string} [source] Source for the attachments * @returns {import('../../chats').FileAttachment[]} List of attachments */ @@ -21,7 +21,37 @@ function getAttachments(source) { return getDataBankAttachments(true); } - return getDataBankAttachmentsForSource(source); + return getDataBankAttachmentsForSource(source, true); +} + +/** + * Get attachment by a single name or URL. + * @param {import('../../chats').FileAttachment[]} attachments List of attachments + * @param {string} value Name or URL of the attachment + * @returns {import('../../chats').FileAttachment} Attachment + */ +function getAttachmentByField(attachments, value) { + const match = (a) => String(a).trim().toLowerCase() === String(value).trim().toLowerCase(); + const fullMatchByURL = attachments.find(it => match(it.url)); + const fullMatchByName = attachments.find(it => match(it.name)); + return fullMatchByURL || fullMatchByName; +} + +/** + * Get attachment by multiple fields. + * @param {import('../../chats').FileAttachment[]} attachments List of attachments + * @param {string[]} values Name and URL of the attachment to search for + * @returns + */ +function getAttachmentByFields(attachments, values) { + for (const value of values) { + const attachment = getAttachmentByField(attachments, value); + if (attachment) { + return attachment; + } + } + + return null; } /** @@ -48,10 +78,7 @@ async function getDataBankText(args, value) { } const attachments = getAttachments(args?.source); - const match = (a) => String(a).trim().toLowerCase() === String(value).trim().toLowerCase(); - const fullMatchByURL = attachments.find(it => match(it.url)); - const fullMatchByName = attachments.find(it => match(it.name)); - const attachment = fullMatchByURL || fullMatchByName; + const attachment = getAttachmentByField(attachments, value); if (!attachment) { toastr.warning('Attachment not found.'); @@ -85,9 +112,7 @@ async function uploadDataBankAttachment(args, value) { async function updateDataBankAttachment(args, value) { const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat'; const attachments = getAttachments(source); - const fullMatchByURL = attachments.find(it => String(it.url).trim().toLowerCase() === String(args?.url).trim().toLowerCase()); - const fullMatchByName = attachments.find(it => String(it.name).trim().toLowerCase() === String(args?.name).trim().toLowerCase()); - const attachment = fullMatchByURL || fullMatchByName; + const attachment = getAttachmentByFields(attachments, [args?.url, args?.name]); if (!attachment) { toastr.warning('Attachment not found.'); @@ -109,10 +134,7 @@ async function updateDataBankAttachment(args, value) { async function deleteDataBankAttachment(args, value) { const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat'; const attachments = getAttachments(source); - const match = (a) => String(a).trim().toLowerCase() === String(value).trim().toLowerCase(); - const fullMatchByURL = attachments.find(it => match(it.url)); - const fullMatchByName = attachments.find(it => match(it.name)); - const attachment = fullMatchByURL || fullMatchByName; + const attachment = getAttachmentByField(attachments, value); if (!attachment) { toastr.warning('Attachment not found.'); @@ -123,6 +145,53 @@ async function deleteDataBankAttachment(args, value) { return ''; } +/** + * Callback for disabling an attachment in the data bank. + * @param {object} args Named arguments + * @param {string} value Name or URL of the attachment + * @returns {Promise} Empty string + */ +async function disableDataBankAttachment(args, value) { + const attachments = getAttachments(args?.source); + const attachment = getAttachmentByField(attachments, value); + + if (!attachment) { + toastr.warning('Attachment not found.'); + return ''; + } + + if (extension_settings.disabled_attachments.includes(attachment.url)) { + return ''; + } + + extension_settings.disabled_attachments.push(attachment.url); + return ''; +} + +/** + * Callback for enabling an attachment in the data bank. + * @param {object} args Named arguments + * @param {string} value Name or URL of the attachment + * @returns {Promise} Empty string + */ +async function enableDataBankAttachment(args, value) { + const attachments = getAttachments(args?.source); + const attachment = getAttachmentByField(attachments, value); + + if (!attachment) { + toastr.warning('Attachment not found.'); + return ''; + } + + const index = extension_settings.disabled_attachments.indexOf(attachment.url); + if (index === -1) { + return ''; + } + + extension_settings.disabled_attachments.splice(index, 1); + return ''; +} + jQuery(async () => { const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {}); $('#extensionsMenu').prepend(buttons); @@ -191,6 +260,32 @@ jQuery(async () => { returns: ARGUMENT_TYPE.STRING, })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'db-disable', + callback: disableDataBankAttachment, + aliases: ['databank-disable', 'data-bank-disable'], + helpString: 'Disable an attachment in the Data Bank by its name or URL. Optionally, provide the source of the attachment.', + namedArgumentList: [ + new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES), + ], + unnamedArgumentList: [ + new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false), + ], + })); + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'db-enable', + callback: enableDataBankAttachment, + aliases: ['databank-enable', 'data-bank-enable'], + helpString: 'Enable an attachment in the Data Bank by its name or URL. Optionally, provide the source of the attachment.', + namedArgumentList: [ + new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES), + ], + unnamedArgumentList: [ + new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false), + ], + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'db-delete', callback: deleteDataBankAttachment, From 7af27bb6a9e8252166e42aa8176ec32e2ee7c5f2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 30 May 2024 22:42:21 +0300 Subject: [PATCH 43/44] Remove schedulers from SD.Next --- public/scripts/extensions/stable-diffusion/index.js | 2 +- public/scripts/extensions/stable-diffusion/settings.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index dc104103f..10937f504 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -1847,7 +1847,7 @@ async function loadSchedulers() { schedulers = ['N/A']; break; case sources.vlad: - schedulers = await getAutoRemoteSchedulers(); + schedulers = ['N/A']; break; case sources.drawthings: schedulers = ['N/A']; diff --git a/public/scripts/extensions/stable-diffusion/settings.html b/public/scripts/extensions/stable-diffusion/settings.html index d87ed88df..a419162e5 100644 --- a/public/scripts/extensions/stable-diffusion/settings.html +++ b/public/scripts/extensions/stable-diffusion/settings.html @@ -222,7 +222,7 @@
-
+
From 07cfc1fb0be40095441c775efc2c18f3ab4509de Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 30 May 2024 22:51:43 +0300 Subject: [PATCH 44/44] Fix CLIP skip for SD.Next --- public/scripts/extensions/stable-diffusion/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 10937f504..8e272e513 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -2567,10 +2567,13 @@ async function generateAutoImage(prompt, negativePrompt) { denoising_strength: extension_settings.sd.denoising_strength, hr_second_pass_steps: extension_settings.sd.hr_second_pass_steps, seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, + // For AUTO1111 override_settings: { CLIP_stop_at_last_layers: extension_settings.sd.clip_skip, }, override_settings_restore_afterwards: true, + // For SD.Next + clip_skip: extension_settings.sd.clip_skip, // Ensure generated img is saved to disk save_images: true, send_images: true,