<script lang="ts"> import type { Message } from "$lib/types/Message"; import { createEventDispatcher } from "svelte"; import CarbonSendAltFilled from "~icons/carbon/send-alt-filled"; import CarbonExport from "~icons/carbon/export"; import CarbonStopFilledAlt from "~icons/carbon/stop-filled-alt"; import CarbonClose from "~icons/carbon/close"; import EosIconsLoading from "~icons/eos-icons/loading"; import ChatMessages from "./ChatMessages.svelte"; import ChatInput from "./ChatInput.svelte"; import StopGeneratingBtn from "../StopGeneratingBtn.svelte"; import type { Model } from "$lib/types/Model"; import WebSearchToggle from "../WebSearchToggle.svelte"; import LoginModal from "../LoginModal.svelte"; import type { WebSearchUpdate } from "$lib/types/MessageUpdate"; import { page } from "$app/stores"; import DisclaimerModal from "../DisclaimerModal.svelte"; import FileDropzone from "./FileDropzone.svelte"; import RetryBtn from "../RetryBtn.svelte"; import UploadBtn from "../UploadBtn.svelte"; import file2base64 from "$lib/utils/file2base64"; import { useSettingsStore } from "$lib/stores/settings"; export let messages: Message[] = []; export let loading = false; export let pending = false; export let shared = false; export let currentModel: Model; export let models: Model[]; export let webSearchMessages: WebSearchUpdate[] = []; export let preprompt: string | undefined = undefined; export let files: File[] = []; $: isReadOnly = !models.some((model) => model.id === currentModel.id); let loginModalOpen = false; let message: string; const dispatch = createEventDispatcher<{ message: string; share: void; stop: void; retry: { id: Message["id"]; content: string }; }>(); const handleSubmit = () => { if (loading) return; dispatch("message", message); message = ""; }; let lastTarget: EventTarget | null = null; let onDrag = false; const onDragEnter = (e: DragEvent) => { lastTarget = e.target; onDrag = true; }; const onDragLeave = (e: DragEvent) => { if (e.target === lastTarget) { onDrag = false; } }; const onDragOver = (e: DragEvent) => { e.preventDefault(); }; $: lastIsError = messages[messages.length - 1]?.from === "user" && !loading; $: sources = files.map((file) => file2base64(file)); const settings = useSettingsStore(); </script> <div class="relative min-h-0 min-w-0"> {#if !$settings.ethicsModalAccepted} <DisclaimerModal /> {:else if loginModalOpen} <LoginModal on:close={() => { loginModalOpen = false; }} /> {/if} <ChatMessages {loading} {pending} settings={$page.data.settings} {currentModel} {models} {messages} readOnly={isReadOnly} isAuthor={!shared} {webSearchMessages} {preprompt} on:message={(ev) => { if ($page.data.loginRequired) { loginModalOpen = true; } else { dispatch("message", ev.detail); } }} on:vote on:retry={(ev) => { if (!loading) dispatch("retry", ev.detail); }} /> <div class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:border-t max-md:bg-white max-md:dark:bg-gray-900 sm:px-5 md:py-8 xl:max-w-4xl [&>*]:pointer-events-auto" > {#if sources.length} <div class="flex flex-row flex-wrap justify-center gap-2.5 max-md:pb-3"> {#each sources as source, index} {#await source then src} <div class="relative h-16 w-16 overflow-hidden rounded-lg shadow-lg"> <img src={`data:image/*;base64,${src}`} alt="input content" class="h-full w-full rounded-lg bg-gray-400 object-cover dark:bg-gray-900" /> <!-- add a button on top that deletes this image from sources --> <button class="absolute left-1 top-1" on:click={() => { files = files.filter((_, i) => i !== index); }} > <CarbonClose class="text-md font-black text-gray-300 hover:text-gray-100" /> </button> </div> {/await} {/each} </div> {/if} <div class="w-full"> <div class="flex w-full pb-3"> {#if $page.data.settings?.searchEnabled} <WebSearchToggle /> {/if} {#if loading} <StopGeneratingBtn classNames="ml-auto" on:click={() => dispatch("stop")} /> {:else if lastIsError} <RetryBtn classNames="ml-auto" on:click={() => dispatch("retry", { id: messages[messages.length - 1].id, content: messages[messages.length - 1].content, })} /> {:else if currentModel.multimodal} <UploadBtn bind:files classNames="ml-auto" /> {/if} </div> <form on:dragover={onDragOver} on:dragenter={onDragEnter} on:dragleave={onDragLeave} tabindex="-1" aria-label="file dropzone" on:submit|preventDefault={handleSubmit} class="relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 focus-within:border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:focus-within:border-gray-500 {isReadOnly ? 'opacity-30' : ''}" > {#if onDrag && currentModel.multimodal} <FileDropzone bind:files bind:onDrag /> {:else} <div class="flex w-full flex-1 border-none bg-transparent"> {#if lastIsError} <ChatInput value="Sorry, something went wrong. Please try again." disabled={true} /> {:else} <ChatInput placeholder="Ask anything" bind:value={message} on:submit={handleSubmit} on:keypress={(ev) => { if ($page.data.loginRequired) { ev.preventDefault(); loginModalOpen = true; } }} maxRows={6} disabled={isReadOnly || lastIsError} /> {/if} {#if loading} <button class="btn mx-1 my-1 inline-block h-[2.4rem] self-end rounded-lg bg-transparent p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100 md:hidden" on:click={() => dispatch("stop")} > <CarbonStopFilledAlt /> </button> <div class="mx-1 my-1 hidden h-[2.4rem] items-center p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100 md:flex" > <EosIconsLoading /> </div> {:else} <button class="btn mx-1 my-1 h-[2.4rem] self-end rounded-lg bg-transparent p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100" disabled={!message || isReadOnly} type="submit" > <CarbonSendAltFilled /> </button> {/if} </div> {/if} </form> <div class="mt-2 flex justify-between self-stretch px-1 text-xs text-gray-400/90 max-md:mb-2 max-sm:gap-2" > <p> Model: <a href={currentModel.modelUrl || "https://huggingface.co/" + currentModel.name} target="_blank" rel="noreferrer" class="hover:underline">{currentModel.displayName}</a > <span class="max-sm:hidden">ยท</span><br class="sm:hidden" /> Generated content may be inaccurate or false. </p> {#if messages.length} <button class="flex flex-none items-center hover:text-gray-400 hover:underline max-sm:rounded-lg max-sm:bg-gray-50 max-sm:px-2.5 dark:max-sm:bg-gray-800" type="button" on:click={() => dispatch("share")} > <CarbonExport class="text-[.6rem] sm:mr-1.5 sm:text-primary-500" /> <div class="max-sm:hidden">Share this conversation</div> </button> {/if} </div> </div> </div> </div>