Grok Code Filter Menu 1.21.18

Добавляет меню фильтров к блокам кода в чате Grok с сохранением настроек

// ==UserScript==
// @name         Grok Code Filter Menu 1.21.18
// @namespace    http://tampermonkey.net/
// @version      1.21.18
// @description  Добавляет меню фильтров к блокам кода в чате Grok с сохранением настроек
// @author       tapeavion
// @license      MIT
// @match        https://grok.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @downloadURL
// @updateURL
// ==/UserScript==

(function() {
    'use strict';

    // Стили
    const style = document.createElement('style');
    style.textContent = `
        .filter-menu-btn {
            position: absolute;
            top: 4px;
            right: 460px;
            height: 31px !important;
            z-index: 1;
            padding: 4px 8px;
            background: #1d5752;
            color: #b9bcc1;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 12px;
            transition: background 0.2s ease, color 0.2s ease;
        }
        .filter-menu-btn:hover {
            background: #4a8983;
            color: #ffffff;
        }
        .filter-menu {
            position: absolute;
            top: 40px;
            right: 10px;
            background: #2d2d2d;
            border: 1px solid #444;
            border-radius: 8px;
            padding: 5px;
            z-index: 9999;
            display: none;
            box-shadow: 0 2px 4px rgba(0,0,0,0.3);
            width: 200px;
            max-height: 550px;
            overflow-y: auto;
        }
        .filter-item {
            display: flex;
            align-items: center;
            padding: 5px 0;
            color: #a0a0a0;
            font-size: 12px;
        }
        .filter-item input[type="checkbox"] {
            margin-right: 5px;
        }
        .filter-item label {
            flex: 1;
            cursor: pointer;
        }
        .filter-slider {
            display: none;
            margin: 5px 0 5px 20px;
            width: calc(100% - 20px);
        }
        .filter-slider-label {
            display: none;
            color: #a0a0a0;
            font-size: 12px;
            margin: 2px 0 2px 20px;
        }
        .language-select {
            width: 100%;
            padding: 5px;
            margin-bottom: 5px;
            background: #3a3a3a;
            color: #a0a0a0;
            border: none;
            border-radius: 4px;
            font-size: 12px;
        }
        .color-picker {
            margin: 5px 0 5px 20px;
            width: calc(100% - 20px);
        }
        .color-picker-label {
            display: block;
            color: #a0a0a0;
            font-size: 12px;
            margin: 2px 0 2px 20px;
        }
        button.inline-flex {
            background-color: #1d5752 !important;
            opacity: 0;
            animation: fadeIn 1s ease-in-out forwards;
        }
        button.inline-flex:hover {
            background-color: #1d5752 !important;
            opacity: 1;
        }
        @keyframes fadeIn {
            0% { opacity: 0; }
            100% { opacity: 1; }
        }
    `;
    document.head.appendChild(style);

    // Определение языка пользователя
    const userLang = navigator.language || navigator.languages[0];
    const isRussian = userLang.startsWith('ru');
    const defaultLang = isRussian ? 'ru' : 'en';
    const savedLang = localStorage.getItem('filterMenuLang') || defaultLang;

    // Локализация
    const translations = {
        ru: {
            filtersBtn: 'Фильтры',
            sliderLabel: 'Степень:',
            commentColorLabel: 'Цвет комментариев:',
            filters: [
                { name: 'Негатив', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
                { name: 'Сепия', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
                { name: 'Ч/Б', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
                { name: 'Размытие', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' },
                { name: 'Контраст', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 },
                { name: 'Яркость', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 },
                { name: 'Поворот оттенка', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' },
                { name: 'Насыщенность', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 },
                { name: 'Прозрачность', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }
            ],
            langSelect: 'Выберите язык:',
            langOptions: [
                { value: 'ru', label: 'Русский' },
                { value: 'en', label: 'English' }
            ]
        },
        en: {
            filtersBtn: 'Filters',
            sliderLabel: 'Level:',
            commentColorLabel: 'Comment color:',
            filters: [
                { name: 'Invert', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
                { name: 'Sepia', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
                { name: 'Grayscale', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
                { name: 'Blur', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' },
                { name: 'Contrast', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 },
                { name: 'Brightness', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 },
                { name: 'Hue Rotate', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' },
                { name: 'Saturate', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 },
                { name: 'Opacity', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }
            ],
            langSelect: 'Select language:',
            langOptions: [
                { value: 'ru', label: 'Русский' },
                { value: 'en', label: 'English' }
            ]
        }
    };

    // Глобальная переменная для текущего цвета комментариев
    let currentCommentColor = localStorage.getItem('commentColor') || '#5c6370';
    // Массив для хранения всех пар headerBlock и codeContainer
    const codeBlockRegistry = [];

    // Функция создания меню фильтров
    function addFilterMenu(headerBlock, codeContainer) {
        if (headerBlock.querySelector('.filter-menu-btn')) {
            console.log('Фильтр уже существует для заголовка:', headerBlock);
            return;
        }

        // Сохраняем пару headerBlock и codeContainer
        codeBlockRegistry.push({ headerBlock, codeContainer });
        console.log('Добавлен кодовый блок в реестр:', codeContainer.outerHTML);

        let currentLang = savedLang;
        const filterBtn = document.createElement('button');
        filterBtn.className = 'filter-menu-btn';
        filterBtn.textContent = translations[currentLang].filtersBtn;

        const filterMenu = document.createElement('div');
        filterMenu.className = 'filter-menu';

        // Целевой блок — контейнер кода
        const targetBlock = codeContainer;

        // Загружаем сохраненные настройки
        const savedFilterStates = JSON.parse(localStorage.getItem('codeFilterStates') || '{}');
        const savedFilterValues = JSON.parse(localStorage.getItem('codeFilterValues') || '{}');

        // Инициализируем значения по умолчанию
        const filters = translations[currentLang].filters;
        filters.forEach(filter => {
            if (!(filter.value in savedFilterStates)) {
                savedFilterStates[filter.value] = false;
            }
            if (!(filter.value in savedFilterValues)) {
                savedFilterValues[filter.value] = filter.default;
            }
        });

        // Применяем сохраненные фильтры
        function applyFilters() {
            const activeFilters = filters
                .filter(filter => savedFilterStates[filter.value])
                .map(filter => {
                    const unit = filter.unit || '';
                    const value = savedFilterValues[filter.value];
                    return `${filter.value}(${value}${unit})`;
                });
            targetBlock.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
            console.log('Применены фильтры к контейнеру:', targetBlock, activeFilters);
        }

        // Применяем цвет комментариев
       // Альтернативный подход: применение цвета через глобальный стиль
function applyGlobalCommentColor() {
    const existingStyle = document.getElementById('custom-comment-style');
    if (existingStyle) existingStyle.remove();

    const style = document.createElement('style');
    style.id = 'custom-comment-style';
    style.textContent = `
        .hljs-comment, span[style*="color: rgb(92, 99, 112)"] {
            color: ${currentCommentColor} !important;
        }
    `;
    document.head.appendChild(style);
    console.log('Применен глобальный стиль для комментариев:', currentCommentColor);
}
        applyFilters();
        applyGlobalCommentColor();

        // Создаем выпадающий список для выбора языка
        const langSelect = document.createElement('select');
        langSelect.className = 'language-select';
        const langLabel = document.createElement('label');
        langLabel.textContent = translations[currentLang].langSelect;
        langLabel.style.color = '#a0a0a0';
        langLabel.style.fontSize = '12px';
        langLabel.style.marginBottom = '2px';
        langLabel.style.display = 'block';

        translations[currentLang].langOptions.forEach(option => {
            const opt = document.createElement('option');
            opt.value = option.value;
            opt.textContent = option.label;
            if (option.value === currentLang) {
                opt.selected = true;
            }
            langSelect.appendChild(opt);
        });

        // Создаем элемент для выбора цвета комментариев
        const colorPickerLabel = document.createElement('label');
        colorPickerLabel.className = 'color-picker-label';
        colorPickerLabel.textContent = translations[currentLang].commentColorLabel;

        const colorPicker = document.createElement('input');
        colorPicker.type = 'color';
        colorPicker.className = 'color-picker';
        colorPicker.value = currentCommentColor;

        colorPicker.addEventListener('input', () => {
            currentCommentColor = colorPicker.value;
            localStorage.setItem('commentColor', currentCommentColor);
            console.log('Изменен цвет комментариев:', currentCommentColor);
            refreshAllCodeBlocks();
        });

        // Функция обновления интерфейса при смене языка
        function updateLanguage(lang) {
            currentLang = lang;
            localStorage.setItem('filterMenuLang', currentLang);
            filterBtn.textContent = translations[currentLang].filtersBtn;
            langLabel.textContent = translations[currentLang].langSelect;
            colorPickerLabel.textContent = translations[currentLang].commentColorLabel;
            filterMenu.innerHTML = '';
            filterMenu.appendChild(langLabel);
            filterMenu.appendChild(langSelect);
            filterMenu.appendChild(colorPickerLabel);
            filterMenu.appendChild(colorPicker);
            renderFilters();
        }

        // Обработчик смены языка
        langSelect.addEventListener('change', () => {
            updateLanguage(langSelect.value);
        });

        // Рендеринг фильтров
        function renderFilters() {
            const filters = translations[currentLang].filters;
            filters.forEach(filter => {
                const filterItem = document.createElement('div');
                filterItem.className = 'filter-item';

                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.checked = savedFilterStates[filter.value];
                checkbox.id = `filter-${filter.value}`;

                const label = document.createElement('label');
                label.htmlFor = `filter-${filter.value}`;
                label.textContent = filter.name;

                const sliderLabel = document.createElement('label');
                sliderLabel.className = 'filter-slider-label';
                sliderLabel.textContent = translations[currentLang].sliderLabel;

                const slider = document.createElement('input');
                slider.type = 'range';
                slider.className = 'filter-slider';
                slider.min = filter.min;
                slider.max = filter.max;
                slider.step = filter.step;
                slider.value = savedFilterValues[filter.value];

                if (checkbox.checked && filter.hasSlider) {
                    slider.style.display = 'block';
                    sliderLabel.style.display = 'block';
                }

                checkbox.addEventListener('change', () => {
                    savedFilterStates[filter.value] = checkbox.checked;
                    localStorage.setItem('codeFilterStates', JSON.stringify(savedFilterStates));
                    if (filter.hasSlider) {
                        slider.style.display = checkbox.checked ? 'block' : 'none';
                        sliderLabel.style.display = checkbox.checked ? 'block' : 'none';
                    }
                    console.log('Изменен фильтр:', filter.value, checkbox.checked);
                    refreshAllCodeBlocks();
                });

                slider.addEventListener('input', () => {
                    savedFilterValues[filter.value] = slider.value;
                    localStorage.setItem('codeFilterValues', JSON.stringify(savedFilterValues));
                    console.log('Изменено значение фильтра:', filter.value, slider.value);
                    refreshAllCodeBlocks();
                });

                filterItem.appendChild(checkbox);
                filterItem.appendChild(label);
                filterMenu.appendChild(filterItem);
                filterMenu.appendChild(sliderLabel);
                filterMenu.appendChild(slider);
            });
        }

        // Инициализация
        filterMenu.appendChild(langLabel);
        filterMenu.appendChild(langSelect);
        filterMenu.appendChild(colorPickerLabel);
        filterMenu.appendChild(colorPicker);
        renderFilters();

        // Обработчики для кнопки
        filterBtn.addEventListener('click', () => {
            filterMenu.style.display = filterMenu.style.display === 'block' ? 'none' : 'block';
        });

        document.addEventListener('click', (e) => {
            if (!filterBtn.contains(e.target) && !filterMenu.contains(e.target)) {
                filterMenu.style.display = 'none';
            }
        });

        headerBlock.style.position = 'relative';
        headerBlock.appendChild(filterBtn);
        headerBlock.appendChild(filterMenu);
    }

    // Функция логирования структуры DOM для отладки
    function logDomStructure(headerBlock) {
        console.log('Заголовок блока кода:', headerBlock.outerHTML);
        console.log('Следующий элемент (nextElementSibling):', headerBlock.nextElementSibling?.outerHTML || 'Не найден');
        console.log('Родительский элемент:', headerBlock.parentElement.outerHTML);
        console.log('Все <code> в родителе:', Array.from(headerBlock.parentElement.querySelectorAll('code')).map(el => el.outerHTML));
        console.log('Все .not-prose в родителе:', Array.from(headerBlock.parentElement.querySelectorAll('.not-prose')).map(el => el.outerHTML));
    }

    // Функция для управления селекторами и контейнерами
    function manageSelectorsAndContainers(headerBlocks, headerSelectorUsed, containerSelectorUsed, codeContainer) {
        const savedSelectors = JSON.parse(localStorage.getItem('codeBlockSelectors') || '{}');

        // Сохраняем успешные селекторы и контейнеры
        if (headerBlocks.length > 0 && codeContainer) {
            savedSelectors[headerSelectorUsed] = savedSelectors[headerSelectorUsed] || {};
            savedSelectors[headerSelectorUsed].headerCount = headerBlocks.length;
            savedSelectors[headerSelectorUsed].containerSelector = containerSelectorUsed;
            savedSelectors[headerSelectorUsed].lastUsed = Date.now();
            localStorage.setItem('codeBlockSelectors', JSON.stringify(savedSelectors));
            console.log('Сохранены селекторы:', headerSelectorUsed, containerSelectorUsed);
        }

        // Очистка устаревших селекторов (старше 7 дней)
        const oneWeek = 7 * 24 * 60 * 60 * 1000;
        Object.keys(savedSelectors).forEach(selector => {
            if (Date.now() - savedSelectors[selector].lastUsed > oneWeek) {
                delete savedSelectors[selector];
            }
        });
        localStorage.setItem('codeBlockSelectors', JSON.stringify(savedSelectors));
        return savedSelectors;
    }

    // Функция для обновления всех кодовых блоков
    function refreshAllCodeBlocks() {
        const savedFilterStates = JSON.parse(localStorage.getItem('codeFilterStates') || '{}');
        const savedFilterValues = JSON.parse(localStorage.getItem('codeFilterValues') || '{}');
        const filters = [
            { value: 'invert', default: 1 },
            { value: 'sepia', default: 1 },
            { value: 'grayscale', default: 1 },
            { value: 'blur', unit: 'px', default: 2 },
            { value: 'contrast', default: 2 },
            { value: 'brightness', default: 1.5 },
            { value: 'hue-rotate', unit: 'deg', default: 90 },
            { value: 'saturate', default: 1 },
            { value: 'opacity', default: 1 }
        ];

        // Применяем к зарегистрированным блокам
        console.log('Реестр кодовых блоков:', codeBlockRegistry.length);
        codeBlockRegistry.forEach(({ codeContainer }) => {
            const activeFilters = filters
                .filter(filter => savedFilterStates[filter.value])
                .map(filter => {
                    const unit = filter.unit || '';
                    const value = savedFilterValues[filter.value] || filter.default;
                    return `${filter.value}(${value}${unit})`;
                });
            codeContainer.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
            console.log('Применены фильтры к зарегистрированному контейнеру:', codeContainer, activeFilters);

            const commentElements = codeContainer.querySelectorAll('span[style*="color: rgb(136, 136, 136)"], .hljs-comment');
            console.log('Найдено комментариев в зарегистрированном контейнере:', commentElements.length);
            commentElements.forEach(element => {
                element.style.color = currentCommentColor;
            });
        });

        // Глобальное применение к новым или незарегистрированным блокам
        const allCodeContainers = document.querySelectorAll('.not-prose div > code, .not-prose pre > code');
        allCodeContainers.forEach(codeContainer => {
            if (!codeBlockRegistry.some(reg => reg.codeContainer === codeContainer.parentElement)) {
                const activeFilters = filters
                    .filter(filter => savedFilterStates[filter.value])
                    .map(filter => {
                        const unit = filter.unit || '';
                        const value = savedFilterValues[filter.value] || filter.default;
                        return `${filter.value}(${value}${unit})`;
                    });
                codeContainer.parentElement.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
                console.log('Применены фильтры к глобальному .not-prose контейнеру:', codeContainer.parentElement, activeFilters);

                const commentElements = codeContainer.querySelectorAll('span[style*="color: rgb(136, 136, 136)"], .hljs-comment');
                console.log('Найдено комментариев в глобальном .not-prose контейнере:', commentElements.length);
                commentElements.forEach(element => {
                    element.style.color = currentCommentColor;
                });
            }
        });
    }

    // Функция поиска и обработки блоков кода
    function processCodeBlocks() {
        // Селекторы для заголовков блоков кода
        const headerSelectors = [
            'div[class*="flex"][class*="rounded-t"] > span.font-mono.text-xs',
            'div[class*="flex"][class*="bg-surface"] > span',
            'div > span[class*="font-mono"]'
        ];

        // Селекторы для контейнеров кода
        const containerSelectors = [
            'nextElementSibling',
            '.not-prose div[style*="overflow-x: auto"]',
            '.not-prose div > code',
            '.not-prose pre > code',
            '.not-prose div[style*="background: hsl"]'
        ];

        // Загружаем сохраненные селекторы
        const savedSelectors = JSON.parse(localStorage.getItem('codeBlockSelectors') || '{}');
        let headerBlocks = [];
        let headerSelectorUsed = null;
        let containerSelectorUsed = null;
        let codeContainer = null;

        // Сначала пытаемся использовать сохраненные селекторы
        for (const savedSelector of Object.keys(savedSelectors)) {
            const headers = Array.from(document.querySelectorAll(savedSelector))
                .filter(span => {
                    const text = span.textContent.toLowerCase();
                    return ['javascript', 'css', 'html', 'python', 'java', 'cpp', 'json', 'bash', 'sql', 'xml', 'yaml', 'markdown'].includes(text);
                })
                .map(span => span.closest('div'));
            if (headers.length > 0) {
                headerBlocks = [...new Set(headers)];
                headerSelectorUsed = savedSelector;
                const savedContainerSelector = savedSelectors[savedSelector].containerSelector;

                // Проверяем сохраненный селектор контейнера
                for (const headerBlock of headers) {
                    if (savedContainerSelector === 'nextElementSibling') {
                        codeContainer = headerBlock.nextElementSibling?.querySelector('code') ? headerBlock.nextElementSibling : null;
                    } else if (savedContainerSelector === '.not-prose div > code') {
                        codeContainer = headerBlock.parentElement.querySelector('.not-prose div > code')?.parentElement;
                    } else if (savedContainerSelector === '.not-prose pre > code') {
                        codeContainer = headerBlock.parentElement.querySelector('.not-prose pre > code')?.parentElement;
                    } else {
                        codeContainer = headerBlock.parentElement.querySelector(savedContainerSelector);
                    }
                    if (codeContainer) {
                        containerSelectorUsed = savedContainerSelector;
                        console.log('Использованы сохраненные селекторы:', headerSelectorUsed, containerSelectorUsed);
                        break;
                    }
                }
                if (codeContainer) break;
            }
        }

        // Если сохраненные селекторы не сработали, ищем новые
        if (headerBlocks.length === 0) {
            for (const selector of headerSelectors) {
                const headers = Array.from(document.querySelectorAll(selector))
                    .filter(span => {
                        const text = span.textContent.toLowerCase();
                        return ['javascript', 'css', 'html', 'python', 'java', 'cpp', 'json', 'bash', 'sql', 'xml', 'yaml', 'markdown'].includes(text);
                    })
                    .map(span => span.closest('div'));
                if (headers.length > 0) {
                    headerBlocks = [...new Set(headers)];
                    headerSelectorUsed = selector;
                    break;
                }
            }
        }

        console.log('Найдено заголовков блоков кода:', headerBlocks.length);

        headerBlocks.forEach(headerBlock => {
            const langSpan = headerBlock.querySelector('span.font-mono.text-xs');
            if (!langSpan) {
                console.log('Заголовок без span с языком:', headerBlock);
                return;
            }

            // Пытаемся найти контейнер кода
            codeContainer = null;
            if (headerBlock.nextElementSibling?.querySelector('code')) {
                codeContainer = headerBlock.nextElementSibling;
                containerSelectorUsed = 'nextElementSibling';
            } else if (headerBlock.parentElement.querySelector('.not-prose div[style*="overflow-x: auto"]')) {
                codeContainer = headerBlock.parentElement.querySelector('.not-prose div[style*="overflow-x: auto"]');
                containerSelectorUsed = '.not-prose div[style*="overflow-x: auto"]';
            } else if (headerBlock.parentElement.querySelector('.not-prose div > code')) {
                codeContainer = headerBlock.parentElement.querySelector('.not-prose div > code')?.parentElement;
                containerSelectorUsed = '.not-prose div > code';
            } else if (headerBlock.parentElement.querySelector('.not-prose pre > code')) {
                codeContainer = headerBlock.parentElement.querySelector('.not-prose pre > code')?.parentElement;
                containerSelectorUsed = '.not-prose pre > code';
            } else if (headerBlock.parentElement.querySelector('.not-prose div[style*="background: hsl"]')) {
                codeContainer = headerBlock.parentElement.querySelector('.not-prose div[style*="background: hsl"]');
                containerSelectorUsed = '.not-prose div[style*="background: hsl"]';
            }

            if (codeContainer) {
                console.log('Найден контейнер кода для заголовка:', codeContainer.outerHTML);
                addFilterMenu(headerBlock, codeContainer);
                // Сохраняем успешные селекторы
                manageSelectorsAndContainers(headerBlocks, headerSelectorUsed, containerSelectorUsed, codeContainer);
            } else {
                console.log('Контейнер кода не найден для заголовка:', headerBlock.outerHTML);
                logDomStructure(headerBlock);
            }
        });

        // Обновляем все кодовые блоки после обработки
        refreshAllCodeBlocks();
    }

    // Инициализация
    setTimeout(() => {
        console.log('Инициализация processCodeBlocks');
        processCodeBlocks();
    }, 2000);

    // Наблюдатель за изменениями DOM
    const observer = new MutationObserver((mutations) => {
        console.log('Обнаружены изменения DOM, вызывается processCodeBlocks');
        processCodeBlocks();
    });
    observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['class', 'style']
    });

    // Инициализируем глобальное применение фильтров
    refreshAllCodeBlocks();
})();