|  | <script lang="ts"> | 
					
						
						|  | import { marked, type MarkedOptions } from "marked"; | 
					
						
						|  | import markedKatex from "marked-katex-extension"; | 
					
						
						|  | import type { Message } from "$lib/types/Message"; | 
					
						
						|  | import { afterUpdate, createEventDispatcher, tick } from "svelte"; | 
					
						
						|  | import { deepestChild } from "$lib/utils/deepestChild"; | 
					
						
						|  | import { page } from "$app/stores"; | 
					
						
						|  |  | 
					
						
						|  | import CodeBlock from "../CodeBlock.svelte"; | 
					
						
						|  | import CopyToClipBoardBtn from "../CopyToClipBoardBtn.svelte"; | 
					
						
						|  | import IconLoading from "../icons/IconLoading.svelte"; | 
					
						
						|  | import CarbonRotate360 from "~icons/carbon/rotate-360"; | 
					
						
						|  | import CarbonDownload from "~icons/carbon/download"; | 
					
						
						|  | import CarbonThumbsUp from "~icons/carbon/thumbs-up"; | 
					
						
						|  | import CarbonThumbsDown from "~icons/carbon/thumbs-down"; | 
					
						
						|  | import CarbonPen from "~icons/carbon/pen"; | 
					
						
						|  | import CarbonChevronLeft from "~icons/carbon/chevron-left"; | 
					
						
						|  | import CarbonChevronRight from "~icons/carbon/chevron-right"; | 
					
						
						|  |  | 
					
						
						|  | import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken"; | 
					
						
						|  | import type { Model } from "$lib/types/Model"; | 
					
						
						|  |  | 
					
						
						|  | import OpenWebSearchResults from "../OpenWebSearchResults.svelte"; | 
					
						
						|  | import type { WebSearchUpdate } from "$lib/types/MessageUpdate"; | 
					
						
						|  | import { base } from "$app/paths"; | 
					
						
						|  | import { useConvTreeStore } from "$lib/stores/convTree"; | 
					
						
						|  |  | 
					
						
						|  | function sanitizeMd(md: string) { | 
					
						
						|  | let ret = md | 
					
						
						|  | .replace(/<\|[a-z]*$/, "") | 
					
						
						|  | .replace(/<\|[a-z]+\|$/, "") | 
					
						
						|  | .replace(/<$/, "") | 
					
						
						|  | .replaceAll(PUBLIC_SEP_TOKEN, " ") | 
					
						
						|  | .replaceAll(/<\|[a-z]+\|>/g, " ") | 
					
						
						|  | .replaceAll(/<br\s?\/?>/gi, "\n") | 
					
						
						|  | .replaceAll("<", "<") | 
					
						
						|  | .trim(); | 
					
						
						|  |  | 
					
						
						|  | for (const stop of [...(model.parameters?.stop ?? []), "<|endoftext|>"]) { | 
					
						
						|  | if (ret.endsWith(stop)) { | 
					
						
						|  | ret = ret.slice(0, -stop.length).trim(); | 
					
						
						|  | } | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | return ret; | 
					
						
						|  | } | 
					
						
						|  | function unsanitizeMd(md: string) { | 
					
						
						|  | return md.replaceAll("<", "<"); | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | export let model: Model; | 
					
						
						|  | export let id: Message["id"]; | 
					
						
						|  | export let messages: Message[]; | 
					
						
						|  | export let loading = false; | 
					
						
						|  | export let isAuthor = true; | 
					
						
						|  | export let readOnly = false; | 
					
						
						|  | export let isTapped = false; | 
					
						
						|  |  | 
					
						
						|  | $: message = messages.find((m) => m.id === id) ?? ({} as Message); | 
					
						
						|  |  | 
					
						
						|  | const dispatch = createEventDispatcher<{ | 
					
						
						|  | retry: { content?: string; id: Message["id"] }; | 
					
						
						|  | vote: { score: Message["score"]; id: Message["id"] }; | 
					
						
						|  | }>(); | 
					
						
						|  |  | 
					
						
						|  | let contentEl: HTMLElement; | 
					
						
						|  | let loadingEl: IconLoading; | 
					
						
						|  | let pendingTimeout: ReturnType<typeof setTimeout>; | 
					
						
						|  | let isCopied = false; | 
					
						
						|  |  | 
					
						
						|  | let initialized = false; | 
					
						
						|  |  | 
					
						
						|  | const renderer = new marked.Renderer(); | 
					
						
						|  |  | 
					
						
						|  | renderer.codespan = (code) => { | 
					
						
						|  | // Unsanitize double-sanitized code | 
					
						
						|  | return `<code>${code.replaceAll("&", "&")}</code>`; | 
					
						
						|  | }; | 
					
						
						|  |  | 
					
						
						|  | renderer.link = (href, title, text) => { | 
					
						
						|  | return `<a href="${href?.replace(/>$/, "")}" target="_blank" rel="noreferrer">${text}</a>`; | 
					
						
						|  | }; | 
					
						
						|  |  | 
					
						
						|  | // eslint-disable-next-line @typescript-eslint/no-unused-vars | 
					
						
						|  | const { extensions, ...defaults } = marked.getDefaults() as MarkedOptions & { | 
					
						
						|  | // eslint-disable-next-line @typescript-eslint/no-explicit-any | 
					
						
						|  | extensions: any; | 
					
						
						|  | }; | 
					
						
						|  | const options: MarkedOptions = { | 
					
						
						|  | ...defaults, | 
					
						
						|  | gfm: true, | 
					
						
						|  | breaks: true, | 
					
						
						|  | renderer, | 
					
						
						|  | }; | 
					
						
						|  |  | 
					
						
						|  | marked.use( | 
					
						
						|  | markedKatex({ | 
					
						
						|  | throwOnError: false, | 
					
						
						|  | // output: "html", | 
					
						
						|  | }) | 
					
						
						|  | ); | 
					
						
						|  |  | 
					
						
						|  | $: tokens = marked.lexer(sanitizeMd(message.content)); | 
					
						
						|  |  | 
					
						
						|  | $: emptyLoad = | 
					
						
						|  | !message.content && (webSearchIsDone || (searchUpdates && searchUpdates.length === 0)); | 
					
						
						|  |  | 
					
						
						|  | afterUpdate(() => { | 
					
						
						|  | loadingEl?.$destroy(); | 
					
						
						|  | clearTimeout(pendingTimeout); | 
					
						
						|  |  | 
					
						
						|  | // Add loading animation to the last message if update takes more than 600ms | 
					
						
						|  | if (isLast && loading && emptyLoad) { | 
					
						
						|  | pendingTimeout = setTimeout(() => { | 
					
						
						|  | if (contentEl) { | 
					
						
						|  | loadingEl = new IconLoading({ | 
					
						
						|  | target: deepestChild(contentEl), | 
					
						
						|  | props: { classNames: "loading inline ml-2 first:ml-0" }, | 
					
						
						|  | }); | 
					
						
						|  | } | 
					
						
						|  | }, 600); | 
					
						
						|  | } | 
					
						
						|  | }); | 
					
						
						|  |  | 
					
						
						|  | function handleKeyDown(e: KeyboardEvent) { | 
					
						
						|  | if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { | 
					
						
						|  | editFormEl.requestSubmit(); | 
					
						
						|  | } | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | $: searchUpdates = (message.updates?.filter(({ type }) => type === "webSearch") ?? | 
					
						
						|  | []) as WebSearchUpdate[]; | 
					
						
						|  |  | 
					
						
						|  | $: downloadLink = | 
					
						
						|  | message.from === "user" ? `${$page.url.pathname}/message/${message.id}/prompt` : undefined; | 
					
						
						|  |  | 
					
						
						|  | let webSearchIsDone = true; | 
					
						
						|  |  | 
					
						
						|  | $: webSearchIsDone = | 
					
						
						|  | searchUpdates.length > 0 && searchUpdates[searchUpdates.length - 1].messageType === "sources"; | 
					
						
						|  |  | 
					
						
						|  | $: webSearchSources = | 
					
						
						|  | searchUpdates && | 
					
						
						|  | searchUpdates?.filter(({ messageType }) => messageType === "sources")?.[0]?.sources; | 
					
						
						|  |  | 
					
						
						|  | $: if (isCopied) { | 
					
						
						|  | setTimeout(() => { | 
					
						
						|  | isCopied = false; | 
					
						
						|  | }, 1000); | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | $: editMode = $convTreeStore.editing === message.id; | 
					
						
						|  | let editContentEl: HTMLTextAreaElement; | 
					
						
						|  | let editFormEl: HTMLFormElement; | 
					
						
						|  |  | 
					
						
						|  | $: if (editMode) { | 
					
						
						|  | tick(); | 
					
						
						|  | if (editContentEl) { | 
					
						
						|  | editContentEl.value = message.content; | 
					
						
						|  | editContentEl?.focus(); | 
					
						
						|  | } | 
					
						
						|  | } | 
					
						
						|  |  | 
					
						
						|  | $: isLast = (message && message.children?.length === 0) ?? false; | 
					
						
						|  |  | 
					
						
						|  | $: childrenToRender = 0; | 
					
						
						|  | $: nChildren = message?.children?.length ?? 0; | 
					
						
						|  |  | 
					
						
						|  | $: { | 
					
						
						|  | if (initialized) { | 
					
						
						|  | childrenToRender = Math.max(0, nChildren - 1); | 
					
						
						|  | } else { | 
					
						
						|  | childrenToRender = 0; | 
					
						
						|  | initialized = true; | 
					
						
						|  | } | 
					
						
						|  | } | 
					
						
						|  | const convTreeStore = useConvTreeStore(); | 
					
						
						|  |  | 
					
						
						|  | $: if (message.children?.length === 0) $convTreeStore.leaf = message.id; | 
					
						
						|  | </script> | 
					
						
						|  |  | 
					
						
						|  | {#if message.from === "assistant"} | 
					
						
						|  | <div | 
					
						
						|  | class="group relative -mb-6 flex items-start justify-start gap-4 pb-4 leading-relaxed" | 
					
						
						|  | role="presentation" | 
					
						
						|  | on:click={() => (isTapped = !isTapped)} | 
					
						
						|  | on:keydown={() => (isTapped = !isTapped)} | 
					
						
						|  | > | 
					
						
						|  | {#if $page.data?.assistant?.avatar} | 
					
						
						|  | <img | 
					
						
						|  | src="{base}/settings/assistants/{$page.data.assistant._id}/avatar.jpg" | 
					
						
						|  | alt="Avatar" | 
					
						
						|  | class="mt-5 h-3 w-3 flex-none select-none rounded-full shadow-lg" | 
					
						
						|  | /> | 
					
						
						|  | {:else} | 
					
						
						|  | <img | 
					
						
						|  | alt="" | 
					
						
						|  | src="https://huggingface.co/avatars/2edb18bd0206c16b433841a47f53fa8e.svg" | 
					
						
						|  | class="mt-5 h-3 w-3 flex-none select-none rounded-full shadow-lg" | 
					
						
						|  | /> | 
					
						
						|  | {/if} | 
					
						
						|  | <div | 
					
						
						|  | class="relative min-h-[calc(2rem+theme(spacing[3.5])*2)] min-w-[60px] break-words rounded-2xl border border-gray-100 bg-gradient-to-br from-gray-50 px-5 py-3.5 text-gray-600 prose-pre:my-2 dark:border-gray-800 dark:from-gray-800/40 dark:text-gray-300" | 
					
						
						|  | > | 
					
						
						|  | {#if searchUpdates && searchUpdates.length > 0} | 
					
						
						|  | <OpenWebSearchResults | 
					
						
						|  | classNames={tokens.length ? "mb-3.5" : ""} | 
					
						
						|  | webSearchMessages={searchUpdates} | 
					
						
						|  | /> | 
					
						
						|  | {/if} | 
					
						
						|  |  | 
					
						
						|  | <div | 
					
						
						|  | class="prose max-w-none max-sm:prose-sm dark:prose-invert prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900" | 
					
						
						|  | bind:this={contentEl} | 
					
						
						|  | > | 
					
						
						|  | {#each tokens as token} | 
					
						
						|  | {#if token.type === "code"} | 
					
						
						|  | <CodeBlock lang={token.lang} code={unsanitizeMd(token.text)} /> | 
					
						
						|  | {:else} | 
					
						
						|  | <!-- eslint-disable-next-line svelte/no-at-html-tags --> | 
					
						
						|  | {@html marked.parse(token.raw, options)} | 
					
						
						|  | {/if} | 
					
						
						|  | {/each} | 
					
						
						|  | </div> | 
					
						
						|  |  | 
					
						
						|  | <!-- Web Search sources --> | 
					
						
						|  | {#if webSearchSources?.length} | 
					
						
						|  | <div class="mt-4 flex flex-wrap items-center gap-x-2 gap-y-1.5 text-sm"> | 
					
						
						|  | <div class="text-gray-400">Sources:</div> | 
					
						
						|  | {#each webSearchSources as { link, title, hostname }} | 
					
						
						|  | <a | 
					
						
						|  | class="flex items-center gap-2 whitespace-nowrap rounded-lg border bg-white px-2 py-1.5 leading-none hover:border-gray-300 dark:border-gray-800 dark:bg-gray-900 dark:hover:border-gray-700" | 
					
						
						|  | href={link} | 
					
						
						|  | target="_blank" | 
					
						
						|  | > | 
					
						
						|  | <img | 
					
						
						|  | class="h-3.5 w-3.5 rounded" | 
					
						
						|  | src="https://www.google.com/s2/favicons?sz=64&domain_url={hostname}" | 
					
						
						|  | alt="{title} favicon" | 
					
						
						|  | /> | 
					
						
						|  | <div>{hostname.replace(/^www\./, "")}</div> | 
					
						
						|  | </a> | 
					
						
						|  | {/each} | 
					
						
						|  | </div> | 
					
						
						|  | {/if} | 
					
						
						|  | </div> | 
					
						
						|  | {#if !loading && message.content} | 
					
						
						|  | <div | 
					
						
						|  | class="absolute bottom-1 right-0 -mb-4 flex max-md:transition-all md:bottom-0 md:group-hover:visible md:group-hover:opacity-100 | 
					
						
						|  | {message.score ? 'visible opacity-100' : 'invisible max-md:-translate-y-4 max-md:opacity-0'} | 
					
						
						|  | {isTapped || isCopied ? 'max-md:visible max-md:translate-y-0 max-md:opacity-100' : ''} | 
					
						
						|  | " | 
					
						
						|  | > | 
					
						
						|  | {#if isAuthor} | 
					
						
						|  | <button | 
					
						
						|  | class="btn rounded-sm p-1 text-sm text-gray-400 focus:ring-0 hover:text-gray-500 dark:text-gray-400 dark:hover:text-gray-300 | 
					
						
						|  | {message.score && message.score > 0 | 
					
						
						|  | ? 'text-green-500 hover:text-green-500 dark:text-green-400 hover:dark:text-green-400' | 
					
						
						|  | : ''}" | 
					
						
						|  | title={message.score === 1 ? "Remove +1" : "+1"} | 
					
						
						|  | type="button" | 
					
						
						|  | on:click={() => | 
					
						
						|  | dispatch("vote", { score: message.score === 1 ? 0 : 1, id: message.id })} | 
					
						
						|  | > | 
					
						
						|  | <CarbonThumbsUp class="h-[1.14em] w-[1.14em]" /> | 
					
						
						|  | </button> | 
					
						
						|  | <button | 
					
						
						|  | class="btn rounded-sm p-1 text-sm text-gray-400 focus:ring-0 hover:text-gray-500 dark:text-gray-400 dark:hover:text-gray-300 | 
					
						
						|  | {message.score && message.score < 0 | 
					
						
						|  | ? 'text-red-500 hover:text-red-500 dark:text-red-400 hover:dark:text-red-400' | 
					
						
						|  | : ''}" | 
					
						
						|  | title={message.score === -1 ? "Remove -1" : "-1"} | 
					
						
						|  | type="button" | 
					
						
						|  | on:click={() => | 
					
						
						|  | dispatch("vote", { score: message.score === -1 ? 0 : -1, id: message.id })} | 
					
						
						|  | > | 
					
						
						|  | <CarbonThumbsDown class="h-[1.14em] w-[1.14em]" /> | 
					
						
						|  | </button> | 
					
						
						|  | {/if} | 
					
						
						|  | <button | 
					
						
						|  | class="btn rounded-sm p-1 text-sm text-gray-400 focus:ring-0 hover:text-gray-500 dark:text-gray-400 dark:hover:text-gray-300" | 
					
						
						|  | title="Retry" | 
					
						
						|  | type="button" | 
					
						
						|  | on:click={() => dispatch("retry", { id: message.id })} | 
					
						
						|  | > | 
					
						
						|  | <CarbonRotate360 /> | 
					
						
						|  | </button> | 
					
						
						|  | <CopyToClipBoardBtn | 
					
						
						|  | on:click={() => { | 
					
						
						|  | isCopied = true; | 
					
						
						|  | }} | 
					
						
						|  | classNames="ml-1.5 !rounded-sm !p-1 !text-sm !text-gray-400 focus:!ring-0 hover:!text-gray-500 dark:!text-gray-400 dark:hover:!text-gray-300 !border-none !shadow-none" | 
					
						
						|  | value={message.content} | 
					
						
						|  | /> | 
					
						
						|  | </div> | 
					
						
						|  | {/if} | 
					
						
						|  | </div> | 
					
						
						|  | <slot name="childrenNav" /> | 
					
						
						|  | {/if} | 
					
						
						|  | {#if message.from === "user"} | 
					
						
						|  | <div | 
					
						
						|  | class="group relative w-full items-start justify-start gap-4 max-sm:text-sm" | 
					
						
						|  | role="presentation" | 
					
						
						|  | on:click={() => (isTapped = !isTapped)} | 
					
						
						|  | on:keydown={() => (isTapped = !isTapped)} | 
					
						
						|  | > | 
					
						
						|  | <div class="flex w-full flex-col"> | 
					
						
						|  | {#if message.files && message.files.length > 0} | 
					
						
						|  | <div class="mx-auto grid w-fit grid-cols-2 gap-5 px-5"> | 
					
						
						|  | {#each message.files as file} | 
					
						
						|  |  | 
					
						
						|  | {#if file.length === 64} | 
					
						
						|  | <img | 
					
						
						|  | src={$page.url.pathname + "/output/" + file} | 
					
						
						|  | alt="input from user" | 
					
						
						|  | class="my-2 aspect-auto max-h-48 rounded-lg shadow-lg" | 
					
						
						|  | /> | 
					
						
						|  | {:else} | 
					
						
						|  |  | 
					
						
						|  | <img | 
					
						
						|  | src={"data:image/*;base64," + file} | 
					
						
						|  | alt="input from user" | 
					
						
						|  | class="my-2 aspect-auto max-h-48 rounded-lg shadow-lg" | 
					
						
						|  | /> | 
					
						
						|  | {/if} | 
					
						
						|  | {/each} | 
					
						
						|  | </div> | 
					
						
						|  | {/if} | 
					
						
						|  |  | 
					
						
						|  | <div class="flex w-full flex-row flex-nowrap"> | 
					
						
						|  | {#if !editMode} | 
					
						
						|  | <p | 
					
						
						|  | class="disabled w-full appearance-none whitespace-break-spaces text-wrap break-words bg-inherit px-5 py-3.5 text-gray-500 dark:text-gray-400" | 
					
						
						|  | > | 
					
						
						|  | {message.content.trim()} | 
					
						
						|  | </p> | 
					
						
						|  | {:else} | 
					
						
						|  | <form | 
					
						
						|  | class="flex w-full flex-col" | 
					
						
						|  | bind:this={editFormEl} | 
					
						
						|  | on:submit|preventDefault={() => { | 
					
						
						|  | dispatch("retry", { content: editContentEl.value, id: message.id }); | 
					
						
						|  | $convTreeStore.editing = null; | 
					
						
						|  | }} | 
					
						
						|  | > | 
					
						
						|  | <textarea | 
					
						
						|  | class="w-full whitespace-break-spaces break-words rounded-xl bg-gray-100 px-5 py-3.5 text-gray-500 *:h-max dark:bg-gray-800 dark:text-gray-400" | 
					
						
						|  | rows="5" | 
					
						
						|  | bind:this={editContentEl} | 
					
						
						|  | value={message.content.trim()} | 
					
						
						|  | on:keydown={handleKeyDown} | 
					
						
						|  | required | 
					
						
						|  | /> | 
					
						
						|  | <div class="flex w-full flex-row flex-nowrap items-center justify-center gap-2 pt-2"> | 
					
						
						|  | <button | 
					
						
						|  | type="submit" | 
					
						
						|  | class="btn rounded-lg px-3 py-1.5 text-sm | 
					
						
						|  | {loading | 
					
						
						|  | ? 'bg-gray-300 text-gray-400 dark:bg-gray-700 dark:text-gray-600' | 
					
						
						|  | : 'bg-gray-200 text-gray-600 focus:ring-0   hover:text-gray-800 dark:bg-gray-800 dark:text-gray-300 dark:hover:text-gray-200'} | 
					
						
						|  | " | 
					
						
						|  | disabled={loading} | 
					
						
						|  | > | 
					
						
						|  | Submit | 
					
						
						|  | </button> | 
					
						
						|  | <button | 
					
						
						|  | type="button" | 
					
						
						|  | class="btn rounded-sm p-2 text-sm text-gray-400 focus:ring-0 hover:text-gray-500 dark:text-gray-400 dark:hover:text-gray-300" | 
					
						
						|  | on:click={() => { | 
					
						
						|  | $convTreeStore.editing = null; | 
					
						
						|  | }} | 
					
						
						|  | > | 
					
						
						|  | Cancel | 
					
						
						|  | </button> | 
					
						
						|  | </div> | 
					
						
						|  | </form> | 
					
						
						|  | {/if} | 
					
						
						|  | {#if !loading && !editMode} | 
					
						
						|  | <div | 
					
						
						|  | class=" | 
					
						
						|  | max-md:opacity-0' invisible absolute | 
					
						
						|  | right-0 top-3.5 z-10 h-max max-md:-translate-y-4 max-md:transition-all md:bottom-0 md:group-hover:visible md:group-hover:opacity-100 {isTapped || | 
					
						
						|  | isCopied | 
					
						
						|  | ? 'max-md:visible max-md:translate-y-0 max-md:opacity-100' | 
					
						
						|  | : ''}" | 
					
						
						|  | > | 
					
						
						|  | <div class="mx-auto flex flex-row flex-nowrap gap-2"> | 
					
						
						|  | {#if downloadLink} | 
					
						
						|  | <a | 
					
						
						|  | class="rounded-lg border border-gray-100 bg-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 max-sm:!hidden md:hidden dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-300" | 
					
						
						|  | title="Download prompt and parameters" | 
					
						
						|  | type="button" | 
					
						
						|  | target="_blank" | 
					
						
						|  | href={downloadLink} | 
					
						
						|  | > | 
					
						
						|  | <CarbonDownload /> | 
					
						
						|  | </a> | 
					
						
						|  | {/if} | 
					
						
						|  | {#if !readOnly} | 
					
						
						|  | <button | 
					
						
						|  | class="cursor-pointer rounded-lg border border-gray-100 bg-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 md:hidden lg:-right-2 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-300" | 
					
						
						|  | title="Branch" | 
					
						
						|  | type="button" | 
					
						
						|  | on:click={() => ($convTreeStore.editing = message.id)} | 
					
						
						|  | > | 
					
						
						|  | <CarbonPen /> | 
					
						
						|  | </button> | 
					
						
						|  | {/if} | 
					
						
						|  | </div> | 
					
						
						|  | </div> | 
					
						
						|  | {/if} | 
					
						
						|  | </div> | 
					
						
						|  | <slot name="childrenNav" /> | 
					
						
						|  | </div> | 
					
						
						|  | </div> | 
					
						
						|  | {/if} | 
					
						
						|  |  | 
					
						
						|  | {#if nChildren > 0} | 
					
						
						|  | <svelte:self | 
					
						
						|  | {loading} | 
					
						
						|  | {messages} | 
					
						
						|  | {isAuthor} | 
					
						
						|  | {readOnly} | 
					
						
						|  | {model} | 
					
						
						|  | id={messages.find((m) => m.id === id)?.children?.[childrenToRender]} | 
					
						
						|  | on:retry | 
					
						
						|  | on:vote | 
					
						
						|  | on:continue | 
					
						
						|  | > | 
					
						
						|  | <svelte:fragment slot="childrenNav"> | 
					
						
						|  | {#if nChildren > 1 && $convTreeStore.editing === null} | 
					
						
						|  | <div | 
					
						
						|  | class="font-white z-10 -mt-1 ml-3.5 mr-auto flex h-6 w-fit select-none flex-row items-center justify-center gap-1 text-sm" | 
					
						
						|  | > | 
					
						
						|  | <button | 
					
						
						|  | class="inline text-lg font-thin text-gray-400 disabled:pointer-events-none disabled:opacity-25 hover:text-gray-800 dark:text-gray-500 dark:hover:text-gray-200" | 
					
						
						|  | on:click={() => (childrenToRender = Math.max(0, childrenToRender - 1))} | 
					
						
						|  | disabled={childrenToRender === 0 || loading} | 
					
						
						|  | > | 
					
						
						|  | <CarbonChevronLeft class="text-sm" /> | 
					
						
						|  | </button> | 
					
						
						|  | <span class=" text-gray-400 dark:text-gray-500"> | 
					
						
						|  | {childrenToRender + 1} / {nChildren} | 
					
						
						|  | </span> | 
					
						
						|  | <button | 
					
						
						|  | class="inline text-lg font-thin text-gray-400 disabled:pointer-events-none disabled:opacity-25 hover:text-gray-800 dark:text-gray-500 dark:hover:text-gray-200" | 
					
						
						|  | on:click={() => | 
					
						
						|  | (childrenToRender = Math.min( | 
					
						
						|  | message?.children?.length ?? 1 - 1, | 
					
						
						|  | childrenToRender + 1 | 
					
						
						|  | ))} | 
					
						
						|  | disabled={childrenToRender === nChildren - 1 || loading} | 
					
						
						|  | > | 
					
						
						|  | <CarbonChevronRight class="text-sm" /> | 
					
						
						|  | </button> | 
					
						
						|  | </div> | 
					
						
						|  | {/if} | 
					
						
						|  | </svelte:fragment> | 
					
						
						|  | </svelte:self> | 
					
						
						|  | {/if} | 
					
						
						|  |  |