forked from open-webui/open-webui
		
	feat: add frontend support for locally sharing chats
This commit is contained in:
		
							parent
							
								
									bfbfdae1c5
								
							
						
					
					
						commit
						196f91d68c
					
				
					 9 changed files with 433 additions and 106 deletions
				
			
		|  | @ -218,6 +218,102 @@ export const getChatById = async (token: string, id: string) => { | |||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const getChatByShareId = async (token: string, share_id: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/share/${share_id}`, { | ||||
| 		method: 'GET', | ||||
| 		headers: { | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			...(token && { authorization: `Bearer ${token}` }) | ||||
| 		} | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
| 		}) | ||||
| 		.then((json) => { | ||||
| 			return json; | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			error = err; | ||||
| 
 | ||||
| 			console.log(err); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const shareChatById = async (token: string, id: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			...(token && { authorization: `Bearer ${token}` }) | ||||
| 		} | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
| 		}) | ||||
| 		.then((json) => { | ||||
| 			return json; | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			error = err; | ||||
| 
 | ||||
| 			console.log(err); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const deleteSharedChatById = async (token: string, id: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, { | ||||
| 		method: 'DELETE', | ||||
| 		headers: { | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			...(token && { authorization: `Bearer ${token}` }) | ||||
| 		} | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
| 		}) | ||||
| 		.then((json) => { | ||||
| 			return json; | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			error = err; | ||||
| 
 | ||||
| 			console.log(err); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const updateChatById = async (token: string, id: string, chat: object) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ | |||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let chatId = ''; | ||||
| 	export let readOnly = false; | ||||
| 	export let sendPrompt: Function; | ||||
| 	export let continueGeneration: Function; | ||||
| 	export let regenerateResponse: Function; | ||||
|  | @ -317,6 +318,7 @@ | |||
| 							<UserMessage | ||||
| 								on:delete={() => messageDeleteHandler(message.id)} | ||||
| 								user={$user} | ||||
| 								{readOnly} | ||||
| 								{message} | ||||
| 								isFirstMessage={messageIdx === 0} | ||||
| 								siblings={message.parentId !== null | ||||
|  | @ -335,6 +337,7 @@ | |||
| 								modelfiles={selectedModelfiles} | ||||
| 								siblings={history.messages[message.parentId]?.childrenIds ?? []} | ||||
| 								isLastMessage={messageIdx + 1 === messages.length} | ||||
| 								{readOnly} | ||||
| 								{confirmEditResponseMessage} | ||||
| 								{showPreviousMessage} | ||||
| 								{showNextMessage} | ||||
|  |  | |||
|  | @ -33,6 +33,8 @@ | |||
| 
 | ||||
| 	export let isLastMessage = true; | ||||
| 
 | ||||
| 	export let readOnly = false; | ||||
| 
 | ||||
| 	export let confirmEditResponseMessage: Function; | ||||
| 	export let showPreviousMessage: Function; | ||||
| 	export let showNextMessage: Function; | ||||
|  | @ -469,6 +471,7 @@ | |||
| 											</div> | ||||
| 										{/if} | ||||
| 
 | ||||
| 										{#if !readOnly} | ||||
| 											<Tooltip content="Edit" placement="bottom"> | ||||
| 												<button | ||||
| 													class="{isLastMessage | ||||
|  | @ -494,6 +497,7 @@ | |||
| 													</svg> | ||||
| 												</button> | ||||
| 											</Tooltip> | ||||
| 										{/if} | ||||
| 
 | ||||
| 										<Tooltip content="Copy" placement="bottom"> | ||||
| 											<button | ||||
|  | @ -521,6 +525,7 @@ | |||
| 											</button> | ||||
| 										</Tooltip> | ||||
| 
 | ||||
| 										{#if !readOnly} | ||||
| 											<Tooltip content="Good Response" placement="bottom"> | ||||
| 												<button | ||||
| 													class="{isLastMessage | ||||
|  | @ -574,6 +579,7 @@ | |||
| 													> | ||||
| 												</button> | ||||
| 											</Tooltip> | ||||
| 										{/if} | ||||
| 
 | ||||
| 										<Tooltip content="Read Aloud" placement="bottom"> | ||||
| 											<button | ||||
|  | @ -656,7 +662,7 @@ | |||
| 											</button> | ||||
| 										</Tooltip> | ||||
| 
 | ||||
| 										{#if $config.images} | ||||
| 										{#if $config.images && !readOnly} | ||||
| 											<Tooltip content="Generate Image" placement="bottom"> | ||||
| 												<button | ||||
| 													class="{isLastMessage | ||||
|  | @ -752,7 +758,7 @@ | |||
| 											</Tooltip> | ||||
| 										{/if} | ||||
| 
 | ||||
| 										{#if isLastMessage} | ||||
| 										{#if isLastMessage && !readOnly} | ||||
| 											<Tooltip content="Continue Response" placement="bottom"> | ||||
| 												<button | ||||
| 													type="button" | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ | |||
| 	export let message; | ||||
| 	export let siblings; | ||||
| 	export let isFirstMessage: boolean; | ||||
| 	export let readOnly: boolean; | ||||
| 
 | ||||
| 	export let confirmEditMessage: Function; | ||||
| 	export let showPreviousMessage: Function; | ||||
|  | @ -250,6 +251,7 @@ | |||
| 							</div> | ||||
| 						{/if} | ||||
| 
 | ||||
| 						{#if !readOnly} | ||||
| 							<Tooltip content="Edit" placement="bottom"> | ||||
| 								<button | ||||
| 									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button" | ||||
|  | @ -273,6 +275,7 @@ | |||
| 									</svg> | ||||
| 								</button> | ||||
| 							</Tooltip> | ||||
| 						{/if} | ||||
| 
 | ||||
| 						<Tooltip content="Copy" placement="bottom"> | ||||
| 							<button | ||||
|  | @ -298,7 +301,7 @@ | |||
| 							</button> | ||||
| 						</Tooltip> | ||||
| 
 | ||||
| 						{#if !isFirstMessage} | ||||
| 						{#if !isFirstMessage && !readOnly} | ||||
| 							<Tooltip content="Delete" placement="bottom"> | ||||
| 								<button | ||||
| 									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition" | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ | |||
| 
 | ||||
| 	export let downloadChat: Function; | ||||
| 	export let shareChat: Function; | ||||
| 	export let shareLocalChat: Function; | ||||
| 
 | ||||
| 	export let show = false; | ||||
| </script> | ||||
|  | @ -23,6 +24,17 @@ | |||
| 			{$i18n.t('Share to OpenWebUI Community')} | ||||
| 		</button> | ||||
| 
 | ||||
| 		<button | ||||
| 			class=" self-center px-8 py-1.5 w-full rounded-full text-sm font-medium bg-blue-600 hover:bg-blue-500 text-white mt-1.5" | ||||
| 			type="button" | ||||
| 			on:click={() => { | ||||
| 				shareLocalChat(); | ||||
| 				show = false; | ||||
| 			}} | ||||
| 		> | ||||
| 			{$i18n.t('Create local share link')} | ||||
| 		</button> | ||||
| 
 | ||||
| 		<div class="flex justify-center space-x-1 mt-1.5"> | ||||
| 			<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
| 	const { saveAs } = fileSaver; | ||||
| 
 | ||||
| 	import { Separator } from 'bits-ui'; | ||||
| 	import { getChatById } from '$lib/apis/chats'; | ||||
| 	import { getChatById, shareChatById } from '$lib/apis/chats'; | ||||
| 	import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores'; | ||||
| 
 | ||||
| 	import { slide } from 'svelte/transition'; | ||||
|  | @ -19,6 +19,7 @@ | |||
| 	import ChevronUpDown from '../icons/ChevronUpDown.svelte'; | ||||
| 	import Menu from './Navbar/Menu.svelte'; | ||||
| 	import TagChatModal from '../chat/TagChatModal.svelte'; | ||||
| 	import { copyToClipboard } from '$lib/utils'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
|  | @ -32,7 +33,7 @@ | |||
| 	export let addTag: Function; | ||||
| 	export let deleteTag: Function; | ||||
| 
 | ||||
| 	export let showModelSelector = false; | ||||
| 	export let showModelSelector = true; | ||||
| 
 | ||||
| 	let showShareChatModal = false; | ||||
| 	let showTagChatModal = false; | ||||
|  | @ -64,6 +65,23 @@ | |||
| 		); | ||||
| 	}; | ||||
| 
 | ||||
| 	const shareLocalChat = async () => { | ||||
| 		const chat = await getChatById(localStorage.token, $chatId); | ||||
| 		console.log('shareLocal', chat); | ||||
| 		if (chat.share_id) { | ||||
| 			const shareUrl = `${window.location.origin}/s/${chat.share_id}`; | ||||
| 			toast.info( | ||||
| 				$i18n.t('Chat is already shared at {{shareUrl}}, copied to clipboard', { shareUrl }) | ||||
| 			); | ||||
| 			copyToClipboard(shareUrl); | ||||
| 		} else { | ||||
| 			const sharedChat = await shareChatById(localStorage.token, $chatId); | ||||
| 			const shareUrl = `${window.location.origin}/s/${sharedChat.id}`; | ||||
| 			toast.info($i18n.t('Chat is now shared at {{shareUrl}}, copied to clipboard', { shareUrl })); | ||||
| 			copyToClipboard(shareUrl); | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	const downloadChat = async () => { | ||||
| 		const chat = (await getChatById(localStorage.token, $chatId)).chat; | ||||
| 		console.log('download', chat); | ||||
|  | @ -80,7 +98,7 @@ | |||
| 	}; | ||||
| </script> | ||||
| 
 | ||||
| <ShareChatModal bind:show={showShareChatModal} {downloadChat} {shareChat} /> | ||||
| <ShareChatModal bind:show={showShareChatModal} {downloadChat} {shareChat} {shareLocalChat} /> | ||||
| <!-- <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 | ||||
|  | @ -135,8 +153,10 @@ | |||
| 		</div> --> | ||||
| 
 | ||||
| 		<div class="flex items-center w-full max-w-full"> | ||||
| 			<div class="w-full flex-1 overflow-hidden max-w-full"> | ||||
| 			<div class="flex-1 overflow-hidden max-w-full"> | ||||
| 				{#if showModelSelector} | ||||
| 					<ModelSelector bind:selectedModels /> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class="self-start flex flex-none items-center"> | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ | |||
| 	let messagesContainerElement: HTMLDivElement; | ||||
| 	let currentRequestId = null; | ||||
| 
 | ||||
| 	let showModelSelector = false; | ||||
| 	let showModelSelector = true; | ||||
| 	let selectedModels = ['']; | ||||
| 
 | ||||
| 	let selectedModelfile = null; | ||||
|  |  | |||
|  | @ -56,7 +56,7 @@ | |||
| 	let currentRequestId = null; | ||||
| 
 | ||||
| 	// let chatId = $page.params.id; | ||||
| 	let showModelSelector = false; | ||||
| 	let showModelSelector = true; | ||||
| 	let selectedModels = ['']; | ||||
| 	let selectedModelfile = null; | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										187
									
								
								src/routes/(app)/s/[id]/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								src/routes/(app)/s/[id]/+page.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,187 @@ | |||
| <script lang="ts"> | ||||
| 	import { onMount, tick, getContext } from 'svelte'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { page } from '$app/stores'; | ||||
| 
 | ||||
| 	import { modelfiles, settings, chatId, WEBUI_NAME } from '$lib/stores'; | ||||
| 	import { convertMessagesToHistory } from '$lib/utils'; | ||||
| 
 | ||||
| 	import { getChatByShareId } from '$lib/apis/chats'; | ||||
| 
 | ||||
| 	import Messages from '$lib/components/chat/Messages.svelte'; | ||||
| 	import Navbar from '$lib/components/layout/Navbar.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	let loaded = false; | ||||
| 
 | ||||
| 	let autoScroll = true; | ||||
| 	let processing = ''; | ||||
| 	let messagesContainerElement: HTMLDivElement; | ||||
| 
 | ||||
| 	// let chatId = $page.params.id; | ||||
| 	let showModelSelector = false; | ||||
| 	let selectedModels = ['']; | ||||
| 
 | ||||
| 	let selectedModelfiles = {}; | ||||
| 	$: selectedModelfiles = selectedModels.reduce((a, tagName, i, arr) => { | ||||
| 		const modelfile = | ||||
| 			$modelfiles.filter((modelfile) => modelfile.tagName === tagName)?.at(0) ?? undefined; | ||||
| 
 | ||||
| 		return { | ||||
| 			...a, | ||||
| 			...(modelfile && { [tagName]: modelfile }) | ||||
| 		}; | ||||
| 	}, {}); | ||||
| 
 | ||||
| 	let chat = null; | ||||
| 
 | ||||
| 	let title = ''; | ||||
| 	let files = []; | ||||
| 
 | ||||
| 	let messages = []; | ||||
| 	let history = { | ||||
| 		messages: {}, | ||||
| 		currentId: null | ||||
| 	}; | ||||
| 
 | ||||
| 	$: if (history.currentId !== null) { | ||||
| 		let _messages = []; | ||||
| 
 | ||||
| 		let currentMessage = history.messages[history.currentId]; | ||||
| 		while (currentMessage !== null) { | ||||
| 			_messages.unshift({ ...currentMessage }); | ||||
| 			currentMessage = | ||||
| 				currentMessage.parentId !== null ? history.messages[currentMessage.parentId] : null; | ||||
| 		} | ||||
| 		messages = _messages; | ||||
| 	} else { | ||||
| 		messages = []; | ||||
| 	} | ||||
| 
 | ||||
| 	$: if ($page.params.id) { | ||||
| 		(async () => { | ||||
| 			if (await loadSharedChat()) { | ||||
| 				await tick(); | ||||
| 				loaded = true; | ||||
| 
 | ||||
| 				window.setTimeout(() => scrollToBottom(), 0); | ||||
| 				const chatInput = document.getElementById('chat-textarea'); | ||||
| 				chatInput?.focus(); | ||||
| 			} else { | ||||
| 				await goto('/'); | ||||
| 			} | ||||
| 		})(); | ||||
| 	} | ||||
| 
 | ||||
| 	////////////////////////// | ||||
| 	// Web functions | ||||
| 	////////////////////////// | ||||
| 
 | ||||
| 	const loadSharedChat = async () => { | ||||
| 		await chatId.set($page.params.id); | ||||
| 		chat = await getChatByShareId(localStorage.token, $chatId).catch(async (error) => { | ||||
| 			await goto('/'); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 		if (chat) { | ||||
| 			const chatContent = chat.chat; | ||||
| 
 | ||||
| 			if (chatContent) { | ||||
| 				console.log(chatContent); | ||||
| 
 | ||||
| 				selectedModels = | ||||
| 					(chatContent?.models ?? undefined) !== undefined | ||||
| 						? chatContent.models | ||||
| 						: [chatContent.models ?? '']; | ||||
| 				history = | ||||
| 					(chatContent?.history ?? undefined) !== undefined | ||||
| 						? chatContent.history | ||||
| 						: convertMessagesToHistory(chatContent.messages); | ||||
| 				title = chatContent.title; | ||||
| 
 | ||||
| 				let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||
| 				await settings.set({ | ||||
| 					..._settings, | ||||
| 					system: chatContent.system ?? _settings.system, | ||||
| 					options: chatContent.options ?? _settings.options | ||||
| 				}); | ||||
| 				autoScroll = true; | ||||
| 				await tick(); | ||||
| 
 | ||||
| 				if (messages.length > 0) { | ||||
| 					history.messages[messages.at(-1).id].done = true; | ||||
| 				} | ||||
| 				await tick(); | ||||
| 
 | ||||
| 				return true; | ||||
| 			} else { | ||||
| 				return null; | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	const scrollToBottom = () => { | ||||
| 		if (messagesContainerElement) { | ||||
| 			messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight; | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	onMount(async () => { | ||||
| 		if (!($settings.saveChatHistory ?? true)) { | ||||
| 			await goto('/'); | ||||
| 		} | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <svelte:head> | ||||
| 	<title> | ||||
| 		{title | ||||
| 			? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}` | ||||
| 			: `${$WEBUI_NAME}`} | ||||
| 	</title> | ||||
| </svelte:head> | ||||
| 
 | ||||
| {#if loaded} | ||||
| 	<div class="min-h-screen max-h-screen w-full flex flex-col"> | ||||
| 		<Navbar | ||||
| 			{title} | ||||
| 			bind:selectedModels | ||||
| 			bind:showModelSelector | ||||
| 			shareEnabled={false} | ||||
| 			initNewChat={async () => { | ||||
| 				goto('/'); | ||||
| 			}} | ||||
| 		/> | ||||
| 		<div class="flex flex-col flex-auto"> | ||||
| 			<div | ||||
| 				class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0" | ||||
| 				id="messages-container" | ||||
| 				bind:this={messagesContainerElement} | ||||
| 				on:scroll={(e) => { | ||||
| 					autoScroll = | ||||
| 						messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <= | ||||
| 						messagesContainerElement.clientHeight + 5; | ||||
| 				}} | ||||
| 			> | ||||
| 				<div class=" h-full w-full flex flex-col py-4"> | ||||
| 					<Messages | ||||
| 						chatId={$chatId} | ||||
| 						readOnly={true} | ||||
| 						{selectedModels} | ||||
| 						{selectedModelfiles} | ||||
| 						{processing} | ||||
| 						bind:history | ||||
| 						bind:messages | ||||
| 						bind:autoScroll | ||||
| 						bottomPadding={files.length > 0} | ||||
| 						sendPrompt={() => {}} | ||||
| 						continueGeneration={() => {}} | ||||
| 						regenerateResponse={() => {}} | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| {/if} | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Jun Siang Cheah
						Jun Siang Cheah