AoPS Alcumus Problem Translator

Translates Alcumus problem descriptions to Chinese using free LLM.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         AoPS Alcumus Problem Translator
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  Translates Alcumus problem descriptions to Chinese using free LLM.
// @author       Dakai
// @match        https://artofproblemsolving.com/alcumus/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js
// @resource     KATEX_CSS https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // *** IMPORTANT:  REPLACE WITH YOUR ACTUAL GEMINI API KEY ***
    const GEMINI_API_KEY = ""; // Replace with your actual API key
    const OPENROUTER_API_KEY=""
    const OPENROUTER_MODELS = ['x-ai/grok-4.1-fast:free', 'moonshotai/kimi-k2:free', 'mistralai/mistral-small-3.1-24b-instruct:free', 'google/gemini-2.0-flash-exp:free'];

   // Load KaTeX CSS
    const katexCSS = GM_getResourceText("KATEX_CSS");
    GM_addStyle(katexCSS);
     // Styling for the button
    GM_addStyle(`
        .translate-button {
            background-color: #4CAF50; /* Green */
            border: none;
            color: white;
            padding: 5px 10px;
            text-align: center;
            text-decoration: none;
            display: inline-block;
            font-size: 14px;
            margin: 4px 2px;
            cursor: pointer;
            border-radius: 5px; /* Rounded corners */
        }

        .translated-text {
            margin-top: 10px;
            padding: 10px;
            border: 1px solid #ccc;
            background-color: #f9f9f9;
            white-space: pre-wrap; /*  Preserve line breaks AND wrap text*/
        }
    `);

        let currentOpenRouterModelIndex = 0; // Keep track of the current model

        function translateWithOpenRouter(text, callback) {
        if (!OPENROUTER_API_KEY) {
            callback("Translation failed: OpenRouter API key is missing.");
            return;
        }

        const model = OPENROUTER_MODELS[currentOpenRouterModelIndex];
        const apiUrl = "https://openrouter.ai/api/v1/chat/completions";

        const requestData = {
            model: model,
            messages: [{ role: "user",
                                    content: `Translate the following English text to Simplified Chinese. Keep all LaTeX math expressions in their original form surrounded by $ for inline math and $$ for display math. Only translate the non-mathematical English text: ${text}`

                       }],
        };

        GM_xmlhttpRequest({
            method: "POST",
            url: apiUrl,
            headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${OPENROUTER_API_KEY}`,
                "HTTP-Referer": "https://artofproblemsolving.com", // Required by OpenRouter
                "X-Title": "AoPS Alcumus Translator" // Required by OpenRouter
            },
            data: JSON.stringify(requestData),
            onload: function(response) {
                if (response.status >= 200 && response.status < 300) {
                    try {
                        const jsonResponse = JSON.parse(response.responseText);
                        if (jsonResponse.choices && jsonResponse.choices.length > 0 && jsonResponse.choices[0].message && jsonResponse.choices[0].message.content) {
                            const translatedText = jsonResponse.choices[0].message.content;
                            callback(translatedText);
                        } else {
                            callback(`Translation failed: OpenRouter - Unexpected API response format (Model: ${model})`);
                            console.error("OpenRouter - Unexpected API response format:", jsonResponse, model);
                        }
                    } catch (e) {
                        callback(`Translation failed: OpenRouter - Could not parse API response (Model: ${model})`);
                        console.error("OpenRouter - Error parsing API response:", e, response.responseText, model);
                    }
                } else {
                    callback(`Translation failed: OpenRouter - API error ${response.status} - ${response.statusText} (Model: ${model})`);
                    console.error("OpenRouter - API error:", response.status, response.statusText, response.responseText, model);
                }
            },
            onerror: function(error) {
                callback(`Translation failed: OpenRouter - Network error (Model: ${model})`);
                console.error("OpenRouter - Network error:", error, model);
            }
        });
    }



    function translateToChinese(text, callback) {
        if (GEMINI_API_KEY)  { // Try Gemini first if a key is provided
        const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent?key=${GEMINI_API_KEY}`;

            const requestData = {
                contents: [{
                    parts: [{
                      text: `Translate the following English text to Simplified Chinese. Keep all LaTeX math expressions in their original form surrounded by $ for inline math and $$ for display math. Only translate the non-mathematical English text: ${text}`
                    }]
                }]
            };

            GM_xmlhttpRequest({
                method: "POST",
                url: apiUrl,
                headers: {
                    "Content-Type": "application/json"
                },
                data: JSON.stringify(requestData),
                onload: function(response) {
                    if (response.status === 503) {
                       console.warn("Gemini API overloaded, falling back to OpenRouter.");
                        //If gemini failed, immediately proceed to OpenRouter.
                        currentOpenRouterModelIndex = 0;  //Reset to the first model
                        translateWithOpenRouter(text, callback);

                    } else if (response.status >= 200 && response.status < 300) {
                        try {
                            const jsonResponse = JSON.parse(response.responseText);
                            if (jsonResponse.candidates && jsonResponse.candidates.length > 0 && jsonResponse.candidates[0].content && jsonResponse.candidates[0].content.parts && jsonResponse.candidates[0].content.parts.length > 0) {
                                const translatedText = jsonResponse.candidates[0].content.parts[0].text;
                                callback(translatedText);
                            } else {
                                 console.warn("Gemini API returned unexpected format, falling back to OpenRouter.");
                                currentOpenRouterModelIndex = 0;  //Reset to the first model
                                translateWithOpenRouter(text, callback);
                            }
                        } catch (e) {
                            console.warn("Gemini API returned invalid JSON, falling back to OpenRouter.");
                            currentOpenRouterModelIndex = 0;  //Reset to the first model
                            translateWithOpenRouter(text, callback);
                        }
                    } else {
                        console.warn("Gemini API error, falling back to OpenRouter.");
                        currentOpenRouterModelIndex = 0;  //Reset to the first model
                        translateWithOpenRouter(text, callback);
                    }
                },
                onerror: function(error) {
                    console.warn("Gemini API network error, falling back to OpenRouter.");
                    currentOpenRouterModelIndex = 0;  //Reset to the first model
                    translateWithOpenRouter(text, callback);
                }
            });
        } else {
            console.log("No Gemini API key provided, using OpenRouter directly.");
            translateWithOpenRouter(text, callback); //If no Gemini Key is provided, proceed to OpenRouter.
        }
    }

    function getTextWithAlt(element) {
        let text = "";
        for (const node of element.childNodes) {
            if (node.nodeType === Node.TEXT_NODE) {
                text += node.textContent;
            } else if (node.nodeType === Node.ELEMENT_NODE) {
                if (node.tagName === 'IMG') {
                    if (node.alt) {
                        text += node.alt; // Append the alt text
                    }
                } else {
                    text += getTextWithAlt(node); // Recursive call for other elements
                }
            }
        }
        console.log(text);
        return text;
    }


    function renderKaTeX(element) {
        if (typeof renderMathInElement === 'undefined') {
            console.error('KaTeX auto-render not loaded');
            return;
        }

        try {
            // Use KaTeX auto-render to find and render all math
            renderMathInElement(element, {
                delimiters: [
                    {left: '$$', right: '$$', display: true},
                    {left: '$', right: '$', display: false},
                ],
                throwOnError: false,
                errorColor: '#cc0000'
            });
            console.log('✓ KaTeX rendering complete!');
        } catch (err) {
            console.error('✗ KaTeX rendering error:', err);
        }
    }


    function displayTranslatedText(translatedText, translatedTextDiv) {
        translatedTextDiv.textContent = translatedText; // Use textContent first to avoid HTML injection
        translatedTextDiv.style.display = "block";

        console.log('📝 Content to render:', translatedText);
        console.log('📝 KaTeX available:', typeof katex !== 'undefined');

        // Render math with KaTeX
        if (typeof katex !== 'undefined') {
            // Simple approach: manually parse and render
            renderMathInText(translatedTextDiv, translatedText);
        } else {
            console.error('✗ KaTeX not loaded!');
        }
    }

    function renderMathInText(element, text) {
        // Clear the element
        element.innerHTML = '';

        // Regular expression to match $...$ and $$...$$
        const mathRegex = /(\$\$[\s\S]+?\$\$|\$[^\$\n]+?\$)/g;

        let lastIndex = 0;
        let match;

        while ((match = mathRegex.exec(text)) !== null) {
            // Add text before the math
            if (match.index > lastIndex) {
                const textNode = document.createTextNode(text.substring(lastIndex, match.index));
                element.appendChild(textNode);
            }

            // Render the math
            const mathText = match[0];
            const isDisplay = mathText.startsWith('$$');
            const mathContent = isDisplay ? mathText.slice(2, -2) : mathText.slice(1, -1);

            const span = document.createElement('span');
            try {
                katex.render(mathContent, span, {
                    displayMode: isDisplay,
                    throwOnError: false,
                    errorColor: '#cc0000'
                });
                console.log('✓ Rendered:', mathContent.substring(0, 30) + '...');
            } catch (err) {
                console.error('✗ Failed to render:', mathContent, err);
                span.textContent = mathText; // Fallback to original text
            }
            element.appendChild(span);

            lastIndex = match.index + match[0].length;
        }

        // Add remaining text
        if (lastIndex < text.length) {
            const textNode = document.createTextNode(text.substring(lastIndex));
            element.appendChild(textNode);
        }
    }

  function addButton(problemTextDiv) {
        if (problemTextDiv) {
            const translateButton = document.createElement("button");
            translateButton.classList.add("translate-button");
            translateButton.textContent = "Translate to Chinese";
            problemTextDiv.parentNode.insertBefore(translateButton, problemTextDiv.nextSibling);

            const translatedTextDiv = document.createElement("div");
            translatedTextDiv.classList.add("translated-text");
            translatedTextDiv.style.display = "none";
            problemTextDiv.parentNode.insertBefore(translatedTextDiv, translateButton.nextSibling);

            translateButton.addEventListener("click", function() {
                const problemText = getTextWithAlt(problemTextDiv);
                translateButton.disabled = true;
                translateButton.textContent = "Translating...";

                translateToChinese(problemText, function(translatedText) {
                    displayTranslatedText(translatedText, translatedTextDiv);
                    translateButton.textContent = "Translate to Chinese";
                    translateButton.disabled = false;
                });
            });
        }
    }

    function observeForProblemText() {
        const observer = new MutationObserver(function(mutations) {
            const problemTextDiv = document.querySelector(".alc-problem-text");
            if (problemTextDiv) {
                addButton(problemTextDiv);
                observer.disconnect();
            }
        });

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

    window.addEventListener("load", observeForProblemText);
})();