Grok Code Filter Menu 1.21.15

Adds a filter menu to the code blocks in the Grok chat while maintaining the settings

// ==UserScript==
// @name         Grok Code Filter Menu 1.21.15
// @namespace    http://tampermonkey.net/
// @version      1.21.15
// @description  Adds a filter menu to the code blocks in the Grok chat while maintaining the settings
// @author       tapeavion
// @license      MIT
// @match        https://grok.com/chat*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

 (function() {
    'use strict';

    // Стили
    const style = document.createElement('style');
    style.textContent = `
        .filter-menu-btn {
            position: absolute;
            top: 4px;
            right: 430px;
            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; /* Анимация появления за 1 секунду */
              }

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

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

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

        // Применяем цвет комментариев
        function applyCommentColor() {
            const commentElements = codeContainer.querySelectorAll('span[style*="color: rgb(92, 99, 112)"], .hljs-comment');
            commentElements.forEach(element => {
                element.style.color = currentCommentColor;
            });
        }
        applyFilters();
        applyCommentColor();

        // Создаем выпадающий список для выбора языка
        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);
            document.querySelectorAll('span[style*="color: rgb(92, 99, 112)"], .hljs-comment').forEach(element => {
                element.style.color = currentCommentColor;
            });
        });

        // Функция обновления интерфейса при смене языка
        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';
                    }
                    applyFilters();
                });

                slider.addEventListener('input', () => {
                    savedFilterValues[filter.value] = slider.value;
                    localStorage.setItem('codeFilterValues', JSON.stringify(savedFilterValues));
                    applyFilters();
                });

                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('Все <div> с overflow-x: auto в родителе:', Array.from(headerBlock.parentElement.querySelectorAll('div[style*="overflow-x: auto"]')).map(el => el.outerHTML));
    }

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

        let headerBlocks = [];
        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'));
            headerBlocks.push(...headers);
            if (headerBlocks.length > 0) break; // Прерываем, если нашли заголовки
        }
        headerBlocks = [...new Set(headerBlocks)]; // Удаляем дубликаты
        console.log('Найдено заголовков блоков кода:', headerBlocks.length);

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

            // Пытаемся найти контейнер кода
            let codeContainer = null;

            // Вариант 1: Следующий элемент
            if (headerBlock.nextElementSibling?.querySelector('code')) {
                codeContainer = headerBlock.nextElementSibling;
            }

            // Вариант 2: div с overflow-x: auto в родителе
            if (!codeContainer) {
                codeContainer = headerBlock.parentElement.querySelector('div[style*="overflow-x: auto"]');
            }

            // Вариант 3: div с code в родителе
            if (!codeContainer) {
                codeContainer = headerBlock.parentElement.querySelector('div > code')?.parentElement;
            }

            // Вариант 4: pre с code в родителе
            if (!codeContainer) {
                codeContainer = headerBlock.parentElement.querySelector('pre > code')?.parentElement;
            }

            // Вариант 5: Любой div с background hsl в родителе
            if (!codeContainer) {
                codeContainer = headerBlock.parentElement.querySelector('div[style*="background: hsl"]');
            }

            if (codeContainer) {
                console.log('Найден контейнер кода для заголовка:', codeContainer.outerHTML);
                addFilterMenu(headerBlock, codeContainer);

                // Применяем сохраненные фильтры и цвет комментариев
                const savedFilterStates = JSON.parse(localStorage.getItem('codeFilterStates') || '{}');
                const savedFilterValues = JSON.parse(localStorage.getItem('codeFilterValues') || '{}');
                const filters = [
                    { value: 'invert' },
                    { value: 'sepia' },
                    { value: 'grayscale' },
                    { value: 'blur', unit: 'px' },
                    { value: 'contrast' },
                    { value: 'brightness' },
                    { value: 'hue-rotate', unit: 'deg' },
                    { value: 'saturate' },
                    { value: 'opacity' }
                ];
                const activeFilters = filters
                    .filter(filter => savedFilterStates[filter.value])
                    .map(filter => {
                        const unit = filter.unit || '';
                        const value = savedFilterValues[filter.value] || (filter.value === 'blur' ? 2 : filter.value === 'brightness' ? 1.5 : filter.value === 'contrast' ? 2 : filter.value === 'hue-rotate' ? 90 : 1);
                        return `${filter.value}(${value}${unit})`;
                    });
                codeContainer.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';

                const commentElements = codeContainer.querySelectorAll('span[style*="color: rgb(92, 99, 112)"], .hljs-comment');
                commentElements.forEach(element => {
                    element.style.color = currentCommentColor;
                });
            } else {
                console.log('Контейнер кода не найден для заголовка:', headerBlock.outerHTML);
                logDomStructure(headerBlock); // Логируем структуру для анализа
            }
        });
    }

    // Инициализация
    setTimeout(processCodeBlocks, 2000); // Увеличил задержку для асинхронной загрузки
    processCodeBlocks();

    // Наблюдатель за изменениями DOM
    const observer = new MutationObserver(() => {
        processCodeBlocks();
    });
    observer.observe(document.body, { childList: true, subtree: true, attributes: true });
})();