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.

נכון ליום 19-11-2025. ראה הגרסה האחרונה.

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 or Violentmonkey 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==
// @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();
    }

})();