Grok Code Filter Menu

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

Від 28.03.2025. Дивіться остання версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension 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.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Grok Code Filter Menu
// @namespace    http://tampermonkey.net/
// @version      1.21.10
// @description  Добавляет меню фильтров к блокам кода в чате Grok с сохранением настроек
// @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';

    // Стили
    GM_addStyle(`
        .filter-menu-btn {
            position: absolute;
            top: 4px;
            right: 160px;
            z-index: 9999;
            padding: 4px 8px;
            background: #2d2d2d;
            color: #a0a0a0;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 12px;
            transition: background 0.2s ease, color 0.2s ease;
        }
        .filter-menu-btn:hover {
            background: #4a4a4a;
            color: #ffffff;
        }
        .filter-menu {
            position: absolute;
            top: 32px;
            right: 160px;
            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: 400px;
            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;
        }
    `);

    // Определение языка пользователя
    const userLang = navigator.language || navigator.languages[0];
    const isRussian = userLang.startsWith('ru');
    const defaultLang = isRussian ? 'ru' : 'en';
    const savedLang = GM_getValue('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' }
            ]
        }
    };

    // Функция создания меню фильтров
    function addFilterMenu(codeBlock) {
        if (codeBlock.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 = codeBlock.querySelector('div[style*="background: rgb(18, 19, 20)"]') || codeBlock;

        // Загружаем сохраненные настройки
        const savedFilterStates = GM_getValue('codeFilterStates', {});
        const savedFilterValues = GM_getValue('codeFilterValues', {});
        const savedCommentColor = GM_getValue('commentColor', '#5c6370'); // Оригинальный цвет в HEX

        // Инициализируем значения по умолчанию
        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 = codeBlock.querySelectorAll('span[style*="color: rgb(92, 99, 112)"]');
            commentElements.forEach(element => {
                element.style.color = savedCommentColor;
            });
        }
        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 = savedCommentColor;

        colorPicker.addEventListener('input', () => {
            const newColor = colorPicker.value;
            GM_setValue('commentColor', newColor);
            const commentElements = document.querySelectorAll('span[style*="color: rgb(92, 99, 112)"]');
            commentElements.forEach(element => {
                element.style.color = newColor;
            });
        });

        // Функция обновления интерфейса при смене языка
        function updateLanguage(lang) {
            currentLang = lang;
            GM_setValue('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;
                    GM_setValue('codeFilterStates', 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;
                    GM_setValue('codeFilterValues', 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';
            }
        });

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

    // Функция поиска и обработки блоков кода
    function processCodeBlocks() {
        const codeBlocks = document.querySelectorAll('div[class*="relative"][class*="mt-3"][class*="mb-3"][class*="-mx-4"]');
        console.log('Найдено блоков кода:', codeBlocks.length);
        codeBlocks.forEach(block => {
            if (block.querySelector('div[style*="background: rgb(18, 19, 20)"]')) {
                addFilterMenu(block);
                const savedFilterStates = GM_getValue('codeFilterStates', {});
                const savedFilterValues = GM_getValue('codeFilterValues', {});
                const savedCommentColor = GM_getValue('commentColor', '#5c6370');
                const targetBlock = block.querySelector('div[style*="background: rgb(18, 19, 20)"]') || block;
                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})`;
                    });
                targetBlock.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
                const commentElements = block.querySelectorAll('span[style*="color: rgb(92, 99, 112)"]');
                commentElements.forEach(element => {
                    element.style.color = savedCommentColor;
                });
            }
        });
    }

    setTimeout(processCodeBlocks, 1000);
    processCodeBlocks();

    const observer = new MutationObserver(() => {
        processCodeBlocks();
    });
    observer.observe(document.body, { childList: true, subtree: true });
})();