// ==UserScript== // @name Enhanced Grok Export v2.4 // @description Export Grok conversations with improved detection, share to X integration and more // @version 2.4.0 // @author iikoshteruu // @grant none // @match *://grok.com/* // @match *://x.com/* // @license MIT // @namespace https://github.com/iikoshteruu/enhanced-grok-export // @homepageURL https://github.com/iikoshteruu/enhanced-grok-export // @supportURL https://github.com/iikoshteruu/enhanced-grok-export/issues // @downloadURL none // ==/UserScript== (function() { 'use strict'; console.log('Enhanced Grok Export v2.2 starting...'); // Configuration const CONFIG = { buttonText: 'Export Full', formats: ['txt', 'md', 'json', 'pdf'], defaultFormat: 'md', debug: true, autoScroll: true, scrollDelay: 1000, maxScrollAttempts: 50, shareToX: { enabled: true, maxLength: 280, hashtagSuggestions: ['#Grok', '#AI', '#XAI'] } }; let isExporting = false; function debugLog(message, data = null) { if (CONFIG.debug) { console.log('[Grok Export v2.2]', message, data || ''); } } // Auto-scroll to load all conversation content async function loadFullConversation() { debugLog('Starting full conversation loading...'); return new Promise((resolve) => { let scrollAttempts = 0; let lastScrollHeight = 0; let unchangedCount = 0; const scrollInterval = setInterval(() => { window.scrollTo(0, 0); const currentScrollHeight = document.body.scrollHeight; debugLog(`Scroll attempt ${scrollAttempts + 1}, Height: ${currentScrollHeight}`); if (currentScrollHeight === lastScrollHeight) { unchangedCount++; } else { unchangedCount = 0; lastScrollHeight = currentScrollHeight; } scrollAttempts++; if (scrollAttempts >= CONFIG.maxScrollAttempts || unchangedCount >= 3) { clearInterval(scrollInterval); debugLog(`Scroll complete. Total attempts: ${scrollAttempts}`); setTimeout(() => { window.scrollTo(0, 0); setTimeout(() => { window.scrollTo(0, document.body.scrollHeight); setTimeout(() => { resolve(); }, 1000); }, 500); }, 500); } }, CONFIG.scrollDelay); }); } // Enhanced conversation detection for Grok function getConversationData() { debugLog('Starting Grok conversation data extraction...'); const messages = []; const strategies = [ () => { const messageContainers = document.querySelectorAll('div[class*="css-146c3p1"]'); debugLog(`Found ${messageContainers.length} containers with css-146c3p1`); return Array.from(messageContainers); }, () => { const contentSpans = document.querySelectorAll('span[class*="css-1jxf684"]'); debugLog(`Found ${contentSpans.length} content spans with css-1jxf684`); return Array.from(contentSpans).map(span => { let parent = span.parentElement; while (parent && !parent.classList.toString().includes('css-146c3p1')) { parent = parent.parentElement; } return parent; }).filter(Boolean); }, () => { const ltrDivs = document.querySelectorAll('div[dir="ltr"]'); debugLog(`Found ${ltrDivs.length} divs with dir='ltr'`); return Array.from(ltrDivs).filter(div => { const text = div.textContent?.trim() || ''; return text.length > 10 && text.length < 50000; }); } ]; let messageElements = []; for (let i = 0; i < strategies.length; i++) { try { messageElements = strategies[i](); debugLog(`Strategy ${i + 1} found ${messageElements.length} elements`); if (messageElements.length > 0) { messageElements = messageElements.filter(el => { const text = el.textContent?.trim() || ''; return text.length > 10 && text.length < 50000; }); if (messageElements.length > 0) { debugLog(`Using strategy ${i + 1} with ${messageElements.length} valid elements`); break; } } } catch (error) { debugLog(`Strategy ${i + 1} failed:`, error.message); } } debugLog(`Processing ${messageElements.length} message elements...`); const processedTexts = new Set(); messageElements.forEach((element, index) => { try { const clone = element.cloneNode(true); const unwanted = clone.querySelectorAll( 'svg, button, input, select, nav, header, footer, script, style, ' + '[aria-hidden="true"], [class*="icon"], [class*="button"]' ); unwanted.forEach(el => el.remove()); const text = clone.textContent?.trim() || ''; if (text && text.length > 10 && !processedTexts.has(text)) { processedTexts.add(text); const speakerInfo = detectGrokSpeakerAdvanced(element, text, index, messageElements); messages.push({ id: `msg_${index}`, speaker: speakerInfo.speaker, content: text, mode: speakerInfo.mode, timestamp: new Date().toISOString(), index: index, length: text.length, element: element, debugInfo: speakerInfo.debugInfo }); debugLog(`Message ${index + 1}: ${speakerInfo.speaker} [${speakerInfo.mode}] (${text.length} chars)`, speakerInfo.debugInfo); } } catch (error) { debugLog(`Error processing element ${index}:`, error.message); } }); messages.sort((a, b) => a.index - b.index); debugLog(`Extracted ${messages.length} unique messages`); return messages; } // REVISED: Speaker detection using position and content analysis function detectGrokSpeakerAdvanced(element, text, index, allElements) { let debugInfo = { scores: {}, reasoning: [] }; // Mode detection for Grok let mode = 'standard'; if (text.includes('š¤') || text.includes('Let me think') || text.includes('Step ') || text.includes('First,') || text.includes('Then,') || text.includes('Finally,')) { mode = 'think'; } else if (text.includes('š') || text.includes('š') || text.includes('LOL') || text.includes('haha') || text.includes('funny')) { mode = 'fun'; } else if (text.includes('According to') || text.includes('Based on recent') || text.includes('Source:') || text.includes('https://')) { mode = 'deepsearch'; } let grokScore = 0; let humanScore = 0; // 1. MESSAGE LENGTH ANALYSIS (Most reliable indicator) if (text.length > 400) { grokScore += 4; debugInfo.reasoning.push(`Very long message (${text.length} chars, likely GROK)`); } else if (text.length > 200) { grokScore += 2; debugInfo.reasoning.push(`Long message (${text.length} chars, likely GROK)`); } else if (text.length < 50) { humanScore += 2; debugInfo.reasoning.push(`Short message (${text.length} chars, likely HUMAN)`); } // 2. ENHANCED CONTENT PATTERN ANALYSIS const grokIndicators = [ { pattern: /^(I'll|I can|I'd be happy|Here's|Let me|I understand|Certainly|Absolutely|Looking at)/i, score: 3, name: 'Grok response starter' }, { pattern: /^(Yo, I'm right here|Hey there|What's up|Oof|Thanks for sharing)/i, score: 4, name: 'Grok casual phrases' }, { pattern: /^(From your|Based on your|Looking at your|The error|This means|Why It's Happening)/i, score: 4, name: 'Grok analysis starters' }, { pattern: /```/, score: 3, name: 'Code block' }, { pattern: /(docker|container|build|error|issue|problem|fix|solution)/i, score: 2, name: 'Technical terms' }, { pattern: /^(Based on|According to|The analysis|This approach|In summary|Overview)/i, score: 2, name: 'Analytical language' }, { pattern: /(implementation|algorithm|analysis|explanation|methodology|digital realm)/i, score: 1, name: 'Technical/AI terms' }, { pattern: /\n\n/, score: 1, name: 'Structured paragraphs' }, { pattern: /(fully alive|kicking in the digital realm|locked in|squash|tackle this)/i, score: 4, name: 'Grok personality phrases' }, { pattern: /^(Let's|Why|Steps to|Here's how)/i, score: 3, name: 'Instructional language' }, { pattern: /(requirements\.txt|Dockerfile|app\.py|netstat|TrueNAS)/i, score: 2, name: 'Project-specific terms' } ]; const humanIndicators = [ { pattern: /^(hi|hello|hey|can you|could you|please|help|i need|i want)/i, score: 3, name: 'Human greeting/request' }, { pattern: /^(grok|are you|do you remember)/i, score: 5, name: 'Addressing Grok directly' }, { pattern: /\?$/, score: 3, name: 'Ends with question' }, { pattern: /^(ok|okay|thanks|thank you|great|perfect|yes|no|good|nice)/i, score: 2, name: 'Acknowledgment' }, { pattern: /^(let's|lets|now|next|alright|ready|this site can't be reached)/i, score: 2, name: 'Directive/status language' }, { pattern: /\b(you|your)\b/i, score: 1, name: 'Addressing someone' }, { pattern: /^(root@truenas|trying|nano)/i, score: 4, name: 'User commands/actions' } ]; grokIndicators.forEach(({ pattern, score, name }) => { if (pattern.test(text)) { grokScore += score; debugInfo.reasoning.push(`${name} (+${score} GROK)`); } }); humanIndicators.forEach(({ pattern, score, name }) => { if (pattern.test(text)) { humanScore += score; debugInfo.reasoning.push(`${name} (+${score} HUMAN)`); } }); // 3. CONVERSATIONAL CONTEXT ANALYSIS if (index > 0 && allElements[index - 1]) { const prevText = allElements[index - 1].textContent?.trim() || ''; // If previous message was a question and this is a long answer if (prevText.includes('?') && prevText.length < 200 && text.length > 150) { grokScore += 3; debugInfo.reasoning.push('Long response to question (likely GROK)'); } // If this is a short follow-up to a long message if (prevText.length > 300 && text.length < 100) { humanScore += 2; debugInfo.reasoning.push('Short follow-up to long message (likely HUMAN)'); } } // 4. QUESTION vs STATEMENT ANALYSIS const questionCount = (text.match(/\?/g) || []).length; if (questionCount > 0 && text.length < 150) { humanScore += questionCount * 2; debugInfo.reasoning.push(`Contains ${questionCount} questions (likely HUMAN)`); } // 5. CONVERSATION POSITION ANALYSIS const messagesSoFar = index + 1; // First message is typically human if (index === 0) { humanScore += 2; debugInfo.reasoning.push('First message (likely HUMAN)'); } // Look at nearby message lengths for pattern const nearbyLengths = []; for (let i = Math.max(0, index - 2); i <= Math.min(allElements.length - 1, index + 2); i++) { if (i !== index && allElements[i]) { nearbyLengths.push(allElements[i].textContent?.length || 0); } } const avgNearbyLength = nearbyLengths.length > 0 ? nearbyLengths.reduce((sum, len) => sum + len, 0) / nearbyLengths.length : 0; if (text.length > avgNearbyLength * 2 && text.length > 200) { grokScore += 2; debugInfo.reasoning.push('Much longer than nearby messages (likely GROK)'); } // 6. MODE BONUS if (mode !== 'standard') { grokScore += 3; debugInfo.reasoning.push(`${mode} mode detected (likely GROK)`); } // Store scores for debugging debugInfo.scores = { grokScore, humanScore }; // FINAL DECISION with balanced thresholds let speaker; if (grokScore >= humanScore + 2) { // Require clear Grok advantage speaker = 'Grok'; } else if (humanScore >= grokScore + 1) { // Easier for human detection speaker = 'Human'; } else { // BALANCED FALLBACK based on conversation patterns // Strong human indicators if (text.startsWith('root@') || text.includes('nano ') || text.includes('ls -l') || text.includes('docker run') || text.includes('docker build') || text.length < 25) { speaker = 'Human'; debugInfo.reasoning.push('Fallback: Clear user command/input assumed HUMAN'); } // Clear questions else if (text.includes('?') && text.length < 100) { speaker = 'Human'; debugInfo.reasoning.push('Fallback: Short question assumed HUMAN'); } // Long technical explanations else if (text.length > 300 && (text.includes('Fix:') || text.includes('Issue:') || text.includes('Solution:'))) { speaker = 'Grok'; debugInfo.reasoning.push('Fallback: Long technical explanation assumed GROK'); } // Medium length with technical terms else if (text.length > 150 && (text.includes('docker') || text.includes('container') || text.includes('error'))) { speaker = 'Grok'; debugInfo.reasoning.push('Fallback: Medium technical content assumed GROK'); } // Short technical references or status updates else if (text.length < 100 && !text.includes('Here\'s') && !text.includes('Let\'s')) { speaker = 'Human'; debugInfo.reasoning.push('Fallback: Short non-explanatory content assumed HUMAN'); } // Final alternating fallback else { speaker = index % 2 === 0 ? 'Human' : 'Grok'; debugInfo.reasoning.push(`Final fallback: Alternating pattern (${speaker})`); } } debugInfo.finalDecision = `${speaker} (GROK: ${grokScore}, HUMAN: ${humanScore})`; return { speaker, mode, debugInfo }; } // FIXED PDF GENERATION - No external dependencies function formatAsPDF(messages) { try { debugLog('Generating PDF document...'); // Create a rich text document formatted like a PDF let content = ''; // PDF-style header content += 'ā'.repeat(80) + '\n'; content += ' GROK CONVERSATION EXPORT\n'; content += 'ā'.repeat(80) + '\n\n'; // Document metadata content += `EXPORT INFORMATION:\n`; content += `${'ā'.repeat(40)}\n`; content += `Generated: ${new Date().toLocaleString()}\n`; content += `Total Messages: ${messages.length}\n`; content += `Source URL: ${window.location.href}\n`; content += `Export Version: Enhanced Grok Export v2.2\n\n`; // Statistics section const stats = { humanMessages: messages.filter(m => m.speaker === 'Human').length, grokMessages: messages.filter(m => m.speaker === 'Grok').length, thinkMode: messages.filter(m => m.mode === 'think').length, funMode: messages.filter(m => m.mode === 'fun').length, deepSearchMode: messages.filter(m => m.mode === 'deepsearch').length, totalChars: messages.reduce((sum, m) => sum + m.content.length, 0), avgLength: Math.round(messages.reduce((sum, m) => sum + m.content.length, 0) / messages.length) }; content += `CONVERSATION STATISTICS:\n`; content += `${'ā'.repeat(40)}\n`; content += `āāāāāāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāā\n`; content += `ā Human Messages ā ${stats.humanMessages.toString().padStart(7)} ā\n`; content += `ā Grok Messages ā ${stats.grokMessages.toString().padStart(7)} ā\n`; content += `āāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāā¤\n`; content += `ā Think Mode ā ${stats.thinkMode.toString().padStart(7)} ā\n`; content += `ā Fun Mode ā ${stats.funMode.toString().padStart(7)} ā\n`; content += `ā DeepSearch Mode ā ${stats.deepSearchMode.toString().padStart(7)} ā\n`; content += `āāāāāāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāā¤\n`; content += `ā Total Characters ā ${stats.totalChars.toString().padStart(7)} ā\n`; content += `ā Average Message Length ā ${stats.avgLength.toString().padStart(7)} ā\n`; content += `āāāāāāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāā\n\n`; content += 'ā'.repeat(80) + '\n'; content += ' CONVERSATION CONTENT\n'; content += 'ā'.repeat(80) + '\n\n'; // Message content messages.forEach((msg, index) => { const modeIndicator = msg.mode !== 'standard' ? ` [${msg.mode.toUpperCase()}]` : ''; // Message header content += `${index + 1}. ${msg.speaker}${modeIndicator}\n`; content += `${'ā'.repeat(Math.max(20, msg.speaker.length + modeIndicator.length + 4))}\n`; // Message content with proper wrapping const wrappedContent = wrapText(msg.content, 76); content += wrappedContent + '\n\n'; // Visual separator between messages if (index < messages.length - 1) { content += 'āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ āŖ\n\n'; } }); // Document footer content += '\n' + 'ā'.repeat(80) + '\n'; content += ` End of Document - ${messages.length} Messages\n`; content += 'ā'.repeat(80) + '\n'; return new Blob([content], { type: 'text/plain;charset=utf-8' }); } catch (error) { debugLog('PDF generation failed:', error); throw new Error(`PDF generation failed: ${error.message}`); } } // Helper function for text wrapping function wrapText(text, maxWidth) { const words = text.split(' '); const lines = []; let currentLine = ''; words.forEach(word => { if ((currentLine + word).length <= maxWidth) { currentLine += (currentLine ? ' ' : '') + word; } else { if (currentLine) lines.push(currentLine); currentLine = word; } }); if (currentLine) lines.push(currentLine); return lines.join('\n'); } // Create Share to X modal function createShareModal(messages) { const modal = document.createElement('div'); modal.id = 'grok-share-modal'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; display: flex; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; `; const content = document.createElement('div'); content.style.cssText = ` background: white; border-radius: 16px; padding: 24px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 20px 40px rgba(0,0,0,0.3); `; content.innerHTML = `