Greasy Fork

MyDealz JSONL-Kommentar Extraktor für LLM mit Markdown 🧙‍♂️

Exportiert Kommentare als LLM- und Markdown-optimiertes JSONL. Version 3.4: Finale Bereinigung des Prompt-Textes von sprachlichen Artefakten.

当前为 2025-06-10 提交的版本,查看 最新版本

// ==UserScript==
// @name         MyDealz JSONL-Kommentar Extraktor für LLM mit Markdown 🧙‍♂️
// @namespace    violentmonkey
// @version      3.4
// @description  Exportiert Kommentare als LLM- und Markdown-optimiertes JSONL. Version 3.4: Finale Bereinigung des Prompt-Textes von sprachlichen Artefakten.
// @match        https://www.mydealz.de/diskussion/*
// @match        https://www.mydealz.de/deals/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // --- Konfiguration ---
    const SCRIPT_VERSION = '3.4'; // Wird jetzt in den Metadaten verwendet

    // 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' };

    let collectedComments = [];
    let exportBtn = null;
    let scriptStart = Date.now();

    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) 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;
            } else { stableCount = 0; }
            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;
            }
            const obj = { text };
            if (like > 0) obj.like = like;
            if (helpful > 0) obj.helpful = helpful;
            if (funny > 0) obj.funny = 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) return '';
        return cleanText(descEl.innerText || descEl.textContent || '');
    }

    // BEREINIGT: Die fremdsprachigen Wörter wurden entfernt.
    function buildIntroText(commentCount) {
        const { title, url } = getThreadTitleAndUrl();
        return (
`# Analyse der ${commentCount} Kommentare 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
`
       );
    }
    
    function openExportWindowWithLLM(jsonlData, header, filename) {
        const w = window.open('', 'blank', 'width=950,height=800,resizable=yes,scrollbars=yes');
        if (!w) {
            showNotification('Popup blockiert! Bitte Popups für diese Seite erlauben.', 'error');
            throw new Error("Popup wurde blockiert.");
        }

        const popupStyle = `<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); } .export-btns, .ki-btn-row { display: flex; align-items: center; margin-bottom: 12px; flex-wrap: wrap; gap: 8px; } 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; } #copyBtn { background: #d32f2f; color: #fff; } #saveBtn { background: #007bff; color: #fff; } #copiedMsg { min-width: 60px; display: inline-block; color: #4caf50; font-weight: bold; opacity: 0; transition: opacity .3s; } .ki-btn { background: #eee; color: #333; } .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; }</style>`;
        const popupBodyHTML = `<div class="export-btns"><button id="copyBtn">Copy All</button><button id="saveBtn">Save .jsonl</button><span id="copiedMsg">&nbsp;</span></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>`;

        w.document.title = 'MyDealz Kommentar-Export';
        w.document.head.innerHTML = popupStyle;
        w.document.body.innerHTML = popupBodyHTML;
        w.document.getElementById('exportText').textContent = header + jsonlData;

        // Event handler für die Buttons im Popup
        const btnC = w.document.getElementById('copyBtn');
        const btnS = w.document.getElementById('saveBtn');
        const msg = w.document.getElementById('copiedMsg');
        btnC.onclick = () => { w.navigator.clipboard.writeText(header + jsonlData).then(() => { msg.textContent = 'Copied!'; msg.style.opacity = 1; setTimeout(() => { msg.style.opacity = 0; msg.textContent = '\xa0'; }, 2000); }); };
        btnS.onclick = () => { const blob = new Blob([header + jsonlData], { type: 'application/jsonl;charset=utf-8' }); 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); };
        KI_LINKS.forEach(btn => { const el = w.document.getElementById(btn.id); if (el) el.onclick = () => window.open(btn.url, '_blank'); });
    }

    async function runExport() {
        try {
            exportBtn.disabled = true;
            exportBtn.style.background = BTN_COLORS.NORMAL;
            scriptStart = Date.now();
            collectedComments = [];

            exportBtn.textContent = 'Starte Export...';
            await sleep(INTERVAL.UI_RENDER);

            const soll = getTotalCommentsFromLink();
            const first = document.querySelector(SELECTORS.FIRST_PAGE);
            if (first) {
                first.click();
                exportBtn.textContent = 'Gehe zu Seite 1...';
                await sleep(INTERVAL.PAGE);
            }

            let pageCount = 1;
            while (true) {
                const currentPageStr = document.querySelector(SELECTORS.CURRENT_PAGE)?.textContent.trim() || `~${pageCount}`;
                exportBtn.textContent = `Seite ${currentPageStr}: Erweitere Antworten...`;
                await sleep(INTERVAL.UI_RENDER);
                await expandAllRepliesRobust();

                exportBtn.textContent = `Seite ${currentPageStr}: Sammle Kommentare...`;
                await sleep(INTERVAL.UI_RENDER);
                collectCommentsOnPage();

                const btn = document.querySelector(SELECTORS.NEXT_PAGE);
                if (!btn) break;

                pageCount++;
                btn.click();
                exportBtn.textContent = `Lade Seite ${pageCount}...`;
                await sleep(INTERVAL.PAGE);
            }

            exportBtn.textContent = 'Bereite Export vor...';
            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)": soll,
                    "Export-Zeitpunkt": new Date().toISOString(),
                    "Laufzeit des Skripts": duration + "s",
                    "Skript-Version": SCRIPT_VERSION
                }
            };

            const jsonlData = JSON.stringify(metaObj) + '\n' + JSON.stringify({ "maindescription": maindescription }) + '\n' + collectedComments.map(obj => JSON.stringify(obj)).join('\n');
            const header = buildIntroText(soll);
            openExportWindowWithLLM(jsonlData, header, title.replace(/[\\/:*?"<>|]/g, '') || 'mydealz-comments');

            exportBtn.textContent = 'Fertig!';
            exportBtn.style.background = BTN_COLORS.SUCCESS;
            setTimeout(() => {
                exportBtn.textContent = 'Kommentare als JSONL exportieren';
                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 = 'Fehler! (Siehe Konsole)';
                 exportBtn.style.background = BTN_COLORS.ERROR;
            }
            setTimeout(() => {
                exportBtn.textContent = 'Kommentare als JSONL exportieren';
                exportBtn.style.background = BTN_COLORS.NORMAL;
                exportBtn.disabled = false;
            }, 5000);
        }
    }

    function injectExportBtn() {
        exportBtn = document.createElement('button');
        exportBtn.textContent = 'Kommentare als JSONL exportieren';
        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) {
            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);
    }
})();