Class: CopyForAi::ContentInjector

Inherits:
Object
  • Object
show all
Defined in:
lib/copy_for_ai/content_injector.rb

Constant Summary collapse

BUTTON_AND_SCRIPT =
"<style>\n  .copy-for-ai-container {\n    display: inline;\n  }\n  .copy-for-ai-btn {\n    background: none;\n    color: #CC0000;\n    padding: 0;\n    cursor: pointer;\n    border: none;\n    font-family: inherit;\n    font-size: inherit;\n    text-decoration: underline;\n    display: inline;\n    transition: color 0.15s ease;\n  }\n  .copy-for-ai-btn:hover {\n    color: #990000;\n  }\n  .copy-for-ai-btn.copied {\n    color: #00AA00;\n  }\n  .copy-for-ai-format-select {\n    background: #333;\n    color: #CC0000;\n    border: 1px solid #555;\n    border-radius: 3px;\n    padding: 1px 4px;\n    font-size: 11px;\n    font-family: inherit;\n    cursor: pointer;\n    margin-left: 4px;\n    vertical-align: baseline;\n  }\n  .copy-for-ai-format-select:hover {\n    border-color: #CC0000;\n  }\n  .copy-for-ai-format-select:focus {\n    outline: none;\n    border-color: #CC0000;\n  }\n  .copy-for-ai-floating {\n    position: fixed;\n    top: 10px;\n    right: 10px;\n    z-index: 99999;\n    background: #222;\n    padding: 8px 12px;\n    border-radius: 4px;\n    box-shadow: 0 2px 8px rgba(0,0,0,0.3);\n  }\n  .copy-for-ai-floating .copy-for-ai-btn {\n    background: #CC0000;\n    color: #fff;\n    padding: 4px 12px;\n    border-radius: 3px;\n    text-decoration: none;\n  }\n  .copy-for-ai-floating .copy-for-ai-btn:hover {\n    background: #990000;\n    color: #fff;\n  }\n  .copy-for-ai-floating .copy-for-ai-btn.copied {\n    background: #00AA00;\n  }\n  .copy-for-ai-floating .copy-for-ai-format-select {\n    background: #444;\n    margin-left: 8px;\n  }\n</style>\n<script>\n  (function() {\n    var currentFormat = localStorage.getItem('copyForAiFormat') || 'toon';\n\n    function initCopyForAi() {\n      // Create container\n      var container = document.createElement('span');\n      container.className = 'copy-for-ai-container';\n\n      // Create the button\n      var btn = document.createElement('button');\n      btn.className = 'copy-for-ai-btn';\n      btn.textContent = 'Copy for AI';\n      btn.title = 'Copy error details for AI agents';\n\n      // Create format selector\n      var select = document.createElement('select');\n      select.className = 'copy-for-ai-format-select';\n      select.title = 'Select output format';\n\n      var formats = [\n        { value: 'toon', label: 'TOON' },\n        { value: 'markdown', label: 'MD' },\n        { value: 'plaintext', label: 'TXT' }\n      ];\n\n      formats.forEach(function(fmt) {\n        var option = document.createElement('option');\n        option.value = fmt.value;\n        option.textContent = fmt.label;\n        if (fmt.value === currentFormat) {\n          option.selected = true;\n        }\n        select.appendChild(option);\n      });\n\n      select.addEventListener('change', function() {\n        currentFormat = select.value;\n        localStorage.setItem('copyForAiFormat', currentFormat);\n      });\n\n      container.appendChild(btn);\n      container.appendChild(select);\n\n      var inserted = false;\n\n      // Strategy 1: Find the trace links and insert after the last one\n      var traceLinks = document.querySelectorAll('a');\n      var lastTraceLink = null;\n\n      for (var i = 0; i < traceLinks.length; i++) {\n        var link = traceLinks[i];\n        var text = link.textContent.toLowerCase();\n        if (text.includes('application trace') || text.includes('framework trace') || text.includes('full trace')) {\n          lastTraceLink = link;\n        }\n      }\n\n      if (lastTraceLink) {\n        var separator = document.createTextNode(' | ');\n        lastTraceLink.parentNode.insertBefore(separator, lastTraceLink.nextSibling);\n        lastTraceLink.parentNode.insertBefore(container, separator.nextSibling);\n        inserted = true;\n      }\n\n      // Strategy 2: Look for header with Rails.root info\n      if (!inserted) {\n        var allText = document.body.innerText;\n        if (allText.includes('Rails.root:')) {\n          var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);\n          while (walker.nextNode()) {\n            if (walker.currentNode.textContent.includes('Rails.root:')) {\n              var parent = walker.currentNode.parentElement;\n              if (parent) {\n                parent.parentElement.insertBefore(container, parent.nextSibling);\n                inserted = true;\n                break;\n              }\n            }\n          }\n        }\n      }\n\n      // Strategy 3: Insert after h2\n      if (!inserted) {\n        var h2 = document.querySelector('h2');\n        if (h2 && h2.parentElement) {\n          h2.parentElement.insertBefore(container, h2.nextSibling);\n          inserted = true;\n        }\n      }\n\n      // Strategy 4: Floating container as fallback\n      if (!inserted) {\n        container.classList.add('copy-for-ai-floating');\n        document.body.appendChild(container);\n      }\n\n      btn.addEventListener('click', function(e) {\n        e.preventDefault();\n        var errorData = extractErrorData();\n        var output;\n\n        switch (currentFormat) {\n          case 'toon':\n            output = formatAsToon(errorData);\n            break;\n          case 'markdown':\n            output = formatAsMarkdown(errorData);\n            break;\n          case 'plaintext':\n            output = formatAsPlaintext(errorData);\n            break;\n          default:\n            output = formatAsToon(errorData);\n        }\n\n        copyToClipboard(output, btn);\n      });\n    }\n\n    function extractErrorData() {\n      var data = {\n        errorType: '',\n        errorMessage: '',\n        railsRoot: '',\n        source: null,\n        traces: { application: null, framework: null, full: null },\n        routes: null\n      };\n\n      // Extract error type from h1\n      var h1 = document.querySelector('h1');\n      data.errorType = h1 ? h1.textContent.trim() : 'Unknown Error';\n\n      // Extract error message from h2\n      var h2 = document.querySelector('h2');\n      data.errorMessage = h2 ? h2.textContent.trim() : '';\n\n      // Extract Rails.root info\n      var bodyText = document.body.innerText;\n      var railsRootMatch = bodyText.match(/Rails\\\\.root:\\\\s*([^\\\\n]+)/);\n      if (railsRootMatch) {\n        data.railsRoot = railsRootMatch[1].trim();\n      }\n\n      // Extract source code\n      data.source = extractSourceCode();\n\n      // Extract traces\n      data.traces = extractAllTraces();\n\n      // Extract routes\n      data.routes = extractRoutes();\n\n      return data;\n    }\n\n    // ===== TOON FORMAT =====\n    function formatAsToon(data) {\n      var lines = [];\n\n      // Error info as simple key-value pairs\n      lines.push('error_type: ' + data.errorType);\n      if (data.errorMessage) {\n        lines.push('error_message: ' + escapeForToon(data.errorMessage));\n      }\n      if (data.railsRoot) {\n        lines.push('rails_root: ' + data.railsRoot);\n      }\n\n      // Source code\n      if (data.source) {\n        lines.push('');\n        lines.push('source:');\n        if (data.source.file) {\n          lines.push('  file: ' + data.source.file);\n        }\n        if (data.source.line) {\n          lines.push('  line: ' + data.source.line);\n        }\n        if (data.source.code) {\n          lines.push('  code: |');\n          data.source.code.split('\\\\n').forEach(function(codeLine) {\n            lines.push('    ' + codeLine);\n          });\n        }\n      }\n\n      // Traces - use compact array notation\n      if (data.traces.application) {\n        lines.push('');\n        lines.push('application_trace:');\n        formatTraceForToon(data.traces.application).forEach(function(t) {\n          lines.push('  ' + t);\n        });\n      }\n      if (data.traces.framework) {\n        lines.push('');\n        lines.push('framework_trace:');\n        formatTraceForToon(data.traces.framework).forEach(function(t) {\n          lines.push('  ' + t);\n        });\n      }\n      if (data.traces.full) {\n        lines.push('');\n        lines.push('full_trace:');\n        formatTraceForToon(data.traces.full).forEach(function(t) {\n          lines.push('  ' + t);\n        });\n      }\n\n      // Routes\n      if (data.routes) {\n        lines.push('');\n        lines.push('available_routes:');\n        data.routes.split('\\\\n').slice(0, 15).forEach(function(route) {\n          lines.push('  ' + route.trim());\n        });\n      }\n\n      return lines.join('\\\\n');\n    }\n\n    function formatTraceForToon(trace) {\n      if (!trace) return [];\n      return trace.split('\\\\n').slice(0, 25).map(function(line) {\n        return line.trim();\n      }).filter(function(line) {\n        return line.length > 0;\n      });\n    }\n\n    function escapeForToon(str) {\n      // If string contains special chars, wrap in quotes\n      if (str.includes('\\\\n') || str.includes(':') || str.includes('\"')) {\n        return '\"' + str.replace(/\"/g, '\\\\\\\\\"').replace(/\\\\n/g, ' ') + '\"';\n      }\n      return str;\n    }\n\n    // ===== MARKDOWN FORMAT =====\n    function formatAsMarkdown(data) {\n      var lines = [];\n\n      lines.push('# Rails Error: ' + data.errorType);\n      lines.push('');\n\n      if (data.errorMessage) {\n        lines.push('## Error Message');\n        lines.push('');\n        lines.push(data.errorMessage);\n        lines.push('');\n      }\n\n      if (data.railsRoot) {\n        lines.push('**Rails.root:** `' + data.railsRoot + '`');\n        lines.push('');\n      }\n\n      if (data.source) {\n        lines.push('## Source Code');\n        lines.push('');\n        if (data.source.file) {\n          lines.push('**File:** `' + data.source.file + '`');\n        }\n        if (data.source.line) {\n          lines.push('**Line:** ' + data.source.line);\n        }\n        lines.push('');\n        if (data.source.code) {\n          lines.push('```ruby');\n          lines.push(data.source.code);\n          lines.push('```');\n          lines.push('');\n        }\n      }\n\n      if (data.traces.application) {\n        lines.push('## Application Trace');\n        lines.push('');\n        lines.push('```');\n        lines.push(data.traces.application);\n        lines.push('```');\n        lines.push('');\n      }\n\n      if (data.traces.framework) {\n        lines.push('## Framework Trace');\n        lines.push('');\n        lines.push('```');\n        lines.push(data.traces.framework);\n        lines.push('```');\n        lines.push('');\n      }\n\n      if (data.traces.full) {\n        lines.push('## Full Trace');\n        lines.push('');\n        lines.push('```');\n        lines.push(data.traces.full);\n        lines.push('```');\n        lines.push('');\n      }\n\n      if (data.routes) {\n        lines.push('## Available Routes');\n        lines.push('');\n        lines.push('```');\n        lines.push(data.routes);\n        lines.push('```');\n        lines.push('');\n      }\n\n      return lines.join('\\\\n');\n    }\n\n    // ===== PLAINTEXT FORMAT =====\n    function formatAsPlaintext(data) {\n      var lines = [];\n\n      lines.push('RAILS ERROR: ' + data.errorType);\n      lines.push('='.repeat(50));\n      lines.push('');\n\n      if (data.errorMessage) {\n        lines.push('ERROR MESSAGE:');\n        lines.push(data.errorMessage);\n        lines.push('');\n      }\n\n      if (data.railsRoot) {\n        lines.push('RAILS ROOT: ' + data.railsRoot);\n        lines.push('');\n      }\n\n      if (data.source) {\n        lines.push('SOURCE CODE:');\n        if (data.source.file) {\n          lines.push('File: ' + data.source.file);\n        }\n        if (data.source.line) {\n          lines.push('Line: ' + data.source.line);\n        }\n        if (data.source.code) {\n          lines.push('');\n          lines.push(data.source.code);\n        }\n        lines.push('');\n      }\n\n      if (data.traces.application) {\n        lines.push('APPLICATION TRACE:');\n        lines.push('-'.repeat(30));\n        lines.push(data.traces.application);\n        lines.push('');\n      }\n\n      if (data.traces.framework) {\n        lines.push('FRAMEWORK TRACE:');\n        lines.push('-'.repeat(30));\n        lines.push(data.traces.framework);\n        lines.push('');\n      }\n\n      if (data.traces.full) {\n        lines.push('FULL TRACE:');\n        lines.push('-'.repeat(30));\n        lines.push(data.traces.full);\n        lines.push('');\n      }\n\n      if (data.routes) {\n        lines.push('AVAILABLE ROUTES:');\n        lines.push('-'.repeat(30));\n        lines.push(data.routes);\n        lines.push('');\n      }\n\n      return lines.join('\\\\n');\n    }\n\n    // ===== HELPER FUNCTIONS =====\n    function extractSourceCode() {\n      var result = {};\n\n      var sourceExtracts = document.querySelectorAll('[id*=\"source\"], [class*=\"source\"], .extract');\n      for (var i = 0; i < sourceExtracts.length; i++) {\n        var section = sourceExtracts[i];\n\n        var headers = section.querySelectorAll('h4, h5, .info, [class*=\"file\"]');\n        if (headers.length > 0) {\n          var headerText = headers[0].textContent.trim();\n          var match = headerText.match(/([^:]+\\\\.rb):?(\\\\d+)?/);\n          if (match) {\n            result.file = match[1].trim();\n            if (match[2]) result.line = match[2];\n          }\n        }\n\n        var code = section.querySelector('pre, code, .code');\n        if (code) {\n          result.code = code.textContent.trim();\n          break;\n        }\n      }\n\n      return (result.code || result.file) ? result : null;\n    }\n\n    function extractAllTraces() {\n      var traces = {\n        application: null,\n        framework: null,\n        full: null\n      };\n\n      var appTrace = document.querySelector('#Application-Trace, #application-trace, [id*=\"Application\"][id*=\"Trace\"]');\n      var frameworkTrace = document.querySelector('#Framework-Trace, #framework-trace, [id*=\"Framework\"][id*=\"Trace\"]');\n      var fullTrace = document.querySelector('#Full-Trace, #full-trace, [id*=\"Full\"][id*=\"Trace\"]');\n\n      if (appTrace) {\n        var pre = appTrace.querySelector('pre, code');\n        if (pre) traces.application = pre.textContent.trim();\n      }\n      if (frameworkTrace) {\n        var pre = frameworkTrace.querySelector('pre, code');\n        if (pre) traces.framework = pre.textContent.trim();\n      }\n      if (fullTrace) {\n        var pre = fullTrace.querySelector('pre, code');\n        if (pre) traces.full = pre.textContent.trim();\n      }\n\n      if (!traces.application && !traces.framework && !traces.full) {\n        var allPres = document.querySelectorAll('pre');\n        for (var i = 0; i < allPres.length; i++) {\n          var pre = allPres[i];\n          var text = pre.textContent;\n          if (text.includes(':in `') || text.includes('.rb:')) {\n            if (!traces.full) {\n              traces.full = text.trim();\n            }\n          }\n        }\n      }\n\n      return traces;\n    }\n\n    function extractRoutes() {\n      var routesTable = document.querySelector('table');\n      if (routesTable) {\n        var rows = routesTable.querySelectorAll('tr');\n        if (rows.length > 0) {\n          var routeLines = [];\n          rows.forEach(function(row) {\n            var cells = row.querySelectorAll('th, td');\n            if (cells.length >= 3) {\n              routeLines.push(\n                (cells[0].textContent.trim().padEnd(10) + ' ') +\n                (cells[1].textContent.trim().padEnd(40) + ' ') +\n                cells[2].textContent.trim()\n              );\n            }\n          });\n          if (routeLines.length > 1) {\n            return routeLines.slice(0, 20).join('\\\\n') + (rows.length > 20 ? '\\\\n... and more' : '');\n          }\n        }\n      }\n      return null;\n    }\n\n    function copyToClipboard(text, btn) {\n      if (navigator.clipboard && navigator.clipboard.writeText) {\n        navigator.clipboard.writeText(text).then(function() {\n          showCopiedFeedback(btn);\n        }).catch(function(err) {\n          fallbackCopy(text, btn);\n        });\n      } else {\n        fallbackCopy(text, btn);\n      }\n    }\n\n    function fallbackCopy(text, btn) {\n      var textarea = document.createElement('textarea');\n      textarea.value = text;\n      textarea.style.position = 'fixed';\n      textarea.style.left = '-9999px';\n      document.body.appendChild(textarea);\n      textarea.select();\n      try {\n        document.execCommand('copy');\n        showCopiedFeedback(btn);\n      } catch (err) {\n        alert('Failed to copy. Error details:\\\\n\\\\n' + text.substring(0, 1000));\n      }\n      document.body.removeChild(textarea);\n    }\n\n    function showCopiedFeedback(btn) {\n      var originalText = btn.textContent;\n      btn.textContent = 'Copied!';\n      btn.classList.add('copied');\n      setTimeout(function() {\n        btn.textContent = originalText;\n        btn.classList.remove('copied');\n      }, 2000);\n    }\n\n    // Initialize when DOM is ready\n    if (document.readyState === 'loading') {\n      document.addEventListener('DOMContentLoaded', initCopyForAi);\n    } else {\n      initCopyForAi();\n    }\n  })();\n</script>\n"

Class Method Summary collapse

Class Method Details

.inject(html) ⇒ Object



599
600
601
602
603
604
605
606
607
# File 'lib/copy_for_ai/content_injector.rb', line 599

def inject(html)
  # Inject before closing </body> tag
  if html.include?("</body>")
    html.sub("</body>", "#{BUTTON_AND_SCRIPT}</body>")
  else
    # If no body tag, append to end
    html + BUTTON_AND_SCRIPT
  end
end