mirror of
				https://github.com/open-webui/open-webui
				synced 2025-06-26 18:26:48 +00:00 
			
		
		
		
	enh: client-side pdf generation
This commit is contained in:
		
							parent
							
								
									c57db1828f
								
							
						
					
					
						commit
						d93828e923
					
				
							
								
								
									
										18
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -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" | ||||
| 			} | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
| @ -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); | ||||
| 		const containerElement = document.getElementById('messages-container'); | ||||
| 
 | ||||
| 		// Create a URL for the blob | ||||
| 		const url = window.URL.createObjectURL(blob); | ||||
| 		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 | ||||
| 				}); | ||||
| 
 | ||||
| 		// Create a link element to trigger the download | ||||
| 		const a = document.createElement('a'); | ||||
| 		a.href = url; | ||||
| 		a.download = `chat-${chat.chat.title}.pdf`; | ||||
| 				const imgData = canvas.toDataURL('image/png'); | ||||
| 
 | ||||
| 		// Append the link to the body and click it programmatically | ||||
| 		document.body.appendChild(a); | ||||
| 		a.click(); | ||||
| 				// 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 | ||||
| 
 | ||||
| 		// Remove the link from the body | ||||
| 		document.body.removeChild(a); | ||||
| 				const imgHeight = (canvas.height * imgWidth) / canvas.width; // Maintain aspect ratio | ||||
| 				let heightLeft = imgHeight; | ||||
| 				let position = 0; | ||||
| 
 | ||||
| 		// Revoke the URL to release memory | ||||
| 		window.URL.revokeObjectURL(url); | ||||
| 				// 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 () => { | ||||
|  | ||||
| @ -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 () => { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user