Browse Source

enh: client-side pdf generation

Timothy Jaeryang Baek 1 month ago
parent
commit
d93828e923

+ 14 - 4
package-lock.json

@@ -37,6 +37,7 @@
 				"file-saver": "^2.0.5",
 				"fuse.js": "^7.0.0",
 				"highlight.js": "^11.9.0",
+				"html2canvas-pro": "^1.5.8",
 				"i18next": "^23.10.0",
 				"i18next-browser-languagedetector": "^7.2.0",
 				"i18next-resources-to-backend": "^1.2.0",
@@ -3884,7 +3885,6 @@
 			"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
 			"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
 			"license": "MIT",
-			"optional": true,
 			"engines": {
 				"node": ">= 0.6.0"
 			}
@@ -4759,7 +4759,6 @@
 			"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
 			"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
 			"license": "MIT",
-			"optional": true,
 			"dependencies": {
 				"utrie": "^1.0.2"
 			}
@@ -6842,6 +6841,19 @@
 				"node": ">=8.0.0"
 			}
 		},
+		"node_modules/html2canvas-pro": {
+			"version": "1.5.8",
+			"resolved": "https://registry.npmjs.org/html2canvas-pro/-/html2canvas-pro-1.5.8.tgz",
+			"integrity": "sha512-bVGAU7IvhBwBlRAmX6QhekX8lsaxmYoF6zIwf/HNlHscjx+KN8jw/U4PQRYqeEVm9+m13hcS1l5ChJB9/e29Lw==",
+			"license": "MIT",
+			"dependencies": {
+				"css-line-break": "^2.1.0",
+				"text-segmentation": "^1.0.3"
+			},
+			"engines": {
+				"node": ">=16.0.0"
+			}
+		},
 		"node_modules/htmlparser2": {
 			"version": "8.0.2",
 			"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
@@ -11472,7 +11484,6 @@
 			"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
 			"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
 			"license": "MIT",
-			"optional": true,
 			"dependencies": {
 				"utrie": "^1.0.2"
 			}
@@ -11821,7 +11832,6 @@
 			"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
 			"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
 			"license": "MIT",
-			"optional": true,
 			"dependencies": {
 				"base64-arraybuffer": "^1.0.2"
 			}

+ 1 - 0
package.json

@@ -80,6 +80,7 @@
 		"file-saver": "^2.0.5",
 		"fuse.js": "^7.0.0",
 		"highlight.js": "^11.9.0",
+		"html2canvas-pro": "^1.5.8",
 		"i18next": "^23.10.0",
 		"i18next-browser-languagedetector": "^7.2.0",
 		"i18next-resources-to-backend": "^1.2.0",

+ 44 - 22
src/lib/components/layout/Navbar/Menu.svelte

@@ -6,6 +6,9 @@
 	import fileSaver from 'file-saver';
 	const { saveAs } = fileSaver;
 
+	import jsPDF from 'jspdf';
+	import html2canvas from 'html2canvas-pro';
+
 	import { downloadChatAsPDF } from '$lib/apis/utils';
 	import { copyToClipboard, createMessagesList } from '$lib/utils';
 
@@ -14,7 +17,8 @@
 		showControls,
 		showArtifacts,
 		mobile,
-		temporaryChatEnabled
+		temporaryChatEnabled,
+		theme
 	} from '$lib/stores';
 	import { flyAndScale } from '$lib/utils/transitions';
 
@@ -58,27 +62,45 @@
 	};
 
 	const downloadPdf = async () => {
-		const history = chat.chat.history;
-		const messages = createMessagesList(history, history.currentId);
-		const blob = await downloadChatAsPDF(localStorage.token, chat.chat.title, messages);
-
-		// Create a URL for the blob
-		const url = window.URL.createObjectURL(blob);
-
-		// Create a link element to trigger the download
-		const a = document.createElement('a');
-		a.href = url;
-		a.download = `chat-${chat.chat.title}.pdf`;
-
-		// Append the link to the body and click it programmatically
-		document.body.appendChild(a);
-		a.click();
-
-		// Remove the link from the body
-		document.body.removeChild(a);
-
-		// Revoke the URL to release memory
-		window.URL.revokeObjectURL(url);
+		const containerElement = document.getElementById('messages-container');
+
+		if (containerElement) {
+			try {
+				const canvas = await html2canvas(containerElement, {
+					backgroundColor: $theme.includes('dark') ? '#000' : '#fff',
+					scale: 2, // Increases resolution for better quality
+					height: containerElement.scrollHeight,
+					windowHeight: containerElement.scrollHeight
+				});
+
+				const imgData = canvas.toDataURL('image/png');
+
+				// A4 size in mm
+				const pdf = new jsPDF('p', 'mm', 'a4');
+				const imgWidth = 210; // A4 width in mm
+				const pageHeight = 297; // A4 height in mm
+
+				const imgHeight = (canvas.height * imgWidth) / canvas.width; // Maintain aspect ratio
+				let heightLeft = imgHeight;
+				let position = 0;
+
+				// First page
+				pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
+				heightLeft -= pageHeight;
+
+				// If content overflows, add new pages
+				while (heightLeft > 0) {
+					position -= pageHeight;
+					pdf.addPage();
+					pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
+					heightLeft -= pageHeight;
+				}
+
+				pdf.save('document.pdf');
+			} catch (error) {
+				console.error('Error generating PDF', error);
+			}
+		}
 	};
 
 	const downloadJSONExport = async () => {

+ 42 - 26
src/lib/components/layout/Sidebar/ChatMenu.svelte

@@ -6,6 +6,9 @@
 	import fileSaver from 'file-saver';
 	const { saveAs } = fileSaver;
 
+	import jsPDF from 'jspdf';
+	import html2canvas from 'html2canvas-pro';
+
 	const dispatch = createEventDispatcher();
 
 	import Dropdown from '$lib/components/common/Dropdown.svelte';
@@ -23,7 +26,7 @@
 		getChatPinnedStatusById,
 		toggleChatPinnedStatusById
 	} from '$lib/apis/chats';
-	import { chats } from '$lib/stores';
+	import { chats, theme } from '$lib/stores';
 	import { createMessagesList } from '$lib/utils';
 	import { downloadChatAsPDF } from '$lib/apis/utils';
 	import Download from '$lib/components/icons/Download.svelte';
@@ -76,32 +79,45 @@
 	};
 
 	const downloadPdf = async () => {
-		const chat = await getChatById(localStorage.token, chatId);
-		if (!chat) {
-			return;
+		const containerElement = document.getElementById('messages-container');
+
+		if (containerElement) {
+			try {
+				const canvas = await html2canvas(containerElement, {
+					backgroundColor: $theme.includes('dark') ? '#1a202c' : '#fff',
+					scale: 2, // Increases resolution for better quality
+					height: containerElement.scrollHeight,
+					windowHeight: containerElement.scrollHeight
+				});
+
+				const imgData = canvas.toDataURL('image/png');
+
+				// A4 size in mm
+				const pdf = new jsPDF('p', 'mm', 'a4');
+				const imgWidth = 210; // A4 width in mm
+				const pageHeight = 297; // A4 height in mm
+
+				const imgHeight = (canvas.height * imgWidth) / canvas.width; // Maintain aspect ratio
+				let heightLeft = imgHeight;
+				let position = 0;
+
+				// First page
+				pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
+				heightLeft -= pageHeight;
+
+				// If content overflows, add new pages
+				while (heightLeft > 0) {
+					position -= pageHeight;
+					pdf.addPage();
+					pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
+					heightLeft -= pageHeight;
+				}
+
+				pdf.save('document.pdf');
+			} catch (error) {
+				console.error('Error generating PDF', error);
+			}
 		}
-
-		const history = chat.chat.history;
-		const messages = createMessagesList(history, history.currentId);
-		const blob = await downloadChatAsPDF(localStorage.token, chat.chat.title, messages);
-
-		// Create a URL for the blob
-		const url = window.URL.createObjectURL(blob);
-
-		// Create a link element to trigger the download
-		const a = document.createElement('a');
-		a.href = url;
-		a.download = `chat-${chat.chat.title}.pdf`;
-
-		// Append the link to the body and click it programmatically
-		document.body.appendChild(a);
-		a.click();
-
-		// Remove the link from the body
-		document.body.removeChild(a);
-
-		// Revoke the URL to release memory
-		window.URL.revokeObjectURL(url);
 	};
 
 	const downloadJSONExport = async () => {