Spaces:
Build error
Build error
| <script lang="ts"> | |
| import { toast } from 'svelte-sonner'; | |
| import DOMPurify from 'dompurify'; | |
| import { marked } from 'marked'; | |
| import { getContext, tick } from 'svelte'; | |
| const i18n = getContext('i18n'); | |
| import { chatCompletion } from '$lib/apis/openai'; | |
| import ChatBubble from '$lib/components/icons/ChatBubble.svelte'; | |
| import LightBulb from '$lib/components/icons/LightBulb.svelte'; | |
| import Markdown from '../Messages/Markdown.svelte'; | |
| import Skeleton from '../Messages/Skeleton.svelte'; | |
| export let id = ''; | |
| export let model = null; | |
| export let messages = []; | |
| export let onAdd = () => {}; | |
| let floatingInput = false; | |
| let selectedText = ''; | |
| let floatingInputValue = ''; | |
| let prompt = ''; | |
| let responseContent = null; | |
| let responseDone = false; | |
| const autoScroll = async () => { | |
| // Scroll to bottom only if the scroll is at the bottom give 50px buffer | |
| const responseContainer = document.getElementById('response-container'); | |
| if ( | |
| responseContainer.scrollHeight - responseContainer.clientHeight <= | |
| responseContainer.scrollTop + 50 | |
| ) { | |
| responseContainer.scrollTop = responseContainer.scrollHeight; | |
| } | |
| }; | |
| const askHandler = async () => { | |
| if (!model) { | |
| toast.error('Model not selected'); | |
| return; | |
| } | |
| prompt = [ | |
| // Blockquote each line of the selected text | |
| ...selectedText.split('\n').map((line) => `> ${line}`), | |
| '', | |
| // Then your question | |
| floatingInputValue | |
| ].join('\n'); | |
| floatingInputValue = ''; | |
| responseContent = ''; | |
| const [res, controller] = await chatCompletion(localStorage.token, { | |
| model: model, | |
| messages: [ | |
| ...messages, | |
| { | |
| role: 'user', | |
| content: prompt | |
| } | |
| ].map((message) => ({ | |
| role: message.role, | |
| content: message.content | |
| })), | |
| stream: true // Enable streaming | |
| }); | |
| if (res && res.ok) { | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| const processStream = async () => { | |
| while (true) { | |
| // Read data chunks from the response stream | |
| const { done, value } = await reader.read(); | |
| if (done) { | |
| break; | |
| } | |
| // Decode the received chunk | |
| const chunk = decoder.decode(value, { stream: true }); | |
| // Process lines within the chunk | |
| const lines = chunk.split('\n').filter((line) => line.trim() !== ''); | |
| for (const line of lines) { | |
| if (line.startsWith('data: ')) { | |
| if (line.startsWith('data: [DONE]')) { | |
| responseDone = true; | |
| await tick(); | |
| autoScroll(); | |
| continue; | |
| } else { | |
| // Parse the JSON chunk | |
| try { | |
| const data = JSON.parse(line.slice(6)); | |
| // Append the `content` field from the "choices" object | |
| if (data.choices && data.choices[0]?.delta?.content) { | |
| responseContent += data.choices[0].delta.content; | |
| autoScroll(); | |
| } | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| // Process the stream in the background | |
| await processStream(); | |
| } else { | |
| toast.error('An error occurred while fetching the explanation'); | |
| } | |
| }; | |
| const explainHandler = async () => { | |
| if (!model) { | |
| toast.error('Model not selected'); | |
| return; | |
| } | |
| const quotedText = selectedText | |
| .split('\n') | |
| .map((line) => `> ${line}`) | |
| .join('\n'); | |
| prompt = `${quotedText}\n\nExplain`; | |
| responseContent = ''; | |
| const [res, controller] = await chatCompletion(localStorage.token, { | |
| model: model, | |
| messages: [ | |
| ...messages, | |
| { | |
| role: 'user', | |
| content: prompt | |
| } | |
| ].map((message) => ({ | |
| role: message.role, | |
| content: message.content | |
| })), | |
| stream: true // Enable streaming | |
| }); | |
| if (res && res.ok) { | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| const processStream = async () => { | |
| while (true) { | |
| // Read data chunks from the response stream | |
| const { done, value } = await reader.read(); | |
| if (done) { | |
| break; | |
| } | |
| // Decode the received chunk | |
| const chunk = decoder.decode(value, { stream: true }); | |
| // Process lines within the chunk | |
| const lines = chunk.split('\n').filter((line) => line.trim() !== ''); | |
| for (const line of lines) { | |
| if (line.startsWith('data: ')) { | |
| if (line.startsWith('data: [DONE]')) { | |
| responseDone = true; | |
| await tick(); | |
| autoScroll(); | |
| continue; | |
| } else { | |
| // Parse the JSON chunk | |
| try { | |
| const data = JSON.parse(line.slice(6)); | |
| // Append the `content` field from the "choices" object | |
| if (data.choices && data.choices[0]?.delta?.content) { | |
| responseContent += data.choices[0].delta.content; | |
| autoScroll(); | |
| } | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| // Process the stream in the background | |
| await processStream(); | |
| } else { | |
| toast.error('An error occurred while fetching the explanation'); | |
| } | |
| }; | |
| const addHandler = async () => { | |
| const messages = [ | |
| { | |
| role: 'user', | |
| content: prompt | |
| }, | |
| { | |
| role: 'assistant', | |
| content: responseContent | |
| } | |
| ]; | |
| onAdd({ | |
| modelId: model, | |
| parentId: id, | |
| messages: messages | |
| }); | |
| }; | |
| export const closeHandler = () => { | |
| responseContent = null; | |
| responseDone = false; | |
| floatingInput = false; | |
| floatingInputValue = ''; | |
| }; | |
| </script> | |
| <div | |
| id={`floating-buttons-${id}`} | |
| class="absolute rounded-lg mt-1 text-xs z-9999" | |
| style="display: none" | |
| > | |
| {#if responseContent === null} | |
| {#if !floatingInput} | |
| <div | |
| class="flex flex-row gap-0.5 shrink-0 p-1 bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-lg shadow-xl" | |
| > | |
| <button | |
| class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit" | |
| on:click={async () => { | |
| selectedText = window.getSelection().toString(); | |
| floatingInput = true; | |
| await tick(); | |
| setTimeout(() => { | |
| const input = document.getElementById('floating-message-input'); | |
| if (input) { | |
| input.focus(); | |
| } | |
| }, 0); | |
| }} | |
| > | |
| <ChatBubble className="size-3 shrink-0" /> | |
| <div class="shrink-0">{$i18n.t('Ask')}</div> | |
| </button> | |
| <button | |
| class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit" | |
| on:click={() => { | |
| selectedText = window.getSelection().toString(); | |
| explainHandler(); | |
| }} | |
| > | |
| <LightBulb className="size-3 shrink-0" /> | |
| <div class="shrink-0">{$i18n.t('Explain')}</div> | |
| </button> | |
| </div> | |
| {:else} | |
| <div | |
| class="py-1 flex dark:text-gray-100 bg-gray-50 dark:bg-gray-800 border border-gray-100 dark:border-gray-850 w-72 rounded-full shadow-xl" | |
| > | |
| <input | |
| type="text" | |
| id="floating-message-input" | |
| class="ml-5 bg-transparent outline-hidden w-full flex-1 text-sm" | |
| placeholder={$i18n.t('Ask a question')} | |
| bind:value={floatingInputValue} | |
| on:keydown={(e) => { | |
| if (e.key === 'Enter') { | |
| askHandler(); | |
| } | |
| }} | |
| /> | |
| <div class="ml-1 mr-2"> | |
| <button | |
| class="{floatingInputValue !== '' | |
| ? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 ' | |
| : 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 m-0.5 self-center" | |
| on:click={() => { | |
| askHandler(); | |
| }} | |
| > | |
| <svg | |
| xmlns="http://www.w3.org/2000/svg" | |
| viewBox="0 0 16 16" | |
| fill="currentColor" | |
| class="size-4" | |
| > | |
| <path | |
| fill-rule="evenodd" | |
| d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z" | |
| clip-rule="evenodd" | |
| /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| {/if} | |
| {:else} | |
| <div class="bg-white dark:bg-gray-850 dark:text-gray-100 rounded-xl shadow-xl w-80 max-w-full"> | |
| <div | |
| class="bg-gray-50/50 dark:bg-gray-800 dark:text-gray-100 text-medium rounded-xl px-3.5 py-3 w-full" | |
| > | |
| <div class="font-medium"> | |
| <Markdown id={`${id}-float-prompt`} content={prompt} /> | |
| </div> | |
| </div> | |
| <div | |
| class="bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-xl px-3.5 py-3 w-full" | |
| > | |
| <div class=" max-h-80 overflow-y-auto w-full markdown-prose-xs" id="response-container"> | |
| {#if responseContent.trim() === ''} | |
| <Skeleton size="sm" /> | |
| {:else} | |
| <Markdown id={`${id}-float-response`} content={responseContent} /> | |
| {/if} | |
| {#if responseDone} | |
| <div class="flex justify-end pt-3 text-sm font-medium"> | |
| <button | |
| class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full" | |
| on:click={addHandler} | |
| > | |
| {$i18n.t('Add')} | |
| </button> | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |