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`);
}
};