Civitai Prompt Autocomplete & Tag Wiki

Adds tag autocomplete and wiki lookup features

目前為 2025-02-16 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Civitai Prompt Autocomplete & Tag Wiki
// @namespace    http://tampermonkey.net/
// @version      3.1
// @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
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // All variable declarations moved to top
    let promptInput = null;
    let suggestionsBox = null;
    let currentSuggestions = [];
    let selectedSuggestionIndex = -1;
    let debounceTimer;
    const debounceDelay = 50;
    let lastCurrentWord = "";
    let wikiOverlay = null;
    let wikiSearchContainer = null;
    let wikiContent = null;
    let currentPosts = [];
    let currentPostIndex = 0;
    let wikiInitialized = false;
    const customTags = {
        'quality': 'masterpiece, best quality, amazing quality, very detailed',
        'quality_pony': 'score_9, score_8_up, score_7_up, score_6_up',
        // Add more custom tags here following the same format
    };

    // Modify the CSS
    GM_addStyle(`
        #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;
        }

        .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;
        }

        .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; /* Changed from absolute to 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; /* Increased z-index */
            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; }
        }

        // Update header styles
        .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;
        }
    `);

    // Replace all initialization code with this new version
    function handleInputEvents(e) {
        const input = e.target;
        if (input.id === 'input_prompt') {
            const currentWord = getCurrentWord(input.value, input.selectionStart);
            lastCurrentWord = currentWord;
            fetchSuggestions(currentWord);
        }
    }

    function handleKeydownEvents(e) {
        if (e.target.id !== 'input_prompt') return;
        
        if (e.key === 'ArrowDown') {
            e.preventDefault();
            if (suggestionsBox?.style.display === 'block' && currentSuggestions.length > 0) {
                selectedSuggestionIndex = Math.min(selectedSuggestionIndex + 1, currentSuggestions.length - 1);
                updateSuggestionSelection();
            }
        } else if (e.key === 'ArrowUp') {
            e.preventDefault();
            if (suggestionsBox?.style.display === 'block' && currentSuggestions.length > 0) {
                selectedSuggestionIndex = Math.max(selectedSuggestionIndex - 1, -1);
                updateSuggestionSelection();
            }
        } 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();
        }

        promptInput = document.getElementById('input_prompt');
        if (!promptInput) return;

        // Create new suggestions box
        suggestionsBox = document.createElement('div');
        suggestionsBox.id = 'autocomplete-suggestions-box';
        suggestionsBox.style.display = 'none';
        promptInput.parentNode.insertBefore(suggestionsBox, promptInput.nextSibling);

        // 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) && !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 cleanupAutocomplete() {
        if (suggestionsBox) {
            suggestionsBox.remove();
            suggestionsBox = null;
        }
        // Remove old event listeners if prompt input exists
        if (promptInput) {
            const newPromptInput = promptInput.cloneNode(true);
            promptInput.parentNode.replaceChild(newPromptInput, promptInput);
            promptInput = null;
        }
    }

    function fetchSuggestions(term) {
        if (!term) {
            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,
                                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) {
        const currentPrompt = promptInput.value;
        const cursorPosition = promptInput.selectionStart;
        let textBeforeCursor = currentPrompt.substring(0, cursorPosition);
        const textAfterCursor = currentPrompt.substring(cursorPosition);

        // Remove the typed prefix (lastCurrentWord) from textBeforeCursor
        if (lastCurrentWord) {
            const lastWordIndex = textBeforeCursor.lastIndexOf(lastCurrentWord);
            if (lastWordIndex !== -1) {
                textBeforeCursor = textBeforeCursor.substring(0, lastWordIndex);
            }
        }

        // Find the matching suggestion object
        const suggestionObj = currentSuggestions.find(s => s.label === suggestion);
        const textToInsert = suggestionObj?.isCustom ? suggestionObj.insertText : suggestion;

        // Insert suggestion at cursor, preserving newlines
        promptInput.value = textBeforeCursor + textToInsert + ', ' + textAfterCursor;

        // Move cursor to the end of the inserted suggestion
        promptInput.selectionStart = promptInput.selectionEnd = (textBeforeCursor + textToInsert + ', ').length;

        clearSuggestions();
        promptInput.focus();
    }


    function updateSuggestionSelection() {
        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 currentWord;
        if (lastCommaIndex !== -1) {
            currentWord = textBeforeCursor.substring(lastCommaIndex + 1);
        } else {
            currentWord = textBeforeCursor;
        }
        return currentWord.trim();
    }

    // Add debug logging function
    function debug(msg) {
        console.log(`[Wiki Debug] ${msg}`);
    }

    // Initialize wiki interface immediately
    function initializeWiki() {
        if (wikiInitialized) {
            debug('Wiki already initialized');
            return;
        }

        debug('Initializing wiki interface');
        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...';
        
        wikiContent = document.createElement('div');
        wikiContent.className = 'wiki-content';
        wikiContent.style.display = 'none';
        
        wikiSearchContainer.appendChild(searchBar);
        wikiSearchContainer.appendChild(wikiContent);
        wikiOverlay.appendChild(wikiSearchContainer);
        document.body.appendChild(wikiOverlay);

        // Separate 't' key handler
        document.addEventListener('keydown', function(e) {
            // debug(`Key pressed: ${e.key}, Input focused: ${isInputFocused()}`);
            if (e.key === 't' && !isInputFocused()) {
                debug('T key 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') {
                hideWikiSearch();
            }
        });

        wikiOverlay.addEventListener('click', function(e) {
            if (e.target === wikiOverlay) {
                hideWikiSearch();
            }
        });

        setupWikiSearchAutocomplete(searchBar);

        wikiInitialized = true;
        debug('Wiki interface initialized');
    }

    function hideWikiSearch() {
        debug('Hiding wiki search interface');
        wikiOverlay.style.display = 'none';
    }

    // 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';
    }

    // Initialize wiki immediately
    initializeWiki();

    // 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;

        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-navigation">
                        <button class="image-nav-button prev" title="Previous image">←</button>
                        <button class="image-nav-button next" title="Next image">→</button>
                    </div>
                    <div class="wiki-image-container">
                        <img class="wiki-image" src="" alt="Tag example">
                    </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;
    }

    function isInputFocused() {
        const activeElement = document.activeElement;
        return activeElement && (
            activeElement.tagName === 'INPUT' ||
            activeElement.tagName === 'TEXTAREA' ||
            activeElement.isContentEditable
        );
    }

    // 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('.prev');
        const nextButton = imageContainer.querySelector('.next');
        const image = imageContainer.querySelector('.wiki-image');
        
        if (!image) return; // Guard against missing image element

        image.src = 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';

        // Reattach event listeners
        if (prevButton) prevButton.addEventListener('click', () => navigateImage(-1));
        if (nextButton) nextButton.addEventListener('click', () => navigateImage(1));
        
        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[currentPostIndex]);
        }
    }

    // Add keyboard shortcut for closing with escape
    document.addEventListener('keydown', e => {
        if (e.key === 'Escape' && wikiOverlay.style.display === 'block') {
            hideWikiSearch();
        }
    });

    // 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 updateSuggestionsPosition() {
            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);
                updateSuggestionsPosition();
            } else {
                suggestionsBox.style.display = 'none';
            }
        });

        // Update position on scroll or resize
        window.addEventListener('scroll', () => {
            if (suggestionsBox.style.display === 'block') {
                updateSuggestionsPosition();
            }
        });

        window.addEventListener('resize', () => {
            if (suggestionsBox.style.display === 'block') {
                updateSuggestionsPosition();
            }
        });

        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' });
            }
        });
    }

})();