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