From d3a952877a7d1346ea844cf62af38e19741953cc Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 26 Aug 2025 17:34:33 +0400 Subject: [PATCH] refac: pdf export --- src/lib/components/chat/Messages.svelte | 6 +- .../components/chat/Messages/CodeBlock.svelte | 39 +++-- .../chat/Messages/ContentRenderer.svelte | 3 + .../components/chat/Messages/Markdown.svelte | 3 + .../Messages/Markdown/MarkdownTokens.svelte | 3 + .../components/chat/Messages/Message.svelte | 4 + .../Messages/MultiResponseMessages.svelte | 2 + .../chat/Messages/ResponseMessage.svelte | 2 + .../chat/Messages/UserMessage.svelte | 8 +- src/lib/components/layout/Navbar/Menu.svelte | 32 +++- .../components/layout/Sidebar/ChatMenu.svelte | 149 ------------------ 11 files changed, 86 insertions(+), 165 deletions(-) diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index 8c6712ebf9..f7e7a8345d 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -49,6 +49,7 @@ export let addMessages: Function = () => {}; export let readOnly = false; + export let editCodeBlock = true; export let topPadding = false; export let bottomPadding = false; @@ -56,7 +57,7 @@ export let onSelect = (e) => {}; - let messagesCount = 20; + export let messagesCount: number | null = 20; let messagesLoading = false; const loadMoreMessages = async () => { @@ -76,7 +77,7 @@ let _messages = []; let message = history.messages[history.currentId]; - while (message && _messages.length <= messagesCount) { + while (message && (messagesCount !== null ? _messages.length <= messagesCount : true)) { _messages.unshift({ ...message }); message = message.parentId !== null ? history.messages[message.parentId] : null; } @@ -447,6 +448,7 @@ {addMessages} {triggerScroll} {readOnly} + {editCodeBlock} {topPadding} /> {/each} diff --git a/src/lib/components/chat/Messages/CodeBlock.svelte b/src/lib/components/chat/Messages/CodeBlock.svelte index 3b0dfc59a5..8c879bc75e 100644 --- a/src/lib/components/chat/Messages/CodeBlock.svelte +++ b/src/lib/components/chat/Messages/CodeBlock.svelte @@ -1,4 +1,6 @@ @@ -68,6 +69,7 @@ {editMessage} {deleteMessage} {readOnly} + {editCodeBlock} {topPadding} /> {:else if (history.messages[history.messages[messageId].parentId]?.models?.length ?? 1) === 1} @@ -93,6 +95,7 @@ {regenerateResponse} {addMessages} {readOnly} + {editCodeBlock} {topPadding} /> {:else} @@ -116,6 +119,7 @@ {triggerScroll} {addMessages} {readOnly} + {editCodeBlock} {topPadding} /> {/if} diff --git a/src/lib/components/chat/Messages/MultiResponseMessages.svelte b/src/lib/components/chat/Messages/MultiResponseMessages.svelte index 231fca66c0..0e7b108961 100644 --- a/src/lib/components/chat/Messages/MultiResponseMessages.svelte +++ b/src/lib/components/chat/Messages/MultiResponseMessages.svelte @@ -29,6 +29,7 @@ export let isLastMessage; export let readOnly = false; + export let editCodeBlock = true; export let setInputText: Function = () => {}; export let updateChat: Function; @@ -379,6 +380,7 @@ }} {addMessages} {readOnly} + {editCodeBlock} {topPadding} /> {/if} diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index 6010fa48d3..a8722912db 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -138,6 +138,7 @@ export let isLastMessage = true; export let readOnly = false; + export let editCodeBlock = true; export let topPadding = false; let citationsElement: HTMLDivElement; @@ -819,6 +820,7 @@ ($settings?.showFloatingActionButtons ?? true)} save={!readOnly} preview={!readOnly} + {editCodeBlock} {topPadding} done={($settings?.chatFadeStreamingText ?? true) ? (message?.done ?? false) diff --git a/src/lib/components/chat/Messages/UserMessage.svelte b/src/lib/components/chat/Messages/UserMessage.svelte index 489bef01eb..ae99fafefc 100644 --- a/src/lib/components/chat/Messages/UserMessage.svelte +++ b/src/lib/components/chat/Messages/UserMessage.svelte @@ -38,6 +38,7 @@ export let isFirstMessage: boolean; export let readOnly: boolean; + export let editCodeBlock = true; export let topPadding = false; let showDeleteConfirm = false; @@ -332,7 +333,12 @@ : ' w-full'} {$settings.chatDirection === 'RTL' ? 'text-right' : ''}" > {#if message.content} - + {/if} diff --git a/src/lib/components/layout/Navbar/Menu.svelte b/src/lib/components/layout/Navbar/Menu.svelte index 21aee6a07b..a174fe3bbd 100644 --- a/src/lib/components/layout/Navbar/Menu.svelte +++ b/src/lib/components/layout/Navbar/Menu.svelte @@ -1,7 +1,7 @@ +{#if showFullMessages} + +{/if} + { if (e.detail === false) { diff --git a/src/lib/components/layout/Sidebar/ChatMenu.svelte b/src/lib/components/layout/Sidebar/ChatMenu.svelte index c3ce61a815..3342a73cab 100644 --- a/src/lib/components/layout/Sidebar/ChatMenu.svelte +++ b/src/lib/components/layout/Sidebar/ChatMenu.svelte @@ -81,155 +81,6 @@ saveAs(blob, `chat-${chat.chat.title}.txt`); }; - const downloadPdf = async () => { - const chat = await getChatById(localStorage.token, chatId); - - if ($settings?.stylizedPdfExport ?? true) { - const containerElement = document.getElementById('messages-container'); - - if (containerElement) { - try { - const isDarkMode = document.documentElement.classList.contains('dark'); - const virtualWidth = 800; // px, fixed width for cloned element - - // Clone and style - 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'; - clonedElement.style.height = 'auto'; - document.body.appendChild(clonedElement); - - // Wait for DOM update/layout - await new Promise((r) => setTimeout(r, 100)); - - // Render entire content once - const canvas = await html2canvas(clonedElement, { - backgroundColor: isDarkMode ? '#000' : '#fff', - useCORS: true, - scale: 2, // increase resolution - width: virtualWidth - }); - - document.body.removeChild(clonedElement); - - const pdf = new jsPDF('p', 'mm', 'a4'); - const pageWidthMM = 210; - const pageHeightMM = 297; - - // Convert page height in mm to px on canvas scale for cropping - // Get canvas DPI scale: - const pxPerMM = canvas.width / virtualWidth; // width in px / width in px? - // Since 1 page width is 210 mm, but canvas width is 800 px at scale 2 - // Assume 1 mm = px / (pageWidthMM scaled) - // Actually better: Calculate scale factor from px/mm: - // virtualWidth px corresponds directly to 210mm in PDF, so pxPerMM: - const pxPerPDFMM = canvas.width / pageWidthMM; // canvas px per PDF mm - - // Height in px for one page slice: - const pagePixelHeight = Math.floor(pxPerPDFMM * pageHeightMM); - - let offsetY = 0; - let page = 0; - - while (offsetY < canvas.height) { - // Height of slice - const sliceHeight = Math.min(pagePixelHeight, canvas.height - offsetY); - - // Create temp canvas for slice - const pageCanvas = document.createElement('canvas'); - pageCanvas.width = canvas.width; - pageCanvas.height = sliceHeight; - - const ctx = pageCanvas.getContext('2d'); - - // Draw the slice of original canvas onto pageCanvas - ctx.drawImage( - canvas, - 0, - offsetY, - canvas.width, - sliceHeight, - 0, - 0, - canvas.width, - sliceHeight - ); - - const imgData = pageCanvas.toDataURL('image/jpeg', 0.7); - - // Calculate image height in PDF units keeping aspect ratio - const imgHeightMM = (sliceHeight * pageWidthMM) / canvas.width; - - if (page > 0) pdf.addPage(); - - if (isDarkMode) { - pdf.setFillColor(0, 0, 0); - pdf.rect(0, 0, pageWidthMM, pageHeightMM, 'F'); // black bg - } - - pdf.addImage(imgData, 'JPEG', 0, 0, pageWidthMM, imgHeightMM); - - offsetY += sliceHeight; - page++; - } - - 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`); - } - }; - const downloadJSONExport = async () => { const chat = await getChatById(localStorage.token, chatId);