您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a filter menu to the code blocks in the Grok chat while maintaining the settings
// ==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 }); })();