Grok Code Filter Menu 1.21.15

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

Από την 17/04/2025. Δείτε την τελευταία έκδοση.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==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 });
})();