// ==UserScript==
// @name MyDealz JSONL-Kommentar Extraktor für LLM mit Markdown 🧙♂️
// @namespace violentmonkey
// @version 3.8
// @description Exportiert Kommentare als LLM- und Markdown-optimiertes JSONL. Version 3.8: Fixes für aktiven Button-Status und Button-Padding.
// @match https://www.mydealz.de/diskussion/*
// @match https://www.mydealz.de/deals/*
// @icon https://www.mydealz.de/assets/img/emojis/pirate_1b64c.svg
// @grant none
// ==/UserScript==
(function () {
'use strict';
// --- Konfiguration ---
const SCRIPT_VERSION = '3.8';
// KI-Links
const KI_LINKS = [
{ id: 'chatgptBtn', label: 'ChatGPT', url: 'https://chatgpt.com/' },
{ id: 'perplexityBtn', label: 'Perplexity', url: 'https://www.perplexity.ai/' },
{ id: 'claudeBtn', label: 'Claude', url: 'https://claude.ai/' },
{ id: 'geminiBtn', label: 'Gemini', url: 'https://gemini.google.com/' },
{ id: 'mistralBtn', label: 'Mistral', url: 'https://chat.mistral.ai/chat' },
{ id: 'grokBtn', label: 'Grok', url: 'https://grok.com/' }
];
// Selektoren & Konstanten
const SELECTORS = {
REPLY_BTN: 'button[data-t="moreReplies"]:not([disabled])',
NEXT_PAGE: 'button[aria-label="Nächste Seite"]:not([disabled])',
FIRST_PAGE: 'button[aria-label="Erste Seite"]:not([disabled])',
CURRENT_PAGE: 'button[aria-label="Aktuelle Seite"]',
COMMENT_LINK: 'a.button--type-text[href*="#comments"]',
COMMENT_ARTICLE: 'article.comment',
THREAD_TITLE: '.thread-title .text--b.size--all-xl.size--fromW3-xxl'
};
const INTERVAL = { PAGE: 2000, REPLY_WAIT: 800, UI_RENDER: 50 };
const BTN_COLORS = { NORMAL: '#2c7ff3', ERROR: '#e53935', SUCCESS: '#4caf50', INFO: '#607d8b' }; // INFO für Logs
// Neue Konstanten für Textmeldungen
const MESSAGES = {
POPUP_BLOCKED: 'Popup blockiert! Bitte Popups für diese Seite erlauben.',
SCRIPT_RELOAD_PAGE_1: 'Skript setzt Seite auf 1 zurück für vollständigen Export...',
START_EXPORT: 'Starte Export...',
GOTO_PAGE_1: 'Gehe zu Seite 1...',
EXPANDING_REPLIES: (page, current, total) => `Seite ${page}: Erweitere Antworten (${current}/${total} gesammelt)...`,
COLLECTING_COMMENTS: (page, current, total) => `Seite ${page}: Sammle Kommentare (${current}/${total} gesammelt)...`,
LOADING_PAGE: (page, current, total) => `Lade Seite ${page} (${current}/${total} gesammelt)...`,
PREPARING_EXPORT: 'Bereite Export vor...',
EXPORT_COMPLETE: 'Fertig!',
NO_COMMENTS_FOUND: 'Keine Kommentare gefunden!',
EXPORT_WITH_DISCREPANCY: (found, expected) => `Achtung: ${found} von ${expected} Kommentaren erfasst!`,
ERROR_GENERIC: 'Fehler! (Siehe Konsole)',
COPIED: 'Copied!',
NOTHING_TO_COPY: 'Nichts zu kopieren!',
NO_ELEMENT_FOUND: (selector) => `Fehler: Element mit Selektor "${selector}" nicht gefunden.`,
EXPORT_BUTTON_LABEL: 'Kommentare als JSONL exportieren',
POPUP_LOADING: 'Generiere Daten...'
};
// Prompt-Level Definitionen
const PROMPT_LEVELS = {
SHORT: {
label: 'Kurz',
id: 'promptShortBtn', // Hinzugefügt für einfacheren Zugriff
intro: (title, url, commentCountNote) =>
`# Zusammenfassung der Kommentare zur MyDealz-Diskussion [${title}](${url})
## 🤖 Anweisung an die KI
**Deine Rolle:** Du bist ein hilfsbereiter Community-Analyst. Analysiere die Kommentare und fasse sie prägnant zusammen.
**Dein Ziel:** Erstelle eine kurze Zusammenfassung (max. 1-2 Sätze pro Punkt) mit den wichtigsten Meinungen (Pro, Contra, Neutral).
---
## 📋 Gewünschtes Ausgabeformat (Markdown)
### Allgemeine Stimmung
### ✅ Positive Aspekte (Pros)
### ❌ Negative Aspekte (Kritik & Nachteile)
### 💡 Neutrale Beobachtungen & Fragen
### 🏁 Fazit
---
## Daten
`
},
MEDIUM: {
label: 'Durchschnittlich',
id: 'promptMediumBtn', // Hinzugefügt
intro: (title, url, commentCountNote) =>
`# Analyse der ${commentCountNote} zur MyDealz-Diskussion [${title}](${url})
## 🤖 Anweisung an die KI
**Deine Rolle:** Du bist ein hilfsbereiter Community-Analyst. Deine Aufgabe ist es, die bereitgestellten Benutzerkommentare zu analysieren und eine strukturierte, leicht verständliche Zusammenfassung zu erstellen.
**Dein Input:** Die Daten liegen im JSONL-Format vor (jedes Objekt eine neue Zeile):
- \`{"Metadaten": {...}}\`: Enthält Kontext. **Ignoriere diesen Block in deiner finalen Ausgabe.**
- \`{"maindescription": "..."}\`: Die ursprüngliche Deal-Beschreibung als Kontext.
- \`{"text": "...", "like": ...}\`: Die einzelnen Benutzerkommentare mit Bewertungen.
**Dein Ziel:** Erstelle eine Zusammenfassung, die die wichtigsten Meinungen, Pro- & Contra-Punkte sowie wiederkehrende Themen beleuchtet.
---
## 📋 Gewünschtes Ausgabeformat (Markdown)
Bitte halte dich exakt an die folgende Gliederung und nutze passende Emojis:
### Allgemeine Stimmung
*(Ein kurzer Absatz über den allgemeinen Ton der Diskussion. Z.B. "Überwiegend positiv", "gemischte Gefühle", "hitzige Debatte".)*
### ✅ Positive Aspekte (Pros)
- *Liste der positiv bewerteten Punkte, die von Nutzern genannt wurden.*
- *Zum Beispiel: "Gute Qualität", "Schnelle Lieferung", "Preis-Leistungs-Verhältnis top".*
### ❌ Negative Aspekte (Kritik & Nachteile)
- *Liste der Kritikpunkte und genannten Nachteile.*
- *Zum Beispiel: "Hält nicht lange", "Schlechter Kundenservice", "Zu teuer".*
### 💡 Neutrale Beobachtungen & Fragen
- *Liste der wiederkehrenden Fragen oder neutralen Anmerkungen.*
- *Zum Beispiel: "Frage nach der Kompatibilität mit X", "Vergleich mit Produkt Y".*
### 💬 Wichtige Zitate
> **"Ein besonders positives oder repräsentatives Zitat..."**
>
> **"Ein repräsentatives kritisches Zitat oder eine wichtige Beobachtung..."**
### 🏁 Fazit
*(Eine abschließende, neutrale Zusammenfassung der Diskussion in 2-3 prägnanten Sätzen.)*
---
## Daten
`
},
DETAILED: {
label: 'Detailliert',
id: 'promptDetailedBtn', // Hinzugefügt
intro: (title, url, commentCountNote) =>
`# Ausführliche Analyse der ${commentCountNote} zur MyDealz-Diskussion [${title}](${url})
## 🤖 Anweisung an die KI
**Deine Rolle:** Du bist ein sehr detailorientierter Community-Analyst. Deine Aufgabe ist es, die bereitgestellten Benutzerkommentare tiefgehend zu analysieren und eine umfassende, gut strukturierte und leicht verständliche Zusammenfassung zu erstellen.
**Dein Input:** Die Daten liegen im JSONL-Format vor (jedes Objekt eine neue Zeile):
- \`{"Metadaten": {...}}\`: Enthält Kontext. **Ignoriere diesen Block in deiner finalen Ausgabe.**
- \`{"maindescription": "..."}\`: Die ursprüngliche Deal-Beschreibung als Kontext.
- \`{"text": "...", "like": ...}\`: Die einzelnen Benutzerkommentare mit Bewertungen.
**Dein Ziel:** Erstelle eine ausführliche und nuancierte Zusammenfassung, die alle wichtigen Meinungen, detaillierte Pro- & Contra-Punkte (ggf. mit Sub-Punkten), wiederkehrende Themen, häufig gestellte Fragen und bemerkenswerte Beobachtungen beleuchtet. Gehe auch auf die Stärke der Meinungen ein (z.B. "starke Zustimmung", "vereinzelt kritisiert").
---
## 📋 Gewünschtes Ausgabeformat (Markdown)
Bitte halte dich exakt an die folgende Gliederung und nutze passende Emojis:
### Allgemeine Stimmung
*(Ein ausführlicher Absatz über den allgemeinen Ton der Diskussion, die vorherrschenden Emotionen und die Dynamik der Debatte.)*
### ✅ Positive Aspekte (Pros)
- *Detaillierte Liste der positiv bewerteten Punkte, die von Nutzern genannt wurden. Füge, wenn möglich, Beispiele oder Kontext hinzu.*
- *Unterpunkte für spezifische Details oder Nuancen.*
### ❌ Negative Aspekte (Kritik & Nachteile)
- *Detaillierte Liste der Kritikpunkte und genannten Nachteile. Beschreibe die Art der Kritik und ihre Häufigkeit.*
- *Unterpunkte für spezifische Probleme oder wiederkehrende Beschwerden.*
### 💡 Neutrale Beobachtungen & Häufig gestellte Fragen
- *Ausführliche Liste der wiederkehrenden Fragen, neutralen Anmerkungen oder Informationen, die keine klare positive oder negative Wertung haben.*
- *Unterpunkte für spezifische Fragen oder Vergleiche.*
### 📈 Auffällige Trends & Muster
*(Ein Absatz über besondere Muster in den Kommentaren, z.B. bestimmte Argumentationsketten, Vergleiche mit anderen Produkten oder die Reaktion auf bestimmte Aspekte des Deals.)*
### 💬 Wichtige Zitate
> **"Ein besonders prägnantes oder repräsentatives positives Zitat, das die Stimmung gut einfängt."**
>
> **"Ein repräsentatives kritisches Zitat, das ein Hauptproblem oder eine starke Meinungsverschiedenheit verdeutlicht."**
>
> **"Ein neutrales, aufschlussreiches Zitat, das eine wichtige Beobachtung oder Frage hervorhebt."**
### 🏁 Fazit
*(Eine abschließende, ausgewogene und detaillierte Zusammenfassung der gesamten Diskussion in 3-5 prägnanten Sätzen, die die Essenz der Kommentare einfängt.)*
---
## Daten
`
}
};
let collectedComments = [];
let exportBtn = null;
let scriptStart = Date.now();
let totalExpectedComments = 0; // Speichert die erwartete Anzahl Kommentare
let currentPromptLevel = 'MEDIUM'; // Speichert den aktuell aktiven Prompt-Level
// Array zum Speichern von Konsolenmeldungen für Debug-Zwecke im Popup
const consoleLogs = [];
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
// Überschreiben von console.log, console.error und console.warn, um Meldungen abzufangen
console.log = function (...args) {
consoleLogs.push({ type: 'log', message: args.map(a => String(a)).join(' ') });
originalConsoleLog.apply(console, args);
};
console.error = function (...args) {
consoleLogs.push({ type: 'error', message: args.map(a => String(a)).join(' ') });
originalConsoleError.apply(console, args);
};
console.warn = function (...args) {
consoleLogs.push({ type: 'warn', message: args.map(a => String(a)).join(' ') });
originalConsoleWarn.apply(console, args);
};
const sleep = ms => new Promise(r => setTimeout(r, ms));
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
const bgColor = type === 'error' ? BTN_COLORS.ERROR : BTN_COLORS.NORMAL;
Object.assign(notification.style, {
position: 'fixed', top: '80px', right: '20px', padding: '12px 20px', background: bgColor, color: '#fff',
border: 'none', borderRadius: '4px', fontSize: '14px', zIndex: 10000, boxShadow: '0 4px 8px rgba(0,0,0,0.2)',
opacity: '0', transition: 'opacity 0.3s ease-in-out'
});
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => notification.style.opacity = '1', 10);
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => document.body.removeChild(notification), 300);
}, 5000);
}
function getTotalCommentsFromLink() {
const link = document.querySelector(SELECTORS.COMMENT_LINK);
if (!link) {
console.warn(MESSAGES.NO_ELEMENT_FOUND(SELECTORS.COMMENT_LINK));
return 0;
}
const m = link.textContent.match(/\d+/);
return m ? parseInt(m[0], 10) : 0;
}
async function expandAllRepliesRobust() {
let lastCount = -1;
let stableCount = 0;
while (true) {
const btns = Array.from(document.querySelectorAll(SELECTORS.REPLY_BTN)).filter(btn => btn.offsetParent !== null);
if (btns.length === 0) break;
if (btns.length === lastCount) {
stableCount++;
if (stableCount >= 3) break; // Warte 3 Iterationen ohne Änderung
} else { stableCount = 0; } // Reset, wenn sich Anzahl ändert
lastCount = btns.length;
btns.forEach(btn => btn.click());
await sleep(INTERVAL.REPLY_WAIT);
}
}
function cleanText(text) {
return text.replace(/[\n\r\t]+/g, ' ').replace(/\s\s+/g, ' ').trim();
}
function collectCommentsOnPage() {
const articles = document.querySelectorAll(SELECTORS.COMMENT_ARTICLE);
for (const article of articles) {
const bodyNode = article.querySelector('.comment-body .userHtml-content');
const text = bodyNode ? cleanText(bodyNode.textContent) : '';
const reactionsBtn = article.querySelector('button.comment-reactions');
let like = 0, helpful = 0, funny = 0;
if (reactionsBtn) {
const likeSpan = reactionsBtn.querySelector('.comment-like');
const helpfulSpan = reactionsBtn.querySelector('.comment-helpful');
const funnySpan = reactionsBtn.querySelector('.comment-funny');
like = likeSpan ? parseInt(likeSpan.textContent.trim(), 10) || 0 : 0;
helpful = helpfulSpan ? parseInt(helpfulSpan.textContent.trim(), 10) || 0 : 0;
funny = funnySpan ? parseInt(funnySpan.textContent.trim(), 10) || 0 : 0;
}
// Destrukturierung wie vorgeschlagen
const obj = { text, ...(like > 0 && { like }), ...(helpful > 0 && { helpful }), ...(funny > 0 && { funny }) };
collectedComments.push(obj);
}
}
function getThreadTitleAndUrl() {
let title = '';
const el = document.querySelector(SELECTORS.THREAD_TITLE);
if (el) {
title = el.textContent.trim();
} else {
title = document.title.replace(/\|.*$/, '').trim();
}
let url = window.location.origin + window.location.pathname;
return { title, url };
}
function getMainDescription() {
let descEl = document.querySelector('.picker-highlight') ||
document.querySelector('.userHtml-content:not(.comment-body .userHtml-content)') ||
document.querySelector('.thread--content');
if (!descEl) {
console.warn(MESSAGES.NO_ELEMENT_FOUND('.picker-highlight, .userHtml-content:not(.comment-body .userHtml-content), .thread--content'));
return '';
}
return cleanText(descEl.innerText || descEl.textContent || '');
}
// Angepasste Funktion zum Erstellen des Intro-Textes mit Level-Parameter
function buildIntroText(commentCount, actualCommentCount, level = 'MEDIUM') {
const { title, url } = getThreadTitleAndUrl();
let commentCountNote = `${commentCount} Kommentare`;
if (actualCommentCount < commentCount) {
commentCountNote = `${actualCommentCount} von ${commentCount} Kommentaren (potenziell unvollständig)`;
} else if (actualCommentCount > commentCount) {
commentCountNote = `${actualCommentCount} Kommentare (mehr als erwartet, alle erfasst)`;
}
const promptTemplate = PROMPT_LEVELS[level.toUpperCase()]?.intro;
if (!promptTemplate) {
console.error(`Unbekanntes Prompt-Level: ${level}. Nutze Standard 'MEDIUM'.`);
return PROMPT_LEVELS.MEDIUM.intro(title, url, commentCountNote);
}
return promptTemplate(title, url, commentCountNote);
}
function openExportWindowWithLLM(jsonlData, initialHeader, filename) {
const w = window.open('', 'blank', 'width=950,height=800,resizable=yes,scrollbars=yes');
if (!w) {
showNotification(MESSAGES.POPUP_BLOCKED, 'error');
throw new Error("Popup wurde blockiert.");
}
w.document.title = 'MyDealz Kommentar-Export';
w.document.head.innerHTML = `
<style>
html, body { height: 100%; margin: 0; padding: 0; box-sizing: border-box; overflow: hidden; }
body { font-family: sans-serif; margin: 20px; min-width: 600px; min-height: 400px; box-sizing: border-box; display: flex; flex-direction: column; align-items: stretch; height: calc(100vh - 40px); }
.button-row { display: flex; align-items: center; margin-bottom: 12px; flex-wrap: wrap; gap: 8px; padding: 5px; } /* Padding hinzugefügt */
.button-row-left { display: flex; align-items: center; gap: 8px; }
.button-row-right { display: flex; align-items: center; gap: 8px; margin-left: auto; } /* Rechtsbündig */
.ki-btn-row { display: flex; align-items: center; margin-bottom: 12px; flex-wrap: wrap; gap: 8px; padding: 5px; } /* Padding hinzugefügt */
button {
display: inline-flex;
align-items: center;
padding: 10px 16px;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s; /* Hover-Effekt */
}
button:hover:not(:disabled) {
transform: translateY(-1px); /* Leichter "Lift"-Effekt */
filter: brightness(1.1); /* Leichter heller beim Hover */
}
button:active:not(:disabled) {
transform: translateY(0); /* Zurücksetzen beim Klick */
filter: brightness(0.9); /* Leichter dunkler beim Klick */
}
#copyBtn { background: #d32f2f; color: #fff; }
#saveBtn { background: #007bff; color: #fff; }
.prompt-level-btn { background: #6c757d; color: #fff; } /* Grau für Prompt-Level */
.prompt-level-btn.active { background: #28a745; } /* Grün, wenn aktiv */
#showLogsBtn { background: ${BTN_COLORS.INFO}; color: #fff; } /* INFO Farbe für Logs */
#copiedMsg { min-width: 60px; display: inline-block; color: #4caf50; font-weight: bold; opacity: 0; transition: opacity .3s; }
.ki-btn { background: #eee; color: #333; }
.ki-btn:hover { background: #e0e0e0; } /* Hellerer Hover für KI-Buttons */
.main-content { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
pre { flex: 1 1 auto; min-height: 200px; max-height: 70vh; white-space: pre-wrap; word-wrap: break-word; border: 1px solid #ccc; padding: 12px; border-radius: 4px; overflow: auto; margin: 0; background: #fafafa; }
#loadingOverlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.9);
display: flex; justify-content: center; align-items: center; z-index: 10001; font-size: 24px; font-weight: bold;
}
#errorMsg { color: #e53935; font-weight: bold; margin-left: 10px; }
.log-container {
flex: 1 1 auto; min-height: 200px; max-height: 70vh; white-space: pre-wrap; word-wrap: break-word; border: 1px solid #ccc; padding: 12px; border-radius: 4px; overflow: auto; margin: 0; background: #fafafa;
display: none; /* Initial versteckt */
}
.log-message { font-family: monospace; font-size: 13px; margin: 0; padding: 2px 0; }
.log-message.error { color: #e53935; }
.log-message.warn { color: #ff9800; }
.log-message.log { color: #333; }
</style>
`;
w.document.body.innerHTML = `
<div id="loadingOverlay">${MESSAGES.POPUP_LOADING}</div>
<div class="button-row">
<div class="button-row-left">
<button id="copyBtn">Copy All</button>
<button id="saveBtn">Save .jsonl</button>
<button id="${PROMPT_LEVELS.SHORT.id}" class="prompt-level-btn">${PROMPT_LEVELS.SHORT.label}</button>
<button id="${PROMPT_LEVELS.MEDIUM.id}" class="prompt-level-btn">${PROMPT_LEVELS.MEDIUM.label}</button>
<button id="${PROMPT_LEVELS.DETAILED.id}" class="prompt-level-btn">${PROMPT_LEVELS.DETAILED.label}</button>
</div>
<div class="button-row-right">
<span id="copiedMsg"> </span>
<span id="errorMsg"></span>
<button id="showLogsBtn">Error Logs</button>
</div>
</div>
<div class="ki-btn-row">
${KI_LINKS.map(btn => `<button class="ki-btn" id="${btn.id}">${btn.label}</button>`).join('')}
</div>
<div class="main-content">
<pre id="exportText"></pre>
<div id="logContainer" class="log-container"></div>
</div>
`;
// Zeige Lade-Overlay
const loadingOverlay = w.document.getElementById('loadingOverlay');
loadingOverlay.style.display = 'flex';
// Verzögere das Setzen des Inhalts, um die Ladeanzeige sichtbar zu machen
setTimeout(() => {
const exportTextPre = w.document.getElementById('exportText');
exportTextPre.textContent = initialHeader + jsonlData;
loadingOverlay.style.display = 'none'; // Lade-Overlay ausblenden
// Event handler für die Buttons im Popup
const btnC = w.document.getElementById('copyBtn');
const btnS = w.document.getElementById('saveBtn');
const btnLogs = w.document.getElementById('showLogsBtn');
const msg = w.document.getElementById('copiedMsg');
const errorMsgSpan = w.document.getElementById('errorMsg');
const logContainerDiv = w.document.getElementById('logContainer');
// Prompt-Level Buttons
const promptShortBtn = w.document.getElementById(PROMPT_LEVELS.SHORT.id);
const promptMediumBtn = w.document.getElementById(PROMPT_LEVELS.MEDIUM.id);
const promptDetailedBtn = w.document.getElementById(PROMPT_LEVELS.DETAILED.id);
const promptLevelBtns = [promptShortBtn, promptMediumBtn, promptDetailedBtn];
// Setze den initial aktiven Button
w.document.getElementById(PROMPT_LEVELS[currentPromptLevel].id).classList.add('active');
const updatePromptLevel = (level) => {
currentPromptLevel = level; // Aktuellen Level speichern
// Diese Daten müssen aktuell gehalten werden, da sie sich nach dem initialen Export nicht ändern
const { title, url } = getThreadTitleAndUrl();
const maindescription = getMainDescription();
// Den Daten-Teil ohne Metadaten und maindescription generieren, da diese konstant bleiben
const commentsJsonl = collectedComments.map(obj => JSON.stringify(obj)).join('\n');
const metaObj = {
"Metadaten": {
"Titel": title,
"URL": url,
"Kommentare (gefunden)": collectedComments.length,
"Kommentare (erwartet)": totalExpectedComments,
"Export-Zeitpunkt": new Date().toISOString(),
"Laufzeit des Skripts": Math.round((Date.now() - scriptStart) / 1000) + "s", // Aktualisierte Laufzeit
"Skript-Version": SCRIPT_VERSION
}
};
const newHeader = buildIntroText(totalExpectedComments, collectedComments.length, level);
// Wichtig: Den vollständigen String neu zusammensetzen, da der JSONL-Teil auch von Metadaten und maindescription abhängt
exportTextPre.textContent = newHeader + JSON.stringify(metaObj) + '\n' + JSON.stringify({ "maindescription": maindescription }) + '\n' + commentsJsonl;
// Setze den aktiven Status der Buttons zurück und dann den neuen aktiven Button
promptLevelBtns.forEach(btn => btn.classList.remove('active'));
w.document.getElementById(PROMPT_LEVELS[level].id).classList.add('active');
};
promptShortBtn.onclick = () => updatePromptLevel('SHORT');
promptMediumBtn.onclick = () => updatePromptLevel('MEDIUM');
promptDetailedBtn.onclick = () => updatePromptLevel('DETAILED');
btnC.onclick = () => {
if (!exportTextPre.textContent) { // Prüfen, ob überhaupt Inhalt da ist
errorMsgSpan.textContent = MESSAGES.NOTHING_TO_COPY;
setTimeout(() => errorMsgSpan.textContent = '', 3000);
return;
}
w.navigator.clipboard.writeText(exportTextPre.textContent).then(() => { // Aktuellen Inhalt kopieren
msg.textContent = MESSAGES.COPIED;
msg.style.opacity = 1;
errorMsgSpan.textContent = ''; // Fehlermeldung löschen
setTimeout(() => { msg.style.opacity = 0; msg.textContent = '\xa0'; }, 2000);
}).catch(err => {
errorMsgSpan.textContent = `Copy-Fehler: ${err.message}`;
console.error("Fehler beim Kopieren:", err);
});
};
btnS.onclick = () => {
try {
const blob = new Blob([exportTextPre.textContent], { type: 'application/jsonl;charset=utf-8' }); // Aktuellen Inhalt speichern
const url = w.URL.createObjectURL(blob);
const a = w.document.createElement('a');
a.href = url;
a.download = filename + '.jsonl';
w.document.body.appendChild(a);
a.click();
w.URL.revokeObjectURL(url);
w.document.body.removeChild(a);
errorMsgSpan.textContent = ''; // Fehlermeldung löschen
} catch (err) {
errorMsgSpan.textContent = `Speicher-Fehler: ${err.message}`;
console.error("Fehler beim Speichern:", err);
}
};
// Event Listener für den Debug-Button
btnLogs.onclick = () => {
if (logContainerDiv.style.display === 'none') {
// Logs anzeigen
logContainerDiv.innerHTML = '';
consoleLogs.forEach(entry => {
const p = w.document.createElement('p');
p.className = `log-message ${entry.type}`;
p.textContent = `[${entry.type.toUpperCase()}] ${entry.message}`;
logContainerDiv.appendChild(p);
});
logContainerDiv.style.display = 'block';
exportTextPre.style.display = 'none'; // Daten-Pre ausblenden
btnLogs.textContent = 'Hide Logs';
} else {
// Logs verstecken
logContainerDiv.style.display = 'none';
exportTextPre.style.display = 'block'; // Daten-Pre wieder einblenden
btnLogs.textContent = 'Error Logs';
}
};
KI_LINKS.forEach(btn => {
const el = w.document.getElementById(btn.id);
if (el) el.onclick = () => window.open(btn.url, '_blank');
});
}, 50); // Kleine Verzögerung für das Lade-Overlay
}
async function runExport() {
try {
exportBtn.disabled = true;
exportBtn.style.background = BTN_COLORS.NORMAL;
scriptStart = Date.now();
collectedComments = [];
consoleLogs.length = 0; // Konsolen-Logs vor neuem Export leeren
exportBtn.textContent = MESSAGES.START_EXPORT;
await sleep(INTERVAL.UI_RENDER);
totalExpectedComments = getTotalCommentsFromLink(); // Gesamtanzahl der Kommentare am Anfang erfassen
const first = document.querySelector(SELECTORS.FIRST_PAGE);
if (first) {
first.click();
exportBtn.textContent = MESSAGES.GOTO_PAGE_1;
await sleep(INTERVAL.PAGE);
}
let pageCount = 1;
while (true) {
const currentPageStr = document.querySelector(SELECTORS.CURRENT_PAGE)?.textContent.trim() || `~${pageCount}`;
const currentCommentsCollected = collectedComments.length;
exportBtn.textContent = MESSAGES.EXPANDING_REPLIES(currentPageStr, currentCommentsCollected, totalExpectedComments);
await sleep(INTERVAL.UI_RENDER);
await expandAllRepliesRobust();
exportBtn.textContent = MESSAGES.COLLECTING_COMMENTS(currentPageStr, currentCommentsCollected, totalExpectedComments);
await sleep(INTERVAL.UI_RENDER);
collectCommentsOnPage();
const btn = document.querySelector(SELECTORS.NEXT_PAGE);
if (!btn) break;
pageCount++;
btn.click();
exportBtn.textContent = MESSAGES.LOADING_PAGE(pageCount, collectedComments.length, totalExpectedComments);
await sleep(INTERVAL.PAGE);
}
exportBtn.textContent = MESSAGES.PREPARING_EXPORT;
await sleep(INTERVAL.UI_RENDER);
const ist = collectedComments.length;
const duration = Math.round((Date.now() - scriptStart) / 1000);
const { title, url } = getThreadTitleAndUrl();
const maindescription = getMainDescription();
const metaObj = {
"Metadaten": {
"Titel": title,
"URL": url,
"Kommentare (gefunden)": ist,
"Kommentare (erwartet)": totalExpectedComments,
"Export-Zeitpunkt": new Date().toISOString(),
"Laufzeit des Skripts": duration + "s",
"Skript-Version": SCRIPT_VERSION
}
};
// Meldung bei 0 Kommentaren oder Diskrepanz
if (ist === 0) {
exportBtn.textContent = MESSAGES.NO_COMMENTS_FOUND;
exportBtn.style.background = BTN_COLORS.ERROR;
showNotification(MESSAGES.NO_COMMENTS_FOUND, 'error');
} else if (ist < totalExpectedComments) {
exportBtn.textContent = MESSAGES.EXPORT_WITH_DISCREPANCY(ist, totalExpectedComments);
exportBtn.style.background = BTN_COLORS.ERROR; // Hinweis auf Problem
showNotification(MESSAGES.EXPORT_WITH_DISCREPANCY(ist, totalExpectedComments), 'error');
} else {
exportBtn.textContent = MESSAGES.EXPORT_COMPLETE;
exportBtn.style.background = BTN_COLORS.SUCCESS;
}
// JSONL-Daten für den Export erstellen
const jsonlData = JSON.stringify(metaObj) + '\n' + JSON.stringify({ "maindescription": maindescription }) + '\n' + collectedComments.map(obj => JSON.stringify(obj)).join('\n');
// Initialen Header mit DEFAULT (MEDIUM) Level erstellen
const initialHeader = buildIntroText(totalExpectedComments, ist, currentPromptLevel); // currentPromptLevel verwenden
openExportWindowWithLLM(jsonlData, initialHeader, title.replace(/[\\/:*?"<>|]/g, '') || 'mydealz-comments');
setTimeout(() => {
exportBtn.textContent = MESSAGES.EXPORT_BUTTON_LABEL;
exportBtn.style.background = BTN_COLORS.NORMAL;
exportBtn.disabled = false;
}, 4000);
} catch (error) {
console.error("Fehler beim Kommentar-Export:", error);
if (error.message !== "Popup wurde blockiert.") {
exportBtn.textContent = MESSAGES.ERROR_GENERIC;
exportBtn.style.background = BTN_COLORS.ERROR;
showNotification(MESSAGES.ERROR_GENERIC, 'error');
}
setTimeout(() => {
exportBtn.textContent = MESSAGES.EXPORT_BUTTON_LABEL;
exportBtn.style.background = BTN_COLORS.NORMAL;
exportBtn.disabled = false;
}, 5000);
}
}
function injectExportBtn() {
exportBtn = document.createElement('button');
exportBtn.textContent = MESSAGES.EXPORT_BUTTON_LABEL;
Object.assign(exportBtn.style, {
position: 'fixed', top: '20px', right: '20px', padding: '10px 16px', background: BTN_COLORS.NORMAL,
color: '#fff', border: 'none', borderRadius: '4px', fontSize: '14px', cursor: 'pointer', zIndex: 9999,
transition: 'background-color 0.3s'
});
exportBtn.onclick = runExport;
document.body.appendChild(exportBtn);
}
function ensureStartOnPageOne() {
const url = new URL(window.location.href);
const isCommentPage = url.hash.includes('comments');
const pageParam = url.searchParams.get('page');
if (pageParam && pageParam !== '1' && isCommentPage) {
showNotification(MESSAGES.SCRIPT_RELOAD_PAGE_1); // Benachrichtigung einblenden
url.searchParams.set('page', '1');
url.hash = 'comments';
window.location.href = url.toString();
} else {
injectExportBtn();
}
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
ensureStartOnPageOne();
} else {
window.addEventListener('load', ensureStartOnPageOne);
}
})();