Grok Code Filter Menu 1.21.25

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

Устаревшая версия за 14.09.2025. Перейдите к последней версии.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

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