Chess.com Stockfish Bot (v1.161 Smooth Drag + WASM NNUE)

chess

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
/*
 * Copyright (c) 2026 [email protected] / proprietary
 * All rights reserved.
 *
 * This code is proprietary and confidential.
 *
 * 1. USE: You are permitted to execute and use this software for personal purposes.
 * 2. MODIFICATION: You are NOT permitted to modify, merge, publish, distribute,
 * sublicense, and/or sell copies of this software.
 * 3. DISTRIBUTION: You are NOT permitted to distribute this software or derivative
 * works of this software.
 */
// @name         Chess.com Stockfish Bot (v1.161 Smooth Drag + WASM NNUE)
// @namespace    BottleOrg Scripts
// @version      5.1
// @description  chess
// @author       BottleOrg
// @match        https://www.chess.com/*
// @icon         https://www.chess.com/bundles/web/images/offline-play/standardboard.1d6f9426.png
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// @run-at       document-start
// @license      All Rights Reserved
// ==/UserScript==

(function() {
    'use strict';

    const STATE = {
        isCoach: false,
        showThreats: false,
        isMove: false,
        depth: 15,
        humanDelay: 400,
        isThinking: false,
        lastFen: "",
        bestMove: null,
        threatMove: null,
        hasMovedThisTurn: false
    };

    let arrowLayer = null;
    let engineWorker = null;
    let engineQueue =[];
    let engineBusy = false;

    function saveState() {
        GM_setValue('sf_state_wasm_v1', JSON.stringify({
            isCoach: STATE.isCoach,
            showThreats: STATE.showThreats,
            isMove: STATE.isMove,
            depth: STATE.depth,
            humanDelay: STATE.humanDelay
        }));
    }

    function loadState() {
        const saved = GM_getValue('sf_state_wasm_v1');
        if (saved) {
            try {
                const p = JSON.parse(saved);
                if(p.isCoach !== undefined) STATE.isCoach = p.isCoach;
                if(p.showThreats !== undefined) STATE.showThreats = p.showThreats;
                if(p.isMove !== undefined) STATE.isMove = p.isMove;
                if(p.depth !== undefined) STATE.depth = p.depth;
                if(p.humanDelay !== undefined) STATE.humanDelay = p.humanDelay;
            } catch (e) { console.warn("Failed to load settings."); }
        }
    }
    loadState();

    function initEngine() {
        updateStatus('Loading JS Engine...');

        GM_xmlhttpRequest({
            method: "GET",
            url: "https://unpkg.com/[email protected]/src/stockfish-nnue-16-single.js",
            onload: (resJs) => {
                if (resJs.status !== 200) {
                    updateStatus('Error: CDN Blocked');
                    return;
                }
                
                let jsCode = resJs.responseText;

                jsCode = jsCode.replace(/import\.meta\.url/g, '"https://unpkg.com/[email protected]/src/stockfish-nnue-16-single.js"');
                jsCode = jsCode.replace(/stockfish-nnue-16-single\.wasm/g, "https://unpkg.com/[email protected]/src/stockfish-nnue-16-single.wasm");
                jsCode = jsCode.replace(/stockfish-nnue-16-single\.nnue/g, "https://unpkg.com/[email protected]/src/stockfish-nnue-16-single.nnue");

                const blobCode = `
                    ${jsCode}

                    if (typeof Stockfish === "function") {
                        Stockfish().then(function(engine) {
                            engine.addMessageListener(function(msg) {
                                postMessage(msg);
                            });
                            self.onmessage = function(ev) {
                                engine.postMessage(ev.data);
                            };
                            postMessage("readyok_custom");
                        }).catch(function(err) {
                            postMessage("error: " + err);
                        });
                    }
                `;

                try {
                    const blob = new Blob([blobCode], { type: "application/javascript" });
                    engineWorker = new Worker(URL.createObjectURL(blob));
                } catch(e) {
                    updateStatus('Error: Worker Blocked');
                    return;
                }

                engineWorker.onmessage = (e) => {
                    const msg = (e.data + "").trim();
                    if (msg === "readyok_custom") {
                        engineWorker.postMessage("uci");
                    } else if (msg === "uciok") {
                        engineWorker.postMessage("setoption name Use NNUE value false");
                        engineWorker.postMessage("isready");
                    } else if (msg === "readyok") {
                        updateStatus('Ready');
                    } else if (msg.startsWith("bestmove")) {
                        const parts = msg.split(" ");
                        const move = parts[1] || "0000";
                        if (engineQueue.length > 0) {
                            const task = engineQueue.shift();
                            engineBusy = false;
                            STATE.isThinking = engineQueue.length > 0;
                            updateStatus(STATE.isThinking ? 'Thinking...' : 'Ready');
                            task.resolve({ move: move.toLowerCase(), evalStr: task.evalStr });
                            processQueue();
                        }
                    } else if (msg.includes("score cp") || msg.includes("score mate")) {
                        const scoreMatch = msg.match(/score (cp|mate) (-?\d+)/);
                        if (scoreMatch && engineQueue.length > 0) {
                            const type = scoreMatch[1];
                            let val = parseInt(scoreMatch[2], 10);
                            let evalStr = "";
                            const isBlack = engineQueue[0].fen.split(' ')[1] === 'b';

                            if (type === "mate") {
                                let displayVal = val;
                                if (isBlack) displayVal = -val;
                                evalStr = (displayVal > 0 ? "+M" : "-M") + Math.abs(displayVal);
                            } else {
                                if (isBlack) val = -val;
                                evalStr = (val > 0 ? "+" : "") + (val / 100).toFixed(2);
                            }

                            engineQueue[0].evalStr = evalStr;
                            if (!engineQueue[0].isThreat) {
                                document.getElementById('val-eval').innerText = evalStr;
                            }
                        }
                    }
                };
            },
            onerror: () => updateStatus('Error: Network JS')
        });
    }

    function fetchEngineMove(fen, isThreat = false) {
        return new Promise((resolve) => {
            const timeout = setTimeout(() => {
                if (engineQueue[0] && engineQueue[0].resolve === resolve) {
                    engineQueue.shift();
                    engineBusy = false;
                    resolve({ move: "0000", evalStr: "0.00" });
                    processQueue();
                }
            }, 30000);

            engineQueue.push({
                resolve: (res) => { clearTimeout(timeout); resolve(res); },
                fen,
                evalStr: "0.00",
                isThreat
            });
            processQueue();
        });
    }

    function processQueue() {
        if (engineBusy || engineQueue.length === 0 || !engineWorker) return;
        engineBusy = true;
        STATE.isThinking = true;
        updateStatus('Thinking...');

        const task = engineQueue[0];
        engineWorker.postMessage(`position fen ${task.fen}`);
        engineWorker.postMessage(`go depth ${STATE.depth}`);
    }

    function createArrowLayer() {
        if (!document.getElementById('stockfish-arrows')) {
            arrowLayer = document.createElement('div');
            arrowLayer.id = 'stockfish-arrows';
            arrowLayer.style.cssText = `
                position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
                pointer-events: none !important; z-index: 9999;
            `;
            document.body.appendChild(arrowLayer);
        }
    }

    function clearArrows() {
        if (arrowLayer) arrowLayer.innerHTML = '';
    }

    function renderArrows() {
        clearArrows();
        if (STATE.showThreats && STATE.threatMove) {
            drawArrowSvg(STATE.threatMove.from, STATE.threatMove.to, '#e74c3c', 20, 0.5);
        }
        if (STATE.isCoach && STATE.bestMove) {
            drawArrowSvg(STATE.bestMove.from, STATE.bestMove.to, '#81b64c', 10, 0.9);
        }
    }

    function drawArrowSvg(fromSq, toSq, color, width, opacity) {
        createArrowLayer();
        const start = getSquareCoords(fromSq);
        const end = getSquareCoords(toSq);
        if (!start || !end) return;

        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.style.cssText = "width: 100%; height: 100%; position: absolute; left: 0; top: 0; pointer-events: none !important;";

        const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
        const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
        const markerId = "arrowhead" + color.replace('#', '') + width;

        marker.setAttribute("id", markerId);
        marker.setAttribute("markerWidth", "4");
        marker.setAttribute("markerHeight", "4");
        marker.setAttribute("refX", "2");
        marker.setAttribute("refY", "2");
        marker.setAttribute("orient", "auto");

        const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
        polygon.setAttribute("points", "0 0, 4 2, 0 4");
        polygon.setAttribute("fill", color);
        polygon.setAttribute("pointer-events", "none");

        marker.appendChild(polygon);
        defs.appendChild(marker);
        svg.appendChild(defs);

        const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
        line.setAttribute("x1", start.x);
        line.setAttribute("y1", start.y);
        line.setAttribute("x2", end.x);
        line.setAttribute("y2", end.y);
        line.setAttribute("stroke", color);
        line.setAttribute("stroke-width", width.toString());
        line.setAttribute("stroke-opacity", opacity.toString());
        line.setAttribute("stroke-linecap", "round");
        line.setAttribute("marker-end", `url(#${markerId})`);
        line.setAttribute("pointer-events", "none");

        svg.appendChild(line);
        arrowLayer.appendChild(svg);
    }

    function getBoard() {
        let board = document.querySelector('wc-chess-board') || document.querySelector('chess-board') || document.getElementById('board-single');
        if (board && !board.game && window.unsafeWindow) {
            const unsafeBoard = unsafeWindow.document.querySelector('wc-chess-board') || unsafeWindow.document.querySelector('chess-board');
            if (unsafeBoard && unsafeBoard.game) return unsafeBoard;
        }
        return board;
    }

    function getBoardOrientation() {
        const board = getBoard();
        if (board && board.classList && board.classList.contains('flipped')) return 'black';
        return 'white';
    }

    function getSquareCoords(square) {
        const board = document.querySelector('wc-chess-board') || document.querySelector('chess-board');
        if (!board) return null;
        const rect = board.getBoundingClientRect();
        if (rect.width < 10) return null;

        const size = rect.width / 8;
        const file = square.charCodeAt(0) - 97;
        const rank = square.charCodeAt(1) - 49;
        const isBlack = getBoardOrientation() === 'black';

        let xIdx = isBlack ? (7 - file) : file;
        let yIdx = isBlack ? rank : (7 - rank);

        return { x: rect.left + (xIdx * size) + (size / 2), y: rect.top + (yIdx * size) + (size / 2) };
    }

    function dispatchPointerEvent(type, x, y) {
        const target = document.elementFromPoint(x, y) || document.body;
        const isDown = type.includes('down') || type.includes('move');

        const ev = new PointerEvent(type, {
            clientX: x, clientY: y,
            bubbles: true, cancelable: true, composed: true,
            pointerId: 1, pointerType: 'mouse', isPrimary: true,
            button: 0, buttons: isDown ? 1 : 0
        });
        target.dispatchEvent(ev);

        if (type !== 'pointercancel') {
            const mouseType = type.replace('pointer', 'mouse');
            target.dispatchEvent(new MouseEvent(mouseType, {
                clientX: x, clientY: y, bubbles: true, cancelable: true, composed: true,
                button: 0, buttons: isDown ? 1 : 0
            }));
        }
    }

    function simulateClick(x, y) {
        dispatchPointerEvent('pointerdown', x, y);
        dispatchPointerEvent('pointerup', x, y);
        const el = document.elementFromPoint(x, y) || document.body;
        el.dispatchEvent(new MouseEvent('click', { clientX: x, clientY: y, bubbles: true, cancelable: true }));
    }

    function movePiece(from, to, promoChar) {
        const start = getSquareCoords(from);
        const end = getSquareCoords(to);
        if (!start || !end) return;

        if (arrowLayer) arrowLayer.hidden = true;

        setTimeout(() => {
            dispatchPointerEvent('pointerdown', start.x, start.y);

            const steps = 15;
            const stepDelay = Math.max(5, (STATE.humanDelay * 0.5) / steps);

            for (let i = 1; i <= steps; i++) {
                setTimeout(() => {
                    const curX = start.x + (end.x - start.x) * (i / steps);
                    const curY = start.y + (end.y - start.y) * (i / steps);
                    dispatchPointerEvent('pointermove', curX, curY);

                    if (i === steps) {
                        dispatchPointerEvent('pointerup', end.x, end.y);

                        setTimeout(() => {
                            simulateClick(start.x, start.y);
                            setTimeout(() => {
                                simulateClick(end.x, end.y);
                                setTimeout(() => { STATE.hasMovedThisTurn = false; }, 2000);
                            }, 20);

                            if (arrowLayer) arrowLayer.hidden = false;

                            if (promoChar) {
                                setTimeout(() => {
                                    let prefix = from.charAt(1) === '7' ? 'w' : 'b';
                                    let sels =[
                                        `.promotion-piece.${prefix}${promoChar}`,
                                        `.promotion-piece.${promoChar}`,
                                        `.promotion-piece.wq, .promotion-piece.bq`,
                                        `.promotion-window .${promoChar}`
                                    ];
                                    for (let sel of sels) {
                                        const els = document.querySelectorAll(sel);
                                        if (els.length > 0) {
                                            const rect = els[0].getBoundingClientRect();
                                            const px = rect.left + rect.width / 2;
                                            const py = rect.top + rect.height / 2;
                                            simulateClick(px, py);
                                            break;
                                        }
                                    }
                                }, 300);
                            }
                        }, 50);
                    }
                }, i * stepDelay);
            }
        }, Math.random() * (STATE.humanDelay * 0.4));
    }

    function normalizeFen(fen) {
        if (!fen || typeof fen !== 'string') return "";
        let parts = fen.trim().split(/\s+/);
        if (parts.length < 4) return "";
        while (parts.length < 6) {
            if (parts.length === 4) parts.push('0');
            else if (parts.length === 5) parts.push('1');
        }
        return parts.join(' ');
    }

    function getNullMoveFen(fen) {
        let parts = fen.split(' ');
        if (parts.length < 6) return null;
        parts[1] = parts[1] === 'w' ? 'b' : 'w';
        parts[3] = '-';
        if (parts[1] === 'w') parts[5] = (parseInt(parts[5], 10) + 1).toString();
        return parts.join(' ');
    }

    function getFenFromDOM() {
        const board = getBoard();
        if (!board) return null;
        let pieces = board.querySelectorAll('.piece');
        if (!pieces.length) return null;

        let boardArray = Array(8).fill("").map(() => Array(8).fill(""));
        for (let p of pieces) {
            let cls = p.className;
            let matchColorPiece = cls.match(/(w|b)(p|n|b|r|q|k)/);
            let matchSquare = cls.match(/square-(\d)(\d)/);
            if (!matchSquare) matchSquare = cls.match(/sq-(\d)(\d)/);

            if (matchColorPiece && matchSquare) {
                let color = matchColorPiece[1];
                let piece = matchColorPiece[2];
                let file = parseInt(matchSquare[1], 10) - 1;
                let rank = 8 - parseInt(matchSquare[2], 10);
                if(rank >= 0 && rank < 8 && file >= 0 && file < 8) {
                    boardArray[rank][file] = color === 'w' ? piece.toUpperCase() : piece.toLowerCase();
                }
            }
        }

        let fen = "";
        for (let r = 0; r < 8; r++) {
            let empty = 0;
            for (let f = 0; f < 8; f++) {
                if (boardArray[r][f] === "") { empty++; }
                else {
                    if (empty > 0) { fen += empty; empty = 0; }
                    fen += boardArray[r][f];
                }
            }
            if (empty > 0) fen += empty;
            if (r < 7) fen += "/";
        }

        let turn = 'w';
        const bottomClock = document.querySelector('.clock-bottom');
        if (bottomClock) {
            if (bottomClock.classList.contains('clock-player-turn') || bottomClock.classList.contains('is-active')) {
                turn = getBoardOrientation() === 'white' ? 'w' : 'b';
            } else {
                turn = getBoardOrientation() === 'white' ? 'b' : 'w';
            }
        }
        return fen + ` ${turn} - - 0 1`;
    }

    async function processTurnSequentially(fen, isMyTurn) {
        if (!engineWorker) return;

        try {
            if (isMyTurn && (STATE.isCoach || STATE.isMove)) {
                const res = await fetchEngineMove(fen, false);
                const m = res.move;
                if (m && m !== "0000" && m !== "(none)") {
                    STATE.bestMove = { from: m.substring(0, 2), to: m.substring(2, 4), promo: m.length > 4 ? m.substring(4, 5) : null };
                    renderArrows();
                    if (STATE.isMove && !STATE.hasMovedThisTurn) {
                        STATE.hasMovedThisTurn = true;
                        movePiece(STATE.bestMove.from, STATE.bestMove.to, STATE.bestMove.promo);
                    }
                }
            }

            if (STATE.showThreats || !isMyTurn) {
                let threatFen = isMyTurn ? getNullMoveFen(fen) : fen;
                if (threatFen) {
                    const isActualEval = !isMyTurn;
                    const res2 = await fetchEngineMove(threatFen, !isActualEval);
                    const m = res2.move;
                    if (m && m !== "0000" && m !== "(none)") {
                        STATE.threatMove = { from: m.substring(0,2), to: m.substring(2,4) };
                        renderArrows();
                    }
                }
            }
        } catch (err) {
            console.error(err);
        }
    }

    function mainLoop() {
        if (STATE.isThinking || !engineWorker) return;
        const board = getBoard();
        if (!board) return;

        let rawFen = (board.game && board.game.getFEN) ? board.game.getFEN() : getFenFromDOM();
        let fen = normalizeFen(rawFen);
        if (!fen) return;

        let myColor = 'w';
        const isPuzzle = window.location.href.includes('puzzle');

        if (!isPuzzle && board.game && typeof board.game.getPlayingAs === 'function') {
            const p = board.game.getPlayingAs();
            if (p === 1 || p === 'w' || p === 'white') myColor = 'w';
            else if (p === 2 || p === 'b' || p === 'black') myColor = 'b';
        } else {
            myColor = getBoardOrientation() === 'white' ? 'w' : 'b';
        }

        const turnStr = fen.split(' ')[1];
        let isMyTurn = (turnStr === myColor);

        if (fen !== STATE.lastFen) {
            STATE.lastFen = fen;
            STATE.bestMove = null;
            STATE.threatMove = null;
            STATE.hasMovedThisTurn = false;
            renderArrows();
            processTurnSequentially(fen, isMyTurn);
        } else {
            if (isMyTurn && STATE.isMove && STATE.bestMove && !STATE.hasMovedThisTurn) {
                STATE.hasMovedThisTurn = true;
                movePiece(STATE.bestMove.from, STATE.bestMove.to, STATE.bestMove.promo);
            } else if (isMyTurn && STATE.isMove && !STATE.bestMove && !STATE.isThinking) {
                processTurnSequentially(fen, isMyTurn);
            }
        }
    }

    function buildGUI() {
        const id = 'stockfish-gui-v8';
        if (document.getElementById(id)) return;

        const div = document.createElement('div');
        div.id = id;
        div.style.cssText = `
            position: fixed; top: 80px; right: 20px; width: 250px;
            background: #1e1e1e; border: 1px solid #3a3a3a; border-radius: 6px;
            color: #e0e0e0; font-family: 'Segoe UI', sans-serif; font-size: 13px;
            z-index: 100000; box-shadow: 0 4px 15px rgba(0,0,0,0.5);
        `;

        div.innerHTML = `
            <div style="padding:10px 12px; background:#252525; border-bottom:1px solid #3a3a3a; font-weight:600; display:flex; align-items:center; gap:10px; cursor:move; border-radius: 6px 6px 0 0;" id="${id}-drag">
                <div id="bot-status" style="width:10px; height:10px; background:#555; border-radius:50%; box-shadow: 0 0 5px #555; transition: 0.3s;"></div>
                <span>Stockfish Local NNUE</span>
            </div>
            <div style="padding:12px;">
                <div style="display:flex; justify-content:space-between; margin-bottom: 12px; font-size: 14px; background: #2a2a2a; padding: 6px; border-radius: 4px;">
                    <span style="color:#aaa;">Evaluation:</span>
                    <strong id="val-eval" style="color:#81b64c;">0.00</strong>
                </div>

                <div style="margin-bottom: 12px; background: #222; padding: 8px; border-radius: 4px; border: 1px solid #444;">
                    <div style="width:100%; padding:5px; background:#2ecc71; color:#fff; border:none; border-radius:4px; text-align:center; font-weight:bold; margin-bottom: 6px;">Local Wasm Engine Active</div>
                    <div style="font-size:11px; color:#aaa; text-align:center;">Stockfish 16.0 (NNUE, 1-Thread)</div>
                </div>

                <div style="display:flex; flex-direction:column; gap:10px;">
                    <label style="display:flex;align-items:center;cursor:pointer;">
                        <input type="checkbox" id="chk-coach" style="margin-right:8px; width:16px; height:16px; accent-color:#81b64c;" ${STATE.isCoach ? 'checked' : ''}> Coach Mode (Green)
                    </label>
                    <label style="display:flex;align-items:center;cursor:pointer;">
                        <input type="checkbox" id="chk-threats" style="margin-right:8px; width:16px; height:16px; accent-color:#e74c3c;" ${STATE.showThreats ? 'checked' : ''}> Show Threats (Red)
                    </label>
                    <label style="display:flex;align-items:center;cursor:pointer;">
                        <input type="checkbox" id="chk-move" style="margin-right:8px; width:16px; height:16px; accent-color:#81b64c;" ${STATE.isMove ? 'checked' : ''}> Auto Move
                    </label>
                </div>

                <div style="margin-top:14px; padding-top:12px; border-top:1px solid #3a3a3a; display:flex; flex-direction:column; gap:12px;">
                    <div style="display:flex; justify-content:space-between; align-items:center;">
                        <span>Depth Cap: <b id="val-depth" style="color:#81b64c;">${STATE.depth}</b></span>
                        <div style="display:flex; gap:6px;">
                            <button id="btn-dm" style="background:#333;color:#fff;border:none;width:26px;height:26px;border-radius:4px;cursor:pointer;font-weight:bold;transition:0.2s;">+</button>
                            <button id="btn-dp" style="background:#333;color:#fff;border:none;width:26px;height:26px;border-radius:4px;cursor:pointer;font-weight:bold;transition:0.2s;">+</button>
                        </div>
                    </div>
                    <div style="display:flex; flex-direction:column; gap:6px;">
                        <div style="display:flex; justify-content:space-between;">
                            <span>Delay: <b id="val-delay" style="color:#81b64c;">${STATE.humanDelay}ms</b></span>
                        </div>
                        <input type="range" id="slider-delay" min="50" max="1500" step="50" value="${STATE.humanDelay}" style="width:100%; accent-color:#81b64c;">
                    </div>
                </div>
            </div>
        `;
        document.body.appendChild(div);

        const bind = (cid, key, customCb) => {
            const el = document.getElementById(cid);
            if(el) el.addEventListener('change', e => { STATE[key] = e.target.checked; saveState(); if(customCb) customCb(); });
        };
        bind('chk-coach', 'isCoach', renderArrows);
        bind('chk-threats', 'showThreats', renderArrows);
        bind('chk-move', 'isMove');

        document.getElementById('btn-dm').onclick = () => { STATE.depth = Math.max(1, STATE.depth-1); document.getElementById('val-depth').innerText = STATE.depth; saveState(); };
        document.getElementById('btn-dp').onclick = () => { STATE.depth = Math.min(20, STATE.depth+1); document.getElementById('val-depth').innerText = STATE.depth; saveState(); };

        document.getElementById('slider-delay').oninput = (e) => {
            STATE.humanDelay = parseInt(e.target.value, 10);
            document.getElementById('val-delay').innerText = STATE.humanDelay + "ms";
            saveState();
        };

        const head = document.getElementById(`${id}-drag`);
        let isDrag = false, sX, sY, iL, iT;
        head.onmousedown = e => { isDrag = true; sX = e.clientX; sY = e.clientY; const r = div.getBoundingClientRect(); iL = r.left; iT = r.top; };
        document.onmousemove = e => { if (isDrag) { div.style.left = (iL + e.clientX - sX) + 'px'; div.style.top = (iT + e.clientY - sY) + 'px'; }};
        document.onmouseup = () => { isDrag = false; };
    }

    function updateStatus(status) {
        const el = document.getElementById('bot-status');
        if (!el) return;
        const colors = {
            'Thinking...': '#f1c40f',
            'Ready': '#2ecc71',
            'Waiting...': '#3498db',
            'Error: Network JS': '#e74c3c',
            'Error: Worker Blocked': '#e74c3c',
            'Error: CDN Blocked': '#e74c3c',
            'Loading JS Engine...': '#9b59b6'
        };
        el.style.backgroundColor = colors[status] || '#555';
        el.style.boxShadow = `0 0 8px ${colors[status] || '#555'}`;
        el.title = status;
    }

    function init() {
        const check = setInterval(() => {
            if (document.body) {
                clearInterval(check);
                buildGUI();
                initEngine();
                setInterval(mainLoop, 200);
            }
        }, 100);
    }

    init();

})();