// ==UserScript==
// @name Grok Filter Code Menu 1.21.27
// @version 1.21.27
// @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/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @namespace http://tampermonkey.net/
// ==/UserScript==
// ========== tampermonkey Grok Filter Code Menu 1.21.25 =============== //
// ========== tampermonkey Grok Filter Code Menu 1.21.25 =============== //
// ========== tampermonkey Grok Filter Code Menu 1.21.25 =============== //
(function() {
'use strict';
// Флаг для включения/выключения логов
let LOGS_ENABLED = false; // false, true
// Функция логирования с проверкой флага
function log(...args) {
if (LOGS_ENABLED) {
console.log(...args); // Исправлено: было log(...args), что вызывало рекурсию
}
}
// Вкл/выкл логов снаружи (для удобства отладки в консоли)
window.toggleLogs = function(state) {
if (typeof state === 'boolean') {
LOGS_ENABLED = state;
} else {
LOGS_ENABLED = !LOGS_ENABLED; // переключатель
}
log(`Логи ${LOGS_ENABLED ? 'включены' : 'выключены'}`);
};
// Определение языка пользователя
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() // Храним соответствие headerBlock -> {codeContainer, filterBtn, filterMenu}
};
// Загрузка настроек
function loadSettings(callback) {
chrome.storage.local.get(
['filterMenuLang', 'codeFilterStates', 'codeFilterValues'], // Удалено: 'commentColor'
(result) => {
const settings = {
filterMenuLang: result.filterMenuLang || defaultLang,
codeFilterStates: result.codeFilterStates || {},
codeFilterValues: result.codeFilterValues || {},
// Удалено: commentColor: result.commentColor || 'rgb(106, 153, 85)'
};
state.settings = settings;
callback(settings);
}
);
}
// ===================== инъекция статического Фона CSS ======================
const styleElement = document.createElement('style');
styleElement.textContent = `
pre.shiki.slack-dark {
background: rgb(34 34 34) !important;
}
`;
document.head.appendChild(styleElement);
// ===================== инъекция статического Фона CSS ======================
// Применение фильтров к конкретному блоку с retry
// Обновлённая функция applyFilters (убрали логику фона)
function applyFilters(targetBlock, filterStates, filterValues, retries = 10) {
const preElement = targetBlock.querySelector('pre.shiki');
if (!preElement) {
if (retries > 0) {
setTimeout(() => applyFilters(targetBlock, filterStates, filterValues, retries - 1), 100);
} else {
console.warn('Элемент <pre> не найден в контейнере:', targetBlock);
}
return;
}
// Сбрасываем CSS-фильтр перед применением своих
preElement.style.setProperty('filter', 'none', 'important');
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.setProperty('filter', activeFilters.length > 0 ? activeFilters.join(' ') : 'none', 'important');
}
// Обновление фильтров для всех блоков
function updateAllFilters() {
state.codeBlocks.forEach(({codeContainer}) => {
applyFilters(codeContainer, state.settings.codeFilterStates, state.settings.codeFilterValues);
});
chrome.storage.local.set({
codeFilterStates: state.settings.codeFilterStates,
codeFilterValues: state.settings.codeFilterValues
});
}
// Удалена функция: applyCommentColor
// Удалена функция: updateAllCommentColors
// Создание меню фильтров
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);
});
// Удалено: Создание выбора цвета (colorPickerLabel, colorPicker, debounce, обработчик input)
// Удалено: function debounce(...) { ... }
// Обновление интерфейса при смене языка
function updateLanguage(lang) {
currentLang = lang;
state.settings.filterMenuLang = lang;
chrome.storage.local.set({ 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 = ...; filterMenu.appendChild(resetColorBtn);
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()}`; // Уникальный ID
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
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-l1.border.border-border-l1 > span.font-mono.text-xs',
'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',
'js',
'kotlin',
'properties',
'typescript',
'text',
'scss',
'css',
'html',
'python',
'java',
'vue',
'cpp',
'json',
'bash',
'powershell',
'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) {
log('Не найден span с языком:', headerBlock);
return;
}
const codeContainer = findCodeContainer(headerBlock);
if (codeContainer) {
addFilterMenu(headerBlock, codeContainer);
// Наблюдатель за изменениями внутри codeContainer
const codeObserver = new MutationObserver(() => {
requestAnimationFrame(() => {
// Удалено: applyCommentColor(codeContainer, state.settings.commentColor);
});
});
codeObserver.observe(codeContainer, { childList: true, subtree: true, attributes: true });
} else {
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"], div[class*="bg-surface-l1"]') ||
node.querySelector('[data-testid="code-block"], div.message-bubble, div.response-content-markdown, div.shiki, div[class*="code"], div[class*="flex"][class*="rounded-t"], div[class*="bg-surface-l1"]')
);
});
});
if (relevantChanges) {
processCodeBlocks();
setTimeout(processCodeBlocks, 1000);
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();
(function() {
// Проверяем, не добавлен ли уже этот стиль (по уникальному селектору, чтобы избежать дубликатов)
if (document.querySelector('#grok-styles-inject')) {
console.log('Стили уже инжектированы.');
return;
}
var style = document.createElement('style');
style.id = 'grok-styles-inject'; // Уникальный ID для идентификации
style.type = 'text/css';
style.innerHTML = `
/* ==================== БАР с кнопками копировать, свернуть, фильтр, ВЕРХНИЙ ==================== */
.flex.flex-row.px-4.py-2.h-10.items-center.rounded-t-xl.bg-surface-l1.border.border-border-l1 {
background: #cfb252 !important;
color: black !important;
height: 98px !important;
}
/* ------ новый селектор для markdown js --------*/
.flex.flex-row.px-4.py-2.h-10.items-center.rounded-t-xl.bg-surface-l2.border.border-border-l1 {
background: #cfb252 !important;
color: black !important;
height: 75px !important;
}
/* ==================== БАР Полоска с кнопками копировать, свернуть, фильтр, ВЕРХНИЙ ==================== */
/*======== контейнер с кодом code ========*/
pre.shiki.slack-dark {
background: rgb(34 34 34) !important;
}
/*======== контейнер с кодом code ========*/
/*======== color-comments ========*/
button#color-comments-btn {
position: absolute !important;
right: 520px !important;
}
/*======== color-comments ========*/
.filter-menu {
z-index: 100002 !important;
top: 100px;
right: 4px;
z-index: 9999;
display: none;
box-shadow: rgba(0, 0, 0, 0.3) 0px 2px 4px;
width: 285px;
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;
}
.filter-item label {
flex: 1;
cursor: pointer;
}
.filter-menu-btn {
position: absolute;
top: 62px;
right: 375px;
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;
}
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;
}
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
/* ---------------- filter цвет фона и слайдер -------------------- */
.filter-slider {
display: none;
margin: 5px 0 5px 20px;
width: calc(100% - 20px);
background: #173034; /* Темный фон для пустой части трека */
}
.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-item input[type="checkbox"] {
width: 29px;
height: 29px;
}
.filter-slider {
-webkit-appearance: none;
appearance: none;
width: 225px;
height: 15px;
outline: none;
right: 15px;
position: relative;
border-radius: 31px !important;
}
/* Стили для ползунка в WebKit-браузерах (Chrome, Safari, Edge) */
.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); /* Тень для эффекта */
}
/* Стили для ползунка в Firefox */
.filter-slider::-moz-range-thumb {
width: 20px; /* Ширина ползунка */
height: 20px; /* Высота ползунка */
background: #4CAF50; /* Цвет ползунка */
border-radius: 31px !important;
cursor: pointer;
border: none; /* Убираем стандартную границу Firefox */
}
/* Цвет заполнения для WebKit-браузеров */
.filter-slider::-webkit-slider-runnable-track {
background: linear-gradient(to right, #218a73 var(--value), #173034 var(--value)); /* Градиент для заполнения */
left: 110px !important;
border-radius: 31px !important;
border: 3px solid #55dfc5 !important;
}
/* Цвет заполнения для Firefox */
.filter-slider::-moz-range-progress {
background: #4CAF50; /* Цвет заполненной части */
height: 6px; /* Толщина заполненной части */
border-radius: 31px !important;
}
/* ---------------- filter цвет фона и слайдер -------------------- */
.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;
}
`;
document.head.appendChild(style);
console.log('Стили успешно инжектированы.');
})();