AI Multi-Collector Universal

Универсальный сборщик кода для DeepSeek, Gemini и ChatGPT

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         AI Multi-Collector Universal
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  Универсальный сборщик кода для DeepSeek, Gemini и ChatGPT
// @author       LUMOOOX
// @license      MIT
// @match        https://chat.deepseek.com/*
// @match        https://gemini.google.com/*
// @match        https://chatgpt.com/*
// @icon         data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%233b82f6' viewBox='0 0 24 24'%3E%3Cpath d='M8 6L2 12L8 18L9.5 16.5L5 12L9.5 7.5L8 6ZM16 6L14.5 7.5L19 12L14.5 16.5L16 18L22 12L16 6Z'/%3E%3C/svg%3E
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-end
// @homepageURL  https://github.com/LUMOOOX/ai-multi-collector
// @supportURL   https://github.com/LUMOOOX/ai-multi-collector/issues
// ==/UserScript==

(function() {
    'use strict';

    if (window.__aiCollectorInstalled) return;
    window.__aiCollectorInstalled = true;

// ========== 1. НАСТРОЙКИ ЛОКАЛИЗАЦИИ ==========
    const LANG = (navigator.language || navigator.userLanguage || 'en').toLowerCase().startsWith('ru') ? 'ru' : 'en';

    const TEXTS = {
        en: {
            ready: 'Ready', select: 'Select', copy: 'Copy', reset: 'Reset',
            clickBot: 'Click on bot message', clickArea: 'Click on message area',
            botOnly: 'Bot replies only', selectFirst: 'Select message first',
            noCode: 'No code found', selected: 'Selected', resetMsg: 'Reset',
            copied: 'Copied', block: 'block', blocks: 'blocks', chars: 'chars'
        },
        ru: {
            ready: 'Готов', select: 'Выбрать', copy: 'Копировать', reset: 'Сброс',
            clickBot: 'Кликните по ответу бота', clickArea: 'Кликните по области сообщения',
            botOnly: 'Только ответы бота', selectFirst: 'Сначала выберите',
            noCode: 'Код не найден', selected: 'Выбрано', resetMsg: 'Сброшено',
            copied: 'Скопировано', block: 'блок', blocks: 'блоков', chars: 'симв.'
        }
    };
    const t = TEXTS[LANG];

// ========== 2. КОНФИГУРАЦИЯ ПЛАТФОРМ ==========
    const PLATFORMS = {
        deepseek: {
            name: 'DeepSeek',
            botSelectors: '.ds-markdown, .message-content[data-message-role="assistant"], [data-message-id]',
            sidebarWidth: 280, minTextLength: 60,
            roleAttribute: 'data-message-role', roleValue: 'assistant'
        },
        gemini: {
            name: 'Gemini',
            botSelectors: 'model-response, .message-content',
            sidebarWidth: 300, minTextLength: 60,
            roleAttribute: null, roleValue: null
        },
        chatgpt: {
            name: 'ChatGPT',
            botSelectors: '[data-message-author-role="assistant"], .markdown, .prose',
            sidebarWidth: 260, minTextLength: 60,
            roleAttribute: 'data-message-author-role', roleValue: 'assistant'
        }
    };

// ========== 3. ОПРЕДЕЛЕНИЕ ТЕКУЩЕЙ ПЛАТФОРМЫ ==========
    let currentPlatform = null;
    if (location.hostname.includes('chat.deepseek.com')) {
        currentPlatform = PLATFORMS.deepseek;
    } else if (location.hostname.includes('gemini.google.com')) {
        currentPlatform = PLATFORMS.gemini;
    } else if (location.hostname.includes('chat.openai.com') || location.hostname.includes('chatgpt.com')) {
        currentPlatform = PLATFORMS.chatgpt;
    } else {
        console.warn('[AI-Collector] Platform not supported');
        return;
    }

    const CONFIG = {
        botSelectors: currentPlatform.botSelectors,
        sidebarWidth: currentPlatform.sidebarWidth,
        minTextLength: currentPlatform.minTextLength,
        roleAttribute: currentPlatform.roleAttribute,
        roleValue: currentPlatform.roleValue
    };

    console.log(`[${currentPlatform.name}] Started (${LANG})`);

// ========== 4. CSS СТИЛИ ==========
    GM_addStyle(`
        #ai-collector-panel,
        #ai-collector-panel *,
        #ai-collector-panel .ai-btn,
        #ai-collector-panel .ai-title,
        #ai-collector-panel .ai-status {
            box-sizing: border-box !important;
            text-transform: none !important;
            letter-spacing: normal !important;
            line-height: 1.2 !important;
            text-decoration: none !important;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important;
        }

        #ai-collector-panel {
            position: fixed !important;
            top: 150px !important;
            right: 50px !important;
            z-index: 2147483647 !important;
            width: 110px !important;
            overflow: hidden !important;
            margin: 0 !important;
            padding: 0 !important;
            background: #1e1e2e !important;
            border-radius: 12px !important;
            font-size: 13px !important;
            border: 0px solid #3b82f6 !important;
            opacity: 0.1 !important;
            transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
        }
        #ai-collector-panel:hover {
            opacity: 1 !important;
        }
        .ai-title-bar {
            background: #2a2a3a !important;
            padding: 8px 10px !important;
            border-bottom: 1px solid #3b82f6 !important;
            text-align: center !important;
        }
        .ai-title {
            font-size: 11px !important;
            font-weight: 600 !important;
            color: #cbd5e1 !important;
        }
        .ai-content {
            padding: 8px !important;
        }
        .ai-btn {
            display: block !important;
            width: 100% !important;
            height: 32px !important;
            padding: 0px 10px !important;
            margin: 6px 0 !important;
            border: none !important;
            border-radius: 8px !important;
            font-size: 12px !important;
            font-weight: 500 !important;
            line-height: 32px !important;
            cursor: pointer !important;
            color: white !important;
            text-align: center !important;
        }
        .ai-pick { background: #3b82f6 !important; }
        .ai-copy { background: #059669 !important; }
        .ai-reset { background: #6b7280 !important; }
        .ai-status {
            display: block !important;
            color: #94a3b8 !important;
            font-size: 11px !important;
            text-align: center !important;
            margin-top: 8px !important;
            padding-top: 8px !important;
            border-top: 1px solid #334155 !important;
        }
        .ai-selected {
            outline: 1px dashed #3b82f6 !important;
            outline-offset: 2px !important;
            background: rgba(59, 130, 246, 0.05) !important;
            border-radius: 8px !important;
            padding: 2px !important;
            margin: -2px !important;
        }
        .ai-selected .action-bar,
        .ai-selected [data-testid="action-bar"],
        .ai-selected .feedback-buttons,
        .ai-selected .copy-button,
        .ai-selected .regenerate-button {
            outline: none !important;
            background: transparent !important;
        }
        @media (prefers-color-scheme: light) {
            #ai-collector-panel {
                background: #ffffff !important;
                border: 1px solid #cbd5e1 !important;
            }
            .ai-title-bar {
                background: #f1f5f9 !important;
                border-bottom-color: #cbd5e1 !important;
            }
            .ai-title {
                color: #1e293b !important;
            }
            .ai-status {
                border-top-color: #e2e8f0 !important;
            }
        }
        .ai-hidden {
            display: none !important;
        }
    `);

// ========== 5. ПЕРЕМЕННЫЕ И ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ==========
    let selected = null;
    let picking = false;
    let panel = null;
    let timer = null;

    function setStatus(msg, color) {
        const el = document.querySelector('#ai-status');
        if (!el) return;
        if (timer) clearTimeout(timer);
        el.textContent = msg;
        el.style.color = color || '#94a3b8';
        timer = setTimeout(() => {
            if (el.textContent === msg) {
                el.textContent = t.ready;
                el.style.color = '#94a3b8';
            }
        }, 1500);
    }

    function isMainArea(x, y) {
        if (x < CONFIG.sidebarWidth) return false;
        if (y < 50) return false;
        return true;
    }

    function saveLast() {
        if (selected && selected.innerText) {
            const prefix = currentPlatform.name + '_';
            GM_setValue(prefix + 'lastMsg', selected.innerText.slice(0, 100));
        }
    }

// ========== 6. ОСНОВНЫЕ ФУНКЦИИ ==========
    function isBotMessage(element) {
        if (!element) return false;
        let msgElement = element.closest(CONFIG.botSelectors);
        if (!msgElement) return false;
        if (CONFIG.roleAttribute && CONFIG.roleValue) {
            const role = msgElement.getAttribute(CONFIG.roleAttribute);
            if (role === CONFIG.roleValue) return true;
        }
        if (msgElement.tagName === 'MODEL-RESPONSE') return true;
        const classes = msgElement.className || '';
        if (classes.includes('ds-markdown')) return true;
        const text = msgElement.innerText || '';
        if (text.length < 30) return false;
        const hasCode = msgElement.querySelector('pre, code');
        if (hasCode && text.length > 100) return true;
        if (text.length > 200) return true;
        return false;
    }

    function findBotMessage(element) {
        if (!element) return null;

        let botEl = element.closest(CONFIG.botSelectors);
        if (botEl && isBotMessage(botEl)) {
            if (currentPlatform.name === 'Gemini') {
                const textContainer = botEl.querySelector('.markdown, .prose, [data-message-content]');
                if (textContainer) return textContainer;
            }
            return botEl;
        }

        if (currentPlatform.name === 'Gemini') {
            let current = element;
            for (let i = 0; i < 5 && current && current !== document.body; i++) {
                if (isBotMessage(current)) {
                    const textContainer = current.querySelector('.markdown, .prose, [data-message-content]');
                    if (textContainer) return textContainer;
                    return current;
                }
                current = current.parentElement;
            }
        }

        return null;
    }

    function isValidMessage(el) {
        if (!el) return false;
        let len = (el.innerText || '').length;
        return len >= CONFIG.minTextLength && len < 100000;
    }

    function selectMessage(el) {
        if (selected) selected.classList.remove('ai-selected');
        selected = el;
        selected.classList.add('ai-selected');
        saveLast();
        setStatus(t.selected, '#10b981');
        picking = false;
    }

    function resetSelection() {
        if (selected) selected.classList.remove('ai-selected');
        selected = null;
        picking = false;
        setStatus(t.resetMsg, '#94a3b8');
        const prefix = currentPlatform.name + '_';
        GM_setValue(prefix + 'lastMsg', '');
    }

// ========== 7. ФУНКЦИЯ КОПИРОВАНИЯ ==========
    async function copyCode() {
        if (!selected) {
            setStatus(t.selectFirst, '#f59e0b');
            return;
        }

        let blocks = [];
        let seenSignatures = new Set();

        const STOP_WORDS = new Set([
            'javascript', 'python', 'java', 'c++', 'c#', 'c', 'go', 'rust',
            'ruby', 'php', 'html', 'css', 'sql', 'typescript', 'swift',
            'kotlin', 'scala', 'perl', 'shell', 'bash', 'powershell',
            'json', 'xml', 'yaml', 'markdown', 'txt', 'text'
        ]);

        function cleanBlock(text, platform) {
            if (platform !== 'ChatGPT') return text;

            let lines = text.split('\n');
            if (lines.length === 0) return text;

            let firstLine = lines[0].trim().toLowerCase();

            if (firstLine.startsWith('```') && firstLine.length > 3) {
                let lang = firstLine.substring(3).trim();
                if (STOP_WORDS.has(lang) || /^[a-z]+$/i.test(lang)) {
                    lines[0] = '```';
                    return lines.join('\n').trim();
                }
            }

            if (STOP_WORDS.has(firstLine)) {
                console.log(`[AI-Collector] Removed header: "${lines[0].trim()}"`);
                lines.shift();
                return lines.join('\n').trim();
            }
            return text;
        }

        function isTotallyJunk(text) {
            let trimmed = text.trim().toLowerCase();
            if (STOP_WORDS.has(trimmed)) return true;
            return false;
        }

        function getSignature(text) {
            return text.substring(0, 100).trim().replace(/\s+/g, ' ');
        }

        let allPres = selected.querySelectorAll('pre');
        console.log(`[AI-Collector] Pre elements found: ${allPres.length}`);

        let filteredCount = 0;
        let cleanedCount = 0;

        for (let p of allPres) {
            let rawTxt = p.innerText.trim();
            if (!rawTxt) continue;
            if (isTotallyJunk(rawTxt)) {
                filteredCount++;
                console.log(`[AI-Collector] Skipped junk block: "${rawTxt}"`);
                continue;
            }
            let cleanedTxt = cleanBlock(rawTxt, currentPlatform.name);
            if (cleanedTxt !== rawTxt) cleanedCount++;
            if (!cleanedTxt) continue;
            let signature = getSignature(cleanedTxt);
            if (!seenSignatures.has(signature)) {
                blocks.push(cleanedTxt);
                seenSignatures.add(signature);
                console.log(`[AI-Collector] Block added (${cleanedTxt.length} chars)`);
            }
        }

        if (blocks.length === 0 && currentPlatform.name !== 'ChatGPT') {
            let allCodes = selected.querySelectorAll('code');
            for (let c of allCodes) {
                if (c.closest('pre')) continue;
                let rawTxt = c.innerText.trim();
                if (!rawTxt) continue;
                if (isTotallyJunk(rawTxt)) continue;
                let cleanedTxt = cleanBlock(rawTxt, currentPlatform.name);
                if (!cleanedTxt) continue;
                let signature = getSignature(cleanedTxt);
                if (!seenSignatures.has(signature)) {
                    blocks.push(cleanedTxt);
                    seenSignatures.add(signature);
                }
            }
        }

        if (blocks.length === 0) {
            let matches = (selected.innerText || '').match(/```[\s\S]*?```/g) || [];
            for (let m of matches) {
                let clean = m.replace(/^```\w*\n/, '').replace(/\n```$/, '').trim();
                if (!clean) continue;
                if (isTotallyJunk(clean)) continue;
                let cleanedTxt = cleanBlock(clean, currentPlatform.name);
                if (!cleanedTxt) continue;
                let signature = getSignature(cleanedTxt);
                if (!seenSignatures.has(signature)) {
                    blocks.push(cleanedTxt);
                    seenSignatures.add(signature);
                }
            }
        }

        console.log(`[AI-Collector] Junk blocks filtered: ${filteredCount}`);
        console.log(`[AI-Collector] Headers cleaned: ${cleanedCount}`);
        console.log(`[AI-Collector] Total blocks: ${blocks.length}`);

        if (blocks.length === 0) {
            setStatus(t.noCode, '#ef4444');
            return;
        }

        let result = blocks.join('\n\n');
        const totalChars = result.length;

        let blockWord = blocks.length === 1 ? t.block : t.blocks;
        let statusMsg = `${t.copied}: ${blocks.length} ${blockWord} (${totalChars} ${t.chars})`;

        try {
            await navigator.clipboard.writeText(result);
            setStatus(statusMsg, '#10b981');
            console.log(`[AI-Collector] Platform: ${currentPlatform.name}`);
            console.log(`[AI-Collector] Blocks copied: ${blocks.length}`);
            console.log(`[AI-Collector] Total size: ${totalChars} chars`);
        } catch(e) {
            let ta = document.createElement('textarea');
            ta.value = result;
            ta.style.cssText = 'position:fixed;top:-1000px';
            document.body.appendChild(ta);
            ta.select();
            document.execCommand('copy');
            ta.remove();
            setStatus(statusMsg, '#10b981');
            console.log(`[AI-Collector] Platform: ${currentPlatform.name} (fallback)`);
            console.log(`[AI-Collector] Blocks copied: ${blocks.length}`);
            console.log(`[AI-Collector] Total size: ${totalChars} chars`);
        }
    }

// ========== 8. СОЗДАНИЕ UI ПАНЕЛИ ==========
    function createPanel() {
        if (document.getElementById('ai-collector-panel')) return document.getElementById('ai-collector-panel');
        let div = document.createElement('div');
        div.id = 'ai-collector-panel';

        let titleBar = document.createElement('div');
        titleBar.className = 'ai-title-bar';

        let title = document.createElement('span');
        title.className = 'ai-title';
        title.textContent = 'LUMOOOX';

        titleBar.appendChild(title);

        let content = document.createElement('div');
        content.className = 'ai-content';

        let btnPick = document.createElement('button');
        btnPick.textContent = t.select;
        btnPick.className = 'ai-btn ai-pick';
        btnPick.onclick = () => { picking = true; setStatus(t.clickBot, '#f59e0b'); };

        let btnCopy = document.createElement('button');
        btnCopy.textContent = t.copy;
        btnCopy.className = 'ai-btn ai-copy';
        btnCopy.onclick = copyCode;

        let btnReset = document.createElement('button');
        btnReset.textContent = t.reset;
        btnReset.className = 'ai-btn ai-reset';
        btnReset.onclick = resetSelection;

        let statusDiv = document.createElement('div');
        statusDiv.id = 'ai-status';
        statusDiv.className = 'ai-status';
        statusDiv.textContent = t.ready;

        content.appendChild(btnPick);
        content.appendChild(btnCopy);
        content.appendChild(btnReset);
        content.appendChild(statusDiv);

        div.appendChild(titleBar);
        div.appendChild(content);

        document.body.appendChild(div);
        return div;
    }

// ========== 9. ОБРАБОТЧИКИ СОБЫТИЙ ==========
    function handleClick(e) {
        if (!picking && !selected) return;

        if (e.target.closest && e.target.closest('#ai-collector-panel')) return;

        if (picking) {
            if (!isMainArea(e.clientX, e.clientY)) {
                setStatus(t.clickArea, '#ef4444');
                picking = false;
                return;
            }
            let msg = findBotMessage(e.target);
            if (!msg || !isValidMessage(msg)) {
                setStatus(t.botOnly, '#ef4444');
                picking = false;
                return;
            }
            selectMessage(msg);
            e.preventDefault();
            e.stopPropagation();
            return;
        }

        if (selected && !picking) {
            const clickedOnSelected = selected.contains(e.target);
            if (!clickedOnSelected) resetSelection();
        }
    }

    function handleKey(e) {
        if (e.ctrlKey && e.shiftKey && (e.key.toLowerCase() === 'h' || e.key.toLowerCase() === 'р')) {
            e.preventDefault();
            if (panel) {
                panel.classList.toggle('ai-hidden');
            }
            return;
        }

        if (e.ctrlKey && (e.key.toLowerCase() === 'b' || e.key.toLowerCase() === 'и')) {
            e.preventDefault();
            picking = true;
            setStatus(t.clickBot, '#f59e0b');
            return;
        }

        if (e.ctrlKey && (e.key.toLowerCase() === 'c' || e.key.toLowerCase() === 'с')) {
            if (selected) {
                e.preventDefault();
                copyCode();
            }
            return;
        }

        if (e.key === 'Escape') {
            resetSelection();
            picking = false;
        }
    }

// ========== 10. ЗАПУСК СКРИПТА ==========
    function init() {
        panel = createPanel();
        const prefix = currentPlatform.name + '_';
        let last = GM_getValue(prefix + 'lastMsg', '');
        if (last) {
            setTimeout(() => {
                let candidates = document.querySelectorAll(CONFIG.botSelectors);
                for (let m of candidates) {
                    if (isBotMessage(m) && (m.innerText || '').startsWith(last)) {
                        selectMessage(m);
                        break;
                    }
                }
            }, 500);
        }
        console.log(`[${currentPlatform.name}] Ready`);
    }

    document.addEventListener('click', handleClick, true);
    document.addEventListener('keydown', handleKey);

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();