feat: v09 with newline after separator and better quote parsing
This commit is contained in:
550
chatwoot_quote_generator-0.9.user.js
Normal file
550
chatwoot_quote_generator-0.9.user.js
Normal file
@ -0,0 +1,550 @@
|
|||||||
|
// ==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 = '<p></p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
})();
|
Reference in New Issue
Block a user