// ==UserScript== // @name Duck.ai Answer Copier V3 (keep line breaks v2) // @version 2.5 // @description Copies new AI answers to clipboard from Duck.ai (excludes reasoning) and preserves line breaks/paragraphs // @author GNUser // @match https://duck.ai/* // @grant GM_setClipboard // @run-at document-end // ==/UserScript== (function() { 'use strict'; let lastAnswerText = ''; let lastContainerId = null; let debounceTimer = null; const observer = new MutationObserver(() => { clearTimeout(debounceTimer); debounceTimer = setTimeout(checkForNewAnswer, 300); }); function checkForNewAnswer() { const containers = document.querySelectorAll('[data-activeresponse="true"]'); if (containers.length === 0) return; const latestContainer = containers[containers.length - 1]; const containerId = latestContainer.dataset.id || latestContainer.textContent.length; if (lastContainerId !== containerId) { lastContainerId = containerId; console.log('New response detected, ready to copy'); } const answerText = extractAnswerText(latestContainer); if (answerText && answerText.length > 20 && answerText !== lastAnswerText) { lastAnswerText = answerText; GM_setClipboard(answerText); console.log('✓ Answer copied to clipboard (' + answerText.length + ' chars)'); showNotification('Answer copied!'); } } function extractAnswerText(container) { const clone = container.cloneNode(true); // Remove reasoning/thinking sections (unchanged selectors) clone.querySelectorAll('[class*="Rkf36nZWET9BmlbubWjr"]').forEach(el => el.remove()); clone.querySelectorAll('[class*="swFO3435qp4ZavU5Pg5o"]').forEach(el => el.remove()); clone.querySelectorAll('button').forEach(btn => { if (btn.textContent.includes('Reasoning') || btn.textContent.includes('Thinking')) { const parent = btn.closest('[class*="ROSvG0TIGDWviuEY4B3S"]'); if (parent) parent.remove(); } }); const responseSection = clone.querySelector('[class*="ucB71mHYvyHsWKjYfYKI"]') || clone; const text = getTextPreserveBreaks(responseSection).trim(); return normalizeLineBreaks(text); } // Walk DOM and insert \n between block elements and for
, preserving spacing. function getTextPreserveBreaks(node) { let out = ''; const blockTags = new Set([ 'ADDRESS','ARTICLE','ASIDE','BLOCKQUOTE','DETAILS','DIALOG','DD','DIV','DL','DT','FIELDSET', 'FIGCAPTION','FIGURE','FOOTER','FORM','H1','H2','H3','H4','H5','H6','HEADER','HR','LI','MAIN', 'NAV','OL','P','PRE','SECTION','TABLE','TFOOT','UL','TR' ]); function walk(n) { if (n.nodeType === Node.TEXT_NODE) { out += n.nodeValue; return; } if (n.nodeType !== Node.ELEMENT_NODE) return; const tag = n.tagName; if (tag === 'BR') { out += '\n'; return; } const isBlock = blockTags.has(tag) || getComputedStyle(n).display === 'block'; if (isBlock && out && !out.endsWith('\n')) out += '\n'; for (const child of n.childNodes) walk(child); if (isBlock && !out.endsWith('\n')) out += '\n'; } walk(node); // Collapse multiple trailing/leading single newlines into double-newline paragraph separation // then trim excessive blanks out = out.replace(/\n{3,}/g, '\n\n'); return out; } function normalizeLineBreaks(s) { // Normalize CRLF/CR -> LF, collapse spaces around newlines return s.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/[ \t]+\n/g, '\n').replace(/\n[ \t]+/g, '\n').replace(/\n{2,}/g, '\n\n'); } function showNotification(message) { const notif = document.createElement('div'); notif.textContent = message; notif.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: #4CBA3C; color: white; padding: 12px 16px; border-radius: 8px; z-index: 10000; font-size: 14px; font-weight: 500; box-shadow: 0 2px 8px rgba(0,0,0,0.2); `; document.body.appendChild(notif); setTimeout(() => notif.remove(), 2000); } observer.observe(document.body, { childList: true, subtree: true, characterData: true }); })();