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