// ==UserScript==
// @name NovelAI 快捷键权重调整
// @namespace https://novelai.net
// @match https://novelai.net/image
// @match https://novelai.github.io/image
// @icon https://novelai.net/_next/static/media/goose_blue.1580a990.svg
// @license MIT
// @version 1.7
// @author Takoro
// @description 通过Ctrl+↑/↓快速调整输入光标所在的Tag权重,Ctrl+←/→移动Tag位置(使用时会自动格式化提示词,删除多余空格+逗号,自动在逗号后添加空格,不会删除换行)。支持所有Prompt输入框。
// ==/UserScript==
(function() {
'use strict';
function getActiveInputElement() {
const selection = window.getSelection();
if (!selection.rangeCount) return null;
const node = selection.focusNode;
const pElement = node.nodeType === 3 ? node.parentElement.closest('p') : node.closest('p');
if (pElement && (
pElement.closest('.prompt-input-box-prompt') ||
pElement.closest('.prompt-input-box-base-prompt') ||
pElement.closest('.prompt-input-box-negative-prompt') ||
pElement.closest('.prompt-input-box-undesired-content') ||
pElement.closest('[class*="character-prompt-input"]')
)) {
return pElement;
}
return null;
}
function getSelectedTagInfo(inputElement) {
if (!inputElement) return null;
const selection = window.getSelection();
if (!selection.rangeCount) return null;
const range = selection.getRangeAt(0);
const node = range.startContainer;
const offset = range.startOffset;
const fullText = inputElement.textContent || '';
let globalOffset = 0;
if (node.nodeType === 3) {
const treeWalker = document.createTreeWalker(
inputElement,
NodeFilter.SHOW_TEXT
);
let currentNode;
while ((currentNode = treeWalker.nextNode())) {
if (currentNode === node) break;
globalOffset += currentNode.length;
}
globalOffset += offset;
} else {
globalOffset = offset;
}
let start = globalOffset;
while (start > 0 && fullText[start - 1] !== ',' && fullText[start - 1] !== '\n') {
start--;
}
let end = globalOffset;
while (end < fullText.length && fullText[end] !== ',' && fullText[end] !== '\n') {
end++;
}
if (fullText[start] === ',' || fullText[start] === '\n') start++;
if (fullText[end - 1] === ',') end--;
const tagText = fullText.slice(start, end).trim();
return tagText ? { tagText, start, end, fullText } : null;
}
function parseWeight(text) {
const cleanText = text.replace(/:{2,}/g, '::').replace(/:+$/, '');
const weightMatch = cleanText.match(/^(-?[\d.]+)::(.+?)(?:::|$)/);
if (weightMatch) {
const weight = parseFloat(weightMatch[1]);
return {
weight: isNaN(weight) ? 1.0 : weight,
tag: weightMatch[2].trim()
};
}
return { weight: 1.0, tag: text.trim() };
}
function adjustWeight(text, direction) {
const { weight, tag } = parseWeight(text);
let newWeight = weight + (direction * 0.1);
newWeight = Math.round(newWeight * 10) / 10;
if (Math.abs(newWeight - 1.0) < 0.001) {
return tag;
}
return `${newWeight}::${tag}::`;
}
function modifyInputText(inputElement, newText, start, end, fullText) {
const newContent = fullText.slice(0, start) + newText + fullText.slice(end);
if (inputElement.childNodes.length === 1 && inputElement.firstChild.nodeType === 3) {
inputElement.firstChild.textContent = newContent;
} else {
const newTextNode = document.createTextNode(newContent);
inputElement.innerHTML = '';
inputElement.appendChild(newTextNode);
}
const newRange = document.createRange();
newRange.setStart(inputElement.firstChild, start);
newRange.setEnd(inputElement.firstChild, start + newText.length);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(newRange);
const inputEvent = new Event('input', { bubbles: true });
inputElement.dispatchEvent(inputEvent);
}
// 更新整个输入框内容并设置光标位置
function updateInputContent(inputElement, newContent, selectionStart, selectionEnd) {
if (inputElement.childNodes.length === 1 && inputElement.firstChild.nodeType === 3) {
inputElement.firstChild.textContent = newContent;
} else {
const newTextNode = document.createTextNode(newContent);
inputElement.innerHTML = '';
inputElement.appendChild(newTextNode);
}
const newRange = document.createRange();
newRange.setStart(inputElement.firstChild, selectionStart);
newRange.setEnd(inputElement.firstChild, selectionEnd);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(newRange);
const inputEvent = new Event('input', { bubbles: true });
inputElement.dispatchEvent(inputEvent);
}
// 移动标签函数
function moveTag(inputElement, tagInfo, direction) {
const { tagText, fullText } = tagInfo;
// 分割文本成标签数组,基于逗号和换行
const tags = fullText.split(/[\n,]+/).map(t => t.trim()).filter(t => t !== '');
// 找到当前标签的索引
const currentTag = tagText.trim();
const index = tags.findIndex(t => t === currentTag);
if (index === -1) return; // 未找到标签
let newIndex;
if (direction === -1) { // 向左移动
if (index === 0) return; // 已经在最左,无法移动
newIndex = index - 1;
[tags[index], tags[newIndex]] = [tags[newIndex], tags[index]]; // 交换
} else if (direction === 1) { // 向右移动
if (index === tags.length - 1) return; // 已经在最右,无法移动
newIndex = index + 1;
[tags[index], tags[newIndex]] = [tags[newIndex], tags[index]]; // 交换
} else {
return;
}
// 重新构建文本,用逗号和空格连接
const newFullText = tags.join(', ') + ',';
// 计算新文本中每个标签的位置
function getTagPositions(tags) {
let currentPos = 0;
const positions = [];
for (let i = 0; i < tags.length; i++) {
const start = currentPos;
const end = start + tags[i].length;
positions.push({ start, end });
currentPos = end;
if (i < tags.length - 1) {
currentPos += 2; // 添加 ', ' 的长度
}
}
// 添加最后一个逗号的位置
positions.push({ start: currentPos, end: currentPos + 1 });
return positions;
}
const positions = getTagPositions(tags);
const newStart = positions[newIndex].start;
const newEnd = positions[newIndex].end;
// 更新输入框
updateInputContent(inputElement, newFullText, newStart, newEnd);
}
function handleKeydown(event) {
const inputElement = getActiveInputElement();
if (!inputElement) return;
if (!event.ctrlKey) return;
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
event.preventDefault();
event.stopPropagation();
const tagInfo = getSelectedTagInfo(inputElement);
if (!tagInfo) return;
const direction = (event.key === 'ArrowUp') ? 1 : -1;
const newText = adjustWeight(tagInfo.tagText, direction);
modifyInputText(inputElement, newText, tagInfo.start, tagInfo.end, tagInfo.fullText);
} else if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
event.preventDefault();
event.stopPropagation();
const tagInfo = getSelectedTagInfo(inputElement);
if (!tagInfo) return;
const direction = (event.key === 'ArrowLeft') ? -1 : 1;
moveTag(inputElement, tagInfo, direction);
}
}
function init() {
const checkInterval = setInterval(() => {
const inputElement = getActiveInputElement();
if (inputElement) {
clearInterval(checkInterval);
document.addEventListener('keydown', handleKeydown, true);
}
}, 500);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();