feat: v14 with better popup
This commit is contained in:
826
chatwoot_quote_generator-0.14.user.js
Normal file
826
chatwoot_quote_generator-0.14.user.js
Normal file
@ -0,0 +1,826 @@
|
|||||||
|
// ==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();
|
||||||
|
})();
|
Reference in New Issue
Block a user