From 998938cd3d435ebc0dcb5b184dedee851288416d Mon Sep 17 00:00:00 2001 From: Yandrik Date: Tue, 24 Jun 2025 15:15:21 +0200 Subject: [PATCH] INITIAL COMMIT --- chatwoot_quote_generator-0.8.user.js | 526 +++++++++++++++++++++++++++ 1 file changed, 526 insertions(+) create mode 100644 chatwoot_quote_generator-0.8.user.js diff --git a/chatwoot_quote_generator-0.8.user.js b/chatwoot_quote_generator-0.8.user.js new file mode 100644 index 0000000..6aac7b4 --- /dev/null +++ b/chatwoot_quote_generator-0.8.user.js @@ -0,0 +1,526 @@ +// ==UserScript== +// @name Chatwoot Quote Generator +// @namespace http://tampermonkey.net/ +// @version 0.8 +// @description Generate a formatted quote from Chatwoot conversation +// @author You +// @match https://chatwoot.abawo.de/app/* +// @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); + } + + // 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(); + } + }); + + // Now extract text from the cleaned node + // Check for paragraphs + if (clonedNode.querySelectorAll('p').length > 0) { + const paragraphs = clonedNode.querySelectorAll('p'); + return Array.from(paragraphs) + .map(p => p.textContent.trim()) + .filter(t => t) + .join('\n'); + } else { + // Fall back to textContent if no paragraphs + return clonedNode.textContent.trim(); + } + } + + // 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}`; + } + + 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(); +})(); \ No newline at end of file