- // ==UserScript==
- // @name Grok Filter Code 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/*
- // @match https://grok.com/*
- // @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 });
- })();