Artifacts For Poe

Add preview, download, and new window functionality for code blocks on Poe.com, inspired by Anthropic's Artifacts feature.

// ==UserScript==
// @name         Artifacts For Poe
// @namespace    http://tampermonkey.net/
// @version      2024-06-29
// @description  Add preview, download, and new window functionality for code blocks on Poe.com, inspired by Anthropic's Artifacts feature.
// @author       teezzz20
// @license      MIT
// @match        https://poe.com/chat/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=poe.com
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        .code-header-buttons {
            display: flex;
            justify-content: flex-end;
            gap: 6px;
            margin-left: auto;
        }
        .artifact-button {
            background: transparent;
            display: flex;
            flex-direction: row;
            border: none;
            gap: 6px;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            font-weight: 450;
            font-size: inherit;
            text-align: center;
            padding: .6em 1rem;
            margin: 0;
            border-radius: 1.5em;
        }
        .artifact-button:hover {
            background-image: linear-gradient(rgb(255 255 255/8%) 0 0);
        }
        .preview-iframe {
            width: 100%;
            height: 80vh;
            border: 1px solid #ccc;
            margin-top: 10px;
        }
    `);

    function createButton(text, svgPath) {
        const button = document.createElement('button');
        button.className = 'artifact-button';
        button.innerHTML = `<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="${svgPath}"></path></svg> ${text}`;
        return button;
    }

    async function getClipboardContent() {
        try {
            return await navigator.clipboard.readText();
        } catch (err) {
            console.error('Failed to read clipboard contents: ', err);
        }
    }

    async function togglePreview(codeHeader, previewButton) {
        const copyButton = codeHeader.querySelector('[class*="MarkdownCodeBlock_copyButton"]');
        const codeBlockWrapper = codeHeader.parentElement;
        const preTag = codeBlockWrapper.querySelector('pre[class*="MarkdownCodeBlock_preTag"]');

        if (!preTag) {
            console.error('Pre tag not found');
            return;
        }

        if (previewButton.textContent.includes('Preview')) {
            copyButton.click();
            await new Promise(resolve => setTimeout(resolve, 100));
            const code = await getClipboardContent();

            if (code) {
                const iframe = document.createElement('iframe');
                iframe.className = 'preview-iframe';
                iframe.srcdoc = code;
                codeBlockWrapper.insertBefore(iframe, preTag.nextSibling);
                iframe.onload = function() {
                    this.style.height = this.contentWindow.document.body.scrollHeight + 'px';
                };
                previewButton.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"></path></svg> Code';
                preTag.style.display = 'none';
            }
        } else {
            const iframe = codeBlockWrapper.querySelector('.preview-iframe');
            if (iframe) iframe.remove();
            previewButton.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"></path></svg> Preview';
            preTag.style.display = 'block';
        }
    }

    function detectFileTypeAndName(code) {
        const fileTypes = [
            { type: 'html', ext: 'html', detect: (code) => /<html|<!DOCTYPE html>/i.test(code) },
            { type: 'css', ext: 'css', detect: (code) => /^\s*(\.|#|[a-z])[^{]+\{/im.test(code) },
            { type: 'javascript', ext: 'js', detect: (code) => /function|const|let|var|=>/i.test(code) },
            { type: 'python', ext: 'py', detect: (code) => /def|class|import|from|if __name__ == ['"]__main__['"]:/i.test(code) },
            { type: 'svg', ext: 'svg', detect: (code) => /<svg/i.test(code) },
        ];

        for (const { type, ext, detect } of fileTypes) {
            if (detect(code)) {
                return { name: `code.${ext}`, type };
            }
        }

        return { name: 'code.txt', type: 'text' };
    }

    function downloadCode(code) {
        const { name, type } = detectFileTypeAndName(code);
        const blob = new Blob([code], { type: `text/${type}` });
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = name;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }

    function openInNewWindow(code) {
        const newWindow = window.open('', '_blank');
        newWindow.document.write(code);
        newWindow.document.close();
    }

    function observeNewCodeBlocks() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach((node) => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            const codeHeader = node.querySelector('[class*="MarkdownCodeBlock_codeHeader"]');
                            if (codeHeader && !codeHeader.querySelector('.artifact-button')) {
                                const buttonContainer = document.createElement('div');
                                buttonContainer.className = 'code-header-buttons';

                                const previewButton = createButton('Preview', 'M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z');
                                const downloadButton = createButton('Download', 'M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z');
                                const newWindowButton = createButton('New Window', 'M19 19H5V5h7V3H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z');

                                buttonContainer.appendChild(downloadButton);
                                buttonContainer.appendChild(newWindowButton);
                                buttonContainer.appendChild(previewButton);

                                codeHeader.appendChild(buttonContainer);

                                previewButton.addEventListener('click', () => togglePreview(codeHeader, previewButton));
                                downloadButton.addEventListener('click', async () => {
                                    const copyButton = codeHeader.querySelector('[class*="MarkdownCodeBlock_copyButton"]');
                                    copyButton.click();
                                    await new Promise(resolve => setTimeout(resolve, 100));
                                    const code = await getClipboardContent();
                                    if (code) downloadCode(code);
                                });
                                newWindowButton.addEventListener('click', async () => {
                                    const copyButton = codeHeader.querySelector('[class*="MarkdownCodeBlock_copyButton"]');
                                    copyButton.click();
                                    await new Promise(resolve => setTimeout(resolve, 100));
                                    const code = await getClipboardContent();
                                    if (code) openInNewWindow(code);
                                });
                            }
                        }
                    });
                }
            });
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    // Start observing
    observeNewCodeBlocks();
})();