ChatGPT Artefacts

Claude-like Artefacts inside ChatGPT Code Blocks. Open in Side Panel or Open in New Tab.

当前为 2024-09-19 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ChatGPT Artefacts
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Claude-like Artefacts inside ChatGPT Code Blocks. Open in Side Panel or Open in New Tab.
// @match        https://chatgpt.com/*
// @grant        GM_addElement
// @grant        GM_addStyle
// @author       @MartianInGreen
// @license      MIT
// @run-at       document-end
// ==/UserScript==

// @attribution  https://gist.github.com/CurtisAccelerate/64a20b1d5df6240119bb0a3f4b5abf31
// Base of script made by https://github.com/CurtisAccelerate @ https://gist.github.com/CurtisAccelerate/64a20b1d5df6240119bb0a3f4b5abf31 / https://x.com/BBacktesting/status/1804481588941533255

(function() {
    'use strict';

    let panel;
    let isDragging = false;
    let startX;
    let startWidth;

    function createSlideOutPanel(codeBlock) {
        if (panel) {
            panel.remove();
        }

        panel = document.createElement('div');
        panel.style.cssText = `
            position: fixed;
            top: 0;
            right: 0;
            width: 600px;
            height: 100%;
            background: #f7f7f8;
            box-shadow: -2px 0 5px rgba(0,0,0,0.3);
            z-index: 1000;
            display: flex;
            flex-direction: column;
            transform: translateX(100%);
            transition: transform 0.3s ease-in-out;
        `;

        const header = document.createElement('div');
        header.style.cssText = `
            padding: 10px;
            background: #282c34;
            color: #fff;
            display: flex;
            justify-content: space-between;
            align-items: center;
            cursor: move;
        `;

        const closeButton = document.createElement('button');
        closeButton.textContent = 'Close';
        closeButton.style.cssText = `
            background: #ff5f57;
            border: none;
            border-radius: 5px;
            padding: 5px 10px;
            cursor: pointer;
            color: white;
        `;
        closeButton.onclick = () => panel.style.transform = 'translateX(100%)';

        header.appendChild(closeButton);
        panel.appendChild(header);

        const contentContainer = document.createElement('div');
        contentContainer.style.cssText = `
            padding: 10px;
            overflow-y: auto;
            flex-grow: 1;
        `;

        const iframe = document.createElement('iframe');
        iframe.style.cssText = `
            width: 100%;
            height: 100%;
            border: none;
            margin: 0;
            padding: 0;
        `;
        contentContainer.appendChild(iframe);
        panel.appendChild(contentContainer);

        document.body.appendChild(panel);

        // Clone the code block and remove the "Run Demo" and "Open in New Tab" buttons if they exist
        const cleanCodeBlock = codeBlock.cloneNode(true);
        const runDemoButton = cleanCodeBlock.querySelector('.run-demo-button');
        const openTabButton = cleanCodeBlock.querySelector('.open-tab-button');
        if (runDemoButton) {
            runDemoButton.remove();
        }
        if (openTabButton) {
            openTabButton.remove();
        }

        const doc = iframe.contentDocument || iframe.contentWindow.document;
        doc.open();
        doc.write(cleanCodeBlock.textContent);
        doc.close();

        setTimeout(() => panel.style.transform = 'translateX(0)', 0);

        header.addEventListener('mousedown', startDragging);
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', stopDragging);

        // Close the panel if clicking outside
        document.addEventListener('click', function(event) {
            if (!panel.contains(event.target) && !event.target.closest('.run-demo-button') && !event.target.closest('.open-tab-button')) {
                panel.style.transform = 'translateX(100%)';
            }
        }, { once: true });
    }

    function openInNewTab(codeBlock) {
        const cleanCodeBlock = codeBlock.cloneNode(true);
        const runDemoButton = cleanCodeBlock.querySelector('.run-demo-button');
        const openTabButton = cleanCodeBlock.querySelector('.open-tab-button');
        if (runDemoButton) {
            runDemoButton.remove();
        }
        if (openTabButton) {
            openTabButton.remove();
        }

        const currentUrl = window.location.href;
        const newUrl = currentUrl + '/artefact';

        const newWindow = window.open(newUrl, '_blank');
        if (newWindow) {
            newWindow.document.open();
            newWindow.document.write(cleanCodeBlock.textContent);

            // Update the URL display without navigating
            newWindow.history.pushState(null, '', newUrl);

            newWindow.document.close();
        } else {
            alert('Failed to open new tab. Please allow pop-ups for this site.');
        }
    }


    function addButtonsNextToCopy(codeBlock) {
        const outerContainer = codeBlock.closest('.relative');
        if (!outerContainer) return;

        const copyButtonContainer = outerContainer.querySelector('.absolute.bottom-0.right-2');
        if (!copyButtonContainer) return;

        // Check if the buttons are already added to prevent duplicates
        if (outerContainer.querySelector('.run-demo-button') || outerContainer.querySelector('.open-tab-button')) {
            return;
        }

        // Create a new span for our custom buttons
        const customButtonsSpan = document.createElement('span');
        customButtonsSpan.className = 'custom-buttons-span';
        customButtonsSpan.style.cssText = `
            display: flex;
            flex-direction: row;
            align-items: center;
            margin-right: 5px;
        `;

        const buttonStyle = `
            padding: 0 8px;  /* Adjust padding to control the button's width */
            height: 24px;  /* Set the fixed height to 24px */
            background: #2f2f2f;
            border: none;
            color: #b4b4b4;
            cursor: pointer;
            font-size: 13px;
            font-weight: 500;
            border-radius: 4px;
            display: flex;
            align-items: center;
            justify-content: center;  /* Center the text */
            transition: background 0.3s, color 0.3s;
        `;

        // Create "Run Demo" button
        const runDemoButton = document.createElement('button');
        runDemoButton.innerHTML = `
            <div style="display: flex; align-items: center;">
                <div style="margin-right: 4px; display: flex; align-items: center;">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <polygon points="5 3 19 12 5 21 5 3"></polygon>
                    </svg>
                </div>
                <div>Open Demo</div>
            </div>
        `;
        runDemoButton.className = 'run-demo-button custom-tooltip';
        runDemoButton.style.cssText = buttonStyle;
        runDemoButton.onclick = (e) => {
            e.stopPropagation();
            createSlideOutPanel(codeBlock);
        };

        // Create "Open in New Tab" button
        const openTabButton = document.createElement('button');
        openTabButton.innerHTML = `
            <div style="display: flex; align-items: center;">
                <div style="margin-right: 4px; display: flex; align-items: center;">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
                        <polyline points="15 3 21 3 21 9"></polyline>
                        <line x1="10" y1="14" x2="21" y2="3"></line>
                    </svg>
                </div>
                <div>Open in New Tab</div>
            </div>
        `;
        openTabButton.className = 'open-tab-button custom-tooltip';
        openTabButton.style.cssText = buttonStyle;
        openTabButton.onclick = (e) => {
            e.stopPropagation();
            openInNewTab(codeBlock);
        };

        // Add hover effects
        [runDemoButton, openTabButton].forEach(button => {
            button.addEventListener('mouseover', () => {
                button.style.background = '#3f3f3f';
                button.style.color = '#ffffff';
            });
            button.addEventListener('mouseout', () => {
                button.style.background = '#2f2f2f';
                button.style.color = '#b4b4b4';
            });
        });

        // Add hover text for the buttons
        runDemoButton.setAttribute('data-hover-text', 'Open Demo in Slideout Panel');
        openTabButton.setAttribute('data-hover-text', 'Open in New Tab');

        // Add the new buttons to the custom span
        customButtonsSpan.appendChild(runDemoButton);
        customButtonsSpan.appendChild(openTabButton);

        // Insert the custom span before the existing span containing the "Copy code" button
        copyButtonContainer.insertBefore(customButtonsSpan, copyButtonContainer.firstChild);
    }

    function processCodeBlocks() {
        const codeBlocks = document.querySelectorAll('.overflow-y-auto');
        codeBlocks.forEach(codeBlock => {
            if (codeBlock.closest('.markdown')) {
                addButtonsNextToCopy(codeBlock);
            }
        });
    }

    function observeChat() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach((node) => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            const codeBlocks = node.querySelectorAll('.overflow-y-auto');
                            codeBlocks.forEach(codeBlock => {
                                if (codeBlock.closest('.markdown')) {
                                    addButtonsNextToCopy(codeBlock);
                                }
                            });
                        }
                    });
                }
            });
        });

        const chatContainer = document.querySelector('main');
        if (chatContainer) {
            observer.observe(chatContainer, { childList: true, subtree: true });
        }
    }

    function addIndicator() {
        const indicator = document.createElement('div');
        indicator.textContent = 'Artefacts Active';
        indicator.style.cssText = `
            position: fixed;
            top: 10px;
            right: 10px;
            z-index: 1000;
            padding: 5px 10px;
            background-color: #4CAF50;
            color: white;
            border-radius: 5px;
            font-size: 12px;
        `;
        document.body.appendChild(indicator);
        setTimeout(() => indicator.style.display = 'none', 3000);
    }

    function initializeProcessor() {
        addIndicator();
        processCodeBlocks();
        observeChat();
    }

    // Use MutationObserver to wait for the chat interface to load
    const bodyObserver = new MutationObserver((mutations) => {
        if (document.querySelector('main')) {
            bodyObserver.disconnect();
            setTimeout(initializeProcessor, 500); // Delay execution by 0.5 seconds
        }
    });

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

    // Dragging functionality for the slide-out panel
    function startDragging(e) {
        isDragging = true;
        startX = e.clientX;
        startWidth = parseInt(document.defaultView.getComputedStyle(panel).width, 10);
        document.documentElement.addEventListener('mousemove', dragPanel, false);
        document.documentElement.addEventListener('mouseup', stopDragging, false);
    }

    function dragPanel(e) {
        if (!isDragging) return;
        let newWidth = startWidth - (e.clientX - startX);
        if (newWidth < 300) newWidth = 300;
        if (newWidth > 900) newWidth = 900;
        panel.style.width = newWidth + 'px';
    }

    function stopDragging(e) {
        isDragging = false;
        document.documentElement.removeEventListener('mousemove', dragPanel, false);
        document.documentElement.removeEventListener('mouseup', stopDragging, false);
    }

})();

GM_addStyle(`
    .custom-tooltip {
        position: relative;
        z-index: 10;
    }
    .custom-tooltip::after {
        content: attr(data-hover-text);
        position: absolute;
        bottom: 120%;
        left: 50%;
        transform: translateX(-50%);
        background-color: #333;
        color: white;
        padding: 4px 8px;
        border-radius: 4px;
        font-size: 12px;
        white-space: nowrap;
        opacity: 0;
        transition: opacity 0.3s;
        pointer-events: none;
        z-index: 11;
    }
    .custom-tooltip:hover::after {
        opacity: 1;
    }
    .custom-tooltip:hover + [role="tooltip"] {
        display: none !important;
    }
`);