Files
chatwoot-quote-generator-us…/chatwoot_quote_generator-0.14.user.js
2025-06-24 17:15:35 +02:00

827 lines
31 KiB
JavaScript

// ==UserScript==
// @name Chatwoot Quote Generator
// @namespace http://tampermonkey.net/
// @version 0.14
// @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',
showExclusionIndicator: true,
autoOpenParticipants: true
};
// 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',
show_exclusion: 'Show "..." for excluded messages:',
excluded_messages: '... (older messages hidden)',
auto_open: 'Auto-open participants section:'
},
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',
show_exclusion: 'Zeige "..." für ausgeschlossene Nachrichten:',
excluded_messages: '... (ältere Nachrichten versteckt)',
auto_open: 'Teilnehmerbereich automatisch öffnen:'
}
};
// 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;
}
// Helper functions for auto-opening UI elements
function isSidebarOpen() {
// Check if sidebar is open by looking for the caret direction
const sidebarToggle = document.querySelector('button[data-v-2defdc7c=""] .i-ph-caret-right-fill');
return !!sidebarToggle; // If caret-right-fill exists, sidebar is open
}
function isParticipantsSectionExpanded() {
// Check if participants section is expanded by looking for the minus icon
const participantsSection = document.querySelector('[data-v-70238fc0=""] button');
if (!participantsSection) return false;
// Look for minus icon (expanded state)
const minusIcon = participantsSection.querySelector('svg path[d*="M3.997 13H20"]');
return !!minusIcon;
}
function hasParticipantsLoaded() {
// Check if participants have loaded (not showing "No one is participating")
const participantsText = document.querySelector('[data-v-d343fa58=""] .total-watchers');
if (!participantsText) return false;
const text = participantsText.textContent.trim();
return !text.includes('No one is participating') && text.includes('participating');
}
function clickSidebarToggle() {
const sidebarToggle = document.querySelector('button[data-v-2defdc7c=""]');
if (sidebarToggle) {
sidebarToggle.click();
return true;
}
return false;
}
function clickParticipantsToggle() {
const participantsToggle = document.querySelector('[data-v-70238fc0=""] button');
if (participantsToggle) {
participantsToggle.click();
return true;
}
return false;
}
// Function to wait with timeout
function waitFor(conditionFn, timeout = 1000) {
return new Promise((resolve) => {
const startTime = Date.now();
const checkCondition = () => {
if (conditionFn()) {
resolve(true);
} else if (Date.now() - startTime > timeout) {
resolve(false);
} else {
setTimeout(checkCondition, 50);
}
};
checkCondition();
});
}
// Function to attempt auto-opening sidebar and participants section
async function ensureParticipantsSectionAccessible() {
if (!config.autoOpenParticipants) {
return false;
}
let opened = false;
// Step 1: Open sidebar if closed
if (!isSidebarOpen()) {
console.log('Opening sidebar...');
if (clickSidebarToggle()) {
opened = true;
await waitFor(isSidebarOpen, 200);
}
}
// Step 2: Expand participants section if collapsed
if (!isParticipantsSectionExpanded()) {
console.log('Expanding participants section...');
if (clickParticipantsToggle()) {
opened = true;
await waitFor(isParticipantsSectionExpanded, 200);
}
}
// Step 3: Wait for participants to load
if (opened) {
console.log('Waiting for participants to load...');
await waitFor(hasParticipantsLoaded, 600);
}
return opened;
}
// Function to build lookup table from conversation participants
function buildAgentLookupTable() {
const lookupTable = {};
// Try to find the conversation participants section
const participantsSection = document.querySelector('[data-v-d343fa58=""][inbox-id]');
if (!participantsSection) {
return lookupTable;
}
// Look for participant entries in overlapping thumbnails section
const thumbnails = participantsSection.querySelectorAll('.overlapping-thumbnails .user-thumbnail-box');
thumbnails.forEach(thumbnail => {
const nameAttr = thumbnail.getAttribute('data-original-title');
const avatarContainer = thumbnail.querySelector('.avatar-container');
if (nameAttr && avatarContainer) {
const initials = avatarContainer.textContent.trim();
if (initials && nameAttr) {
lookupTable[initials] = nameAttr;
}
}
});
// Also look in the dropdown participant list if available
const dropdownItems = participantsSection.querySelectorAll('.dropdown-menu__item');
dropdownItems.forEach(item => {
const nameSpan = item.querySelector('span[title]');
const avatarContainer = item.querySelector('.avatar-container');
if (nameSpan && avatarContainer) {
const fullName = nameSpan.getAttribute('title') || nameSpan.textContent.trim();
const initials = avatarContainer.textContent.trim();
if (initials && fullName) {
lookupTable[initials] = fullName;
}
}
});
return lookupTable;
}
// Function to extract agent initials from avatar and get full name if available
function extractAgentInitials(messageElement, agentLookup = {}) {
// Look for avatar container within the message
const avatarContainer = messageElement.querySelector('.sender--info .avatar-container');
let initials = null;
if (avatarContainer) {
initials = avatarContainer.textContent.trim();
} else {
// Fallback: look for other possible avatar selectors
const altAvatarContainer = messageElement.querySelector('.user-thumbnail-box .avatar-container');
if (altAvatarContainer) {
initials = altAvatarContainer.textContent.trim();
}
}
if (initials && initials.length >= 1) {
// Try to get full name from lookup table
const fullName = agentLookup[initials];
if (fullName) {
return fullName;
}
return initials;
}
// If no initials found, return generic fallback
return "Agent";
}
// Function to extract message data
async function extractMessages() {
// Build agent lookup table from conversation participants
let agentLookup = buildAgentLookupTable();
// If we don't have enough data and auto-open is enabled, try to access participants section
if (Object.keys(agentLookup).length === 0 && config.autoOpenParticipants) {
const opened = await ensureParticipantsSectionAccessible();
if (opened) {
// Try building lookup table again after opening UI elements
agentLookup = buildAgentLookupTable();
}
}
// 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 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;
if (isCustomer) {
senderName = customerName;
} else {
// Extract agent name/initials from the avatar within this specific message
const agentName = extractAgentInitials(li, agentLookup);
senderName = `${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
});
}
});
// Keep track of whether messages were excluded
const messagesExcluded = messages.length > config.maxMessages;
// Limit the number of messages based on configuration
if (messagesExcluded) {
messages.splice(0, messages.length - config.maxMessages);
}
return { messages, messagesExcluded };
}
// Function to format messages as a quote
function formatQuote(data) {
let quote = "";
// Add indicator for excluded messages if enabled and messages were excluded
if (config.showExclusionIndicator && data.messagesExcluded) {
quote += `> ${t('excluded_messages')}\n>\n`;
}
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 = '12px';
popup.style.backgroundColor = '#1e293b'; // Dark slate background
popup.style.border = '1px solid #475569'; // Lighter border
popup.style.borderRadius = '8px';
popup.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.3)';
popup.style.width = '280px';
popup.style.color = '#f1f5f9'; // Light text
// Add title
const title = document.createElement('h3');
title.textContent = t('quote_config');
title.style.margin = '0 0 12px 0';
title.style.fontSize = '16px';
title.style.fontWeight = 'bold';
title.style.color = '#f8fafc'; // Very light text for title
popup.appendChild(title);
// Add max messages configuration
const maxMessagesLabel = document.createElement('label');
maxMessagesLabel.textContent = t('max_messages') + ' ';
maxMessagesLabel.style.display = 'block';
maxMessagesLabel.style.marginBottom = '6px';
maxMessagesLabel.style.fontSize = '13px';
maxMessagesLabel.style.color = '#cbd5e1'; // Light gray text
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 = '12px';
maxMessagesInput.style.padding = '6px 8px';
maxMessagesInput.style.boxSizing = 'border-box';
maxMessagesInput.style.backgroundColor = '#334155'; // Dark input background
maxMessagesInput.style.border = '1px solid #475569';
maxMessagesInput.style.borderRadius = '4px';
maxMessagesInput.style.color = '#f1f5f9'; // Light input text
popup.appendChild(maxMessagesInput);
// Add language selector
const languageLabel = document.createElement('label');
languageLabel.textContent = t('language') + ' ';
languageLabel.style.display = 'block';
languageLabel.style.marginBottom = '6px';
languageLabel.style.fontSize = '13px';
languageLabel.style.color = '#cbd5e1';
popup.appendChild(languageLabel);
const languageSelect = document.createElement('select');
languageSelect.style.width = '100%';
languageSelect.style.marginBottom = '12px';
languageSelect.style.padding = '6px 8px';
languageSelect.style.boxSizing = 'border-box';
languageSelect.style.backgroundColor = '#334155';
languageSelect.style.border = '1px solid #475569';
languageSelect.style.borderRadius = '4px';
languageSelect.style.color = '#f1f5f9';
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 show exclusion indicator checkbox
const showExclusionLabel = document.createElement('label');
showExclusionLabel.textContent = t('show_exclusion') + ' ';
showExclusionLabel.style.display = 'block';
showExclusionLabel.style.marginBottom = '6px';
showExclusionLabel.style.fontSize = '13px';
showExclusionLabel.style.color = '#cbd5e1';
popup.appendChild(showExclusionLabel);
const showExclusionCheckbox = document.createElement('input');
showExclusionCheckbox.type = 'checkbox';
showExclusionCheckbox.checked = config.showExclusionIndicator;
showExclusionCheckbox.style.marginBottom = '12px';
showExclusionCheckbox.style.marginRight = '6px';
showExclusionCheckbox.style.accentColor = '#3b82f6'; // Blue accent for checkbox
popup.appendChild(showExclusionCheckbox);
// Add a line break after the checkbox
popup.appendChild(document.createElement('br'));
// Add auto-open participants checkbox
const autoOpenLabel = document.createElement('label');
autoOpenLabel.textContent = t('auto_open') + ' ';
autoOpenLabel.style.display = 'block';
autoOpenLabel.style.marginBottom = '6px';
autoOpenLabel.style.fontSize = '13px';
autoOpenLabel.style.color = '#cbd5e1';
popup.appendChild(autoOpenLabel);
const autoOpenCheckbox = document.createElement('input');
autoOpenCheckbox.type = 'checkbox';
autoOpenCheckbox.checked = config.autoOpenParticipants;
autoOpenCheckbox.style.marginBottom = '12px';
autoOpenCheckbox.style.marginRight = '6px';
autoOpenCheckbox.style.accentColor = '#3b82f6';
popup.appendChild(autoOpenCheckbox);
// Add a line break after the checkbox
popup.appendChild(document.createElement('br'));
// Add save button
const saveButton = document.createElement('button');
saveButton.textContent = t('save');
saveButton.style.padding = '8px 16px';
saveButton.style.backgroundColor = '#3b82f6'; // Blue background
saveButton.style.color = 'white';
saveButton.style.border = 'none';
saveButton.style.borderRadius = '6px';
saveButton.style.cursor = 'pointer';
saveButton.style.marginRight = '8px';
saveButton.style.fontSize = '13px';
saveButton.style.fontWeight = '500';
saveButton.addEventListener('click', function() {
config.maxMessages = parseInt(maxMessagesInput.value, 10) || 5;
config.language = languageSelect.value;
config.showExclusionIndicator = showExclusionCheckbox.checked;
config.autoOpenParticipants = autoOpenCheckbox.checked;
saveConfig();
popup.remove();
});
popup.appendChild(saveButton);
// Add cancel button
const cancelButton = document.createElement('button');
cancelButton.textContent = t('cancel');
cancelButton.style.padding = '8px 16px';
cancelButton.style.backgroundColor = '#475569'; // Dark gray background
cancelButton.style.color = '#f1f5f9'; // Light text
cancelButton.style.border = '1px solid #64748b';
cancelButton.style.borderRadius = '6px';
cancelButton.style.cursor = 'pointer';
cancelButton.style.fontSize = '13px';
cancelButton.style.fontWeight = '500';
cancelButton.addEventListener('click', function() {
popup.remove();
});
popup.appendChild(cancelButton);
// Calculate popup dimensions and position it with bottom-right at click position
// Need to temporarily add it to the DOM to get the dimensions
popup.style.visibility = 'hidden';
document.body.appendChild(popup);
const popupWidth = popup.offsetWidth;
const popupHeight = popup.offsetHeight;
document.body.removeChild(popup);
popup.style.visibility = 'visible';
// Position popup so bottom-right corner is at click position
const leftPos = Math.max(x - popupWidth, 10); // Ensure it doesn't go off the left edge
const topPos = Math.max(y - popupHeight, 10); // Ensure it doesn't go off the top edge
popup.style.left = `${leftPos}px`;
popup.style.top = `${topPos}px`;
// 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', async function(e) {
const data = await 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 {
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 #3b82f6;
outline-offset: 1px;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
#chatwoot-quote-config button:hover {
opacity: 0.9;
transform: translateY(-1px);
transition: all 0.2s ease;
}
#chatwoot-quote-config button:active {
transform: translateY(0);
}
#chatwoot-quote-config input[type="number"]::-webkit-outer-spin-button,
#chatwoot-quote-config input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
#chatwoot-quote-config input[type="number"] {
-moz-appearance: textfield;
}
`;
document.head.appendChild(style);
}
init();
})();