forked from open-webui/open-webui
		
	feat: chat pdf download
This commit is contained in:
		
							parent
							
								
									944efd2cd8
								
							
						
					
					
						commit
						000bea84ae
					
				
					 5 changed files with 104 additions and 97 deletions
				
			
		|  | @ -1,9 +1,6 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import { getContext, onMount } from 'svelte'; | 	import { getContext, onMount } from 'svelte'; | ||||||
| 
 | 
 | ||||||
| 	import fileSaver from 'file-saver'; |  | ||||||
| 	const { saveAs } = fileSaver; |  | ||||||
| 
 |  | ||||||
| 	import { toast } from 'svelte-sonner'; | 	import { toast } from 'svelte-sonner'; | ||||||
| 	import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats'; | 	import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats'; | ||||||
| 	import { chatId, modelfiles } from '$lib/stores'; | 	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; | 	export let show = false; | ||||||
| 
 | 
 | ||||||
| 	onMount(async () => { | 	onMount(async () => { | ||||||
|  | @ -159,19 +141,6 @@ | ||||||
| 								{/if} | 								{/if} | ||||||
| 							</button> | 							</button> | ||||||
| 						</div> | 						</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> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
|  |  | ||||||
|  | @ -2,21 +2,13 @@ | ||||||
| 	import { getContext } from 'svelte'; | 	import { getContext } from 'svelte'; | ||||||
| 	import { toast } from 'svelte-sonner'; | 	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 { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores'; | ||||||
| 
 | 
 | ||||||
| 	import { slide } from 'svelte/transition'; | 	import { slide } from 'svelte/transition'; | ||||||
| 	import ShareChatModal from '../chat/ShareChatModal.svelte'; | 	import ShareChatModal from '../chat/ShareChatModal.svelte'; | ||||||
| 	import TagInput from '../common/Tags/TagInput.svelte'; |  | ||||||
| 	import ModelSelector from '../chat/ModelSelector.svelte'; | 	import ModelSelector from '../chat/ModelSelector.svelte'; | ||||||
| 	import Tooltip from '../common/Tooltip.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 Menu from './Navbar/Menu.svelte'; | ||||||
| 	import TagChatModal from '../chat/TagChatModal.svelte'; |  | ||||||
| 
 | 
 | ||||||
| 	const i18n = getContext('i18n'); | 	const i18n = getContext('i18n'); | ||||||
| 
 | 
 | ||||||
|  | @ -24,6 +16,7 @@ | ||||||
| 	export let title: string = $WEBUI_NAME; | 	export let title: string = $WEBUI_NAME; | ||||||
| 	export let shareEnabled: boolean = false; | 	export let shareEnabled: boolean = false; | ||||||
| 
 | 
 | ||||||
|  | 	export let chat; | ||||||
| 	export let selectedModels; | 	export let selectedModels; | ||||||
| 
 | 
 | ||||||
| 	export let tags = []; | 	export let tags = []; | ||||||
|  | @ -33,63 +26,15 @@ | ||||||
| 	export let showModelSelector = true; | 	export let showModelSelector = true; | ||||||
| 
 | 
 | ||||||
| 	let showShareChatModal = false; | 	let showShareChatModal = false; | ||||||
| 	let showTagChatModal = false; | 	let showDownloadChatModal = false; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <ShareChatModal bind:show={showShareChatModal} /> | <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"> | <nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30"> | ||||||
| 	<div | 	<div | ||||||
| 		class=" flex {$settings?.fullScreenMode ?? null ? 'max-w-full' : 'max-w-3xl'}  | 		class=" flex {$settings?.fullScreenMode ?? null ? 'max-w-full' : 'max-w-3xl'}  | ||||||
| 		 w-full mx-auto px-3" | 		 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 items-center w-full max-w-full"> | ||||||
| 			<div class="flex-1 overflow-hidden max-w-full"> | 			<div class="flex-1 overflow-hidden max-w-full"> | ||||||
| 				{#if showModelSelector} | 				{#if showModelSelector} | ||||||
|  | @ -132,10 +77,14 @@ | ||||||
| 					</Tooltip> | 					</Tooltip> | ||||||
| 				{:else} | 				{:else} | ||||||
| 					<Menu | 					<Menu | ||||||
|  | 						{chat} | ||||||
| 						{shareEnabled} | 						{shareEnabled} | ||||||
| 						shareHandler={() => { | 						shareHandler={() => { | ||||||
| 							showShareChatModal = !showShareChatModal; | 							showShareChatModal = !showShareChatModal; | ||||||
| 						}} | 						}} | ||||||
|  | 						downloadHandler={() => { | ||||||
|  | 							showDownloadChatModal = !showDownloadChatModal; | ||||||
|  | 						}} | ||||||
| 						{tags} | 						{tags} | ||||||
| 						{deleteTag} | 						{deleteTag} | ||||||
| 						{addTag} | 						{addTag} | ||||||
|  |  | ||||||
|  | @ -1,23 +1,58 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import { DropdownMenu } from 'bits-ui'; | 	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 { flyAndScale } from '$lib/utils/transitions'; | ||||||
| 
 | 
 | ||||||
| 	import Dropdown from '$lib/components/common/Dropdown.svelte'; | 	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'; | 	import Tags from '$lib/components/common/Tags.svelte'; | ||||||
| 
 | 
 | ||||||
| 	export let shareEnabled: boolean = false; | 	export let shareEnabled: boolean = false; | ||||||
| 	export let shareHandler: Function; | 	export let shareHandler: Function; | ||||||
|  | 	export let downloadHandler: Function; | ||||||
|  | 
 | ||||||
| 	// export let tagHandler: Function; | 	// export let tagHandler: Function; | ||||||
| 
 | 
 | ||||||
|  | 	export let chat; | ||||||
| 	export let tags; | 	export let tags; | ||||||
| 	export let deleteTag: Function; | 	export let deleteTag: Function; | ||||||
| 	export let addTag: Function; | 	export let addTag: Function; | ||||||
| 
 | 
 | ||||||
| 	export let onClose: 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> | </script> | ||||||
| 
 | 
 | ||||||
| <Dropdown | <Dropdown | ||||||
|  | @ -31,14 +66,14 @@ | ||||||
| 
 | 
 | ||||||
| 	<div slot="content"> | 	<div slot="content"> | ||||||
| 		<DropdownMenu.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} | 			sideOffset={8} | ||||||
| 			side="bottom" | 			side="bottom" | ||||||
| 			align="end" | 			align="end" | ||||||
| 			transition={flyAndScale} | 			transition={flyAndScale} | ||||||
| 		> | 		> | ||||||
| 			<DropdownMenu.Item | 			<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 () => { | 				on:click={async () => { | ||||||
| 					await showSettings.set(!$showSettings); | 					await showSettings.set(!$showSettings); | ||||||
| 				}} | 				}} | ||||||
|  | @ -49,7 +84,7 @@ | ||||||
| 					viewBox="0 0 24 24" | 					viewBox="0 0 24 24" | ||||||
| 					stroke-width="1.5" | 					stroke-width="1.5" | ||||||
| 					stroke="currentColor" | 					stroke="currentColor" | ||||||
| 					class="w-5 h-5" | 					class="size-4" | ||||||
| 				> | 				> | ||||||
| 					<path | 					<path | ||||||
| 						stroke-linecap="round" | 						stroke-linecap="round" | ||||||
|  | @ -67,7 +102,7 @@ | ||||||
| 
 | 
 | ||||||
| 			{#if shareEnabled} | 			{#if shareEnabled} | ||||||
| 				<DropdownMenu.Item | 				<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={() => { | 					on:click={() => { | ||||||
| 						shareHandler(); | 						shareHandler(); | ||||||
| 					}} | 					}} | ||||||
|  | @ -87,7 +122,59 @@ | ||||||
| 					<div class="flex items-center">Share</div> | 					<div class="flex items-center">Share</div> | ||||||
| 				</DropdownMenu.Item> | 				</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"> | 				<div class="flex p-1"> | ||||||
| 					<Tags {tags} {deleteTag} {addTag} /> | 					<Tags {tags} {deleteTag} {addTag} /> | ||||||
|  |  | ||||||
|  | @ -847,6 +847,7 @@ | ||||||
| 		bind:selectedModels | 		bind:selectedModels | ||||||
| 		bind:showModelSelector | 		bind:showModelSelector | ||||||
| 		shareEnabled={messages.length > 0} | 		shareEnabled={messages.length > 0} | ||||||
|  | 		{chat} | ||||||
| 		{initNewChat} | 		{initNewChat} | ||||||
| 		{tags} | 		{tags} | ||||||
| 		{addTag} | 		{addTag} | ||||||
|  |  | ||||||
|  | @ -865,6 +865,7 @@ | ||||||
| 	<div class="min-h-screen max-h-screen w-full flex flex-col"> | 	<div class="min-h-screen max-h-screen w-full flex flex-col"> | ||||||
| 		<Navbar | 		<Navbar | ||||||
| 			{title} | 			{title} | ||||||
|  | 			{chat} | ||||||
| 			bind:selectedModels | 			bind:selectedModels | ||||||
| 			bind:showModelSelector | 			bind:showModelSelector | ||||||
| 			shareEnabled={messages.length > 0} | 			shareEnabled={messages.length > 0} | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek