Chrome extension that lets you add keywords and annotations to your Grok convos

Started by ww408, March 12, 2025, 01:29:01 PM

Previous topic - Next topic

ww408

The conversations with these AI chat bots accumulate so much text and there isn't a built-in way to oragnize it all, not even anchor links for each message. My initial vision for this was fancy, including anchor links for each message, and the like, but Grok failed to accomplish that, unsurprisingly, so I simply asked it to make me an extension that would allow me to edit the web page, as if I was in a document editor. So, you can add, remove, resize, bold, italicize, or underline any text, including the bot's, and you can also highlight text. These changes revert on refresh, but Save Page WE* can save the edited version (and it's also good for saving chat bot convos, in general, and it's also good for bloating the pages). This extension was made by Grok.

*Explanation of how it works: https://gnuzilla.gnu.org/extension.php?id=724283


This script creates a directory that contains the necessary components of the extension, which then need to be loaded into Chrome/Chromium using the Load unpacked button in the Developer mode version of Extensions. Google or ask AI for instructions on side-loading extensions if you need help.

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

// Directory management
const dirName = 'y';
const dirPath = path.join(process.cwd(), dirName);

if (fs.existsSync(dirPath)) fs.rmSync(dirPath, { recursive: true, force: true });
fs.mkdirSync(dirPath);

// File contents
const files = {
  'manifest.json': JSON.stringify({
    manifest_version: 3,
    name: "Grok Text Editor",
    version: "1.9",
    description: "Edit, highlight, erase, and format Grok chat text",
    permissions: ["activeTab"],
    content_scripts: [{
      matches: ["https://grok.com/*"],
      js: ["content.js"],
      css: ["styles.css"]
    }]
  }, null, 2),

  'content.js': `
    function init() {
      // Inject edit toggle button
      const editBtn = document.createElement('button');
      editBtn.id = 'edit-toggle';
      editBtn.textContent = '📝';
      editBtn.title = 'Toggle Edit Mode (Double-click to keep others)';
      document.body.appendChild(editBtn);

      // Inject highlight toggle button
      const highlightBtn = document.createElement('button');
      highlightBtn.id = 'highlight-toggle';
      highlightBtn.textContent = '✎';
      highlightBtn.title = 'Toggle Highlight Mode (Double-click to keep others)';
      document.body.appendChild(highlightBtn);

      // Inject erase toggle button
      const eraseBtn = document.createElement('button');
      eraseBtn.id = 'erase-toggle';
      eraseBtn.textContent = '🧽';
      eraseBtn.title = 'Toggle Erase Mode (Double-click clears all)';
      document.body.appendChild(eraseBtn);

      // Inject font palette
      const palette = document.createElement('div');
      palette.id = 'font-palette';
      palette.innerHTML = \`
        <button id="bold-btn">𝐁</button>
        <button id="italic-btn">𝐈</button>
        <button id="underline-btn">𝑈</button>
        <input id="color-input" type="text" placeholder="#hex" maxlength="7" size="7">
        <input id="size-input" type="number" min="8" max="100" value="16" size="4">
        <button id="toggle-format">⟳</button>
        <button id="cursor-toggle">|</button>
        <button id="deselect-btn">✗</button>
      \`;
      document.body.appendChild(palette);

      let isEditable = false;
      let isHighlighting = false;
      let isErasing = false;
      let isTypingCursor = false;
      let isCustomFormat = true;
      let defaultStyles = {};
      let hideTimeout = null;
      let lastRange = null;

      editBtn.style.backgroundColor = isEditable ? '#d0ffd0' : '#f0f0f0';
      highlightBtn.style.backgroundColor = isHighlighting ? '#d0ffd0' : '#f0f0f0';
      eraseBtn.style.backgroundColor = isErasing ? '#d0ffd0' : '#f0f0f0';
      document.getElementById('cursor-toggle').style.backgroundColor = isTypingCursor ? '#d0ffd0' : '#f0f0f0';

      // Palette visibility control
      palette.addEventListener('mouseenter', () => {
        clearTimeout(hideTimeout);
        palette.style.opacity = '1';
      });

      palette.addEventListener('mouseleave', () => {
        hideTimeout = setTimeout(() => {
          palette.style.opacity = '0';
        }, 1350); // 1.35 seconds
      });

      // Store selection range on mouseup
      document.addEventListener('mouseup', (e) => {
        if (isHighlighting && e.target.closest('.message-bubble')) {
          const selection = window.getSelection();
          if (selection.rangeCount) {
            const range = selection.getRangeAt(0);
            const selectedText = selection.toString().trim();
            if (selectedText && !range.commonAncestorContainer.parentNode.className.includes('highlight')) {
              const span = document.createElement('span');
              span.className = 'highlight';
              span.textContent = selectedText;
              range.deleteContents();
              range.insertNode(span);
            }
          }
        }
        const selection = window.getSelection();
        if (selection.rangeCount) {
          lastRange = selection.getRangeAt(0);
          console.log('Selection stored:', lastRange.toString());
        }
      });

      // Toggle edit mode
      editBtn.addEventListener('click', () => {
        isEditable = !isEditable;
        editBtn.style.backgroundColor = isEditable ? '#d0ffd0' : '#f0f0f0';
        if (isEditable) {
          isHighlighting = false;
          highlightBtn.style.backgroundColor = '#f0f0f0';
          isErasing = false;
          eraseBtn.style.backgroundColor = '#f0f0f0';
        }
        updateEditState();
      });

      editBtn.addEventListener('dblclick', () => {
        isEditable = !isEditable;
        editBtn.style.backgroundColor = isEditable ? '#d0ffd0' : '#f0f0f0';
        updateEditState();
      });

      // Toggle highlighting
      highlightBtn.addEventListener('click', () => {
        isHighlighting = !isHighlighting;
        highlightBtn.style.backgroundColor = isHighlighting ? '#d0ffd0' : '#f0f0f0';
        if (isHighlighting) {
          isEditable = false;
          editBtn.style.backgroundColor = '#f0f0f0';
          isErasing = false;
          eraseBtn.style.backgroundColor = '#f0f0f0';
          updateEditState();
        }
      });

      highlightBtn.addEventListener('dblclick', () => {
        isHighlighting = !isHighlighting;
        highlightBtn.style.backgroundColor = isHighlighting ? '#d0ffd0' : '#f0f0f0';
      });

      // Toggle erasing
      eraseBtn.addEventListener('click', () => {
        isErasing = !isErasing;
        eraseBtn.style.backgroundColor = isErasing ? '#d0ffd0' : '#f0f0f0';
        if (isErasing) {
          isEditable = false;
          editBtn.style.backgroundColor = '#f0f0f0';
          isHighlighting = false;
          highlightBtn.style.backgroundColor = '#f0f0f0';
          updateEditState();
        }
      });

      eraseBtn.addEventListener('dblclick', () => {
        const highlights = document.querySelectorAll('.highlight');
        highlights.forEach(highlight => highlight.replaceWith(highlight.textContent));
        isErasing = false;
        eraseBtn.style.backgroundColor = '#f0f0f0';
      });

      // Toggle cursor
      document.getElementById('cursor-toggle').addEventListener('click', () => {
        isTypingCursor = !isTypingCursor;
        document.getElementById('cursor-toggle').style.backgroundColor = isTypingCursor ? '#d0ffd0' : '#f0f0f0';
        updateEditState();
      });

      // Update edit state
      function updateEditState() {
        const bubbles = document.querySelectorAll('.message-bubble');
        bubbles.forEach(bubble => {
          bubble.contentEditable = isEditable;
          bubble.style.outline = isEditable ? '1px dashed #ccc' : 'none';
          bubble.style.cursor = isEditable && isTypingCursor ? 'text' : 'default';
          if (isEditable && !defaultStyles[bubble.id]) {
            defaultStyles[bubble.id] = {
              fontWeight: bubble.style.fontWeight,
              fontStyle: bubble.style.fontStyle,
              textDecoration: bubble.style.textDecoration,
              color: bubble.style.color,
              fontSize: bubble.style.fontSize
            };
          }
        });
      }

      // Erase individual highlights
      document.addEventListener('click', (e) => {
        if (isErasing && e.target.className === 'highlight') {
          e.target.replaceWith(e.target.textContent);
        }
      });

      // Font palette logic
      const applyStyle = (style, value) => {
        if (!isEditable || !lastRange) {
          console.log('No editable state or range:', { isEditable, lastRange });
          return;
        }
        const selection = window.getSelection();
        selection.removeAllRanges();
        selection.addRange(lastRange);
        const selectedText = selection.toString().trim();
        if (!selectedText) {
          console.log('No text selected');
          return;
        }
        const range = selection.getRangeAt(0);
        if (range.commonAncestorContainer.parentNode.closest('.message-bubble')) {
          const span = document.createElement('span');
          span.style[style] = value;
          span.textContent = selectedText;
          range.deleteContents();
          range.insertNode(span);
          selection.removeAllRanges();
          selection.addRange(range);
          lastRange = range;
          console.log(\`Applied \${style}: \${value} to "\${selectedText}"\`);
        } else {
          console.log('Not in message-bubble');
        }
      };

      document.getElementById('bold-btn').addEventListener('click', () => applyStyle('fontWeight', 'bold'));
      document.getElementById('italic-btn').addEventListener('click', () => applyStyle('fontStyle', 'italic'));
      document.getElementById('underline-btn').addEventListener('click', () => applyStyle('textDecoration', 'underline'));
      document.getElementById('color-input').addEventListener('input', (e) => {
        console.log('Color input triggered:', e.target.value);
        const color = e.target.value;
        if (/^#[0-9A-F]{6}$/i.test(color)) applyStyle('color', color);
      });
      document.getElementById('size-input').addEventListener('input', (e) => {
        console.log('Size input triggered:', e.target.value);
        const size = Math.min(100, Math.max(8, parseInt(e.target.value) || 16));
        e.target.value = size;
        applyStyle('fontSize', size + 'px');
      });

      document.getElementById('toggle-format').addEventListener('click', () => {
        isCustomFormat = !isCustomFormat;
        const bubbles = document.querySelectorAll('.message-bubble');
        bubbles.forEach(bubble => {
          if (isCustomFormat) {
            // Custom format stays as edited
          } else {
            const def = defaultStyles[bubble.id];
            if (def) {
              bubble.querySelectorAll('span:not(.highlight)').forEach(span => {
                span.style.fontWeight = def.fontWeight || '';
                span.style.fontStyle = def.fontStyle || '';
                span.style.textDecoration = def.textDecoration || '';
                span.style.color = def.color || '';
                span.style.fontSize = def.fontSize || '';
              });
            }
          }
        });
      });

      document.getElementById('deselect-btn').addEventListener('click', () => {
        window.getSelection().removeAllRanges();
        lastRange = null;
        console.log('Selection deselected');
      });
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
      init();
    } else {
      document.addEventListener('DOMContentLoaded', init);
    }
  `,

  'styles.css': `
    #edit-toggle {
      position: fixed;
      top: 50px;
      right: 96px;
      width: 30px;
      height: 30px;
      font-size: 18px;
      text-align: center;
      background: #f0f0f0;
      border: 1px solid #ccc;
      border-radius: 5px;
      cursor: pointer;
      z-index: 1000;
    }
    #highlight-toggle {
      position: fixed;
      top: 50px;
      right: 56px;
      width: 30px;
      height: 30px;
      font-size: 18px;
      text-align: center;
      background: #f0f0f0;
      border: 1px solid #ccc;
      border-radius: 5px;
      cursor: pointer;
      z-index: 1000;
    }
    #erase-toggle {
      position: fixed;
      top: 50px;
      right: 16px;
      width: 30px;
      height: 30px;
      font-size: 18px;
      text-align: center;
      background: #f0f0f0;
      border: 1px solid #ccc;
      border-radius: 5px;
      cursor: pointer;
      z-index: 1000;
    }
    #edit-toggle:hover, #highlight-toggle:hover, #erase-toggle:hover {
      background: #e0e0e0;
    }
    #font-palette {
      position: fixed;
      bottom: 28px;
      left: 58.456%;
      transform: translateX(-50%);
      display: flex;
      gap: 5px;
      background: #f0f0f0;
      padding: 5px;
      border: 1px solid #ccc;
      border-radius: 5px;
      z-index: 1000;
      opacity: 0;
      transition: opacity 0.2s;
    }
    #font-palette:hover {
      opacity: 1;
    }
    #font-palette button {
      width: 30px;
      height: 30px;
      font-size: 18px;
      text-align: center;
      background: #fff;
      border: 1px solid #ccc;
      border-radius: 3px;
      cursor: pointer;
    }
    #font-palette button:hover {
      background: #e0e0e0;
    }
    #font-palette #cursor-toggle {
      color: white;
      font-weight: bold;
    }
    #font-palette input[type="text"] {
      width: 60px;
      height: 30px;
      font-size: 14px;
      border: 1px solid #ccc;
      border-radius: 3px;
      padding: 0 5px;
    }
    #font-palette input[type="number"] {
      width: 40px;
      height: 30px;
      font-size: 14px;
      border: 1px solid #ccc;
      border-radius: 3px;
      padding: 0 5px;
    }
    .message-bubble {
      user-select: text !important;
    }
    .highlight {
      background: black;
      color: #f0f0f0;
    }
  `
};

// Write files to 'y' directory
Object.entries(files).forEach(([filename, content]) => {
  fs.writeFileSync(path.join(dirPath, filename), content, 'utf8');
});

ww408

Quote from: ww408 on March 12, 2025, 01:29:01 PMThese changes revert on refresh, but Save Page WE* can save the edited version (and it's also good for saving chat bot convos, in general, and it's also good for bloating the pages).

And now I have a script that strips down SPWE pages (albeit with some glicthes that need to be ironed out):

Processes all files that contain Grok in the current directory by default. That can be overridden by specifying a file(s), e.g. <script name> <file name>. Wildcards are supported, so if a file is named apple tree.html app* can be used. The input file is renamed with the prefix, Save Page WE and the processed file assumes the name of the original, by default. If the argument, del is passed, the input file is deleted. If you always want the input file to be deleted, run <script name> config del <file name>: this will create a config file in /home/<username> that modulates the script. <script name> config keep will reverse this. Alternatively, you can deactivate the config file using <script name> config off (or turn it back on using on). The second option is whether or not you want code boxes collapsed by default. <script name> collapse collapses them by default; <script name> config collapse makes it permanent. When collapse is set as the default, you can use <script name> expand as a one-off or revert to the original using either <script name> config expand or <script name> config off (but note that off also affects the delete/keep setting).

#!/usr/bin/env node
const fs = require('fs').promises;
const path = require('path');
const { JSDOM } = require('jsdom');

// Log errors to file instead of terminal
async function logError(message, error) {
    const logEntry = `${new Date().toISOString()} - ${message}${error ? `: ${error.stack || error}` : ''}\n`;
    await fs.appendFile(path.join(process.cwd(), 'error.log'), logEntry, 'utf8').catch(err => {
        console.error('Failed to write to error.log:', err); // Last resort if logging fails
    });
}

// Extract savepage metadata (unchanged)
function extractSavePageMeta(htmlContent) {
    const dom = new JSDOM(htmlContent);
    const doc = dom.window.document;
    const fromUrl = doc.querySelector('meta[name="savepage-from"]')?.getAttribute('content') || '';
    const url = doc.querySelector('meta[name="savepage-url"]')?.getAttribute('content') || null;
    return {
        url: fromUrl.includes('http') ? fromUrl : url,
        title: doc.querySelector('meta[name="savepage-title"]')?.getAttribute('content') || 'Untitled Conversation',
        pubdate: doc.querySelector('meta[name="savepage-pubdate"]')?.getAttribute('content') || null
    };
}

// HTML template (unchanged from last concise version)
const htmlTemplate = (content, meta) => `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${meta.title}</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 20px auto; padding: 0 20px; }
        .metadata { font-size: 0.8em; margin-bottom: 10px; }
        .conversation-title { font-weight: bold; margin: 15px 0 20px; }
        .message { margin-bottom: 20px; }
        .grok-message { padding-left: 20px; }
        .label { font-weight: bold; margin-right: 5px; }
        details { margin: 10px 0; }
        summary { cursor: pointer; }
        code { background: #f4f4f4; padding: 10px; border-radius: 4px; display: block; white-space: pre-wrap; }
    </style>
</head>
<body>
    ${meta.url ? `<div class="metadata"><a href="${meta.url}">${meta.url}</a></div>` : ''}
    <div class="conversation-title">${meta.title}</div>
    ${meta.pubdate && meta.pubdate !== 'Unknown' ? `<div class="metadata">Published: ${meta.pubdate}</div>` : ''}
    <div id="conversation">${content}</div>
</body>
</html>
`;

// Process a single file (quiet now)
async function processFile(filePath, deleteInput, collapseCode) {
    try {
        const ext = path.extname(filePath).toLowerCase();
        const isTxt = ext === '.txt' || !ext;
        const htmlContent = await fs.readFile(filePath, 'utf8');
        let conversationContent = '';

        if (isTxt) {
            const lines = htmlContent.split('\n');
            let currentSpeaker = null;
            lines.forEach(line => {
                line = line.trim();
                if (!line) return;
                if (line.toLowerCase().startsWith('user:')) {
                    currentSpeaker = 'user';
                    conversationContent += `<div class="message user-message"><span class="label">User:</span><div><p>${line.slice(5).trim()}</p></div></div>`;
                } else if (line.toLowerCase().startsWith('grok:')) {
                    currentSpeaker = 'grok';
                    conversationContent += `<div class="message grok-message"><span class="label">Grok:</span><div><p>${line.slice(5).trim()}</p></div></div>`;
                } else if (currentSpeaker) {
                    conversationContent += `<div class="message ${currentSpeaker}-message"><span class="label"></span><div><p>${line}</p></div></div>`;
                }
            });
        } else {
            const dom = new JSDOM(htmlContent);
            const doc = dom.window.document;
            const messageRows = doc.querySelectorAll('.message-row, div > div > div > code');

            messageRows.forEach(row => {
                if (row.tagName.toLowerCase() === 'code') {
                    const codeContent = row.textContent.trim();
                    conversationContent += `<details ${collapseCode ? '' : 'open'}><summary>Code</summary><code>${codeContent}</code></details>`;
                    return;
                }

                const messageBubble = row.querySelector('.message-bubble');
                if (!messageBubble) return;

                const isUserMessage = row.classList.contains('items-end');
                const label = isUserMessage ? 'User:' : 'Grok:';
                const className = isUserMessage ? 'user-message' : 'grok-message';

                let content = messageBubble.innerHTML;
                const tempDiv = doc.createElement('div');
                tempDiv.innerHTML = content;

                tempDiv.querySelectorAll('button, .flex.-ml-1.text-sm.gap-2.mb-3').forEach(el => el.remove());
                tempDiv.querySelectorAll('span.whitespace-pre-wrap').forEach(span => {
                    const p = doc.createElement('p');
                    p.textContent = span.textContent;
                    span.replaceWith(p);
                });
                tempDiv.querySelectorAll('code').forEach(code => {
                    const details = doc.createElement('details');
                    details.innerHTML = `<summary>Code</summary><code>${code.textContent.trim()}</code>`;
                    if (!collapseCode) details.setAttribute('open', '');
                    code.replaceWith(details);
                });
                tempDiv.querySelectorAll('*').forEach(el => {
                    el.removeAttribute('style');
                    el.removeAttribute('class');
                    el.removeAttribute('dir');
                });

                conversationContent += `<div class="message ${className}"><span class="label">${label}</span><div>${tempDiv.innerHTML}</div></div>`;
            });
        }

        const meta = extractSavePageMeta(htmlContent);
        const baseName = path.basename(filePath, path.extname(filePath)).replace(/ Save Page WE$/, '');
        const outputFile = `${baseName}.html`;

        if (deleteInput) {
            await fs.unlink(filePath);
        } else {
            const newName = `${baseName} Save Page WE${ext || '.html'}`;
            await fs.rename(filePath, newName);
        }

        await fs.writeFile(outputFile, htmlTemplate(conversationContent, meta), 'utf8');
    } catch (error) {
        await logError(`Error processing ${filePath}`, error);
        console.error(`Error with ${filePath} - check error.log`);
    }
}

// Config management (quiet unless error)
async function manageConfig(action, filesToProcess) {
    const homeDir = require('os').homedir();
    const configPath = path.join(homeDir, '.grokwe_config');
    const configPathOff = `${configPath}.off`;
    let config = {};

    try {
        if (action === 'del' || action === 'keep') config.deleteByDefault = action === 'del';
        if (action === 'collapse' || action === 'expand') config.collapseByDefault = action === 'collapse';
        if (action === 'off' && await fs.stat(configPath).then(() => true).catch(() => false)) {
            await fs.rename(configPath, configPathOff);
            return;
        }
        if (action === 'on' && await fs.stat(configPathOff).then(() => true).catch(() => false)) {
            await fs.rename(configPathOff, configPath);
            return;
        }

        const collapseCode = config.collapseByDefault || false;
        if (filesToProcess?.length) {
            for (const file of filesToProcess) await processFile(file, config.deleteByDefault || false, collapseCode);
        } else {
            const files = await fs.readdir(process.cwd());
            const grokFiles = files.filter(f => /grok/i.test(f) && (f.endsWith('.html') || f.endsWith('.txt')));
            for (const file of grokFiles) await processFile(path.join(process.cwd(), file), config.deleteByDefault || false, collapseCode);
        }

        if (action) await fs.writeFile(configPath, JSON.stringify(config), 'utf8');
    } catch (error) {
        await logError('Error managing config', error);
        console.error('Config error - check error.log');
    }
}

// Read config (unchanged)
async function readConfig() {
    const configPath = path.join(require('os').homedir(), '.grokwe_config');
    try {
        return JSON.parse(await fs.readFile(configPath, 'utf8'));
    } catch {
        return { deleteByDefault: false, collapseByDefault: false };
    }
}

// Main (quiet unless no files)
async function main() {
    const args = process.argv.slice(2);
    if (args[0] === 'config' && ['del', 'keep', 'collapse', 'expand', 'off', 'on'].includes(args[1])) {
        await manageConfig(args[1], args.slice(2).filter(a => !['--del', '--keep', '--collapse'].includes(a)));
        return;
    }

    const config = await readConfig();
    const deleteInput = args.includes('--del') || (config.deleteByDefault && !args.includes('--keep'));
    const collapseCode = args.includes('--collapse') || (config.collapseByDefault && !args.includes('--expand'));

    const files = args.length ? args.filter(a => !['--del', '--keep', '--collapse', '--expand'].includes(a)) :
        (await fs.readdir(process.cwd())).filter(f => /grok/i.test(f) && (f.endsWith('.html') || f.endsWith('.txt')));
   
    if (!files.length) {
        console.log('No Grok files found.');
        return;
    }
    for (const file of files) await processFile(file, deleteInput, collapseCode);
}

main().catch(async error => {
    await logError('Main error', error);
    console.error('Main error - check error.log');
});