From 877e99110c3b8d0fe3e3e109e2254a169a6d760b Mon Sep 17 00:00:00 2001 From: nikhil7174 Date: Thu, 10 Oct 2024 15:52:10 +0530 Subject: [PATCH] refactored summarizer and rephraser --- .../content/content_scripts/aiSummarizer.js | 416 ++++----- .../content/content_scripts/rephraser.js | 794 ++++++++++-------- .../extension/content/ui/style.css | 4 +- .../ping_ai_copilot/extension/manifest.json | 4 +- .../extension/service_worker/background.js | 132 +-- 5 files changed, 727 insertions(+), 623 deletions(-) diff --git a/components/ping_ai_copilot/extension/content/content_scripts/aiSummarizer.js b/components/ping_ai_copilot/extension/content/content_scripts/aiSummarizer.js index 306cbc4896a9..cf880fb1b49c 100644 --- a/components/ping_ai_copilot/extension/content/content_scripts/aiSummarizer.js +++ b/components/ping_ai_copilot/extension/content/content_scripts/aiSummarizer.js @@ -1,209 +1,221 @@ -let summarizer = null; -let summaryBox = null; -let isSummarizerVisible = false; -let isTextSelected = false; - -const debounce = (func, delay) => { - let timeoutId; - return (...args) => { - clearTimeout(timeoutId); - timeoutId = setTimeout(() => func(...args), delay); +const TextSummarizer = (() => { + + let state = { + summarizer: null, + summaryBox: null, + isSummarizerVisible: false, + isTextSelected: false, + currentSelectedText: '', }; -}; - -const initializeExtension = () => { - document.removeEventListener('mouseup', handleTextSelection); - document.addEventListener('mouseup', debounce(handleTextSelection, 190)); - document.addEventListener('click', handleDocumentClick); -} - -const handleTextSelection = (event) => { - const selection = window.getSelection(); - const selectedText = selection.toString().trim(); - const wordCount = selectedText.split(/\s+/).length; - if (wordCount >= 10 && wordCount <= 1000 && !(summaryBox && summaryBox.contains(selection.anchorNode))) { - if (!isSummarizerVisible) { - showSummarizeIcon(selectedText, event); - isTextSelected = true; + + const COPIED_MESSAGE_TIMEOUT = 2000; + + const debounce = (func, delay) => { + let timeoutId; + return (...args) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => func(...args), delay); + }; + }; + + const copyToClipboard = (text) => { + navigator.clipboard.writeText(text).then(() => { + showCopiedMessage(); + }).catch(err => { + console.error('Failed to copy:', err); + }); + }; + + const showCopiedMessage = () => { + const copyButton = document.getElementById('copy-button'); + if (!copyButton) return; + + copyButton.style.display = 'none'; + + const copiedMessage = document.createElement('div'); + copiedMessage.id = 'copiedMessage'; + copiedMessage.textContent = 'Copied!'; + + copyButton.parentNode.insertBefore(copiedMessage, copyButton); + + setTimeout(() => { + copiedMessage.remove(); + copyButton.style.display = 'block'; + }, COPIED_MESSAGE_TIMEOUT); + }; + + const handleTextSelection = (event) => { + const selection = window.getSelection(); + const selectedText = selection.toString().trim(); + const wordCount = selectedText.split(/\s+/).length; + + state.currentSelectedText = selectedText; + + if (wordCount >= 10 && wordCount <= 1000 && !(state.summaryBox && state.summaryBox.contains(selection.anchorNode))) { + if (!state.isSummarizerVisible) { + showSummarizeIcon(event); + state.isTextSelected = true; + } + } else { + hideSummarizeIcon(); + state.isTextSelected = false; } - } else { - hideSummarizeIcon(); - isTextSelected = false; - } -} - -const handleDocumentClick = (event) => { - if(summaryBox && !summaryBox.contains(event.target)){ - hideSummaryBox(); - } - if(isTextSelected && summarizer){ - if(summaryBox && !summaryBox.contains(event.target)){ - hideSummarizeIcon() + }; + + const handleDocumentClick = (event) => { + if (state.summaryBox && !state.summaryBox.contains(event.target)) { hideSummaryBox(); } - else if(!summaryBox){ - hideSummarizeIcon() - hideSummaryBox(); - } - } -} - -const showSummarizeIcon = (selectedText, event) => { - if (!summarizer) { - summarizer = document.createElement('div'); - summarizer.id = 'summarizer-icon'; - - const iconImage = document.createElement('img'); - iconImage.src = chrome.runtime.getURL('extension/assets/aiSummarizerIcon.svg'); - iconImage.alt = 'Summarize Icon'; - iconImage.id = 'iconImage'; - - summarizer.appendChild(iconImage); - - summarizer.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - sendTextToSummarize(selectedText); - }); - document.body.appendChild(summarizer); - } - - const selection = window.getSelection(); - const range = selection.getRangeAt(0); - const endRange = document.createRange(); - endRange.setStart(range.endContainer, range.endOffset); - endRange.setEnd(range.endContainer, range.endOffset); - - let rect; - if (range.endContainer.nodeType === Node.TEXT_NODE) { - const text = range.endContainer.textContent; - let wordStart = range.endOffset; - - while (wordStart > 0 && /\S/.test(text[wordStart - 1])) { - wordStart--; + if (state.isTextSelected && state.summarizer) { + if (state.summaryBox && !state.summaryBox.contains(event.target)) { + hideSummarizeIcon(); + hideSummaryBox(); + } else if (!state.summaryBox) { + hideSummarizeIcon(); + hideSummaryBox(); + } + } + }; + + // UI Management + const showSummarizeIcon = (event) => { + if (!state.summarizer) { + state.summarizer = document.createElement('div'); + state.summarizer.id = 'summarizer-icon'; + + const iconImage = document.createElement('img'); + iconImage.src = chrome.runtime.getURL('extension/assets/aiSummarizerIcon.svg'); + iconImage.alt = 'Summarize Icon'; + iconImage.id = 'iconImage'; + + state.summarizer.appendChild(iconImage); + + state.summarizer.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + sendTextToSummarize(state.currentSelectedText); + }); + document.body.appendChild(state.summarizer); + } + + const selection = window.getSelection(); + const range = selection.getRangeAt(0); + const endRange = document.createRange(); + endRange.setStart(range.endContainer, range.endOffset); + endRange.setEnd(range.endContainer, range.endOffset); + + let rect; + if (range.endContainer.nodeType === Node.TEXT_NODE) { + const text = range.endContainer.textContent; + let wordStart = range.endOffset; + + while (wordStart > 0 && /\S/.test(text[wordStart - 1])) { + wordStart--; + } + endRange.setStart(range.endContainer, wordStart); + rect = endRange.getBoundingClientRect(); + } else { + rect = range.getBoundingClientRect(); } - endRange.setStart(range.endContainer, wordStart); - rect = endRange.getBoundingClientRect(); - } else { - rect = range.getBoundingClientRect(); - } - - const scrollX = window.scrollX || document.documentElement.scrollLeft; - const scrollY = window.scrollY || document.documentElement.scrollTop; - - let top; let left; - if(event.detail === 3){ - top = rect.bottom + scrollY + 1; - left = rect.right + scrollX + 1 - } - top = rect.bottom + scrollY + 5; - left = rect.right + scrollX + 5; - - summarizer.style.top = `${top}px`; - summarizer.style.left = `${left}px`; - summarizer.style.display = 'inline-block'; - isSummarizerVisible = true; -}; - -const hideSummarizeIcon = () => { - if (summarizer) { - summarizer.style.display = 'none'; - isSummarizerVisible = false; - } -} - -const sendTextToSummarize = (text) => { - showSummaryBox(true); - try { - chrome.runtime.sendMessage({ action: 'summarize', text: text }); - } catch (error) { - console.error('Failed to send message:', error); - showSummaryBox(false, 'An error occurred. Please refresh the page and try again.'); - } -} - -const showSummaryBox = (isLoading, summary = '', headerText = '') => { - if (!summaryBox) { - summaryBox = document.createElement('div'); - summaryBox.id = 'summary-box'; - document.body.appendChild(summaryBox); - } - summaryBox.innerHTML = ` -
- ${isLoading ? '' : `

${headerText}

`} - ${isLoading ? '' : ``} -
- ${isLoading ? ` -
-
-
-
+ + const scrollX = window.scrollX || document.documentElement.scrollLeft; + const scrollY = window.scrollY || document.documentElement.scrollTop; + + let top, left; + if (event.detail === 3) { + top = rect.bottom + scrollY + 1; + left = rect.right + scrollX + 1; + } else { + top = rect.bottom + scrollY + 5; + left = rect.right + scrollX + 5; + } + + state.summarizer.style.top = `${top}px`; + state.summarizer.style.left = `${left}px`; + state.summarizer.style.display = 'inline-block'; + state.isSummarizerVisible = true; + }; + + const hideSummarizeIcon = () => { + if (state.summarizer) { + state.summarizer.style.display = 'none'; + state.isSummarizerVisible = false; + } + }; + + const showSummaryBox = (isLoading, summary = '', headerText = '') => { + if (!state.summaryBox) { + state.summaryBox = document.createElement('div'); + state.summaryBox.id = 'summary-box'; + document.body.appendChild(state.summaryBox); + } + + state.summaryBox.innerHTML = ` +
+ ${isLoading ? '' : `

${headerText}

`} + ${isLoading ? '' : ``}
- ` : ` - - `} - `; - - if (!isLoading) { - document.getElementById('copy-button').addEventListener('click', (e) => { - e.stopPropagation(); - copyToClipboard(summary); - }); - } - - summaryBox.style.animation = 'slideUp 0.3s ease-out'; - summaryBox.style.display = 'block'; - setTimeout(() => { - const height = summaryBox.scrollHeight; - summaryBox.style.maxHeight = `${height}px`; - summaryBox.style.opacity = '1'; - }, 10); -} - -const hideSummaryBox = () => { - if (summaryBox) { - summaryBox.style.animation = 'slideDown 0.3s ease-out'; + ${isLoading ? ` +
+
+
+
+
+ ` : ` + + `} + `; + + if (!isLoading) { + document.getElementById('copy-button').addEventListener('click', (e) => { + e.stopPropagation(); + copyToClipboard(summary); + }); + } + + state.summaryBox.style.animation = 'slideUp 0.3s ease-out'; + state.summaryBox.style.display = 'block'; setTimeout(() => { - summaryBox.style.display = 'none'; - }, 300); - } -} - -const copyToClipboard = (text) => { - navigator.clipboard.writeText(text).then(() => { - showCopiedMessage(); - }).catch(err => { - console.error('Failed to copy:', err); - }); -} - -const showCopiedMessage = () => { - const copyButton = document.getElementById('copy-button'); - if (!copyButton) return; - - copyButton.style.display = 'none'; - - const copiedMessage = document.createElement('div'); - copiedMessage.id = 'copiedMessage'; - copiedMessage.textContent = 'Copied!'; - - copyButton.parentNode.insertBefore(copiedMessage, copyButton); - - setTimeout(() => { - copiedMessage.remove(); - copyButton.style.display = 'block'; - }, 2000); -} - -// Listen for messages from the background script -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.action === 'displaySummary') { - showSummaryBox(false, request.summary, request.headerText); - } -}); - -initializeExtension(); + const height = state.summaryBox.scrollHeight; + state.summaryBox.style.maxHeight = `${height}px`; + state.summaryBox.style.opacity = '1'; + }, 10); + }; + + const hideSummaryBox = () => { + if (state.summaryBox) { + state.summaryBox.style.animation = 'slideDown 0.3s ease-out'; + setTimeout(() => { + state.summaryBox.style.display = 'none'; + }, 300); + } + }; + + const sendTextToSummarize = async(text) => { + showSummaryBox(true); + try { + const response = await chrome.runtime.sendMessage({ action: 'summarize', text: text }); + if (response.success) { + showSummaryBox(false, response.summary, response.headerText); + } else + showSummaryBox(false, 'An error occurred. Please refresh the page and try again.'); + } catch (error) { + console.error('Failed to send message:', error); + showSummaryBox(false, 'An error occurred. Please refresh the page and try again.'); + } + }; + + const initialize = () => { + document.removeEventListener('mouseup', handleTextSelection); + document.addEventListener('mouseup', debounce(handleTextSelection, 190)); + document.addEventListener('click', handleDocumentClick); + }; + + return { + initialize, + }; +})(); + +TextSummarizer.initialize(); \ No newline at end of file diff --git a/components/ping_ai_copilot/extension/content/content_scripts/rephraser.js b/components/ping_ai_copilot/extension/content/content_scripts/rephraser.js index d95d8e2d61eb..3bcd8e94a01f 100644 --- a/components/ping_ai_copilot/extension/content/content_scripts/rephraser.js +++ b/components/ping_ai_copilot/extension/content/content_scripts/rephraser.js @@ -1,418 +1,480 @@ -let prevActiveElement = null; -let originalText = ''; -let rephrasedText = ''; -let rephraseButton = null; -let isFetching = false; -let abortController = null; -let pillContainer = null; -let showRephraseButton = true; -let isCanceled = false; -let hasRephrasedBefore = false; - -const isDarkBackground = (color) => { - const rgb = color.match(/\d+/g); - if (rgb) { - const [r, g, b] = rgb.map(Number); - return (r * 0.299 + g * 0.587 + b * 0.114) < 128; - } - return false; -}; - -const getIconColor = (textBox) => { - const bgColor = window.getComputedStyle(textBox).backgroundColor; - return isDarkBackground(bgColor) ? 'dark' : 'light'; -}; - -const applyGradientAnimation = (textBox) => { - const gradient = 'linear-gradient(270deg, #F100C1, #00CED1, #F100C1)'; - const animation = 'gradientAnimation 5s ease infinite'; - - textBox.style.backgroundImage = gradient; - textBox.style.backgroundSize = '200% 200%'; - textBox.style.animation = animation; - textBox.style.WebkitBackgroundClip = 'text'; - textBox.style.WebkitTextFillColor = 'transparent'; -} - -const removeGradientColor = (textBox) => { - textBox.style.backgroundImage = 'none'; - textBox.style.animation = 'none'; - textBox.style.WebkitBackgroundClip = 'initial'; - textBox.style.WebkitTextFillColor = 'initial'; -} - -const createPillContainer = (textBox, img) => { - if (pillContainer) { - pillContainer.remove(); - } - - const iconColor = getIconColor(textBox); - const pillBgColor = iconColor === 'light' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; - const textColor = iconColor === 'light' ? 'black' : 'white'; - - pillContainer = document.createElement('div'); - pillContainer.classList.add('pill-container'); - pillContainer.style.backgroundColor = pillBgColor; - if (iconColor === 'light') { - pillContainer.classList.remove('pill-dark-shadow') - pillContainer.classList.add('pill-light-shadow') - } - else { - pillContainer.classList.remove('pill-light-shadow'); - pillContainer.classList.add('pill-dark-shadow'); - } - - const leftImg = document.createElement('div'); - leftImg.classList.add('pill-img-container'); - const leftImgIcon = document.createElement('img'); - leftImgIcon.src = chrome.runtime.getURL(`extension/assets/back-${iconColor}.svg`); - leftImgIcon.alt = 'Back'; - leftImgIcon.classList.add('icon'); - leftImg.appendChild(leftImgIcon); - - const leftTooltip = document.createElement('span'); - leftTooltip.textContent = 'Back'; - leftTooltip.classList.add('tooltip', 'left-tooltip', `tooltip-hover-${iconColor}`); - leftImg.appendChild(leftTooltip); - pillContainer.appendChild(leftImg); - - const separator = document.createElement('span'); - separator.textContent = '|'; - separator.style.color = iconColor === 'light' ? 'black' : 'white'; - separator.classList.add('separator'); - pillContainer.appendChild(separator); - - const rightImg = document.createElement('div'); - rightImg.classList.add('pill-img-container'); - const rightImgIcon = document.createElement('img'); - rightImgIcon.src = chrome.runtime.getURL(`extension/assets/rewrite-${iconColor}.svg`); - rightImgIcon.alt = 'Retry'; - rightImgIcon.classList.add('icon'); - rightImg.appendChild(rightImgIcon); - - const rightTooltip = document.createElement('span'); - rightTooltip.textContent = 'Retry'; - rightTooltip.classList.add('tooltip', 'right-tooltip', `tooltip-hover-${iconColor}`); - rightImg.appendChild(rightTooltip); - pillContainer.appendChild(rightImg); - - document.body.appendChild(pillContainer); - - // Adjust position based on scroll offsets - const rect = textBox.getBoundingClientRect(); - pillContainer.style.left = `${rect.right - pillContainer.offsetWidth - 5 + window.scrollX}px`; - pillContainer.style.top = `${rect.bottom - pillContainer.offsetHeight + window.scrollY}px`; - - leftImgIcon.addEventListener('click', () => { - if (prevActiveElement) { - undoRephraseText(textBox); - } - }); +const TextRephraser = (() => { + + const state = { + prevActiveElement: null, + originalText: '', + rephrasedText: '', + rephraseButton: null, + isFetching: false, + abortController: null, + pillContainer: null, + showRephraseButton: true, + isCanceled: false, + hasRephrasedBefore: false + }; - rightImgIcon.addEventListener('click', () => { - if (prevActiveElement) { - rephraseText(textBox); + // Constants + const MIN_WORDS = 10; + const MIN_WIDTH = 600; + const MIN_HEIGHT = 50; + const PILL_TIMEOUT = 1500; + + const isDarkBackground = (color) => { + const rgb = color.match(/\d+/g); + if (rgb) { + const [r, g, b] = rgb.map(Number); + return (r * 0.299 + g * 0.587 + b * 0.114) < 128; } - }); - - pillContainer.addEventListener('mouseleave', (event) => { - // Check if the mouse is moving back to the rephrase button - const buttonRect = rephraseButton ? rephraseButton.getBoundingClientRect() : null; - if (!buttonRect || - event.clientX < buttonRect.left || - event.clientX > buttonRect.right || - event.clientY < buttonRect.top || - event.clientY > buttonRect.bottom) { - pillContainer.remove(); - pillContainer = null; - if (isFetching && rephraseButton) { - img.src = chrome.runtime.getURL(`extension/assets/cross-light.svg`); - rephraseButton.style.display = 'flex'; - rephraseButton.style.justifyContent = 'center'; - rephraseButton.style.alignItems = 'center'; - rephraseButton.onClick = () => { - undoRephraseText(textBox, img); - } - } - else if (rephraseButton) { - rephraseButton.style.display = 'flex'; - rephraseButton.style.justifyContent = 'center'; - rephraseButton.style.alignItems = 'center'; + return false; + }; + + const getIconColor = (textBox) => { + const bgColor = window.getComputedStyle(textBox).backgroundColor; + return isDarkBackground(bgColor) ? 'dark' : 'light'; + }; + + const getAssetUrl = (name, color, hover = false) => { + const hoverSuffix = hover ? '-hover' : ''; + return chrome.runtime.getURL(`extension/assets/${name}-${color}${hoverSuffix}.svg`); + }; + + // Text styling functions + const applyGradientAnimation = (textBox) => { + const gradient = 'linear-gradient(270deg, #F100C1, #00CED1, #F100C1)'; + Object.assign(textBox.style, { + backgroundImage: gradient, + backgroundSize: '200% 200%', + animation: 'gradientAnimation 5s ease infinite', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent' + }); + }; + + const removeGradientColor = (textBox) => { + Object.assign(textBox.style, { + backgroundImage: 'none', + animation: 'none', + WebkitBackgroundClip: 'initial', + WebkitTextFillColor: 'initial' + }); + }; + + // Text manipulation functions + const typeText = async (textBox, text, delay = 16) => { + let index = 0; + while (index < text.length && !state.isCanceled) { + const char = text[index] === ' ' ? '\u00A0' : text[index]; + if (textBox.tagName === 'INPUT' || textBox.tagName === 'TEXTAREA') { + textBox.value += char; + } else if (textBox.isContentEditable) { + textBox.innerText += char; } + index++; + await new Promise(resolve => setTimeout(resolve, delay)); } - }); -}; + }; -const typeText = async (textBox, text, delay = 16) => { - let index = 0; - while (index < text.length) { - if (isCanceled) return; - if (textBox.tagName === 'INPUT' || textBox.tagName === 'TEXTAREA') { - text[index] == ' ' ? textBox.value += '\u00A0' : textBox.value += text[index]; - } else if (textBox.isContentEditable) { - text[index] == ' ' ? textBox.innerText += '\u00A0' : textBox.innerText += text[index]; + const shouldShowRephraseButton = (text) => { + const words = text.trim().split(/\s+/); + return words.length >= MIN_WORDS; + }; + + // UI Component Creation + const createPillContainer = (textBox, img) => { + if (state.pillContainer) { + state.pillContainer.remove(); } - index++; - await new Promise(resolve => setTimeout(resolve, delay)); - } -} - -const rephraseText = async (textBox, img) => { - originalText = textBox.value || textBox.innerText; - applyGradientAnimation(textBox); - if (img) { + const iconColor = getIconColor(textBox); - img.src = chrome.runtime.getURL(`extension/assets/cross-light.svg`); - img.alt = "stop" - img.style.width = '20px'; - img.style.height = '20px'; - const buttonContainer = img.parentElement; + const pillBgColor = iconColor === 'light' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; - if (iconColor === 'light') { - buttonContainer.classList.remove('dark-shadow') - buttonContainer.classList.add('light-shadow') - } - else { - buttonContainer.classList.remove('light-shadow'); - buttonContainer.classList.add('dark-shadow'); - } - } - isFetching = true; - isCanceled = false; + const container = document.createElement('div'); + container.classList.add('pill-container'); + container.style.backgroundColor = pillBgColor; + container.classList.add(`pill-${iconColor}-shadow`); - try { - const response = await chrome.runtime.sendMessage({ action: 'rephrase', text: originalText }); + const leftButton = createPillButton('back', iconColor, 'Back', () => { + if (state.prevActiveElement) { + undoRephraseText(textBox); + } + }); - if (isCanceled) { - return; + const separator = document.createElement('span'); + separator.textContent = '|'; + separator.style.color = iconColor === 'light' ? 'black' : 'white'; + separator.classList.add('separator'); + + const rightButton = createPillButton('rewrite', iconColor, 'Retry', () => { + if (state.prevActiveElement) { + rephraseText(textBox); + } + }); + + container.append(leftButton, separator, rightButton); + document.body.appendChild(container); + + const rect = textBox.getBoundingClientRect(); + Object.assign(container.style, { + left: `${rect.right - container.offsetWidth - 5 + window.scrollX}px`, + top: `${rect.bottom - container.offsetHeight + window.scrollY}px` + }); + + state.pillContainer = container; + setupPillContainerEvents(container, textBox, img); + }; + + const createPillButton = (iconName, iconColor, tooltipText, onClick) => { + const button = document.createElement('div'); + button.classList.add('pill-img-container'); + + const icon = document.createElement('img'); + icon.src = getAssetUrl(iconName, iconColor); + icon.alt = tooltipText; + icon.classList.add('icon'); + + const tooltip = document.createElement('span'); + tooltip.textContent = tooltipText; + tooltip.classList.add('tooltip', `${tooltipText.toLowerCase()}-tooltip`, `tooltip-hover-${iconColor}`); + + button.append(icon, tooltip); + button.addEventListener('click', onClick); + + return button; + }; + + const setupPillContainerEvents = (container, textBox, img) => { + container.addEventListener('mouseleave', (event) => { + const buttonRect = state.rephraseButton?.getBoundingClientRect(); + if (!buttonRect || + event.clientX < buttonRect.left || + event.clientX > buttonRect.right || + event.clientY < buttonRect.top || + event.clientY > buttonRect.bottom) { + container.remove(); + state.pillContainer = null; + + if (state.rephraseButton) { + state.rephraseButton.style.display = 'flex'; + if (state.isFetching) { + img.src = getAssetUrl('cross', 'light'); + state.rephraseButton.onClick = () => undoRephraseText(textBox, img); + } + } + } + }); + }; + + const rephraseText = async (textBox, img) => { + state.originalText = textBox.value || textBox.innerText; + state.isFetching = true; + state.isCanceled = false; + applyGradientAnimation(textBox); + + if (img) { + updateButtonForFetching(textBox, img); } - // Clear the textBox before typing the new text - if (prevActiveElement.tagName === 'INPUT' || prevActiveElement.tagName === 'TEXTAREA') { - prevActiveElement.value = ''; - } else if (prevActiveElement.isContentEditable) { - prevActiveElement.innerText = ''; + try { + const response = await chrome.runtime.sendMessage({ + action: 'rephrase', + text: state.originalText + }); + console.log('Rephrase response:', response); + + if (!state.isCanceled) { + await handleSuccessfulRephrase(textBox, response.rephrase, img); + } + } catch (error) { + console.error('Rephrase failed:', error); + handleRephraseFailed(textBox, img); + } finally { + focusTextBox(textBox); } + }; + const handleSuccessfulRephrase = async (textBox, rephrasedText, img) => { + clearTextBox(state.prevActiveElement); removeGradientColor(textBox); - // Type the rephrased text with a smooth effect - await typeText(prevActiveElement, response.rephrase); + await typeText(state.prevActiveElement, rephrasedText); - isFetching = false; - removeGradientColor(prevActiveElement); + state.isFetching = false; + removeGradientColor(state.prevActiveElement); - if (rephraseButton) { - rephraseButton.remove(); - rephraseButton = null; + if (state.rephraseButton) { + state.rephraseButton.remove(); + state.rephraseButton = null; } - showRephraseButton = false; - hasRephrasedBefore = true; - // Show pill for 3 seconds - createPillContainer(prevActiveElement, img); + state.showRephraseButton = false; + state.hasRephrasedBefore = true; + + createPillContainer(state.prevActiveElement, img); setTimeout(() => { - if (pillContainer) { - pillContainer.remove(); - pillContainer = null; + if (state.pillContainer) { + state.pillContainer.remove(); + state.pillContainer = null; } - showRephraseButton = true; - addButtonToTextBox(prevActiveElement); - }, 2000); - } catch (error) { - console.error('Failed to send message:', error); - isFetching = false; + state.showRephraseButton = true; + addButtonToTextBox(state.prevActiveElement); + }, PILL_TIMEOUT); + }; + + const handleRephraseFailed = (textBox, img) => { + state.isFetching = false; if (img) { - const iconColor = getIconColor(textBox); - img.src = chrome.runtime.getURL(`extension/assets/rephrase-${iconColor}.svg`); + img.src = getAssetUrl('rephrase', getIconColor(textBox)); } removeGradientColor(textBox); - } finally { - textBox.focus(); - if (textBox.tagName === 'INPUT' || textBox.tagName === 'TEXTAREA') { - const length = textBox.value.length; - textBox.setSelectionRange(length, length); - } else if (textBox.isContentEditable) { - const range = document.createRange(); - const sel = window.getSelection(); - range.selectNodeContents(textBox); - range.collapse(false); - sel.removeAllRanges(); - sel.addRange(range); - } - } -} - -const undoRephraseText = (textBox, img) => { - isFetching = false; - isCanceled = true; - removeGradientColor(textBox); - if (img) { + }; + + const undoRephraseText = (textBox, img) => { + state.isFetching = false; + state.isCanceled = true; + removeGradientColor(textBox); + const iconColor = getIconColor(textBox); - img.src = chrome.runtime.getURL(`extension/assets/rephrase-${iconColor}.svg`); - } - if (prevActiveElement) { - if (prevActiveElement.tagName === 'INPUT' || prevActiveElement.tagName === 'TEXTAREA') { - prevActiveElement.value = originalText; - } else if (prevActiveElement.isContentEditable) { - prevActiveElement.innerText = originalText; + img.src = getAssetUrl('rephrase', iconColor); + img.alt = 'Rephrase'; + img.dataset.currentIcon = 'rephrase'; + Object.assign(img.style, { + width: '17px', + height: '17px' + }); + + if (state.prevActiveElement) { + if (state.prevActiveElement.tagName === 'INPUT' || state.prevActiveElement.tagName === 'TEXTAREA') { + state.prevActiveElement.value = state.originalText; + } else if (state.prevActiveElement.isContentEditable) { + state.prevActiveElement.innerText = state.originalText; + } } - } -} - -const shouldShowRephraseButton = (text) => { - const words = text.trim().split(/\s+/); - return words.length >= 10; -}; - -const addButtonToTextBox = (textBox) => { - - // Check if the input box is too small - const rect = textBox.getBoundingClientRect(); - const minWidth = 600; - const minHeight = 50; - - if (rect.width < minWidth && rect.height < minHeight) { - return; - } - - const text = textBox.value || textBox.innerText; - if (!shouldShowRephraseButton(text)) { - if (rephraseButton) { - rephraseButton.remove(); - rephraseButton = null; + }; + + const addButtonToTextBox = (textBox) => { + const rect = textBox.getBoundingClientRect(); + if (rect.width < MIN_WIDTH && rect.height < MIN_HEIGHT) { + return; } - return; - } - if (rephraseButton) { - rephraseButton.remove(); - } - - const buttonContainer = document.createElement('div'); - buttonContainer.classList.add('rephrase-button-container'); - const iconColor = getIconColor(textBox); - if (iconColor === 'light') { - buttonContainer.classList.remove('dark-shadow') - buttonContainer.classList.add('light-shadow') - } - else { - buttonContainer.classList.remove('light-shadow'); - buttonContainer.classList.add('dark-shadow'); - } - const img = document.createElement('img'); - img.src = chrome.runtime.getURL(`extension/assets/rephrase-${iconColor}.svg`); - img.alt = 'Rephrase'; - img.style.width = '17px'; - img.style.height = '17px'; - img.style.objectFit = 'contain'; - - buttonContainer.appendChild(img); - - const showPill = () => { - if (hasRephrasedBefore) { - buttonContainer.style.display = 'none'; - createPillContainer(textBox, img); + + const text = textBox.value || textBox.innerText; + if (!shouldShowRephraseButton(text)) { + if (state.rephraseButton) { + state.rephraseButton.remove(); + state.rephraseButton = null; + } + return; } + + createRephraseButton(textBox, rect); }; - const hidePill = () => { - if (pillContainer) { - pillContainer.remove(); - pillContainer = null; + const createRephraseButton = (textBox, rect) => { + if (state.rephraseButton) { + state.rephraseButton.remove(); } - buttonContainer.style.display = 'flex'; - buttonContainer.style.justifyContent = 'center'; - buttonContainer.style.alignItems = 'center'; + + const buttonContainer = document.createElement('div'); + buttonContainer.classList.add('rephrase-button-container'); + + const iconColor = getIconColor(textBox); + buttonContainer.classList.add(`${iconColor}-shadow`); + + const img = createButtonImage(iconColor); + buttonContainer.appendChild(img); + + positionButton(buttonContainer, rect, img); + setupButtonEvents(buttonContainer, textBox, img, iconColor); + + document.body.appendChild(buttonContainer); + state.rephraseButton = buttonContainer; + observeTextBox(textBox, buttonContainer); }; - buttonContainer.addEventListener('mouseenter', () => { - if (!isFetching) { - if (hasRephrasedBefore) { - showPill(); - } else { - img.src = chrome.runtime.getURL(`extension/assets/rephrase-${iconColor}-hover.svg`); + const createButtonImage = (iconColor) => { + const img = document.createElement('img'); + img.src = getAssetUrl('rephrase', iconColor); + img.alt = 'Rephrase'; + img.dataset.currentIcon = 'rephrase'; + Object.assign(img.style, { + width: '17px', + height: '17px', + objectFit: 'contain' + }); + return img; + }; + + const positionButton = (buttonContainer, rect, img) => { + const containerHeight = img.style.height; + const containerWidth = img.style.width; + Object.assign(buttonContainer.style, { + left: `${rect.right - parseInt(containerWidth) - 30 + window.scrollX}px`, + top: `${rect.bottom - parseInt(containerHeight) - 10 + window.scrollY}px`, + display: 'flex', + justifyContent: 'center', + alignItems: 'center' + }); + }; + + const setupButtonEvents = (buttonContainer, textBox, img, iconColor) => { + buttonContainer.addEventListener('mouseenter', () => { + if (!state.isFetching) { + if (state.hasRephrasedBefore) { + showPill(buttonContainer, textBox, img); + } else { + if (img.dataset.currentIcon === 'rephrase') { + img.src = getAssetUrl('rephrase', iconColor, true); + } + } } - } - }); - - buttonContainer.addEventListener('mouseleave', (event) => { - if (!isFetching) { - if (hasRephrasedBefore) { - // Check if the mouse is moving to the pill - const pillRect = pillContainer ? pillContainer.getBoundingClientRect() : null; - if (!pillRect || - event.clientX < pillRect.left || - event.clientX > pillRect.right || - event.clientY < pillRect.top || - event.clientY > pillRect.bottom) { - hidePill(); + }); + + buttonContainer.addEventListener('mouseleave', (event) => { + if (!state.isFetching) { + if (state.hasRephrasedBefore) { + handlePillMouseLeave(event, buttonContainer); + } else { + if (img.dataset.currentIcon === 'rephrase') { + img.src = getAssetUrl('rephrase', iconColor); + } } - } else { - img.src = chrome.runtime.getURL(`extension/assets/rephrase-${iconColor}.svg`); } - } - }); - - const containerHeight = img.style.height; - const containerWidth = img.style.width; + }); - buttonContainer.style.left = `${rect.right - parseInt(containerWidth) - 30 + window.scrollX}px`; - buttonContainer.style.top = `${rect.bottom - parseInt(containerHeight) - 10 + window.scrollY}px`; + buttonContainer.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); - document.body.appendChild(buttonContainer); + if (state.isFetching) { + undoRephraseText(textBox, img); + } else { + rephraseText(textBox, img); + } + }); + }; - rephraseButton = buttonContainer; + const showPill = (buttonContainer, textBox, img) => { + buttonContainer.style.display = 'none'; + createPillContainer(textBox, img); + }; - buttonContainer.addEventListener('click', async (e) => { - e.preventDefault(); - e.stopPropagation(); + const handlePillMouseLeave = (event, buttonContainer) => { + const pillRect = state.pillContainer ? state.pillContainer.getBoundingClientRect() : null; + if (!pillRect || + event.clientX < pillRect.left || + event.clientX > pillRect.right || + event.clientY < pillRect.top || + event.clientY > pillRect.bottom) { + hidePill(buttonContainer); + } + }; - if (isFetching) { - // If fetching, stop the process and revert to original text - undoRephraseText(textBox, img); - } else { - // Start rephrasing process - rephraseText(textBox, img); + const hidePill = (buttonContainer) => { + if (state.pillContainer) { + state.pillContainer.remove(); + state.pillContainer = null; } - }); - - // Observe changes to the DOM to detect when the text box is removed or hidden - const observer = new MutationObserver((mutationsList) => { - for (let mutation of mutationsList) { - if (mutation.type === 'childList' || mutation.type === 'attributes') { - if (!document.body.contains(textBox) || textBox.offsetParent === null) { - buttonContainer.remove(); - observer.disconnect(); + buttonContainer.style.display = 'flex'; + }; + + const observeTextBox = (textBox, buttonContainer) => { + const observer = new MutationObserver((mutationsList) => { + for (let mutation of mutationsList) { + if (mutation.type === 'childList' || mutation.type === 'attributes') { + if (!document.body.contains(textBox) || textBox.offsetParent === null) { + buttonContainer.remove(); + observer.disconnect(); + } } } + }); + observer.observe(document.body, { childList: true, subtree: true, attributes: true }); + }; + + const handleTextBoxFocus = (activeElement) => { + if (state.showRephraseButton) { + if (state.pillContainer) { + state.pillContainer.remove(); + } + addButtonToTextBox(activeElement); + } else { + createPillContainer(activeElement); } - }); - observer.observe(document.body, { childList: true, subtree: true, attributes: true }); -}; -document.addEventListener('focusin', (event) => { - const activeElement = event.target; + state.showRephraseButton = true; - if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable) { + if (state.prevActiveElement) { + removeGradientColor(state.prevActiveElement); + } + state.prevActiveElement = activeElement; + }; - if (showRephraseButton) { - if (pillContainer) pillContainer.remove(); - addButtonToTextBox(activeElement); + const updateButtonForFetching = (textBox, img) => { + img.src = getAssetUrl('cross', 'light'); + img.alt = 'stop'; + img.dataset.currentIcon = 'cross'; + Object.assign(img.style, { + width: '20px', + height: '20px' + }); + + const buttonContainer = img.parentElement; + const iconColor = getIconColor(textBox); + buttonContainer.classList.remove('dark-shadow', 'light-shadow'); + buttonContainer.classList.add(`${iconColor}-shadow`); + }; + + const clearTextBox = (element) => { + if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') { + element.value = ''; + } else if (element.isContentEditable) { + element.innerText = ''; } - else - createPillContainer(activeElement); + }; - showRephraseButton = true; + const focusTextBox = (textBox) => { + textBox.focus(); + if (textBox.tagName === 'INPUT' || textBox.tagName === 'TEXTAREA') { + const length = textBox.value.length; + textBox.setSelectionRange(length, length); + } else if (textBox.isContentEditable) { + const range = document.createRange(); + const sel = window.getSelection(); + range.selectNodeContents(textBox); + range.collapse(false); + sel.removeAllRanges(); + sel.addRange(range); + } + }; - if (prevActiveElement) { - removeGradientColor(prevActiveElement); + // Event handlers + const focusinHandler = (event) => { + const activeElement = event.target; + if (isValidTextBox(activeElement)) { + handleTextBoxFocus(activeElement); + } + }; + + const inputHandler = (event) => { + const activeElement = event.target; + if (isValidTextBox(activeElement)) { + addButtonToTextBox(activeElement); } - prevActiveElement = activeElement; - } -}); - -document.addEventListener('input', (event) => { - const activeElement = event.target; - if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable) { - addButtonToTextBox(activeElement); - } -}); \ No newline at end of file + }; + + const isValidTextBox = (element) => { + return element.tagName === 'INPUT' || + element.tagName === 'TEXTAREA' || + element.isContentEditable; + }; + + const initialize = () => { + document.addEventListener('focusin', focusinHandler); + document.addEventListener('input', inputHandler); + }; + + return { + initialize, + }; +})(); + +TextRephraser.initialize(); \ No newline at end of file diff --git a/components/ping_ai_copilot/extension/content/ui/style.css b/components/ping_ai_copilot/extension/content/ui/style.css index df082315d3d7..22a9882cef4c 100644 --- a/components/ping_ai_copilot/extension/content/ui/style.css +++ b/components/ping_ai_copilot/extension/content/ui/style.css @@ -56,13 +56,13 @@ z-index: 10000001; } -.left-tooltip { +.back-tooltip { bottom: 106%; left: 30%; transform: translateX(-50%); } -.right-tooltip { +.retry-tooltip { bottom: 106%; left: 70%; transform: translateX(-50%); diff --git a/components/ping_ai_copilot/extension/manifest.json b/components/ping_ai_copilot/extension/manifest.json index b8aeb621c64a..fcd231c9f3aa 100644 --- a/components/ping_ai_copilot/extension/manifest.json +++ b/components/ping_ai_copilot/extension/manifest.json @@ -4,6 +4,8 @@ "version": "1.0", "description": "Ai copilot - by Ping.", "permissions": [ + "storage", + "tabs", "activeTab", "scripting", "declarativeNetRequest", @@ -31,5 +33,5 @@ "type": "module" }, - "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArfOx1MW/cb3YPNlmT37CuISYgRbtR1SIdgnx/cfTyXO/PuD1VVsQWLDmrZGDmYVCzZvP36t75uhpJH4IoXL58U16yhdXZeSlb0LKcgMZB6cMNyjznV4NTEeY+tLnwGaB1TVdkJgSlY09psyfvcdzQd8xz9CNE6CXDzEq8+uMSaoAyEJ3nP78yV33nBrMj3jbjTi1fr2QsrpoISql/pJ9Zr5V0QbK4wIqln20ly96KuAO5c1DM9z9VnoYFdirEZBfkT/4gB7pBfyd4ScoMhXuaa9w53N8Espu1bC0RGmaKB679rGQdaBTrEUGF+PNfsucjnyrsnup6GMVhc91CXTDjQIDBQAA" + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArfOx1MW/cb3YPNlmT37CuISYgRbtR1SIdgnx/cfTyXO/PuD1VVsQWLDmrZGDmYVCzZvP36t75uhpJH4IoXL58U16yhdXZeSlb0LKcgMZB6cMNyjznV4NTEeY+tLnwGaB1TVdkJgSlY09psyfvcdzQd8xz9CNE6CXDzEq8+uMSaoAyEJ3nP78yV33nBrMj3jbjTi1fr2QsrpoISql/pJ9Zr5V0QbK4wIqln20ly96KuAO5c1DM9z9VnoYFdirEZBfkT/4gB7pBfyd4ScoMhXuaa9w53N8Espu1bC0RGmaKB679rGQdaBTrEUGF+PNfsucjnyrsnup6GMVhc91CXTDjQIDAQAA" } diff --git a/components/ping_ai_copilot/extension/service_worker/background.js b/components/ping_ai_copilot/extension/service_worker/background.js index f64a51f142eb..6857f1024c86 100644 --- a/components/ping_ai_copilot/extension/service_worker/background.js +++ b/components/ping_ai_copilot/extension/service_worker/background.js @@ -1,56 +1,84 @@ import { translations } from "../constants/constants.js"; +const API_ENDPOINT = "https://openai-text-summarizer.azurewebsites.net"; + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.action === 'summarize') { - (async () => { - try { - let ln = chrome.i18n.getUILanguage(); - let headerText = 'Text summary'; - const translation = translations.find(t => t.code === ln); - if (translation) { - headerText = translation.translation; - } - const response = await fetch('https://openai-text-summarizer.azurewebsites.net/summarize', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ text: request.text, lang: ln }), - }); - - const data = await response.json(); - chrome.tabs.sendMessage(sender.tab.id, { action: 'displaySummary', summary: data.summary, headerText: headerText }, response => { - if (chrome.runtime.lastError) { - console.error(chrome.runtime.lastError); - } - }); - } catch (error) { - chrome.tabs.sendMessage(sender.tab.id, { action: 'displaySummary', summary: 'An error occurred while summarizing the text' }, response => { - if (chrome.runtime.lastError) { - console.error(chrome.runtime.lastError); - } - }); - } - })(); - return true; // Indicates that the response will be sent asynchronously + switch (request.action) { + case 'reload': + handleReloadMessage(sendResponse); + break; + case 'summarize': + handleSummarizeMessage(request, sendResponse); + break; + case 'rephrase': + handleRephraseMessage(request, sendResponse); + break; + default: + console.warn('Unknown action:', request.action); + sendResponse({ error: 'Unknown action' }); + } + + return true; +}); + +const handleSummarizeMessage = async (request, sendResponse) => { + try { + const ln = chrome.i18n.getUILanguage(); + let headerText = 'Text summary'; + + const translation = translations.find(t => t.code === ln); + if (translation) { + headerText = translation.translation; + } + + const response = await fetch(`${API_ENDPOINT}/summarize`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: request.text, lang: ln }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + sendResponse({ success: true, summary: data.summary, headerText: headerText }); + } catch (error) { + console.error('Summarize error:', error); + sendResponse({ + success: false, + error: error.message, + summary: "An error occurred while summarizing the text" + }); } - if (request.action === 'rephrase') { - (async () => { - try { - let ln = chrome.i18n.getUILanguage(); - const response = await fetch('https://openai-text-summarizer.azurewebsites.net/rephrase', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ text: request.text, lang: ln }), - }); - - const data = await response.json(); - sendResponse({ rephrase: data.rText }) - } catch (error) { - sendResponse({ rephrase: "An error occurred while rephrasing the text" }) - } - })(); - return true; +} + +const handleRephraseMessage = async (request, sendResponse) => { + try { + const ln = chrome.i18n.getUILanguage(); + const response = await fetch(`${API_ENDPOINT}/rephrase`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: request.text, lang: ln }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (!data.rText) { + throw new Error('Response missing rText property'); + } + + sendResponse({ success: true, rephrase: data.rText }); + } catch (error) { + console.error('Rephrase error:', error); + sendResponse({ + success: false, + error: error.message, + rephrase: "An error occurred while rephrasing the text" + }); } -}); \ No newline at end of file +} \ No newline at end of file