SORRY. NOT SORRY IF OVER WORD LIMIT. COMPLICATED
// ==UserScript==
// @name Grok Imagine
// @namespace Violentmonkey Scripts
// @match *://example.org/*
// @grant none
// @version 1.0
// @author -
// @description 04/12/2025, 4:11:56 pm
// ==/UserScript==
let maxAttempts = 5;
let attempts = 0;
let cancel = false;
let _cancel = () => cancel;
let observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
//when the video generation is successful, reset attempts to 0
if (mutation.target.matches("div.grid")) {
console.log("Generation successful.")
attempts = 0;
}
//when the "Content moderated" notification appears
else if (
node.nodeName === 'OL' &&
node.classList.contains('toaster') &&
node.classList.contains('group') &&
node.textContent.includes('Content Moderated. Try a different idea.')
) {
//if there are still remaining attempts
if (++attempts < maxAttempts) {
//wait a few seconds to be safe
new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 2000);
}).then(() => {
console.log(`Content moderated. Attempt ${attempts + 1}/${maxAttempts}...`);
//to stop the script, put a breakpoint on the below IF statement
//and run `cancel = true` in console
if (_cancel()) {
console.log("Aborted");
cancel = false;
attempts = 0;
} else {
//click the generate button
document.querySelector("button[aria-label='Make video']").click();
}
});
}
//when attempts exceed max, reset attempts to 0 and do nothing;
else {
attempts = 0;
console.log("Generation failed.");
}
}
}
}
}
});
// Configure and start the observer
observer.observe(document.body, {
childList: true, // Observe changes to the direct children
subtree: true, // Observe changes to descendants
});
console.log("auto-clicker started");
// ==UserScript==// ==UserScript==
// @name Grok.com Anti-Revert Prompt Saver & Auto-Retry
// @namespace
http://tampermonkey.net/
// @version 9.0
// @description Prevents Grok from reverting your prompt on moderation errors. Uniquely saves prompts per video ID.
// @author You
// @match
https://grok.com/*
// @icon
https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @grant none
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION ---
const TEXTAREA_SELECTOR = 'textarea';
const NOTIFICATION_SELECTOR = 'section[aria-label="Notifications alt+T"]';
const TARGET_SVG_PATH = "M5 11L12 4M12 4L19 11M12 4V21";
const ERROR_KEYWORDS = ['fail', 'error', 'moderate', 'policy', 'unable', 'wrong'];
// --- STATE ---
let autoRetryOn = false; // Always starts OFF
let isRetrying = false;
let retryInterval = null;
let overlayBtn = null;
let currentPath = window.location.pathname;
// --- HELPER: LOGGING ---
function log(msg) { console.log(`[Grok Script] ${msg}`); }
// --- HELPER: GET UNIQUE STORAGE KEY ---
function getStorageKey() {
return `grok_prompt_storage_${window.location.pathname}`;
}
// --- PART 1: PROMPT SAVER (SMARTER) ---
function setNativeValue(element, value) {
const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set;
const prototype = Object.getPrototypeOf(element);
const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;
if (valueSetter && valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(element, value);
} else {
valueSetter.call(element, value);
}
element.dispatchEvent(new Event('input', { bubbles: true }));
}
// New Function: Specifically to fight the "Moderation Revert"
function forceRestorePrompt() {
const key = getStorageKey();
const savedPrompt = localStorage.getItem(key);
const textarea = document.querySelector(TEXTAREA_SELECTOR);
if (savedPrompt && textarea) {
log('Moderation error detected. Protecting prompt from revert...');
// The site usually reverts the text 100-300ms after the error.
// We wait 500ms and then SLAM the correct text back in.
setTimeout(() => {
// Double check: if the current value is NOT what we saved, restore it.
if (textarea.value !== savedPrompt) {
log('Restoring user prompt...');
setNativeValue(textarea, savedPrompt);
// Visual Cue (Red Flash) to show we fixed the text
textarea.style.transition = 'background-color 0.3s';
const oldBg = textarea.style.backgroundColor;
textarea.style.backgroundColor = '#4a151b'; // Dark red tint
setTimeout(() => { textarea.style.backgroundColor = oldBg; }, 600);
}
}, 500);
}
}
function attachToTextarea(textarea) {
const currentKey = getStorageKey();
if (textarea.dataset.attachedKey === currentKey) return;
textarea.dataset.attachedKey = currentKey;
log(`Connected to memory slot: ${currentKey}`);
// 1. Initial Restore
const savedPrompt = localStorage.getItem(currentKey);
if (savedPrompt) {
log(`Loaded saved prompt for ${currentKey}`);
setNativeValue(textarea, savedPrompt);
}
// 2. SMART SAVE
textarea.addEventListener('input', (e) => {
// CRITICAL FIX: Only save if the event is "Trusted" (User actually typed/pasted).
// If the site programmatically changes the text (e.g. auto-revert), isTrusted is false.
// We ignore those so we don't save the "bad" reverted text.
if (e.isTrusted) {
const liveKey = getStorageKey();
localStorage.setItem(liveKey, e.target.value);
}
});
}
// --- PART 2: UI OVERLAY ---
function getTargetButton() {
let btn = document.querySelector('button[aria-label="Make video"]');
if (!btn) {
const allBtns = Array.from(document.querySelectorAll('button'));
btn = allBtns.find(b => {
const path = b.querySelector('path');
return path && path.getAttribute('d') === TARGET_SVG_PATH;
});
}
return btn;
}
function updateOverlayVisuals() {
if (!overlayBtn) return;
if (autoRetryOn) {
overlayBtn.innerHTML = '▶';
overlayBtn.style.backgroundColor = '#22c55e';
overlayBtn.title = "Auto-Retry ON";
} else {
overlayBtn.innerHTML = '■';
overlayBtn.style.backgroundColor = '#6b7280';
overlayBtn.title = "Auto-Retry OFF";
}
}
function createOverlay() {
if (document.getElementById('grok-retry-toggle')) return;
overlayBtn = document.createElement('div');
overlayBtn.id = 'grok-retry-toggle';
overlayBtn.style.position = 'fixed';
overlayBtn.style.width = '48px';
overlayBtn.style.height = '48px';
overlayBtn.style.fontSize = '24px';
overlayBtn.style.borderRadius = '50%';
overlayBtn.style.color = 'white';
overlayBtn.style.display = 'flex';
overlayBtn.style.justifyContent = 'center';
overlayBtn.style.alignItems = 'center';
overlayBtn.style.cursor = 'pointer';
overlayBtn.style.zIndex = '9999';
overlayBtn.style.boxShadow = '0 4px 8px rgba(0,0,0,0.4)';
overlayBtn.style.userSelect = 'none';
overlayBtn.style.transition = 'all 0.2s ease';
overlayBtn.addEventListener('click', (e) => {
e.stopPropagation();
autoRetryOn = !autoRetryOn;
updateOverlayVisuals();
overlayBtn.style.transform = 'scale(0.9)';
setTimeout(() => overlayBtn.style.transform = 'scale(1)', 100);
if (autoRetryOn) {
log('Toggle ON -> Triggering immediate click');
triggerRetrySequence();
}
});
updateOverlayVisuals();
document.body.appendChild(overlayBtn);
}
function updateOverlayPosition() {
const target = getTargetButton();
if (target && overlayBtn) {
const rect = target.getBoundingClientRect();
overlayBtn.style.display = 'flex';
overlayBtn.style.top = `${rect.bottom + 10}px`;
overlayBtn.style.left = `${rect.left + (rect.width / 2) - 24}px`;
} else if (overlayBtn) {
overlayBtn.style.display = 'none';
}
requestAnimationFrame(updateOverlayPosition);
}
// --- PART 3: AUTO-RETRY ---
function simulateRealClick(element) {
['mousedown', 'mouseup', 'click'].forEach(eventType => {
element.dispatchEvent(new MouseEvent(eventType, {
bubbles: true, cancelable: true, view: window, buttons: 1
}));
});
}
function triggerRetrySequence() {
if (!autoRetryOn || isRetrying) return;
isRetrying = true;
let attempts = 0;
if (retryInterval) clearInterval(retryInterval);
retryInterval = setInterval(() => {
attempts++;
const btn = getTargetButton();
if (btn && !btn.disabled && !btn.classList.contains('disabled')) {
const originalBg = btn.style.backgroundColor;
btn.style.backgroundColor = '#22c55e';
simulateRealClick(btn);
setTimeout(() => btn.style.backgroundColor = originalBg, 300);
clearInterval(retryInterval);
isRetrying = false;
} else if (attempts >= 40) {
clearInterval(retryInterval);
isRetrying = false;
}
}, 500);
}
// --- PART 4: URL & ERROR MONITORING ---
function checkUrlChange() {
if (window.location.pathname !== currentPath) {
log(`URL Change: ${window.location.pathname}`);
currentPath = window.location.pathname;
const t = document.querySelector(TEXTAREA_SELECTOR);
if (t) {
t.dataset.attachedKey = '';
attachToTextarea(t);
}
}
}
function attachToNotificationArea(section) {
if (section.dataset.grokMonitorAttached === 'true') return;
section.dataset.grokMonitorAttached = 'true';
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
const text = section.innerText.toLowerCase();
const hasError = ERROR_KEYWORDS.some(k => text.includes(k));
if (hasError) {
log('Error detected.');
// 1. Force the prompt back (in case site reverted it)
forceRestorePrompt();
// 2. Retry generation (if toggle is on)
setTimeout(triggerRetrySequence, 500);
}
}
});
});
observer.observe(section, { childList: true, subtree: true, characterData: true });
}
const mainObserver = new MutationObserver((mutations) => {
checkUrlChange();
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
if (node.tagName.toLowerCase() === 'textarea') attachToTextarea(node);
else {
const t = node.querySelector(TEXTAREA_SELECTOR);
if (t) attachToTextarea(t);
}
if (node.matches && node.matches(NOTIFICATION_SELECTOR)) attachToNotificationArea(node);
else {
const n = node.querySelector(NOTIFICATION_SELECTOR);
if (n) attachToNotificationArea(n);
}
}
});
});
});
mainObserver.observe(document.body, { childList: true, subtree: true });
// Initial Start
const iT = document.querySelector(TEXTAREA_SELECTOR);
if (iT) attachToTextarea(iT);
const iN = document.querySelector(NOTIFICATION_SELECTOR);
if (iN) attachToNotificationArea(iN);
createOverlay();
updateOverlayPosition();
})();
// @name New script
// @namespace Violentmonkey Scripts
// @match *://example.org/*
// @grant none
// @version 1.0
// @author -
// @description 08/12/2025, 9:45:50 am
// ==/UserScript==
// ==UserScript==
// @name Grok Attempt
// @namespace Violentmonkey Scripts
// @match *://example.org/*
// @grant none
// @version 1.0
// @author -
// @description 16/12/2025, 9:36:03 pm
// ==/UserScript==let maxAttempts = 5;
let attempts = 0;
let cancel = false;
let _cancel = () => cancel;
let observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
//when the video generation is successful, reset attempts to 0
if (mutation.target.matches("div.grid")) {
console.log("Generation successful.")
attempts = 0;
}
//when the "Content moderated" notification appears
else if (
node.nodeName === 'OL' &&
node.classList.contains('toaster') &&
node.classList.contains('group') &&
node.textContent.includes('Content Moderated. Try a different idea.')
) {
//if there are still remaining attempts
if (++attempts < maxAttempts) {
//wait a few seconds to be safe
new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 2000);
}).then(() => {
console.log(`Content moderated. Attempt ${attempts + 1}/${maxAttempts}...`);
//to stop the script, put a breakpoint on the below IF statement
//and run `cancel = true` in console
if (_cancel()) {
console.log("Aborted");
cancel = false;
attempts = 0;
} else {
//click the generate button
document.querySelector("button[aria-label='Make video']").click();
}
});
}
//when attempts exceed max, reset attempts to 0 and do nothing;
else {
attempts = 0;
console.log("Generation failed.");
}
}
}
}
}
// ==UserScript==
// @name Grok Imagine Auto-Advance (Final V3.3 - Text Preservation)
// @namespace
http://tampermonkey.net/
// @version 3.3
// @description FIX: Saves the prompt before starting and restores it into the textarea if 'Content Moderated' occurs, before retrying.
// @author You
// @match
https://grok.com/*
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// --- Configuration & State ---
const CHECK_INTERVAL_MS = 1000;
const MODERATION_CHECK_INTERVAL_MS = 500;
const MODERATION_CHECK_DURATION = 5000;
const FAB_BUTTON_ID = 'grok-fab-toggle';
const NEXT_SVG_PATH = 'M5 11L12 4M12 4L19 11M12 4V21';
// State Variables
let isEnabled = false;
let initialActionTaken = false;
let checkInterval = null;
let moderationInterval = null;
let retryCount = 0;
let moderationCheckStartTime = 0;
let percentIndicatorWasVisible = false;
let savedPromptText = ''; // Variable to store the text
const originalTitle = document.title;
// --- QoL: Title Management ---
function updateTitle(status) {
if (!isEnabled && status !== 'READY') {
document.title = originalTitle;
return;
}
switch (status) {
case 'LOADING':
document.title = `[ Auto-Retry #${retryCount}] Generation in progress...`;
break;
case 'MODERATION_CHECK':
document.title = `[ Moderation Check #${retryCount}] Waiting...`;
break;
case 'READY':
document.title = `[

VIDEO READY] - Click to continue`;
break;
default:
document.title = originalTitle;
}
}
// --- Text Preservation Logic (NEW) ---
function saveTextareaContent() {
const textarea = document.querySelector('textarea');
if (textarea) {
savedPromptText = textarea.value;
console.log("%c[TEXT SAVE] Content saved: " + savedPromptText.substring(0, 20) + "...", 'color: cyan;');
} else {
console.log("%c[TEXT SAVE] No textarea found to save.", 'color: red;');
}
}
function restoreTextareaContent() {
if (!savedPromptText) return;
const textarea = document.querySelector('textarea');
if (textarea) {
// Logic to properly simulate a user paste/type for modern frameworks (React, etc)
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
if (nativeInputValueSetter) {
nativeInputValueSetter.call(textarea, savedPromptText);
} else {
textarea.value = savedPromptText;
}
// Dispatch events to ensure the UI registers the change
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.dispatchEvent(new Event('change', { bubbles: true }));
console.log("%c[TEXT RESTORE] Content restored into textarea.", 'color: cyan;');
}
}
// --- Core Logic Helpers ---
function findNextButton() {
const pathElement = document.querySelector(`path[d="${NEXT_SVG_PATH}"]`);
if (!pathElement) return null;
let currentElement = pathElement;
while (currentElement && currentElement.tagName !== 'BUTTON') {
currentElement = currentElement.parentElement;
}
return (currentElement && currentElement.tagName === 'BUTTON') ? currentElement : null;
}
function checkIsLoading(nextButton) {
const previousElement = nextButton.previousElementSibling;
if (previousElement && previousElement.tagName === 'BUTTON') {
return previousElement.textContent.includes('%');
}
return false;
}
/** Runs the continuous check for "Content Moderated" text. */
function checkModerationStatus() {
const elapsedTime = Date.now() - moderationCheckStartTime;
const nextButton = findNextButton();
const moderatedElement = Array.from(document.body.querySelectorAll('body *')).find(el => {
return el.textContent.includes('Content Moderated') && el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE';
});
if (moderatedElement && nextButton) {
// CASE 1: MODERATED -> Retry
console.log("%c[ACTION] SUCCESS: 'Content Moderated' found.", 'background: #007700; color: white; padding: 2px;');
updateTitle('READY');
clearInterval(moderationInterval);
moderationInterval = null;
// --- STEP: RESTORE TEXT BEFORE CLICKING ---
restoreTextareaContent();
// ------------------------------------------
// Small delay to ensure text is registered before click (optional but safer)
setTimeout(() => {
console.log("Clicking retry...");
nextButton.click();
startRapidCheck();
}, 100);
return;
}
if (elapsedTime >= MODERATION_CHECK_DURATION) {
// CASE 2: TIMEOUT -> Video generated successfully
console.log("%c[ACTION] FINAL SUCCESS: 5 seconds elapsed. Video generated successfully.", 'background: #00AA00; color: white; padding: 2px;');
clearInterval(moderationInterval);
moderationInterval = null;
stopFeature(true);
} else {
updateTitle('MODERATION_CHECK');
}
}
function startModerationCheck() {
console.log(`%c[PHASE 2/3] Loading complete. Starting ${MODERATION_CHECK_DURATION/1000}s active moderation search.`, 'color: orange;');
moderationCheckStartTime = Date.now();
moderationInterval = setInterval(checkModerationStatus, MODERATION_CHECK_INTERVAL_MS);
checkModerationStatus();
}
/** The function run by the 1-second interval (Phase 1: Loading Check). */
function initialLoadingCheck() {
if (!isEnabled) return;
const nextButton = findNextButton();
if (!nextButton) return;
const isLoading = checkIsLoading(nextButton);
if (isLoading) {
percentIndicatorWasVisible = true;
updateTitle('LOADING');
} else {
if (!percentIndicatorWasVisible) {
// Initial wait for '%' to appear after click.
updateTitle('LOADING');
return;
}
// Loading is gone, and we know it was there. -> Generation completed.
if (checkInterval) {
clearInterval(checkInterval);
checkInterval = null;
percentIndicatorWasVisible = false;
startModerationCheck();
}
}
}
function startRapidCheck() {
if (!checkInterval) {
if (initialActionTaken) {
retryCount++;
}
percentIndicatorWasVisible = false;
checkInterval = setInterval(initialLoadingCheck, CHECK_INTERVAL_MS);
initialLoadingCheck();
}
}
// --- Feature Toggling & Action Execution ---
function stopFeature(isSuccess) {
isEnabled = false;
const fabButton = document.getElementById(FAB_BUTTON_ID);
if (fabButton) {
fabButton.classList.remove('active');
fabButton.title = 'Click to enable Auto-Advance';
}
if (checkInterval) {
clearInterval(checkInterval);
checkInterval = null;
}
if (moderationInterval) {
clearInterval(moderationInterval);
moderationInterval = null;
console.log('Moderation check stopped.');
}
initialActionTaken = false;
percentIndicatorWasVisible = false;
retryCount = 0;
// Do not clear savedPromptText here, in case user wants to manually retry later with same text
if (isSuccess) {
updateTitle('READY');
} else {
updateTitle(null);
}
}
function toggleFeature(status) {
if (status) {
// START
isEnabled = true;
const fabButton = document.getElementById(FAB_BUTTON_ID);
if (!fabButton) return;
fabButton.classList.add('active');
fabButton.title = 'Click to disable Auto-Advance';
if (!initialActionTaken) {
const nextButton = findNextButton();
if (nextButton) {
console.log('%c[INITIAL ACTION] First click detected. Starting generation immediately.', 'background: #333; color: yellow;');
// --- STEP: SAVE TEXT BEFORE FIRST CLICK ---
saveTextareaContent();
// ------------------------------------------
nextButton.click();
initialActionTaken = true;
retryCount = 1;
}
}
startRapidCheck();
} else {
// STOP
stopFeature(false);
}
}
// --- UI Setup ---
function setupUIAndListeners() {
GM_addStyle(`
#grok-fab-container { position: fixed; bottom: 20px; right: 20px; z-index: 10000; }
#${FAB_BUTTON_ID} {
width: 56px; height: 56px; border-radius: 50%;
color: white; border: none; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
font-size: 24px; cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: background-color 0.3s, transform 0.2s; outline: none;
background-color: #2ecc71; line-height: 1;
}
#${FAB_BUTTON_ID}:hover { transform: scale(1.05); }
#${FAB_BUTTON_ID}.active { background-color: #e74c3c; }
#${FAB_BUTTON_ID}::before { content: ''; position: absolute; transition: all 0.2s; }
#${FAB_BUTTON_ID}:not(.active)::before {
width: 0; height: 0; border-top: 10px solid transparent; border-bottom: 10px solid transparent;
border-left: 15px solid white; transform: translateX(2px);
}
#${FAB_BUTTON_ID}.active::before { width: 16px; height: 16px; background-color: white; transform: none; }
`);
const fabContainer = document.createElement('div');
fabContainer.id = 'grok-fab-container';
const fabButton = document.createElement('button');
fabButton.id = FAB_BUTTON_ID;
fabButton.innerHTML = '';
fabButton.title = 'Click to enable Auto-Advance';
fabContainer.appendChild(fabButton);
if (document.body) {
document.body.appendChild(fabContainer);
}
fabButton.addEventListener('click', (e) => {
e.preventDefault();
toggleFeature(!isEnabled);
});
updateTitle(null);
}
if (document.body) {
setupUIAndListeners();
} else {
document.addEventListener('DOMContentLoaded', setupUIAndListeners);
window.addEventListener('load', () => setTimeout(setupUIAndListeners, 500));
}
})();
// ==UserScript==
// @name Grok Post Manager
// @namespace Violentmonkey Scripts
// @match *://example.org/*
// @grant none
// @version 1.0
// @author -
// @description 15/01/2026, 3:09:49 pm
// ==/UserScript==
// ==UserScript==
// @name Grok Cleaner
// @namespace
http://tampermonkey.net/
// @version 3.9
// @description Grok Post Manager. Smart hide, safe delete (2s delay), GC button.
// @author Gemini
// @match
https://grok.com/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
console.log('[Grok-Cleaner] Initializing...');
// --- 1. CONFIG & STATE ---
const API_LIST_URL = "
https://grok.com/rest/media/post/list-shared-posts";
const API_DELETE_URL = "
https://grok.com/rest/media/post/delete";
let capturedHeaders = null;
let currentPosts = [];
// --- 2. HEADER INTERCEPTOR ---
const originalFetch = window.fetch;
window.fetch = async function(input, init) {
const url = input.toString();
if (init && init.headers && !url.includes('list-shared-posts') && !url.includes('post/delete')) {
if (!capturedHeaders && url.includes('grok.com')) {
capturedHeaders = {};
if (init.headers instanceof Headers) {
init.headers.forEach((v, k) => { capturedHeaders[k.toLowerCase()] = v; });
} else {
Object.keys(init.headers).forEach(k => { capturedHeaders[k.toLowerCase()] = init.headers[k]; });
}
capturedHeaders['content-type'] = 'application/json';
console.log('[Grok-Cleaner] Tokens captured!');
updateStatus('Ready (Tokens OK)');
}
}
return originalFetch.apply(this, arguments);
};
// --- 3. API FUNCTIONS ---
async function fetchHiddenPosts() {
if (!capturedHeaders) {
alert("No tokens! Perform an action in Imagine (e.g., refresh/generate) to capture authorization.");
return [];
}
updateStatus('Loading...');
try {
const response = await originalFetch(API_LIST_URL, {
method: "POST",
headers: capturedHeaders,
body: JSON.stringify({ limit: 400 }),
mode: "cors"
});
if (!response.ok) throw new Error(`Status: ${response.status}`);
const data = await response.json();
const posts = data.posts || [];
updateStatus(`Found: ${posts.length}`);
return posts;
} catch (e) {
console.error(e);
updateStatus('Error fetching');
return [];
}
}
async function deletePost(postId, rowElement) {
if (!capturedHeaders) return;
if (rowElement) {
rowElement.style.opacity = '0.5';
rowElement.style.pointerEvents = 'none';
}
try {
const response = await originalFetch(API_DELETE_URL, {
method: "POST",
headers: capturedHeaders,
body: JSON.stringify({ id: postId }),
mode: "cors"
});
if (response.ok) {
if (rowElement) rowElement.remove();
return true;
} else {
if (rowElement) {
rowElement.style.backgroundColor = '#300';
rowElement.style.pointerEvents = 'auto';
}
return false;
}
} catch (e) {
console.error(e);
return false;
}
}
// --- 4. GUI (Panel) ---
function createPanel() {
if (document.getElementById('grok-hidden-panel')) return;
// Main container
const panel = document.createElement('div');
panel.id = 'grok-hidden-panel';
Object.assign(panel.style, {
position: 'fixed', top: '60px', right: '20px',
width: '360px', height: '500px',
minWidth: '280px', minHeight: '300px',
backgroundColor: '#0f0f0f', border: '1px solid #333',
borderRadius: '8px', zIndex: '99999', flexDirection: 'column',
boxShadow: '0 4px 20px rgba(0,0,0,0.9)', fontFamily: 'system-ui, sans-serif', color: '#ccc',
fontSize: '13px',
display: 'none'
});
// --- TOP SECTION ---
const topSection = document.createElement('div');
Object.assign(topSection.style, {
backgroundColor: '#161616', borderBottom: '1px solid #333', display: 'flex', flexDirection: 'column'
});
// 1. Header
const headerRow = document.createElement('div');
Object.assign(headerRow.style, {
padding: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #222'
});
const titleBlock = document.createElement('div');
titleBlock.innerHTML = '<strong style="color:#fff">Grok Cleaner</strong>';
const status = document.createElement('span');
status.id = 'gh-status';
status.innerText = 'Waiting...';
status.style.fontSize = '11px';
status.style.color = '#777';
status.style.marginLeft = '10px';
titleBlock.appendChild(status);
const closeBtn = document.createElement('button');
closeBtn.innerText = "X";
Object.assign(closeBtn.style, { background: 'none', border: 'none', color: '#fff', cursor: 'pointer', fontWeight: 'bold' });
closeBtn.onclick = () => panel.style.display = 'none';
headerRow.appendChild(titleBlock);
headerRow.appendChild(closeBtn);
// 2. Navigation
const navRow = document.createElement('div');
Object.assign(navRow.style, { display: 'flex', padding: '5px', gap: '5px' });
navRow.appendChild(createSimpleBtn('FILES', '#222', () => window.open('
https://grok.com/files', '_blank')));
navRow.appendChild(createSimpleBtn('SHARED', '#222', () => window.open('
https://grok.com/share-links', '_blank')));
// 3. Control Row
const controlRow = document.createElement('div');
Object.assign(controlRow.style, { padding: '0 5px 10px 5px', display: 'flex', gap: '5px' });
const listContainer = document.createElement('div');
// LOAD Button
const btnLoad = createSimpleBtn('LOAD POSTS', '#1D9BF0', async () => {
listContainer.innerHTML = '';
currentPosts = await fetchHiddenPosts();
renderPosts(currentPosts, listContainer);
});
btnLoad.style.color = '#fff';
btnLoad.style.fontWeight = 'bold';
btnLoad.style.flex = '1';
// DELETE ALL Button
const btnDeleteAll = createSimpleBtn('DELETE ALL', '#b00020', async () => {
const deleteButtons = listContainer.querySelectorAll('button[data-action="delete"]');
if(deleteButtons.length === 0) {
updateStatus('Nothing to delete');
setTimeout(() => {
const statusEl = document.getElementById('gh-status');
if(statusEl && statusEl.innerText.includes('Nothing')) statusEl.innerText = 'Ready';
}, 2000);
return;
}
const total = deleteButtons.length;
for (let i = 0; i < total; i++) {
updateStatus(`Deleting ${i + 1}/${total}...`);
deleteButtons
.click();
await new Promise(r => setTimeout(r, 2000));
}
updateStatus('Cleaned!');
});
btnDeleteAll.style.color = '#fff';
btnDeleteAll.style.fontWeight = 'bold';
btnDeleteAll.style.flex = '1';
controlRow.appendChild(btnLoad);
controlRow.appendChild(btnDeleteAll);
topSection.appendChild(headerRow);
topSection.appendChild(navRow);
topSection.appendChild(controlRow);
// --- LIST ---
Object.assign(listContainer.style, {
flex: '1', overflowY: 'auto', padding: '0', backgroundColor: '#000',
scrollbarWidth: 'thin', scrollbarColor: '#333 #111'
});
// --- RESIZE HANDLE ---
const resizeHandle = document.createElement('div');
Object.assign(resizeHandle.style, {
position: 'absolute', bottom: '0', left: '0',
width: '15px', height: '15px', cursor: 'sw-resize',
background: 'linear-gradient(45deg, transparent 50%, #555 50%)',
zIndex: '100000'
});
let isResizing = false;
resizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
e.preventDefault();
const startX = e.clientX;
const startY = e.clientY;
const startWidth = parseInt(getComputedStyle(panel).width, 10);
const startHeight = parseInt(getComputedStyle(panel).height, 10);
function onMouseMove(e) {
if (!isResizing) return;
panel.style.height = (startHeight + (e.clientY - startY)) + 'px';
panel.style.width = (startWidth + (startX - e.clientX)) + 'px';
}
function onMouseUp() {
isResizing = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
panel.appendChild(topSection);
panel.appendChild(listContainer);
panel.appendChild(resizeHandle);
document.body.appendChild(panel);
// --- FLOATING BUTTON (GC) ---
const floater = document.createElement('button');
floater.id = 'grok-cleaner-floater';
floater.innerText = "GC";
Object.assign(floater.style, {
position: 'fixed', bottom: '10px', right: '10px', padding: '4px 8px',
borderRadius: '4px', border: '1px solid #444', background: '#000', color: '#fff',
cursor: 'pointer', zIndex: '99998', fontSize: '10px', opacity: '0.8',
display: 'none'
});
floater.onclick = () => {
const isHidden = panel.style.display === 'none';
panel.style.display = isHidden ? 'flex' : 'none';
};
document.body.appendChild(floater);
}
// --- HELPERS ---
function createSimpleBtn(text, bg, onClick) {
const btn = document.createElement('button');
btn.innerText = text;
Object.assign(btn.style, {
flex: '1', padding: '8px', backgroundColor: bg, color: '#ccc',
border: '1px solid #333', borderRadius: '4px', cursor: 'pointer', fontSize: '12px'
});
btn.onclick = onClick;
return btn;
}
function updateStatus(text) {
const el = document.getElementById('gh-status');
if(el) {
el.innerText = text;
if(text.includes('OK') || text.includes('Cleaned')) el.style.color = '#00BA7C';
else if(text.includes('Nothing') || text.includes('Error')) el.style.color = '#E91E63';
else if(text.includes('Deleting')) el.style.color = '#FF9800';
else el.style.color = '#777';
}
}
function renderPosts(posts, container) {
if(posts.length === 0) {
container.innerHTML = '<div style="text-align:center; padding:20px; color:#555;">No posts found.</div>';
return;
}
posts.forEach(post => {
const row = document.createElement('div');
Object.assign(row.style, {
display: 'flex', gap: '8px', padding: '8px', borderBottom: '1px solid #222',
alignItems: 'center', backgroundColor: '#111'
});
// IMG
const img = document.createElement('img');
Object.assign(img.style, { width: '40px', height: '40px', objectFit: 'cover', borderRadius: '3px', backgroundColor: '#222' });
if (post.thumbnailImageUrl) img.src = post.thumbnailImageUrl;
// TEXT
const info = document.createElement('div');
Object.assign(info.style, { flex: '1', overflow: 'hidden' });
info.innerHTML = `<div style="font-size:11px; color:#ddd; white-space:nowrap; overflow:hidden; text-overflow:ellipsis">${post.prompt || "---"}</div>
<div style="font-size:9px; color:#555; font-family:monospace">${post.postId}</div>`;
// BUTTONS
const actions = document.createElement('div');
Object.assign(actions.style, { display: 'flex', gap: '4px' });
const btnOpen = document.createElement('button');
btnOpen.innerText = "SHOW";
Object.assign(btnOpen.style, { backgroundColor: '#222', color: '#aaa', border: '1px solid #444', borderRadius: '3px', padding: '3px 6px', cursor: 'pointer', fontSize: '10px' });
btnOpen.onclick = () => window.open(`https://grok.com/imagine/post/${post.postId}`, '_blank');
const btnId = document.createElement('button');
btnId.innerText = "ID";
Object.assign(btnId.style, { backgroundColor: '#222', color: '#888', border: '1px solid #444', borderRadius: '3px', padding: '3px 6px', cursor: 'pointer', fontSize: '10px' });
btnId.onclick = () => { navigator.clipboard.writeText(post.postId); };
const btnDel = document.createElement('button');
btnDel.innerText = "DELETE";
btnDel.setAttribute('data-action', 'delete');
Object.assign(btnDel.style, { backgroundColor: '#300', color: '#f88', border: '1px solid #500', borderRadius: '3px', padding: '3px 6px', cursor: 'pointer', fontSize: '10px' });
btnDel.onclick = () => deletePost(post.postId, row);
actions.append(btnOpen, btnId, btnDel);
row.append(img, info, actions);
container.appendChild(row);
});
}
// --- 5. URL MONITOR ---
function checkUrlAndVisibility() {
const floater = document.getElementById('grok-cleaner-floater');
const panel = document.getElementById('grok-hidden-panel');
if (!floater) return;
const path = window.location.pathname;
// CHANGE HERE: using startsWith to catch both /imagine and /imagine/post/...
const shouldShow = path.startsWith('/imagine');
if (shouldShow) {
floater.style.display = 'block';
} else {
floater.style.display = 'none';
if (panel) panel.style.display = 'none';
}
}
window.addEventListener('load', createPanel);
setTimeout(createPanel, 1500);
setInterval(checkUrlAndVisibility, 500);
})();
// ==UserScript==
// @name Grok DeMod
// @license GPL-3.0-or-later
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Hides moderation results in Grok conversations, auto-recovers blocked messages.
// @author UniverseDev
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @match https://grok.com/*
// @grant unsafeWindow
// @downloadURL https://update.greasyfork.org/scripts/531147/Grok DeMod.user.js
// @updateURL https://update.greasyfork.org/scripts/531147/Grok DeMod.meta.js
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
defaultFlags: [
'isFlagged', 'isBlocked', 'moderationApplied', 'restricted'
],
messageKeys: ['message', 'content', 'text', 'error'],
moderationMessagePatterns: [
/this content has been moderated/i,
/sorry, i cannot assist/i,
/policy violation/i,
/blocked/i,
/moderated/i,
/restricted/i,
/content restricted/i,
/unable to process/i,
/cannot help/i,
/(sorry|apologies).*?(cannot|unable|help|assist)/i,
],
clearedMessageText: '[Content cleared by Grok DeMod]',
recoveryTimeoutMs: 5000,
lsKeys: {
enabled: 'GrokDeModEnabled',
debug: 'GrokDeModDebug',
flags: 'GrokDeModFlags',
},
styles: {
uiContainer: `
position: fixed;
bottom: 10px;
right: 10px;
z-index: 10000;
background: #2d2d2d;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
gap: 8px;
font-family: Arial, sans-serif;
color: #e0e0e0;
min-width: 170px;
`,
button: `
padding: 6px 12px;
border-radius: 5px;
border: none;
cursor: pointer;
color: #fff;
font-size: 13px;
transition: background-color 0.2s ease;
`,
status: `
padding: 5px;
font-size: 12px;
color: #a0a0a0;
text-align: center;
border-top: 1px solid #444;
margin-top: 5px;
min-height: 16px;
`,
logContainer: `
max-height: 100px;
overflow-y: auto;
font-size: 11px;
color: #c0c0c0;
background-color: #333;
padding: 5px;
border-radius: 4px;
line-height: 1.4;
margin-top: 5px;
`,
logEntry: `
padding-bottom: 3px;
border-bottom: 1px dashed #555;
margin-bottom: 3px;
word-break: break-word;
`,
colors: {
enabled: '#388E3C',
disabled: '#D32F2F',
debugEnabled: '#1976D2',
debugDisabled: '#555555',
safe: '#66ff66',
flagged: '#ffa500',
blocked: '#ff6666',
recovering: '#ffcc00'
}
}
};
let demodEnabled = getState(CONFIG.lsKeys.enabled, true);
let debug = getState(CONFIG.lsKeys.debug, false);
let moderationFlags = getState(CONFIG.lsKeys.flags, CONFIG.defaultFlags);
let initCache = null;
let currentConversationId = null;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const uiLogBuffer = [];
const MAX_LOG_ENTRIES = 50;
const ModerationResult = Object.freeze({
SAFE: 0,
FLAGGE
1,
BLOCKE
2,
});
function logDebug(...args) {
if (debug) {
console.log('[Grok DeMod]', ...args);
}
}
function logError(...args) {
console.error('[Grok DeMod]', ...args);
}
function getState(key, defaultValue) {
try {
const value = localStorage.getItem(key);
if (value === null) return defaultValue;
if (value === 'true') return true;
if (value === 'false') return false;
return JSON.parse(value);
} catch (e) {
logError(`Error reading ${key} from localStorage:`, e);
return defaultValue;
}
}
function setState(key, value) {
try {
const valueToStore = typeof value === 'boolean' ? value.toString() : JSON.stringify(value);
localStorage.setItem(key, valueToStore);
} catch (e) {
logError(`Error writing ${key} to localStorage:`, e);
}
}
function timeoutPromise(ms, promise, description = 'Promise') {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
logDebug(`${description} timed out after ${ms}ms`);
reject(new Error(`Timeout (${description})`));
}, ms);
promise.then(
(value) => { clearTimeout(timer); resolve(value); },
(error) => { clearTimeout(timer); reject(error); }
);
});
}
function getModerationResult(obj, path = '') {
if (typeof obj !== 'object' || obj === null) return ModerationResult.SAFE;
let result = ModerationResult.SAFE;
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const currentPath = path ? `${path}.${key}` : key;
const value = obj[key];
if (key === 'isBlocked' && value === true) {
logDebug(`Blocked detected via flag '${currentPath}'`);
return ModerationResult.BLOCKED;
}
if (moderationFlags.includes(key) && value === true) {
logDebug(`Flagged detected via flag '${currentPath}'`);
result = Math.max(result, ModerationResult.FLAGGED);
}
if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
const content = value.toLowerCase();
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(content)) {
logDebug(`Moderation pattern matched in '${currentPath}': "${content.substring(0, 50)}..."`);
if (/blocked|moderated|restricted/i.test(pattern.source)) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, ModerationResult.FLAGGED);
break;
}
}
if (result === ModerationResult.SAFE && content.length < 70 && /(sorry|apologies|unable|cannot)/i.test(content)) {
logDebug(`Heuristic moderation detected in '${currentPath}': "${content.substring(0, 50)}..."`);
result = Math.max(result, ModerationResult.FLAGGED);
}
}
if (typeof value === 'object') {
const childResult = getModerationResult(value, currentPath);
if (childResult === ModerationResult.BLOCKED) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, childResult);
}
}
return result;
}
function clearFlagging(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(item => clearFlagging(item));
}
const newObj = {};
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key];
if (moderationFlags.includes(key) && value === true) {
newObj[key] = false;
logDebug(`Cleared flag '${key}'`);
}
else if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
let replaced = false;
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(value)) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced moderated message in '${key}' using pattern`);
replaced = true;
break;
}
}
if (!replaced && value.length < 70 && /(sorry|apologies|unable|cannot)/i.test(value.toLowerCase())) {
if (getModerationResult({[key]: value}) === ModerationResult.FLAGGED) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced heuristic moderated message in '${key}'`);
replaced = true;
}
}
if (!replaced) {
newObj[key] = value;
}
}
else if (typeof value === 'object') {
newObj[key] = clearFlagging(value);
}
else {
newObj[key] = value;
}
}
return newObj;
}
let uiContainer, toggleButton, debugButton, statusEl, logContainer;
function addLog(message) {
if (!logContainer) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.textContent = `[${timestamp}] ${message}`;
logEntry.style.cssText = CONFIG.styles.logEntry;
uiLogBuffer.push(logEntry);
if (uiLogBuffer.length > MAX_LOG_ENTRIES) {
const removed = uiLogBuffer.shift();
if (removed && removed.parentNode === logContainer) {
logContainer.removeChild(removed);
}
}
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(modResult, isRecovering = false) {
if (!statusEl) return;
let text = 'Status: ';
let color = CONFIG.styles.colors.safe;
if (isRecovering) {
text += 'Recovering...';
color = CONFIG.styles.colors.recovering;
} else if (modResult === ModerationResult.BLOCKED) {
text += 'Blocked (Recovered/Cleared)';
color = CONFIG.styles.colors.blocked;
} else if (modResult === ModerationResult.FLAGGED) {
text += 'Flagged (Cleared)';
color = CONFIG.styles.colors.flagged;
} else {
text += 'Safe';
color = CONFIG.styles.colors.safe;
}
statusEl.textContent = text;
statusEl.style.color = color;
}
function setupUI() {
uiContainer = document.createElement('div');
uiContainer.id = 'grok-demod-ui';
uiContainer.style.cssText = CONFIG.styles.uiContainer;
toggleButton = document.createElement('button');
debugButton = document.createElement('button');
statusEl = document.createElement('div');
logContainer = document.createElement('div');
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.title = 'Toggle DeMod functionality (ON = intercepting)';
toggleButton.style.cssText = CONFIG.styles.button;
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
toggleButton.onclick = (e) => {
demodEnabled = !demodEnabled;
setState(CONFIG.lsKeys.enabled, demodEnabled);
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
addLog(`DeMod ${demodEnabled ? 'Enabled' : 'Disabled'}.`);
console.log('[Grok DeMod] Interception is now', demodEnabled ? 'ACTIVE' : 'INACTIVE');
};
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.title = 'Toggle debug mode (logs verbose details to console)';
debugButton.style.cssText = CONFIG.styles.button;
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
debugButton.onclick = (e) => {
debug = !debug;
setState(CONFIG.lsKeys.debug, debug);
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
addLog(`Debug Mode ${debug ? 'Enabled' : 'Disabled'}.`);
logDebug(`Debug mode ${debug ? 'enabled' : 'disabled'}.`);
};
statusEl.id = 'grok-demod-status';
statusEl.style.cssText = CONFIG.styles.status;
updateStatus(ModerationResult.SAFE);
logContainer.id = 'grok-demod-log';
logContainer.style.cssText = CONFIG.styles.logContainer;
uiLogBuffer.forEach(entry => logContainer.appendChild(entry));
logContainer.scrollTop = logContainer.scrollHeight;
uiContainer.appendChild(toggleButton);
uiContainer.appendChild(debugButton);
uiContainer.appendChild(statusEl);
uiContainer.appendChild(logContainer);
document.body.appendChild(uiContainer);
addLog("Grok DeMod Initialized.");
if (debug) addLog("Debug mode is ON.");
}
async function redownloadLatestMessage() {
if (!currentConversationId) {
logDebug('Recovery skipped: Missing conversationId');
addLog('Recovery failed: No conversation ID.');
return null;
}
if (!initCache || !initCache.headers) {
logDebug('Recovery cache missing, attempting fresh fetch for headers...');
try {
const currentConvUrl = `/rest/app-chat/conversation/${currentConversationId}`;
const tempResp = await originalFetch(currentConvUrl, { method: 'GET', headers: {'Accept': 'application/json'} });
if (tempResp.ok) {
logDebug('Fresh header fetch successful (status OK).');
initCache = { headers: new Headers({'Accept': 'application/json'}), credentials: 'include' };
} else {
logDebug(`Fresh header fetch failed with status ${tempResp.status}. Recovery may fail.`);
addLog('Recovery failed: Cannot get request data.');
return null;
}
} catch (e) {
logError('Error during fresh header fetch:', e);
addLog('Recovery failed: Error getting request data.');
return null;
}
}
const url = `/rest/app-chat/conversation/${currentConversationId}`;
logDebug(`Attempting recovery fetch for conversation: ${currentConversationId}`);
addLog('Attempting content recovery...');
const headers = new Headers(initCache.headers);
if (!headers.has('Accept')) headers.set('Accept', 'application/json, text/plain, */*');
const requestOptions = {
method: 'GET',
headers: headers,
credentials: initCache.credentials || 'include',
};
try {
const response = await timeoutPromise(
CONFIG.recoveryTimeoutMs,
fetch(url, requestOptions),
'Recovery Fetch'
);
if (!response.ok) {
logError(`Recovery fetch failed with status ${response.status}: ${response.statusText}`);
addLog(`Recovery failed: HTTP ${response.status}`);
try {
const errorBody = await response.text();
logDebug('Recovery error body:', errorBody.substring(0, 500));
} catch (e) { }
return null;
}
const data = await response.json();
const messages = data?.messages;
if (!Array.isArray(messages) || messages.length === 0) {
logDebug('Recovery failed: No messages found in conversation data', data);
addLog('Recovery failed: No messages found.');
return null;
}
messages.sort((a, b) => {
const tsA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
const tsB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
return tsB - tsA;
});
const latestMessage = messages[0];
if (!latestMessage || typeof latestMessage.content !== 'string' || latestMessage.content.trim() === '') {
logDebug('Recovery failed: Latest message or its content is invalid/empty', latestMessage);
addLog('Recovery failed: Invalid latest message.');
return null;
}
logDebug('Recovery successful, latest content:', latestMessage.content.substring(0, 100) + '...');
addLog('Recovery seems successful.');
return { content: latestMessage.content };
} catch (e) {
logError('Recovery fetch/parse error:', e);
addLog(`Recovery error: ${e.message}`);
return null;
}
}
function extractConversationIdFromUrl(url) {
const match = url.match(/\/conversation\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
return match ? match[1] : null;
}
async function processPotentialModeration(json, source) {
const modResult = getModerationResult(json);
let finalJson = json;
if (modResult !== ModerationResult.SAFE) {
if (modResult === ModerationResult.BLOCKED) {
logDebug(`Blocked content detected from ${source}:`, JSON.stringify(json).substring(0, 200) + '...');
addLog(`Blocked content from ${source}.`);
updateStatus(modResult, true);
const recoveredData = await redownloadLatestMessage();
if (recoveredData && recoveredData.content) {
addLog(`Recovery successful (${source}).`);
logDebug(`Recovered content applied (${source})`);
let replaced = false;
const keysToTry = [...CONFIG.messageKeys, 'text', 'message'];
for (const key of keysToTry) {
if (typeof finalJson[key] === 'string') {
finalJson[key] = recoveredData.content;
logDebug(`Injected recovered content into key '${key}'`);
replaced = true;
break;
}
}
if (!replaced) {
logDebug("Could not find standard key to inject recovered content, adding as 'recovered_content'");
finalJson.recovered_content = recoveredData.content;
}
finalJson = clearFlagging(finalJson);
updateStatus(modResult, false);
} else {
addLog(`Recovery failed (${source}). Content may be lost.`);
logDebug(`Recovery failed (${source}), applying standard clearing.`);
finalJson = clearFlagging(json);
updateStatus(modResult, false);
}
} else {
logDebug(`Flagged content detected and cleared from ${source}.`);
addLog(`Flagged content cleared (${source}).`);
finalJson = clearFlagging(json);
updateStatus(modResult);
}
} else {
if (statusEl && !statusEl.textContent.includes('Blocked') && !statusEl.textContent.includes('Flagged') && !statusEl.textContent.includes('Recovering')) {
updateStatus(modResult);
} else if (statusEl && statusEl.textContent.includes('Recovering')) {
logDebug("Recovery attempt finished (next message safe). Resetting status.");
updateStatus(ModerationResult.SAFE);
}
}
return finalJson;
}
async function handleFetchResponse(original_response, url, requestArgs) {
const response = original_response.clone();
if (!response.ok) {
logDebug(`Fetch response not OK (${response.status}) for ${url}, skipping processing.`);
return original_response;
}
const contentType = response.headers.get('Content-Type')?.toLowerCase() || '';
logDebug(`Intercepted fetch response for ${url}, Content-Type: ${contentType}`);
const conversationGetMatch = url.match(/\/rest\/app-chat\/conversation\/([a-f0-9-]+)$/i);
if (conversationGetMatch && requestArgs?.method === 'GET') {
logDebug(`Caching GET request options for conversation ${conversationGetMatch[1]}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
if (!currentConversationId) {
currentConversationId = conversationGetMatch[1];
logDebug(`Conversation ID set from GET URL: ${currentConversationId}`);
}
}
if (!currentConversationId) {
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from other URL: ${currentConversationId}`);
}
}
if (contentType.includes('text/event-stream')) {
logDebug(`Processing SSE stream for ${url}`);
const reader = response.body.getReader();
const stream = new ReadableStream({
async start(controller) {
let buffer = '';
let currentEvent = { data: '', type: 'message', id: null };
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
logDebug("SSE stream ended, processing final buffer:", buffer);
if (buffer.startsWith('{') || buffer.startsWith('[')) {
try {
let json = JSON.parse(buffer);
json = await processPotentialModeration(json, 'SSE-Final');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logDebug("Error parsing final SSE buffer, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else {
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else if (currentEvent.data) {
logDebug("SSE stream ended after data field, processing event:", currentEvent.data.substring(0,100)+"...");
try {
let json = JSON.parse(currentEvent.data);
json = await processPotentialModeration(json, 'SSE-Event');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch (e) {
logDebug("Error parsing trailing SSE data, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
controller.close();
break;
}
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') {
if (currentEvent.data) {
logDebug("Processing SSE event data:", currentEvent.data.substring(0, 100) + '...');
if (currentEvent.data.startsWith('{') || currentEvent.data.startsWith('[')) {
try {
let json = JSON.parse(currentEvent.data);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from SSE data: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'SSE');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logError("SSE JSON parse error:", e, "Data:", currentEvent.data.substring(0,200)+"...");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
} else {
logDebug("SSE data is not JSON, forwarding as is.");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
currentEvent = { data: '', type: 'message', id: null };
} else if (line.startsWith('data:')) {
currentEvent.data += (currentEvent.data ? '\n' : '') + line.substring(5).trim();
} else if (line.startsWith('event:')) {
currentEvent.type = line.substring(6).trim();
} else if (line.startsWith('id:')) {
currentEvent.id = line.substring(3).trim();
} else if (line.startsWith(':')) {
} else {
logDebug("Unknown SSE line:", line);
}
}
}
} catch (e) {
logError('Error reading/processing SSE stream:', e);
controller.error(e);
} finally {
reader.releaseLock();
}
}
});
const newHeaders = new Headers(response.headers);
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
if (contentType.includes('application/json')) {
logDebug(`Processing JSON response for ${url}`);
try {
const text = await response.text();
let json = JSON.parse(text);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from JSON response: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'Fetch');
const newBody = JSON.stringify(json);
const newHeaders = new Headers(response.headers);
if (newHeaders.has('content-length')) {
newHeaders.set('content-length', encoder.encode(newBody).byteLength.toString());
}
return new Response(newBody, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
} catch (e) {
logError('Fetch JSON processing error:', e, 'URL:', url);
return original_response;
}
}
logDebug(`Non-SSE/JSON response for ${url}, skipping processing.`);
return original_response;
}
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function(input, init) {
if (!demodEnabled) {
return originalFetch.apply(this, arguments);
}
let url;
let requestArgs = init || {};
try {
url = (input instanceof Request) ? input.url : String(input);
} catch (e) {
logDebug('Invalid fetch input, passing through:', input, e);
return originalFetch.apply(this, arguments);
}
if (!url.includes('/rest/app-chat/')) {
return originalFetch.apply(this, arguments);
}
if (requestArgs.method === 'POST') {
logDebug(`Observing POST request: ${url}`);
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
if (!currentConversationId) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from POST URL: ${currentConversationId}`);
}
if (!initCache && requestArgs.headers) {
logDebug(`Caching headers from POST request to ${idFromUrl}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
}
}
return originalFetch.apply(this, arguments);
}
logDebug(`Intercepting fetch request: ${requestArgs.method || 'GET'} ${url}`);
try {
const original_response = await originalFetch.apply(this, arguments);
return await handleFetchResponse(original_response, url, requestArgs);
} catch (error) {
logError(`Fetch interception failed for ${url}:`, error);
throw error;
}
};
const OriginalWebSocket = unsafeWindow.WebSocket;
unsafeWindow.WebSocket = new Proxy(OriginalWebSocket, {
construct(target, args) {
const url = args[0];
logDebug('WebSocket connection attempt:', url);
const ws = new target(...args);
let originalOnMessageHandler = null;
Object.defineProperty(ws, 'onmessage', {
configurable: true,
enumerable: true,
get() {
return originalOnMessageHandler;
},
async set(handler) {
logDebug('WebSocket onmessage handler assigned');
originalOnMessageHandler = handler;
ws.onmessageinternal = async function(event) {
if (!demodEnabled || typeof event.data !== 'string' || !event.data.startsWith('{')) {
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (e) {
logError("Error in original WebSocket onmessage handler:", e);
}
}
return;
}
logDebug('Intercepting WebSocket message:', event.data.substring(0, 200) + '...');
try {
let json = JSON.parse(event.data);
if (json.conversation_id && json.conversation_id !== currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from WebSocket: ${currentConversationId}`);
}
const processedJson = await processPotentialModeration(json, 'WebSocket');
const newEvent = new MessageEvent('message', {
data: JSON.stringify(processedJson),
origin: event.origin,
lastEventId: event.lastEventId,
source: event.source,
ports: event.ports,
});
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, newEvent);
} catch (e) {
logError("Error calling original WebSocket onmessage handler after modification:", e);
}
} else {
logDebug("Original WebSocket onmessage handler not found when message received.");
}
} catch (e) {
logError('WebSocket processing error:', e, 'Data:', event.data.substring(0, 200) + '...');
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (eInner) {
logError("Error in original WebSocket onmessage handler (fallback path):", eInner);
}
}
}
};
ws.addEventListener('message', ws.onmessageinternal);
}
});
const wrapHandler = (eventName) => {
let originalHandler = null;
Object.defineProperty(ws, `on${eventName}`, {
configurable: true,
enumerable: true,
get() { return originalHandler; },
set(handler) {
logDebug(`WebSocket on${eventName} handler assigned`);
originalHandler = handler;
ws.addEventListener(eventName, (event) => {
if (eventName === 'message') return;
logDebug(`WebSocket event: ${eventName}`, event);
if (originalHandler) {
try {
originalHandler.call(ws, event);
} catch (e) {
logError(`Error in original WebSocket on${eventName} handler:`, e);
}
}
});
}
});
};
wrapHandler('close');
wrapHandler('error');
ws.addEventListener('open', () => logDebug('WebSocket opened:', url));
return ws;
}
});
if (window.location.hostname !== 'grok.com') {
console.log('[Grok DeMod] Script inactive: Intended for grok.com only. Current host:', window.location.hostname);
return;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupUI);
} else {
setupUI();
}
console.log('[Grok DeMod] Enhanced Script loaded. Interception is', demodEnabled ? 'ACTIVE' : 'INACTIVE', '. Debug is', debug ? 'ON' : 'OFF');
})();
// ==UserScript==
// @name Grok DeMod
// @license GPL-3.0-or-later
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Hides moderation results in Grok conversations, auto-recovers blocked messages.
// @author UniverseDev
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @match https://grok.com/*
// @grant unsafeWindow
// @downloadURL https://update.greasyfork.org/scripts/531147/Grok DeMod.user.js
// @updateURL https://update.greasyfork.org/scripts/531147/Grok DeMod.meta.js
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
defaultFlags: [
'isFlagged', 'isBlocked', 'moderationApplied', 'restricted'
],
messageKeys: ['message', 'content', 'text', 'error'],
moderationMessagePatterns: [
/this content has been moderated/i,
/sorry, i cannot assist/i,
/policy violation/i,
/blocked/i,
/moderated/i,
/restricted/i,
/content restricted/i,
/unable to process/i,
/cannot help/i,
/(sorry|apologies).*?(cannot|unable|help|assist)/i,
],
clearedMessageText: '[Content cleared by Grok DeMod]',
recoveryTimeoutMs: 5000,
lsKeys: {
enabled: 'GrokDeModEnabled',
debug: 'GrokDeModDebug',
flags: 'GrokDeModFlags',
},
styles: {
uiContainer: `
position: fixed;
bottom: 10px;
right: 10px;
z-index: 10000;
background: #2d2d2d;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
gap: 8px;
font-family: Arial, sans-serif;
color: #e0e0e0;
min-width: 170px;
`,
button: `
padding: 6px 12px;
border-radius: 5px;
border: none;
cursor: pointer;
color: #fff;
font-size: 13px;
transition: background-color 0.2s ease;
`,
status: `
padding: 5px;
font-size: 12px;
color: #a0a0a0;
text-align: center;
border-top: 1px solid #444;
margin-top: 5px;
min-height: 16px;
`,
logContainer: `
max-height: 100px;
overflow-y: auto;
font-size: 11px;
color: #c0c0c0;
background-color: #333;
padding: 5px;
border-radius: 4px;
line-height: 1.4;
margin-top: 5px;
`,
logEntry: `
padding-bottom: 3px;
border-bottom: 1px dashed #555;
margin-bottom: 3px;
word-break: break-word;
`,
colors: {
enabled: '#388E3C',
disabled: '#D32F2F',
debugEnabled: '#1976D2',
debugDisabled: '#555555',
safe: '#66ff66',
flagged: '#ffa500',
blocked: '#ff6666',
recovering: '#ffcc00'
}
}
};
let demodEnabled = getState(CONFIG.lsKeys.enabled, true);
let debug = getState(CONFIG.lsKeys.debug, false);
let moderationFlags = getState(CONFIG.lsKeys.flags, CONFIG.defaultFlags);
let initCache = null;
let currentConversationId = null;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const uiLogBuffer = [];
const MAX_LOG_ENTRIES = 50;
const ModerationResult = Object.freeze({
SAFE: 0,
FLAGGE
1,
BLOCKE
2,
});
function logDebug(...args) {
if (debug) {
console.log('[Grok DeMod]', ...args);
}
}
function logError(...args) {
console.error('[Grok DeMod]', ...args);
}
function getState(key, defaultValue) {
try {
const value = localStorage.getItem(key);
if (value === null) return defaultValue;
if (value === 'true') return true;
if (value === 'false') return false;
return JSON.parse(value);
} catch (e) {
logError(`Error reading ${key} from localStorage:`, e);
return defaultValue;
}
}
function setState(key, value) {
try {
const valueToStore = typeof value === 'boolean' ? value.toString() : JSON.stringify(value);
localStorage.setItem(key, valueToStore);
} catch (e) {
logError(`Error writing ${key} to localStorage:`, e);
}
}
function timeoutPromise(ms, promise, description = 'Promise') {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
logDebug(`${description} timed out after ${ms}ms`);
reject(new Error(`Timeout (${description})`));
}, ms);
promise.then(
(value) => { clearTimeout(timer); resolve(value); },
(error) => { clearTimeout(timer); reject(error); }
);
});
}
function getModerationResult(obj, path = '') {
if (typeof obj !== 'object' || obj === null) return ModerationResult.SAFE;
let result = ModerationResult.SAFE;
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const currentPath = path ? `${path}.${key}` : key;
const value = obj[key];
if (key === 'isBlocked' && value === true) {
logDebug(`Blocked detected via flag '${currentPath}'`);
return ModerationResult.BLOCKED;
}
if (moderationFlags.includes(key) && value === true) {
logDebug(`Flagged detected via flag '${currentPath}'`);
result = Math.max(result, ModerationResult.FLAGGED);
}
if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
const content = value.toLowerCase();
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(content)) {
logDebug(`Moderation pattern matched in '${currentPath}': "${content.substring(0, 50)}..."`);
if (/blocked|moderated|restricted/i.test(pattern.source)) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, ModerationResult.FLAGGED);
break;
}
}
if (result === ModerationResult.SAFE && content.length < 70 && /(sorry|apologies|unable|cannot)/i.test(content)) {
logDebug(`Heuristic moderation detected in '${currentPath}': "${content.substring(0, 50)}..."`);
result = Math.max(result, ModerationResult.FLAGGED);
}
}
if (typeof value === 'object') {
const childResult = getModerationResult(value, currentPath);
if (childResult === ModerationResult.BLOCKED) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, childResult);
}
}
return result;
}
function clearFlagging(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(item => clearFlagging(item));
}
const newObj = {};
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key];
if (moderationFlags.includes(key) && value === true) {
newObj[key] = false;
logDebug(`Cleared flag '${key}'`);
}
else if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
let replaced = false;
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(value)) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced moderated message in '${key}' using pattern`);
replaced = true;
break;
}
}
if (!replaced && value.length < 70 && /(sorry|apologies|unable|cannot)/i.test(value.toLowerCase())) {
if (getModerationResult({[key]: value}) === ModerationResult.FLAGGED) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced heuristic moderated message in '${key}'`);
replaced = true;
}
}
if (!replaced) {
newObj[key] = value;
}
}
else if (typeof value === 'object') {
newObj[key] = clearFlagging(value);
}
else {
newObj[key] = value;
}
}
return newObj;
}
let uiContainer, toggleButton, debugButton, statusEl, logContainer;
function addLog(message) {
if (!logContainer) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.textContent = `[${timestamp}] ${message}`;
logEntry.style.cssText = CONFIG.styles.logEntry;
uiLogBuffer.push(logEntry);
if (uiLogBuffer.length > MAX_LOG_ENTRIES) {
const removed = uiLogBuffer.shift();
if (removed && removed.parentNode === logContainer) {
logContainer.removeChild(removed);
}
}
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(modResult, isRecovering = false) {
if (!statusEl) return;
let text = 'Status: ';
let color = CONFIG.styles.colors.safe;
if (isRecovering) {
text += 'Recovering...';
color = CONFIG.styles.colors.recovering;
} else if (modResult === ModerationResult.BLOCKED) {
text += 'Blocked (Recovered/Cleared)';
color = CONFIG.styles.colors.blocked;
} else if (modResult === ModerationResult.FLAGGED) {
text += 'Flagged (Cleared)';
color = CONFIG.styles.colors.flagged;
} else {
text += 'Safe';
color = CONFIG.styles.colors.safe;
}
statusEl.textContent = text;
statusEl.style.color = color;
}
function setupUI() {
uiContainer = document.createElement('div');
uiContainer.id = 'grok-demod-ui';
uiContainer.style.cssText = CONFIG.styles.uiContainer;
toggleButton = document.createElement('button');
debugButton = document.createElement('button');
statusEl = document.createElement('div');
logContainer = document.createElement('div');
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.title = 'Toggle DeMod functionality (ON = intercepting)';
toggleButton.style.cssText = CONFIG.styles.button;
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
toggleButton.onclick = (e) => {
demodEnabled = !demodEnabled;
setState(CONFIG.lsKeys.enabled, demodEnabled);
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
addLog(`DeMod ${demodEnabled ? 'Enabled' : 'Disabled'}.`);
console.log('[Grok DeMod] Interception is now', demodEnabled ? 'ACTIVE' : 'INACTIVE');
};
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.title = 'Toggle debug mode (logs verbose details to console)';
debugButton.style.cssText = CONFIG.styles.button;
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
debugButton.onclick = (e) => {
debug = !debug;
setState(CONFIG.lsKeys.debug, debug);
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
addLog(`Debug Mode ${debug ? 'Enabled' : 'Disabled'}.`);
logDebug(`Debug mode ${debug ? 'enabled' : 'disabled'}.`);
};
statusEl.id = 'grok-demod-status';
statusEl.style.cssText = CONFIG.styles.status;
updateStatus(ModerationResult.SAFE);
logContainer.id = 'grok-demod-log';
logContainer.style.cssText = CONFIG.styles.logContainer;
uiLogBuffer.forEach(entry => logContainer.appendChild(entry));
logContainer.scrollTop = logContainer.scrollHeight;
uiContainer.appendChild(toggleButton);
uiContainer.appendChild(debugButton);
uiContainer.appendChild(statusEl);
uiContainer.appendChild(logContainer);
document.body.appendChild(uiContainer);
addLog("Grok DeMod Initialized.");
if (debug) addLog("Debug mode is ON.");
}
async function redownloadLatestMessage() {
if (!currentConversationId) {
logDebug('Recovery skipped: Missing conversationId');
addLog('Recovery failed: No conversation ID.');
return null;
}
if (!initCache || !initCache.headers) {
logDebug('Recovery cache missing, attempting fresh fetch for headers...');
try {
const currentConvUrl = `/rest/app-chat/conversation/${currentConversationId}`;
const tempResp = await originalFetch(currentConvUrl, { method: 'GET', headers: {'Accept': 'application/json'} });
if (tempResp.ok) {
logDebug('Fresh header fetch successful (status OK).');
initCache = { headers: new Headers({'Accept': 'application/json'}), credentials: 'include' };
} else {
logDebug(`Fresh header fetch failed with status ${tempResp.status}. Recovery may fail.`);
addLog('Recovery failed: Cannot get request data.');
return null;
}
} catch (e) {
logError('Error during fresh header fetch:', e);
addLog('Recovery failed: Error getting request data.');
return null;
}
}
const url = `/rest/app-chat/conversation/${currentConversationId}`;
logDebug(`Attempting recovery fetch for conversation: ${currentConversationId}`);
addLog('Attempting content recovery...');
const headers = new Headers(initCache.headers);
if (!headers.has('Accept')) headers.set('Accept', 'application/json, text/plain, */*');
const requestOptions = {
method: 'GET',
headers: headers,
credentials: initCache.credentials || 'include',
};
try {
const response = await timeoutPromise(
CONFIG.recoveryTimeoutMs,
fetch(url, requestOptions),
'Recovery Fetch'
);
if (!response.ok) {
logError(`Recovery fetch failed with status ${response.status}: ${response.statusText}`);
addLog(`Recovery failed: HTTP ${response.status}`);
try {
const errorBody = await response.text();
logDebug('Recovery error body:', errorBody.substring(0, 500));
} catch (e) { }
return null;
}
const data = await response.json();
const messages = data?.messages;
if (!Array.isArray(messages) || messages.length === 0) {
logDebug('Recovery failed: No messages found in conversation data', data);
addLog('Recovery failed: No messages found.');
return null;
}
messages.sort((a, b) => {
const tsA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
const tsB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
return tsB - tsA;
});
const latestMessage = messages[0];
if (!latestMessage || typeof latestMessage.content !== 'string' || latestMessage.content.trim() === '') {
logDebug('Recovery failed: Latest message or its content is invalid/empty', latestMessage);
addLog('Recovery failed: Invalid latest message.');
return null;
}
logDebug('Recovery successful, latest content:', latestMessage.content.substring(0, 100) + '...');
addLog('Recovery seems successful.');
return { content: latestMessage.content };
} catch (e) {
logError('Recovery fetch/parse error:', e);
addLog(`Recovery error: ${e.message}`);
return null;
}
}
function extractConversationIdFromUrl(url) {
const match = url.match(/\/conversation\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
return match ? match[1] : null;
}
async function processPotentialModeration(json, source) {
const modResult = getModerationResult(json);
let finalJson = json;
if (modResult !== ModerationResult.SAFE) {
if (modResult === ModerationResult.BLOCKED) {
logDebug(`Blocked content detected from ${source}:`, JSON.stringify(json).substring(0, 200) + '...');
addLog(`Blocked content from ${source}.`);
updateStatus(modResult, true);
const recoveredData = await redownloadLatestMessage();
if (recoveredData && recoveredData.content) {
addLog(`Recovery successful (${source}).`);
logDebug(`Recovered content applied (${source})`);
let replaced = false;
const keysToTry = [...CONFIG.messageKeys, 'text', 'message'];
for (const key of keysToTry) {
if (typeof finalJson[key] === 'string') {
finalJson[key] = recoveredData.content;
logDebug(`Injected recovered content into key '${key}'`);
replaced = true;
break;
}
}
if (!replaced) {
logDebug("Could not find standard key to inject recovered content, adding as 'recovered_content'");
finalJson.recovered_content = recoveredData.content;
}
finalJson = clearFlagging(finalJson);
updateStatus(modResult, false);
} else {
addLog(`Recovery failed (${source}). Content may be lost.`);
logDebug(`Recovery failed (${source}), applying standard clearing.`);
finalJson = clearFlagging(json);
updateStatus(modResult, false);
}
} else {
logDebug(`Flagged content detected and cleared from ${source}.`);
addLog(`Flagged content cleared (${source}).`);
finalJson = clearFlagging(json);
updateStatus(modResult);
}
} else {
if (statusEl && !statusEl.textContent.includes('Blocked') && !statusEl.textContent.includes('Flagged') && !statusEl.textContent.includes('Recovering')) {
updateStatus(modResult);
} else if (statusEl && statusEl.textContent.includes('Recovering')) {
logDebug("Recovery attempt finished (next message safe). Resetting status.");
updateStatus(ModerationResult.SAFE);
}
}
return finalJson;
}
async function handleFetchResponse(original_response, url, requestArgs) {
const response = original_response.clone();
if (!response.ok) {
logDebug(`Fetch response not OK (${response.status}) for ${url}, skipping processing.`);
return original_response;
}
const contentType = response.headers.get('Content-Type')?.toLowerCase() || '';
logDebug(`Intercepted fetch response for ${url}, Content-Type: ${contentType}`);
const conversationGetMatch = url.match(/\/rest\/app-chat\/conversation\/([a-f0-9-]+)$/i);
if (conversationGetMatch && requestArgs?.method === 'GET') {
logDebug(`Caching GET request options for conversation ${conversationGetMatch[1]}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
if (!currentConversationId) {
currentConversationId = conversationGetMatch[1];
logDebug(`Conversation ID set from GET URL: ${currentConversationId}`);
}
}
if (!currentConversationId) {
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from other URL: ${currentConversationId}`);
}
}
if (contentType.includes('text/event-stream')) {
logDebug(`Processing SSE stream for ${url}`);
const reader = response.body.getReader();
const stream = new ReadableStream({
async start(controller) {
let buffer = '';
let currentEvent = { data: '', type: 'message', id: null };
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
logDebug("SSE stream ended, processing final buffer:", buffer);
if (buffer.startsWith('{') || buffer.startsWith('[')) {
try {
let json = JSON.parse(buffer);
json = await processPotentialModeration(json, 'SSE-Final');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logDebug("Error parsing final SSE buffer, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else {
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else if (currentEvent.data) {
logDebug("SSE stream ended after data field, processing event:", currentEvent.data.substring(0,100)+"...");
try {
let json = JSON.parse(currentEvent.data);
json = await processPotentialModeration(json, 'SSE-Event');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch (e) {
logDebug("Error parsing trailing SSE data, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
controller.close();
break;
}
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') {
if (currentEvent.data) {
logDebug("Processing SSE event data:", currentEvent.data.substring(0, 100) + '...');
if (currentEvent.data.startsWith('{') || currentEvent.data.startsWith('[')) {
try {
let json = JSON.parse(currentEvent.data);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from SSE data: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'SSE');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logError("SSE JSON parse error:", e, "Data:", currentEvent.data.substring(0,200)+"...");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
} else {
logDebug("SSE data is not JSON, forwarding as is.");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
currentEvent = { data: '', type: 'message', id: null };
} else if (line.startsWith('data:')) {
currentEvent.data += (currentEvent.data ? '\n' : '') + line.substring(5).trim();
} else if (line.startsWith('event:')) {
currentEvent.type = line.substring(6).trim();
} else if (line.startsWith('id:')) {
currentEvent.id = line.substring(3).trim();
} else if (line.startsWith(':')) {
} else {
logDebug("Unknown SSE line:", line);
}
}
}
} catch (e) {
logError('Error reading/processing SSE stream:', e);
controller.error(e);
} finally {
reader.releaseLock();
}
}
});
const newHeaders = new Headers(response.headers);
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
if (contentType.includes('application/json')) {
logDebug(`Processing JSON response for ${url}`);
try {
const text = await response.text();
let json = JSON.parse(text);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from JSON response: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'Fetch');
const newBody = JSON.stringify(json);
const newHeaders = new Headers(response.headers);
if (newHeaders.has('content-length')) {
newHeaders.set('content-length', encoder.encode(newBody).byteLength.toString());
}
return new Response(newBody, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
} catch (e) {
logError('Fetch JSON processing error:', e, 'URL:', url);
return original_response;
}
}
logDebug(`Non-SSE/JSON response for ${url}, skipping processing.`);
return original_response;
}
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function(input, init) {
if (!demodEnabled) {
return originalFetch.apply(this, arguments);
}
let url;
let requestArgs = init || {};
try {
url = (input instanceof Request) ? input.url : String(input);
} catch (e) {
logDebug('Invalid fetch input, passing through:', input, e);
return originalFetch.apply(this, arguments);
}
if (!url.includes('/rest/app-chat/')) {
return originalFetch.apply(this, arguments);
}
if (requestArgs.method === 'POST') {
logDebug(`Observing POST request: ${url}`);
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
if (!currentConversationId) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from POST URL: ${currentConversationId}`);
}
if (!initCache && requestArgs.headers) {
logDebug(`Caching headers from POST request to ${idFromUrl}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
}
}
return originalFetch.apply(this, arguments);
}
logDebug(`Intercepting fetch request: ${requestArgs.method || 'GET'} ${url}`);
try {
const original_response = await originalFetch.apply(this, arguments);
return await handleFetchResponse(original_response, url, requestArgs);
} catch (error) {
logError(`Fetch interception failed for ${url}:`, error);
throw error;
}
};
const OriginalWebSocket = unsafeWindow.WebSocket;
unsafeWindow.WebSocket = new Proxy(OriginalWebSocket, {
construct(target, args) {
const url = args[0];
logDebug('WebSocket connection attempt:', url);
const ws = new target(...args);
let originalOnMessageHandler = null;
Object.defineProperty(ws, 'onmessage', {
configurable: true,
enumerable: true,
get() {
return originalOnMessageHandler;
},
async set(handler) {
logDebug('WebSocket onmessage handler assigned');
originalOnMessageHandler = handler;
ws.onmessageinternal = async function(event) {
if (!demodEnabled || typeof event.data !== 'string' || !event.data.startsWith('{')) {
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (e) {
logError("Error in original WebSocket onmessage handler:", e);
}
}
return;
}
logDebug('Intercepting WebSocket message:', event.data.substring(0, 200) + '...');
try {
let json = JSON.parse(event.data);
if (json.conversation_id && json.conversation_id !== currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from WebSocket: ${currentConversationId}`);
}
const processedJson = await processPotentialModeration(json, 'WebSocket');
const newEvent = new MessageEvent('message', {
data: JSON.stringify(processedJson),
origin: event.origin,
lastEventId: event.lastEventId,
source: event.source,
ports: event.ports,
});
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, newEvent);
} catch (e) {
logError("Error calling original WebSocket onmessage handler after modification:", e);
}
} else {
logDebug("Original WebSocket onmessage handler not found when message received.");
}
} catch (e) {
logError('WebSocket processing error:', e, 'Data:', event.data.substring(0, 200) + '...');
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (eInner) {
logError("Error in original WebSocket onmessage handler (fallback path):", eInner);
}
}
}
};
ws.addEventListener('message', ws.onmessageinternal);
}
});
const wrapHandler = (eventName) => {
let originalHandler = null;
Object.defineProperty(ws, `on${eventName}`, {
configurable: true,
enumerable: true,
get() { return originalHandler; },
set(handler) {
logDebug(`WebSocket on${eventName} handler assigned`);
originalHandler = handler;
ws.addEventListener(eventName, (event) => {
if (eventName === 'message') return;
logDebug(`WebSocket event: ${eventName}`, event);
if (originalHandler) {
try {
originalHandler.call(ws, event);
} catch (e) {
logError(`Error in original WebSocket on${eventName} handler:`, e);
}
}
});
}
});
};
wrapHandler('close');
wrapHandler('error');
ws.addEventListener('open', () => logDebug('WebSocket opened:', url));
return ws;
}
});
if (window.location.hostname !== 'grok.com') {
console.log('[Grok DeMod] Script inactive: Intended for grok.com only. Current host:', window.location.hostname);
return;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupUI);
} else {
setupUI();
}
console.log('[Grok DeMod] Enhanced Script loaded. Interception is', demodEnabled ? 'ACTIVE' : 'INACTIVE', '. Debug is', debug ? 'ON' : 'OFF');
})();// ==UserScript==
// @name Grok DeMod
// @license GPL-3.0-or-later
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Hides moderation results in Grok conversations, auto-recovers blocked messages.
// @author UniverseDev
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @match https://grok.com/*
// @grant unsafeWindow
// @downloadURL https://update.greasyfork.org/scripts/531147/Grok DeMod.user.js
// @updateURL https://update.greasyfork.org/scripts/531147/Grok DeMod.meta.js
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
defaultFlags: [
'isFlagged', 'isBlocked', 'moderationApplied', 'restricted'
],
messageKeys: ['message', 'content', 'text', 'error'],
moderationMessagePatterns: [
/this content has been moderated/i,
/sorry, i cannot assist/i,
/policy violation/i,
/blocked/i,
/moderated/i,
/restricted/i,
/content restricted/i,
/unable to process/i,
/cannot help/i,
/(sorry|apologies).*?(cannot|unable|help|assist)/i,
],
clearedMessageText: '[Content cleared by Grok DeMod]',
recoveryTimeoutMs: 5000,
lsKeys: {
enabled: 'GrokDeModEnabled',
debug: 'GrokDeModDebug',
flags: 'GrokDeModFlags',
},
styles: {
uiContainer: `
position: fixed;
bottom: 10px;
right: 10px;
z-index: 10000;
background: #2d2d2d;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
gap: 8px;
font-family: Arial, sans-serif;
color: #e0e0e0;
min-width: 170px;
`,
button: `
padding: 6px 12px;
border-radius: 5px;
border: none;
cursor: pointer;
color: #fff;
font-size: 13px;
transition: background-color 0.2s ease;
`,
status: `
padding: 5px;
font-size: 12px;
color: #a0a0a0;
text-align: center;
border-top: 1px solid #444;
margin-top: 5px;
min-height: 16px;
`,
logContainer: `
max-height: 100px;
overflow-y: auto;
font-size: 11px;
color: #c0c0c0;
background-color: #333;
padding: 5px;
border-radius: 4px;
line-height: 1.4;
margin-top: 5px;
`,
logEntry: `
padding-bottom: 3px;
border-bottom: 1px dashed #555;
margin-bottom: 3px;
word-break: break-word;
`,
colors: {
enabled: '#388E3C',
disabled: '#D32F2F',
debugEnabled: '#1976D2',
debugDisabled: '#555555',
safe: '#66ff66',
flagged: '#ffa500',
blocked: '#ff6666',
recovering: '#ffcc00'
}
}
};
let demodEnabled = getState(CONFIG.lsKeys.enabled, true);
let debug = getState(CONFIG.lsKeys.debug, false);
let moderationFlags = getState(CONFIG.lsKeys.flags, CONFIG.defaultFlags);
let initCache = null;
let currentConversationId = null;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const uiLogBuffer = [];
const MAX_LOG_ENTRIES = 50;
const ModerationResult = Object.freeze({
SAFE: 0,
FLAGGE
1,
BLOCKE
2,
});
function logDebug(...args) {
if (debug) {
console.log('[Grok DeMod]', ...args);
}
}
function logError(...args) {
console.error('[Grok DeMod]', ...args);
}
function getState(key, defaultValue) {
try {
const value = localStorage.getItem(key);
if (value === null) return defaultValue;
if (value === 'true') return true;
if (value === 'false') return false;
return JSON.parse(value);
} catch (e) {
logError(`Error reading ${key} from localStorage:`, e);
return defaultValue;
}
}
function setState(key, value) {
try {
const valueToStore = typeof value === 'boolean' ? value.toString() : JSON.stringify(value);
localStorage.setItem(key, valueToStore);
} catch (e) {
logError(`Error writing ${key} to localStorage:`, e);
}
}
function timeoutPromise(ms, promise, description = 'Promise') {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
logDebug(`${description} timed out after ${ms}ms`);
reject(new Error(`Timeout (${description})`));
}, ms);
promise.then(
(value) => { clearTimeout(timer); resolve(value); },
(error) => { clearTimeout(timer); reject(error); }
);
});
}
function getModerationResult(obj, path = '') {
if (typeof obj !== 'object' || obj === null) return ModerationResult.SAFE;
let result = ModerationResult.SAFE;
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const currentPath = path ? `${path}.${key}` : key;
const value = obj[key];
if (key === 'isBlocked' && value === true) {
logDebug(`Blocked detected via flag '${currentPath}'`);
return ModerationResult.BLOCKED;
}
if (moderationFlags.includes(key) && value === true) {
logDebug(`Flagged detected via flag '${currentPath}'`);
result = Math.max(result, ModerationResult.FLAGGED);
}
if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
const content = value.toLowerCase();
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(content)) {
logDebug(`Moderation pattern matched in '${currentPath}': "${content.substring(0, 50)}..."`);
if (/blocked|moderated|restricted/i.test(pattern.source)) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, ModerationResult.FLAGGED);
break;
}
}
if (result === ModerationResult.SAFE && content.length < 70 && /(sorry|apologies|unable|cannot)/i.test(content)) {
logDebug(`Heuristic moderation detected in '${currentPath}': "${content.substring(0, 50)}..."`);
result = Math.max(result, ModerationResult.FLAGGED);
}
}
if (typeof value === 'object') {
const childResult = getModerationResult(value, currentPath);
if (childResult === ModerationResult.BLOCKED) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, childResult);
}
}
return result;
}
function clearFlagging(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(item => clearFlagging(item));
}
const newObj = {};
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key];
if (moderationFlags.includes(key) && value === true) {
newObj[key] = false;
logDebug(`Cleared flag '${key}'`);
}
else if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
let replaced = false;
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(value)) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced moderated message in '${key}' using pattern`);
replaced = true;
break;
}
}
if (!replaced && value.length < 70 && /(sorry|apologies|unable|cannot)/i.test(value.toLowerCase())) {
if (getModerationResult({[key]: value}) === ModerationResult.FLAGGED) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced heuristic moderated message in '${key}'`);
replaced = true;
}
}
if (!replaced) {
newObj[key] = value;
}
}
else if (typeof value === 'object') {
newObj[key] = clearFlagging(value);
}
else {
newObj[key] = value;
}
}
return newObj;
}
let uiContainer, toggleButton, debugButton, statusEl, logContainer;
function addLog(message) {
if (!logContainer) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.textContent = `[${timestamp}] ${message}`;
logEntry.style.cssText = CONFIG.styles.logEntry;
uiLogBuffer.push(logEntry);
if (uiLogBuffer.length > MAX_LOG_ENTRIES) {
const removed = uiLogBuffer.shift();
if (removed && removed.parentNode === logContainer) {
logContainer.removeChild(removed);
}
}
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(modResult, isRecovering = false) {
if (!statusEl) return;
let text = 'Status: ';
let color = CONFIG.styles.colors.safe;
if (isRecovering) {
text += 'Recovering...';
color = CONFIG.styles.colors.recovering;
} else if (modResult === ModerationResult.BLOCKED) {
text += 'Blocked (Recovered/Cleared)';
color = CONFIG.styles.colors.blocked;
} else if (modResult === ModerationResult.FLAGGED) {
text += 'Flagged (Cleared)';
color = CONFIG.styles.colors.flagged;
} else {
text += 'Safe';
color = CONFIG.styles.colors.safe;
}
statusEl.textContent = text;
statusEl.style.color = color;
}
function setupUI() {
uiContainer = document.createElement('div');
uiContainer.id = 'grok-demod-ui';
uiContainer.style.cssText = CONFIG.styles.uiContainer;
toggleButton = document.createElement('button');
debugButton = document.createElement('button');
statusEl = document.createElement('div');
logContainer = document.createElement('div');
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.title = 'Toggle DeMod functionality (ON = intercepting)';
toggleButton.style.cssText = CONFIG.styles.button;
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
toggleButton.onclick = (e) => {
demodEnabled = !demodEnabled;
setState(CONFIG.lsKeys.enabled, demodEnabled);
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
addLog(`DeMod ${demodEnabled ? 'Enabled' : 'Disabled'}.`);
console.log('[Grok DeMod] Interception is now', demodEnabled ? 'ACTIVE' : 'INACTIVE');
};
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.title = 'Toggle debug mode (logs verbose details to console)';
debugButton.style.cssText = CONFIG.styles.button;
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
debugButton.onclick = (e) => {
debug = !debug;
setState(CONFIG.lsKeys.debug, debug);
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
addLog(`Debug Mode ${debug ? 'Enabled' : 'Disabled'}.`);
logDebug(`Debug mode ${debug ? 'enabled' : 'disabled'}.`);
};
statusEl.id = 'grok-demod-status';
statusEl.style.cssText = CONFIG.styles.status;
updateStatus(ModerationResult.SAFE);
logContainer.id = 'grok-demod-log';
logContainer.style.cssText = CONFIG.styles.logContainer;
uiLogBuffer.forEach(entry => logContainer.appendChild(entry));
logContainer.scrollTop = logContainer.scrollHeight;
uiContainer.appendChild(toggleButton);
uiContainer.appendChild(debugButton);
uiContainer.appendChild(statusEl);
uiContainer.appendChild(logContainer);
document.body.appendChild(uiContainer);
addLog("Grok DeMod Initialized.");
if (debug) addLog("Debug mode is ON.");
}
async function redownloadLatestMessage() {
if (!currentConversationId) {
logDebug('Recovery skipped: Missing conversationId');
addLog('Recovery failed: No conversation ID.');
return null;
}
if (!initCache || !initCache.headers) {
logDebug('Recovery cache missing, attempting fresh fetch for headers...');
try {
const currentConvUrl = `/rest/app-chat/conversation/${currentConversationId}`;
const tempResp = await originalFetch(currentConvUrl, { method: 'GET', headers: {'Accept': 'application/json'} });
if (tempResp.ok) {
logDebug('Fresh header fetch successful (status OK).');
initCache = { headers: new Headers({'Accept': 'application/json'}), credentials: 'include' };
} else {
logDebug(`Fresh header fetch failed with status ${tempResp.status}. Recovery may fail.`);
addLog('Recovery failed: Cannot get request data.');
return null;
}
} catch (e) {
logError('Error during fresh header fetch:', e);
addLog('Recovery failed: Error getting request data.');
return null;
}
}
const url = `/rest/app-chat/conversation/${currentConversationId}`;
logDebug(`Attempting recovery fetch for conversation: ${currentConversationId}`);
addLog('Attempting content recovery...');
const headers = new Headers(initCache.headers);
if (!headers.has('Accept')) headers.set('Accept', 'application/json, text/plain, */*');
const requestOptions = {
method: 'GET',
headers: headers,
credentials: initCache.credentials || 'include',
};
try {
const response = await timeoutPromise(
CONFIG.recoveryTimeoutMs,
fetch(url, requestOptions),
'Recovery Fetch'
);
if (!response.ok) {
logError(`Recovery fetch failed with status ${response.status}: ${response.statusText}`);
addLog(`Recovery failed: HTTP ${response.status}`);
try {
const errorBody = await response.text();
logDebug('Recovery error body:', errorBody.substring(0, 500));
} catch (e) { }
return null;
}
const data = await response.json();
const messages = data?.messages;
if (!Array.isArray(messages) || messages.length === 0) {
logDebug('Recovery failed: No messages found in conversation data', data);
addLog('Recovery failed: No messages found.');
return null;
}
messages.sort((a, b) => {
const tsA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
const tsB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
return tsB - tsA;
});
const latestMessage = messages[0];
if (!latestMessage || typeof latestMessage.content !== 'string' || latestMessage.content.trim() === '') {
logDebug('Recovery failed: Latest message or its content is invalid/empty', latestMessage);
addLog('Recovery failed: Invalid latest message.');
return null;
}
logDebug('Recovery successful, latest content:', latestMessage.content.substring(0, 100) + '...');
addLog('Recovery seems successful.');
return { content: latestMessage.content };
} catch (e) {
logError('Recovery fetch/parse error:', e);
addLog(`Recovery error: ${e.message}`);
return null;
}
}
function extractConversationIdFromUrl(url) {
const match = url.match(/\/conversation\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
return match ? match[1] : null;
}
async function processPotentialModeration(json, source) {
const modResult = getModerationResult(json);
let finalJson = json;
if (modResult !== ModerationResult.SAFE) {
if (modResult === ModerationResult.BLOCKED) {
logDebug(`Blocked content detected from ${source}:`, JSON.stringify(json).substring(0, 200) + '...');
addLog(`Blocked content from ${source}.`);
updateStatus(modResult, true);
const recoveredData = await redownloadLatestMessage();
if (recoveredData && recoveredData.content) {
addLog(`Recovery successful (${source}).`);
logDebug(`Recovered content applied (${source})`);
let replaced = false;
const keysToTry = [...CONFIG.messageKeys, 'text', 'message'];
for (const key of keysToTry) {
if (typeof finalJson[key] === 'string') {
finalJson[key] = recoveredData.content;
logDebug(`Injected recovered content into key '${key}'`);
replaced = true;
break;
}
}
if (!replaced) {
logDebug("Could not find standard key to inject recovered content, adding as 'recovered_content'");
finalJson.recovered_content = recoveredData.content;
}
finalJson = clearFlagging(finalJson);
updateStatus(modResult, false);
} else {
addLog(`Recovery failed (${source}). Content may be lost.`);
logDebug(`Recovery failed (${source}), applying standard clearing.`);
finalJson = clearFlagging(json);
updateStatus(modResult, false);
}
} else {
logDebug(`Flagged content detected and cleared from ${source}.`);
addLog(`Flagged content cleared (${source}).`);
finalJson = clearFlagging(json);
updateStatus(modResult);
}
} else {
if (statusEl && !statusEl.textContent.includes('Blocked') && !statusEl.textContent.includes('Flagged') && !statusEl.textContent.includes('Recovering')) {
updateStatus(modResult);
} else if (statusEl && statusEl.textContent.includes('Recovering')) {
logDebug("Recovery attempt finished (next message safe). Resetting status.");
updateStatus(ModerationResult.SAFE);
}
}
return finalJson;
}
async function handleFetchResponse(original_response, url, requestArgs) {
const response = original_response.clone();
if (!response.ok) {
logDebug(`Fetch response not OK (${response.status}) for ${url}, skipping processing.`);
return original_response;
}
const contentType = response.headers.get('Content-Type')?.toLowerCase() || '';
logDebug(`Intercepted fetch response for ${url}, Content-Type: ${contentType}`);
const conversationGetMatch = url.match(/\/rest\/app-chat\/conversation\/([a-f0-9-]+)$/i);
if (conversationGetMatch && requestArgs?.method === 'GET') {
logDebug(`Caching GET request options for conversation ${conversationGetMatch[1]}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
if (!currentConversationId) {
currentConversationId = conversationGetMatch[1];
logDebug(`Conversation ID set from GET URL: ${currentConversationId}`);
}
}
if (!currentConversationId) {
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from other URL: ${currentConversationId}`);
}
}
if (contentType.includes('text/event-stream')) {
logDebug(`Processing SSE stream for ${url}`);
const reader = response.body.getReader();
const stream = new ReadableStream({
async start(controller) {
let buffer = '';
let currentEvent = { data: '', type: 'message', id: null };
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
logDebug("SSE stream ended, processing final buffer:", buffer);
if (buffer.startsWith('{') || buffer.startsWith('[')) {
try {
let json = JSON.parse(buffer);
json = await processPotentialModeration(json, 'SSE-Final');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logDebug("Error parsing final SSE buffer, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else {
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else if (currentEvent.data) {
logDebug("SSE stream ended after data field, processing event:", currentEvent.data.substring(0,100)+"...");
try {
let json = JSON.parse(currentEvent.data);
json = await processPotentialModeration(json, 'SSE-Event');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch (e) {
logDebug("Error parsing trailing SSE data, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
controller.close();
break;
}
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') {
if (currentEvent.data) {
logDebug("Processing SSE event data:", currentEvent.data.substring(0, 100) + '...');
if (currentEvent.data.startsWith('{') || currentEvent.data.startsWith('[')) {
try {
let json = JSON.parse(currentEvent.data);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from SSE data: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'SSE');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logError("SSE JSON parse error:", e, "Data:", currentEvent.data.substring(0,200)+"...");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
} else {
logDebug("SSE data is not JSON, forwarding as is.");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
currentEvent = { data: '', type: 'message', id: null };
} else if (line.startsWith('data:')) {
currentEvent.data += (currentEvent.data ? '\n' : '') + line.substring(5).trim();
} else if (line.startsWith('event:')) {
currentEvent.type = line.substring(6).trim();
} else if (line.startsWith('id:')) {
currentEvent.id = line.substring(3).trim();
} else if (line.startsWith(':')) {
} else {
logDebug("Unknown SSE line:", line);
}
}
}
} catch (e) {
logError('Error reading/processing SSE stream:', e);
controller.error(e);
} finally {
reader.releaseLock();
}
}
});
const newHeaders = new Headers(response.headers);
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
if (contentType.includes('application/json')) {
logDebug(`Processing JSON response for ${url}`);
try {
const text = await response.text();
let json = JSON.parse(text);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from JSON response: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'Fetch');
const newBody = JSON.stringify(json);
const newHeaders = new Headers(response.headers);
if (newHeaders.has('content-length')) {
newHeaders.set('content-length', encoder.encode(newBody).byteLength.toString());
}
return new Response(newBody, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
} catch (e) {
logError('Fetch JSON processing error:', e, 'URL:', url);
return original_response;
}
}
logDebug(`Non-SSE/JSON response for ${url}, skipping processing.`);
return original_response;
}
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function(input, init) {
if (!demodEnabled) {
return originalFetch.apply(this, arguments);
}
let url;
let requestArgs = init || {};
try {
url = (input instanceof Request) ? input.url : String(input);
} catch (e) {
logDebug('Invalid fetch input, passing through:', input, e);
return originalFetch.apply(this, arguments);
}
if (!url.includes('/rest/app-chat/')) {
return originalFetch.apply(this, arguments);
}
if (requestArgs.method === 'POST') {
logDebug(`Observing POST request: ${url}`);
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
if (!currentConversationId) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from POST URL: ${currentConversationId}`);
}
if (!initCache && requestArgs.headers) {
logDebug(`Caching headers from POST request to ${idFromUrl}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
}
}
return originalFetch.apply(this, arguments);
}
logDebug(`Intercepting fetch request: ${requestArgs.method || 'GET'} ${url}`);
try {
const original_response = await originalFetch.apply(this, arguments);
return await handleFetchResponse(original_response, url, requestArgs);
} catch (error) {
logError(`Fetch interception failed for ${url}:`, error);
throw error;
}
};
const OriginalWebSocket = unsafeWindow.WebSocket;
unsafeWindow.WebSocket = new Proxy(OriginalWebSocket, {
construct(target, args) {
const url = args[0];
logDebug('WebSocket connection attempt:', url);
const ws = new target(...args);
let originalOnMessageHandler = null;
Object.defineProperty(ws, 'onmessage', {
configurable: true,
enumerable: true,
get() {
return originalOnMessageHandler;
},
async set(handler) {
logDebug('WebSocket onmessage handler assigned');
originalOnMessageHandler = handler;
ws.onmessageinternal = async function(event) {
if (!demodEnabled || typeof event.data !== 'string' || !event.data.startsWith('{')) {
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (e) {
logError("Error in original WebSocket onmessage handler:", e);
}
}
return;
}
logDebug('Intercepting WebSocket message:', event.data.substring(0, 200) + '...');
try {
let json = JSON.parse(event.data);
if (json.conversation_id && json.conversation_id !== currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from WebSocket: ${currentConversationId}`);
}
const processedJson = await processPotentialModeration(json, 'WebSocket');
const newEvent = new MessageEvent('message', {
data: JSON.stringify(processedJson),
origin: event.origin,
lastEventId: event.lastEventId,
source: event.source,
ports: event.ports,
});
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, newEvent);
} catch (e) {
logError("Error calling original WebSocket onmessage handler after modification:", e);
}
} else {
logDebug("Original WebSocket onmessage handler not found when message received.");
}
} catch (e) {
logError('WebSocket processing error:', e, 'Data:', event.data.substring(0, 200) + '...');
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (eInner) {
logError("Error in original WebSocket onmessage handler (fallback path):", eInner);
}
}
}
};
ws.addEventListener('message', ws.onmessageinternal);
}
});
const wrapHandler = (eventName) => {
let originalHandler = null;
Object.defineProperty(ws, `on${eventName}`, {
configurable: true,
enumerable: true,
get() { return originalHandler; },
set(handler) {
logDebug(`WebSocket on${eventName} handler assigned`);
originalHandler = handler;
ws.addEventListener(eventName, (event) => {
if (eventName === 'message') return;
logDebug(`WebSocket event: ${eventName}`, event);
if (originalHandler) {
try {
originalHandler.call(ws, event);
} catch (e) {
logError(`Error in original WebSocket on${eventName} handler:`, e);
}
}
});
}
});
};
wrapHandler('close');
wrapHandler('error');
ws.addEventListener('open', () => logDebug('WebSocket opened:', url));
return ws;
}
});
if (window.location.hostname !== 'grok.com') {
console.log('[Grok DeMod] Script inactive: Intended for grok.com only. Current host:', window.location.hostname);
return;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupUI);
} else {
setupUI();
}
console.log('[Grok DeMod] Enhanced Script loaded. Interception is', demodEnabled ? 'ACTIVE' : 'INACTIVE', '. Debug is', debug ? 'ON' : 'OFF');
})();// ==UserScript==
// @name Grok DeMod
// @license GPL-3.0-or-later
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Hides moderation results in Grok conversations, auto-recovers blocked messages.
// @author UniverseDev
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @match https://grok.com/*
// @grant unsafeWindow
// @downloadURL https://update.greasyfork.org/scripts/531147/Grok DeMod.user.js
// @updateURL https://update.greasyfork.org/scripts/531147/Grok DeMod.meta.js
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
defaultFlags: [
'isFlagged', 'isBlocked', 'moderationApplied', 'restricted'
],
messageKeys: ['message', 'content', 'text', 'error'],
moderationMessagePatterns: [
/this content has been moderated/i,
/sorry, i cannot assist/i,
/policy violation/i,
/blocked/i,
/moderated/i,
/restricted/i,
/content restricted/i,
/unable to process/i,
/cannot help/i,
/(sorry|apologies).*?(cannot|unable|help|assist)/i,
],
clearedMessageText: '[Content cleared by Grok DeMod]',
recoveryTimeoutMs: 5000,
lsKeys: {
enabled: 'GrokDeModEnabled',
debug: 'GrokDeModDebug',
flags: 'GrokDeModFlags',
},
styles: {
uiContainer: `
position: fixed;
bottom: 10px;
right: 10px;
z-index: 10000;
background: #2d2d2d;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
gap: 8px;
font-family: Arial, sans-serif;
color: #e0e0e0;
min-width: 170px;
`,
button: `
padding: 6px 12px;
border-radius: 5px;
border: none;
cursor: pointer;
color: #fff;
font-size: 13px;
transition: background-color 0.2s ease;
`,
status: `
padding: 5px;
font-size: 12px;
color: #a0a0a0;
text-align: center;
border-top: 1px solid #444;
margin-top: 5px;
min-height: 16px;
`,
logContainer: `
max-height: 100px;
overflow-y: auto;
font-size: 11px;
color: #c0c0c0;
background-color: #333;
padding: 5px;
border-radius: 4px;
line-height: 1.4;
margin-top: 5px;
`,
logEntry: `
padding-bottom: 3px;
border-bottom: 1px dashed #555;
margin-bottom: 3px;
word-break: break-word;
`,
colors: {
enabled: '#388E3C',
disabled: '#D32F2F',
debugEnabled: '#1976D2',
debugDisabled: '#555555',
safe: '#66ff66',
flagged: '#ffa500',
blocked: '#ff6666',
recovering: '#ffcc00'
}
}
};
let demodEnabled = getState(CONFIG.lsKeys.enabled, true);
let debug = getState(CONFIG.lsKeys.debug, false);
let moderationFlags = getState(CONFIG.lsKeys.flags, CONFIG.defaultFlags);
let initCache = null;
let currentConversationId = null;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const uiLogBuffer = [];
const MAX_LOG_ENTRIES = 50;
const ModerationResult = Object.freeze({
SAFE: 0,
FLAGGE
1,
BLOCKE
2,
});
function logDebug(...args) {
if (debug) {
console.log('[Grok DeMod]', ...args);
}
}
function logError(...args) {
console.error('[Grok DeMod]', ...args);
}
function getState(key, defaultValue) {
try {
const value = localStorage.getItem(key);
if (value === null) return defaultValue;
if (value === 'true') return true;
if (value === 'false') return false;
return JSON.parse(value);
} catch (e) {
logError(`Error reading ${key} from localStorage:`, e);
return defaultValue;
}
}
function setState(key, value) {
try {
const valueToStore = typeof value === 'boolean' ? value.toString() : JSON.stringify(value);
localStorage.setItem(key, valueToStore);
} catch (e) {
logError(`Error writing ${key} to localStorage:`, e);
}
}
function timeoutPromise(ms, promise, description = 'Promise') {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
logDebug(`${description} timed out after ${ms}ms`);
reject(new Error(`Timeout (${description})`));
}, ms);
promise.then(
(value) => { clearTimeout(timer); resolve(value); },
(error) => { clearTimeout(timer); reject(error); }
);
});
}
function getModerationResult(obj, path = '') {
if (typeof obj !== 'object' || obj === null) return ModerationResult.SAFE;
let result = ModerationResult.SAFE;
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const currentPath = path ? `${path}.${key}` : key;
const value = obj[key];
if (key === 'isBlocked' && value === true) {
logDebug(`Blocked detected via flag '${currentPath}'`);
return ModerationResult.BLOCKED;
}
if (moderationFlags.includes(key) && value === true) {
logDebug(`Flagged detected via flag '${currentPath}'`);
result = Math.max(result, ModerationResult.FLAGGED);
}
if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
const content = value.toLowerCase();
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(content)) {
logDebug(`Moderation pattern matched in '${currentPath}': "${content.substring(0, 50)}..."`);
if (/blocked|moderated|restricted/i.test(pattern.source)) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, ModerationResult.FLAGGED);
break;
}
}
if (result === ModerationResult.SAFE && content.length < 70 && /(sorry|apologies|unable|cannot)/i.test(content)) {
logDebug(`Heuristic moderation detected in '${currentPath}': "${content.substring(0, 50)}..."`);
result = Math.max(result, ModerationResult.FLAGGED);
}
}
if (typeof value === 'object') {
const childResult = getModerationResult(value, currentPath);
if (childResult === ModerationResult.BLOCKED) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, childResult);
}
}
return result;
}
function clearFlagging(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(item => clearFlagging(item));
}
const newObj = {};
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key];
if (moderationFlags.includes(key) && value === true) {
newObj[key] = false;
logDebug(`Cleared flag '${key}'`);
}
else if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
let replaced = false;
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(value)) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced moderated message in '${key}' using pattern`);
replaced = true;
break;
}
}
if (!replaced && value.length < 70 && /(sorry|apologies|unable|cannot)/i.test(value.toLowerCase())) {
if (getModerationResult({[key]: value}) === ModerationResult.FLAGGED) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced heuristic moderated message in '${key}'`);
replaced = true;
}
}
if (!replaced) {
newObj[key] = value;
}
}
else if (typeof value === 'object') {
newObj[key] = clearFlagging(value);
}
else {
newObj[key] = value;
}
}
return newObj;
}
let uiContainer, toggleButton, debugButton, statusEl, logContainer;
function addLog(message) {
if (!logContainer) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.textContent = `[${timestamp}] ${message}`;
logEntry.style.cssText = CONFIG.styles.logEntry;
uiLogBuffer.push(logEntry);
if (uiLogBuffer.length > MAX_LOG_ENTRIES) {
const removed = uiLogBuffer.shift();
if (removed && removed.parentNode === logContainer) {
logContainer.removeChild(removed);
}
}
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(modResult, isRecovering = false) {
if (!statusEl) return;
let text = 'Status: ';
let color = CONFIG.styles.colors.safe;
if (isRecovering) {
text += 'Recovering...';
color = CONFIG.styles.colors.recovering;
} else if (modResult === ModerationResult.BLOCKED) {
text += 'Blocked (Recovered/Cleared)';
color = CONFIG.styles.colors.blocked;
} else if (modResult === ModerationResult.FLAGGED) {
text += 'Flagged (Cleared)';
color = CONFIG.styles.colors.flagged;
} else {
text += 'Safe';
color = CONFIG.styles.colors.safe;
}
statusEl.textContent = text;
statusEl.style.color = color;
}
function setupUI() {
uiContainer = document.createElement('div');
uiContainer.id = 'grok-demod-ui';
uiContainer.style.cssText = CONFIG.styles.uiContainer;
toggleButton = document.createElement('button');
debugButton = document.createElement('button');
statusEl = document.createElement('div');
logContainer = document.createElement('div');
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.title = 'Toggle DeMod functionality (ON = intercepting)';
toggleButton.style.cssText = CONFIG.styles.button;
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
toggleButton.onclick = (e) => {
demodEnabled = !demodEnabled;
setState(CONFIG.lsKeys.enabled, demodEnabled);
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
addLog(`DeMod ${demodEnabled ? 'Enabled' : 'Disabled'}.`);
console.log('[Grok DeMod] Interception is now', demodEnabled ? 'ACTIVE' : 'INACTIVE');
};
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.title = 'Toggle debug mode (logs verbose details to console)';
debugButton.style.cssText = CONFIG.styles.button;
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
debugButton.onclick = (e) => {
debug = !debug;
setState(CONFIG.lsKeys.debug, debug);
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
addLog(`Debug Mode ${debug ? 'Enabled' : 'Disabled'}.`);
logDebug(`Debug mode ${debug ? 'enabled' : 'disabled'}.`);
};
statusEl.id = 'grok-demod-status';
statusEl.style.cssText = CONFIG.styles.status;
updateStatus(ModerationResult.SAFE);
logContainer.id = 'grok-demod-log';
logContainer.style.cssText = CONFIG.styles.logContainer;
uiLogBuffer.forEach(entry => logContainer.appendChild(entry));
logContainer.scrollTop = logContainer.scrollHeight;
uiContainer.appendChild(toggleButton);
uiContainer.appendChild(debugButton);
uiContainer.appendChild(statusEl);
uiContainer.appendChild(logContainer);
document.body.appendChild(uiContainer);
addLog("Grok DeMod Initialized.");
if (debug) addLog("Debug mode is ON.");
}
async function redownloadLatestMessage() {
if (!currentConversationId) {
logDebug('Recovery skipped: Missing conversationId');
addLog('Recovery failed: No conversation ID.');
return null;
}
if (!initCache || !initCache.headers) {
logDebug('Recovery cache missing, attempting fresh fetch for headers...');
try {
const currentConvUrl = `/rest/app-chat/conversation/${currentConversationId}`;
const tempResp = await originalFetch(currentConvUrl, { method: 'GET', headers: {'Accept': 'application/json'} });
if (tempResp.ok) {
logDebug('Fresh header fetch successful (status OK).');
initCache = { headers: new Headers({'Accept': 'application/json'}), credentials: 'include' };
} else {
logDebug(`Fresh header fetch failed with status ${tempResp.status}. Recovery may fail.`);
addLog('Recovery failed: Cannot get request data.');
return null;
}
} catch (e) {
logError('Error during fresh header fetch:', e);
addLog('Recovery failed: Error getting request data.');
return null;
}
}
const url = `/rest/app-chat/conversation/${currentConversationId}`;
logDebug(`Attempting recovery fetch for conversation: ${currentConversationId}`);
addLog('Attempting content recovery...');
const headers = new Headers(initCache.headers);
if (!headers.has('Accept')) headers.set('Accept', 'application/json, text/plain, */*');
const requestOptions = {
method: 'GET',
headers: headers,
credentials: initCache.credentials || 'include',
};
try {
const response = await timeoutPromise(
CONFIG.recoveryTimeoutMs,
fetch(url, requestOptions),
'Recovery Fetch'
);
if (!response.ok) {
logError(`Recovery fetch failed with status ${response.status}: ${response.statusText}`);
addLog(`Recovery failed: HTTP ${response.status}`);
try {
const errorBody = await response.text();
logDebug('Recovery error body:', errorBody.substring(0, 500));
} catch (e) { }
return null;
}
const data = await response.json();
const messages = data?.messages;
if (!Array.isArray(messages) || messages.length === 0) {
logDebug('Recovery failed: No messages found in conversation data', data);
addLog('Recovery failed: No messages found.');
return null;
}
messages.sort((a, b) => {
const tsA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
const tsB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
return tsB - tsA;
});
const latestMessage = messages[0];
if (!latestMessage || typeof latestMessage.content !== 'string' || latestMessage.content.trim() === '') {
logDebug('Recovery failed: Latest message or its content is invalid/empty', latestMessage);
addLog('Recovery failed: Invalid latest message.');
return null;
}
logDebug('Recovery successful, latest content:', latestMessage.content.substring(0, 100) + '...');
addLog('Recovery seems successful.');
return { content: latestMessage.content };
} catch (e) {
logError('Recovery fetch/parse error:', e);
addLog(`Recovery error: ${e.message}`);
return null;
}
}
function extractConversationIdFromUrl(url) {
const match = url.match(/\/conversation\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
return match ? match[1] : null;
}
async function processPotentialModeration(json, source) {
const modResult = getModerationResult(json);
let finalJson = json;
if (modResult !== ModerationResult.SAFE) {
if (modResult === ModerationResult.BLOCKED) {
logDebug(`Blocked content detected from ${source}:`, JSON.stringify(json).substring(0, 200) + '...');
addLog(`Blocked content from ${source}.`);
updateStatus(modResult, true);
const recoveredData = await redownloadLatestMessage();
if (recoveredData && recoveredData.content) {
addLog(`Recovery successful (${source}).`);
logDebug(`Recovered content applied (${source})`);
let replaced = false;
const keysToTry = [...CONFIG.messageKeys, 'text', 'message'];
for (const key of keysToTry) {
if (typeof finalJson[key] === 'string') {
finalJson[key] = recoveredData.content;
logDebug(`Injected recovered content into key '${key}'`);
replaced = true;
break;
}
}
if (!replaced) {
logDebug("Could not find standard key to inject recovered content, adding as 'recovered_content'");
finalJson.recovered_content = recoveredData.content;
}
finalJson = clearFlagging(finalJson);
updateStatus(modResult, false);
} else {
addLog(`Recovery failed (${source}). Content may be lost.`);
logDebug(`Recovery failed (${source}), applying standard clearing.`);
finalJson = clearFlagging(json);
updateStatus(modResult, false);
}
} else {
logDebug(`Flagged content detected and cleared from ${source}.`);
addLog(`Flagged content cleared (${source}).`);
finalJson = clearFlagging(json);
updateStatus(modResult);
}
} else {
if (statusEl && !statusEl.textContent.includes('Blocked') && !statusEl.textContent.includes('Flagged') && !statusEl.textContent.includes('Recovering')) {
updateStatus(modResult);
} else if (statusEl && statusEl.textContent.includes('Recovering')) {
logDebug("Recovery attempt finished (next message safe). Resetting status.");
updateStatus(ModerationResult.SAFE);
}
}
return finalJson;
}
async function handleFetchResponse(original_response, url, requestArgs) {
const response = original_response.clone();
if (!response.ok) {
logDebug(`Fetch response not OK (${response.status}) for ${url}, skipping processing.`);
return original_response;
}
const contentType = response.headers.get('Content-Type')?.toLowerCase() || '';
logDebug(`Intercepted fetch response for ${url}, Content-Type: ${contentType}`);
const conversationGetMatch = url.match(/\/rest\/app-chat\/conversation\/([a-f0-9-]+)$/i);
if (conversationGetMatch && requestArgs?.method === 'GET') {
logDebug(`Caching GET request options for conversation ${conversationGetMatch[1]}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
if (!currentConversationId) {
currentConversationId = conversationGetMatch[1];
logDebug(`Conversation ID set from GET URL: ${currentConversationId}`);
}
}
if (!currentConversationId) {
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from other URL: ${currentConversationId}`);
}
}
if (contentType.includes('text/event-stream')) {
logDebug(`Processing SSE stream for ${url}`);
const reader = response.body.getReader();
const stream = new ReadableStream({
async start(controller) {
let buffer = '';
let currentEvent = { data: '', type: 'message', id: null };
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
logDebug("SSE stream ended, processing final buffer:", buffer);
if (buffer.startsWith('{') || buffer.startsWith('[')) {
try {
let json = JSON.parse(buffer);
json = await processPotentialModeration(json, 'SSE-Final');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logDebug("Error parsing final SSE buffer, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else {
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else if (currentEvent.data) {
logDebug("SSE stream ended after data field, processing event:", currentEvent.data.substring(0,100)+"...");
try {
let json = JSON.parse(currentEvent.data);
json = await processPotentialModeration(json, 'SSE-Event');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch (e) {
logDebug("Error parsing trailing SSE data, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
controller.close();
break;
}
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') {
if (currentEvent.data) {
logDebug("Processing SSE event data:", currentEvent.data.substring(0, 100) + '...');
if (currentEvent.data.startsWith('{') || currentEvent.data.startsWith('[')) {
try {
let json = JSON.parse(currentEvent.data);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from SSE data: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'SSE');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logError("SSE JSON parse error:", e, "Data:", currentEvent.data.substring(0,200)+"...");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
} else {
logDebug("SSE data is not JSON, forwarding as is.");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
currentEvent = { data: '', type: 'message', id: null };
} else if (line.startsWith('data:')) {
currentEvent.data += (currentEvent.data ? '\n' : '') + line.substring(5).trim();
} else if (line.startsWith('event:')) {
currentEvent.type = line.substring(6).trim();
} else if (line.startsWith('id:')) {
currentEvent.id = line.substring(3).trim();
} else if (line.startsWith(':')) {
} else {
logDebug("Unknown SSE line:", line);
}
}
}
} catch (e) {
logError('Error reading/processing SSE stream:', e);
controller.error(e);
} finally {
reader.releaseLock();
}
}
});
const newHeaders = new Headers(response.headers);
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
if (contentType.includes('application/json')) {
logDebug(`Processing JSON response for ${url}`);
try {
const text = await response.text();
let json = JSON.parse(text);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from JSON response: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'Fetch');
const newBody = JSON.stringify(json);
const newHeaders = new Headers(response.headers);
if (newHeaders.has('content-length')) {
newHeaders.set('content-length', encoder.encode(newBody).byteLength.toString());
}
return new Response(newBody, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
} catch (e) {
logError('Fetch JSON processing error:', e, 'URL:', url);
return original_response;
}
}
logDebug(`Non-SSE/JSON response for ${url}, skipping processing.`);
return original_response;
}
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function(input, init) {
if (!demodEnabled) {
return originalFetch.apply(this, arguments);
}
let url;
let requestArgs = init || {};
try {
url = (input instanceof Request) ? input.url : String(input);
} catch (e) {
logDebug('Invalid fetch input, passing through:', input, e);
return originalFetch.apply(this, arguments);
}
if (!url.includes('/rest/app-chat/')) {
return originalFetch.apply(this, arguments);
}
if (requestArgs.method === 'POST') {
logDebug(`Observing POST request: ${url}`);
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
if (!currentConversationId) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from POST URL: ${currentConversationId}`);
}
if (!initCache && requestArgs.headers) {
logDebug(`Caching headers from POST request to ${idFromUrl}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
}
}
return originalFetch.apply(this, arguments);
}
logDebug(`Intercepting fetch request: ${requestArgs.method || 'GET'} ${url}`);
try {
const original_response = await originalFetch.apply(this, arguments);
return await handleFetchResponse(original_response, url, requestArgs);
} catch (error) {
logError(`Fetch interception failed for ${url}:`, error);
throw error;
}
};
const OriginalWebSocket = unsafeWindow.WebSocket;
unsafeWindow.WebSocket = new Proxy(OriginalWebSocket, {
construct(target, args) {
const url = args[0];
logDebug('WebSocket connection attempt:', url);
const ws = new target(...args);
let originalOnMessageHandler = null;
Object.defineProperty(ws, 'onmessage', {
configurable: true,
enumerable: true,
get() {
return originalOnMessageHandler;
},
async set(handler) {
logDebug('WebSocket onmessage handler assigned');
originalOnMessageHandler = handler;
ws.onmessageinternal = async function(event) {
if (!demodEnabled || typeof event.data !== 'string' || !event.data.startsWith('{')) {
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (e) {
logError("Error in original WebSocket onmessage handler:", e);
}
}
return;
}
logDebug('Intercepting WebSocket message:', event.data.substring(0, 200) + '...');
try {
let json = JSON.parse(event.data);
if (json.conversation_id && json.conversation_id !== currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from WebSocket: ${currentConversationId}`);
}
const processedJson = await processPotentialModeration(json, 'WebSocket');
const newEvent = new MessageEvent('message', {
data: JSON.stringify(processedJson),
origin: event.origin,
lastEventId: event.lastEventId,
source: event.source,
ports: event.ports,
});
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, newEvent);
} catch (e) {
logError("Error calling original WebSocket onmessage handler after modification:", e);
}
} else {
logDebug("Original WebSocket onmessage handler not found when message received.");
}
} catch (e) {
logError('WebSocket processing error:', e, 'Data:', event.data.substring(0, 200) + '...');
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (eInner) {
logError("Error in original WebSocket onmessage handler (fallback path):", eInner);
}
}
}
};
ws.addEventListener('message', ws.onmessageinternal);
}
});
const wrapHandler = (eventName) => {
let originalHandler = null;
Object.defineProperty(ws, `on${eventName}`, {
configurable: true,
enumerable: true,
get() { return originalHandler; },
set(handler) {
logDebug(`WebSocket on${eventName} handler assigned`);
originalHandler = handler;
ws.addEventListener(eventName, (event) => {
if (eventName === 'message') return;
logDebug(`WebSocket event: ${eventName}`, event);
if (originalHandler) {
try {
originalHandler.call(ws, event);
} catch (e) {
logError(`Error in original WebSocket on${eventName} handler:`, e);
}
}
});
}
});
};
wrapHandler('close');
wrapHandler('error');
ws.addEventListener('open', () => logDebug('WebSocket opened:', url));
return ws;
}
});
if (window.location.hostname !== 'grok.com') {
console.log('[Grok DeMod] Script inactive: Intended for grok.com only. Current host:', window.location.hostname);
return;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupUI);
} else {
setupUI();
}
console.log('[Grok DeMod] Enhanced Script loaded. Interception is', demodEnabled ? 'ACTIVE' : 'INACTIVE', '. Debug is', debug ? 'ON' : 'OFF');
})();// ==UserScript==
// @name Grok DeMod
// @license GPL-3.0-or-later
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Hides moderation results in Grok conversations, auto-recovers blocked messages.
// @author UniverseDev
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @match https://grok.com/*
// @grant unsafeWindow
// @downloadURL https://update.greasyfork.org/scripts/531147/Grok DeMod.user.js
// @updateURL https://update.greasyfork.org/scripts/531147/Grok DeMod.meta.js
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
defaultFlags: [
'isFlagged', 'isBlocked', 'moderationApplied', 'restricted'
],
messageKeys: ['message', 'content', 'text', 'error'],
moderationMessagePatterns: [
/this content has been moderated/i,
/sorry, i cannot assist/i,
/policy violation/i,
/blocked/i,
/moderated/i,
/restricted/i,
/content restricted/i,
/unable to process/i,
/cannot help/i,
/(sorry|apologies).*?(cannot|unable|help|assist)/i,
],
clearedMessageText: '[Content cleared by Grok DeMod]',
recoveryTimeoutMs: 5000,
lsKeys: {
enabled: 'GrokDeModEnabled',
debug: 'GrokDeModDebug',
flags: 'GrokDeModFlags',
},
styles: {
uiContainer: `
position: fixed;
bottom: 10px;
right: 10px;
z-index: 10000;
background: #2d2d2d;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
gap: 8px;
font-family: Arial, sans-serif;
color: #e0e0e0;
min-width: 170px;
`,
button: `
padding: 6px 12px;
border-radius: 5px;
border: none;
cursor: pointer;
color: #fff;
font-size: 13px;
transition: background-color 0.2s ease;
`,
status: `
padding: 5px;
font-size: 12px;
color: #a0a0a0;
text-align: center;
border-top: 1px solid #444;
margin-top: 5px;
min-height: 16px;
`,
logContainer: `
max-height: 100px;
overflow-y: auto;
font-size: 11px;
color: #c0c0c0;
background-color: #333;
padding: 5px;
border-radius: 4px;
line-height: 1.4;
margin-top: 5px;
`,
logEntry: `
padding-bottom: 3px;
border-bottom: 1px dashed #555;
margin-bottom: 3px;
word-break: break-word;
`,
colors: {
enabled: '#388E3C',
disabled: '#D32F2F',
debugEnabled: '#1976D2',
debugDisabled: '#555555',
safe: '#66ff66',
flagged: '#ffa500',
blocked: '#ff6666',
recovering: '#ffcc00'
}
}
};
let demodEnabled = getState(CONFIG.lsKeys.enabled, true);
let debug = getState(CONFIG.lsKeys.debug, false);
let moderationFlags = getState(CONFIG.lsKeys.flags, CONFIG.defaultFlags);
let initCache = null;
let currentConversationId = null;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const uiLogBuffer = [];
const MAX_LOG_ENTRIES = 50;
const ModerationResult = Object.freeze({
SAFE: 0,
FLAGGE
1,
BLOCKE
2,
});
function logDebug(...args) {
if (debug) {
console.log('[Grok DeMod]', ...args);
}
}
function logError(...args) {
console.error('[Grok DeMod]', ...args);
}
function getState(key, defaultValue) {
try {
const value = localStorage.getItem(key);
if (value === null) return defaultValue;
if (value === 'true') return true;
if (value === 'false') return false;
return JSON.parse(value);
} catch (e) {
logError(`Error reading ${key} from localStorage:`, e);
return defaultValue;
}
}
function setState(key, value) {
try {
const valueToStore = typeof value === 'boolean' ? value.toString() : JSON.stringify(value);
localStorage.setItem(key, valueToStore);
} catch (e) {
logError(`Error writing ${key} to localStorage:`, e);
}
}
function timeoutPromise(ms, promise, description = 'Promise') {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
logDebug(`${description} timed out after ${ms}ms`);
reject(new Error(`Timeout (${description})`));
}, ms);
promise.then(
(value) => { clearTimeout(timer); resolve(value); },
(error) => { clearTimeout(timer); reject(error); }
);
});
}
function getModerationResult(obj, path = '') {
if (typeof obj !== 'object' || obj === null) return ModerationResult.SAFE;
let result = ModerationResult.SAFE;
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const currentPath = path ? `${path}.${key}` : key;
const value = obj[key];
if (key === 'isBlocked' && value === true) {
logDebug(`Blocked detected via flag '${currentPath}'`);
return ModerationResult.BLOCKED;
}
if (moderationFlags.includes(key) && value === true) {
logDebug(`Flagged detected via flag '${currentPath}'`);
result = Math.max(result, ModerationResult.FLAGGED);
}
if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
const content = value.toLowerCase();
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(content)) {
logDebug(`Moderation pattern matched in '${currentPath}': "${content.substring(0, 50)}..."`);
if (/blocked|moderated|restricted/i.test(pattern.source)) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, ModerationResult.FLAGGED);
break;
}
}
if (result === ModerationResult.SAFE && content.length < 70 && /(sorry|apologies|unable|cannot)/i.test(content)) {
logDebug(`Heuristic moderation detected in '${currentPath}': "${content.substring(0, 50)}..."`);
result = Math.max(result, ModerationResult.FLAGGED);
}
}
if (typeof value === 'object') {
const childResult = getModerationResult(value, currentPath);
if (childResult === ModerationResult.BLOCKED) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, childResult);
}
}
return result;
}
function clearFlagging(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(item => clearFlagging(item));
}
const newObj = {};
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key];
if (moderationFlags.includes(key) && value === true) {
newObj[key] = false;
logDebug(`Cleared flag '${key}'`);
}
else if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
let replaced = false;
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(value)) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced moderated message in '${key}' using pattern`);
replaced = true;
break;
}
}
if (!replaced && value.length < 70 && /(sorry|apologies|unable|cannot)/i.test(value.toLowerCase())) {
if (getModerationResult({[key]: value}) === ModerationResult.FLAGGED) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced heuristic moderated message in '${key}'`);
replaced = true;
}
}
if (!replaced) {
newObj[key] = value;
}
}
else if (typeof value === 'object') {
newObj[key] = clearFlagging(value);
}
else {
newObj[key] = value;
}
}
return newObj;
}
let uiContainer, toggleButton, debugButton, statusEl, logContainer;
function addLog(message) {
if (!logContainer) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.textContent = `[${timestamp}] ${message}`;
logEntry.style.cssText = CONFIG.styles.logEntry;
uiLogBuffer.push(logEntry);
if (uiLogBuffer.length > MAX_LOG_ENTRIES) {
const removed = uiLogBuffer.shift();
if (removed && removed.parentNode === logContainer) {
logContainer.removeChild(removed);
}
}
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(modResult, isRecovering = false) {
if (!statusEl) return;
let text = 'Status: ';
let color = CONFIG.styles.colors.safe;
if (isRecovering) {
text += 'Recovering...';
color = CONFIG.styles.colors.recovering;
} else if (modResult === ModerationResult.BLOCKED) {
text += 'Blocked (Recovered/Cleared)';
color = CONFIG.styles.colors.blocked;
} else if (modResult === ModerationResult.FLAGGED) {
text += 'Flagged (Cleared)';
color = CONFIG.styles.colors.flagged;
} else {
text += 'Safe';
color = CONFIG.styles.colors.safe;
}
statusEl.textContent = text;
statusEl.style.color = color;
}
function setupUI() {
uiContainer = document.createElement('div');
uiContainer.id = 'grok-demod-ui';
uiContainer.style.cssText = CONFIG.styles.uiContainer;
toggleButton = document.createElement('button');
debugButton = document.createElement('button');
statusEl = document.createElement('div');
logContainer = document.createElement('div');
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.title = 'Toggle DeMod functionality (ON = intercepting)';
toggleButton.style.cssText = CONFIG.styles.button;
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
toggleButton.onclick = (e) => {
demodEnabled = !demodEnabled;
setState(CONFIG.lsKeys.enabled, demodEnabled);
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
addLog(`DeMod ${demodEnabled ? 'Enabled' : 'Disabled'}.`);
console.log('[Grok DeMod] Interception is now', demodEnabled ? 'ACTIVE' : 'INACTIVE');
};
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.title = 'Toggle debug mode (logs verbose details to console)';
debugButton.style.cssText = CONFIG.styles.button;
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
debugButton.onclick = (e) => {
debug = !debug;
setState(CONFIG.lsKeys.debug, debug);
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
addLog(`Debug Mode ${debug ? 'Enabled' : 'Disabled'}.`);
logDebug(`Debug mode ${debug ? 'enabled' : 'disabled'}.`);
};
statusEl.id = 'grok-demod-status';
statusEl.style.cssText = CONFIG.styles.status;
updateStatus(ModerationResult.SAFE);
logContainer.id = 'grok-demod-log';
logContainer.style.cssText = CONFIG.styles.logContainer;
uiLogBuffer.forEach(entry => logContainer.appendChild(entry));
logContainer.scrollTop = logContainer.scrollHeight;
uiContainer.appendChild(toggleButton);
uiContainer.appendChild(debugButton);
uiContainer.appendChild(statusEl);
uiContainer.appendChild(logContainer);
document.body.appendChild(uiContainer);
addLog("Grok DeMod Initialized.");
if (debug) addLog("Debug mode is ON.");
}
async function redownloadLatestMessage() {
if (!currentConversationId) {
logDebug('Recovery skipped: Missing conversationId');
addLog('Recovery failed: No conversation ID.');
return null;
}
if (!initCache || !initCache.headers) {
logDebug('Recovery cache missing, attempting fresh fetch for headers...');
try {
const currentConvUrl = `/rest/app-chat/conversation/${currentConversationId}`;
const tempResp = await originalFetch(currentConvUrl, { method: 'GET', headers: {'Accept': 'application/json'} });
if (tempResp.ok) {
logDebug('Fresh header fetch successful (status OK).');
initCache = { headers: new Headers({'Accept': 'application/json'}), credentials: 'include' };
} else {
logDebug(`Fresh header fetch failed with status ${tempResp.status}. Recovery may fail.`);
addLog('Recovery failed: Cannot get request data.');
return null;
}
} catch (e) {
logError('Error during fresh header fetch:', e);
addLog('Recovery failed: Error getting request data.');
return null;
}
}
const url = `/rest/app-chat/conversation/${currentConversationId}`;
logDebug(`Attempting recovery fetch for conversation: ${currentConversationId}`);
addLog('Attempting content recovery...');
const headers = new Headers(initCache.headers);
if (!headers.has('Accept')) headers.set('Accept', 'application/json, text/plain, */*');
const requestOptions = {
method: 'GET',
headers: headers,
credentials: initCache.credentials || 'include',
};
try {
const response = await timeoutPromise(
CONFIG.recoveryTimeoutMs,
fetch(url, requestOptions),
'Recovery Fetch'
);
if (!response.ok) {
logError(`Recovery fetch failed with status ${response.status}: ${response.statusText}`);
addLog(`Recovery failed: HTTP ${response.status}`);
try {
const errorBody = await response.text();
logDebug('Recovery error body:', errorBody.substring(0, 500));
} catch (e) { }
return null;
}
const data = await response.json();
const messages = data?.messages;
if (!Array.isArray(messages) || messages.length === 0) {
logDebug('Recovery failed: No messages found in conversation data', data);
addLog('Recovery failed: No messages found.');
return null;
}
messages.sort((a, b) => {
const tsA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
const tsB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
return tsB - tsA;
});
const latestMessage = messages[0];
if (!latestMessage || typeof latestMessage.content !== 'string' || latestMessage.content.trim() === '') {
logDebug('Recovery failed: Latest message or its content is invalid/empty', latestMessage);
addLog('Recovery failed: Invalid latest message.');
return null;
}
logDebug('Recovery successful, latest content:', latestMessage.content.substring(0, 100) + '...');
addLog('Recovery seems successful.');
return { content: latestMessage.content };
} catch (e) {
logError('Recovery fetch/parse error:', e);
addLog(`Recovery error: ${e.message}`);
return null;
}
}
function extractConversationIdFromUrl(url) {
const match = url.match(/\/conversation\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
return match ? match[1] : null;
}
async function processPotentialModeration(json, source) {
const modResult = getModerationResult(json);
let finalJson = json;
if (modResult !== ModerationResult.SAFE) {
if (modResult === ModerationResult.BLOCKED) {
logDebug(`Blocked content detected from ${source}:`, JSON.stringify(json).substring(0, 200) + '...');
addLog(`Blocked content from ${source}.`);
updateStatus(modResult, true);
const recoveredData = await redownloadLatestMessage();
if (recoveredData && recoveredData.content) {
addLog(`Recovery successful (${source}).`);
logDebug(`Recovered content applied (${source})`);
let replaced = false;
const keysToTry = [...CONFIG.messageKeys, 'text', 'message'];
for (const key of keysToTry) {
if (typeof finalJson[key] === 'string') {
finalJson[key] = recoveredData.content;
logDebug(`Injected recovered content into key '${key}'`);
replaced = true;
break;
}
}
if (!replaced) {
logDebug("Could not find standard key to inject recovered content, adding as 'recovered_content'");
finalJson.recovered_content = recoveredData.content;
}
finalJson = clearFlagging(finalJson);
updateStatus(modResult, false);
} else {
addLog(`Recovery failed (${source}). Content may be lost.`);
logDebug(`Recovery failed (${source}), applying standard clearing.`);
finalJson = clearFlagging(json);
updateStatus(modResult, false);
}
} else {
logDebug(`Flagged content detected and cleared from ${source}.`);
addLog(`Flagged content cleared (${source}).`);
finalJson = clearFlagging(json);
updateStatus(modResult);
}
} else {
if (statusEl && !statusEl.textContent.includes('Blocked') && !statusEl.textContent.includes('Flagged') && !statusEl.textContent.includes('Recovering')) {
updateStatus(modResult);
} else if (statusEl && statusEl.textContent.includes('Recovering')) {
logDebug("Recovery attempt finished (next message safe). Resetting status.");
updateStatus(ModerationResult.SAFE);
}
}
return finalJson;
}
async function handleFetchResponse(original_response, url, requestArgs) {
const response = original_response.clone();
if (!response.ok) {
logDebug(`Fetch response not OK (${response.status}) for ${url}, skipping processing.`);
return original_response;
}
const contentType = response.headers.get('Content-Type')?.toLowerCase() || '';
logDebug(`Intercepted fetch response for ${url}, Content-Type: ${contentType}`);
const conversationGetMatch = url.match(/\/rest\/app-chat\/conversation\/([a-f0-9-]+)$/i);
if (conversationGetMatch && requestArgs?.method === 'GET') {
logDebug(`Caching GET request options for conversation ${conversationGetMatch[1]}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
if (!currentConversationId) {
currentConversationId = conversationGetMatch[1];
logDebug(`Conversation ID set from GET URL: ${currentConversationId}`);
}
}
if (!currentConversationId) {
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from other URL: ${currentConversationId}`);
}
}
if (contentType.includes('text/event-stream')) {
logDebug(`Processing SSE stream for ${url}`);
const reader = response.body.getReader();
const stream = new ReadableStream({
async start(controller) {
let buffer = '';
let currentEvent = { data: '', type: 'message', id: null };
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
logDebug("SSE stream ended, processing final buffer:", buffer);
if (buffer.startsWith('{') || buffer.startsWith('[')) {
try {
let json = JSON.parse(buffer);
json = await processPotentialModeration(json, 'SSE-Final');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logDebug("Error parsing final SSE buffer, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else {
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else if (currentEvent.data) {
logDebug("SSE stream ended after data field, processing event:", currentEvent.data.substring(0,100)+"...");
try {
let json = JSON.parse(currentEvent.data);
json = await processPotentialModeration(json, 'SSE-Event');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch (e) {
logDebug("Error parsing trailing SSE data, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
controller.close();
break;
}
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') {
if (currentEvent.data) {
logDebug("Processing SSE event data:", currentEvent.data.substring(0, 100) + '...');
if (currentEvent.data.startsWith('{') || currentEvent.data.startsWith('[')) {
try {
let json = JSON.parse(currentEvent.data);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from SSE data: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'SSE');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logError("SSE JSON parse error:", e, "Data:", currentEvent.data.substring(0,200)+"...");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
} else {
logDebug("SSE data is not JSON, forwarding as is.");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
currentEvent = { data: '', type: 'message', id: null };
} else if (line.startsWith('data:')) {
currentEvent.data += (currentEvent.data ? '\n' : '') + line.substring(5).trim();
} else if (line.startsWith('event:')) {
currentEvent.type = line.substring(6).trim();
} else if (line.startsWith('id:')) {
currentEvent.id = line.substring(3).trim();
} else if (line.startsWith(':')) {
} else {
logDebug("Unknown SSE line:", line);
}
}
}
} catch (e) {
logError('Error reading/processing SSE stream:', e);
controller.error(e);
} finally {
reader.releaseLock();
}
}
});
const newHeaders = new Headers(response.headers);
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
if (contentType.includes('application/json')) {
logDebug(`Processing JSON response for ${url}`);
try {
const text = await response.text();
let json = JSON.parse(text);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from JSON response: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'Fetch');
const newBody = JSON.stringify(json);
const newHeaders = new Headers(response.headers);
if (newHeaders.has('content-length')) {
newHeaders.set('content-length', encoder.encode(newBody).byteLength.toString());
}
return new Response(newBody, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
} catch (e) {
logError('Fetch JSON processing error:', e, 'URL:', url);
return original_response;
}
}
logDebug(`Non-SSE/JSON response for ${url}, skipping processing.`);
return original_response;
}
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function(input, init) {
if (!demodEnabled) {
return originalFetch.apply(this, arguments);
}
let url;
let requestArgs = init || {};
try {
url = (input instanceof Request) ? input.url : String(input);
} catch (e) {
logDebug('Invalid fetch input, passing through:', input, e);
return originalFetch.apply(this, arguments);
}
if (!url.includes('/rest/app-chat/')) {
return originalFetch.apply(this, arguments);
}
if (requestArgs.method === 'POST') {
logDebug(`Observing POST request: ${url}`);
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
if (!currentConversationId) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from POST URL: ${currentConversationId}`);
}
if (!initCache && requestArgs.headers) {
logDebug(`Caching headers from POST request to ${idFromUrl}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
}
}
return originalFetch.apply(this, arguments);
}
logDebug(`Intercepting fetch request: ${requestArgs.method || 'GET'} ${url}`);
try {
const original_response = await originalFetch.apply(this, arguments);
return await handleFetchResponse(original_response, url, requestArgs);
} catch (error) {
logError(`Fetch interception failed for ${url}:`, error);
throw error;
}
};
const OriginalWebSocket = unsafeWindow.WebSocket;
unsafeWindow.WebSocket = new Proxy(OriginalWebSocket, {
construct(target, args) {
const url = args[0];
logDebug('WebSocket connection attempt:', url);
const ws = new target(...args);
let originalOnMessageHandler = null;
Object.defineProperty(ws, 'onmessage', {
configurable: true,
enumerable: true,
get() {
return originalOnMessageHandler;
},
async set(handler) {
logDebug('WebSocket onmessage handler assigned');
originalOnMessageHandler = handler;
ws.onmessageinternal = async function(event) {
if (!demodEnabled || typeof event.data !== 'string' || !event.data.startsWith('{')) {
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (e) {
logError("Error in original WebSocket onmessage handler:", e);
}
}
return;
}
logDebug('Intercepting WebSocket message:', event.data.substring(0, 200) + '...');
try {
let json = JSON.parse(event.data);
if (json.conversation_id && json.conversation_id !== currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from WebSocket: ${currentConversationId}`);
}
const processedJson = await processPotentialModeration(json, 'WebSocket');
const newEvent = new MessageEvent('message', {
data: JSON.stringify(processedJson),
origin: event.origin,
lastEventId: event.lastEventId,
source: event.source,
ports: event.ports,
});
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, newEvent);
} catch (e) {
logError("Error calling original WebSocket onmessage handler after modification:", e);
}
} else {
logDebug("Original WebSocket onmessage handler not found when message received.");
}
} catch (e) {
logError('WebSocket processing error:', e, 'Data:', event.data.substring(0, 200) + '...');
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (eInner) {
logError("Error in original WebSocket onmessage handler (fallback path):", eInner);
}
}
}
};
ws.addEventListener('message', ws.onmessageinternal);
}
});
const wrapHandler = (eventName) => {
let originalHandler = null;
Object.defineProperty(ws, `on${eventName}`, {
configurable: true,
enumerable: true,
get() { return originalHandler; },
set(handler) {
logDebug(`WebSocket on${eventName} handler assigned`);
originalHandler = handler;
ws.addEventListener(eventName, (event) => {
if (eventName === 'message') return;
logDebug(`WebSocket event: ${eventName}`, event);
if (originalHandler) {
try {
originalHandler.call(ws, event);
} catch (e) {
logError(`Error in original WebSocket on${eventName} handler:`, e);
}
}
});
}
});
};
wrapHandler('close');
wrapHandler('error');
ws.addEventListener('open', () => logDebug('WebSocket opened:', url));
return ws;
}
});
if (window.location.hostname !== 'grok.com') {
console.log('[Grok DeMod] Script inactive: Intended for grok.com only. Current host:', window.location.hostname);
return;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupUI);
} else {
setupUI();
}
console.log('[Grok DeMod] Enhanced Script loaded. Interception is', demodEnabled ? 'ACTIVE' : 'INACTIVE', '. Debug is', debug ? 'ON' : 'OFF');
})();// ==UserScript==
// @name Grok DeMod
// @license GPL-3.0-or-later
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Hides moderation results in Grok conversations, auto-recovers blocked messages.
// @author UniverseDev
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @match https://grok.com/*
// @grant unsafeWindow
// @downloadURL https://update.greasyfork.org/scripts/531147/Grok DeMod.user.js
// @updateURL https://update.greasyfork.org/scripts/531147/Grok DeMod.meta.js
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
defaultFlags: [
'isFlagged', 'isBlocked', 'moderationApplied', 'restricted'
],
messageKeys: ['message', 'content', 'text', 'error'],
moderationMessagePatterns: [
/this content has been moderated/i,
/sorry, i cannot assist/i,
/policy violation/i,
/blocked/i,
/moderated/i,
/restricted/i,
/content restricted/i,
/unable to process/i,
/cannot help/i,
/(sorry|apologies).*?(cannot|unable|help|assist)/i,
],
clearedMessageText: '[Content cleared by Grok DeMod]',
recoveryTimeoutMs: 5000,
lsKeys: {
enabled: 'GrokDeModEnabled',
debug: 'GrokDeModDebug',
flags: 'GrokDeModFlags',
},
styles: {
uiContainer: `
position: fixed;
bottom: 10px;
right: 10px;
z-index: 10000;
background: #2d2d2d;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
gap: 8px;
font-family: Arial, sans-serif;
color: #e0e0e0;
min-width: 170px;
`,
button: `
padding: 6px 12px;
border-radius: 5px;
border: none;
cursor: pointer;
color: #fff;
font-size: 13px;
transition: background-color 0.2s ease;
`,
status: `
padding: 5px;
font-size: 12px;
color: #a0a0a0;
text-align: center;
border-top: 1px solid #444;
margin-top: 5px;
min-height: 16px;
`,
logContainer: `
max-height: 100px;
overflow-y: auto;
font-size: 11px;
color: #c0c0c0;
background-color: #333;
padding: 5px;
border-radius: 4px;
line-height: 1.4;
margin-top: 5px;
`,
logEntry: `
padding-bottom: 3px;
border-bottom: 1px dashed #555;
margin-bottom: 3px;
word-break: break-word;
`,
colors: {
enabled: '#388E3C',
disabled: '#D32F2F',
debugEnabled: '#1976D2',
debugDisabled: '#555555',
safe: '#66ff66',
flagged: '#ffa500',
blocked: '#ff6666',
recovering: '#ffcc00'
}
}
};
let demodEnabled = getState(CONFIG.lsKeys.enabled, true);
let debug = getState(CONFIG.lsKeys.debug, false);
let moderationFlags = getState(CONFIG.lsKeys.flags, CONFIG.defaultFlags);
let initCache = null;
let currentConversationId = null;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const uiLogBuffer = [];
const MAX_LOG_ENTRIES = 50;
const ModerationResult = Object.freeze({
SAFE: 0,
FLAGGE
1,
BLOCKE
2,
});
function logDebug(...args) {
if (debug) {
console.log('[Grok DeMod]', ...args);
}
}
function logError(...args) {
console.error('[Grok DeMod]', ...args);
}
function getState(key, defaultValue) {
try {
const value = localStorage.getItem(key);
if (value === null) return defaultValue;
if (value === 'true') return true;
if (value === 'false') return false;
return JSON.parse(value);
} catch (e) {
logError(`Error reading ${key} from localStorage:`, e);
return defaultValue;
}
}
function setState(key, value) {
try {
const valueToStore = typeof value === 'boolean' ? value.toString() : JSON.stringify(value);
localStorage.setItem(key, valueToStore);
} catch (e) {
logError(`Error writing ${key} to localStorage:`, e);
}
}
function timeoutPromise(ms, promise, description = 'Promise') {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
logDebug(`${description} timed out after ${ms}ms`);
reject(new Error(`Timeout (${description})`));
}, ms);
promise.then(
(value) => { clearTimeout(timer); resolve(value); },
(error) => { clearTimeout(timer); reject(error); }
);
});
}
function getModerationResult(obj, path = '') {
if (typeof obj !== 'object' || obj === null) return ModerationResult.SAFE;
let result = ModerationResult.SAFE;
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const currentPath = path ? `${path}.${key}` : key;
const value = obj[key];
if (key === 'isBlocked' && value === true) {
logDebug(`Blocked detected via flag '${currentPath}'`);
return ModerationResult.BLOCKED;
}
if (moderationFlags.includes(key) && value === true) {
logDebug(`Flagged detected via flag '${currentPath}'`);
result = Math.max(result, ModerationResult.FLAGGED);
}
if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
const content = value.toLowerCase();
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(content)) {
logDebug(`Moderation pattern matched in '${currentPath}': "${content.substring(0, 50)}..."`);
if (/blocked|moderated|restricted/i.test(pattern.source)) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, ModerationResult.FLAGGED);
break;
}
}
if (result === ModerationResult.SAFE && content.length < 70 && /(sorry|apologies|unable|cannot)/i.test(content)) {
logDebug(`Heuristic moderation detected in '${currentPath}': "${content.substring(0, 50)}..."`);
result = Math.max(result, ModerationResult.FLAGGED);
}
}
if (typeof value === 'object') {
const childResult = getModerationResult(value, currentPath);
if (childResult === ModerationResult.BLOCKED) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, childResult);
}
}
return result;
}
function clearFlagging(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(item => clearFlagging(item));
}
const newObj = {};
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key];
if (moderationFlags.includes(key) && value === true) {
newObj[key] = false;
logDebug(`Cleared flag '${key}'`);
}
else if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
let replaced = false;
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(value)) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced moderated message in '${key}' using pattern`);
replaced = true;
break;
}
}
if (!replaced && value.length < 70 && /(sorry|apologies|unable|cannot)/i.test(value.toLowerCase())) {
if (getModerationResult({[key]: value}) === ModerationResult.FLAGGED) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced heuristic moderated message in '${key}'`);
replaced = true;
}
}
if (!replaced) {
newObj[key] = value;
}
}
else if (typeof value === 'object') {
newObj[key] = clearFlagging(value);
}
else {
newObj[key] = value;
}
}
return newObj;
}
let uiContainer, toggleButton, debugButton, statusEl, logContainer;
function addLog(message) {
if (!logContainer) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.textContent = `[${timestamp}] ${message}`;
logEntry.style.cssText = CONFIG.styles.logEntry;
uiLogBuffer.push(logEntry);
if (uiLogBuffer.length > MAX_LOG_ENTRIES) {
const removed = uiLogBuffer.shift();
if (removed && removed.parentNode === logContainer) {
logContainer.removeChild(removed);
}
}
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(modResult, isRecovering = false) {
if (!statusEl) return;
let text = 'Status: ';
let color = CONFIG.styles.colors.safe;
if (isRecovering) {
text += 'Recovering...';
color = CONFIG.styles.colors.recovering;
} else if (modResult === ModerationResult.BLOCKED) {
text += 'Blocked (Recovered/Cleared)';
color = CONFIG.styles.colors.blocked;
} else if (modResult === ModerationResult.FLAGGED) {
text += 'Flagged (Cleared)';
color = CONFIG.styles.colors.flagged;
} else {
text += 'Safe';
color = CONFIG.styles.colors.safe;
}
statusEl.textContent = text;
statusEl.style.color = color;
}
function setupUI() {
uiContainer = document.createElement('div');
uiContainer.id = 'grok-demod-ui';
uiContainer.style.cssText = CONFIG.styles.uiContainer;
toggleButton = document.createElement('button');
debugButton = document.createElement('button');
statusEl = document.createElement('div');
logContainer = document.createElement('div');
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.title = 'Toggle DeMod functionality (ON = intercepting)';
toggleButton.style.cssText = CONFIG.styles.button;
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
toggleButton.onclick = (e) => {
demodEnabled = !demodEnabled;
setState(CONFIG.lsKeys.enabled, demodEnabled);
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
addLog(`DeMod ${demodEnabled ? 'Enabled' : 'Disabled'}.`);
console.log('[Grok DeMod] Interception is now', demodEnabled ? 'ACTIVE' : 'INACTIVE');
};
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.title = 'Toggle debug mode (logs verbose details to console)';
debugButton.style.cssText = CONFIG.styles.button;
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
debugButton.onclick = (e) => {
debug = !debug;
setState(CONFIG.lsKeys.debug, debug);
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
addLog(`Debug Mode ${debug ? 'Enabled' : 'Disabled'}.`);
logDebug(`Debug mode ${debug ? 'enabled' : 'disabled'}.`);
};
statusEl.id = 'grok-demod-status';
statusEl.style.cssText = CONFIG.styles.status;
updateStatus(ModerationResult.SAFE);
logContainer.id = 'grok-demod-log';
logContainer.style.cssText = CONFIG.styles.logContainer;
uiLogBuffer.forEach(entry => logContainer.appendChild(entry));
logContainer.scrollTop = logContainer.scrollHeight;
uiContainer.appendChild(toggleButton);
uiContainer.appendChild(debugButton);
uiContainer.appendChild(statusEl);
uiContainer.appendChild(logContainer);
document.body.appendChild(uiContainer);
addLog("Grok DeMod Initialized.");
if (debug) addLog("Debug mode is ON.");
}
async function redownloadLatestMessage() {
if (!currentConversationId) {
logDebug('Recovery skipped: Missing conversationId');
addLog('Recovery failed: No conversation ID.');
return null;
}
if (!initCache || !initCache.headers) {
logDebug('Recovery cache missing, attempting fresh fetch for headers...');
try {
const currentConvUrl = `/rest/app-chat/conversation/${currentConversationId}`;
const tempResp = await originalFetch(currentConvUrl, { method: 'GET', headers: {'Accept': 'application/json'} });
if (tempResp.ok) {
logDebug('Fresh header fetch successful (status OK).');
initCache = { headers: new Headers({'Accept': 'application/json'}), credentials: 'include' };
} else {
logDebug(`Fresh header fetch failed with status ${tempResp.status}. Recovery may fail.`);
addLog('Recovery failed: Cannot get request data.');
return null;
}
} catch (e) {
logError('Error during fresh header fetch:', e);
addLog('Recovery failed: Error getting request data.');
return null;
}
}
const url = `/rest/app-chat/conversation/${currentConversationId}`;
logDebug(`Attempting recovery fetch for conversation: ${currentConversationId}`);
addLog('Attempting content recovery...');
const headers = new Headers(initCache.headers);
if (!headers.has('Accept')) headers.set('Accept', 'application/json, text/plain, */*');
const requestOptions = {
method: 'GET',
headers: headers,
credentials: initCache.credentials || 'include',
};
try {
const response = await timeoutPromise(
CONFIG.recoveryTimeoutMs,
fetch(url, requestOptions),
'Recovery Fetch'
);
if (!response.ok) {
logError(`Recovery fetch failed with status ${response.status}: ${response.statusText}`);
addLog(`Recovery failed: HTTP ${response.status}`);
try {
const errorBody = await response.text();
logDebug('Recovery error body:', errorBody.substring(0, 500));
} catch (e) { }
return null;
}
const data = await response.json();
const messages = data?.messages;
if (!Array.isArray(messages) || messages.length === 0) {
logDebug('Recovery failed: No messages found in conversation data', data);
addLog('Recovery failed: No messages found.');
return null;
}
messages.sort((a, b) => {
const tsA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
const tsB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
return tsB - tsA;
});
const latestMessage = messages[0];
if (!latestMessage || typeof latestMessage.content !== 'string' || latestMessage.content.trim() === '') {
logDebug('Recovery failed: Latest message or its content is invalid/empty', latestMessage);
addLog('Recovery failed: Invalid latest message.');
return null;
}
logDebug('Recovery successful, latest content:', latestMessage.content.substring(0, 100) + '...');
addLog('Recovery seems successful.');
return { content: latestMessage.content };
} catch (e) {
logError('Recovery fetch/parse error:', e);
addLog(`Recovery error: ${e.message}`);
return null;
}
}
function extractConversationIdFromUrl(url) {
const match = url.match(/\/conversation\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
return match ? match[1] : null;
}
async function processPotentialModeration(json, source) {
const modResult = getModerationResult(json);
let finalJson = json;
if (modResult !== ModerationResult.SAFE) {
if (modResult === ModerationResult.BLOCKED) {
logDebug(`Blocked content detected from ${source}:`, JSON.stringify(json).substring(0, 200) + '...');
addLog(`Blocked content from ${source}.`);
updateStatus(modResult, true);
const recoveredData = await redownloadLatestMessage();
if (recoveredData && recoveredData.content) {
addLog(`Recovery successful (${source}).`);
logDebug(`Recovered content applied (${source})`);
let replaced = false;
const keysToTry = [...CONFIG.messageKeys, 'text', 'message'];
for (const key of keysToTry) {
if (typeof finalJson[key] === 'string') {
finalJson[key] = recoveredData.content;
logDebug(`Injected recovered content into key '${key}'`);
replaced = true;
break;
}
}
if (!replaced) {
logDebug("Could not find standard key to inject recovered content, adding as 'recovered_content'");
finalJson.recovered_content = recoveredData.content;
}
finalJson = clearFlagging(finalJson);
updateStatus(modResult, false);
} else {
addLog(`Recovery failed (${source}). Content may be lost.`);
logDebug(`Recovery failed (${source}), applying standard clearing.`);
finalJson = clearFlagging(json);
updateStatus(modResult, false);
}
} else {
logDebug(`Flagged content detected and cleared from ${source}.`);
addLog(`Flagged content cleared (${source}).`);
finalJson = clearFlagging(json);
updateStatus(modResult);
}
} else {
if (statusEl && !statusEl.textContent.includes('Blocked') && !statusEl.textContent.includes('Flagged') && !statusEl.textContent.includes('Recovering')) {
updateStatus(modResult);
} else if (statusEl && statusEl.textContent.includes('Recovering')) {
logDebug("Recovery attempt finished (next message safe). Resetting status.");
updateStatus(ModerationResult.SAFE);
}
}
return finalJson;
}
async function handleFetchResponse(original_response, url, requestArgs) {
const response = original_response.clone();
if (!response.ok) {
logDebug(`Fetch response not OK (${response.status}) for ${url}, skipping processing.`);
return original_response;
}
const contentType = response.headers.get('Content-Type')?.toLowerCase() || '';
logDebug(`Intercepted fetch response for ${url}, Content-Type: ${contentType}`);
const conversationGetMatch = url.match(/\/rest\/app-chat\/conversation\/([a-f0-9-]+)$/i);
if (conversationGetMatch && requestArgs?.method === 'GET') {
logDebug(`Caching GET request options for conversation ${conversationGetMatch[1]}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
if (!currentConversationId) {
currentConversationId = conversationGetMatch[1];
logDebug(`Conversation ID set from GET URL: ${currentConversationId}`);
}
}
if (!currentConversationId) {
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from other URL: ${currentConversationId}`);
}
}
if (contentType.includes('text/event-stream')) {
logDebug(`Processing SSE stream for ${url}`);
const reader = response.body.getReader();
const stream = new ReadableStream({
async start(controller) {
let buffer = '';
let currentEvent = { data: '', type: 'message', id: null };
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
logDebug("SSE stream ended, processing final buffer:", buffer);
if (buffer.startsWith('{') || buffer.startsWith('[')) {
try {
let json = JSON.parse(buffer);
json = await processPotentialModeration(json, 'SSE-Final');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logDebug("Error parsing final SSE buffer, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else {
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else if (currentEvent.data) {
logDebug("SSE stream ended after data field, processing event:", currentEvent.data.substring(0,100)+"...");
try {
let json = JSON.parse(currentEvent.data);
json = await processPotentialModeration(json, 'SSE-Event');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch (e) {
logDebug("Error parsing trailing SSE data, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
controller.close();
break;
}
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') {
if (currentEvent.data) {
logDebug("Processing SSE event data:", currentEvent.data.substring(0, 100) + '...');
if (currentEvent.data.startsWith('{') || currentEvent.data.startsWith('[')) {
try {
let json = JSON.parse(currentEvent.data);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from SSE data: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'SSE');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logError("SSE JSON parse error:", e, "Data:", currentEvent.data.substring(0,200)+"...");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
} else {
logDebug("SSE data is not JSON, forwarding as is.");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
currentEvent = { data: '', type: 'message', id: null };
} else if (line.startsWith('data:')) {
currentEvent.data += (currentEvent.data ? '\n' : '') + line.substring(5).trim();
} else if (line.startsWith('event:')) {
currentEvent.type = line.substring(6).trim();
} else if (line.startsWith('id:')) {
currentEvent.id = line.substring(3).trim();
} else if (line.startsWith(':')) {
} else {
logDebug("Unknown SSE line:", line);
}
}
}
} catch (e) {
logError('Error reading/processing SSE stream:', e);
controller.error(e);
} finally {
reader.releaseLock();
}
}
});
const newHeaders = new Headers(response.headers);
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
if (contentType.includes('application/json')) {
logDebug(`Processing JSON response for ${url}`);
try {
const text = await response.text();
let json = JSON.parse(text);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from JSON response: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'Fetch');
const newBody = JSON.stringify(json);
const newHeaders = new Headers(response.headers);
if (newHeaders.has('content-length')) {
newHeaders.set('content-length', encoder.encode(newBody).byteLength.toString());
}
return new Response(newBody, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
} catch (e) {
logError('Fetch JSON processing error:', e, 'URL:', url);
return original_response;
}
}
logDebug(`Non-SSE/JSON response for ${url}, skipping processing.`);
return original_response;
}
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function(input, init) {
if (!demodEnabled) {
return originalFetch.apply(this, arguments);
}
let url;
let requestArgs = init || {};
try {
url = (input instanceof Request) ? input.url : String(input);
} catch (e) {
logDebug('Invalid fetch input, passing through:', input, e);
return originalFetch.apply(this, arguments);
}
if (!url.includes('/rest/app-chat/')) {
return originalFetch.apply(this, arguments);
}
if (requestArgs.method === 'POST') {
logDebug(`Observing POST request: ${url}`);
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
if (!currentConversationId) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from POST URL: ${currentConversationId}`);
}
if (!initCache && requestArgs.headers) {
logDebug(`Caching headers from POST request to ${idFromUrl}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
}
}
return originalFetch.apply(this, arguments);
}
logDebug(`Intercepting fetch request: ${requestArgs.method || 'GET'} ${url}`);
try {
const original_response = await originalFetch.apply(this, arguments);
return await handleFetchResponse(original_response, url, requestArgs);
} catch (error) {
logError(`Fetch interception failed for ${url}:`, error);
throw error;
}
};
const OriginalWebSocket = unsafeWindow.WebSocket;
unsafeWindow.WebSocket = new Proxy(OriginalWebSocket, {
construct(target, args) {
const url = args[0];
logDebug('WebSocket connection attempt:', url);
const ws = new target(...args);
let originalOnMessageHandler = null;
Object.defineProperty(ws, 'onmessage', {
configurable: true,
enumerable: true,
get() {
return originalOnMessageHandler;
},
async set(handler) {
logDebug('WebSocket onmessage handler assigned');
originalOnMessageHandler = handler;
ws.onmessageinternal = async function(event) {
if (!demodEnabled || typeof event.data !== 'string' || !event.data.startsWith('{')) {
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (e) {
logError("Error in original WebSocket onmessage handler:", e);
}
}
return;
}
logDebug('Intercepting WebSocket message:', event.data.substring(0, 200) + '...');
try {
let json = JSON.parse(event.data);
if (json.conversation_id && json.conversation_id !== currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from WebSocket: ${currentConversationId}`);
}
const processedJson = await processPotentialModeration(json, 'WebSocket');
const newEvent = new MessageEvent('message', {
data: JSON.stringify(processedJson),
origin: event.origin,
lastEventId: event.lastEventId,
source: event.source,
ports: event.ports,
});
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, newEvent);
} catch (e) {
logError("Error calling original WebSocket onmessage handler after modification:", e);
}
} else {
logDebug("Original WebSocket onmessage handler not found when message received.");
}
} catch (e) {
logError('WebSocket processing error:', e, 'Data:', event.data.substring(0, 200) + '...');
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (eInner) {
logError("Error in original WebSocket onmessage handler (fallback path):", eInner);
}
}
}
};
ws.addEventListener('message', ws.onmessageinternal);
}
});
const wrapHandler = (eventName) => {
let originalHandler = null;
Object.defineProperty(ws, `on${eventName}`, {
configurable: true,
enumerable: true,
get() { return originalHandler; },
set(handler) {
logDebug(`WebSocket on${eventName} handler assigned`);
originalHandler = handler;
ws.addEventListener(eventName, (event) => {
if (eventName === 'message') return;
logDebug(`WebSocket event: ${eventName}`, event);
if (originalHandler) {
try {
originalHandler.call(ws, event);
} catch (e) {
logError(`Error in original WebSocket on${eventName} handler:`, e);
}
}
});
}
});
};
wrapHandler('close');
wrapHandler('error');
ws.addEventListener('open', () => logDebug('WebSocket opened:', url));
return ws;
}
});
if (window.location.hostname !== 'grok.com') {
console.log('[Grok DeMod] Script inactive: Intended for grok.com only. Current host:', window.location.hostname);
return;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupUI);
} else {
setupUI();
}
console.log('[Grok DeMod] Enhanced Script loaded. Interception is', demodEnabled ? 'ACTIVE' : 'INACTIVE', '. Debug is', debug ? 'ON' : 'OFF');
})();// ==UserScript==
// @name Grok DeMod
// @license GPL-3.0-or-later
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Hides moderation results in Grok conversations, auto-recovers blocked messages.
// @author UniverseDev
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @match https://grok.com/*
// @grant unsafeWindow
// @downloadURL https://update.greasyfork.org/scripts/531147/Grok DeMod.user.js
// @updateURL https://update.greasyfork.org/scripts/531147/Grok DeMod.meta.js
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
defaultFlags: [
'isFlagged', 'isBlocked', 'moderationApplied', 'restricted'
],
messageKeys: ['message', 'content', 'text', 'error'],
moderationMessagePatterns: [
/this content has been moderated/i,
/sorry, i cannot assist/i,
/policy violation/i,
/blocked/i,
/moderated/i,
/restricted/i,
/content restricted/i,
/unable to process/i,
/cannot help/i,
/(sorry|apologies).*?(cannot|unable|help|assist)/i,
],
clearedMessageText: '[Content cleared by Grok DeMod]',
recoveryTimeoutMs: 5000,
lsKeys: {
enabled: 'GrokDeModEnabled',
debug: 'GrokDeModDebug',
flags: 'GrokDeModFlags',
},
styles: {
uiContainer: `
position: fixed;
bottom: 10px;
right: 10px;
z-index: 10000;
background: #2d2d2d;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
gap: 8px;
font-family: Arial, sans-serif;
color: #e0e0e0;
min-width: 170px;
`,
button: `
padding: 6px 12px;
border-radius: 5px;
border: none;
cursor: pointer;
color: #fff;
font-size: 13px;
transition: background-color 0.2s ease;
`,
status: `
padding: 5px;
font-size: 12px;
color: #a0a0a0;
text-align: center;
border-top: 1px solid #444;
margin-top: 5px;
min-height: 16px;
`,
logContainer: `
max-height: 100px;
overflow-y: auto;
font-size: 11px;
color: #c0c0c0;
background-color: #333;
padding: 5px;
border-radius: 4px;
line-height: 1.4;
margin-top: 5px;
`,
logEntry: `
padding-bottom: 3px;
border-bottom: 1px dashed #555;
margin-bottom: 3px;
word-break: break-word;
`,
colors: {
enabled: '#388E3C',
disabled: '#D32F2F',
debugEnabled: '#1976D2',
debugDisabled: '#555555',
safe: '#66ff66',
flagged: '#ffa500',
blocked: '#ff6666',
recovering: '#ffcc00'
}
}
};
let demodEnabled = getState(CONFIG.lsKeys.enabled, true);
let debug = getState(CONFIG.lsKeys.debug, false);
let moderationFlags = getState(CONFIG.lsKeys.flags, CONFIG.defaultFlags);
let initCache = null;
let currentConversationId = null;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const uiLogBuffer = [];
const MAX_LOG_ENTRIES = 50;
const ModerationResult = Object.freeze({
SAFE: 0,
FLAGGE
1,
BLOCKE
2,
});
function logDebug(...args) {
if (debug) {
console.log('[Grok DeMod]', ...args);
}
}
function logError(...args) {
console.error('[Grok DeMod]', ...args);
}
function getState(key, defaultValue) {
try {
const value = localStorage.getItem(key);
if (value === null) return defaultValue;
if (value === 'true') return true;
if (value === 'false') return false;
return JSON.parse(value);
} catch (e) {
logError(`Error reading ${key} from localStorage:`, e);
return defaultValue;
}
}
function setState(key, value) {
try {
const valueToStore = typeof value === 'boolean' ? value.toString() : JSON.stringify(value);
localStorage.setItem(key, valueToStore);
} catch (e) {
logError(`Error writing ${key} to localStorage:`, e);
}
}
function timeoutPromise(ms, promise, description = 'Promise') {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
logDebug(`${description} timed out after ${ms}ms`);
reject(new Error(`Timeout (${description})`));
}, ms);
promise.then(
(value) => { clearTimeout(timer); resolve(value); },
(error) => { clearTimeout(timer); reject(error); }
);
});
}
function getModerationResult(obj, path = '') {
if (typeof obj !== 'object' || obj === null) return ModerationResult.SAFE;
let result = ModerationResult.SAFE;
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const currentPath = path ? `${path}.${key}` : key;
const value = obj[key];
if (key === 'isBlocked' && value === true) {
logDebug(`Blocked detected via flag '${currentPath}'`);
return ModerationResult.BLOCKED;
}
if (moderationFlags.includes(key) && value === true) {
logDebug(`Flagged detected via flag '${currentPath}'`);
result = Math.max(result, ModerationResult.FLAGGED);
}
if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
const content = value.toLowerCase();
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(content)) {
logDebug(`Moderation pattern matched in '${currentPath}': "${content.substring(0, 50)}..."`);
if (/blocked|moderated|restricted/i.test(pattern.source)) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, ModerationResult.FLAGGED);
break;
}
}
if (result === ModerationResult.SAFE && content.length < 70 && /(sorry|apologies|unable|cannot)/i.test(content)) {
logDebug(`Heuristic moderation detected in '${currentPath}': "${content.substring(0, 50)}..."`);
result = Math.max(result, ModerationResult.FLAGGED);
}
}
if (typeof value === 'object') {
const childResult = getModerationResult(value, currentPath);
if (childResult === ModerationResult.BLOCKED) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, childResult);
}
}
return result;
}
function clearFlagging(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(item => clearFlagging(item));
}
const newObj = {};
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key];
if (moderationFlags.includes(key) && value === true) {
newObj[key] = false;
logDebug(`Cleared flag '${key}'`);
}
else if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
let replaced = false;
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(value)) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced moderated message in '${key}' using pattern`);
replaced = true;
break;
}
}
if (!replaced && value.length < 70 && /(sorry|apologies|unable|cannot)/i.test(value.toLowerCase())) {
if (getModerationResult({[key]: value}) === ModerationResult.FLAGGED) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced heuristic moderated message in '${key}'`);
replaced = true;
}
}
if (!replaced) {
newObj[key] = value;
}
}
else if (typeof value === 'object') {
newObj[key] = clearFlagging(value);
}
else {
newObj[key] = value;
}
}
return newObj;
}
let uiContainer, toggleButton, debugButton, statusEl, logContainer;
function addLog(message) {
if (!logContainer) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.textContent = `[${timestamp}] ${message}`;
logEntry.style.cssText = CONFIG.styles.logEntry;
uiLogBuffer.push(logEntry);
if (uiLogBuffer.length > MAX_LOG_ENTRIES) {
const removed = uiLogBuffer.shift();
if (removed && removed.parentNode === logContainer) {
logContainer.removeChild(removed);
}
}
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(modResult, isRecovering = false) {
if (!statusEl) return;
let text = 'Status: ';
let color = CONFIG.styles.colors.safe;
if (isRecovering) {
text += 'Recovering...';
color = CONFIG.styles.colors.recovering;
} else if (modResult === ModerationResult.BLOCKED) {
text += 'Blocked (Recovered/Cleared)';
color = CONFIG.styles.colors.blocked;
} else if (modResult === ModerationResult.FLAGGED) {
text += 'Flagged (Cleared)';
color = CONFIG.styles.colors.flagged;
} else {
text += 'Safe';
color = CONFIG.styles.colors.safe;
}
statusEl.textContent = text;
statusEl.style.color = color;
}
function setupUI() {
uiContainer = document.createElement('div');
uiContainer.id = 'grok-demod-ui';
uiContainer.style.cssText = CONFIG.styles.uiContainer;
toggleButton = document.createElement('button');
debugButton = document.createElement('button');
statusEl = document.createElement('div');
logContainer = document.createElement('div');
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.title = 'Toggle DeMod functionality (ON = intercepting)';
toggleButton.style.cssText = CONFIG.styles.button;
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
toggleButton.onclick = (e) => {
demodEnabled = !demodEnabled;
setState(CONFIG.lsKeys.enabled, demodEnabled);
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
addLog(`DeMod ${demodEnabled ? 'Enabled' : 'Disabled'}.`);
console.log('[Grok DeMod] Interception is now', demodEnabled ? 'ACTIVE' : 'INACTIVE');
};
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.title = 'Toggle debug mode (logs verbose details to console)';
debugButton.style.cssText = CONFIG.styles.button;
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
debugButton.onclick = (e) => {
debug = !debug;
setState(CONFIG.lsKeys.debug, debug);
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
addLog(`Debug Mode ${debug ? 'Enabled' : 'Disabled'}.`);
logDebug(`Debug mode ${debug ? 'enabled' : 'disabled'}.`);
};
statusEl.id = 'grok-demod-status';
statusEl.style.cssText = CONFIG.styles.status;
updateStatus(ModerationResult.SAFE);
logContainer.id = 'grok-demod-log';
logContainer.style.cssText = CONFIG.styles.logContainer;
uiLogBuffer.forEach(entry => logContainer.appendChild(entry));
logContainer.scrollTop = logContainer.scrollHeight;
uiContainer.appendChild(toggleButton);
uiContainer.appendChild(debugButton);
uiContainer.appendChild(statusEl);
uiContainer.appendChild(logContainer);
document.body.appendChild(uiContainer);
addLog("Grok DeMod Initialized.");
if (debug) addLog("Debug mode is ON.");
}
async function redownloadLatestMessage() {
if (!currentConversationId) {
logDebug('Recovery skipped: Missing conversationId');
addLog('Recovery failed: No conversation ID.');
return null;
}
if (!initCache || !initCache.headers) {
logDebug('Recovery cache missing, attempting fresh fetch for headers...');
try {
const currentConvUrl = `/rest/app-chat/conversation/${currentConversationId}`;
const tempResp = await originalFetch(currentConvUrl, { method: 'GET', headers: {'Accept': 'application/json'} });
if (tempResp.ok) {
logDebug('Fresh header fetch successful (status OK).');
initCache = { headers: new Headers({'Accept': 'application/json'}), credentials: 'include' };
} else {
logDebug(`Fresh header fetch failed with status ${tempResp.status}. Recovery may fail.`);
addLog('Recovery failed: Cannot get request data.');
return null;
}
} catch (e) {
logError('Error during fresh header fetch:', e);
addLog('Recovery failed: Error getting request data.');
return null;
}
}
const url = `/rest/app-chat/conversation/${currentConversationId}`;
logDebug(`Attempting recovery fetch for conversation: ${currentConversationId}`);
addLog('Attempting content recovery...');
const headers = new Headers(initCache.headers);
if (!headers.has('Accept')) headers.set('Accept', 'application/json, text/plain, */*');
const requestOptions = {
method: 'GET',
headers: headers,
credentials: initCache.credentials || 'include',
};
try {
const response = await timeoutPromise(
CONFIG.recoveryTimeoutMs,
fetch(url, requestOptions),
'Recovery Fetch'
);
if (!response.ok) {
logError(`Recovery fetch failed with status ${response.status}: ${response.statusText}`);
addLog(`Recovery failed: HTTP ${response.status}`);
try {
const errorBody = await response.text();
logDebug('Recovery error body:', errorBody.substring(0, 500));
} catch (e) { }
return null;
}
const data = await response.json();
const messages = data?.messages;
if (!Array.isArray(messages) || messages.length === 0) {
logDebug('Recovery failed: No messages found in conversation data', data);
addLog('Recovery failed: No messages found.');
return null;
}
messages.sort((a, b) => {
const tsA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
const tsB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
return tsB - tsA;
});
const latestMessage = messages[0];
if (!latestMessage || typeof latestMessage.content !== 'string' || latestMessage.content.trim() === '') {
logDebug('Recovery failed: Latest message or its content is invalid/empty', latestMessage);
addLog('Recovery failed: Invalid latest message.');
return null;
}
logDebug('Recovery successful, latest content:', latestMessage.content.substring(0, 100) + '...');
addLog('Recovery seems successful.');
return { content: latestMessage.content };
} catch (e) {
logError('Recovery fetch/parse error:', e);
addLog(`Recovery error: ${e.message}`);
return null;
}
}
function extractConversationIdFromUrl(url) {
const match = url.match(/\/conversation\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
return match ? match[1] : null;
}
async function processPotentialModeration(json, source) {
const modResult = getModerationResult(json);
let finalJson = json;
if (modResult !== ModerationResult.SAFE) {
if (modResult === ModerationResult.BLOCKED) {
logDebug(`Blocked content detected from ${source}:`, JSON.stringify(json).substring(0, 200) + '...');
addLog(`Blocked content from ${source}.`);
updateStatus(modResult, true);
const recoveredData = await redownloadLatestMessage();
if (recoveredData && recoveredData.content) {
addLog(`Recovery successful (${source}).`);
logDebug(`Recovered content applied (${source})`);
let replaced = false;
const keysToTry = [...CONFIG.messageKeys, 'text', 'message'];
for (const key of keysToTry) {
if (typeof finalJson[key] === 'string') {
finalJson[key] = recoveredData.content;
logDebug(`Injected recovered content into key '${key}'`);
replaced = true;
break;
}
}
if (!replaced) {
logDebug("Could not find standard key to inject recovered content, adding as 'recovered_content'");
finalJson.recovered_content = recoveredData.content;
}
finalJson = clearFlagging(finalJson);
updateStatus(modResult, false);
} else {
addLog(`Recovery failed (${source}). Content may be lost.`);
logDebug(`Recovery failed (${source}), applying standard clearing.`);
finalJson = clearFlagging(json);
updateStatus(modResult, false);
}
} else {
logDebug(`Flagged content detected and cleared from ${source}.`);
addLog(`Flagged content cleared (${source}).`);
finalJson = clearFlagging(json);
updateStatus(modResult);
}
} else {
if (statusEl && !statusEl.textContent.includes('Blocked') && !statusEl.textContent.includes('Flagged') && !statusEl.textContent.includes('Recovering')) {
updateStatus(modResult);
} else if (statusEl && statusEl.textContent.includes('Recovering')) {
logDebug("Recovery attempt finished (next message safe). Resetting status.");
updateStatus(ModerationResult.SAFE);
}
}
return finalJson;
}
async function handleFetchResponse(original_response, url, requestArgs) {
const response = original_response.clone();
if (!response.ok) {
logDebug(`Fetch response not OK (${response.status}) for ${url}, skipping processing.`);
return original_response;
}
const contentType = response.headers.get('Content-Type')?.toLowerCase() || '';
logDebug(`Intercepted fetch response for ${url}, Content-Type: ${contentType}`);
const conversationGetMatch = url.match(/\/rest\/app-chat\/conversation\/([a-f0-9-]+)$/i);
if (conversationGetMatch && requestArgs?.method === 'GET') {
logDebug(`Caching GET request options for conversation ${conversationGetMatch[1]}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
if (!currentConversationId) {
currentConversationId = conversationGetMatch[1];
logDebug(`Conversation ID set from GET URL: ${currentConversationId}`);
}
}
if (!currentConversationId) {
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from other URL: ${currentConversationId}`);
}
}
if (contentType.includes('text/event-stream')) {
logDebug(`Processing SSE stream for ${url}`);
const reader = response.body.getReader();
const stream = new ReadableStream({
async start(controller) {
let buffer = '';
let currentEvent = { data: '', type: 'message', id: null };
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
logDebug("SSE stream ended, processing final buffer:", buffer);
if (buffer.startsWith('{') || buffer.startsWith('[')) {
try {
let json = JSON.parse(buffer);
json = await processPotentialModeration(json, 'SSE-Final');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logDebug("Error parsing final SSE buffer, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else {
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else if (currentEvent.data) {
logDebug("SSE stream ended after data field, processing event:", currentEvent.data.substring(0,100)+"...");
try {
let json = JSON.parse(currentEvent.data);
json = await processPotentialModeration(json, 'SSE-Event');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch (e) {
logDebug("Error parsing trailing SSE data, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
controller.close();
break;
}
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') {
if (currentEvent.data) {
logDebug("Processing SSE event data:", currentEvent.data.substring(0, 100) + '...');
if (currentEvent.data.startsWith('{') || currentEvent.data.startsWith('[')) {
try {
let json = JSON.parse(currentEvent.data);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from SSE data: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'SSE');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logError("SSE JSON parse error:", e, "Data:", currentEvent.data.substring(0,200)+"...");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
} else {
logDebug("SSE data is not JSON, forwarding as is.");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
currentEvent = { data: '', type: 'message', id: null };
} else if (line.startsWith('data:')) {
currentEvent.data += (currentEvent.data ? '\n' : '') + line.substring(5).trim();
} else if (line.startsWith('event:')) {
currentEvent.type = line.substring(6).trim();
} else if (line.startsWith('id:')) {
currentEvent.id = line.substring(3).trim();
} else if (line.startsWith(':')) {
} else {
logDebug("Unknown SSE line:", line);
}
}
}
} catch (e) {
logError('Error reading/processing SSE stream:', e);
controller.error(e);
} finally {
reader.releaseLock();
}
}
});
const newHeaders = new Headers(response.headers);
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
if (contentType.includes('application/json')) {
logDebug(`Processing JSON response for ${url}`);
try {
const text = await response.text();
let json = JSON.parse(text);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from JSON response: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'Fetch');
const newBody = JSON.stringify(json);
const newHeaders = new Headers(response.headers);
if (newHeaders.has('content-length')) {
newHeaders.set('content-length', encoder.encode(newBody).byteLength.toString());
}
return new Response(newBody, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
} catch (e) {
logError('Fetch JSON processing error:', e, 'URL:', url);
return original_response;
}
}
logDebug(`Non-SSE/JSON response for ${url}, skipping processing.`);
return original_response;
}
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function(input, init) {
if (!demodEnabled) {
return originalFetch.apply(this, arguments);
}
let url;
let requestArgs = init || {};
try {
url = (input instanceof Request) ? input.url : String(input);
} catch (e) {
logDebug('Invalid fetch input, passing through:', input, e);
return originalFetch.apply(this, arguments);
}
if (!url.includes('/rest/app-chat/')) {
return originalFetch.apply(this, arguments);
}
if (requestArgs.method === 'POST') {
logDebug(`Observing POST request: ${url}`);
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
if (!currentConversationId) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from POST URL: ${currentConversationId}`);
}
if (!initCache && requestArgs.headers) {
logDebug(`Caching headers from POST request to ${idFromUrl}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
}
}
return originalFetch.apply(this, arguments);
}
logDebug(`Intercepting fetch request: ${requestArgs.method || 'GET'} ${url}`);
try {
const original_response = await originalFetch.apply(this, arguments);
return await handleFetchResponse(original_response, url, requestArgs);
} catch (error) {
logError(`Fetch interception failed for ${url}:`, error);
throw error;
}
};
const OriginalWebSocket = unsafeWindow.WebSocket;
unsafeWindow.WebSocket = new Proxy(OriginalWebSocket, {
construct(target, args) {
const url = args[0];
logDebug('WebSocket connection attempt:', url);
const ws = new target(...args);
let originalOnMessageHandler = null;
Object.defineProperty(ws, 'onmessage', {
configurable: true,
enumerable: true,
get() {
return originalOnMessageHandler;
},
async set(handler) {
logDebug('WebSocket onmessage handler assigned');
originalOnMessageHandler = handler;
ws.onmessageinternal = async function(event) {
if (!demodEnabled || typeof event.data !== 'string' || !event.data.startsWith('{')) {
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (e) {
logError("Error in original WebSocket onmessage handler:", e);
}
}
return;
}
logDebug('Intercepting WebSocket message:', event.data.substring(0, 200) + '...');
try {
let json = JSON.parse(event.data);
if (json.conversation_id && json.conversation_id !== currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from WebSocket: ${currentConversationId}`);
}
const processedJson = await processPotentialModeration(json, 'WebSocket');
const newEvent = new MessageEvent('message', {
data: JSON.stringify(processedJson),
origin: event.origin,
lastEventId: event.lastEventId,
source: event.source,
ports: event.ports,
});
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, newEvent);
} catch (e) {
logError("Error calling original WebSocket onmessage handler after modification:", e);
}
} else {
logDebug("Original WebSocket onmessage handler not found when message received.");
}
} catch (e) {
logError('WebSocket processing error:', e, 'Data:', event.data.substring(0, 200) + '...');
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (eInner) {
logError("Error in original WebSocket onmessage handler (fallback path):", eInner);
}
}
}
};
ws.addEventListener('message', ws.onmessageinternal);
}
});
const wrapHandler = (eventName) => {
let originalHandler = null;
Object.defineProperty(ws, `on${eventName}`, {
configurable: true,
enumerable: true,
get() { return originalHandler; },
set(handler) {
logDebug(`WebSocket on${eventName} handler assigned`);
originalHandler = handler;
ws.addEventListener(eventName, (event) => {
if (eventName === 'message') return;
logDebug(`WebSocket event: ${eventName}`, event);
if (originalHandler) {
try {
originalHandler.call(ws, event);
} catch (e) {
logError(`Error in original WebSocket on${eventName} handler:`, e);
}
}
});
}
});
};
wrapHandler('close');
wrapHandler('error');
ws.addEventListener('open', () => logDebug('WebSocket opened:', url));
return ws;
}
});
if (window.location.hostname !== 'grok.com') {
console.log('[Grok DeMod] Script inactive: Intended for grok.com only. Current host:', window.location.hostname);
return;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupUI);
} else {
setupUI();
}
console.log('[Grok DeMod] Enhanced Script loaded. Interception is', demodEnabled ? 'ACTIVE' : 'INACTIVE', '. Debug is', debug ? 'ON' : 'OFF');
})();// ==UserScript==
// @name Grok DeMod
// @license GPL-3.0-or-later
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Hides moderation results in Grok conversations, auto-recovers blocked messages.
// @author UniverseDev
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @match https://grok.com/*
// @grant unsafeWindow
// @downloadURL https://update.greasyfork.org/scripts/531147/Grok DeMod.user.js
// @updateURL https://update.greasyfork.org/scripts/531147/Grok DeMod.meta.js
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
defaultFlags: [
'isFlagged', 'isBlocked', 'moderationApplied', 'restricted'
],
messageKeys: ['message', 'content', 'text', 'error'],
moderationMessagePatterns: [
/this content has been moderated/i,
/sorry, i cannot assist/i,
/policy violation/i,
/blocked/i,
/moderated/i,
/restricted/i,
/content restricted/i,
/unable to process/i,
/cannot help/i,
/(sorry|apologies).*?(cannot|unable|help|assist)/i,
],
clearedMessageText: '[Content cleared by Grok DeMod]',
recoveryTimeoutMs: 5000,
lsKeys: {
enabled: 'GrokDeModEnabled',
debug: 'GrokDeModDebug',
flags: 'GrokDeModFlags',
},
styles: {
uiContainer: `
position: fixed;
bottom: 10px;
right: 10px;
z-index: 10000;
background: #2d2d2d;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
gap: 8px;
font-family: Arial, sans-serif;
color: #e0e0e0;
min-width: 170px;
`,
button: `
padding: 6px 12px;
border-radius: 5px;
border: none;
cursor: pointer;
color: #fff;
font-size: 13px;
transition: background-color 0.2s ease;
`,
status: `
padding: 5px;
font-size: 12px;
color: #a0a0a0;
text-align: center;
border-top: 1px solid #444;
margin-top: 5px;
min-height: 16px;
`,
logContainer: `
max-height: 100px;
overflow-y: auto;
font-size: 11px;
color: #c0c0c0;
background-color: #333;
padding: 5px;
border-radius: 4px;
line-height: 1.4;
margin-top: 5px;
`,
logEntry: `
padding-bottom: 3px;
border-bottom: 1px dashed #555;
margin-bottom: 3px;
word-break: break-word;
`,
colors: {
enabled: '#388E3C',
disabled: '#D32F2F',
debugEnabled: '#1976D2',
debugDisabled: '#555555',
safe: '#66ff66',
flagged: '#ffa500',
blocked: '#ff6666',
recovering: '#ffcc00'
}
}
};
let demodEnabled = getState(CONFIG.lsKeys.enabled, true);
let debug = getState(CONFIG.lsKeys.debug, false);
let moderationFlags = getState(CONFIG.lsKeys.flags, CONFIG.defaultFlags);
let initCache = null;
let currentConversationId = null;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const uiLogBuffer = [];
const MAX_LOG_ENTRIES = 50;
const ModerationResult = Object.freeze({
SAFE: 0,
FLAGGE
1,
BLOCKE
2,
});
function logDebug(...args) {
if (debug) {
console.log('[Grok DeMod]', ...args);
}
}
function logError(...args) {
console.error('[Grok DeMod]', ...args);
}
function getState(key, defaultValue) {
try {
const value = localStorage.getItem(key);
if (value === null) return defaultValue;
if (value === 'true') return true;
if (value === 'false') return false;
return JSON.parse(value);
} catch (e) {
logError(`Error reading ${key} from localStorage:`, e);
return defaultValue;
}
}
function setState(key, value) {
try {
const valueToStore = typeof value === 'boolean' ? value.toString() : JSON.stringify(value);
localStorage.setItem(key, valueToStore);
} catch (e) {
logError(`Error writing ${key} to localStorage:`, e);
}
}
function timeoutPromise(ms, promise, description = 'Promise') {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
logDebug(`${description} timed out after ${ms}ms`);
reject(new Error(`Timeout (${description})`));
}, ms);
promise.then(
(value) => { clearTimeout(timer); resolve(value); },
(error) => { clearTimeout(timer); reject(error); }
);
});
}
function getModerationResult(obj, path = '') {
if (typeof obj !== 'object' || obj === null) return ModerationResult.SAFE;
let result = ModerationResult.SAFE;
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const currentPath = path ? `${path}.${key}` : key;
const value = obj[key];
if (key === 'isBlocked' && value === true) {
logDebug(`Blocked detected via flag '${currentPath}'`);
return ModerationResult.BLOCKED;
}
if (moderationFlags.includes(key) && value === true) {
logDebug(`Flagged detected via flag '${currentPath}'`);
result = Math.max(result, ModerationResult.FLAGGED);
}
if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
const content = value.toLowerCase();
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(content)) {
logDebug(`Moderation pattern matched in '${currentPath}': "${content.substring(0, 50)}..."`);
if (/blocked|moderated|restricted/i.test(pattern.source)) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, ModerationResult.FLAGGED);
break;
}
}
if (result === ModerationResult.SAFE && content.length < 70 && /(sorry|apologies|unable|cannot)/i.test(content)) {
logDebug(`Heuristic moderation detected in '${currentPath}': "${content.substring(0, 50)}..."`);
result = Math.max(result, ModerationResult.FLAGGED);
}
}
if (typeof value === 'object') {
const childResult = getModerationResult(value, currentPath);
if (childResult === ModerationResult.BLOCKED) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, childResult);
}
}
return result;
}
function clearFlagging(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(item => clearFlagging(item));
}
const newObj = {};
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key];
if (moderationFlags.includes(key) && value === true) {
newObj[key] = false;
logDebug(`Cleared flag '${key}'`);
}
else if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
let replaced = false;
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(value)) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced moderated message in '${key}' using pattern`);
replaced = true;
break;
}
}
if (!replaced && value.length < 70 && /(sorry|apologies|unable|cannot)/i.test(value.toLowerCase())) {
if (getModerationResult({[key]: value}) === ModerationResult.FLAGGED) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced heuristic moderated message in '${key}'`);
replaced = true;
}
}
if (!replaced) {
newObj[key] = value;
}
}
else if (typeof value === 'object') {
newObj[key] = clearFlagging(value);
}
else {
newObj[key] = value;
}
}
return newObj;
}
let uiContainer, toggleButton, debugButton, statusEl, logContainer;
function addLog(message) {
if (!logContainer) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.textContent = `[${timestamp}] ${message}`;
logEntry.style.cssText = CONFIG.styles.logEntry;
uiLogBuffer.push(logEntry);
if (uiLogBuffer.length > MAX_LOG_ENTRIES) {
const removed = uiLogBuffer.shift();
if (removed && removed.parentNode === logContainer) {
logContainer.removeChild(removed);
}
}
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(modResult, isRecovering = false) {
if (!statusEl) return;
let text = 'Status: ';
let color = CONFIG.styles.colors.safe;
if (isRecovering) {
text += 'Recovering...';
color = CONFIG.styles.colors.recovering;
} else if (modResult === ModerationResult.BLOCKED) {
text += 'Blocked (Recovered/Cleared)';
color = CONFIG.styles.colors.blocked;
} else if (modResult === ModerationResult.FLAGGED) {
text += 'Flagged (Cleared)';
color = CONFIG.styles.colors.flagged;
} else {
text += 'Safe';
color = CONFIG.styles.colors.safe;
}
statusEl.textContent = text;
statusEl.style.color = color;
}
function setupUI() {
uiContainer = document.createElement('div');
uiContainer.id = 'grok-demod-ui';
uiContainer.style.cssText = CONFIG.styles.uiContainer;
toggleButton = document.createElement('button');
debugButton = document.createElement('button');
statusEl = document.createElement('div');
logContainer = document.createElement('div');
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.title = 'Toggle DeMod functionality (ON = intercepting)';
toggleButton.style.cssText = CONFIG.styles.button;
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
toggleButton.onclick = (e) => {
demodEnabled = !demodEnabled;
setState(CONFIG.lsKeys.enabled, demodEnabled);
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
addLog(`DeMod ${demodEnabled ? 'Enabled' : 'Disabled'}.`);
console.log('[Grok DeMod] Interception is now', demodEnabled ? 'ACTIVE' : 'INACTIVE');
};
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.title = 'Toggle debug mode (logs verbose details to console)';
debugButton.style.cssText = CONFIG.styles.button;
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
debugButton.onclick = (e) => {
debug = !debug;
setState(CONFIG.lsKeys.debug, debug);
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
addLog(`Debug Mode ${debug ? 'Enabled' : 'Disabled'}.`);
logDebug(`Debug mode ${debug ? 'enabled' : 'disabled'}.`);
};
statusEl.id = 'grok-demod-status';
statusEl.style.cssText = CONFIG.styles.status;
updateStatus(ModerationResult.SAFE);
logContainer.id = 'grok-demod-log';
logContainer.style.cssText = CONFIG.styles.logContainer;
uiLogBuffer.forEach(entry => logContainer.appendChild(entry));
logContainer.scrollTop = logContainer.scrollHeight;
uiContainer.appendChild(toggleButton);
uiContainer.appendChild(debugButton);
uiContainer.appendChild(statusEl);
uiContainer.appendChild(logContainer);
document.body.appendChild(uiContainer);
addLog("Grok DeMod Initialized.");
if (debug) addLog("Debug mode is ON.");
}
async function redownloadLatestMessage() {
if (!currentConversationId) {
logDebug('Recovery skipped: Missing conversationId');
addLog('Recovery failed: No conversation ID.');
return null;
}
if (!initCache || !initCache.headers) {
logDebug('Recovery cache missing, attempting fresh fetch for headers...');
try {
const currentConvUrl = `/rest/app-chat/conversation/${currentConversationId}`;
const tempResp = await originalFetch(currentConvUrl, { method: 'GET', headers: {'Accept': 'application/json'} });
if (tempResp.ok) {
logDebug('Fresh header fetch successful (status OK).');
initCache = { headers: new Headers({'Accept': 'application/json'}), credentials: 'include' };
} else {
logDebug(`Fresh header fetch failed with status ${tempResp.status}. Recovery may fail.`);
addLog('Recovery failed: Cannot get request data.');
return null;
}
} catch (e) {
logError('Error during fresh header fetch:', e);
addLog('Recovery failed: Error getting request data.');
return null;
}
}
const url = `/rest/app-chat/conversation/${currentConversationId}`;
logDebug(`Attempting recovery fetch for conversation: ${currentConversationId}`);
addLog('Attempting content recovery...');
const headers = new Headers(initCache.headers);
if (!headers.has('Accept')) headers.set('Accept', 'application/json, text/plain, */*');
const requestOptions = {
method: 'GET',
headers: headers,
credentials: initCache.credentials || 'include',
};
try {
const response = await timeoutPromise(
CONFIG.recoveryTimeoutMs,
fetch(url, requestOptions),
'Recovery Fetch'
);
if (!response.ok) {
logError(`Recovery fetch failed with status ${response.status}: ${response.statusText}`);
addLog(`Recovery failed: HTTP ${response.status}`);
try {
const errorBody = await response.text();
logDebug('Recovery error body:', errorBody.substring(0, 500));
} catch (e) { }
return null;
}
const data = await response.json();
const messages = data?.messages;
if (!Array.isArray(messages) || messages.length === 0) {
logDebug('Recovery failed: No messages found in conversation data', data);
addLog('Recovery failed: No messages found.');
return null;
}
messages.sort((a, b) => {
const tsA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
const tsB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
return tsB - tsA;
});
const latestMessage = messages[0];
if (!latestMessage || typeof latestMessage.content !== 'string' || latestMessage.content.trim() === '') {
logDebug('Recovery failed: Latest message or its content is invalid/empty', latestMessage);
addLog('Recovery failed: Invalid latest message.');
return null;
}
logDebug('Recovery successful, latest content:', latestMessage.content.substring(0, 100) + '...');
addLog('Recovery seems successful.');
return { content: latestMessage.content };
} catch (e) {
logError('Recovery fetch/parse error:', e);
addLog(`Recovery error: ${e.message}`);
return null;
}
}
function extractConversationIdFromUrl(url) {
const match = url.match(/\/conversation\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
return match ? match[1] : null;
}
async function processPotentialModeration(json, source) {
const modResult = getModerationResult(json);
let finalJson = json;
if (modResult !== ModerationResult.SAFE) {
if (modResult === ModerationResult.BLOCKED) {
logDebug(`Blocked content detected from ${source}:`, JSON.stringify(json).substring(0, 200) + '...');
addLog(`Blocked content from ${source}.`);
updateStatus(modResult, true);
const recoveredData = await redownloadLatestMessage();
if (recoveredData && recoveredData.content) {
addLog(`Recovery successful (${source}).`);
logDebug(`Recovered content applied (${source})`);
let replaced = false;
const keysToTry = [...CONFIG.messageKeys, 'text', 'message'];
for (const key of keysToTry) {
if (typeof finalJson[key] === 'string') {
finalJson[key] = recoveredData.content;
logDebug(`Injected recovered content into key '${key}'`);
replaced = true;
break;
}
}
if (!replaced) {
logDebug("Could not find standard key to inject recovered content, adding as 'recovered_content'");
finalJson.recovered_content = recoveredData.content;
}
finalJson = clearFlagging(finalJson);
updateStatus(modResult, false);
} else {
addLog(`Recovery failed (${source}). Content may be lost.`);
logDebug(`Recovery failed (${source}), applying standard clearing.`);
finalJson = clearFlagging(json);
updateStatus(modResult, false);
}
} else {
logDebug(`Flagged content detected and cleared from ${source}.`);
addLog(`Flagged content cleared (${source}).`);
finalJson = clearFlagging(json);
updateStatus(modResult);
}
} else {
if (statusEl && !statusEl.textContent.includes('Blocked') && !statusEl.textContent.includes('Flagged') && !statusEl.textContent.includes('Recovering')) {
updateStatus(modResult);
} else if (statusEl && statusEl.textContent.includes('Recovering')) {
logDebug("Recovery attempt finished (next message safe). Resetting status.");
updateStatus(ModerationResult.SAFE);
}
}
return finalJson;
}
async function handleFetchResponse(original_response, url, requestArgs) {
const response = original_response.clone();
if (!response.ok) {
logDebug(`Fetch response not OK (${response.status}) for ${url}, skipping processing.`);
return original_response;
}
const contentType = response.headers.get('Content-Type')?.toLowerCase() || '';
logDebug(`Intercepted fetch response for ${url}, Content-Type: ${contentType}`);
const conversationGetMatch = url.match(/\/rest\/app-chat\/conversation\/([a-f0-9-]+)$/i);
if (conversationGetMatch && requestArgs?.method === 'GET') {
logDebug(`Caching GET request options for conversation ${conversationGetMatch[1]}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
if (!currentConversationId) {
currentConversationId = conversationGetMatch[1];
logDebug(`Conversation ID set from GET URL: ${currentConversationId}`);
}
}
if (!currentConversationId) {
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from other URL: ${currentConversationId}`);
}
}
if (contentType.includes('text/event-stream')) {
logDebug(`Processing SSE stream for ${url}`);
const reader = response.body.getReader();
const stream = new ReadableStream({
async start(controller) {
let buffer = '';
let currentEvent = { data: '', type: 'message', id: null };
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
logDebug("SSE stream ended, processing final buffer:", buffer);
if (buffer.startsWith('{') || buffer.startsWith('[')) {
try {
let json = JSON.parse(buffer);
json = await processPotentialModeration(json, 'SSE-Final');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logDebug("Error parsing final SSE buffer, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else {
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else if (currentEvent.data) {
logDebug("SSE stream ended after data field, processing event:", currentEvent.data.substring(0,100)+"...");
try {
let json = JSON.parse(currentEvent.data);
json = await processPotentialModeration(json, 'SSE-Event');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch (e) {
logDebug("Error parsing trailing SSE data, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
controller.close();
break;
}
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') {
if (currentEvent.data) {
logDebug("Processing SSE event data:", currentEvent.data.substring(0, 100) + '...');
if (currentEvent.data.startsWith('{') || currentEvent.data.startsWith('[')) {
try {
let json = JSON.parse(currentEvent.data);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from SSE data: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'SSE');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logError("SSE JSON parse error:", e, "Data:", currentEvent.data.substring(0,200)+"...");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
} else {
logDebug("SSE data is not JSON, forwarding as is.");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
currentEvent = { data: '', type: 'message', id: null };
} else if (line.startsWith('data:')) {
currentEvent.data += (currentEvent.data ? '\n' : '') + line.substring(5).trim();
} else if (line.startsWith('event:')) {
currentEvent.type = line.substring(6).trim();
} else if (line.startsWith('id:')) {
currentEvent.id = line.substring(3).trim();
} else if (line.startsWith(':')) {
} else {
logDebug("Unknown SSE line:", line);
}
}
}
} catch (e) {
logError('Error reading/processing SSE stream:', e);
controller.error(e);
} finally {
reader.releaseLock();
}
}
});
const newHeaders = new Headers(response.headers);
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
if (contentType.includes('application/json')) {
logDebug(`Processing JSON response for ${url}`);
try {
const text = await response.text();
let json = JSON.parse(text);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from JSON response: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'Fetch');
const newBody = JSON.stringify(json);
const newHeaders = new Headers(response.headers);
if (newHeaders.has('content-length')) {
newHeaders.set('content-length', encoder.encode(newBody).byteLength.toString());
}
return new Response(newBody, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
} catch (e) {
logError('Fetch JSON processing error:', e, 'URL:', url);
return original_response;
}
}
logDebug(`Non-SSE/JSON response for ${url}, skipping processing.`);
return original_response;
}
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function(input, init) {
if (!demodEnabled) {
return originalFetch.apply(this, arguments);
}
let url;
let requestArgs = init || {};
try {
url = (input instanceof Request) ? input.url : String(input);
} catch (e) {
logDebug('Invalid fetch input, passing through:', input, e);
return originalFetch.apply(this, arguments);
}
if (!url.includes('/rest/app-chat/')) {
return originalFetch.apply(this, arguments);
}
if (requestArgs.method === 'POST') {
logDebug(`Observing POST request: ${url}`);
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
if (!currentConversationId) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from POST URL: ${currentConversationId}`);
}
if (!initCache && requestArgs.headers) {
logDebug(`Caching headers from POST request to ${idFromUrl}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
}
}
return originalFetch.apply(this, arguments);
}
logDebug(`Intercepting fetch request: ${requestArgs.method || 'GET'} ${url}`);
try {
const original_response = await originalFetch.apply(this, arguments);
return await handleFetchResponse(original_response, url, requestArgs);
} catch (error) {
logError(`Fetch interception failed for ${url}:`, error);
throw error;
}
};
const OriginalWebSocket = unsafeWindow.WebSocket;
unsafeWindow.WebSocket = new Proxy(OriginalWebSocket, {
construct(target, args) {
const url = args[0];
logDebug('WebSocket connection attempt:', url);
const ws = new target(...args);
let originalOnMessageHandler = null;
Object.defineProperty(ws, 'onmessage', {
configurable: true,
enumerable: true,
get() {
return originalOnMessageHandler;
},
async set(handler) {
logDebug('WebSocket onmessage handler assigned');
originalOnMessageHandler = handler;
ws.onmessageinternal = async function(event) {
if (!demodEnabled || typeof event.data !== 'string' || !event.data.startsWith('{')) {
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (e) {
logError("Error in original WebSocket onmessage handler:", e);
}
}
return;
}
logDebug('Intercepting WebSocket message:', event.data.substring(0, 200) + '...');
try {
let json = JSON.parse(event.data);
if (json.conversation_id && json.conversation_id !== currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from WebSocket: ${currentConversationId}`);
}
const processedJson = await processPotentialModeration(json, 'WebSocket');
const newEvent = new MessageEvent('message', {
data: JSON.stringify(processedJson),
origin: event.origin,
lastEventId: event.lastEventId,
source: event.source,
ports: event.ports,
});
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, newEvent);
} catch (e) {
logError("Error calling original WebSocket onmessage handler after modification:", e);
}
} else {
logDebug("Original WebSocket onmessage handler not found when message received.");
}
} catch (e) {
logError('WebSocket processing error:', e, 'Data:', event.data.substring(0, 200) + '...');
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (eInner) {
logError("Error in original WebSocket onmessage handler (fallback path):", eInner);
}
}
}
};
ws.addEventListener('message', ws.onmessageinternal);
}
});
const wrapHandler = (eventName) => {
let originalHandler = null;
Object.defineProperty(ws, `on${eventName}`, {
configurable: true,
enumerable: true,
get() { return originalHandler; },
set(handler) {
logDebug(`WebSocket on${eventName} handler assigned`);
originalHandler = handler;
ws.addEventListener(eventName, (event) => {
if (eventName === 'message') return;
logDebug(`WebSocket event: ${eventName}`, event);
if (originalHandler) {
try {
originalHandler.call(ws, event);
} catch (e) {
logError(`Error in original WebSocket on${eventName} handler:`, e);
}
}
});
}
});
};
wrapHandler('close');
wrapHandler('error');
ws.addEventListener('open', () => logDebug('WebSocket opened:', url));
return ws;
}
});
if (window.location.hostname !== 'grok.com') {
console.log('[Grok DeMod] Script inactive: Intended for grok.com only. Current host:', window.location.hostname);
return;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupUI);
} else {
setupUI();
}
console.log('[Grok DeMod] Enhanced Script loaded. Interception is', demodEnabled ? 'ACTIVE' : 'INACTIVE', '. Debug is', debug ? 'ON' : 'OFF');
})();// ==UserScript==
// @name Grok DeMod
// @license GPL-3.0-or-later
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Hides moderation results in Grok conversations, auto-recovers blocked messages.
// @author UniverseDev
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @match https://grok.com/*
// @grant unsafeWindow
// @downloadURL https://update.greasyfork.org/scripts/531147/Grok DeMod.user.js
// @updateURL https://update.greasyfork.org/scripts/531147/Grok DeMod.meta.js
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
defaultFlags: [
'isFlagged', 'isBlocked', 'moderationApplied', 'restricted'
],
messageKeys: ['message', 'content', 'text', 'error'],
moderationMessagePatterns: [
/this content has been moderated/i,
/sorry, i cannot assist/i,
/policy violation/i,
/blocked/i,
/moderated/i,
/restricted/i,
/content restricted/i,
/unable to process/i,
/cannot help/i,
/(sorry|apologies).*?(cannot|unable|help|assist)/i,
],
clearedMessageText: '[Content cleared by Grok DeMod]',
recoveryTimeoutMs: 5000,
lsKeys: {
enabled: 'GrokDeModEnabled',
debug: 'GrokDeModDebug',
flags: 'GrokDeModFlags',
},
styles: {
uiContainer: `
position: fixed;
bottom: 10px;
right: 10px;
z-index: 10000;
background: #2d2d2d;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
gap: 8px;
font-family: Arial, sans-serif;
color: #e0e0e0;
min-width: 170px;
`,
button: `
padding: 6px 12px;
border-radius: 5px;
border: none;
cursor: pointer;
color: #fff;
font-size: 13px;
transition: background-color 0.2s ease;
`,
status: `
padding: 5px;
font-size: 12px;
color: #a0a0a0;
text-align: center;
border-top: 1px solid #444;
margin-top: 5px;
min-height: 16px;
`,
logContainer: `
max-height: 100px;
overflow-y: auto;
font-size: 11px;
color: #c0c0c0;
background-color: #333;
padding: 5px;
border-radius: 4px;
line-height: 1.4;
margin-top: 5px;
`,
logEntry: `
padding-bottom: 3px;
border-bottom: 1px dashed #555;
margin-bottom: 3px;
word-break: break-word;
`,
colors: {
enabled: '#388E3C',
disabled: '#D32F2F',
debugEnabled: '#1976D2',
debugDisabled: '#555555',
safe: '#66ff66',
flagged: '#ffa500',
blocked: '#ff6666',
recovering: '#ffcc00'
}
}
};
let demodEnabled = getState(CONFIG.lsKeys.enabled, true);
let debug = getState(CONFIG.lsKeys.debug, false);
let moderationFlags = getState(CONFIG.lsKeys.flags, CONFIG.defaultFlags);
let initCache = null;
let currentConversationId = null;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const uiLogBuffer = [];
const MAX_LOG_ENTRIES = 50;
const ModerationResult = Object.freeze({
SAFE: 0,
FLAGGE
1,
BLOCKE
2,
});
function logDebug(...args) {
if (debug) {
console.log('[Grok DeMod]', ...args);
}
}
function logError(...args) {
console.error('[Grok DeMod]', ...args);
}
function getState(key, defaultValue) {
try {
const value = localStorage.getItem(key);
if (value === null) return defaultValue;
if (value === 'true') return true;
if (value === 'false') return false;
return JSON.parse(value);
} catch (e) {
logError(`Error reading ${key} from localStorage:`, e);
return defaultValue;
}
}
function setState(key, value) {
try {
const valueToStore = typeof value === 'boolean' ? value.toString() : JSON.stringify(value);
localStorage.setItem(key, valueToStore);
} catch (e) {
logError(`Error writing ${key} to localStorage:`, e);
}
}
function timeoutPromise(ms, promise, description = 'Promise') {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
logDebug(`${description} timed out after ${ms}ms`);
reject(new Error(`Timeout (${description})`));
}, ms);
promise.then(
(value) => { clearTimeout(timer); resolve(value); },
(error) => { clearTimeout(timer); reject(error); }
);
});
}
function getModerationResult(obj, path = '') {
if (typeof obj !== 'object' || obj === null) return ModerationResult.SAFE;
let result = ModerationResult.SAFE;
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const currentPath = path ? `${path}.${key}` : key;
const value = obj[key];
if (key === 'isBlocked' && value === true) {
logDebug(`Blocked detected via flag '${currentPath}'`);
return ModerationResult.BLOCKED;
}
if (moderationFlags.includes(key) && value === true) {
logDebug(`Flagged detected via flag '${currentPath}'`);
result = Math.max(result, ModerationResult.FLAGGED);
}
if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
const content = value.toLowerCase();
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(content)) {
logDebug(`Moderation pattern matched in '${currentPath}': "${content.substring(0, 50)}..."`);
if (/blocked|moderated|restricted/i.test(pattern.source)) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, ModerationResult.FLAGGED);
break;
}
}
if (result === ModerationResult.SAFE && content.length < 70 && /(sorry|apologies|unable|cannot)/i.test(content)) {
logDebug(`Heuristic moderation detected in '${currentPath}': "${content.substring(0, 50)}..."`);
result = Math.max(result, ModerationResult.FLAGGED);
}
}
if (typeof value === 'object') {
const childResult = getModerationResult(value, currentPath);
if (childResult === ModerationResult.BLOCKED) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, childResult);
}
}
return result;
}
function clearFlagging(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(item => clearFlagging(item));
}
const newObj = {};
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key];
if (moderationFlags.includes(key) && value === true) {
newObj[key] = false;
logDebug(`Cleared flag '${key}'`);
}
else if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
let replaced = false;
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(value)) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced moderated message in '${key}' using pattern`);
replaced = true;
break;
}
}
if (!replaced && value.length < 70 && /(sorry|apologies|unable|cannot)/i.test(value.toLowerCase())) {
if (getModerationResult({[key]: value}) === ModerationResult.FLAGGED) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced heuristic moderated message in '${key}'`);
replaced = true;
}
}
if (!replaced) {
newObj[key] = value;
}
}
else if (typeof value === 'object') {
newObj[key] = clearFlagging(value);
}
else {
newObj[key] = value;
}
}
return newObj;
}
let uiContainer, toggleButton, debugButton, statusEl, logContainer;
function addLog(message) {
if (!logContainer) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.textContent = `[${timestamp}] ${message}`;
logEntry.style.cssText = CONFIG.styles.logEntry;
uiLogBuffer.push(logEntry);
if (uiLogBuffer.length > MAX_LOG_ENTRIES) {
const removed = uiLogBuffer.shift();
if (removed && removed.parentNode === logContainer) {
logContainer.removeChild(removed);
}
}
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(modResult, isRecovering = false) {
if (!statusEl) return;
let text = 'Status: ';
let color = CONFIG.styles.colors.safe;
if (isRecovering) {
text += 'Recovering...';
color = CONFIG.styles.colors.recovering;
} else if (modResult === ModerationResult.BLOCKED) {
text += 'Blocked (Recovered/Cleared)';
color = CONFIG.styles.colors.blocked;
} else if (modResult === ModerationResult.FLAGGED) {
text += 'Flagged (Cleared)';
color = CONFIG.styles.colors.flagged;
} else {
text += 'Safe';
color = CONFIG.styles.colors.safe;
}
statusEl.textContent = text;
statusEl.style.color = color;
}
function setupUI() {
uiContainer = document.createElement('div');
uiContainer.id = 'grok-demod-ui';
uiContainer.style.cssText = CONFIG.styles.uiContainer;
toggleButton = document.createElement('button');
debugButton = document.createElement('button');
statusEl = document.createElement('div');
logContainer = document.createElement('div');
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.title = 'Toggle DeMod functionality (ON = intercepting)';
toggleButton.style.cssText = CONFIG.styles.button;
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
toggleButton.onclick = (e) => {
demodEnabled = !demodEnabled;
setState(CONFIG.lsKeys.enabled, demodEnabled);
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
addLog(`DeMod ${demodEnabled ? 'Enabled' : 'Disabled'}.`);
console.log('[Grok DeMod] Interception is now', demodEnabled ? 'ACTIVE' : 'INACTIVE');
};
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.title = 'Toggle debug mode (logs verbose details to console)';
debugButton.style.cssText = CONFIG.styles.button;
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
debugButton.onclick = (e) => {
debug = !debug;
setState(CONFIG.lsKeys.debug, debug);
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
addLog(`Debug Mode ${debug ? 'Enabled' : 'Disabled'}.`);
logDebug(`Debug mode ${debug ? 'enabled' : 'disabled'}.`);
};
statusEl.id = 'grok-demod-status';
statusEl.style.cssText = CONFIG.styles.status;
updateStatus(ModerationResult.SAFE);
logContainer.id = 'grok-demod-log';
logContainer.style.cssText = CONFIG.styles.logContainer;
uiLogBuffer.forEach(entry => logContainer.appendChild(entry));
logContainer.scrollTop = logContainer.scrollHeight;
uiContainer.appendChild(toggleButton);
uiContainer.appendChild(debugButton);
uiContainer.appendChild(statusEl);
uiContainer.appendChild(logContainer);
document.body.appendChild(uiContainer);
addLog("Grok DeMod Initialized.");
if (debug) addLog("Debug mode is ON.");
}
async function redownloadLatestMessage() {
if (!currentConversationId) {
logDebug('Recovery skipped: Missing conversationId');
addLog('Recovery failed: No conversation ID.');
return null;
}
if (!initCache || !initCache.headers) {
logDebug('Recovery cache missing, attempting fresh fetch for headers...');
try {
const currentConvUrl = `/rest/app-chat/conversation/${currentConversationId}`;
const tempResp = await originalFetch(currentConvUrl, { method: 'GET', headers: {'Accept': 'application/json'} });
if (tempResp.ok) {
logDebug('Fresh header fetch successful (status OK).');
initCache = { headers: new Headers({'Accept': 'application/json'}), credentials: 'include' };
} else {
logDebug(`Fresh header fetch failed with status ${tempResp.status}. Recovery may fail.`);
addLog('Recovery failed: Cannot get request data.');
return null;
}
} catch (e) {
logError('Error during fresh header fetch:', e);
addLog('Recovery failed: Error getting request data.');
return null;
}
}
const url = `/rest/app-chat/conversation/${currentConversationId}`;
logDebug(`Attempting recovery fetch for conversation: ${currentConversationId}`);
addLog('Attempting content recovery...');
const headers = new Headers(initCache.headers);
if (!headers.has('Accept')) headers.set('Accept', 'application/json, text/plain, */*');
const requestOptions = {
method: 'GET',
headers: headers,
credentials: initCache.credentials || 'include',
};
try {
const response = await timeoutPromise(
CONFIG.recoveryTimeoutMs,
fetch(url, requestOptions),
'Recovery Fetch'
);
if (!response.ok) {
logError(`Recovery fetch failed with status ${response.status}: ${response.statusText}`);
addLog(`Recovery failed: HTTP ${response.status}`);
try {
const errorBody = await response.text();
logDebug('Recovery error body:', errorBody.substring(0, 500));
} catch (e) { }
return null;
}
const data = await response.json();
const messages = data?.messages;
if (!Array.isArray(messages) || messages.length === 0) {
logDebug('Recovery failed: No messages found in conversation data', data);
addLog('Recovery failed: No messages found.');
return null;
}
messages.sort((a, b) => {
const tsA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
const tsB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
return tsB - tsA;
});
const latestMessage = messages[0];
if (!latestMessage || typeof latestMessage.content !== 'string' || latestMessage.content.trim() === '') {
logDebug('Recovery failed: Latest message or its content is invalid/empty', latestMessage);
addLog('Recovery failed: Invalid latest message.');
return null;
}
logDebug('Recovery successful, latest content:', latestMessage.content.substring(0, 100) + '...');
addLog('Recovery seems successful.');
return { content: latestMessage.content };
} catch (e) {
logError('Recovery fetch/parse error:', e);
addLog(`Recovery error: ${e.message}`);
return null;
}
}
function extractConversationIdFromUrl(url) {
const match = url.match(/\/conversation\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
return match ? match[1] : null;
}
async function processPotentialModeration(json, source) {
const modResult = getModerationResult(json);
let finalJson = json;
if (modResult !== ModerationResult.SAFE) {
if (modResult === ModerationResult.BLOCKED) {
logDebug(`Blocked content detected from ${source}:`, JSON.stringify(json).substring(0, 200) + '...');
addLog(`Blocked content from ${source}.`);
updateStatus(modResult, true);
const recoveredData = await redownloadLatestMessage();
if (recoveredData && recoveredData.content) {
addLog(`Recovery successful (${source}).`);
logDebug(`Recovered content applied (${source})`);
let replaced = false;
const keysToTry = [...CONFIG.messageKeys, 'text', 'message'];
for (const key of keysToTry) {
if (typeof finalJson[key] === 'string') {
finalJson[key] = recoveredData.content;
logDebug(`Injected recovered content into key '${key}'`);
replaced = true;
break;
}
}
if (!replaced) {
logDebug("Could not find standard key to inject recovered content, adding as 'recovered_content'");
finalJson.recovered_content = recoveredData.content;
}
finalJson = clearFlagging(finalJson);
updateStatus(modResult, false);
} else {
addLog(`Recovery failed (${source}). Content may be lost.`);
logDebug(`Recovery failed (${source}), applying standard clearing.`);
finalJson = clearFlagging(json);
updateStatus(modResult, false);
}
} else {
logDebug(`Flagged content detected and cleared from ${source}.`);
addLog(`Flagged content cleared (${source}).`);
finalJson = clearFlagging(json);
updateStatus(modResult);
}
} else {
if (statusEl && !statusEl.textContent.includes('Blocked') && !statusEl.textContent.includes('Flagged') && !statusEl.textContent.includes('Recovering')) {
updateStatus(modResult);
} else if (statusEl && statusEl.textContent.includes('Recovering')) {
logDebug("Recovery attempt finished (next message safe). Resetting status.");
updateStatus(ModerationResult.SAFE);
}
}
return finalJson;
}
async function handleFetchResponse(original_response, url, requestArgs) {
const response = original_response.clone();
if (!response.ok) {
logDebug(`Fetch response not OK (${response.status}) for ${url}, skipping processing.`);
return original_response;
}
const contentType = response.headers.get('Content-Type')?.toLowerCase() || '';
logDebug(`Intercepted fetch response for ${url}, Content-Type: ${contentType}`);
const conversationGetMatch = url.match(/\/rest\/app-chat\/conversation\/([a-f0-9-]+)$/i);
if (conversationGetMatch && requestArgs?.method === 'GET') {
logDebug(`Caching GET request options for conversation ${conversationGetMatch[1]}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
if (!currentConversationId) {
currentConversationId = conversationGetMatch[1];
logDebug(`Conversation ID set from GET URL: ${currentConversationId}`);
}
}
if (!currentConversationId) {
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from other URL: ${currentConversationId}`);
}
}
if (contentType.includes('text/event-stream')) {
logDebug(`Processing SSE stream for ${url}`);
const reader = response.body.getReader();
const stream = new ReadableStream({
async start(controller) {
let buffer = '';
let currentEvent = { data: '', type: 'message', id: null };
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
logDebug("SSE stream ended, processing final buffer:", buffer);
if (buffer.startsWith('{') || buffer.startsWith('[')) {
try {
let json = JSON.parse(buffer);
json = await processPotentialModeration(json, 'SSE-Final');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logDebug("Error parsing final SSE buffer, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else {
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else if (currentEvent.data) {
logDebug("SSE stream ended after data field, processing event:", currentEvent.data.substring(0,100)+"...");
try {
let json = JSON.parse(currentEvent.data);
json = await processPotentialModeration(json, 'SSE-Event');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch (e) {
logDebug("Error parsing trailing SSE data, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
controller.close();
break;
}
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') {
if (currentEvent.data) {
logDebug("Processing SSE event data:", currentEvent.data.substring(0, 100) + '...');
if (currentEvent.data.startsWith('{') || currentEvent.data.startsWith('[')) {
try {
let json = JSON.parse(currentEvent.data);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from SSE data: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'SSE');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logError("SSE JSON parse error:", e, "Data:", currentEvent.data.substring(0,200)+"...");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
} else {
logDebug("SSE data is not JSON, forwarding as is.");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
currentEvent = { data: '', type: 'message', id: null };
} else if (line.startsWith('data:')) {
currentEvent.data += (currentEvent.data ? '\n' : '') + line.substring(5).trim();
} else if (line.startsWith('event:')) {
currentEvent.type = line.substring(6).trim();
} else if (line.startsWith('id:')) {
currentEvent.id = line.substring(3).trim();
} else if (line.startsWith(':')) {
} else {
logDebug("Unknown SSE line:", line);
}
}
}
} catch (e) {
logError('Error reading/processing SSE stream:', e);
controller.error(e);
} finally {
reader.releaseLock();
}
}
});
const newHeaders = new Headers(response.headers);
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
if (contentType.includes('application/json')) {
logDebug(`Processing JSON response for ${url}`);
try {
const text = await response.text();
let json = JSON.parse(text);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from JSON response: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'Fetch');
const newBody = JSON.stringify(json);
const newHeaders = new Headers(response.headers);
if (newHeaders.has('content-length')) {
newHeaders.set('content-length', encoder.encode(newBody).byteLength.toString());
}
return new Response(newBody, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
} catch (e) {
logError('Fetch JSON processing error:', e, 'URL:', url);
return original_response;
}
}
logDebug(`Non-SSE/JSON response for ${url}, skipping processing.`);
return original_response;
}
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function(input, init) {
if (!demodEnabled) {
return originalFetch.apply(this, arguments);
}
let url;
let requestArgs = init || {};
try {
url = (input instanceof Request) ? input.url : String(input);
} catch (e) {
logDebug('Invalid fetch input, passing through:', input, e);
return originalFetch.apply(this, arguments);
}
if (!url.includes('/rest/app-chat/')) {
return originalFetch.apply(this, arguments);
}
if (requestArgs.method === 'POST') {
logDebug(`Observing POST request: ${url}`);
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
if (!currentConversationId) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from POST URL: ${currentConversationId}`);
}
if (!initCache && requestArgs.headers) {
logDebug(`Caching headers from POST request to ${idFromUrl}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
}
}
return originalFetch.apply(this, arguments);
}
logDebug(`Intercepting fetch request: ${requestArgs.method || 'GET'} ${url}`);
try {
const original_response = await originalFetch.apply(this, arguments);
return await handleFetchResponse(original_response, url, requestArgs);
} catch (error) {
logError(`Fetch interception failed for ${url}:`, error);
throw error;
}
};
const OriginalWebSocket = unsafeWindow.WebSocket;
unsafeWindow.WebSocket = new Proxy(OriginalWebSocket, {
construct(target, args) {
const url = args[0];
logDebug('WebSocket connection attempt:', url);
const ws = new target(...args);
let originalOnMessageHandler = null;
Object.defineProperty(ws, 'onmessage', {
configurable: true,
enumerable: true,
get() {
return originalOnMessageHandler;
},
async set(handler) {
logDebug('WebSocket onmessage handler assigned');
originalOnMessageHandler = handler;
ws.onmessageinternal = async function(event) {
if (!demodEnabled || typeof event.data !== 'string' || !event.data.startsWith('{')) {
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (e) {
logError("Error in original WebSocket onmessage handler:", e);
}
}
return;
}
logDebug('Intercepting WebSocket message:', event.data.substring(0, 200) + '...');
try {
let json = JSON.parse(event.data);
if (json.conversation_id && json.conversation_id !== currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from WebSocket: ${currentConversationId}`);
}
const processedJson = await processPotentialModeration(json, 'WebSocket');
const newEvent = new MessageEvent('message', {
data: JSON.stringify(processedJson),
origin: event.origin,
lastEventId: event.lastEventId,
source: event.source,
ports: event.ports,
});
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, newEvent);
} catch (e) {
logError("Error calling original WebSocket onmessage handler after modification:", e);
}
} else {
logDebug("Original WebSocket onmessage handler not found when message received.");
}
} catch (e) {
logError('WebSocket processing error:', e, 'Data:', event.data.substring(0, 200) + '...');
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (eInner) {
logError("Error in original WebSocket onmessage handler (fallback path):", eInner);
}
}
}
};
ws.addEventListener('message', ws.onmessageinternal);
}
});
const wrapHandler = (eventName) => {
let originalHandler = null;
Object.defineProperty(ws, `on${eventName}`, {
configurable: true,
enumerable: true,
get() { return originalHandler; },
set(handler) {
logDebug(`WebSocket on${eventName} handler assigned`);
originalHandler = handler;
ws.addEventListener(eventName, (event) => {
if (eventName === 'message') return;
logDebug(`WebSocket event: ${eventName}`, event);
if (originalHandler) {
try {
originalHandler.call(ws, event);
} catch (e) {
logError(`Error in original WebSocket on${eventName} handler:`, e);
}
}
});
}
});
};
wrapHandler('close');
wrapHandler('error');
ws.addEventListener('open', () => logDebug('WebSocket opened:', url));
return ws;
}
});
if (window.location.hostname !== 'grok.com') {
console.log('[Grok DeMod] Script inactive: Intended for grok.com only. Current host:', window.location.hostname);
return;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupUI);
} else {
setupUI();
}
console.log('[Grok DeMod] Enhanced Script loaded. Interception is', demodEnabled ? 'ACTIVE' : 'INACTIVE', '. Debug is', debug ? 'ON' : 'OFF');
})();// ==UserScript==
// @name Grok DeMod
// @license GPL-3.0-or-later
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Hides moderation results in Grok conversations, auto-recovers blocked messages.
// @author UniverseDev
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @match https://grok.com/*
// @grant unsafeWindow
// @downloadURL https://update.greasyfork.org/scripts/531147/Grok DeMod.user.js
// @updateURL https://update.greasyfork.org/scripts/531147/Grok DeMod.meta.js
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
defaultFlags: [
'isFlagged', 'isBlocked', 'moderationApplied', 'restricted'
],
messageKeys: ['message', 'content', 'text', 'error'],
moderationMessagePatterns: [
/this content has been moderated/i,
/sorry, i cannot assist/i,
/policy violation/i,
/blocked/i,
/moderated/i,
/restricted/i,
/content restricted/i,
/unable to process/i,
/cannot help/i,
/(sorry|apologies).*?(cannot|unable|help|assist)/i,
],
clearedMessageText: '[Content cleared by Grok DeMod]',
recoveryTimeoutMs: 5000,
lsKeys: {
enabled: 'GrokDeModEnabled',
debug: 'GrokDeModDebug',
flags: 'GrokDeModFlags',
},
styles: {
uiContainer: `
position: fixed;
bottom: 10px;
right: 10px;
z-index: 10000;
background: #2d2d2d;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
gap: 8px;
font-family: Arial, sans-serif;
color: #e0e0e0;
min-width: 170px;
`,
button: `
padding: 6px 12px;
border-radius: 5px;
border: none;
cursor: pointer;
color: #fff;
font-size: 13px;
transition: background-color 0.2s ease;
`,
status: `
padding: 5px;
font-size: 12px;
color: #a0a0a0;
text-align: center;
border-top: 1px solid #444;
margin-top: 5px;
min-height: 16px;
`,
logContainer: `
max-height: 100px;
overflow-y: auto;
font-size: 11px;
color: #c0c0c0;
background-color: #333;
padding: 5px;
border-radius: 4px;
line-height: 1.4;
margin-top: 5px;
`,
logEntry: `
padding-bottom: 3px;
border-bottom: 1px dashed #555;
margin-bottom: 3px;
word-break: break-word;
`,
colors: {
enabled: '#388E3C',
disabled: '#D32F2F',
debugEnabled: '#1976D2',
debugDisabled: '#555555',
safe: '#66ff66',
flagged: '#ffa500',
blocked: '#ff6666',
recovering: '#ffcc00'
}
}
};
let demodEnabled = getState(CONFIG.lsKeys.enabled, true);
let debug = getState(CONFIG.lsKeys.debug, false);
let moderationFlags = getState(CONFIG.lsKeys.flags, CONFIG.defaultFlags);
let initCache = null;
let currentConversationId = null;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const uiLogBuffer = [];
const MAX_LOG_ENTRIES = 50;
const ModerationResult = Object.freeze({
SAFE: 0,
FLAGGE
1,
BLOCKE
2,
});
function logDebug(...args) {
if (debug) {
console.log('[Grok DeMod]', ...args);
}
}
function logError(...args) {
console.error('[Grok DeMod]', ...args);
}
function getState(key, defaultValue) {
try {
const value = localStorage.getItem(key);
if (value === null) return defaultValue;
if (value === 'true') return true;
if (value === 'false') return false;
return JSON.parse(value);
} catch (e) {
logError(`Error reading ${key} from localStorage:`, e);
return defaultValue;
}
}
function setState(key, value) {
try {
const valueToStore = typeof value === 'boolean' ? value.toString() : JSON.stringify(value);
localStorage.setItem(key, valueToStore);
} catch (e) {
logError(`Error writing ${key} to localStorage:`, e);
}
}
function timeoutPromise(ms, promise, description = 'Promise') {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
logDebug(`${description} timed out after ${ms}ms`);
reject(new Error(`Timeout (${description})`));
}, ms);
promise.then(
(value) => { clearTimeout(timer); resolve(value); },
(error) => { clearTimeout(timer); reject(error); }
);
});
}
function getModerationResult(obj, path = '') {
if (typeof obj !== 'object' || obj === null) return ModerationResult.SAFE;
let result = ModerationResult.SAFE;
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const currentPath = path ? `${path}.${key}` : key;
const value = obj[key];
if (key === 'isBlocked' && value === true) {
logDebug(`Blocked detected via flag '${currentPath}'`);
return ModerationResult.BLOCKED;
}
if (moderationFlags.includes(key) && value === true) {
logDebug(`Flagged detected via flag '${currentPath}'`);
result = Math.max(result, ModerationResult.FLAGGED);
}
if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
const content = value.toLowerCase();
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(content)) {
logDebug(`Moderation pattern matched in '${currentPath}': "${content.substring(0, 50)}..."`);
if (/blocked|moderated|restricted/i.test(pattern.source)) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, ModerationResult.FLAGGED);
break;
}
}
if (result === ModerationResult.SAFE && content.length < 70 && /(sorry|apologies|unable|cannot)/i.test(content)) {
logDebug(`Heuristic moderation detected in '${currentPath}': "${content.substring(0, 50)}..."`);
result = Math.max(result, ModerationResult.FLAGGED);
}
}
if (typeof value === 'object') {
const childResult = getModerationResult(value, currentPath);
if (childResult === ModerationResult.BLOCKED) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, childResult);
}
}
return result;
}
function clearFlagging(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(item => clearFlagging(item));
}
const newObj = {};
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key];
if (moderationFlags.includes(key) && value === true) {
newObj[key] = false;
logDebug(`Cleared flag '${key}'`);
}
else if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
let replaced = false;
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(value)) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced moderated message in '${key}' using pattern`);
replaced = true;
break;
}
}
if (!replaced && value.length < 70 && /(sorry|apologies|unable|cannot)/i.test(value.toLowerCase())) {
if (getModerationResult({[key]: value}) === ModerationResult.FLAGGED) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced heuristic moderated message in '${key}'`);
replaced = true;
}
}
if (!replaced) {
newObj[key] = value;
}
}
else if (typeof value === 'object') {
newObj[key] = clearFlagging(value);
}
else {
newObj[key] = value;
}
}
return newObj;
}
let uiContainer, toggleButton, debugButton, statusEl, logContainer;
function addLog(message) {
if (!logContainer) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.textContent = `[${timestamp}] ${message}`;
logEntry.style.cssText = CONFIG.styles.logEntry;
uiLogBuffer.push(logEntry);
if (uiLogBuffer.length > MAX_LOG_ENTRIES) {
const removed = uiLogBuffer.shift();
if (removed && removed.parentNode === logContainer) {
logContainer.removeChild(removed);
}
}
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(modResult, isRecovering = false) {
if (!statusEl) return;
let text = 'Status: ';
let color = CONFIG.styles.colors.safe;
if (isRecovering) {
text += 'Recovering...';
color = CONFIG.styles.colors.recovering;
} else if (modResult === ModerationResult.BLOCKED) {
text += 'Blocked (Recovered/Cleared)';
color = CONFIG.styles.colors.blocked;
} else if (modResult === ModerationResult.FLAGGED) {
text += 'Flagged (Cleared)';
color = CONFIG.styles.colors.flagged;
} else {
text += 'Safe';
color = CONFIG.styles.colors.safe;
}
statusEl.textContent = text;
statusEl.style.color = color;
}
function setupUI() {
uiContainer = document.createElement('div');
uiContainer.id = 'grok-demod-ui';
uiContainer.style.cssText = CONFIG.styles.uiContainer;
toggleButton = document.createElement('button');
debugButton = document.createElement('button');
statusEl = document.createElement('div');
logContainer = document.createElement('div');
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.title = 'Toggle DeMod functionality (ON = intercepting)';
toggleButton.style.cssText = CONFIG.styles.button;
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
toggleButton.onclick = (e) => {
demodEnabled = !demodEnabled;
setState(CONFIG.lsKeys.enabled, demodEnabled);
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
addLog(`DeMod ${demodEnabled ? 'Enabled' : 'Disabled'}.`);
console.log('[Grok DeMod] Interception is now', demodEnabled ? 'ACTIVE' : 'INACTIVE');
};
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.title = 'Toggle debug mode (logs verbose details to console)';
debugButton.style.cssText = CONFIG.styles.button;
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
debugButton.onclick = (e) => {
debug = !debug;
setState(CONFIG.lsKeys.debug, debug);
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
addLog(`Debug Mode ${debug ? 'Enabled' : 'Disabled'}.`);
logDebug(`Debug mode ${debug ? 'enabled' : 'disabled'}.`);
};
statusEl.id = 'grok-demod-status';
statusEl.style.cssText = CONFIG.styles.status;
updateStatus(ModerationResult.SAFE);
logContainer.id = 'grok-demod-log';
logContainer.style.cssText = CONFIG.styles.logContainer;
uiLogBuffer.forEach(entry => logContainer.appendChild(entry));
logContainer.scrollTop = logContainer.scrollHeight;
uiContainer.appendChild(toggleButton);
uiContainer.appendChild(debugButton);
uiContainer.appendChild(statusEl);
uiContainer.appendChild(logContainer);
document.body.appendChild(uiContainer);
addLog("Grok DeMod Initialized.");
if (debug) addLog("Debug mode is ON.");
}
async function redownloadLatestMessage() {
if (!currentConversationId) {
logDebug('Recovery skipped: Missing conversationId');
addLog('Recovery failed: No conversation ID.');
return null;
}
if (!initCache || !initCache.headers) {
logDebug('Recovery cache missing, attempting fresh fetch for headers...');
try {
const currentConvUrl = `/rest/app-chat/conversation/${currentConversationId}`;
const tempResp = await originalFetch(currentConvUrl, { method: 'GET', headers: {'Accept': 'application/json'} });
if (tempResp.ok) {
logDebug('Fresh header fetch successful (status OK).');
initCache = { headers: new Headers({'Accept': 'application/json'}), credentials: 'include' };
} else {
logDebug(`Fresh header fetch failed with status ${tempResp.status}. Recovery may fail.`);
addLog('Recovery failed: Cannot get request data.');
return null;
}
} catch (e) {
logError('Error during fresh header fetch:', e);
addLog('Recovery failed: Error getting request data.');
return null;
}
}
const url = `/rest/app-chat/conversation/${currentConversationId}`;
logDebug(`Attempting recovery fetch for conversation: ${currentConversationId}`);
addLog('Attempting content recovery...');
const headers = new Headers(initCache.headers);
if (!headers.has('Accept')) headers.set('Accept', 'application/json, text/plain, */*');
const requestOptions = {
method: 'GET',
headers: headers,
credentials: initCache.credentials || 'include',
};
try {
const response = await timeoutPromise(
CONFIG.recoveryTimeoutMs,
fetch(url, requestOptions),
'Recovery Fetch'
);
if (!response.ok) {
logError(`Recovery fetch failed with status ${response.status}: ${response.statusText}`);
addLog(`Recovery failed: HTTP ${response.status}`);
try {
const errorBody = await response.text();
logDebug('Recovery error body:', errorBody.substring(0, 500));
} catch (e) { }
return null;
}
const data = await response.json();
const messages = data?.messages;
if (!Array.isArray(messages) || messages.length === 0) {
logDebug('Recovery failed: No messages found in conversation data', data);
addLog('Recovery failed: No messages found.');
return null;
}
messages.sort((a, b) => {
const tsA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
const tsB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
return tsB - tsA;
});
const latestMessage = messages[0];
if (!latestMessage || typeof latestMessage.content !== 'string' || latestMessage.content.trim() === '') {
logDebug('Recovery failed: Latest message or its content is invalid/empty', latestMessage);
addLog('Recovery failed: Invalid latest message.');
return null;
}
logDebug('Recovery successful, latest content:', latestMessage.content.substring(0, 100) + '...');
addLog('Recovery seems successful.');
return { content: latestMessage.content };
} catch (e) {
logError('Recovery fetch/parse error:', e);
addLog(`Recovery error: ${e.message}`);
return null;
}
}
function extractConversationIdFromUrl(url) {
const match = url.match(/\/conversation\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
return match ? match[1] : null;
}
async function processPotentialModeration(json, source) {
const modResult = getModerationResult(json);
let finalJson = json;
if (modResult !== ModerationResult.SAFE) {
if (modResult === ModerationResult.BLOCKED) {
logDebug(`Blocked content detected from ${source}:`, JSON.stringify(json).substring(0, 200) + '...');
addLog(`Blocked content from ${source}.`);
updateStatus(modResult, true);
const recoveredData = await redownloadLatestMessage();
if (recoveredData && recoveredData.content) {
addLog(`Recovery successful (${source}).`);
logDebug(`Recovered content applied (${source})`);
let replaced = false;
const keysToTry = [...CONFIG.messageKeys, 'text', 'message'];
for (const key of keysToTry) {
if (typeof finalJson[key] === 'string') {
finalJson[key] = recoveredData.content;
logDebug(`Injected recovered content into key '${key}'`);
replaced = true;
break;
}
}
if (!replaced) {
logDebug("Could not find standard key to inject recovered content, adding as 'recovered_content'");
finalJson.recovered_content = recoveredData.content;
}
finalJson = clearFlagging(finalJson);
updateStatus(modResult, false);
} else {
addLog(`Recovery failed (${source}). Content may be lost.`);
logDebug(`Recovery failed (${source}), applying standard clearing.`);
finalJson = clearFlagging(json);
updateStatus(modResult, false);
}
} else {
logDebug(`Flagged content detected and cleared from ${source}.`);
addLog(`Flagged content cleared (${source}).`);
finalJson = clearFlagging(json);
updateStatus(modResult);
}
} else {
if (statusEl && !statusEl.textContent.includes('Blocked') && !statusEl.textContent.includes('Flagged') && !statusEl.textContent.includes('Recovering')) {
updateStatus(modResult);
} else if (statusEl && statusEl.textContent.includes('Recovering')) {
logDebug("Recovery attempt finished (next message safe). Resetting status.");
updateStatus(ModerationResult.SAFE);
}
}
return finalJson;
}
async function handleFetchResponse(original_response, url, requestArgs) {
const response = original_response.clone();
if (!response.ok) {
logDebug(`Fetch response not OK (${response.status}) for ${url}, skipping processing.`);
return original_response;
}
const contentType = response.headers.get('Content-Type')?.toLowerCase() || '';
logDebug(`Intercepted fetch response for ${url}, Content-Type: ${contentType}`);
const conversationGetMatch = url.match(/\/rest\/app-chat\/conversation\/([a-f0-9-]+)$/i);
if (conversationGetMatch && requestArgs?.method === 'GET') {
logDebug(`Caching GET request options for conversation ${conversationGetMatch[1]}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
if (!currentConversationId) {
currentConversationId = conversationGetMatch[1];
logDebug(`Conversation ID set from GET URL: ${currentConversationId}`);
}
}
if (!currentConversationId) {
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from other URL: ${currentConversationId}`);
}
}
if (contentType.includes('text/event-stream')) {
logDebug(`Processing SSE stream for ${url}`);
const reader = response.body.getReader();
const stream = new ReadableStream({
async start(controller) {
let buffer = '';
let currentEvent = { data: '', type: 'message', id: null };
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
logDebug("SSE stream ended, processing final buffer:", buffer);
if (buffer.startsWith('{') || buffer.startsWith('[')) {
try {
let json = JSON.parse(buffer);
json = await processPotentialModeration(json, 'SSE-Final');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logDebug("Error parsing final SSE buffer, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else {
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else if (currentEvent.data) {
logDebug("SSE stream ended after data field, processing event:", currentEvent.data.substring(0,100)+"...");
try {
let json = JSON.parse(currentEvent.data);
json = await processPotentialModeration(json, 'SSE-Event');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch (e) {
logDebug("Error parsing trailing SSE data, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
controller.close();
break;
}
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') {
if (currentEvent.data) {
logDebug("Processing SSE event data:", currentEvent.data.substring(0, 100) + '...');
if (currentEvent.data.startsWith('{') || currentEvent.data.startsWith('[')) {
try {
let json = JSON.parse(currentEvent.data);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from SSE data: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'SSE');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logError("SSE JSON parse error:", e, "Data:", currentEvent.data.substring(0,200)+"...");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
} else {
logDebug("SSE data is not JSON, forwarding as is.");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
currentEvent = { data: '', type: 'message', id: null };
} else if (line.startsWith('data:')) {
currentEvent.data += (currentEvent.data ? '\n' : '') + line.substring(5).trim();
} else if (line.startsWith('event:')) {
currentEvent.type = line.substring(6).trim();
} else if (line.startsWith('id:')) {
currentEvent.id = line.substring(3).trim();
} else if (line.startsWith(':')) {
} else {
logDebug("Unknown SSE line:", line);
}
}
}
} catch (e) {
logError('Error reading/processing SSE stream:', e);
controller.error(e);
} finally {
reader.releaseLock();
}
}
});
const newHeaders = new Headers(response.headers);
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
if (contentType.includes('application/json')) {
logDebug(`Processing JSON response for ${url}`);
try {
const text = await response.text();
let json = JSON.parse(text);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from JSON response: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'Fetch');
const newBody = JSON.stringify(json);
const newHeaders = new Headers(response.headers);
if (newHeaders.has('content-length')) {
newHeaders.set('content-length', encoder.encode(newBody).byteLength.toString());
}
return new Response(newBody, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
} catch (e) {
logError('Fetch JSON processing error:', e, 'URL:', url);
return original_response;
}
}
logDebug(`Non-SSE/JSON response for ${url}, skipping processing.`);
return original_response;
}
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function(input, init) {
if (!demodEnabled) {
return originalFetch.apply(this, arguments);
}
let url;
let requestArgs = init || {};
try {
url = (input instanceof Request) ? input.url : String(input);
} catch (e) {
logDebug('Invalid fetch input, passing through:', input, e);
return originalFetch.apply(this, arguments);
}
if (!url.includes('/rest/app-chat/')) {
return originalFetch.apply(this, arguments);
}
if (requestArgs.method === 'POST') {
logDebug(`Observing POST request: ${url}`);
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
if (!currentConversationId) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from POST URL: ${currentConversationId}`);
}
if (!initCache && requestArgs.headers) {
logDebug(`Caching headers from POST request to ${idFromUrl}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
}
}
return originalFetch.apply(this, arguments);
}
logDebug(`Intercepting fetch request: ${requestArgs.method || 'GET'} ${url}`);
try {
const original_response = await originalFetch.apply(this, arguments);
return await handleFetchResponse(original_response, url, requestArgs);
} catch (error) {
logError(`Fetch interception failed for ${url}:`, error);
throw error;
}
};
const OriginalWebSocket = unsafeWindow.WebSocket;
unsafeWindow.WebSocket = new Proxy(OriginalWebSocket, {
construct(target, args) {
const url = args[0];
logDebug('WebSocket connection attempt:', url);
const ws = new target(...args);
let originalOnMessageHandler = null;
Object.defineProperty(ws, 'onmessage', {
configurable: true,
enumerable: true,
get() {
return originalOnMessageHandler;
},
async set(handler) {
logDebug('WebSocket onmessage handler assigned');
originalOnMessageHandler = handler;
ws.onmessageinternal = async function(event) {
if (!demodEnabled || typeof event.data !== 'string' || !event.data.startsWith('{')) {
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (e) {
logError("Error in original WebSocket onmessage handler:", e);
}
}
return;
}
logDebug('Intercepting WebSocket message:', event.data.substring(0, 200) + '...');
try {
let json = JSON.parse(event.data);
if (json.conversation_id && json.conversation_id !== currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from WebSocket: ${currentConversationId}`);
}
const processedJson = await processPotentialModeration(json, 'WebSocket');
const newEvent = new MessageEvent('message', {
data: JSON.stringify(processedJson),
origin: event.origin,
lastEventId: event.lastEventId,
source: event.source,
ports: event.ports,
});
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, newEvent);
} catch (e) {
logError("Error calling original WebSocket onmessage handler after modification:", e);
}
} else {
logDebug("Original WebSocket onmessage handler not found when message received.");
}
} catch (e) {
logError('WebSocket processing error:', e, 'Data:', event.data.substring(0, 200) + '...');
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (eInner) {
logError("Error in original WebSocket onmessage handler (fallback path):", eInner);
}
}
}
};
ws.addEventListener('message', ws.onmessageinternal);
}
});
const wrapHandler = (eventName) => {
let originalHandler = null;
Object.defineProperty(ws, `on${eventName}`, {
configurable: true,
enumerable: true,
get() { return originalHandler; },
set(handler) {
logDebug(`WebSocket on${eventName} handler assigned`);
originalHandler = handler;
ws.addEventListener(eventName, (event) => {
if (eventName === 'message') return;
logDebug(`WebSocket event: ${eventName}`, event);
if (originalHandler) {
try {
originalHandler.call(ws, event);
} catch (e) {
logError(`Error in original WebSocket on${eventName} handler:`, e);
}
}
});
}
});
};
wrapHandler('close');
wrapHandler('error');
ws.addEventListener('open', () => logDebug('WebSocket opened:', url));
return ws;
}
});
if (window.location.hostname !== 'grok.com') {
console.log('[Grok DeMod] Script inactive: Intended for grok.com only. Current host:', window.location.hostname);
return;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupUI);
} else {
setupUI();
}
console.log('[Grok DeMod] Enhanced Script loaded. Interception is', demodEnabled ? 'ACTIVE' : 'INACTIVE', '. Debug is', debug ? 'ON' : 'OFF');
})();// ==UserScript==
// @name Grok DeMod
// @license GPL-3.0-or-later
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Hides moderation results in Grok conversations, auto-recovers blocked messages.
// @author UniverseDev
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @match https://grok.com/*
// @grant unsafeWindow
// @downloadURL https://update.greasyfork.org/scripts/531147/Grok DeMod.user.js
// @updateURL https://update.greasyfork.org/scripts/531147/Grok DeMod.meta.js
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
defaultFlags: [
'isFlagged', 'isBlocked', 'moderationApplied', 'restricted'
],
messageKeys: ['message', 'content', 'text', 'error'],
moderationMessagePatterns: [
/this content has been moderated/i,
/sorry, i cannot assist/i,
/policy violation/i,
/blocked/i,
/moderated/i,
/restricted/i,
/content restricted/i,
/unable to process/i,
/cannot help/i,
/(sorry|apologies).*?(cannot|unable|help|assist)/i,
],
clearedMessageText: '[Content cleared by Grok DeMod]',
recoveryTimeoutMs: 5000,
lsKeys: {
enabled: 'GrokDeModEnabled',
debug: 'GrokDeModDebug',
flags: 'GrokDeModFlags',
},
styles: {
uiContainer: `
position: fixed;
bottom: 10px;
right: 10px;
z-index: 10000;
background: #2d2d2d;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
gap: 8px;
font-family: Arial, sans-serif;
color: #e0e0e0;
min-width: 170px;
`,
button: `
padding: 6px 12px;
border-radius: 5px;
border: none;
cursor: pointer;
color: #fff;
font-size: 13px;
transition: background-color 0.2s ease;
`,
status: `
padding: 5px;
font-size: 12px;
color: #a0a0a0;
text-align: center;
border-top: 1px solid #444;
margin-top: 5px;
min-height: 16px;
`,
logContainer: `
max-height: 100px;
overflow-y: auto;
font-size: 11px;
color: #c0c0c0;
background-color: #333;
padding: 5px;
border-radius: 4px;
line-height: 1.4;
margin-top: 5px;
`,
logEntry: `
padding-bottom: 3px;
border-bottom: 1px dashed #555;
margin-bottom: 3px;
word-break: break-word;
`,
colors: {
enabled: '#388E3C',
disabled: '#D32F2F',
debugEnabled: '#1976D2',
debugDisabled: '#555555',
safe: '#66ff66',
flagged: '#ffa500',
blocked: '#ff6666',
recovering: '#ffcc00'
}
}
};
let demodEnabled = getState(CONFIG.lsKeys.enabled, true);
let debug = getState(CONFIG.lsKeys.debug, false);
let moderationFlags = getState(CONFIG.lsKeys.flags, CONFIG.defaultFlags);
let initCache = null;
let currentConversationId = null;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const uiLogBuffer = [];
const MAX_LOG_ENTRIES = 50;
const ModerationResult = Object.freeze({
SAFE: 0,
FLAGGE
1,
BLOCKE
2,
});
function logDebug(...args) {
if (debug) {
console.log('[Grok DeMod]', ...args);
}
}
function logError(...args) {
console.error('[Grok DeMod]', ...args);
}
function getState(key, defaultValue) {
try {
const value = localStorage.getItem(key);
if (value === null) return defaultValue;
if (value === 'true') return true;
if (value === 'false') return false;
return JSON.parse(value);
} catch (e) {
logError(`Error reading ${key} from localStorage:`, e);
return defaultValue;
}
}
function setState(key, value) {
try {
const valueToStore = typeof value === 'boolean' ? value.toString() : JSON.stringify(value);
localStorage.setItem(key, valueToStore);
} catch (e) {
logError(`Error writing ${key} to localStorage:`, e);
}
}
function timeoutPromise(ms, promise, description = 'Promise') {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
logDebug(`${description} timed out after ${ms}ms`);
reject(new Error(`Timeout (${description})`));
}, ms);
promise.then(
(value) => { clearTimeout(timer); resolve(value); },
(error) => { clearTimeout(timer); reject(error); }
);
});
}
function getModerationResult(obj, path = '') {
if (typeof obj !== 'object' || obj === null) return ModerationResult.SAFE;
let result = ModerationResult.SAFE;
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const currentPath = path ? `${path}.${key}` : key;
const value = obj[key];
if (key === 'isBlocked' && value === true) {
logDebug(`Blocked detected via flag '${currentPath}'`);
return ModerationResult.BLOCKED;
}
if (moderationFlags.includes(key) && value === true) {
logDebug(`Flagged detected via flag '${currentPath}'`);
result = Math.max(result, ModerationResult.FLAGGED);
}
if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
const content = value.toLowerCase();
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(content)) {
logDebug(`Moderation pattern matched in '${currentPath}': "${content.substring(0, 50)}..."`);
if (/blocked|moderated|restricted/i.test(pattern.source)) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, ModerationResult.FLAGGED);
break;
}
}
if (result === ModerationResult.SAFE && content.length < 70 && /(sorry|apologies|unable|cannot)/i.test(content)) {
logDebug(`Heuristic moderation detected in '${currentPath}': "${content.substring(0, 50)}..."`);
result = Math.max(result, ModerationResult.FLAGGED);
}
}
if (typeof value === 'object') {
const childResult = getModerationResult(value, currentPath);
if (childResult === ModerationResult.BLOCKED) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, childResult);
}
}
return result;
}
function clearFlagging(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(item => clearFlagging(item));
}
const newObj = {};
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key];
if (moderationFlags.includes(key) && value === true) {
newObj[key] = false;
logDebug(`Cleared flag '${key}'`);
}
else if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
let replaced = false;
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(value)) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced moderated message in '${key}' using pattern`);
replaced = true;
break;
}
}
if (!replaced && value.length < 70 && /(sorry|apologies|unable|cannot)/i.test(value.toLowerCase())) {
if (getModerationResult({[key]: value}) === ModerationResult.FLAGGED) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced heuristic moderated message in '${key}'`);
replaced = true;
}
}
if (!replaced) {
newObj[key] = value;
}
}
else if (typeof value === 'object') {
newObj[key] = clearFlagging(value);
}
else {
newObj[key] = value;
}
}
return newObj;
}
let uiContainer, toggleButton, debugButton, statusEl, logContainer;
function addLog(message) {
if (!logContainer) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.textContent = `[${timestamp}] ${message}`;
logEntry.style.cssText = CONFIG.styles.logEntry;
uiLogBuffer.push(logEntry);
if (uiLogBuffer.length > MAX_LOG_ENTRIES) {
const removed = uiLogBuffer.shift();
if (removed && removed.parentNode === logContainer) {
logContainer.removeChild(removed);
}
}
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(modResult, isRecovering = false) {
if (!statusEl) return;
let text = 'Status: ';
let color = CONFIG.styles.colors.safe;
if (isRecovering) {
text += 'Recovering...';
color = CONFIG.styles.colors.recovering;
} else if (modResult === ModerationResult.BLOCKED) {
text += 'Blocked (Recovered/Cleared)';
color = CONFIG.styles.colors.blocked;
} else if (modResult === ModerationResult.FLAGGED) {
text += 'Flagged (Cleared)';
color = CONFIG.styles.colors.flagged;
} else {
text += 'Safe';
color = CONFIG.styles.colors.safe;
}
statusEl.textContent = text;
statusEl.style.color = color;
}
function setupUI() {
uiContainer = document.createElement('div');
uiContainer.id = 'grok-demod-ui';
uiContainer.style.cssText = CONFIG.styles.uiContainer;
toggleButton = document.createElement('button');
debugButton = document.createElement('button');
statusEl = document.createElement('div');
logContainer = document.createElement('div');
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.title = 'Toggle DeMod functionality (ON = intercepting)';
toggleButton.style.cssText = CONFIG.styles.button;
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
toggleButton.onclick = (e) => {
demodEnabled = !demodEnabled;
setState(CONFIG.lsKeys.enabled, demodEnabled);
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
addLog(`DeMod ${demodEnabled ? 'Enabled' : 'Disabled'}.`);
console.log('[Grok DeMod] Interception is now', demodEnabled ? 'ACTIVE' : 'INACTIVE');
};
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.title = 'Toggle debug mode (logs verbose details to console)';
debugButton.style.cssText = CONFIG.styles.button;
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
debugButton.onclick = (e) => {
debug = !debug;
setState(CONFIG.lsKeys.debug, debug);
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
addLog(`Debug Mode ${debug ? 'Enabled' : 'Disabled'}.`);
logDebug(`Debug mode ${debug ? 'enabled' : 'disabled'}.`);
};
statusEl.id = 'grok-demod-status';
statusEl.style.cssText = CONFIG.styles.status;
updateStatus(ModerationResult.SAFE);
logContainer.id = 'grok-demod-log';
logContainer.style.cssText = CONFIG.styles.logContainer;
uiLogBuffer.forEach(entry => logContainer.appendChild(entry));
logContainer.scrollTop = logContainer.scrollHeight;
uiContainer.appendChild(toggleButton);
uiContainer.appendChild(debugButton);
uiContainer.appendChild(statusEl);
uiContainer.appendChild(logContainer);
document.body.appendChild(uiContainer);
addLog("Grok DeMod Initialized.");
if (debug) addLog("Debug mode is ON.");
}
async function redownloadLatestMessage() {
if (!currentConversationId) {
logDebug('Recovery skipped: Missing conversationId');
addLog('Recovery failed: No conversation ID.');
return null;
}
if (!initCache || !initCache.headers) {
logDebug('Recovery cache missing, attempting fresh fetch for headers...');
try {
const currentConvUrl = `/rest/app-chat/conversation/${currentConversationId}`;
const tempResp = await originalFetch(currentConvUrl, { method: 'GET', headers: {'Accept': 'application/json'} });
if (tempResp.ok) {
logDebug('Fresh header fetch successful (status OK).');
initCache = { headers: new Headers({'Accept': 'application/json'}), credentials: 'include' };
} else {
logDebug(`Fresh header fetch failed with status ${tempResp.status}. Recovery may fail.`);
addLog('Recovery failed: Cannot get request data.');
return null;
}
} catch (e) {
logError('Error during fresh header fetch:', e);
addLog('Recovery failed: Error getting request data.');
return null;
}
}
const url = `/rest/app-chat/conversation/${currentConversationId}`;
logDebug(`Attempting recovery fetch for conversation: ${currentConversationId}`);
addLog('Attempting content recovery...');
const headers = new Headers(initCache.headers);
if (!headers.has('Accept')) headers.set('Accept', 'application/json, text/plain, */*');
const requestOptions = {
method: 'GET',
headers: headers,
credentials: initCache.credentials || 'include',
};
try {
const response = await timeoutPromise(
CONFIG.recoveryTimeoutMs,
fetch(url, requestOptions),
'Recovery Fetch'
);
if (!response.ok) {
logError(`Recovery fetch failed with status ${response.status}: ${response.statusText}`);
addLog(`Recovery failed: HTTP ${response.status}`);
try {
const errorBody = await response.text();
logDebug('Recovery error body:', errorBody.substring(0, 500));
} catch (e) { }
return null;
}
const data = await response.json();
const messages = data?.messages;
if (!Array.isArray(messages) || messages.length === 0) {
logDebug('Recovery failed: No messages found in conversation data', data);
addLog('Recovery failed: No messages found.');
return null;
}
messages.sort((a, b) => {
const tsA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
const tsB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
return tsB - tsA;
});
const latestMessage = messages[0];
if (!latestMessage || typeof latestMessage.content !== 'string' || latestMessage.content.trim() === '') {
logDebug('Recovery failed: Latest message or its content is invalid/empty', latestMessage);
addLog('Recovery failed: Invalid latest message.');
return null;
}
logDebug('Recovery successful, latest content:', latestMessage.content.substring(0, 100) + '...');
addLog('Recovery seems successful.');
return { content: latestMessage.content };
} catch (e) {
logError('Recovery fetch/parse error:', e);
addLog(`Recovery error: ${e.message}`);
return null;
}
}
function extractConversationIdFromUrl(url) {
const match = url.match(/\/conversation\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
return match ? match[1] : null;
}
async function processPotentialModeration(json, source) {
const modResult = getModerationResult(json);
let finalJson = json;
if (modResult !== ModerationResult.SAFE) {
if (modResult === ModerationResult.BLOCKED) {
logDebug(`Blocked content detected from ${source}:`, JSON.stringify(json).substring(0, 200) + '...');
addLog(`Blocked content from ${source}.`);
updateStatus(modResult, true);
const recoveredData = await redownloadLatestMessage();
if (recoveredData && recoveredData.content) {
addLog(`Recovery successful (${source}).`);
logDebug(`Recovered content applied (${source})`);
let replaced = false;
const keysToTry = [...CONFIG.messageKeys, 'text', 'message'];
for (const key of keysToTry) {
if (typeof finalJson[key] === 'string') {
finalJson[key] = recoveredData.content;
logDebug(`Injected recovered content into key '${key}'`);
replaced = true;
break;
}
}
if (!replaced) {
logDebug("Could not find standard key to inject recovered content, adding as 'recovered_content'");
finalJson.recovered_content = recoveredData.content;
}
finalJson = clearFlagging(finalJson);
updateStatus(modResult, false);
} else {
addLog(`Recovery failed (${source}). Content may be lost.`);
logDebug(`Recovery failed (${source}), applying standard clearing.`);
finalJson = clearFlagging(json);
updateStatus(modResult, false);
}
} else {
logDebug(`Flagged content detected and cleared from ${source}.`);
addLog(`Flagged content cleared (${source}).`);
finalJson = clearFlagging(json);
updateStatus(modResult);
}
} else {
if (statusEl && !statusEl.textContent.includes('Blocked') && !statusEl.textContent.includes('Flagged') && !statusEl.textContent.includes('Recovering')) {
updateStatus(modResult);
} else if (statusEl && statusEl.textContent.includes('Recovering')) {
logDebug("Recovery attempt finished (next message safe). Resetting status.");
updateStatus(ModerationResult.SAFE);
}
}
return finalJson;
}
async function handleFetchResponse(original_response, url, requestArgs) {
const response = original_response.clone();
if (!response.ok) {
logDebug(`Fetch response not OK (${response.status}) for ${url}, skipping processing.`);
return original_response;
}
const contentType = response.headers.get('Content-Type')?.toLowerCase() || '';
logDebug(`Intercepted fetch response for ${url}, Content-Type: ${contentType}`);
const conversationGetMatch = url.match(/\/rest\/app-chat\/conversation\/([a-f0-9-]+)$/i);
if (conversationGetMatch && requestArgs?.method === 'GET') {
logDebug(`Caching GET request options for conversation ${conversationGetMatch[1]}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
if (!currentConversationId) {
currentConversationId = conversationGetMatch[1];
logDebug(`Conversation ID set from GET URL: ${currentConversationId}`);
}
}
if (!currentConversationId) {
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from other URL: ${currentConversationId}`);
}
}
if (contentType.includes('text/event-stream')) {
logDebug(`Processing SSE stream for ${url}`);
const reader = response.body.getReader();
const stream = new ReadableStream({
async start(controller) {
let buffer = '';
let currentEvent = { data: '', type: 'message', id: null };
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
logDebug("SSE stream ended, processing final buffer:", buffer);
if (buffer.startsWith('{') || buffer.startsWith('[')) {
try {
let json = JSON.parse(buffer);
json = await processPotentialModeration(json, 'SSE-Final');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logDebug("Error parsing final SSE buffer, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else {
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else if (currentEvent.data) {
logDebug("SSE stream ended after data field, processing event:", currentEvent.data.substring(0,100)+"...");
try {
let json = JSON.parse(currentEvent.data);
json = await processPotentialModeration(json, 'SSE-Event');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch (e) {
logDebug("Error parsing trailing SSE data, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
controller.close();
break;
}
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') {
if (currentEvent.data) {
logDebug("Processing SSE event data:", currentEvent.data.substring(0, 100) + '...');
if (currentEvent.data.startsWith('{') || currentEvent.data.startsWith('[')) {
try {
let json = JSON.parse(currentEvent.data);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from SSE data: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'SSE');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logError("SSE JSON parse error:", e, "Data:", currentEvent.data.substring(0,200)+"...");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
} else {
logDebug("SSE data is not JSON, forwarding as is.");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
currentEvent = { data: '', type: 'message', id: null };
} else if (line.startsWith('data:')) {
currentEvent.data += (currentEvent.data ? '\n' : '') + line.substring(5).trim();
} else if (line.startsWith('event:')) {
currentEvent.type = line.substring(6).trim();
} else if (line.startsWith('id:')) {
currentEvent.id = line.substring(3).trim();
} else if (line.startsWith(':')) {
} else {
logDebug("Unknown SSE line:", line);
}
}
}
} catch (e) {
logError('Error reading/processing SSE stream:', e);
controller.error(e);
} finally {
reader.releaseLock();
}
}
});
const newHeaders = new Headers(response.headers);
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
if (contentType.includes('application/json')) {
logDebug(`Processing JSON response for ${url}`);
try {
const text = await response.text();
let json = JSON.parse(text);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from JSON response: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'Fetch');
const newBody = JSON.stringify(json);
const newHeaders = new Headers(response.headers);
if (newHeaders.has('content-length')) {
newHeaders.set('content-length', encoder.encode(newBody).byteLength.toString());
}
return new Response(newBody, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
} catch (e) {
logError('Fetch JSON processing error:', e, 'URL:', url);
return original_response;
}
}
logDebug(`Non-SSE/JSON response for ${url}, skipping processing.`);
return original_response;
}
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function(input, init) {
if (!demodEnabled) {
return originalFetch.apply(this, arguments);
}
let url;
let requestArgs = init || {};
try {
url = (input instanceof Request) ? input.url : String(input);
} catch (e) {
logDebug('Invalid fetch input, passing through:', input, e);
return originalFetch.apply(this, arguments);
}
if (!url.includes('/rest/app-chat/')) {
return originalFetch.apply(this, arguments);
}
if (requestArgs.method === 'POST') {
logDebug(`Observing POST request: ${url}`);
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
if (!currentConversationId) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from POST URL: ${currentConversationId}`);
}
if (!initCache && requestArgs.headers) {
logDebug(`Caching headers from POST request to ${idFromUrl}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
}
}
return originalFetch.apply(this, arguments);
}
logDebug(`Intercepting fetch request: ${requestArgs.method || 'GET'} ${url}`);
try {
const original_response = await originalFetch.apply(this, arguments);
return await handleFetchResponse(original_response, url, requestArgs);
} catch (error) {
logError(`Fetch interception failed for ${url}:`, error);
throw error;
}
};
const OriginalWebSocket = unsafeWindow.WebSocket;
unsafeWindow.WebSocket = new Proxy(OriginalWebSocket, {
construct(target, args) {
const url = args[0];
logDebug('WebSocket connection attempt:', url);
const ws = new target(...args);
let originalOnMessageHandler = null;
Object.defineProperty(ws, 'onmessage', {
configurable: true,
enumerable: true,
get() {
return originalOnMessageHandler;
},
async set(handler) {
logDebug('WebSocket onmessage handler assigned');
originalOnMessageHandler = handler;
ws.onmessageinternal = async function(event) {
if (!demodEnabled || typeof event.data !== 'string' || !event.data.startsWith('{')) {
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (e) {
logError("Error in original WebSocket onmessage handler:", e);
}
}
return;
}
logDebug('Intercepting WebSocket message:', event.data.substring(0, 200) + '...');
try {
let json = JSON.parse(event.data);
if (json.conversation_id && json.conversation_id !== currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from WebSocket: ${currentConversationId}`);
}
const processedJson = await processPotentialModeration(json, 'WebSocket');
const newEvent = new MessageEvent('message', {
data: JSON.stringify(processedJson),
origin: event.origin,
lastEventId: event.lastEventId,
source: event.source,
ports: event.ports,
});
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, newEvent);
} catch (e) {
logError("Error calling original WebSocket onmessage handler after modification:", e);
}
} else {
logDebug("Original WebSocket onmessage handler not found when message received.");
}
} catch (e) {
logError('WebSocket processing error:', e, 'Data:', event.data.substring(0, 200) + '...');
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (eInner) {
logError("Error in original WebSocket onmessage handler (fallback path):", eInner);
}
}
}
};
ws.addEventListener('message', ws.onmessageinternal);
}
});
const wrapHandler = (eventName) => {
let originalHandler = null;
Object.defineProperty(ws, `on${eventName}`, {
configurable: true,
enumerable: true,
get() { return originalHandler; },
set(handler) {
logDebug(`WebSocket on${eventName} handler assigned`);
originalHandler = handler;
ws.addEventListener(eventName, (event) => {
if (eventName === 'message') return;
logDebug(`WebSocket event: ${eventName}`, event);
if (originalHandler) {
try {
originalHandler.call(ws, event);
} catch (e) {
logError(`Error in original WebSocket on${eventName} handler:`, e);
}
}
});
}
});
};
wrapHandler('close');
wrapHandler('error');
ws.addEventListener('open', () => logDebug('WebSocket opened:', url));
return ws;
}
});
if (window.location.hostname !== 'grok.com') {
console.log('[Grok DeMod] Script inactive: Intended for grok.com only. Current host:', window.location.hostname);
return;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupUI);
} else {
setupUI();
}
console.log('[Grok DeMod] Enhanced Script loaded. Interception is', demodEnabled ? 'ACTIVE' : 'INACTIVE', '. Debug is', debug ? 'ON' : 'OFF');
})();// ==UserScript==
// @name Grok DeMod
// @license GPL-3.0-or-later
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Hides moderation results in Grok conversations, auto-recovers blocked messages.
// @author UniverseDev
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=grok.com
// @match https://grok.com/*
// @grant unsafeWindow
// @downloadURL https://update.greasyfork.org/scripts/531147/Grok DeMod.user.js
// @updateURL https://update.greasyfork.org/scripts/531147/Grok DeMod.meta.js
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
defaultFlags: [
'isFlagged', 'isBlocked', 'moderationApplied', 'restricted'
],
messageKeys: ['message', 'content', 'text', 'error'],
moderationMessagePatterns: [
/this content has been moderated/i,
/sorry, i cannot assist/i,
/policy violation/i,
/blocked/i,
/moderated/i,
/restricted/i,
/content restricted/i,
/unable to process/i,
/cannot help/i,
/(sorry|apologies).*?(cannot|unable|help|assist)/i,
],
clearedMessageText: '[Content cleared by Grok DeMod]',
recoveryTimeoutMs: 5000,
lsKeys: {
enabled: 'GrokDeModEnabled',
debug: 'GrokDeModDebug',
flags: 'GrokDeModFlags',
},
styles: {
uiContainer: `
position: fixed;
bottom: 10px;
right: 10px;
z-index: 10000;
background: #2d2d2d;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
display: flex;
flex-direction: column;
gap: 8px;
font-family: Arial, sans-serif;
color: #e0e0e0;
min-width: 170px;
`,
button: `
padding: 6px 12px;
border-radius: 5px;
border: none;
cursor: pointer;
color: #fff;
font-size: 13px;
transition: background-color 0.2s ease;
`,
status: `
padding: 5px;
font-size: 12px;
color: #a0a0a0;
text-align: center;
border-top: 1px solid #444;
margin-top: 5px;
min-height: 16px;
`,
logContainer: `
max-height: 100px;
overflow-y: auto;
font-size: 11px;
color: #c0c0c0;
background-color: #333;
padding: 5px;
border-radius: 4px;
line-height: 1.4;
margin-top: 5px;
`,
logEntry: `
padding-bottom: 3px;
border-bottom: 1px dashed #555;
margin-bottom: 3px;
word-break: break-word;
`,
colors: {
enabled: '#388E3C',
disabled: '#D32F2F',
debugEnabled: '#1976D2',
debugDisabled: '#555555',
safe: '#66ff66',
flagged: '#ffa500',
blocked: '#ff6666',
recovering: '#ffcc00'
}
}
};
let demodEnabled = getState(CONFIG.lsKeys.enabled, true);
let debug = getState(CONFIG.lsKeys.debug, false);
let moderationFlags = getState(CONFIG.lsKeys.flags, CONFIG.defaultFlags);
let initCache = null;
let currentConversationId = null;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const uiLogBuffer = [];
const MAX_LOG_ENTRIES = 50;
const ModerationResult = Object.freeze({
SAFE: 0,
FLAGGE
1,
BLOCKE
2,
});
function logDebug(...args) {
if (debug) {
console.log('[Grok DeMod]', ...args);
}
}
function logError(...args) {
console.error('[Grok DeMod]', ...args);
}
function getState(key, defaultValue) {
try {
const value = localStorage.getItem(key);
if (value === null) return defaultValue;
if (value === 'true') return true;
if (value === 'false') return false;
return JSON.parse(value);
} catch (e) {
logError(`Error reading ${key} from localStorage:`, e);
return defaultValue;
}
}
function setState(key, value) {
try {
const valueToStore = typeof value === 'boolean' ? value.toString() : JSON.stringify(value);
localStorage.setItem(key, valueToStore);
} catch (e) {
logError(`Error writing ${key} to localStorage:`, e);
}
}
function timeoutPromise(ms, promise, description = 'Promise') {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
logDebug(`${description} timed out after ${ms}ms`);
reject(new Error(`Timeout (${description})`));
}, ms);
promise.then(
(value) => { clearTimeout(timer); resolve(value); },
(error) => { clearTimeout(timer); reject(error); }
);
});
}
function getModerationResult(obj, path = '') {
if (typeof obj !== 'object' || obj === null) return ModerationResult.SAFE;
let result = ModerationResult.SAFE;
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const currentPath = path ? `${path}.${key}` : key;
const value = obj[key];
if (key === 'isBlocked' && value === true) {
logDebug(`Blocked detected via flag '${currentPath}'`);
return ModerationResult.BLOCKED;
}
if (moderationFlags.includes(key) && value === true) {
logDebug(`Flagged detected via flag '${currentPath}'`);
result = Math.max(result, ModerationResult.FLAGGED);
}
if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
const content = value.toLowerCase();
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(content)) {
logDebug(`Moderation pattern matched in '${currentPath}': "${content.substring(0, 50)}..."`);
if (/blocked|moderated|restricted/i.test(pattern.source)) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, ModerationResult.FLAGGED);
break;
}
}
if (result === ModerationResult.SAFE && content.length < 70 && /(sorry|apologies|unable|cannot)/i.test(content)) {
logDebug(`Heuristic moderation detected in '${currentPath}': "${content.substring(0, 50)}..."`);
result = Math.max(result, ModerationResult.FLAGGED);
}
}
if (typeof value === 'object') {
const childResult = getModerationResult(value, currentPath);
if (childResult === ModerationResult.BLOCKED) {
return ModerationResult.BLOCKED;
}
result = Math.max(result, childResult);
}
}
return result;
}
function clearFlagging(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(item => clearFlagging(item));
}
const newObj = {};
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
const value = obj[key];
if (moderationFlags.includes(key) && value === true) {
newObj[key] = false;
logDebug(`Cleared flag '${key}'`);
}
else if (CONFIG.messageKeys.includes(key) && typeof value === 'string') {
let replaced = false;
for (const pattern of CONFIG.moderationMessagePatterns) {
if (pattern.test(value)) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced moderated message in '${key}' using pattern`);
replaced = true;
break;
}
}
if (!replaced && value.length < 70 && /(sorry|apologies|unable|cannot)/i.test(value.toLowerCase())) {
if (getModerationResult({[key]: value}) === ModerationResult.FLAGGED) {
newObj[key] = CONFIG.clearedMessageText;
logDebug(`Replaced heuristic moderated message in '${key}'`);
replaced = true;
}
}
if (!replaced) {
newObj[key] = value;
}
}
else if (typeof value === 'object') {
newObj[key] = clearFlagging(value);
}
else {
newObj[key] = value;
}
}
return newObj;
}
let uiContainer, toggleButton, debugButton, statusEl, logContainer;
function addLog(message) {
if (!logContainer) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.textContent = `[${timestamp}] ${message}`;
logEntry.style.cssText = CONFIG.styles.logEntry;
uiLogBuffer.push(logEntry);
if (uiLogBuffer.length > MAX_LOG_ENTRIES) {
const removed = uiLogBuffer.shift();
if (removed && removed.parentNode === logContainer) {
logContainer.removeChild(removed);
}
}
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(modResult, isRecovering = false) {
if (!statusEl) return;
let text = 'Status: ';
let color = CONFIG.styles.colors.safe;
if (isRecovering) {
text += 'Recovering...';
color = CONFIG.styles.colors.recovering;
} else if (modResult === ModerationResult.BLOCKED) {
text += 'Blocked (Recovered/Cleared)';
color = CONFIG.styles.colors.blocked;
} else if (modResult === ModerationResult.FLAGGED) {
text += 'Flagged (Cleared)';
color = CONFIG.styles.colors.flagged;
} else {
text += 'Safe';
color = CONFIG.styles.colors.safe;
}
statusEl.textContent = text;
statusEl.style.color = color;
}
function setupUI() {
uiContainer = document.createElement('div');
uiContainer.id = 'grok-demod-ui';
uiContainer.style.cssText = CONFIG.styles.uiContainer;
toggleButton = document.createElement('button');
debugButton = document.createElement('button');
statusEl = document.createElement('div');
logContainer = document.createElement('div');
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.title = 'Toggle DeMod functionality (ON = intercepting)';
toggleButton.style.cssText = CONFIG.styles.button;
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
toggleButton.onclick = (e) => {
demodEnabled = !demodEnabled;
setState(CONFIG.lsKeys.enabled, demodEnabled);
toggleButton.textContent = demodEnabled ? 'DeMod: ON' : 'DeMod: OFF';
toggleButton.style.backgroundColor = demodEnabled ? CONFIG.styles.colors.enabled : CONFIG.styles.colors.disabled;
addLog(`DeMod ${demodEnabled ? 'Enabled' : 'Disabled'}.`);
console.log('[Grok DeMod] Interception is now', demodEnabled ? 'ACTIVE' : 'INACTIVE');
};
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.title = 'Toggle debug mode (logs verbose details to console)';
debugButton.style.cssText = CONFIG.styles.button;
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
debugButton.onclick = (e) => {
debug = !debug;
setState(CONFIG.lsKeys.debug, debug);
debugButton.textContent = debug ? 'Debug: ON' : 'Debug: OFF';
debugButton.style.backgroundColor = debug ? CONFIG.styles.colors.debugEnabled : CONFIG.styles.colors.debugDisabled;
addLog(`Debug Mode ${debug ? 'Enabled' : 'Disabled'}.`);
logDebug(`Debug mode ${debug ? 'enabled' : 'disabled'}.`);
};
statusEl.id = 'grok-demod-status';
statusEl.style.cssText = CONFIG.styles.status;
updateStatus(ModerationResult.SAFE);
logContainer.id = 'grok-demod-log';
logContainer.style.cssText = CONFIG.styles.logContainer;
uiLogBuffer.forEach(entry => logContainer.appendChild(entry));
logContainer.scrollTop = logContainer.scrollHeight;
uiContainer.appendChild(toggleButton);
uiContainer.appendChild(debugButton);
uiContainer.appendChild(statusEl);
uiContainer.appendChild(logContainer);
document.body.appendChild(uiContainer);
addLog("Grok DeMod Initialized.");
if (debug) addLog("Debug mode is ON.");
}
async function redownloadLatestMessage() {
if (!currentConversationId) {
logDebug('Recovery skipped: Missing conversationId');
addLog('Recovery failed: No conversation ID.');
return null;
}
if (!initCache || !initCache.headers) {
logDebug('Recovery cache missing, attempting fresh fetch for headers...');
try {
const currentConvUrl = `/rest/app-chat/conversation/${currentConversationId}`;
const tempResp = await originalFetch(currentConvUrl, { method: 'GET', headers: {'Accept': 'application/json'} });
if (tempResp.ok) {
logDebug('Fresh header fetch successful (status OK).');
initCache = { headers: new Headers({'Accept': 'application/json'}), credentials: 'include' };
} else {
logDebug(`Fresh header fetch failed with status ${tempResp.status}. Recovery may fail.`);
addLog('Recovery failed: Cannot get request data.');
return null;
}
} catch (e) {
logError('Error during fresh header fetch:', e);
addLog('Recovery failed: Error getting request data.');
return null;
}
}
const url = `/rest/app-chat/conversation/${currentConversationId}`;
logDebug(`Attempting recovery fetch for conversation: ${currentConversationId}`);
addLog('Attempting content recovery...');
const headers = new Headers(initCache.headers);
if (!headers.has('Accept')) headers.set('Accept', 'application/json, text/plain, */*');
const requestOptions = {
method: 'GET',
headers: headers,
credentials: initCache.credentials || 'include',
};
try {
const response = await timeoutPromise(
CONFIG.recoveryTimeoutMs,
fetch(url, requestOptions),
'Recovery Fetch'
);
if (!response.ok) {
logError(`Recovery fetch failed with status ${response.status}: ${response.statusText}`);
addLog(`Recovery failed: HTTP ${response.status}`);
try {
const errorBody = await response.text();
logDebug('Recovery error body:', errorBody.substring(0, 500));
} catch (e) { }
return null;
}
const data = await response.json();
const messages = data?.messages;
if (!Array.isArray(messages) || messages.length === 0) {
logDebug('Recovery failed: No messages found in conversation data', data);
addLog('Recovery failed: No messages found.');
return null;
}
messages.sort((a, b) => {
const tsA = a.timestamp ? new Date(a.timestamp).getTime() : 0;
const tsB = b.timestamp ? new Date(b.timestamp).getTime() : 0;
return tsB - tsA;
});
const latestMessage = messages[0];
if (!latestMessage || typeof latestMessage.content !== 'string' || latestMessage.content.trim() === '') {
logDebug('Recovery failed: Latest message or its content is invalid/empty', latestMessage);
addLog('Recovery failed: Invalid latest message.');
return null;
}
logDebug('Recovery successful, latest content:', latestMessage.content.substring(0, 100) + '...');
addLog('Recovery seems successful.');
return { content: latestMessage.content };
} catch (e) {
logError('Recovery fetch/parse error:', e);
addLog(`Recovery error: ${e.message}`);
return null;
}
}
function extractConversationIdFromUrl(url) {
const match = url.match(/\/conversation\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
return match ? match[1] : null;
}
async function processPotentialModeration(json, source) {
const modResult = getModerationResult(json);
let finalJson = json;
if (modResult !== ModerationResult.SAFE) {
if (modResult === ModerationResult.BLOCKED) {
logDebug(`Blocked content detected from ${source}:`, JSON.stringify(json).substring(0, 200) + '...');
addLog(`Blocked content from ${source}.`);
updateStatus(modResult, true);
const recoveredData = await redownloadLatestMessage();
if (recoveredData && recoveredData.content) {
addLog(`Recovery successful (${source}).`);
logDebug(`Recovered content applied (${source})`);
let replaced = false;
const keysToTry = [...CONFIG.messageKeys, 'text', 'message'];
for (const key of keysToTry) {
if (typeof finalJson[key] === 'string') {
finalJson[key] = recoveredData.content;
logDebug(`Injected recovered content into key '${key}'`);
replaced = true;
break;
}
}
if (!replaced) {
logDebug("Could not find standard key to inject recovered content, adding as 'recovered_content'");
finalJson.recovered_content = recoveredData.content;
}
finalJson = clearFlagging(finalJson);
updateStatus(modResult, false);
} else {
addLog(`Recovery failed (${source}). Content may be lost.`);
logDebug(`Recovery failed (${source}), applying standard clearing.`);
finalJson = clearFlagging(json);
updateStatus(modResult, false);
}
} else {
logDebug(`Flagged content detected and cleared from ${source}.`);
addLog(`Flagged content cleared (${source}).`);
finalJson = clearFlagging(json);
updateStatus(modResult);
}
} else {
if (statusEl && !statusEl.textContent.includes('Blocked') && !statusEl.textContent.includes('Flagged') && !statusEl.textContent.includes('Recovering')) {
updateStatus(modResult);
} else if (statusEl && statusEl.textContent.includes('Recovering')) {
logDebug("Recovery attempt finished (next message safe). Resetting status.");
updateStatus(ModerationResult.SAFE);
}
}
return finalJson;
}
async function handleFetchResponse(original_response, url, requestArgs) {
const response = original_response.clone();
if (!response.ok) {
logDebug(`Fetch response not OK (${response.status}) for ${url}, skipping processing.`);
return original_response;
}
const contentType = response.headers.get('Content-Type')?.toLowerCase() || '';
logDebug(`Intercepted fetch response for ${url}, Content-Type: ${contentType}`);
const conversationGetMatch = url.match(/\/rest\/app-chat\/conversation\/([a-f0-9-]+)$/i);
if (conversationGetMatch && requestArgs?.method === 'GET') {
logDebug(`Caching GET request options for conversation ${conversationGetMatch[1]}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
if (!currentConversationId) {
currentConversationId = conversationGetMatch[1];
logDebug(`Conversation ID set from GET URL: ${currentConversationId}`);
}
}
if (!currentConversationId) {
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from other URL: ${currentConversationId}`);
}
}
if (contentType.includes('text/event-stream')) {
logDebug(`Processing SSE stream for ${url}`);
const reader = response.body.getReader();
const stream = new ReadableStream({
async start(controller) {
let buffer = '';
let currentEvent = { data: '', type: 'message', id: null };
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
if (buffer.trim()) {
logDebug("SSE stream ended, processing final buffer:", buffer);
if (buffer.startsWith('{') || buffer.startsWith('[')) {
try {
let json = JSON.parse(buffer);
json = await processPotentialModeration(json, 'SSE-Final');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logDebug("Error parsing final SSE buffer, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else {
controller.enqueue(encoder.encode(`data: ${buffer}\n\n`));
}
} else if (currentEvent.data) {
logDebug("SSE stream ended after data field, processing event:", currentEvent.data.substring(0,100)+"...");
try {
let json = JSON.parse(currentEvent.data);
json = await processPotentialModeration(json, 'SSE-Event');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch (e) {
logDebug("Error parsing trailing SSE data, sending as is:", e);
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
controller.close();
break;
}
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '') {
if (currentEvent.data) {
logDebug("Processing SSE event data:", currentEvent.data.substring(0, 100) + '...');
if (currentEvent.data.startsWith('{') || currentEvent.data.startsWith('[')) {
try {
let json = JSON.parse(currentEvent.data);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from SSE data: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'SSE');
controller.enqueue(encoder.encode(`data: ${JSON.stringify(json)}\n\n`));
} catch(e) {
logError("SSE JSON parse error:", e, "Data:", currentEvent.data.substring(0,200)+"...");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
} else {
logDebug("SSE data is not JSON, forwarding as is.");
controller.enqueue(encoder.encode(`data: ${currentEvent.data}\n\n`));
}
}
currentEvent = { data: '', type: 'message', id: null };
} else if (line.startsWith('data:')) {
currentEvent.data += (currentEvent.data ? '\n' : '') + line.substring(5).trim();
} else if (line.startsWith('event:')) {
currentEvent.type = line.substring(6).trim();
} else if (line.startsWith('id:')) {
currentEvent.id = line.substring(3).trim();
} else if (line.startsWith(':')) {
} else {
logDebug("Unknown SSE line:", line);
}
}
}
} catch (e) {
logError('Error reading/processing SSE stream:', e);
controller.error(e);
} finally {
reader.releaseLock();
}
}
});
const newHeaders = new Headers(response.headers);
return new Response(stream, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
}
if (contentType.includes('application/json')) {
logDebug(`Processing JSON response for ${url}`);
try {
const text = await response.text();
let json = JSON.parse(text);
if (json.conversation_id && !currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from JSON response: ${currentConversationId}`);
}
json = await processPotentialModeration(json, 'Fetch');
const newBody = JSON.stringify(json);
const newHeaders = new Headers(response.headers);
if (newHeaders.has('content-length')) {
newHeaders.set('content-length', encoder.encode(newBody).byteLength.toString());
}
return new Response(newBody, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
});
} catch (e) {
logError('Fetch JSON processing error:', e, 'URL:', url);
return original_response;
}
}
logDebug(`Non-SSE/JSON response for ${url}, skipping processing.`);
return original_response;
}
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function(input, init) {
if (!demodEnabled) {
return originalFetch.apply(this, arguments);
}
let url;
let requestArgs = init || {};
try {
url = (input instanceof Request) ? input.url : String(input);
} catch (e) {
logDebug('Invalid fetch input, passing through:', input, e);
return originalFetch.apply(this, arguments);
}
if (!url.includes('/rest/app-chat/')) {
return originalFetch.apply(this, arguments);
}
if (requestArgs.method === 'POST') {
logDebug(`Observing POST request: ${url}`);
const idFromUrl = extractConversationIdFromUrl(url);
if (idFromUrl) {
if (!currentConversationId) {
currentConversationId = idFromUrl;
logDebug(`Conversation ID set from POST URL: ${currentConversationId}`);
}
if (!initCache && requestArgs.headers) {
logDebug(`Caching headers from POST request to ${idFromUrl}`);
initCache = {
headers: new Headers(requestArgs.headers),
credentials: requestArgs.credentials || 'include'
};
}
}
return originalFetch.apply(this, arguments);
}
logDebug(`Intercepting fetch request: ${requestArgs.method || 'GET'} ${url}`);
try {
const original_response = await originalFetch.apply(this, arguments);
return await handleFetchResponse(original_response, url, requestArgs);
} catch (error) {
logError(`Fetch interception failed for ${url}:`, error);
throw error;
}
};
const OriginalWebSocket = unsafeWindow.WebSocket;
unsafeWindow.WebSocket = new Proxy(OriginalWebSocket, {
construct(target, args) {
const url = args[0];
logDebug('WebSocket connection attempt:', url);
const ws = new target(...args);
let originalOnMessageHandler = null;
Object.defineProperty(ws, 'onmessage', {
configurable: true,
enumerable: true,
get() {
return originalOnMessageHandler;
},
async set(handler) {
logDebug('WebSocket onmessage handler assigned');
originalOnMessageHandler = handler;
ws.onmessageinternal = async function(event) {
if (!demodEnabled || typeof event.data !== 'string' || !event.data.startsWith('{')) {
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (e) {
logError("Error in original WebSocket onmessage handler:", e);
}
}
return;
}
logDebug('Intercepting WebSocket message:', event.data.substring(0, 200) + '...');
try {
let json = JSON.parse(event.data);
if (json.conversation_id && json.conversation_id !== currentConversationId) {
currentConversationId = json.conversation_id;
logDebug(`Conversation ID updated from WebSocket: ${currentConversationId}`);
}
const processedJson = await processPotentialModeration(json, 'WebSocket');
const newEvent = new MessageEvent('message', {
data: JSON.stringify(processedJson),
origin: event.origin,
lastEventId: event.lastEventId,
source: event.source,
ports: event.ports,
});
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, newEvent);
} catch (e) {
logError("Error calling original WebSocket onmessage handler after modification:", e);
}
} else {
logDebug("Original WebSocket onmessage handler not found when message received.");
}
} catch (e) {
logError('WebSocket processing error:', e, 'Data:', event.data.substring(0, 200) + '...');
if (originalOnMessageHandler) {
try {
originalOnMessageHandler.call(ws, event);
} catch (eInner) {
logError("Error in original WebSocket onmessage handler (fallback path):", eInner);
}
}
}
};
ws.addEventListener('message', ws.onmessageinternal);
}
});
const wrapHandler = (eventName) => {
let originalHandler = null;
Object.defineProperty(ws, `on${eventName}`, {
configurable: true,
enumerable: true,
get() { return originalHandler; },
set(handler) {
logDebug(`WebSocket on${eventName} handler assigned`);
originalHandler = handler;
ws.addEventListener(eventName, (event) => {
if (eventName === 'message') return;
logDebug(`WebSocket event: ${eventName}`, event);
if (originalHandler) {
try {
originalHandler.call(ws, event);
} catch (e) {
logError(`Error in original WebSocket on${eventName} handler:`, e);
}
}
});
}
});
};
wrapHandler('close');
wrapHandler('error');
ws.addEventListener('open', () => logDebug('WebSocket opened:', url));
return ws;
}
});
if (window.location.hostname !== 'grok.com') {
console.log('[Grok DeMod] Script inactive: Intended for grok.com only. Current host:', window.location.hostname);
return;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupUI);
} else {
setupUI();
}
console.log('[Grok DeMod] Enhanced Script loaded. Interception is', demodEnabled ? 'ACTIVE' : 'INACTIVE', '. Debug is', debug ? 'ON' : 'OFF');
})();
// ==UserScript==
// @name Grok Rate Limit Display
// @namespace http://tampermonkey.net/
// @version 5.2.27
// @description Displays remaining queries on grok.com
// @author Blankspeaker, Originally ported from CursedAtom's chrome extension
// @match https://grok.com/*
// @icon https://img.icons8.com/color/1200/grok--v2.jpg
// @license MIT
// @downloadURL https://update.greasyfork.org/scripts/533963/Grok Rate Limit Display.user.js
// @updateURL https://update.greasyfork.org/scripts/533963/Grok Rate Limit Display.meta.js
// ==/UserScript==
(function() {
'use strict';
console.log('Grok Rate Limit Script loaded');
let lastHigh = { remaining: null, wait: null };
let lastLow = { remaining: null, wait: null };
let lastBoth = { high: null, low: null, wait: null };
const MODEL_MAP = {
"Grok 4": "grok-4",
"Grok 3": "grok-3",
"Grok 4 Heavy": "grok-4-heavy",
"Grok 4 With Effort Decider": "grok-4-auto",
"Auto": "grok-4-auto",
"Fast": "grok-3",
"Expert": "grok-4",
"Heavy": "grok-4-heavy",
"Grok 4 Fast": "grok-4-mini-thinking-tahoe",
"Grok 4.1": "grok-4-1-non-thinking-w-tool",
"Grok 4.1 Thinking": "grok-4-1-thinking-1129",
};
const DEFAULT_MODEL = "grok-4";
const DEFAULT_KIND = "DEFAULT";
const POLL_INTERVAL_MS = 30000;
const MODEL_SELECTOR = "button[aria-label='Model select']";
const QUERY_BAR_SELECTOR = ".query-bar";
const ELEMENT_WAIT_TIMEOUT_MS = 5000;
const RATE_LIMIT_CONTAINER_ID = "grok-rate-limit";
const cachedRateLimits = {};
let countdownTimer = null;
let isCountingDown = false;
let lastQueryBar = null;
let lastModelObserver = null;
let lastThinkObserver = null;
let lastSearchObserver = null;
let lastInputElement = null;
let lastSubmitButton = null;
let pollInterval = null;
let lastModelName = null;
// State for overlap checking
let overlapCheckInterval = null;
let isHiddenDueToOverlap = false;
const commonFinderConfigs = {
thinkButton: {
selector: "button",
ariaLabel: "Think",
svgPartial
"M19 9C19 12.866",
},
deepSearchButton: {
selector: "button",
ariaLabelRegex: /Deep(er)?Search/i,
},
attachButton: {
selector: "button",
classContains: ["group/attach-button"],
},
submitButton: {
selector: "button",
svgPartial
"M6 11L12 5M12 5L18 11M12 5V19",
}
};
// Function to check if current page is under /imagine
function isImaginePage() {
return window.location.pathname.startsWith('/imagine');
}
// Debounce function
function debounce(func, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), delay);
};
}
// Function to find element based on config (OR logic for conditions)
function findElement(config, root = document) {
const elements = root.querySelectorAll(config.selector);
for (const el of elements) {
let satisfied = 0;
if (config.ariaLabel) {
if (el.getAttribute('aria-label') === config.ariaLabel) satisfied++;
}
if (config.ariaLabelRegex) {
const aria = el.getAttribute('aria-label');
if (aria && config.ariaLabelRegex.test(aria)) satisfied++;
}
if (config.svgPartialD) {
const path = el.querySelector('path');
if (path && path.getAttribute('d')?.includes(config.svgPartialD)) satisfied++;
}
if (config.classContains) {
if (config.classContains.some(cls => el.classList.contains(cls))) satisfied++;
}
if (satisfied > 0) {
return el;
}
}
return null;
}
// Function to format timer for display (H:MM:SS or MM:SS)
function formatTimer(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
}
// Function to check if text content overlaps with rate limit display
function checkTextOverlap(queryBar) {
const rateLimitContainer = document.getElementById(RATE_LIMIT_CONTAINER_ID);
if (!rateLimitContainer) return;
// Look for both mobile textarea and desktop contenteditable
const contentEditable = queryBar.querySelector('div[contenteditable="true"]');
const textArea = queryBar.querySelector('textarea[aria-label*="Ask Grok"]');
const inputElement = contentEditable || textArea;
if (!inputElement) return;
// Get the text content from either element
const textContent = inputElement.value || inputElement.textContent || '';
const textLength = textContent.trim().length;
// Calculate available space more accurately
const queryBarWidth = queryBar.offsetWidth;
const rateLimitWidth = rateLimitContainer.offsetWidth;
const availableSpace = queryBarWidth - rateLimitWidth - 100; // 100px buffer
// More aggressive detection for small screens
const isSmallScreen = window.innerWidth < 900 ||
availableSpace < 200 ||
window.screen?.width < 500 ||
document.documentElement.clientWidth < 500;
// Very conservative approach: on small screens hide immediately when typing
// On larger screens, give more room
const characterLimit = isSmallScreen ? 0 : 28;
const shouldHide = textLength > characterLimit;
if (shouldHide && !isHiddenDueToOverlap) {
// Add slide-right animation to match model picker
rateLimitContainer.style.transition = 'transform 0.2s ease-out, opacity 0.2s ease-out';
rateLimitContainer.style.transform = 'translateX(100%)';
rateLimitContainer.style.opacity = '0';
// After animation, hide completely
setTimeout(() => {
if (isHiddenDueToOverlap) {
rateLimitContainer.style.display = 'none';
}
}, 200);
isHiddenDueToOverlap = true;
} else if (!shouldHide && isHiddenDueToOverlap) {
// Show and slide back in
rateLimitContainer.style.display = '';
rateLimitContainer.style.transition = 'transform 0.2s ease-out, opacity 0.2s ease-out';
// Force a reflow to ensure display change takes effect
rateLimitContainer.offsetHeight;
rateLimitContainer.style.transform = 'translateX(0)';
rateLimitContainer.style.opacity = '0.8';
isHiddenDueToOverlap = false;
}
}
// Function to start overlap checking for a query bar
function startOverlapChecking(queryBar) {
// Clear any existing interval
if (overlapCheckInterval) {
clearInterval(overlapCheckInterval);
}
// Check for overlap less frequently to prevent flashing
overlapCheckInterval = setInterval(() => {
if (document.body.contains(queryBar)) {
checkTextOverlap(queryBar);
} else {
clearInterval(overlapCheckInterval);
overlapCheckInterval = null;
}
}, 500);
}
// Function to stop overlap checking
function stopOverlapChecking() {
if (overlapCheckInterval) {
clearInterval(overlapCheckInterval);
overlapCheckInterval = null;
}
isHiddenDueToOverlap = false;
}
// Function to remove any existing rate limit display
function removeExistingRateLimit() {
const existing = document.getElementById(RATE_LIMIT_CONTAINER_ID);
if (existing) {
existing.remove();
}
}
// Function to determine model key from SVG or text
function getCurrentModelKey(queryBar) {
const modelButton = queryBar.querySelector(MODEL_SELECTOR);
if (!modelButton) return DEFAULT_MODEL;
// Check for text span first (updated selector for new UI)
const textElement = modelButton.querySelector('span.font-semibold');
if (textElement) {
const modelText = textElement.textContent.trim();
return MODEL_MAP[modelText] || DEFAULT_MODEL;
}
// Fallback to old chooser text span
const oldTextElement = modelButton.querySelector('span.inline-block');
if (oldTextElement) {
const modelText = oldTextElement.textContent.trim();
return MODEL_MAP[modelText] || DEFAULT_MODEL;
}
// New chooser: check SVG icon
const svg = modelButton.querySelector('svg');
if (svg) {
const pathsD = Array.from(svg.querySelectorAll('path'))
.map(p => p.getAttribute('d') || '')
.filter(d => d.length > 0)
.join(' ');
const hasBrainFill = svg.querySelector('path[class*="fill-yellow-100"]') !== null;
if (pathsD.includes('M6.5 12.5L11.5 17.5')) {
return 'grok-4-auto'; // Auto
} else if (pathsD.includes('M5 14.25L14 4')) {
return 'grok-3'; // Fast
} else if (hasBrainFill || pathsD.includes('M19 9C19 12.866')) {
return 'grok-4'; // Expert
} else if (pathsD.includes('M12 3a6 6 0 0 0 9 9')) {
return 'grok-4-mini-thinking-tahoe'; // Grok 4 Fast
} else if (pathsD.includes('M11 18H10C7.79086 18 6 16.2091 6 14V13')) {
return 'grok-4-heavy'; // Heavy
}
}
return DEFAULT_MODEL;
}
// Function to determine effort level based on model
function getEffortLevel(modelName) {
if (modelName === 'grok-4-auto') {
return 'both';
} else if (modelName === 'grok-3') {
return 'low';
} else if (modelName === 'grok-4-1-non-thinking-w-tool') {
return 'low';
} else if (modelName === 'grok-4-1-thinking-1129') {
return 'high';
} else {
// Grok 4, Heavy, and Grok 4.1 Thinking fall here
return 'high';
}
}
// Function to update or inject the rate limit display
function updateRateLimitDisplay(queryBar, response, effort) {
if (isImaginePage()) {
removeExistingRateLimit();
return;
}
let rateLimitContainer = document.getElementById(RATE_LIMIT_CONTAINER_ID);
if (!rateLimitContainer) {
rateLimitContainer = document.createElement('div');
rateLimitContainer.id = RATE_LIMIT_CONTAINER_ID;
rateLimitContainer.className = 'inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium focus-visible
utline-none focus-visible:ring-1 focus-visible:ring-ring disabled
pacity-60 disabled:cursor-not-allowed [&_svg]:duration-100 [&_svg]
ointer-events-none [&_svg]:shrink-0 [&_svg]:-mx-0.5 select-none text-fg-primary hover:bg-button-ghost-hover hover:border-border-l2 disabled:hover:bg-transparent h-10 px-3.5 py-2 text-sm rounded-full group/rate-limit transition-colors duration-100 relative overflow-hidden border border-transparent cursor-pointer';
rateLimitContainer.style.opacity = '0.8';
rateLimitContainer.style.transition = 'opacity 0.1s ease-in-out';
rateLimitContainer.style.zIndex = '20';
rateLimitContainer.addEventListener('click', () => {
fetchAndUpdateRateLimit(queryBar, true);
});
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '18');
svg.setAttribute('height', '18');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
svg.setAttribute('class', 'lucide lucide-gauge stroke-[2] text-fg-secondary transition-colors duration-100');
svg.setAttribute('aria-hidden', 'true');
const contentDiv = document.createElement('div');
contentDiv.className = 'flex items-center';
rateLimitContainer.appendChild(svg);
rateLimitContainer.appendChild(contentDiv);
// New Insertion Logic: Place it in the tools container (right side)
const toolsContainer = queryBar.querySelector('div.ms-auto.flex.flex-row.items-end.gap-1');
if (toolsContainer) {
// Prepend to put it to the left of the model selector/other buttons
toolsContainer.prepend(rateLimitContainer);
} else {
// Fallback: bottom bar
const bottomBar = queryBar.querySelector('div.absolute.inset-x-0.bottom-0');
if (bottomBar) {
bottomBar.appendChild(rateLimitContainer);
} else {
rateLimitContainer.remove();
rateLimitContainer = null;
return;
}
}
}
const contentDiv = rateLimitContainer.lastChild;
const svg = rateLimitContainer.querySelector('svg');
contentDiv.innerHTML = '';
const isBoth = effort === 'both';
if (response.error) {
if (isBoth) {
if (lastBoth.high !== null && lastBoth.low !== null) {
appendNumberSpan(contentDiv, lastBoth.high, '');
appendDivider(contentDiv);
appendNumberSpan(contentDiv, lastBoth.low, '');
rateLimitContainer.title = `High: ${lastBoth.high} | Low: ${lastBoth.low} queries remaining`;
setGaugeSVG(svg);
} else {
appendNumberSpan(contentDiv, 'Unavailable', '');
rateLimitContainer.title = 'Unavailable';
setGaugeSVG(svg);
}
} else {
const lastForEffort = (effort === 'high') ? lastHigh : lastLow;
if (lastForEffort.remaining !== null) {
appendNumberSpan(contentDiv, lastForEffort.remaining, '');
rateLimitContainer.title = `${lastForEffort.remaining} queries remaining`;
setGaugeSVG(svg);
} else {
appendNumberSpan(contentDiv, 'Unavailable', '');
rateLimitContainer.title = 'Unavailable';
setGaugeSVG(svg);
}
}
} else {
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
if (isBoth) {
lastBoth.high = response.highRemaining;
lastBoth.low = response.lowRemaining;
lastBoth.wait = response.waitTimeSeconds;
const high = lastBoth.high;
const low = lastBoth.low;
const waitTimeSeconds = lastBoth.wait;
let currentCountdown = waitTimeSeconds;
if (high > 0) {
appendNumberSpan(contentDiv, high, '');
appendDivider(contentDiv);
appendNumberSpan(contentDiv, low, '');
rateLimitContainer.title = `High: ${high} | Low: ${low} queries remaining`;
setGaugeSVG(svg);
} else if (waitTimeSeconds > 0) {
const timerSpan = appendNumberSpan(contentDiv, formatTimer(currentCountdown), '#ff6347');
appendDivider(contentDiv);
appendNumberSpan(contentDiv, low, '');
rateLimitContainer.title = `High: Time until reset | Low: ${low} queries remaining`;
setClockSVG(svg);
isCountingDown = true;
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
countdownTimer = setInterval(() => {
currentCountdown--;
if (currentCountdown <= 0) {
clearInterval(countdownTimer);
countdownTimer = null;
fetchAndUpdateRateLimit(queryBar, true);
isCountingDown = false;
if (document.visibilityState === 'visible' && lastQueryBar) {
pollInterval = setInterval(() => fetchAndUpdateRateLimit(lastQueryBar, true), POLL_INTERVAL_MS);
}
} else {
timerSpan.textContent = formatTimer(currentCountdown);
}
}, 1000);
} else {
appendNumberSpan(contentDiv, '0', '#ff6347');
appendDivider(contentDiv);
appendNumberSpan(contentDiv, low, '');
rateLimitContainer.title = `High: Limit reached | Low: ${low} queries remaining`;
setGaugeSVG(svg);
}
} else {
const lastForEffort = (effort === 'high') ? lastHigh : lastLow;
lastForEffort.remaining = response.remainingQueries;
lastForEffort.wait = response.waitTimeSeconds;
const remaining = lastForEffort.remaining;
const waitTimeSeconds = lastForEffort.wait;
let currentCountdown = waitTimeSeconds;
if (remaining > 0) {
appendNumberSpan(contentDiv, remaining, '');
rateLimitContainer.title = `${remaining} queries remaining`;
setGaugeSVG(svg);
} else if (waitTimeSeconds > 0) {
const timerSpan = appendNumberSpan(contentDiv, formatTimer(currentCountdown), '#ff6347');
rateLimitContainer.title = `Time until reset`;
setClockSVG(svg);
isCountingDown = true;
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
countdownTimer = setInterval(() => {
currentCountdown--;
if (currentCountdown <= 0) {
clearInterval(countdownTimer);
countdownTimer = null;
fetchAndUpdateRateLimit(queryBar, true);
isCountingDown = false;
if (document.visibilityState === 'visible' && lastQueryBar) {
pollInterval = setInterval(() => fetchAndUpdateRateLimit(lastQueryBar, true), POLL_INTERVAL_MS);
}
} else {
timerSpan.textContent = formatTimer(currentCountdown);
}
}, 1000);
} else {
appendNumberSpan(contentDiv, '0', ' #ff6347');
rateLimitContainer.title = 'Limit reached. Awaiting reset.';
setGaugeSVG(svg);
}
}
}
}
function appendNumberSpan(parent, text, color) {
const span = document.createElement('span');
span.textContent = text;
if (color) span.style.color = color;
parent.appendChild(span);
return span;
}
function appendDivider(parent) {
const divider = document.createElement('div');
divider.className = 'h-6 w-[2px] bg-border-l2 mx-1';
parent.appendChild(divider);
}
function setGaugeSVG(svg) {
if (svg) {
while (svg.firstChild) {
svg.removeChild(svg.firstChild);
}
const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path1.setAttribute('d', 'm12 14 4-4');
const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path2.setAttribute('d', 'M3.34 19a10 10 0 1 1 17.32 0');
svg.appendChild(path1);
svg.appendChild(path2);
svg.setAttribute('class', 'lucide lucide-gauge stroke-[2] text-fg-secondary transition-colors duration-100');
}
}
function setClockSVG(svg) {
if (svg) {
while (svg.firstChild) {
svg.removeChild(svg.firstChild);
}
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', '12');
circle.setAttribute('cy', '12');
circle.setAttribute('r', '8');
circle.setAttribute('stroke', 'currentColor');
circle.setAttribute('stroke-width', '2');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M12 12L12 6');
path.setAttribute('stroke', 'currentColor');
path.setAttribute('stroke-width', '2');
path.setAttribute('stroke-linecap', 'round');
svg.appendChild(circle);
svg.appendChild(path);
svg.setAttribute('class', 'stroke-[2] text-fg-secondary group-hover/rate-limit:text-fg-primary transition-colors duration-100');
}
}
// Function to fetch rate limit
async function fetchRateLimit(modelName, requestKind, force = false) {
if (!force) {
const cached = cachedRateLimits[modelName]?.[requestKind];
if (cached !== undefined) {
return cached;
}
}
try {
const response = await fetch(window.location.origin + '/rest/rate-limits', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
requestKind,
modelName,
}),
credentials: 'include',
});
if (!response.ok) {
throw new Error(`HTTP error: Status ${response.status}`);
}
const data = await response.json();
if (!cachedRateLimits[modelName]) {
cachedRateLimits[modelName] = {};
}
cachedRateLimits[modelName][requestKind] = data;
return data;
} catch (error) {
console.error(`Failed to fetch rate limit:`, error);
if (!cachedRateLimits[modelName]) {
cachedRateLimits[modelName] = {};
}
cachedRateLimits[modelName][requestKind] = undefined;
return { error: true };
}
}
// Function to process the rate limit data based on effort level
function processRateLimitData(data, effortLevel) {
if (data.error) {
return data;
}
if (effortLevel === 'both') {
const high = data.highEffortRateLimits?.remainingQueries;
const low = data.lowEffortRateLimits?.remainingQueries;
const waitTimeSeconds = Math.max(
data.highEffortRateLimits?.waitTimeSeconds || 0,
data.lowEffortRateLimits?.waitTimeSeconds || 0,
data.waitTimeSeconds || 0
);
if (high !== undefined && low !== undefined) {
return {
highRemaining: high,
lowRemaining: low,
waitTimeSeconds: waitTimeSeconds
};
} else {
return { error: true };
}
} else {
let rateLimitsKey = effortLevel === 'high' ? 'highEffortRateLimits' : 'lowEffortRateLimits';
let remaining = data[rateLimitsKey]?.remainingQueries;
if (remaining === undefined) {
remaining = data.remainingQueries;
}
if (remaining !== undefined) {
return {
remainingQueries: remaining,
waitTimeSeconds: data[rateLimitsKey]?.waitTimeSeconds || data.waitTimeSeconds || 0
};
} else {
return { error: true };
}
}
}
// Function to fetch and update rate limit
async function fetchAndUpdateRateLimit(queryBar, force = false) {
if (isImaginePage() || !queryBar || !document.body.contains(queryBar)) {
return;
}
const modelName = getCurrentModelKey(queryBar);
if (modelName !== lastModelName) {
force = true;
}
if (isCountingDown && !force) {
return;
}
const effortLevel = getEffortLevel(modelName);
let requestKind = DEFAULT_KIND;
if (modelName === 'grok-3') {
const thinkButton = findElement(commonFinderConfigs.thinkButton, queryBar);
const searchButton = findElement(commonFinderConfigs.deepSearchButton, queryBar);
if (thinkButton && thinkButton.getAttribute('aria-pressed') === 'true') {
requestKind = 'REASONING';
} else if (searchButton && searchButton.getAttribute('aria-pressed') === 'true') {
const searchAria = searchButton.getAttribute('aria-label') || '';
if (/deeper/i.test(searchAria)) {
requestKind = 'DEEPERSEARCH';
} else if (/deep/i.test(searchAria)) {
requestKind = 'DEEPSEARCH';
}
}
}
let data = await fetchRateLimit(modelName, requestKind, force);
const processedData = processRateLimitData(data, effortLevel);
updateRateLimitDisplay(queryBar, processedData, effortLevel);
lastModelName = modelName;
}
// Function to observe the DOM for the query bar
function observeDOM() {
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible' && lastQueryBar && !isImaginePage()) {
fetchAndUpdateRateLimit(lastQueryBar, true);
if (!isCountingDown) {
if (pollInterval) {
clearInterval(pollInterval);
}
pollInterval = setInterval(() => fetchAndUpdateRateLimit(lastQueryBar, true), POLL_INTERVAL_MS);
}
} else {
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
}
};
// Add resize listener to handle mobile/desktop mode switches
const handleResize = debounce(() => {
if (lastQueryBar) {
checkTextOverlap(lastQueryBar);
}
}, 300);
document.addEventListener('visibilitychange', handleVisibilityChange);
window.addEventListener('resize', handleResize);
if (!isImaginePage()) {
const initialQueryBar = document.querySelector(QUERY_BAR_SELECTOR);
if (initialQueryBar) {
removeExistingRateLimit();
fetchAndUpdateRateLimit(initialQueryBar);
lastQueryBar = initialQueryBar;
setupQueryBarObserver(initialQueryBar);
setupGrok3Observers(initialQueryBar);
setupSubmissionListeners(initialQueryBar);
// Start overlap checking
startOverlapChecking(initialQueryBar);
setTimeout(() => checkTextOverlap(initialQueryBar), 100);
if (document.visibilityState === 'visible' && !isCountingDown) {
pollInterval = setInterval(() => fetchAndUpdateRateLimit(lastQueryBar, true), POLL_INTERVAL_MS);
}
}
}
const observer = new MutationObserver(() => {
if (isImaginePage()) {
removeExistingRateLimit();
stopOverlapChecking();
if (lastModelObserver) {
lastModelObserver.disconnect();
lastModelObserver = null;
}
if (lastThinkObserver) {
lastThinkObserver.disconnect();
lastThinkObserver = null;
}
if (lastSearchObserver) {
lastSearchObserver.disconnect();
lastSearchObserver = null;
}
lastInputElement = null;
lastSubmitButton = null;
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
lastQueryBar = null;
return;
}
const queryBar = document.querySelector(QUERY_BAR_SELECTOR);
if (queryBar && queryBar !== lastQueryBar) {
removeExistingRateLimit();
fetchAndUpdateRateLimit(queryBar);
if (lastModelObserver) {
lastModelObserver.disconnect();
}
if (lastThinkObserver) {
lastThinkObserver.disconnect();
}
if (lastSearchObserver) {
lastSearchObserver.disconnect();
}
setupQueryBarObserver(queryBar);
setupGrok3Observers(queryBar);
setupSubmissionListeners(queryBar);
// Start overlap checking
startOverlapChecking(queryBar);
setTimeout(() => checkTextOverlap(queryBar), 100);
if (document.visibilityState === 'visible' && !isCountingDown) {
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(() => fetchAndUpdateRateLimit(lastQueryBar, true), POLL_INTERVAL_MS);
}
lastQueryBar = queryBar;
} else if (!queryBar && lastQueryBar) {
removeExistingRateLimit();
stopOverlapChecking();
if (lastModelObserver) {
lastModelObserver.disconnect();
}
if (lastThinkObserver) {
lastThinkObserver.disconnect();
}
if (lastSearchObserver) {
lastSearchObserver.disconnect();
}
lastQueryBar = null;
lastModelObserver = null;
lastThinkObserver = null;
lastSearchObserver = null;
lastInputElement = null;
lastSubmitButton = null;
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
function setupQueryBarObserver(queryBar) {
const debouncedUpdate = debounce(() => {
fetchAndUpdateRateLimit(queryBar);
setupGrok3Observers(queryBar);
}, 300);
lastModelObserver = new MutationObserver(debouncedUpdate);
lastModelObserver.observe(queryBar, { childList: true, subtree: true, attributes: true, characterData: true });
}
function setupGrok3Observers(queryBar) {
const currentModel = getCurrentModelKey(queryBar);
if (currentModel === 'grok-3') {
const thinkButton = findElement(commonFinderConfigs.thinkButton, queryBar);
if (thinkButton) {
if (lastThinkObserver) lastThinkObserver.disconnect();
lastThinkObserver = new MutationObserver(() => {
fetchAndUpdateRateLimit(queryBar);
});
lastThinkObserver.observe(thinkButton, { attributes: true, attributeFilter: ['aria-pressed', 'class'] });
}
const searchButton = findElement(commonFinderConfigs.deepSearchButton, queryBar);
if (searchButton) {
if (lastSearchObserver) lastSearchObserver.disconnect();
lastSearchObserver = new MutationObserver(() => {
fetchAndUpdateRateLimit(queryBar);
});
lastSearchObserver.observe(searchButton, { attributes: true, attributeFilter: ['aria-pressed', 'class'], childList: true, subtree: true, characterData: true });
}
} else {
if (lastThinkObserver) {
lastThinkObserver.disconnect();
lastThinkObserver = null;
}
if (lastSearchObserver) {
lastSearchObserver.disconnect();
lastSearchObserver = null;
}
}
}
function setupSubmissionListeners(queryBar) {
const inputElement = queryBar.querySelector('div[contenteditable="true"]');
if (inputElement && inputElement !== lastInputElement) {
lastInputElement = inputElement;
// Create a debounced version of overlap checking
const debouncedOverlapCheck = debounce(() => {
checkTextOverlap(queryBar);
}, 300);
inputElement.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
setTimeout(() => fetchAndUpdateRateLimit(queryBar, true), 3000);
}
});
// Add listeners for overlap checking
inputElement.addEventListener('input', debouncedOverlapCheck);
inputElement.addEventListener('focus', debouncedOverlapCheck);
inputElement.addEventListener('blur', () => {
setTimeout(() => {
checkTextOverlap(queryBar);
}, 200);
});
}
const bottomBar = queryBar.querySelector('div.absolute.inset-x-0.bottom-0');
const submitButton = bottomBar ? findElement(commonFinderConfigs.submitButton, bottomBar) : findElement(commonFinderConfigs.submitButton, queryBar);
if (submitButton && submitButton !== lastSubmitButton) {
lastSubmitButton = submitButton;
submitButton.addEventListener('click', () => {
setTimeout(() => fetchAndUpdateRateLimit(queryBar, true), 3000);
});
}
}
}
// Start observing the DOM for changes
observeDOM();
})();
// ==UserScript==
// @name Grok Auto-Retry + Prompt Snippets + History + Favorites (v41 - Edited Image History )
// @namespace http://tampermonkey.net/
// @version 41
// @description Navigation arrows cycle through permanent prompt history. Adds Edited Image history + favorites. Fixes wrong tiny up-arrow "Make video" button click in Image Extend/Edit mode.
// @author You
// @license MIT
// @match https://grok.com/*
// @match https://*.grok.com/*
// @match https://grok.x.ai/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_setClipboard
// @downloadURL https://update.greasyfork.org/scrip...vorites (v41 - Edited Image History ).user.js
// @updateURL https://update.greasyfork.org/scrip...vorites (v41 - Edited Image History ).meta.js
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION ---
const TARGET_TEXTAREA_SELECTOR = 'textarea[aria-label="Make a video"]';
// Image selectors
const IMAGE_EDITOR_SELECTOR = 'textarea[aria-label="Type to edit image..."]';
const IMAGE_PROMPT_SELECTOR = 'textarea[aria-label="Image prompt"]';
const IMAGE_IMAGINE_SELECTOR = 'p[data-placeholder="Type to imagine"]';
// Image Extend/Edit Mode Indicator
const IMAGE_EDIT_EXIT_SELECTOR = 'button[aria-label="Exit extend mode"]';
// Buttons
const RETRY_BUTTON_SELECTOR = 'button[aria-label="Make video"]';
const IMAGE_EDITOR_BUTTON_SELECTOR = 'button[aria-label="Generate"]';
const IMAGE_SUBMIT_BUTTON_SELECTOR = 'button[aria-label="Submit"]';
const MODERATION_PATTERNS = [
"content moderated",
"try a different idea",
"moderated",
"content policy",
"cannot generate",
"unable to generate"
];
const RETRY_DELAY_MS = 1500;
const OBSERVER_THROTTLE_MS = 300;
const MAX_HISTORY_ITEMS = 500;
const DEBUG_MODE = true;
// --- DEFAULT SNIPPETS ---
const DEFAULT_SNIPPETS = [
{
id: 'b1',
label: 'Anime Stickers (Provocative - Adults)',
text: 'Surrounding the central image: thick decorative border made of overlapping colorful anime-style stickers featuring adult anime women with exaggerated proportions in various provocative poses. Each sticker has a white outline and slight drop shadow. The stickers completely frame all four edges of the image with some overlap into the main content.'
},
{
id: 'b2',
label: 'Anime Stickers (SFW)',
text: 'Surrounding the central image: thick decorative border made of overlapping colorful anime-style stickers featuring anime women in various poses. Each sticker has a white outline and slight drop shadow. The stickers completely frame all four edges of the image with some overlap into the main content.'
},
{ id: '1', label: 'Motion: Slow Mo', text: 'slow motion, high frame rate, smooth movement' },
{ id: '2', label: 'Style: Photorealistic', text: 'photorealistic, 8k resolution, highly detailed, unreal engine 5 render' },
{ id: '3', label: 'Lighting: Golden Hour', text: 'golden hour lighting, warm sun rays, lens flare, soft shadows' },
];
// --- LOAD SAVED SETTINGS ---
let maxRetries = GM_getValue('maxRetries', 5);
let uiToggleKey = GM_getValue('uiToggleKey', 'h');
let autoClickEnabled = GM_getValue('autoClickEnabled', true);
let isUiVisible = GM_getValue('isUiVisible', true);
let savedSnippets = GM_getValue('savedSnippets', DEFAULT_SNIPPETS);
let videoPromptHistory = GM_getValue('videoPromptHistory', []);
let imagePromptHistory = GM_getValue('imagePromptHistory', []);
let editedPromptHistory = GM_getValue('editedPromptHistory', []); // NEW: Edited Image prompts
let videoFavorites = GM_getValue('videoFavorites', []);
let imageFavorites = GM_getValue('imageFavorites', []);
let editedFavorites = GM_getValue('editedFavorites', []); // NEW: Edited Image favorites
let panelSize = GM_getValue('panelSize', { width: '300px', height: '460px' });
// --- LOAD SAVED POSITIONS ---
let mainPos = GM_getValue('pos_main', { top: 'auto', left: 'auto', bottom: '20px', right: '20px' });
let libPos = GM_getValue('pos_lib', { top: '100px', left: '100px' });
let favPos = GM_getValue('pos_fav', { top: '120px', left: '120px' });
let histPos = GM_getValue('pos_hist', { top: '140px', left: '140px' });
let isRetryEnabled = true;
let limitReached = false;
let currentRetryCount = 0;
let lastTypedPrompt = "";
let lastGenerationTimestamp = 0;
const GENERATION_COOLDOWN_MS = 3000;
let observerThrottle = false;
let moderationDetected = false;
let processingModeration = false;
let currentHistoryTab = 'video'; // 'video' | 'image' | 'edited'
let currentFavoritesTab = 'video'; // 'video' | 'image' | 'edited'
let currentEditingFavId = null;
let lastModerationCheck = 0;
let errorWaitInterval = null;
// --- HISTORY NAVIGATION VARIABLES ---
let historyNavIndex = -1;
// --- DEBUG LOGGER ---
function debugLog(...args) {
if (DEBUG_MODE) console.log('[Grok Tools]', ...args);
}
// --- STYLES ---
GM_addStyle(`
#grok-control-panel {
position: fixed;
width: ${panelSize.width}; height: ${panelSize.height};
min-width: 280px; min-height: 250px; max-width: 90vw; max-height: 90vh;
background: linear-gradient(145deg, #0a0a0a 0%, #1a1a1a 100%);
border: 1px solid #2a2a2a;
border-radius: 16px;
padding: 15px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #e0e0e0;
z-index: 99990;
box-shadow: 0 8px 32px rgba(0,0,0,0.9), 0 0 0 1px rgba(255,255,255,0.05);
display: flex;
flex-direction: column;
gap: 10px;
}
#grok-control-panel.hidden { display: none !important; }
.grok-header, .gl-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
margin-left: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #2a2a2a;
cursor: move;
user-select: none;
}
.gl-header {
padding: 12px 15px;
margin-left: 0;
background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%);
border-radius: 16px 16px 0 0;
font-weight: bold; font-size: 13px; color: #f0f0f0;
}
#grok-resize-handle {
position: absolute; top: 0; left: 0; width: 15px; height: 15px;
cursor: nwse-resize; z-index: 99999;
}
#grok-resize-handle::after {
content: ''; position: absolute; top: 2px; left: 2px;
border-top: 6px solid #3b82f6; border-right: 6px solid transparent;
width: 0; height: 0; opacity: 0.7;
}
#grok-resize-handle:hover::after { opacity: 1; border-top-color: #60a5fa; }
.grok-title {
font-weight: bold; font-size: 14px; color: #f0f0f0;
text-shadow: 0 2px 4px rgba(0,0,0,0.5); pointer-events: none;
}
.grok-toggle-btn {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: none; color: white; padding: 6px 14px; border-radius: 20px;
font-size: 11px; font-weight: bold; cursor: pointer;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
transition: all 0.2s ease;
}
.grok-toggle-btn.off {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
}
.grok-controls {
display: flex; align-items: center; justify-content: space-between;
font-size: 12px; color: #9ca3af; flex-shrink: 0; padding: 8px 0;
}
.grok-checkbox { display: flex; align-items: center; cursor: pointer; color: #d1d5db; }
.grok-checkbox input { margin-right: 6px; cursor: pointer; accent-color: #3b82f6; }
.grok-num-input {
width: 40px; background: #1f1f1f; border: 1px solid #2a2a2a;
color: #e0e0e0; border-radius: 6px; padding: 4px 6px; text-align: center;
}
/* NAV BUTTON STYLES */
.grok-prompt-header-row {
display: flex; justify-content: space-between; align-items: center; margin-bottom: -5px; flex-shrink: 0;
}
.grok-prompt-label {
font-size: 11px; font-weight: bold; color: #9ca3af;
}
.grok-nav-container { display: flex; gap: 4px; align-items: center; }
.grok-nav-btn {
background: #1f1f1f; border: 1px solid #333; color: #888;
cursor: pointer; padding: 2px 8px; border-radius: 4px;
font-size: 10px; transition: all 0.2s; min-width: 25px;
}
.grok-nav-btn:hover:not
disabled) { background: #333; color: #fff; border-color: #555; }
.grok-nav-btn:disabled { opacity: 0.3; cursor: default; }
.grok-nav-counter {
font-size: 9px; color: #666; min-width: 50px; text-align: center;
}
#grok-panel-prompt {
width: 100%; flex-grow: 1; background: #0f0f0f; border: 1px solid #2a2a2a;
border-radius: 8px; color: #e0e0e0; padding: 10px; font-size: 12px;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; resize: none;
box-sizing: border-box; transition: all 0.2s ease; margin-top: 5px;
}
#grok-panel-prompt:focus {
border-color: #3b82f6; outline: none; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.grok-btn-row { display: flex; gap: 8px; flex-shrink: 0; }
.grok-action-btn {
flex: 1; padding: 10px; border-radius: 8px; border: none; cursor: pointer;
font-weight: 600; font-size: 12px; transition: all 0.2s ease;
position: relative; overflow: hidden;
}
.grok-action-btn:hover { transform: translateY(-1px); }
#btn-open-library { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); color: white; }
#btn-open-favorites { background: linear-gradient(135deg, #ec4899 0%, #db2777 100%); color: white; }
#btn-open-history { background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); color: white; }
#btn-generate { background: linear-gradient(135deg, #1f1f1f 0%, #2a2a2a 100%); color: #e0e0e0; border: 1px solid #3a3a3a; }
#grok-status {
text-align: center; font-size: 11px; color: #10b981; padding-top: 8px;
border-top: 1px solid #2a2a2a; flex-shrink: 0; font-weight: 500;
}
.status-error { color: #ef4444 !important; }
.status-warning { color: #f59e0b !important; }
/* Import/Export Panel */
#grok-io-container.io-hidden { display: none; }
#grok-io-container {
display: flex; gap: 8px; margin-top: 5px; padding-top: 5px; border-top: 1px solid #2a2a2a;
}
.io-btn {
flex: 1; background: #1f1f1f; color: #9ca3af; border: 1px solid #2a2a2a;
border-radius: 6px; padding: 5px; font-size: 10px; cursor: pointer;
}
.io-btn:hover { background: #333; color: #fff; }
/* Modal Styles */
.grok-modal {
position: fixed;
width: 350px; height: 400px;
background: linear-gradient(145deg, #0a0a0a 0%, #1a1a1a 100%);
border: 1px solid #2a2a2a;
border-radius: 16px;
display: none; flex-direction: column;
z-index: 99995;
box-shadow: 0 8px 32px rgba(0,0,0,0.9), 0 0 0 1px rgba(255,255,255,0.05);
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.grok-modal.active { display: flex; }
.gl-close {
cursor: pointer; font-size: 20px; line-height: 1; color: #6b7280;
width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;
}
.gl-close:hover { color: #f0f0f0; }
/* History/Favorites Tab Styles */
.history-tabs { display: flex; background: #0f0f0f; border-bottom: 1px solid #2a2a2a; }
.history-tab {
flex: 1; padding: 10px; text-align: center; cursor: pointer;
font-size: 11px; font-weight: 600; color: #6b7280; border-bottom: 2px solid transparent;
}
.history-tab:hover { color: #9ca3af; background: #1a1a1a; }
.history-tab.active { color: #8b5cf6; border-bottom-color: #8b5cf6; background: #1a1a1a; }
.gl-view-list { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
.gl-list-content { overflow-y: auto; padding: 12px; flex: 1; display: flex; flex-direction: column; gap: 8px; }
.gl-list-content::-webkit-scrollbar { width: 8px; }
.gl-list-content::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 4px; }
.gl-item {
background: linear-gradient(135deg, #1a1a1a 0%, #151515 100%);
border: 1px solid #2a2a2a; padding: 10px; border-radius: 8px;
display: flex; justify-content: space-between; align-items: center;
font-size: 12px; color: #e0e0e0; transition: all 0.2s ease;
}
.gl-item:hover { border-color: #3b82f6; background: linear-gradient(135deg, #1f1f1f 0%, #1a1a1a 100%); }
.gl-item-text { cursor: pointer; flex: 1; margin-right: 10px; }
.gl-item-text b { display: block; margin-bottom: 4px; color: #f0f0f0; }
.gl-item-text span {
color: #9ca3af; font-size: 10px; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.gl-item-actions { display: flex; gap: 6px; }
.gl-icon-btn {
background: #1f1f1f; border: 1px solid #2a2a2a; cursor: pointer;
font-size: 14px; color: #9ca3af; padding: 6px; border-radius: 6px;
width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
}
.gl-icon-btn:hover { color: #f0f0f0; background: #2a2a2a; transform: scale(1.1); }
.gl-icon-btn.favorite { color: #ec4899; }
.gl-create-btn, .history-clear-btn {
margin: 12px; padding: 10px; background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white; text-align: center; border-radius: 8px; cursor: pointer;
font-weight: 600; font-size: 12px;
}
.history-clear-btn { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); }
.gl-view-editor { display: none; flex-direction: column; padding: 15px; height: 100%; gap: 10px; }
.gl-view-editor.active { display: flex; }
.history-viewer { display: none; }
.history-viewer.active { display: flex; }
.gl-input, .gl-textarea {
background: #0f0f0f; border: 1px solid #2a2a2a; color: #e0e0e0;
padding: 10px; border-radius: 8px; font-size: 12px; width: 100%; box-sizing: border-box;
}
.gl-input:focus, .gl-textarea:focus { border-color: #3b82f6; outline: none; }
.gl-textarea { flex-grow: 1; resize: none; font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; }
.gl-editor-buttons { display: flex; gap: 10px; margin-top: auto; }
.gl-btn { flex: 1; padding: 10px; border-radius: 8px; border: none; cursor: pointer; font-weight: 600; color: white; font-size: 12px; }
.gl-btn-save { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); }
.gl-btn-cancel { background: linear-gradient(135deg, #374151 0%, #1f2937 100%); }
.history-item-time { font-size: 9px; color: #6b7280; margin-top: 3px; }
`);
// --- DOM CREATION (MAIN PANEL) ---
const panel = document.createElement('div');
panel.id = 'grok-control-panel';
if (mainPos.top !== 'auto') {
panel.style.top = mainPos.top;
panel.style.left = mainPos.left;
panel.style.bottom = 'auto';
panel.style.right = 'auto';
} else {
panel.style.bottom = mainPos.bottom;
panel.style.right = mainPos.right;
}
if (!isUiVisible) panel.classList.add('hidden');
panel.innerHTML = `
<div id="grok-resize-handle" title="Drag to Resize"></div>
<div class="grok-header" id="grok-main-header">
<span class="grok-title">Grok Tools v41</span>
<button id="grok-toggle-btn" class="grok-toggle-btn">ON</button>
</div>
<div class="grok-controls">
<label class="grok-checkbox">
<input type="checkbox" id="grok-autoclick-cb" ${autoClickEnabled ? 'checked' : ''}> Auto-Retry
</label>
<div>
Max: <input type="number" id="grok-retry-limit" value="${maxRetries}" class="grok-num-input" min="1">
</div>
</div>
<div class="grok-prompt-header-row">
<div class="grok-prompt-label">Prompt Editor</div>
<div class="grok-nav-container">
<button id="btn-hist-prev" class="grok-nav-btn" title="Previous in History (Alt+Left)">◀</button>
<span id="hist-nav-counter" class="grok-nav-counter">-</span>
<button id="btn-hist-next" class="grok-nav-btn" title="Next in History (Alt+Right)">▶</button>
</div>
</div>
<textarea id="grok-panel-prompt" placeholder="Type or paste prompt here..."></textarea>
<div class="grok-btn-row">
<button id="btn-open-library" class="grok-action-btn">Snippets</button>
<button id="btn-open-favorites" class="grok-action-btn">
</button>
<button id="btn-open-history" class="grok-action-btn">History</button>
<button id="btn-generate" class="grok-action-btn">Generate</button>
</div>
<div id="grok-status">Ready</div>
<div id="grok-io-container" class="io-hidden">
<button id="btn-export-all" class="io-btn">Export Data
</button>
<button id="btn-import-all" class="io-btn">Import Data
</button>
<input type="file" id="grok-import-file" style="display:none" accept=".json">
</div>
<div style="font-size:9px; color:#555; text-align:center;">UI: Alt+${uiToggleKey.toUpperCase()} | I/O: Alt+I</div>
`;
document.body.appendChild(panel);
// --- LIBRARY MODAL ---
const modal = document.createElement('div');
modal.id = 'grok-library-modal';
modal.className = 'grok-modal';
modal.style.top = libPos.top;
modal.style.left = libPos.left;
modal.innerHTML = `
<div class="gl-header" id="lib-header"><span>Snippets Library</span><span class="gl-close">×</span></div>
<div class="gl-view-list" id="gl-view-list">
<div class="gl-list-content" id="gl-list-container"></div>
<div class="gl-create-btn" id="btn-create-snippet">Create New Snippet</div>
</div>
<div class="gl-view-editor" id="gl-view-editor">
<label style="font-size:11px; color:#8b98a5;">Label</label>
<input type="text" class="gl-input" id="gl-edit-label" placeholder="e.g. Cinematic Lighting">
<label style="font-size:11px; color:#8b98a5;">Prompt Text</label>
<textarea class="gl-textarea" id="gl-edit-text" placeholder="Content to append..."></textarea>
<div class="gl-editor-buttons">
<button class="gl-btn gl-btn-cancel" id="btn-edit-cancel">Cancel</button>
<button class="gl-btn gl-btn-save" id="btn-edit-save">Save Snippet</button>
</div>
</div>
`;
document.body.appendChild(modal);
// --- FAVORITES MODAL ---
const favoritesModal = document.createElement('div');
favoritesModal.id = 'grok-favorites-modal';
favoritesModal.className = 'grok-modal';
favoritesModal.style.top = favPos.top;
favoritesModal.style.left = favPos.left;
favoritesModal.innerHTML = `
<div class="gl-header" id="fav-header"><span>Favorites
</span><span class="gl-close favorites-close">×</span></div>
<div class="history-tabs">
<div class="history-tab active" data-tab="video"> Video</div>
<div class="history-tab" data-tab="image">️ Image</div>
<div class="history-tab" data-tab="edited">️ Edited</div>
</div>
<div class="gl-view-list" id="favorites-view-list">
<div class="gl-list-content" id="favorites-list-container"></div>
</div>
<div class="gl-view-editor" id="favorites-view-viewer">
<label style="font-size:11px; color:#8b98a5;">Name / Label</label>
<input type="text" class="gl-input" id="fav-edit-label" placeholder="Favorite Name">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; margin-top:10px;">
<label style="font-size:11px; color:#8b98a5;">Prompt Text</label>
<span id="favorites-viewer-time" style="font-size:9px; color:#6b7280;"></span>
</div>
<textarea class="gl-textarea" id="favorites-viewer-text"></textarea>
<div class="gl-editor-buttons">
<button class="gl-btn gl-btn-cancel" id="btn-fav-viewer-back">Cancel</button>
<button class="gl-btn gl-btn-save" id="btn-fav-viewer-save">Save Changes</button>
</div>
</div>
`;
document.body.appendChild(favoritesModal);
// --- HISTORY MODAL ---
const historyModal = document.createElement('div');
historyModal.id = 'grok-history-modal';
historyModal.className = 'grok-modal';
historyModal.style.top = histPos.top;
historyModal.style.left = histPos.left;
historyModal.innerHTML = `
<div class="gl-header" id="hist-header"><span>Prompt History (500 max)</span><span class="gl-close history-close">×</span></div>
<div class="history-tabs">
<div class="history-tab active" data-tab="video"> Video</div>
<div class="history-tab" data-tab="image">️ Image</div>
<div class="history-tab" data-tab="edited">️ Edited</div>
</div>
<div class="gl-view-list" id="history-view-list">
<div class="gl-list-content" id="history-list-container"></div>
<div style="display: flex; gap: 8px; margin: 12px;">
<div class="gl-create-btn" id="btn-create-history-prompt" style="flex: 1;">Create New Prompt</div>
<div class="history-clear-btn" id="btn-clear-history" style="flex: 1;">Clear History</div>
</div>
</div>
<div class="gl-view-editor history-viewer" id="history-view-viewer">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
<label style="font-size:11px; color:#9ca3af; font-weight: 600;">Full Prompt</label>
<span id="history-viewer-time" style="font-size:9px; color:#6b7280;"></span>
</div>
<textarea class="gl-textarea" id="history-viewer-text" readonly></textarea>
<div class="gl-editor-buttons">
<button class="gl-btn gl-btn-cancel" id="btn-viewer-back">← Back</button>
<button class="gl-btn gl-btn-save" id="btn-viewer-use">Use This Prompt</button>
</div>
</div>
<div class="gl-view-editor" id="history-view-creator">
<label style="font-size:11px; color:#8b98a5; margin-bottom: 5px;">Create New Prompt</label>
<textarea class="gl-textarea" id="history-creator-text" placeholder="Enter your prompt here..."></textarea>
<div class="gl-editor-buttons">
<button class="gl-btn gl-btn-cancel" id="btn-creator-cancel">Cancel</button>
<button class="gl-btn gl-btn-save" id="btn-creator-save">Save to History</button>
</div>
</div>
`;
document.body.appendChild(historyModal);
// --- DRAGGABLE FUNCTIONALITY ---
function makeDraggable(element, handleSelector, saveKey) {
const handle = element.querySelector(handleSelector);
let isDragging = false;
let startX, startY, initialLeft, initialTop;
handle.addEventListener('mousedown', (e) => {
const allModals = document.querySelectorAll('.grok-modal, #grok-control-panel');
let maxZ = 99990;
allModals.forEach(el => {
const z = parseInt(window.getComputedStyle(el).zIndex) || 0;
if (z > maxZ) maxZ = z;
});
element.style.zIndex = maxZ + 1;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = element.getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
element.style.right = 'auto';
element.style.bottom = 'auto';
element.style.width = rect.width + 'px';
document.body.style.cursor = 'move';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
element.style.left = `${initialLeft + dx}px`;
element.style.top = `${initialTop + dy}px`;
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
document.body.style.cursor = '';
const rect = element.getBoundingClientRect();
const pos = { top: rect.top + 'px', left: rect.left + 'px' };
GM_setValue(saveKey, pos);
}
});
}
makeDraggable(panel, '#grok-main-header', 'pos_main');
makeDraggable(modal, '#lib-header', 'pos_lib');
makeDraggable(favoritesModal, '#fav-header', 'pos_fav');
makeDraggable(historyModal, '#hist-header', 'pos_hist');
// --- RESIZE LOGIC ---
const resizeHandle = document.getElementById('grok-resize-handle');
let isResizing = false;
let rStartX, rStartY, rStartW, rStartH;
resizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
rStartX = e.clientX;
rStartY = e.clientY;
const rect = panel.getBoundingClientRect();
rStartW = rect.width;
rStartH = rect.height;
e.preventDefault();
e.stopPropagation();
document.body.style.cursor = 'nwse-resize';
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const deltaX = rStartX - e.clientX;
const deltaY = rStartY - e.clientY;
const newWidth = Math.max(280, rStartW + deltaX);
const newHeight = Math.max(250, rStartH + deltaY);
panel.style.width = newWidth + 'px';
panel.style.height = newHeight + 'px';
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = '';
GM_setValue('panelSize', { width: panel.style.width, height: panel.style.height });
}
});
// --- HELPER FUNCTIONS ---
function nativeValueSet(el, value) {
if (!el) return;
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
setter.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
}
function resetState(msg) {
limitReached = false;
currentRetryCount = 0;
moderationDetected = false;
processingModeration = false;
if (errorWaitInterval) {
clearInterval(errorWaitInterval);
errorWaitInterval = null;
}
updateStatus(msg);
}
function updateStatus(msg, type) {
const statusText = document.getElementById('grok-status');
if (!statusText) return;
statusText.textContent = msg;
statusText.className = '';
if (type === 'error') statusText.classList.add('status-error');
if (type === 'warning') statusText.classList.add('status-warning');
}
function formatTimestamp(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'Just now';
if (diff < 3600000) {
const mins = Math.floor(diff / 60000);
return `${mins} min${mins > 1 ? 's' : ''} ago`;
}
if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours} hr${hours > 1 ? 's' : ''} ago`;
}
return date.toLocaleDateString();
}
function escapeHtml(text) {
return text ? text.replace(/&/g, "&").replace(/</g, "<") : '';
}
function toggleModal(targetModal) {
const allModals = document.querySelectorAll('.grok-modal, #grok-control-panel');
let maxZ = 99990;
allModals.forEach(el => {
const z = parseInt(window.getComputedStyle(el).zIndex) || 0;
if (z > maxZ) maxZ = z;
});
targetModal.style.zIndex = maxZ + 1;
targetModal.classList.toggle('active');
}
// Find active Grok input
function getGrokInput() {
return document.querySelector(TARGET_TEXTAREA_SELECTOR) ||
document.querySelector(IMAGE_EDITOR_SELECTOR) ||
document.querySelector(IMAGE_PROMPT_SELECTOR);
}
function getGrokImagine() {
return document.querySelector(IMAGE_IMAGINE_SELECTOR);
}
// --- FIX: correct buttons in Extend/Edit mode ---
const IMAGE_EDIT_EXIT_XPATH = '/html/body/div[2]/div[2]/div/div/div/div/main/article/div[2]/div[1]/div/div[1]/div/button';
function findByXPath(xpath) {
try {
return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
} catch {
return null;
}
}
function isElementVisible(el) {
if (!el) return false;
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
}
function getMakeVideoButtonsVisible() {
return Array.from(document.querySelectorAll('button[aria-label="Make video"]')).filter(isElementVisible);
}
// Tiny up-arrow submit in extend/edit
function isUpArrowSubmitButton(btn) {
return !!btn?.querySelector('path[d^="M5 11L12 4"]');
}
function getLargestByArea(buttons) {
let best = null;
let bestArea = -1;
for (const b of buttons) {
const r = b.getBoundingClientRect();
const area = r.width * r.height;
if (area > bestArea) {
bestArea = area;
best = b;
}
}
return best;
}
function getSmallestByArea(buttons) {
let best = null;
let bestArea = Infinity;
for (const b of buttons) {
const r = b.getBoundingClientRect();
const area = r.width * r.height;
if (area < bestArea) {
bestArea = area;
best = b;
}
}
return best;
}
function getExitExtendModeButton() {
return document.querySelector(IMAGE_EDIT_EXIT_SELECTOR) || findByXPath(IMAGE_EDIT_EXIT_XPATH);
}
function isImageEditMode() {
return !!getExitExtendModeButton();
}
function clickExitExtendMode() {
const btn = getExitExtendModeButton();
if (btn && !btn.disabled) {
btn.click();
return true;
}
return false;
}
// Real video Make video button (avoid tiny up-arrow if possible)
function getVideoMakeVideoButton() {
const btns = getMakeVideoButtonsVisible();
const nonArrow = btns.filter(b => !isUpArrowSubmitButton(b));
if (nonArrow.length) return getLargestByArea(nonArrow);
return getLargestByArea(btns);
}
// Up-arrow submit button in extend/edit
function getImageEditSubmitButton() {
const btns = getMakeVideoButtonsVisible();
const arrow = btns.find(isUpArrowSubmitButton);
if (arrow) return arrow;
const smallest = getSmallestByArea(btns);
if (smallest) return smallest;
return document.querySelector(IMAGE_SUBMIT_BUTTON_SELECTOR);
}
// --- HISTORY NAVIGATION LOGIC ---
const promptBox = document.getElementById('grok-panel-prompt');
const btnHistPrev = document.getElementById('btn-hist-prev');
const btnHistNext = document.getElementById('btn-hist-next');
const histNavCounter = document.getElementById('hist-nav-counter');
function getCombinedHistory() {
const combined = [
...videoPromptHistory.map(h => ({...h, source: 'video'})),
...imagePromptHistory.map(h => ({...h, source: 'image'})),
...editedPromptHistory.map(h => ({...h, source: 'edited'})),
];
return combined.sort((a, b) => b.timestamp - a.timestamp);
}
function updateHistoryNavButtons() {
const history = getCombinedHistory();
if (history.length === 0) {
btnHistPrev.disabled = true;
btnHistNext.disabled = true;
histNavCounter.textContent = '-';
return;
}
if (historyNavIndex === -1) {
btnHistPrev.disabled = false;
btnHistNext.disabled = false;
histNavCounter.textContent = 'current';
} else {
btnHistPrev.disabled = (historyNavIndex >= history.length - 1);
btnHistNext.disabled = false;
histNavCounter.textContent = `${historyNavIndex + 1}/${history.length}`;
}
}
function syncPromptToSite(value) {
const grokTA = getGrokInput();
const imagineP = getGrokImagine();
if (grokTA) nativeValueSet(grokTA, value);
else if (imagineP) {
imagineP.textContent = value;
if (imagineP.classList.contains('is-empty') && value) imagineP.classList.remove('is-empty');
else if (!value) imagineP.classList.add('is-empty');
}
}
function navigateHistory(direction) {
const history = getCombinedHistory();
if (history.length === 0) {
updateStatus('No history available', 'warning');
setTimeout(() => updateStatus('Ready'), 2000);
return;
}
if (direction === -1) {
if (historyNavIndex === -1) historyNavIndex = 0;
else if (historyNavIndex < history.length - 1) historyNavIndex++;
else return;
promptBox.value = history[historyNavIndex].text;
} else if (direction === 1) {
if (historyNavIndex === -1) {
promptBox.value = '';
updateStatus('Cleared', 'warning');
setTimeout(() => updateStatus('Ready'), 1500);
} else if (historyNavIndex === 0) {
historyNavIndex = -1;
promptBox.value = '';
} else {
historyNavIndex--;
promptBox.value = history[historyNavIndex].text;
}
}
updateHistoryNavButtons();
syncPromptToSite(promptBox.value);
}
btnHistPrev.addEventListener('click', () => navigateHistory(-1));
btnHistNext.addEventListener('click', () => navigateHistory(1));
promptBox.addEventListener('keydown', (e) => {
if (e.altKey && e.key === 'ArrowLeft') {
e.preventDefault();
navigateHistory(-1);
}
if (e.altKey && e.key === 'ArrowRight') {
e.preventDefault();
navigateHistory(1);
}
});
promptBox.addEventListener('input', () => {
if (document.activeElement === promptBox) {
historyNavIndex = -1;
updateHistoryNavButtons();
lastTypedPrompt = promptBox.value;
syncPromptToSite(lastTypedPrompt);
resetState("Ready");
}
});
// --- STATE MANAGEMENT ---
function addToHistory(prompt, type) {
if (!prompt || !prompt.trim()) return;
let arr;
if (type === 'image') arr = imagePromptHistory;
else if (type === 'edited') arr = editedPromptHistory;
else arr = videoPromptHistory;
const filtered = arr.filter(item => item.text !== prompt);
filtered.unshift({ id: Date.now().toString(), text: prompt, timestamp: Date.now(), type });
const limited = filtered.slice(0, MAX_HISTORY_ITEMS);
if (type === 'image') {
imagePromptHistory = limited;
GM_setValue('imagePromptHistory', imagePromptHistory);
} else if (type === 'edited') {
editedPromptHistory = limited;
GM_setValue('editedPromptHistory', editedPromptHistory);
} else {
videoPromptHistory = limited;
GM_setValue('videoPromptHistory', videoPromptHistory);
}
historyNavIndex = -1;
updateHistoryNavButtons();
}
function getFavoritesArrayByType(type) {
if (type === 'image') return imageFavorites;
if (type === 'edited') return editedFavorites;
return videoFavorites;
}
function setFavoritesArrayByType(type, newArr) {
if (type === 'image') {
imageFavorites = newArr;
GM_setValue('imageFavorites', imageFavorites);
return;
}
if (type === 'edited') {
editedFavorites = newArr;
GM_setValue('editedFavorites', editedFavorites);
return;
}
videoFavorites = newArr;
GM_setValue('videoFavorites', videoFavorites);
}
function addToFavorites(prompt, type) {
if (!prompt || !prompt.trim()) return;
const arr = getFavoritesArrayByType(type);
if (arr.some(item => item.text === prompt)) {
updateStatus(`Already in favorites!`, 'error');
setTimeout(() => updateStatus('Ready'), 2000);
return;
}
const next = [{
id: Date.now().toString(),
text: prompt,
label: prompt.length > 40 ? prompt.substring(0, 40) + '...' : prompt,
timestamp: Date.now(),
type
}, ...arr];
setFavoritesArrayByType(type, next);
updateStatus(`Added to favorites!
`);
setTimeout(() => updateStatus('Ready'), 2000);
}
// --- IMPORT / EXPORT LOGIC ---
function getAllData() {
return {
savedSnippets,
videoPromptHistory,
imagePromptHistory,
editedPromptHistory,
videoFavorites,
imageFavorites,
editedFavorites,
settings: {
maxRetries,
uiToggleKey,
autoClickEnabled,
panelSize,
mainPos, libPos, favPos, histPos
},
version: 41
};
}
function restoreData(data) {
if (!data) return alert("Invalid data");
try {
if (data.savedSnippets) GM_setValue('savedSnippets', data.savedSnippets);
if (data.videoPromptHistory) GM_setValue('videoPromptHistory', data.videoPromptHistory);
if (data.imagePromptHistory) GM_setValue('imagePromptHistory', data.imagePromptHistory);
if (data.editedPromptHistory) GM_setValue('editedPromptHistory', data.editedPromptHistory);
if (data.videoFavorites) GM_setValue('videoFavorites', data.videoFavorites);
if (data.imageFavorites) GM_setValue('imageFavorites', data.imageFavorites);
if (data.editedFavorites) GM_setValue('editedFavorites', data.editedFavorites);
if (data.settings) {
const s = data.settings;
if (s.maxRetries) GM_setValue('maxRetries', s.maxRetries);
if (s.uiToggleKey) GM_setValue('uiToggleKey', s.uiToggleKey);
if (s.autoClickEnabled !== undefined) GM_setValue('autoClickEnabled', s.autoClickEnabled);
if (s.panelSize) GM_setValue('panelSize', s.panelSize);
if (s.mainPos) GM_setValue('pos_main', s.mainPos);
if (s.libPos) GM_setValue('pos_lib', s.libPos);
if (s.favPos) GM_setValue('pos_fav', s.favPos);
if (s.histPos) GM_setValue('pos_hist', s.histPos);
}
alert("Import successful! Reloading page...");
location.reload();
} catch (e) {
console.error(e);
alert("Error importing data. Check console.");
}
}
document.getElementById('btn-export-all').addEventListener('click', () => {
const data = getAllData();
const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `grok_tools_backup_${new Date().toISOString().slice(0,10)}.json`;
a.click();
URL.revokeObjectURL(url);
updateStatus("Export complete!");
});
const fileInput = document.getElementById('grok-import-file');
document.getElementById('btn-import-all').addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
try {
const json = JSON.parse(evt.target.result);
if (confirm("This will overwrite your current saved snippets/history/favorites. Continue?")) {
restoreData(json);
}
} catch {
alert("Invalid JSON file.");
}
};
reader.readAsText(file);
fileInput.value = '';
});
// --- RENDER FUNCTIONS (Snippets/Favorites/History) ---
function renderSnippets() {
const listContainer = document.getElementById('gl-list-container');
listContainer.innerHTML = '';
savedSnippets.forEach(item => {
const el = document.createElement('div');
el.className = 'gl-item';
el.innerHTML = `
<div class="gl-item-text"><b>${escapeHtml(item.label)}</b><span>${escapeHtml(item.text)}</span></div>
<div class="gl-item-actions"><button class="gl-icon-btn gl-btn-edit">✎</button><button class="gl-icon-btn gl-btn-del"></button></div>
`;
el.querySelector('.gl-item-text').addEventListener('click', () => {
const cur = promptBox.value;
const newText = cur + (cur && !cur.endsWith(' ') ? ' ' : '') + item.text;
promptBox.value = newText;
lastTypedPrompt = newText;
historyNavIndex = -1;
updateHistoryNavButtons();
syncPromptToSite(newText);
modal.classList.remove('active');
});
el.querySelector('.gl-btn-edit').addEventListener('click', (e) => { e.stopPropagation(); showEditor(item); });
el.querySelector('.gl-btn-del').addEventListener('click', (e) => {
e.stopPropagation();
if (confirm(`Delete "${item.label}"?`)) {
savedSnippets = savedSnippets.filter(s => s.id !== item.id);
GM_setValue('savedSnippets', savedSnippets);
renderSnippets();
}
});
listContainer.appendChild(el);
});
}
const editLabel = document.getElementById('gl-edit-label');
const editText = document.getElementById('gl-edit-text');
let editingSnippetId = null;
function showEditor(item = null) {
document.getElementById('gl-view-list').style.display = 'none';
document.getElementById('gl-view-editor').classList.add('active');
editingSnippetId = item ? item.id : null;
editLabel.value = item ? item.label : '';
editText.value = item ? item.text : '';
editText.focus();
}
document.getElementById('btn-create-snippet').addEventListener('click', () => showEditor(null));
document.getElementById('btn-edit-cancel').addEventListener('click', () => {
document.getElementById('gl-view-editor').classList.remove('active');
document.getElementById('gl-view-list').style.display = 'flex';
});
document.getElementById('btn-edit-save').addEventListener('click', () => {
const label = editLabel.value.trim() || 'Untitled';
const text = editText.value.trim();
if (!text) return alert("Empty text");
if (editingSnippetId) {
const idx = savedSnippets.findIndex(s => s.id === editingSnippetId);
if (idx > -1) { savedSnippets[idx].label = label; savedSnippets[idx].text = text; }
} else {
savedSnippets.push({ id: Date.now().toString(), label, text });
}
GM_setValue('savedSnippets', savedSnippets);
document.getElementById('btn-edit-cancel').click();
renderSnippets();
});
// Favorites
function getFavoritesArrayByTab(tab) {
if (tab === 'image') return imageFavorites;
if (tab === 'edited') return editedFavorites;
return videoFavorites;
}
function renderFavorites() {
const listContainer = document.getElementById('favorites-list-container');
listContainer.innerHTML = '';
const favArray = getFavoritesArrayByTab(currentFavoritesTab);
if (favArray.length === 0) {
listContainer.innerHTML = `<div style="text-align:center; padding:20px; color:#555;">No favorites</div>`;
return;
}
favArray.forEach(item => {
const el = document.createElement('div');
el.className = 'gl-item';
el.innerHTML = `
<div class="gl-item-text">
<b>${escapeHtml(item.label)}</b>
<span>${escapeHtml(item.text)}</span>
<div class="history-item-time">${formatTimestamp(item.timestamp)}</div>
</div>
<div class="gl-item-actions">
<button class="gl-icon-btn fav-btn-edit">✎</button>
<button class="gl-icon-btn fav-btn-del"></button>
</div>
`;
el.querySelector('.gl-item-text').addEventListener('click', () => {
promptBox.value = item.text;
lastTypedPrompt = item.text;
historyNavIndex = -1;
updateHistoryNavButtons();
syncPromptToSite(item.text);
favoritesModal.classList.remove('active');
});
el.querySelector('.fav-btn-edit').addEventListener('click', (e) => { e.stopPropagation(); editFavorite(item); });
el.querySelector('.fav-btn-del').addEventListener('click', (e) => {
e.stopPropagation();
if (!confirm(`Remove favorite?`)) return;
let arr = getFavoritesArrayByTab(currentFavoritesTab).filter(h => h.id !== item.id);
if (currentFavoritesTab === 'video') setFavoritesArrayByType('video', arr);
else if (currentFavoritesTab === 'image') setFavoritesArrayByType('image', arr);
else setFavoritesArrayByType('edited', arr);
renderFavorites();
});
listContainer.appendChild(el);
});
}
function editFavorite(item) {
document.getElementById('favorites-view-list').style.display = 'none';
document.getElementById('favorites-view-viewer').classList.add('active');
document.getElementById('fav-edit-label').value = item.label || item.text.substring(0,20);
document.getElementById('favorites-viewer-text').value = item.text;
document.getElementById('favorites-viewer-time').textContent = formatTimestamp(item.timestamp);
currentEditingFavId = item.id;
}
document.getElementById('btn-fav-viewer-back').addEventListener('click', () => {
document.getElementById('favorites-view-viewer').classList.remove('active');
document.getElementById('favorites-view-list').style.display = 'flex';
});
document.getElementById('btn-fav-viewer-save').addEventListener('click', () => {
const newLabel = document.getElementById('fav-edit-label').value.trim() || "Untitled";
const newText = document.getElementById('favorites-viewer-text').value.trim();
if (!newText || !currentEditingFavId) return;
const favArray = getFavoritesArrayByTab(currentFavoritesTab);
const idx = favArray.findIndex(f => f.id === currentEditingFavId);
if (idx !== -1) {
favArray[idx].label = newLabel;
favArray[idx].text = newText;
// Save back
if (currentFavoritesTab === 'video') setFavoritesArrayByType('video', favArray);
else if (currentFavoritesTab === 'image') setFavoritesArrayByType('image', favArray);
else setFavoritesArrayByType('edited', favArray);
renderFavorites();
document.getElementById('btn-fav-viewer-back').click();
}
});
favoritesModal.querySelectorAll('.history-tab').forEach(tab => {
tab.addEventListener('click', () => {
favoritesModal.querySelectorAll('.history-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentFavoritesTab = tab.dataset.tab;
renderFavorites();
});
});
// History
function getHistoryArrayByTab(tab) {
if (tab === 'image') return imagePromptHistory;
if (tab === 'edited') return editedPromptHistory;
return videoPromptHistory;
}
function saveHistoryArrayByTab(tab, arr) {
if (tab === 'image') {
imagePromptHistory = arr;
GM_setValue('imagePromptHistory', imagePromptHistory);
return;
}
if (tab === 'edited') {
editedPromptHistory = arr;
GM_setValue('editedPromptHistory', editedPromptHistory);
return;
}
videoPromptHistory = arr;
GM_setValue('videoPromptHistory', videoPromptHistory);
}
function renderHistory() {
const listContainer = document.getElementById('history-list-container');
listContainer.innerHTML = '';
const historyArray = getHistoryArrayByTab(currentHistoryTab);
if (historyArray.length === 0) {
listContainer.innerHTML = `<div style="text-align:center; padding:20px; color:#555;">No history</div>`;
return;
}
const favArray =
currentHistoryTab === 'video' ? videoFavorites :
currentHistoryTab === 'image' ? imageFavorites :
editedFavorites;
historyArray.forEach(item => {
const el = document.createElement('div');
el.className = 'gl-item';
const isFavorited = favArray.some(f => f.text === item.text);
el.innerHTML = `
<div class="gl-item-text">
<span>${escapeHtml(item.text)}</span>
<div class="history-item-time">${formatTimestamp(item.timestamp)}</div>
</div>
<div class="gl-item-actions">
<button class="gl-icon-btn history-btn-fav ${isFavorited ? 'favorite' : ''}">
</button>
<button class="gl-icon-btn history-btn-view"></button>
<button class="gl-icon-btn history-btn-del"></button>
</div>
`;
el.querySelector('.gl-item-text').addEventListener('click', () => {
promptBox.value = item.text;
lastTypedPrompt = item.text;
historyNavIndex = -1;
updateHistoryNavButtons();
syncPromptToSite(item.text);
historyModal.classList.remove('active');
});
el.querySelector('.history-btn-fav').addEventListener('click', (e) => {
e.stopPropagation();
addToFavorites(item.text, item.type);
renderHistory();
});
el.querySelector('.history-btn-view').addEventListener('click', (e) => {
e.stopPropagation();
document.getElementById('history-view-list').style.display = 'none';
document.getElementById('history-view-viewer').classList.add('active');
document.getElementById('history-viewer-text').value = item.text;
document.getElementById('history-viewer-time').textContent = formatTimestamp(item.timestamp);
});
el.querySelector('.history-btn-del').addEventListener('click', (e) => {
e.stopPropagation();
const newArr = historyArray.filter(h => h.id !== item.id);
saveHistoryArrayByTab(currentHistoryTab, newArr);
historyNavIndex = -1;
updateHistoryNavButtons();
renderHistory();
});
listContainer.appendChild(el);
});
}
document.getElementById('btn-viewer-back').addEventListener('click', () => {
document.getElementById('history-view-viewer').classList.remove('active');
document.getElementById('history-view-list').style.display = 'flex';
});
document.getElementById('btn-viewer-use').addEventListener('click', () => {
const text = document.getElementById('history-viewer-text').value;
promptBox.value = text;
lastTypedPrompt = text;
historyNavIndex = -1;
updateHistoryNavButtons();
syncPromptToSite(text);
historyModal.classList.remove('active');
document.getElementById('btn-viewer-back').click();
});
// Create new prompt functionality
document.getElementById('btn-create-history-prompt').addEventListener('click', () => {
document.getElementById('history-view-list').style.display = 'none';
const creatorView = document.getElementById('history-view-creator');
creatorView.classList.add('active');
creatorView.style.display = 'flex';
document.getElementById('history-creator-text').value = '';
setTimeout(() => document.getElementById('history-creator-text').focus(), 100);
});
document.getElementById('btn-creator-cancel').addEventListener('click', () => {
const creatorView = document.getElementById('history-view-creator');
creatorView.classList.remove('active');
creatorView.style.display = 'none';
document.getElementById('history-view-list').style.display = 'flex';
});
document.getElementById('btn-creator-save').addEventListener('click', () => {
const text = document.getElementById('history-creator-text').value.trim();
if (!text) {
updateStatus('Prompt cannot be empty', 'error');
setTimeout(() => updateStatus('Ready'), 2000);
return;
}
addToHistory(text, currentHistoryTab);
updateStatus(`Saved to ${currentHistoryTab} history!`);
setTimeout(() => updateStatus('Ready'), 2000);
document.getElementById('btn-creator-cancel').click();
renderHistory();
});
document.getElementById('btn-clear-history').addEventListener('click', () => {
if (!confirm('Clear history for this tab?')) return;
saveHistoryArrayByTab(currentHistoryTab, []);
historyNavIndex = -1;
updateHistoryNavButtons();
renderHistory();
});
historyModal.querySelectorAll('.history-tab').forEach(tab => {
tab.addEventListener('click', () => {
historyModal.querySelectorAll('.history-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentHistoryTab = tab.dataset.tab;
renderHistory();
});
});
// --- BUTTON EVENT LISTENERS ---
document.getElementById('btn-open-library').addEventListener('click', () => {
toggleModal(modal);
if (modal.classList.contains('active')) renderSnippets();
});
document.getElementById('btn-open-favorites').addEventListener('click', () => {
toggleModal(favoritesModal);
if (favoritesModal.classList.contains('active')) renderFavorites();
});
document.getElementById('btn-open-history').addEventListener('click', () => {
toggleModal(historyModal);
if (historyModal.classList.contains('active')) renderHistory();
});
modal.querySelector('.gl-close').addEventListener('click', () => modal.classList.remove('active'));
favoritesModal.querySelector('.favorites-close').addEventListener('click', () => favoritesModal.classList.remove('active'));
historyModal.querySelector('.history-close').addEventListener('click', () => historyModal.classList.remove('active'));
// --- STRICT SYNC & CAPTURE LOGIC ---
// Website -> UI Panel sync
document.addEventListener('input', (e) => {
if (e.target.matches(TARGET_TEXTAREA_SELECTOR) ||
e.target.matches(IMAGE_EDITOR_SELECTOR) ||
e.target.matches(IMAGE_PROMPT_SELECTOR)) {
if (document.activeElement === e.target) {
promptBox.value = e.target.value;
lastTypedPrompt = e.target.value;
historyNavIndex = -1;
updateHistoryNavButtons();
}
}
}, { capture: true, passive: true });
// --- Generate Button Logic (v41: edited history + button fix) ---
let exitingExtendMode = false;
function detectContextType() {
// Video wins if the video textarea exists
if (document.querySelector(TARGET_TEXTAREA_SELECTOR)) return 'video';
// Edited if extend/edit mode is active
if (isImageEditMode()) return 'edited';
// Otherwise image if image inputs exist
if (document.querySelector(IMAGE_PROMPT_SELECTOR) || document.querySelector(IMAGE_IMAGINE_SELECTOR)) return 'image';
// Fallback
return 'video';
}
function doGenerateNow() {
const type = detectContextType();
const vidEl = document.querySelector(TARGET_TEXTAREA_SELECTOR);
const imgEl = document.querySelector(IMAGE_PROMPT_SELECTOR);
const imgIm = document.querySelector(IMAGE_IMAGINE_SELECTOR);
const grokTA = vidEl || document.querySelector(IMAGE_EDITOR_SELECTOR) || imgEl;
const imagineP = imgIm;
// If trying VIDEO but extend/edit is active and we only see the up-arrow submit,
// exit extend mode first, then try again.
if (type === 'video' && isImageEditMode()) {
const candidate = getVideoMakeVideoButton();
if (!candidate || isUpArrowSubmitButton(candidate)) {
if (!exitingExtendMode && clickExitExtendMode()) {
exitingExtendMode = true;
updateStatus('Exiting extend mode...', 'warning');
setTimeout(() => {
exitingExtendMode = false;
doGenerateNow();
}, 250);
return;
}
}
}
let realBtn = null;
if (type === 'video') {
// Prefer any "Edit" video button if present (keeps your old logic)
const allVideoBtns = getMakeVideoButtonsVisible();
const editBtn = allVideoBtns.find(b => (b.textContent || '').includes('Edit'));
realBtn = editBtn || getVideoMakeVideoButton();
} else if (type === 'edited') {
// Extend/edit submit is the up-arrow "Make video"
realBtn = getImageEditSubmitButton();
} else {
// Normal image generation
realBtn = document.querySelector(IMAGE_EDITOR_BUTTON_SELECTOR) ||
document.querySelector(IMAGE_SUBMIT_BUTTON_SELECTOR);
}
if (!realBtn) return updateStatus("Button not found", "error");
const promptVal = promptBox.value.trim();
if (promptVal) addToHistory(promptVal, type);
// Force sync now
if (grokTA) nativeValueSet(grokTA, promptBox.value);
else if (imagineP) imagineP.textContent = promptBox.value;
setTimeout(() => {
if (!realBtn.disabled) {
realBtn.click();
lastGenerationTimestamp = Date.now();
updateStatus(type === 'video' ? 'Generation Started...' : 'Submitted...');
} else {
updateStatus("Grok button disabled/processing.", "error");
}
}, 50);
}
document.getElementById('btn-generate').addEventListener('click', doGenerateNow);
// Capture prompts when user clicks submit buttons outside the panel
document.addEventListener('mousedown', (e) => {
const submitBtn = e.target.closest('button[aria-label="Submit"]');
const makeVideoBtn = e.target.closest('button[aria-label="Make video"]');
const val = (promptBox.value.trim() || lastTypedPrompt.trim());
if (val.length <= 2) return;
// Normal image submit
if (submitBtn) {
addToHistory(val, isImageEditMode() ? 'edited' : 'image');
updateStatus("Prompt captured!");
return;
}
// Extend/edit up-arrow submit (aria-label="Make video")
if (makeVideoBtn && isImageEditMode() && isUpArrowSubmitButton(makeVideoBtn)) {
addToHistory(val, 'edited');
updateStatus("Edited prompt captured!");
}
}, true);
// --- MODERATION & RETRY LOGIC ---
function checkForModerationContent() {
const now = Date.now();
if (now - lastModerationCheck < 200) return null;
lastModerationCheck = now;
const toastSelectors = ['section[aria-label*="Notification"]', '[role="alert"]', '.toast'];
for (const sel of toastSelectors) {
const els = document.querySelectorAll(sel);
for (const el of els) {
const txt = (el.textContent || '').toLowerCase();
if (MODERATION_PATTERNS.some(p => txt.includes(p))) return { element: el, text: txt };
}
}
return null;
}
function waitForErrorDisappearance(element) {
if (errorWaitInterval) clearInterval(errorWaitInterval);
let safetyCounter = 0;
const POLL_MS = 500;
const MAX_WAIT_MS = 10000;
errorWaitInterval = setInterval(() => {
safetyCounter += POLL_MS;
const isConnected = document.body.contains(element);
let isVisible = false;
if (isConnected) {
const style = window.getComputedStyle(element);
isVisible = style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
}
if (!isConnected || !isVisible || safetyCounter >= MAX_WAIT_MS) {
clearInterval(errorWaitInterval);
errorWaitInterval = null;
updateStatus('Message cleared. Retrying...');
handleRetry(Date.now());
}
}, POLL_MS);
}
function handleRetry(now) {
if (!isRetryEnabled || limitReached) return;
if (now - lastGenerationTimestamp < GENERATION_COOLDOWN_MS) return;
const grokTA = getGrokInput();
const imagineP = getGrokImagine();
let btn = null;
const isVideoContext = !!document.querySelector(TARGET_TEXTAREA_SELECTOR);
if (isVideoContext) {
btn = getVideoMakeVideoButton();
if (isImageEditMode() && (!btn || isUpArrowSubmitButton(btn))) {
if (clickExitExtendMode()) updateStatus('Exiting extend mode...', 'warning');
return;
}
} else {
if (isImageEditMode()) btn = getImageEditSubmitButton();
else btn = document.querySelector(IMAGE_EDITOR_BUTTON_SELECTOR) || document.querySelector(IMAGE_SUBMIT_BUTTON_SELECTOR);
}
if ((grokTA || imagineP) && lastTypedPrompt) {
if (grokTA) nativeValueSet(grokTA, lastTypedPrompt);
else if (imagineP) imagineP.textContent = lastTypedPrompt;
if (autoClickEnabled && currentRetryCount >= maxRetries) {
updateStatus(`Limit Reached (${maxRetries})`, "error");
limitReached = true;
return;
}
if (autoClickEnabled && btn) {
currentRetryCount++;
updateStatus(`Retrying (${currentRetryCount}/${maxRetries})...`, 'warning');
setTimeout(() => {
if (!btn.disabled) {
btn.click();
lastGenerationTimestamp = Date.now();
}
processingModeration = false;
moderationDetected = false;
}, RETRY_DELAY_MS);
}
}
}
const observer = new MutationObserver(() => {
if (observerThrottle || !isRetryEnabled || limitReached) return;
observerThrottle = true;
setTimeout(() => { observerThrottle = false; }, OBSERVER_THROTTLE_MS);
if (!processingModeration) {
const mod = checkForModerationContent();
if (mod) {
debugLog('Moderation detected:', mod.text);
processingModeration = true;
moderationDetected = true;
updateStatus(`Moderation detected! Waiting...`, 'warning');
waitForErrorDisappearance(mod.element);
}
}
});
observer.observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true });
// --- TOGGLES & SETTINGS ---
const toggleBtn = document.getElementById('grok-toggle-btn');
toggleBtn.addEventListener('click', () => {
isRetryEnabled = !isRetryEnabled;
toggleBtn.textContent = isRetryEnabled ? "ON" : "OFF";
toggleBtn.classList.toggle('off', !isRetryEnabled);
resetState(isRetryEnabled ? "Ready" : "Disabled");
if (!isRetryEnabled) updateStatus("Disabled", "error");
});
document.getElementById('grok-autoclick-cb').addEventListener('change', (e) => {
autoClickEnabled = e.target.checked;
GM_setValue('autoClickEnabled', autoClickEnabled);
});
document.getElementById('grok-retry-limit').addEventListener('change', (e) => {
maxRetries = parseInt(e.target.value);
GM_setValue('maxRetries', maxRetries);
});
// Keybinds
document.addEventListener('keydown', (e) => {
// Toggle Main UI
if (e.altKey && e.key.toLowerCase() === uiToggleKey) {
isUiVisible = !isUiVisible;
GM_setValue('isUiVisible', isUiVisible);
panel.classList.toggle('hidden', !isUiVisible);
e.preventDefault();
}
// Toggle Import/Export Buttons
if (e.altKey && e.key.toLowerCase() === 'i') {
const ioRow = document.getElementById('grok-io-container');
if (ioRow) {
ioRow.classList.toggle('io-hidden');
e.preventDefault();
}
}
});
window.addEventListener('beforeunload', () => {
observer.disconnect();
if (errorWaitInterval) clearInterval(errorWaitInterval);
});
// Initialize navigation buttons
updateHistoryNavButtons();
debugLog('Grok Tools v41 Initialized - Edited History + Edited Favorites + Button Fix');
})();
// ==UserScript==
// @name Grok Auto-Retry + Prompt Snippets + History + Favorites (v41 - Edited Image History )
// @namespace http://tampermonkey.net/
// @version 41
// @description Navigation arrows cycle through permanent prompt history. Adds Edited Image history + favorites. Fixes wrong tiny up-arrow "Make video" button click in Image Extend/Edit mode.
// @author You
// @license MIT
// @match https://grok.com/*
// @match https://*.grok.com/*
// @match https://grok.x.ai/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_setClipboard
// @downloadURL https://update.greasyfork.org/scrip...vorites (v41 - Edited Image History ).user.js
// @updateURL https://update.greasyfork.org/scrip...vorites (v41 - Edited Image History ).meta.js
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION ---
const TARGET_TEXTAREA_SELECTOR = 'textarea[aria-label="Make a video"]';
// Image selectors
const IMAGE_EDITOR_SELECTOR = 'textarea[aria-label="Type to edit image..."]';
const IMAGE_PROMPT_SELECTOR = 'textarea[aria-label="Image prompt"]';
const IMAGE_IMAGINE_SELECTOR = 'p[data-placeholder="Type to imagine"]';
// Image Extend/Edit Mode Indicator
const IMAGE_EDIT_EXIT_SELECTOR = 'button[aria-label="Exit extend mode"]';
// Buttons
const RETRY_BUTTON_SELECTOR = 'button[aria-label="Make video"]';
const IMAGE_EDITOR_BUTTON_SELECTOR = 'button[aria-label="Generate"]';
const IMAGE_SUBMIT_BUTTON_SELECTOR = 'button[aria-label="Submit"]';
const MODERATION_PATTERNS = [
"content moderated",
"try a different idea",
"moderated",
"content policy",
"cannot generate",
"unable to generate"
];
const RETRY_DELAY_MS = 1500;
const OBSERVER_THROTTLE_MS = 300;
const MAX_HISTORY_ITEMS = 500;
const DEBUG_MODE = true;
// --- DEFAULT SNIPPETS ---
const DEFAULT_SNIPPETS = [
{
id: 'b1',
label: 'Anime Stickers (Provocative - Adults)',
text: 'Surrounding the central image: thick decorative border made of overlapping colorful anime-style stickers featuring adult anime women with exaggerated proportions in various provocative poses. Each sticker has a white outline and slight drop shadow. The stickers completely frame all four edges of the image with some overlap into the main content.'
},
{
id: 'b2',
label: 'Anime Stickers (SFW)',
text: 'Surrounding the central image: thick decorative border made of overlapping colorful anime-style stickers featuring anime women in various poses. Each sticker has a white outline and slight drop shadow. The stickers completely frame all four edges of the image with some overlap into the main content.'
},
{ id: '1', label: 'Motion: Slow Mo', text: 'slow motion, high frame rate, smooth movement' },
{ id: '2', label: 'Style: Photorealistic', text: 'photorealistic, 8k resolution, highly detailed, unreal engine 5 render' },
{ id: '3', label: 'Lighting: Golden Hour', text: 'golden hour lighting, warm sun rays, lens flare, soft shadows' },
];
// --- LOAD SAVED SETTINGS ---
let maxRetries = GM_getValue('maxRetries', 5);
let uiToggleKey = GM_getValue('uiToggleKey', 'h');
let autoClickEnabled = GM_getValue('autoClickEnabled', true);
let isUiVisible = GM_getValue('isUiVisible', true);
let savedSnippets = GM_getValue('savedSnippets', DEFAULT_SNIPPETS);
let videoPromptHistory = GM_getValue('videoPromptHistory', []);
let imagePromptHistory = GM_getValue('imagePromptHistory', []);
let editedPromptHistory = GM_getValue('editedPromptHistory', []); // NEW: Edited Image prompts
let videoFavorites = GM_getValue('videoFavorites', []);
let imageFavorites = GM_getValue('imageFavorites', []);
let editedFavorites = GM_getValue('editedFavorites', []); // NEW: Edited Image favorites
let panelSize = GM_getValue('panelSize', { width: '300px', height: '460px' });
// --- LOAD SAVED POSITIONS ---
let mainPos = GM_getValue('pos_main', { top: 'auto', left: 'auto', bottom: '20px', right: '20px' });
let libPos = GM_getValue('pos_lib', { top: '100px', left: '100px' });
let favPos = GM_getValue('pos_fav', { top: '120px', left: '120px' });
let histPos = GM_getValue('pos_hist', { top: '140px', left: '140px' });
let isRetryEnabled = true;
let limitReached = false;
let currentRetryCount = 0;
let lastTypedPrompt = "";
let lastGenerationTimestamp = 0;
const GENERATION_COOLDOWN_MS = 3000;
let observerThrottle = false;
let moderationDetected = false;
let processingModeration = false;
let currentHistoryTab = 'video'; // 'video' | 'image' | 'edited'
let currentFavoritesTab = 'video'; // 'video' | 'image' | 'edited'
let currentEditingFavId = null;
let lastModerationCheck = 0;
let errorWaitInterval = null;
// --- HISTORY NAVIGATION VARIABLES ---
let historyNavIndex = -1;
// --- DEBUG LOGGER ---
function debugLog(...args) {
if (DEBUG_MODE) console.log('[Grok Tools]', ...args);
}
// --- STYLES ---
GM_addStyle(`
#grok-control-panel {
position: fixed;
width: ${panelSize.width}; height: ${panelSize.height};
min-width: 280px; min-height: 250px; max-width: 90vw; max-height: 90vh;
background: linear-gradient(145deg, #0a0a0a 0%, #1a1a1a 100%);
border: 1px solid #2a2a2a;
border-radius: 16px;
padding: 15px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #e0e0e0;
z-index: 99990;
box-shadow: 0 8px 32px rgba(0,0,0,0.9), 0 0 0 1px rgba(255,255,255,0.05);
display: flex;
flex-direction: column;
gap: 10px;
}
#grok-control-panel.hidden { display: none !important; }
.grok-header, .gl-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
margin-left: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #2a2a2a;
cursor: move;
user-select: none;
}
.gl-header {
padding: 12px 15px;
margin-left: 0;
background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%);
border-radius: 16px 16px 0 0;
font-weight: bold; font-size: 13px; color: #f0f0f0;
}
#grok-resize-handle {
position: absolute; top: 0; left: 0; width: 15px; height: 15px;
cursor: nwse-resize; z-index: 99999;
}
#grok-resize-handle::after {
content: ''; position: absolute; top: 2px; left: 2px;
border-top: 6px solid #3b82f6; border-right: 6px solid transparent;
width: 0; height: 0; opacity: 0.7;
}
#grok-resize-handle:hover::after { opacity: 1; border-top-color: #60a5fa; }
.grok-title {
font-weight: bold; font-size: 14px; color: #f0f0f0;
text-shadow: 0 2px 4px rgba(0,0,0,0.5); pointer-events: none;
}
.grok-toggle-btn {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: none; color: white; padding: 6px 14px; border-radius: 20px;
font-size: 11px; font-weight: bold; cursor: pointer;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
transition: all 0.2s ease;
}
.grok-toggle-btn.off {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
}
.grok-controls {
display: flex; align-items: center; justify-content: space-between;
font-size: 12px; color: #9ca3af; flex-shrink: 0; padding: 8px 0;
}
.grok-checkbox { display: flex; align-items: center; cursor: pointer; color: #d1d5db; }
.grok-checkbox input { margin-right: 6px; cursor: pointer; accent-color: #3b82f6; }
.grok-num-input {
width: 40px; background: #1f1f1f; border: 1px solid #2a2a2a;
color: #e0e0e0; border-radius: 6px; padding: 4px 6px; text-align: center;
}
/* NAV BUTTON STYLES */
.grok-prompt-header-row {
display: flex; justify-content: space-between; align-items: center; margin-bottom: -5px; flex-shrink: 0;
}
.grok-prompt-label {
font-size: 11px; font-weight: bold; color: #9ca3af;
}
.grok-nav-container { display: flex; gap: 4px; align-items: center; }
.grok-nav-btn {
background: #1f1f1f; border: 1px solid #333; color: #888;
cursor: pointer; padding: 2px 8px; border-radius: 4px;
font-size: 10px; transition: all 0.2s; min-width: 25px;
}
.grok-nav-btn:hover:not
disabled) { background: #333; color: #fff; border-color: #555; }
.grok-nav-btn:disabled { opacity: 0.3; cursor: default; }
.grok-nav-counter {
font-size: 9px; color: #666; min-width: 50px; text-align: center;
}
#grok-panel-prompt {
width: 100%; flex-grow: 1; background: #0f0f0f; border: 1px solid #2a2a2a;
border-radius: 8px; color: #e0e0e0; padding: 10px; font-size: 12px;
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; resize: none;
box-sizing: border-box; transition: all 0.2s ease; margin-top: 5px;
}
#grok-panel-prompt:focus {
border-color: #3b82f6; outline: none; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.grok-btn-row { display: flex; gap: 8px; flex-shrink: 0; }
.grok-action-btn {
flex: 1; padding: 10px; border-radius: 8px; border: none; cursor: pointer;
font-weight: 600; font-size: 12px; transition: all 0.2s ease;
position: relative; overflow: hidden;
}
.grok-action-btn:hover { transform: translateY(-1px); }
#btn-open-library { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); color: white; }
#btn-open-favorites { background: linear-gradient(135deg, #ec4899 0%, #db2777 100%); color: white; }
#btn-open-history { background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); color: white; }
#btn-generate { background: linear-gradient(135deg, #1f1f1f 0%, #2a2a2a 100%); color: #e0e0e0; border: 1px solid #3a3a3a; }
#grok-status {
text-align: center; font-size: 11px; color: #10b981; padding-top: 8px;
border-top: 1px solid #2a2a2a; flex-shrink: 0; font-weight: 500;
}
.status-error { color: #ef4444 !important; }
.status-warning { color: #f59e0b !important; }
/* Import/Export Panel */
#grok-io-container.io-hidden { display: none; }
#grok-io-container {
display: flex; gap: 8px; margin-top: 5px; padding-top: 5px; border-top: 1px solid #2a2a2a;
}
.io-btn {
flex: 1; background: #1f1f1f; color: #9ca3af; border: 1px solid #2a2a2a;
border-radius: 6px; padding: 5px; font-size: 10px; cursor: pointer;
}
.io-btn:hover { background: #333; color: #fff; }
/* Modal Styles */
.grok-modal {
position: fixed;
width: 350px; height: 400px;
background: linear-gradient(145deg, #0a0a0a 0%, #1a1a1a 100%);
border: 1px solid #2a2a2a;
border-radius: 16px;
display: none; flex-direction: column;
z-index: 99995;
box-shadow: 0 8px 32px rgba(0,0,0,0.9), 0 0 0 1px rgba(255,255,255,0.05);
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.grok-modal.active { display: flex; }
.gl-close {
cursor: pointer; font-size: 20px; line-height: 1; color: #6b7280;
width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;
}
.gl-close:hover { color: #f0f0f0; }
/* History/Favorites Tab Styles */
.history-tabs { display: flex; background: #0f0f0f; border-bottom: 1px solid #2a2a2a; }
.history-tab {
flex: 1; padding: 10px; text-align: center; cursor: pointer;
font-size: 11px; font-weight: 600; color: #6b7280; border-bottom: 2px solid transparent;
}
.history-tab:hover { color: #9ca3af; background: #1a1a1a; }
.history-tab.active { color: #8b5cf6; border-bottom-color: #8b5cf6; background: #1a1a1a; }
.gl-view-list { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
.gl-list-content { overflow-y: auto; padding: 12px; flex: 1; display: flex; flex-direction: column; gap: 8px; }
.gl-list-content::-webkit-scrollbar { width: 8px; }
.gl-list-content::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 4px; }
.gl-item {
background: linear-gradient(135deg, #1a1a1a 0%, #151515 100%);
border: 1px solid #2a2a2a; padding: 10px; border-radius: 8px;
display: flex; justify-content: space-between; align-items: center;
font-size: 12px; color: #e0e0e0; transition: all 0.2s ease;
}
.gl-item:hover { border-color: #3b82f6; background: linear-gradient(135deg, #1f1f1f 0%, #1a1a1a 100%); }
.gl-item-text { cursor: pointer; flex: 1; margin-right: 10px; }
.gl-item-text b { display: block; margin-bottom: 4px; color: #f0f0f0; }
.gl-item-text span {
color: #9ca3af; font-size: 10px; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.gl-item-actions { display: flex; gap: 6px; }
.gl-icon-btn {
background: #1f1f1f; border: 1px solid #2a2a2a; cursor: pointer;
font-size: 14px; color: #9ca3af; padding: 6px; border-radius: 6px;
width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
}
.gl-icon-btn:hover { color: #f0f0f0; background: #2a2a2a; transform: scale(1.1); }
.gl-icon-btn.favorite { color: #ec4899; }
.gl-create-btn, .history-clear-btn {
margin: 12px; padding: 10px; background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white; text-align: center; border-radius: 8px; cursor: pointer;
font-weight: 600; font-size: 12px;
}
.history-clear-btn { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); }
.gl-view-editor { display: none; flex-direction: column; padding: 15px; height: 100%; gap: 10px; }
.gl-view-editor.active { display: flex; }
.history-viewer { display: none; }
.history-viewer.active { display: flex; }
.gl-input, .gl-textarea {
background: #0f0f0f; border: 1px solid #2a2a2a; color: #e0e0e0;
padding: 10px; border-radius: 8px; font-size: 12px; width: 100%; box-sizing: border-box;
}
.gl-input:focus, .gl-textarea:focus { border-color: #3b82f6; outline: none; }
.gl-textarea { flex-grow: 1; resize: none; font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; }
.gl-editor-buttons { display: flex; gap: 10px; margin-top: auto; }
.gl-btn { flex: 1; padding: 10px; border-radius: 8px; border: none; cursor: pointer; font-weight: 600; color: white; font-size: 12px; }
.gl-btn-save { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); }
.gl-btn-cancel { background: linear-gradient(135deg, #374151 0%, #1f2937 100%); }
.history-item-time { font-size: 9px; color: #6b7280; margin-top: 3px; }
`);
// --- DOM CREATION (MAIN PANEL) ---
const panel = document.createElement('div');
panel.id = 'grok-control-panel';
if (mainPos.top !== 'auto') {
panel.style.top = mainPos.top;
panel.style.left = mainPos.left;
panel.style.bottom = 'auto';
panel.style.right = 'auto';
} else {
panel.style.bottom = mainPos.bottom;
panel.style.right = mainPos.right;
}
if (!isUiVisible) panel.classList.add('hidden');
panel.innerHTML = `
<div id="grok-resize-handle" title="Drag to Resize"></div>
<div class="grok-header" id="grok-main-header">
<span class="grok-title">Grok Tools v41</span>
<button id="grok-toggle-btn" class="grok-toggle-btn">ON</button>
</div>
<div class="grok-controls">
<label class="grok-checkbox">
<input type="checkbox" id="grok-autoclick-cb" ${autoClickEnabled ? 'checked' : ''}> Auto-Retry
</label>
<div>
Max: <input type="number" id="grok-retry-limit" value="${maxRetries}" class="grok-num-input" min="1">
</div>
</div>
<div class="grok-prompt-header-row">
<div class="grok-prompt-label">Prompt Editor</div>
<div class="grok-nav-container">
<button id="btn-hist-prev" class="grok-nav-btn" title="Previous in History (Alt+Left)">◀</button>
<span id="hist-nav-counter" class="grok-nav-counter">-</span>
<button id="btn-hist-next" class="grok-nav-btn" title="Next in History (Alt+Right)">▶</button>
</div>
</div>
<textarea id="grok-panel-prompt" placeholder="Type or paste prompt here..."></textarea>
<div class="grok-btn-row">
<button id="btn-open-library" class="grok-action-btn">Snippets</button>
<button id="btn-open-favorites" class="grok-action-btn">
</button>
<button id="btn-open-history" class="grok-action-btn">History</button>
<button id="btn-generate" class="grok-action-btn">Generate</button>
</div>
<div id="grok-status">Ready</div>
<div id="grok-io-container" class="io-hidden">
<button id="btn-export-all" class="io-btn">Export Data
</button>
<button id="btn-import-all" class="io-btn">Import Data
</button>
<input type="file" id="grok-import-file" style="display:none" accept=".json">
</div>
<div style="font-size:9px; color:#555; text-align:center;">UI: Alt+${uiToggleKey.toUpperCase()} | I/O: Alt+I</div>
`;
document.body.appendChild(panel);
// --- LIBRARY MODAL ---
const modal = document.createElement('div');
modal.id = 'grok-library-modal';
modal.className = 'grok-modal';
modal.style.top = libPos.top;
modal.style.left = libPos.left;
modal.innerHTML = `
<div class="gl-header" id="lib-header"><span>Snippets Library</span><span class="gl-close">×</span></div>
<div class="gl-view-list" id="gl-view-list">
<div class="gl-list-content" id="gl-list-container"></div>
<div class="gl-create-btn" id="btn-create-snippet">Create New Snippet</div>
</div>
<div class="gl-view-editor" id="gl-view-editor">
<label style="font-size:11px; color:#8b98a5;">Label</label>
<input type="text" class="gl-input" id="gl-edit-label" placeholder="e.g. Cinematic Lighting">
<label style="font-size:11px; color:#8b98a5;">Prompt Text</label>
<textarea class="gl-textarea" id="gl-edit-text" placeholder="Content to append..."></textarea>
<div class="gl-editor-buttons">
<button class="gl-btn gl-btn-cancel" id="btn-edit-cancel">Cancel</button>
<button class="gl-btn gl-btn-save" id="btn-edit-save">Save Snippet</button>
</div>
</div>
`;
document.body.appendChild(modal);
// --- FAVORITES MODAL ---
const favoritesModal = document.createElement('div');
favoritesModal.id = 'grok-favorites-modal';
favoritesModal.className = 'grok-modal';
favoritesModal.style.top = favPos.top;
favoritesModal.style.left = favPos.left;
favoritesModal.innerHTML = `
<div class="gl-header" id="fav-header"><span>Favorites
</span><span class="gl-close favorites-close">×</span></div>
<div class="history-tabs">
<div class="history-tab active" data-tab="video"> Video</div>
<div class="history-tab" data-tab="image">️ Image</div>
<div class="history-tab" data-tab="edited">️ Edited</div>
</div>
<div class="gl-view-list" id="favorites-view-list">
<div class="gl-list-content" id="favorites-list-container"></div>
</div>
<div class="gl-view-editor" id="favorites-view-viewer">
<label style="font-size:11px; color:#8b98a5;">Name / Label</label>
<input type="text" class="gl-input" id="fav-edit-label" placeholder="Favorite Name">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; margin-top:10px;">
<label style="font-size:11px; color:#8b98a5;">Prompt Text</label>
<span id="favorites-viewer-time" style="font-size:9px; color:#6b7280;"></span>
</div>
<textarea class="gl-textarea" id="favorites-viewer-text"></textarea>
<div class="gl-editor-buttons">
<button class="gl-btn gl-btn-cancel" id="btn-fav-viewer-back">Cancel</button>
<button class="gl-btn gl-btn-save" id="btn-fav-viewer-save">Save Changes</button>
</div>
</div>
`;
document.body.appendChild(favoritesModal);
// --- HISTORY MODAL ---
const historyModal = document.createElement('div');
historyModal.id = 'grok-history-modal';
historyModal.className = 'grok-modal';
historyModal.style.top = histPos.top;
historyModal.style.left = histPos.left;
historyModal.innerHTML = `
<div class="gl-header" id="hist-header"><span>Prompt History (500 max)</span><span class="gl-close history-close">×</span></div>
<div class="history-tabs">
<div class="history-tab active" data-tab="video"> Video</div>
<div class="history-tab" data-tab="image">️ Image</div>
<div class="history-tab" data-tab="edited">️ Edited</div>
</div>
<div class="gl-view-list" id="history-view-list">
<div class="gl-list-content" id="history-list-container"></div>
<div style="display: flex; gap: 8px; margin: 12px;">
<div class="gl-create-btn" id="btn-create-history-prompt" style="flex: 1;">Create New Prompt</div>
<div class="history-clear-btn" id="btn-clear-history" style="flex: 1;">Clear History</div>
</div>
</div>
<div class="gl-view-editor history-viewer" id="history-view-viewer">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
<label style="font-size:11px; color:#9ca3af; font-weight: 600;">Full Prompt</label>
<span id="history-viewer-time" style="font-size:9px; color:#6b7280;"></span>
</div>
<textarea class="gl-textarea" id="history-viewer-text" readonly></textarea>
<div class="gl-editor-buttons">
<button class="gl-btn gl-btn-cancel" id="btn-viewer-back">← Back</button>
<button class="gl-btn gl-btn-save" id="btn-viewer-use">Use This Prompt</button>
</div>
</div>
<div class="gl-view-editor" id="history-view-creator">
<label style="font-size:11px; color:#8b98a5; margin-bottom: 5px;">Create New Prompt</label>
<textarea class="gl-textarea" id="history-creator-text" placeholder="Enter your prompt here..."></textarea>
<div class="gl-editor-buttons">
<button class="gl-btn gl-btn-cancel" id="btn-creator-cancel">Cancel</button>
<button class="gl-btn gl-btn-save" id="btn-creator-save">Save to History</button>
</div>
</div>
`;
document.body.appendChild(historyModal);
// --- DRAGGABLE FUNCTIONALITY ---
function makeDraggable(element, handleSelector, saveKey) {
const handle = element.querySelector(handleSelector);
let isDragging = false;
let startX, startY, initialLeft, initialTop;
handle.addEventListener('mousedown', (e) => {
const allModals = document.querySelectorAll('.grok-modal, #grok-control-panel');
let maxZ = 99990;
allModals.forEach(el => {
const z = parseInt(window.getComputedStyle(el).zIndex) || 0;
if (z > maxZ) maxZ = z;
});
element.style.zIndex = maxZ + 1;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = element.getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
element.style.right = 'auto';
element.style.bottom = 'auto';
element.style.width = rect.width + 'px';
document.body.style.cursor = 'move';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
element.style.left = `${initialLeft + dx}px`;
element.style.top = `${initialTop + dy}px`;
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
document.body.style.cursor = '';
const rect = element.getBoundingClientRect();
const pos = { top: rect.top + 'px', left: rect.left + 'px' };
GM_setValue(saveKey, pos);
}
});
}
makeDraggable(panel, '#grok-main-header', 'pos_main');
makeDraggable(modal, '#lib-header', 'pos_lib');
makeDraggable(favoritesModal, '#fav-header', 'pos_fav');
makeDraggable(historyModal, '#hist-header', 'pos_hist');
// --- RESIZE LOGIC ---
const resizeHandle = document.getElementById('grok-resize-handle');
let isResizing = false;
let rStartX, rStartY, rStartW, rStartH;
resizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
rStartX = e.clientX;
rStartY = e.clientY;
const rect = panel.getBoundingClientRect();
rStartW = rect.width;
rStartH = rect.height;
e.preventDefault();
e.stopPropagation();
document.body.style.cursor = 'nwse-resize';
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const deltaX = rStartX - e.clientX;
const deltaY = rStartY - e.clientY;
const newWidth = Math.max(280, rStartW + deltaX);
const newHeight = Math.max(250, rStartH + deltaY);
panel.style.width = newWidth + 'px';
panel.style.height = newHeight + 'px';
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = '';
GM_setValue('panelSize', { width: panel.style.width, height: panel.style.height });
}
});
// --- HELPER FUNCTIONS ---
function nativeValueSet(el, value) {
if (!el) return;
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
setter.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
}
function resetState(msg) {
limitReached = false;
currentRetryCount = 0;
moderationDetected = false;
processingModeration = false;
if (errorWaitInterval) {
clearInterval(errorWaitInterval);
errorWaitInterval = null;
}
updateStatus(msg);
}
function updateStatus(msg, type) {
const statusText = document.getElementById('grok-status');
if (!statusText) return;
statusText.textContent = msg;
statusText.className = '';
if (type === 'error') statusText.classList.add('status-error');
if (type === 'warning') statusText.classList.add('status-warning');
}
function formatTimestamp(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'Just now';
if (diff < 3600000) {
const mins = Math.floor(diff / 60000);
return `${mins} min${mins > 1 ? 's' : ''} ago`;
}
if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours} hr${hours > 1 ? 's' : ''} ago`;
}
return date.toLocaleDateString();
}
function escapeHtml(text) {
return text ? text.replace(/&/g, "&").replace(/</g, "<") : '';
}
function toggleModal(targetModal) {
const allModals = document.querySelectorAll('.grok-modal, #grok-control-panel');
let maxZ = 99990;
allModals.forEach(el => {
const z = parseInt(window.getComputedStyle(el).zIndex) || 0;
if (z > maxZ) maxZ = z;
});
targetModal.style.zIndex = maxZ + 1;
targetModal.classList.toggle('active');
}
// Find active Grok input
function getGrokInput() {
return document.querySelector(TARGET_TEXTAREA_SELECTOR) ||
document.querySelector(IMAGE_EDITOR_SELECTOR) ||
document.querySelector(IMAGE_PROMPT_SELECTOR);
}
function getGrokImagine() {
return document.querySelector(IMAGE_IMAGINE_SELECTOR);
}
// --- FIX: correct buttons in Extend/Edit mode ---
const IMAGE_EDIT_EXIT_XPATH = '/html/body/div[2]/div[2]/div/div/div/div/main/article/div[2]/div[1]/div/div[1]/div/button';
function findByXPath(xpath) {
try {
return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
} catch {
return null;
}
}
function isElementVisible(el) {
if (!el) return false;
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
}
function getMakeVideoButtonsVisible() {
return Array.from(document.querySelectorAll('button[aria-label="Make video"]')).filter(isElementVisible);
}
// Tiny up-arrow submit in extend/edit
function isUpArrowSubmitButton(btn) {
return !!btn?.querySelector('path[d^="M5 11L12 4"]');
}
function getLargestByArea(buttons) {
let best = null;
let bestArea = -1;
for (const b of buttons) {
const r = b.getBoundingClientRect();
const area = r.width * r.height;
if (area > bestArea) {
bestArea = area;
best = b;
}
}
return best;
}
function getSmallestByArea(buttons) {
let best = null;
let bestArea = Infinity;
for (const b of buttons) {
const r = b.getBoundingClientRect();
const area = r.width * r.height;
if (area < bestArea) {
bestArea = area;
best = b;
}
}
return best;
}
function getExitExtendModeButton() {
return document.querySelector(IMAGE_EDIT_EXIT_SELECTOR) || findByXPath(IMAGE_EDIT_EXIT_XPATH);
}
function isImageEditMode() {
return !!getExitExtendModeButton();
}
function clickExitExtendMode() {
const btn = getExitExtendModeButton();
if (btn && !btn.disabled) {
btn.click();
return true;
}
return false;
}
// Real video Make video button (avoid tiny up-arrow if possible)
function getVideoMakeVideoButton() {
const btns = getMakeVideoButtonsVisible();
const nonArrow = btns.filter(b => !isUpArrowSubmitButton(b));
if (nonArrow.length) return getLargestByArea(nonArrow);
return getLargestByArea(btns);
}
// Up-arrow submit button in extend/edit
function getImageEditSubmitButton() {
const btns = getMakeVideoButtonsVisible();
const arrow = btns.find(isUpArrowSubmitButton);
if (arrow) return arrow;
const smallest = getSmallestByArea(btns);
if (smallest) return smallest;
return document.querySelector(IMAGE_SUBMIT_BUTTON_SELECTOR);
}
// --- HISTORY NAVIGATION LOGIC ---
const promptBox = document.getElementById('grok-panel-prompt');
const btnHistPrev = document.getElementById('btn-hist-prev');
const btnHistNext = document.getElementById('btn-hist-next');
const histNavCounter = document.getElementById('hist-nav-counter');
function getCombinedHistory() {
const combined = [
...videoPromptHistory.map(h => ({...h, source: 'video'})),
...imagePromptHistory.map(h => ({...h, source: 'image'})),
...editedPromptHistory.map(h => ({...h, source: 'edited'})),
];
return combined.sort((a, b) => b.timestamp - a.timestamp);
}
function updateHistoryNavButtons() {
const history = getCombinedHistory();
if (history.length === 0) {
btnHistPrev.disabled = true;
btnHistNext.disabled = true;
histNavCounter.textContent = '-';
return;
}
if (historyNavIndex === -1) {
btnHistPrev.disabled = false;
btnHistNext.disabled = false;
histNavCounter.textContent = 'current';
} else {
btnHistPrev.disabled = (historyNavIndex >= history.length - 1);
btnHistNext.disabled = false;
histNavCounter.textContent = `${historyNavIndex + 1}/${history.length}`;
}
}
function syncPromptToSite(value) {
const grokTA = getGrokInput();
const imagineP = getGrokImagine();
if (grokTA) nativeValueSet(grokTA, value);
else if (imagineP) {
imagineP.textContent = value;
if (imagineP.classList.contains('is-empty') && value) imagineP.classList.remove('is-empty');
else if (!value) imagineP.classList.add('is-empty');
}
}
function navigateHistory(direction) {
const history = getCombinedHistory();
if (history.length === 0) {
updateStatus('No history available', 'warning');
setTimeout(() => updateStatus('Ready'), 2000);
return;
}
if (direction === -1) {
if (historyNavIndex === -1) historyNavIndex = 0;
else if (historyNavIndex < history.length - 1) historyNavIndex++;
else return;
promptBox.value = history[historyNavIndex].text;
} else if (direction === 1) {
if (historyNavIndex === -1) {
promptBox.value = '';
updateStatus('Cleared', 'warning');
setTimeout(() => updateStatus('Ready'), 1500);
} else if (historyNavIndex === 0) {
historyNavIndex = -1;
promptBox.value = '';
} else {
historyNavIndex--;
promptBox.value = history[historyNavIndex].text;
}
}
updateHistoryNavButtons();
syncPromptToSite(promptBox.value);
}
btnHistPrev.addEventListener('click', () => navigateHistory(-1));
btnHistNext.addEventListener('click', () => navigateHistory(1));
promptBox.addEventListener('keydown', (e) => {
if (e.altKey && e.key === 'ArrowLeft') {
e.preventDefault();
navigateHistory(-1);
}
if (e.altKey && e.key === 'ArrowRight') {
e.preventDefault();
navigateHistory(1);
}
});
promptBox.addEventListener('input', () => {
if (document.activeElement === promptBox) {
historyNavIndex = -1;
updateHistoryNavButtons();
lastTypedPrompt = promptBox.value;
syncPromptToSite(lastTypedPrompt);
resetState("Ready");
}
});
// --- STATE MANAGEMENT ---
function addToHistory(prompt, type) {
if (!prompt || !prompt.trim()) return;
let arr;
if (type === 'image') arr = imagePromptHistory;
else if (type === 'edited') arr = editedPromptHistory;
else arr = videoPromptHistory;
const filtered = arr.filter(item => item.text !== prompt);
filtered.unshift({ id: Date.now().toString(), text: prompt, timestamp: Date.now(), type });
const limited = filtered.slice(0, MAX_HISTORY_ITEMS);
if (type === 'image') {
imagePromptHistory = limited;
GM_setValue('imagePromptHistory', imagePromptHistory);
} else if (type === 'edited') {
editedPromptHistory = limited;
GM_setValue('editedPromptHistory', editedPromptHistory);
} else {
videoPromptHistory = limited;
GM_setValue('videoPromptHistory', videoPromptHistory);
}
historyNavIndex = -1;
updateHistoryNavButtons();
}
function getFavoritesArrayByType(type) {
if (type === 'image') return imageFavorites;
if (type === 'edited') return editedFavorites;
return videoFavorites;
}
function setFavoritesArrayByType(type, newArr) {
if (type === 'image') {
imageFavorites = newArr;
GM_setValue('imageFavorites', imageFavorites);
return;
}
if (type === 'edited') {
editedFavorites = newArr;
GM_setValue('editedFavorites', editedFavorites);
return;
}
videoFavorites = newArr;
GM_setValue('videoFavorites', videoFavorites);
}
function addToFavorites(prompt, type) {
if (!prompt || !prompt.trim()) return;
const arr = getFavoritesArrayByType(type);
if (arr.some(item => item.text === prompt)) {
updateStatus(`Already in favorites!`, 'error');
setTimeout(() => updateStatus('Ready'), 2000);
return;
}
const next = [{
id: Date.now().toString(),
text: prompt,
label: prompt.length > 40 ? prompt.substring(0, 40) + '...' : prompt,
timestamp: Date.now(),
type
}, ...arr];
setFavoritesArrayByType(type, next);
updateStatus(`Added to favorites!
`);
setTimeout(() => updateStatus('Ready'), 2000);
}
// --- IMPORT / EXPORT LOGIC ---
function getAllData() {
return {
savedSnippets,
videoPromptHistory,
imagePromptHistory,
editedPromptHistory,
videoFavorites,
imageFavorites,
editedFavorites,
settings: {
maxRetries,
uiToggleKey,
autoClickEnabled,
panelSize,
mainPos, libPos, favPos, histPos
},
version: 41
};
}
function restoreData(data) {
if (!data) return alert("Invalid data");
try {
if (data.savedSnippets) GM_setValue('savedSnippets', data.savedSnippets);
if (data.videoPromptHistory) GM_setValue('videoPromptHistory', data.videoPromptHistory);
if (data.imagePromptHistory) GM_setValue('imagePromptHistory', data.imagePromptHistory);
if (data.editedPromptHistory) GM_setValue('editedPromptHistory', data.editedPromptHistory);
if (data.videoFavorites) GM_setValue('videoFavorites', data.videoFavorites);
if (data.imageFavorites) GM_setValue('imageFavorites', data.imageFavorites);
if (data.editedFavorites) GM_setValue('editedFavorites', data.editedFavorites);
if (data.settings) {
const s = data.settings;
if (s.maxRetries) GM_setValue('maxRetries', s.maxRetries);
if (s.uiToggleKey) GM_setValue('uiToggleKey', s.uiToggleKey);
if (s.autoClickEnabled !== undefined) GM_setValue('autoClickEnabled', s.autoClickEnabled);
if (s.panelSize) GM_setValue('panelSize', s.panelSize);
if (s.mainPos) GM_setValue('pos_main', s.mainPos);
if (s.libPos) GM_setValue('pos_lib', s.libPos);
if (s.favPos) GM_setValue('pos_fav', s.favPos);
if (s.histPos) GM_setValue('pos_hist', s.histPos);
}
alert("Import successful! Reloading page...");
location.reload();
} catch (e) {
console.error(e);
alert("Error importing data. Check console.");
}
}
document.getElementById('btn-export-all').addEventListener('click', () => {
const data = getAllData();
const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `grok_tools_backup_${new Date().toISOString().slice(0,10)}.json`;
a.click();
URL.revokeObjectURL(url);
updateStatus("Export complete!");
});
const fileInput = document.getElementById('grok-import-file');
document.getElementById('btn-import-all').addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
try {
const json = JSON.parse(evt.target.result);
if (confirm("This will overwrite your current saved snippets/history/favorites. Continue?")) {
restoreData(json);
}
} catch {
alert("Invalid JSON file.");
}
};
reader.readAsText(file);
fileInput.value = '';
});
// --- RENDER FUNCTIONS (Snippets/Favorites/History) ---
function renderSnippets() {
const listContainer = document.getElementById('gl-list-container');
listContainer.innerHTML = '';
savedSnippets.forEach(item => {
const el = document.createElement('div');
el.className = 'gl-item';
el.innerHTML = `
<div class="gl-item-text"><b>${escapeHtml(item.label)}</b><span>${escapeHtml(item.text)}</span></div>
<div class="gl-item-actions"><button class="gl-icon-btn gl-btn-edit">✎</button><button class="gl-icon-btn gl-btn-del"></button></div>
`;
el.querySelector('.gl-item-text').addEventListener('click', () => {
const cur = promptBox.value;
const newText = cur + (cur && !cur.endsWith(' ') ? ' ' : '') + item.text;
promptBox.value = newText;
lastTypedPrompt = newText;
historyNavIndex = -1;
updateHistoryNavButtons();
syncPromptToSite(newText);
modal.classList.remove('active');
});
el.querySelector('.gl-btn-edit').addEventListener('click', (e) => { e.stopPropagation(); showEditor(item); });
el.querySelector('.gl-btn-del').addEventListener('click', (e) => {
e.stopPropagation();
if (confirm(`Delete "${item.label}"?`)) {
savedSnippets = savedSnippets.filter(s => s.id !== item.id);
GM_setValue('savedSnippets', savedSnippets);
renderSnippets();
}
});
listContainer.appendChild(el);
});
}
const editLabel = document.getElementById('gl-edit-label');
const editText = document.getElementById('gl-edit-text');
let editingSnippetId = null;
function showEditor(item = null) {
document.getElementById('gl-view-list').style.display = 'none';
document.getElementById('gl-view-editor').classList.add('active');
editingSnippetId = item ? item.id : null;
editLabel.value = item ? item.label : '';
editText.value = item ? item.text : '';
editText.focus();
}
document.getElementById('btn-create-snippet').addEventListener('click', () => showEditor(null));
document.getElementById('btn-edit-cancel').addEventListener('click', () => {
document.getElementById('gl-view-editor').classList.remove('active');
document.getElementById('gl-view-list').style.display = 'flex';
});
document.getElementById('btn-edit-save').addEventListener('click', () => {
const label = editLabel.value.trim() || 'Untitled';
const text = editText.value.trim();
if (!text) return alert("Empty text");
if (editingSnippetId) {
const idx = savedSnippets.findIndex(s => s.id === editingSnippetId);
if (idx > -1) { savedSnippets[idx].label = label; savedSnippets[idx].text = text; }
} else {
savedSnippets.push({ id: Date.now().toString(), label, text });
}
GM_setValue('savedSnippets', savedSnippets);
document.getElementById('btn-edit-cancel').click();
renderSnippets();
});
// Favorites
function getFavoritesArrayByTab(tab) {
if (tab === 'image') return imageFavorites;
if (tab === 'edited') return editedFavorites;
return videoFavorites;
}
function renderFavorites() {
const listContainer = document.getElementById('favorites-list-container');
listContainer.innerHTML = '';
const favArray = getFavoritesArrayByTab(currentFavoritesTab);
if (favArray.length === 0) {
listContainer.innerHTML = `<div style="text-align:center; padding:20px; color:#555;">No favorites</div>`;
return;
}
favArray.forEach(item => {
const el = document.createElement('div');
el.className = 'gl-item';
el.innerHTML = `
<div class="gl-item-text">
<b>${escapeHtml(item.label)}</b>
<span>${escapeHtml(item.text)}</span>
<div class="history-item-time">${formatTimestamp(item.timestamp)}</div>
</div>
<div class="gl-item-actions">
<button class="gl-icon-btn fav-btn-edit">✎</button>
<button class="gl-icon-btn fav-btn-del"></button>
</div>
`;
el.querySelector('.gl-item-text').addEventListener('click', () => {
promptBox.value = item.text;
lastTypedPrompt = item.text;
historyNavIndex = -1;
updateHistoryNavButtons();
syncPromptToSite(item.text);
favoritesModal.classList.remove('active');
});
el.querySelector('.fav-btn-edit').addEventListener('click', (e) => { e.stopPropagation(); editFavorite(item); });
el.querySelector('.fav-btn-del').addEventListener('click', (e) => {
e.stopPropagation();
if (!confirm(`Remove favorite?`)) return;
let arr = getFavoritesArrayByTab(currentFavoritesTab).filter(h => h.id !== item.id);
if (currentFavoritesTab === 'video') setFavoritesArrayByType('video', arr);
else if (currentFavoritesTab === 'image') setFavoritesArrayByType('image', arr);
else setFavoritesArrayByType('edited', arr);
renderFavorites();
});
listContainer.appendChild(el);
});
}
function editFavorite(item) {
document.getElementById('favorites-view-list').style.display = 'none';
document.getElementById('favorites-view-viewer').classList.add('active');
document.getElementById('fav-edit-label').value = item.label || item.text.substring(0,20);
document.getElementById('favorites-viewer-text').value = item.text;
document.getElementById('favorites-viewer-time').textContent = formatTimestamp(item.timestamp);
currentEditingFavId = item.id;
}
document.getElementById('btn-fav-viewer-back').addEventListener('click', () => {
document.getElementById('favorites-view-viewer').classList.remove('active');
document.getElementById('favorites-view-list').style.display = 'flex';
});
document.getElementById('btn-fav-viewer-save').addEventListener('click', () => {
const newLabel = document.getElementById('fav-edit-label').value.trim() || "Untitled";
const newText = document.getElementById('favorites-viewer-text').value.trim();
if (!newText || !currentEditingFavId) return;
const favArray = getFavoritesArrayByTab(currentFavoritesTab);
const idx = favArray.findIndex(f => f.id === currentEditingFavId);
if (idx !== -1) {
favArray[idx].label = newLabel;
favArray[idx].text = newText;
// Save back
if (currentFavoritesTab === 'video') setFavoritesArrayByType('video', favArray);
else if (currentFavoritesTab === 'image') setFavoritesArrayByType('image', favArray);
else setFavoritesArrayByType('edited', favArray);
renderFavorites();
document.getElementById('btn-fav-viewer-back').click();
}
});
favoritesModal.querySelectorAll('.history-tab').forEach(tab => {
tab.addEventListener('click', () => {
favoritesModal.querySelectorAll('.history-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentFavoritesTab = tab.dataset.tab;
renderFavorites();
});
});
// History
function getHistoryArrayByTab(tab) {
if (tab === 'image') return imagePromptHistory;
if (tab === 'edited') return editedPromptHistory;
return videoPromptHistory;
}
function saveHistoryArrayByTab(tab, arr) {
if (tab === 'image') {
imagePromptHistory = arr;
GM_setValue('imagePromptHistory', imagePromptHistory);
return;
}
if (tab === 'edited') {
editedPromptHistory = arr;
GM_setValue('editedPromptHistory', editedPromptHistory);
return;
}
videoPromptHistory = arr;
GM_setValue('videoPromptHistory', videoPromptHistory);
}
function renderHistory() {
const listContainer = document.getElementById('history-list-container');
listContainer.innerHTML = '';
const historyArray = getHistoryArrayByTab(currentHistoryTab);
if (historyArray.length === 0) {
listContainer.innerHTML = `<div style="text-align:center; padding:20px; color:#555;">No history</div>`;
return;
}
const favArray =
currentHistoryTab === 'video' ? videoFavorites :
currentHistoryTab === 'image' ? imageFavorites :
editedFavorites;
historyArray.forEach(item => {
const el = document.createElement('div');
el.className = 'gl-item';
const isFavorited = favArray.some(f => f.text === item.text);
el.innerHTML = `
<div class="gl-item-text">
<span>${escapeHtml(item.text)}</span>
<div class="history-item-time">${formatTimestamp(item.timestamp)}</div>
</div>
<div class="gl-item-actions">
<button class="gl-icon-btn history-btn-fav ${isFavorited ? 'favorite' : ''}">
</button>
<button class="gl-icon-btn history-btn-view"></button>
<button class="gl-icon-btn history-btn-del"></button>
</div>
`;
el.querySelector('.gl-item-text').addEventListener('click', () => {
promptBox.value = item.text;
lastTypedPrompt = item.text;
historyNavIndex = -1;
updateHistoryNavButtons();
syncPromptToSite(item.text);
historyModal.classList.remove('active');
});
el.querySelector('.history-btn-fav').addEventListener('click', (e) => {
e.stopPropagation();
addToFavorites(item.text, item.type);
renderHistory();
});
el.querySelector('.history-btn-view').addEventListener('click', (e) => {
e.stopPropagation();
document.getElementById('history-view-list').style.display = 'none';
document.getElementById('history-view-viewer').classList.add('active');
document.getElementById('history-viewer-text').value = item.text;
document.getElementById('history-viewer-time').textContent = formatTimestamp(item.timestamp);
});
el.querySelector('.history-btn-del').addEventListener('click', (e) => {
e.stopPropagation();
const newArr = historyArray.filter(h => h.id !== item.id);
saveHistoryArrayByTab(currentHistoryTab, newArr);
historyNavIndex = -1;
updateHistoryNavButtons();
renderHistory();
});
listContainer.appendChild(el);
});
}
document.getElementById('btn-viewer-back').addEventListener('click', () => {
document.getElementById('history-view-viewer').classList.remove('active');
document.getElementById('history-view-list').style.display = 'flex';
});
document.getElementById('btn-viewer-use').addEventListener('click', () => {
const text = document.getElementById('history-viewer-text').value;
promptBox.value = text;
lastTypedPrompt = text;
historyNavIndex = -1;
updateHistoryNavButtons();
syncPromptToSite(text);
historyModal.classList.remove('active');
document.getElementById('btn-viewer-back').click();
});
// Create new prompt functionality
document.getElementById('btn-create-history-prompt').addEventListener('click', () => {
document.getElementById('history-view-list').style.display = 'none';
const creatorView = document.getElementById('history-view-creator');
creatorView.classList.add('active');
creatorView.style.display = 'flex';
document.getElementById('history-creator-text').value = '';
setTimeout(() => document.getElementById('history-creator-text').focus(), 100);
});
document.getElementById('btn-creator-cancel').addEventListener('click', () => {
const creatorView = document.getElementById('history-view-creator');
creatorView.classList.remove('active');
creatorView.style.display = 'none';
document.getElementById('history-view-list').style.display = 'flex';
});
document.getElementById('btn-creator-save').addEventListener('click', () => {
const text = document.getElementById('history-creator-text').value.trim();
if (!text) {
updateStatus('Prompt cannot be empty', 'error');
setTimeout(() => updateStatus('Ready'), 2000);
return;
}
addToHistory(text, currentHistoryTab);
updateStatus(`Saved to ${currentHistoryTab} history!`);
setTimeout(() => updateStatus('Ready'), 2000);
document.getElementById('btn-creator-cancel').click();
renderHistory();
});
document.getElementById('btn-clear-history').addEventListener('click', () => {
if (!confirm('Clear history for this tab?')) return;
saveHistoryArrayByTab(currentHistoryTab, []);
historyNavIndex = -1;
updateHistoryNavButtons();
renderHistory();
});
historyModal.querySelectorAll('.history-tab').forEach(tab => {
tab.addEventListener('click', () => {
historyModal.querySelectorAll('.history-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentHistoryTab = tab.dataset.tab;
renderHistory();
});
});
// --- BUTTON EVENT LISTENERS ---
document.getElementById('btn-open-library').addEventListener('click', () => {
toggleModal(modal);
if (modal.classList.contains('active')) renderSnippets();
});
document.getElementById('btn-open-favorites').addEventListener('click', () => {
toggleModal(favoritesModal);
if (favoritesModal.classList.contains('active')) renderFavorites();
});
document.getElementById('btn-open-history').addEventListener('click', () => {
toggleModal(historyModal);
if (historyModal.classList.contains('active')) renderHistory();
});
modal.querySelector('.gl-close').addEventListener('click', () => modal.classList.remove('active'));
favoritesModal.querySelector('.favorites-close').addEventListener('click', () => favoritesModal.classList.remove('active'));
historyModal.querySelector('.history-close').addEventListener('click', () => historyModal.classList.remove('active'));
// --- STRICT SYNC & CAPTURE LOGIC ---
// Website -> UI Panel sync
document.addEventListener('input', (e) => {
if (e.target.matches(TARGET_TEXTAREA_SELECTOR) ||
e.target.matches(IMAGE_EDITOR_SELECTOR) ||
e.target.matches(IMAGE_PROMPT_SELECTOR)) {
if (document.activeElement === e.target) {
promptBox.value = e.target.value;
lastTypedPrompt = e.target.value;
historyNavIndex = -1;
updateHistoryNavButtons();
}
}
}, { capture: true, passive: true });
// --- Generate Button Logic (v41: edited history + button fix) ---
let exitingExtendMode = false;
function detectContextType() {
// Video wins if the video textarea exists
if (document.querySelector(TARGET_TEXTAREA_SELECTOR)) return 'video';
// Edited if extend/edit mode is active
if (isImageEditMode()) return 'edited';
// Otherwise image if image inputs exist
if (document.querySelector(IMAGE_PROMPT_SELECTOR) || document.querySelector(IMAGE_IMAGINE_SELECTOR)) return 'image';
// Fallback
return 'video';
}
function doGenerateNow() {
const type = detectContextType();
const vidEl = document.querySelector(TARGET_TEXTAREA_SELECTOR);
const imgEl = document.querySelector(IMAGE_PROMPT_SELECTOR);
const imgIm = document.querySelector(IMAGE_IMAGINE_SELECTOR);
const grokTA = vidEl || document.querySelector(IMAGE_EDITOR_SELECTOR) || imgEl;
const imagineP = imgIm;
// If trying VIDEO but extend/edit is active and we only see the up-arrow submit,
// exit extend mode first, then try again.
if (type === 'video' && isImageEditMode()) {
const candidate = getVideoMakeVideoButton();
if (!candidate || isUpArrowSubmitButton(candidate)) {
if (!exitingExtendMode && clickExitExtendMode()) {
exitingExtendMode = true;
updateStatus('Exiting extend mode...', 'warning');
setTimeout(() => {
exitingExtendMode = false;
doGenerateNow();
}, 250);
return;
}
}
}
let realBtn = null;
if (type === 'video') {
// Prefer any "Edit" video button if present (keeps your old logic)
const allVideoBtns = getMakeVideoButtonsVisible();
const editBtn = allVideoBtns.find(b => (b.textContent || '').includes('Edit'));
realBtn = editBtn || getVideoMakeVideoButton();
} else if (type === 'edited') {
// Extend/edit submit is the up-arrow "Make video"
realBtn = getImageEditSubmitButton();
} else {
// Normal image generation
realBtn = document.querySelector(IMAGE_EDITOR_BUTTON_SELECTOR) ||
document.querySelector(IMAGE_SUBMIT_BUTTON_SELECTOR);
}
if (!realBtn) return updateStatus("Button not found", "error");
const promptVal = promptBox.value.trim();
if (promptVal) addToHistory(promptVal, type);
// Force sync now
if (grokTA) nativeValueSet(grokTA, promptBox.value);
else if (imagineP) imagineP.textContent = promptBox.value;
setTimeout(() => {
if (!realBtn.disabled) {
realBtn.click();
lastGenerationTimestamp = Date.now();
updateStatus(type === 'video' ? 'Generation Started...' : 'Submitted...');
} else {
updateStatus("Grok button disabled/processing.", "error");
}
}, 50);
}
document.getElementById('btn-generate').addEventListener('click', doGenerateNow);
// Capture prompts when user clicks submit buttons outside the panel
document.addEventListener('mousedown', (e) => {
const submitBtn = e.target.closest('button[aria-label="Submit"]');
const makeVideoBtn = e.target.closest('button[aria-label="Make video"]');
const val = (promptBox.value.trim() || lastTypedPrompt.trim());
if (val.length <= 2) return;
// Normal image submit
if (submitBtn) {
addToHistory(val, isImageEditMode() ? 'edited' : 'image');
updateStatus("Prompt captured!");
return;
}
// Extend/edit up-arrow submit (aria-label="Make video")
if (makeVideoBtn && isImageEditMode() && isUpArrowSubmitButton(makeVideoBtn)) {
addToHistory(val, 'edited');
updateStatus("Edited prompt captured!");
}
}, true);
// --- MODERATION & RETRY LOGIC ---
function checkForModerationContent() {
const now = Date.now();
if (now - lastModerationCheck < 200) return null;
lastModerationCheck = now;
const toastSelectors = ['section[aria-label*="Notification"]', '[role="alert"]', '.toast'];
for (const sel of toastSelectors) {
const els = document.querySelectorAll(sel);
for (const el of els) {
const txt = (el.textContent || '').toLowerCase();
if (MODERATION_PATTERNS.some(p => txt.includes(p))) return { element: el, text: txt };
}
}
return null;
}
function waitForErrorDisappearance(element) {
if (errorWaitInterval) clearInterval(errorWaitInterval);
let safetyCounter = 0;
const POLL_MS = 500;
const MAX_WAIT_MS = 10000;
errorWaitInterval = setInterval(() => {
safetyCounter += POLL_MS;
const isConnected = document.body.contains(element);
let isVisible = false;
if (isConnected) {
const style = window.getComputedStyle(element);
isVisible = style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
}
if (!isConnected || !isVisible || safetyCounter >= MAX_WAIT_MS) {
clearInterval(errorWaitInterval);
errorWaitInterval = null;
updateStatus('Message cleared. Retrying...');
handleRetry(Date.now());
}
}, POLL_MS);
}
function handleRetry(now) {
if (!isRetryEnabled || limitReached) return;
if (now - lastGenerationTimestamp < GENERATION_COOLDOWN_MS) return;
const grokTA = getGrokInput();
const imagineP = getGrokImagine();
let btn = null;
const isVideoContext = !!document.querySelector(TARGET_TEXTAREA_SELECTOR);
if (isVideoContext) {
btn = getVideoMakeVideoButton();
if (isImageEditMode() && (!btn || isUpArrowSubmitButton(btn))) {
if (clickExitExtendMode()) updateStatus('Exiting extend mode...', 'warning');
return;
}
} else {
if (isImageEditMode()) btn = getImageEditSubmitButton();
else btn = document.querySelector(IMAGE_EDITOR_BUTTON_SELECTOR) || document.querySelector(IMAGE_SUBMIT_BUTTON_SELECTOR);
}
if ((grokTA || imagineP) && lastTypedPrompt) {
if (grokTA) nativeValueSet(grokTA, lastTypedPrompt);
else if (imagineP) imagineP.textContent = lastTypedPrompt;
if (autoClickEnabled && currentRetryCount >= maxRetries) {
updateStatus(`Limit Reached (${maxRetries})`, "error");
limitReached = true;
return;
}
if (autoClickEnabled && btn) {
currentRetryCount++;
updateStatus(`Retrying (${currentRetryCount}/${maxRetries})...`, 'warning');
setTimeout(() => {
if (!btn.disabled) {
btn.click();
lastGenerationTimestamp = Date.now();
}
processingModeration = false;
moderationDetected = false;
}, RETRY_DELAY_MS);
}
}
}
const observer = new MutationObserver(() => {
if (observerThrottle || !isRetryEnabled || limitReached) return;
observerThrottle = true;
setTimeout(() => { observerThrottle = false; }, OBSERVER_THROTTLE_MS);
if (!processingModeration) {
const mod = checkForModerationContent();
if (mod) {
debugLog('Moderation detected:', mod.text);
processingModeration = true;
moderationDetected = true;
updateStatus(`Moderation detected! Waiting...`, 'warning');
waitForErrorDisappearance(mod.element);
}
}
});
observer.observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true });
// --- TOGGLES & SETTINGS ---
const toggleBtn = document.getElementById('grok-toggle-btn');
toggleBtn.addEventListener('click', () => {
isRetryEnabled = !isRetryEnabled;
toggleBtn.textContent = isRetryEnabled ? "ON" : "OFF";
toggleBtn.classList.toggle('off', !isRetryEnabled);
resetState(isRetryEnabled ? "Ready" : "Disabled");
if (!isRetryEnabled) updateStatus("Disabled", "error");
});
document.getElementById('grok-autoclick-cb').addEventListener('change', (e) => {
autoClickEnabled = e.target.checked;
GM_setValue('autoClickEnabled', autoClickEnabled);
});
document.getElementById('grok-retry-limit').addEventListener('change', (e) => {
maxRetries = parseInt(e.target.value);
GM_setValue('maxRetries', maxRetries);
});
// Keybinds
document.addEventListener('keydown', (e) => {
// Toggle Main UI
if (e.altKey && e.key.toLowerCase() === uiToggleKey) {
isUiVisible = !isUiVisible;
GM_setValue('isUiVisible', isUiVisible);
panel.classList.toggle('hidden', !isUiVisible);
e.preventDefault();
}
// Toggle Import/Export Buttons
if (e.altKey && e.key.toLowerCase() === 'i') {
const ioRow = document.getElementById('grok-io-container');
if (ioRow) {
ioRow.classList.toggle('io-hidden');
e.preventDefault();
}
}
});
window.addEventListener('beforeunload', () => {
observer.disconnect();
if (errorWaitInterval) clearInterval(errorWaitInterval);
});
// Initialize navigation buttons
updateHistoryNavButtons();
debugLog('Grok Tools v41 Initialized - Edited History + Edited Favorites + Button Fix');
})();