diff --git a/src/lib/components/chat/Settings/Interface.svelte b/src/lib/components/chat/Settings/Interface.svelte index f78fa5593..f17c8b40d 100644 --- a/src/lib/components/chat/Settings/Interface.svelte +++ b/src/lib/components/chat/Settings/Interface.svelte @@ -54,6 +54,9 @@ height: '' }; + // chat export + let stylizedPdfExport = true; + // Admin - Show Update Available Toast let showUpdateToast = true; let showChangelog = true; @@ -152,6 +155,11 @@ saveSettings({ hapticFeedback: hapticFeedback }); }; + const toggleStylizedPdfExport = async () => { + stylizedPdfExport = !stylizedPdfExport; + saveSettings({ stylizedPdfExport: stylizedPdfExport }); + }; + const toggleUserLocation = async () => { userLocation = !userLocation; @@ -302,6 +310,11 @@ notificationSound = $settings?.notificationSound ?? true; notificationSoundAlways = $settings?.notificationSoundAlways ?? false; + iframeSandboxAllowSameOrigin = $settings?.iframeSandboxAllowSameOrigin ?? false; + iframeSandboxAllowForms = $settings?.iframeSandboxAllowForms ?? false; + + stylizedPdfExport = $settings?.stylizedPdfExport ?? true; + hapticFeedback = $settings.hapticFeedback ?? false; ctrlEnterToSend = $settings.ctrlEnterToSend ?? false; @@ -964,6 +977,28 @@ +
+
+
+ {$i18n.t('Stylized PDF Export')} +
+ + +
+
+
{$i18n.t('Voice')}
diff --git a/src/lib/components/layout/Navbar/Menu.svelte b/src/lib/components/layout/Navbar/Menu.svelte index 8ae3b2d0a..1118f3e3d 100644 --- a/src/lib/components/layout/Navbar/Menu.svelte +++ b/src/lib/components/layout/Navbar/Menu.svelte @@ -19,7 +19,8 @@ mobile, temporaryChatEnabled, theme, - user + user, + settings } from '$lib/stores'; import { flyAndScale } from '$lib/utils/transitions'; @@ -63,76 +64,124 @@ }; const downloadPdf = async () => { - const containerElement = document.getElementById('messages-container'); + if ($settings?.stylizedPdfExport ?? true) { + const containerElement = document.getElementById('messages-container'); - if (containerElement) { - try { - const isDarkMode = document.documentElement.classList.contains('dark'); - const virtualWidth = 800; // Fixed width in px - const pagePixelHeight = 1200; // Each slice height (adjust to avoid canvas bugs; generally 2–4k is safe) + if (containerElement) { + try { + const isDarkMode = document.documentElement.classList.contains('dark'); + const virtualWidth = 800; // Fixed width in px + const pagePixelHeight = 1200; // Each slice height (adjust to avoid canvas bugs; generally 2–4k is safe) - // Clone & style once - const clonedElement = containerElement.cloneNode(true); - clonedElement.classList.add('text-black'); - clonedElement.classList.add('dark:text-white'); - clonedElement.style.width = `${virtualWidth}px`; - clonedElement.style.position = 'absolute'; - clonedElement.style.left = '-9999px'; // Offscreen - clonedElement.style.height = 'auto'; - document.body.appendChild(clonedElement); + // Clone & style once + const clonedElement = containerElement.cloneNode(true); + clonedElement.classList.add('text-black'); + clonedElement.classList.add('dark:text-white'); + clonedElement.style.width = `${virtualWidth}px`; + clonedElement.style.position = 'absolute'; + clonedElement.style.left = '-9999px'; // Offscreen + clonedElement.style.height = 'auto'; + document.body.appendChild(clonedElement); - // Get total height after attached to DOM - const totalHeight = clonedElement.scrollHeight; - let offsetY = 0; - let page = 0; + // Get total height after attached to DOM + const totalHeight = clonedElement.scrollHeight; + let offsetY = 0; + let page = 0; - // Prepare PDF - const pdf = new jsPDF('p', 'mm', 'a4'); - const imgWidth = 210; // A4 mm - const pageHeight = 297; // A4 mm + // Prepare PDF + const pdf = new jsPDF('p', 'mm', 'a4'); + const imgWidth = 210; // A4 mm + const pageHeight = 297; // A4 mm - while (offsetY < totalHeight) { - // For each slice, adjust scrollTop to show desired part - clonedElement.scrollTop = offsetY; + while (offsetY < totalHeight) { + // For each slice, adjust scrollTop to show desired part + clonedElement.scrollTop = offsetY; - // Optionally: mask/hide overflowing content via CSS if needed - clonedElement.style.maxHeight = `${pagePixelHeight}px`; - // Only render the visible part - const canvas = await html2canvas(clonedElement, { - backgroundColor: isDarkMode ? '#000' : '#fff', - useCORS: true, - scale: 2, - width: virtualWidth, - height: Math.min(pagePixelHeight, totalHeight - offsetY), - // Optionally: y offset for correct region? - windowWidth: virtualWidth - //windowHeight: pagePixelHeight, - }); - const imgData = canvas.toDataURL('image/png'); - // Maintain aspect ratio - const imgHeight = (canvas.height * imgWidth) / canvas.width; - const position = 0; // Always first line, since we've clipped vertically + // Optionally: mask/hide overflowing content via CSS if needed + clonedElement.style.maxHeight = `${pagePixelHeight}px`; + // Only render the visible part + const canvas = await html2canvas(clonedElement, { + backgroundColor: isDarkMode ? '#000' : '#fff', + useCORS: true, + scale: 2, + width: virtualWidth, + height: Math.min(pagePixelHeight, totalHeight - offsetY), + // Optionally: y offset for correct region? + windowWidth: virtualWidth + //windowHeight: pagePixelHeight, + }); + const imgData = canvas.toDataURL('image/png'); + // Maintain aspect ratio + const imgHeight = (canvas.height * imgWidth) / canvas.width; + const position = 0; // Always first line, since we've clipped vertically - if (page > 0) pdf.addPage(); + if (page > 0) pdf.addPage(); - // Set page background for dark mode - if (isDarkMode) { - pdf.setFillColor(0, 0, 0); - pdf.rect(0, 0, imgWidth, pageHeight, 'F'); // black bg + // Set page background for dark mode + if (isDarkMode) { + pdf.setFillColor(0, 0, 0); + pdf.rect(0, 0, imgWidth, pageHeight, 'F'); // black bg + } + + pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); + + offsetY += pagePixelHeight; + page++; } - pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); + document.body.removeChild(clonedElement); - offsetY += pagePixelHeight; - page++; + pdf.save(`chat-${chat.chat.title}.pdf`); + } catch (error) { + console.error('Error generating PDF', error); } - - document.body.removeChild(clonedElement); - - pdf.save(`chat-${chat.chat.title}.pdf`); - } catch (error) { - console.error('Error generating PDF', error); } + } else { + console.log('Downloading PDF'); + + const chatText = await getChatAsText(); + + const doc = new jsPDF(); + + // Margins + const left = 15; + const top = 20; + const right = 15; + const bottom = 20; + + const pageWidth = doc.internal.pageSize.getWidth(); + const pageHeight = doc.internal.pageSize.getHeight(); + const usableWidth = pageWidth - left - right; + const usableHeight = pageHeight - top - bottom; + + // Font size and line height + const fontSize = 8; + doc.setFontSize(fontSize); + const lineHeight = fontSize * 1; // adjust if needed + + // Split the markdown into lines (handles \n) + const paragraphs = chatText.split('\n'); + + let y = top; + + for (let paragraph of paragraphs) { + // Wrap each paragraph to fit the width + const lines = doc.splitTextToSize(paragraph, usableWidth); + + for (let line of lines) { + // If the line would overflow the bottom, add a new page + if (y + lineHeight > pageHeight - bottom) { + doc.addPage(); + y = top; + } + doc.text(line, left, y); + y += lineHeight * 0.5; + } + // Add empty line at paragraph breaks + y += lineHeight * 0.1; + } + + doc.save(`chat-${chat.chat.title}.pdf`); } }; diff --git a/src/lib/components/layout/Sidebar/ChatMenu.svelte b/src/lib/components/layout/Sidebar/ChatMenu.svelte index 6634e9c77..04623502e 100644 --- a/src/lib/components/layout/Sidebar/ChatMenu.svelte +++ b/src/lib/components/layout/Sidebar/ChatMenu.svelte @@ -26,7 +26,7 @@ getChatPinnedStatusById, toggleChatPinnedStatusById } from '$lib/apis/chats'; - import { chats, theme, user } from '$lib/stores'; + import { chats, settings, theme, user } from '$lib/stores'; import { createMessagesList } from '$lib/utils'; import { downloadChatAsPDF } from '$lib/apis/utils'; import Download from '$lib/components/icons/Download.svelte'; @@ -81,76 +81,124 @@ const downloadPdf = async () => { const chat = await getChatById(localStorage.token, chatId); - const containerElement = document.getElementById('messages-container'); + if ($settings?.stylizedPdfExport ?? true) { + const containerElement = document.getElementById('messages-container'); - if (containerElement) { - try { - const isDarkMode = document.documentElement.classList.contains('dark'); - const virtualWidth = 800; // Fixed width in px - const pagePixelHeight = 1200; // Each slice height (adjust to avoid canvas bugs; generally 2–4k is safe) + if (containerElement) { + try { + const isDarkMode = document.documentElement.classList.contains('dark'); + const virtualWidth = 800; // Fixed width in px + const pagePixelHeight = 1200; // Each slice height (adjust to avoid canvas bugs; generally 2–4k is safe) - // Clone & style once - const clonedElement = containerElement.cloneNode(true); - clonedElement.classList.add('text-black'); - clonedElement.classList.add('dark:text-white'); - clonedElement.style.width = `${virtualWidth}px`; - clonedElement.style.position = 'absolute'; - clonedElement.style.left = '-9999px'; // Offscreen - clonedElement.style.height = 'auto'; - document.body.appendChild(clonedElement); + // Clone & style once + const clonedElement = containerElement.cloneNode(true); + clonedElement.classList.add('text-black'); + clonedElement.classList.add('dark:text-white'); + clonedElement.style.width = `${virtualWidth}px`; + clonedElement.style.position = 'absolute'; + clonedElement.style.left = '-9999px'; // Offscreen + clonedElement.style.height = 'auto'; + document.body.appendChild(clonedElement); - // Get total height after attached to DOM - const totalHeight = clonedElement.scrollHeight; - let offsetY = 0; - let page = 0; + // Get total height after attached to DOM + const totalHeight = clonedElement.scrollHeight; + let offsetY = 0; + let page = 0; - // Prepare PDF - const pdf = new jsPDF('p', 'mm', 'a4'); - const imgWidth = 210; // A4 mm - const pageHeight = 297; // A4 mm + // Prepare PDF + const pdf = new jsPDF('p', 'mm', 'a4'); + const imgWidth = 210; // A4 mm + const pageHeight = 297; // A4 mm - while (offsetY < totalHeight) { - // For each slice, adjust scrollTop to show desired part - clonedElement.scrollTop = offsetY; + while (offsetY < totalHeight) { + // For each slice, adjust scrollTop to show desired part + clonedElement.scrollTop = offsetY; - // Optionally: mask/hide overflowing content via CSS if needed - clonedElement.style.maxHeight = `${pagePixelHeight}px`; - // Only render the visible part - const canvas = await html2canvas(clonedElement, { - backgroundColor: isDarkMode ? '#000' : '#fff', - useCORS: true, - scale: 2, - width: virtualWidth, - height: Math.min(pagePixelHeight, totalHeight - offsetY), - // Optionally: y offset for correct region? - windowWidth: virtualWidth - //windowHeight: pagePixelHeight, - }); - const imgData = canvas.toDataURL('image/png'); - // Maintain aspect ratio - const imgHeight = (canvas.height * imgWidth) / canvas.width; - const position = 0; // Always first line, since we've clipped vertically + // Optionally: mask/hide overflowing content via CSS if needed + clonedElement.style.maxHeight = `${pagePixelHeight}px`; + // Only render the visible part + const canvas = await html2canvas(clonedElement, { + backgroundColor: isDarkMode ? '#000' : '#fff', + useCORS: true, + scale: 2, + width: virtualWidth, + height: Math.min(pagePixelHeight, totalHeight - offsetY), + // Optionally: y offset for correct region? + windowWidth: virtualWidth + //windowHeight: pagePixelHeight, + }); + const imgData = canvas.toDataURL('image/png'); + // Maintain aspect ratio + const imgHeight = (canvas.height * imgWidth) / canvas.width; + const position = 0; // Always first line, since we've clipped vertically - if (page > 0) pdf.addPage(); + if (page > 0) pdf.addPage(); - // Set page background for dark mode - if (isDarkMode) { - pdf.setFillColor(0, 0, 0); - pdf.rect(0, 0, imgWidth, pageHeight, 'F'); // black bg + // Set page background for dark mode + if (isDarkMode) { + pdf.setFillColor(0, 0, 0); + pdf.rect(0, 0, imgWidth, pageHeight, 'F'); // black bg + } + + pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); + + offsetY += pagePixelHeight; + page++; } - pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); + document.body.removeChild(clonedElement); - offsetY += pagePixelHeight; - page++; + pdf.save(`chat-${chat.chat.title}.pdf`); + } catch (error) { + console.error('Error generating PDF', error); } - - document.body.removeChild(clonedElement); - - pdf.save(`chat-${chat.chat.title}.pdf`); - } catch (error) { - console.error('Error generating PDF', error); } + } else { + console.log('Downloading PDF'); + + const chatText = await getChatAsText(chat); + + const doc = new jsPDF(); + + // Margins + const left = 15; + const top = 20; + const right = 15; + const bottom = 20; + + const pageWidth = doc.internal.pageSize.getWidth(); + const pageHeight = doc.internal.pageSize.getHeight(); + const usableWidth = pageWidth - left - right; + const usableHeight = pageHeight - top - bottom; + + // Font size and line height + const fontSize = 8; + doc.setFontSize(fontSize); + const lineHeight = fontSize * 1; // adjust if needed + + // Split the markdown into lines (handles \n) + const paragraphs = chatText.split('\n'); + + let y = top; + + for (let paragraph of paragraphs) { + // Wrap each paragraph to fit the width + const lines = doc.splitTextToSize(paragraph, usableWidth); + + for (let line of lines) { + // If the line would overflow the bottom, add a new page + if (y + lineHeight > pageHeight - bottom) { + doc.addPage(); + y = top; + } + doc.text(line, left, y); + y += lineHeight; + } + // Add empty line at paragraph breaks + y += lineHeight * 0.5; + } + + doc.save(`chat-${chat.chat.title}.pdf`); } };