Spaces:
Runtime error
Runtime error
| <script lang="ts"> | |
| import { PUBLIC_BACKEND_WS_URL } from '$env/static/public'; | |
| import { onMount, tick } from 'svelte'; | |
| import { nanoid } from 'nanoid'; | |
| import { chatsStore, selectedChatId, loadingState, lastBase64Image } from '$lib/store'; | |
| import type { Message, Chat, InferenceResponse, ImageFile } from '$lib/types'; | |
| import { MessageType, Sender } from '$lib/types'; | |
| import { timeFormater, fetchImageBase64 } from '$lib/utils'; | |
| import ChatInput from '$lib/ChatInput.svelte'; | |
| import ChatMessage from '$lib/ChatMessage.svelte'; | |
| import ChatNewBtn from '$lib/ChatNewBtn.svelte'; | |
| import IconDelete from './Icons/IconDelete.svelte'; | |
| $: isLoading = !($loadingState === '' || $loadingState === 'Complete'); | |
| let chatBoxEl: HTMLDivElement; | |
| let chatInputEl: HTMLInputElement; | |
| function clearStateMsg(t = 5000) { | |
| setTimeout(() => { | |
| $loadingState = ''; | |
| }, t); | |
| } | |
| onMount(() => { | |
| const observer = new ResizeObserver(() => { | |
| window.scrollTo(0, chatBoxEl.getBoundingClientRect().height); | |
| if ('parentIFrame' in window) { | |
| (window as any).parentIFrame.scrollTo(0, chatBoxEl.getBoundingClientRect().height); | |
| } | |
| }); | |
| observer.observe(chatBoxEl); | |
| // generateImage(); | |
| }); | |
| $: chatData = $chatsStore.find((chat) => chat.id === $selectedChatId); | |
| $: messages = chatData?.messages.sort((a, b) => a.timestamp - b.timestamp) || []; | |
| $: if ($selectedChatId) { | |
| // find last message with image type | |
| $lastBase64Image = | |
| [...messages].reverse().find((message) => message.type === MessageType.IMAGE)?.content || | |
| null; | |
| } | |
| // this is when page starts or user deletes all chats | |
| function newChat() { | |
| const chatId = nanoid(); | |
| const chat: Chat = { | |
| id: chatId, | |
| blurb: `New Chat - ${chatId}`, | |
| messages: [], | |
| timestamp: new Date().getTime() | |
| }; | |
| $chatsStore = [chat].concat($chatsStore); | |
| $selectedChatId = chat.id; | |
| } | |
| function deleteChat(id: string) { | |
| $chatsStore = $chatsStore.filter((chat) => chat.id !== id); | |
| } | |
| // this is called when user send a text or image | |
| // if image then update last image store | |
| // if text and has image send to inference | |
| function submitMessage(event: CustomEvent) { | |
| if ($chatsStore.length === 0) { | |
| newChat(); | |
| } | |
| const { type, content } = event.detail; | |
| const message: Message = { | |
| sender: Sender.USER, | |
| id: nanoid(), | |
| type: type, | |
| content: content, | |
| timestamp: new Date().getTime() | |
| }; | |
| updateChatStore(message); | |
| if (type === MessageType.IMAGE) { | |
| // upate last image to be the last image sent | |
| $lastBase64Image = content; | |
| } else if (type === MessageType.TEXT) { | |
| // if the last message was an image, then we want to run inference | |
| // on the image and the text | |
| if (!$lastBase64Image) { | |
| const message: Message = { | |
| sender: Sender.BOT, | |
| id: nanoid(), | |
| type: MessageType.TEXT, | |
| content: "Sorry, I don't have an image to work with.", | |
| timestamp: new Date().getTime() | |
| }; | |
| updateChatStore(message); | |
| return; | |
| } | |
| runInference($lastBase64Image, content); | |
| } | |
| } | |
| // hack to update store | |
| function updateChatStore(message: Message) { | |
| $chatsStore = $chatsStore.map((chat) => { | |
| if (chat.id === $selectedChatId) { | |
| chat.messages.push(message); | |
| } | |
| return chat; | |
| }); | |
| } | |
| // run inference via websockets | |
| async function runInference(image: string, prompt: string) { | |
| if (isLoading || image === '' || prompt === '') { | |
| return; | |
| } | |
| $loadingState = 'Pending'; | |
| const sessionHash = crypto.randomUUID(); | |
| const hashpayload = { | |
| fn_index: 1, | |
| session_hash: sessionHash | |
| }; | |
| const datapayload = { | |
| data: [ | |
| prompt, // prompt | |
| 10.5, // text guidance | |
| 1.5, // image guidance | |
| image, | |
| 15, // steps | |
| '', // negative promtp, | |
| 512, // width | |
| 512, // height | |
| 0 // seed | |
| ] | |
| }; | |
| const websocket = new WebSocket(`wss://${PUBLIC_BACKEND_WS_URL}/queue/join`); | |
| // websocket.onopen = async function (event) { | |
| // websocket.send(JSON.stringify({ hash: sessionHash })); | |
| // }; | |
| websocket.onclose = (evt) => { | |
| if (!evt.wasClean) { | |
| $loadingState = 'Error'; | |
| } | |
| }; | |
| websocket.onmessage = async function (event) { | |
| try { | |
| const data = JSON.parse(event.data); | |
| $loadingState = ''; | |
| switch (data.msg) { | |
| case 'send_hash': | |
| websocket.send(JSON.stringify(hashpayload)); | |
| break; | |
| case 'send_data': | |
| $loadingState = 'Sending Data'; | |
| websocket.send(JSON.stringify({ ...hashpayload, ...datapayload })); | |
| break; | |
| case 'queue_full': | |
| $loadingState = 'Queue full'; | |
| websocket.close(); | |
| return; | |
| case 'estimation': | |
| const { rank, queue_size } = data; | |
| $loadingState = `On queue ${rank}/${queue_size}`; | |
| break; | |
| case 'process_generating': | |
| $loadingState = data.success ? 'Generating' : 'Error'; | |
| break; | |
| // here is success | |
| // got an image in response from inference | |
| // then update the chat store | |
| case 'process_completed': | |
| try { | |
| const response = data.output as InferenceResponse; | |
| const imageData = response.data[0] as ImageFile[]; | |
| const nsfwData = data.output.data[1] as boolean[] | null; | |
| console.log('imageData', imageData); | |
| console.log('nsfwData', nsfwData); | |
| if (nsfwData && nsfwData[0]) { | |
| const message: Message = { | |
| sender: Sender.BOT, | |
| id: nanoid(), | |
| type: MessageType.TEXT, | |
| content: | |
| 'Sorry this prompt possibly generates NSFW content. Please try another prompt.', | |
| timestamp: new Date().getTime() | |
| }; | |
| updateChatStore(message); | |
| } else { | |
| const fileName = imageData[0].name; | |
| const imageBase64 = await fetchImageBase64( | |
| `https://${PUBLIC_BACKEND_WS_URL}/file=${fileName}` | |
| ); | |
| const message: Message = { | |
| sender: Sender.BOT, | |
| id: nanoid(), | |
| type: MessageType.IMAGE, | |
| content: imageBase64, | |
| timestamp: new Date().getTime() | |
| }; | |
| $lastBase64Image = imageBase64; | |
| updateChatStore(message); | |
| } | |
| $loadingState = data.success ? 'Complete' : 'Error'; | |
| clearStateMsg(); | |
| } catch (err) { | |
| const tError = err as Error; | |
| $loadingState = tError?.message; | |
| clearStateMsg(10000); | |
| } | |
| websocket.close(); | |
| return; | |
| case 'process_starts': | |
| $loadingState = 'Processing'; | |
| break; | |
| } | |
| } catch (e) { | |
| console.error(e); | |
| $loadingState = 'Error'; | |
| } | |
| }; | |
| } | |
| </script> | |
| <div> | |
| <h1 class="text-2xl">CHATS</h1> | |
| <div class="grid min-h-[40rem] grid-cols-4"> | |
| <div class="col-span-1 flex flex-col border-r p-4 relative"> | |
| <div class="sticky top-3"> | |
| <ChatNewBtn on:click={newChat} /> | |
| <div class="max-h-[40rem] flex flex-col gap-2 overflow-y-scroll"> | |
| {#if $chatsStore.length} | |
| {#each $chatsStore as chat} | |
| <div class="flex flex-col relative"> | |
| <button | |
| class="disabled:opacity-60 disabled:cursor-progress" | |
| on:click={() => ($selectedChatId = chat.id)} | |
| disabled={isLoading} | |
| > | |
| <div | |
| class=" flex flex-col h-16 items-start justify-center rounded-xl bg-gray-100 px-4 text-gray-900 | |
| {chat.id === $selectedChatId ? 'bg-gray-400' : ''}" | |
| > | |
| <h3 class="w-full truncate font-semibold">{chat.blurb}</h3> | |
| <p class="w-full truncate text-sm text-gray-500"> | |
| {timeFormater(new Date(chat.timestamp))} | |
| </p> | |
| </div> | |
| </button> | |
| <button | |
| class="text-black absolute right-1 bottom-1 disabled:opacity-60" | |
| on:click={() => deleteChat(chat.id)} | |
| disabled={isLoading} | |
| > | |
| <IconDelete /> | |
| </button> | |
| </div> | |
| {/each} | |
| {:else} | |
| <div | |
| class="flex h-16 flex-col items-start justify-center rounded-xl bg-gray-100 px-4 text-gray-900" | |
| > | |
| <h3 class="w-full truncate font-semibold">No chats</h3> | |
| <p class="w-full truncate text-sm text-gray-500">Start a new Chat!</p> | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-span-3 flex flex-col" bind:this={chatBoxEl}> | |
| {#each messages as message} | |
| <ChatMessage {message} /> | |
| {/each} | |
| <ChatInput on:submitMessage={submitMessage} bind:inputEl={chatInputEl} disabled={isLoading} /> | |
| </div> | |
| <div class="top-0 right-0 z-10"> | |
| Loading: {$loadingState} | |
| </div> | |
| </div> | |
| </div> | |
| <style lang="postcss" scoped> | |
| </style> | |