Grok Code Filter Menu

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

当前为 2025-03-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Grok Code Filter Menu
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.21.9
  5. // @license MIT
  6. // @description Adds a filter menu to the code blocks in the Grok chat while maintaining the settings
  7. // @author tapeavion
  8. // @match https://grok.com/chat/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
  10. // @grant GM_addStyle
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // ==/UserScript==
  14.  
  15.  
  16.  
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. // Стили
  22. GM_addStyle(`
  23. .filter-menu-btn {
  24. position: absolute;
  25. top: 4px;
  26. right: 345px;
  27. z-index: 9999;
  28. padding: 4px 8px;
  29. background: #2d2d2d;
  30. color: #a0a0a0;
  31. border: none;
  32. border-radius: 8px;
  33. cursor: pointer;
  34. font-size: 12px;
  35. transition: background 0.2s ease, color 0.2s ease;
  36. }
  37. .filter-menu-btn:hover {
  38. background: #4a4a4a;
  39. color: #ffffff;
  40. }
  41. .filter-menu {
  42. position: absolute;
  43. top: 32px;
  44. right: 160px;
  45. background: #2d2d2d;
  46. border: 1px solid #444;
  47. border-radius: 8px;
  48. padding: 5px;
  49. z-index: 9999;
  50. display: none;
  51. box-shadow: 0 2px 4px rgba(0,0,0,0.3);
  52. width: 200px;
  53. max-height: 400px;
  54. overflow-y: auto;
  55. }
  56. .filter-item {
  57. display: flex;
  58. align-items: center;
  59. padding: 5px 0;
  60. color: #a0a0a0;
  61. font-size: 12px;
  62. }
  63. .filter-item input[type="checkbox"] {
  64. margin-right: 5px;
  65. }
  66. .filter-item label {
  67. flex: 1;
  68. cursor: pointer;
  69. }
  70. .filter-slider {
  71. display: none;
  72. margin: 5px 0 5px 20px;
  73. width: calc(100% - 20px);
  74. }
  75. .filter-slider-label {
  76. display: none;
  77. color: #a0a0a0;
  78. font-size: 12px;
  79. margin: 2px 0 2px 20px;
  80. }
  81. .language-select {
  82. width: 100%;
  83. padding: 5px;
  84. margin-bottom: 5px;
  85. background: #3a3a3a;
  86. color: #a0a0a0;
  87. border: none;
  88. border-radius: 4px;
  89. font-size: 12px;
  90. }
  91. .color-picker {
  92. margin: 5px 0 5px 20px;
  93. width: calc(100% - 20px);
  94. }
  95. .color-picker-label {
  96. display: block;
  97. color: #a0a0a0;
  98. font-size: 12px;
  99. margin: 2px 0 2px 20px;
  100. }
  101. `);
  102.  
  103. // Определение языка пользователя
  104. const userLang = navigator.language || navigator.languages[0];
  105. const isRussian = userLang.startsWith('ru');
  106. const defaultLang = isRussian ? 'ru' : 'en';
  107. const savedLang = GM_getValue('filterMenuLang', defaultLang);
  108.  
  109. // Локализация
  110. const translations = {
  111. ru: {
  112. filtersBtn: 'Фильтры',
  113. sliderLabel: 'Степень:',
  114. commentColorLabel: 'Цвет комментариев:',
  115. filters: [
  116. { name: 'Негатив', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  117. { name: 'Сепия', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  118. { name: 'Ч/Б', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  119. { name: 'Размытие', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' },
  120. { name: 'Контраст', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 },
  121. { name: 'Яркость', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 },
  122. { name: 'Поворот оттенка', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' },
  123. { name: 'Насыщенность', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 },
  124. { name: 'Прозрачность', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }
  125. ],
  126. langSelect: 'Выберите язык:',
  127. langOptions: [
  128. { value: 'ru', label: 'Русский' },
  129. { value: 'en', label: 'English' }
  130. ]
  131. },
  132. en: {
  133. filtersBtn: 'Filters',
  134. sliderLabel: 'Level:',
  135. commentColorLabel: 'Comment color:',
  136. filters: [
  137. { name: 'Invert', value: 'invert', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  138. { name: 'Sepia', value: 'sepia', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  139. { name: 'Grayscale', value: 'grayscale', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 },
  140. { name: 'Blur', value: 'blur', hasSlider: true, min: 0, max: 5, step: 0.1, default: 2, unit: 'px' },
  141. { name: 'Contrast', value: 'contrast', hasSlider: true, min: 0, max: 3, step: 0.1, default: 2 },
  142. { name: 'Brightness', value: 'brightness', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1.5 },
  143. { name: 'Hue Rotate', value: 'hue-rotate', hasSlider: true, min: 0, max: 360, step: 1, default: 90, unit: 'deg' },
  144. { name: 'Saturate', value: 'saturate', hasSlider: true, min: 0, max: 3, step: 0.1, default: 1 },
  145. { name: 'Opacity', value: 'opacity', hasSlider: true, min: 0, max: 1, step: 0.1, default: 1 }
  146. ],
  147. langSelect: 'Select language:',
  148. langOptions: [
  149. { value: 'ru', label: 'Русский' },
  150. { value: 'en', label: 'English' }
  151. ]
  152. }
  153. };
  154.  
  155. // Функция создания меню фильтров
  156. function addFilterMenu(codeBlock) {
  157. if (codeBlock.querySelector('.filter-menu-btn')) return;
  158.  
  159. let currentLang = savedLang;
  160. const filterBtn = document.createElement('button');
  161. filterBtn.className = 'filter-menu-btn';
  162. filterBtn.textContent = translations[currentLang].filtersBtn;
  163.  
  164. const filterMenu = document.createElement('div');
  165. filterMenu.className = 'filter-menu';
  166.  
  167. const targetBlock = codeBlock.querySelector('div[style*="background: rgb(18, 19, 20)"]') || codeBlock;
  168.  
  169. // Загружаем сохраненные настройки
  170. const savedFilterStates = GM_getValue('codeFilterStates', {});
  171. const savedFilterValues = GM_getValue('codeFilterValues', {});
  172. const savedCommentColor = GM_getValue('commentColor', '#5c6370'); // Оригинальный цвет в HEX
  173.  
  174. // Инициализируем значения по умолчанию
  175. const filters = translations[currentLang].filters;
  176. filters.forEach(filter => {
  177. if (!(filter.value in savedFilterStates)) {
  178. savedFilterStates[filter.value] = false;
  179. }
  180. if (!(filter.value in savedFilterValues)) {
  181. savedFilterValues[filter.value] = filter.default;
  182. }
  183. });
  184.  
  185. // Применяем сохраненные фильтры
  186. function applyFilters() {
  187. const activeFilters = filters
  188. .filter(filter => savedFilterStates[filter.value])
  189. .map(filter => {
  190. const unit = filter.unit || '';
  191. const value = savedFilterValues[filter.value];
  192. return `${filter.value}(${value}${unit})`;
  193. });
  194. targetBlock.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
  195. }
  196.  
  197. // Применяем цвет комментариев
  198. function applyCommentColor() {
  199. const commentElements = codeBlock.querySelectorAll('.comment, .punctuation-comment, [class*="comment"]');
  200. commentElements.forEach(element => {
  201. element.style.color = savedCommentColor;
  202. });
  203. }
  204. applyFilters();
  205. applyCommentColor();
  206.  
  207. // Создаем выпадающий список для выбора языка
  208. const langSelect = document.createElement('select');
  209. langSelect.className = 'language-select';
  210. const langLabel = document.createElement('label');
  211. langLabel.textContent = translations[currentLang].langSelect;
  212. langLabel.style.color = '#a0a0a0';
  213. langLabel.style.fontSize = '12px';
  214. langLabel.style.marginBottom = '2px';
  215. langLabel.style.display = 'block';
  216.  
  217. translations[currentLang].langOptions.forEach(option => {
  218. const opt = document.createElement('option');
  219. opt.value = option.value;
  220. opt.textContent = option.label;
  221. if (option.value === currentLang) {
  222. opt.selected = true;
  223. }
  224. langSelect.appendChild(opt);
  225. });
  226.  
  227. // Создаем элемент для выбора цвета комментариев
  228. const colorPickerLabel = document.createElement('label');
  229. colorPickerLabel.className = 'color-picker-label';
  230. colorPickerLabel.textContent = translations[currentLang].commentColorLabel;
  231.  
  232. const colorPicker = document.createElement('input');
  233. colorPicker.type = 'color';
  234. colorPicker.className = 'color-picker';
  235. colorPicker.value = savedCommentColor;
  236.  
  237. colorPicker.addEventListener('input', () => {
  238. const newColor = colorPicker.value;
  239. GM_setValue('commentColor', newColor);
  240. const commentElements = document.querySelectorAll('.comment, .punctuation-comment, [class*="comment"]');
  241. commentElements.forEach(element => {
  242. element.style.color = newColor;
  243. });
  244. });
  245.  
  246. // Функция обновления интерфейса при смене языка
  247. function updateLanguage(lang) {
  248. currentLang = lang;
  249. GM_setValue('filterMenuLang', currentLang);
  250. filterBtn.textContent = translations[currentLang].filtersBtn;
  251. langLabel.textContent = translations[currentLang].langSelect;
  252. colorPickerLabel.textContent = translations[currentLang].commentColorLabel;
  253. filterMenu.innerHTML = ''; // Очищаем меню
  254. filterMenu.appendChild(langLabel);
  255. filterMenu.appendChild(langSelect);
  256. filterMenu.appendChild(colorPickerLabel);
  257. filterMenu.appendChild(colorPicker);
  258. renderFilters();
  259. }
  260.  
  261. // Обработчик смены языка
  262. langSelect.addEventListener('change', () => {
  263. updateLanguage(langSelect.value);
  264. });
  265.  
  266. // Рендеринг фильтров
  267. function renderFilters() {
  268. const filters = translations[currentLang].filters;
  269. filters.forEach(filter => {
  270. const filterItem = document.createElement('div');
  271. filterItem.className = 'filter-item';
  272.  
  273. const checkbox = document.createElement('input');
  274. checkbox.type = 'checkbox';
  275. checkbox.checked = savedFilterStates[filter.value];
  276. checkbox.id = `filter-${filter.value}`;
  277.  
  278. const label = document.createElement('label');
  279. label.htmlFor = `filter-${filter.value}`;
  280. label.textContent = filter.name;
  281.  
  282. const sliderLabel = document.createElement('label');
  283. sliderLabel.className = 'filter-slider-label';
  284. sliderLabel.textContent = translations[currentLang].sliderLabel;
  285.  
  286. const slider = document.createElement('input');
  287. slider.type = 'range';
  288. slider.className = 'filter-slider';
  289. slider.min = filter.min;
  290. slider.max = filter.max;
  291. slider.step = filter.step;
  292. slider.value = savedFilterValues[filter.value];
  293.  
  294. if (checkbox.checked && filter.hasSlider) {
  295. slider.style.display = 'block';
  296. sliderLabel.style.display = 'block';
  297. }
  298.  
  299. checkbox.addEventListener('change', () => {
  300. savedFilterStates[filter.value] = checkbox.checked;
  301. GM_setValue('codeFilterStates', savedFilterStates);
  302. if (filter.hasSlider) {
  303. slider.style.display = checkbox.checked ? 'block' : 'none';
  304. sliderLabel.style.display = checkbox.checked ? 'block' : 'none';
  305. }
  306. applyFilters();
  307. });
  308.  
  309. slider.addEventListener('input', () => {
  310. savedFilterValues[filter.value] = slider.value;
  311. GM_setValue('codeFilterValues', savedFilterValues);
  312. applyFilters();
  313. });
  314.  
  315. filterItem.appendChild(checkbox);
  316. filterItem.appendChild(label);
  317. filterMenu.appendChild(filterItem);
  318. filterMenu.appendChild(sliderLabel);
  319. filterMenu.appendChild(slider);
  320. });
  321. }
  322.  
  323. // Инициализация
  324. filterMenu.appendChild(langLabel);
  325. filterMenu.appendChild(langSelect);
  326. filterMenu.appendChild(colorPickerLabel);
  327. filterMenu.appendChild(colorPicker);
  328. renderFilters();
  329.  
  330. // Обработчики для кнопки
  331. filterBtn.addEventListener('click', () => {
  332. filterMenu.style.display = filterMenu.style.display === 'block' ? 'none' : 'block';
  333. });
  334.  
  335. document.addEventListener('click', (e) => {
  336. if (!filterBtn.contains(e.target) && !filterMenu.contains(e.target)) {
  337. filterMenu.style.display = 'none';
  338. }
  339. });
  340.  
  341. codeBlock.style.position = 'relative';
  342. codeBlock.appendChild(filterBtn);
  343. codeBlock.appendChild(filterMenu);
  344. }
  345.  
  346. // Функция поиска и обработки блоков кода
  347. function processCodeBlocks() {
  348. const codeBlocks = document.querySelectorAll('div[class*="relative"][class*="mt-3"][class*="mb-3"][class*="-mx-4"]');
  349. console.log('Найдено блоков кода:', codeBlocks.length);
  350. codeBlocks.forEach(block => {
  351. if (block.querySelector('div[style*="background: rgb(18, 19, 20)"]')) {
  352. addFilterMenu(block);
  353. const savedFilterStates = GM_getValue('codeFilterStates', {});
  354. const savedFilterValues = GM_getValue('codeFilterValues', {});
  355. const savedCommentColor = GM_getValue('commentColor', '#5c6370');
  356. const targetBlock = block.querySelector('div[style*="background: rgb(18, 19, 20)"]') || block;
  357. const filters = [
  358. { value: 'invert' },
  359. { value: 'sepia' },
  360. { value: 'grayscale' },
  361. { value: 'blur', unit: 'px' },
  362. { value: 'contrast' },
  363. { value: 'brightness' },
  364. { value: 'hue-rotate', unit: 'deg' },
  365. { value: 'saturate' },
  366. { value: 'opacity' }
  367. ];
  368. const activeFilters = filters
  369. .filter(filter => savedFilterStates[filter.value])
  370. .map(filter => {
  371. const unit = filter.unit || '';
  372. const value = savedFilterValues[filter.value] || (filter.value === 'blur' ? 2 : filter.value === 'brightness' ? 1.5 : filter.value === 'contrast' ? 2 : filter.value === 'hue-rotate' ? 90 : 1);
  373. return `${filter.value}(${value}${unit})`;
  374. });
  375. targetBlock.style.filter = activeFilters.length > 0 ? activeFilters.join(' ') : 'none';
  376. const commentElements = block.querySelectorAll('.comment, .punctuation-comment, [class*="comment"]');
  377. commentElements.forEach(element => {
  378. element.style.color = savedCommentColor;
  379. });
  380. }
  381. });
  382. }
  383.  
  384. setTimeout(processCodeBlocks, 1000);
  385. processCodeBlocks();
  386.  
  387. const observer = new MutationObserver(() => {
  388. processCodeBlocks();
  389. });
  390. observer.observe(document.body, { childList: true, subtree: true });
  391. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址