Vereint den LLM Snapshotter (Gesamtseite) und den Komp.-Analysator (Element-Pick) in einem Tool. Optimiert für Token-Effizienz, DevTools-Optik & UI-Usability.
Verzia zo dňa
// ==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();
}
})();