Tag suggestions and weight shortcuts for NovelAI prompts. 通过Ctrl+↑/↓快速调整输入光标所在的Tag权重,Ctrl+←/→移动Tag位置。自动格式化Prompt。
// ==UserScript==
// @name Novelai Prompt Helper / Novelai 提示词增强
// @namespace https://novelai.net
// @version 1.1.5
// @description Tag suggestions and weight shortcuts for NovelAI prompts. 通过Ctrl+↑/↓快速调整输入光标所在的Tag权重,Ctrl+←/→移动Tag位置。自动格式化Prompt。
// @author Takoro
// @match https://novelai.net/image
// @match https://novelai.github.io/image
// @icon https://www.google.com/s2/favicons?sz=64&domain=novelai.net
// @license MIT
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_openInTab
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @connect danbooru.donmai.us
// @connect raw.githubusercontent.com
// ==/UserScript==
(function () {
'use strict';
if (typeof window.__NAIWeightAdjusting !== 'boolean') {
window.__NAIWeightAdjusting = false;
}
if (typeof window.__NAIWeightAdjustingUntil !== 'number') {
window.__NAIWeightAdjustingUntil = 0;
}
const LOCALE_KEY = 'nai_prompt_helper_locale_v1';
const AUTOFORMAT_KEY = 'nai_prompt_helper_autoformat_enabled_v1';
const LOCALE_MESSAGES = {
zh: {
infoTitle: 'Did you mean...?',
hints: [
'按住 Ctrl + 左键点击标签可以打开对应的 Wiki 页面。',
'紫色标签代表系列/IP,绿色标签代表角色。',
'标签后面的数字表示相关作品数量。',
'可以尝试使用 Tab、Enter 或方向键选择候选项。',
'Takoro'
],
fallbackHint: 'Danbooru API 连接失败,已切换到本地缓存。',
apiTimeoutLog: '[NAI Prompt Helper] 网络请求超时,改用本地缓存。',
logReady: '[NAI Prompt Helper] 标签联想模块就绪。',
menuSwitchLabel: '切换到英文界面',
menuEnableFormat: '启用自动格式化',
menuDisableFormat: '禁用自动格式化',
},
en: {
infoTitle: 'Did you mean...?',
hints: [
'Ctrl + Click a tag to open its Danbooru wiki page.',
'Purple tags are series/IP names; green tags are characters.',
'Numbers after a tag show total post counts.',
'Use Tab, Enter, or arrow keys to pick a suggestion.',
'Takoro'
],
fallbackHint: 'Danbooru API error, switching to cached suggestions.',
apiTimeoutLog: '[NAI Prompt Helper] API timeout, using cached suggestions.',
logReady: '[NAI Prompt Helper] Tag suggestions ready.',
menuSwitchLabel: 'Switch to Chinese UI',
menuEnableFormat: 'Enable Auto-Formatting',
menuDisableFormat: 'Disable Auto-Formatting',
}
};
const SUPPORTED_LOCALES = Object.keys(LOCALE_MESSAGES);
function detectInitialLocale() {
try {
if (typeof GM_getValue === 'function') {
const stored = GM_getValue(LOCALE_KEY);
if (stored && SUPPORTED_LOCALES.includes(stored)) {
return stored;
}
}
} catch (error) { }
const nav = (navigator.language || navigator.userLanguage || '').toLowerCase();
if (nav.startsWith('zh')) { return 'zh'; }
return 'en';
}
let currentLocale = detectInitialLocale();
let autoFormatEnabled = true;
try {
if (typeof GM_getValue === 'function') {
const storedValue = GM_getValue(AUTOFORMAT_KEY);
if (typeof storedValue === 'boolean') {
autoFormatEnabled = storedValue;
} else {
GM_setValue(AUTOFORMAT_KEY, true);
}
}
} catch (e) { }
let registeredMenuIds = [];
const isChineseLocale = currentLocale === 'zh';
function t(key) {
const bundle = LOCALE_MESSAGES[currentLocale] || LOCALE_MESSAGES.en;
return bundle[key] ?? LOCALE_MESSAGES.en[key] ?? key;
}
function getHints() {
const bundle = LOCALE_MESSAGES[currentLocale] || LOCALE_MESSAGES.en;
return bundle.hints || [];
}
function clearLocaleMenus() {
if (typeof GM_unregisterMenuCommand !== 'function') return;
registeredMenuIds.forEach(id => {
try { GM_unregisterMenuCommand(id); } catch (error) { }
});
registeredMenuIds = [];
}
function registerMenus() {
if (typeof GM_registerMenuCommand !== 'function') return;
clearLocaleMenus();
const otherLocale = currentLocale === 'zh' ? 'en' : 'zh';
const localeLabel = LOCALE_MESSAGES[currentLocale].menuSwitchLabel;
try {
const localeCmdId = GM_registerMenuCommand(localeLabel, () => {
try {
if (typeof GM_setValue === 'function') {
GM_setValue(LOCALE_KEY, otherLocale);
}
} catch (error) { }
window.location.reload();
});
if (typeof localeCmdId !== 'undefined') {
registeredMenuIds.push(localeCmdId);
}
} catch (error) { }
const formatLabel = autoFormatEnabled ? t('menuDisableFormat') : t('menuEnableFormat');
try {
const formatCmdId = GM_registerMenuCommand(formatLabel, () => {
try {
if (typeof GM_setValue === 'function') {
GM_setValue(AUTOFORMAT_KEY, !autoFormatEnabled);
}
} catch (error) { }
window.location.reload();
});
if (typeof formatCmdId !== 'undefined') {
registeredMenuIds.push(formatCmdId);
}
} catch (error) { }
}
function formatPromptText(text) {
const structure = WeightShortcuts.parsePromptStructure(text);
WeightShortcuts.normalizeTree(structure);
return WeightShortcuts.serializeTree(structure);
}
registerMenus();
const TagAssist = (() => {
const MAX_SUGGESTIONS = 10;
const DEBOUNCE_DELAY = 400;
const API_BASE_URL = 'https://danbooru.donmai.us';
const TRANSLATION_URL = 'https://raw.githubusercontent.com/Yellow-Rush/zh_CN-Tags/main/danbooru.csv';
const CACHE_KEY = 'danbooru_translations_cache';
const CACHE_DURATION_MS = 7 * 24 * 60 * 60 * 1000;
const TITLE_COLOR = '#e3dccc';
const SHORT_QUERY_PREFIX_LENGTH = 2;
const ALLOWED_CHARS_REGEX = /^[a-zA-Z\d_\-\s'\^=@()\u4e00-\u9fa5\u3040-\u309F\u30A0-\u30FF]+$/;
const BREAK_CHARS = ',{}[]:.';
let popup = null;
let selectedIndex = -1;
let currentMatches = [];
let isPopupActive = false;
let isKeyboardNavigation = false;
let apiAbortController = null;
let lastKnownRange = null;
let translations = new Map();
GM_addStyle(`
.autocomplete-container { position: absolute; background: #0e0f21; border: 1px solid #3B3B52; border-radius: 8px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); max-height: 450px; display: flex; flex-direction: column; z-index: 100000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; color: #EAEAEB; min-width: 350px; max-width: 500px; }
.suggestion-info-display { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: #1a1c30; border-bottom: 1px solid #3B3B52; font-size: 13.5px; line-height: 1.6; min-height: 17px; font-weight: bold; }
.info-hint { color: #8a8a9e; font-weight: normal; font-size: 12px; margin-left: 10px; white-space: nowrap; }
.info-hint-error { color: #ff7b7b; font-weight: bold; font-size: 12px; margin-left: 10px; white-space: nowrap; }
.info-title { color: ${TITLE_COLOR}; }
.suggestion-scroll-wrapper { padding: 7px; overflow-y: auto; }
.suggestion-flex { display: flex; flex-wrap: wrap; gap: 6px; }
.suggestion-item { display: flex; align-items: center; justify-content: space-between; padding: 4px 10px; background: #22253f; border: 1px solid transparent; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; font-size: 14px; height: 26px; white-space: nowrap; flex-shrink: 0; }
.suggestion-item:hover, .suggestion-item.selected { background: #34395f; border-color: #F5F3C2; }
.suggestion-text { overflow: hidden; text-overflow: ellipsis; color: #EAEAEB; }
.suggestion-count { color: #8a8a9e; margin-left: 12px; font-size: 12px; }
.suggestion-item[data-category='4'] .suggestion-text { color: #a6f3a6; }
.suggestion-item[data-category='3'] .suggestion-text { color: #d6bcf5; }
`);
const normalizeQuery = (query) => query.trim().replace(/\s+/g, '_');
function shouldSkipWeightSuggestions(textBeforeCursor, delimiterIndex) {
if (delimiterIndex < 0 || textBeforeCursor[delimiterIndex] !== ':') return false;
if (textBeforeCursor.substring(delimiterIndex - 1, delimiterIndex + 1) === '::') { return false; }
let i = delimiterIndex - 1;
if (i >= 0 && textBeforeCursor[i] === ':') {
i--;
while (i >= 0 && /\s/.test(textBeforeCursor[i])) i--;
if (i >= 0 && /[\d.]/.test(textBeforeCursor[i])) {
let end = i;
while (i >= 0 && /[\d.]/.test(textBeforeCursor[i])) i--;
const numericPart = textBeforeCursor.substring(i + 1, end + 1);
if (numericPart && /^\d+(?:\.\d+)?$/.test(numericPart)) {
const precedingChar = i >= 0 ? textBeforeCursor[i] : '';
if (i < 0 || BREAK_CHARS.includes(precedingChar) || /\s/.test(precedingChar)) { return true; }
}
}
return false;
}
while (i >= 0 && /\s/.test(textBeforeCursor[i])) i--;
if (i < 0) return false;
let end = i;
while (i >= 0 && /[\d.]/.test(textBeforeCursor[i])) i--;
const numericPart = textBeforeCursor.substring(i + 1, end + 1);
if (!numericPart || !/^\d+(?:\.\d+)?$/.test(numericPart)) return false;
const precedingChar = i >= 0 ? textBeforeCursor[i] : '';
if (i >= 0 && !BREAK_CHARS.includes(precedingChar) && !/\s/.test(precedingChar)) return false;
return true;
}
function getClonedSelectionRange() {
const selection = window.getSelection();
if (!selection || !selection.rangeCount) return null;
return selection.getRangeAt(0).cloneRange();
}
function loadTranslations() {
if (!isChineseLocale) { translations = new Map(); return; }
const cachedData = GM_getValue(CACHE_KEY);
if (cachedData && cachedData.timestamp && (Date.now() - cachedData.timestamp < CACHE_DURATION_MS)) {
translations = new Map(cachedData.translations); return;
}
GM_xmlhttpRequest({
method: "GET", url: TRANSLATION_URL,
onload: function (response) {
if (response.status === 200) {
const lines = response.responseText.split('\n').filter(line => line.trim());
lines.forEach(line => {
const columns = line.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
if (columns.length >= 2) {
const en = (columns[0] || '').trim().replace(/^"|"$/g, '');
const zh = (columns[1] || '').trim().replace(/^"|"$/g, '');
if (en && zh) { translations.set(en, zh); }
}
});
GM_setValue(CACHE_KEY, { translations: Array.from(translations.entries()), timestamp: Date.now() });
}
},
});
}
function openWikiPage(tagName) { if (!tagName) return; GM_openInTab(`${API_BASE_URL}/wiki_pages/show_or_new?title=${tagName}`, { active: true }); }
function getRandomHint() {
const hints = getHints();
if (!hints.length || Math.random() < 0.5) { return ""; }
return hints[Math.floor(Math.random() * hints.length)];
}
function searchLocalSuggestions(query, input) {
if (!isChineseLocale || !translations.size) return;
const queryLower = query.toLowerCase();
const rankedItems = [];
for (const [en, zh] of translations.entries()) {
const zhLower = zh.toLowerCase();
let score = 0;
if (zhLower.startsWith(queryLower)) score = 1;
else if (zhLower.includes(queryLower)) score = 2;
if (score > 0) { rankedItems.push({ score: score, data: { en: en, count: undefined, category: 0 } }); }
}
const finalMatches = rankedItems.sort((a, b) => a.score - b.score).map(item => item.data).slice(0, MAX_SUGGESTIONS);
updatePopup(input, finalMatches);
}
function searchLocalFallback(query, input) {
if (!isChineseLocale || !translations.size) { hidePopup(); return; }
const queryLower = query.toLowerCase(), normalizedQuery = normalizeQuery(queryLower), spacedQuery = queryLower.replace(/\s+/g, ' ');
if (!normalizedQuery) { hidePopup(); return; }
const rankedItems = [];
for (const en of translations.keys()) {
const enLower = en.toLowerCase();
let score = 0;
const enNormalized = normalizeQuery(enLower), enSpaced = enLower.replace(/_/g, ' ');
if (enNormalized.startsWith(normalizedQuery) || enSpaced.startsWith(spacedQuery)) score = 1;
else if (enNormalized.includes(normalizedQuery) || enSpaced.includes(spacedQuery)) score = 2;
if (score > 0) { rankedItems.push({ score: score, data: { en: en, count: undefined, category: 0 } }); }
}
const finalMatches = rankedItems.sort((a, b) => a.score - b.score).map(item => item.data).slice(0, MAX_SUGGESTIONS);
if (finalMatches.length > 0) { updatePopup(input, finalMatches, t('fallbackHint')); } else { hidePopup(); }
}
function fetchSuggestions(query, input) {
const normalizedQuery = normalizeQuery(query);
if (!normalizedQuery) { hidePopup(); return; }
if (apiAbortController) apiAbortController.abort();
apiAbortController = new AbortController();
const searchPattern = normalizedQuery.length <= SHORT_QUERY_PREFIX_LENGTH ? `${normalizedQuery}*` : `*${normalizedQuery}*`;
const params = new URLSearchParams({ 'search[name_matches]': searchPattern, 'search[order]': 'count', 'limit': MAX_SUGGESTIONS, 'search[hide_empty]': 'true' });
GM_xmlhttpRequest({
method: "GET",
url: `${API_BASE_URL}/tags.json?${params.toString()}`,
signal: apiAbortController.signal,
timeout: 1500,
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
"Referer": "https://danbooru.donmai.us/"
},
ontimeout: function () {
console.error(`[NAI Prompt Helper] API 请求超时 (ontimeout)。(Timeout: 1500ms). URL: ${API_BASE_URL}/tags.json?${params.toString()}`);
searchLocalFallback(query, input);
},
onload: function (response) {
if (response.status === 200) {
const matches = JSON.parse(response.responseText).map(tag => ({ en: tag.name, count: tag.post_count, category: tag.category }));
updatePopup(input, matches);
} else {
console.error(`[NAI Prompt Helper] API 错误: 收到 HTTP 状态 ${response.status} ${response.statusText}`);
console.warn(`[NAI Prompt Helper] 失败的URL: ${response.finalUrl}`);
searchLocalFallback(query, input);
}
},
onerror: (error) => {
console.error('[NAI Prompt Helper] API 请求失败 (onerror)。 详细信息:', error);
if (error.readyState !== 0) { // 忽略 abort 信号
searchLocalFallback(query, input);
}
}
});
}
function updatePopup(input, matches, overrideHint = null) {
createPopup();
let hintHTML = overrideHint ? `<span class="info-hint-error">${overrideHint}</span>` : (getRandomHint() ? `<span class="info-hint">${getRandomHint()}</span>` : '');
popup.innerHTML = `<div class="suggestion-info-display"><span class="info-title">${t('infoTitle')}</span>${hintHTML}</div><div class="suggestion-scroll-wrapper"><div class="suggestion-flex"></div></div>`;
const flexContainer = popup.querySelector('.suggestion-flex');
currentMatches = matches;
if (matches.length === 0) { hidePopup(); return; }
matches.forEach((tag, index) => {
const item = document.createElement('div');
item.className = 'suggestion-item';
item.dataset.category = tag.category || '0';
const enText = tag.en.replace(/_/g, ' '), zhText = translations.get(tag.en), displayText = zhText ? `${enText} (${zhText})` : enText;
let countHTML = '';
if (tag.count !== undefined && tag.count !== null) {
const countText = tag.count > 1000 ? `${(tag.count / 1000).toFixed(1)}k` : tag.count;
countHTML = `<span class="suggestion-count">${countText}</span>`;
}
item.innerHTML = `<span class="suggestion-text">${displayText}</span>${countHTML}`;
item.addEventListener('mouseover', () => {
isKeyboardNavigation = false;
if (selectedIndex !== index) { selectedIndex = index; updateSelectionUI(); }
});
item.addEventListener('mousedown', (e) => {
e.preventDefault();
selectedIndex = index;
updateSelectionUI();
if (e.ctrlKey) { openWikiPage(tag.en); }
else {
const currentInput = getActiveInputElement();
if (currentInput) {
const rangeToRestore = lastKnownRange ? lastKnownRange.cloneRange() : getClonedSelectionRange();
currentInput.focus();
requestAnimationFrame(() => insertTag(currentInput, tag.en, rangeToRestore));
}
}
});
flexContainer.appendChild(item);
});
positionPopup(input);
popup.style.display = 'flex';
isPopupActive = true;
if (matches.length > 0) { selectedIndex = 0; updateSelectionUI(); }
}
function findNodeAndOffsetFromGlobal(root, globalOffset) {
const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
let accumulatedLength = 0;
let lastTextNode = null;
let currentNode;
while (currentNode = treeWalker.nextNode()) {
lastTextNode = currentNode;
const nodeLength = currentNode.textContent.length;
if (accumulatedLength + nodeLength >= globalOffset) {
return { node: currentNode, offset: globalOffset - accumulatedLength };
}
accumulatedLength += nodeLength;
}
if (lastTextNode) {
return { node: lastTextNode, offset: lastTextNode.textContent.length };
}
return { node: root, offset: 0 };
}
function insertTag(input, tag, rangeToRestore) {
const cleanTag = tag.replace(/_/g, ' ');
const workingRange = rangeToRestore ? rangeToRestore.cloneRange() : getClonedSelectionRange();
if (!workingRange) return;
const tempRange = document.createRange();
tempRange.setStart(input, 0);
tempRange.setEnd(workingRange.startContainer, workingRange.startOffset);
const globalCursorPos = tempRange.toString().length;
const fullText = input.textContent;
if (autoFormatEnabled) {
let start = globalCursorPos;
while (start > 0 && !BREAK_CHARS.includes(fullText[start - 1])) { start--; }
let end = globalCursorPos;
while (end < fullText.length && !BREAK_CHARS.includes(fullText[end])) { end++; }
const textBefore = fullText.substring(0, start);
const textAfter = fullText.substring(end);
const newFullText = textBefore + cleanTag + textAfter;
let formattedText = formatPromptText(newFullText);
formattedText = formattedText.trim();
if (formattedText.length > 0 && !formattedText.endsWith(',')) {
formattedText += ', ';
} else if (formattedText.endsWith(',')) {
formattedText += ' ';
}
let newCursorPos = -1;
const searchStart = Math.max(0, start - 15);
const foundIndex = formattedText.indexOf(cleanTag, searchStart);
if (foundIndex !== -1) {
newCursorPos = foundIndex + cleanTag.length + 2;
} else {
newCursorPos = start + cleanTag.length + 2;
}
if (newCursorPos >= 2 && formattedText.substring(newCursorPos - 2, newCursorPos) === '::') {
newCursorPos -= 2;
}
newCursorPos = Math.min(newCursorPos, formattedText.length);
WeightShortcuts.updateInputContent(input, formattedText, newCursorPos, newCursorPos);
} else {
try {
if (!input.contains(workingRange.startContainer)) return;
let start = globalCursorPos;
while (start > 0 && !BREAK_CHARS.includes(fullText[start - 1])) { start--; }
let end = globalCursorPos;
while (end < fullText.length && !BREAK_CHARS.includes(fullText[end])) { end++; }
const { node: startNode, offset: startOffset } = findNodeAndOffsetFromGlobal(input, start);
const { node: endNode, offset: endOffset } = findNodeAndOffsetFromGlobal(input, end);
const replacementRange = document.createRange();
replacementRange.setStart(startNode, startOffset);
replacementRange.setEnd(endNode, endOffset);
replacementRange.deleteContents();
let textToInsert = cleanTag;
const textBefore = fullText.substring(0, start).trimEnd();
let prefix = '';
if (textBefore.length > 0) {
if (textBefore.endsWith(',')) {
prefix = ' ';
} else {
prefix = ', ';
}
}
textToInsert = prefix + textToInsert + ', ';
const newTextNode = document.createTextNode(textToInsert);
replacementRange.insertNode(newTextNode);
replacementRange.setStartAfter(newTextNode);
replacementRange.collapse(true);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(replacementRange);
const inputEvent = new Event('input', { bubbles: true, composed: true });
input.dispatchEvent(inputEvent);
} catch (error) {
console.error("[NAI Prompt Helper] An error occurred during surgical insertTag:", error);
}
}
window.__NAIWeightAdjustingUntil = Date.now() + 500;
hidePopup();
}
function createPopup() { if (!popup) { popup = document.createElement('div'); popup.className = 'autocomplete-container'; document.body.appendChild(popup); } }
function hidePopup() { if (popup) popup.style.display = 'none'; if (apiAbortController) apiAbortController.abort(); selectedIndex = -1; isPopupActive = false; currentMatches = []; isKeyboardNavigation = false; }
const debounce = (func, delay) => { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), delay); }; };
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('[class*="prompt-input"]') || pElement.closest('[class*="character-prompt-input"]'))) { return pElement; } return null; }
function positionPopup(input) { let rect; const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0).cloneRange(); if (range.collapsed) { const tempSpan = document.createElement('span'); tempSpan.appendChild(document.createTextNode('\u200b')); range.insertNode(tempSpan); rect = tempSpan.getBoundingClientRect(); tempSpan.remove(); } else { rect = range.getBoundingClientRect(); } } if (!rect || (rect.width === 0 && rect.height === 0)) { rect = input.getBoundingClientRect(); } popup.style.top = `${rect.bottom + window.scrollY + 5}px`; popup.style.left = `${rect.left + window.scrollX}px`; }
function updateSelectionUI() { const items = popup.querySelectorAll('.suggestion-item'); items.forEach((item, index) => item.classList.toggle('selected', index === selectedIndex)); const selectedEl = popup.querySelector('.selected'); if (selectedEl) selectedEl.scrollIntoView({ block: 'nearest' }); }
function saveSelection() {
const selection = window.getSelection();
const activeInput = getActiveInputElement();
if (activeInput && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
if (activeInput.contains(range.commonAncestorContainer)) { lastKnownRange = range.cloneRange(); }
}
}
const handleInput = debounce(() => {
const now = Date.now();
if (window.__NAIWeightAdjusting || now < window.__NAIWeightAdjustingUntil) { hidePopup(); return; }
const input = getActiveInputElement(); if (!input) { hidePopup(); return; }
const textBeforeCursor = (() => { const sel = window.getSelection(); if (!sel.rangeCount) return ''; const range = sel.getRangeAt(0).cloneRange(); const parent = sel.focusNode.parentElement; if (!parent) return ''; range.selectNodeContents(parent); range.setEnd(sel.focusNode, sel.focusOffset); return range.toString(); })();
if (textBeforeCursor.endsWith(',')) { hidePopup(); return; }
const lastDelimiterIndex = Math.max(textBeforeCursor.lastIndexOf(','), textBeforeCursor.lastIndexOf(':'), textBeforeCursor.lastIndexOf('['), textBeforeCursor.lastIndexOf('{'));
const currentQuery = textBeforeCursor.substring(lastDelimiterIndex + 1).trim();
if (shouldSkipWeightSuggestions(textBeforeCursor, lastDelimiterIndex)) { hidePopup(); return; }
if (currentQuery.length < 1) { hidePopup(); return; }
if (!ALLOWED_CHARS_REGEX.test(currentQuery)) { hidePopup(); return; }
if (/^\d*\.?\d*$/.test(currentQuery)) { hidePopup(); return; }
if (/[\u4e00-\u9fa5]/.test(currentQuery)) { searchLocalSuggestions(currentQuery, input); } else { fetchSuggestions(currentQuery, input); }
}, DEBOUNCE_DELAY);
function handleKeydown(e) {
if (e.ctrlKey) { return; }
if (e.key === ',') { hidePopup(); return; }
if (!isPopupActive) return;
const keyMap = { 'ArrowDown': 1, 'ArrowRight': 1, 'ArrowUp': -1, 'ArrowLeft': -1 };
if (keyMap[e.key] !== undefined) {
e.preventDefault();
isKeyboardNavigation = true;
selectedIndex = (selectedIndex + keyMap[e.key] + currentMatches.length) % currentMatches.length;
updateSelectionUI();
} else if (e.key === 'Enter' || e.key === 'Tab') {
e.preventDefault(); e.stopPropagation();
if (selectedIndex >= 0 && currentMatches[selectedIndex]) {
const input = getActiveInputElement();
if (input) {
const rangeForInsert = getClonedSelectionRange();
insertTag(input, currentMatches[selectedIndex].en, rangeForInsert);
}
} else { hidePopup(); }
} else if (e.key === 'Escape') { e.preventDefault(); hidePopup(); }
else { isKeyboardNavigation = false; }
}
function handleClickOutside(e) { const input = getActiveInputElement(); if (isPopupActive && popup && !popup.contains(e.target) && !input?.contains(e.target)) { hidePopup(); } }
function init() {
loadTranslations();
document.addEventListener('input', handleInput);
document.addEventListener('keydown', handleKeydown, true);
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keyup', saveSelection);
document.addEventListener('mouseup', saveSelection);
document.addEventListener('selectionchange', saveSelection);
}
return { init };
})();
const WeightShortcuts = (() => {
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, cursorOffset: globalOffset } : null;
}
function updateInputContent(inputElement, newContent, selectionStart, selectionEnd) {
window.__NAIWeightAdjusting = true;
window.__NAIWeightAdjustingUntil = Date.now() + 500;
try {
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();
const textLength = inputElement.firstChild.textContent.length;
const safeIndex = Math.max(0, Math.min(textLength, selectionEnd));
newRange.setStart(inputElement.firstChild, safeIndex);
newRange.setEnd(inputElement.firstChild, safeIndex);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(newRange);
const inputEvent = new Event('input', { bubbles: true });
inputElement.dispatchEvent(inputEvent);
} finally { window.__NAIWeightAdjusting = false; }
}
function parsePromptStructure(text) {
return parseSequence(text, 0, false).items;
}
function parseSequence(text, startIndex, stopAtClose) {
const items = [];
let i = startIndex;
const readSeparator = (input, index, shouldStopAtClose) => {
let cursor = index; let separator = '';
while (cursor < input.length) {
if (shouldStopAtClose && input.startsWith('::', cursor)) { break; }
const ch = input[cursor];
if (ch === ',') { separator += ch; cursor++; while (cursor < input.length && input[cursor] === ' ') { separator += input[cursor]; cursor++; } }
else if (ch === '\n' || ch === '\r') { separator += ch; cursor++; }
else if (ch === ' ' || ch === '\t') { separator += ch; cursor++; }
else { break; }
}
return { separator, nextIndex: cursor };
};
while (i < text.length) {
if (stopAtClose && text.startsWith('::', i)) { return { items, index: i + 2, closeStart: i, closeEnd: i + 2 }; }
while (i < text.length && (text[i] === ' ' || text[i] === '\t')) { i++; }
if (stopAtClose && text.startsWith('::', i)) { return { items, index: i + 2, closeStart: i, closeEnd: i + 2 }; }
if (i >= text.length) { break; }
const doubleColonMatch = text.slice(i).match(/^(-?\d+(?:\.\d+)?)(::)(?!:)/);
if (doubleColonMatch) {
const groupStart = i;
const weightString = doubleColonMatch[1];
const parsedWeight = parseFloat(weightString);
i += doubleColonMatch[0].length;
const childResult = parseSequence(text, i, true);
const groupNode = { type: 'group', weight: isNaN(parsedWeight) ? 1.0 : parsedWeight, format: 'doubleColon', start: groupStart, weightStart: groupStart, weightEnd: groupStart + weightString.length, prefixEnd: i, closeStart: childResult.closeStart ?? childResult.index, closeEnd: childResult.closeEnd ?? childResult.index, children: childResult.items };
i = childResult.index;
const separatorInfo = readSeparator(text, i, stopAtClose);
i = separatorInfo.nextIndex;
items.push({ node: groupNode, separator: separatorInfo.separator });
continue;
}
const groupMatch = text.slice(i).match(/^(-?\d+(?:\.\d+)?)(\s*):(?!:)/);
if (groupMatch) {
const groupStart = i;
const weightString = groupMatch[1];
const parsedWeight = parseFloat(weightString);
i += groupMatch[0].length;
const childResult = parseSequence(text, i, true);
const groupNode = { type: 'group', weight: isNaN(parsedWeight) ? 1.0 : parsedWeight, format: 'singleColon', start: groupStart, weightStart: groupStart, weightEnd: groupStart + weightString.length, prefixEnd: i, closeStart: childResult.closeStart ?? childResult.index, closeEnd: childResult.closeEnd ?? childResult.index, children: childResult.items };
i = childResult.index;
const separatorInfo = readSeparator(text, i, stopAtClose);
i = separatorInfo.nextIndex;
items.push({ node: groupNode, separator: separatorInfo.separator });
continue;
}
const tagRawStart = i;
while (i < text.length && text[i] !== ',' && text[i] !== '\n' && text[i] !== '\r') {
if (stopAtClose && text.startsWith('::', i)) { break; }
if (i === tagRawStart) {
const aheadMatch = text.slice(i).match(/^(-?\d+(?:\.\d+)?)(\s*):(?!:)/);
if (aheadMatch) { break; }
}
i++;
}
const rawSegment = text.slice(tagRawStart, i);
if (rawSegment.trim()) {
let leadingSpaces = 0;
while (leadingSpaces < rawSegment.length && /\s/.test(rawSegment[leadingSpaces])) { leadingSpaces++; }
let trailingSpaces = 0;
while (trailingSpaces < rawSegment.length - leadingSpaces && /\s/.test(rawSegment[rawSegment.length - 1 - trailingSpaces])) { trailingSpaces++; }
const contentStart = tagRawStart + leadingSpaces;
const contentEnd = i - trailingSpaces;
const tagText = text.slice(contentStart, contentEnd);
const tagNode = { type: 'tag', text: tagText, start: contentStart, end: contentEnd };
const separatorInfo = readSeparator(text, i, stopAtClose);
i = separatorInfo.nextIndex;
items.push({ node: tagNode, separator: separatorInfo.separator });
} else {
const separatorInfo = readSeparator(text, i, stopAtClose);
i = separatorInfo.nextIndex;
if (items.length) { items[items.length - 1].separator += separatorInfo.separator; }
}
}
return { items, index: i };
}
function normalizeSequence(sequence) {
for (let i = 0; i < sequence.length; i++) {
sequence[i].separator = i < sequence.length - 1 ? ', ' : '';
if (sequence[i].node.type === 'group') { normalizeSequence(sequence[i].node.children); }
}
}
function normalizeTree(sequence) { normalizeSequence(sequence); }
function formatWeightValue(value) {
const rounded = Math.round(value * 20) / 20;
let str = rounded.toFixed(2);
return str.replace(/\.?0+$/, '');
}
function serializeTree(sequence) {
const state = { output: '', offset: 0 };
serializeSequence(sequence, state);
return state.output;
}
function serializeSequence(sequence, state) {
for (let i = 0; i < sequence.length; i++) {
serializeNode(sequence[i].node, state);
const sep = sequence[i].separator || '';
state.output += sep;
state.offset += sep.length;
}
}
function serializeNode(node, state) {
if (node.type === 'tag') {
node.serializeStart = state.offset;
node.serializeEnd = state.offset + node.text.length;
state.output += node.text;
state.offset += node.text.length;
} else if (node.type === 'group') {
const weightStr = formatWeightValue(node.weight);
const format = node.format || 'singleColon';
if (format === 'doubleColon') {
state.output += `${weightStr}::`;
state.offset += weightStr.length + 2;
} else {
state.output += `${weightStr}:`;
state.offset += weightStr.length + 1;
}
serializeSequence(node.children, state);
state.output += '::';
state.offset += 2;
}
}
function findTagNodeByOffset(sequence, offset, ancestors = []) {
for (let i = 0; i < sequence.length; i++) {
const item = sequence[i];
const node = item.node;
if (node.type === 'tag') {
if (node.start <= offset && offset <= node.end) { return { item, sequence, index: i, ancestors }; }
} else if (node.type === 'group') {
const result = findTagNodeByOffset(node.children, offset, ancestors.concat({ groupNode: node, entry: item, parentSequence: sequence, index: i }));
if (result) { return result; }
}
}
return null;
}
function removeGroupWrapper(groupInfo) {
const { parentSequence, index, groupNode } = groupInfo;
const children = groupNode.children;
parentSequence.splice(index, 1, ...children);
}
function wrapTagWithGroup(target, newWeight) {
const { sequence, index, item } = target;
const tagNode = item.node, originalSeparator = item.separator || '';
sequence.splice(index, 1);
const groupNode = { type: 'group', weight: newWeight, format: 'doubleColon', children: [{ node: tagNode, separator: '' }] };
sequence.splice(index, 0, { node: groupNode, separator: originalSeparator });
}
function adjustWeightForTag(target, direction) {
const step = 0.05;
const tagNode = target.item.node;
const ancestors = target.ancestors;
if (ancestors.length > 0) {
const outerGroupInfo = ancestors[0];
const groupNode = outerGroupInfo.groupNode;
let newWeight = groupNode.weight + (direction * step);
newWeight = Math.round(newWeight * 20) / 20;
if (Math.abs(newWeight - 1.0) < 0.001) { removeGroupWrapper(outerGroupInfo); }
else { groupNode.weight = newWeight; }
return tagNode;
}
let newWeight = 1.0 + (direction * step);
newWeight = Math.round(newWeight * 20) / 20;
if (Math.abs(newWeight - 1.0) < 0.001) { return tagNode; }
wrapTagWithGroup(target, newWeight);
return tagNode;
}
function resolveMovementContext(target) {
let { sequence, index, item, ancestors } = target;
let movedAsGroup = false;
if (ancestors.length) {
const immediate = ancestors[ancestors.length - 1];
if (immediate.groupNode.children.length === 1) {
movedAsGroup = true;
sequence = immediate.parentSequence;
index = immediate.index;
item = immediate.entry;
ancestors = ancestors.slice(0, -1);
}
}
return { sequence, index, item, ancestors, movedAsGroup };
}
function moveTagWithinStructure(target, direction) {
const context = resolveMovementContext(target);
const { sequence, index, item, ancestors } = context;
const isOriginalWeighted = target.ancestors.length > 0;
if (direction === -1) {
if (index > 0) {
const prevItem = sequence[index - 1];
if (!isOriginalWeighted && prevItem.node.type === 'group') {
sequence.splice(index, 1);
item.separator = '';
prevItem.node.children.push(item);
return true;
}
sequence[index - 1] = item;
sequence[index] = prevItem;
return true;
}
if (!ancestors.length) { return false; }
const parentContext = ancestors[ancestors.length - 1];
const { parentSequence, index: parentIndex } = parentContext;
sequence.splice(index, 1);
if (sequence.length === 0) {
parentSequence.splice(parentIndex, 1);
parentSequence.splice(parentIndex, 0, item);
} else { parentSequence.splice(parentIndex, 0, item); }
return true;
}
if (direction === 1) {
if (index < sequence.length - 1) {
const nextItem = sequence[index + 1];
if (!isOriginalWeighted && nextItem.node.type === 'group') {
sequence.splice(index, 1);
item.separator = '';
nextItem.node.children.unshift(item);
return true;
}
sequence[index + 1] = item;
sequence[index] = nextItem;
return true;
}
if (!ancestors.length) { return false; }
const parentContext = ancestors[ancestors.length - 1];
const { parentSequence, index: parentIndex } = parentContext;
sequence.splice(index, 1);
if (sequence.length === 0) {
parentSequence.splice(parentIndex, 1);
parentSequence.splice(parentIndex, 0, item);
} else { parentSequence.splice(parentIndex + 1, 0, item); }
return true;
}
return false;
}
function handleKeydown(event) {
const inputElement = getActiveInputElement();
if (!inputElement || !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 leadingWhitespace = tagInfo.fullText.slice(tagInfo.start, tagInfo.end).match(/^\s*/)[0];
const structure = parsePromptStructure(tagInfo.fullText);
const offsetForSearch = tagInfo.cursorOffset ?? (tagInfo.start + leadingWhitespace.length);
const target = findTagNodeByOffset(structure, offsetForSearch); if (!target) { return; }
const tagNode = adjustWeightForTag(target, direction);
normalizeTree(structure);
const serialized = serializeTree(structure);
const newStart = tagNode.serializeStart ?? (tagInfo.start + leadingWhitespace.length);
const newEnd = tagNode.serializeEnd ?? (newStart + tagNode.text.length);
updateInputContent(inputElement, serialized, newStart, newEnd);
} 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;
const leadingWhitespace = tagInfo.fullText.slice(tagInfo.start, tagInfo.end).match(/^\s*/)[0];
const structure = parsePromptStructure(tagInfo.fullText);
const offsetForSearch = tagInfo.cursorOffset ?? (tagInfo.start + leadingWhitespace.length);
const target = findTagNodeByOffset(structure, offsetForSearch); if (!target) { return; }
const tagNode = target.item.node;
if (!moveTagWithinStructure(target, direction)) { return; }
normalizeTree(structure);
const serialized = serializeTree(structure);
const newStart = tagNode.serializeStart ?? (tagInfo.start + leadingWhitespace.length);
const newEnd = tagNode.serializeEnd ?? (newStart + tagNode.text.length);
updateInputContent(inputElement, serialized, newStart, newEnd);
}
}
function init() {
const checkInterval = setInterval(() => {
const inputElement = getActiveInputElement();
if (inputElement) { clearInterval(checkInterval); document.addEventListener('keydown', handleKeydown, true); }
}, 500);
}
return { init, parsePromptStructure, normalizeTree, serializeTree, updateInputContent };
})();
function init() {
TagAssist.init();
WeightShortcuts.init();
}
if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); }
else { init(); }
})();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址