Deepseek Code Artifact

Turning Deepseek Codeblock into a dedicated Artifact like cluade.

// ==UserScript==
// @name         Deepseek Code Artifact
// @namespace    https://github.com/Yuichi-Aragi/Userscript/blob/main/CodeArtifactPro.user.js
// @version      3.2
// @description  Turning Deepseek Codeblock into a dedicated Artifact like cluade.
// @author       YA
// @match        https://chat.deepseek.com/a/chat/s/*
// @grant        GM_addStyle
// @grant        GM_notification
// @grant        GM_getResourceText
// @grant        GM_xmlhttpRequest
// @resource     prismCSS https://cdnjs.cloudflare.com/ajax/libs/prismjs/1.29.0/themes/prism-tomorrow.min.css
// @noframes
// @license MIT
// ==/UserScript==

(function() {
    'use strict';
    
    const SCRIPT_ID = 'code-artifact-pro-enterprise';
    const config = {
        panelHeightRatio: 0.8,
        debounceTimeout: 150,
        maxCodeSize: 100000, // 100KB
        mobileBreakpoint: 768,
        zIndex: 2147483647
    };
    
    let artifactPanel, observer, mutationDebounce;
    const processedNodes = new WeakSet();
    const mutationQueue = [];
    const state = {
        isPanelOpen: false,
        lastFocusedElement: null,
        prismLoaded: false
    };

    // ==================== PRISM LOADER ====================
    function loadPrism() {
        return new Promise((resolve, reject) => {
            if (typeof Prism !== 'undefined') {
                state.prismLoaded = true;
                return resolve();
            }

            GM_xmlhttpRequest({
                method: 'GET',
                url: 'https://cdnjs.cloudflare.com/ajax/libs/prismjs/1.29.0/prism.min.js',
                onload: function(response) {
                    if (response.status === 200) {
                        try {
                            eval(response.responseText);
                            state.prismLoaded = true;
                            resolve();
                        } catch (e) {
                            reject(e);
                        }
                    } else {
                        reject(new Error(`Prism load failed: ${response.status}`));
                    }
                },
                onerror: reject
            });
        });
    }

    // ==================== STYLE MANAGEMENT ====================
    function ensureStyles() {
        const existing = document.querySelector(`style[data-css="${SCRIPT_ID}"]`);
        if (existing) return;

        const style = document.createElement('style');
        style.dataset.css = SCRIPT_ID;
        style.textContent = GM_getResourceText('prismCSS') + getDynamicStyles();
        document.head.appendChild(style);
    }

    function getDynamicStyles() {
        return `
         
        .md-code-block-banner-wrap,
        .md-code-block-banner,
        .md-code-block-infostring,
        .md-code-block-action,
        .ds-markdown-code-copy-button {
            display: none !important;
            visibility: hidden !important;
            opacity: 0 !important;
            height: 0 !important;
            width: 0 !important;
            padding: 0 !important;
            margin: 0 !important;
            border: none !important;
            pointer-events: none !important;
        }

       
        .md-code-block-banner-wrap {
            position: absolute !important;
            top: -9999px !important;
            left: -9999px !important;
        }

            :root {
                --artifact-bg: #1e1e1e;
                --artifact-header: #252526;
                --artifact-accent: #007acc;
                --artifact-text: #d4d4d4;
            }

            .artifact-overlay {
                position: fixed;
                top: 0;
                left: 0;
                width: 100vw;
                height: 100vh;
                background: rgba(0,0,0,0.95);
                z-index: ${config.zIndex};
                display: flex;
                justify-content: center;
                align-items: center;
                touch-action: none;
                opacity: 0;
                pointer-events: none;
                transition: opacity 0.3s ease;
            }

            .artifact-overlay.visible {
                opacity: 1;
                pointer-events: auto;
            }

            .artifact-panel {
                width: min(95%, 1200px);
                height: ${config.panelHeightRatio * 100}vh;
                background: var(--artifact-bg);
                border-radius: 12px;
                box-shadow: 0 12px 24px rgba(0,0,0,0.3);
                overflow: hidden;
                display: flex;
                flex-direction: column;
                transform: scale(0.98);
                transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            }

            .artifact-panel.active {
                transform: scale(1);
            }

            .artifact-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 14px 20px;
                background: var(--artifact-header);
                border-bottom: 1px solid rgba(255,255,255,0.1);
            }

            .artifact-title {
                color: #fff;
                font: 500 16px/1.4 system-ui, sans-serif;
                display: flex;
                align-items: center;
                gap: 8px;
            }

            .artifact-buttons {
                display: flex;
                gap: 8px;
            }

            .artifact-btn {
                background: rgba(255,255,255,0.08);
                border: none;
                color: #fff;
                padding: 8px 16px;
                border-radius: 6px;
                font: 13px/1 system-ui, sans-serif;
                display: flex;
                align-items: center;
                gap: 6px;
                cursor: pointer;
                transition: all 0.2s ease;
                min-width: 80px;
                justify-content: center;
            }

            .artifact-btn:hover {
                background: rgba(255,255,255,0.15);
            }

            .artifact-btn:focus {
                outline: 2px solid var(--artifact-accent);
                outline-offset: 2px;
            }

            .artifact-content {
                flex: 1;
                overflow: auto;
                position: relative;
                -webkit-overflow-scrolling: touch;
            }

            .artifact-code {
                font-family: 'Fira Code', 'Consolas', monospace;
                font-size: 14px;
                tab-size: 4;
                margin: 0;
                padding: 20px !important;
                background: transparent !important;
            }

            .artifact-placeholder {
                position: relative;
                background: rgba(0,122,204,0.1);
                color: var(--artifact-accent);
                padding: 12px 20px;
                border-radius: 8px;
                border: 1px solid rgba(0,122,204,0.3);
                font: 500 14px/1.4 system-ui, sans-serif;
                cursor: pointer;
                transition: all 0.2s ease;
                margin: 8px 0;
                user-select: none;
            }

            .artifact-placeholder:hover {
                background: rgba(0,122,204,0.2);
            }

            @media (max-width: ${config.mobileBreakpoint}px) {
                .artifact-panel {
                    width: 100%;
                    height: 95vh !important;
                    border-radius: 0;
                }
                
                .artifact-btn {
                    padding: 12px 18px;
                    min-width: auto;
                }
                
                .artifact-placeholder {
                    padding: 10px 16px;
                    font-size: 13px;
                }
            }

            pre[class*="language-"] {
                background: transparent !important;
                margin: 0 !important;
            }
        `;
    }

    // ==================== CORE COMPONENTS ====================
    function createArtifactPanel() {
        const overlay = document.createElement('div');
        overlay.className = `artifact-overlay ${SCRIPT_ID}-overlay`;
        overlay.setAttribute('role', 'dialog');
        overlay.setAttribute('aria-modal', 'true');
        overlay.setAttribute('aria-labelledby', 'artifact-title');
        
        overlay.innerHTML = `
            <div class="artifact-panel">
                <div class="artifact-header">
                    <div class="artifact-title" id="artifact-title">
                        <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
                            <path d="M14 3v4a1 1 0 0 0 1 1h4l-5-5m-2 14H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5v5a2 2 0 0 0 2 2h5v6a2 2 0 0 1-2 2h-4m-8-4h2m0 0h2m-2 0v-2m0 2v2"/>
                        </svg>
                        Code Artifact Pro
                    </div>
                    <div class="artifact-buttons">
                        <button class="artifact-btn artifact-copy" aria-label="Copy code">
                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <rect x="9" y="9" width="13" height="13" rx="2"/>
                                <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
                            </svg>
                            Copy
                        </button>
                        <button class="artifact-btn artifact-close" aria-label="Close viewer">
                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <line x1="18" y1="6" x2="6" y2="18"/>
                                <line x1="6" y1="6" x2="18" y2="18"/>
                            </svg>
                            Close
                        </button>
                    </div>
                </div>
                <div class="artifact-content">
                    <pre class="artifact-code"><code class="language-plaintext"></code></pre>
                </div>
            </div>
        `;
        
        return overlay;
    }

    // ==================== DOM PROCESSING ====================
    function processMutations() {
        mutationQueue.forEach(({ addedNodes }) => {
            addedNodes.forEach(node => {
                if (node.nodeType === 1) {
                    if (node.matches('pre')) processPreElement(node);
                    node.querySelectorAll('pre').forEach(processPreElement);
                }
            });
        });
        mutationQueue.length = 0;
    }

    function initObserver() {
        if (observer) observer.disconnect();
        
        observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                if (mutation.addedNodes.length) {
                    mutationQueue.push(mutation);
                    clearTimeout(mutationDebounce);
                    mutationDebounce = setTimeout(processMutations, config.debounceTimeout);
                }
            });
        });

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

    function processPreElement(pre) {
        if (processedNodes.has(pre) || pre.closest(`.${SCRIPT_ID}-overlay`)) return;
        processedNodes.add(pre);

        const placeholder = document.createElement('div');
        placeholder.className = 'artifact-placeholder';
        placeholder.textContent = 'View Code Artifact';
        pre.style.display = 'none';
        pre.parentNode.insertBefore(placeholder, pre);
    }

    // ==================== PANEL CONTROLS ====================
    async function showCode(pre) {
        if (!state.prismLoaded) {
            showNotification('Syntax highlighter not ready yet. Please try again.', true);
            return;
        }

        if (pre.textContent.length > config.maxCodeSize) {
            showNotification('Code exceeds maximum display size (100KB)', true);
            return;
        }

        state.lastFocusedElement = document.activeElement;
        const codeBlock = artifactPanel.querySelector('code');
        const language = detectLanguage(pre);
        
        codeBlock.className = `language-${language}`;
        codeBlock.textContent = pre.textContent.trim();
        
        try {
            Prism.highlightElement(codeBlock);
        } catch (error) {
            codeBlock.className = 'language-plaintext';
            console.warn('Prism highlighting failed:', error);
        }
        
        showPanel();
    }

    function showPanel() {
        artifactPanel.classList.add('visible');
        artifactPanel.querySelector('.artifact-panel').classList.add('active');
        document.documentElement.style.overflow = 'hidden';
        document.addEventListener('keydown', handleKeyPress);
        adjustPanelHeight();
        state.isPanelOpen = true;
    }

    function hidePanel() {
        artifactPanel.classList.remove('visible');
        artifactPanel.querySelector('.artifact-panel').classList.remove('active');
        document.documentElement.style.overflow = '';
        document.removeEventListener('keydown', handleKeyPress);
        artifactPanel.querySelector('code').textContent = '';
        
        if (state.lastFocusedElement) {
            state.lastFocusedElement.focus({ preventScroll: true });
        }
        state.isPanelOpen = false;
    }

    function handleKeyPress(event) {
        if (event.key === 'Escape') hidePanel();
        if (event.key === 'Tab') maintainFocus(event);
    }

    function maintainFocus(event) {
        const focusable = [...artifactPanel.querySelectorAll('button')];
        const first = focusable[0];
        const last = focusable[focusable.length - 1];

        if (event.shiftKey && document.activeElement === first) {
            last.focus();
            event.preventDefault();
        } else if (!event.shiftKey && document.activeElement === last) {
            first.focus();
            event.preventDefault();
        }
    }

    // ==================== UTILITIES ====================
    function detectLanguage(pre) {
        let languageElement = pre.closest('.md-code-block-banner-wrap')?.querySelector('.md-code-block-infostring');
        let parent = pre.parentElement;
        
        while (parent && !languageElement) {
            languageElement = parent.querySelector('.md-code-block-infostring');
            parent = parent.parentElement;
        }

        const lang = languageElement?.textContent.trim().toLowerCase() || 
                    Array.from(pre.classList).find(c => c.startsWith('language-'))?.split('-')[1] || 
                    'plaintext';
        
        return Prism.languages[lang] ? lang : 'plaintext';
    }

    async function handleCopy() {
        const code = artifactPanel.querySelector('code').textContent;
        try {
            await navigator.clipboard.writeText(code);
            showNotification('Code copied to clipboard!');
        } catch (err) {
            legacyCopy(code);
        }
    }

    function legacyCopy(text) {
        const textarea = document.createElement('textarea');
        textarea.value = text;
        textarea.style.position = 'fixed';
        document.body.appendChild(textarea);
        textarea.select();
        
        try {
            const success = document.execCommand('copy');
            if (!success) throw new Error('Copy failed');
            showNotification('Code copied to clipboard!');
        } catch (err) {
            showNotification('Failed to copy! Please copy manually.', true);
        } finally {
            document.body.removeChild(textarea);
        }
    }

    function showNotification(message, isError = false) {
        if (typeof GM_notification === 'function') {
            GM_notification({
                title: isError ? 'Error' : 'Success',
                text: message,
                timeout: 2000
            });
        } else {
            const notification = document.createElement('div');
            notification.className = `artifact-notification ${isError ? 'error' : 'success'}`;
            notification.textContent = message;
            document.body.appendChild(notification);
            
            setTimeout(() => {
                notification.remove();
            }, 2000);
        }
    }

    function adjustPanelHeight() {
        const panel = artifactPanel.querySelector('.artifact-panel');
        panel.style.height = `${window.innerHeight * config.panelHeightRatio}px`;
    }

    // ==================== LIFECYCLE MANAGEMENT ====================
    async function init() {
        try {
            await loadPrism();
            ensureStyles();
            artifactPanel = createArtifactPanel();
            document.body.appendChild(artifactPanel);
            
            document.body.addEventListener('click', event => {
                const target = event.target;
                
                if (target.closest('.artifact-placeholder')) {
                    showCode(target.closest('.artifact-placeholder').nextElementSibling);
                }
                else if (target.closest('.artifact-close')) {
                    hidePanel();
                }
                else if (target.closest('.artifact-copy')) {
                    handleCopy();
                }
            });

            initObserver();
            document.querySelectorAll('pre').forEach(processPreElement);
            window.addEventListener('resize', adjustPanelHeight);
        } catch (error) {
            console.error('Initialization error:', error);
            showNotification('Failed to initialize code viewer!', true);
        }
    }

    function cleanup() {
        if (observer) observer.disconnect();
        artifactPanel?.remove();
        document.querySelector(`style[data-css="${SCRIPT_ID}"]`)?.remove();
        window.removeEventListener('resize', adjustPanelHeight);
        document.removeEventListener('keydown', handleKeyPress);
    }

    // ==================== INITIALIZATION ====================
    if (!window[SCRIPT_ID]) {
        window[SCRIPT_ID] = true;
        
        const run = async () => {
            if (document.readyState === 'complete') {
                await init();
            } else {
                window.addEventListener('load', async () => {
                    await init();
                });
            }
        };

        run().catch(error => {
            console.error('Error initializing script:', error);
            showNotification('Failed to load code viewer!', true);
        });
        
        window.addEventListener('unload', cleanup);
        document.addEventListener('visibilitychange', () => {
            if (document.visibilityState === 'hidden') cleanup();
        });
    }
})();