فهرست منبع

refac: pdf export

Timothy Jaeryang Baek 1 ماه پیش
والد
کامیت
d3a952877a

+ 4 - 2
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}

+ 28 - 11
src/lib/components/chat/Messages/CodeBlock.svelte

@@ -1,4 +1,6 @@
 <script lang="ts">
+	import hljs from 'highlight.js';
+
 	import mermaid from 'mermaid';
 
 	import { v4 as uuidv4 } from 'uuid';
@@ -22,6 +24,7 @@
 	const i18n = getContext('i18n');
 
 	export let id = '';
+	export let edit = true;
 
 	export let onSave = (e) => {};
 	export let onUpdate = (e) => {};
@@ -512,17 +515,31 @@
 				<div class=" pt-7 bg-gray-50 dark:bg-gray-850"></div>
 
 				{#if !collapsed}
-					<CodeEditor
-						value={code}
-						{id}
-						{lang}
-						onSave={() => {
-							saveCode();
-						}}
-						onChange={(value) => {
-							_code = value;
-						}}
-					/>
+					{#if edit}
+						<CodeEditor
+							value={code}
+							{id}
+							{lang}
+							onSave={() => {
+								saveCode();
+							}}
+							onChange={(value) => {
+								_code = value;
+							}}
+						/>
+					{:else}
+						<pre
+							class=" hljs p-4 px-5 overflow-x-auto"
+							style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
+								stdout ||
+								stderr ||
+								result) &&
+								'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
+								class="language-{lang} rounded-t-none whitespace-pre text-sm"
+								>{@html hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value ||
+									code}</code
+							></pre>
+					{/if}
 				{:else}
 					<div
 						class="bg-gray-50 dark:bg-black dark:text-white rounded-b-lg! pt-2 pb-2 px-4 flex flex-col gap-2 text-xs"

+ 3 - 0
src/lib/components/chat/Messages/ContentRenderer.svelte

@@ -30,6 +30,8 @@
 	export let save = false;
 	export let preview = false;
 	export let floatingButtons = true;
+
+	export let editCodeBlock = true;
 	export let topPadding = false;
 
 	export let onSave = (e) => {};
@@ -138,6 +140,7 @@
 		{save}
 		{preview}
 		{done}
+		{editCodeBlock}
 		{topPadding}
 		sourceIds={(sources ?? []).reduce((acc, source) => {
 			let ids = [];

+ 3 - 0
src/lib/components/chat/Messages/Markdown.svelte

@@ -14,6 +14,8 @@
 	export let model = null;
 	export let save = false;
 	export let preview = false;
+
+	export let editCodeBlock = true;
 	export let topPadding = false;
 
 	export let sourceIds = [];
@@ -52,6 +54,7 @@
 		{done}
 		{save}
 		{preview}
+		{editCodeBlock}
 		{topPadding}
 		{onTaskClick}
 		{onSourceClick}

+ 3 - 0
src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte

@@ -32,6 +32,8 @@
 
 	export let save = false;
 	export let preview = false;
+
+	export let editCodeBlock = true;
 	export let topPadding = false;
 
 	export let onSave: Function = () => {};
@@ -106,6 +108,7 @@
 				{attributes}
 				{save}
 				{preview}
+				edit={editCodeBlock}
 				stickyButtonsClassName={topPadding ? 'top-8' : 'top-0'}
 				onSave={(value) => {
 					onSave({

+ 4 - 0
src/lib/components/chat/Messages/Message.svelte

@@ -41,6 +41,7 @@
 	export let addMessages;
 	export let triggerScroll;
 	export let readOnly = false;
+	export let editCodeBlock = true;
 	export let topPadding = false;
 </script>
 
@@ -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}

+ 2 - 0
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}

+ 2 - 0
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)

+ 7 - 1
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}
-								<Markdown id={`${chatId}-${message.id}`} content={message.content} {topPadding} />
+								<Markdown
+									id={`${chatId}-${message.id}`}
+									content={message.content}
+									{editCodeBlock}
+									{topPadding}
+								/>
 							{/if}
 						</div>
 					</div>

+ 30 - 2
src/lib/components/layout/Navbar/Menu.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
 	import { DropdownMenu } from 'bits-ui';
-	import { getContext } from 'svelte';
+	import { getContext, tick } from 'svelte';
 
 	import fileSaver from 'file-saver';
 	const { saveAs } = fileSaver;
@@ -35,6 +35,7 @@
 	import Folder from '$lib/components/icons/Folder.svelte';
 	import Share from '$lib/components/icons/Share.svelte';
 	import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
+	import Messages from '$lib/components/chat/Messages.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -50,6 +51,8 @@
 	export let chat;
 	export let onClose: Function = () => {};
 
+	let showFullMessages = false;
+
 	const getChatAsText = async () => {
 		const history = chat.chat.history;
 		const messages = createMessagesList(history, history.currentId);
@@ -72,8 +75,10 @@
 
 	const downloadPdf = async () => {
 		if ($settings?.stylizedPdfExport ?? true) {
-			const containerElement = document.getElementById('messages-container');
+			showFullMessages = true;
+			await tick();
 
+			const containerElement = document.getElementById('full-messages-container');
 			if (containerElement) {
 				try {
 					const isDarkMode = document.documentElement.classList.contains('dark');
@@ -164,6 +169,8 @@
 					}
 
 					pdf.save(`chat-${chat.chat.title}.pdf`);
+
+					showFullMessages = false;
 				} catch (error) {
 					console.error('Error generating PDF', error);
 				}
@@ -235,6 +242,27 @@
 	};
 </script>
 
+{#if showFullMessages}
+	<div class="hidden w-full h-full flex-col">
+		<div id="full-messages-container">
+			<Messages
+				className="h-full flex pt-4 pb-8 w-full"
+				chatId={`chat-preview-${chat?.id ?? ''}`}
+				user={$user}
+				readOnly={true}
+				history={chat.chat.history}
+				messages={chat.chat.messages}
+				autoScroll={true}
+				sendMessage={() => {}}
+				continueResponse={() => {}}
+				regenerateResponse={() => {}}
+				messagesCount={null}
+				editCodeBlock={false}
+			/>
+		</div>
+	</div>
+{/if}
+
 <Dropdown
 	on:change={(e) => {
 		if (e.detail === false) {

+ 0 - 149
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);