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", | 				"file-saver": "^2.0.5", | ||||||
| 				"fuse.js": "^7.0.0", | 				"fuse.js": "^7.0.0", | ||||||
| 				"highlight.js": "^11.9.0", | 				"highlight.js": "^11.9.0", | ||||||
|  | 				"html2canvas-pro": "^1.5.8", | ||||||
| 				"i18next": "^23.10.0", | 				"i18next": "^23.10.0", | ||||||
| 				"i18next-browser-languagedetector": "^7.2.0", | 				"i18next-browser-languagedetector": "^7.2.0", | ||||||
| 				"i18next-resources-to-backend": "^1.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", | 			"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", | ||||||
| 			"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", | 			"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", | ||||||
| 			"license": "MIT", | 			"license": "MIT", | ||||||
| 			"optional": true, |  | ||||||
| 			"engines": { | 			"engines": { | ||||||
| 				"node": ">= 0.6.0" | 				"node": ">= 0.6.0" | ||||||
| 			} | 			} | ||||||
| @ -4759,7 +4759,6 @@ | |||||||
| 			"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", | 			"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", | ||||||
| 			"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", | 			"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", | ||||||
| 			"license": "MIT", | 			"license": "MIT", | ||||||
| 			"optional": true, |  | ||||||
| 			"dependencies": { | 			"dependencies": { | ||||||
| 				"utrie": "^1.0.2" | 				"utrie": "^1.0.2" | ||||||
| 			} | 			} | ||||||
| @ -6842,6 +6841,19 @@ | |||||||
| 				"node": ">=8.0.0" | 				"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": { | 		"node_modules/htmlparser2": { | ||||||
| 			"version": "8.0.2", | 			"version": "8.0.2", | ||||||
| 			"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", | 			"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", | 			"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", | ||||||
| 			"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", | 			"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", | ||||||
| 			"license": "MIT", | 			"license": "MIT", | ||||||
| 			"optional": true, |  | ||||||
| 			"dependencies": { | 			"dependencies": { | ||||||
| 				"utrie": "^1.0.2" | 				"utrie": "^1.0.2" | ||||||
| 			} | 			} | ||||||
| @ -11821,7 +11832,6 @@ | |||||||
| 			"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", | 			"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", | ||||||
| 			"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", | 			"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", | ||||||
| 			"license": "MIT", | 			"license": "MIT", | ||||||
| 			"optional": true, |  | ||||||
| 			"dependencies": { | 			"dependencies": { | ||||||
| 				"base64-arraybuffer": "^1.0.2" | 				"base64-arraybuffer": "^1.0.2" | ||||||
| 			} | 			} | ||||||
|  | |||||||
| @ -80,6 +80,7 @@ | |||||||
| 		"file-saver": "^2.0.5", | 		"file-saver": "^2.0.5", | ||||||
| 		"fuse.js": "^7.0.0", | 		"fuse.js": "^7.0.0", | ||||||
| 		"highlight.js": "^11.9.0", | 		"highlight.js": "^11.9.0", | ||||||
|  | 		"html2canvas-pro": "^1.5.8", | ||||||
| 		"i18next": "^23.10.0", | 		"i18next": "^23.10.0", | ||||||
| 		"i18next-browser-languagedetector": "^7.2.0", | 		"i18next-browser-languagedetector": "^7.2.0", | ||||||
| 		"i18next-resources-to-backend": "^1.2.0", | 		"i18next-resources-to-backend": "^1.2.0", | ||||||
|  | |||||||
| @ -6,6 +6,9 @@ | |||||||
| 	import fileSaver from 'file-saver'; | 	import fileSaver from 'file-saver'; | ||||||
| 	const { saveAs } = fileSaver; | 	const { saveAs } = fileSaver; | ||||||
| 
 | 
 | ||||||
|  | 	import jsPDF from 'jspdf'; | ||||||
|  | 	import html2canvas from 'html2canvas-pro'; | ||||||
|  | 
 | ||||||
| 	import { downloadChatAsPDF } from '$lib/apis/utils'; | 	import { downloadChatAsPDF } from '$lib/apis/utils'; | ||||||
| 	import { copyToClipboard, createMessagesList } from '$lib/utils'; | 	import { copyToClipboard, createMessagesList } from '$lib/utils'; | ||||||
| 
 | 
 | ||||||
| @ -14,7 +17,8 @@ | |||||||
| 		showControls, | 		showControls, | ||||||
| 		showArtifacts, | 		showArtifacts, | ||||||
| 		mobile, | 		mobile, | ||||||
| 		temporaryChatEnabled | 		temporaryChatEnabled, | ||||||
|  | 		theme | ||||||
| 	} from '$lib/stores'; | 	} from '$lib/stores'; | ||||||
| 	import { flyAndScale } from '$lib/utils/transitions'; | 	import { flyAndScale } from '$lib/utils/transitions'; | ||||||
| 
 | 
 | ||||||
| @ -58,27 +62,45 @@ | |||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const downloadPdf = async () => { | 	const downloadPdf = async () => { | ||||||
| 		const history = chat.chat.history; | 		const containerElement = document.getElementById('messages-container'); | ||||||
| 		const messages = createMessagesList(history, history.currentId); |  | ||||||
| 		const blob = await downloadChatAsPDF(localStorage.token, chat.chat.title, messages); |  | ||||||
| 
 | 
 | ||||||
| 		// Create a URL for the blob | 		if (containerElement) { | ||||||
| 		const url = window.URL.createObjectURL(blob); | 			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 imgData = canvas.toDataURL('image/png'); | ||||||
| 		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 | 				// A4 size in mm | ||||||
| 		document.body.appendChild(a); | 				const pdf = new jsPDF('p', 'mm', 'a4'); | ||||||
| 		a.click(); | 				const imgWidth = 210; // A4 width in mm | ||||||
|  | 				const pageHeight = 297; // A4 height in mm | ||||||
| 
 | 
 | ||||||
| 		// Remove the link from the body | 				const imgHeight = (canvas.height * imgWidth) / canvas.width; // Maintain aspect ratio | ||||||
| 		document.body.removeChild(a); | 				let heightLeft = imgHeight; | ||||||
|  | 				let position = 0; | ||||||
| 
 | 
 | ||||||
| 		// Revoke the URL to release memory | 				// First page | ||||||
| 		window.URL.revokeObjectURL(url); | 				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 () => { | 	const downloadJSONExport = async () => { | ||||||
|  | |||||||
| @ -6,6 +6,9 @@ | |||||||
| 	import fileSaver from 'file-saver'; | 	import fileSaver from 'file-saver'; | ||||||
| 	const { saveAs } = fileSaver; | 	const { saveAs } = fileSaver; | ||||||
| 
 | 
 | ||||||
|  | 	import jsPDF from 'jspdf'; | ||||||
|  | 	import html2canvas from 'html2canvas-pro'; | ||||||
|  | 
 | ||||||
| 	const dispatch = createEventDispatcher(); | 	const dispatch = createEventDispatcher(); | ||||||
| 
 | 
 | ||||||
| 	import Dropdown from '$lib/components/common/Dropdown.svelte'; | 	import Dropdown from '$lib/components/common/Dropdown.svelte'; | ||||||
| @ -23,7 +26,7 @@ | |||||||
| 		getChatPinnedStatusById, | 		getChatPinnedStatusById, | ||||||
| 		toggleChatPinnedStatusById | 		toggleChatPinnedStatusById | ||||||
| 	} from '$lib/apis/chats'; | 	} from '$lib/apis/chats'; | ||||||
| 	import { chats } from '$lib/stores'; | 	import { chats, theme } from '$lib/stores'; | ||||||
| 	import { createMessagesList } from '$lib/utils'; | 	import { createMessagesList } from '$lib/utils'; | ||||||
| 	import { downloadChatAsPDF } from '$lib/apis/utils'; | 	import { downloadChatAsPDF } from '$lib/apis/utils'; | ||||||
| 	import Download from '$lib/components/icons/Download.svelte'; | 	import Download from '$lib/components/icons/Download.svelte'; | ||||||
| @ -76,32 +79,45 @@ | |||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const downloadPdf = async () => { | 	const downloadPdf = async () => { | ||||||
| 		const chat = await getChatById(localStorage.token, chatId); | 		const containerElement = document.getElementById('messages-container'); | ||||||
| 		if (!chat) { | 
 | ||||||
| 			return; | 		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; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 		const history = chat.chat.history; | 				pdf.save('document.pdf'); | ||||||
| 		const messages = createMessagesList(history, history.currentId); | 			} catch (error) { | ||||||
| 		const blob = await downloadChatAsPDF(localStorage.token, chat.chat.title, messages); | 				console.error('Error generating PDF', error); | ||||||
| 
 | 			} | ||||||
| 		// 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 () => { | 	const downloadJSONExport = async () => { | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user