|
// ==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(); |
|
})(); |
The require needs bumped to https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@2,npm/@violentmonkey/ui@0.7.9 for it to work
Browser: OperaGX
Userscript: Manager Violentmonkey