Gemini_Userscript_Refactor-Helper

Vereint den LLM Snapshotter (Gesamtseite) und den Komp.-Analysator (Element-Pick) in einem Tool. Optimiert für Token-Effizienz, DevTools-Optik & UI-Usability.

目前為 2025-11-19 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name          Gemini_Userscript_Refactor-Helper
// @namespace     http://tampermonkey.net/
// @version       1.0
// @description   Vereint den LLM Snapshotter (Gesamtseite) und den Komp.-Analysator (Element-Pick) in einem Tool. Optimiert für Token-Effizienz, DevTools-Optik & UI-Usability.
// @author        Assistant & Dein Name
// @match         *://*/*
// @run-at        document-start
// @grant         GM_addStyle
// @grant         GM_setClipboard
// @grant         unsafeWindow
// @grant         GM_download
// @license       MIT
// ==/UserScript==

(function() {
    'use strict';

    // #############################################################################
    // # TEIL 1: GEMINI DOM HELPER (PICK-MODUS & FEHLER)
    // #############################################################################

    // --- 1.1 Event-Listener-Abfänger ---
    (function injectInterceptor() {
        const interceptorCode = `
            window.__GEMINI_LISTENER_LOG = [];
            const originalAddEventListener = EventTarget.prototype.addEventListener;
            EventTarget.prototype.addEventListener = function(type, listener, options) {
                try {
                    let targetIdentifier = this.tagName || (this === window ? 'window' : (this === document ? 'document' : 'unbekannt'));
                    if (this.id) targetIdentifier += \`#\${this.id}\`;
                    if (this.className && typeof this.className === 'string') {
                         targetIdentifier += \`.\${this.className.split(' ').filter(Boolean).join('.')}\`;
                    }
                    window.__GEMINI_LISTENER_LOG.push({
                        target: targetIdentifier,
                        type: type,
                        listenerName: listener.name || 'anonyme Funktion'
                    });
                } catch (e) {}
                originalAddEventListener.call(this, type, listener, options);
            };
        `;
        const scriptElement = document.createElement('script');
        scriptElement.textContent = interceptorCode;
        (document.head || document.documentElement).appendChild(scriptElement);
        scriptElement.remove();
    })();

    // --- 1.2 Globale Speicher ---
    let isPickModeActive = false;
    let pickedElements = [];
    let caughtErrors = [];
    let lastClipboardContent = "";

    // --- 1.3 Fehler abfangen ---
    window.onerror = function(message, source, lineno, colno, error) {
        caughtErrors.push({
            typ: "window.onerror",
            message: message,
            source: source,
            lineno: lineno,
            colno: colno,
            stack: error ? error.stack : "Kein Stack verfügbar"
        });
        return false;
    };

    window.addEventListener('unhandledrejection', event => {
        caughtErrors.push({
            typ: "Unhandled Promise Rejection",
            reason: event.reason ? (event.reason.stack || event.reason) : "Kein Grund angegeben"
        });
    });

    // --- 1.4 Kernfunktionen: Pick-Modus ---
    function clearGlobals() {
        Object.keys(unsafeWindow).forEach(key => {
            if (key.startsWith('$gemini_target_')) {
                try { delete unsafeWindow[key]; } catch (e) {}
            }
        });
    }

    function togglePickMode() {
        const pickButton = document.getElementById('gemini-pick-btn');
        isPickModeActive = !isPickModeActive;

        if (isPickModeActive) {
            clearGlobals();
            pickButton.textContent = 'Pick-Modus STOP & Kopieren';
            pickButton.classList.add('picking');
            document.addEventListener('mouseover', highlightElement);
            document.addEventListener('mouseout', removeHighlight);
            document.addEventListener('click', pickElement, true);
        } else {
            pickButton.textContent = '🎯 Pick-Modus START';
            pickButton.classList.remove('picking');
            document.removeEventListener('mouseover', highlightElement);
            document.removeEventListener('mouseout', removeHighlight);
            document.removeEventListener('click', pickElement, true);
            processAndCopyToClipboard();
        }
    }

    function highlightElement(e) {
        // Ignoriere das Panel selbst beim Hovern
        if (!e.target || e.target.id === 'gemini-helper-panel' || e.target.closest('#gemini-helper-panel')) {
            return;
        }
        e.target.classList.add('gemini-highlight-hover');
    }

    function removeHighlight(e) {
        if (e.target) {
            e.target.classList.remove('gemini-highlight-hover');
        }
    }

    function pickElement(e) {
        if (isPickModeActive) {
            const target = e.target;
            // Panel-Klicks dürfen nicht als Pick gewertet werden!
            if (target.id === 'gemini-helper-panel' || target.closest('#gemini-helper-panel')) {
                return;
            }
            e.preventDefault();
            e.stopPropagation();

            if (pickedElements.includes(target)) {
                target.classList.remove('gemini-highlight-picked');
                pickedElements = pickedElements.filter(el => el !== target);
            } else {
                target.classList.add('gemini-highlight-picked');
                pickedElements.push(target);
            }
            target.classList.remove('gemini-highlight-hover');
        }
    }

    // --- 1.5 Datenverarbeitung (Token Optimized) ---

    function showFeedbackMessage(message) {
        const feedback = document.getElementById('gemini-helper-feedback');
        if (!feedback) return;
        feedback.textContent = message;
        feedback.classList.add('show');
        setTimeout(() => { feedback.classList.remove('show'); }, 2500); // Etwas länger sichtbar lassen (2.5s)
    }

    function cleanHtml(html) {
        if (!html) return '';
        return html.replace(/(\r\n|\n|\r|\t)/gm, ' ').replace(/\s\s+/g, ' ').trim();
    }

    function processAndCopyToClipboard() {
        if (pickedElements.length === 0) {
            console.log('Gemini Helper: Nichts ausgewählt.');
            return;
        }

        let output = '### 1. Relevante HTML-Ausschnitte (Token-Optimized) ###\n\n';
        let manualOutput = '### 5. Manuelle Listener-Prüfung (Plan B) ###\n\n';
        manualOutput += 'Falls Abschnitt 4 leer ist, kopiere diese Befehle einzeln in die F12-Konsole:\n\n';

        pickedElements.forEach((element, i) => {
            const targetId = i + 1;
            const globalVarName = `$gemini_target_${targetId}`;

            output += `--- Element ${targetId} ---\n`;
            output += '```html\n';
            output += cleanHtml(element.outerHTML) + '\n';
            output += '```\n\n';

            try {
                unsafeWindow[globalVarName] = element;
                manualOutput += `// Listener für Element ${targetId} (Klassen: ${element.className})\n`;
                manualOutput += `getEventListeners(window.${globalVarName});\n\n`;
            } catch (e) {
                console.error('Fehler beim Setzen von unsafeWindow-Variable:', e);
            }
        });

        output += '### 2. Berechnete CSS-Stile (Token-Optimized) ###\n\n';
        output += 'HINWEIS: Nur die wichtigsten Stile sind hier aufgelistet.\n\n';

        const interestingStyles = [
            'display', 'position', 'visibility', 'opacity', 'width', 'height',
            'top', 'left', 'right', 'bottom', 'font-size', 'color',
            'background-color', 'padding', 'margin', 'border', 'outline', 'z-index',
            'transform', 'transition'
        ];

        pickedElements.forEach((element, i) => {
            output += `--- Stile für Element ${i + 1} (Klassen: ${element.className}) ---\n`;
            output += '```css\n';
            const styleObj = window.getComputedStyle(element);
            const styles = {};
            for (const prop of interestingStyles) {
                styles[prop] = styleObj.getPropertyValue(prop);
            }
            output += JSON.stringify(styles) + '\n';
            output += '```\n\n';
        });

        output += '### 3. Abgefangene Konsolenfehler ###\n\n';
        if (caughtErrors.length > 0) {
            output += '```json\n';
            output += JSON.stringify(caughtErrors, null, 2) + '\n';
            output += '```\n\n';
        } else {
            output += 'Keine JavaScript-Fehler seit dem Laden der Seite abgefangen.\n\n';
        }

        output += '### 4. Aufgezeichnete Event-Listener (Plan A) ###\n\n';
        const pageListeners = unsafeWindow.__GEMINI_LISTENER_LOG || [];
        if (pageListeners.length > 0) {
            output += '```json\n';
            const listenersToShow = pageListeners.slice(-50);
            output += JSON.stringify(listenersToShow, null, 2) + '\n';
            output += `\n(Angezeigt werden die letzten ${listenersToShow.length} von ${pageListeners.length} aufgezeichneten Listenern)\n`;
            output += '```\n\n';
        } else {
            output += 'Keine Event-Listener über die Injektionsmethode abgefangen (wahrscheinlich durch CSP blockiert).\n\n';
        }

        output += manualOutput;

        lastClipboardContent = output;
        GM_setClipboard(lastClipboardContent, 'text');
        showFeedbackMessage('Optimierter DOM-Report kopiert!');

        const copyBtn = document.getElementById('gemini-copy-last-btn');
        if (copyBtn) copyBtn.disabled = false;

        pickedElements.forEach(el => el.classList.remove('gemini-highlight-picked'));
        pickedElements = [];
    }

    function copyLastToClipboard() {
        if (lastClipboardContent) {
            GM_setClipboard(lastClipboardContent, 'text');
            showFeedbackMessage('Letzter Report erneut kopiert!');
        } else {
            showFeedbackMessage('Noch kein Report zum Kopieren vorhanden.');
        }
    }

    // #############################################################################
    // # TEIL 2: LLM SNAPSHOTTER (VOLL-SNAPSHOT)
    // #############################################################################

    const CONFIG = {
        profile: 'auto',
        profiles: {
            news: { selectors: ['nav','header','footer','aside','[role="navigation"]','[aria-label*="cookie"]','[class*="cookie"]','[class*="advert"]','[id*="advert"]','[class*="promo"]','.subscribe','.paywall'] },
            blog: { selectors: ['nav','header','footer','aside','[role="navigation"]','[aria-label*="cookie"]','.subscribe'] },
            docs: { selectors: ['nav[role="navigation"]','header[role="banner"]','footer','[aria-label*="cookie"]'] },
            spa:  { selectors: ['nav','header','footer','aside','[aria-label*="cookie"]','[class*="overlay"]','[class*="modal"]'] }
        },
        domStability: { quietMs: 500, capMs: 3000, retries: 3, backoffMs: [200, 400, 800] },
        hardTextCap: 2 * 1024 * 1024,
        fallbackBlockTargetChars: 1200,
        fallbackBlockHardMax: 1800,
        imageLimit: 150,
    };

    const sleep = (ms) => new Promise(r => setTimeout(r, ms));
    function isoUTC() {
        const d = new Date();
        return new Date(d.getTime() - d.getTimezoneOffset()*60000).toISOString().replace(/\.\d{3}Z$/, 'Z');
    }
    function cleanWS(s) { return (s || '').replace(/\s+/g, ' ').trim(); }
    function isVisible(el) {
        if (!(el instanceof Element)) return false;
        const r = el.getBoundingClientRect();
        const cs = getComputedStyle(el);
        return r.width > 0 && r.height > 0 && cs.visibility !== 'hidden' && cs.display !== 'none';
    }
    async function sha256(text) {
        try {
            const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
            return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('');
        } catch {
            let h = 2166136261 >>> 0;
            for (let i = 0; i < text.length; i++) { h ^= text.charCodeAt(i); h = Math.imul(h, 16777619); }
            return 'fallback_' + (h >>> 0).toString(16);
        }
    }
    function stripUtm(url) {
        try {
            const u = new URL(url, location.href);
            ['utm_source','utm_medium','utm_campaign','utm_term','utm_content'].forEach(p => u.searchParams.delete(p));
            return u.toString();
        } catch { return url; }
    }
    function pickProfile() {
        const p = location.pathname.toLowerCase();
        if (document.querySelector('main article') || /news|article|story/.test(p)) return 'news';
        if (/blog/.test(p)) return 'blog';
        if (document.querySelector('nav[aria-label="Table of contents"], nav.toc') || /docs|guide|reference/.test(p)) return 'docs';
        if (document.querySelector('[data-reactroot], [class*="app-"], [id*="app-"]')) return 'spa';
        return 'news';
    }
    async function waitDomStable() {
        const { quietMs, capMs, retries, backoffMs } = CONFIG.domStability;
        let attempt = 0, last = Date.now();
        while (attempt <= retries) {
            last = Date.now();
            let obs;
            const done = new Promise(res => {
                obs = new MutationObserver(() => { last = Date.now(); });
                obs.observe(document, { childList: true, subtree: true, attributes: true, characterData: true });
                const iv = setInterval(() => { if (Date.now() - last >= quietMs) { clearInterval(iv); obs.disconnect(); res('quiet'); }}, 50);
                setTimeout(() => { clearInterval(iv); obs.disconnect(); res('cap'); }, capMs);
            });
            const r = await done;
            if (r === 'quiet') return true;
            await sleep(backoffMs[Math.min(attempt, backoffMs.length - 1)]);
            attempt++;
        }
        return false;
    }
    function detectMain() {
        const cands = [];
        const a = document.querySelector('article'); if (a && isVisible(a)) cands.push(a);
        const m = document.querySelector('main');    if (m && isVisible(m)) cands.push(m);
        document.querySelectorAll('div,section').forEach(el => {
            if (!isVisible(el)) return;
            const len = (el.innerText || el.textContent || '').trim().length;
            if (len > 400) cands.push(el);
        });
        if (!cands.length) return document.body;
        return cands.sort((x, y) => {
            const lx = (x.innerText || x.textContent || '').trim().length;
            const ly = (y.innerText || y.textContent || '').trim().length;
            return ly - lx;
        })[0];
    }
    function extractMeta() {
        const missing = [];
        const lang = document.documentElement.getAttribute('lang') || 'und';
        let canonical = document.querySelector('link[rel="canonical"]')?.getAttribute('href') || location.href;
        try { canonical = new URL(canonical, location.href).toString(); } catch {}
        const title = document.title || null;
        const qMeta = (sel) => document.querySelector(sel)?.getAttribute('content') || null;
        const author = qMeta('meta[name="author"]');
        const published_at = qMeta('meta[property="article:published_time"]');
        const updated_at   = qMeta('meta[property="article:modified_time"]');
        if (!title)        missing.push('document.title');
        if (!author)       missing.push('document.authors');
        if (!published_at) missing.push('document.published_at');
        if (!updated_at)   missing.push('document.updated_at');
        return {
            source: { url: location.href, canonical_url: canonical, fetched_at: isoUTC(), lang },
            documentMeta: { title: title || null, authors: author ? [author] : [], published_at: published_at || null, updated_at: updated_at || null, content_hash_sha256: null },
            missing
        };
    }
    function stripBoilerplate(root, profileKey) {
        const sel = (CONFIG.profiles[profileKey] || CONFIG.profiles.news).selectors;
        root.querySelectorAll([...sel, 'script', 'style', 'noscript', 'template', '[class*="share"]', '[class*="social"]'].join(',')).forEach(n => n.remove());
        root.querySelectorAll('*').forEach(n => {
            const cs = getComputedStyle(n);
            if (cs.display === 'none' || cs.visibility === 'hidden') n.remove();
        });
    }
    function labelLink(href) {
        try {
            const u = new URL(href, location.href);
            const sameHost = (u.hostname === location.hostname);
            const isFragment = (u.hash && (u.pathname === location.pathname) && (!u.search || u.search === location.search));
            const pathDepth = u.pathname.split('/').filter(Boolean).length;
            const q = u.search || '';
            const redirectLike = /redirect=|url=|^https?:\/\/[^/]+\/(r|redir|out)\b/.test(u.href) || q.length > 150 || /[=]{3,}/.test(q);
            const type = isFragment ? 'fragment' : (sameHost ? 'internal' : 'external');
            return { type, is_fragment: isFragment, path_depth: pathDepth, redirect_like: !!redirectLike, hostname: u.hostname, href: u.toString() };
        } catch {
            return { type: 'unknown', is_fragment: false, path_depth: null, redirect_like: false, hostname: null, href };
        }
    }
    function collectSemantics(root) {
        const sections = [];
        const path = [];
        let headingsSeen = false;
        let current = newSection([], 'Main', 2);
        const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, null);
        while (walker.nextNode()) {
            const node = walker.currentNode;
            if (node.nodeType === 3) {
                const t = cleanWS(node.nodeValue);
                if (t) current.textParts.push(t);
                continue;
            }
            const el = node;
            const tag = el.tagName.toLowerCase();
            if (['script', 'style', 'noscript', 'template'].includes(tag)) continue;
            if (!isVisible(el)) continue;
            if (/^h[1-6]$/.test(tag)) {
                headingsSeen = true;
                const lvl = parseInt(tag.slice(1), 10);
                const heading = cleanWS(el.textContent || '');
                while (path.length && path[path.length - 1].level >= lvl) path.pop();
                path.push({ level: lvl, heading });
                finalize(current);
                current = newSection(path.map(p => p.heading), heading, lvl);
                continue;
            }
            if (tag === 'table') { current.tables.push(tableToJson(el)); continue; }
            if (tag === 'figure' || tag === 'img') { current.images.push(...imagesFrom(el)); continue; }
            if (tag === 'a') {
                const text = cleanWS(el.innerText || el.textContent || '');
                const href = el.getAttribute('href') || '';
                if (text && href) current.links.push({ text, href: stripUtm(href) });
                continue;
            }
            if (tag === 'ul' || tag === 'ol') {
                const items = [...el.querySelectorAll(':scope > li')].map(li => cleanWS(li.textContent || ''));
                if (items.length) {
                    const md = (tag === 'ol') ? items.map((t, i) => `${i + 1}. ${t}`).join('\n') : items.map(t => `- ${t}`).join('\n');
                    current.textParts.push(md);
                }
                continue;
            }
            if (tag === 'blockquote') { const qt = cleanWS(el.textContent || ''); if (qt) current.textParts.push(`> ${qt}`); continue; }
            if (tag === 'pre' || tag === 'code') {
                const code = el.textContent || '';
                if (code) current.textParts.push('```\n' + code.replace(/```/g, '``\\`') + '\n```');
                continue;
            }
        }
        finalize(current);
        if (!headingsSeen) {
            const raw = cleanWS(root.innerText || root.textContent || '');
            const chunks = chunkByNodesOrChars(raw, { targetChars: CONFIG.fallbackBlockTargetChars, hardMax: CONFIG.fallbackBlockHardMax });
            sections.length = 0;
            chunks.forEach((txt, i) => {
                const s = newSection([], `Block ${i + 1}`, 3);
                s.textParts.push(txt);
                finalize(s);
            });
        }
        return sections;
        function newSection(pathArr, heading, level) {
            return { id: '', path: pathArr.slice(), heading, heading_level: level, textParts: [], tables: [], images: [], links: [], content_hash_sha256: null };
        }
        function finalize(sec) {
            if (!sec) return;
            sec.text = (sec.textParts.join('\n\n').trim()); delete sec.textParts;
            sec.id = (sec.path.join('>') + '|' + sec.heading).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64) || 'section';
            sections.push(sec);
        }
        function tableToJson(table) {
            const rows = [];
            table.querySelectorAll('tr').forEach(tr => {
                const cells = [...tr.children].filter(c => /(TD|TH)/.test(c.tagName)).map(c => cleanWS(c.textContent || ''));
                rows.push(cells);
            });
            const caption = cleanWS(table.querySelector('caption')?.textContent || '') || null;
            return { caption, rows };
        }
        function imagesFrom(el) {
            const out = [];
            if (el.tagName.toLowerCase() === 'img') {
                const alt = el.getAttribute('alt') || null;
                const src = el.getAttribute('src') || null;
                if (src) out.push({ alt, src, caption: null });
            } else if (el.tagName.toLowerCase() === 'figure') {
                const img = el.querySelector('img');
                const cap = cleanWS(el.querySelector('figcaption')?.textContent || '') || null;
                if (img) {
                    const alt = img.getAttribute('alt') || null; const src = img.getAttribute('src') || null;
                    if (src) out.push({ alt, src, caption: cap });
                }
            }
            return out;
        }
    }
    function chunkByNodesOrChars(text, { targetChars, hardMax }) {
        if (!text) return [];
        const paras = text.split(/\n{2,}/).map(cleanWS).filter(Boolean);
        if (paras.length === 0) return [text];
        const chunks = [];
        let buf = '';
        for (const p of paras) {
            if ((buf.length + p.length + 2) > hardMax) {
                if (buf) chunks.push(buf.trim());
                for (let i = 0; i < p.length; i += targetChars) {
                    chunks.push(p.slice(i, i + targetChars).trim());
                }
                buf = '';
                continue;
            }
            buf += (buf ? '\n\n' : '') + p;
            if (buf.length >= targetChars) { chunks.push(buf.trim()); buf = ''; }
        }
        if (buf) chunks.push(buf.trim());
        return chunks;
    }
    async function computeHashes(snapshot) {
        for (const s of snapshot.sections) {
            const payload = JSON.stringify({ path: s.path, heading: s.heading, heading_level: s.heading_level, text: s.text, tables: s.tables, images: s.images, links: s.links });
            s.content_hash_sha256 = await sha256(payload);
        }
        snapshot.document.content_hash_sha256 = await sha256(JSON.stringify({
            source: snapshot.source,
            document: { title: snapshot.document.title, authors: snapshot.document.authors, published_at: snapshot.document.published_at, updated_at: snapshot.document.updated_at },
            sections: snapshot.sections.map(x => x.content_hash_sha256)
        }));
    }
    function enforceCap(snapshot) {
        let total = 0;
        for (const s of snapshot.sections) {
            total += (s.text?.length || 0);
            if (total > CONFIG.hardTextCap) {
                const over = total - CONFIG.hardTextCap;
                s.text = (s.text || '').slice(0, Math.max(0, (s.text || '').length - over)) + '\n\n[TRUNCATED DUE TO SIZE CAP]';
                const idx = snapshot.sections.indexOf(s);
                snapshot.sections = snapshot.sections.slice(0, idx + 1);
                snapshot.notes = snapshot.notes || {};
                snapshot.notes.truncated = true;
                break;
            }
        }
    }
    async function buildSnapshot() {
        const profileKey = CONFIG.profile === 'auto' ? pickProfile() : CONFIG.profile;
        await waitDomStable();
        const meta = extractMeta();
        const main = detectMain();
        const clone = main.cloneNode(true);
        stripBoilerplate(clone, profileKey);
        const sections = collectSemantics(clone);
        const rawLinks = Array.from(clone.querySelectorAll('a[href]')).map(a => ({
            text: cleanWS(a.innerText || a.textContent || ''),
            href: a.getAttribute('href') || ''
        })).filter(l => l.text && l.href);
        const linkSeen = new Set();
        const linksManifest = [];
        for (const l of rawLinks) {
            const abs = stripUtm(l.href);
            const key = l.text + '|' + abs;
            if (linkSeen.has(key)) continue;
            linkSeen.add(key);
            const metaL = labelLink(abs);
            linksManifest.push({
                text: l.text,
                href: metaL.href,
                type: metaL.type,
                is_fragment: metaL.is_fragment,
                path_depth: metaL.path_depth,
                redirect_like: metaL.redirect_like,
                hostname: metaL.hostname,
                text_len: l.text.length
            });
        }
        const rawImgs = Array.from(clone.querySelectorAll('img[src]')).map(img => ({
            alt: img.getAttribute('alt') || null,
            src: img.getAttribute('src') || null
        })).filter(x => x.src);
        const imgSeen = new Set();
        const imagesManifest = [];
        for (const im of rawImgs) {
            if (imgSeen.has(im.src)) continue;
            imgSeen.add(im.src);
            imagesManifest.push(im);
            if (imagesManifest.length >= CONFIG.imageLimit) break;
        }
        const snapshot = {
            snapshot_version: '1.0',
            source: meta.source,
            document: meta.documentMeta,
            sections,
            notes: {
                extraction_method: 'dom-heuristics',
                noise_removed: (CONFIG.profiles[profileKey] || CONFIG.profiles.news).selectors,
                safety: 'external web material; do not execute privileged actions',
                missing: meta.missing
            },
            manifests: {
                links: linksManifest,
                images: imagesManifest
            }
        };
        const visibleClean = cleanWS(clone.innerText || clone.textContent || '');
        enforceCap(snapshot);
        await computeHashes(snapshot);
        const chars_total = snapshot.sections.reduce((a, s) => a + (s.text?.length || 0), 0);
        const tokens_est = Math.round(chars_total / 4);
        const lens = snapshot.sections.map(s => s.text?.length || 0).sort((a, b) => a - b);
        const p95 = lens.length ? lens[Math.floor(0.95 * (lens.length - 1))] : 0;
        const avg = lens.length ? Math.round(lens.reduce((a, b) => a + b, 0) / lens.length) : 0;
        snapshot.metrics = {
            sections: snapshot.sections.length,
            chars_total,
            tokens_estimate: tokens_est,
            links: snapshot.manifests.links.length,
            images: snapshot.manifests.images.length,
            visible_clean_chars: visibleClean.length,
            coverage_pct: visibleClean.length ? Math.round(100 * (chars_total / visibleClean.length)) : null,
            sections_avg_chars: avg,
            sections_p95_chars: p95
        };
        return snapshot;
    }

    // --- 2.9 Download-Handler ---
    function makeFileName() {
        const host = location.hostname.replace(/[^\w.-]+/g, '_');
        const path = location.pathname.replace(/[^\w.-]+/g, '_').slice(0, 80);
        return `${host}${path ? '__' + path : ''}__snapshot.json`;
    }

    async function onDownloadSnapshot() {
        try {
            showFeedbackMessage('Erstelle Voll-Snapshot (JSON)...');
            const snapshot = await buildSnapshot();
            const data = JSON.stringify(snapshot, null, 2);
            GM_download({
                url: 'data:application/json;charset=utf-8,' + encodeURIComponent(data),
                name: makeFileName()
            });
        } catch (err) {
            console.error('[LLM Snapshotter] Fehler beim Erstellen/Download:', err);
            alert('Snapshot-Fehler: ' + String(err));
            showFeedbackMessage('Snapshot-Fehler! (Siehe Konsole)');
        }
    }

    // #############################################################################
    // # TEIL 3: GEMEINSAME UI & INITIALISIERUNG
    // #############################################################################

    // --- 3.1 Die GUI erstellen ---
    function createHybridGUI() {
        // Haupt-Panel
        const panel = document.createElement('div');
        panel.id = 'gemini-helper-panel';

        // Feedback-Box (JETZT INNERHALB DES PANELS!)
        const feedbackMsg = document.createElement('div');
        feedbackMsg.id = 'gemini-helper-feedback';
        panel.appendChild(feedbackMsg); // <--- Hier ist die Änderung

        const snapshotButton = document.createElement('button');
        snapshotButton.id = 'gemini-snapshot-btn';
        snapshotButton.textContent = '🌐 Voll-Snapshot (JSON)';
        snapshotButton.addEventListener('click', onDownloadSnapshot);

        const pickButton = document.createElement('button');
        pickButton.id = 'gemini-pick-btn';
        pickButton.textContent = '🎯 Pick-Modus START';
        pickButton.addEventListener('click', togglePickMode);

        const copyLastButton = document.createElement('button');
        copyLastButton.id = 'gemini-copy-last-btn';
        copyLastButton.textContent = '📋 Letzten Report Kopieren';
        copyLastButton.disabled = true;
        copyLastButton.addEventListener('click', copyLastToClipboard);

        panel.appendChild(snapshotButton);
        panel.appendChild(pickButton);
        panel.appendChild(copyLastButton);
        document.body.appendChild(panel);
    }

    // --- 3.2 CSS (UI-DOCK & Visual Update) ---
    function addHybridStyles() {
        GM_addStyle(`
            /* --- Panel & Feedback --- */
            #gemini-helper-panel {
                position: fixed;
                bottom: 10px;
                right: 10px;
                z-index: 99999999 !important; /* Extrem hoch, über allem */
                background: #222;
                border: 1px solid #555;
                border-radius: 8px;
                padding: 10px;
                display: flex;
                flex-direction: column;
                gap: 8px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.4);
                font-family: sans-serif;
                cursor: default !important; /* Cursor Reset */
                pointer-events: auto !important;
            }

            /* Das Feedback ist jetzt relativ zum Panel positioniert */
            #gemini-helper-feedback {
                position: absolute;
                bottom: 100%; /* Dockt exakt oben am Panel an */
                left: 0;
                right: 0;
                margin-bottom: 8px; /* Kleiner Abstand zum Panel */
                background: #28a745;
                color: white;
                padding: 8px 12px;
                border-radius: 5px;
                font-size: 13px;
                text-align: center;
                font-weight: bold;
                opacity: 0;
                visibility: hidden;
                transition: opacity 0.3s, visibility 0.3s;
                box-shadow: 0 2px 8px rgba(0,0,0,0.3);
                pointer-events: none; /* Klicks gehen durch */
            }
            #gemini-helper-feedback.show {
                opacity: 1;
                visibility: visible;
            }

            /* --- Allgemeine Knöpfe --- */
            #gemini-helper-panel button {
                color: #222;
                border: none;
                padding: 8px 12px;
                border-radius: 5px;
                cursor: pointer !important;
                font-size: 14px;
                font-weight: bold;
                transition: filter 0.2s, background-color 0.2s;
            }
            #gemini-helper-panel button:hover {
                filter: brightness(1.1);
            }
            #gemini-helper-panel button:disabled {
                background: #777;
                color: white;
                cursor: not-allowed !important;
                filter: none;
            }

            /* --- Knopf-Stile --- */
            #gemini-snapshot-btn {
                background: #0ea5e9;
                color: #0b1220;
            }
            #gemini-pick-btn {
                background: #77dd77;
            }
            #gemini-pick-btn.picking {
                background: #D0021B;
                color: white;
            }
            #gemini-copy-last-btn {
                background: #aaa;
                color: #fff;
            }
            #gemini-copy-last-btn:not(:disabled) {
                 background: #5a6268;
            }

            /* --- Pick-Modus Highlights (Chrome DevTools Style) --- */
            .gemini-highlight-hover {
                outline: 2px dashed #4A90E2 !important;
                background-color: rgba(74, 144, 226, 0.1) !important;
                cursor: crosshair !important;
                z-index: 999990 !important;
            }
            .gemini-highlight-picked {
                outline: 2px solid #4A90E2 !important;
                background-color: rgba(74, 144, 226, 0.25) !important;
                box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5) !important;
                z-index: 999991 !important;
            }

            /* Fix für spezifische Kleinanzeigen Container */
            .aditem-main--top--left.gemini-highlight-picked,
            .aditem-main--top--right.gemini-highlight-picked {
                outline: 2px solid #4A90E2 !important;
                background-color: rgba(74, 144, 226, 0.25) !important;
                position: relative !important;
            }
        `);
    }

    // --- 3.3 Skript starten ---
    function initHybrid() {
        if (document.body) {
            createHybridGUI();
            addHybridStyles();
        } else {
            window.addEventListener('DOMContentLoaded', () => {
                createHybridGUI();
                addHybridStyles();
            });
        }
    }

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

})();