Commit 3c9fc785 authored by Timothy J. Baek's avatar Timothy J. Baek
Browse files

fix: styling

parent bf2ff47d
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { onMount, tick, getContext } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { settings } from '$lib/stores'; import { settings, showSidebar } from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils'; import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import Prompts from './MessageInput/PromptCommands.svelte'; import Prompts from './MessageInput/PromptCommands.svelte';
...@@ -291,6 +291,7 @@ ...@@ -291,6 +291,7 @@
}; };
onMount(() => { onMount(() => {
console.log(document.getElementById('sidebar'));
window.setTimeout(() => chatTextAreaElement?.focus(), 0); window.setTimeout(() => chatTextAreaElement?.focus(), 0);
const dropZone = document.querySelector('body'); const dropZone = document.querySelector('body');
...@@ -389,142 +390,216 @@ ...@@ -389,142 +390,216 @@
</div> </div>
{/if} {/if}
<div class="w-full absolute bottom-0"> <div class="fixed bottom-0 {$showSidebar ? 'left-0 lg:left-[260px]' : 'left-0'} right-0">
<div class="px-2.5 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center"> <div class="w-full">
<div class="flex flex-col max-w-3xl w-full"> <div class=" px-2.5 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
<div class="relative"> <div class="flex flex-col max-w-3xl w-full">
{#if autoScroll === false && messages.length > 0} <div class="relative">
<div class=" absolute -top-12 left-0 right-0 flex justify-center"> {#if autoScroll === false && messages.length > 0}
<button <div class=" absolute -top-12 left-0 right-0 flex justify-center">
class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full" <button
on:click={() => { class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full"
autoScroll = true; on:click={() => {
scrollToBottom(); autoScroll = true;
}} scrollToBottom();
> }}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
> >
<path <svg
fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z" viewBox="0 0 20 20"
clip-rule="evenodd" fill="currentColor"
/> class="w-5 h-5"
</svg> >
</button> <path
</div> fill-rule="evenodd"
{/if} d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z"
</div> clip-rule="evenodd"
/>
</svg>
</button>
</div>
{/if}
</div>
<div class="w-full relative"> <div class="w-full relative">
{#if prompt.charAt(0) === '/'} {#if prompt.charAt(0) === '/'}
<Prompts bind:this={promptsElement} bind:prompt /> <Prompts bind:this={promptsElement} bind:prompt />
{:else if prompt.charAt(0) === '#'} {:else if prompt.charAt(0) === '#'}
<Documents <Documents
bind:this={documentsElement} bind:this={documentsElement}
bind:prompt bind:prompt
on:url={(e) => { on:url={(e) => {
console.log(e); console.log(e);
uploadWeb(e.detail); uploadWeb(e.detail);
}} }}
on:select={(e) => { on:select={(e) => {
console.log(e); console.log(e);
files = [ files = [
...files, ...files,
{ {
type: e?.detail?.type ?? 'doc', type: e?.detail?.type ?? 'doc',
...e.detail, ...e.detail,
upload_status: true upload_status: true
} }
]; ];
}} }}
/> />
{:else if prompt.charAt(0) === '@'} {:else if prompt.charAt(0) === '@'}
<Models <Models
bind:this={modelsElement} bind:this={modelsElement}
bind:prompt bind:prompt
bind:user bind:user
bind:chatInputPlaceholder bind:chatInputPlaceholder
{messages} {messages}
/> />
{/if} {/if}
<!-- {#if messages.length == 0 && suggestionPrompts.length !== 0} <!-- {#if messages.length == 0 && suggestionPrompts.length !== 0}
<Suggestions {suggestionPrompts} {submitPrompt} /> <Suggestions {suggestionPrompts} {submitPrompt} />
{/if} --> {/if} -->
</div>
</div> </div>
</div> </div>
</div> <div class="bg-white dark:bg-gray-900">
<div class="bg-white dark:bg-gray-900"> <div class="max-w-3xl px-2.5 mx-auto inset-x-0">
<div class="max-w-3xl px-2.5 mx-auto inset-x-0"> <div class=" pb-2">
<div class=" pb-2"> <input
<input bind:this={filesInputElement}
bind:this={filesInputElement} bind:files={inputFiles}
bind:files={inputFiles} type="file"
type="file" hidden
hidden multiple
multiple on:change={async () => {
on:change={async () => { if (inputFiles && inputFiles.length > 0) {
if (inputFiles && inputFiles.length > 0) { const _inputFiles = Array.from(inputFiles);
const _inputFiles = Array.from(inputFiles); _inputFiles.forEach((file) => {
_inputFiles.forEach((file) => { if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) {
if (['image/gif', 'image/jpeg', 'image/png'].includes(file['type'])) { let reader = new FileReader();
let reader = new FileReader(); reader.onload = (event) => {
reader.onload = (event) => { files = [
files = [ ...files,
...files, {
{ type: 'image',
type: 'image', url: `${event.target.result}`
url: `${event.target.result}` }
} ];
]; inputFiles = null;
inputFiles = null; filesInputElement.value = '';
};
reader.readAsDataURL(file);
} else if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
uploadDoc(file);
filesInputElement.value = ''; filesInputElement.value = '';
}; } else {
reader.readAsDataURL(file); toast.error(
} else if ( $i18n.t(
SUPPORTED_FILE_TYPE.includes(file['type']) || `Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1)) { file_type: file['type'] }
) { )
uploadDoc(file); );
filesInputElement.value = ''; uploadDoc(file);
} else { filesInputElement.value = '';
toast.error( }
$i18n.t( });
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`, } else {
{ file_type: file['type'] } toast.error($i18n.t(`File not found.`));
) }
); }}
uploadDoc(file); />
filesInputElement.value = ''; <form
} class=" flex flex-col relative w-full rounded-3xl px-1.5 border border-gray-100 dark:border-gray-850 bg-white dark:bg-gray-900 dark:text-gray-100"
}); on:submit|preventDefault={() => {
} else { submitPrompt(prompt, user);
toast.error($i18n.t(`File not found.`)); }}
} >
}} {#if files.length > 0}
/> <div class="mx-2 mt-2 mb-1 flex flex-wrap gap-2">
<form {#each files as file, fileIdx}
class=" flex flex-col relative w-full rounded-3xl px-1.5 border border-gray-100 dark:border-gray-850 bg-white dark:bg-gray-900 dark:text-gray-100" <div class=" relative group">
on:submit|preventDefault={() => { {#if file.type === 'image'}
submitPrompt(prompt, user); <img src={file.url} alt="input" class=" h-16 w-16 rounded-xl object-cover" />
}} {:else if file.type === 'doc'}
> <div
{#if files.length > 0} class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
<div class="mx-2 mt-2 mb-1 flex flex-wrap gap-2"> >
{#each files as file, fileIdx} <div class="p-2.5 bg-red-400 text-white rounded-lg">
<div class=" relative group"> {#if file.upload_status}
{#if file.type === 'image'} <svg
<img src={file.url} alt="input" class=" h-16 w-16 rounded-xl object-cover" /> xmlns="http://www.w3.org/2000/svg"
{:else if file.type === 'doc'} viewBox="0 0 24 24"
<div fill="currentColor"
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none" class="w-6 h-6"
> >
<div class="p-2.5 bg-red-400 text-white rounded-lg"> <path
{#if file.upload_status} fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/>
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
/>
</svg>
{:else}
<svg
class=" w-6 h-6 translate-y-[0.5px]"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="2.5"
/><circle
class="spinner_qM83 spinner_ZTLf"
cx="20"
cy="12"
r="2.5"
/></svg
>
{/if}
</div>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file.name}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
</div>
</div>
{:else if file.type === 'collection'}
<div
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
...@@ -532,421 +607,349 @@ ...@@ -532,421 +607,349 @@
class="w-6 h-6" class="w-6 h-6"
> >
<path <path
fill-rule="evenodd" d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/> />
<path <path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z" d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
/> />
</svg> </svg>
{:else}
<svg
class=" w-6 h-6 translate-y-[0.5px]"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="2.5"
/><circle
class="spinner_qM83 spinner_ZTLf"
cx="20"
cy="12"
r="2.5"
/></svg
>
{/if}
</div>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file.name}
</div> </div>
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div> <div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file?.title ?? `#${file.name}`}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
</div>
</div> </div>
</div> {/if}
{:else if file.type === 'collection'}
<div <div class=" absolute -top-1 -right-1">
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none" <button
> class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
<div class="p-2.5 bg-red-400 text-white rounded-lg"> type="button"
on:click={() => {
files.splice(fileIdx, 1);
files = files;
}}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 20 20"
fill="currentColor" fill="currentColor"
class="w-6 h-6" class="w-4 h-4"
> >
<path <path
d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z" d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
<path
d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
/> />
</svg> </svg>
</div> </button>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file?.title ?? `#${file.name}`}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
</div>
</div> </div>
{/if} </div>
{/each}
</div>
{/if}
<div class=" absolute -top-1 -right-1"> <div class=" flex">
{#if fileUploadEnabled}
<div class=" self-end mb-2 ml-1">
<Tooltip content={$i18n.t('Upload files')}>
<button <button
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition" class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
type="button" type="button"
on:click={() => { on:click={() => {
files.splice(fileIdx, 1); filesInputElement.click();
files = files;
}} }}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 16 16"
fill="currentColor" fill="currentColor"
class="w-4 h-4" class="w-[1.2rem] h-[1.2rem]"
> >
<path <path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/> />
</svg> </svg>
</button> </button>
</div> </Tooltip>
</div> </div>
{/each} {/if}
</div>
{/if}
<div class=" flex">
{#if fileUploadEnabled}
<div class=" self-end mb-2 ml-1">
<Tooltip content={$i18n.t('Upload files')}>
<button
class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
type="button"
on:click={() => {
filesInputElement.click();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-[1.2rem] h-[1.2rem]"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
</Tooltip>
</div>
{/if}
<textarea <textarea
id="chat-textarea" id="chat-textarea"
bind:this={chatTextAreaElement} bind:this={chatTextAreaElement}
class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
? '' ? ''
: ' pl-4'} rounded-xl resize-none h-[48px]" : ' pl-4'} rounded-xl resize-none h-[48px]"
placeholder={chatInputPlaceholder !== '' placeholder={chatInputPlaceholder !== ''
? chatInputPlaceholder ? chatInputPlaceholder
: isRecording : isRecording
? $i18n.t('Listening...') ? $i18n.t('Listening...')
: $i18n.t('Send a Message')} : $i18n.t('Send a Message')}
bind:value={prompt} bind:value={prompt}
on:keypress={(e) => { on:keypress={(e) => {
if (window.innerWidth > 1024) { if (window.innerWidth > 1024) {
if (e.keyCode == 13 && !e.shiftKey) { if (e.keyCode == 13 && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
} }
if (prompt !== '' && e.keyCode == 13 && !e.shiftKey) { if (prompt !== '' && e.keyCode == 13 && !e.shiftKey) {
submitPrompt(prompt, user); submitPrompt(prompt, user);
}
} }
} }}
}} on:keydown={async (e) => {
on:keydown={async (e) => { const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
// Check if Ctrl + R is pressed // Check if Ctrl + R is pressed
if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') { if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
e.preventDefault(); e.preventDefault();
console.log('regenerate'); console.log('regenerate');
const regenerateButton = [ const regenerateButton = [
...document.getElementsByClassName('regenerate-response-button') ...document.getElementsByClassName('regenerate-response-button')
]?.at(-1); ]?.at(-1);
regenerateButton?.click(); regenerateButton?.click();
} }
if (prompt === '' && e.key == 'ArrowUp') { if (prompt === '' && e.key == 'ArrowUp') {
e.preventDefault(); e.preventDefault();
const userMessageElement = [ const userMessageElement = [
...document.getElementsByClassName('user-message') ...document.getElementsByClassName('user-message')
]?.at(-1); ]?.at(-1);
const editButton = [ const editButton = [
...document.getElementsByClassName('edit-user-message-button') ...document.getElementsByClassName('edit-user-message-button')
]?.at(-1); ]?.at(-1);
console.log(userMessageElement); console.log(userMessageElement);
userMessageElement.scrollIntoView({ block: 'center' }); userMessageElement.scrollIntoView({ block: 'center' });
editButton?.click(); editButton?.click();
} }
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') { if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') {
e.preventDefault(); e.preventDefault();
(promptsElement || documentsElement || modelsElement).selectUp(); (promptsElement || documentsElement || modelsElement).selectUp();
const commandOptionButton = [ const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button') ...document.getElementsByClassName('selected-command-option-button')
]?.at(-1); ]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' }); commandOptionButton.scrollIntoView({ block: 'center' });
} }
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') { if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') {
e.preventDefault(); e.preventDefault();
(promptsElement || documentsElement || modelsElement).selectDown(); (promptsElement || documentsElement || modelsElement).selectDown();
const commandOptionButton = [ const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button') ...document.getElementsByClassName('selected-command-option-button')
]?.at(-1); ]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' }); commandOptionButton.scrollIntoView({ block: 'center' });
} }
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Enter') { if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
const commandOptionButton = [ const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button') ...document.getElementsByClassName('selected-command-option-button')
]?.at(-1); ]?.at(-1);
if (commandOptionButton) { if (commandOptionButton) {
commandOptionButton?.click(); commandOptionButton?.click();
} else { } else {
document.getElementById('send-message-button')?.click(); document.getElementById('send-message-button')?.click();
}
} }
}
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Tab') { if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Tab') {
e.preventDefault(); e.preventDefault();
const commandOptionButton = [ const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button') ...document.getElementsByClassName('selected-command-option-button')
]?.at(-1); ]?.at(-1);
commandOptionButton?.click(); commandOptionButton?.click();
} else if (e.key === 'Tab') { } else if (e.key === 'Tab') {
const words = findWordIndices(prompt); const words = findWordIndices(prompt);
if (words.length > 0) { if (words.length > 0) {
const word = words.at(0); const word = words.at(0);
const fullPrompt = prompt; const fullPrompt = prompt;
prompt = prompt.substring(0, word?.endIndex + 1); prompt = prompt.substring(0, word?.endIndex + 1);
await tick(); await tick();
e.target.scrollTop = e.target.scrollHeight; e.target.scrollTop = e.target.scrollHeight;
prompt = fullPrompt; prompt = fullPrompt;
await tick(); await tick();
e.preventDefault(); e.preventDefault();
e.target.setSelectionRange(word?.startIndex, word.endIndex + 1); e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
}
}
}}
rows="1"
on:input={(e) => {
e.target.style.height = '';
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
user = null;
}}
on:focus={(e) => {
e.target.style.height = '';
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
}}
on:paste={(e) => {
const clipboardData = e.clipboardData || window.clipboardData;
if (clipboardData && clipboardData.items) {
for (const item of clipboardData.items) {
if (item.type.indexOf('image') !== -1) {
const blob = item.getAsFile();
const reader = new FileReader();
reader.onload = function (e) {
files = [
...files,
{
type: 'image',
url: `${e.target.result}`
}
];
};
reader.readAsDataURL(blob);
} }
} }
} }}
}} rows="1"
/> on:input={(e) => {
e.target.style.height = '';
<div class="self-end mb-2 flex space-x-1 mr-1"> e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
{#if messages.length == 0 || messages.at(-1).done == true} user = null;
<Tooltip content={$i18n.t('Record voice')}> }}
{#if speechRecognitionEnabled} on:focus={(e) => {
<button e.target.style.height = '';
id="voice-input-button" e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-1.5 mr-0.5 self-center" }}
type="button" on:paste={(e) => {
on:click={() => { const clipboardData = e.clipboardData || window.clipboardData;
speechRecognitionHandler();
}} if (clipboardData && clipboardData.items) {
> for (const item of clipboardData.items) {
{#if isRecording} if (item.type.indexOf('image') !== -1) {
<svg const blob = item.getAsFile();
class=" w-5 h-5 translate-y-[0.5px]" const reader = new FileReader();
fill="currentColor"
viewBox="0 0 24 24" reader.onload = function (e) {
xmlns="http://www.w3.org/2000/svg" files = [
><style> ...files,
.spinner_qM83 { {
animation: spinner_8HQG 1.05s infinite; type: 'image',
url: `${e.target.result}`
} }
.spinner_oXPr { ];
animation-delay: 0.1s; };
}
.spinner_ZTLf { reader.readAsDataURL(blob);
animation-delay: 0.2s; }
} }
@keyframes spinner_8HQG { }
0%, }}
57.14% { />
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0); <div class="self-end mb-2 flex space-x-1 mr-1">
{#if messages.length == 0 || messages.at(-1).done == true}
<Tooltip content={$i18n.t('Record voice')}>
{#if speechRecognitionEnabled}
<button
id="voice-input-button"
class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-1.5 mr-0.5 self-center"
type="button"
on:click={() => {
speechRecognitionHandler();
}}
>
{#if isRecording}
<svg
class=" w-5 h-5 translate-y-[0.5px]"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
} }
28.57% { .spinner_oXPr {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33); animation-delay: 0.1s;
transform: translateY(-6px);
} }
100% { .spinner_ZTLf {
transform: translate(0); animation-delay: 0.2s;
} }
} @keyframes spinner_8HQG {
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle 0%,
class="spinner_qM83 spinner_oXPr" 57.14% {
cx="12" animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
cy="12" transform: translate(0);
r="2.5" }
/><circle 28.57% {
class="spinner_qM83 spinner_ZTLf" animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
cx="20" transform: translateY(-6px);
cy="12" }
r="2.5" 100% {
/></svg transform: translate(0);
> }
{:else} }
<svg </style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
xmlns="http://www.w3.org/2000/svg" class="spinner_qM83 spinner_oXPr"
viewBox="0 0 20 20" cx="12"
fill="currentColor" cy="12"
class="w-5 h-5 translate-y-[0.5px]" r="2.5"
> /><circle
<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" /> class="spinner_qM83 spinner_ZTLf"
<path cx="20"
d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z" cy="12"
/> r="2.5"
</svg> /></svg
{/if} >
</button> {:else}
{/if} <svg
</Tooltip> xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5 translate-y-[0.5px]"
>
<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" />
<path
d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z"
/>
</svg>
{/if}
</button>
{/if}
</Tooltip>
<Tooltip content={$i18n.t('Send message')}> <Tooltip content={$i18n.t('Send message')}>
<button
id="send-message-button"
class="{prompt !== ''
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
: 'text-white bg-gray-100 dark:text-gray-900 dark:bg-gray-800 disabled'} transition rounded-full p-1.5 self-center"
type="submit"
disabled={prompt === ''}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-5 h-5"
>
<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>
</Tooltip>
{:else}
<button <button
id="send-message-button" class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
class="{prompt !== '' on:click={stopResponse}
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
: 'text-white bg-gray-100 dark:text-gray-900 dark:bg-gray-800 disabled'} transition rounded-full p-1.5 self-center"
type="submit"
disabled={prompt === ''}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
class="w-5 h-5" class="w-5 h-5"
> >
<path <path
fill-rule="evenodd" 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" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
clip-rule="evenodd" clip-rule="evenodd"
/> />
</svg> </svg>
</button> </button>
</Tooltip> {/if}
{:else} </div>
<button
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
on:click={stopResponse}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-5 h-5"
>
<path
fill-rule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z"
clip-rule="evenodd"
/>
</svg>
</button>
{/if}
</div> </div>
</div> </form>
</form>
<div class="mt-1.5 text-xs text-gray-500 text-center"> <div class="mt-1.5 text-xs text-gray-500 text-center">
{$i18n.t('LLMs can make mistakes. Verify important information.')} {$i18n.t('LLMs can make mistakes. Verify important information.')}
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -297,7 +297,7 @@ ...@@ -297,7 +297,7 @@
}} }}
/> />
{:else} {:else}
<div class="pt-2 pb-28"> <div class="{$settings?.fullScreenMode ?? null ? 'w-full' : 'mx-auto'} pt-2 pb-28">
{#key chatId} {#key chatId}
{#each messages as message, messageIdx} {#each messages as message, messageIdx}
<div class=" w-full"> <div class=" w-full">
......
<script lang="ts"> <script lang="ts">
import { v4 as uuidv4 } from 'uuid'; import { goto } from '$app/navigation';
import { user, chats, settings, showSettings, chatId, tags, showSidebar } from '$lib/stores';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { goto, invalidateAll } from '$app/navigation';
import { page } from '$app/stores';
import { user, chats, settings, showSettings, chatId, tags } from '$lib/stores';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -30,6 +24,7 @@ ...@@ -30,6 +24,7 @@
import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte'; import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
const BREAKPOINT = 1024; const BREAKPOINT = 1024;
let show = false; let show = false;
let navElement; let navElement;
...@@ -50,7 +45,7 @@ ...@@ -50,7 +45,7 @@
let isEditing = false; let isEditing = false;
onMount(async () => { onMount(async () => {
show = window.innerWidth > BREAKPOINT; showSidebar.set(window.innerWidth > BREAKPOINT);
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
let touchstart; let touchstart;
...@@ -61,10 +56,10 @@ ...@@ -61,10 +56,10 @@
const swipeDistance = Math.abs(touchend.screenX - touchstart.screenX); const swipeDistance = Math.abs(touchend.screenX - touchstart.screenX);
if (touchstart.clientX < 40 && swipeDistance >= screenWidth / 4) { if (touchstart.clientX < 40 && swipeDistance >= screenWidth / 4) {
if (touchend.screenX < touchstart.screenX) { if (touchend.screenX < touchstart.screenX) {
show = false; showSidebar.set(false);
} }
if (touchend.screenX > touchstart.screenX) { if (touchend.screenX > touchstart.screenX) {
show = true; showSidebar.set(true);
} }
} }
} }
...@@ -80,8 +75,8 @@ ...@@ -80,8 +75,8 @@
}; };
const onResize = () => { const onResize = () => {
if (show && window.innerWidth < BREAKPOINT) { if ($showSidebar && window.innerWidth < BREAKPOINT) {
show = false; showSidebar.set(false);
} }
}; };
...@@ -167,13 +162,15 @@ ...@@ -167,13 +162,15 @@
<div <div
bind:this={navElement} bind:this={navElement}
class="h-screen max-h-[100dvh] min-h-screen {show id="sidebar"
class="h-screen max-h-[100dvh] min-h-screen {$showSidebar
? 'lg:relative w-[260px]' ? 'lg:relative w-[260px]'
: '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition fixed z-50 top-0 left-0 : '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition fixed z-50 top-0 left-0
" "
data-state={$showSidebar}
> >
<div <div
class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] {show class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] {$showSidebar
? '' ? ''
: 'invisible'}" : 'invisible'}"
> >
...@@ -466,7 +463,7 @@ ...@@ -466,7 +463,7 @@
on:click={() => { on:click={() => {
selectedChatId = chat.id; selectedChatId = chat.id;
if (window.innerWidth < 1024) { if (window.innerWidth < 1024) {
show = false; showSidebar.set(false);
} }
}} }}
draggable="false" draggable="false"
...@@ -803,14 +800,14 @@ ...@@ -803,14 +800,14 @@
> >
<Tooltip <Tooltip
placement="right" placement="right"
content={`${show ? $i18n.t('Close') : $i18n.t('Open')} ${$i18n.t('sidebar')}`} content={`${$showSidebar ? $i18n.t('Close') : $i18n.t('Open')} ${$i18n.t('sidebar')}`}
touch={false} touch={false}
> >
<button <button
id="sidebar-toggle-button" id="sidebar-toggle-button"
class=" group" class=" group"
on:click={() => { on:click={() => {
show = !show; showSidebar.set(!$showSidebar);
}} }}
><span class="" data-state="closed" ><span class="" data-state="closed"
><div ><div
......
...@@ -34,6 +34,8 @@ export const documents = writable([ ...@@ -34,6 +34,8 @@ export const documents = writable([
]); ]);
export const settings: Writable<Settings> = writable({}); export const settings: Writable<Settings> = writable({});
export const showSidebar = writable(false);
export const showSettings = writable(false); export const showSettings = writable(false);
export const showChangelog = writable(false); export const showChangelog = writable(false);
......
...@@ -877,7 +877,6 @@ ...@@ -877,7 +877,6 @@
/> />
</div> </div>
</div> </div>
<MessageInput bind:files bind:prompt bind:autoScroll {messages} {submitPrompt} {stopResponse} />
</div> </div>
</div> </div>
<MessageInput bind:files bind:prompt bind:autoScroll {messages} {submitPrompt} {stopResponse} />
...@@ -900,17 +900,16 @@ ...@@ -900,17 +900,16 @@
/> />
</div> </div>
</div> </div>
<MessageInput
bind:files
bind:prompt
bind:autoScroll
suggestionPrompts={selectedModelfile?.suggestionPrompts ??
$config.default_prompt_suggestions}
{messages}
{submitPrompt}
{stopResponse}
/>
</div> </div>
</div> </div>
<MessageInput
bind:files
bind:prompt
bind:autoScroll
suggestionPrompts={selectedModelfile?.suggestionPrompts ?? $config.default_prompt_suggestions}
{messages}
{submitPrompt}
{stopResponse}
/>
{/if} {/if}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment