Guest User

E6Autotagger - Modded sorting

a guest
Jul 28th, 2025
28
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 67.81 KB | Source Code | 0 0
  1. // ==UserScript==
  2. // @name         E6 Autotagger 2.3.4
  3. // @version      2.3.4
  4. // @author       Jax (Slop_Dragon)
  5. // @description  Adds a button that automatically tags e621 images using local AI
  6. // @icon         https://www.google.com/s2/favicons?domain=e621.net
  7. // @match        https://e621.net/uploads/new
  8. // @match        https://e926.net/uploads/new
  9. // @match        https://e6ai.net/uploads/new
  10. // @match        https://e621.net/posts/*
  11. // @match        https://e926.net/posts/*
  12. // @match        https://e6ai.net/posts/*
  13. // @license      CC BY-NC-SA 4.0
  14. // @grant        GM_getValue
  15. // @grant        GM_setValue
  16. // @grant        GM_registerMenuCommand
  17. // @grant        GM_xmlhttpRequest
  18. // @connect      127.0.0.1
  19. // @connect      localhost
  20. // @connect      e621.net
  21. // @connect      e926.net
  22. // @connect      e6ai.net
  23. // ==/UserScript==
  24.  
  25. (function() {
  26.     'use strict';
  27.  
  28.     const DEFAULT_CONFIG = {
  29.         localEndpoint: 'http://127.0.0.1:7860',
  30.         confidence: 0.25,
  31.         tagBlacklist: '',
  32.         constantTags: '',
  33.         rescaleTagBox: true,
  34.         preserveExistingTags: false,
  35.         sortTagsAlphabetically: false,
  36.         enableAutoTagOnEdit: false,
  37.         sortingMode: 'flat',
  38.         requestTimeout: 10000,
  39.         maxRetries: 2,
  40.     };
  41.  
  42.     const CSS = {
  43.         button: `
  44.             .ai-tag-button {
  45.                 display: inline-block;
  46.                 border: 1px solid var(--fg-color-alt, #666);
  47.                 padding: 5px 10px;
  48.                 border-radius: 6px;
  49.                 cursor: pointer;
  50.                 margin: 5px 0;
  51.                 background-color: var(--button-bg-color, #4a4a4a);
  52.                 color: var(--button-text-color, white);
  53.                 font-size: 16px;
  54.                 height: 28px;
  55.                 line-height: 18px;
  56.             }
  57.             .ai-tag-button:disabled {
  58.                 opacity: 0.5;
  59.                 cursor: not-allowed;
  60.             }
  61.             .ai-sort-button {
  62.                 font-size: 16px;
  63.                 height: 28px;
  64.                 line-height: 18px;
  65.                 border-radius: 6px;
  66.             }
  67.         `,
  68.         suggestions: `
  69.             .tag-suggestions-container {
  70.                 position: absolute;
  71.                 max-height: 200px;
  72.                 width: 100%;
  73.                 overflow-y: auto;
  74.                 background-color: var(--bg-color, #333);
  75.                 border: 1px solid var(--fg-color-alt, #666);
  76.                 border-radius: 6px;
  77.                 z-index: 1000;
  78.                 display: none;
  79.                 font-family: Verdana, Geneva, sans-serif;
  80.             }
  81.             .tag-suggestion {
  82.                 padding: 5px 10px;
  83.                 cursor: pointer;
  84.                 border-bottom: 1px solid var(--border-color, #555);
  85.                 font-family: Verdana, Geneva, sans-serif;
  86.             }
  87.             .tag-suggestion.active {
  88.                 background-color: var(--bg-color-alt, #444);
  89.             }
  90.             .tag-count {
  91.                 float: right;
  92.                 opacity: 0.7;
  93.             }
  94.         `,
  95.         dialog: `
  96.             #e6-autotagger-config {
  97.                 padding: 20px;
  98.                 border-radius: 6px;
  99.                 background-color: var(--dialog-bg-color, #2a2a2a);
  100.                 color: var(--text-color, #fff);
  101.                 border: 1px solid var(--fg-color-alt, #666);
  102.                 min-width: 400px;
  103.                 max-width: 600px;
  104.                 max-height: 80vh;
  105.                 overflow-y: auto;
  106.                 z-index: 999;
  107.                 font-family: Verdana, Geneva, sans-serif;
  108.             }
  109.             .config-row {
  110.                 margin-bottom: 15px;
  111.                 position: relative;
  112.             }
  113.             .config-input {
  114.                 width: 100%;
  115.                 padding: 5px;
  116.                 background-color: #ffffff !important;
  117.                 color: #000000 !important;
  118.                 border: 1px solid var(--fg-color-alt, #666);
  119.                 border-radius: 6px;
  120.                 box-sizing: border-box;
  121.                 resize: vertical;
  122.                 max-height: 150px;
  123.                 font-family: Verdana, Geneva, sans-serif;
  124.                 height: 28px;
  125.             }
  126.             select.config-input {
  127.                 height: auto;
  128.                 padding: 5px;
  129.                 line-height: 1.5;
  130.                 text-align: left;
  131.                 text-align-last: left;
  132.                 text-indent: 1px;
  133.                 appearance: menulist;
  134.             }
  135.             .config-status {
  136.                 margin-top: 10px;
  137.                 padding: 10px;
  138.                 border-radius: 6px;
  139.                 font-family: Verdana, Geneva, sans-serif;
  140.             }
  141.         `,
  142.         textarea: `
  143.             #post_tags, #post_tag_string {
  144.                 background-color: #ffffff !important;
  145.                 color: #000000 !important;
  146.                 font-family: Verdana, Geneva, sans-serif;
  147.             }
  148.         `
  149.     };
  150.  
  151.     const SELECTORS = {
  152.         button: '.ai-tag-button',
  153.         warningText: '.ai-warning-text',
  154.         confidenceInput: '.ai-confidence-input',
  155.         uploadPreview: '.upload_preview_img',
  156.         tagTextarea: '#post_tags',
  157.         editTagTextarea: '#post_tag_string',
  158.     };
  159.  
  160.     const TAG_CATEGORIES = {
  161.         0: '#b4c7d9',
  162.         1: '#ed5d1f',
  163.         3: '#d0d',    
  164.         4: '#0f0',    
  165.         5: '#ed5881',
  166.         6: '#ff3f3f',
  167.         7: '#fff'    
  168.     };
  169.  
  170.     const state = {
  171.         config: null,
  172.         connectionCheckInterval: null,
  173.         lastSuccessfulCheck: 0,
  174.         initializedPages: new Set(),
  175.         isWatchingEditButton: false,
  176.         observers: [],
  177.         eventListeners: []
  178.     };
  179.  
  180.     const elementCache = new Map();
  181.  
  182.     const DEBUG = {
  183.         enabled: false,
  184.         log: function(...args) {
  185.             if (this.enabled) console.log('[E6T]', ...args);
  186.         },
  187.         info: function(...args) {
  188.             if (this.enabled) console.info('[E6T]', ...args);
  189.         },
  190.         warn: function(...args) {
  191.             if (this.enabled) console.warn('[E6T]', ...args);
  192.         },
  193.         error: function(...args) {
  194.             if (this.enabled) console.error('[E6T]', ...args);
  195.         },
  196.         toggle: function() {
  197.             this.enabled = !this.enabled;
  198.             console.log(`[E6Tagger] Debug logging is now ${this.enabled ? 'enabled' : 'disabled'}`);
  199.             return this.enabled;
  200.         }
  201.     };
  202.    
  203.     GM_registerMenuCommand('Toggle Console logs', () => DEBUG.toggle());
  204.  
  205.     const loadConfig = () => {
  206.         DEBUG.log('Config', 'Loading configuration');
  207.         const config = { ...DEFAULT_CONFIG };
  208.        
  209.         for (const key in DEFAULT_CONFIG) {
  210.             config[key] = GM_getValue(key, DEFAULT_CONFIG[key]);
  211.         }
  212.  
  213.         if (!config.localEndpoint.endsWith('/api/predict')) {
  214.             config.localEndpoint = config.localEndpoint.replace(/\/$/, '') + '/api/predict';
  215.             DEBUG.log('Config', 'Adjusted API endpoint format', config.localEndpoint);
  216.         }
  217.  
  218.         state.config = config;
  219.         DEBUG.log('Config', 'Configuration loaded', config);
  220.         return config;
  221.     };
  222.  
  223.     const saveConfig = (newConfig) => {
  224.         DEBUG.log('Config', 'Saving new configuration', newConfig);
  225.        
  226.         for (const key in newConfig) {
  227.             if (key in DEFAULT_CONFIG) {
  228.                 GM_setValue(key, newConfig[key]);
  229.             }
  230.         }
  231.         state.config = { ...state.config, ...newConfig };
  232.         DEBUG.log('Config', 'Configuration saved successfully');
  233.     };
  234.  
  235.     const getElement = (selector, parent = document, forceRefresh = false) => {
  236.         const cacheKey = parent === document ? selector : `${parent.id || 'parent'}-${selector}`;
  237.        
  238.         if (!forceRefresh && elementCache.has(cacheKey)) {
  239.             return elementCache.get(cacheKey);
  240.         }
  241.        
  242.         const element = parent.querySelector(selector);
  243.         if (element) {
  244.             elementCache.set(cacheKey, element);
  245.         }
  246.         return element;
  247.     };
  248.  
  249.     const createDebounce = (func, delay) => {
  250.         let timer;
  251.         return (...args) => {
  252.             clearTimeout(timer);
  253.             timer = setTimeout(() => func(...args), delay);
  254.         };
  255.     };
  256.  
  257.     const addStyles = () => {
  258.         if (!document.getElementById('e6-autotagger-styles')) {
  259.             const computedStyle = getComputedStyle(document.body);
  260.             const themeMainAttr = document.body.getAttribute('data-th-main') || 'bloodlust';
  261.            
  262.             const getThemeColors = () => {
  263.                 const cssVars = {
  264.                     '--bg-color': '#333',
  265.                     '--bg-color-alt': '#444',
  266.                     '--text-color': '#fff',
  267.                     '--button-bg-color': '#4a4a4a',
  268.                     '--button-text-color': 'white',
  269.                     '--dialog-bg-color': '#2a2a2a',
  270.                     '--input-bg-color': '#ffffff',
  271.                     '--input-text-color': '#000000',
  272.                     '--fg-color-alt': '#666',
  273.                     '--border-color': '#555'
  274.                 };
  275.                
  276.                 try {
  277.                     const accentColor = document.querySelector('meta[name="theme-color"]')?.getAttribute('content') || '#00549e';
  278.                     cssVars['--accent-color'] = accentColor;
  279.                    
  280.                     const navBg = computedStyle.getPropertyValue('--nav-bg-color') ||
  281.                                  getBackgroundColor('.navigation') ||
  282.                                  '#2e2e2e';
  283.                    
  284.                     const pageBg = computedStyle.getPropertyValue('--page-bg-color') ||
  285.                                   getBackgroundColor('#page') ||
  286.                                   '#343434';
  287.                    
  288.                     const sectionColor = computedStyle.getPropertyValue('--color-section') ||
  289.                                        getBackgroundColor('.box-section') ||
  290.                                        getBackgroundColor('.section') ||
  291.                                        navBg;
  292.  
  293.                     cssVars['--bg-color'] = pageBg;
  294.                     cssVars['--bg-color-alt'] = adjustBrightness(pageBg, 20);
  295.                     cssVars['--dialog-bg-color'] = sectionColor;
  296.                     cssVars['--input-bg-color'] = '#ffffff';
  297.                     cssVars['--input-text-color'] = '#000000';
  298.                     cssVars['--button-bg-color'] = '#ffffff';
  299.                     cssVars['--button-text-color'] = '#000000';
  300.                    
  301.                     const isDark = isColorDark(pageBg);
  302.                     cssVars['--text-color'] = isDark ? '#fff' : '#000';
  303.                    
  304.                     cssVars['--fg-color-alt'] = adjustBrightness(pageBg, isDark ? 50 : -50);
  305.                     cssVars['--border-color'] = adjustBrightness(pageBg, isDark ? 30 : -30);
  306.                 } catch (e) {
  307.                     console.error('Error detecting theme colors:', e);
  308.                 }
  309.                
  310.                 return cssVars;
  311.             };
  312.            
  313.             const getBackgroundColor = (selector) => {
  314.                 const element = document.querySelector(selector);
  315.                 if (!element) return null;
  316.                 return window.getComputedStyle(element).backgroundColor;
  317.             };
  318.            
  319.             const isColorDark = (color) => {
  320.                 try {
  321.                     let r, g, b;
  322.                    
  323.                     if (color.startsWith('#')) {
  324.                         const hex = color.substring(1);
  325.                         r = parseInt(hex.substr(0, 2), 16);
  326.                         g = parseInt(hex.substr(2, 2), 16);
  327.                         b = parseInt(hex.substr(4, 2), 16);
  328.                     } else if (color.startsWith('rgb')) {
  329.                         const rgbValues = color.match(/\d+/g);
  330.                         if (rgbValues && rgbValues.length >= 3) {
  331.                             r = parseInt(rgbValues[0]);
  332.                             g = parseInt(rgbValues[1]);
  333.                             b = parseInt(rgbValues[2]);
  334.                         } else {
  335.                             return true;
  336.                         }
  337.                     } else {
  338.                         return true;
  339.                     }
  340.                    
  341.                     const brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
  342.                     return brightness < 0.5;
  343.                 } catch (e) {
  344.                     console.error('Error checking color brightness:', e);
  345.                     return true;
  346.                 }
  347.             };
  348.            
  349.             const adjustBrightness = (color, percent) => {
  350.                 try {
  351.                     let r, g, b;
  352.                    
  353.                     if (color.startsWith('#')) {
  354.                         const hex = color.substring(1);
  355.                         r = parseInt(hex.substr(0, 2), 16);
  356.                         g = parseInt(hex.substr(2, 2), 16);
  357.                         b = parseInt(hex.substr(4, 2), 16);
  358.                     } else if (color.startsWith('rgb')) {
  359.                         const rgbValues = color.match(/\d+/g);
  360.                         if (rgbValues && rgbValues.length >= 3) {
  361.                             r = parseInt(rgbValues[0]);
  362.                             g = parseInt(rgbValues[1]);
  363.                             b = parseInt(rgbValues[2]);
  364.                         } else {
  365.                             return color;
  366.                         }
  367.                     } else {
  368.                         return color;
  369.                     }
  370.                    
  371.                     r = Math.max(0, Math.min(255, r + percent));
  372.                     g = Math.max(0, Math.min(255, g + percent));
  373.                     b = Math.max(0, Math.min(255, b + percent));
  374.                    
  375.                     return `rgb(${r}, ${g}, ${b})`;
  376.                 } catch (e) {
  377.                     console.error('Error adjusting color brightness:', e);
  378.                     return color;
  379.                 }
  380.             };
  381.            
  382.             const themeColors = getThemeColors();
  383.            
  384.             const cssVarsString = Object.entries(themeColors)
  385.                 .map(([key, value]) => `${key}: ${value};`)
  386.                 .join('\n');
  387.            
  388.             const styleElement = document.createElement('style');
  389.             styleElement.id = 'e6-autotagger-styles';
  390.             styleElement.textContent = `:root {\n${cssVarsString}\n}\n${Object.values(CSS).join('\n')}`;
  391.             document.head.appendChild(styleElement);
  392.         }
  393.     };
  394.  
  395.     const registerEventListener = (element, event, handler, options) => {
  396.         element.addEventListener(event, handler, options);
  397.         state.eventListeners.push({ element, event, handler });
  398.     };
  399.  
  400.     const registerObserver = (observer) => {
  401.         state.observers.push(observer);
  402.         return observer;
  403.     };
  404.  
  405.     const clearResources = () => {
  406.         state.eventListeners.forEach(({ element, event, handler }) => {
  407.             element.removeEventListener(event, handler);
  408.         });
  409.         state.eventListeners = [];
  410.  
  411.         state.observers.forEach(observer => observer.disconnect());
  412.         state.observers = [];
  413.  
  414.         if (state.connectionCheckInterval) {
  415.             clearInterval(state.connectionCheckInterval);
  416.             state.connectionCheckInterval = null;
  417.         }
  418.  
  419.         elementCache.clear();
  420.     };
  421.  
  422.     const normalizeTag = tag => {
  423.         tag = tag.toLowerCase().trim();
  424.         return tag.includes(' ') ? tag.replace(/\s+/g, '_') :
  425.             tag.includes('_') ? tag.replace(/_+/g, '_') : tag;
  426.     };
  427.  
  428.     const formatTags = (tagString, existingTags = '') => {
  429.         DEBUG.log('Tags', 'Formatting tags', {
  430.             tagStringLength: tagString?.length,
  431.             existingTagsLength: existingTags?.length
  432.         });
  433.        
  434.         tagString = typeof tagString === 'string' ? tagString : '';
  435.         const config = state.config;
  436.  
  437.         const blacklist = config.tagBlacklist
  438.             .split(',')
  439.             .map(normalizeTag)
  440.             .filter(tag => tag.length > 0);
  441.        
  442.         DEBUG.log('Tags', 'Blacklisted tags', blacklist);
  443.  
  444.         const newTags = tagString.split(',')
  445.             .map(tag => tag.trim())
  446.             .filter(tag => tag.length > 0 && !blacklist.includes(normalizeTag(tag)))
  447.             .map(tag => tag.replace(/\s+/g, '_'));
  448.        
  449.         DEBUG.log('Tags', 'Processed new tags', {
  450.             count: newTags.length,
  451.             tagsAfterBlacklist: newTags.length
  452.         });
  453.  
  454.         let resultTags = [];
  455.  
  456.         if (config.preserveExistingTags && existingTags.trim()) {
  457.             const existingTagsArray = existingTags.trim().split(/[\s\n]+/).filter(tag => tag.length > 0);
  458.             DEBUG.log('Tags', 'Preserving existing tags', { count: existingTagsArray.length });
  459.             resultTags = [...new Set([...existingTagsArray, ...newTags])];
  460.         } else {
  461.             resultTags = [...new Set(newTags)];
  462.         }
  463.        
  464.         DEBUG.log('Tags', 'Combined tags after deduplication', { count: resultTags.length });
  465.  
  466.         resultTags.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
  467.  
  468.         let groupedTags = {};
  469.         let result;
  470.        
  471.         switch (config.sortingMode) {
  472.             case 'grouped':
  473.                 DEBUG.log('Tags', 'Using grouped sorting mode');
  474.                 groupedTags = {};
  475.                 resultTags.forEach(tag => {
  476.                     const firstLetter = tag.charAt(0).toLowerCase();
  477.                     if (!groupedTags[firstLetter]) {
  478.                         groupedTags[firstLetter] = [];
  479.                     }
  480.                     groupedTags[firstLetter].push(tag);
  481.                 });
  482.                 result = Object.keys(groupedTags)
  483.                     .sort()
  484.                     .map(letter => groupedTags[letter].join('  ') + '  ') // double spaces between and after each line
  485.                     .join('\n');
  486.                 break;
  487.            
  488.             case 'oneperline':
  489.                 DEBUG.log('Tags', 'Using one-per-line sorting mode');
  490.                 result = resultTags.map(tag => tag + '  ').join('\n'); // Add double space after each tag
  491.                 break;
  492.            
  493.             default:
  494.                 DEBUG.log('Tags', 'Using flat sorting mode');
  495.                 result = resultTags.join('  ') + '  '; // Regular double trailing space between and at the end
  496.                 break;
  497.         }
  498.        
  499.         DEBUG.log('Tags', 'Final formatted tags', {
  500.             length: result.length,
  501.             sortingMode: config.sortingMode
  502.         });
  503.        
  504.         return result;
  505.     };
  506.  
  507.     const checkConnection = (updateUI = true) => {
  508.         DEBUG.log('Connection', 'Checking connection to API endpoint', state.config?.localEndpoint);
  509.         return new Promise((resolve, reject) => {
  510.             const config = state.config || loadConfig();
  511.            
  512.             GM_xmlhttpRequest({
  513.                 method: "POST",
  514.                 url: config.localEndpoint,
  515.                 headers: { "Content-Type": "application/json" },
  516.                 data: JSON.stringify({ data: ["", config.confidence], fn_index: 0 }),
  517.                 timeout: config.requestTimeout,
  518.                 onload: response => {
  519.                     try {
  520.                         JSON.parse(response.responseText);
  521.                         state.lastSuccessfulCheck = Date.now();
  522.                         DEBUG.info('Connection', 'Connection successful');
  523.                         if (updateUI) setConnectionState(true);
  524.                         resolve(true);
  525.                     } catch (e) {
  526.                         if (Date.now() - state.lastSuccessfulCheck > 5000 && updateUI) {
  527.                             setConnectionState(false);
  528.                         }
  529.                         DEBUG.error('Connection', 'Invalid response format', e);
  530.                         reject(new Error("Invalid response format"));
  531.                     }
  532.                 },
  533.                 onerror: (err) => {
  534.                     if (Date.now() - state.lastSuccessfulCheck > 5000 && updateUI) {
  535.                         setConnectionState(false);
  536.                     }
  537.                     DEBUG.error('Connection', 'Connection failed', err);
  538.                     reject(err);
  539.                 },
  540.                 ontimeout: () => {
  541.                     if (Date.now() - state.lastSuccessfulCheck > 5000 && updateUI) {
  542.                         setConnectionState(false);
  543.                     }
  544.                     DEBUG.error('Connection', 'Connection timed out');
  545.                     reject(new Error("Connection timed out"));
  546.                 }
  547.             });
  548.         });
  549.     };
  550.  
  551.     const setConnectionState = isConnected => {
  552.         const button = getElement(SELECTORS.button);
  553.         const warningText = getElement(SELECTORS.warningText);
  554.  
  555.         if (!button || !warningText) return;
  556.  
  557.         button.textContent = isConnected ? "Generate Tags" : "Connect";
  558.         button.disabled = false;
  559.         button.style.opacity = "1";
  560.         button.style.cursor = "pointer";
  561.        
  562.         if (isConnected) {
  563.             button.style.backgroundColor = "var(--color-rating-safe, #a6ffb0)";
  564.             button.style.color = "#000";
  565.             button.style.borderColor = "var(--color-rating-safe, #a6ffb0)";
  566.         } else {
  567.             button.style.backgroundColor = "var(--color-rating-questionable, #ffff00)";
  568.             button.style.color = "#000";
  569.             button.style.borderColor = "var(--color-rating-questionable, #ffff00)";
  570.         }
  571.        
  572.         warningText.textContent = isConnected ? "⚠️ Manually review tags" : "⚠️ Not connected to AI endpoint";
  573.         warningText.style.color = isConnected ? "yellow" : "red";
  574.         warningText.style.fontWeight = "bold";
  575.         warningText.style.fontSize = "14px";
  576.     };
  577.  
  578.     const startConnectionCheck = () => {
  579.         if (state.connectionCheckInterval) {
  580.             clearInterval(state.connectionCheckInterval);
  581.         }
  582.        
  583.         checkConnection().catch(() => {});
  584.        
  585.         state.connectionCheckInterval = setInterval(() => {
  586.             checkConnection().catch(() => {});
  587.         }, 30000);
  588.        
  589.         return state.connectionCheckInterval;
  590.     };
  591.  
  592.     const fetchImage = async (imageUrl) => {
  593.         return new Promise((resolve, reject) => {
  594.             GM_xmlhttpRequest({
  595.                 method: "GET",
  596.                 url: imageUrl,
  597.                 responseType: "blob",
  598.                 timeout: state.config.requestTimeout,
  599.                 onload: response => resolve(response.response),
  600.                 onerror: err => reject(new Error(`Error fetching image: ${err.error || 'Unknown error'}`)),
  601.                 ontimeout: () => reject(new Error("Image fetch timed out"))
  602.             });
  603.         });
  604.     };
  605.  
  606.     const sendToAI = async (imageDataUrl, retryCount = 0) => {
  607.         const config = state.config || loadConfig();
  608.         DEBUG.log('API Send', 'Preparing to send to AI with confidence:', config.confidence);
  609.        
  610.         try {
  611.             return await new Promise((resolve, reject) => {
  612.                 const payload = {
  613.                     data: [imageDataUrl, config.confidence],
  614.                     fn_index: 0
  615.                 };
  616.                 DEBUG.log('API Send', 'Sending payload:', payload);
  617.  
  618.                 GM_xmlhttpRequest({
  619.                     method: "POST",
  620.                     url: config.localEndpoint,
  621.                     headers: {
  622.                         "Content-Type": "application/json",
  623.                         "Accept": "application/json"
  624.                     },
  625.                     data: JSON.stringify(payload),
  626.                     timeout: config.requestTimeout,
  627.                     onload: response => {
  628.                         try {
  629.                             const result = JSON.parse(response.responseText);
  630.                             resolve(result);
  631.                         } catch (err) {
  632.                             reject(new Error(`Invalid AI response: ${err.message}`));
  633.                         }
  634.                     },
  635.                     onerror: err => reject(new Error(`AI request failed: ${err.error || 'Unknown error'}`)),
  636.                     ontimeout: () => reject(new Error("AI request timed out"))
  637.                 });
  638.             });
  639.         } catch (error) {
  640.             if (retryCount < config.maxRetries) {
  641.                 console.log(`AI request failed, retrying (${retryCount + 1}/${config.maxRetries})...`);
  642.                 await new Promise(resolve => setTimeout(resolve, 1000));
  643.                 return sendToAI(imageDataUrl, retryCount + 1);
  644.             }
  645.             throw error;
  646.         }
  647.     };
  648.  
  649.     const processImage = async (button, textarea, throbber) => {
  650.         if (!button || !textarea) {
  651.             DEBUG.warn('Process', 'Missing button or textarea elements');
  652.             return;
  653.         }
  654.        
  655.         DEBUG.log('Process', 'Starting image processing');
  656.        
  657.         const img = getElement(SELECTORS.uploadPreview) ||
  658.                     document.querySelector('#image') ||
  659.                     document.querySelector('.original-file-unchanged') ||
  660.                     document.querySelector('#image-container img') ||
  661.                     document.querySelector('img[id^="image-"]') ||
  662.                     document.querySelector('.image-container img') ||
  663.                     document.querySelector('#preview img');
  664.  
  665.         if (!img) {
  666.             DEBUG.error('Process', 'Could not find image preview');
  667.             alert("Could not find the image preview. Please try again.");
  668.             return;
  669.         }
  670.  
  671.         button.disabled = true;
  672.         button.style.opacity = "0.5";
  673.         if (throbber) textarea.parentElement.insertBefore(throbber, textarea);
  674.        
  675.         DEBUG.log('Process', 'Found image', { src: img.src, width: img.width, height: img.height });
  676.  
  677.         try {
  678.             DEBUG.log('Process', 'Fetching image blob');
  679.             const imageBlob = await fetchImage(img.src);
  680.             DEBUG.log('Process', 'Image blob fetched', { size: imageBlob.size, type: imageBlob.type });
  681.            
  682.             const reader = new FileReader();
  683.             const imageDataUrl = await new Promise((resolve, reject) => {
  684.                 reader.onloadend = () => resolve(reader.result);
  685.                 reader.onerror = reject;
  686.                 reader.readAsDataURL(imageBlob);
  687.             });
  688.            
  689.             DEBUG.log('Process', 'Image converted to base64', { dataUrlLength: imageDataUrl.length });
  690.            
  691.             DEBUG.log('Process', 'Sending image to AI for processing');
  692.             const aiResponse = await sendToAI(imageDataUrl);
  693.            
  694.             DEBUG.log('Process', 'Received AI response', aiResponse);
  695.            
  696.             if (aiResponse.data && aiResponse.data[0]) {
  697.                 const tagString = typeof aiResponse.data[0] === 'string' ? aiResponse.data[0] : '';
  698.                 const existingTags = state.config.preserveExistingTags ? textarea.value : '';
  699.  
  700.                 DEBUG.info('Process', 'Processing tags', {
  701.                     newTagCount: tagString.split(',').filter(t => t.trim()).length,
  702.                     existingTagCount: existingTags.split(/\s+/).filter(t => t.trim()).length,
  703.                     preserveExisting: state.config.preserveExistingTags
  704.                 });
  705.  
  706.                 textarea.value = formatTags(tagString, existingTags);
  707.                 DEBUG.info('Process', 'Tags applied to textarea', {
  708.                     finalTagCount: textarea.value.split(/\s+/).filter(t => t.trim()).length
  709.                 });
  710.  
  711.                 ['input', 'change'].forEach(eventType => {
  712.                     textarea.dispatchEvent(new Event(eventType, { bubbles: true }));
  713.                 });
  714.  
  715.                 if (state.config.rescaleTagBox) {
  716.                     resizeTagBox(textarea);
  717.                 }
  718.  
  719.                 textarea.focus();
  720.                 textarea.blur();
  721.  
  722.                 state.lastSuccessfulCheck = Date.now();
  723.                 setConnectionState(true);
  724.             } else {
  725.                 DEBUG.error('Process', 'Invalid AI response format', aiResponse);
  726.                 throw new Error("Invalid response format from AI");
  727.             }
  728.         } catch (error) {
  729.             DEBUG.error('Process', 'Error processing image', error);
  730.             alert(error.message);
  731.             checkConnection();
  732.         } finally {
  733.             button.disabled = false;
  734.             button.style.opacity = "1";
  735.             if (throbber) throbber.remove();
  736.             DEBUG.log('Process', 'Image processing completed');
  737.         }
  738.     };
  739.  
  740.     const addAutocompleteToTextarea = (textarea, containerDiv, isCommaSeparated = false) => {
  741.         if (!textarea || !containerDiv) return;
  742.        
  743.         const suggestionsClass = isCommaSeparated ? 'tag-suggestions-container-config' : 'tag-suggestions-container';
  744.         const existingSuggestions = containerDiv.querySelector(`.${suggestionsClass}`);
  745.         if (existingSuggestions) return;
  746.        
  747.         const suggestionsContainer = document.createElement('div');
  748.         suggestionsContainer.className = `${suggestionsClass} tag-suggestions-container`;
  749.         suggestionsContainer.style.zIndex = '1001';
  750.         containerDiv.appendChild(suggestionsContainer);
  751.  
  752.         let currentSuggestions = [];
  753.         let activeIndex = -1;
  754.         let currentTagSegment = { term: '', start: -1, end: -1 };
  755.  
  756.         const fetchSuggestions = createDebounce((term) => {
  757.             term = term.trim();
  758.             if (!term || term.length < 3) {
  759.                 suggestionsContainer.style.display = 'none';
  760.                 currentSuggestions = [];
  761.                 activeIndex = -1;
  762.                 return;
  763.             }
  764.  
  765.             suggestionsContainer.innerHTML = '';
  766.             suggestionsContainer.style.display = 'none';
  767.  
  768.             GM_xmlhttpRequest({
  769.                 method: "GET",
  770.                 url: `https://e621.net/tags/autocomplete.json?search[name_matches]=${encodeURIComponent(term)}&expiry=7`,
  771.                 headers: {
  772.                     "Content-Type": "application/json",
  773.                     "Accept": "application/json"
  774.                 },
  775.                 onload: (response) => {
  776.                     try {
  777.                         const responseData = JSON.parse(response.responseText);
  778.                         const tags = Array.isArray(responseData) ? responseData : [];
  779.                        
  780.                         if (tags.length === 0) {
  781.                             suggestionsContainer.style.display = 'none';
  782.                             currentSuggestions = [];
  783.                             activeIndex = -1;
  784.                             return;
  785.                         }
  786.  
  787.                         const fragment = document.createDocumentFragment();
  788.                         currentSuggestions = tags;
  789.                         activeIndex = -1;
  790.  
  791.                         tags.forEach((tag, index) => {
  792.                             const suggestion = document.createElement('div');
  793.                             suggestion.className = 'tag-suggestion';
  794.                             suggestion.dataset.index = index;
  795.                             suggestion.textContent = tag.name.replace(/_/g, ' ');
  796.                             suggestion.style.color = TAG_CATEGORIES[tag.category] || '#fff';
  797.  
  798.                             const countLabel = document.createElement('span');
  799.                             countLabel.className = 'tag-count';
  800.                             countLabel.textContent = tag.post_count.toLocaleString();
  801.                             suggestion.appendChild(countLabel);
  802.  
  803.                             fragment.appendChild(suggestion);
  804.                         });
  805.  
  806.                         suggestionsContainer.innerHTML = '';
  807.                         suggestionsContainer.appendChild(fragment);
  808.  
  809.                         suggestionsContainer.addEventListener('click', handleSuggestionClick);
  810.                         suggestionsContainer.addEventListener('mouseover', handleSuggestionMouseover);
  811.  
  812.                         const rect = textarea.getBoundingClientRect();
  813.                         const parentRect = containerDiv.getBoundingClientRect();
  814.                         suggestionsContainer.style.position = 'absolute';
  815.                         suggestionsContainer.style.top = `${rect.bottom - parentRect.top}px`;
  816.                         suggestionsContainer.style.left = `${rect.left - parentRect.left}px`;
  817.                         suggestionsContainer.style.width = `${rect.width}px`;
  818.                         suggestionsContainer.style.display = 'block';
  819.                     } catch (error) {
  820.                         console.error("Error parsing tag suggestions:", error);
  821.                         suggestionsContainer.style.display = 'none';
  822.                     }
  823.                 },
  824.                 onerror: () => {
  825.                     suggestionsContainer.style.display = 'none';
  826.                 }
  827.             });
  828.         }, 300);
  829.  
  830.         const handleSuggestionClick = (e) => {
  831.             const suggestion = e.target.closest('.tag-suggestion');
  832.             if (suggestion) {
  833.                 const index = parseInt(suggestion.dataset.index, 10);
  834.                 if (index >= 0 && index < currentSuggestions.length) {
  835.                     addTagToTextarea(textarea, currentSuggestions[index].name, isCommaSeparated);
  836.                     suggestionsContainer.style.display = 'none';
  837.                 }
  838.             }
  839.         };
  840.  
  841.         const handleSuggestionMouseover = (e) => {
  842.              const suggestion = e.target.closest('.tag-suggestion');
  843.              if (suggestion) {
  844.                  const suggestions = suggestionsContainer.querySelectorAll('.tag-suggestion');
  845.                  suggestions.forEach(s => s.classList.remove('active'));
  846.                  suggestion.classList.add('active');
  847.                  activeIndex = parseInt(suggestion.dataset.index, 10);
  848.              }
  849.          };
  850.  
  851.         const addTagToTextarea = (textarea, tagName, isList) => {
  852.             const currentValue = textarea.value;
  853.             const cursorPosition = textarea.selectionStart;
  854.  
  855.             let newValue;
  856.             let newCursorPos;
  857.  
  858.             if (isList) {
  859.                 const segmentStart = currentTagSegment.start;
  860.                 const segmentEnd = currentTagSegment.end;
  861.  
  862.                 if (segmentStart === -1 || segmentEnd === -1) {
  863.                     console.error("Cannot add tag: segment info missing.");
  864.                     return;
  865.                 }
  866.  
  867.                 const textBeforeSegment = currentValue.substring(0, segmentStart);
  868.                 const textAfterSegment = currentValue.substring(segmentEnd);
  869.                
  870.                 let trailingCommaSpace = "";
  871.                 const nextChar = textAfterSegment.trimStart().charAt(0);
  872.                  if (textAfterSegment.trim().length > 0 && nextChar !== ',') {
  873.                     trailingCommaSpace = ", ";
  874.                  } else if (textAfterSegment.trimStart().startsWith(',')) {
  875.                      trailingCommaSpace = " ";
  876.                  }
  877.  
  878.                 newValue = textBeforeSegment + tagName + trailingCommaSpace + textAfterSegment.trimStart();
  879.                 newCursorPos = textBeforeSegment.length + tagName.length + trailingCommaSpace.length;
  880.  
  881.             } else {
  882.                 const textBeforeCursor = currentValue.substring(0, cursorPosition);
  883.                 const textAfterCursor = currentValue.substring(cursorPosition);
  884.                 const lastSpacePos = textBeforeCursor.lastIndexOf(' ');
  885.                 const startPos = lastSpacePos === -1 ? 0 : lastSpacePos + 1;
  886.  
  887.                 const newTextBeforeCursor = currentValue.substring(0, startPos) + tagName;
  888.                 newValue = newTextBeforeCursor + " " + textAfterCursor.trimStart();
  889.                 newCursorPos = newTextBeforeCursor.length + 1;
  890.             }
  891.  
  892.             textarea.value = newValue;
  893.             textarea.setSelectionRange(newCursorPos, newCursorPos);
  894.             textarea.focus();
  895.            
  896.             ['input', 'change'].forEach(eventType => {
  897.                 textarea.dispatchEvent(new Event(eventType, { bubbles: true }));
  898.             });
  899.             suggestionsContainer.style.display = 'none';
  900.             currentSuggestions = [];
  901.             activeIndex = -1;
  902.         };
  903.  
  904.         const navigateSuggestions = (direction) => {
  905.             const suggestions = suggestionsContainer.querySelectorAll('.tag-suggestion');
  906.             if (!suggestions.length) return;
  907.            
  908.             if (activeIndex >= 0 && activeIndex < suggestions.length) {
  909.                 suggestions[activeIndex].classList.remove('active');
  910.             }
  911.  
  912.             activeIndex += direction;
  913.             if (activeIndex < 0) activeIndex = suggestions.length - 1;
  914.             if (activeIndex >= suggestions.length) activeIndex = 0;
  915.  
  916.             suggestions[activeIndex].classList.add('active');
  917.             suggestions[activeIndex].scrollIntoView({ block: 'nearest' });
  918.         };
  919.  
  920.         const handleInput = (e) => {
  921.             const cursorPosition = textarea.selectionStart;
  922.             const currentValue = textarea.value;
  923.             let term = '';
  924.             let termStart = -1;
  925.             let termEnd = -1;
  926.  
  927.             if (isCommaSeparated) {
  928.                 termStart = currentValue.lastIndexOf(',', cursorPosition - 1);
  929.                 termStart = termStart === -1 ? 0 : termStart + 1;
  930.  
  931.                 termEnd = currentValue.indexOf(',', cursorPosition);
  932.                  termEnd = termEnd === -1 ? currentValue.length : termEnd;
  933.  
  934.                 term = currentValue.substring(termStart, termEnd).trim();
  935.  
  936.                  currentTagSegment = { term: term, start: termStart, end: termEnd };
  937.             } else {
  938.                  const textBeforeCursor = currentValue.substring(0, cursorPosition);
  939.                  const lastSpacePos = textBeforeCursor.lastIndexOf(' ');
  940.                  termStart = lastSpacePos === -1 ? 0 : lastSpacePos + 1;
  941.                  term = textBeforeCursor.substring(termStart).trim();
  942.             }
  943.  
  944.             fetchSuggestions(term);
  945.         };
  946.  
  947.         const handleClickOutside = (e) => {
  948.             if (!suggestionsContainer.contains(e.target) && e.target !== textarea) {
  949.                 suggestionsContainer.style.display = 'none';
  950.             }
  951.         };
  952.  
  953.         const handleKeyDown = (e) => {
  954.             if (suggestionsContainer.style.display !== 'block') return;
  955.  
  956.             switch (e.key) {
  957.                 case 'ArrowDown':
  958.                     e.preventDefault();
  959.                     navigateSuggestions(1);
  960.                     break;
  961.                 case 'ArrowUp':
  962.                     e.preventDefault();
  963.                     navigateSuggestions(-1);
  964.                     break;
  965.                 case 'Enter':
  966.                 case 'Tab':
  967.                  {
  968.                     if (activeIndex === -1) return;
  969.                     e.preventDefault();
  970.                     const activeElement = suggestionsContainer.querySelector('.tag-suggestion.active');
  971.                     if (activeElement) {
  972.                         const index = parseInt(activeElement.dataset.index, 10);
  973.                         if (index >= 0 && index < currentSuggestions.length) {
  974.                             addTagToTextarea(textarea, currentSuggestions[index].name, isCommaSeparated);
  975.                         }
  976.                     } else {
  977.                          suggestionsContainer.style.display = 'none';
  978.                     }
  979.                     break;
  980.                 }
  981.                 case 'Escape':
  982.                     e.preventDefault();
  983.                     suggestionsContainer.style.display = 'none';
  984.                     break;
  985.             }
  986.         };
  987.  
  988.         registerEventListener(textarea, 'input', handleInput);
  989.         registerEventListener(document, 'click', handleClickOutside);
  990.         registerEventListener(textarea, 'keydown', handleKeyDown);
  991.  
  992.         return {
  993.             destroy: () => {
  994.                  suggestionsContainer.removeEventListener('click', handleSuggestionClick);
  995.                  suggestionsContainer.removeEventListener('mouseover', handleSuggestionMouseover);
  996.                  suggestionsContainer.remove();
  997.             }
  998.         };
  999.     };
  1000.  
  1001.     const createThrobber = () => {
  1002.         const throbber = document.createElement('div');
  1003.         throbber.className = 'ai-throbber'; // LMAO
  1004.         throbber.style.cssText = 'display: inline-block; margin-left: 10px; width: 16px; height: 16px; border: 3px solid rgba(255,255,255,0.3); border-radius: 50%; border-top-color: white; animation: ai-spin 1s linear infinite;';
  1005.        
  1006.         const styleAnimation = document.createElement('style');
  1007.         styleAnimation.textContent = '@keyframes ai-spin { to { transform: rotate(360deg); } }';
  1008.         document.head.appendChild(styleAnimation);
  1009.        
  1010.         return throbber;
  1011.     };
  1012.  
  1013.     const applyConstantTags = (textarea) => {
  1014.         if (!textarea) {
  1015.             DEBUG.warn('ConstantTags', 'No textarea found');
  1016.             return;
  1017.         }
  1018.        
  1019.         const config = state.config || loadConfig();
  1020.         if (!config.constantTags.trim()) {
  1021.             DEBUG.log('ConstantTags', 'No constant tags configured, skipping');
  1022.             return;
  1023.         }
  1024.  
  1025.         DEBUG.info('ConstantTags', 'Applying constant tags', { constantTags: config.constantTags });
  1026.        
  1027.         const isEditPage = window.location.href.includes('/posts/') && !window.location.href.includes('/uploads/new');
  1028.         if (isEditPage && !config.enableAutoTagOnEdit) {
  1029.             DEBUG.warn('ConstantTags', 'Constant tags on edit is disabled, skipping constant tags application');
  1030.             return;
  1031.         }
  1032.  
  1033.         const formattedConstantTags = config.constantTags
  1034.             .split(',')
  1035.             .map(tag => tag.trim())
  1036.             .map(tag => tag.replace(/\s+/g, '_'))
  1037.             .join(' ');
  1038.        
  1039.         DEBUG.log('ConstantTags', 'Formatted constant tags', { formattedConstantTags });
  1040.  
  1041.         textarea.value = formatTags(formattedConstantTags, config.preserveExistingTags ? textarea.value : '');
  1042.         DEBUG.info('ConstantTags', 'Constant tags applied successfully');
  1043.  
  1044.         ['input', 'change'].forEach(eventType => {
  1045.             textarea.dispatchEvent(new Event(eventType, { bubbles: true }));
  1046.         });
  1047.     };
  1048.  
  1049.     const getTagTextarea = () => {
  1050.         return getElement(SELECTORS.tagTextarea) || getElement(SELECTORS.editTagTextarea);
  1051.     };  
  1052.  
  1053.     const updateConfidence = value => {
  1054.         value = Math.max(0.1, Math.min(1, parseFloat(value) || 0.25));
  1055.         DEBUG.log('Confidence Update', 'Attempting to save confidence:', value);
  1056.        
  1057.         saveConfig({ confidence: value });
  1058.         DEBUG.log('Confidence Update', 'Current state.config.confidence after save:', state.config.confidence);
  1059.  
  1060.         const input = getElement(SELECTORS.confidenceInput);
  1061.         if (input) input.value = value.toFixed(2);
  1062.  
  1063.         const configSlider = document.getElementById('confidence-slider');
  1064.         if (configSlider) configSlider.value = value;
  1065.  
  1066.         const configLabel = document.getElementById('confidence-label');
  1067.         if (configLabel) configLabel.textContent = `Confidence Threshold: ${value}`;
  1068.     };
  1069.  
  1070.     const addControls = () => {
  1071.         const textarea = getTagTextarea();
  1072.         if (!textarea) return null;
  1073.  
  1074.         if (getElement(SELECTORS.button)) return textarea;
  1075.  
  1076.         textarea.style.backgroundColor = '#ffffff';
  1077.         textarea.style.color = '#000000';
  1078.  
  1079.         const textareaContainer = textarea.parentElement;
  1080.         if (!textareaContainer) return null;
  1081.  
  1082.         const isUploadPage = window.location.href.includes('/uploads/new');
  1083.         const buttonFont = isUploadPage ? 'Arial, sans-serif' : 'Verdana, Geneva, sans-serif';
  1084.  
  1085.         const controlsContainer = document.createElement('div');
  1086.         controlsContainer.className = 'ai-controls-container';
  1087.         controlsContainer.style.cssText = 'display: flex; flex-direction: column; margin-bottom: 10px;';
  1088.         textareaContainer.insertBefore(controlsContainer, textarea);
  1089.  
  1090.         const warningText = document.createElement('span');
  1091.         warningText.className = 'ai-warning-text';
  1092.         warningText.textContent = '⚠️ Not connected to AI endpoint';
  1093.         warningText.style.cssText = `font-size: 14px; color: red; font-family: ${buttonFont}; font-weight: bold; margin-bottom: 5px;`;
  1094.         controlsContainer.appendChild(warningText);
  1095.        
  1096.         const controlsRow = document.createElement('div');
  1097.         controlsRow.style.cssText = 'display: flex; align-items: center; flex-wrap: wrap;';
  1098.         controlsContainer.appendChild(controlsRow);
  1099.  
  1100.         const button = document.createElement('button');
  1101.         button.className = 'ai-tag-button';
  1102.         button.textContent = 'Connect';
  1103.         button.style.height = '28px';
  1104.         button.style.borderRadius = '6px';
  1105.         button.style.fontSize = '16px';
  1106.         button.style.marginRight = '10px';
  1107.         button.style.fontFamily = buttonFont;
  1108.         controlsRow.appendChild(button);
  1109.  
  1110.         const sortButton = document.createElement('button');
  1111.         sortButton.className = 'ai-sort-button';
  1112.         sortButton.textContent = 'Sort';
  1113.         sortButton.type = 'button';
  1114.         sortButton.style.cssText = `font-size: 16px; padding: 3px 10px; background-color: #ffffff; color: #000000; border: 1px solid #666; border-radius: 6px; height: 28px; font-family: ${buttonFont}; margin-right: 10px;`;
  1115.         controlsRow.appendChild(sortButton);
  1116.  
  1117.         const confidenceContainer = document.createElement('div');
  1118.         confidenceContainer.style.cssText = 'display: flex; align-items: center; margin-right: 10px;';
  1119.         controlsRow.appendChild(confidenceContainer);
  1120.  
  1121.         const confidenceLabel = document.createElement('label');
  1122.         confidenceLabel.textContent = 'Confidence:';
  1123.         confidenceLabel.style.cssText = `font-size: 14px; margin-right: 5px; font-family: ${buttonFont};`;
  1124.         confidenceContainer.appendChild(confidenceLabel);
  1125.  
  1126.         const confidenceInput = document.createElement('input');
  1127.         confidenceInput.type = 'number';
  1128.         confidenceInput.className = 'ai-confidence-input';
  1129.         confidenceInput.min = '0.1';
  1130.         confidenceInput.max = '1';
  1131.         confidenceInput.step = '0.05';
  1132.         confidenceInput.value = (state.config || loadConfig()).confidence;
  1133.         confidenceInput.style.cssText = `width: 60px; font-size: 14px; padding: 2px 5px; background-color: #ffffff !important; color: #000000 !important; border: 1px solid #666; border-radius: 6px; height: 28px; font-family: ${buttonFont};`;
  1134.         confidenceContainer.appendChild(confidenceInput);
  1135.  
  1136.         registerEventListener(button, 'click', async () => {
  1137.             if (button.textContent === 'Connect') {
  1138.                 try {
  1139.                     button.disabled = true;
  1140.                     button.style.opacity = '0.5';
  1141.                     button.textContent = 'Connecting...';
  1142.                     await checkConnection(true);
  1143.                 } catch (error) {
  1144.                     alert(`Failed to connect: ${error.message}`);
  1145.                     setConnectionState(false);
  1146.                 } finally {
  1147.                     button.disabled = false;
  1148.                     button.style.opacity = '1';
  1149.                 }
  1150.             } else {
  1151.                 const throbber = createThrobber();
  1152.                 processImage(button, textarea, throbber);
  1153.             }
  1154.         });
  1155.  
  1156.         registerEventListener(confidenceInput, 'change', () => {
  1157.             updateConfidence(confidenceInput.value);
  1158.         });
  1159.  
  1160.         registerEventListener(sortButton, 'click', () => {
  1161.             if (!textarea.value.trim()) return;
  1162.            
  1163.             const config = state.config || loadConfig();
  1164.             textarea.value = formatTags(textarea.value.replace(/\s+/g, ','));
  1165.            
  1166.             ['input', 'change'].forEach(eventType => {
  1167.                 textarea.dispatchEvent(new Event(eventType, { bubbles: true }));
  1168.             });
  1169.            
  1170.             if (config.rescaleTagBox) {
  1171.                 resizeTagBox(textarea);
  1172.             }
  1173.         });
  1174.  
  1175.         addAutocompleteToTextarea(textarea, textareaContainer);
  1176.  
  1177.         setTimeout(() => applyConstantTags(textarea), 500);
  1178.  
  1179.         return textarea;
  1180.     };
  1181.  
  1182.     const showConfigUI = () => {
  1183.         DEBUG.log('Config', 'Opening configuration dialog');
  1184.         const existingDialog = document.getElementById('e6-autotagger-config');
  1185.         if (existingDialog) {
  1186.             DEBUG.log('Config', 'Removing existing dialog');
  1187.             existingDialog.remove();
  1188.         }
  1189.  
  1190.         const config = state.config || loadConfig();
  1191.         DEBUG.log('Config', 'Current configuration', config);
  1192.  
  1193.         const dialog = document.createElement('dialog');
  1194.         dialog.id = 'e6-autotagger-config';
  1195.         document.body.appendChild(dialog);
  1196.  
  1197.         const form = document.createElement('form');
  1198.         form.method = 'dialog';
  1199.         form.style.fontFamily = 'Verdana, Geneva, sans-serif';
  1200.         dialog.appendChild(form);
  1201.  
  1202.         const title = document.createElement('h2');
  1203.         title.textContent = 'E6 Autotagger Configuration';
  1204.         title.style.marginTop = '0';
  1205.         title.style.fontFamily = 'Verdana, Geneva, sans-serif';
  1206.         form.appendChild(title);
  1207.  
  1208.         addConfigInput(form, 'localEndpoint', 'AI Endpoint URL', config.localEndpoint, 'text',
  1209.             'Enter the URL of your local AI endpoint (without /api/predict)');
  1210.  
  1211.         const confidenceContainer = document.createElement('div');
  1212.         confidenceContainer.className = 'config-row';
  1213.         form.appendChild(confidenceContainer);
  1214.  
  1215.         const confidenceLabel = document.createElement('label');
  1216.         confidenceLabel.id = 'confidence-label';
  1217.         confidenceLabel.textContent = `Confidence Threshold: ${config.confidence}`;
  1218.         confidenceLabel.style.fontFamily = 'Verdana, Geneva, sans-serif';
  1219.         confidenceContainer.appendChild(confidenceLabel);
  1220.  
  1221.         const confidenceSlider = document.createElement('input');
  1222.         confidenceSlider.type = 'range';
  1223.         confidenceSlider.id = 'confidence-slider';
  1224.         confidenceSlider.min = '0.1';
  1225.         confidenceSlider.max = '1';
  1226.         confidenceSlider.step = '0.05';
  1227.         confidenceSlider.value = config.confidence;
  1228.         confidenceSlider.className = 'config-input';
  1229.         confidenceSlider.addEventListener('input', () => {
  1230.             confidenceLabel.textContent = `Confidence Threshold: ${confidenceSlider.value}`;
  1231.         });
  1232.         confidenceContainer.appendChild(confidenceSlider);
  1233.  
  1234.         addConfigInput(form, 'requestTimeout', 'Request Timeout (ms)', config.requestTimeout, 'number',
  1235.             'Maximum time to wait for API response in milliseconds');
  1236.  
  1237.         addConfigInput(form, 'maxRetries', 'Max Retries', config.maxRetries, 'number',
  1238.             'Number of times to retry failed requests');
  1239.  
  1240.         const blacklistContainer = document.createElement('div');
  1241.         blacklistContainer.className = 'config-row';
  1242.         form.appendChild(blacklistContainer);
  1243.         const blacklistTextarea = addConfigInput(blacklistContainer, 'tagBlacklist', 'Tag Blacklist', config.tagBlacklist, 'textarea',
  1244.             'Comma-separated list of tags to exclude');
  1245.         addAutocompleteToTextarea(blacklistTextarea, blacklistContainer, true);
  1246.  
  1247.         const autoTagsContainer = document.createElement('div');
  1248.         autoTagsContainer.className = 'config-row';
  1249.         form.appendChild(autoTagsContainer);
  1250.         const autoTagsTextarea = addConfigInput(autoTagsContainer, 'constantTags', 'Constant Tags', config.constantTags, 'textarea',
  1251.             'Comma-separated list of tags to add automatically');
  1252.         addAutocompleteToTextarea(autoTagsTextarea, autoTagsContainer, true);
  1253.  
  1254.         addConfigCheckbox(form, 'preserveExistingTags', 'Preserve Existing Tags', config.preserveExistingTags,
  1255.             'Keep existing tags when generating new ones');
  1256.  
  1257.         addConfigCheckbox(form, 'rescaleTagBox', 'Rescale Tag Box on Sort/Generate', config.rescaleTagBox,
  1258.             'Automatically resize the tag box to show all tags');
  1259.  
  1260.         addConfigCheckbox(form, 'enableAutoTagOnEdit', 'Enable Constant Tags on Edit', config.enableAutoTagOnEdit,
  1261.             'Apply constant tags when editing existing posts');
  1262.  
  1263.         const sortingContainer = document.createElement('div');
  1264.         sortingContainer.className = 'config-row';
  1265.         form.appendChild(sortingContainer);
  1266.  
  1267.         const sortingLabel = document.createElement('label');
  1268.         sortingLabel.textContent = 'Tag Sorting Mode';
  1269.         sortingLabel.style.fontFamily = 'Verdana, Geneva, sans-serif';
  1270.         sortingContainer.appendChild(sortingLabel);
  1271.  
  1272.         const sortingSelect = document.createElement('select');
  1273.         sortingSelect.className = 'config-input';
  1274.         sortingSelect.name = 'sortingMode';
  1275.         sortingSelect.style.height = 'auto';
  1276.         sortingSelect.style.padding = '5px';
  1277.        
  1278.         const sortingModes = [
  1279.             { value: 'flat', label: 'Flat (space-separated)' },
  1280.             { value: 'grouped', label: 'Grouped (by first letter)' },
  1281.             { value: 'oneperline', label: 'One tag per line' }
  1282.         ];
  1283.        
  1284.         sortingModes.forEach(mode => {
  1285.             const option = document.createElement('option');
  1286.             option.value = mode.value;
  1287.             option.textContent = mode.label;
  1288.             option.selected = config.sortingMode === mode.value;
  1289.             sortingSelect.appendChild(option);
  1290.         });
  1291.        
  1292.         sortingContainer.appendChild(sortingSelect);
  1293.  
  1294.         const buttonsContainer = document.createElement('div');
  1295.         buttonsContainer.style.cssText = 'display: flex; justify-content: space-between; margin-top: 20px;';
  1296.         form.appendChild(buttonsContainer);
  1297.  
  1298.         const testButton = document.createElement('button');
  1299.         testButton.textContent = 'Test Connection';
  1300.         testButton.type = 'button';
  1301.         testButton.style.cssText = 'padding: 8px 16px; background-color: #ffffff; color: #000000; border: 1px solid #666; border-radius: 6px; cursor: pointer; height: 28px; font-family: Verdana, Geneva, sans-serif; font-size: 12px;';
  1302.         buttonsContainer.appendChild(testButton);
  1303.  
  1304.         const buttonGroup = document.createElement('div');
  1305.         buttonsContainer.appendChild(buttonGroup);
  1306.  
  1307.         const cancelButton = document.createElement('button');
  1308.         cancelButton.textContent = 'Cancel';
  1309.         cancelButton.type = 'button';
  1310.         cancelButton.style.cssText = 'padding: 8px 16px; background-color: #ffffff; color: #000000; border: 1px solid #666; border-radius: 6px; cursor: pointer; margin-right: 10px; height: 28px; font-family: Verdana, Geneva, sans-serif; font-size: 12px;';
  1311.         buttonGroup.appendChild(cancelButton);
  1312.  
  1313.         const saveButton = document.createElement('button');
  1314.         saveButton.textContent = 'Save';
  1315.         saveButton.type = 'button';
  1316.         saveButton.style.cssText = 'padding: 8px 16px; background-color: #ffffff; color: #000000; border: 1px solid #666; border-radius: 6px; cursor: pointer; height: 28px; font-family: Verdana, Geneva, sans-serif; font-size: 12px;';
  1317.         buttonGroup.appendChild(saveButton);
  1318.  
  1319.         const statusArea = document.createElement('div');
  1320.         statusArea.className = 'config-status';
  1321.         statusArea.style.display = 'none';
  1322.         form.appendChild(statusArea);
  1323.  
  1324.         function addConfigInput(parent, name, label, value, type, placeholder) {
  1325.             const labelElem = document.createElement('label');
  1326.             labelElem.textContent = label;
  1327.             labelElem.style.fontFamily = 'Verdana, Geneva, sans-serif';
  1328.             parent.appendChild(labelElem);
  1329.  
  1330.             const input = document.createElement(type === 'textarea' ? 'textarea' : 'input');
  1331.             if (type !== 'textarea') input.type = type;
  1332.             input.className = 'config-input';
  1333.             input.name = name;
  1334.             input.value = value;
  1335.             input.placeholder = placeholder;
  1336.             if (type === 'textarea') input.rows = 3;
  1337.             input.style.backgroundColor = '#ffffff';
  1338.             input.style.color = '#000000';
  1339.             input.style.fontFamily = 'Verdana, Geneva, sans-serif';
  1340.             input.style.height = type === 'textarea' ? 'auto' : '28px';
  1341.             parent.appendChild(input);
  1342.  
  1343.             return input;
  1344.         }
  1345.  
  1346.         function addConfigCheckbox(parent, name, label, checked, description) {
  1347.             const container = document.createElement('div');
  1348.             container.className = 'config-row';
  1349.             container.style.display = 'flex';
  1350.             container.style.alignItems = 'center';
  1351.             parent.appendChild(container);
  1352.  
  1353.             const checkbox = document.createElement('input');
  1354.             checkbox.type = 'checkbox';
  1355.             checkbox.name = name;
  1356.             checkbox.checked = checked;
  1357.             checkbox.style.marginRight = '10px';
  1358.             container.appendChild(checkbox);
  1359.  
  1360.             const labelElem = document.createElement('label');
  1361.             labelElem.textContent = label;
  1362.             labelElem.style.fontFamily = 'Verdana, Geneva, sans-serif';
  1363.             container.appendChild(labelElem);
  1364.  
  1365.             if (description) {
  1366.                 const descElem = document.createElement('span');
  1367.                 descElem.textContent = ` - ${description}`;
  1368.                 descElem.style.fontSize = '12px';
  1369.                 descElem.style.opacity = '0.8';
  1370.                 descElem.style.fontFamily = 'Verdana, Geneva, sans-serif';
  1371.                 container.appendChild(descElem);
  1372.             }
  1373.  
  1374.             return checkbox;
  1375.         }
  1376.  
  1377.         const showStatus = (message, isError = false) => {
  1378.             statusArea.textContent = message;
  1379.             statusArea.style.backgroundColor = isError ? '#ff3333' : '#4caf50';
  1380.             statusArea.style.color = 'white';
  1381.             statusArea.style.display = 'block';
  1382.            
  1383.             setTimeout(() => {
  1384.                 statusArea.style.display = 'none';
  1385.             }, 3000);
  1386.         };
  1387.  
  1388.         registerEventListener(testButton, 'click', async () => {
  1389.             testButton.disabled = true;
  1390.            
  1391.             try {
  1392.                 const endpointInput = form.querySelector('input[name="localEndpoint"]');
  1393.                 const endpoint = endpointInput.value.trim();
  1394.                
  1395.                 if (!endpoint) {
  1396.                     throw new Error('Endpoint URL is required');
  1397.                 }
  1398.                
  1399.                 const originalEndpoint = config.localEndpoint;
  1400.                 const formattedEndpoint = endpoint.endsWith('/api/predict') ?
  1401.                     endpoint : endpoint.replace(/\/$/, '') + '/api/predict';
  1402.                
  1403.                 state.config = { ...config, localEndpoint: formattedEndpoint };
  1404.                
  1405.                 await checkConnection(false);
  1406.                 showStatus('Connection successful!');
  1407.                
  1408.                 state.config = { ...config, localEndpoint: originalEndpoint };
  1409.             } catch (error) {
  1410.                 showStatus(`Connection failed: ${error.message}`, true);
  1411.             } finally {
  1412.                 testButton.disabled = false;
  1413.             }
  1414.         });
  1415.  
  1416.         registerEventListener(saveButton, 'click', () => {
  1417.             try {
  1418.                 DEBUG.log('Config', 'Saving configuration');
  1419.                 const newConfig = {
  1420.                     localEndpoint: form.querySelector('[name="localEndpoint"]').value.trim(),
  1421.                     confidence: parseFloat(form.querySelector('#confidence-slider').value),
  1422.                     tagBlacklist: form.querySelector('[name="tagBlacklist"]').value.trim(),
  1423.                     constantTags: form.querySelector('[name="constantTags"]').value.trim(),
  1424.                     preserveExistingTags: form.querySelector('[name="preserveExistingTags"]').checked,
  1425.                     rescaleTagBox: form.querySelector('[name="rescaleTagBox"]').checked,
  1426.                     enableAutoTagOnEdit: form.querySelector('[name="enableAutoTagOnEdit"]').checked,
  1427.                     sortingMode: form.querySelector('[name="sortingMode"]').value,
  1428.                     requestTimeout: parseInt(form.querySelector('[name="requestTimeout"]').value) || DEFAULT_CONFIG.requestTimeout,
  1429.                     maxRetries: parseInt(form.querySelector('[name="maxRetries"]').value) || DEFAULT_CONFIG.maxRetries
  1430.                 };
  1431.                
  1432.                 saveConfig(newConfig);
  1433.                 DEBUG.info('Config', 'Configuration saved successfully');
  1434.                
  1435.                 const confidenceInput = getElement(SELECTORS.confidenceInput);
  1436.                 if (confidenceInput) confidenceInput.value = newConfig.confidence;
  1437.                
  1438.                 setTimeout(() => {
  1439.                     try {
  1440.                         checkConnection().catch(err => {
  1441.                             DEBUG.error('Config', 'Connection check failed after saving settings', err);
  1442.                         });
  1443.                     } catch (err) {
  1444.                         DEBUG.error('Config', 'Error during connection check', err);
  1445.                     }
  1446.                 }, 0);
  1447.                
  1448.                 showStatus('Settings saved successfully!');
  1449.             } catch (error) {
  1450.                 DEBUG.error('Config', 'Error saving settings', error);
  1451.                 console.error('Error saving settings:', error);
  1452.                 showStatus(`Error saving settings: ${error.message}`, true);
  1453.             }
  1454.         });
  1455.  
  1456.         registerEventListener(cancelButton, 'click', () => {
  1457.             dialog.close();
  1458.         });
  1459.  
  1460.         registerEventListener(dialog, 'click', (e) => {
  1461.             if (e.target === dialog) {
  1462.                 dialog.close();
  1463.             }
  1464.         });
  1465.  
  1466.         dialog.showModal();
  1467.     };
  1468.  
  1469.     const checkForEditPage = () => {
  1470.         const currentUrl = window.location.href;
  1471.  
  1472.         if (currentUrl.includes('/posts/') && !currentUrl.includes('/uploads/new')) {
  1473.             const tagTextarea = getTagTextarea();
  1474.             if (tagTextarea) {
  1475.                 if (!state.initializedPages.has(currentUrl)) {
  1476.                     console.log("Detected edit page with tag textarea, initializing...");
  1477.                     state.initializedPages.add(currentUrl);
  1478.                     init();
  1479.                 }
  1480.                 return;
  1481.             }
  1482.  
  1483.             const sideEditButton = document.getElementById('side-edit-link');
  1484.             const postEditButton = document.getElementById('post-edit-link');
  1485.  
  1486.             const editButtons = [sideEditButton, postEditButton].filter(button => button !== null);
  1487.  
  1488.             if (editButtons.length > 0 && !state.isWatchingEditButton) {
  1489.                 console.log("Found edit buttons, watching for clicks...");
  1490.                 state.isWatchingEditButton = true;
  1491.  
  1492.                 const handleEditAction = () => {
  1493.                     console.log("Edit action triggered, waiting for tag textarea...");
  1494.  
  1495.                     setTimeout(() => {
  1496.                         const textarea = getTagTextarea();
  1497.                         if (textarea) {
  1498.                             console.log("Tag textarea found immediately after edit action");
  1499.                             if (!state.initializedPages.has(currentUrl)) {
  1500.                                 state.initializedPages.add(currentUrl);
  1501.                                 init();
  1502.                             }
  1503.                             return;
  1504.                         }
  1505.  
  1506.                         const observer = registerObserver(new MutationObserver((mutations, obs) => {
  1507.                             const tagTextarea = getTagTextarea();
  1508.                             if (tagTextarea) {
  1509.                                 console.log("Tag textarea appeared after edit action");
  1510.                                 obs.disconnect();
  1511.  
  1512.                                 setTimeout(() => {
  1513.                                     if (!state.initializedPages.has(currentUrl)) {
  1514.                                         state.initializedPages.add(currentUrl);
  1515.                                         init();
  1516.                                     }
  1517.                                 }, 300);
  1518.                             }
  1519.                         }));
  1520.  
  1521.                         observer.observe(document.body, {
  1522.                             childList: true,
  1523.                             subtree: true,
  1524.                             attributes: true,
  1525.                             characterData: false
  1526.                         });
  1527.  
  1528.                         setTimeout(() => {
  1529.                             observer.disconnect();
  1530.                         }, 10000);
  1531.                     }, 300);
  1532.                 };
  1533.  
  1534.                 editButtons.forEach(button => {
  1535.                     registerEventListener(button, 'click', handleEditAction);
  1536.                 });
  1537.  
  1538.                 registerEventListener(document, 'keydown', (e) => {
  1539.                     if (e.key === 'e' &&
  1540.                         !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) &&
  1541.                         !e.ctrlKey && !e.altKey && !e.metaKey) {
  1542.                         handleEditAction();
  1543.                     }
  1544.                 });
  1545.             }
  1546.         }
  1547.     };
  1548.  
  1549.     const handleUrlChange = () => {
  1550.         let lastUrl = location.href;
  1551.  
  1552.         console.log("Setting up URL change monitor");
  1553.  
  1554.         const observer = registerObserver(new MutationObserver(() => {
  1555.             if (location.href !== lastUrl) {
  1556.                 console.log("URL changed from", lastUrl, "to", location.href);
  1557.                 lastUrl = location.href;
  1558.  
  1559.                 clearResources();
  1560.                 state.initializedPages.clear();
  1561.                 state.isWatchingEditButton = false;
  1562.  
  1563.                 setTimeout(() => {
  1564.                     checkForEditPage();
  1565.                 }, 500);
  1566.             }
  1567.         }));
  1568.  
  1569.         observer.observe(document, {subtree: true, childList: true});
  1570.     };
  1571.  
  1572.     const init = () => {
  1573.         DEBUG.info('Init', 'Initializing E6 Autotagger');
  1574.         loadConfig();
  1575.         addStyles();
  1576.        
  1577.         const textarea = addControls();
  1578.         if (textarea) {
  1579.             DEBUG.log('Init', 'Found tag textarea, adding controls');
  1580.             startConnectionCheck();
  1581.             state.initializedPages.add(window.location.href);
  1582.             DEBUG.info('Init', 'E6 Autotagger initialized successfully', { url: window.location.href });
  1583.         } else {
  1584.             DEBUG.log("Init", "No tag textarea found, may be on a view-only page");
  1585.             checkForEditPage();
  1586.         }
  1587.     };
  1588.  
  1589.     GM_registerMenuCommand('Configure E6 Autotagger', showConfigUI);
  1590.  
  1591.     if (document.readyState === "complete" || document.readyState === "interactive") {
  1592.         setTimeout(init, 1);
  1593.     } else {
  1594.         registerEventListener(document, "DOMContentLoaded", init);
  1595.         registerEventListener(window, "load", init);
  1596.     }
  1597.  
  1598.     registerEventListener(document, "DOMContentLoaded", () => {
  1599.         setTimeout(checkForEditPage, 500);
  1600.     });
  1601.  
  1602.     registerEventListener(window, "load", () => {
  1603.         setTimeout(checkForEditPage, 500);
  1604.         handleUrlChange();
  1605.     });
  1606.  
  1607.     const resizeTagBox = (textarea) => {
  1608.         if (!textarea) return;
  1609.  
  1610.         for (let i = 0; i < 2; i++) {
  1611.         textarea.style.height = 'auto'; // Reset height
  1612.         textarea.style.height = textarea.scrollHeight + 'px'; // Set height to fit content
  1613.  
  1614.         // Adjust width
  1615.         const tempSpan = document.createElement('span');
  1616.         tempSpan.style.visibility = 'hidden';
  1617.         tempSpan.style.whiteSpace = 'pre';
  1618.         tempSpan.style.position = 'absolute';
  1619.         tempSpan.style.font = window.getComputedStyle(textarea).font;
  1620.         tempSpan.textContent = textarea.value;
  1621.  
  1622.         document.body.appendChild(tempSpan);
  1623.         const contentWidth = tempSpan.offsetWidth;
  1624.         document.body.removeChild(tempSpan);
  1625.  
  1626.         textarea.style.width = (contentWidth + 150) + 'px';
  1627.     }
  1628. };
  1629. })();
Add Comment
Please, Sign In to add comment