Browse Source

feat: chat pdf download

Timothy J. Baek 1 năm trước cách đây
mục cha
commit
000bea84ae

+ 0 - 31
src/lib/components/chat/ShareChatModal.svelte

@@ -1,9 +1,6 @@
 <script lang="ts">
 	import { getContext, onMount } from 'svelte';
 
-	import fileSaver from 'file-saver';
-	const { saveAs } = fileSaver;
-
 	import { toast } from 'svelte-sonner';
 	import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats';
 	import { chatId, modelfiles } from '$lib/stores';
@@ -55,21 +52,6 @@
 		);
 	};
 
-	const downloadChat = async () => {
-		const _chat = chat.chat;
-		console.log('download', chat);
-
-		const chatText = _chat.messages.reduce((a, message, i, arr) => {
-			return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
-		}, '');
-
-		let blob = new Blob([chatText], {
-			type: 'text/plain'
-		});
-
-		saveAs(blob, `chat-${_chat.title}.txt`);
-	};
-
 	export let show = false;
 
 	onMount(async () => {
@@ -159,19 +141,6 @@
 								{/if}
 							</button>
 						</div>
-						<div class="flex gap-1 mt-1.5">
-							<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div>
-							<button
-								class=" text-right rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline"
-								type="button"
-								on:click={() => {
-									downloadChat();
-									show = false;
-								}}
-							>
-								{$i18n.t('Download as a File')}
-							</button>
-						</div>
 					</div>
 				</div>
 			</div>

+ 6 - 57
src/lib/components/layout/Navbar.svelte

@@ -2,21 +2,13 @@
 	import { getContext } from 'svelte';
 	import { toast } from 'svelte-sonner';
 
-	import { Separator } from 'bits-ui';
-	import { getChatById, shareChatById } from '$lib/apis/chats';
 	import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores';
 
 	import { slide } from 'svelte/transition';
 	import ShareChatModal from '../chat/ShareChatModal.svelte';
-	import TagInput from '../common/Tags/TagInput.svelte';
 	import ModelSelector from '../chat/ModelSelector.svelte';
 	import Tooltip from '../common/Tooltip.svelte';
-
-	import EllipsisVertical from '../icons/EllipsisVertical.svelte';
-	import ChevronDown from '../icons/ChevronDown.svelte';
-	import ChevronUpDown from '../icons/ChevronUpDown.svelte';
 	import Menu from './Navbar/Menu.svelte';
-	import TagChatModal from '../chat/TagChatModal.svelte';
 
 	const i18n = getContext('i18n');
 
@@ -24,6 +16,7 @@
 	export let title: string = $WEBUI_NAME;
 	export let shareEnabled: boolean = false;
 
+	export let chat;
 	export let selectedModels;
 
 	export let tags = [];
@@ -33,63 +26,15 @@
 	export let showModelSelector = true;
 
 	let showShareChatModal = false;
-	let showTagChatModal = false;
+	let showDownloadChatModal = false;
 </script>
 
 <ShareChatModal bind:show={showShareChatModal} />
-<!-- <TagChatModal bind:show={showTagChatModal} {tags} {deleteTag} {addTag} /> -->
 <nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
 	<div
 		class=" flex {$settings?.fullScreenMode ?? null ? 'max-w-full' : 'max-w-3xl'} 
 		 w-full mx-auto px-3"
 	>
-		<!-- {#if shareEnabled}
-			<div class="flex items-center w-full max-w-full">
-				<div class=" flex-1 self-center font-medium line-clamp-1">
-					<div>
-						{title != '' ? title : $WEBUI_NAME}
-					</div>
-				</div>
-				<div class="pl-2 self-center flex items-center">
-					<div class=" mr-1">
-						<Tags {tags} {deleteTag} {addTag} />
-					</div>
-
-					<Tooltip content="Share">
-						<button
-							class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
-							on:click={async () => {
-								showShareChatModal = !showShareChatModal;
-
-								// console.log(showShareChatModal);
-							}}
-						>
-							<div class=" m-auto self-center">
-								<svg
-									xmlns="http://www.w3.org/2000/svg"
-									viewBox="0 0 24 24"
-									fill="currentColor"
-									class="w-4 h-4"
-								>
-									<path
-										fill-rule="evenodd"
-										d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
-										clip-rule="evenodd"
-									/>
-								</svg>
-							</div>
-						</button>
-					</Tooltip>
-				</div>
-			</div>
-		{/if} -->
-
-		<!-- <div class=" flex-1 self-center font-medium line-clamp-1">
-			<div>
-				{title != '' ? title : $WEBUI_NAME}
-			</div>
-		</div> -->
-
 		<div class="flex items-center w-full max-w-full">
 			<div class="flex-1 overflow-hidden max-w-full">
 				{#if showModelSelector}
@@ -132,10 +77,14 @@
 					</Tooltip>
 				{:else}
 					<Menu
+						{chat}
 						{shareEnabled}
 						shareHandler={() => {
 							showShareChatModal = !showShareChatModal;
 						}}
+						downloadHandler={() => {
+							showDownloadChatModal = !showDownloadChatModal;
+						}}
 						{tags}
 						{deleteTag}
 						{addTag}

+ 96 - 9
src/lib/components/layout/Navbar/Menu.svelte

@@ -1,23 +1,58 @@
 <script lang="ts">
 	import { DropdownMenu } from 'bits-ui';
+
+	import fileSaver from 'file-saver';
+	const { saveAs } = fileSaver;
+
+	import { jsPDF } from 'jspdf';
+
+	import { showSettings } from '$lib/stores';
 	import { flyAndScale } from '$lib/utils/transitions';
 
 	import Dropdown from '$lib/components/common/Dropdown.svelte';
-	import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
-	import Pencil from '$lib/components/icons/Pencil.svelte';
-	import Tooltip from '$lib/components/common/Tooltip.svelte';
-	import { showSettings } from '$lib/stores';
 	import Tags from '$lib/components/common/Tags.svelte';
 
 	export let shareEnabled: boolean = false;
 	export let shareHandler: Function;
+	export let downloadHandler: Function;
+
 	// export let tagHandler: Function;
 
+	export let chat;
 	export let tags;
 	export let deleteTag: Function;
 	export let addTag: Function;
 
 	export let onClose: Function = () => {};
+
+	const downloadChatAsTxt = async () => {
+		const _chat = chat.chat;
+		console.log('download', chat);
+
+		const chatText = _chat.messages.reduce((a, message, i, arr) => {
+			return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
+		}, '');
+
+		let blob = new Blob([chatText], {
+			type: 'text/plain'
+		});
+
+		saveAs(blob, `chat-${_chat.title}.txt`);
+	};
+
+	const downloadChatAsPdf = async () => {
+		const _chat = chat.chat;
+		console.log('download', chat);
+
+		const doc = new jsPDF();
+
+		const chatText = _chat.messages.reduce((a, message, i, arr) => {
+			return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
+		}, '');
+
+		doc.text(chatText, 10, 10);
+		doc.save(`chat-${_chat.title}.pdf`);
+	};
 </script>
 
 <Dropdown
@@ -31,14 +66,14 @@
 
 	<div slot="content">
 		<DropdownMenu.Content
-			class="w-full max-w-[150px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
+			class="w-full max-w-[200px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow-lg"
 			sideOffset={8}
 			side="bottom"
 			align="end"
 			transition={flyAndScale}
 		>
 			<DropdownMenu.Item
-				class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer"
+				class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
 				on:click={async () => {
 					await showSettings.set(!$showSettings);
 				}}
@@ -49,7 +84,7 @@
 					viewBox="0 0 24 24"
 					stroke-width="1.5"
 					stroke="currentColor"
-					class="w-5 h-5"
+					class="size-4"
 				>
 					<path
 						stroke-linecap="round"
@@ -67,7 +102,7 @@
 
 			{#if shareEnabled}
 				<DropdownMenu.Item
-					class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer"
+					class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
 					on:click={() => {
 						shareHandler();
 					}}
@@ -87,7 +122,59 @@
 					<div class="flex items-center">Share</div>
 				</DropdownMenu.Item>
 
-				<hr class="border-gray-100 dark:border-gray-800 my-1" />
+				<!-- <DropdownMenu.Item
+					class="flex gap-2 items-center px-3 py-2 text-sm  font-medium cursor-pointer"
+					on:click={() => {
+						downloadHandler();
+					}}
+				/> -->
+				<DropdownMenu.Sub>
+					<DropdownMenu.SubTrigger
+						class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
+					>
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							fill="none"
+							viewBox="0 0 24 24"
+							stroke-width="1.5"
+							stroke="currentColor"
+							class="size-4"
+						>
+							<path
+								stroke-linecap="round"
+								stroke-linejoin="round"
+								d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
+							/>
+						</svg>
+
+						<div class="flex items-center">Download</div>
+					</DropdownMenu.SubTrigger>
+					<DropdownMenu.SubContent
+						class="w-full rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow-lg"
+						transition={flyAndScale}
+						sideOffset={8}
+					>
+						<DropdownMenu.Item
+							class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
+							on:click={() => {
+								downloadChatAsTxt();
+							}}
+						>
+							<div class="flex items-center line-clamp-1">Plain text (.txt)</div>
+						</DropdownMenu.Item>
+
+						<DropdownMenu.Item
+							class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md"
+							on:click={() => {
+								downloadChatAsPdf();
+							}}
+						>
+							<div class="flex items-center line-clamp-1">PDF document (.pdf)</div>
+						</DropdownMenu.Item>
+					</DropdownMenu.SubContent>
+				</DropdownMenu.Sub>
+
+				<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
 
 				<div class="flex p-1">
 					<Tags {tags} {deleteTag} {addTag} />

+ 1 - 0
src/routes/(app)/+page.svelte

@@ -847,6 +847,7 @@
 		bind:selectedModels
 		bind:showModelSelector
 		shareEnabled={messages.length > 0}
+		{chat}
 		{initNewChat}
 		{tags}
 		{addTag}

+ 1 - 0
src/routes/(app)/c/[id]/+page.svelte

@@ -865,6 +865,7 @@
 	<div class="min-h-screen max-h-screen w-full flex flex-col">
 		<Navbar
 			{title}
+			{chat}
 			bind:selectedModels
 			bind:showModelSelector
 			shareEnabled={messages.length > 0}