大模型竞技场代码下载器 (通用修复版)

在 LMArena.ai 页面为代码块添加下载按钮。通用修复版,兼容所有脚本管理器。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         LMArena Code Downloader (Universal Fix)
// @name:zh-CN   大模型竞技场代码下载器 (通用修复版)
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Adds a download button to code blocks on lmarena.ai. Universal fix for all script managers.
// @description:zh-CN 在 LMArena.ai 页面为代码块添加下载按钮。通用修复版,兼容所有脚本管理器。
// @author       AI & Human
// @match        https://lmarena.ai/*
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWRvd25sb2FkIj48cGF0aCBkPSJNMjEgMTV2NGEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMlYxNSIvPjxwYXRoIGQ9Im03IDEwIDEgNSA1LTUtNSIvPjxwYXRoIGQ9Ik0xMiAxNVY0Ii8+PC9zdmc+
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // --- 辅助函数 ---

    /**
     * 向页面添加全局CSS样式,替代GM_addStyle,兼容所有环境。
     * @param {string} css - 要添加的CSS规则字符串。
     */
    function addGlobalStyle(css) {
        const head = document.head || document.getElementsByTagName('head')[0];
        if (!head) { return; }
        const style = document.createElement('style');
        style.type = 'text/css';
        style.innerHTML = css;
        head.appendChild(style);
    }

    function getFileExtension(preElement) {
        const codeElement = preElement.querySelector('code');
        if (!codeElement) return 'txt';

        const className = codeElement.className || '';
        const match = className.match(/language-(\w+)/);
        if (match && match[1]) {
            const langMap = {
                python: 'py', javascript: 'js', typescript: 'ts', html: 'html',
                css: 'css', java: 'java', csharp: 'cs', cpp: 'cpp', c: 'c',
                bash: 'sh', shell: 'sh', json: 'json', markdown: 'md',
                go: 'go', rust: 'rs', sql: 'sql', yaml: 'yml', xml: 'xml',
            };
            return langMap[match[1].toLowerCase()] || match[1];
        }
        return 'txt';
    }

    function getModelName(preElement) {
        const messageContainer = preElement.closest('div.flex.gap-6');
        if (messageContainer) {
            const modelNameElement = messageContainer.querySelector('p.font-mono');
            if (modelNameElement) {
                return modelNameElement.textContent.trim().replace(/[^a-zA-Z0-9-.]/g, '_');
            }
        }
        return 'unknown-model';
    }

    function downloadCode(codeContent, filename) {
        const blob = new Blob([codeContent], { type: 'text/plain;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    // --- 核心逻辑 ---

    function addDownloadButtons() {
        // 使用更精确的选择器,专门针对聊天内容区的代码块
        const codeBlocks = document.querySelectorAll('main ol div.prose pre:not([data-download-button-added])');

        if (codeBlocks.length > 0) {
            console.log(`[LMArena Downloader] Found ${codeBlocks.length} new code blocks to process.`);
        }

        codeBlocks.forEach(pre => {
            pre.setAttribute('data-download-button-added', 'true');

            // 按钮将被添加到<pre>标签内的相对定位的div中
            const relativeContainer = pre.querySelector('div.relative');
            if (!relativeContainer) {
                console.error('[LMArena Downloader] Could not find relative container for a code block.');
                return;
            }

            // 找到原生的复制按钮,我们的按钮会放在它旁边
            const nativeCopyButton = relativeContainer.querySelector('button[data-sentry-component="CopyButton"]');
            if (!nativeCopyButton) {
                console.warn('[LMArena Downloader] Could not find the native copy button. The download button will be appended at the end.');
            }

            const downloadButton = document.createElement('button');
            downloadButton.className = 'code-downloader-button';
            downloadButton.title = 'Download code';
            downloadButton.innerHTML = `
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V15"/>
                    <polyline points="7 10 12 15 17 10"/>
                    <line x1="12" y1="15" x2="12" y2="3"/>
                </svg>
            `;

            downloadButton.addEventListener('click', (e) => {
                e.stopPropagation();
                const codeElement = pre.querySelector('code');
                const codeContent = codeElement ? (codeElement.textContent || '') : '';
                const extension = getFileExtension(pre);
                const modelName = getModelName(pre);
                const filename = `${modelName}-${Date.now()}.${extension}`;
                downloadCode(codeContent, filename);
            });

            // 如果找到了复制按钮,就把下载按钮插在它前面,否则就添加到容器末尾
            if (nativeCopyButton) {
                nativeCopyButton.parentElement.insertBefore(downloadButton, nativeCopyButton);
            } else {
                relativeContainer.appendChild(downloadButton);
            }
        });
    }

    // --- 启动与监听 ---

    // 1. 先注入样式
    addGlobalStyle(`
        .code-downloader-button {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            padding: 6px;
            border-radius: 6px;
            cursor: pointer;
            color: var(--interactive-active);
            transition: background-color 0.2s, color 0.2s;
            border: none;
            background-color: transparent;
        }
        .code-downloader-button:hover {
            background-color: var(--surface-hover, #f0f0f0);
            color: var(--text-primary, #000);
        }
        .code-downloader-button svg {
            width: 18px;
            height: 18px;
        }
    `);


    // 2. 使用 MutationObserver 监听 DOM 变化
    const observer = new MutationObserver((mutations) => {
        if (mutations.some(m => m.addedNodes.length > 0)) {
            setTimeout(addDownloadButtons, 300);
        }
    });

    // 3. 等待核心聊天区域出现后,再开始执行和监听
    const startupInterval = setInterval(() => {
        const chatArea = document.querySelector('main ol');
        if (chatArea) {
            console.log('[LMArena Downloader] Chat area found. Initializing script.');
            clearInterval(startupInterval);

            addDownloadButtons(); // 立即执行一次

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

})();