diff --git a/src-tauri/src/app/setup.rs b/src-tauri/src/app/setup.rs index 3a701f119..54ff8804a 100644 --- a/src-tauri/src/app/setup.rs +++ b/src-tauri/src/app/setup.rs @@ -88,6 +88,7 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box .initialization_script(include_str!("../scripts/export.js")) .initialization_script(include_str!("../scripts/markdown.export.js")) .initialization_script(include_str!("../scripts/cmd.js")) + .initialization_script(include_str!("../scripts/chat.js")) } main_win.build().unwrap(); diff --git a/src-tauri/src/app/window.rs b/src-tauri/src/app/window.rs index 1e56da91a..60448fbf4 100644 --- a/src-tauri/src/app/window.rs +++ b/src-tauri/src/app/window.rs @@ -31,6 +31,7 @@ pub fn tray_window(handle: &tauri::AppHandle) { .initialization_script(include_str!("../vendors/floating-ui-core.js")) .initialization_script(include_str!("../vendors/floating-ui-dom.js")) .initialization_script(include_str!("../scripts/cmd.js")) + .initialization_script(include_str!("../scripts/chat.js")) .initialization_script(include_str!("../scripts/popup.core.js")) } diff --git a/src-tauri/src/scripts/chat.js b/src-tauri/src/scripts/chat.js new file mode 100644 index 000000000..aab5c975d --- /dev/null +++ b/src-tauri/src/scripts/chat.js @@ -0,0 +1,95 @@ +async function init() { + new MutationObserver(function (mutationsList) { + for (const mutation of mutationsList) { + if (mutation.target.closest("form")) { + chatBtns(); + } + } + }).observe(document.body, { + childList: true, + subtree: true, + }); +} + +function chatBtns() { + Array.from(document.querySelectorAll("main >div>div>div>div>div")) + .forEach(i => { + if (i.querySelector('.chat-item-copy')) return; + if (!i.querySelector('button.rounded-md')) return; + const cpbtn = i.querySelector('button.rounded-md').cloneNode(true); + cpbtn.classList.add('chat-item-copy'); + cpbtn.title = 'Copy to clipboard'; + cpbtn.innerHTML = setIcon('copy'); + i.querySelector('.self-end').appendChild(cpbtn); + cpbtn.onclick = () => { + copyToClipboard(i?.innerText?.trim() || '', cpbtn); + } + + const saybtn = i.querySelector('button.rounded-md').cloneNode(true); + saybtn.classList.add('chat-item-voice'); + saybtn.title = 'Say'; + saybtn.innerHTML = setIcon('voice'); + i.querySelector('.self-end').appendChild(saybtn); + let amISpeaking = null; + const synth = window.speechSynthesis; + saybtn.onclick = () => { + const txt = i?.innerText?.trim() || ''; + const lang = 'en-US'; + if (!txt) return; + if (amISpeaking) { + amISpeaking = null; + synth.cancel(); + saybtn.innerHTML = setIcon('voice'); + return; + } + const utterance = new SpeechSynthesisUtterance(txt); + utterance.lang = lang; + utterance.voice = speechSynthesis.getVoices().find(voice => voice.lang === lang); + synth.speak(utterance); + amISpeaking = synth.speaking; + saybtn.innerHTML = setIcon('speaking'); + utterance.addEventListener('end', () => { + saybtn.innerHTML = setIcon('voice'); + }); + } + }) +} + +function copyToClipboard(text, btn) { + window.clearTimeout(window.__cpTimeout); + btn.innerHTML = setIcon('cpok'); + if (navigator.clipboard) { + navigator.clipboard.writeText(text); + } else { + var textarea = document.createElement('textarea'); + document.body.appendChild(textarea); + textarea.style.position = 'fixed'; + textarea.style.clip = 'rect(0 0 0 0)'; + textarea.style.top = '10px'; + textarea.value = text; + textarea.select(); + document.execCommand('copy', true); + document.body.removeChild(textarea); + } + window.__cpTimeout = setTimeout(() => { + btn.innerHTML = setIcon('copy'); + }, 1000); +} + +function setIcon(type) { + return { + copy: ``, + cpok: ``, + voice: ``, + speaking: ``, + }[type]; +} + +if ( + document.readyState === "complete" || + document.readyState === "interactive" +) { + init(); +} else { + document.addEventListener("DOMContentLoaded", init); +} \ No newline at end of file diff --git a/src-tauri/src/scripts/export.js b/src-tauri/src/scripts/export.js index 6fe0f60df..6ef2f7b6e 100644 --- a/src-tauri/src/scripts/export.js +++ b/src-tauri/src/scripts/export.js @@ -15,6 +15,7 @@ async function init() { if (!actionsArea) { return; } + if (shouldAddButtons(actionsArea)) { let TryAgainButton = actionsArea.querySelector("button"); if (!TryAgainButton) { @@ -23,302 +24,263 @@ async function init() { TryAgainButton = parentNode.querySelector("button"); } addActionsButtons(actionsArea, TryAgainButton, chatConf); - copyBtns(); } else if (shouldRemoveButtons()) { removeButtons(); } }, 1000); -} -window.addEventListener('resize', init); - -const Format = { - PNG: "png", - PDF: "pdf", -}; + const Format = { + PNG: "png", + PDF: "pdf", + }; -function shouldRemoveButtons() { - if (document.querySelector("form .text-2xl")) { - return true; + function shouldRemoveButtons() { + if (document.querySelector("form .text-2xl")) { + return true; + } + return false; } - return false; -} -function shouldAddButtons(actionsArea) { - // first, check if there's a "Try Again" button and no other buttons - const buttons = actionsArea.querySelectorAll("button"); + function shouldAddButtons(actionsArea) { + // first, check if there's a "Try Again" button and no other buttons + const buttons = actionsArea.querySelectorAll("button"); - const hasTryAgainButton = Array.from(buttons).some((button) => { - return !/download-/.test(button.id); - }); + const hasTryAgainButton = Array.from(buttons).some((button) => { + return !/download-/.test(button.id); + }); - const stopBtn = buttons?.[0]?.innerText; + const stopBtn = buttons?.[0]?.innerText; - if (/Stop generating/ig.test(stopBtn)) { - return false; - } + if (/Stop generating/ig.test(stopBtn)) { + return false; + } - if (buttons.length === 2 && (/Regenerate response/ig.test(stopBtn) || buttons[1].innerText === '')) { - return true; - } + if (buttons.length === 2 && (/Regenerate response/ig.test(stopBtn) || buttons[1].innerText === '')) { + return true; + } - if (hasTryAgainButton && buttons.length === 1) { - return true; - } + if (hasTryAgainButton && buttons.length === 1) { + return true; + } + + // otherwise, check if open screen is not visible + const isOpenScreen = document.querySelector("h1.text-4xl"); + if (isOpenScreen) { + return false; + } + + // check if the conversation is finished and there are no share buttons + const finishedConversation = document.querySelector("form button>svg"); + const hasShareButtons = actionsArea.querySelectorAll("button[share-ext]"); + if (finishedConversation && !hasShareButtons.length) { + return true; + } - // otherwise, check if open screen is not visible - const isOpenScreen = document.querySelector("h1.text-4xl"); - if (isOpenScreen) { return false; } - // check if the conversation is finished and there are no share buttons - const finishedConversation = document.querySelector("form button>svg"); - const hasShareButtons = actionsArea.querySelectorAll("button[share-ext]"); - if (finishedConversation && !hasShareButtons.length) { - return true; + function removeButtons() { + const downloadButton = document.getElementById("download-png-button"); + const downloadPdfButton = document.getElementById("download-pdf-button"); + const downloadMdButton = document.getElementById("download-markdown-button"); + if (downloadButton) { + downloadButton.remove(); + } + if (downloadPdfButton) { + downloadPdfButton.remove(); + } + if (downloadPdfButton) { + downloadMdButton.remove(); + } } - return false; -} - -function removeButtons() { - const downloadButton = document.getElementById("download-png-button"); - const downloadPdfButton = document.getElementById("download-pdf-button"); - const downloadMdButton = document.getElementById("download-markdown-button"); - if (downloadButton) { - downloadButton.remove(); + function addActionsButtons(actionsArea, TryAgainButton) { + const downloadButton = TryAgainButton.cloneNode(true); + // Export markdown + const exportMd = TryAgainButton.cloneNode(true); + exportMd.id = "download-markdown-button"; + downloadButton.setAttribute("share-ext", "true"); + exportMd.title = "Export Markdown"; + + exportMd.innerHTML = setIcon('md'); + exportMd.onclick = () => { + exportMarkdown(); + }; + actionsArea.appendChild(exportMd); + + // Generate PNG + downloadButton.id = "download-png-button"; + downloadButton.setAttribute("share-ext", "true"); + downloadButton.title = "Generate PNG"; + downloadButton.innerHTML = setIcon('png'); + downloadButton.onclick = () => { + downloadThread(); + }; + actionsArea.appendChild(downloadButton); + + // Generate PDF + const downloadPdfButton = TryAgainButton.cloneNode(true); + downloadPdfButton.id = "download-pdf-button"; + downloadButton.setAttribute("share-ext", "true"); + downloadPdfButton.title = "Download PDF"; + downloadPdfButton.innerHTML = setIcon('pdf'); + downloadPdfButton.onclick = () => { + downloadThread({ as: Format.PDF }); + }; + actionsArea.appendChild(downloadPdfButton); } - if (downloadPdfButton) { - downloadPdfButton.remove(); - } - if (downloadPdfButton) { - downloadMdButton.remove(); - } -} - -function addActionsButtons(actionsArea, TryAgainButton) { - const downloadButton = TryAgainButton.cloneNode(true); - // Export markdown - const exportMd = TryAgainButton.cloneNode(true); - exportMd.id = "download-markdown-button"; - downloadButton.setAttribute("share-ext", "true"); - exportMd.title = "Export Markdown"; - exportMd.innerHTML = setIcon('md'); - exportMd.onclick = () => { - exportMarkdown(); - }; - actionsArea.appendChild(exportMd); - - // Generate PNG - downloadButton.id = "download-png-button"; - downloadButton.setAttribute("share-ext", "true"); - downloadButton.title = "Generate PNG"; - downloadButton.innerHTML = setIcon('png'); - downloadButton.onclick = () => { - downloadThread(); - }; - actionsArea.appendChild(downloadButton); - - // Generate PDF - const downloadPdfButton = TryAgainButton.cloneNode(true); - downloadPdfButton.id = "download-pdf-button"; - downloadButton.setAttribute("share-ext", "true"); - downloadPdfButton.title = "Download PDF"; - downloadPdfButton.innerHTML = setIcon('pdf'); - downloadPdfButton.onclick = () => { - downloadThread({ as: Format.PDF }); - }; - actionsArea.appendChild(downloadPdfButton); -} -async function exportMarkdown() { - const content = Array.from(document.querySelectorAll('main .items-center>div')).map(i => { - let j = i.cloneNode(true); - if (/dark\:bg-gray-800/.test(i.getAttribute('class'))) { - j.innerHTML = `
${i.innerHTML}
`; - } - return j.innerHTML; - }).join(''); - const data = ExportMD.turndown(content); - const { id, filename } = getName(); - await invoke('save_file', { name: `notes/${id}.md`, content: data }); - await invoke('download_list', { pathname: 'chat.notes.json', filename, id, dir: 'notes' }); -} - -function downloadThread({ as = Format.PNG } = {}) { - const elements = new Elements(); - elements.fixLocation(); - const pixelRatio = window.devicePixelRatio; - const minRatio = as === Format.PDF ? 2 : 2.5; - window.devicePixelRatio = Math.max(pixelRatio, minRatio); - - html2canvas(elements.thread, { - letterRendering: true, - }).then(async function (canvas) { - elements.restoreLocation(); - window.devicePixelRatio = pixelRatio; - const imgData = canvas.toDataURL("image/png"); - requestAnimationFrame(() => { - if (as === Format.PDF) { - return handlePdf(imgData, canvas, pixelRatio); - } else { - handleImg(imgData); + async function exportMarkdown() { + const content = Array.from(document.querySelectorAll('main .items-center>div')).map(i => { + let j = i.cloneNode(true); + if (/dark\:bg-gray-800/.test(i.getAttribute('class'))) { + j.innerHTML = `
${i.innerHTML}
`; } - }); - }); -} - -async function handleImg(imgData) { - const binaryData = atob(imgData.split("base64,")[1]); - const data = []; - for (let i = 0; i < binaryData.length; i++) { - data.push(binaryData.charCodeAt(i)); + return j.innerHTML; + }).join(''); + const data = ExportMD.turndown(content); + const { id, filename } = getName(); + await invoke('save_file', { name: `notes/${id}.md`, content: data }); + await invoke('download_list', { pathname: 'chat.notes.json', filename, id, dir: 'notes' }); } - const { pathname, id, filename } = getName(); - await invoke('download', { name: `download/img/${id}.png`, blob: data }); - await invoke('download_list', { pathname, filename, id, dir: 'download' }); -} - -async function handlePdf(imgData, canvas, pixelRatio) { - const { jsPDF } = window.jspdf; - const orientation = canvas.width > canvas.height ? "l" : "p"; - var pdf = new jsPDF(orientation, "pt", [ - canvas.width / pixelRatio, - canvas.height / pixelRatio, - ]); - var pdfWidth = pdf.internal.pageSize.getWidth(); - var pdfHeight = pdf.internal.pageSize.getHeight(); - pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight, '', 'FAST'); - const { pathname, id, filename } = getName(); - const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument()); - await invoke('download', { name: `download/pdf/${id}.pdf`, blob: Array.from(new Uint8Array(data)) }); - await invoke('download_list', { pathname, filename, id, dir: 'download' }); -} -function getName() { - const id = window.crypto.getRandomValues(new Uint32Array(1))[0].toString(36); - const name = document.querySelector('nav .overflow-y-auto a.hover\\:bg-gray-800')?.innerText?.trim() || ''; - return { filename: name ? name : id, id, pathname: 'chat.download.json' }; -} - -class Elements { - constructor() { - this.init(); + function downloadThread({ as = Format.PNG } = {}) { + const elements = new Elements(); + elements.fixLocation(); + const pixelRatio = window.devicePixelRatio; + const minRatio = as === Format.PDF ? 2 : 2.5; + window.devicePixelRatio = Math.max(pixelRatio, minRatio); + + html2canvas(elements.thread, { + letterRendering: true, + }).then(async function (canvas) { + elements.restoreLocation(); + window.devicePixelRatio = pixelRatio; + const imgData = canvas.toDataURL("image/png"); + requestAnimationFrame(() => { + if (as === Format.PDF) { + return handlePdf(imgData, canvas, pixelRatio); + } else { + handleImg(imgData); + } + }); + }); } - init() { - // this.threadWrapper = document.querySelector(".cdfdFe"); - this.spacer = document.querySelector("[class*='h-48'].w-full.flex-shrink-0"); - this.thread = document.querySelector( - "[class*='react-scroll-to-bottom']>[class*='react-scroll-to-bottom']>div" - ); - - // fix: old chat https://github.com/lencx/ChatGPT/issues/185 - if (!this.thread) { - this.thread = document.querySelector("main .overflow-y-auto"); - } - // h-full overflow-y-auto - this.positionForm = document.querySelector("form").parentNode; - // this.styledThread = document.querySelector("main"); - // this.threadContent = document.querySelector(".gAnhyd"); - this.scroller = Array.from( - document.querySelectorAll('[class*="react-scroll-to"]') - ).filter((el) => el.classList.contains("h-full"))[0]; - - // fix: old chat - if (!this.scroller) { - this.scroller = document.querySelector('main .overflow-y-auto'); + async function handleImg(imgData) { + const binaryData = atob(imgData.split("base64,")[1]); + const data = []; + for (let i = 0; i < binaryData.length; i++) { + data.push(binaryData.charCodeAt(i)); } - - this.hiddens = Array.from(document.querySelectorAll(".overflow-hidden")); - this.images = Array.from(document.querySelectorAll("img[srcset]")); + const { pathname, id, filename } = getName(); + await invoke('download', { name: `download/img/${id}.png`, blob: data }); + await invoke('download_list', { pathname, filename, id, dir: 'download' }); } - fixLocation() { - this.hiddens.forEach((el) => { - el.classList.remove("overflow-hidden"); - }); - this.spacer.style.display = "none"; - this.thread.style.maxWidth = "960px"; - this.thread.style.marginInline = "auto"; - this.positionForm.style.display = "none"; - this.scroller.classList.remove("h-full"); - this.scroller.style.minHeight = "100vh"; - this.images.forEach((img) => { - const srcset = img.getAttribute("srcset"); - img.setAttribute("srcset_old", srcset); - img.setAttribute("srcset", ""); - }); - //Fix to the text shifting down when generating the canvas - document.body.style.lineHeight = "0.5"; + + async function handlePdf(imgData, canvas, pixelRatio) { + const { jsPDF } = window.jspdf; + const orientation = canvas.width > canvas.height ? "l" : "p"; + var pdf = new jsPDF(orientation, "pt", [ + canvas.width / pixelRatio, + canvas.height / pixelRatio, + ]); + var pdfWidth = pdf.internal.pageSize.getWidth(); + var pdfHeight = pdf.internal.pageSize.getHeight(); + pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight, '', 'FAST'); + const { pathname, id, filename } = getName(); + const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument()); + await invoke('download', { name: `download/pdf/${id}.pdf`, blob: Array.from(new Uint8Array(data)) }); + await invoke('download_list', { pathname, filename, id, dir: 'download' }); } - restoreLocation() { - this.hiddens.forEach((el) => { - el.classList.add("overflow-hidden"); - }); - this.spacer.style.display = null; - this.thread.style.maxWidth = null; - this.thread.style.marginInline = null; - this.positionForm.style.display = null; - this.scroller.classList.add("h-full"); - this.scroller.style.minHeight = null; - this.images.forEach((img) => { - const srcset = img.getAttribute("srcset_old"); - img.setAttribute("srcset", srcset); - img.setAttribute("srcset_old", ""); - }); - document.body.style.lineHeight = null; + + function getName() { + const id = window.crypto.getRandomValues(new Uint32Array(1))[0].toString(36); + const name = document.querySelector('nav .overflow-y-auto a.hover\\:bg-gray-800')?.innerText?.trim() || ''; + return { filename: name ? name : id, id, pathname: 'chat.download.json' }; } -} -function setIcon(type) { - return { - // link: ``, - png: ``, - pdf: ``, - md: ``, - copy: ``, - cpok: `` - }[type]; -} + class Elements { + constructor() { + this.init(); + } + init() { + // this.threadWrapper = document.querySelector(".cdfdFe"); + this.spacer = document.querySelector("[class*='h-48'].w-full.flex-shrink-0"); + this.thread = document.querySelector( + "[class*='react-scroll-to-bottom']>[class*='react-scroll-to-bottom']>div" + ); + + // fix: old chat https://github.com/lencx/ChatGPT/issues/185 + if (!this.thread) { + this.thread = document.querySelector("main .overflow-y-auto"); + } -function copyBtns() { - Array.from(document.querySelectorAll("main >div>div>div>div>div")) - .forEach(i => { - if (i.querySelector('.chat-item-copy')) return; - if (!i.querySelector('button.rounded-md')) return; - const btn = i.querySelector('button.rounded-md').cloneNode(true); - btn.classList.add('chat-item-copy'); - btn.title = 'Copy to clipboard'; - btn.innerHTML = setIcon('copy'); - i.querySelector('.self-end').appendChild(btn); - btn.onclick = () => { - copyToClipboard(i?.innerText?.trim() || '', btn); + // h-full overflow-y-auto + this.positionForm = document.querySelector("form").parentNode; + // this.styledThread = document.querySelector("main"); + // this.threadContent = document.querySelector(".gAnhyd"); + this.scroller = Array.from( + document.querySelectorAll('[class*="react-scroll-to"]') + ).filter((el) => el.classList.contains("h-full"))[0]; + + // fix: old chat + if (!this.scroller) { + this.scroller = document.querySelector('main .overflow-y-auto'); } - }) -} -function copyToClipboard(text, btn) { - window.clearTimeout(window.__cpTimeout); - btn.innerHTML = setIcon('cpok'); - if (navigator.clipboard) { - navigator.clipboard.writeText(text); - } else { - var textarea = document.createElement('textarea'); - document.body.appendChild(textarea); - textarea.style.position = 'fixed'; - textarea.style.clip = 'rect(0 0 0 0)'; - textarea.style.top = '10px'; - textarea.value = text; - textarea.select(); - document.execCommand('copy', true); - document.body.removeChild(textarea); + this.hiddens = Array.from(document.querySelectorAll(".overflow-hidden")); + this.images = Array.from(document.querySelectorAll("img[srcset]")); + } + fixLocation() { + this.hiddens.forEach((el) => { + el.classList.remove("overflow-hidden"); + }); + this.spacer.style.display = "none"; + this.thread.style.maxWidth = "960px"; + this.thread.style.marginInline = "auto"; + this.positionForm.style.display = "none"; + this.scroller.classList.remove("h-full"); + this.scroller.style.minHeight = "100vh"; + this.images.forEach((img) => { + const srcset = img.getAttribute("srcset"); + img.setAttribute("srcset_old", srcset); + img.setAttribute("srcset", ""); + }); + //Fix to the text shifting down when generating the canvas + document.body.style.lineHeight = "0.5"; + } + restoreLocation() { + this.hiddens.forEach((el) => { + el.classList.add("overflow-hidden"); + }); + this.spacer.style.display = null; + this.thread.style.maxWidth = null; + this.thread.style.marginInline = null; + this.positionForm.style.display = null; + this.scroller.classList.add("h-full"); + this.scroller.style.minHeight = null; + this.images.forEach((img) => { + const srcset = img.getAttribute("srcset_old"); + img.setAttribute("srcset", srcset); + img.setAttribute("srcset_old", ""); + }); + document.body.style.lineHeight = null; + } + } + + function setIcon(type) { + return { + // link: ``, + png: ``, + pdf: ``, + md: ``, + }[type]; } - window.__cpTimeout = setTimeout(() => { - btn.innerHTML = setIcon('copy'); - }, 1000); } +window.addEventListener('resize', init); + if ( document.readyState === "complete" || document.readyState === "interactive"