radames's picture
add some comments
61996ad
<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>