diff --git a/src/lib/components/chat/AssistantIntroduction.svelte b/src/lib/components/chat/AssistantIntroduction.svelte index 24f57ef62e6..2f079f30ca0 100644 --- a/src/lib/components/chat/AssistantIntroduction.svelte +++ b/src/lib/components/chat/AssistantIntroduction.svelte @@ -124,7 +124,10 @@ -
+
diff --git a/src/lib/components/chat/ChatWindow.svelte b/src/lib/components/chat/ChatWindow.svelte index 227d1adb1cb..284c50476a2 100644 --- a/src/lib/components/chat/ChatWindow.svelte +++ b/src/lib/components/chat/ChatWindow.svelte @@ -226,7 +226,7 @@ on:drop|preventDefault={() => (onDrag = false)} /> -
+
{#if loginModalOpen} { @@ -238,11 +238,12 @@ class="scrollbar-custom mr-1 h-full overflow-y-auto" use:snapScrollToBottom={messages.length ? [...messages] : false} bind:this={chatContainer} + id="chat-container" >
- {#if $page.data?.assistant && !!messages.length} + {#if $page.data?.assistant && !!messages.length && !$page.data.embeddedAssistantId} - {:else if preprompt && preprompt != currentModel.preprompt} + {:else if preprompt && preprompt != currentModel.preprompt && !$page.data.embeddedAssistantId} {/if} @@ -455,6 +456,7 @@

Model: diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 16405644cfa..c8b573e200d 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -11,8 +11,9 @@ import type { ConvSidebar } from "$lib/types/ConvSidebar"; import { toolFromConfigs } from "$lib/server/tools"; import { MetricsServer } from "$lib/server/metrics"; import type { ToolFront, ToolInputFile } from "$lib/types/Tool"; +import { error } from "@sveltejs/kit"; -export const load: LayoutServerLoad = async ({ locals, depends, request }) => { +export const load: LayoutServerLoad = async ({ locals, depends, request, url }) => { depends(UrlDependency.ConversationList); const settings = await collections.settings.findOne(authCondition(locals)); @@ -44,7 +45,7 @@ export const load: LayoutServerLoad = async ({ locals, depends, request }) => { const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? ""); - const assistant = assistantActive + let assistant = assistantActive ? JSON.parse( JSON.stringify( await collections.assistants.findOne({ @@ -54,6 +55,17 @@ export const load: LayoutServerLoad = async ({ locals, depends, request }) => { ) : null; + const embeddedAssistantId = url.searchParams.get("embeddedAssistantId"); + if (embeddedAssistantId) { + const embeddedAssistant = await collections.assistants.findOne({ + _id: new ObjectId(embeddedAssistantId), + }); + if (!embeddedAssistant) { + error(404, "Embedded Assistant not found."); + } + assistant = JSON.parse(JSON.stringify(embeddedAssistant)); + } + const conversations = await collections.conversations .find(authCondition(locals)) .sort({ updatedAt: -1 }) @@ -245,5 +257,6 @@ export const load: LayoutServerLoad = async ({ locals, depends, request }) => { loginRequired, loginEnabled: requiresUser, guestMode: requiresUser && messagesBeforeLogin > 0, + embeddedAssistantId: url.searchParams.get("embeddedAssistantId"), }; }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 17259c17e5f..d331f3ee5ad 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -203,49 +203,69 @@ {#if envPublic.PUBLIC_APPLE_APP_ID} {/if} + + {#if !$page.data.embeddedAssistantId} + + {/if} {#if !$settings.ethicsModalAccepted && $page.url.pathname !== `${base}/privacy` && PUBLIC_APP_DISCLAIMER === "1"} {/if} - (isNavCollapsed = !isNavCollapsed)} - classNames="absolute inset-y-0 z-10 my-auto {!isNavCollapsed - ? 'left-[280px]' - : 'left-0'} *:transition-transform" -/> - -

- (isNavOpen = ev.detail)} title={mobileNavTitle}> - shareConversation(ev.detail.id, ev.detail.title)} - on:deleteConversation={(ev) => deleteConversation(ev.detail)} - on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)} - /> - - - {#if currentError} - - {/if} - -
+ (isNavOpen = ev.detail)} + title={mobileNavTitle} + > + shareConversation(ev.detail.id, ev.detail.title)} + on:deleteConversation={(ev) => deleteConversation(ev.detail)} + on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)} + /> + + + {#if currentError} + + {/if} + +
+{:else} +
+ +
+{/if} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 0f6c00212c9..0ff7aa9e842 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -44,7 +44,8 @@ body: JSON.stringify({ model, preprompt: $settings.customPrompts[$settings.activeModel], - assistantId: data.assistant?._id, + assistantId: data.embeddedAssistantId ?? data.assistant?._id, + // todo: embeddedAssistantId should be an actual field so that it can check }), }); @@ -63,6 +64,16 @@ files, }); + // embedded assistant + if (data.embeddedAssistantId) { + await goto( + `${base}/conversation/${conversationId}/?embeddedAssistantId=${encodeURIComponent( + data.embeddedAssistantId + )}` + ); + return; + } + // invalidateAll to update list of conversations await goto(`${base}/conversation/${conversationId}`, { invalidateAll: true }); } catch (err) { diff --git a/src/routes/api/assistant/[id]/embed-snippet/+server.ts b/src/routes/api/assistant/[id]/embed-snippet/+server.ts new file mode 100644 index 00000000000..449d70fb652 --- /dev/null +++ b/src/routes/api/assistant/[id]/embed-snippet/+server.ts @@ -0,0 +1,153 @@ +export async function GET({ params }) { + const { id } = params; + + const script = `(function() { + function resizeIframeToContentSize(iframe) { + if (iframe.contentWindow) { + const maxHeight = window.innerHeight * 0.8; // 80% of window height + const chatContainerEl = iframe.contentWindow.document.getElementById('chat-container'); + if(chatContainerEl){ + const contentHeight = chatContainerEl.scrollHeight; + iframe.style.height = Math.max(400, Math.min(contentHeight, maxHeight)) + "px"; + } + } + } + + document.addEventListener('DOMContentLoaded', function() { + const button = document.createElement('button'); + button.className = 'fixed z-[1002] bottom-5 right-5 z-50 px-4 gap-1 py-1 bg-black rounded-full text-white rounded cursor-pointer hover:bg-gray-800 border border-gray-200/30 transition-colors flex items-center focus:outline-none'; + + const img = document.createElement('img'); + img.src = 'https://huggingface.co/chat/huggingchat/logo.svg'; + img.alt = 'HuggingChat Logo'; + img.className = 'size-5 mr-0.5 flex-none'; + + const text = document.createTextNode('Chat'); + + button.appendChild(img); + button.appendChild(text); + + const modal = document.createElement('div'); + modal.className = 'hidden fixed inset-0 z-[1001] overflow-auto bg-black bg-opacity-50'; + + const modalContent = document.createElement('div'); + modalContent.className = 'bg-white max-w-2xl rounded-xl overflow-hidden bottom-16 right-5 absolute max-sm:left-5 sm:w-[460px] shadow-2xl'; + + const iframe = document.createElement('iframe'); + iframe.className = 'w-full'; + iframe.style.height = '400px'; // Set an initial height + iframe.src = \`http://localhost:5173/chat/?embeddedAssistantId=${id}\`; + + iframe.onload = function() { + const iframeWindow = this.contentWindow; + const iframeDocument = iframeWindow.document; + + let lastHeight = 0; + + function checkSize() { + const chatContainer = iframeDocument.getElementById('chat-container'); + if (chatContainer) { + const newHeight = chatContainer.scrollHeight; + if (newHeight !== lastHeight) { + resizeIframeToContentSize(iframe); + lastHeight = newHeight; + } + } + requestAnimationFrame(checkSize); + } + + // Start continuous size checking + checkSize(); + + // Set up MutationObserver as a backup + const observer = new MutationObserver(() => { + resizeIframeToContentSize(iframe); + }); + + function initMutationObserver() { + const chatContainer = iframeDocument.getElementById('chat-container'); + if (chatContainer) { + console.error('Chat container found, setting up MutationObserver'); + observer.observe(chatContainer, { childList: true, subtree: true, attributes: true, characterData: true }); + } else { + console.error('Chat container not found, retrying...'); + setTimeout(initMutationObserver, 500); // Retry after 500ms + } + } + + // Start trying to initialize the MutationObserver + initMutationObserver(); + + // Resize on load + resizeIframeToContentSize(iframe); + + // Add event listener for Escape key in iframe + iframeDocument.addEventListener('keydown', function(event) { + if (event.key === 'Escape') { + closeModal(); + } + }); + }; + + modalContent.appendChild(iframe); + modal.appendChild(modalContent); + + // Store the original overflow style + let originalOverflow; + + function toggleModal() { + if (modal.classList.contains('hidden')) { + modal.classList.remove('hidden'); + resizeIframeToContentSize(iframe); + // Store the original overflow and prevent scrolling + originalOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + } else { + modal.classList.add('hidden'); + // Restore the original overflow + document.body.style.overflow = originalOverflow; + } + } + + button.onclick = toggleModal; + + window.onclick = function(event) { + if (event.target == modal) { + toggleModal(); + } + }; + + document.addEventListener('keydown', function(event) { + if (event.key === 'Escape') { + closeModal(); + } + }); + + // Prevent default scrolling when modal is open + document.addEventListener('scroll', function(event) { + if (!modal.classList.contains('hidden')) { + event.preventDefault(); + return false; + } + }, { passive: false }); + + // Add resize event listener to adjust iframe height when window is resized + window.addEventListener('resize', function() { + if (!modal.classList.contains('hidden')) { + resizeIframeToContentSize(iframe); + } + }); + + document.body.appendChild(button); + document.body.appendChild(modal); + }); +})(); +`; + + return new Response(script, { + headers: { + "Content-Type": "application/javascript", + "Access-Control-Allow-Origin": "*", + }, + }); +} diff --git a/src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte b/src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte index 6e6e656451c..b3d6e81e0fa 100644 --- a/src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte +++ b/src/routes/settings/(nav)/assistants/[assistantId]/+page.svelte @@ -31,6 +31,11 @@ envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || $page.url.origin}${base}`; $: shareUrl = `${prefix}/assistant/${assistant?._id}`; + $: embedHtml = + ``.replaceAll( + "HF_SCRIPT", + "script" + ); // replaceAll("HF_SCRIPT", "script") is needed to escape, otherwise svelte compiler breaks let displayReportModal = false; @@ -194,6 +199,26 @@
+
+

Embed this assistant

+ +

Put the code below in your html head section.

+ +
+ + +
+ Copy +
+
+
+
+

System Instructions