🏠 Home 

Civitai Prompt Autocomplete & Tag Wiki

Adds tag autocomplete and wiki lookup features


Install this script?
// ==UserScript==
// @name         Civitai Prompt Autocomplete & Tag Wiki
// @namespace    http://tampermonkey.net/
// @version      4.6
// @description  Adds tag autocomplete and wiki lookup features
// @author       AndroidXL
// @match        https://civitai.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=civitai.com
// @grant        GM.xmlHttpRequest
// @license      MIT
// ==/UserScript==
(function() {
'use strict';
// All variable declarations moved to top
let promptInput = null;
let negativePromptInput = null;  // Add negative prompt input reference
let activeInput = null;  // Track which input is currently active
let suggestionsBox = null;
let currentSuggestions = [];
let selectedSuggestionIndex = -1;
let debounceTimer;
const debounceDelay = 50;
let lastCurrentWord = "";
let lastStartPos = 0; // New variable to track word start position
let wikiOverlay = null;
let wikiSearchContainer = null;
let wikiContent = null;
let currentPosts = [];
let currentPostIndex = 0;
let wikiInitialized = false;
let autocompleteEnabled = true; // Default state for autocomplete
let wikiHotkey = 't'; // Default hotkey for wiki
let settingsOpen = false;
// Wiki history navigation variables
let wikiHistory = [];
let historyIndex = -1;
let isNavigatingHistory = false;
// Initialize customTags with defaults, will be overridden by localStorage if available
let customTags = {
'quality': 'masterpiece, best quality, amazing quality, very detailed',
'quality_pony': 'score_9, score_8_up, score_7_up, score_6_up',
};
// Create and inject styles without GM_addStyle
const styleElement = document.createElement('style');
styleElement.textContent = `
#autocomplete-suggestions-box {
position: absolute;
background-color: #1a1b1e;
border: 1px solid #333;
border-radius: 5px;
margin-top: 2px;
z-index: 100;
overflow-y: auto;
max-height: 150px;
width: calc(100% - 6px);
padding: 2px;
box-shadow: 2px 2px 5px rgba(0,0,0,0.3);
}
#autocomplete-suggestions-box div {
padding: 4px 8px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #C1C2C5;
font-size: 14px;
}
#autocomplete-suggestions-box div:hover {
background-color: #282a2d;
}
.autocomplete-selected {
background-color: #383a3e;
}
.suggestion-count {
color: #98C379;
font-weight: normal;
margin-left: 8px;
font-size: 0.9em;
}
.wiki-search-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 9999;
display: none;
overflow-y: auto;
padding: 20px;
}
.wiki-search-container {
position: relative;
width: 90%;
max-width: 800px;
margin: 40px auto;
transition: all 0.3s ease;
}
.wiki-search-bar {
width: 100%;
padding: 12px;
background: rgba(26,27,30,0.95);
border: 1px solid #383a3e;
border-radius: 8px;
color: #fff;
font-size: 16px;
}
/* Container for all buttons on the right */
.wiki-buttons-container {
position: absolute;
top: 12px;
right: 12px;
display: flex;
align-items: center;
gap: 8px;
z-index: 10002;
}
.wiki-settings-button {
background: rgba(26,27,30,0.95);
color: #C1C2C5;
border: 1px solid #383a3e;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
font-size: 14px;
height: 30px;
display: flex;
align-items: center;
}
/* Wiki navigation buttons */
.wiki-nav-history {
display: flex;
gap: 5px;
}
.wiki-nav-button {
background: rgba(26,27,30,0.95);
color: #C1C2C5;
border: 1px solid #383a3e;
border-radius: 4px;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 16px;
opacity: 0.7;
transition: opacity 0.3s, background-color 0.3s;
}
.wiki-nav-button:hover:not(:disabled) {
background: #383a3e;
opacity: 1;
}
.wiki-nav-button:disabled {
cursor: not-allowed;
opacity: 0.3;
}
.wiki-settings-button:hover {
background: #383a3e;
}
.wiki-settings-panel {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 600px;
background: rgba(26,27,30,0.98);
border: 1px solid #383a3e;
border-radius: 8px;
padding: 20px;
z-index: 10003;
color: #C1C2C5;
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
}
.wiki-settings-panel h2 {
margin-top: 0;
border-bottom: 1px solid #383a3e;
padding-bottom: 10px;
}
.settings-section {
margin-bottom: 20px;
}
.settings-section h3 {
margin-bottom: 10px;
font-size: 16px;
color: #98C379;
}
.hotkey-setting {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.hotkey-setting label {
margin-right: 10px;
}
.hotkey-setting input {
width: 50px;
background: #1a1b1e;
border: 1px solid #383a3e;
border-radius: 4px;
padding: 5px;
color: #fff;
text-align: center;
}
.custom-tags-section {
margin-top: 15px;
}
.custom-tag-row {
display: flex;
margin-bottom: 8px;
gap: 10px;
}
.custom-tag-name,
.custom-tag-value {
flex: 1;
background: #1a1b1e;
border: 1px solid #383a3e;
border-radius: 4px;
padding: 5px 8px;
color: #fff;
}
.custom-tag-controls {
display: flex;
gap: 5px;
}
.btn {
background: #383a3e;
color: #C1C2C5;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
font-size: 14px;
}
.btn:hover {
background: #4a4c52;
}
.btn-save {
background: #2c6e49;
}
.btn-save:hover {
background: #358f5f;
}
.btn-delete {
background: #6e2c2c;
}
.btn-delete:hover {
background: #913a3a;
}
.btn-add {
background: #2c4a6e;
margin-top: 10px;
}
.btn-add:hover {
background: #385d89;
}
.settings-panel-footer {
display: flex;
justify-content: flex-end;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #383a3e;
gap: 10px;
}
.wiki-content {
background: rgba(26,27,30,0.95);
border-radius: 8px;
margin-top: 20px;
padding: 20px;
width: 100%;
position: relative;
}
.wiki-text-content {
padding-right: 420px;
min-height: 500px;
word-break: break-word;
overflow-wrap: break-word;
}
.wiki-description {
line-height: 1.4;
white-space: pre-line;
font-size: 15px;
}
.wiki-image-section {
position: absolute;
top: 20px;
right: 20px;
width: 400px;
background: rgba(0,0,0,0.2);
border-radius: 8px;
padding: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
.wiki-image-navigation {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0 10px;
position: relative;
height: 40px;
}
.image-nav-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0,0,0,0.7);
color: white;
border: none;
width: 40px;
height: 40px;
cursor: pointer;
border-radius: 20px;
opacity: 0.8;
transition: opacity 0.3s, background-color 0.3s;
font-size: 18px;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
}
.image-nav-button:hover {
opacity: 1;
background: rgba(0,0,0,0.9);
}
.image-nav-button.prev {
left: 10px;
}
.image-nav-button.next {
right: 10px;
}
.wiki-image-container {
width: 100%;
height: 350px;
position: relative;
margin: 0;
background: rgba(0,0,0,0.1);
border-radius: 4px;
overflow: hidden;
}
.wiki-image {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 4px;
transition: transform 0.3s ease;
}
.wiki-image:hover {
transform: scale(1.03);
}
.wiki-image-section {
position: absolute;
top: 20px;
right: 20px;
width: 400px;
background: rgba(0,0,0,0.2);
border-radius: 8px;
padding: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
.wiki-image-navigation {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0 10px;
}
.image-nav-button {
background: rgba(0,0,0,0.5);
color: white;
border: none;
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
opacity: 0.7;
transition: opacity 0.3s;
font-size: 16px;
}
.wiki-image-container {
width: 100%;
height: 350px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
margin: 0;
background: rgba(0,0,0,0.1);
border-radius: 4px;
}
.wiki-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 4px;
}
.wiki-nav-buttons {
width: 100%;
display: flex;
justify-content: center;
}
.wiki-button {
padding: 8px 16px;
background: #383a3e;
border: none;
border-radius: 4px;
color: #fff;
cursor: pointer;
width: 100%;
text-align: center;
}
.wiki-tag {
display: inline-block;
margin: 2px 4px;
padding: 2px 4px;
background: rgba(97, 175, 239, 0.1);
border-radius: 3px;
color: #61afef;
cursor: pointer;
text-decoration: underline;
}
.wiki-tag:hover {
background: rgba(97, 175, 239, 0.2);
}
.wiki-link {
color: #98c379;
text-decoration: underline;
}
.wiki-loading {
text-align: center;
padding: 20px;
}
.wiki-description {
line-height: 1.6;
white-space: pre-wrap;
font-size: 15px;
}
.wiki-description p {
margin: 1em 0;
}
.wiki-search-suggestions {
position: fixed;
margin-top: 2px;
background: rgba(26,27,30,0.95);
border: 1px solid #383a3e;
border-radius: 0 0 8px 8px;
max-height: 200px;
overflow-y: auto;
z-index: 10001;
width: 90%;
max-width: 800px;
left: 50%;
transform: translateX(-50%);
}
.wiki-search-suggestion {
padding: 8px 12px;
cursor: pointer;
color: #fff;
}
.wiki-search-suggestion:hover,
.wiki-search-suggestion.selected {
background: #383a3e;
}
.no-images-message {
color: #666;
text-align: center;
padding: 20px;
font-style: italic;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.wiki-description h1 { font-size: 1.8em; margin: 0.8em 0 0.4em; }
.wiki-description h2 { font-size: 1.6em; margin: 0.7em 0 0.4em; }
.wiki-description h3 { font-size: 1.4em; margin: 0.6em 0 0.4em; }
.wiki-description h4 { font-size: 1.2em; margin: 0.5em 0 0.4em; }
.wiki-description h5 { font-size: 1.1em; margin: 0.5em 0 0.4em; }
.wiki-description h6 { font-size: 1em; margin: 0.5em 0 0.4em; }
.wiki-description p { margin: 0.5em 0; }
.wiki-description ul {
margin: 0.5em 0 0.5em 1.5em;
padding: 0;
}
.wiki-description li {
margin: 0.3em 0;
line-height: 1.4;
}
/* Autocomplete toggle checkbox styles */
.autocomplete-toggle {
display: flex;
align-items: center;
margin-bottom: 5px;
font-size: 0.9em;
color: #C1C2C5;
}
.autocomplete-toggle input {
margin-right: 5px;
}
.tag-validation-error {
color: #f55;
font-size: 12px;
margin-top: 5px;
}
`;
document.head.appendChild(styleElement);
// Load settings from localStorage
function loadSettings() {
try {
// Load autocomplete preference
const savedAutoComplete = localStorage.getItem('civitai-autocomplete-enabled');
if (savedAutoComplete !== null) {
autocompleteEnabled = savedAutoComplete === 'true';
}
// Load wiki hotkey
const savedHotkey = localStorage.getItem('civitai-wiki-hotkey');
if (savedHotkey) {
wikiHotkey = savedHotkey;
}
// Load custom tags
const savedTags = localStorage.getItem('civitai-custom-tags');
if (savedTags) {
try {
customTags = JSON.parse(savedTags);
} catch (e) {
console.error('Error parsing custom tags:', e);
// If parsing fails, keep the default tags
}
}
debug('Settings loaded from localStorage');
} catch (e) {
console.error('Error loading settings:', e);
// Use defaults if there's an error
}
}
// Save settings to localStorage
function saveSettings() {
try {
localStorage.setItem('civitai-autocomplete-enabled', autocompleteEnabled);
localStorage.setItem('civitai-wiki-hotkey', wikiHotkey);
localStorage.setItem('civitai-custom-tags', JSON.stringify(customTags));
debug('Settings saved to localStorage');
} catch (e) {
console.error('Error saving settings:', e);
}
}
// Load settings when script starts
loadSettings();
// Replace handleInputEvents function
function handleInputEvents(e) {
const input = e.target;
if ((input.id === 'input_prompt' || input.id === 'input_negativePrompt') && autocompleteEnabled) {
activeInput = input; // Set the active input
const currentWordObj = getCurrentWord(input.value, input.selectionStart);
lastCurrentWord = currentWordObj.word;
lastStartPos = currentWordObj.startPos;
fetchSuggestions(lastCurrentWord);
// Position suggestions box below the active input
if (suggestionsBox) {
const inputRect = input.getBoundingClientRect();
suggestionsBox.style.position = 'absolute';
suggestionsBox.style.left = `${inputRect.left}px`;
suggestionsBox.style.top = `${inputRect.bottom + window.scrollY}px`;
suggestionsBox.style.width = `${inputRect.width}px`;
}
}
}
// Create the toggle checkbox
function createAutocompleteToggle() {
const toggleContainer = document.createElement('div');
toggleContainer.className = 'autocomplete-toggle';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = 'autocomplete-toggle-checkbox';
checkbox.checked = autocompleteEnabled;
const label = document.createElement('label');
label.htmlFor = 'autocomplete-toggle-checkbox';
label.textContent = 'Enable Tag Autocomplete';
toggleContainer.appendChild(checkbox);
toggleContainer.appendChild(label);
checkbox.addEventListener('change', function() {
autocompleteEnabled = this.checked;
saveSettings();
if (!autocompleteEnabled) {
clearSuggestions();
}
});
return toggleContainer;
}
function handleKeydownEvents(e) {
if (e.target.id !== 'input_prompt' && e.target.id !== 'input_negativePrompt') return;
activeInput = e.target; // Update active input
if (e.key === 'ArrowDown') {
if (suggestionsBox?.style.display === 'block' && currentSuggestions.length > 0) {
e.preventDefault();
selectedSuggestionIndex = Math.min(selectedSuggestionIndex + 1, currentSuggestions.length - 1);
updat###ggestionSelection();
}
} else if (e.key === 'ArrowUp') {
if (suggestionsBox?.style.display === 'block' && currentSuggestions.length > 0) {
e.preventDefault();
selectedSuggestionIndex = Math.max(selectedSuggestionIndex - 1, -1);
updat###ggestionSelection();
}
} else if (e.key === 'Tab' || e.key === 'Enter') {
if (suggestionsBox?.style.display === 'block' && currentSuggestions.length > 0) {
e.preventDefault();
if (selectedSuggestionIndex !== -1) {
insertSuggestion(currentSuggestions[selectedSuggestionIndex].label);
} else {
insertSuggestion(currentSuggestions[0].label);
}
}
} else if (e.key === 'Escape') {
clearSuggestions();
}
}
function setupAutocomplete() {
// Clean up old elements
if (suggestionsBox) {
suggestionsBox.remove();
}
// Remove old toggle if it exists
const oldToggle = document.getElementById('autocomplete-toggle-checkbox');
if (oldToggle && oldToggle.parentNode) {
oldToggle.parentNode.remove();
}
// Get both input elements
promptInput = document.getElementById('input_prompt');
negativePromptInput = document.getElementById('input_negativePrompt');
// Exit if neither input exists
if (!promptInput && !negativePromptInput) return;
// Create suggestions box (attach to document body instead of a specific input)
suggestionsBox = document.createElement('div');
suggestionsBox.id = 'autocomplete-suggestions-box';
suggestionsBox.style.display = 'none';
document.body.appendChild(suggestionsBox);
// Create the toggle and insert it before the positive prompt if it exists
if (promptInput) {
const toggleContainer = createAutocompleteToggle();
promptInput.parentNode.parentNode.parentNode.parentNode.insertBefore(
toggleContainer,
promptInput.parentNode.parentNode.parentNode.parentNode.firstChild
);
}
// Remove old event listeners and add new ones using event delegation
document.removeEventListener('input', handleInputEvents, true);
document.removeEventListener('keydown', handleKeydownEvents, true);
document.addEventListener('input', handleInputEvents, true);
document.addEventListener('keydown', handleKeydownEvents, true);
// Handle clicks outside
document.addEventListener('click', (e) => {
if ((!promptInput?.contains(e.target) && !negativePromptInput?.contains(e.target)) &&
!suggestionsBox?.contains(e.target)) {
clearSuggestions();
}
});
}
// Set up a more aggressive observer
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
const addedNodes = Array.from(mutation.addedNodes);
const hasPromptInput = addedNodes.some(node =>
node.id === 'input_prompt' ||
node.querySelector?.('#input_prompt')
);
if (hasPromptInput || !document.getElementById('autocomplete-suggestions-box')) {
setupAutocomplete();
break;
}
}
});
// Start observing with more specific config
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['id']
});
// Initial setup
setupAutocomplete();
initializeWiki();
function fetchSuggestions(term) {
if (!term || !autocompleteEnabled) {
clearSuggestions();
return;
}
// First, check custom tags
const matchingCustomTags = Object.keys(customTags)
.filter(tag => tag.toLowerCase().startsWith(term.toLowerCase()))
.map(tag => ({
label: tag,
count: '⭐', // Star to indicate custom tag
isCustom: true,
insertText: customTags[tag]
}));
// If we have matching custom tags, show them immediately
if (matchingCustomTags.length > 0) {
currentSuggestions = matchingCustomTags;
showSuggestions();
}
// Continue with API request for regular tags
const apiTerm = term.replace(/ /g, '_');
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
GM.xmlHttpRequest({
method: 'GET',
url: `https://gelbooru.com/index.php?page=autocomplete2&term=${encodeURIComponent(apiTerm)}&type=tag_query&limit=10`,
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
const fetchedSuggestions = data.map(item => ({
label: item.label.replace(/[()]/g, '\\$&'),
count: item.post_count,
isCustom: false
}));
// Combine custom and API suggestions
filterAndShowSuggestions([...matchingCustomTags, ...fetchedSuggestions]);
} catch (e) {
console.error("Error parsing Gelbooru API response:", e);
clearSuggestions();
}
} else {
console.error("Gelbooru API request failed:", response.status, response.statusText);
clearSuggestions();
}
},
onerror: function(error) {
console.error("Gelbooru API request error:", error);
clearSuggestions();
}
});
}, debounceDelay);
}
function filterAndShowSuggestions(fetchedSuggestions) {
const existingTags = promptInput.value.split(',').map(tag => tag.trim().toLowerCase());
const filteredSuggestions = fetchedSuggestions.filter(suggestion => {
return !existingTags.includes(suggestion.label.toLowerCase())
});
currentSuggestions = filteredSuggestions;
showSuggestions();
}
function showSuggestions() {
if (currentSuggestions.length === 0) {
clearSuggestions();
return;
}
suggestionsBox.innerHTML = '';
currentSuggestions.forEach((suggestion, index) => {
const suggestionDiv = document.createElement('div');
suggestionDiv.innerHTML = `${suggestion.label} <span class="suggestion-count">[${suggestion.count}]</span>`;
suggestionDiv.addEventListener('click', () => {
insertSuggestion(suggestion.label);
});
suggestionsBox.appendChild(suggestionDiv);
});
suggestionsBox.style.display = 'block';
selectedSuggestionIndex = -1;
}
function clearSuggestions() {
if (suggestionsBox) {
suggestionsBox.style.display = 'none';
suggestionsBox.innerHTML = '';
}
currentSuggestions = [];
selectedSuggestionIndex = -1;
}
function insertSuggestion(suggestion) {
// Make sure we have an active input
if (!activeInput) return;
// Find the matching suggestion object
const suggestionObj = currentSuggestions.find(s => s.label === suggestion);
const textToInsert = (suggestionObj?.isCustom ? suggestionObj.insertText : suggestion)
// Use setRangeText to replace the current word with the suggestion
const start = lastStartPos;
const end = activeInput.selectionStart;
// Focus the input to ensure changes register in the undo stack
activeInput.focus();
// Create a composition session to properly register in the undo stack
// First delete the current word manually
activeInput.setSelectionRange(start, end);
document.execCommand('delete');
// Then insert the new text with execCommand
document.execCommand('insertText', false, textToInsert + ', ');
// Simulate focus and blur to mimic user interaction
activeInput.focus();
activeInput.blur();
setTimeout(() => {
activeInput.focus();
}, 0); // Delay refocus to allow React to process
// Clear suggestions and keep focus
clearSuggestions();
activeInput.focus();
}
function updat###ggestionSelection() {
if (!suggestionsBox) return;
const suggestionDivs = suggestionsBox.querySelectorAll('div');
suggestionDivs.forEach((div, index) => {
if (index === selectedSuggestionIndex) {
div.classList.add('autocomplete-selected');
div.scrollIntoView({ block: 'nearest' });
} else {
div.classList.remove('autocomplete-selected');
}
});
}
function getCurrentWord(text, cursorPosition) {
if (cursorPosition === undefined) cursorPosition = text.length;
const textBeforeCursor = text.substring(0, cursorPosition);
const lastCommaIndex = textBeforeCursor.lastIndexOf(',');
let startPos, word;
if (lastCommaIndex !== -1) {
startPos = lastCommaIndex + 1;
word = textBeforeCursor.substring(startPos).trim();
// Find the exact position where the trimmed word starts
if (word) {
const leadingSpaces = textBeforeCursor.substring(startPos).length - textBeforeCursor.substring(startPos).trimLeft().length;
startPos = startPos + leadingSpaces;
}
} else {
startPos = 0;
word = textBeforeCursor.trim();
// If the text has leading spaces, adjust the start position
if (word && textBeforeCursor !== word) {
startPos = textBeforeCursor.indexOf(word);
}
}
return { word, startPos };
}
// Add debug logging function
function debug(msg) {
console.log(`[Wiki Debug] ${msg}`);
}
// Create settings panel DOM
function createSettingsPanel() {
const settingsPanel = document.createElement('div');
settingsPanel.className = 'wiki-settings-panel';
// Header
const header = document.createElement('h2');
header.textContent = 'Wiki & Autocomplete Settings';
settingsPanel.appendChild(header);
// Hotkey section
const hotkeySection = document.createElement('div');
hotkeySection.className = 'settings-section';
const hotkeyTitle = document.createElement('h3');
hotkeyTitle.textContent = 'Hotkeys';
hotkeySection.appendChild(hotkeyTitle);
const hotkeyContent = document.createElement('div');
hotkeyContent.className = 'hotkey-setting';
const hotkeyLabel = document.createElement('label');
hotkeyLabel.textContent = 'Wiki search hotkey:';
const hotkeyInput = document.createElement('input');
hotkeyInput.type = 'text';
hotkeyInput.value = wikiHotkey;
hotkeyInput.maxLength = 1;
hotkeyInput.addEventListener('keydown', function(e) {
e.preventDefault();
this.value = e.key.toLowerCase();
});
hotkeyContent.appendChild(hotkeyLabel);
hotkeyContent.appendChild(hotkeyInput);
hotkeySection.appendChild(hotkeyContent);
settingsPanel.appendChild(hotkeySection);
// Custom tags section
const tagsSection = document.createElement('div');
tagsSection.className = 'settings-section';
const tagsTitle = document.createElement('h3');
tagsTitle.textContent = 'Custom Tags';
tagsSection.appendChild(tagsTitle);
const tagsContainer = document.createElement('div');
tagsContainer.className = 'custom-tags-section';
// Create UI for each existing tag
Object.keys(customTags).forEach(tag => {
const tagRow = createTagRow(tag, customTags[tag]);
tagsContainer.appendChild(tagRow);
});
// Add new tag button
const addTagBtn = document.createElement('button');
addTagBtn.className = 'btn btn-add';
addTagBtn.textContent = '+ Add New Tag';
addTagBtn.addEventListener('click', function() {
const newTagRow = createTagRow('', '');
tagsContainer.insertBefore(newTagRow, addTagBtn);
newTagRow.querySelector('.custom-tag-name').focus();
});
tagsContainer.appendChild(addTagBtn);
tagsSection.appendChild(tagsContainer);
settingsPanel.appendChild(tagsSection);
// Footer with buttons
const footer = document.createElement('div');
footer.className = 'settings-panel-footer';
const cancelBtn = document.createElement('button');
cancelBtn.className = 'btn';
cancelBtn.textContent = 'Cancel';
cancelBtn.addEventListener('click', hideSettingsPanel);
const saveBtn = document.createElement('button');
saveBtn.className = 'btn btn-save';
saveBtn.textContent = 'Save Settings';
saveBtn.addEventListener('click', function() {
const errors = validateAndSaveSettings(hotkeyInput, tagsContainer);
if (errors.length === 0) {
hideSettingsPanel();
} else {
// Display errors
const existingError = settingsPanel.querySelector('.tag-validation-error');
if (existingError) existingError.remove();
const errorDiv = document.createElement('div');
errorDiv.className = 'tag-validation-error';
errorDiv.textContent = errors.join(', ');
footer.insertBefore(errorDiv, cancelBtn);
}
});
footer.appendChild(cancelBtn);
footer.appendChild(saveBtn);
settingsPanel.appendChild(footer);
return settingsPanel;
}
// Helper function to create a tag row
function createTagRow(name, value) {
const row = document.createElement('div');
row.className = 'custom-tag-row';
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.className = 'custom-tag-name';
nameInput.placeholder = 'Tag name';
nameInput.value = name;
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.className = 'custom-tag-value';
valueInput.placeholder = 'Tag value (comma separated)';
valueInput.value = value;
const controlsDiv = document.createElement('div');
controlsDiv.className = 'custom-tag-controls';
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn btn-delete';
deleteBtn.textContent = '🗑️';
deleteBtn.title = 'Delete tag';
deleteBtn.addEventListener('click', function() {
row.remove();
});
controlsDiv.appendChild(deleteBtn);
row.appendChild(nameInput);
row.appendChild(valueInput);
row.appendChild(controlsDiv);
return row;
}
// Validate settings and save
function validateAndSaveSettings(hotkeyInput, tagsContainer) {
const errors = [];
// Validate hotkey
const newHotkey = hotkeyInput.value.trim();
if (!newHotkey) {
errors.push('Hotkey cannot be empty');
} else {
wikiHotkey = newHotkey;
}
// Validate and collect tags
const newCustomTags = {};
const tagRows = tagsContainer.querySelectorAll('.custom-tag-row');
const tagNames = new Set();
tagRows.forEach(row => {
const nameInput = row.querySelector('.custom-tag-name');
const valueInput = row.querySelector('.custom-tag-value');
const name = nameInput.value.trim();
const value = valueInput.value.trim();
if (name && value) {
if (tagNames.has(name)) {
errors.push(`Duplicate tag name: ${name}`);
} else {
tagNames.add(name);
newCustomTags[name] = value;
}
} else if (name || value) {
errors.push(`Tag ${name || 'name'} is missing ${name ? 'value' : 'name'}`);
}
// Skip empty rows (both name and value empty)
});
if (errors.length === 0) {
customTags = newCustomTags;
saveSettings();
}
return errors;
}
// Show settings panel
function showSettingsPanel() {
settingsOpen = true;
// Remove any existing panel
const existingPanel = document.querySelector('.wiki-settings-panel');
if (existingPanel) existingPanel.remove();
// Create and append new panel
const settingsPanel = createSettingsPanel();
wikiOverlay.appendChild(settingsPanel);
}
// Hide settings panel
function hideSettingsPanel() {
const panel = document.querySelector('.wiki-settings-panel');
if (panel) panel.remove();
settingsOpen = false;
}
// Initialize wiki interface immediately
function initializeWiki() {
if (wikiInitialized) {
debug('Wiki already initialized');
return;
}
debug('Initializing wiki interface');
// Make sure settings are loaded
loadSettings();
// Continue with wiki initialization
wikiOverlay = document.createElement('div');
wikiOverlay.className = 'wiki-search-overlay';
wikiSearchContainer = document.createElement('div');
wikiSearchContainer.className = 'wiki-search-container';
const searchBar = document.createElement('input');
searchBar.className = 'wiki-search-bar';
searchBar.placeholder = 'Search tag wiki...';
// Create container for all buttons
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'wiki-buttons-container';
// Add navigation history buttons
const navContainer = document.createElement('div');
navContainer.className = 'wiki-nav-history';
const backButton = document.createElement('button');
backButton.className = 'wiki-nav-button back';
backButton.textContent = '<';
backButton.disabled = true;
backButton.title = 'Go back to previous tag';
backButton.addEventListener('click', navigateWikiHistory.bind(null, -1));
const forwardButton = document.createElement('button');
forwardButton.className = 'wiki-nav-button forward';
forwardButton.textContent = '>';
forwardButton.disabled = true;
forwardButton.title = 'Go forward to next tag';
forwardButton.addEventListener('click', navigateWikiHistory.bind(null, 1));
navContainer.appendChild(backButton);
navContainer.appendChild(forwardButton);
// Add settings button
const settingsButton = document.createElement('button');
settingsButton.className = 'wiki-settings-button';
settingsButton.textContent = '⚙️ Settings';
settingsButton.addEventListener('click', function(e) {
e.preventDefault();
showSettingsPanel();
});
// Add navigation buttons first, then settings button
buttonsContainer.appendChild(navContainer);
buttonsContainer.appendChild(settingsButton);
wikiContent = document.createElement('div');
wikiContent.className = 'wiki-content';
wikiContent.style.display = 'none';
wikiSearchContainer.appendChild(searchBar);
wikiSearchContainer.appendChild(buttonsContainer);
wikiSearchContainer.appendChild(wikiContent);
wikiOverlay.appendChild(wikiSearchContainer);
document.body.appendChild(wikiOverlay);
// Separate key handler based on configurable hotkey
document.addEventListener('keydown', function(e) {
if (e.key.toLowerCase() === wikiHotkey.toLowerCase() && !isInputFocused()) {
debug(`Hotkey ${wikiHotkey} pressed, showing wiki search`);
e.preventDefault();
showWikiSearch();
}
});
searchBar.addEventListener('keydown', async function(e) {
if (e.key === 'Enter') {
e.preventDefault();
await loadWikiInfo(searchBar.value);
} else if (e.key === 'Escape') {
if (settingsOpen) {
hideSettingsPanel();
} else {
hideWikiSearch();
}
}
});
wikiOverlay.addEventListener('click', function(e) {
if (e.target === wikiOverlay) {
if (settingsOpen) {
hideSettingsPanel();
} else {
hideWikiSearch();
}
}
});
setupWikiSearchAutocomplete(searchBar);
wikiInitialized = true;
debug('Wiki interface initialized');
}
// Navigate through wiki history
function navigateWikiHistory(direction) {
if (!wikiHistory.length) return;
const newIndex = historyIndex + direction;
if (newIndex >= 0 && newIndex < wikiHistory.length) {
isNavigatingHistory = true;
historyIndex = newIndex;
updateHistoryButtons();
loadWikiInfo(wikiHistory[historyIndex]);
}
}
// Update the state of history navigation buttons
function updateHistoryButtons() {
const backButton = document.querySelector('.wiki-nav-button.back');
const forwardButton = document.querySelector('.wiki-nav-button.forward');
if (!backButton || !forwardButton) return;
backButton.disabled = historyIndex <= 0;
forwardButton.disabled = historyIndex >= wikiHistory.length - 1;
}
function hideWikiSearch() {
debug('Hiding wiki search interface');
wikiOverlay.style.display = 'none';
hideSettingsPanel();
}
// Modified showWikiSearch function
function showWikiSearch() {
if (!wikiInitialized) {
debug('Attempting to show wiki before initialization');
initializeWiki();
}
debug('Showing wiki search interface');
wikiOverlay.style.display = 'block';
const searchBar = wikiSearchContainer.querySelector('.wiki-search-bar');
searchBar.value = '';
searchBar.focus();
wikiContent.style.display = 'none';
// Reset navigation buttons when opening search
updateHistoryButtons();
}
// Add keyboard shortcut for closing with escape
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && wikiOverlay.style.display === 'block') {
if (settingsOpen) {
hideSettingsPanel();
} else {
hideWikiSearch();
}
}
});
// Initialize wiki immediately
initializeWiki();
// The rest of the script remains unchanged
function isInputFocused() {
const activeElement = document.activeElement;
return activeElement && (
activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.isContentEditable
);
}
// Wiki helper functions
async function loadWikiInfo(tag) {
// Reset animation
wikiSearchContainer.style.animation = 'none';
wikiSearchContainer.offsetHeight; // Trigger reflow
wikiSearchContainer.style.animation = null;
// Update search bar value
const searchBar = wikiSearchContainer.querySelector('.wiki-search-bar');
searchBar.value = tag;
// Add to history if not navigating through history
if (!isNavigatingHistory) {
// If we're in the middle of the history and searching a new tag,
// remove all entries after current position
if (historyIndex < wikiHistory.length - 1 && historyIndex >= 0) {
wikiHistory = wikiHistory.slice(0, historyIndex + 1);
}
// Don't add duplicate consecutive entries
if (wikiHistory.length === 0 || wikiHistory[wikiHistory.length - 1] !== tag) {
wikiHistory.push(tag);
historyIndex = wikiHistory.length - 1;
}
} else {
// Reset the flag after navigation
isNavigatingHistory = false;
}
// Update button states
updateHistoryButtons();
wikiContent.innerHTML = '<div class="wiki-loading">Loading...</div>';
wikiContent.style.display = 'block';
wikiSearchContainer.style.animation = 'slideUp 0.3s forwards';
try {
const [wikiData, postsData] = await Promise.all([
fetchDanbooruWiki(tag),
fetchDanbooruPosts(tag)
]);
currentPosts = postsData;
currentPostIndex = 0;
displayWikiContent(wikiData, tag);
if (currentPosts.length > 0) {
displayPostImage(currentPosts[0]);
}
} catch (error) {
wikiContent.innerHTML = `<div class="error">Error loading wiki: ${error.message}</div>`;
}
}
function fetchDanbooruWiki(tag) {
// Convert to lowercase and replace spaces with underscores
const formattedTag = tag.trim().toLowerCase().replace(/\s+/g, '_');
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: `https://danbooru.donmai.us/wiki_pages.json?search[title]=${encodeURIComponent(formattedTag)}`,
onload: response => resolve(JSON.parse(response.responseText)),
onerror: reject
});
});
}
function fetchDanbooruPosts(tag) {
const formattedTag = tag.trim().toLowerCase().replace(/\s+/g, '_');
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: `https://danbooru.donmai.us/posts.json?tags=${encodeURIComponent(formattedTag)}&limit=10`,
onload: response => resolve(JSON.parse(response.responseText)),
onerror: reject
});
});
}
function displayWikiContent(wikiData, tag) {
const hasWiki = wikiData && wikiData[0];
const hasPosts = currentPosts && currentPosts.length > 0;
wikiContent.innerHTML = `
<div class="wiki-text-content">
<h2>${tag}</h2>
<div class="wiki-description">
${hasWiki ? `<p>${formatWikiText(wikiData[0].body)}</p>` :
`<p>No wiki information available for this tag${hasPosts ? ', but images are available.' : '.'}</p>`}
</div>
</div>
<div class="wiki-image-section">
${hasPosts ? `
<div class="wiki-image-container">
<button class="image-nav-button prev" title="Previous image">←</button>
<img class="wiki-image" src="" alt="Tag example">
<button class="image-nav-button next" title="Next image">→</button>
</div>
<div class="wiki-nav-buttons">
<button class="wiki-button view-on-danbooru">View on Danbooru</button>
</div>
` : `
<div class="no-images-message">No images available for this tag</div>
`}
</div>
`;
// Always attach wiki tag event listeners
attachWikiEventListeners();
// Only display images if we have posts
if (hasPosts) {
displayPostImage(currentPosts[0]);
}
}
function formatWikiText(text) {
// Remove backticks that sometimes wrap the content
text = text.replace(/^`|`$/g, '');
// First handle the complex patterns
text = text
// Handle list items with proper indentation
.replace(/^\* (.+)$/gm, '<li>$1</li>')
// Handle Danbooru internal paths (using absolute URLs)
.replace(/"([^"]+)":\s*\/((?:[\w-]+\/)*[\w-]+(?:\?[^"\s]+)?)/g, (match, text, path) => {
const fullUrl = `https://danbooru.donmai.us/${path.trim()}`;
return `<a class="wiki-link" href="${fullUrl}" target="_blank">${text}</a>`;
})
// Handle named links with square brackets
.replace(/"([^"]+)":\[([^\]]+)\]/g, '<a class="wiki-link" href="$2" target="_blank">$1</a>')
// Handle post references
.replace(/!post #(\d+)/g, '<a class="wiki-link" href="https://danbooru.donmai.us/posts/$1" target="_blank">post #$1</a>')
// Handle external links with proper URL capture (must come before wiki links)
.replace(/"([^"]+)":\s*(https?:\/\/[^\s"]+)/g, '<a class="wiki-link" href="$2" target="_blank">$1</a>')
// Handle wiki links with display text, preserving special characters
.replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, (match, tag, display) => {
const cleanTag = tag.trim();
return `<span class="wiki-tag" data-tag="${cleanTag}">${display}</span>`;
})
// Handle simple wiki links, preserving special characters
.replace(/\[\[([^\]]+)\]\]/g, (match, tag) => {
const cleanTag = tag.trim();
return `<span class="wiki-tag" data-tag="${cleanTag}">${cleanTag}</span>`;
})
// Handle BBCode
.replace(/\[b\](.*?)\[\/b\]/g, '<strong>$1</strong>')
.replace(/\[i\](.*?)\[\/i\]/g, '<em>$1</em>')
.replace(/\[code\](.*?)\[\/code\]/g, '<code>$1</code>')
.replace(/\[u\](.*?)\[\/u\]/g, '<u>$1</u>')
// Handle headers with proper spacing
.replace(/^h([1-6])\.\s*(.+)$/gm, (_, size, content) => `\n<h${size}>${content}</h${size}>\n`)
// Add spacing after tag name at start of line
// Handle line breaks and paragraphs
text = text
.replace(/\r\n/g, '\n')  // Normalize line endings
.replace(/\n\n+/g, '</p><p>')
.replace(/\n/g, '<br>');
// Wrap lists in ul tags
text = text.replace(/(<li>.*?<\/li>)\s*(?=<li>|$)/gs, '<ul>$1</ul>');
// Wrap in paragraph if not already wrapped
if (!text.startsWith('<p>')) {
text = `<p>${text}</p>`;
}
return text;
}
// Separate the keyboard handler into its own function
function handleWikiKeydown(e) {
if (wikiOverlay.style.display === 'block') {
if (e.key === 'ArrowLeft') navigateImage(-1);
if (e.key === 'ArrowRight') navigateImage(1);
}
}
function attachWikiEventListeners() {
const prevButton = wikiContent.querySelector('.image-nav-button.prev');
const nextButton = wikiContent.querySelector('.image-nav-button.next');
const viewButton = wikiContent.querySelector('.view-on-danbooru');
const wikiImage = wikiContent.querySelector('.wiki-image');
const wikiTags = wikiContent.querySelectorAll('.wiki-tag');
// Only attach image navigation related listeners if we have posts
if (currentPosts.length > 0) {
if (prevButton) {
prevButton.addEventListener('click', () => navigateImage(-1));
}
if (nextButton) {
nextButton.addEventListener('click', () => navigateImage(1));
}
// Add keyboard navigation only if we have posts
document.removeEventListener('keydown', handleWikiKeydown);
document.addEventListener('keydown', handleWikiKeydown);
if (wikiImage) {
wikiImage.addEventListener('click', () => {
if (currentPosts[currentPostIndex]) {
window.open(currentPosts[currentPostIndex].large_file_url, '_blank');
}
});
}
if (viewButton) {
viewButton.addEventListener('click', () => {
if (currentPosts[currentPostIndex]) {
window.open(`https://danbooru.donmai.us/posts/${currentPosts[currentPostIndex].id}`, '_blank');
}
});
}
}
// Wiki tag navigation works regardless of posts
if (wikiTags) {
wikiTags.forEach(tag => {
tag.addEventListener('click', () => {
const tagName = tag.dataset.tag;
loadWikiInfo(tagName);
});
});
}
}
function displayPostImage(post) {
const imageContainer = wikiContent.querySelector('.wiki-image-container');
if (!imageContainer) return; // Guard against missing container
if (!post || (!post.preview_file_url && !post.file_url)) return;
const prevButton = imageContainer.querySelector('.image-nav-button.prev');
const nextButton = imageContainer.querySelector('.image-nav-button.next');
const image = imageContainer.querySelector('.wiki-image');
if (!image) return; // Guard against missing image element
image.src = post.large_file_url || post.preview_file_url || post.file_url;
if (prevButton) prevButton.style.visibility = currentPostIndex <= 0 ? 'hidden' : 'visible';
if (nextButton) nextButton.style.visibility = currentPostIndex >= currentPosts.length - 1 ? 'hidden' : 'visible';
// Remove any existing event listeners first to prevent duplicates
const newPrevButton = prevButton.cloneNode(true);
const newNextButton = nextButton.cloneNode(true);
prevButton.parentNode.replaceChild(newPrevButton, prevButton);
nextButton.parentNode.replaceChild(newNextButton, nextButton);
// Attach fresh event listeners
newPrevButton.addEventListener('click', (e) => {
e.stopPropagation();
navigateImage(-1);
});
newNextButton.addEventListener('click', (e) => {
e.stopPropagation();
navigateImage(1);
});
// Reattach image click listener
image.addEventListener('click', () => {
window.open(post.large_file_url || post.file_url, '_blank');
});
}
function navigateImage(direction) {
const newIndex = currentPostIndex + direction;
if (newIndex >= 0 && newIndex < currentPosts.length) {
currentPostIndex = newIndex;
displayPostImage(currentPosts[newIndex]);
}
}
// Add new function for wiki search autocomplete
function setupWikiSearchAutocomplete(searchBar) {
const suggestionsBox = document.createElement('div');
suggestionsBox.className = 'wiki-search-suggestions';
suggestionsBox.style.display = 'none';
document.body.appendChild(suggestionsBox); // Append to body instead
let selectedIndex = -1;
// Update suggestions box position when showing
function updat###ggestionsPosition() {
const searchBarRect = searchBar.getBoundingClientRect();
suggestionsBox.style.top = `${searchBarRect.bottom + window.scrollY}px`;
}
searchBar.addEventListener('input', () => {
const term = searchBar.value.replace(/\s+/g, '_').trim();
if (term) {
fetchSuggestionsForWiki(term, suggestionsBox);
updat###ggestionsPosition();
} else {
suggestionsBox.style.display = 'none';
}
});
// Update position on scroll or resize
window.addEventListener('scroll', () => {
if (suggestionsBox.style.display === 'block') {
updat###ggestionsPosition();
}
});
window.addEventListener('resize', () => {
if (suggestionsBox.style.display === 'block') {
updat###ggestionsPosition();
}
});
searchBar.addEventListener('keydown', (e) => {
const suggestions = suggestionsBox.children;
if (suggestions.length === 0) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, suggestions.length - 1);
updateWikiSuggestionSelection(suggestions, selectedIndex);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, -1);
updateWikiSuggestionSelection(suggestions, selectedIndex);
} else if (e.key === 'Enter' && selectedIndex !== -1) {
e.preventDefault();
searchBar.value = suggestions[selectedIndex].textContent;
suggestionsBox.style.display = 'none';
loadWikiInfo(searchBar.value);
}
});
// Close suggestions when clicking outside
document.addEventListener('click', (e) => {
if (!searchBar.contains(e.target) && !suggestionsBox.contains(e.target)) {
suggestionsBox.style.display = 'none';
}
});
}
function fetchSuggestionsForWiki(term, suggestionsBox) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
GM.xmlHttpRequest({
method: 'GET',
url: `https://gelbooru.com/index.php?page=autocomplete2&term=${encodeURIComponent(term)}&type=tag_query&limit=10`,
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
showWikiSuggestions(data, suggestionsBox);
} catch (e) {
console.error("Error parsing suggestions:", e);
}
}
}
});
}, debounceDelay);
}
function showWikiSuggestions(suggestions, suggestionsBox) {
suggestionsBox.innerHTML = '';
if (suggestions.length === 0) {
suggestionsBox.style.display = 'none';
return;
}
suggestions.forEach(suggestion => {
const div = document.createElement('div');
div.className = 'wiki-search-suggestion';
div.textContent = suggestion.label;
div.addEventListener('click', () => {
const searchBar = suggestionsBox.parentNode.querySelector('.wiki-search-bar');
searchBar.value = suggestion.label;
suggestionsBox.style.display = 'none';
loadWikiInfo(suggestion.label);
});
suggestionsBox.appendChild(div);
});
suggestionsBox.style.display = 'block';
}
function updateWikiSuggestionSelection(suggestions, selectedIndex) {
Array.from(suggestions).forEach((suggestion, index) => {
suggestion.classList.toggle('selected', index === selectedIndex);
if (index === selectedIndex) {
suggestion.scrollIntoView({ block: 'nearest' });
}
});
}
// Ensure script runs as soon as DOM is ready
document.addEventListener('DOMContentLoaded', () => {
loadSettings();
setupAutocomplete();
initializeWiki();
});
})();