|
// ==UserScript== |
|
// @name NAI A1111-style attention editing |
|
// @namespace hdg-nai-a1111-prompt |
|
// @match https://novelai.net/* |
|
// @grant none |
|
// @version 1.2.5 |
|
// @author Anonymous |
|
// @description Make NAI Prompting Great Again |
|
// @require https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@2,npm/@violentmonkey/ui@latest |
|
// @updateURL https://gist.github.com/catboxanon/9c3003f19bfb3b306d3e47bdd6b68ca7/raw/hdg-nai-a1111-prompt.user.js |
|
// @downloadURL https://gist.github.com/catboxanon/9c3003f19bfb3b306d3e47bdd6b68ca7/raw/hdg-nai-a1111-prompt.user.js |
|
// ==/UserScript== |
|
|
|
(async function() { |
|
let opts = { |
|
"keyedit_delimiters": ",\\/!?%^*;{}=`~[]$:<>\r\n\t", |
|
"keyedit_delimiters_whitespace": [ |
|
"Tab", |
|
"Carriage Return", |
|
"Line Feed" |
|
], |
|
"keyedit_target": 'textarea[autocomplete="off"]', |
|
"observer_auto_save": false, |
|
"observer_auto_generate": false, |
|
"wildcards": false, |
|
} |
|
|
|
const optsNames = { |
|
"observer_auto_save": "Auto save", |
|
"observer_auto_generate": "Generate forever", |
|
"wildcards": "Enable wildcard support (CTRL + \\)" |
|
} |
|
|
|
const reExtra = /<(?:[^:^>]+:([^:]+))>/gm; |
|
|
|
let savedOpts = localStorage.getItem('hdg-nai-a1111-prompt'); |
|
|
|
if (savedOpts) { |
|
try { |
|
const parsedOpts = JSON.parse(savedOpts); |
|
for (const key in parsedOpts) { |
|
if (Object.keys(opts).includes(key)) { |
|
opts[key] = parsedOpts[key]; |
|
} |
|
} |
|
} catch(err) { |
|
console.error(err); |
|
} |
|
} else { |
|
localStorage.setItem('hdg-nai-a1111-prompt', JSON.stringify(opts)); |
|
} |
|
|
|
function timeout(ms) { |
|
return new Promise(resolve => setTimeout(resolve, ms)); |
|
} |
|
|
|
function initSettingsPanel() { |
|
let form = ''; |
|
for (const key in opts) { |
|
let value = opts[key]; |
|
let valueLabel = optsNames[key]; |
|
|
|
const excludedKeys = [ |
|
'keyedit_delimiters', |
|
'keyedit_target', |
|
]; |
|
|
|
if (excludedKeys.includes(key)) { |
|
continue; |
|
} |
|
if (Array.isArray(value)) { |
|
continue; |
|
} else if (typeof value === 'boolean') { |
|
form += ` |
|
<label for="${key}">${valueLabel}</label><br> |
|
<input type="checkbox" id="${key}" name="${key}" ${value ? 'checked' : ''}><br> |
|
`; |
|
} else { |
|
form += ` |
|
<label for="${key}">${key}</label><br> |
|
<input type="text" id="${key}" name="${key}" value="${value}"><br> |
|
`; |
|
} |
|
} |
|
form += `<button type="submit" id="save" value="Save">Save</button>`; |
|
form += `<button type="button" id="close">Close</button>`; |
|
form = `<form id="hdgnaia1111prompt">` + form; |
|
form += `</form>`; |
|
|
|
const settingsPanel = VM.getPanel({ |
|
content: form, |
|
theme: 'dark', |
|
}); |
|
|
|
settingsPanel.wrapper.style.position = 'fixed'; |
|
settingsPanel.wrapper.style.left = '50%'; |
|
settingsPanel.wrapper.style.top = '50%'; |
|
settingsPanel.wrapper.style.transform = 'translate(-50%, -50%)'; |
|
|
|
settingsPanel.body.innerHTML = form; |
|
|
|
const formEl = settingsPanel.body.querySelector('form'); |
|
formEl.addEventListener('submit', (evt) => { |
|
evt.preventDefault(); |
|
|
|
const optsArray = Array.from(evt.target.querySelectorAll('input')).map((x) => ( |
|
[`${x.id}`, x.type === "checkbox" ? x.checked : x.value] |
|
)); |
|
for (const opt of optsArray) { |
|
let optKey = opt[0]; |
|
let optVal = opt[1]; |
|
|
|
if (typeof optVal === 'string' || optVal instanceof String) { |
|
optVal = optVal === "on" ? true : false; |
|
} |
|
|
|
opts[optKey] = optVal; |
|
}; |
|
|
|
localStorage.setItem('hdg-nai-a1111-prompt', JSON.stringify(opts)); |
|
settingsPanel.hide(); |
|
}); |
|
|
|
const closeButton = settingsPanel.body.querySelector('form #close') |
|
closeButton.addEventListener('click', (evt) => { |
|
evt.preventDefault(); |
|
settingsPanel.hide(); |
|
}) |
|
|
|
document.addEventListener('keydown', (evt) => { |
|
if (evt.key == "x" && evt.ctrlKey && evt.altKey) { |
|
if (document.querySelector(`#${settingsPanel.id}`)) { |
|
settingsPanel.hide(); |
|
} else { |
|
settingsPanel.show(); |
|
} |
|
} |
|
}); |
|
|
|
return settingsPanel; |
|
} |
|
|
|
const settingsPanel = initSettingsPanel(); |
|
|
|
function setNativeValue(element, value) { |
|
const { set: valueSetter } = Object.getOwnPropertyDescriptor(element, 'value') || {} |
|
const prototype = Object.getPrototypeOf(element) |
|
const { set: prototypeValueSetter } = Object.getOwnPropertyDescriptor(prototype, 'value') || {} |
|
|
|
if (prototypeValueSetter && valueSetter !== prototypeValueSetter) { |
|
prototypeValueSetter.call(element, value) |
|
} else if (valueSetter) { |
|
valueSetter.call(element, value) |
|
} else { |
|
throw new Error('The given element does not have a value setter') |
|
} |
|
} |
|
|
|
function keyupEditAttention(event) { |
|
let target = event.originalTarget || event.composedPath()[0]; |
|
|
|
if (!target.matches(opts.keyedit_target)) return; |
|
if (!(event.metaKey || event.ctrlKey)) return; |
|
|
|
let isPlus = event.key == "ArrowUp"; |
|
let isMinus = event.key == "ArrowDown"; |
|
if (!isPlus && !isMinus) return; |
|
|
|
let selectionStart = target.selectionStart; |
|
let selectionEnd = target.selectionEnd; |
|
let text = target.value; |
|
if (!(text.length > 0)) return; |
|
|
|
function selectCurrentWord() { |
|
if (selectionStart !== selectionEnd) return false; |
|
const whitespace_delimiters = { |
|
"Tab": "\t", |
|
"Carriage Return": "\r", |
|
"Line Feed": "\n" |
|
}; |
|
let delimiters = opts.keyedit_delimiters; |
|
|
|
for (let i of opts.keyedit_delimiters_whitespace) { |
|
delimiters += whitespace_delimiters[i]; |
|
} |
|
|
|
// seek backward to find beginning |
|
while (!delimiters.includes(text[selectionStart - 1]) && selectionStart > 0) { |
|
selectionStart--; |
|
} |
|
|
|
// seek forward to find end |
|
while (!delimiters.includes(text[selectionEnd]) && selectionEnd < text.length) { |
|
selectionEnd++; |
|
} |
|
|
|
// deselect surrounding whitespace |
|
while (target.textContent.slice(selectionStart, selectionStart + 1) == " " && selectionStart < selectionEnd) { |
|
selectionStart++; |
|
} |
|
while (target.textContent.slice(selectionEnd - 1, selectionEnd) == " " && selectionEnd > selectionStart) { |
|
selectionEnd--; |
|
} |
|
|
|
target.setSelectionRange(selectionStart, selectionEnd); |
|
return true; |
|
} |
|
|
|
selectCurrentWord(); |
|
|
|
event.preventDefault(); |
|
|
|
const start = selectionStart > 0 ? text[selectionStart - 1] : ""; |
|
const end = text[selectionEnd]; |
|
const deltaCurrent = !["{", "["].includes(start) ? 0 : (start == "{" ? 1 : -1); |
|
const deltaUser = isPlus ? 1 : -1; |
|
let selectionStartDelta = 0; |
|
let selectionEndDelta = 0; |
|
|
|
function addBrackets(str, isPlus) { |
|
if (isPlus) { |
|
str = `{${str}}`; |
|
} else { |
|
str = `[${str}]`; |
|
} |
|
return str; |
|
} |
|
|
|
/* modify text */ |
|
let modifiedText = text.slice(selectionStart, selectionEnd); |
|
if (deltaCurrent == 0 || deltaCurrent == deltaUser) { |
|
modifiedText = addBrackets(modifiedText, isPlus); |
|
selectionStartDelta += 1; |
|
selectionEndDelta += 1; |
|
} else { |
|
selectionStart--; |
|
selectionEnd++; |
|
selectionEndDelta -= 2; |
|
} |
|
|
|
text = text.slice(0, selectionStart) |
|
+ modifiedText |
|
+ text.slice(selectionEnd); |
|
|
|
target.focus(); |
|
setNativeValue(target, text); |
|
target.selectionStart = selectionStart + selectionStartDelta; |
|
target.selectionEnd = selectionEnd + selectionEndDelta; |
|
|
|
target.dispatchEvent((new Event('input', { bubbles: true }))); |
|
} |
|
|
|
function getGenerateButton(el) { |
|
return el.querySelector('div button > span + div')?.parentElement; |
|
} |
|
|
|
async function generateWithWildcards() { |
|
const prompt = document.querySelector(opts.keyedit_target); |
|
if (!prompt) { |
|
return; |
|
} |
|
|
|
let originalPrompt = prompt.value; |
|
let newPrompt = ''; |
|
const parts = prompt.value.split(reExtra); |
|
for (const part of parts) { |
|
if (part.length <= 0) { |
|
continue; |
|
} |
|
const choices = part.split('$'); |
|
const choice = choices[~~(Math.random() * choices.length)]; |
|
newPrompt += choice; |
|
} |
|
|
|
console.debug(`Wildcard prompt: ${newPrompt}`); |
|
|
|
setNativeValue(prompt, newPrompt); |
|
prompt.dispatchEvent((new Event('input', { bubbles: true }))); |
|
|
|
const gen = getGenerateButton(document); |
|
if (!gen) { |
|
return |
|
} |
|
|
|
// lol |
|
setTimeout(() => { |
|
gen.click(); |
|
}, 250); |
|
setTimeout(() => { |
|
setNativeValue(prompt, originalPrompt); |
|
prompt.dispatchEvent((new Event('input', { bubbles: true }))); |
|
setTimeout(() => { |
|
prompt.focus(); |
|
setTimeout(() => { |
|
prompt.blur(); |
|
setTimeout(() => { |
|
prompt.focus(); |
|
}, 250); |
|
}, 250); |
|
}, 250); |
|
}, 500); |
|
} |
|
|
|
window.addEventListener('keydown', (event) => { |
|
keyupEditAttention(event); |
|
}); |
|
|
|
document.addEventListener('keydown', (evt) => { |
|
if (evt.key == "\\" && evt.ctrlKey && !evt.shiftKey && !evt.altKey) { |
|
evt.preventDefault(); |
|
evt.stopPropagation(); |
|
|
|
if (opts['wildcards']) { |
|
generateWithWildcards(); |
|
} |
|
} |
|
}); |
|
|
|
window.addEventListener('keyup', (event) => { |
|
let target = event.originalTarget || event.composedPath()[0]; |
|
const tagSuggestionsExist = document.querySelector('div[style*="opacity: 1"][style*="transform: none"] span'); |
|
if (tagSuggestionsExist && target.matches(opts.keyedit_target) && (event.metaKey || event.ctrlKey)) { |
|
event.preventDefault(); |
|
event.stopPropagation(); |
|
keyupEditAttention(event); |
|
} |
|
}); |
|
|
|
async function observers() { |
|
(new MutationObserver((mutations) => { |
|
for (const mutation of mutations) { |
|
// Ignore all mutations when settings page is open |
|
if (document.querySelector(`#${settingsPanel.id}`)) { |
|
break; |
|
} |
|
|
|
// Save image on each generation finish |
|
try { |
|
if ( |
|
mutation?.target |
|
&& mutation?.target?.firstChild |
|
&& !mutation?.target?.id |
|
&& mutation?.target?.firstChild?.getAttribute('role') == 'button' |
|
&& mutation?.target?.firstChild?.getAttribute('aria-label') != null |
|
) { |
|
setTimeout(() => { |
|
const saveButtons = Array.from( |
|
document.querySelectorAll('div[data-projection-id] button')) |
|
.filter(button => { |
|
if (!button.firstChild || button.firstChild.nodeType != 1) { |
|
return false; |
|
} |
|
const computedStyles = getComputedStyle(button.firstChild); |
|
const maskImage = Array.from(computedStyles).filter((css) => (css.includes('maskImage') || css.includes('mask-image')))?.[0]; |
|
if (maskImage && computedStyles[maskImage].includes('/save.')) { |
|
return true; |
|
} |
|
return false; |
|
}); |
|
if (saveButtons.length > 0 && opts['observer_auto_save']) { |
|
for (const saveButton of saveButtons) { |
|
saveButton.click(); |
|
} |
|
} |
|
}, 500); |
|
} |
|
} catch { ; } |
|
} |
|
})).observe(document.body, {childList: true, subtree: true}); |
|
|
|
(new MutationObserver((mutations) => { |
|
for (const mutation of mutations) { |
|
// Generate forever |
|
const generateButton = getGenerateButton(mutation?.target?.parentElement); |
|
if ( |
|
generateButton |
|
&& mutation?.target |
|
&& mutation?.target.childElementCount <= 2 |
|
&& !generateButton.offsetParent |
|
&& generateButton.getAttribute('disabled') === null |
|
) { |
|
setTimeout(() => { |
|
if (!document.querySelector('#historyContainer div[role="button"][aria-label]')) { |
|
return; |
|
} |
|
|
|
if (opts['observer_auto_generate'] && opts['wildcards']) { |
|
generateWithWildcards(); |
|
} |
|
|
|
if (opts['observer_auto_generate']) { |
|
generateButton.click(); |
|
} |
|
}, 500); |
|
} |
|
} |
|
})).observe(document.body, {attributes: true, subtree: true, attributeFilter: ['disabled']}); |
|
} |
|
|
|
await observers(); |
|
})(); |
ugh, seems like they broke this. Seems like the boxes aren't even textareas anymore.
Don't know why they haven't put something like this in the base site yet. Feels bad to use without it.