Last active
May 14, 2023 15:56
-
-
Save programmerq/4828014ab37255bddf1c340cfa3fd7ec to your computer and use it in GitHub Desktop.
renderer for chatgpt code blocks that can have a visual representation.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Render SVGs in Code Blocks | |
// @namespace http://tampermonkey.net/ | |
// @version 0.1 | |
// @description Render SVGs in code blocks on https://chat.openai.com | |
// @author Jeff Anderson | |
// @match https://chat.openai.com/* | |
// @require https://github.com/mdaines/viz.js/releases/download/v2.1.2/viz.js | |
// @require https://github.com/mdaines/viz.js/releases/download/v2.1.2/full.render.js | |
// @grant none | |
// ==/UserScript== | |
(function () { | |
'use strict'; | |
const observerConfig = {childList: true, subtree: true}; | |
// Helper function to decode HTML entities | |
function decodeHtml(html) { | |
const txt = document.createElement('textarea'); | |
txt.innerHTML = html; | |
return txt.value; | |
} | |
const renderTasks = { | |
'language-html': function (codeBlock) { | |
console.log('Running render task for language-html'); | |
const content = decodeHtml(codeBlock.innerHTML.replace(/<span[^>]*>|<\/span>/g, '')); | |
if (/<svg[^>]*>/i.test(content)) { | |
// Call the 'language-svg' function if SVG tag is present | |
console.log('SVG tag found in language-html block, calling language-svg task'); | |
renderTasks['language-svg'](codeBlock, content); | |
} | |
}, | |
'language-svg-old': function (codeBlock, content) { | |
console.log('Running render task for language-svg'); | |
// Use the provided content if available, or decode the content from the code block | |
content = content || decodeHtml(codeBlock.innerHTML.replace(/<span[^>]*>|<\/span>/g, '')); | |
const svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); | |
svgElement.innerHTML = content; | |
codeBlock.closest('pre').parentNode.insertBefore(svgElement, codeBlock.closest('pre').nextSibling); | |
}, | |
'language-svg': function renderSvg(codeBlock) { | |
console.log('Running render task for language-svg'); | |
const svgContent = decodeHtml(codeBlock.innerHTML.replace(/<span[^>]*>|<\/span>/g, '')); | |
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); | |
svg.innerHTML = svgContent; | |
// Determine the natural aspect ratio of the SVG | |
const width = svg.firstChild.getAttribute('width') || 200; | |
const height = svg.firstChild.getAttribute('height') || 200; | |
// Set the viewBox to preserve the aspect ratio | |
svg.firstChild.setAttribute('viewBox', `0 0 ${width} ${height}`); | |
// Set the width and height of the SVG to fit its container | |
svg.firstChild.setAttribute('width', '100%'); | |
svg.firstChild.setAttribute('height', 'auto'); | |
codeBlock.closest('pre').parentNode.insertBefore(svg, codeBlock.closest('pre').nextSibling); | |
}, | |
'language-graphviz': function (codeBlock) { | |
console.log('Running render task for language-graphviz'); | |
const viz = new Viz(); | |
const graphvizCode = codeBlock.innerText; | |
viz.renderSVGElement(graphvizCode) | |
.then(function (element) { | |
const div = document.createElement('div'); | |
div.appendChild(element); | |
codeBlock.parentElement.parentElement.appendChild(div); | |
}) | |
.catch(error => { | |
// Create an error element to append to the page | |
const errorElement = document.createElement('p'); | |
errorElement.textContent = 'Error rendering Graphviz diagram'; | |
errorElement.style.color = 'red'; | |
codeBlock.parentElement.parentElement.appendChild(errorElement); | |
// Log the error to the console for debugging | |
console.error('Error rendering Graphviz diagram:', error); | |
}); | |
}, | |
}; | |
let retryTimeouts = new Map(); | |
function processCodeBlock(codeBlock, className) { | |
console.log(`Processing code block with class: className`); | |
if (retryTimeouts.has(codeBlock)) { | |
clearTimeout(retryTimeouts.get(codeBlock)); | |
} | |
var renderTask = renderTasks[className]; | |
let previousContent = ''; | |
const retryRender = () => { | |
const currentContent = codeBlock.innerText; | |
if (previousContent === currentContent) { | |
try { | |
renderTask(codeBlock); | |
console.log(`Rendered successfully: ${codeBlock.className}`); | |
retryTimeouts.delete(codeBlock); | |
} catch (err) { | |
console.error(`Error processing code block: ${err}`); | |
} | |
} else { | |
previousContent = currentContent; | |
retryTimeouts.set(codeBlock, setTimeout(retryRender, 1000)); | |
} | |
}; | |
retryTimeouts.set(codeBlock, setTimeout(retryRender, 1000)); | |
} | |
let observer; | |
function startObserver() { | |
if (observer) { | |
observer.disconnect(); | |
console.log("Disconnected old observer"); | |
} | |
const observerTarget = document.querySelector('body'); | |
if (observerTarget) { | |
console.log("Found observer target, starting observer"); | |
observer = new MutationObserver(handleMutations); | |
observer.observe(observerTarget, observerConfig); | |
} else { | |
console.log("Could not find observer target element"); | |
setTimeout(startObserver, 1000); // Retry after 1 second | |
} | |
} | |
function handleMutations(mutationsList) { | |
for (let mutation of mutationsList) { | |
if (mutation.type === 'childList') { | |
for (let addedNode of mutation.addedNodes) { | |
if (addedNode.nodeType === Node.ELEMENT_NODE) { | |
for (let className in renderTasks) { | |
const codeBlocks = addedNode.querySelectorAll(`code.${className}`); | |
codeBlocks.forEach(codeBlock => { | |
console.log('Processing code block with class:', className); | |
processCodeBlock(codeBlock, className); | |
}); | |
} | |
} | |
} | |
} | |
} | |
} | |
// Process any existing code blocks on page load | |
function processExistingCodeBlocks() { | |
for (let className in renderTasks) { | |
console.log('Processing existing code blocks with class:', className); | |
const xpath = `//div[2]/code[contains(@class, "${className}")]`; | |
const codeBlocks = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); | |
for (let i = 0; i < codeBlocks.snapshotLength; i++) { | |
console.log('processing done'); | |
processCodeBlock(codeBlocks.snapshotItem(i), className); | |
} | |
} | |
} | |
window.addEventListener('load', (event) => { | |
console.log('Document loaded, starting script'); | |
processExistingCodeBlocks(); | |
startObserver(); | |
}); | |
window.addEventListener('popstate', (event) => { | |
console.log('URL changed, restarting observer'); | |
startObserver(); | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment