// ==UserScript== // @name Chatwoot Quote Generator // @namespace http://tampermonkey.net/ // @version 0.9 // @description Generate a formatted quote from Chatwoot conversation // @author You // @match https://chatwoot.abawo.de/* // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; // Configuration with default values let config = { maxMessages: 5, language: 'en' }; // Separator to identify the end of quotes const QUOTE_SEPARATOR = "----------------- "; // Month translations for German date formatting const monthTranslations = { 'Jan': 'Jan', 'Feb': 'Feb', 'Mar': 'Mär', 'Apr': 'Apr', 'May': 'Mai', 'Jun': 'Jun', 'Jul': 'Jul', 'Aug': 'Aug', 'Sep': 'Sep', 'Oct': 'Okt', 'Nov': 'Nov', 'Dec': 'Dez' }; // Translations const translations = { en: { wrote_at: 'wrote at', from_abawo: 'from abawo', max_messages: 'Maximum messages to quote:', language: 'Language:', save: 'Save', cancel: 'Cancel', quote_config: 'Quote Configuration', english: 'English', german: 'German' }, de: { wrote_at: 'schrieb am', from_abawo: 'von abawo', max_messages: 'Maximale Anzahl zu zitierender Nachrichten:', language: 'Sprache:', save: 'Speichern', cancel: 'Abbrechen', quote_config: 'Zitat-Konfiguration', english: 'Englisch', german: 'Deutsch' } }; // Helper function to get translation function t(key) { return translations[config.language][key] || translations.en[key]; } // Function to format date in German style function formatDateGerman(dateStr) { // Common format is "Jun 17, 2:33 PM" try { // Extract components from the date string const parts = dateStr.match(/(\w+)\s+(\d+),\s+(\d+):(\d+)\s+(\w+)/); if (!parts) return dateStr; // Return original if no match const month = monthTranslations[parts[1]] || parts[1]; const day = parts[2]; let hour = parseInt(parts[3]); const minute = parts[4]; const ampm = parts[5]; // Convert to 24-hour format if (ampm === 'PM' && hour < 12) hour += 12; if (ampm === 'AM' && hour === 12) hour = 0; // Format as "17. Jun um 14:33" return `${day}. ${month} um ${hour.toString().padStart(2, '0')}:${minute}`; } catch (e) { console.error('Error formatting date:', e); return dateStr; // Return original on error } } // Load saved configuration if available try { const savedConfig = GM_getValue('chatwootQuoteConfig'); if (savedConfig) { config = JSON.parse(savedConfig); } } catch (e) { console.error('Failed to load configuration:', e); } // Save configuration function saveConfig() { GM_setValue('chatwootQuoteConfig', JSON.stringify(config)); } // Function to wait for an element to be available in the DOM function waitForElement(selector, callback, maxWaitTime = 10000) { const startTime = Date.now(); const checkExist = setInterval(function() { const element = document.querySelector(selector); if (element) { clearInterval(checkExist); callback(element); } else if (Date.now() - startTime > maxWaitTime) { clearInterval(checkExist); console.log(`Element ${selector} not found after ${maxWaitTime}ms`); } }, 100); } // IMPROVED: Function to extract message text excluding blockquotes and HR elements function extractTextExcludingQuotes(textContentElement) { if (!textContentElement) return ''; // Check if there are blockquotes const blockquotes = textContentElement.querySelectorAll('blockquote'); const hrElements = textContentElement.querySelectorAll('hr'); // If there are no blockquotes or hr elements, return the full text if (blockquotes.length === 0 && hrElements.length === 0) { // Customer email messages often have a different structure if (textContentElement.querySelector('div[dir="auto"]')) { return textContentElement.querySelector('div[dir="auto"]').textContent.trim(); } // Messages with paragraphs else if (textContentElement.querySelectorAll('p').length > 0) { const paragraphs = textContentElement.querySelectorAll('p'); return Array.from(paragraphs) .map(p => p.textContent.trim()) .filter(t => t) .join('\n'); } // Fallback for any other format else { return textContentElement.textContent.trim(); } } // Clone the node to avoid modifying the original const clonedNode = textContentElement.cloneNode(true); // Remove all blockquotes blockquotes.forEach((blockquote, index) => { const correspondingBlockquote = clonedNode.querySelectorAll('blockquote')[index]; if (correspondingBlockquote) { correspondingBlockquote.remove(); } }); // Remove all hr elements hrElements.forEach((hr, index) => { const correspondingHr = clonedNode.querySelectorAll('hr')[index]; if (correspondingHr) { correspondingHr.remove(); } }); // IMPROVED: Extract text with better line break handling let text = ''; // Process paragraph elements if they exist if (clonedNode.querySelectorAll('p').length > 0) { const paragraphs = clonedNode.querySelectorAll('p'); text = Array.from(paragraphs) .map(p => p.textContent.trim()) .filter(t => t) .join('\n'); } else { // Get the text content text = clonedNode.textContent.trim(); } // IMPROVED: Clean up problematic formatting // Replace double >> with single > text = text.replace(/>>\s*>/g, '> '); // Fix missing spaces after commas in quoted content text = text.replace(/,(?=\S)/g, ', '); // Ensure proper line breaks for quoted content // Look for patterns like "> text> more text" and fix them text = text.replace(/>\s*([^>]+?)>\s*/g, "> $1\n> "); // Fix cases where quote separator is in the middle of text if (text.includes(QUOTE_SEPARATOR)) { const parts = text.split(QUOTE_SEPARATOR); text = parts[0].trim(); } // Normalize multiple consecutive line breaks text = text.replace(/\n{3,}/g, '\n\n'); return text; } // Function to extract message data function extractMessages() { // Get customer name const customerNameElement = document.querySelector('.conversation-details-wrap .text-base.font-medium.truncate.leading-tight.text-n-slate-12'); const customerName = customerNameElement ? customerNameElement.textContent.trim() : "Customer"; // Get agent name const agentNameElement = document.querySelector('.conversation-details-wrap h3.flex-shrink.max-w-full.min-w-0.my-0.text-base.capitalize.break-words.text-n-slate-12'); const agentName = agentNameElement ? agentNameElement.textContent.trim() : "Agent"; // Get all message items const messages = []; const messageItems = document.querySelectorAll('.conversation-panel > li'); messageItems.forEach(li => { // Skip system messages (center) if (li.classList.contains('center')) return; const bubble = li.querySelector('.bubble'); if (!bubble) return; // Skip private notes if (bubble.classList.contains('is-private')) return; const isCustomer = li.classList.contains('left'); // Determine sender name let senderName = isCustomer ? customerName : `${agentName} ${t('from_abawo')}`; // Extract timestamp const timeElement = li.querySelector('.time'); const time = timeElement ? timeElement.textContent.trim() : ""; // Format the time according to the selected language const formattedTime = config.language === 'de' ? formatDateGerman(time) : time; // Extract message text const textWrap = bubble.querySelector('.message-text__wrap'); if (!textWrap) return; const textContentElement = textWrap.querySelector('.text-content'); // Extract text while excluding blockquotes let text = extractTextExcludingQuotes(textContentElement); if (text) { messages.push({ isCustomer, name: senderName, time: formattedTime, text }); } }); // Limit the number of messages based on configuration if (messages.length > config.maxMessages) { messages.splice(0, messages.length - config.maxMessages); } return { messages }; } // Function to format messages as a quote function formatQuote(data) { let quote = ""; data.messages.forEach((message, index) => { if (index > 0) { quote += "\n>\n"; // Empty line with ">" between messages } quote += `> ${message.name} ${t('wrote_at')} ${message.time}:\n> ${message.text.replace(/\n/g, '\n> ')}`; }); // Add separator to mark the end of quotes if (quote) { quote += `\n\n${QUOTE_SEPARATOR}\n\n`; } return quote; } // Function to add the quote to the text area function addQuoteToTextArea(quote) { const textArea = document.querySelector('.ProseMirror'); if (!textArea) return; // Focus the editor textArea.focus(); // Clear the editor if it's empty if (textArea.querySelector('.empty-node')) { textArea.innerHTML = '
'; } // Try multiple methods to insert text try { // Try using execCommand document.execCommand('insertText', false, quote); } catch (e) { console.log("execCommand method failed:", e); // Fallback to creating a text node try { const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); const textNode = document.createTextNode(quote); range.insertNode(textNode); } else { // If no selection, add to the beginning const p = textArea.querySelector('p') || textArea; if (p.firstChild) { p.insertBefore(document.createTextNode(quote), p.firstChild); } else { p.appendChild(document.createTextNode(quote)); } } } catch (err) { console.error("Failed to insert text:", err); } } } // Function to create and show configuration popup function showConfigPopup(x, y) { // Remove any existing popup const existingPopup = document.getElementById('chatwoot-quote-config'); if (existingPopup) { existingPopup.remove(); } // Create popup container const popup = document.createElement('div'); popup.id = 'chatwoot-quote-config'; popup.style.position = 'fixed'; popup.style.zIndex = '9999'; popup.style.padding = '10px'; popup.style.backgroundColor = 'white'; popup.style.border = '1px solid #ccc'; popup.style.borderRadius = '4px'; popup.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.1)'; popup.style.width = '250px'; // Add title const title = document.createElement('h3'); title.textContent = t('quote_config'); title.style.margin = '0 0 10px 0'; title.style.fontSize = '14px'; title.style.fontWeight = 'bold'; popup.appendChild(title); // Add max messages configuration const maxMessagesLabel = document.createElement('label'); maxMessagesLabel.textContent = t('max_messages') + ' '; maxMessagesLabel.style.display = 'block'; maxMessagesLabel.style.marginBottom = '5px'; maxMessagesLabel.style.fontSize = '12px'; popup.appendChild(maxMessagesLabel); const maxMessagesInput = document.createElement('input'); maxMessagesInput.type = 'number'; maxMessagesInput.min = '1'; maxMessagesInput.max = '20'; maxMessagesInput.value = config.maxMessages; maxMessagesInput.style.width = '100%'; maxMessagesInput.style.marginBottom = '10px'; maxMessagesInput.style.padding = '5px'; maxMessagesInput.style.boxSizing = 'border-box'; popup.appendChild(maxMessagesInput); // Add language selector const languageLabel = document.createElement('label'); languageLabel.textContent = t('language') + ' '; languageLabel.style.display = 'block'; languageLabel.style.marginBottom = '5px'; languageLabel.style.fontSize = '12px'; popup.appendChild(languageLabel); const languageSelect = document.createElement('select'); languageSelect.style.width = '100%'; languageSelect.style.marginBottom = '10px'; languageSelect.style.padding = '5px'; languageSelect.style.boxSizing = 'border-box'; const languages = [ { value: 'en', label: t('english') }, { value: 'de', label: t('german') } ]; languages.forEach(lang => { const option = document.createElement('option'); option.value = lang.value; option.textContent = lang.label; if (config.language === lang.value) { option.selected = true; } languageSelect.appendChild(option); }); popup.appendChild(languageSelect); // Add save button const saveButton = document.createElement('button'); saveButton.textContent = t('save'); saveButton.style.padding = '5px 10px'; saveButton.style.backgroundColor = '#4f46e5'; saveButton.style.color = 'white'; saveButton.style.border = 'none'; saveButton.style.borderRadius = '4px'; saveButton.style.cursor = 'pointer'; saveButton.style.marginRight = '5px'; saveButton.addEventListener('click', function() { config.maxMessages = parseInt(maxMessagesInput.value, 10) || 5; config.language = languageSelect.value; saveConfig(); popup.remove(); }); popup.appendChild(saveButton); // Add cancel button const cancelButton = document.createElement('button'); cancelButton.textContent = t('cancel'); cancelButton.style.padding = '5px 10px'; cancelButton.style.backgroundColor = '#f3f4f6'; cancelButton.style.color = '#374151'; cancelButton.style.border = '1px solid #d1d5db'; cancelButton.style.borderRadius = '4px'; cancelButton.style.cursor = 'pointer'; cancelButton.addEventListener('click', function() { popup.remove(); }); popup.appendChild(cancelButton); // Calculate popup height to position it above the click // Need to temporarily add it to the DOM to get the height popup.style.visibility = 'hidden'; document.body.appendChild(popup); const popupHeight = popup.offsetHeight; document.body.removeChild(popup); popup.style.visibility = 'visible'; // Position the popup above the mouse click popup.style.left = `${x}px`; popup.style.top = `${Math.max(y - popupHeight - 10, 10)}px`; // Ensure it doesn't go off the top of the screen // Close popup when clicking outside document.addEventListener('click', function closePopup(e) { if (!popup.contains(e.target) && e.target.id !== 'chatwoot-quote-btn') { popup.remove(); document.removeEventListener('click', closePopup); } }); document.body.appendChild(popup); } // Function to create and add the quote button function addQuoteButton() { waitForElement('[data-v-b5303981] .right-wrap', (rightWrap) => { // Check if button already exists if (document.getElementById('chatwoot-quote-btn')) return; // Create the button const quoteButton = document.createElement('button'); quoteButton.id = 'chatwoot-quote-btn'; quoteButton.className = 'flex-shrink-0 inline-flex items-center min-w-0 gap-2 transition-all duration-200 ease-in-out border-0 rounded-lg outline-1 outline disabled:opacity-50 bg-n-slate-9/10 text-n-slate-12 hover:enabled:bg-n-slate-9/20 focus-visible:bg-n-slate-9/20 outline-transparent h-8 px-3 text-sm justify-center'; quoteButton.innerHTML = '|> Quote'; quoteButton.style.marginRight = '8px'; // Add click event quoteButton.addEventListener('click', function(e) { const data = extractMessages(); const quote = formatQuote(data); addQuoteToTextArea(quote); }); // Add right-click event for configuration quoteButton.addEventListener('contextmenu', function(e) { e.preventDefault(); showConfigPopup(e.clientX, e.clientY); return false; }); // Insert the button before the send button const sendButton = rightWrap.querySelector('button[type="submit"]'); if (sendButton) { rightWrap.insertBefore(quoteButton, sendButton); } }); } // Initialize the script function init() { // Add button on page load window.addEventListener('load', function() { setTimeout(addQuoteButton, 1000); }); // Try adding button immediately addQuoteButton(); // Also check periodically for dynamic page changes setInterval(addQuoteButton, 3000); // Add CSS for the button and popup const style = document.createElement('style'); style.textContent = ` #chatwoot-quote-btn:hover { background-color: rgba(0, 0, 0, 0.1) !important; } #chatwoot-quote-config { color: #1f2937; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } #chatwoot-quote-config input:focus, #chatwoot-quote-config select:focus { outline: 2px solid #4f46e5; outline-offset: 1px; } #chatwoot-quote-config button:hover { opacity: 0.9; } `; document.head.appendChild(style); } init(); })();