Grok Code Filter Menu 1.21.25

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

Versão de: 14/09/2025. Veja: a última versão.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Grok Code Filter Menu 1.21.25
// @namespace    http://tampermonkey.net/
// @version      1.21.25
// @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/c/*
// @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 styles = `
        .filter-menu-btn {
            position: absolute;
            top: 4px;
            right: 485px;
            height: 31px !important;
            z-index: 1;
            padding: 4px 12px;
            background: #1d5752;
            color: #dcfff9;
            border: 2px solid aquamarine;
            border-radius: 8px;
            cursor: pointer;
            font-size: 12px;
            transition: background 0.2s ease, color 0.2s ease;
        }
        .filter-menu-btn:hover {
            background: #4a8983;
        }
        .filter-menu {
            position: fixed;
            z-index: 100000 !important;
            top: 100px;
            right: 4px;
            display: none;
            box-shadow: rgba(0, 0, 0, 0.3) 0px 2px 4px;
            width: 255px;
            max-height: 750px;
            overflow-y: auto;
            background: rgb(45, 45, 45);
            border-radius: 8px;
            padding: 5px;
            border-width: 2px !important;
            border-style: solid !important;
            border-color: rgb(93, 255, 247) !important;
            border-image: initial !important;
        }
        .filter-item {
            display: flex;
            align-items: center;
            padding: 5px 0;
            color: #a0a0a0;
            font-size: 12px;
        }
        .filter-item input[type="checkbox"] {
            margin-right: 5px;
            width: 29px;
            height: 29px;
        }
        .filter-item label {
            flex: 1;
            cursor: pointer;
        }
        label {
            color: #80ebff;
        }
        label.color-picker-label {
            color: #40bb97;
        }
        .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; }
        }
        .filter-slider {
            display: none;
            margin: 5px 0 5px 20px;
            width: calc(100% - 20px);
            background: #173034;
            -webkit-appearance: none;
            appearance: none;
            height: 15px;
            outline: none;
            right: 15px;
            position: relative;
            border-radius: 31px !important;
        }
        .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: 31px !important;
            font-size: 12px;
        }
        .filter-slider::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 20px;
            height: 20px;
            background: #35805f;
            border: 3px solid #164a53 !important;
            border-radius: 31px !important;
            cursor: pointer;
            box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
        }
        .filter-slider::-moz-range-thumb {
            width: 20px;
            height: 20px;
            background: #4CAF50;
            border-radius: 31px !important;
            cursor: pointer;
            border: none;
        }
        .filter-slider::-webkit-slider-runnable-track {
            background: linear-gradient(to right, #218a73 var(--value), #173034 var(--value));
            border-radius: 31px !important;
            border: 3px solid #55dfc5 !important;
        }
        .filter-slider::-moz-range-progress {
            background: #4CAF50;
            height: 6px;
            border-radius: 31px !important;
        }
        .reset-colorGrok6h63ew45-btn {
            background-color: #173034 !important;
            color: #218a73 !important;
            padding: 5px 10px;
            border: 1px solid #218a73 !important;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            margin-top: 5px;
            display: block;
            width: 100%;
            text-align: center;
        }
        .reset-colorGrok6h63ew45-btn:hover {
            background-color: #719e8b !important;
            color: #051b16 !important;
        }
    `;

    // Добавляем стили в документ
    const styleSheet = document.createElement('style');
    styleSheet.textContent = styles;
    document.head.appendChild(styleSheet);

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

    // Локализация
    const translations = {
        ru: {
            filtersBtn: 'Фильтры',
            sliderLabel: 'Степень:',
            commentColorLabel: 'Цвет комментариев:',
            resetColorBtn: 'Сбросить цвет',
            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:',
            resetColorBtn: 'Reset 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' }
            ]
        }
    };

    // Глобальный объект для хранения настроек и контейнеров
    const state = {
        settings: null,
        codeBlocks: new Map()
    };

    // Загрузка настроек
    function loadSettings(callback) {
        const settings = {
            filterMenuLang: GM_getValue('filterMenuLang', defaultLang),
            codeFilterStates: GM_getValue('codeFilterStates', {}),
            codeFilterValues: GM_getValue('codeFilterValues', {}),
            commentColor: GM_getValue('commentColor', 'rgb(106, 153, 85)')
        };
        state.settings = settings;
        callback(settings);
    }

    // Применение фильтров к конкретному блоку с retry
    function applyFilters(targetBlock, filterStates, filterValues, retries = 5) {
        const preElement = targetBlock.querySelector('pre');
        if (!preElement) {
            if (retries > 0) {
                setTimeout(() => applyFilters(targetBlock, filterStates, filterValues, retries - 1), 50);
            }
            return;
        }
        const filters = translations[state.settings.filterMenuLang].filters;
        const activeFilters = filters
            .filter(filter => filterStates[filter.value])
            .map(filter => {
                const unit = filter.unit || '';
                const value = filterValues[filter.value] || filter.default;
                return `${filter.value}(${value}${unit})`;
            });
        preElement.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
    }

    // Обновление фильтров для всех блоков
    function updateAllFilters() {
        state.codeBlocks.forEach(({codeContainer}) => {
            applyFilters(codeContainer, state.settings.codeFilterStates, state.settings.codeFilterValues);
        });
        GM_setValue('codeFilterStates', state.settings.codeFilterStates);
        GM_setValue('codeFilterValues', state.settings.codeFilterValues);
    }

    // Применение цвета комментариев с retry
    function applyCommentColor(codeContainer, commentColor) {
        const commentElements = codeContainer.querySelectorAll(
            '.hljs-comment, span[style*="color: rgb(106, 153, 85)"], span[style*="color: #6a9955"], ' +
            'span[style*="color: rgb(92, 99, 112)"]'
        );

        if (commentElements.length > 0) {
            commentElements.forEach(element => {
                element.style.setProperty('color', commentColor, 'important');
            });
        } else {
            if (window.location.search.includes('debug')) {
                const lang = codeContainer.previousElementSibling?.querySelector('span.font-mono.text-xs')?.textContent || 'Неизвестный';
                const snippet = codeContainer.textContent.substring(0, 50) + '...';
                console.warn(`Не найдены элементы комментариев в контейнере (язык: ${lang}, фрагмент: "${snippet}"):`, codeContainer);
            }
        }
    }

    // Обновление цвета комментариев для всех блоков
    function updateAllCommentColors() {
        requestAnimationFrame(() => {
            state.codeBlocks.forEach(({codeContainer}) => {
                applyCommentColor(codeContainer, state.settings.commentColor);
            });
            GM_setValue('commentColor', state.settings.commentColor);
        });
    }

    // Функция debounce
    function debounce(func, wait) {
        let timeout;
        return function (...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

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

        let currentLang = state.settings.filterMenuLang;
        let currentCommentColor = state.settings.commentColor;
        let savedFilterStates = { ...state.settings.codeFilterStates };
        let savedFilterValues = { ...state.settings.codeFilterValues };

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

        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', debounce(() => {
            currentCommentColor = colorPicker.value;
            state.settings.commentColor = currentCommentColor;
            requestAnimationFrame(() => {
                updateAllCommentColors();
            });
        }, 100));

        function updateLanguage(lang) {
            currentLang = lang;
            state.settings.filterMenuLang = lang;
            GM_setValue('filterMenuLang', lang);
            filterBtn.textContent = translations[currentLang].filtersBtn;
            langLabel.textContent = translations[currentLang].langSelect;
            colorPickerLabel.textContent = translations[currentLang].commentColorLabel;
            renderFilters();
        }

        langSelect.addEventListener('change', () => updateLanguage(langSelect.value));

        function renderFilters() {
            filterMenu.innerHTML = '';
            filterMenu.appendChild(langLabel);
            filterMenu.appendChild(langSelect);
            filterMenu.appendChild(colorPickerLabel);
            filterMenu.appendChild(colorPicker);

            const resetColorBtn = document.createElement('button');
            resetColorBtn.className = 'reset-colorGrok6h63ew45-btn';
            resetColorBtn.textContent = translations[currentLang].resetColorBtn || 'Сбросить цвет';
            filterMenu.appendChild(resetColorBtn);

            resetColorBtn.addEventListener('click', () => {
                const defaultColor = 'rgb(106, 153, 85)';
                currentCommentColor = defaultColor;
                state.settings.commentColor = defaultColor;
                colorPicker.value = '#6a9955';
                requestAnimationFrame(() => {
                    updateAllCommentColors();
                });
            });

            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}-${Date.now()}`;

                const label = document.createElement('label');
                label.htmlFor = checkbox.id;
                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];

                const updateSlider = () => {
                    const value = ((slider.value - slider.min) / (slider.max - slider.min)) * 100;
                    slider.style.background = `linear-gradient(to right, #218a73 ${value}%, #173034 ${value}%)`;
                };

                slider.addEventListener('input', updateSlider);
                updateSlider();

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

                checkbox.addEventListener('change', () => {
                    savedFilterStates[filter.value] = checkbox.checked;
                    state.settings.codeFilterStates = savedFilterStates;
                    if (filter.hasSlider) {
                        slider.style.display = checkbox.checked ? 'block' : 'none';
                        sliderLabel.style.display = checkbox.checked ? 'block' : 'none';
                    }
                    updateAllFilters();
                });

                slider.addEventListener('input', () => {
                    savedFilterValues[filter.value] = slider.value;
                    state.settings.codeFilterValues = savedFilterValues;
                    updateAllFilters();
                });

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

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

        state.codeBlocks.set(headerBlock, { codeContainer, filterBtn, filterMenu });

        applyFilters(codeContainer, savedFilterStates, savedFilterValues);
        applyCommentColor(codeContainer, currentCommentColor);
    }

    // Обработка блоков кода
    function processCodeBlocks() {
        if (!state.settings) {
            loadSettings((settings) => {
                processCodeBlocksInternal(settings);
            });
            return;
        }
        processCodeBlocksInternal(state.settings);
    }

    function findCodeContainer(bar) {
        let sibling = bar.nextElementSibling;
        while (sibling) {
            if (sibling.matches('div.shiki') || sibling.querySelector('pre, code') || sibling.matches('div[class*="code"]') || sibling.matches('div.sticky')) {
                if (sibling.matches('div.shiki')) {
                    return sibling;
                }
            }
            sibling = sibling.nextElementSibling;
        }
        return null;
    }

    function processCodeBlocksInternal(settings) {
        const headerSelectors = [
            'div.flex.flex-row.px-4.py-2.h-10.items-center.rounded-t-xl.bg-surface-l2.border.border-border-l1 > span.font-mono.text-xs',
            'div.flex.flex-row.items-center.rounded-t-xl.bg-surface-l2.border > span.font-mono.text-xs',
            'div[class*="flex"][class*="rounded-t"] > span.font-mono.text-xs',
            'div[class*="flex"][class*="bg-surface"] > span',
            'div > span[class*="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',
                        'typescript',
                        'text',
                        'css',
                        'html',
                        'python',
                        'java',
                        'cpp',
                        'json',
                        'bash',
                        'sql',
                        'xml',
                        'yaml',
                        'markdown'
                    ].includes(text);
                })
                .map(span => span.closest('div'));
            headerBlocks.push(...headers);
        }
        headerBlocks = [...new Set(headerBlocks)];

        state.codeBlocks.forEach((value, key) => {
            if (!document.body.contains(key)) {
                state.codeBlocks.delete(key);
            }
        });

        headerBlocks.forEach(headerBlock => {
            if (state.codeBlocks.has(headerBlock)) return;

            const langSpan = headerBlock.querySelector('span.font-mono.text-xs');
            if (!langSpan) {
                console.log('Не найден span с языком:', headerBlock);
                return;
            }

            const codeContainer = findCodeContainer(headerBlock);

            if (codeContainer) {
                addFilterMenu(headerBlock, codeContainer);
                const codeObserver = new MutationObserver(() => {
                    requestAnimationFrame(() => {
                        applyCommentColor(codeContainer, state.settings.commentColor);
                    });
                });
                codeObserver.observe(codeContainer, { childList: true, subtree: true, attributes: true });
            } else {
                console.log('Контейнер кода не найден для:', headerBlock);
            }
        });
    }

    // Инициализация
    processCodeBlocks();

    // Наблюдатель за изменениями DOM
    const observer = new MutationObserver((mutations) => {
        const relevantChanges = mutations.some(mutation => {
            return mutation.addedNodes.length > 0 && Array.from(mutation.addedNodes).some(node => {
                return node.nodeType === 1 && (node.getAttribute('data-testid') === 'code-block' || node.matches('div.message-bubble, .flex.flex-col.items-center, div.response-content-markdown, div.shiki, div[class*="code"], div[class*="flex"][class*="rounded-t"]') || node.querySelector('[data-testid="code-block"], div.message-bubble, div.response-content-markdown, div.shiki, div[class*="code"], div[class*="flex"][class*="rounded-t"]'));
            });
        });
        if (relevantChanges) {
            processCodeBlocks();
            setTimeout(processCodeBlocks, 1000);
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });
})();